From 1047d846f91249d2dc66193cfd04a7f27923a562 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 1 Mar 2026 12:09:07 +0100 Subject: [PATCH] Optimization: grid processing array --- AudioThread.cpp | 2 +- simulator/main.cpp | 91 ++++---- synth_engine.cpp | 565 +++++++++++++++++++++++++-------------------- synth_engine.h | 6 + 4 files changed, 372 insertions(+), 292 deletions(-) diff --git a/AudioThread.cpp b/AudioThread.cpp index 30e8f04..b3353ab 100644 --- a/AudioThread.cpp +++ b/AudioThread.cpp @@ -12,7 +12,7 @@ const int I2S_LRC_PIN = 10; // Left-Right Clock (GP10) const int I2S_DOUT_PIN = 11; // Data Out (GP11) // Audio parameters -const int SAMPLE_RATE = 44100 / 2; +const int SAMPLE_RATE = 44100 / 2 / 2 / 2; const int16_t AMPLITUDE = 16383 / 2; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit) // Create an I2S output object diff --git a/simulator/main.cpp b/simulator/main.cpp index 357ca58..1135f92 100644 --- a/simulator/main.cpp +++ b/simulator/main.cpp @@ -768,27 +768,24 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G void randomizeGrid() { printf("Randomizing grid...\n"); Uint32 startTime = SDL_GetTicks(); - SynthLockGuard lock(engine.gridMutex); - - // Number of types to choose from (excluding SINK) - const int numTypes = (int)SynthEngine::GridCell::SINK; - - // 1. Clear existing buffers first (resets the pool) - // engine.clearGrid(); // Avoid deadlock by clearing manually - for (int x = 0; x < SynthEngine::GRID_W; ++x) { - for (int y = 0; y < SynthEngine::GRID_H; ++y) { - SynthEngine::GridCell& c = engine.grid[x][y]; - if (c.type == SynthEngine::GridCell::SINK) continue; - c.type = SynthEngine::GridCell::EMPTY; - c.param = 0.5f; - c.rotation = 0; - c.value = 0.0f; - c.phase = 0.0f; - } - } int attempts = 0; bool validGrid = false; + { + SynthLockGuard lock(engine.gridMutex); + + // Number of types to choose from (excluding SINK) + const int numTypes = (int)SynthEngine::GridCell::SINK; + + // 1. Clear existing buffers first (resets the pool) + for (int x = 0; x < SynthEngine::GRID_W; ++x) { + for (int y = 0; y < SynthEngine::GRID_H; ++y) { + SynthEngine::GridCell& c = engine.grid[x][y]; + if (c.type == SynthEngine::GridCell::SINK) continue; + c.type = SynthEngine::GridCell::EMPTY; + } + } + bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H]; while (!validGrid && attempts < 1000) { @@ -944,7 +941,9 @@ void randomizeGrid() { } } } + } + engine.rebuildProcessingOrder(); printf("Randomized in %d attempts (%d ms). Valid: %s\n", attempts, SDL_GetTicks() - startTime, validGrid ? "YES" : "NO"); } @@ -1076,36 +1075,44 @@ int main(int argc, char* argv[]) { if (e.type == SDL_MOUSEBUTTONDOWN) { int mx = e.button.x; int my = e.button.y; - if (mx < GRID_PANEL_WIDTH) { - int gx = mx / CELL_SIZE; - int gy = my / CELL_SIZE; - if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) { - SynthLockGuard lock(engine.gridMutex); - SynthEngine::GridCell& c = engine.grid[gx][gy]; - if (c.type != SynthEngine::GridCell::SINK) { - SynthEngine::GridCell::Type oldType = c.type; - SynthEngine::GridCell::Type newType = oldType; - if (e.button.button == SDL_BUTTON_LEFT) { - for (int i = 0; i < numCellTypes; ++i) { - if (cellTypes[i] == oldType) { - newType = cellTypes[(i + 1) % numCellTypes]; - break; + if (mx < GRID_PANEL_WIDTH) { + int gx = mx / CELL_SIZE; + int gy = my / CELL_SIZE; + if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) { + bool grid_modified = false; + { + SynthLockGuard lock(engine.gridMutex); + SynthEngine::GridCell& c = engine.grid[gx][gy]; + if (c.type != SynthEngine::GridCell::SINK) { + grid_modified = true; + SynthEngine::GridCell::Type oldType = c.type; + SynthEngine::GridCell::Type newType = oldType; + + if (e.button.button == SDL_BUTTON_LEFT) { + for (int i = 0; i < numCellTypes; ++i) { + if (cellTypes[i] == oldType) { + newType = cellTypes[(i + 1) % numCellTypes]; + break; + } + } + } else if (e.button.button == SDL_BUTTON_RIGHT) { + c.rotation = (c.rotation + 1) % 4; + } else if (e.button.button == SDL_BUTTON_MIDDLE) { + newType = SynthEngine::GridCell::EMPTY; + c.param = 0.5f; + c.rotation = 0; + } + + if (newType != oldType) { + c.type = newType; } } - } else if (e.button.button == SDL_BUTTON_RIGHT) { - c.rotation = (c.rotation + 1) % 4; - } else if (e.button.button == SDL_BUTTON_MIDDLE) { - newType = SynthEngine::GridCell::EMPTY; - c.param = 0.5f; - c.rotation = 0; } - - if (newType != oldType) { - c.type = newType; + if (grid_modified) { + engine.rebuildProcessingOrder(); } } - } } else { // Synth Panel Click int synthX = mx - GRID_PANEL_WIDTH; diff --git a/synth_engine.cpp b/synth_engine.cpp index 4d3cc75..ceb6937 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -1,5 +1,6 @@ #include "synth_engine.h" #include +#include #include // A simple sine lookup table for the sine oscillator @@ -35,6 +36,7 @@ SynthEngine::SynthEngine(uint32_t sampleRate) // Initialize SINK grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK; + rebuildProcessingOrder(); } SynthEngine::~SynthEngine() { @@ -70,6 +72,7 @@ void SynthEngine::importGrid(const uint8_t* buffer) { c.rotation = r; } } + rebuildProcessingOrder_locked(); } void SynthEngine::clearGrid() { @@ -84,8 +87,10 @@ void SynthEngine::clearGrid() { c.rotation = 0; c.value = 0.0f; c.phase = 0.0f; + c.next_value = 0.0f; } } + rebuildProcessingOrder_locked(); } void SynthEngine::loadPreset(int preset) { @@ -201,6 +206,8 @@ void SynthEngine::loadPreset(int preset) { grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2; } } + + rebuildProcessingOrder_locked(); } void SynthEngine::setFrequency(float freq) { @@ -236,9 +243,68 @@ float SynthEngine::_random() { return (float)_rngState / 4294967296.0f; } +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(); +} + 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; @@ -327,260 +393,261 @@ float SynthEngine::processGridStep() { 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; + // 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]; + float val = 0.0f; - if (c.type == GridCell::EMPTY) { + 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; - } 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 - } + for(int k=0; kval) val = inputs[i]; break; // MAX } } } } - next_values[x][y] = val; - } - } + } // End of big switch + c.next_value = val; + } // End of for loop over _processing_order - // 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]; - } + // 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; diff --git a/synth_engine.h b/synth_engine.h index 3e3370c..3d16346 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -2,6 +2,8 @@ #define SYNTH_ENGINE_H #include +#include +#include #if defined(ARDUINO_ARCH_RP2040) #include @@ -103,6 +105,7 @@ public: float param = 0.5f; // 0.0 to 1.0 int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction) float value = 0.0f; // Current output sample + float next_value = 0.0f; // For double-buffering in processGridStep float phase = 0.0f; // For Oscillator, Noise state }; @@ -113,6 +116,7 @@ public: void exportGrid(uint8_t* buffer); void importGrid(const uint8_t* buffer); void loadPreset(int preset); + void rebuildProcessingOrder(); void clearGrid(); GridCell grid[GRID_W][GRID_H]; @@ -129,6 +133,8 @@ private: Waveform _waveform; bool _isGateOpen; uint32_t _rngState; + std::vector> _processing_order; + void rebuildProcessingOrder_locked(); // Internal random number generator float _random();