Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d361ea1e46 | ||
|
|
3ca5f193e6 |
@@ -80,8 +80,6 @@ start:
|
|||||||
CALL putLander
|
CALL putLander
|
||||||
CALL putGauge
|
CALL putGauge
|
||||||
CALL putOrbital
|
CALL putOrbital
|
||||||
CALL putThrust ; The instruments, once, the way the tiles are.
|
|
||||||
CALL putAlarm
|
|
||||||
|
|
||||||
INIA 0x01
|
INIA 0x01
|
||||||
OUTA 0x02 ; Key mode.
|
OUTA 0x02 ; Key mode.
|
||||||
@@ -2796,20 +2794,36 @@ runPend:
|
|||||||
BNA runPend
|
BNA runPend
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; ---- The thruster, which is a bang and then a rumble ----
|
; ---- Why every sound loads its patch at the moment it plays ----
|
||||||
;
|
;
|
||||||
; Both on their own channels because one is struck and the other is HELD: the rumble's note
|
; CHANNEL THREE IS TWO INSTRUMENTS. The bang and the latch share it, and a patch is the whole
|
||||||
; goes on when a thruster lights and its gate does not come up until every thruster is out.
|
; voice, so the channel has to be told which of them it is about to be. Loading at the moment
|
||||||
putThrust:
|
; of the note costs fifty writes at a moment already doing more than fifty, and means a sound
|
||||||
RSTA
|
; is what its patch says whatever played before it.
|
||||||
OUTA 0x41
|
;
|
||||||
SETD.0 ThrustOnPatch
|
; The other three channels have one sound each and could be set up once at startup. They are
|
||||||
CALL playPatch
|
; not, because a single rule is worth more here than the writes it saves: every sound loads
|
||||||
INIA 0x01
|
; its own patch, so there is no question of which sounds are safe to set up early and which
|
||||||
OUTA 0x41
|
; are not.
|
||||||
SETD.0 ThrustHoldPatch
|
;
|
||||||
CALL playPatch
|
; ---- What this used to be working around ----
|
||||||
RET
|
;
|
||||||
|
; The LFOs belonged to the DEVICE and not to a channel: two of them against four voices, with
|
||||||
|
; a patch carrying LFO settings the way it carries everything else, so whichever patch loaded
|
||||||
|
; last owned them for every channel at once.
|
||||||
|
;
|
||||||
|
; That is what made the low fuel warning sound right sometimes and not others. Its trill is a
|
||||||
|
; saw LFO on the pitch, and the patches on channel three carry an LFO that is switched OFF, so
|
||||||
|
; the first landing, docking or crash of a run took the warning's trill away and left a plain
|
||||||
|
; tone. Before any of them it was right, which is exactly the shape an intermittent fault has.
|
||||||
|
;
|
||||||
|
; Loading at the moment of the note made each sound correct AS IT STARTED, which was as far as
|
||||||
|
; a program could get on its own. It could not fix two sounds OVERLAPPING - a warning going off
|
||||||
|
; mid-burn still re-tuned the rumble's LFO for as long as it lasted.
|
||||||
|
;
|
||||||
|
; The device was fixed instead: an LFO now belongs to the channel it was written to, so four
|
||||||
|
; channels are four independent instruments in this respect as in every other. Overlapping
|
||||||
|
; sounds no longer interfere, and the paragraph above is history rather than a caveat.
|
||||||
|
|
||||||
; ---- Lit or not, and only the CHANGES matter ----
|
; ---- Lit or not, and only the CHANGES matter ----
|
||||||
;
|
;
|
||||||
@@ -2852,6 +2866,8 @@ thrustHaveSet:
|
|||||||
BRQ thrustNoPop ; Nothing newly lit, whatever else is still burning.
|
BRQ thrustNoPop ; Nothing newly lit, whatever else is still burning.
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0x41
|
OUTA 0x41
|
||||||
|
SETD.0 ThrustOnPatch
|
||||||
|
CALL playPatch
|
||||||
INIA 0d36
|
INIA 0d36
|
||||||
OUTA 0x44 ; The bang of it catching.
|
OUTA 0x44 ; The bang of it catching.
|
||||||
thrustNoPop:
|
thrustNoPop:
|
||||||
@@ -2870,6 +2886,8 @@ thrustNoPop:
|
|||||||
INIA 0x01
|
INIA 0x01
|
||||||
STA.0
|
STA.0
|
||||||
OUTA 0x41
|
OUTA 0x41
|
||||||
|
SETD.0 ThrustHoldPatch
|
||||||
|
CALL playPatch ; After the pop's, so the held sound owns the LFO while it runs.
|
||||||
INIA 0d60
|
INIA 0d60
|
||||||
OUTA 0x44 ; Held, until the gate comes up.
|
OUTA 0x44 ; Held, until the gate comes up.
|
||||||
BRI thrustRemember
|
BRI thrustRemember
|
||||||
@@ -2965,16 +2983,11 @@ watchFuelDone:
|
|||||||
; Effects count down from the top: the bang has three and this has two. They are separate
|
; Effects count down from the top: the bang has three and this has two. They are separate
|
||||||
; channels rather than one reused, because a crash while the warning is still sounding should
|
; channels rather than one reused, because a crash while the warning is still sounding should
|
||||||
; not cut it off - and on a machine with four voices there is no reason to be clever about it.
|
; not cut it off - and on a machine with four voices there is no reason to be clever about it.
|
||||||
putAlarm:
|
|
||||||
INIA 0x02
|
|
||||||
OUTA 0x41
|
|
||||||
SETD.0 AlarmPatch
|
|
||||||
CALL playPatch
|
|
||||||
RET
|
|
||||||
|
|
||||||
alarm:
|
alarm:
|
||||||
INIA 0x02
|
INIA 0x02
|
||||||
OUTA 0x41
|
OUTA 0x41
|
||||||
|
SETD.0 AlarmPatch
|
||||||
|
CALL playPatch ; Its own LFO back, whatever the last sound left there.
|
||||||
INIA 0d72 ; An octave over middle C, which is where a warning lives.
|
INIA 0d72 ; An octave over middle C, which is where a warning lives.
|
||||||
OUTA 0x44
|
OUTA 0x44
|
||||||
RET
|
RET
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+12
-4
@@ -237,10 +237,18 @@ static void soundParameter(uint8_t value) {
|
|||||||
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// The LFOs belong to the device rather than to a channel, so whichever channel is
|
// ---- The LFOs belong to the channel ----
|
||||||
// selected makes no difference to these.
|
//
|
||||||
case SP_LFO0: setLfo(&synth.lfos[0], which, value); break;
|
// They used to belong to the device, so whichever channel was selected made no
|
||||||
case SP_LFO1: setLfo(&synth.lfos[1], which, value); break;
|
// difference: two channels playing two different sounds shared one pair of LFOs, and
|
||||||
|
// whichever loaded its settings last owned them for both. A sound with its LFO
|
||||||
|
// switched off silenced the trill under a sound that was still playing.
|
||||||
|
//
|
||||||
|
// Four channels are four independent things, which is the rule the rest of this
|
||||||
|
// device already followed. So these read the selected channel like every parameter
|
||||||
|
// above them, and an LFO written to one channel is inaudible on the other three.
|
||||||
|
case SP_LFO0: setLfo(&v->lfos[0], which, value); break;
|
||||||
|
case SP_LFO1: setLfo(&v->lfos[1], which, value); break;
|
||||||
default:
|
default:
|
||||||
// A parameter number nothing answers to does nothing. A sound device is a poor
|
// A parameter number nothing answers to does nothing. A sound device is a poor
|
||||||
// place to stop the machine, the same as a screen.
|
// place to stop the machine, the same as a screen.
|
||||||
|
|||||||
+76
-38
@@ -10,11 +10,11 @@
|
|||||||
//
|
//
|
||||||
// It is copied rather than submoduled. Two files against tying this build to another
|
// 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
|
// 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.
|
// carried across on purpose - in BOTH directions, which has now happened twice each way.
|
||||||
//
|
//
|
||||||
// ---- What was changed ----
|
// ---- What was changed ----
|
||||||
//
|
//
|
||||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
// 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
|
// 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.
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||||
//
|
//
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
// 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.
|
// 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
|
// 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.
|
// other one has to be told.
|
||||||
//
|
//
|
||||||
@@ -84,8 +91,18 @@ void synthInit(Synth *s, float sampleRate)
|
|||||||
// it is now said rather than assumed, and can be said differently.
|
// it is now said rather than assumed, and can be said differently.
|
||||||
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
||||||
s->voices[i].gate = VOICE_GATE;
|
s->voices[i].gate = VOICE_GATE;
|
||||||
for (int l = 0; l < LFO_COUNT; l++)
|
// Every voice is seeded from lfoTriggerSeed(l), which depends on the LFO and NOT on
|
||||||
lfoStateInit(&s->voices[i].lfoRun[l], lfoTriggerSeed(l));
|
// 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[0].noiseState = 0x9E3779B9u + (uint32_t)i * 2654435761u;
|
||||||
s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u;
|
s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u;
|
||||||
|
|
||||||
@@ -113,16 +130,6 @@ void synthInit(Synth *s, float sampleRate)
|
|||||||
s->voices[i].filter.resModDepth = 0.0f;
|
s->voices[i].filter.resModDepth = 0.0f;
|
||||||
}
|
}
|
||||||
s->voices[0].oscillators[0].active = 1;
|
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)
|
void synthResetPatch(Synth *s)
|
||||||
@@ -167,15 +174,23 @@ void synthResetPatch(Synth *s)
|
|||||||
v->filter.resModDepth = 0.0f;
|
v->filter.resModDepth = 0.0f;
|
||||||
|
|
||||||
for (int l = 0; l < LFO_COUNT; l++) {
|
for (int l = 0; l < LFO_COUNT; l++) {
|
||||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
v->lfos[l].rate = 1.0f;
|
||||||
s->lfos[l].rate = 1.0f;
|
v->lfos[l].waveform = WAVE_SINE;
|
||||||
s->lfos[l].waveform = WAVE_SINE;
|
v->lfos[l].active = 0;
|
||||||
s->lfos[l].active = 0;
|
|
||||||
// Free unless a patch says otherwise, which is what every patch that exists was
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||||
// made against.
|
// made against.
|
||||||
s->lfos[l].mode = LFO_FREE;
|
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->volume = 0.8f;
|
||||||
s->pitchBendRange = 2.0f;
|
s->pitchBendRange = 2.0f;
|
||||||
}
|
}
|
||||||
@@ -377,8 +392,8 @@ const char *waveformName(Waveform w)
|
|||||||
// patch that exists was made against the old behaviour.
|
// patch that exists was made against the old behaviour.
|
||||||
//
|
//
|
||||||
// Retriggered LFOs restart for THIS voice only, whatever the gate - an LFO starting fresh
|
// 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.
|
// per note is wanted under held notes too, and the other voices' cycles must not move.
|
||||||
static void voiceArm(Synth *s, Voice *v)
|
static void voiceArm(Voice *v)
|
||||||
{
|
{
|
||||||
int oneShot = (v->gate == VOICE_TRIGGER);
|
int oneShot = (v->gate == VOICE_TRIGGER);
|
||||||
v->ampEnv.oneShot = oneShot;
|
v->ampEnv.oneShot = oneShot;
|
||||||
@@ -394,8 +409,8 @@ static void voiceArm(Synth *s, Voice *v)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int l = 0; l < LFO_COUNT; l++)
|
for (int l = 0; l < LFO_COUNT; l++)
|
||||||
if (s->lfos[l].mode == LFO_RETRIGGER)
|
if (v->lfos[l].mode == LFO_RETRIGGER)
|
||||||
lfoStateInit(&v->lfoRun[l], lfoTriggerSeed(l));
|
lfoStateInit(&v->lfos[l].run, lfoTriggerSeed(l));
|
||||||
|
|
||||||
envelopeNoteOn(&v->ampEnv);
|
envelopeNoteOn(&v->ampEnv);
|
||||||
envelopeNoteOn(&v->modEnv);
|
envelopeNoteOn(&v->modEnv);
|
||||||
@@ -412,7 +427,7 @@ void synthNoteOn(Synth *s, int midiNote)
|
|||||||
s->voices[i].active = 1;
|
s->voices[i].active = 1;
|
||||||
s->voices[i].filter.low = 0.0f;
|
s->voices[i].filter.low = 0.0f;
|
||||||
s->voices[i].filter.band = 0.0f;
|
s->voices[i].filter.band = 0.0f;
|
||||||
voiceArm(s, &s->voices[i]);
|
voiceArm(&s->voices[i]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +440,7 @@ void synthNoteOn(Synth *s, int midiNote)
|
|||||||
s->voices[i].active = 1;
|
s->voices[i].active = 1;
|
||||||
s->voices[i].filter.low = 0.0f;
|
s->voices[i].filter.low = 0.0f;
|
||||||
s->voices[i].filter.band = 0.0f;
|
s->voices[i].filter.band = 0.0f;
|
||||||
voiceArm(s, &s->voices[i]);
|
voiceArm(&s->voices[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -460,7 +475,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.
|
// where the last one left off is how a click gets into the front of every sound.
|
||||||
v->filter.low = 0.0f;
|
v->filter.low = 0.0f;
|
||||||
v->filter.band = 0.0f;
|
v->filter.band = 0.0f;
|
||||||
voiceArm(s, v);
|
voiceArm(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
void synthChannelOff(Synth *s, int channel)
|
void synthChannelOff(Synth *s, int channel)
|
||||||
@@ -481,13 +496,19 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
|||||||
for (int i = 0; i < frames; i++) {
|
for (int i = 0; i < frames; i++) {
|
||||||
float mix = 0.0f;
|
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++) {
|
for (int v = 0; v < VOICE_COUNT; v++) {
|
||||||
Voice *vv = &s->voices[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;
|
if (!vv->active) continue;
|
||||||
|
|
||||||
float amp = envelopeTick(&vv->ampEnv, sr);
|
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||||
@@ -504,13 +525,6 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
|||||||
continue;
|
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;
|
float oscMix = 0.0f;
|
||||||
int activeOscs = 0;
|
int activeOscs = 0;
|
||||||
for (int o = 0; o < OSC_COUNT; o++) {
|
for (int o = 0; o < OSC_COUNT; o++) {
|
||||||
@@ -643,6 +657,30 @@ void synthSyncVoices(Synth *s)
|
|||||||
s->voices[v].levelSource = s->voices[0].levelSource;
|
s->voices[v].levelSource = s->voices[0].levelSource;
|
||||||
s->voices[v].gate = s->voices[0].gate;
|
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
|
// Sync oscillator settings
|
||||||
for (int o = 0; o < OSC_COUNT; o++) {
|
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].waveform = s->voices[0].oscillators[o].waveform;
|
||||||
|
|||||||
+28
-8
@@ -10,11 +10,11 @@
|
|||||||
//
|
//
|
||||||
// It is copied rather than submoduled. Two files against tying this build to another
|
// 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
|
// 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.
|
// carried across on purpose - in BOTH directions, which has now happened twice each way.
|
||||||
//
|
//
|
||||||
// ---- What was changed ----
|
// ---- What was changed ----
|
||||||
//
|
//
|
||||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
// 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
|
// 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.
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||||
//
|
//
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
// 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.
|
// 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
|
// 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.
|
// other one has to be told.
|
||||||
//
|
//
|
||||||
@@ -119,7 +126,7 @@ typedef enum {
|
|||||||
} LfoMode;
|
} LfoMode;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
LfoState run; // the free-running cycle, read by every voice in LFO_FREE
|
LfoState run; // this LFO's own cycle, free-running or reset at note on
|
||||||
float rate; // Hz
|
float rate; // Hz
|
||||||
Waveform waveform;
|
Waveform waveform;
|
||||||
int active;
|
int active;
|
||||||
@@ -194,10 +201,24 @@ typedef struct {
|
|||||||
// said what shapes the sound; this says who ends it. Without it a voice can only ever
|
// 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.
|
// finish because a key came up, which is no use to a drum.
|
||||||
VoiceGate gate;
|
VoiceGate gate;
|
||||||
// One cycle per LFO per voice, used only by the LFOs set to LFO_RETRIGGER. Runtime state
|
// ---- Its own LFOs, like its own envelopes and its own filter ----
|
||||||
// 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.
|
// They used to belong to the Synth, which is right for ONE INSTRUMENT played polyphonically
|
||||||
LfoState lfoRun[LFO_COUNT];
|
// and wrong for anything else: a patch carries LFO settings, so loading a patch changed
|
||||||
|
// what every other voice heard. That is invisible here, because every voice is given the
|
||||||
|
// same patch - and it is the whole difficulty for anything driving these voices as separate
|
||||||
|
// parts, where two patches disagree about the rate and the last one loaded wins for both.
|
||||||
|
//
|
||||||
|
// A chord is unaffected. synthSyncVoices copies these like everything else, so every voice
|
||||||
|
// runs identical settings, and their cycles are started together in synthInit and advanced
|
||||||
|
// every sample whether the voice is sounding or not - so they do not merely stay close,
|
||||||
|
// they stay bit for bit identical, and one sweep still runs under the whole chord.
|
||||||
|
//
|
||||||
|
// Each LFO's own cycle lives inside it, like the envelope stages and the filter's
|
||||||
|
// integrators, so synthSyncVoices copies the settings and leaves the cycle alone - it is
|
||||||
|
// where this voice is, not what the patch says. LFO_RETRIGGER resets that same cycle at
|
||||||
|
// note on, which is what it always did, only now to a cycle nothing else is listening to.
|
||||||
|
LFO lfos[LFO_COUNT];
|
||||||
} Voice;
|
} Voice;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -207,7 +228,6 @@ typedef struct {
|
|||||||
int lastStolenVoice;
|
int lastStolenVoice;
|
||||||
Voice voices[VOICE_COUNT];
|
Voice voices[VOICE_COUNT];
|
||||||
float volume; // 0.0 to 1.0, master output level
|
float volume; // 0.0 to 1.0, master output level
|
||||||
LFO lfos[LFO_COUNT];
|
|
||||||
} Synth;
|
} Synth;
|
||||||
|
|
||||||
// Envelope functions:
|
// Envelope functions:
|
||||||
|
|||||||
@@ -1075,9 +1075,9 @@ Anywhere a setting asks *what modulates* something, the answer is one of these:
|
|||||||
| 3 | LFO 0. |
|
| 3 | LFO 0. |
|
||||||
| 4 | LFO 1. |
|
| 4 | LFO 1. |
|
||||||
|
|
||||||
**The two LFOs belong to the device and not to a channel**, so writing 0x60 to 0x7F ignores
|
**Each channel has its own pair of LFOs**, so writing 0x60 to 0x7F sets them on whichever
|
||||||
whichever channel is selected. That is what makes them useful: a vibrato that every voice
|
channel is selected, exactly like every parameter above. Four channels are four independent
|
||||||
shares is one wobble rather than four that drift apart.
|
instruments, and an LFO is part of how an instrument sounds.
|
||||||
|
|
||||||
### What A Byte Means:
|
### What A Byte Means:
|
||||||
|
|
||||||
@@ -1148,6 +1148,31 @@ The consequence is worth knowing rather than fixing: a retriggered noise source
|
|||||||
repeatable, so every hit is literally the same noise, the way a sampler is. On a hi-hat that
|
repeatable, so every hit is literally the same noise, the way a sampler is. On a hi-hat that
|
||||||
can read as machine-gunny. Where variation is wanted, leave that LFO or that voice free.
|
can read as machine-gunny. Where variation is wanted, leave that LFO or that voice free.
|
||||||
|
|
||||||
|
### They Used To Be Shared, And The Fault That Caused:
|
||||||
|
|
||||||
|
Worth knowing, because it is the shape of bug this device is built to avoid and the manual
|
||||||
|
said the opposite for a while.
|
||||||
|
|
||||||
|
The two LFOs used to belong to the **device**, so setting one from a patch meant for channel
|
||||||
|
three changed what channel nought heard, immediately and for as long as nothing set it back.
|
||||||
|
A patch naturally carries LFO settings along with everything else, so a program that loaded
|
||||||
|
its instruments once at startup ended up with whichever of them was written *last*, for all
|
||||||
|
of them.
|
||||||
|
|
||||||
|
The failure that produces is unpleasant to diagnose, because it is intermittent by nature: a
|
||||||
|
sound is correct until some unrelated thing plays, and correct again next time the machine
|
||||||
|
starts. A game here lost a warning's trill after the first landing of each run, because the
|
||||||
|
landing's patch happened to carry an LFO that was switched off.
|
||||||
|
|
||||||
|
A program could work around the first half of that by loading each patch immediately before
|
||||||
|
the note that needed it. It could not work around the second: two sounds **overlapping** still
|
||||||
|
interfered, and no amount of care in the program could separate them. So the device was
|
||||||
|
changed rather than the programs, and an LFO now belongs to the channel it was written to.
|
||||||
|
|
||||||
|
**Loading a patch immediately before its note is still good practice**, for the different
|
||||||
|
reason that a channel used by two sounds has to be told which of them it is about to be. A
|
||||||
|
patch is forty-odd writes and costs nothing at a moment already making a sound.
|
||||||
|
|
||||||
### Knowing When It Has Finished:
|
### Knowing When It Has Finished:
|
||||||
|
|
||||||
The status port's bit 0 is set while any channel is still sounding, so a routine can wait for
|
The status port's bit 0 is set while any channel is still sounding, so a routine can wait for
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
|||||||
Flip.sbx 173
|
Flip.sbx 173
|
||||||
Sprite.sbx 442
|
Sprite.sbx 442
|
||||||
Depth.sbx 672
|
Depth.sbx 672
|
||||||
Lander.sbx 9220
|
Lander.sbx 9201
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -496,6 +496,88 @@ PY
|
|||||||
&& result ok "and two hits are the same hit" "sample for sample, noise and all" \
|
&& result ok "and two hits are the same hit" "sample for sample, noise and all" \
|
||||||
|| result no "and two hits are the same hit" "$SAME"
|
|| result no "and two hits are the same hit" "$SAME"
|
||||||
|
|
||||||
|
# ---- An LFO belongs to its CHANNEL, and a patch carries LFO settings ----
|
||||||
|
#
|
||||||
|
# This used to be the other way round, and it was a trap with teeth: four voices shared two
|
||||||
|
# LFOs, so a patch loaded onto channel ONE re-tuned what channel NOUGHT heard. The symptom was
|
||||||
|
# the worst kind - a sound that is right until some unrelated thing plays, and right again
|
||||||
|
# next time the machine starts. In the game it was a warning whose trill vanished after the
|
||||||
|
# first landing of a run, because the landing's patch carries an LFO switched off.
|
||||||
|
#
|
||||||
|
# Three notes on channel nought, all identical in what THEY were told, and the third leg is
|
||||||
|
# what keeps this check honest. Between the first and second, a patch is dropped on channel
|
||||||
|
# ONE that switches the LFO off - which must now do nothing here. Between the second and third
|
||||||
|
# the same thing is written to channel NOUGHT itself - which must still work, or the two
|
||||||
|
# checks above would pass just as well on a device where writing an LFO does nothing at all.
|
||||||
|
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
|
||||||
|
param 0x00 1 # triangle, so the LFO's work on the pitch is plain
|
||||||
|
param 0x51 1 # triggered, so each note ends itself
|
||||||
|
param 0x20 0x00; param 0x21 40; param 0x22 0x00
|
||||||
|
param 0x08 3 # pitch follows LFO 0
|
||||||
|
param 0x09 200 # and a long way
|
||||||
|
param 0x60 0x01; param 0x61 2; param 0x62 40; param 0x63 0x01
|
||||||
|
port 0x44 60; pause one 250
|
||||||
|
|
||||||
|
port 0x41 0x01 # somebody else's instrument, which says the LFO is off
|
||||||
|
param 0x60 0x00
|
||||||
|
port 0x41 0x00
|
||||||
|
port 0x44 60; pause two 250
|
||||||
|
|
||||||
|
port 0x41 0x00 # and now its OWN LFO switched off, which must be heard
|
||||||
|
param 0x60 0x00
|
||||||
|
port 0x44 60; spinForever
|
||||||
|
printf '#Vectors\n Boot start\n'; } | run lfoshared 8000000 || exit 1
|
||||||
|
read -r FIRST SECOND THIRD <<EOT
|
||||||
|
$(python3 - "$BUILD/lfoshared.raw" <<'PY'
|
||||||
|
import struct, sys
|
||||||
|
data = open(sys.argv[1], "rb").read()
|
||||||
|
v = struct.unpack("<%dh" % (len(data) // 2), data)
|
||||||
|
# Three notes, found as sound after silence.
|
||||||
|
at, sounding, quiet = [], False, 0
|
||||||
|
for i, x in enumerate(v):
|
||||||
|
if abs(x) > 300:
|
||||||
|
if not sounding:
|
||||||
|
at.append(i); sounding = True
|
||||||
|
quiet = 0
|
||||||
|
elif sounding:
|
||||||
|
quiet += 1
|
||||||
|
if quiet > 3000: sounding = False
|
||||||
|
if len(at) < 3:
|
||||||
|
print("0 0 0"); raise SystemExit
|
||||||
|
# ---- The PITCH of each, not how far it travels inside one ----
|
||||||
|
#
|
||||||
|
# The LFO here is slower than the note is long, so within one note it barely moves - what it
|
||||||
|
# does is hold the pitch somewhere other than where the note asked for. Which is the honest
|
||||||
|
# thing to measure anyway: a retriggered LFO starts at the same phase every time, so two notes
|
||||||
|
# that agree had the same LFO and one that disagrees did not.
|
||||||
|
def hz(start):
|
||||||
|
w = v[start:start + 3000]
|
||||||
|
peak = max((abs(x) for x in w), default=0)
|
||||||
|
if not peak: return 0
|
||||||
|
gate, cross, side = peak // 10, 0, 0
|
||||||
|
for x in w:
|
||||||
|
if side <= 0 and x > gate: side, cross = 1, cross + 1
|
||||||
|
elif side >= 0 and x < -gate: side, cross = -1, cross + 1
|
||||||
|
return cross * 48000 // (2 * len(w))
|
||||||
|
print(hz(at[0]), hz(at[1]), hz(at[2]))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
EOT
|
||||||
|
# The note asks for middle C, about 262. With the LFO on it is bent well away from that.
|
||||||
|
[ "$FIRST" -gt 0 ] && [ "$SECOND" -gt 0 ] && [ "$THIRD" -gt 0 ] \
|
||||||
|
&& result ok "an LFO bends the pitch it is routed to" "$FIRST hertz against the 262 asked for" \
|
||||||
|
|| result no "an LFO bends the pitch it is routed to" "$FIRST $SECOND $THIRD"
|
||||||
|
# Switched off by a patch meant for ANOTHER CHANNEL, and nothing here changes. This is the
|
||||||
|
# whole point: the two used to share, and a sound could not protect itself from its neighbour.
|
||||||
|
[ "$FIRST" = "$SECOND" ] \
|
||||||
|
&& result ok "another channel's patch leaves it alone" "$FIRST hertz before and after" \
|
||||||
|
|| result no "another channel's patch leaves it alone" "$FIRST became $SECOND, so they are shared"
|
||||||
|
# Switched off on its OWN channel, and the note comes back unbent - so the check above is
|
||||||
|
# measuring an LFO that really is reachable, rather than one nothing can write to.
|
||||||
|
[ "$(( THIRD - FIRST ))" -gt 40 ] \
|
||||||
|
&& result ok "and its own channel's patch does take it away" "$FIRST became $THIRD, which is the note itself" \
|
||||||
|
|| result no "and its own channel's patch does take it away" "$FIRST then $THIRD"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ "$FAIL" -eq 0 ]; then
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
echo "All $PASS sound checks passed."
|
echo "All $PASS sound checks passed."
|
||||||
|
|||||||
Reference in New Issue
Block a user