#include "synth_engine.h" #include #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), _freqToPhaseInc(0.0f), _rngState(12345) { fill_sine_table(); // Initialize with a default frequency setFrequency(440.0f); // Initialize SINK _freqToPhaseInc = 4294967296.0f / (float)_sampleRate; grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK; rebuildProcessingOrder(); } SynthEngine::~SynthEngine() { } size_t SynthEngine::exportGrid(uint8_t* buffer) { SynthLockGuard lock(gridMutex); uint8_t count = 0; for(int y=0; y> FP_SHIFT); buffer[idx++] = (uint8_t)c.rotation; } } } buffer[idx++] = count; return idx; } int SynthEngine::importGrid(const uint8_t* buffer, size_t size) { if (size < 2) return 1; uint8_t countStart = buffer[0]; uint8_t countEnd = buffer[size - 1]; if (countStart != countEnd) return 2; size_t expectedSize = 1 + countStart * 5 + 1; if (size != expectedSize) return 3; SynthLockGuard lock(gridMutex); // Clear grid first 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 = FP_HALF; c.rotation = 0; c.value = 0; c.phase = 0; c.phase_accumulator = 0; c.next_value = 0; } } size_t idx = 1; for(int i=0; i 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 = FP_HALF; c.rotation = 0; c.value = 0; c.phase = 0; c.phase_accumulator = 0; c.next_value = 0; } } rebuildProcessingOrder_locked(); } 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 = (int32_t)(att * FP_ONE); grid[x+2][y+1].type = GridCell::ADSR_RELEASE; grid[x+2][y+1].rotation = 1; // E grid[x+2][y+1].param = (int32_t)(rel * FP_ONE); grid[x+3][y+1].type = GridCell::VCA; grid[x+3][y+1].rotation = 2; // S grid[x+3][y+1].param = 0; // 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) ? FP_HALF : 0; }; 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); } int32_t SynthEngine::_random() { // Simple Linear Congruential Generator _rngState = _rngState * 1664525 + 1013904223; return (int32_t)((_rngState >> 16) & 0xFFFF) - 32768; } void SynthEngine::rebuildProcessingOrder_locked() { _processing_order.clear(); bool visited[GRID_W][GRID_H] = {false}; std::vector> q; // Start BFS from the SINK backwards q.push_back({GRID_W / 2, GRID_H - 1}); visited[GRID_W / 2][GRID_H - 1] = true; int head = 0; while(head < (int)q.size()) { std::pair curr = q[head++]; int cx = curr.first; int cy = curr.second; // Check neighbors to see if they output to (cx, cy) int nx_offsets[4] = {0, 1, 0, -1}; int ny_offsets[4] = {-1, 0, 1, 0}; for(int i=0; i<4; ++i) { int tx = cx + nx_offsets[i]; int ty = cy + ny_offsets[i]; if (tx >= 0 && tx < GRID_W && ty >= 0 && ty < GRID_H && !visited[tx][ty]) { GridCell& neighbor = grid[tx][ty]; bool pointsToCurr = false; if (neighbor.type != GridCell::EMPTY && neighbor.type != GridCell::SINK) { int dx = cx - tx; int dy = cy - ty; int dir = -1; if (dx == 0 && dy == -1) dir = 0; // N else if (dx == 1 && dy == 0) dir = 1; // E else if (dx == 0 && dy == 1) dir = 2; // S else if (dx == -1 && dy == 0) dir = 3; // W if (neighbor.type == GridCell::FORK) { int leftOut = (neighbor.rotation + 3) % 4; int rightOut = (neighbor.rotation + 1) % 4; if (dir == leftOut || dir == rightOut) pointsToCurr = true; } else { if (neighbor.rotation == dir) pointsToCurr = true; } } if (pointsToCurr) { visited[tx][ty] = true; q.push_back({tx, ty}); } } } } _processing_order = q; } void SynthEngine::rebuildProcessingOrder() { SynthLockGuard lock(gridMutex); rebuildProcessingOrder_locked(); } void SynthEngine::updateGraph() { rebuildProcessingOrder_locked(); } int32_t SynthEngine::processGridStep() { 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) -> int32_t { if (!isConnected(tx, ty, from_x, from_y)) return 0; 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 * (FP_ONE - n.param)) >> (FP_SHIFT - 1); if (dir == rightOut) return (n.value * n.param) >> (FP_SHIFT - 1); } return n.value; }; // Helper to sum inputs excluding the output direction auto getSummedInput = [&](int x, int y, GridCell& c) -> int32_t { int32_t sum = 0; 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) -> int32_t { 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) -> int32_t { int32_t gain = 0; 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 : FP_ONE; }; // 1. Calculate next values for active cells for (const auto& cell_coord : _processing_order) { int x = cell_coord.first; int y = cell_coord.second; GridCell& c = grid[x][y]; int32_t val = 0; if (c.type == GridCell::EMPTY) { val = 0; } else if (c.type == GridCell::FIXED_OSCILLATOR) { // Gather inputs for modulation int32_t mod = getInputFromTheBack(x, y, c); // Freq 10 to 1000 Hz. int32_t freq = 10 + ((c.param * 990) >> FP_SHIFT) + ((mod * 500) >> FP_SHIFT); if (freq < 1) freq = 1; // Fixed point phase accumulation uint32_t inc = freq * 97391; c.phase_accumulator += inc; // Top 8 bits of 32-bit accumulator form the 256-entry table index val = sine_table[c.phase_accumulator >> 24]; val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT; } else if (c.type == GridCell::INPUT_OSCILLATOR) { int32_t mod = getInputFromTheBack(x, y, c); // Freq based on current note + octave param (1-5) int octave = 1 + ((c.param * 5) >> FP_SHIFT); // Map 0.0-1.0 to 1-5 // Use the engine's global increment directly to avoid float conversion round-trip uint32_t baseInc = _increment; uint32_t inc = baseInc << (octave - 1); // Apply FM (mod is float, convert to fixed point increment) inc += (int32_t)(((int64_t)mod * 500 * 97391) >> FP_SHIFT); c.phase_accumulator += inc; val = sine_table[c.phase_accumulator >> 24]; val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT; } else if (c.type == GridCell::WAVETABLE) { int32_t mod = getInputFromTheBack(x, y, c); // Track current note frequency + FM. Use direct increment for speed. uint32_t inc = _increment + (int32_t)(((int64_t)mod * 500 * 97391) >> FP_SHIFT); c.phase_accumulator += inc; int wave_select = (c.param * 8) >> FP_SHIFT; bool phase_upper = (c.phase_accumulator & 0x80000000); switch(wave_select) { case 0: val = sine_table[c.phase_accumulator >> 24]; break; case 1: val = (int32_t)((c.phase_accumulator >> 16) & 0xFFFF) - 32768; break; // Saw case 2: val = phase_upper ? -32767 : 32767; break; // Square case 3: val = sine_table[c.phase_accumulator >> 24]; break; // Triangle (fallback) case 4: val = 32767 - (int32_t)((c.phase_accumulator >> 16) & 0xFFFF); break; // Ramp case 5: val = ((c.phase_accumulator >> 30) == 0) ? 32767 : -32767; break; // Pulse 25% default: val = sine_table[c.phase_accumulator >> 24]; break; } val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT; } else if (c.type == GridCell::NOISE) { int32_t mod = getInputFromTheBack(x, y, c); int32_t white = _random(); int shade = (c.param * 5) >> FP_SHIFT; switch(shade) { case 0: // Brown (Leaky integrator) c.phase = (c.phase + (white >> 3)) - (c.phase >> 4); val = c.phase * 3; // Gain up break; case 1: // Pink (Approx: LPF) c.phase = (c.phase >> 1) + (white >> 1); 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) >> 1; // LPF val = white - c.phase; // HPF result break; } // Apply Amplitude Modulation (AM) from input val = (val * (FP_ONE + mod)) >> FP_SHIFT; val = (val * getSideInputGain(x, y, c)) >> FP_SHIFT; } else if (c.type == GridCell::LFO) { // Low Frequency Oscillator (0.1 Hz to 20 Hz) int32_t freq_x10 = 1 + ((c.param * 199) >> FP_SHIFT); uint32_t inc = freq_x10 * 9739; c.phase_accumulator += inc; // Output full range -1.0 to 1.0 val = sine_table[c.phase_accumulator >> 24]; } 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 ? FP_MAX : 0; } else if (c.type == GridCell::ADSR_ATTACK) { // Slew Limiter (Up only) int32_t in = getInputFromTheBack(x, y, c); int32_t rate = (1 << 20) / (1 + (c.param >> 4)); if (in > (c.phase >> 9)) { c.phase += rate; if ((c.phase >> 9) > in) c.phase = in << 9; } else { c.phase = in << 9; } val = c.phase >> 9; } else if (c.type == GridCell::ADSR_DECAY || c.type == GridCell::ADSR_RELEASE) { // Slew Limiter (Down only) int32_t in = getInputFromTheBack(x, y, c); int32_t rate = (1 << 20) / (1 + (c.param >> 4)); if (in < (c.phase >> 9)) { c.phase -= rate; if ((c.phase >> 9) < in) c.phase = in << 9; } else { c.phase = in << 9; } val = c.phase >> 9; } else if (c.type == GridCell::ADSR_SUSTAIN) { // Attenuator int32_t in = getInputFromTheBack(x, y, c); val = (in * c.param) >> FP_SHIFT; } else if (c.type == GridCell::WIRE) { // Sum inputs from all neighbors that point to me val = getSummedInput(x, y, c); } else if (c.type == GridCell::LPF) { // Input from Back int32_t in = getInputFromTheBack(x, y, c); // Simple one-pole LPF int32_t alpha = (c.param * c.param) >> FP_SHIFT; // c.phase stores previous output val = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT); c.phase = val; } else if (c.type == GridCell::HPF) { // Input from Back int32_t in = getInputFromTheBack(x, y, c); int32_t alpha = (c.param * c.param) >> FP_SHIFT; // HPF = Input - LPF int32_t lpf = c.phase + ((alpha * (in - c.phase)) >> FP_SHIFT); c.phase = lpf; val = in - lpf; } else if (c.type == GridCell::VCA) { // Input from Back int32_t in = getInputFromTheBack(x, y, c); // Mod from other directions (sum) int32_t mod = getSummedInput(x, y, c); mod -= in; // Remove signal input from mod sum (it was included in getInput calls) // Gain = Param + Mod int32_t gain = c.param + mod; if (gain < 0) gain = 0; val = (in * gain) >> FP_SHIFT; } else if (c.type == GridCell::BITCRUSHER) { int32_t in = getInputFromTheBack(x, y, c); // Bit depth reduction int32_t mask = 0xFFFF << (16 - (c.param >> 11)); val = in & mask; } else if (c.type == GridCell::DISTORTION) { int32_t in = getInputFromTheBack(x, y, c); // Soft clipping int32_t drive = FP_ONE + (c.param << 2); val = (in * drive) >> FP_SHIFT; if (val > FP_MAX) val = FP_MAX; if (val < FP_MIN) val = FP_MIN; } else if (c.type == GridCell::RECTIFIER) { int32_t in = getInputFromTheBack(x, y, c); // Mix between original and rectified based on param int32_t rect = (in < 0) ? -in : in; val = ((in * (FP_ONE - c.param)) >> FP_SHIFT) + ((rect * c.param) >> FP_SHIFT); } else if (c.type == GridCell::GLITCH) { int32_t in = getInputFromTheBack(x, y, c); // Param controls probability of glitch int32_t chance = c.param >> 2; if ((_random() & 0x7FFF) < chance) { int mode = _random() & 3; if (mode == 0) val = in << 4; // Massive gain (clipping) else if (mode == 1) val = _random(); // White noise burst else val = 0; // Drop out } else { val = in; } } else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) { // Gather inputs int32_t inputs[4]; int count = 0; int outDir = (c.type == GridCell::SINK) ? -1 : c.rotation; int32_t iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0; if(iN!=0) inputs[count++] = iN; int32_t iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0; if(iE!=0) inputs[count++] = iE; int32_t iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0; if(iS!=0) inputs[count++] = iS; int32_t iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0; if(iW!=0) inputs[count++] = iW; if (c.type == GridCell::SINK) { // Sink just sums everything val = 0; for(int k=0; k> FP_SHIFT; if (count == 0) val = 0; else { val = inputs[0]; for (int i=1; i> FP_SHIFT; break; // MUL case 2: val -= inputs[i]; break; // SUB case 3: if(inputs[i]!=0) val = (val << FP_SHIFT) / inputs[i]; break; // DIV case 4: if(inputs[i]val) val = inputs[i]; break; // MAX } } } } } // End of big switch c.next_value = val; } // End of for loop over _processing_order // 2. Update current values from next values for active cells for (const auto& cell_coord : _processing_order) { int x = cell_coord.first; int y = cell_coord.second; grid[x][y].value = grid[x][y].next_value; } 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 Q15 int32_t sample = processGridStep(); // Soft clip grid sample to avoid harsh distortion before filtering. if (sample > FP_MAX) sample = FP_MAX; if (sample < FP_MIN) sample = FP_MIN; // The filters were designed for a signal in the int16 range. // We scale the grid's output to match this expected range. // It is already Q15, so it matches int16 range. // Apply Master Volume and write to buffer buffer[i] = (int16_t)((sample * (int32_t)(_volume * FP_ONE)) >> FP_SHIFT); } }