Fold soundThing's changes back down, and expose the two new switches
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.
This commit is contained in:
@@ -213,6 +213,7 @@ static void setLfo(LFO *l, uint8_t which, uint8_t value) {
|
||||
case SP_LFO_ACTIVE: l->active = value != 0; break;
|
||||
case SP_LFO_WAVE: l->waveform = (Waveform)(value % WAVE_COUNT); break;
|
||||
case SP_LFO_RATE: l->rate = exponential(value, 0.05f, 20.0f); break;
|
||||
case SP_LFO_MODE: l->mode = value ? LFO_RETRIGGER : LFO_FREE; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -228,8 +229,12 @@ static void soundParameter(uint8_t value) {
|
||||
case SP_MODENV: setEnvelope(&v->modEnv, which, value); break;
|
||||
case SP_FILTER: setFilter(&v->filter, parameter, value); break;
|
||||
case SP_LEVEL_SOURCE:
|
||||
// Two settings share a group, because both are about the voice as a whole rather
|
||||
// than about one of its parts.
|
||||
if (parameter == SP_LEVEL_SOURCE) {
|
||||
v->levelSource = sourceFor(value);
|
||||
} else if (parameter == SP_VOICE_GATE) {
|
||||
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
||||
}
|
||||
break;
|
||||
// The LFOs belong to the device rather than to a channel, so whichever channel is
|
||||
|
||||
@@ -82,6 +82,17 @@
|
||||
// LFOs. Nought is the one that could not be said before - see synth.h.
|
||||
#define SP_LEVEL_SOURCE 0x50
|
||||
|
||||
// ---- Whether a note waits to be let go of ----
|
||||
//
|
||||
// 0 is gated and 1 is triggered. Gated is what a keyboard wants: the sound lasts as long as
|
||||
// the finger does, and dropping the gate starts the release. Triggered is what a GAME wants -
|
||||
// a bang, a pickup, a door - where the note is struck and then plays its own length, and
|
||||
// nothing has to remember to end it. Sustain and release have no meaning in a triggered
|
||||
// voice, because both are answers to a question about a key that is not being asked.
|
||||
//
|
||||
// A program driving one-shots does not need port 0x45 at all once this is set.
|
||||
#define SP_VOICE_GATE 0x51
|
||||
|
||||
// The LFOs belong to the whole device rather than to a channel, so these ignore whichever
|
||||
// channel is selected.
|
||||
#define SP_LFO0 0x60 // 0x60-0x6F and 0x70-0x7F
|
||||
@@ -89,6 +100,16 @@
|
||||
#define SP_LFO_ACTIVE 0x00
|
||||
#define SP_LFO_WAVE 0x01
|
||||
#define SP_LFO_RATE 0x02
|
||||
// ---- Whether it starts over when a voice does ----
|
||||
//
|
||||
// 0 free and 1 retriggered. Free is one cycle running under everything, which is what vibrato
|
||||
// across a held chord wants. Retriggered starts at the beginning of its shape every time a
|
||||
// voice begins, which is the only way a one-shot sounds the SAME twice: a free LFO is wherever
|
||||
// the wall clock left it, so the same drum caught at a different moment is a different drum.
|
||||
//
|
||||
// The mode belongs to the LFO and the cycle belongs to the voice, so retriggering costs
|
||||
// nothing to a channel that is not using it.
|
||||
#define SP_LFO_MODE 0x03
|
||||
|
||||
// ---- Samples come from the machine's clock ----
|
||||
//
|
||||
|
||||
+150
-56
@@ -10,25 +10,22 @@
|
||||
//
|
||||
// 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.
|
||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
||||
//
|
||||
// ---- 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.
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// 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
|
||||
|
||||
@@ -42,6 +39,30 @@
|
||||
#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;
|
||||
@@ -58,14 +79,16 @@ void synthInit(Synth *s, float sampleRate)
|
||||
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.
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
@@ -92,13 +115,13 @@ void synthInit(Synth *s, float sampleRate)
|
||||
s->voices[0].oscillators[0].active = 1;
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++) {
|
||||
s->lfos[l].phase = 0.0f;
|
||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
||||
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;
|
||||
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||
// made against.
|
||||
s->lfos[l].mode = LFO_FREE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +130,14 @@ 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.
|
||||
// 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++) {
|
||||
@@ -139,13 +167,13 @@ void synthResetPatch(Synth *s)
|
||||
v->filter.resModDepth = 0.0f;
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++) {
|
||||
s->lfos[l].phase = 0.0f;
|
||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
||||
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;
|
||||
// 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;
|
||||
@@ -191,19 +219,25 @@ static float getModValue(float ampEnv, float modEnv, float lfo0, float lfo1, Mod
|
||||
}
|
||||
}
|
||||
|
||||
float lfoTick(LFO *l, float sampleRate)
|
||||
float lfoTickState(const LFO *l, LfoState *st, float sampleRate)
|
||||
{
|
||||
if (!l->active) return 0.0f;
|
||||
l->phase += l->rate / sampleRate;
|
||||
if (l->phase >= 1.0f) l->phase -= 1.0f;
|
||||
st->phase += l->rate / sampleRate;
|
||||
if (st->phase >= 1.0f) st->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);
|
||||
st->noisePhase += l->rate / sampleRate;
|
||||
if (st->noisePhase >= 1.0f) {
|
||||
st->noisePhase -= 1.0f;
|
||||
st->noiseHeld = nextNoise(&st->noiseState);
|
||||
}
|
||||
}
|
||||
return waveformSample(l->waveform, l->phase, 0.5f, l->noiseHeld);
|
||||
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)
|
||||
@@ -330,6 +364,43 @@ const char *waveformName(Waveform w)
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -341,8 +412,7 @@ void synthNoteOn(Synth *s, int 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);
|
||||
voiceArm(s, &s->voices[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -355,29 +425,28 @@ void synthNoteOn(Synth *s, int 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- A channel is the channel you asked for ----
|
||||
// ---- Asked for by number, rather than allocated ----
|
||||
//
|
||||
// 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.
|
||||
// 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) {
|
||||
@@ -391,8 +460,7 @@ void synthChannelOn(Synth *s, int channel, int midiNote)
|
||||
// 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);
|
||||
voiceArm(s, v);
|
||||
}
|
||||
|
||||
void synthChannelOff(Synth *s, int channel)
|
||||
@@ -413,8 +481,10 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
||||
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);
|
||||
// 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];
|
||||
@@ -423,11 +493,24 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
||||
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||
float mod = envelopeTick(&vv->modEnv, sr);
|
||||
|
||||
if (vv->ampEnv.stage == ENV_IDLE) {
|
||||
// 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++) {
|
||||
@@ -491,6 +574,7 @@ void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLev
|
||||
e->decaySec = decaySec;
|
||||
e->sustainLevel = sustainLevel;
|
||||
e->releaseSec = releaseSec;
|
||||
e->oneShot = 0;
|
||||
}
|
||||
|
||||
void envelopeNoteOn(Envelope *e)
|
||||
@@ -521,9 +605,13 @@ float envelopeTick(Envelope *e, float sampleRate)
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
@@ -549,6 +637,12 @@ float envelopeTick(Envelope *e, float sampleRate)
|
||||
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;
|
||||
|
||||
+70
-24
@@ -10,25 +10,22 @@
|
||||
//
|
||||
// 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.
|
||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
||||
//
|
||||
// ---- 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.
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// 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
|
||||
|
||||
@@ -69,6 +66,17 @@ typedef enum {
|
||||
MOD_SOURCE_LFO2 = 4
|
||||
} ModSource;
|
||||
|
||||
// ---- Whether the key holds the note up ----
|
||||
//
|
||||
// Gated is what a keyboard wants: the sound lasts as long as the finger does, and lifting it
|
||||
// starts the release. Triggered is what a drum wants - the note is struck and then plays its
|
||||
// own length, and the key coming up is not its business. Sustain and release have no meaning
|
||||
// in a triggered voice, because both of them are answers to a question about the key.
|
||||
typedef enum {
|
||||
VOICE_GATE = 0,
|
||||
VOICE_TRIGGER = 1
|
||||
} VoiceGate;
|
||||
|
||||
typedef struct {
|
||||
EnvStage stage;
|
||||
float value; // current output value, 0.0 to 1.0
|
||||
@@ -76,20 +84,46 @@ typedef struct {
|
||||
float decaySec;
|
||||
float sustainLevel;
|
||||
float releaseSec;
|
||||
// Set from the voice's gate at note-on, not read from it live, so that flipping the
|
||||
// switch under a sounding note cannot strand it half-way through a shape it was not
|
||||
// started in. One-shot runs the decay to nothing and finishes there.
|
||||
int oneShot;
|
||||
} Envelope;
|
||||
|
||||
// ---- Where an LFO is in its cycle ----
|
||||
//
|
||||
// Split out of the LFO itself because there is now more than one answer at a time. A free
|
||||
// LFO has one cycle that every voice reads, which is what makes it a single sweep across a
|
||||
// chord. A retriggered one has a cycle PER VOICE, restarted when that voice is struck - and
|
||||
// a voice resetting the shared one would drag every note already sounding along with it.
|
||||
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
|
||||
// varies between C libraries, so the same patch 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;
|
||||
} LfoState;
|
||||
|
||||
// ---- Whether an LFO keeps its own time or starts when struck ----
|
||||
//
|
||||
// Free is one cycle running under everything, which is what vibrato across a held chord
|
||||
// wants. Retriggered starts at the beginning of its shape every time a voice begins, which
|
||||
// is the only way a one-shot sounds the same twice - a free LFO is wherever the wall clock
|
||||
// left it, so the same drum caught at a different moment is a different drum.
|
||||
typedef enum {
|
||||
LFO_FREE = 0,
|
||||
LFO_RETRIGGER = 1
|
||||
} LfoMode;
|
||||
|
||||
typedef struct {
|
||||
LfoState run; // the free-running cycle, read by every voice in LFO_FREE
|
||||
float rate; // Hz
|
||||
Waveform waveform;
|
||||
int active;
|
||||
LfoMode mode;
|
||||
} LFO;
|
||||
|
||||
typedef enum {
|
||||
@@ -119,11 +153,10 @@ typedef struct {
|
||||
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
|
||||
// varies between C libraries, so the same patch 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
|
||||
@@ -155,6 +188,16 @@ typedef struct {
|
||||
// It also makes two things possible that were not: envelope 1 shaping the volume, and an
|
||||
// LFO doing it, which is tremolo.
|
||||
ModSource levelSource;
|
||||
// ---- Whether the key holds this voice up ----
|
||||
//
|
||||
// The other half of what envelope 0 used to decide on its own. Naming the level's source
|
||||
// said what shapes the sound; this says who ends it. Without it a voice can only ever
|
||||
// finish because a key came up, which is no use to a drum.
|
||||
VoiceGate gate;
|
||||
// One cycle per LFO per voice, used only by the LFOs set to LFO_RETRIGGER. Runtime state
|
||||
// like the envelope stages and the filter's integrators, so synthSyncVoices leaves it
|
||||
// alone - it is where this voice is, not what the patch says.
|
||||
LfoState lfoRun[LFO_COUNT];
|
||||
} Voice;
|
||||
|
||||
typedef struct {
|
||||
@@ -184,11 +227,12 @@ void synthResetPatch(Synth *s);
|
||||
void synthNoteOn(Synth *s, int midiNote);
|
||||
void synthNoteOff(Synth *s, int midiNote);
|
||||
|
||||
// ---- 3. A channel is asked for by number ----
|
||||
// ---- A channel 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.
|
||||
// The two above hunt for a free voice and steal round-robin, which is what a keyboard wants:
|
||||
// a player presses keys and does not care which voice sounds them. Something driving this as
|
||||
// HARDWARE does care - channel two is channel two, it keeps its patch between notes, and
|
||||
// nothing may take it away. Both ways of asking are here and neither changes the other.
|
||||
void synthChannelOn(Synth *s, int channel, int midiNote);
|
||||
void synthChannelOff(Synth *s, int channel);
|
||||
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
||||
@@ -196,6 +240,8 @@ void synthSyncVoices(Synth *s);
|
||||
|
||||
// LFO functions:
|
||||
float lfoTick(LFO *l, float sampleRate);
|
||||
// The same advance against a cycle that is not the LFO's own, so a voice can run its own.
|
||||
float lfoTickState(const LFO *l, LfoState *st, float sampleRate);
|
||||
|
||||
// Filter functions:
|
||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate);
|
||||
|
||||
@@ -103,9 +103,19 @@ static const Mapping mappings[] = {
|
||||
{ "lfo0_active", 0x60, AS_BOOL, 0, 0, "LFO 0, on" },
|
||||
{ "lfo0_waveform", 0x61, AS_DIRECT, 0, 0, "waveform" },
|
||||
{ "lfo0_rate", 0x62, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" },
|
||||
{ "lfo0_mode", 0x63, AS_DIRECT, 0, 0, "0 free, 1 starts with a voice" },
|
||||
{ "lfo1_active", 0x70, AS_BOOL, 0, 0, "LFO 1, on" },
|
||||
{ "lfo1_waveform", 0x71, AS_DIRECT, 0, 0, "waveform" },
|
||||
{ "lfo1_rate", 0x72, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" },
|
||||
{ "lfo1_mode", 0x73, AS_DIRECT, 0, 0, "0 free, 1 starts with a voice" },
|
||||
|
||||
// ---- The two that are about the voice as a whole ----
|
||||
//
|
||||
// soundThing gained a field for each of these at the same time the engine did. A patch
|
||||
// written before that has neither, which is why the level source has a default below: it
|
||||
// was always envelope 0 when there was nothing to say otherwise.
|
||||
{ "voice_levelSource", 0x50, AS_DIRECT, 0, 0, "what shapes the level" },
|
||||
{ "voice_gate", 0x51, AS_DIRECT, 0, 0, "0 gated, 1 triggered" },
|
||||
};
|
||||
#define MAPPING_COUNT ((int)(sizeof(mappings) / sizeof(mappings[0])))
|
||||
|
||||
@@ -194,12 +204,23 @@ int main(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- A patch from before the level could be routed ----
|
||||
//
|
||||
// Those files have no voice_levelSource, and what it meant there was always envelope 0.
|
||||
// Said out loud rather than left out, because a channel keeps its patch between notes and
|
||||
// would otherwise carry a previous one's routing into this one.
|
||||
int levelIndex = -1;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (mappings[i].parameter == 0x50) { levelIndex = i; break; }
|
||||
}
|
||||
int assumedLevel = (levelIndex >= 0 && !seen[levelIndex]);
|
||||
if (assumedLevel) {
|
||||
values[levelIndex] = 1; // MOD_SOURCE_AMP_ENV
|
||||
seen[levelIndex] = 1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
||||
// The level routing is soundThing's one blind spot: it has no field for it because there
|
||||
// it is always the amplitude envelope. Said out loud here so a channel that was left set
|
||||
// some other way by a previous patch does not carry it into this one.
|
||||
count++;
|
||||
|
||||
FILE *out = stdout;
|
||||
if (argc > 3) {
|
||||
@@ -215,11 +236,11 @@ int main(int argc, char **argv) {
|
||||
fprintf(out, "#Data\n\n");
|
||||
fprintf(out, "%s:\n", label);
|
||||
fprintf(out, " 0d%-24d; how many pairs follow\n", count);
|
||||
fprintf(out, " 0x50 0x01 ; the level is the amplitude envelope\n");
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (!seen[i]) continue;
|
||||
fprintf(out, " 0x%02X 0d%-3d ; %s\n",
|
||||
mappings[i].parameter, values[i], mappings[i].note);
|
||||
fprintf(out, " 0x%02X 0d%-3d ; %s%s\n",
|
||||
mappings[i].parameter, values[i], mappings[i].note,
|
||||
(i == levelIndex && assumedLevel) ? ", which this patch predates" : "");
|
||||
}
|
||||
if (out != stdout) fclose(out);
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user