mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 02:34:10 +00:00
fix(audio): refcounted monitor-mute arbiter (TLC Part II §2)
The old single monitorMuted atomic had five writers fighting last-writer-wins: the settings checkbox, startup restore, the executor's preload read-force-restore, releaseRoute's unconditional setMonitorMute(true) (which clobbered the user's persisted preference), and the renderer's song-load suppression (un-refcounted — overlapping windows un-suppressed each other early). Native arbiter on SourceChain: userMonitorMute (the preference — checkbox + restore only), refcounted monitorMuteHolds (force-mute overrides), and refcounted suppressions (setMonitorMuteSuppressed keeps its bool surface; true=acquire, false=release, clamped at 0). Effective dry-mute = (holds || pref) && chain empty && no suppression — the suppressed-beats-muted precedence is unchanged. New exports: acquire/releaseMonitorMuteHold, getMonitorMuteState (diag); snapshots regenerated. Executor rewrite: acquires a suppression (dry-during-load, the default) or a hold, and releases exactly what it acquired via a single-fire closure that runs UNCONDITIONALLY (each load owns its acquisition — the stale-snapshot race against a mid-hold user toggle is structurally gone). releaseRoute no longer touches mute state at all. The ownership test now pins: preference API never called, acquire/release balanced. Renderer callers are unchanged: the checkbox writes the preference as before, and the song-load suppression sites now compose instead of racing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2906d2814b
commit
88f881dd1e
@@ -165,6 +165,11 @@ public:
|
||||
// so the brief empty-chain window doesn't silence the player's guitar.
|
||||
void setMonitorMuteSuppressed(bool suppressed) { source0().setMonitorMuteSuppressed(suppressed); }
|
||||
bool isMonitorMuteSuppressed() const { return source0().isMonitorMuteSuppressed(); }
|
||||
// Refcounted force-mute overrides (see SourceChain's arbiter comment).
|
||||
void acquireMonitorMuteHold() { source0().acquireMonitorMuteHold(); }
|
||||
void releaseMonitorMuteHold() { source0().releaseMonitorMuteHold(); }
|
||||
int getMonitorMuteHoldCount() const { return source0().getMonitorMuteHoldCount(); }
|
||||
int getMonitorMuteSuppressCount() const { return source0().getMonitorMuteSuppressCount(); }
|
||||
|
||||
// Full monitor kill — silences the guitar bus entirely (dry + processed),
|
||||
// for monitoring through an external rig. Unlike the per-source mute/gain
|
||||
|
||||
@@ -106,6 +106,9 @@ using slopsmith::addon::SetInputChannel;
|
||||
using slopsmith::addon::SetMonitorKill;
|
||||
using slopsmith::addon::SetMonitorMute;
|
||||
using slopsmith::addon::SetMonitorMuteSuppressed;
|
||||
using slopsmith::addon::AcquireMonitorMuteHold;
|
||||
using slopsmith::addon::ReleaseMonitorMuteHold;
|
||||
using slopsmith::addon::GetMonitorMuteState;
|
||||
using slopsmith::addon::SetMultiBypass;
|
||||
using slopsmith::addon::SetNoiseGate;
|
||||
using slopsmith::addon::SetNoteDetectionEnabled;
|
||||
@@ -346,6 +349,9 @@ static Napi::Object InitModule(Napi::Env env, Napi::Object exports)
|
||||
exports.Set("setInputChannel", Napi::Function::New(env, SetInputChannel));
|
||||
exports.Set("setMonitorMute", Napi::Function::New(env, SetMonitorMute));
|
||||
exports.Set("setMonitorMuteSuppressed", Napi::Function::New(env, SetMonitorMuteSuppressed));
|
||||
exports.Set("acquireMonitorMuteHold", Napi::Function::New(env, AcquireMonitorMuteHold));
|
||||
exports.Set("releaseMonitorMuteHold", Napi::Function::New(env, ReleaseMonitorMuteHold));
|
||||
exports.Set("getMonitorMuteState", Napi::Function::New(env, GetMonitorMuteState));
|
||||
exports.Set("isMonitorMuted", Napi::Function::New(env, IsMonitorMuted));
|
||||
exports.Set("setMonitorKill", Napi::Function::New(env, SetMonitorKill));
|
||||
exports.Set("setNoiseGate", Napi::Function::New(env, SetNoiseGate));
|
||||
|
||||
@@ -252,7 +252,9 @@ void SourceChain::processBlock(const float* const* inputData, int numInputChanne
|
||||
// chain yet. Backing track still plays through. Suppressed during a song-load
|
||||
// chain rebuild so the brief (or failed) empty-chain window doesn't silence
|
||||
// the guitar.
|
||||
if (monitorMuted.load() && !hasProcessors && !monitorMuteSuppressed.load())
|
||||
if ((monitorMuteHolds.load(std::memory_order_acquire) > 0 || userMonitorMute.load())
|
||||
&& !hasProcessors
|
||||
&& monitorMuteSuppress.load(std::memory_order_acquire) == 0)
|
||||
buffer.clear();
|
||||
|
||||
// Full monitor kill: silence the guitar bus unconditionally — dry AND the
|
||||
|
||||
+37
-6
@@ -142,10 +142,40 @@ public:
|
||||
// then a channel index WITHIN the bound device.
|
||||
void setDeviceKey(int key) { deviceKey.store(key, std::memory_order_release); }
|
||||
int getDeviceKey() const { return deviceKey.load(std::memory_order_acquire); }
|
||||
void setMonitorMute(bool mute) { monitorMuted.store(mute); }
|
||||
bool isMonitorMuted() const { return monitorMuted.load(); }
|
||||
void setMonitorMuteSuppressed(bool s) { monitorMuteSuppressed.store(s); }
|
||||
bool isMonitorMuteSuppressed() const { return monitorMuteSuppressed.load(); }
|
||||
// ── Monitor-mute arbiter (TLC Part II §2 fix) ─────────────────────────────
|
||||
// The old single monitorMuted atomic had FIVE writers (settings checkbox,
|
||||
// startup restore, executor preload-mute, executor releaseRoute, renderer
|
||||
// song-load suppression) fighting last-writer-wins — releaseRoute clobbered
|
||||
// the user's persisted preference and overlapping suppression windows
|
||||
// un-suppressed each other. Now three composable inputs:
|
||||
// userMonitorMute — the PREFERENCE (checkbox + startup restore).
|
||||
// monitorMuteHolds — refcounted "force mute" overrides (executor
|
||||
// preload-mute); released, never "restored".
|
||||
// monitorMuteSuppress — refcounted "force unmute" windows (song-load
|
||||
// chain rebuilds). Wins over pref + holds,
|
||||
// preserving the old suppressed-beats-muted rule.
|
||||
// effective dry-mute = (holds>0 || pref) && chain empty && suppress==0.
|
||||
void setMonitorMute(bool mute) { userMonitorMute.store(mute); }
|
||||
bool isMonitorMuted() const { return userMonitorMute.load(); }
|
||||
void acquireMonitorMuteHold() { monitorMuteHolds.fetch_add(1, std::memory_order_acq_rel); }
|
||||
void releaseMonitorMuteHold()
|
||||
{
|
||||
// Clamp at 0: an unpaired release (old callers, crashed holder) must
|
||||
// not underflow into a permanently-forced state.
|
||||
int cur = monitorMuteHolds.load(std::memory_order_acquire);
|
||||
while (cur > 0 && !monitorMuteHolds.compare_exchange_weak(cur, cur - 1, std::memory_order_acq_rel)) {}
|
||||
}
|
||||
// Back-compat surface: true = acquire a suppression, false = release one.
|
||||
// Overlapping windows now compose instead of last-clear-wins.
|
||||
void setMonitorMuteSuppressed(bool s)
|
||||
{
|
||||
if (s) { monitorMuteSuppress.fetch_add(1, std::memory_order_acq_rel); return; }
|
||||
int cur = monitorMuteSuppress.load(std::memory_order_acquire);
|
||||
while (cur > 0 && !monitorMuteSuppress.compare_exchange_weak(cur, cur - 1, std::memory_order_acq_rel)) {}
|
||||
}
|
||||
bool isMonitorMuteSuppressed() const { return monitorMuteSuppress.load(std::memory_order_acquire) > 0; }
|
||||
int getMonitorMuteHoldCount() const { return monitorMuteHolds.load(std::memory_order_acquire); }
|
||||
int getMonitorMuteSuppressCount() const { return monitorMuteSuppress.load(std::memory_order_acquire); }
|
||||
// Full monitor kill — silences the guitar bus UNCONDITIONALLY (dry AND the
|
||||
// processed/amp-sim signal), unlike setMonitorMute which only mutes the dry
|
||||
// pass-through when no processors are loaded. For users who monitor through
|
||||
@@ -200,8 +230,9 @@ private:
|
||||
std::atomic<int> deviceKey{0}; // 0 = primary input device
|
||||
std::atomic<double> verifierAutoOffset{0.0}; // engine: device-latency delta
|
||||
std::atomic<double> verifierUserOffset{0.0}; // renderer: manual fine-tune
|
||||
std::atomic<bool> monitorMuted{true};
|
||||
std::atomic<bool> monitorMuteSuppressed{false};
|
||||
std::atomic<bool> userMonitorMute{true};
|
||||
std::atomic<int> monitorMuteHolds{0};
|
||||
std::atomic<int> monitorMuteSuppress{0};
|
||||
std::atomic<bool> monitorKill{false};
|
||||
std::atomic<uint32_t> nonFiniteChainBlocks{0};
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ Napi::Value SetDevice(const Napi::CallbackInfo& info);
|
||||
Napi::Value SetDeviceType(const Napi::CallbackInfo& info);
|
||||
Napi::Value SetGain(const Napi::CallbackInfo& info);
|
||||
Napi::Value SetInputChannel(const Napi::CallbackInfo& info);
|
||||
Napi::Value AcquireMonitorMuteHold(const Napi::CallbackInfo& info);
|
||||
Napi::Value ReleaseMonitorMuteHold(const Napi::CallbackInfo& info);
|
||||
Napi::Value GetMonitorMuteState(const Napi::CallbackInfo& info);
|
||||
Napi::Value SetMonitorKill(const Napi::CallbackInfo& info);
|
||||
Napi::Value SetMonitorMute(const Napi::CallbackInfo& info);
|
||||
Napi::Value SetMonitorMuteSuppressed(const Napi::CallbackInfo& info);
|
||||
|
||||
@@ -80,6 +80,37 @@ Napi::Value SetMonitorMuteSuppressed(const Napi::CallbackInfo& info)
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
// Refcounted force-mute overrides (monitor-mute arbiter, TLC Part II §2).
|
||||
// The audio-effects executor holds one across a chain load and RELEASES it
|
||||
// afterwards — it never reads/writes the user's mute preference anymore.
|
||||
Napi::Value AcquireMonitorMuteHold(const Napi::CallbackInfo& info)
|
||||
{
|
||||
if (auto liveEngine = snapshotEngine())
|
||||
liveEngine->acquireMonitorMuteHold();
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
Napi::Value ReleaseMonitorMuteHold(const Napi::CallbackInfo& info)
|
||||
{
|
||||
if (auto liveEngine = snapshotEngine())
|
||||
liveEngine->releaseMonitorMuteHold();
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
// Diagnostic/testing view of the arbiter's three inputs.
|
||||
Napi::Value GetMonitorMuteState(const Napi::CallbackInfo& info)
|
||||
{
|
||||
auto env = info.Env();
|
||||
auto obj = Napi::Object::New(env);
|
||||
if (auto liveEngine = snapshotEngine())
|
||||
{
|
||||
obj.Set("userMute", liveEngine->isMonitorMuted());
|
||||
obj.Set("holds", liveEngine->getMonitorMuteHoldCount());
|
||||
obj.Set("suppressions", liveEngine->getMonitorMuteSuppressCount());
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
Napi::Value SetMonitorKill(const Napi::CallbackInfo& info)
|
||||
{
|
||||
// IsBoolean()-guarded (fail-soft no-op on a downlevel/mismatched caller),
|
||||
|
||||
Reference in New Issue
Block a user