From 7ff85048df576f8a2766161dc7a67c3ab098ca62 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sat, 28 Feb 2026 18:25:02 +0100 Subject: [PATCH] Gate control and ADSR as elements --- main.cpp | 158 +++++++++++++++++++++++----------- synth_engine.cpp | 220 +++++++++++++++++++++++++++++------------------ synth_engine.h | 32 +------ 3 files changed, 249 insertions(+), 161 deletions(-) diff --git a/main.cpp b/main.cpp index 593713f..8e70817 100644 --- a/main.cpp +++ b/main.cpp @@ -27,9 +27,6 @@ std::atomic 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 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); diff --git a/synth_engine.cpp b/synth_engine.cpp index 375ffd0..f666fa3 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -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(sampleF * _volume); } diff --git a/synth_engine.h b/synth_engine.h index 91f5deb..e84eabb 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -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 \ No newline at end of file