diff --git a/Source/Emulator/synth.c b/Source/Emulator/synth.c new file mode 100644 index 0000000..f5485da --- /dev/null +++ b/Source/Emulator/synth.c @@ -0,0 +1,590 @@ +// 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; + } +} diff --git a/Source/Emulator/synth.h b/Source/Emulator/synth.h new file mode 100644 index 0000000..5064de3 --- /dev/null +++ b/Source/Emulator/synth.h @@ -0,0 +1,208 @@ +// synth.h +// 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 + +#ifndef SYNTH_H +#define SYNTH_H + +#include + +#define VOICE_COUNT 8 +#define OSC_COUNT 2 +#define LFO_COUNT 2 +#define OSC_MAX_GAIN 4.0f + +typedef enum { + WAVE_SINE, + WAVE_TRIANGLE, + WAVE_SAW, + WAVE_RAMP, + WAVE_PULSE, + WAVE_NOISE, + WAVE_COUNT // handy for the modulo wrap on waveform switching +} Waveform; + +// Envelope structures: +typedef enum { + ENV_IDLE, + ENV_ATTACK, + ENV_DECAY, + ENV_SUSTAIN, + ENV_RELEASE +} EnvStage; + +typedef enum { + MOD_SOURCE_NONE = 0, + MOD_SOURCE_AMP_ENV = 1, + MOD_SOURCE_MOD_ENV = 2, + MOD_SOURCE_LFO = 3, + MOD_SOURCE_LFO2 = 4 +} ModSource; + +typedef struct { + EnvStage stage; + float value; // current output value, 0.0 to 1.0 + float attackSec; + float decaySec; + float sustainLevel; + float releaseSec; +} Envelope; + +typedef struct { + float phase; + float rate; // Hz + Waveform waveform; + int active; + float noiseHeld; + float noisePhase; + // Its own noise, seeded at init. rand() is global state shared with the whole process and + // varies between C libraries, so the same program sounded different on different machines + // and no recorded result could mean anything. One generator EACH rather than one shared, + // because two noise sources drawing from the same stream are not two noise sources. + uint32_t noiseState; +} LFO; + +typedef enum { + FILTER_LOWPASS, + FILTER_HIGHPASS, + FILTER_BANDPASS, + FILTER_COUNT +} FilterType; + +typedef struct { + float cutoff; // Hz + float resonance; // 0.0 (flat) to 0.99 (near self-oscillation) + FilterType type; + int active; + float low, band; // TPT integrator states s1, s2 + int modRouting; + float modDepth; // Hz + int resModRouting; + float resModDepth; // resonance units (-0.99..0.99) +} Filter; + +typedef struct { + float phase; + float dutyCycle; + Waveform waveform; + float detune; // cents, 0 = no detune + float noiseHeld; // last drawn random value for clocked noise + float noisePhase; // tracks when to draw a new noise value + // Its own noise, seeded at init. rand() is global state shared with the whole process and + // varies between C libraries, so the same program sounded different on different machines + // and no recorded result could mean anything. One generator EACH rather than one shared, + // because two noise sources drawing from the same stream are not two noise sources. + uint32_t noiseState; + + float gain; + int active; // whether this oscillator contributes to output + int octave; // transposition in octaves, -2 to +2 + // Modulation routing: one source per destination by design. + // Each parameter (pwm, detune, gain) has exactly one mod source and one depth. + int modRouting[3]; // 0=off, 1=env0, 2=env1, 3=lfo0, 4=lfo1 + float modDepth[3]; // Depth of modulation parameter +} Oscillator; + +typedef struct { + Oscillator oscillators[OSC_COUNT]; + float freqHz; + int active; + int midiNote; + Envelope ampEnv; + Envelope modEnv; + Filter filter; + // ---- What shapes how loud this voice is ---- + // + // Envelope 0 used to, always, with no way to say otherwise - so routing it to a filter or + // an oscillator meant it shaped the volume too, whether that was wanted or not. Every + // other destination here names its source; this one does now as well. + // + // NOT the base-and-depth pair the others use, because a level is not a deviation from a + // resting value - it is a shape from nothing to full, and multiplying is what an amplitude + // envelope does. So this names a source outright, and MOD_SOURCE_NONE means the voice is + // simply at full and whatever env0 is doing is somebody else's business. + // + // It also makes two things possible that were not: envelope 1 shaping the volume, and an + // LFO doing it, which is tremolo. + ModSource levelSource; +} Voice; + +typedef struct { + float sampleRate; + float pitchBend; + float pitchBendRange; + int lastStolenVoice; + Voice voices[VOICE_COUNT]; + float volume; // 0.0 to 1.0, master output level + LFO lfos[LFO_COUNT]; +} Synth; + +// Envelope functions: +void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec); +void envelopeNoteOn(Envelope *e); +void envelopeNoteOff(Envelope *e); +float envelopeTick(Envelope *e, float sampleRate); + +// Oscillator functions: +void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain); +float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate, + float dutyCycle, float detune, float gain); + +// Synth functions: +void synthInit(Synth *s, float sampleRate); +void synthResetPatch(Synth *s); +void synthNoteOn(Synth *s, int midiNote); +void synthNoteOff(Synth *s, int midiNote); + +// ---- 3. A channel is asked for by number ---- +// +// The two above hunt for a free voice and steal round-robin, which is what a keyboard wants +// and what the standalone synthesizer still does. A hardware channel is not allocated: it is +// the third one, and it is the third one every time. +void synthChannelOn(Synth *s, int channel, int midiNote); +void synthChannelOff(Synth *s, int channel); +void synthFillBuffer(Synth *s, int16_t *out, int frames); +void synthSyncVoices(Synth *s); + +// LFO functions: +float lfoTick(LFO *l, float sampleRate); + +// Filter functions: +float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate); +const char *filterTypeName(FilterType t); + +// Waveform functions: +float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld); +const char *waveformName(Waveform w); + +#endif