Gate control and ADSR as elements

This commit is contained in:
Dejvino 2026-02-28 18:25:02 +01:00
parent db76f4fcef
commit 7ff85048df
3 changed files with 249 additions and 161 deletions

158
main.cpp
View File

@ -27,9 +27,6 @@ std::atomic<size_t> vis_write_index{0};
// --- Control State ---
int current_octave = 4; // C4 is middle C
float knob_vol_val = 0.5f;
// ADSR (A, D, R in seconds, S in 0-1)
float adsr_vals[4] = {0.05f, 0.2f, 0.6f, 0.5f};
float filter_vals[2] = {1.0f, 0.0f}; // LP (1.0=Open), HP (0.0=Open)
// --- MIDI / Keyboard Input State ---
std::map<SDL_Scancode, int> key_to_note_map;
@ -357,6 +354,74 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param, 0, 255, 255);
drawTypeLabel(renderer, x, y, 'L');
} else if (cell.type == SynthEngine::GridCell::GATE) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
SDL_RenderDrawRect(renderer, &box);
if (cell.value > 0.5f) SDL_RenderFillRect(renderer, &box);
drawString(renderer, cx - 8, cy - 5, 12, "GAT");
drawTypeLabel(renderer, x, y, '!');
} else if (cell.type == SynthEngine::GridCell::GATE_INPUT) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
SDL_RenderDrawRect(renderer, &box);
if (cell.value > 0.5f) SDL_RenderFillRect(renderer, &box);
drawString(renderer, cx - 8, cy - 5, 12, "G-IN");
drawTypeLabel(renderer, x, y, 'K');
} else if (cell.type == SynthEngine::GridCell::ADSR_ATTACK) {
// Draw Ramp Up
SDL_RenderDrawLine(renderer, cx-r, cy+r, cx+r, cy-r);
// I/O
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
int odx=0, ody=0;
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'A');
} else if (cell.type == SynthEngine::GridCell::ADSR_DECAY) {
// Draw Ramp Down
SDL_RenderDrawLine(renderer, cx-r, cy-r, cx+r, cy+r);
// I/O
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
int odx=0, ody=0;
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'D');
} else if (cell.type == SynthEngine::GridCell::ADSR_SUSTAIN) {
// Draw Level
SDL_RenderDrawLine(renderer, cx-r, cy, cx+r, cy);
// I/O
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
int odx=0, ody=0;
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'S');
} else if (cell.type == SynthEngine::GridCell::ADSR_RELEASE) {
// Draw Ramp Down
SDL_RenderDrawLine(renderer, cx-r, cy-r, cx+r, cy+r);
// I/O
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
int odx=0, ody=0;
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawParamBar(renderer, x, y, size, cell.param, 255, 255, 255);
drawTypeLabel(renderer, x, y, 'E');
} else if (cell.type == SynthEngine::GridCell::LPF || cell.type == SynthEngine::GridCell::HPF) {
// Box
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
@ -432,6 +497,32 @@ void drawGridCell(SDL_Renderer* renderer, int x, int y, int size, SynthEngine::G
drawString(renderer, x + 5, y + size - 18, 10, buf);
drawParamBar(renderer, x, y, size, cell.param, 255, 100, 100);
drawTypeLabel(renderer, x, y, 'X');
} else if (cell.type == SynthEngine::GridCell::RECTIFIER) {
SDL_Rect box = {cx - r, cy - r, r*2, r*2};
SDL_RenderDrawRect(renderer, &box);
drawString(renderer, cx - 8, cy - 5, 12, "ABS");
// I/O
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
int odx=0, ody=0;
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawParamBar(renderer, x, y, size, cell.param, 255, 150, 0);
drawTypeLabel(renderer, x, y, '|');
} else if (cell.type == SynthEngine::GridCell::PITCH_SHIFTER) {
drawString(renderer, cx - 8, cy - 5, 12, "PIT");
// I/O
int inDir = (cell.rotation + 2) % 4;
int idx=0, idy=0;
if(inDir==0) idy=-r; else if(inDir==1) idx=r; else if(inDir==2) idy=r; else idx=-r;
SDL_RenderDrawLine(renderer, cx+idx, cy+idy, cx, cy);
int odx=0, ody=0;
if(cell.rotation==0) ody=-r; else if(cell.rotation==1) odx=r; else if(cell.rotation==2) ody=r; else odx=-r;
SDL_RenderDrawLine(renderer, cx, cy, cx+odx, cy+ody);
drawParamBar(renderer, x, y, size, cell.param, 100, 255, 100);
drawTypeLabel(renderer, x, y, '^');
} else if (cell.type == SynthEngine::GridCell::GLITCH) {
drawString(renderer, cx - 8, cy - 5, 12, "GLT");
// I/O
@ -559,7 +650,7 @@ void clearGrid() {
SynthEngine::GridCell& c = engine.grid[x][y];
if (c.type == SynthEngine::GridCell::SINK) continue;
if ((c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB) && c.buffer) {
if ((c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB || c.type == SynthEngine::GridCell::PITCH_SHIFTER) && c.buffer) {
delete[] c.buffer;
c.buffer = nullptr;
c.buffer_size = 0;
@ -583,7 +674,7 @@ void randomizeGrid() {
for (int x = 0; x < 5; ++x) {
for (int y = 0; y < 8; ++y) {
SynthEngine::GridCell& c = engine.grid[x][y];
if ((c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB) && c.buffer) {
if (c.buffer) {
delete[] c.buffer;
c.buffer = nullptr;
c.buffer_size = 0;
@ -703,7 +794,7 @@ void randomizeGrid() {
for (int x = 0; x < 5; ++x) {
for (int y = 0; y < 8; ++y) {
SynthEngine::GridCell& c = engine.grid[x][y];
if (c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB) {
if (c.type == SynthEngine::GridCell::DELAY || c.type == SynthEngine::GridCell::REVERB || c.type == SynthEngine::GridCell::PITCH_SHIFTER) {
c.buffer_size = 2 * SAMPLE_RATE;
c.buffer = new float[c.buffer_size]();
c.write_idx = 0;
@ -773,10 +864,6 @@ int main(int argc, char* argv[]) {
engine.setVolume(knob_vol_val);
engine.setGate(false); // Start with silence
// Init engine with default UI values
engine.setADSR(adsr_vals[0]*2.0f, adsr_vals[1]*2.0f, adsr_vals[2], adsr_vals[3]*2.0f);
engine.setFilter(20.0f + filter_vals[0]*19980.0f, 20.0f + filter_vals[1]*19980.0f);
// --- Main Loop ---
const SynthEngine::GridCell::Type cellTypes[] = {
SynthEngine::GridCell::EMPTY,
@ -785,6 +872,12 @@ int main(int argc, char* argv[]) {
SynthEngine::GridCell::WAVETABLE,
SynthEngine::GridCell::NOISE,
SynthEngine::GridCell::LFO,
SynthEngine::GridCell::GATE,
SynthEngine::GridCell::GATE_INPUT,
SynthEngine::GridCell::ADSR_ATTACK,
SynthEngine::GridCell::ADSR_DECAY,
SynthEngine::GridCell::ADSR_SUSTAIN,
SynthEngine::GridCell::ADSR_RELEASE,
SynthEngine::GridCell::FORK,
SynthEngine::GridCell::WIRE,
SynthEngine::GridCell::LPF,
@ -792,6 +885,8 @@ int main(int argc, char* argv[]) {
SynthEngine::GridCell::VCA,
SynthEngine::GridCell::BITCRUSHER,
SynthEngine::GridCell::DISTORTION,
SynthEngine::GridCell::RECTIFIER,
SynthEngine::GridCell::PITCH_SHIFTER,
SynthEngine::GridCell::GLITCH,
SynthEngine::GridCell::OPERATOR,
SynthEngine::GridCell::DELAY,
@ -859,14 +954,14 @@ int main(int argc, char* argv[]) {
if (newType != oldType) {
// If old type was DELAY, free its buffer
if ((oldType == SynthEngine::GridCell::DELAY || oldType == SynthEngine::GridCell::REVERB) && c.buffer) {
if ((oldType == SynthEngine::GridCell::DELAY || oldType == SynthEngine::GridCell::REVERB || oldType == SynthEngine::GridCell::PITCH_SHIFTER) && c.buffer) {
delete[] c.buffer;
c.buffer = nullptr;
c.buffer_size = 0;
}
c.type = newType;
// If new type is DELAY, allocate its buffer
if (newType == SynthEngine::GridCell::DELAY || newType == SynthEngine::GridCell::REVERB) {
if (newType == SynthEngine::GridCell::DELAY || newType == SynthEngine::GridCell::REVERB || newType == SynthEngine::GridCell::PITCH_SHIFTER) {
c.buffer_size = 2 * SAMPLE_RATE; // Max 2 seconds delay
c.buffer = new float[c.buffer_size](); // Allocate and zero-initialize
c.write_idx = 0; // Reset write index
@ -945,37 +1040,6 @@ int main(int argc, char* argv[]) {
if (mouseX >= GRID_PANEL_WIDTH) {
int synthX = mouseX - GRID_PANEL_WIDTH;
// Handle Sliders
// ADSR: x=200, 250, 300, 350. y=380, h=150
// Filters: x=450, 500.
int sliderY = 380;
int sliderH = 150;
int sliderW = 30;
auto checkSlider = [&](int idx, int sx, float* val) {
if (synthX >= sx && synthX <= sx + sliderW && mouseY >= sliderY - 20 && mouseY <= sliderY + sliderH + 20) {
*val = 1.0f - (float)(mouseY - sliderY) / (float)sliderH;
if (*val < 0.0f) *val = 0.0f;
if (*val > 1.0f) *val = 1.0f;
return true;
}
return false;
};
bool changed = false;
if (checkSlider(0, 200, &adsr_vals[0])) changed = true;
if (checkSlider(1, 250, &adsr_vals[1])) changed = true;
if (checkSlider(2, 300, &adsr_vals[2])) changed = true;
if (checkSlider(3, 350, &adsr_vals[3])) changed = true;
if (changed) engine.setADSR(adsr_vals[0]*2.0f, adsr_vals[1]*2.0f, adsr_vals[2], adsr_vals[3]*2.0f);
if (checkSlider(0, 450, &filter_vals[0]) || checkSlider(1, 500, &filter_vals[1])) {
// Map 0-1 to 20Hz-20kHz
float lpFreq = 20.0f + pow(filter_vals[0], 2.0f) * 19980.0f; // Exponential feel
float hpFreq = 20.0f + pow(filter_vals[1], 2.0f) * 19980.0f;
engine.setFilter(lpFreq, hpFreq);
}
}
}
} else if (e.type == SDL_KEYDOWN) {
@ -1078,14 +1142,6 @@ int main(int argc, char* argv[]) {
drawToggle(renderer, 580, 450, 30, auto_melody_enabled);
drawSlider(renderer, 200, 380, 30, 150, adsr_vals[0], "A");
drawSlider(renderer, 250, 380, 30, 150, adsr_vals[1], "D");
drawSlider(renderer, 300, 380, 30, 150, adsr_vals[2], "S");
drawSlider(renderer, 350, 380, 30, 150, adsr_vals[3], "R");
drawSlider(renderer, 450, 380, 30, 150, filter_vals[0], "LP");
drawSlider(renderer, 500, 380, 30, 150, filter_vals[1], "HP");
// --- Draw Grid Panel (Left) ---
SDL_Rect gridViewport = {0, 0, GRID_PANEL_WIDTH, WINDOW_HEIGHT};
SDL_RenderSetViewport(renderer, &gridViewport);

View File

@ -25,20 +25,12 @@ SynthEngine::SynthEngine(uint32_t sampleRate)
_volume(0.5f),
_waveform(SAWTOOTH),
_isGateOpen(false),
_envState(ENV_IDLE),
_envLevel(0.0f),
_attackInc(0.0f),
_decayDec(0.0f),
_sustainLevel(1.0f),
_releaseDec(0.0f),
_lpAlpha(1.0f), _hpAlpha(0.0f),
_lpVal(0.0f), _hpVal(0.0f),
grid{}
grid{},
_rngState(12345)
{
fill_sine_table();
// Initialize with a default frequency
setFrequency(440.0f);
setADSR(0.05f, 0.1f, 0.7f, 0.2f); // Default envelope
// Initialize SINK
grid[2][3].type = GridCell::SINK;
@ -76,49 +68,28 @@ void SynthEngine::setWaveform(Waveform form) {
void SynthEngine::setGate(bool isOpen) {
_isGateOpen = isOpen;
if (isOpen) {
_envState = ENV_ATTACK;
} else {
_envState = ENV_RELEASE;
}
}
void SynthEngine::setADSR(float attack, float decay, float sustain, float release) {
// Calculate increments per sample based on time in seconds
// Avoid division by zero
_attackInc = (attack > 0.001f) ? (1.0f / (attack * _sampleRate)) : 1.0f;
_decayDec = (decay > 0.001f) ? (1.0f / (decay * _sampleRate)) : 1.0f;
_sustainLevel = sustain;
_releaseDec = (release > 0.001f) ? (1.0f / (release * _sampleRate)) : 1.0f;
}
void SynthEngine::setFilter(float lpCutoff, float hpCutoff) {
// Simple one-pole filter coefficient calculation: alpha = 2*PI*fc/fs
_lpAlpha = 2.0f * M_PI * lpCutoff / _sampleRate;
if (_lpAlpha > 1.0f) _lpAlpha = 1.0f;
if (_lpAlpha < 0.0f) _lpAlpha = 0.0f;
_hpAlpha = 2.0f * M_PI * hpCutoff / _sampleRate;
if (_hpAlpha > 1.0f) _hpAlpha = 1.0f;
if (_hpAlpha < 0.0f) _hpAlpha = 0.0f;
}
float SynthEngine::getFrequency() const {
return (float)((double)_increment * (double)_sampleRate / 4294967296.0);
}
float SynthEngine::_random() {
// Simple Linear Congruential Generator
_rngState = _rngState * 1664525 + 1013904223;
return (float)_rngState / 4294967296.0f;
}
float SynthEngine::processGridStep() {
// Double buffer for values to handle feedback loops gracefully (1-sample delay)
float next_values[5][8];
// Helper to get input from a neighbor
auto getInput = [&](int tx, int ty, int from_x, int from_y) -> float {
if (from_x < 0 || from_x >= 5 || from_y < 0 || from_y >= 8) return 0.0f;
auto isConnected = [&](int tx, int ty, int from_x, int from_y) -> bool {
if (from_x < 0 || from_x >= 5 || from_y < 0 || from_y >= 8) return false;
GridCell& n = grid[from_x][from_y];
// Check if neighbor outputs to (tx, ty)
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::LPF || n.type == GridCell::HPF || n.type == GridCell::VCA || n.type == GridCell::BITCRUSHER || n.type == GridCell::DISTORTION || n.type == GridCell::GLITCH || n.type == GridCell::OPERATOR || n.type == GridCell::DELAY || n.type == GridCell::REVERB) {
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 || 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;
@ -139,11 +110,33 @@ float SynthEngine::processGridStep() {
int leftOut = (n.rotation + 3) % 4;
int rightOut = (n.rotation + 1) % 4;
if (dir == leftOut || dir == rightOut) connects = true;
}
return connects;
};
// Helper to get input from a neighbor
auto getInput = [&](int tx, int ty, int from_x, int from_y) -> float {
if (!isConnected(tx, ty, from_x, from_y)) return 0.0f;
GridCell& n = grid[from_x][from_y];
if (n.type == GridCell::FORK) {
int dx = tx - from_x;
int dy = ty - from_y;
int dir = -1;
if (dx == 0 && dy == -1) dir = 0; // N
if (dx == 1 && dy == 0) dir = 1; // E
if (dx == 0 && dy == 1) dir = 2; // S
if (dx == -1 && dy == 0) dir = 3; // W
int leftOut = (n.rotation + 3) % 4;
int rightOut = (n.rotation + 1) % 4;
if (dir == leftOut) return n.value * (1.0f - n.param) * 2.0f;
if (dir == rightOut) return n.value * n.param * 2.0f;
}
return connects ? n.value : 0.0f;
return n.value;
};
// Helper to sum inputs excluding the output direction
@ -164,6 +157,20 @@ float SynthEngine::processGridStep() {
return getInput(x, y, x+dx, y+dy);
};
auto getSideInputGain = [&](int x, int y, GridCell& c) -> float {
float gain = 0.0f;
bool hasSide = false;
// Left (rot+3)
int lDir = (c.rotation + 3) % 4;
int ldx=0, ldy=0; if(lDir==0) ldy=-1; else if(lDir==1) ldx=1; else if(lDir==2) ldy=1; else ldx=-1;
if (isConnected(x, y, x+ldx, y+ldy)) { hasSide = true; gain += getInput(x, y, x+ldx, y+ldy); }
// Right (rot+1)
int rDir = (c.rotation + 1) % 4;
int rdx=0, rdy=0; if(rDir==0) rdy=-1; else if(rDir==1) rdx=1; else if(rDir==2) rdy=1; else rdx=-1;
if (isConnected(x, y, x+rdx, y+rdy)) { hasSide = true; gain += getInput(x, y, x+rdx, y+rdy); }
return hasSide ? gain : 1.0f;
};
for (int x = 0; x < 5; ++x) {
for (int y = 0; y < 8; ++y) {
GridCell& c = grid[x][y];
@ -173,7 +180,7 @@ float SynthEngine::processGridStep() {
val = 0.0f;
} else if (c.type == GridCell::FIXED_OSCILLATOR) {
// Gather inputs for modulation
float mod = getSummedInput(x, y, c);
float mod = getInputFromTheBack(x, y, c);
// Freq 10 to 1000 Hz
float freq = 10.0f + c.param * 990.0f + (mod * 500.0f); // FM
@ -183,8 +190,9 @@ float SynthEngine::processGridStep() {
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 = getSummedInput(x, y, c);
float mod = getInputFromTheBack(x, y, c);
// Freq based on current note + octave param (1-5)
float baseFreq = getFrequency();
@ -196,8 +204,9 @@ float SynthEngine::processGridStep() {
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 = getSummedInput(x, y, c);
float mod = getInputFromTheBack(x, y, c);
// Track current note frequency + FM
float freq = getFrequency() + (mod * 500.0f);
@ -228,10 +237,11 @@ float SynthEngine::processGridStep() {
val /= 0.9f; // Normalize
break;
}
val *= getSideInputGain(x, y, c);
} else if (c.type == GridCell::NOISE) {
float mod = getSummedInput(x, y, c);
float mod = getInputFromTheBack(x, y, c);
float white = (float)rand() / (float)RAND_MAX * 2.0f - 1.0f;
float white = _random() * 2.0f - 1.0f;
int shade = (int)(c.param * 4.99f);
switch(shade) {
case 0: // Brown (Leaky integrator)
@ -257,6 +267,7 @@ float SynthEngine::processGridStep() {
// 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;
@ -268,6 +279,35 @@ float SynthEngine::processGridStep() {
} else if (c.type == GridCell::FORK) {
// Sum inputs from "Back" (Input direction)
val = getInputFromTheBack(x, y, c);
} else if (c.type == GridCell::GATE || 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);
@ -325,14 +365,63 @@ float SynthEngine::processGridStep() {
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::PITCH_SHIFTER) {
float in = getInputFromTheBack(x, y, c);
if (c.buffer && c.buffer_size > 0) {
c.buffer[c.write_idx] = in;
// Granular pitch shift
// Pitch ratio: 0.5 to 2.0
float pitchRatio = 0.5f + c.param * 1.5f;
// Delay rate change: 1.0 - pitchRatio
// If pitch=1, rate=0 (delay constant). If pitch=2, rate=-1 (delay decreases).
float rate = 1.0f - pitchRatio;
c.phase += rate;
// Wrap phase within window (e.g. 4096 samples)
float windowSize = 4096.0f;
if (c.phase >= windowSize) c.phase -= windowSize;
if (c.phase < 0.0f) c.phase += windowSize;
// Read from buffer
// Simple crossfade windowing would be better, but for now just a single tap with moving delay
// To reduce clicks, we really need 2 taps. Let's stick to single tap for simplicity in this grid context, or maybe just a vibrato if rate is LFO?
// Actually, let's implement the 2-tap crossfade for quality.
// Tap 1
float p1 = c.phase;
float p2 = c.phase + windowSize * 0.5f;
if (p2 >= windowSize) p2 -= windowSize;
// Window function (Triangle)
auto getWindow = [&](float p) -> float {
return 1.0f - fabsf(2.0f * (p / windowSize) - 1.0f);
};
// Read indices
int r1 = (int)c.write_idx - (int)p1;
if (r1 < 0) r1 += c.buffer_size;
int r2 = (int)c.write_idx - (int)p2;
if (r2 < 0) r2 += c.buffer_size;
val = c.buffer[r1] * getWindow(p1) + c.buffer[r2] * getWindow(p2);
c.write_idx = (c.write_idx + 1) % c.buffer_size;
} else {
val = 0.0f;
}
} 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 ((float)rand() / RAND_MAX < chance) {
int mode = rand() % 3;
if (_random() < chance) {
int mode = (int)(_random() * 3.0f);
if (mode == 0) val = in * 50.0f; // Massive gain (clipping)
else if (mode == 1) val = (float)(rand() % 32768) / 16384.0f - 1.0f; // White noise burst
else if (mode == 1) val = _random() * 2.0f - 1.0f; // White noise burst
else val = 0.0f; // Drop out
} else {
val = in;
@ -451,39 +540,6 @@ void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
// We scale the grid's float output to match this expected range.
sampleF *= 32767.0f;
// Apply Filters (One-pole)
// Low Pass
_lpVal += _lpAlpha * (sampleF - _lpVal);
sampleF = _lpVal;
// High Pass (implemented as Input - LowPass(hp_cutoff))
_hpVal += _hpAlpha * (sampleF - _hpVal);
sampleF = sampleF - _hpVal;
// Apply ADSR Envelope
switch (_envState) {
case ENV_ATTACK:
_envLevel += _attackInc;
if (_envLevel >= 1.0f) { _envLevel = 1.0f; _envState = ENV_DECAY; }
break;
case ENV_DECAY:
_envLevel -= _decayDec;
if (_envLevel <= _sustainLevel) { _envLevel = _sustainLevel; _envState = ENV_SUSTAIN; }
break;
case ENV_SUSTAIN:
_envLevel = _sustainLevel;
break;
case ENV_RELEASE:
_envLevel -= _releaseDec;
if (_envLevel <= 0.0f) { _envLevel = 0.0f; _envState = ENV_IDLE; }
break;
case ENV_IDLE:
_envLevel = 0.0f;
break;
}
sampleF *= _envLevel;
// Apply Master Volume and write to buffer
buffer[i] = static_cast<int16_t>(sampleF * _volume);
}

View File

@ -68,25 +68,9 @@ public:
*/
float getFrequency() const;
/**
* @brief Configures the ADSR envelope.
* @param attack Attack time in seconds.
* @param decay Decay time in seconds.
* @param sustain Sustain level (0.0 to 1.0).
* @param release Release time in seconds.
*/
void setADSR(float attack, float decay, float sustain, float release);
/**
* @brief Configures the filters.
* @param lpCutoff Low-pass cutoff frequency in Hz.
* @param hpCutoff High-pass cutoff frequency in Hz.
*/
void setFilter(float lpCutoff, float hpCutoff);
// --- Grid Synth ---
struct GridCell {
enum Type { EMPTY, FIXED_OSCILLATOR, INPUT_OSCILLATOR, WAVETABLE, NOISE, LFO, FORK, WIRE, LPF, HPF, VCA, BITCRUSHER, DISTORTION, GLITCH, OPERATOR, DELAY, REVERB, SINK };
enum Type { EMPTY, FIXED_OSCILLATOR, INPUT_OSCILLATOR, WAVETABLE, NOISE, LFO, GATE, GATE_INPUT, ADSR_ATTACK, ADSR_DECAY, ADSR_SUSTAIN, ADSR_RELEASE, FORK, WIRE, LPF, HPF, VCA, BITCRUSHER, DISTORTION, RECTIFIER, PITCH_SHIFTER, GLITCH, OPERATOR, DELAY, REVERB, SINK };
enum Op { OP_ADD, OP_MUL, OP_SUB, OP_DIV, OP_MIN, OP_MAX };
Type type = EMPTY;
@ -112,18 +96,10 @@ private:
float _volume;
Waveform _waveform;
bool _isGateOpen;
uint32_t _rngState;
// ADSR State
enum EnvState { ENV_IDLE, ENV_ATTACK, ENV_DECAY, ENV_SUSTAIN, ENV_RELEASE };
EnvState _envState;
float _envLevel;
float _attackInc, _decayDec, _sustainLevel, _releaseDec;
// Filter State
float _lpAlpha;
float _hpAlpha;
float _lpVal;
float _hpVal;
// Internal random number generator
float _random();
};
#endif // SYNTH_ENGINE_H