fix(voice): CC 120 hard-stops voices (incl. Trigger); Mono sizes voices_ to 1; comments corrected

CC 120 -> allSoundsOff/hardStop (immediate silence, stops Trigger one-shots); CC 123 -> allNotesOff/releaseAll (release, unchanged). Mono VoiceEngine sizes voices_ to 1 structurally. Legato same-note re-press edge documented. Three new tests.
This commit is contained in:
2026-07-27 22:08:56 -04:00
parent cc64def2d0
commit 080a7be8ca
4 changed files with 146 additions and 37 deletions
+20 -7
View File
@@ -428,6 +428,11 @@ public:
// TRIGGER mode it is a NO-OP (Trigger ignores note-off and plays through to its play length).
void release();
// HARD STOP — CC 120 (All Sounds Off) semantics. Immediately silences this voice regardless
// of play mode: sets active_ = false with no release ramp. Stops a ringing Trigger one-shot
// instantly (which release() cannot do). RT-safe: no allocation, no lock.
void hardStop();
// True while this voice is producing (or about to produce) sound.
bool active() const { return active_; }
// The note this voice was started on (for note-off routing). Meaningless if idle.
@@ -568,14 +573,19 @@ public:
// the older tail to ring — matches hardware behavior). No-op if none match.
void noteOff(int note);
// PANIC / MIDI All-Notes-Off (CC 123). Clears the MONO held stack and releases EVERY
// active voice: Gate voices enter their release tail; Trigger one-shots ignore release
// by design and play through their bounded play length (they can never be stuck). This
// is the mono stack's ONLY reset path — a phantom entry left by a lost note-off would
// otherwise be resurrected by the next fallback and sustain forever with no key held.
// RT-safe (no allocation, bounded by maxVoices); callable from the audio thread.
// CC 123 — MIDI All-Notes-Off: clears the MONO held stack and RELEASES every active voice
// (Gate voices enter their AHDSR release tail; Trigger one-shots ignore release and play
// through their bounded play length). This is the mono stack's ONLY reset path — a phantom
// entry left by a lost note-off would otherwise be resurrected by the fallback and sustain
// forever with no key held. RT-safe (no allocation, bounded by maxVoices).
void allNotesOff();
// CC 120 — MIDI All-Sounds-Off: hard-stops EVERY voice immediately (active_ = false, no
// release ramp), clears the MONO held stack, and silences even Trigger one-shots that would
// ignore a release. Use for panic; CC 123 for the softer "let gates release" behavior.
// RT-safe (no allocation, bounded by maxVoices); callable from the audio thread.
void allSoundsOff();
// REAL-TIME render (S4): sums all active voices into the caller-provided buffer
// `out[0..frameCount)`, ADDING to whatever is there (the caller clears or mixes —
// this never touches memory it does not own and NEVER allocates). This is the
@@ -680,9 +690,12 @@ public:
// Release the preview IF `note` is the one sounding (a stale off for a replaced note is a
// no-op). Gate zones enter release; Trigger zones ignore note-off and play through.
void noteOff(int note);
// PANIC peer of VoiceEngine::allNotesOff: release the ringing preview UNCONDITIONALLY,
// CC 123 peer of VoiceEngine::allNotesOff: release the ringing preview UNCONDITIONALLY,
// whatever note it is on. Gate enters release; Trigger plays through (bounded). RT-safe.
void releaseAll();
// CC 120 peer of VoiceEngine::allSoundsOff: HARD-STOP the preview immediately (no release
// ramp, silences Trigger one-shots too). RT-safe.
void hardStop();
bool active() const { return voice_.active(); }