#include "synth.h" #include #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif void synthInit(Synth *s, float sampleRate) { s->sampleRate = sampleRate; s->pitchBend = 0.0f; s->pitchBendRange = 2.0f; s->lastStolenVoice = 0; s->volume = 0.8f; for (int i = 0; i < VOICE_COUNT; i++) { s->voices[i].freqHz = 440.0f; s->voices[i].active = 0; s->voices[i].midiNote = -1; oscillatorInit(&s->voices[i].oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN); oscillatorInit(&s->voices[i].oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f); envelopeInit(&s->voices[i].ampEnv, 0.005f, // attack 0.10f, // decay 0.70f, // sustain 0.50f); // release envelopeInit(&s->voices[i].modEnv, 0.005f, // attack 0.50f, // decay 0.0f, // sustain 0.10f); // release s->voices[i].filter.cutoff = 8000.0f; s->voices[i].filter.resonance = 0.0f; s->voices[i].filter.type = FILTER_LOWPASS; s->voices[i].filter.active = 0; s->voices[i].filter.low = 0.0f; s->voices[i].filter.band = 0.0f; s->voices[i].filter.modRouting = MOD_SOURCE_NONE; s->voices[i].filter.modDepth = 0.0f; s->voices[i].filter.resModRouting = MOD_SOURCE_NONE; s->voices[i].filter.resModDepth = 0.0f; } s->voices[0].oscillators[0].active = 1; for (int l = 0; l < LFO_COUNT; l++) { s->lfos[l].phase = 0.0f; s->lfos[l].rate = 1.0f; s->lfos[l].waveform = WAVE_SINE; s->lfos[l].active = 0; s->lfos[l].noiseHeld = 0.0f; s->lfos[l].noisePhase = 0.0f; } } void synthResetPatch(Synth *s) { Voice *v = &s->voices[0]; oscillatorInit(&v->oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN); v->oscillators[0].active = 1; for (int m = 0; m < 3; m++) { v->oscillators[0].modRouting[m] = MOD_SOURCE_NONE; v->oscillators[0].modDepth[m] = 0.0f; } oscillatorInit(&v->oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f); v->oscillators[1].active = 0; for (int m = 0; m < 3; m++) { v->oscillators[1].modRouting[m] = MOD_SOURCE_NONE; v->oscillators[1].modDepth[m] = 0.0f; } envelopeInit(&v->ampEnv, 0.005f, 0.10f, 0.70f, 0.50f); envelopeInit(&v->modEnv, 0.005f, 0.50f, 0.0f, 0.10f); v->filter.cutoff = 8000.0f; v->filter.resonance = 0.0f; v->filter.type = FILTER_LOWPASS; v->filter.active = 0; v->filter.low = 0.0f; v->filter.band = 0.0f; v->filter.modRouting = MOD_SOURCE_NONE; v->filter.modDepth = 0.0f; v->filter.resModRouting = MOD_SOURCE_NONE; v->filter.resModDepth = 0.0f; for (int l = 0; l < LFO_COUNT; l++) { s->lfos[l].phase = 0.0f; s->lfos[l].rate = 1.0f; s->lfos[l].waveform = WAVE_SINE; s->lfos[l].active = 0; s->lfos[l].noiseHeld = 0.0f; s->lfos[l].noisePhase = 0.0f; } s->volume = 0.8f; s->pitchBendRange = 2.0f; } void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain) { o->phase = 0.0f; o->waveform = waveform; o->dutyCycle = dutyCycle; o->detune = detune; o->noiseHeld = 0.0f; o->noisePhase = 0.0f; o->gain = gain; o->octave = 0; } static float getModValue(float ampEnv, float modEnv, float lfo0, float lfo1, ModSource source) { switch (source) { case MOD_SOURCE_AMP_ENV: return ampEnv; case MOD_SOURCE_MOD_ENV: return modEnv; case MOD_SOURCE_LFO: return lfo0; case MOD_SOURCE_LFO2: return lfo1; default: return 0.0f; } } float lfoTick(LFO *l, float sampleRate) { if (!l->active) return 0.0f; l->phase += l->rate / sampleRate; if (l->phase >= 1.0f) l->phase -= 1.0f; if (l->waveform == WAVE_NOISE) { l->noisePhase += l->rate / sampleRate; if (l->noisePhase >= 1.0f) { l->noisePhase -= 1.0f; l->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; } } return waveformSample(l->waveform, l->phase, 0.5f, l->noiseHeld); } float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate) { if (!f->active) return input; if (cutoff < 20.0f) cutoff = 20.0f; if (cutoff > sampleRate * 0.499f) cutoff = sampleRate * 0.499f; if (resonance < 0.0f) resonance = 0.0f; if (resonance > 0.99f) resonance = 0.99f; // Andy Simper TPT SVF (bilinear integration — unconditionally stable) float g = tanf((float)M_PI * cutoff / sampleRate); float Q = 0.5f + resonance * 9.5f; // resonance 0..0.99 → Q 0.5..10.0 float k = 1.0f / Q; float a1 = 1.0f / (1.0f + g * (g + k)); float a2 = g * a1; float a3 = g * a2; // f->band = s1, f->low = s2 (integrator states) float v3 = input - f->low; float v1 = a1 * f->band + a2 * v3; float v2 = f->low + a2 * f->band + a3 * v3; f->band = 2.0f * v1 - f->band; f->low = 2.0f * v2 - f->low; switch (f->type) { case FILTER_LOWPASS: return v2; case FILTER_HIGHPASS: return input - k * v1 - v2; case FILTER_BANDPASS: return v1; default: return v2; } } const char *filterTypeName(FilterType t) { switch (t) { case FILTER_LOWPASS: return "LP"; case FILTER_HIGHPASS: return "HP"; case FILTER_BANDPASS: return "BP"; default: return "??"; } } // float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate, // float dutyCycle, float detune, float gain) // { // float detuneMultiplier = powf(2.0f, o->detune / 1200.0f); // float freq = freqHz * detuneMultiplier * bendMultiplier; // // // Advance phase // o->phase += freq / sampleRate; // if (o->phase >= 1.0f) o->phase -= 1.0f; // // // Clocked noise — draw a new random value once per cycle // if (o->waveform == WAVE_NOISE) { // o->noisePhase += freq / sampleRate; // if (o->noisePhase >= 1.0f) { // o->noisePhase -= 1.0f; // o->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; // } // } // // return waveformSample(o->waveform, o->phase, o->dutyCycle, o->noiseHeld) * o->gain; // } float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate, float dutyCycle, float detune, float gain) { float detuneMultiplier = powf(2.0f, detune / 1200.0f); float freq = freqHz * detuneMultiplier * bendMultiplier; // Advance phase o->phase += freq / sampleRate; if (o->phase >= 1.0f) o->phase -= 1.0f; // Clocked noise — draw a new random value once per cycle if (o->waveform == WAVE_NOISE) { o->noisePhase += freq / sampleRate; if (o->noisePhase >= 1.0f) { o->noisePhase -= 1.0f; o->noiseHeld = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; } } return waveformSample(o->waveform, o->phase, dutyCycle, o->noiseHeld) * gain; } float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld) { switch (w) { case WAVE_SINE: return sinf(2.0f * (float)M_PI * phase); case WAVE_TRIANGLE: return (phase < 0.5f) ? ( 4.0f * phase - 1.0f) : (-4.0f * phase + 3.0f); case WAVE_SAW: return 2.0f * phase - 1.0f; case WAVE_RAMP: return 1.0f - 2.0f * phase; case WAVE_PULSE: return (phase < dutyCycle) ? 1.0f : -1.0f; case WAVE_NOISE: return noiseHeld; default: return 0.0f; } } const char *waveformName(Waveform w) { switch (w) { case WAVE_SINE: return "Sine"; case WAVE_TRIANGLE: return "Triangle"; case WAVE_SAW: return "Saw"; case WAVE_RAMP: return "Ramp"; case WAVE_PULSE: return "Pulse"; case WAVE_NOISE: return "Noise"; default: return "???"; } } void synthNoteOn(Synth *s, int midiNote) { float hz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f); for (int i = 0; i < VOICE_COUNT; i++) { if (!s->voices[i].active) { s->voices[i].freqHz = hz; s->voices[i].midiNote = midiNote; s->voices[i].active = 1; s->voices[i].filter.low = 0.0f; s->voices[i].filter.band = 0.0f; envelopeNoteOn(&s->voices[i].ampEnv); envelopeNoteOn(&s->voices[i].modEnv); return; } } // Steal round-robin int i = s->lastStolenVoice % VOICE_COUNT; s->lastStolenVoice++; s->voices[i].freqHz = hz; s->voices[i].midiNote = midiNote; s->voices[i].active = 1; s->voices[i].filter.low = 0.0f; s->voices[i].filter.band = 0.0f; envelopeNoteOn(&s->voices[i].ampEnv); envelopeNoteOn(&s->voices[i].modEnv); } void synthNoteOff(Synth *s, int midiNote) { for (int i = 0; i < VOICE_COUNT; i++) { if (s->voices[i].active && s->voices[i].midiNote == midiNote) { envelopeNoteOff(&s->voices[i].ampEnv); envelopeNoteOff(&s->voices[i].modEnv); } } } void synthFillBuffer(Synth *s, int16_t *out, int frames) { const float sr = s->sampleRate; const float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f); for (int i = 0; i < frames; i++) { float mix = 0.0f; float lfo0 = lfoTick(&s->lfos[0], sr); float lfo1 = lfoTick(&s->lfos[1], sr); for (int v = 0; v < VOICE_COUNT; v++) { Voice *vv = &s->voices[v]; if (!vv->active) continue; float amp = envelopeTick(&vv->ampEnv, sr); float mod = envelopeTick(&vv->modEnv, sr); if (vv->ampEnv.stage == ENV_IDLE) { vv->active = 0; continue; } float oscMix = 0.0f; int activeOscs = 0; for (int o = 0; o < OSC_COUNT; o++) { Oscillator *osc = &vv->oscillators[o]; if (!osc->active) continue; float dutyCycle = osc->dutyCycle + getModValue(amp, mod, lfo0, lfo1, osc->modRouting[0]) * osc->modDepth[0]; float detune = osc->detune + (float)osc->octave * 1200.0f + getModValue(amp, mod, lfo0, lfo1, osc->modRouting[1]) * osc->modDepth[1]; float gain = osc->gain + getModValue(amp, mod, lfo0, lfo1, osc->modRouting[2]) * osc->modDepth[2]; if (dutyCycle < 0.05f) dutyCycle = 0.05f; if (dutyCycle > 0.95f) dutyCycle = 0.95f; if (gain < 0.0f) gain = 0.0f; if (gain > OSC_MAX_GAIN) gain = OSC_MAX_GAIN; oscMix += oscillatorTick(osc, vv->freqHz, bendMultiplier, sr, dutyCycle, detune, gain); activeOscs++; } if (activeOscs > 0) oscMix /= activeOscs; float cutoff = vv->filter.cutoff + getModValue(amp, mod, lfo0, lfo1, vv->filter.modRouting) * vv->filter.modDepth; float resonance = vv->filter.resonance + getModValue(amp, mod, lfo0, lfo1, vv->filter.resModRouting) * vv->filter.resModDepth; oscMix = filterTick(&vv->filter, oscMix, cutoff, resonance, sr); mix += oscMix * amp; } mix *= (0.2f / VOICE_COUNT) * 4.0f * s->volume; int32_t sample = (int32_t)lrintf(mix * 32767.0f); if (sample > 32767) sample = 32767; if (sample < -32768) sample = -32768; out[i] = (int16_t)sample; } } void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec) { e->stage = ENV_IDLE; e->value = 0.0f; e->attackSec = attackSec; e->decaySec = decaySec; e->sustainLevel = sustainLevel; e->releaseSec = releaseSec; } void envelopeNoteOn(Envelope *e) { e->value = 0.0f; e->stage = ENV_ATTACK; } void envelopeNoteOff(Envelope *e) { // Only trigger release if we're actually playing if (e->stage != ENV_IDLE) e->stage = ENV_RELEASE; } float envelopeTick(Envelope *e, float sampleRate) { switch (e->stage) { case ENV_ATTACK: { float inc = (e->attackSec <= 0.0f) ? 1.0f : (1.0f / (e->attackSec * sampleRate)); e->value += inc; if (e->value >= 1.0f) { e->value = 1.0f; e->stage = ENV_DECAY; } break; } case ENV_DECAY: { float inc = (e->decaySec <= 0.0f) ? 1.0f : (1.0f / (e->decaySec * sampleRate)); e->value -= inc; if (e->value <= e->sustainLevel) { e->value = e->sustainLevel; e->stage = ENV_SUSTAIN; } break; } case ENV_SUSTAIN: e->value = e->sustainLevel; break; case ENV_RELEASE: { float inc = (e->releaseSec <= 0.0f) ? 1.0f : (1.0f / (e->releaseSec * sampleRate)); e->value -= inc; if (e->value <= 0.0f) { e->value = 0.0f; e->stage = ENV_IDLE; } break; } case ENV_IDLE: e->value = 0.0f; break; } return e->value; } void synthSyncVoices(Synth *s) { for (int v = 1; v < VOICE_COUNT; v++) { // Sync oscillator settings for (int o = 0; o < OSC_COUNT; o++) { s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform; s->voices[v].oscillators[o].dutyCycle = s->voices[0].oscillators[o].dutyCycle; s->voices[v].oscillators[o].detune = s->voices[0].oscillators[o].detune; s->voices[v].oscillators[o].gain = s->voices[0].oscillators[o].gain; s->voices[v].oscillators[o].active = s->voices[0].oscillators[o].active; s->voices[v].oscillators[o].octave = s->voices[0].oscillators[o].octave; s->voices[v].oscillators[o].modRouting[0] = s->voices[0].oscillators[o].modRouting[0]; s->voices[v].oscillators[o].modRouting[1] = s->voices[0].oscillators[o].modRouting[1]; s->voices[v].oscillators[o].modRouting[2] = s->voices[0].oscillators[o].modRouting[2]; s->voices[v].oscillators[o].modDepth[0] = s->voices[0].oscillators[o].modDepth[0]; s->voices[v].oscillators[o].modDepth[1] = s->voices[0].oscillators[o].modDepth[1]; s->voices[v].oscillators[o].modDepth[2] = s->voices[0].oscillators[o].modDepth[2]; } // Sync filter params but not state (low/band are per-voice) s->voices[v].filter.cutoff = s->voices[0].filter.cutoff; s->voices[v].filter.resonance = s->voices[0].filter.resonance; s->voices[v].filter.type = s->voices[0].filter.type; s->voices[v].filter.active = s->voices[0].filter.active; s->voices[v].filter.modRouting = s->voices[0].filter.modRouting; s->voices[v].filter.modDepth = s->voices[0].filter.modDepth; s->voices[v].filter.resModRouting = s->voices[0].filter.resModRouting; s->voices[v].filter.resModDepth = s->voices[0].filter.resModDepth; // Sync envelope settings but NOT runtime state // Each voice needs its own stage, value — just copy the parameters s->voices[v].ampEnv.attackSec = s->voices[0].ampEnv.attackSec; s->voices[v].ampEnv.decaySec = s->voices[0].ampEnv.decaySec; s->voices[v].ampEnv.sustainLevel = s->voices[0].ampEnv.sustainLevel; s->voices[v].ampEnv.releaseSec = s->voices[0].ampEnv.releaseSec; // Sync the mod envelope, too. s->voices[v].modEnv.attackSec = s->voices[0].modEnv.attackSec; s->voices[v].modEnv.decaySec = s->voices[0].modEnv.decaySec; s->voices[v].modEnv.sustainLevel = s->voices[0].modEnv.sustainLevel; s->voices[v].modEnv.releaseSec = s->voices[0].modEnv.releaseSec; } }