353 lines
14 KiB
C++
353 lines
14 KiB
C++
#include "synth_engine.h"
|
|
#include <math.h>
|
|
|
|
// 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<int16_t>(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<uint32_t>((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::OPERATOR || n.type == GridCell::NOISE || n.type == GridCell::DELAY) {
|
|
// 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;
|
|
};
|
|
|
|
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 = 0.0f;
|
|
mod += getInput(x, y, x, y-1);
|
|
mod += getInput(x, y, x+1, y);
|
|
mod += getInput(x, y, x, y+1);
|
|
mod += getInput(x, y, x-1, y);
|
|
|
|
// 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 = 0.0f;
|
|
mod += getInput(x, y, x, y-1);
|
|
mod += getInput(x, y, x+1, y);
|
|
mod += getInput(x, y, x, y+1);
|
|
mod += getInput(x, y, x-1, y);
|
|
|
|
// 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)
|
|
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::NOISE) {
|
|
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;
|
|
}
|
|
} else if (c.type == GridCell::FORK) {
|
|
// Sum inputs from "Back" (Input direction)
|
|
// Rotation is "Forward". Input is "Back" (rot+2)
|
|
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;
|
|
// We only read from the specific input neighbor
|
|
val = getInput(x, y, x+dx, y+dy);
|
|
} else if (c.type == GridCell::WIRE) {
|
|
// Sum inputs from all neighbors that point to me
|
|
float sum = 0.0f;
|
|
sum += getInput(x, y, x, y-1); // N
|
|
sum += getInput(x, y, x+1, y); // E
|
|
sum += getInput(x, y, x, y+1); // S
|
|
sum += getInput(x, y, x-1, y); // W
|
|
val = sum * c.param; // Fading
|
|
} else if (c.type == GridCell::DELAY) {
|
|
// Input is from the "Back" (rot+2)
|
|
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;
|
|
float input_val = getInput(x, y, x+dx, y+dy);
|
|
|
|
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::OPERATOR || c.type == GridCell::SINK) {
|
|
// Gather inputs
|
|
float inputs[4];
|
|
int count = 0;
|
|
float iN = getInput(x, y, x, y-1); if(iN!=0) inputs[count++] = iN;
|
|
float iE = getInput(x, y, x+1, y); if(iE!=0) inputs[count++] = iE;
|
|
float iS = getInput(x, y, x, y+1); if(iS!=0) inputs[count++] = iS;
|
|
float iW = getInput(x, y, x-1, y); if(iW!=0) inputs[count++] = iW;
|
|
|
|
if (c.type == GridCell::SINK) {
|
|
// Sink just sums everything
|
|
val = iN + iE + iS + iW;
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
// 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<std::mutex> 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<int16_t>(sampleF * _volume);
|
|
}
|
|
} |