The three changes that went up came back as part of soundThing, along with two more that they made possible. The engine here is now b73e5c0 character for character, except that em-dashes and arrows in comments are written as ASCII because this tree is ASCII only - a local rule, not an improvement, and not sent up. So synth.h's "what was changed" list is gone. There is nothing to list: what has to be kept current is only that if either copy changes, the other one has to be told. ---- What came back ---- A VOICE CAN END ITSELF. Naming the level's source said what shapes a voice; nothing said what ends one, so the only thing that could ever finish one was a key coming up. A game is nearly all one-shots and not one of them wants its length decided by how long a note was held. Exposed as parameter 0x51: 0 gated, 1 triggered. AND A ONE-SHOT IS THE SAME ONE-SHOT TWICE. A triggered voice re-arms its oscillators, and an LFO can be told to start over with each voice - parameter 3 of either LFO. Both halves are needed and the check proves it: with the LFO left free, two triggered hits still differ. Their note warned that whatever applies a patch to a channel has to set these or they hold synthInit's defaults. Checked: Voyager never calls synthSyncVoices, so their 0001 is a no-op here as they predicted, and nothing reaches into an LFO's phase, so the struct split is safe. ---- What it is for ---- Lander's crash is a triggered voice now, so boomOff is gone. Nothing has to remember to end a bang. SoundPatch learnt voice_levelSource, voice_gate and lfo<N>_mode, which the new soundThing writes - without that it would have refused every patch saved from it, since an unknown field stops the tool on purpose. A patch from before those fields still converts, and says in its own comments that it predates the level routing. Three checks, each seen to fail on its own break: a gated voice still sounding with nothing holding it, a triggered one down to nothing with no gate ever dropped, and two hits identical sample for sample. One test bug worth keeping: the first version of the repeatability check struck the second note while the first was still ringing, so what it found and compared as "the second hit" was a point in the middle of the first one's tail. It now looks for sound after SILENCE rather than sound after an offset.
685 lines
26 KiB
C
685 lines
26 KiB
C
// 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 - in BOTH directions, which has now happened once each way.
|
|
//
|
|
// ---- What was changed ----
|
|
//
|
|
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
|
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
|
//
|
|
// It did not start that way. Three changes were made here first - a routed voice level, a
|
|
// seeded noise generator, and channels asked for by number - and all three went up. What came
|
|
// back was those three plus what they made possible: a voice that can end itself rather than
|
|
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
|
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that way.
|
|
//
|
|
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
|
// other one has to be told.
|
|
//
|
|
// Written by Anachronaut
|
|
|
|
#include "synth.h"
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
// ---- The seeds a retrigger goes back to ----
|
|
//
|
|
// Deliberately NOT keyed on the voice, unlike the seeds synthInit hands out. A retriggered
|
|
// one-shot has to sound the same whichever voice happens to be free for it, and a seed that
|
|
// varied per voice would make the same drum a different drum eight ways. Keyed on the
|
|
// oscillator, though, because osc 0 and osc 1 drawing one stream are one noise heard twice.
|
|
static uint32_t oscTriggerSeed(int o)
|
|
{
|
|
return (o == 0) ? 0x9E3779B9u : 0x7F4A7C15u;
|
|
}
|
|
|
|
static uint32_t lfoTriggerSeed(int l)
|
|
{
|
|
return 0x2545F491u + (uint32_t)l * 3266489917u;
|
|
}
|
|
|
|
static void lfoStateInit(LfoState *st, uint32_t seed)
|
|
{
|
|
st->phase = 0.0f;
|
|
st->noiseHeld = 0.0f;
|
|
st->noisePhase = 0.0f;
|
|
st->noiseState = seed;
|
|
}
|
|
|
|
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. The constants are arbitrary and non-zero.
|
|
// Envelope 0 shapes the level, which is what it always did - the difference is that
|
|
// it is now said rather than assumed, and can be said differently.
|
|
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
|
s->voices[i].gate = VOICE_GATE;
|
|
for (int l = 0; l < LFO_COUNT; l++)
|
|
lfoStateInit(&s->voices[i].lfoRun[l], lfoTriggerSeed(l));
|
|
s->voices[i].oscillators[0].noiseState = 0x9E3779B9u + (uint32_t)i * 2654435761u;
|
|
s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u;
|
|
|
|
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++) {
|
|
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
|
s->lfos[l].rate = 1.0f;
|
|
s->lfos[l].waveform = WAVE_SINE;
|
|
s->lfos[l].active = 0;
|
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
|
// made against.
|
|
s->lfos[l].mode = LFO_FREE;
|
|
}
|
|
}
|
|
|
|
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 - now said out loud, so that a patch which routed it elsewhere does not
|
|
// leave the next one silent.
|
|
v->levelSource = MOD_SOURCE_AMP_ENV;
|
|
|
|
// Held up by the key unless a patch says otherwise, which is what every patch that
|
|
// exists was made against.
|
|
v->gate = VOICE_GATE;
|
|
|
|
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++) {
|
|
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
|
s->lfos[l].rate = 1.0f;
|
|
s->lfos[l].waveform = WAVE_SINE;
|
|
s->lfos[l].active = 0;
|
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
|
// made against.
|
|
s->lfos[l].mode = LFO_FREE;
|
|
}
|
|
|
|
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 lfoTickState(const LFO *l, LfoState *st, float sampleRate)
|
|
{
|
|
if (!l->active) return 0.0f;
|
|
st->phase += l->rate / sampleRate;
|
|
if (st->phase >= 1.0f) st->phase -= 1.0f;
|
|
if (l->waveform == WAVE_NOISE) {
|
|
st->noisePhase += l->rate / sampleRate;
|
|
if (st->noisePhase >= 1.0f) {
|
|
st->noisePhase -= 1.0f;
|
|
st->noiseHeld = nextNoise(&st->noiseState);
|
|
}
|
|
}
|
|
return waveformSample(l->waveform, st->phase, 0.5f, st->noiseHeld);
|
|
}
|
|
|
|
// The LFO advancing its own cycle: the free-running one, the same for every voice.
|
|
float lfoTick(LFO *l, float sampleRate)
|
|
{
|
|
return lfoTickState(l, &l->run, sampleRate);
|
|
}
|
|
|
|
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 "???";
|
|
}
|
|
}
|
|
|
|
// Everything about a voice that a note begins rather than inherits.
|
|
//
|
|
// The envelopes are told, once, whether this note is waiting on a key - asked here rather
|
|
// than read live in the mixer so a note already sounding keeps the shape it began with.
|
|
//
|
|
// A TRIGGERED voice also starts its oscillators over. They are the larger half of why the
|
|
// same one-shot came out different every time: the envelopes restarted and the filter was
|
|
// cleared, but the oscillator phase carried on from wherever the last note left it, so a
|
|
// kick began a third of the way into its own cycle depending on what played before it. A
|
|
// gated voice is left alone, because a key being held is not a claim about phase and every
|
|
// patch that exists was made against the old behaviour.
|
|
//
|
|
// Retriggered LFOs restart for THIS voice only, whatever the gate - an LFO starting fresh
|
|
// per note is wanted under held notes too, and it is the shared cycle that must not move.
|
|
static void voiceArm(Synth *s, Voice *v)
|
|
{
|
|
int oneShot = (v->gate == VOICE_TRIGGER);
|
|
v->ampEnv.oneShot = oneShot;
|
|
v->modEnv.oneShot = oneShot;
|
|
|
|
if (v->gate == VOICE_TRIGGER) {
|
|
for (int o = 0; o < OSC_COUNT; o++) {
|
|
v->oscillators[o].phase = 0.0f;
|
|
v->oscillators[o].noiseHeld = 0.0f;
|
|
v->oscillators[o].noisePhase = 0.0f;
|
|
v->oscillators[o].noiseState = oscTriggerSeed(o);
|
|
}
|
|
}
|
|
|
|
for (int l = 0; l < LFO_COUNT; l++)
|
|
if (s->lfos[l].mode == LFO_RETRIGGER)
|
|
lfoStateInit(&v->lfoRun[l], lfoTriggerSeed(l));
|
|
|
|
envelopeNoteOn(&v->ampEnv);
|
|
envelopeNoteOn(&v->modEnv);
|
|
}
|
|
|
|
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;
|
|
voiceArm(s, &s->voices[i]);
|
|
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;
|
|
voiceArm(s, &s->voices[i]);
|
|
}
|
|
|
|
|
|
void synthNoteOff(Synth *s, int midiNote) {
|
|
for (int i = 0; i < VOICE_COUNT; i++) {
|
|
if (s->voices[i].active && s->voices[i].midiNote == midiNote) {
|
|
// A triggered voice plays its own length; the key coming up is not its business.
|
|
// Asked of the envelope rather than the voice, because that is what the note was
|
|
// started with and the switch may have moved since.
|
|
if (s->voices[i].ampEnv.oneShot) continue;
|
|
envelopeNoteOff(&s->voices[i].ampEnv);
|
|
envelopeNoteOff(&s->voices[i].modEnv);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Asked for by number, rather than allocated ----
|
|
//
|
|
// Channel two is channel two, it holds its patch between notes, and a program driving this as
|
|
// hardware can rely on both. synthNoteOn above steals a voice round-robin, which is right for
|
|
// a keyboard and wrong for anything addressing a fixed set of parts.
|
|
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;
|
|
voiceArm(s, v);
|
|
}
|
|
|
|
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;
|
|
|
|
// The shared cycle advances once per sample whatever is listening, so a free LFO is
|
|
// one sweep under everything and does not stall when nothing is sounding.
|
|
float freeLfo0 = lfoTick(&s->lfos[0], sr);
|
|
float freeLfo1 = 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);
|
|
|
|
// A gated voice is over when envelope 0 is, which is after the key came up.
|
|
// A triggered one has no key to wait for, so it is over only when BOTH envelopes
|
|
// are - envelope 0 alone would cut a level that envelope 1 is still shaping.
|
|
int finished = (vv->ampEnv.stage == ENV_IDLE);
|
|
if (vv->ampEnv.oneShot)
|
|
finished = finished && (vv->modEnv.stage == ENV_IDLE);
|
|
if (finished) {
|
|
vv->active = 0;
|
|
continue;
|
|
}
|
|
|
|
// A retriggered LFO reads this voice's own cycle, started when it was struck.
|
|
// A free one reads the shared cycle above, exactly as it always did.
|
|
float lfo0 = (s->lfos[0].mode == LFO_RETRIGGER)
|
|
? lfoTickState(&s->lfos[0], &vv->lfoRun[0], sr) : freeLfo0;
|
|
float lfo1 = (s->lfos[1].mode == LFO_RETRIGGER)
|
|
? lfoTickState(&s->lfos[1], &vv->lfoRun[1], sr) : freeLfo1;
|
|
|
|
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;
|
|
e->oneShot = 0;
|
|
}
|
|
|
|
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;
|
|
// Sustain is where the decay stops and waits for the key. A one-shot has no key
|
|
// to wait for, so it decays the whole way and is finished - and MUST, or a patch
|
|
// with a sustain above nothing would hold a triggered voice open forever.
|
|
float floorLevel = e->oneShot ? 0.0f : e->sustainLevel;
|
|
if (e->value <= floorLevel) {
|
|
e->value = floorLevel;
|
|
e->stage = e->oneShot ? ENV_IDLE : 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 what shapes the level. Without this the voices below hold whatever synthInit
|
|
// gave them, so a patch that routes its level elsewhere is honoured by voice 0 and by
|
|
// nothing else - which sounds like it works until a second note is playing.
|
|
s->voices[v].levelSource = s->voices[0].levelSource;
|
|
s->voices[v].gate = s->voices[0].gate;
|
|
|
|
// 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;
|
|
}
|
|
}
|