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:
+51
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 5,
|
||||||
|
"osc0_dutyCycle": 0.500000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 4.000000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 2,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 2,
|
||||||
|
"osc0_modRouting2": 0,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": 180.000000,
|
||||||
|
"osc0_modDepth2": 4.000000,
|
||||||
|
"osc1_waveform": 5,
|
||||||
|
"osc1_dutyCycle": 0.545000,
|
||||||
|
"osc1_detune": -15.000000,
|
||||||
|
"osc1_gain": 4.000000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 1,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 2,
|
||||||
|
"osc1_modRouting2": 0,
|
||||||
|
"osc1_modDepth0": -0.750000,
|
||||||
|
"osc1_modDepth1": 1104.000000,
|
||||||
|
"osc1_modDepth2": 4.000000,
|
||||||
|
"ampEnv_attack": 2.000000,
|
||||||
|
"ampEnv_decay": 0.100000,
|
||||||
|
"ampEnv_sustain": 0.000000,
|
||||||
|
"ampEnv_release": 0.001000,
|
||||||
|
"modEnv_attack": 0.001000,
|
||||||
|
"modEnv_decay": 0.603263,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 2.000000,
|
||||||
|
"lfo0_rate": 8.796101,
|
||||||
|
"lfo0_waveform": 0,
|
||||||
|
"lfo0_active": 0,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"filter_cutoff": 13294.700195,
|
||||||
|
"filter_resonance": 0.574200,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 1,
|
||||||
|
"filter_modRouting": 2,
|
||||||
|
"filter_modDepth": 11600.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"voice_levelSource": 2,
|
||||||
|
"master_volume": 1.000000,
|
||||||
|
"master_pitchBendRange": 2.000000
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"osc0_waveform": 1,
|
||||||
|
"osc0_dutyCycle": 0.500000,
|
||||||
|
"osc0_detune": 0.000000,
|
||||||
|
"osc0_gain": 4.000000,
|
||||||
|
"osc0_active": 1,
|
||||||
|
"osc0_octave": 2,
|
||||||
|
"osc0_modRouting0": 0,
|
||||||
|
"osc0_modRouting1": 3,
|
||||||
|
"osc0_modRouting2": 1,
|
||||||
|
"osc0_modDepth0": 0.000000,
|
||||||
|
"osc0_modDepth1": -420.000000,
|
||||||
|
"osc0_modDepth2": 4.000000,
|
||||||
|
"osc1_waveform": 1,
|
||||||
|
"osc1_dutyCycle": 0.500000,
|
||||||
|
"osc1_detune": 0.000000,
|
||||||
|
"osc1_gain": 4.000000,
|
||||||
|
"osc1_active": 1,
|
||||||
|
"osc1_octave": 0,
|
||||||
|
"osc1_modRouting0": 0,
|
||||||
|
"osc1_modRouting1": 3,
|
||||||
|
"osc1_modRouting2": 1,
|
||||||
|
"osc1_modDepth0": 0.000000,
|
||||||
|
"osc1_modDepth1": -420.000000,
|
||||||
|
"osc1_modDepth2": 4.000000,
|
||||||
|
"ampEnv_attack": 0.001000,
|
||||||
|
"ampEnv_decay": 0.577635,
|
||||||
|
"ampEnv_sustain": 1.000000,
|
||||||
|
"ampEnv_release": 0.065071,
|
||||||
|
"modEnv_attack": 0.005000,
|
||||||
|
"modEnv_decay": 0.500000,
|
||||||
|
"modEnv_sustain": 0.000000,
|
||||||
|
"modEnv_release": 0.100000,
|
||||||
|
"lfo0_rate": 3.398800,
|
||||||
|
"lfo0_waveform": 2,
|
||||||
|
"lfo0_active": 1,
|
||||||
|
"lfo0_mode": 1,
|
||||||
|
"lfo1_rate": 1.000000,
|
||||||
|
"lfo1_waveform": 0,
|
||||||
|
"lfo1_active": 0,
|
||||||
|
"lfo1_mode": 0,
|
||||||
|
"filter_cutoff": 8000.000000,
|
||||||
|
"filter_resonance": 0.000000,
|
||||||
|
"filter_type": 0,
|
||||||
|
"filter_active": 0,
|
||||||
|
"filter_modRouting": 0,
|
||||||
|
"filter_modDepth": 0.000000,
|
||||||
|
"filter_resModRouting": 0,
|
||||||
|
"filter_resModDepth": 0.000000,
|
||||||
|
"voice_levelSource": 1,
|
||||||
|
"voice_gate": 1,
|
||||||
|
"master_volume": 1.000000,
|
||||||
|
"master_pitchBendRange": 2.000000
|
||||||
|
}
|
||||||
@@ -2537,7 +2537,6 @@ explode:
|
|||||||
CALL startBurst
|
CALL startBurst
|
||||||
CALL boom
|
CALL boom
|
||||||
CALL runBurst
|
CALL runBurst
|
||||||
CALL boomOff
|
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; ---- Dust, for a landing that went right ----
|
; ---- Dust, for a landing that went right ----
|
||||||
@@ -2683,6 +2682,20 @@ putBoom:
|
|||||||
OUTA 0x41 ; Channel three. Every parameter write below lands on it.
|
OUTA 0x41 ; Channel three. Every parameter write below lands on it.
|
||||||
SETD.0 BoomPatch
|
SETD.0 BoomPatch
|
||||||
CALL playPatch
|
CALL playPatch
|
||||||
|
|
||||||
|
; ---- Struck, not held ----
|
||||||
|
;
|
||||||
|
; A gated voice lasts as long as something keeps hold of it, and the gate coming up is what
|
||||||
|
; starts its release - which is what a keyboard is. A crash is not a keyboard. It is struck
|
||||||
|
; once and then plays its own length, and nothing should have to remember to end it.
|
||||||
|
;
|
||||||
|
; Said here rather than left to the patch because it is the PROGRAM that knows this is a
|
||||||
|
; one-shot: the patch decides what the bang sounds like, and this decides that it is a bang.
|
||||||
|
; A patch designed in soundThing carries a gate of its own, and this overrides it on purpose.
|
||||||
|
INIA 0x51
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0x01 ; Triggered.
|
||||||
|
OUTA 0x43
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; ---- A whole patch, out of a table ----
|
; ---- A whole patch, out of a table ----
|
||||||
@@ -2716,14 +2729,6 @@ boom:
|
|||||||
OUTA 0x44 ; Writing the note is what starts it.
|
OUTA 0x44 ; Writing the note is what starts it.
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; And letting go of it, once there is nothing left to watch.
|
|
||||||
boomOff:
|
|
||||||
INIA 0x03
|
|
||||||
OUTA 0x41
|
|
||||||
RSTA
|
|
||||||
OUTA 0x45 ; Dropping the gate is the only thing that ends a note.
|
|
||||||
RET
|
|
||||||
|
|
||||||
; ---- A key, or a button ----
|
; ---- A key, or a button ----
|
||||||
;
|
;
|
||||||
; Somebody flying on a controller should not have to reach for the keyboard to say "yes, I
|
; Somebody flying on a controller should not have to reach for the keyboard to say "yes, I
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
@@ -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_ACTIVE: l->active = value != 0; break;
|
||||||
case SP_LFO_WAVE: l->waveform = (Waveform)(value % WAVE_COUNT); 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_RATE: l->rate = exponential(value, 0.05f, 20.0f); break;
|
||||||
|
case SP_LFO_MODE: l->mode = value ? LFO_RETRIGGER : LFO_FREE; break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,8 +229,12 @@ static void soundParameter(uint8_t value) {
|
|||||||
case SP_MODENV: setEnvelope(&v->modEnv, which, value); break;
|
case SP_MODENV: setEnvelope(&v->modEnv, which, value); break;
|
||||||
case SP_FILTER: setFilter(&v->filter, parameter, value); break;
|
case SP_FILTER: setFilter(&v->filter, parameter, value); break;
|
||||||
case SP_LEVEL_SOURCE:
|
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) {
|
if (parameter == SP_LEVEL_SOURCE) {
|
||||||
v->levelSource = sourceFor(value);
|
v->levelSource = sourceFor(value);
|
||||||
|
} else if (parameter == SP_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 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.
|
// LFOs. Nought is the one that could not be said before - see synth.h.
|
||||||
#define SP_LEVEL_SOURCE 0x50
|
#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
|
// The LFOs belong to the whole device rather than to a channel, so these ignore whichever
|
||||||
// channel is selected.
|
// channel is selected.
|
||||||
#define SP_LFO0 0x60 // 0x60-0x6F and 0x70-0x7F
|
#define SP_LFO0 0x60 // 0x60-0x6F and 0x70-0x7F
|
||||||
@@ -89,6 +100,16 @@
|
|||||||
#define SP_LFO_ACTIVE 0x00
|
#define SP_LFO_ACTIVE 0x00
|
||||||
#define SP_LFO_WAVE 0x01
|
#define SP_LFO_WAVE 0x01
|
||||||
#define SP_LFO_RATE 0x02
|
#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 ----
|
// ---- 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
|
// 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. 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 ----
|
// ---- What was changed ----
|
||||||
//
|
//
|
||||||
// 1. A VOICE'S LEVEL IS A ROUTING. Envelope 0 multiplied the output and there was no way to
|
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
||||||
// say otherwise, so routing it to a filter or an oscillator meant it shaped the volume as
|
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||||
// well whether that was wanted or not - which is most of the trouble with making
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||||
// 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
|
// It did not start that way. Three changes were made here first - a routed voice level, a
|
||||||
// with the whole process and varies between libraries - so the same program would sound
|
// seeded noise generator, and channels asked for by number - and all three went up. What came
|
||||||
// different on different machines and every recorded result would be worthless. It is a
|
// back was those three plus what they made possible: a voice that can end itself rather than
|
||||||
// generator inside the Synth now, and a machine that starts the same way sounds the same
|
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||||
// way.
|
// 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
|
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||||
// round-robin, which is what a keyboard wants. A hardware channel is asked for by number.
|
// other one has to be told.
|
||||||
// The old calls are still here and still do what they did.
|
|
||||||
//
|
//
|
||||||
// Written by Anachronaut
|
// Written by Anachronaut
|
||||||
|
|
||||||
@@ -42,6 +39,30 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#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)
|
void synthInit(Synth *s, float sampleRate)
|
||||||
{
|
{
|
||||||
s->sampleRate = 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[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN);
|
||||||
oscillatorInit(&s->voices[i].oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f);
|
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
|
// 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[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;
|
||||||
|
|
||||||
// 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,
|
envelopeInit(&s->voices[i].ampEnv,
|
||||||
0.005f, // attack
|
0.005f, // attack
|
||||||
0.10f, // decay
|
0.10f, // decay
|
||||||
@@ -92,13 +115,13 @@ void synthInit(Synth *s, float sampleRate)
|
|||||||
s->voices[0].oscillators[0].active = 1;
|
s->voices[0].oscillators[0].active = 1;
|
||||||
|
|
||||||
for (int l = 0; l < LFO_COUNT; l++) {
|
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].rate = 1.0f;
|
||||||
s->lfos[l].waveform = WAVE_SINE;
|
s->lfos[l].waveform = WAVE_SINE;
|
||||||
s->lfos[l].active = 0;
|
s->lfos[l].active = 0;
|
||||||
s->lfos[l].noiseHeld = 0.0f;
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||||
s->lfos[l].noisePhase = 0.0f;
|
// made against.
|
||||||
s->lfos[l].noiseState = 0x2545F491u + (uint32_t)l * 3266489917u;
|
s->lfos[l].mode = LFO_FREE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +130,14 @@ void synthResetPatch(Synth *s)
|
|||||||
Voice *v = &s->voices[0];
|
Voice *v = &s->voices[0];
|
||||||
|
|
||||||
// The level is shaped by envelope 0 unless a patch says otherwise, which is what it
|
// 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;
|
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);
|
oscillatorInit(&v->oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN);
|
||||||
v->oscillators[0].active = 1;
|
v->oscillators[0].active = 1;
|
||||||
for (int m = 0; m < 3; m++) {
|
for (int m = 0; m < 3; m++) {
|
||||||
@@ -139,13 +167,13 @@ 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++) {
|
||||||
s->lfos[l].phase = 0.0f;
|
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
||||||
s->lfos[l].rate = 1.0f;
|
s->lfos[l].rate = 1.0f;
|
||||||
s->lfos[l].waveform = WAVE_SINE;
|
s->lfos[l].waveform = WAVE_SINE;
|
||||||
s->lfos[l].active = 0;
|
s->lfos[l].active = 0;
|
||||||
s->lfos[l].noiseHeld = 0.0f;
|
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||||
s->lfos[l].noisePhase = 0.0f;
|
// made against.
|
||||||
s->lfos[l].noiseState = 0x2545F491u + (uint32_t)l * 3266489917u;
|
s->lfos[l].mode = LFO_FREE;
|
||||||
}
|
}
|
||||||
|
|
||||||
s->volume = 0.8f;
|
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;
|
if (!l->active) return 0.0f;
|
||||||
l->phase += l->rate / sampleRate;
|
st->phase += l->rate / sampleRate;
|
||||||
if (l->phase >= 1.0f) l->phase -= 1.0f;
|
if (st->phase >= 1.0f) st->phase -= 1.0f;
|
||||||
if (l->waveform == WAVE_NOISE) {
|
if (l->waveform == WAVE_NOISE) {
|
||||||
l->noisePhase += l->rate / sampleRate;
|
st->noisePhase += l->rate / sampleRate;
|
||||||
if (l->noisePhase >= 1.0f) {
|
if (st->noisePhase >= 1.0f) {
|
||||||
l->noisePhase -= 1.0f;
|
st->noisePhase -= 1.0f;
|
||||||
l->noiseHeld = nextNoise(&l->noiseState);
|
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)
|
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)
|
void synthNoteOn(Synth *s, int midiNote)
|
||||||
{
|
{
|
||||||
float hz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f);
|
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].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;
|
||||||
envelopeNoteOn(&s->voices[i].ampEnv);
|
voiceArm(s, &s->voices[i]);
|
||||||
envelopeNoteOn(&s->voices[i].modEnv);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,29 +425,28 @@ 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;
|
||||||
envelopeNoteOn(&s->voices[i].ampEnv);
|
voiceArm(s, &s->voices[i]);
|
||||||
envelopeNoteOn(&s->voices[i].modEnv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void synthNoteOff(Synth *s, int midiNote) {
|
void synthNoteOff(Synth *s, int midiNote) {
|
||||||
for (int i = 0; i < VOICE_COUNT; i++) {
|
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||||
if (s->voices[i].active && s->voices[i].midiNote == midiNote) {
|
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].ampEnv);
|
||||||
envelopeNoteOff(&s->voices[i].modEnv);
|
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:
|
// Channel two is channel two, it holds its patch between notes, and a program driving this as
|
||||||
// eight fingers and no say in which voice serves which. A hardware channel is not like that.
|
// hardware can rely on both. synthNoteOn above steals a voice round-robin, which is right for
|
||||||
// Channel two is channel two, it holds its patch between notes, and a program driving it
|
// a keyboard and wrong for anything addressing a fixed set of parts.
|
||||||
// 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)
|
void synthChannelOn(Synth *s, int channel, int midiNote)
|
||||||
{
|
{
|
||||||
if (channel < 0 || channel >= VOICE_COUNT) {
|
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.
|
// 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;
|
||||||
envelopeNoteOn(&v->ampEnv);
|
voiceArm(s, v);
|
||||||
envelopeNoteOn(&v->modEnv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void synthChannelOff(Synth *s, int channel)
|
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++) {
|
for (int i = 0; i < frames; i++) {
|
||||||
float mix = 0.0f;
|
float mix = 0.0f;
|
||||||
|
|
||||||
float lfo0 = lfoTick(&s->lfos[0], sr);
|
// The shared cycle advances once per sample whatever is listening, so a free LFO is
|
||||||
float lfo1 = lfoTick(&s->lfos[1], sr);
|
// 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];
|
||||||
@@ -423,11 +493,24 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
|||||||
float amp = envelopeTick(&vv->ampEnv, sr);
|
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||||
float mod = envelopeTick(&vv->modEnv, 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;
|
vv->active = 0;
|
||||||
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++) {
|
||||||
@@ -491,6 +574,7 @@ void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLev
|
|||||||
e->decaySec = decaySec;
|
e->decaySec = decaySec;
|
||||||
e->sustainLevel = sustainLevel;
|
e->sustainLevel = sustainLevel;
|
||||||
e->releaseSec = releaseSec;
|
e->releaseSec = releaseSec;
|
||||||
|
e->oneShot = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void envelopeNoteOn(Envelope *e)
|
void envelopeNoteOn(Envelope *e)
|
||||||
@@ -521,9 +605,13 @@ float envelopeTick(Envelope *e, float sampleRate)
|
|||||||
case ENV_DECAY: {
|
case ENV_DECAY: {
|
||||||
float inc = (e->decaySec <= 0.0f) ? 1.0f : (1.0f / (e->decaySec * sampleRate));
|
float inc = (e->decaySec <= 0.0f) ? 1.0f : (1.0f / (e->decaySec * sampleRate));
|
||||||
e->value -= inc;
|
e->value -= inc;
|
||||||
if (e->value <= e->sustainLevel) {
|
// Sustain is where the decay stops and waits for the key. A one-shot has no key
|
||||||
e->value = e->sustainLevel;
|
// to wait for, so it decays the whole way and is finished - and MUST, or a patch
|
||||||
e->stage = ENV_SUSTAIN;
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -549,6 +637,12 @@ float envelopeTick(Envelope *e, float sampleRate)
|
|||||||
void synthSyncVoices(Synth *s)
|
void synthSyncVoices(Synth *s)
|
||||||
{
|
{
|
||||||
for (int v = 1; v < VOICE_COUNT; v++) {
|
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
|
// 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;
|
||||||
|
|||||||
+70
-24
@@ -10,25 +10,22 @@
|
|||||||
//
|
//
|
||||||
// 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. 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 ----
|
// ---- What was changed ----
|
||||||
//
|
//
|
||||||
// 1. A VOICE'S LEVEL IS A ROUTING. Envelope 0 multiplied the output and there was no way to
|
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
||||||
// say otherwise, so routing it to a filter or an oscillator meant it shaped the volume as
|
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||||
// well whether that was wanted or not - which is most of the trouble with making
|
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||||
// 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
|
// It did not start that way. Three changes were made here first - a routed voice level, a
|
||||||
// with the whole process and varies between libraries - so the same program would sound
|
// seeded noise generator, and channels asked for by number - and all three went up. What came
|
||||||
// different on different machines and every recorded result would be worthless. It is a
|
// back was those three plus what they made possible: a voice that can end itself rather than
|
||||||
// generator inside the Synth now, and a machine that starts the same way sounds the same
|
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||||
// way.
|
// 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
|
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||||
// round-robin, which is what a keyboard wants. A hardware channel is asked for by number.
|
// other one has to be told.
|
||||||
// The old calls are still here and still do what they did.
|
|
||||||
//
|
//
|
||||||
// Written by Anachronaut
|
// Written by Anachronaut
|
||||||
|
|
||||||
@@ -69,6 +66,17 @@ typedef enum {
|
|||||||
MOD_SOURCE_LFO2 = 4
|
MOD_SOURCE_LFO2 = 4
|
||||||
} ModSource;
|
} 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 {
|
typedef struct {
|
||||||
EnvStage stage;
|
EnvStage stage;
|
||||||
float value; // current output value, 0.0 to 1.0
|
float value; // current output value, 0.0 to 1.0
|
||||||
@@ -76,20 +84,46 @@ typedef struct {
|
|||||||
float decaySec;
|
float decaySec;
|
||||||
float sustainLevel;
|
float sustainLevel;
|
||||||
float releaseSec;
|
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;
|
} 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 {
|
typedef struct {
|
||||||
float phase;
|
float phase;
|
||||||
float rate; // Hz
|
|
||||||
Waveform waveform;
|
|
||||||
int active;
|
|
||||||
float noiseHeld;
|
float noiseHeld;
|
||||||
float noisePhase;
|
float noisePhase;
|
||||||
// Its own noise, seeded at init. rand() is global state shared with the whole process and
|
// 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,
|
// 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.
|
// because two noise sources drawing from the same stream are not two noise sources.
|
||||||
uint32_t noiseState;
|
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;
|
} LFO;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -119,11 +153,10 @@ typedef struct {
|
|||||||
float noiseHeld; // last drawn random value for clocked noise
|
float noiseHeld; // last drawn random value for clocked noise
|
||||||
float noisePhase; // tracks when to draw a new noise value
|
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
|
// 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,
|
// 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.
|
// because two noise sources drawing from the same stream are not two noise sources.
|
||||||
uint32_t noiseState;
|
uint32_t noiseState;
|
||||||
|
|
||||||
float gain;
|
float gain;
|
||||||
int active; // whether this oscillator contributes to output
|
int active; // whether this oscillator contributes to output
|
||||||
int octave; // transposition in octaves, -2 to +2
|
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
|
// It also makes two things possible that were not: envelope 1 shaping the volume, and an
|
||||||
// LFO doing it, which is tremolo.
|
// LFO doing it, which is tremolo.
|
||||||
ModSource levelSource;
|
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;
|
} Voice;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -184,11 +227,12 @@ void synthResetPatch(Synth *s);
|
|||||||
void synthNoteOn(Synth *s, int midiNote);
|
void synthNoteOn(Synth *s, int midiNote);
|
||||||
void synthNoteOff(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
|
// 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
|
// a player presses keys and does not care which voice sounds them. Something driving this as
|
||||||
// the third one, and it is the third one every time.
|
// 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 synthChannelOn(Synth *s, int channel, int midiNote);
|
||||||
void synthChannelOff(Synth *s, int channel);
|
void synthChannelOff(Synth *s, int channel);
|
||||||
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
||||||
@@ -196,6 +240,8 @@ void synthSyncVoices(Synth *s);
|
|||||||
|
|
||||||
// LFO functions:
|
// LFO functions:
|
||||||
float lfoTick(LFO *l, float sampleRate);
|
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:
|
// Filter functions:
|
||||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate);
|
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_active", 0x60, AS_BOOL, 0, 0, "LFO 0, on" },
|
||||||
{ "lfo0_waveform", 0x61, AS_DIRECT, 0, 0, "waveform" },
|
{ "lfo0_waveform", 0x61, AS_DIRECT, 0, 0, "waveform" },
|
||||||
{ "lfo0_rate", 0x62, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" },
|
{ "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_active", 0x70, AS_BOOL, 0, 0, "LFO 1, on" },
|
||||||
{ "lfo1_waveform", 0x71, AS_DIRECT, 0, 0, "waveform" },
|
{ "lfo1_waveform", 0x71, AS_DIRECT, 0, 0, "waveform" },
|
||||||
{ "lfo1_rate", 0x72, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" },
|
{ "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])))
|
#define MAPPING_COUNT ((int)(sizeof(mappings) / sizeof(mappings[0])))
|
||||||
|
|
||||||
@@ -194,12 +204,23 @@ int main(int argc, char **argv) {
|
|||||||
return 1;
|
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;
|
int count = 0;
|
||||||
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
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;
|
FILE *out = stdout;
|
||||||
if (argc > 3) {
|
if (argc > 3) {
|
||||||
@@ -215,11 +236,11 @@ int main(int argc, char **argv) {
|
|||||||
fprintf(out, "#Data\n\n");
|
fprintf(out, "#Data\n\n");
|
||||||
fprintf(out, "%s:\n", label);
|
fprintf(out, "%s:\n", label);
|
||||||
fprintf(out, " 0d%-24d; how many pairs follow\n", count);
|
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++) {
|
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||||
if (!seen[i]) continue;
|
if (!seen[i]) continue;
|
||||||
fprintf(out, " 0x%02X 0d%-3d ; %s\n",
|
fprintf(out, " 0x%02X 0d%-3d ; %s%s\n",
|
||||||
mappings[i].parameter, values[i], mappings[i].note);
|
mappings[i].parameter, values[i], mappings[i].note,
|
||||||
|
(i == levelIndex && assumedLevel) ? ", which this patch predates" : "");
|
||||||
}
|
}
|
||||||
if (out != stdout) fclose(out);
|
if (out != stdout) fclose(out);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1026,6 +1026,7 @@ The high nibble says which part of the voice, the low nibble which setting of it
|
|||||||
| 0x30 - 0x3F | The modulation envelope. |
|
| 0x30 - 0x3F | The modulation envelope. |
|
||||||
| 0x40 - 0x4F | The filter. |
|
| 0x40 - 0x4F | The filter. |
|
||||||
| 0x50 | What shapes the channel's level. |
|
| 0x50 | What shapes the channel's level. |
|
||||||
|
| 0x51 | Whether a note waits to be let go of. |
|
||||||
| 0x60 - 0x6F | LFO 0. |
|
| 0x60 - 0x6F | LFO 0. |
|
||||||
| 0x70 - 0x7F | LFO 1. |
|
| 0x70 - 0x7F | LFO 1. |
|
||||||
|
|
||||||
@@ -1062,6 +1063,7 @@ The high nibble says which part of the voice, the low nibble which setting of it
|
|||||||
| 0 | On, or off at zero. |
|
| 0 | On, or off at zero. |
|
||||||
| 1 | Waveform, from the same six. |
|
| 1 | Waveform, from the same six. |
|
||||||
| 2 | Rate. |
|
| 2 | Rate. |
|
||||||
|
| 3 | 0 free, 1 starts over with every voice. See The Same Sound Twice. |
|
||||||
|
|
||||||
Anywhere a setting asks *what modulates* something, the answer is one of these:
|
Anywhere a setting asks *what modulates* something, the answer is one of these:
|
||||||
|
|
||||||
@@ -1107,6 +1109,45 @@ output, so an envelope routed somewhere useful - opening the filter, bending a p
|
|||||||
has to be shaped like something you would want to hear, and a snare that wants a click of
|
has to be shaped like something you would want to hear, and a snare that wants a click of
|
||||||
filter sweep and a flat body cannot have both.
|
filter sweep and a flat body cannot have both.
|
||||||
|
|
||||||
|
### Struck Or Held:
|
||||||
|
|
||||||
|
Setting 0x51 says whether a note waits to be let go of: **0 gated**, which is how it has always
|
||||||
|
been, and **1 triggered**.
|
||||||
|
|
||||||
|
A gated voice lasts as long as something keeps hold of it. Writing the gate port ends it, and
|
||||||
|
the release begins there. That is what a keyboard is, and it is right for anything a player
|
||||||
|
holds down.
|
||||||
|
|
||||||
|
A triggered voice is struck and then plays its own length. Nothing has to remember to end it,
|
||||||
|
and the gate port need never be written at all. Sustain and release have no meaning in one,
|
||||||
|
because both of them are answers to a question about a key that is not being asked - so in a
|
||||||
|
triggered voice the decay runs to nothing rather than stopping at the sustain level, or a
|
||||||
|
patch with any sustain at all would hold the voice open for ever.
|
||||||
|
|
||||||
|
**A game is nearly all one-shots.** A bang, a pickup, a door: not one of them wants its length
|
||||||
|
decided by how long a note was held, and every one of them would otherwise need a program to
|
||||||
|
come back later and let go of it.
|
||||||
|
|
||||||
|
### The Same Sound Twice:
|
||||||
|
|
||||||
|
Setting 3 of either LFO says whether it starts over when a voice does: **0 free**, which is how
|
||||||
|
it has always been, and **1 retriggered**.
|
||||||
|
|
||||||
|
A free LFO is one cycle running under everything, which is what vibrato across a held chord
|
||||||
|
wants. A retriggered one starts at the beginning of its shape every time a voice begins, and
|
||||||
|
the cycle belongs to the voice rather than to the device - so retriggering costs nothing to a
|
||||||
|
channel not using it.
|
||||||
|
|
||||||
|
This is the other half of a repeatable sound effect, and **neither half is sufficient alone**.
|
||||||
|
A triggered voice re-arms its oscillators, so a hit begins at the same point in its waveform
|
||||||
|
every time and a noise source draws the same noise. But an LFO left free is wherever the wall
|
||||||
|
clock happened to leave it, so the same drum caught at a different moment is still a different
|
||||||
|
drum. Set both and a one-shot is the same one-shot, sample for sample.
|
||||||
|
|
||||||
|
The consequence is worth knowing rather than fixing: a retriggered noise source is *bit*
|
||||||
|
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.
|
||||||
|
|
||||||
### 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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
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 8412
|
Lander.sbx 8409
|
||||||
Pad.sbx 264
|
Pad.sbx 264
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
vars.script 50
|
vars.script 50
|
||||||
|
|||||||
@@ -409,6 +409,93 @@ LATE="$(measure levelEnvelope peak 36000)"
|
|||||||
&& result ok "and by the envelope, which is the normal case" "faded to $LATE" \
|
&& result ok "and by the envelope, which is the normal case" "faded to $LATE" \
|
||||||
|| result no "and by the envelope, which is the normal case" "still $LATE, it never faded"
|
|| result no "and by the envelope, which is the normal case" "still $LATE, it never faded"
|
||||||
|
|
||||||
|
# ---- A triggered voice ends itself ----
|
||||||
|
#
|
||||||
|
# Gated is what a keyboard wants: the sound lasts as long as something holds it, and dropping
|
||||||
|
# the gate is what starts the release. A GAME is nearly all one-shots - a bang, a pickup, a
|
||||||
|
# door - and not one of them wants its length decided by how long a note was held.
|
||||||
|
#
|
||||||
|
# Neither program below ever drops the gate. The gated one is still sounding at the end of it,
|
||||||
|
# because a sustain above nothing is a voice waiting for a key that is never coming; the
|
||||||
|
# triggered one runs its decay to nothing and finishes.
|
||||||
|
for gate in 0 1; do
|
||||||
|
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
|
||||||
|
param 0x51 $gate # gated or triggered
|
||||||
|
param 0x20 0x00 # attack: instant
|
||||||
|
param 0x21 60 # decay: short
|
||||||
|
param 0x22 128 # sustain: half, which a gated voice will hold at
|
||||||
|
port 0x44 60; spinForever
|
||||||
|
printf '#Vectors\n Boot start\n'; } | run gate$gate 3000000 || exit 1
|
||||||
|
done
|
||||||
|
HELD="$(measure gate0 peak 96000)"
|
||||||
|
STRUCK="$(measure gate1 peak 96000)"
|
||||||
|
[ "$HELD" -gt 1000 ] \
|
||||||
|
&& result ok "a gated voice waits to be let go of" "still $HELD with nothing holding it" \
|
||||||
|
|| result no "a gated voice waits to be let go of" "faded to $HELD on its own"
|
||||||
|
[ "$STRUCK" -lt 100 ] \
|
||||||
|
&& result ok "and a triggered one ends itself" "down to $STRUCK, with no gate ever dropped" \
|
||||||
|
|| result no "and a triggered one ends itself" "still $STRUCK, so it is waiting for a key"
|
||||||
|
|
||||||
|
# ---- And it is the same one-shot twice ----
|
||||||
|
#
|
||||||
|
# The point of the whole thing. A triggered voice re-arms its oscillators, so a hit begins in
|
||||||
|
# the same place every time; a free LFO does not, and reading it at a different moment makes
|
||||||
|
# the same drum a different drum. Both halves are needed, which is why this asks for the LFO
|
||||||
|
# to restart as well.
|
||||||
|
#
|
||||||
|
# Two hits, compared SAMPLE FOR SAMPLE. Noise is the hard case and the reason the generator is
|
||||||
|
# seeded: with rand() this could not have been asked at all.
|
||||||
|
#
|
||||||
|
# THE GAP HAS TO OUTLAST THE DECAY. The first version 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. A short decay and a quarter second between them, and the two
|
||||||
|
# are found by looking for sound after silence rather than for sound after an offset.
|
||||||
|
{ printf '#Program\nstart:\n'; port 0x41 0x00; loud
|
||||||
|
param 0x00 5 # noise
|
||||||
|
param 0x51 0x01 # triggered
|
||||||
|
param 0x20 0x00; param 0x21 40; param 0x22 0x00
|
||||||
|
param 0x60 0x01 # LFO 0 on,
|
||||||
|
param 0x62 40 # a slow rate,
|
||||||
|
param 0x63 0x01 # and it starts over with each voice
|
||||||
|
param 0x44 0x03 # which opens the filter
|
||||||
|
param 0x40 0x01; param 0x42 60; param 0x45 200
|
||||||
|
port 0x44 60; pause one 250
|
||||||
|
port 0x44 60; spinForever
|
||||||
|
printf '#Vectors\n Boot start\n'; } | run twice 4000000 || exit 1
|
||||||
|
SAME="$(python3 - "$BUILD/twice.raw" <<'PY'
|
||||||
|
import struct, sys
|
||||||
|
data = open(sys.argv[1], "rb").read()
|
||||||
|
v = struct.unpack("<%dh" % (len(data) // 2), data)
|
||||||
|
# Find the two hits by where sound starts, then compare the same span of each.
|
||||||
|
def onsets(v, floor=200, quiet=2000):
|
||||||
|
# Sound after silence. A run of quiet samples has to separate two hits, or what is found
|
||||||
|
# is one hit and a place in the middle of its own tail.
|
||||||
|
at, run, sounding = [], 0, False
|
||||||
|
for i, x in enumerate(v):
|
||||||
|
if abs(x) > floor:
|
||||||
|
if not sounding:
|
||||||
|
at.append(i)
|
||||||
|
sounding = True
|
||||||
|
run = 0
|
||||||
|
elif sounding:
|
||||||
|
run += 1
|
||||||
|
if run >= quiet:
|
||||||
|
sounding = False
|
||||||
|
return at
|
||||||
|
hits = onsets(v)
|
||||||
|
if len(hits) < 2:
|
||||||
|
print("onehit" if hits else "silent"); raise SystemExit
|
||||||
|
first, second = hits[0], hits[1]
|
||||||
|
span = min(3000, len(v) - second)
|
||||||
|
a = v[first:first + span]
|
||||||
|
b = v[second:second + span]
|
||||||
|
print("same" if a == b else "differ")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
[ "$SAME" = "same" ] \
|
||||||
|
&& 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"
|
||||||
|
|
||||||
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