#include "synth_engine.h" #include // A simple sine lookup table for the sine oscillator const int SINE_TABLE_SIZE = 256; static int16_t sine_table[SINE_TABLE_SIZE]; static bool sine_table_filled = false; /** * @brief Fills the global sine table. Called once on startup. */ void fill_sine_table() { if (sine_table_filled) return; for (int i = 0; i < SINE_TABLE_SIZE; ++i) { // M_PI is not standard C++, but it's common. If it fails, use 3.1415926535... sine_table[i] = static_cast(sin(2.0 * M_PI * i / SINE_TABLE_SIZE) * 32767.0); } sine_table_filled = true; } SynthEngine::SynthEngine(uint32_t sampleRate) : _sampleRate(sampleRate), _phase(0), _increment(0), _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{} { 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; } SynthEngine::~SynthEngine() { for (int x = 0; x < 5; ++x) { for (int y = 0; y < 8; ++y) { if (grid[x][y].buffer) { delete[] grid[x][y].buffer; grid[x][y].buffer = nullptr; } } } } void SynthEngine::setFrequency(float freq) { // Calculate the phase increment for a given frequency. // The phase accumulator is a 32-bit unsigned integer (0 to 2^32-1). // One full cycle of the accumulator represents one cycle of the waveform. // increment = (frequency * 2^32) / sampleRate // The original calculation was incorrect for float frequencies. _increment = static_cast((double)freq * (4294967296.0 / (double)_sampleRate)); } void SynthEngine::setVolume(float vol) { if (vol < 0.0f) vol = 0.0f; if (vol > 1.0f) vol = 1.0f; _volume = vol; } void SynthEngine::setWaveform(Waveform form) { _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::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; 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) { // 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; if (n.rotation == 1 && from_x + 1 == tx && from_y == ty) connects = true; if (n.rotation == 2 && from_y + 1 == ty && from_x == tx) connects = true; if (n.rotation == 3 && from_x - 1 == tx && from_y == ty) connects = true; } else if (n.type == GridCell::FORK) { // Fork outputs to Left (rot+3) and Right (rot+1) relative to its rotation // n.rotation is "Forward" 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; }; // Helper to sum inputs excluding the output direction auto getSummedInput = [&](int x, int y, GridCell& c) -> float { float sum = 0.0f; int outDir = c.rotation; // 0:N, 1:E, 2:S, 3:W if (outDir != 0) sum += getInput(x, y, x, y-1); if (outDir != 1) sum += getInput(x, y, x+1, y); if (outDir != 2) sum += getInput(x, y, x, y+1); if (outDir != 3) sum += getInput(x, y, x-1, y); return sum; }; auto getInputFromTheBack = [&](int x, int y, GridCell& c) -> float { int inDir = (c.rotation + 2) % 4; int dx=0, dy=0; if(inDir==0) dy=-1; else if(inDir==1) dx=1; else if(inDir==2) dy=1; else dx=-1; return getInput(x, y, x+dx, y+dy); }; for (int x = 0; x < 5; ++x) { for (int y = 0; y < 8; ++y) { GridCell& c = grid[x][y]; float val = 0.0f; if (c.type == GridCell::EMPTY) { val = 0.0f; } else if (c.type == GridCell::FIXED_OSCILLATOR) { // Gather inputs for modulation float mod = getSummedInput(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; } else if (c.type == GridCell::INPUT_OSCILLATOR) { float mod = getSummedInput(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; } else if (c.type == GridCell::WAVETABLE) { float mod = getSummedInput(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; } } else if (c.type == GridCell::NOISE) { float mod = getSummedInput(x, y, c); float white = (float)rand() / (float)RAND_MAX * 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); } 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::WIRE) { // Sum inputs from all neighbors that point to me float sum = getSummedInput(x, y, c); val = sum * c.param; // Fading } 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::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 (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 val = 0.0f; // Drop out } else { val = in; } } else if (c.type == GridCell::DELAY) { // Input is from the "Back" (rot+2) float input_val = getInputFromTheBack(x, y, c); if (c.buffer && c.buffer_size > 0) { // Write current input to buffer c.buffer[c.write_idx] = input_val; // Calculate read index based on parameter. Max delay is buffer_size. uint32_t delay_samples = c.param * (c.buffer_size - 1); // Using modulo for wraparound. Need to handle negative result from subtraction. int read_idx = (int)c.write_idx - (int)delay_samples; if (read_idx < 0) { read_idx += c.buffer_size; } // Read delayed value for output val = c.buffer[read_idx]; // Increment write index c.write_idx = (c.write_idx + 1) % c.buffer_size; } else { val = 0.0f; // No buffer, no output } } else if (c.type == GridCell::REVERB) { // Input is from the "Back" (rot+2) float input_val = getInputFromTheBack(x, y, c); if (c.buffer && c.buffer_size > 0) { // Fixed delay for reverb effect (e.g. 50ms) uint32_t delay_samples = (uint32_t)(0.05f * _sampleRate); if (delay_samples >= c.buffer_size) delay_samples = c.buffer_size - 1; int read_idx = (int)c.write_idx - (int)delay_samples; if (read_idx < 0) read_idx += c.buffer_size; float delayed = c.buffer[read_idx]; // Feedback controlled by param (0.0 to 0.95) float feedback = c.param * 0.95f; float newValue = input_val + delayed * feedback; c.buffer[c.write_idx] = newValue; val = newValue; c.write_idx = (c.write_idx + 1) % c.buffer_size; } else { val = 0.0f; } } else if (c.type == GridCell::OPERATOR || c.type == GridCell::SINK) { // Gather inputs float inputs[4]; int count = 0; int outDir = (c.type == GridCell::SINK) ? -1 : c.rotation; float iN = (outDir != 0) ? getInput(x, y, x, y-1) : 0.0f; if(iN!=0) inputs[count++] = iN; float iE = (outDir != 1) ? getInput(x, y, x+1, y) : 0.0f; if(iE!=0) inputs[count++] = iE; float iS = (outDir != 2) ? getInput(x, y, x, y+1) : 0.0f; if(iS!=0) inputs[count++] = iS; float iW = (outDir != 3) ? getInput(x, y, x-1, y) : 0.0f; if(iW!=0) inputs[count++] = iW; if (c.type == GridCell::SINK) { // Sink just sums everything val = 0.0f; for(int k=0; kval) val = inputs[i]; break; // MAX } } } } } next_values[x][y] = val; } } // Update state for(int x=0; x<5; ++x) { for(int y=0; y<8; ++y) { grid[x][y].value = next_values[x][y]; } } return grid[2][3].value; } void SynthEngine::process(int16_t* buffer, uint32_t numFrames) { // Lock grid mutex to prevent UI from changing grid structure mid-process std::lock_guard lock(gridMutex); for (uint32_t i = 0; i < numFrames; ++i) { // The grid is now the primary sound source. // The processGridStep() returns a float in the approx range of -1.0 to 1.0. float sampleF = processGridStep(); // Soft clip grid sample to avoid harsh distortion before filtering. if (sampleF > 1.0f) sampleF = 1.0f; if (sampleF < -1.0f) sampleF = -1.0f; // The filters were designed for a signal in the int16 range. // 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); } }