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:
+70
-24
@@ -10,25 +10,22 @@
|
||||
//
|
||||
// It is copied rather than submoduled. Two files against tying this build to another
|
||||
// repository's history is not a close call, and what a copy costs is that changes have to be
|
||||
// carried across on purpose. So the list below is the whole of the difference, kept current.
|
||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
||||
//
|
||||
// ---- What was changed ----
|
||||
//
|
||||
// 1. A VOICE'S LEVEL IS A ROUTING. Envelope 0 multiplied the output and there was no way to
|
||||
// say otherwise, so routing it to a filter or an oscillator meant it shaped the volume as
|
||||
// well whether that was wanted or not - which is most of the trouble with making
|
||||
// percussion. Every other destination in this synth chooses its source; now this one does
|
||||
// too, and MOD_SOURCE_NONE means the level is simply full.
|
||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
||||
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||
//
|
||||
// 2. NOISE COMES FROM A SEEDED GENERATOR. It drew from rand(), which is global state shared
|
||||
// with the whole process and varies between libraries - so the same program would sound
|
||||
// different on different machines and every recorded result would be worthless. It is a
|
||||
// generator inside the Synth now, and a machine that starts the same way sounds the same
|
||||
// way.
|
||||
// It did not start that way. Three changes were made here first - a routed voice level, a
|
||||
// seeded noise generator, and channels asked for by number - and all three went up. What came
|
||||
// back was those three plus what they made possible: a voice that can end itself rather than
|
||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that way.
|
||||
//
|
||||
// 3. CHANNELS ARE NAMED, NOT ALLOCATED. synthNoteOn hunts for a free voice and steals
|
||||
// round-robin, which is what a keyboard wants. A hardware channel is asked for by number.
|
||||
// The old calls are still here and still do what they did.
|
||||
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||
// other one has to be told.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
@@ -69,6 +66,17 @@ typedef enum {
|
||||
MOD_SOURCE_LFO2 = 4
|
||||
} ModSource;
|
||||
|
||||
// ---- Whether the key holds the note up ----
|
||||
//
|
||||
// Gated is what a keyboard wants: the sound lasts as long as the finger does, and lifting it
|
||||
// starts the release. Triggered is what a drum wants - the note is struck and then plays its
|
||||
// own length, and the key coming up is not its business. Sustain and release have no meaning
|
||||
// in a triggered voice, because both of them are answers to a question about the key.
|
||||
typedef enum {
|
||||
VOICE_GATE = 0,
|
||||
VOICE_TRIGGER = 1
|
||||
} VoiceGate;
|
||||
|
||||
typedef struct {
|
||||
EnvStage stage;
|
||||
float value; // current output value, 0.0 to 1.0
|
||||
@@ -76,20 +84,46 @@ typedef struct {
|
||||
float decaySec;
|
||||
float sustainLevel;
|
||||
float releaseSec;
|
||||
// Set from the voice's gate at note-on, not read from it live, so that flipping the
|
||||
// switch under a sounding note cannot strand it half-way through a shape it was not
|
||||
// started in. One-shot runs the decay to nothing and finishes there.
|
||||
int oneShot;
|
||||
} Envelope;
|
||||
|
||||
// ---- Where an LFO is in its cycle ----
|
||||
//
|
||||
// Split out of the LFO itself because there is now more than one answer at a time. A free
|
||||
// LFO has one cycle that every voice reads, which is what makes it a single sweep across a
|
||||
// chord. A retriggered one has a cycle PER VOICE, restarted when that voice is struck - and
|
||||
// a voice resetting the shared one would drag every note already sounding along with it.
|
||||
typedef struct {
|
||||
float phase;
|
||||
float rate; // Hz
|
||||
Waveform waveform;
|
||||
int active;
|
||||
float noiseHeld;
|
||||
float noisePhase;
|
||||
// Its own noise, seeded at init. rand() is global state shared with the whole process and
|
||||
// varies between C libraries, so the same program sounded different on different machines
|
||||
// varies between C libraries, so the same patch sounded different on different machines
|
||||
// and no recorded result could mean anything. One generator EACH rather than one shared,
|
||||
// because two noise sources drawing from the same stream are not two noise sources.
|
||||
uint32_t noiseState;
|
||||
} LfoState;
|
||||
|
||||
// ---- Whether an LFO keeps its own time or starts when struck ----
|
||||
//
|
||||
// Free is one cycle running under everything, which is what vibrato across a held chord
|
||||
// wants. Retriggered starts at the beginning of its shape every time a voice begins, which
|
||||
// is the only way a one-shot sounds the same twice - a free LFO is wherever the wall clock
|
||||
// left it, so the same drum caught at a different moment is a different drum.
|
||||
typedef enum {
|
||||
LFO_FREE = 0,
|
||||
LFO_RETRIGGER = 1
|
||||
} LfoMode;
|
||||
|
||||
typedef struct {
|
||||
LfoState run; // the free-running cycle, read by every voice in LFO_FREE
|
||||
float rate; // Hz
|
||||
Waveform waveform;
|
||||
int active;
|
||||
LfoMode mode;
|
||||
} LFO;
|
||||
|
||||
typedef enum {
|
||||
@@ -119,11 +153,10 @@ typedef struct {
|
||||
float noiseHeld; // last drawn random value for clocked noise
|
||||
float noisePhase; // tracks when to draw a new noise value
|
||||
// Its own noise, seeded at init. rand() is global state shared with the whole process and
|
||||
// varies between C libraries, so the same program sounded different on different machines
|
||||
// varies between C libraries, so the same patch sounded different on different machines
|
||||
// and no recorded result could mean anything. One generator EACH rather than one shared,
|
||||
// because two noise sources drawing from the same stream are not two noise sources.
|
||||
uint32_t noiseState;
|
||||
|
||||
float gain;
|
||||
int active; // whether this oscillator contributes to output
|
||||
int octave; // transposition in octaves, -2 to +2
|
||||
@@ -155,6 +188,16 @@ typedef struct {
|
||||
// It also makes two things possible that were not: envelope 1 shaping the volume, and an
|
||||
// LFO doing it, which is tremolo.
|
||||
ModSource levelSource;
|
||||
// ---- Whether the key holds this voice up ----
|
||||
//
|
||||
// The other half of what envelope 0 used to decide on its own. Naming the level's source
|
||||
// said what shapes the sound; this says who ends it. Without it a voice can only ever
|
||||
// finish because a key came up, which is no use to a drum.
|
||||
VoiceGate gate;
|
||||
// One cycle per LFO per voice, used only by the LFOs set to LFO_RETRIGGER. Runtime state
|
||||
// like the envelope stages and the filter's integrators, so synthSyncVoices leaves it
|
||||
// alone - it is where this voice is, not what the patch says.
|
||||
LfoState lfoRun[LFO_COUNT];
|
||||
} Voice;
|
||||
|
||||
typedef struct {
|
||||
@@ -184,11 +227,12 @@ void synthResetPatch(Synth *s);
|
||||
void synthNoteOn(Synth *s, int midiNote);
|
||||
void synthNoteOff(Synth *s, int midiNote);
|
||||
|
||||
// ---- 3. A channel is asked for by number ----
|
||||
// ---- A channel asked for by number ----
|
||||
//
|
||||
// The two above hunt for a free voice and steal round-robin, which is what a keyboard wants
|
||||
// and what the standalone synthesizer still does. A hardware channel is not allocated: it is
|
||||
// the third one, and it is the third one every time.
|
||||
// The two above hunt for a free voice and steal round-robin, which is what a keyboard wants:
|
||||
// a player presses keys and does not care which voice sounds them. Something driving this as
|
||||
// HARDWARE does care - channel two is channel two, it keeps its patch between notes, and
|
||||
// nothing may take it away. Both ways of asking are here and neither changes the other.
|
||||
void synthChannelOn(Synth *s, int channel, int midiNote);
|
||||
void synthChannelOff(Synth *s, int channel);
|
||||
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
||||
@@ -196,6 +240,8 @@ void synthSyncVoices(Synth *s);
|
||||
|
||||
// LFO functions:
|
||||
float lfoTick(LFO *l, float sampleRate);
|
||||
// The same advance against a cycle that is not the LFO's own, so a voice can run its own.
|
||||
float lfoTickState(const LFO *l, LfoState *st, float sampleRate);
|
||||
|
||||
// Filter functions:
|
||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate);
|
||||
|
||||
Reference in New Issue
Block a user