instrument: snap live params onto a fresh voice, roll a live drag back on capture loss, serialize the seqlock's two writers

This commit is contained in:
2026-07-30 21:39:56 -04:00
parent 1dade0bfcf
commit bbc7dc70bb
15 changed files with 621 additions and 143 deletions
+27 -7
View File
@@ -77,15 +77,35 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode);
// controls, so this is a routing decision at the editor's commit site rather than a property
// of any one knob; moving a control across the line is a change here and nowhere else.
//
// Deliberately NOT live, each for its own reason: the discrete toggles (play mode, pitch
// engine, filter enable/law, pitch-envelope enable) name a different sound rather than a
// different setting of one; the three capture-anchored overrides (root, loop span, start
// frame) name positions in the decoded PCM; pitch key-track and the velocity->cutoff depth
// feed values a voice latches at note-on by design (the pitch ratio and the velocity-curve
// result), so making them live would retune or re-gain a note already struck; and Trigger's
// %-length and fades resolve a play span that is a fact about the note.
// THE home for why each excluded control is excluded. Five continuous controls are outside the
// live set, plus every discrete toggle:
// - the discrete toggles (play mode, pitch engine, filter enable/law, pitch-envelope enable)
// name a different sound rather than a different setting of one;
// - the three capture-anchored overrides (root, loop span, start frame) name positions in
// the decoded PCM;
// - kKeyTrack and kFilterVel feed values a voice latches at note-on by design (the pitch
// ratio and the velocity-curve result), so live delivery would retune or re-gain a note
// already struck;
// - kTrigLength resolves playEnd_, a fact about the note. kTrigFadeIn/kTrigFadeOut are pure
// amplitude shape and would be live-able in principle, but they live in `sample.play` and
// are baked into SampleData at build time — the engine rebuild copies that verbatim, so
// only a reload can deliver them without widening LiveValues. They fold into the AHD
// alongside Gate's, at which point they inherit its routing; until then they reload.
// Consequence, stated plainly: a Trigger-mode instance gets NO live delivery on its amplitude
// controls. Only the filter and pitch-envelope knobs move a sounding Trigger one-shot.
bool isLiveDeckParam(DeckParam id);
// The editor drag kinds that can commit live, in this pure module's own vocabulary (the
// shell's DragKind maps onto it) so the WHOLE routing decision — not just the predicate — is
// testable without a host.
enum class LiveDragKind { kOther, kDeckKnob, kEnvNode };
// Whether a drag of `kind` commits live. A deck knob is live per isLiveDeckParam (negative ids
// are the shell's processor-side sentinels and out-of-range ids are not controls, so neither
// reaches the enum); an envelope-node drag is live only in Gate, where it edits the AHDSR —
// in Trigger the same drag rewrites the play span, which is not a live control.
bool liveCommitFor(LiveDragKind kind, int paramId, PlayMode playMode);
// The deck's BIPOLAR knob law: 0.5 of the knob's travel is zero depth, the ends are -1 and
// +1. Exact inverses, and exact at the centre detent (0.5 -> 0 -> 0.5), so a knob parked at
// centre can never persist a hair of modulation. Out-of-range norm clamps to the endpoints.