diff --git a/src/audio/AudioEngine.h b/src/audio/AudioEngine.h index a510e91..15d86e4 100644 --- a/src/audio/AudioEngine.h +++ b/src/audio/AudioEngine.h @@ -174,6 +174,21 @@ public: void setMonitorMuteSuppressed(bool suppressed) { source0().setMonitorMuteSuppressed(suppressed); } bool isMonitorMuteSuppressed() const { return source0().isMonitorMuteSuppressed(); } + // Full monitor kill — silences the guitar bus entirely (dry + processed), + // for monitoring through an external rig. Unlike the per-source mute/gain + // controls (which delegate to source0()), this is a GLOBAL "play through my + // own rig" preference, so it's applied to EVERY pooled source — active or + // not — so additional inputs are silenced too and a later addSource() + // inherits it (addSource never resets the flag). The fixed pool's pointers + // are never reassigned, and these are plain atomic stores, so iterating it + // off the control thread is race-free. Default off; see SourceChain. + void setMonitorKill(bool kill) + { + for (auto& s : sources) + if (s) s->setMonitorKill(kill); + } + bool isMonitorKilled() const { return source0().isMonitorKilled(); } + // Number of audio blocks whose signal-chain output had to be scrubbed for // non-finite/runaway samples (issue #403). A nonzero value means the chain // (NAM/IR/VST) emitted garbage that was contained before it reached the diff --git a/src/audio/NodeAddon.cpp b/src/audio/NodeAddon.cpp index 9ffd868..9821fde 100644 --- a/src/audio/NodeAddon.cpp +++ b/src/audio/NodeAddon.cpp @@ -627,6 +627,16 @@ static Napi::Value SetMonitorMuteSuppressed(const Napi::CallbackInfo& info) return info.Env().Undefined(); } +static Napi::Value SetMonitorKill(const Napi::CallbackInfo& info) +{ + // IsBoolean()-guarded (fail-soft no-op on a downlevel/mismatched caller), + // mirroring SetMonitorMuteSuppressed. + auto liveEngine = snapshotEngine(); + if (liveEngine && info.Length() > 0 && info[0].IsBoolean()) + liveEngine->setMonitorKill(info[0].As().Value()); + return info.Env().Undefined(); +} + static Napi::Value SetNoiseGate(const Napi::CallbackInfo& info) { auto env = info.Env(); @@ -3130,6 +3140,7 @@ static Napi::Object InitModule(Napi::Env env, Napi::Object exports) exports.Set("setMonitorMute", Napi::Function::New(env, SetMonitorMute)); exports.Set("setMonitorMuteSuppressed", Napi::Function::New(env, SetMonitorMuteSuppressed)); exports.Set("isMonitorMuted", Napi::Function::New(env, IsMonitorMuted)); + exports.Set("setMonitorKill", Napi::Function::New(env, SetMonitorKill)); exports.Set("setNoiseGate", Napi::Function::New(env, SetNoiseGate)); exports.Set("setTonePolish", Napi::Function::New(env, SetTonePolish)); diff --git a/src/audio/SourceChain.cpp b/src/audio/SourceChain.cpp index fa5ede5..c07ab4a 100644 --- a/src/audio/SourceChain.cpp +++ b/src/audio/SourceChain.cpp @@ -236,6 +236,15 @@ void SourceChain::processBlock(const float* const* inputData, int numInputChanne if (monitorMuted.load() && !hasProcessors && !monitorMuteSuppressed.load()) buffer.clear(); + // Full monitor kill: silence the guitar bus unconditionally — dry AND the + // processed/amp-sim signal — for users who monitor through their own external + // rig. Distinct from the dry-only mute above (which a loaded chain bypasses), + // and NOT subject to the suppression guard. Runs after the chain so the pitch + // detector / metering still see real signal; the backing track is mixed in + // later, so it keeps playing. + if (monitorKill.load()) + buffer.clear(); + // Chain output gain — the amp/tone's output level. Applied to the guitar // signal ONLY, before the backing track is mixed in, so switching tone presets // changes the guitar level without touching the song volume. diff --git a/src/audio/SourceChain.h b/src/audio/SourceChain.h index 2962bb7..47c86a7 100644 --- a/src/audio/SourceChain.h +++ b/src/audio/SourceChain.h @@ -143,6 +143,13 @@ public: bool isMonitorMuted() const { return monitorMuted.load(); } void setMonitorMuteSuppressed(bool s) { monitorMuteSuppressed.store(s); } bool isMonitorMuteSuppressed() const { return monitorMuteSuppressed.load(); } + // 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 + // their own external rig and want zero in-app monitoring. Default off, so + // existing amp-sim monitoring is unaffected. + void setMonitorKill(bool kill) { monitorKill.store(kill); } + bool isMonitorKilled() const { return monitorKill.load(); } float getInputLevel() const { return currentInputLevel.load(); } float getInputPeak() const { return inputPeak.load(); } @@ -192,6 +199,7 @@ private: std::atomic verifierUserOffset{0.0}; // renderer: manual fine-tune std::atomic monitorMuted{true}; std::atomic monitorMuteSuppressed{false}; + std::atomic monitorKill{false}; std::atomic nonFiniteChainBlocks{0}; // ── Lock-free SPSC input rings (see AudioEngine.h for the full rationale) ── diff --git a/src/main/audio-bridge.ts b/src/main/audio-bridge.ts index 9139837..77c0260 100644 --- a/src/main/audio-bridge.ts +++ b/src/main/audio-bridge.ts @@ -24,6 +24,7 @@ type AudioDeviceSettings = { bufferSize: string; inputChannel: string; monitorMute?: boolean; + monitorKill?: boolean; savedAt?: number; }; @@ -85,6 +86,9 @@ function normalizeDeviceSettings(settings: unknown): AudioDeviceSettings | null if (typeof record.monitorMute === 'boolean') { normalized.monitorMute = record.monitorMute; } + if (typeof record.monitorKill === 'boolean') { + normalized.monitorKill = record.monitorKill; + } const savedAt = Number(record.savedAt); if (Number.isFinite(savedAt) && savedAt > 0) { normalized.savedAt = savedAt; @@ -536,6 +540,15 @@ export function initAudioBridge(): void { return audio?.isMonitorMuted() ?? true; }); + ipcMain.handle('audio:setMonitorKill', (_event, kill: boolean) => { + // typeof-guarded fail-soft: a downlevel addon without setMonitorKill is a + // no-op rather than a thrown IPC error. Coerced to a real boolean so the + // N-API IsBoolean() guard sees a clean value. + if (audio && typeof audio.setMonitorKill === 'function') { + audio.setMonitorKill(Boolean(kill)); + } + }); + ipcMain.handle( 'audio:setNoiseGate', ( diff --git a/src/main/preload.ts b/src/main/preload.ts index 262ecb4..5d8372c 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -241,6 +241,7 @@ const feedBackDesktopApi = { setMonitorMuteSuppressed: (suppressed: boolean) => ipcRenderer.invoke('audio:setMonitorMuteSuppressed', suppressed), isMonitorMuted: () => ipcRenderer.invoke('audio:isMonitorMuted'), + setMonitorKill: (kill: boolean) => ipcRenderer.invoke('audio:setMonitorKill', kill), setNoiseGate: (payload: { enabled: boolean; thresholdDb: number; diff --git a/src/renderer/screen.html b/src/renderer/screen.html index 14443de..408a56d 100644 --- a/src/renderer/screen.html +++ b/src/renderer/screen.html @@ -40,7 +40,15 @@ Mute direct monitoring - (only hear guitar through signal chain plugins) + (mutes the dry passthrough only — a loaded amp sim is still heard; use “Disable input monitoring” below to silence everything) + + +
+
diff --git a/src/renderer/screen.js b/src/renderer/screen.js index 5044e2c..97910ef 100644 --- a/src/renderer/screen.js +++ b/src/renderer/screen.js @@ -55,6 +55,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; const inputGainLabel = $('ae-input-gain-label'); const outputGainLabel = $('ae-output-gain-label'); const monitorMuteCheckbox = $('ae-monitor-mute'); + const monitorKillCheckbox = $('ae-monitor-kill'); const chainContainer = $('ae-chain'); const addVstBtn = $('ae-add-vst'); const addNamBtn = $('ae-add-nam'); @@ -122,6 +123,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; bufferSize: bufferSizeSelect.value, inputChannel: inputChannelSelect.value, monitorMute: monitorMuteCheckbox.checked, + monitorKill: monitorKillCheckbox.checked, }; } @@ -181,6 +183,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; inputChannel: normalizeSelectSettingValue(settings.inputChannel), }; if (typeof settings.monitorMute === 'boolean') normalized.monitorMute = settings.monitorMute; + if (typeof settings.monitorKill === 'boolean') normalized.monitorKill = settings.monitorKill; const savedAt = Number(settings.savedAt); if (Number.isFinite(savedAt) && savedAt > 0) normalized.savedAt = savedAt; return normalized; @@ -842,6 +845,14 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; try { await api.setMonitorMute(saved.monitorMute); } catch (e) { console.warn('[audio-engine] setMonitorMute restore failed:', e); } } + if (saved.monitorKill !== undefined) { + monitorKillCheckbox.checked = saved.monitorKill; + // Push immediately too — same reason as monitorMute above; the + // native default is monitorKill{false}, so a saved-true must be + // applied even if the device probe below fails. + try { await api.setMonitorKill?.(saved.monitorKill); } + catch (e) { console.warn('[audio-engine] setMonitorKill restore failed:', e); } + } // Respect refreshDeviceOptions's fail-closed verdict: if the // probe didn't explicitly confirm compatible=true, skip the @@ -1272,6 +1283,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; const inputChannel = parseInt(inputChannelSelect.value); if (Number.isFinite(inputChannel)) await api.setInputChannel(inputChannel); await api.setMonitorMute(monitorMuteCheckbox.checked); + await api.setMonitorKill?.(monitorKillCheckbox.checked); await api.startAudio(); audioRunning = true; toggleBtn.textContent = 'Stop'; @@ -1309,6 +1321,14 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; await saveAppliedDeviceSettings({ monitorMute: monitorMuteCheckbox.checked }); }); + // Disable input monitoring (full kill) — for playing through an external + // rig. Applies live; the dry-only "Mute direct monitoring" can't silence a + // loaded amp sim, this can. + monitorKillCheckbox.addEventListener('change', async () => { + await api.setMonitorKill?.(monitorKillCheckbox.checked); + await saveAppliedDeviceSettings({ monitorKill: monitorKillCheckbox.checked }); + }); + // Gain sliders (UI dB → linear amplitude for engine) inputGainSlider.addEventListener('input', () => { const db = parseFloat(inputGainSlider.value);