Optimization: grid processing array
This commit is contained in:
parent
82bab0698b
commit
1047d846f9
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
565
synth_engine.cpp
565
synth_engine.cpp
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user