Make spline EG drags audible live: held and new notes follow a dragged knot

Contours ride their own seqlock block into a double-buffered engine copy, and the swap step is smoothed. Each drag move publishes contours without a model write; release still commits and reloads.
This commit is contained in:
2026-09-25 13:42:25 -04:00
parent 9ff34b0a9e
commit f7505d0e53
21 changed files with 788 additions and 97 deletions
+15 -4
View File
@@ -23,6 +23,7 @@
#include "core/instrument/map/sample_map.h" // InstrumentParams (the one parameter set)
#include "core/instrument/map/component_state_io.h" // ComponentState codec
#include "core/instrument/engine/limiter.h" // the master bus's post-gain limiter
#include "core/instrument/engine/live_contours.h" // LiveContourBlock (the drawn-contour block)
#include "core/instrument/engine/live_params.h" // LiveParams (the live-parameter block)
#include "core/instrument/engine/meter_accumulate.h" // the meter's block-rate folds + consume
#include "core/instrument/param/param_id.h" // the frozen ParamId space + DeckParam binding
@@ -31,6 +32,7 @@ namespace reasampler::vst {
using instrument::map::ComponentState;
using instrument::map::InstrumentParams;
using instrument::map::PlaySeconds;
using instrument::map::SampleRefEntry;
using instrument::map::SampleRefs;
using instrument::map::kPreviewVelocityDefault;
@@ -206,6 +208,11 @@ public:
// bakes and publishes). UI thread; serialized against reloadInstrument's own publish. Returns drainAutomationToModel's release proof (automation_channel.h); other callers ignore it.
AutomationChannel::ReleaseProof publishLiveParams();
// Publishes `play`'s three drawn contours to every sounding voice WITHOUT writing the
// model — the contour drag's courier (this directory's CLAUDE.md, the authority table).
// UI thread; serialized against reloadInstrument's own contour publish.
void publishLiveContours(const PlaySeconds& play);
// Per-instance channel mode (mono | stereo), guarded by channelModeMutex_, never read
// on the audio thread. Decode policy only (downmix vs L/R split) — the output bus is
// fixed stereo, so a mode change never renegotiates host I/O.
@@ -387,6 +394,10 @@ private:
// Both live_ and draining_ observe this same block — a block owned by a snapshot would
// leave the drain's still-sounding voices deaf to the knob under them.
instrument::engine::LiveParams liveParams_;
// The drawn-contour block, SampleData::liveContours' one target, under liveParams_' ordering
// constraint and for its reason. No automation merge: no contour is a host parameter, so both
// engines read the model/editor publish directly.
instrument::engine::LiveContourBlock liveContours_;
// The block the ENGINE reads, and the ONE thing SampleData::live points at. Written only by
// the audio thread, which merges liveParams_ with the host's automation points once per block
// and republishes ONLY when the RESULT moved — so neither an unchanged model nor a lane
@@ -404,10 +415,10 @@ private:
// The host lane's state and its release protocol; automation_channel.h owns the mechanism and
// this directory's CLAUDE.md the authority model it enforces.
AutomationChannel automation_;
// Serializes liveParams_.publish's two writer sites (reloadInstrument, publishLiveParams)
// only — separate from reloadMutex_ so a knob drag's publish never blocks behind a
// reload's WAV decode. The audio thread never takes this; process() only reads via
// LiveParams::read's lock-free seqlock retry.
// Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams) and of
// liveContours_ (reloadInstrument, publishLiveContours) only — separate from reloadMutex_ so
// a drag's publish never blocks behind a reload's WAV decode. The audio thread never takes
// this; it only reads via the seqlock's lock-free retry.
std::mutex livePublishMutex_;
// 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