// 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 twice each way. // // ---- What was changed ---- // // Nothing. This is soundThing's engine at 71e3cb2, 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. // // The second round trip was the LFOs, which belonged to the Synth and so were shared by every // voice at once. That is right for one instrument played polyphonically and wrong for four // channels playing four different things: whichever patch loaded last owned the LFO for all of // them. They now live in the Voice. It went up from here and came back with a bug fixed and // one hunk that does nothing here - synthSyncVoices carrying the free cycle down, which only // matters to a caller that syncs, and this device never does. // // 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 #include #include #include #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; // Every voice is seeded from lfoTriggerSeed(l), which depends on the LFO and NOT on // the voice: identical seeds, identical rates and identical arithmetic on every // sample is what keeps the free cycles of a chord in step rather than merely near. for (int l = 0; l < LFO_COUNT; l++) { lfoStateInit(&s->voices[i].lfos[l].run, lfoTriggerSeed(l)); s->voices[i].lfos[l].rate = 1.0f; s->voices[i].lfos[l].waveform = WAVE_SINE; s->voices[i].lfos[l].active = 0; // Free unless a patch says otherwise, which is what every patch that exists was // made against. s->voices[i].lfos[l].mode = LFO_FREE; } 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; } 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++) { v->lfos[l].rate = 1.0f; v->lfos[l].waveform = WAVE_SINE; v->lfos[l].active = 0; // Free unless a patch says otherwise, which is what every patch that exists was // made against. v->lfos[l].mode = LFO_FREE; } // The rest of this function sets voice 0 and lets synthSyncVoices carry it down. The // cycles are the exception, because sync deliberately does not touch them: restarting // only voice 0's would leave it a fraction of a turn from every other voice, and a free // LFO under a chord would stop being one sweep. So they all restart together, which is // what the single cycle this replaced did. for (int i = 0; i < VOICE_COUNT; i++) for (int l = 0; l < LFO_COUNT; l++) lfoStateInit(&s->voices[i].lfos[l].run, lfoTriggerSeed(l)); 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 the other voices' cycles must not move. static void voiceArm(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 (v->lfos[l].mode == LFO_RETRIGGER) lfoStateInit(&v->lfos[l].run, 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->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->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(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; for (int v = 0; v < VOICE_COUNT; v++) { Voice *vv = &s->voices[v]; // ---- Before the silent voices are skipped, and that is the load-bearing part ---- // // A cycle that only advanced while its voice sounded would sit still between // notes, and the voices of a chord would fall out of step the moment one of them // ended. Advancing every voice's cycle on every sample is what makes a free LFO // still one sweep under everything: identical seeds stepped by identical // arithmetic the same number of times give identical numbers, not close ones. float lfo0 = lfoTick(&vv->lfos[0], sr); float lfo1 = lfoTick(&vv->lfos[1], sr); 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; } 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 the LFO settings but NOT the cycle, for the same reason as the envelopes and // the filter below: what the patch says is shared, where the voice has got to is its // own. Copying the cycle here would also stamp on a retriggered LFO every frame. for (int l = 0; l < LFO_COUNT; l++) { s->voices[v].lfos[l].rate = s->voices[0].lfos[l].rate; s->voices[v].lfos[l].waveform = s->voices[0].lfos[l].waveform; s->voices[v].lfos[l].active = s->voices[0].lfos[l].active; s->voices[v].lfos[l].mode = s->voices[0].lfos[l].mode; // ---- and, while it is free, the cycle too ---- // // A free LFO is DEFINED as one sweep under everything, and until now that was // true only for as long as every writer of voice 0 remembered to write the other // seven. synthResetPatch remembered; patchLoad did not, and a chord came out of // a patch load permanently out of phase with itself. An invariant that decays the // first time someone forgets is better made structural: carry the cycle down and // it cannot drift, whoever writes what. // // Only while free. A retriggered LFO's cycle is its voice's own business, and // copying it here would stamp on it every frame. if (s->voices[0].lfos[l].mode == LFO_FREE) s->voices[v].lfos[l].run = s->voices[0].lfos[l].run; } // 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; } }