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:
@@ -68,7 +68,8 @@ scattered `#ifdef`s in the VST shell, except the one described below).
|
||||
(`DEF_CLASS2` / `INLINE_UID` / `FUID` from `pluginfactory.h` + `funknown.h`).
|
||||
|
||||
**The three commit tiers (Θ-W3).** An edit reaches the audio by exactly one of three routes, and
|
||||
which route a control takes is decided once, by the pure `isLiveDeckParam` predicate — see
|
||||
which route a control takes is decided once, by the pure `isLiveDeckParam` / `liveCommitFor` pair
|
||||
(`core/instrument/ui/deck_groups`) that the editor's `dragCommitsLive` only maps onto — see
|
||||
`core/instrument/CLAUDE.md`'s "Live parameter delivery" for the rule and its rationale.
|
||||
1. **Full reload** — `reloadInstrument`: bridge read, WAV re-decode, fresh engine, snapshot swap.
|
||||
2. **Engine rebuild** — `rebuildVoiceEngine`: same drain-slot swap around the already-decoded
|
||||
@@ -76,10 +77,9 @@ which route a control takes is decided once, by the pure `isLiveDeckParam` predi
|
||||
3. **Live** — `publishLiveParams` (and `masterGain_`, the original of the shape): a lock-free
|
||||
publish the audio thread observes at block boundaries. No rebuild, no snapshot, no disk.
|
||||
|
||||
`liveParams_` is declared ahead of the instrument slots so it outlives every snapshot pointing at
|
||||
it. The editor's `commitLive` is the tier-3 peer of `commitAndReload` and still writes the
|
||||
parameter set, so persistence is unchanged — `getState` serializes `params_`, never a reload
|
||||
artifact, and the reload was never the persistence trigger.
|
||||
The editor's `commitLive` is the tier-3 peer of `commitAndReload`; why it still writes the
|
||||
parameter set is recorded at its declaration in `reasampler_editor.h`, and why `liveParams_` is
|
||||
declared ahead of the instrument slots at that member in `reasampler_processor.h`.
|
||||
|
||||
**Non-goals / guardrails.**
|
||||
- The instrument never captures and never inserts into the arrange. Playback is a
|
||||
|
||||
@@ -105,7 +105,7 @@ void ReaSamplerEditor::onMouseUp(int x, int y) {
|
||||
return;
|
||||
}
|
||||
// A live control already reached the voices during the drag; its release commits the
|
||||
// final value the same way — no bridge read, no WAV re-decode, no snapshot rebuild.
|
||||
// final value through the same tier.
|
||||
if (dragCommitsLive(kind, paramId)) {
|
||||
commitLive();
|
||||
invalidate();
|
||||
|
||||
@@ -205,8 +205,8 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
case WM_RBUTTONUP:
|
||||
return 0; // claimed so the pair never reaches DefWindowProc (no context menu)
|
||||
case WM_CAPTURECHANGED:
|
||||
// Capture stolen mid-drag (modal dialog, alt-tab, etc.) — restore map_ to its
|
||||
// pre-grab snapshot so the in-flight live-drag mutation is rolled back, then reset
|
||||
// Capture stolen mid-drag (modal dialog, alt-tab, etc.) — restore params_ to its
|
||||
// pre-grab snapshot so the in-flight drag mutation is rolled back, then reset
|
||||
// the drag state machine so stale capture-less WM_MOUSEMOVEs don't keep editing.
|
||||
// Mirror of the panel shell's WM_CAPTURECHANGED handler (panel_window.cpp).
|
||||
if (self) {
|
||||
@@ -219,15 +219,25 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
}
|
||||
if (self->drag_ != DragKind::kNone) {
|
||||
// A scrollbar drag + the processor-side deck knobs (preview velocity -2 /
|
||||
// voice count / master gain) are transient (they mutate no parameter, so
|
||||
// dragStartParams_ is not a rollback target) — reset drag state only.
|
||||
// Every parameter-editing drag rolls its live mutation back to the snapshot.
|
||||
// voice count / master gain) mutate no parameter, so dragStartParams_ is
|
||||
// not a rollback target for them — reset drag state only. Every
|
||||
// parameter-editing drag restores the pre-grab snapshot.
|
||||
const bool transient = self->drag_ == DragKind::kScrollThumb ||
|
||||
(self->drag_ == DragKind::kDeckKnob &&
|
||||
(self->dragParamId_ == -2 ||
|
||||
self->dragParamId_ == static_cast<int>(ParamControl::kVoiceCount) ||
|
||||
self->dragParamId_ == static_cast<int>(ParamControl::kMasterGain)));
|
||||
if (!transient) self->params_ = self->dragStartParams_;
|
||||
if (!transient) {
|
||||
self->params_ = self->dragStartParams_;
|
||||
// A live drag already reached the voices AND the processor's own
|
||||
// parameter set on every move, so restoring params_ alone would leave
|
||||
// the face painting one value while the audio plays — and getState
|
||||
// persists — the abandoned one. Roll back through the same tier the
|
||||
// drag used.
|
||||
if (self->dragCommitsLive(self->drag_, self->dragParamId_)) {
|
||||
self->commitLive();
|
||||
}
|
||||
}
|
||||
self->drag_ = DragKind::kNone;
|
||||
self->dragParamId_ = -1;
|
||||
self->curvePointIndex_ = -1; // curve-node drag state (peer reset)
|
||||
|
||||
@@ -144,13 +144,14 @@ void ReaSamplerEditor::commitLive() {
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::dragCommitsLive(DragKind kind, int paramId) const {
|
||||
if (kind == DragKind::kDeckKnob) {
|
||||
// Negative ids are the processor-side sentinels (preview velocity), not parameter-set
|
||||
// controls, so they never reach the enum.
|
||||
return paramId >= 0 &&
|
||||
instrument::ui::isLiveDeckParam(static_cast<ParamControl>(paramId));
|
||||
}
|
||||
return kind == DragKind::kEnvNode && params_.play.playMode == PlayMode::Gate;
|
||||
// The decision itself is the pure liveCommitFor's; this is only the shell's drag-kind
|
||||
// vocabulary mapped onto it, so the routing is pinned by deck_groups' tests rather than
|
||||
// by inspection of this file.
|
||||
using instrument::ui::LiveDragKind;
|
||||
const LiveDragKind k = kind == DragKind::kDeckKnob ? LiveDragKind::kDeckKnob
|
||||
: kind == DragKind::kEnvNode ? LiveDragKind::kEnvNode
|
||||
: LiveDragKind::kOther;
|
||||
return instrument::ui::liveCommitFor(k, paramId, params_.play.playMode);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::loadSelection(const std::string& id) {
|
||||
|
||||
@@ -153,6 +153,13 @@ void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishLiveParams() {
|
||||
// reloadMutex_ enforces the block's SINGLE-WRITER contract (live_params.h), not the
|
||||
// reload's slot bookkeeping: reloadInstrument publishes the block too, and two concurrent
|
||||
// seqlock writers can leave the generation even mid-write, which a reader would accept as
|
||||
// a coherent — but torn — block. The audio thread never takes this mutex, so the cost is
|
||||
// an off-thread wait behind a reload. Lock order matches reloadInstrument's
|
||||
// (reloadMutex_ then paramsMutex_, taken by instrumentParams below).
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
const int rate = builtSampleRate_.load(std::memory_order_relaxed);
|
||||
if (rate <= 0) return;
|
||||
liveParams_.publish(
|
||||
|
||||
@@ -154,11 +154,10 @@ public:
|
||||
|
||||
// Republishes the live-parameter block from the stored parameter set, resolved against the
|
||||
// rate the loaded capture was built at so an unmoved value folds to exactly the frames the
|
||||
// voices already latched. THE tier-3 commit: no bridge read, no WAV re-decode, no engine
|
||||
// rebuild, no snapshot swap — the sounding note follows the knob. Persistence is
|
||||
// unaffected: getState still serializes params_, so callers pair this with
|
||||
// setInstrumentParams exactly as they paired it with reloadInstrument. No-op before
|
||||
// anything has been decoded (the next reload bakes and publishes). UI thread.
|
||||
// voices already latched. THE tier-3 commit (the three tiers are listed in this
|
||||
// directory's CLAUDE.md). Callers pair this with setInstrumentParams exactly as they
|
||||
// paired it with reloadInstrument. No-op before anything has been decoded (the next reload
|
||||
// bakes and publishes). UI thread; serialized against reloadInstrument's own publish.
|
||||
void publishLiveParams();
|
||||
|
||||
// Per-instance channel mode (mono | stereo), guarded by channelModeMutex_, never read
|
||||
@@ -243,6 +242,13 @@ private:
|
||||
// The rate the loaded capture was decoded/built at, so a live republish resolves the
|
||||
// stored wall-clock seconds to exactly the frames the built SampleData carries. 0 = nothing
|
||||
// built yet.
|
||||
//
|
||||
// ONE BLOCK, ONE RATE: this is stamped by whichever capture built last, and a reload
|
||||
// publishes the new block before installing the new instrument. Swapping to a capture at a
|
||||
// different rate therefore hands drain voices still ringing from the old-rate capture
|
||||
// envelope frame counts resolved at the NEW rate (~8.8% timing shift on a 48k->44.1k swap).
|
||||
// Unavoidable while one block sits above every snapshot, and it touches a release tail
|
||||
// only.
|
||||
std::atomic<int> builtSampleRate_{0};
|
||||
|
||||
// --- The audio-thread handoff (drain slot) ---
|
||||
|
||||
Reference in New Issue
Block a user