Gate control and ADSR as elements
This commit is contained in:
+138
-82
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user