// synth.c // The Voyager's sound, vendored from soundThing. // // ---- Where this came from ---- // // soundThing is a polyphonic subtractive synthesizer written by Anachronaut, and lives in its // own repository. What is here is its VOICE ENGINE and nothing else: synth.c pulls in maths, // stdlib, stdint and stdio and knows nothing about Raylib, MIDI, patches or the user // interface, which is what made it liftable at all. // // It is copied rather than submoduled. Two files against tying this build to another // repository's history is not a close call, and what a copy costs is that changes have to be // carried across on purpose. So the list below is the whole of the difference, kept current. // // ---- What was changed ---- // // 1. A VOICE'S LEVEL IS A ROUTING. Envelope 0 multiplied the output and there was no way to // say otherwise, so routing it to a filter or an oscillator meant it shaped the volume as // well whether that was wanted or not - which is most of the trouble with making // percussion. Every other destination in this synth chooses its source; now this one does // too, and MOD_SOURCE_NONE means the level is simply full. // // 2. NOISE COMES FROM A SEEDED GENERATOR. It drew from rand(), which is global state shared // with the whole process and varies between libraries - so the same program would sound // different on different machines and every recorded result would be worthless. It is a // generator inside the Synth now, and a machine that starts the same way sounds the same // way. // // 3. CHANNELS ARE NAMED, NOT ALLOCATED. synthNoteOn hunts for a free voice and steals // round-robin, which is what a keyboard wants. A hardware channel is asked for by number. // The old calls are still here and still do what they did. // // Written by Anachronaut #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); // A seed each, so that two noise oscillators sounding together are two noises rather // than one heard twice. Any spread will do as long as none of them is zero. s->voices[i].oscillators[0].noiseState = 0x9E3779B9u + (uint32_t)i * 2654435761u; s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u; // Envelope 0 shapes the level, which is what it always did - the difference is that // this now says so, and can be told not to. s->voices[i].levelSource = MOD_SOURCE_AMP_ENV; 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; s->lfos[l].noiseState = 0x2545F491u + (uint32_t)l * 3266489917u; } } void synthResetPatch(Synth *s) { Voice *v = &s->voices[0]; // The level is shaped by envelope 0 unless a patch says otherwise, which is what it // always was - the difference is only that it can now be said otherwise. v->levelSource = MOD_SOURCE_AMP_ENV; 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->lfos[l].noiseState = 0x2545F491u + (uint32_t)l * 3266489917u; } 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->noiseState = 0x9E3779B9u; // Non-zero, or xorshift stays at zero and makes silence. o->gain = gain; o->octave = 0; } // ---- Noise ---- // // A plain 32-bit xorshift, which is all a noise source needs: it has to be the same sequence // every run and it does not have to be a good one. A state of zero stays at zero and makes // silence rather than noise, so every seed below is non-zero on purpose. static float nextNoise(uint32_t *state) { *state ^= *state << 13; *state ^= *state >> 17; *state ^= *state << 5; return (float)(*state / 4294967296.0) * 2.0f - 1.0f; } 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 = nextNoise(&l->noiseState); } } 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 = nextNoise(&o->noiseState); // } // } // // 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 = nextNoise(&o->noiseState); } } 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); } } } // ---- A channel is the channel you asked for ---- // // synthNoteOn hunts for a free voice and steals round-robin, which is what a keyboard wants: // eight fingers and no say in which voice serves which. A hardware channel is not like that. // Channel two is channel two, it holds its patch between notes, and a program driving it // knows perfectly well what it is doing - so these say which one and nothing is stolen. // // The two above are left exactly as they were, because the standalone synthesizer still wants // them and a keyboard has not stopped being a keyboard. void synthChannelOn(Synth *s, int channel, int midiNote) { if (channel < 0 || channel >= VOICE_COUNT) { return; } Voice *v = &s->voices[channel]; v->freqHz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f); v->midiNote = midiNote; v->active = 1; // The filter's memory of the last note is not this note's business. A note beginning // where the last one left off is how a click gets into the front of every sound. v->filter.low = 0.0f; v->filter.band = 0.0f; envelopeNoteOn(&v->ampEnv); envelopeNoteOn(&v->modEnv); } void synthChannelOff(Synth *s, int channel) { if (channel < 0 || channel >= VOICE_COUNT) { return; } // Released rather than stopped: what happens next is the envelope's business, and a note // that ended the instant a key came up would have no release at all. envelopeNoteOff(&s->voices[channel].ampEnv); envelopeNoteOff(&s->voices[channel].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); // ---- How loud this voice is ---- // // Envelope 0 used to be multiplied in here unconditionally, so routing it // anywhere else meant it shaped the volume as well. Now the voice says which // source shapes its level, and MOD_SOURCE_NONE means nothing does. // // Clamped at nothing, because an LFO swings either side of zero and the far side // is not a negative volume, it is silence. Which makes an LFO here tremolo. float level = 1.0f; if (vv->levelSource != MOD_SOURCE_NONE) { level = getModValue(amp, mod, lfo0, lfo1, vv->levelSource); if (level < 0.0f) level = 0.0f; } mix += oscMix * level; } 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; } }