Service both VST3 parameter channels, and promote pitch key-track and Trigger length so all 44 ids issue
The SDK's own single-component sample drains inputParameterChanges in process() and implements setParamNormalized; automation was reading the GUI channel alone. The audio thread now patches a block it solely owns.
This commit is contained in:
@@ -125,17 +125,12 @@ public:
|
||||
// Hands the host our LICE IPlugView editor.
|
||||
Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;
|
||||
|
||||
// A host write of one exposed parameter. Applies it to THE model through that control's
|
||||
// existing commit tier — live publish, or the master-gain atomic — and never through a
|
||||
// fourth route. Nothing reachable from here touches reloadInstrument or rebuildVoiceEngine,
|
||||
// which is structural rather than careful: every reload- and rebuild-tier control is omitted
|
||||
// from the parameter list, so no id maps to one.
|
||||
//
|
||||
// Parameter values reach the audio thread through the SAME block the editor's knobs publish
|
||||
// into, which the engine observes ONCE per render() — i.e. at BLOCK BOUNDARIES, last write
|
||||
// wins for that block. Sample-accurate application would put a per-sample "did anything
|
||||
// change" question on the per-voice-per-sample path, which the phase-wide guardrail forbids.
|
||||
// process() reads no parameter queue and is unchanged by the parameter surface.
|
||||
// The CONTROLLER-side write — a host GUI gesture on the generic panel, and whatever a host
|
||||
// mirrors here for display. Applies it to THE model through that control's existing commit
|
||||
// tier and never through a fourth route. Nothing reachable from here touches reloadInstrument
|
||||
// or rebuildVoiceEngine, which is structural rather than careful: every reload- and
|
||||
// rebuild-tier control is omitted from the parameter list, so no id maps to one.
|
||||
// NOT the automation channel; this directory's CLAUDE.md owns which channel is which.
|
||||
Steinberg::tresult PLUGIN_API setParamNormalized(
|
||||
Steinberg::Vst::ParamID tag, Steinberg::Vst::ParamValue value) override;
|
||||
|
||||
@@ -144,10 +139,20 @@ public:
|
||||
// through IComponentHandler. UI/main thread.
|
||||
void syncParamsFromModel();
|
||||
|
||||
// A knob drag's host-edit bracket, so a host in touch or latch mode records ONE continuous
|
||||
// edit rather than a burst of one-point gestures. Idempotent: a grab while one is open
|
||||
// closes it first, and endParamGesture with none open does nothing. UI thread only.
|
||||
// Folds what the audio thread took from the host's parameter queue back into the model and
|
||||
// the controller cache, suppressing the host notification (those values came FROM it).
|
||||
// UI/main thread; a no-op when nothing was automated. Called wherever the model is about to
|
||||
// be READ as authoritative — getState, the bake, the editor's tick.
|
||||
void drainAutomationToModel();
|
||||
|
||||
// A drag's host-edit bracket, so a host in touch or latch mode records ONE continuous edit
|
||||
// per parameter rather than a burst of one-point ones. Every parameter the drag notifies
|
||||
// opens its bracket on first touch and holds it until endParamGesture — LATCHING rather than
|
||||
// declared up front, because an envelope-node drag moves a set the grab cannot name. The
|
||||
// deck-knob form additionally opens its own id at once, which is the id the host sees a touch
|
||||
// on even if the drag produces no move. Idempotent. UI thread only.
|
||||
void beginParamGesture(instrument::ui::DeckParam deck);
|
||||
void beginParamGestureLatch();
|
||||
void endParamGesture();
|
||||
|
||||
// Additionally exposes REAPER's IReaperUIEmbedInterface (queried by REAPER to drive the
|
||||
@@ -264,6 +269,9 @@ public:
|
||||
return static_cast<double>(masterGain_.load(std::memory_order_relaxed));
|
||||
}
|
||||
void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()]
|
||||
// The mirror alone, with no host notification: for the one writer that runs under
|
||||
// reloadMutex_ and must arm rather than emit. True when the value actually moved.
|
||||
bool publishMasterGainLinear(double linear);
|
||||
|
||||
// The master-bus limiter's single enable (persisted in the parameter set). UI thread only:
|
||||
// a thin wrapper over setInstrumentParams, the one funnel that mirrors the flag onto the
|
||||
@@ -284,6 +292,11 @@ public:
|
||||
// same tick, so its arm would drain on the next one regardless).
|
||||
void flushLatencyRestart();
|
||||
|
||||
// Delivers the master-gain host notification a reload deferred (the bake's reset gain lands
|
||||
// under reloadMutex_, and performEdit may re-enter this object). Drained beside the latency
|
||||
// restart, from the same sites and for the same reason.
|
||||
void flushGainNotify();
|
||||
|
||||
// Fires a one-shot preview note-on/off through the live VoiceEngine — the same
|
||||
// noteOn/noteOff host MIDI uses, so a preview is a real voice (counts against voice
|
||||
// count, can steal/be stolen, respects Poly/Mono + Retrigger/Legato). Off the audio
|
||||
@@ -306,18 +319,33 @@ private:
|
||||
// initialize().
|
||||
void buildParameterList();
|
||||
|
||||
// The normalized value a control reads at, from the model — the projection §6.1 calls a
|
||||
// third surface. Master gain reads the processor's own atomic; everything else reads the
|
||||
// parameter set through the deck's binding.
|
||||
// THE automation read, on the audio thread, at the BLOCK BOUNDARY: the last point of each
|
||||
// queue wins. RT-safe — relaxed atomic stores only. Sample-accurate application would put a
|
||||
// per-sample "did anything change" question on the per-voice-per-sample path, which the
|
||||
// phase-wide guardrail forbids. True when at least one point landed, which is what makes the
|
||||
// merge below it conditional.
|
||||
bool drainInputParameterChanges(Steinberg::Vst::IParameterChanges* changes);
|
||||
|
||||
// The normalized value a control reads at, from the model — a projection of it, never a
|
||||
// cached shadow. Master gain reads the processor's own atomic, pitch key-track the scalar
|
||||
// beside the play bundle; everything else reads the parameter set through the deck's binding.
|
||||
double modelParamNormalized(const InstrumentParams& params,
|
||||
instrument::ui::DeckParam deck) const;
|
||||
|
||||
// Notifies the host of every exposed control whose value differs between the two parameter
|
||||
// sets. Called from setInstrumentParams — the ONE funnel every writer already goes through —
|
||||
// so no internal write can leave the host displaying, and on next touch re-imposing, a
|
||||
// superseded value. The bake's reset is the first non-gesture writer this covers.
|
||||
void notifyParamsFromModel(const InstrumentParams& before, const InstrumentParams& after);
|
||||
// The write peer of that read, and the ONE host-side write of a control's value: both the
|
||||
// controller's setParamNormalized and the audio thread's automation fold go through it, so
|
||||
// neither can miss a control whose value lives outside the parameter set.
|
||||
static void writeDeckParamToModel(InstrumentParams& params, instrument::ui::DeckParam deck,
|
||||
double normalized);
|
||||
|
||||
// Notifies the host of every exposed control whose normalized value moved. `beforeNorms` is
|
||||
// indexed by DeckParam ordinal. Called from setInstrumentParams — the ONE funnel every writer
|
||||
// already goes through — so no internal write can leave the host displaying, and on next
|
||||
// touch re-imposing, a superseded value. The bake's reset is the first non-gesture writer
|
||||
// this covers, and the reason the emission is grouped.
|
||||
void notifyParamsFromModel(const double* beforeNorms, const InstrumentParams& after);
|
||||
void notifyParamChanged(instrument::param::ParamId id, double normalized);
|
||||
bool gestureIsOpen(instrument::param::ParamId id) const;
|
||||
|
||||
// If process() published that the drain instrument is fully idle, move it into the
|
||||
// graveyard and prune — so an edited-away snapshot stops costing memory as soon as its
|
||||
@@ -376,8 +404,13 @@ private:
|
||||
|
||||
ReaperBridge bridge_;
|
||||
|
||||
// The parameter whose drag bracket is currently open, 0 for none. UI thread only.
|
||||
instrument::param::ParamId openGestureId_ = 0;
|
||||
// The parameters whose drag bracket is currently open, and whether a drag is in flight at
|
||||
// all. UI thread only. Past the cap a write degrades to a one-point gesture rather than
|
||||
// dropping — the pre-bracket behaviour, not a new failure mode.
|
||||
static constexpr std::size_t kMaxOpenGestures = 8;
|
||||
instrument::param::ParamId openGestureIds_[kMaxOpenGestures] = {};
|
||||
std::size_t openGestureCount_ = 0;
|
||||
bool gestureLatching_ = false;
|
||||
// Set across setState so a LOAD is not reflected back to the host as an edit. Main thread
|
||||
// only, and non-atomic on purpose: the SDK calls setState there and nowhere else.
|
||||
bool paramNotifySuppressed_ = false;
|
||||
@@ -387,6 +420,26 @@ 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 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 either side moved — so a block carrying neither costs one
|
||||
// relaxed load and the engine's own read shape is unchanged. Two blocks because the seqlock's
|
||||
// single-writer contract is load-bearing and the two writers differ in thread; this
|
||||
// directory's CLAUDE.md owns the argument.
|
||||
instrument::engine::LiveParams automationLive_;
|
||||
// Audio thread only. The last liveParams_ generation merged, and the sticky automation values
|
||||
// re-applied over every merge — without them a model republish (any knob move) would revert
|
||||
// an automated parameter until its lane's next point.
|
||||
std::uint32_t seenModelGeneration_ = 0;
|
||||
static constexpr std::size_t kDeckParamSlots =
|
||||
static_cast<std::size_t>(instrument::ui::DeckParam::kCount);
|
||||
double automationNorm_[kDeckParamSlots] = {};
|
||||
bool automationHeld_[kDeckParamSlots] = {};
|
||||
// The audio thread's publication of those values to the UI thread's fold. automationAny_
|
||||
// makes the idle drain a single load.
|
||||
std::atomic<double> automationPublished_[kDeckParamSlots] = {};
|
||||
std::atomic<bool> automationPending_[kDeckParamSlots] = {};
|
||||
std::atomic<bool> automationAny_{false};
|
||||
// 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
|
||||
@@ -435,6 +488,10 @@ private:
|
||||
// the bake's reset, which the old capture would be the wrong thing to apply it to.
|
||||
// Guarded by reloadMutex_, consumed by publishBuiltLocked.
|
||||
std::optional<double> gainAtNextPublish_;
|
||||
// Armed when that deferred gain lands, delivered by flushGainNotify once reloadMutex_ is
|
||||
// released. Same shape and same reason as latencyRestartPending_ (see it): a host handler
|
||||
// callback must never run inside this mutex.
|
||||
std::atomic<bool> gainNotifyPending_{false};
|
||||
|
||||
// The decoded PCM parked across a deactivate, so an activation cycle costs no disk read
|
||||
// and no WAV decode: activation is "the audio thread may run", not "the sample is
|
||||
|
||||
Reference in New Issue
Block a user