Commit the limiter's audible state on the click and defer only the host's latency restart

setInstrumentParams now arms a sticky pending restart that flushLatencyRestart drains from the sync tick; setState and the bake's adopt flush at their own tails.
This commit is contained in:
2026-08-02 11:45:47 -04:00
parent 41876674e4
commit b956fe0d5a
7 changed files with 76 additions and 44 deletions
+28 -11
View File
@@ -86,6 +86,10 @@ 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();
// 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) reloads — from the
// refs above, which are only fully restored once this function has run to here.
flushLatencyRestart();
return kResultOk;
}
@@ -157,27 +161,40 @@ void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
}
// Every writer of the parameter set — setState, the editor's commits, the bake's adopt —
// funnels through here, so mirroring the limiter flag at this one point is what keeps the
// audio thread's copy and the latency report from ever lagging what is persisted, and
// requesting the restart here (not just from setLimiterEnabled) is what keeps the host's
// PDC from lagging it too. Coalesced: writing the value already held requests nothing.
// audio thread's copy and the latency report from ever lagging what is persisted. The MIRROR
// is inline, because that is the sound the user clicked for; the host notification is not,
// because this funnel is reachable from inside a mouse handler and restartComponent is not
// safe there (see this directory's CLAUDE.md).
publishLimiterEnabled(params.limiterEnabled);
if (limiterFlagChanged && componentHandler) {
// The SDK requires this on the UI thread and answers getLatencySamples only after the
// host's own deactivate/reactivate — so the flag above is already committed by the time
// the host asks. This is a kLatencyChanged restart with the bus untouched, NOT the
// retired per-mode kIoChanged bus renegotiation (see initialize()); do not conflate.
componentHandler->restartComponent(kLatencyChanged);
// Armed AFTER the mirror, so getLatencySamples already answers the new value for the whole
// window the arm stays outstanding. Sticky and idempotent: any number of changes before one
// flush cost one restart, and the flush is the only thing that clears it.
if (limiterFlagChanged) {
latencyRestartPending_.store(true, std::memory_order_release);
}
}
void ReaSamplerProcessor::flushLatencyRestart() {
// Cleared only once it can actually be delivered — an arm raised before the host connected
// its handler waits for a later flush instead of evaporating.
if (!componentHandler) return;
if (!latencyRestartPending_.exchange(false, std::memory_order_acquire)) return;
// The SDK requires this on the UI thread and answers getLatencySamples only after the host's
// own deactivate/reactivate — so the flag is long committed by the time the host asks. This
// is a kLatencyChanged restart with the bus untouched, NOT the retired per-mode kIoChanged
// bus renegotiation (see initialize()); do not conflate.
componentHandler->restartComponent(kLatencyChanged);
}
void ReaSamplerProcessor::publishLimiterEnabled(bool on) {
limiterEnabled_.store(on, std::memory_order_relaxed);
limiter_.setEnabled(on);
}
void ReaSamplerProcessor::setLimiterEnabled(bool on) {
// Thin wrapper: setInstrumentParams is the one funnel that mirrors the flag AND requests
// the restart, so every writer of the parameter set — this one included — agrees.
// Rebased off the PROCESSOR's copy rather than taking a caller-supplied set: an editor
// snapshot may carry edits it has not committed, and writing one back here would clobber
// them. Everything else is setInstrumentParams', the one funnel every writer agrees through.
InstrumentParams params = instrumentParams();
params.limiterEnabled = on;
setInstrumentParams(params);