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
+17 -4
View File
@@ -236,15 +236,23 @@ public:
void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()]
// The master-bus limiter's single enable (persisted in the parameter set). UI thread only:
// a thin wrapper over setInstrumentParams, the one funnel that both mirrors the flag and
// requests the host's kLatencyChanged restart, which the SDK requires be issued from the UI
// thread and which process() must therefore never trigger. Setting the value it already
// holds is a no-op, so repeated clicks on one segment cost no restart.
// a thin wrapper over setInstrumentParams, the one funnel that mirrors the flag onto the
// audio thread INLINE — the sound follows the click — and only ARMS the host's latency
// restart. Setting the value it already holds is a no-op, so repeated clicks on one segment
// cost no restart.
bool limiterEnabled() const {
return limiterEnabled_.load(std::memory_order_relaxed);
}
void setLimiterEnabled(bool on);
// Delivers the armed kLatencyChanged restart, at most once per armed window, and does
// nothing when none is armed. Split off the commit because the SDK requires this on the UI
// thread AND a host may service it synchronously — deactivate/reactivate, which reaches our
// setActive(true) and its reloadInstrument — so it must never run nested inside a mouse
// handler. The editor's sync tick is the general drain; the two callers that can commit with
// no editor open (setState, adoptBakedCapture) flush themselves at their own tails.
void flushLatencyRestart();
// 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
@@ -471,6 +479,11 @@ private:
// getLatencySamples answers from.
instrument::engine::Limiter limiter_;
std::atomic<bool> limiterEnabled_{false};
// Set by the commit funnel when the enable actually changed, cleared only by
// flushLatencyRestart. A sticky bool and not a count on purpose: the host is being told to
// re-ASK, so N changes before one flush need exactly one restart, and whatever
// getLatencySamples answers at that moment is the truth being announced.
std::atomic<bool> latencyRestartPending_{false};
// What the audio thread publishes about the output bus each block, relaxed. The peaks and
// minGain ACCUMULATE (max / min) across every block since the UI last read, and