Decouple the instrument reload from VST3 activation, and make the master meter's accumulate exact

This commit is contained in:
2026-08-02 12:41:57 -04:00
parent 4b0b03d8d5
commit 5c6525fb91
17 changed files with 462 additions and 239 deletions
+21 -15
View File
@@ -153,10 +153,8 @@ InstrumentParams ReaSamplerProcessor::instrumentParams() {
}
void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
bool limiterFlagChanged = false;
{
std::lock_guard<std::mutex> lock(paramsMutex_);
limiterFlagChanged = (params_.limiterEnabled != params.limiterEnabled);
params_ = params;
}
// Every writer of the parameter set — setState, the editor's commits, the bake's adopt —
@@ -167,11 +165,14 @@ void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
// safe there (see this directory's CLAUDE.md).
publishLimiterEnabled(params.limiterEnabled);
// 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);
}
// window the arm stays outstanding. Compared against the last ANNOUNCED enable rather than
// against the previous parameter set: off->on->off inside one tick ends at the latency the
// host already knows, and a restart rebuilds the instance, so announcing a latency that
// never changed is pure cost. Any number of changes before one flush still cost at most one
// restart, and this store is the only one that raises OR lowers the arm.
latencyRestartPending_.store(
params.limiterEnabled != latencyAnnounced_.load(std::memory_order_relaxed),
std::memory_order_release);
}
void ReaSamplerProcessor::flushLatencyRestart() {
@@ -179,6 +180,11 @@ void ReaSamplerProcessor::flushLatencyRestart() {
// its handler waits for a later flush instead of evaporating.
if (!componentHandler) return;
if (!latencyRestartPending_.exchange(false, std::memory_order_acquire)) return;
// Latched BEFORE the call: a host that services the restart synchronously re-enters this
// object inside it, so the next commit must compare against the value the host is about to
// read, not against the one it held before.
latencyAnnounced_.store(limiterEnabled_.load(std::memory_order_relaxed),
std::memory_order_relaxed);
// 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
@@ -202,14 +208,14 @@ void ReaSamplerProcessor::setLimiterEnabled(bool on) {
MasterBusMeter ReaSamplerProcessor::masterBusMeter() {
MasterBusMeter m;
// Exchange, not load: the accumulators hold the window since this was last called, and
// clearing them here is what starts the next window. The audio thread's own fold is a
// load-max-store, so a store landing between this exchange and that store can retain one
// window's peak for one extra frame — it can never LOSE one, which is the property that
// matters for a peak meter.
m.peakL = meterPeakL_.exchange(0.f, std::memory_order_relaxed);
m.peakR = meterPeakR_.exchange(0.f, std::memory_order_relaxed);
m.minGain = meterMinGain_.exchange(1.f, std::memory_order_relaxed);
// Consuming: each read takes the window and reinstalls its identity element, which is what
// starts the next one. The audio thread's fold is an unconditional CAS against exactly that
// (meter_accumulate.h owns the argument), so a fold interleaved with these exchanges lands
// in one window or the other and is never dropped between them.
m.peakL = instrument::engine::consumePeak(meterPeakL_);
m.peakR = instrument::engine::consumePeak(meterPeakR_);
m.minGain = instrument::engine::consumeMinGain(meterMinGain_);
// NOT consumed: the clip is a latch the user clears, not a window.
m.clip = meterClip_.load(std::memory_order_relaxed);
return m;
}