#include "synth_engine.h" #include #include // A simple sine lookup table for the sine oscillator const int SINE_TABLE_SIZE = 256; static int16_t sine_table[SINE_TABLE_SIZE]; static bool sine_table_filled = false; /** * @brief Fills the global sine table. Called once on startup. */ void fill_sine_table() { if (sine_table_filled) return; for (int i = 0; i < SINE_TABLE_SIZE; ++i) { // M_PI is not standard C++, but it's common. If it fails, use 3.1415926535... sine_table[i] = static_cast(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0); } sine_table_filled = true; } SynthEngine::SynthEngine(uint32_t sampleRate) : grid{}, _sampleRate(sampleRate), _phase(0), _increment(0), _volume(0.5f), _waveform(SAWTOOTH), _isGateOpen(false), _rngState(12345) { fill_sine_table(); // Initialize with a default frequency setFrequency(440.0f); // Initialize SINK grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK; } SynthEngine::~SynthEngine() { } void SynthEngine::exportGrid(uint8_t* buffer) { SynthLockGuard lock(gridMutex); size_t idx = 0; for(int y=0; y lock(gridMutex); size_t idx = 0; for(int y=0; y lock(gridMutex); for (int x = 0; x < GRID_W; ++x) { for (int y = 0; y < GRID_H; ++y) { GridCell& c = grid[x][y]; if (c.type == GridCell::SINK) continue; c.type = GridCell::EMPTY; c.param = 0.5f; c.rotation = 0; c.value = 0.0f; c.phase = 0.0f; } } } void SynthEngine::loadPreset(int preset) { clearGrid(); SynthLockGuard lock(gridMutex); auto placeOp = [&](int x, int y, float ratio, float att, float rel) { // Layout: // (x, y) : G-IN (South) // (x, y+1) : WIRE (East) -> Feeds envelope chain // (x+1, y+1): ATT (East) -> // (x+2, y+1): REL (East) // (x+3, y+1): VCA (South) -> Output is here. Gets audio from OSC, gain from envelope. // (x+3, y) : OSC (South) -> Audio source. Gets FM from its back (x+3, y-1). grid[x][y].type = GridCell::GATE_INPUT; grid[x][y].rotation = 2; // S grid[x][y+1].type = GridCell::WIRE; grid[x][y+1].rotation = 1; // E grid[x+1][y+1].type = GridCell::ADSR_ATTACK; grid[x+1][y+1].rotation = 1; // E grid[x+1][y+1].param = att; grid[x+2][y+1].type = GridCell::ADSR_RELEASE; grid[x+2][y+1].rotation = 1; // E grid[x+2][y+1].param = rel; grid[x+3][y+1].type = GridCell::VCA; grid[x+3][y+1].rotation = 2; // S grid[x+3][y+1].param = 0.0f; // Controlled by Env grid[x+3][y].type = GridCell::INPUT_OSCILLATOR; grid[x+3][y].rotation = 2; // S grid[x+3][y].param = (ratio > 1.0f) ? 0.5f : 0.0f; }; int sinkY = GRID_H - 1; int sinkX = GRID_W / 2; if (preset == 1) { // Based on DX7 Algorithm 32 placeOp(0, 0, 1.0f, 0.01f, 0.5f); // Op 1 placeOp(4, 0, 1.0f, 0.05f, 0.3f); // Op 2 placeOp(8, 0, 2.0f, 0.01f, 0.2f); // Op 3 grid[3][2].type = GridCell::WIRE; grid[3][2].rotation = 2; grid[3][3].type = GridCell::WIRE; grid[3][3].rotation = 1; // E grid[4][3].type = GridCell::WIRE; grid[4][3].rotation = 1; // E grid[5][3].type = GridCell::WIRE; grid[5][3].rotation = 1; // E grid[6][3].type = GridCell::WIRE; grid[6][3].rotation = 2; // S grid[7][2].type = GridCell::WIRE; grid[7][2].rotation = 2; grid[7][3].type = GridCell::WIRE; grid[7][3].rotation = 3; // W grid[11][2].type = GridCell::WIRE; grid[11][2].rotation = 2; grid[11][3].type = GridCell::WIRE; grid[11][3].rotation = 3; // W grid[10][3].type = GridCell::WIRE; grid[10][3].rotation = 3; // W grid[9][3].type = GridCell::WIRE; grid[9][3].rotation = 3; // W grid[8][3].type = GridCell::WIRE; grid[8][3].rotation = 3; // W for(int y=4; y((double)freq * (4294967296.0 / (double)_sampleRate)); } void SynthEngine::setVolume(float vol) { if (vol < 0.0f) vol = 0.0f; if (vol > 1.0f) vol = 1.0f; _volume = vol; } void SynthEngine::setWaveform(Waveform form) { _waveform = form; } void SynthEngine::setGate(bool isOpen) { _isGateOpen = isOpen; } float SynthEngine::getFrequency() const { return (float)((double)_increment * (double)_sampleRate / 4294967296.0); } float SynthEngine::_random() { // Simple Linear Congruential Generator _rngState = _rngState * 1664525 + 1013904223; return (float)_rngState / 4294967296.0f; } float SynthEngine::processGridStep() { // Double buffer for values to handle feedback loops gracefully (1-sample delay) static float next_values[GRID_W][GRID_H]; auto isConnected = [&](int tx, int ty, int from_x, int from_y) -> bool { if (from_x < 0 || from_x >= GRID_W || from_y < 0 || from_y >= GRID_H) return false; GridCell& n = grid[from_x][from_y]; bool connects = false; if (n.type == GridCell::WIRE || n.type == GridCell::FIXED_OSCILLATOR || n.type == GridCell::INPUT_OSCILLATOR || n.type == GridCell::WAVETABLE || n.type == GridCell::NOISE || n.type == GridCell::LFO || n.type == GridCell::GATE_INPUT || n.type == GridCell::ADSR_ATTACK || n.type == GridCell::ADSR_DECAY || n.type == GridCell::ADSR_SUSTAIN || n.type == GridCell::ADSR_RELEASE || n.type == GridCell::LPF || n.type == GridCell::HPF || n.type == GridCell::VCA || n.type == GridCell::BITCRUSHER || n.type == GridCell::DISTORTION || n.type == GridCell::RECTIFIER || n.type == GridCell::PITCH_SHIFTER || n.type == GridCell::GLITCH || n.type == GridCell::OPERATOR || n.type == GridCell::DELAY || n.type == GridCell::REVERB) { // Check rotation // 0:N (y-1), 1:E (x+1), 2:S (y+1), 3:W (x-1) if (n.rotation == 0 && from_y - 1 == ty && from_x == tx) connects = true; if (n.rotation == 1 && from_x + 1 == tx && from_y == ty) connects = true; if (n.rotation == 2 && from_y + 1 == ty && from_x == tx) connects = true; if (n.rotation == 3 && from_x - 1 == tx && from_y == ty) connects = true; } else if (n.type == GridCell::FORK) { // Fork outputs to Left (rot+3) and Right (rot+1) relative to its rotation // n.rotation is "Forward" int dx = tx - from_x; int dy = ty - from_y; int dir = -1; if (dx == 0 && dy == -1) dir = 0; // N if (dx == 1 && dy == 0) dir = 1; // E if (dx == 0 && dy == 1) dir = 2; // S if (dx == -1 && dy == 0) dir = 3; // W int leftOut = (n.rotation + 3) % 4; int rightOut = (n.rotation + 1) % 4; if (dir == leftOut || dir == rightOut) connects = true; } return connects; }; // Helper to get input from a neighbor auto getInput = [&](int tx, int ty, int from_x, int from_y) -> float { if (!isConnected(tx, ty, from_x, from_y)) return 0.0f; GridCell& n = grid[from_x][from_y]; if (n.type == GridCell::FORK) { int dx = tx - from_x; int dy = ty - from_y; int dir = -1; if (dx == 0 && dy == -1) dir = 0; // N if (dx == 1 && dy == 0) dir = 1; // E if (dx == 0 && dy == 1) dir = 2; // S if (dx == -1 && dy == 0) dir = 3; // W int leftOut = (n.rotation + 3) % 4; int rightOut = (n.rotation + 1) % 4; if (dir == leftOut) return n.value * (1.0f - n.param) * 2.0f; if (dir == rightOut) return n.value * n.param * 2.0f; } return n.value; }; // Helper to sum inputs excluding the output direction auto getSummedInput = [&](int x, int y, GridCell& c) -> float { float sum = 0.0f; int outDir = c.rotation; // 0:N, 1:E, 2:S, 3:W if (outDir != 0) sum += getInput(x, y, x, y-1); if (outDir != 1) sum += getInput(x, y, x+1, y); if (outDir != 2) sum += getInput(x, y, x, y+1); if (outDir != 3) sum += getInput(x, y, x-1, y); return sum; }; auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> float { int inDir = (c.rotation + 2) % 4; int dx=0, dy=0; if(inDir==0) dy=-1; else if(inDir==1) dx=1; else if(inDir==2) dy=1; else dx=-1; return getInput(x, y, x+dx, y+dy); }; auto getSideInputGain = [&](int x, int y, GridCell& c) -> float { float gain = 0.0f; bool hasSide = false; // Left (rot+3) int lDir = (c.rotation + 3) % 4; int ldx=0, ldy=0; if(lDir==0) ldy=-1; else if(lDir==1) ldx=1; else if(lDir==2) ldy=1; else ldx=-1; if (isConnected(x, y, x+ldx, y+ldy)) { hasSide = true; gain += getInput(x, y, x+ldx, y+ldy); } // Right (rot+1) int rDir = (c.rotation + 1) % 4; int rdx=0, rdy=0; if(rDir==0) rdy=-1; else if(rDir==1) rdx=1; else if(rDir==2) rdy=1; else rdx=-1; if (isConnected(x, y, x+rdx, y+rdy)) { hasSide = true; gain += getInput(x, y, x+rdx, y+rdy); } return hasSide ? gain : 1.0f; }; for (int x = 0; x < GRID_W; ++x) { for (int y = 0; y < GRID_H; ++y) { GridCell& c = grid[x][y]; float val = 0.0f; if (c.type == GridCell::EMPTY) { val = 0.0f; } else if (c.type == GridCell::FIXED_OSCILLATOR) { // Gather inputs for modulation float mod = getInputFromTheBack(x, y, c); // Freq 10 to 1000 Hz float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM if (freq < 1.0f) freq = 1.0f; float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; c.phase += inc; if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; val = (float)sine_table[(int)c.phase] / 32768.0f; val *= getSideInputGain(x, y, c); } else if (c.type == GridCell::INPUT_OSCILLATOR) { float mod = getInputFromTheBack(x, y, c); // Freq based on current note + octave param (1-5) float baseFreq = getFrequency(); int octave = 1 + (int)(c.param * 4.99f); // Map 0.0-1.0 to 1-5 float freq = baseFreq * (float)(1 << (octave - 1)); // 2^(octave-1) freq += (mod * 500.0f); // Apply FM if (freq < 1.0f) freq = 1.0f; // Protect against negative/zero freq float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; c.phase += inc; if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; val = (float)sine_table[(int)c.phase] / 32768.0f; val *= getSideInputGain(x, y, c); } else if (c.type == GridCell::WAVETABLE) { float mod = getInputFromTheBack(x, y, c); // Track current note frequency + FM float freq = getFrequency() + (mod * 500.0f); if (freq < 1.0f) freq = 1.0f; float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; c.phase += inc; if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; float phase_norm = c.phase / (float)SINE_TABLE_SIZE; // 0.0 to 1.0 int wave_select = (int)(c.param * 7.99f); switch(wave_select) { case 0: val = (float)sine_table[(int)c.phase] / 32768.0f; break; case 1: val = (phase_norm * 2.0f) - 1.0f; break; // Saw case 2: val = (phase_norm < 0.5f) ? 1.0f : -1.0f; break; // Square case 3: val = (phase_norm < 0.5f) ? (phase_norm * 4.0f - 1.0f) : (3.0f - phase_norm * 4.0f); break; // Triangle case 4: val = 1.0f - (phase_norm * 2.0f); break; // Ramp case 5: val = (phase_norm < 0.25f) ? 1.0f : -1.0f; break; // Pulse 25% case 6: // Distorted Sine val = sin(phase_norm * 2.0 * M_PI) + sin(phase_norm * 4.0 * M_PI) * 0.3f; val /= 1.3f; // Normalize break; case 7: // Organ-like val = sin(phase_norm * 2.0 * M_PI) * 0.6f + sin(phase_norm * 4.0 * M_PI) * 0.2f + sin(phase_norm * 8.0 * M_PI) * 0.1f; val /= 0.9f; // Normalize break; } val *= getSideInputGain(x, y, c); } else if (c.type == GridCell::NOISE) { float mod = getInputFromTheBack(x, y, c); float white = _random() * 2.0f - 1.0f; int shade = (int)(c.param * 4.99f); switch(shade) { case 0: // Brown (Leaky integrator) c.phase = (c.phase + white * 0.1f) * 0.95f; val = c.phase * 3.0f; // Gain up break; case 1: // Pink (Approx: LPF) c.phase = 0.5f * c.phase + 0.5f * white; val = c.phase; break; case 2: // White val = white; break; case 3: // Yellow (HPF) val = white - c.phase; c.phase = white; // Store last sample break; case 4: // Green (BPF approx) c.phase = (c.phase + white) * 0.5f; // LPF val = white - c.phase; // HPF result break; } // Apply Amplitude Modulation (AM) from input val *= (1.0f + mod); val *= getSideInputGain(x, y, c); } else if (c.type == GridCell::LFO) { // Low Frequency Oscillator (0.1 Hz to 20 Hz) float freq = 0.1f + c.param * 19.9f; float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; c.phase += inc; if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; // Output full range -1.0 to 1.0 val = (float)sine_table[(int)c.phase] / 32768.0f; } else if (c.type == GridCell::FORK) { // Sum inputs from "Back" (Input direction) val = getInputFromTheBack(x, y, c); } else if (c.type == GridCell::GATE_INPUT) { // Outputs 1.0 when gate is open (key pressed), 0.0 otherwise val = _isGateOpen ? 1.0f : 0.0f; } else if (c.type == GridCell::ADSR_ATTACK) { // Slew Limiter (Up only) float in = getInputFromTheBack(x, y, c); float rate = 1.0f / (0.001f + c.param * 2.0f * _sampleRate); // 0.001s to 2s if (in > c.value) { c.value += rate; if (c.value > in) c.value = in; } else { c.value = in; } val = c.value; } else if (c.type == GridCell::ADSR_DECAY || c.type == GridCell::ADSR_RELEASE) { // Slew Limiter (Down only) float in = getInputFromTheBack(x, y, c); float rate = 1.0f / (0.001f + c.param * 2.0f * _sampleRate); if (in < c.value) { c.value -= rate; if (c.value < in) c.value = in; } else { c.value = in; } val = c.value; } else if (c.type == GridCell::ADSR_SUSTAIN) { // Attenuator float in = getInputFromTheBack(x, y, c); val = in * c.param; } else if (c.type == GridCell::WIRE) { // Sum inputs from all neighbors that point to me float sum = getSummedInput(x, y, c); val = sum; } else if (c.type == GridCell::LPF) { // Input from Back float in = getInputFromTheBack(x, y, c); // Simple one-pole LPF // Cutoff mapping: Exponential-ish 20Hz to 15kHz float cutoff = 20.0f + c.param * c.param * 15000.0f; float alpha = 2.0f * M_PI * cutoff / (float)_sampleRate; if (alpha > 1.0f) alpha = 1.0f; // c.phase stores previous output val = c.phase + alpha * (in - c.phase); c.phase = val; } else if (c.type == GridCell::HPF) { // Input from Back float in = getInputFromTheBack(x, y, c); float cutoff = 20.0f + c.param * c.param * 15000.0f; float alpha = 2.0f * M_PI * cutoff / (float)_sampleRate; if (alpha > 1.0f) alpha = 1.0f; // HPF = Input - LPF // c.phase stores LPF state float lpf = c.phase + alpha * (in - c.phase); c.phase = lpf; val = in - lpf; } else if (c.type == GridCell::VCA) { // Input from Back float in = getInputFromTheBack(x, y, c); // Mod from other directions (sum) float mod = getSummedInput(x, y, c); mod -= in; // Remove signal input from mod sum (it was included in getInput calls) // Gain = Param + Mod float gain = c.param + mod; if (gain < 0.0f) gain = 0.0f; val = in * gain; } else if (c.type == GridCell::BITCRUSHER) { float in = getInputFromTheBack(x, y, c); // Bit depth reduction float bits = 1.0f + c.param * 15.0f; // 1 to 16 bits float steps = powf(2.0f, bits); val = roundf(in * steps) / steps; } else if (c.type == GridCell::DISTORTION) { float in = getInputFromTheBack(x, y, c); // Soft clipping float drive = 1.0f + c.param * 20.0f; float x_driven = in * drive; // Simple soft clip: x / (1 + |x|) val = x_driven / (1.0f + fabsf(x_driven)); } else if (c.type == GridCell::RECTIFIER) { float in = getInputFromTheBack(x, y, c); // Mix between original and rectified based on param float rect = fabsf(in); val = in * (1.0f - c.param) + rect * c.param; } else if (c.type == GridCell::GLITCH) { float in = getInputFromTheBack(x, y, c); // Param controls probability of glitch float chance = c.param * 0.2f; // 0 to 20% chance per sample if (_random() < chance) { int mode = (int)(_random() * 3.0f); if (mode == 0) val = in * 50.0f; // Massive gain (clipping) else if (mode == 1) val = _random() * 2.0f - 1.0f; // White noise burst else val = 0.0f; // Drop out } else { val = in; } } else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) { // Gather inputs float inputs[4]; int count = 0; int outDir = (c.type == GridCell::SINK) ? -1 : c.rotation; float iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0.0f; if(iN!=0) inputs[count++] = iN; float iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0.0f; if(iE!=0) inputs[count++] = iE; float iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0.0f; if(iS!=0) inputs[count++] = iS; float iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0.0f; if(iW!=0) inputs[count++] = iW; if (c.type == GridCell::SINK) { // Sink just sums everything val = 0.0f; for(int k=0; kval) val = inputs[i]; break; // MAX } } } } } next_values[x][y] = val; } } // Update state for(int x=0; x < GRID_W; ++x) { for(int y=0; y < GRID_H; ++y) { grid[x][y].value = next_values[x][y]; } } return grid[GRID_W / 2][GRID_H - 1].value; } void SynthEngine::process(int16_t* buffer, uint32_t numFrames) { // Lock grid mutex to prevent UI from changing grid structure mid-process SynthLockGuard lock(gridMutex); for (uint32_t i = 0; i < numFrames; ++i) { // The grid is now the primary sound source. // The processGridStep() returns a float in the approx range of -1.0 to 1.0. float sampleF = processGridStep(); // Soft clip grid sample to avoid harsh distortion before filtering. if (sampleF > 1.0f) sampleF = 1.0f; if (sampleF < -1.0f) sampleF = -1.0f; // The filters were designed for a signal in the int16 range. // We scale the grid's float output to match this expected range. sampleF *= 32767.0f; // Apply Master Volume and write to buffer buffer[i] = static_cast(sampleF * _volume); } }