Report the instrument's automatable parameters to the host under a frozen id table, in signal-flow order, with real units

42 of 44 ids issued: pitch key-track and Trigger length stay reserved
pending a live path. Master gain reclassified Live — it never reloaded.
This commit is contained in:
2026-08-02 15:14:16 -04:00
parent c7afa3a80f
commit bfaa0f2614
38 changed files with 1742 additions and 218 deletions
@@ -24,6 +24,7 @@
#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/engine/voice_engine.h"
#include "core/instrument/param/param_id.h" // the frozen ParamId space + DeckParam binding
namespace reasampler::vst {
@@ -124,6 +125,31 @@ 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.
Steinberg::tresult PLUGIN_API setParamNormalized(
Steinberg::Vst::ParamID tag, Steinberg::Vst::ParamValue value) override;
// Pushes every exposed parameter's normalized value from the model into the controller
// WITHOUT notifying the host — the load direction, which the SDK forbids reflecting back
// 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.
void beginParamGesture(instrument::ui::DeckParam deck);
void endParamGesture();
// Additionally exposes REAPER's IReaperUIEmbedInterface (queried by REAPER to drive the
// inline TCP/MCP embed); all other iids delegate to SingleComponentEffect unchanged.
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
@@ -276,6 +302,23 @@ public:
std::string usageInstanceGuid();
private:
// Builds the unit and parameter lists from the derived exposed set. Called once, from
// 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.
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);
void notifyParamChanged(instrument::param::ParamId id, double normalized);
// 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
// tails die. Off the audio thread only (driven by pollBankSync); safe against a racing
@@ -333,6 +376,12 @@ private:
ReaperBridge bridge_;
// The parameter whose drag bracket is currently open, 0 for none. UI thread only.
instrument::param::ParamId openGestureId_ = 0;
// 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;
// The ONE live-parameter block for this instance, declared ahead of the instrument slots
// so it outlives every snapshot that points at it (members destruct in reverse order).
// Both live_ and draining_ observe this same block — a block owned by a snapshot would