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 -1
View File
@@ -41,6 +41,10 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
// calls setupProcessing before setState on load), which the legacy v3 payload's
// frames->seconds conversion needs.
const ComponentState cs = deserializeComponentState(bytes, sampleRate_);
// A LOAD is not an edit: the SDK is explicit that a controller must never pass a restored
// value back to the host through IComponentHandler. The push into the controller happens
// once at the tail instead, through syncParamsFromModel.
paramNotifySuppressed_ = true;
setSelectedSampleId(cs.selectionId);
setInstrumentParams(cs.params);
// Restore the last-consumed assignment generation so a re-open does not re-apply a
@@ -86,6 +90,12 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
// A new blob is new facts — the legacy lift gets one fresh run per restored state.
legacyLiftConcluded_.store(false, std::memory_order_relaxed);
reloadInstrument();
paramNotifySuppressed_ = false;
// Every exposed parameter now reads the blob's value. Ordering against the host's first
// parameter block is irrelevant BY CONSTRUCTION rather than by assumption: there is one
// model and one funnel per control, so whichever of the two writes last simply wins, and
// the host's display follows the model either way.
syncParamsFromModel();
// This caller has no editor to flush for it. At the TAIL on purpose: a host that services the
// restart synchronously deactivates/reactivates, and our setActive(true) resumes or reloads
// against the refs above, which are only fully restored once this function has run to here.
@@ -153,8 +163,10 @@ InstrumentParams ReaSamplerProcessor::instrumentParams() {
}
void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
InstrumentParams before;
{
std::lock_guard<std::mutex> lock(paramsMutex_);
before = params_;
params_ = params;
}
// Every writer of the parameter set — setState, the editor's commits, the bake's adopt —
@@ -173,6 +185,10 @@ void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
latencyRestartPending_.store(
params.limiterEnabled != latencyAnnounced_.load(std::memory_order_relaxed),
std::memory_order_release);
// The host-notification obligation, at the same one funnel and for the same reason the
// limiter mirror sits here: an internal write that skipped it would leave the host
// displaying — and, on the next touch, re-imposing — the superseded value.
notifyParamsFromModel(before, params);
}
void ReaSamplerProcessor::flushLatencyRestart() {
@@ -319,7 +335,14 @@ void ReaSamplerProcessor::setMasterGainLinear(double linear) {
if (!(linear >= 0.0)) linear = 0.0; // also catches NaN
const double maxLin = masterGainMaxLinear();
if (linear > maxLin) linear = maxLin;
masterGain_.store(static_cast<float>(linear), std::memory_order_relaxed);
const float value = static_cast<float>(linear);
const float previous = masterGain_.exchange(value, std::memory_order_relaxed);
// Gain's own notification funnel — it is the one exposed control that does not ride the
// parameter set, so setInstrumentParams' diff cannot see it. Compared for a real change so a
// reload's republish of an unmoved gain writes nothing into a host's automation lane.
if (paramNotifySuppressed_ || previous == value) return;
notifyParamChanged(instrument::param::kParamMasterGain,
instrument::engine::masterGainNormFromLinear(linear));
}
void ReaSamplerProcessor::previewNoteOn(int note) {