soundThing's comments use em dashes and an arrow, and this repository is plain ASCII throughout because the tooling around it does not do Unicode. Tests/docs.sh caught it the first time it ran against the new file, which is what that check is for. Comments only; nothing the compiler sees has changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
591 lines
22 KiB
C
591 lines
22 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. 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 <math.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
#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;
|
|
}
|
|
}
|