Optimization: grid processing array

This commit is contained in:
Dejvino 2026-03-01 12:09:07 +01:00
parent 82bab0698b
commit 1047d846f9
4 changed files with 372 additions and 292 deletions

View File

@ -12,7 +12,7 @@ const int I2S_LRC_PIN = 10; // Left-Right Clock (GP10)
const int I2S_DOUT_PIN = 11; // Data Out (GP11) const int I2S_DOUT_PIN = 11; // Data Out (GP11)
// Audio parameters // 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) const int16_t AMPLITUDE = 16383 / 2; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit)
// Create an I2S output object // Create an I2S output object

View File

@ -768,27 +768,24 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
void randomizeGrid() { void randomizeGrid() {
printf("Randomizing grid...\n"); printf("Randomizing grid...\n");
Uint32 startTime = SDL_GetTicks(); Uint32 startTime = SDL_GetTicks();
SynthLockGuard<SynthMutex> 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; int attempts = 0;
bool validGrid = false; bool validGrid = false;
{
SynthLockGuard<SynthMutex> 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]; bool visited[SynthEngine::GRID_W][SynthEngine::GRID_H];
while (!validGrid && attempts < 1000) { 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"); 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) { if (e.type == SDL_MOUSEBUTTONDOWN) {
int mx = e.button.x; int mx = e.button.x;
int my = e.button.y; 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<SynthMutex> 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) { if (mx < GRID_PANEL_WIDTH) {
for (int i = 0; i < numCellTypes; ++i) { int gx = mx / CELL_SIZE;
if (cellTypes[i] == oldType) { int gy = my / CELL_SIZE;
newType = cellTypes[(i + 1) % numCellTypes]; if (gx >= 0 && gx < SynthEngine::GRID_W && gy >= 0 && gy < SynthEngine::GRID_H) {
break; bool grid_modified = false;
{
SynthLockGuard<SynthMutex> 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 (grid_modified) {
if (newType != oldType) { engine.rebuildProcessingOrder();
c.type = newType;
} }
} }
}
} else { } else {
// Synth Panel Click // Synth Panel Click
int synthX = mx - GRID_PANEL_WIDTH; int synthX = mx - GRID_PANEL_WIDTH;

View File

@ -1,5 +1,6 @@
#include "synth_engine.h" #include "synth_engine.h"
#include <math.h> #include <math.h>
#include <utility>
#include <string.h> #include <string.h>
// A simple sine lookup table for the sine oscillator // A simple sine lookup table for the sine oscillator
@ -35,6 +36,7 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
// Initialize SINK // Initialize SINK
grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK; grid[GRID_W / 2][GRID_H - 1].type = GridCell::SINK;
rebuildProcessingOrder();
} }
SynthEngine::~SynthEngine() { SynthEngine::~SynthEngine() {
@ -70,6 +72,7 @@ void SynthEngine::importGrid(const uint8_t* buffer) {
c.rotation = r; c.rotation = r;
} }
} }
rebuildProcessingOrder_locked();
} }
void SynthEngine::clearGrid() { void SynthEngine::clearGrid() {
@ -84,8 +87,10 @@ void SynthEngine::clearGrid() {
c.rotation = 0; c.rotation = 0;
c.value = 0.0f; c.value = 0.0f;
c.phase = 0.0f; c.phase = 0.0f;
c.next_value = 0.0f;
} }
} }
rebuildProcessingOrder_locked();
} }
void SynthEngine::loadPreset(int preset) { 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; grid[sinkX][y].type = GridCell::WIRE; grid[sinkX][y].rotation = 2;
} }
} }
rebuildProcessingOrder_locked();
} }
void SynthEngine::setFrequency(float freq) { void SynthEngine::setFrequency(float freq) {
@ -236,9 +243,68 @@ float SynthEngine::_random() {
return (float)_rngState / 4294967296.0f; return (float)_rngState / 4294967296.0f;
} }
void SynthEngine::rebuildProcessingOrder_locked() {
_processing_order.clear();
bool visited[GRID_W][GRID_H] = {false};
std::vector<std::pair<int, int>> 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<int, int> 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<SynthMutex> lock(gridMutex);
rebuildProcessingOrder_locked();
}
float SynthEngine::processGridStep() { 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 { 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; 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; return hasSide ? gain : 1.0f;
}; };
for (int x = 0; x < GRID_W; ++x) { // 1. Calculate next values for active cells
for (int y = 0; y < GRID_H; ++y) { for (const auto& cell_coord : _processing_order) {
GridCell& c = grid[x][y]; int x = cell_coord.first;
float val = 0.0f; 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; val = 0.0f;
} else if (c.type == GridCell::FIXED_OSCILLATOR) { for(int k=0; k<count; ++k) val += inputs[k];
// Gather inputs for modulation } else {
float mod = getInputFromTheBack(x, y, c); // Operator
int opType = (int)(c.param * 5.99f);
// Freq 10 to 1000 Hz if (count == 0) val = 0.0f;
float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM else {
if (freq < 1.0f) freq = 1.0f; val = inputs[0];
for (int i=1; i<count; ++i) {
float inc = freq * (float)SINE_TABLE_SIZE / (float)_sampleRate; switch(opType) {
c.phase += inc; case 0: val += inputs[i]; break; // ADD
if (c.phase >= SINE_TABLE_SIZE) c.phase -= SINE_TABLE_SIZE; case 1: val *= inputs[i]; break; // MUL
val = (float)sine_table[(int)c.phase] / 32768.0f; case 2: val -= inputs[i]; break; // SUB
val *= getSideInputGain(x, y, c); case 3: if(inputs[i]!=0) val /= inputs[i]; break; // DIV
} else if (c.type == GridCell::INPUT_OSCILLATOR) { case 4: if(inputs[i]<val) val = inputs[i]; break; // MIN
float mod = getInputFromTheBack(x, y, c); case 5: if(inputs[i]>val) val = inputs[i]; break; // MAX
// 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; k<count; ++k) val += inputs[k];
} else {
// Operator
int opType = (int)(c.param * 5.99f);
if (count == 0) val = 0.0f;
else {
val = inputs[0];
for (int i=1; i<count; ++i) {
switch(opType) {
case 0: val += inputs[i]; break; // ADD
case 1: val *= inputs[i]; break; // MUL
case 2: val -= inputs[i]; break; // SUB
case 3: if(inputs[i]!=0) val /= inputs[i]; break; // DIV
case 4: if(inputs[i]<val) val = inputs[i]; break; // MIN
case 5: if(inputs[i]>val) 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 // 2. Update current values from next values for active cells
for(int x=0; x < GRID_W; ++x) { for (const auto& cell_coord : _processing_order) {
for(int y=0; y < GRID_H; ++y) { int x = cell_coord.first;
grid[x][y].value = next_values[x][y]; int y = cell_coord.second;
} grid[x][y].value = grid[x][y].next_value;
} }
return grid[GRID_W / 2][GRID_H - 1].value; return grid[GRID_W / 2][GRID_H - 1].value;

View File

@ -2,6 +2,8 @@
#define SYNTH_ENGINE_H #define SYNTH_ENGINE_H
#include <stdint.h> #include <stdint.h>
#include <vector>
#include <utility>
#if defined(ARDUINO_ARCH_RP2040) #if defined(ARDUINO_ARCH_RP2040)
#include <pico/mutex.h> #include <pico/mutex.h>
@ -103,6 +105,7 @@ public:
float param = 0.5f; // 0.0 to 1.0 float param = 0.5f; // 0.0 to 1.0
int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction) int rotation = 0; // 0:N, 1:E, 2:S, 3:W (Output direction)
float value = 0.0f; // Current output sample 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 float phase = 0.0f; // For Oscillator, Noise state
}; };
@ -113,6 +116,7 @@ public:
void exportGrid(uint8_t* buffer); void exportGrid(uint8_t* buffer);
void importGrid(const uint8_t* buffer); void importGrid(const uint8_t* buffer);
void loadPreset(int preset); void loadPreset(int preset);
void rebuildProcessingOrder();
void clearGrid(); void clearGrid();
GridCell grid[GRID_W][GRID_H]; GridCell grid[GRID_W][GRID_H];
@ -129,6 +133,8 @@ private:
Waveform _waveform; Waveform _waveform;
bool _isGateOpen; bool _isGateOpen;
uint32_t _rngState; uint32_t _rngState;
std::vector<std::pair<int, int>> _processing_order;
void rebuildProcessingOrder_locked();
// Internal random number generator // Internal random number generator
float _random(); float _random();