fix(voice): Trigger legato keys on held-stack depth, CC123 allNotesOff clears mono stack, voice-param edits rebuild from decoded PCM (no re-decode), mono sizes 1 shifter ring

This commit is contained in:
2026-07-27 21:54:03 -04:00
parent 885b7f29a2
commit cc64def2d0
5 changed files with 274 additions and 21 deletions
+21 -2
View File
@@ -200,8 +200,9 @@ public:
// --- Phase S voice-system parameters (per-instance, persisted in component state v7) ---
// Read/written on the UI thread (the editor's voice deck) and by getState/setState; guarded
// by voiceParamsMutex_. NOT read on the audio thread — each setter rebuilds the VoiceEngine
// OFF-thread through reloadFromBank's drain-slot swap, so changing polyphony / mode / the
// retrigger toggle never cuts a ringing tail (the same path FA1 added for curve edits).
// OFF-thread via rebuildVoiceEngine (a LIGHT rebuild around the already-decoded keymap; no
// bridge read, no WAV re-decode) published through the same tail-preserving drain-slot swap,
// so changing polyphony / mode / the retrigger toggle never cuts a ringing tail.
int voiceCount();
void setVoiceCount(int count); // clamped to kMinVoiceCount..kMaxVoiceCount
VoiceMode voiceMode();
@@ -240,6 +241,24 @@ private:
// proof (see below) covers the free.
void retireIdleDrain();
// Phase S voice-param LIGHT rebuild (voice-review Major #3): rebuild the engine + preview
// card around a COPY of the LIVE instrument's already-decoded Keymap — no bridge read, no
// filesystem, no WAV re-decode — and publish through the same tail-preserving drain-slot
// swap as a full reload. A polyphony/mode/trigger change touches no audio data, so the
// full reloadFromBank (which re-decodes every zone WAV from disk on the UI thread) was
// pure waste — a visible UI stall on a many-zone instrument. Copying the keymap is safe:
// it is immutable after construction and, under reloadMutex_, the live instrument can
// neither be swapped nor freed while we read it. When nothing is loaded this is a no-op —
// the new params bake into the next real reload. Off the audio thread only.
void rebuildVoiceEngine();
// Publish `built` (null = install silence) into live_: prune the graveyard by the last
// process()-published generation, swap `built` into live_, displace the previous live into
// the drain slot, and park the drain-evicted instrument in the graveyard. REQUIRES
// reloadMutex_ held — factored out so reloadFromBank and rebuildVoiceEngine share the ONE
// safety-critical swap dance (see the handoff proof below).
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
ReaperBridge bridge_;
// --- The audio-thread handoff (S4 real-time discipline, FA1 drain slot) --