From ee7abe3dc997ab93526b1ca9c4bb22884ee9706f Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Sat, 11 Jul 2026 00:08:33 +0200 Subject: [PATCH 1/2] feat(diag): --debug ASIO routing diagnostics ([asio-diag]) Testers report song/game audio on the default WASAPI device while the guitar rides the selected ASIO output. The renderer-side reroute chain ([feedpak-route]/[renderer-bus]) has change-gated logs, but the native side of the story (what the engine actually opened, whether the backing transport/renderer bus is live) was invisible in tester logs. - audio-bridge: debug-gated 2s engine snapshot, change-gated + 30s heartbeat: device types/names, duplex, backing, renderer-bus health - debug:isEnabled IPC + preload audio.debugEnabled() so the static bundle can gate its verbose lines on --debug Co-Authored-By: Claude Fable 5 --- src/main/audio-bridge.ts | 50 ++++++++++++++++++++++++++++++++++++++++ src/main/preload.ts | 4 ++++ 2 files changed, 54 insertions(+) diff --git a/src/main/audio-bridge.ts b/src/main/audio-bridge.ts index 26079af..093b13f 100644 --- a/src/main/audio-bridge.ts +++ b/src/main/audio-bridge.ts @@ -325,6 +325,56 @@ export function initAudioBridge(): void { ipcMain.handle('audio:isAvailable', () => audio !== null); + // Renderer-side diagnostics gate: the static bundle's routing watcher / + // renderer-bus feeder emit verbose [asio-diag] lines only when the app + // runs with --debug / --verbose / SLOPSMITH_DEBUG (issue: ASIO output — + // song audio heard on the default WASAPI device while guitar rides ASIO). + ipcMain.handle('debug:isEnabled', () => isDebugEnabled()); + + // [asio-diag] main-process routing snapshot. Polls the engine every 2s in + // debug mode and logs a one-line snapshot whenever the routing-relevant + // state changes (plus a 30s heartbeat so a log without transitions still + // proves the watcher was alive). Captures the native side of the story the + // renderer diagnostics can't see: which device types/names the engine + // actually opened, whether the backing transport is playing, and whether + // the renderer bus is enabled and moving frames. + if (audio && isDebugEnabled()) { + let lastSnapshot = ''; + let lastLogged = 0; + setInterval(() => { + try { + if (!audio) return; + const running = !!audio.isAudioRunning?.(); + const dev = running ? audio.getCurrentDevice?.() : null; + const bus = audio.getRendererBusMetrics?.() ?? null; + const snapshot = JSON.stringify({ + running, + inputType: dev?.inputType ?? '', + outputType: dev?.outputType ?? '', + input: dev?.input ?? '', + output: dev?.output ?? '', + duplex: dev?.duplex ?? null, + backingPlaying: !!audio.isBackingPlaying?.(), + streamOutputActive: !!audio.isStreamOutputActive?.(), + busEnabled: bus?.enabled ?? null, + // pushed/consumed prove frames are flowing; deltas matter, + // absolute counts churn — bucket to "moving or not". + busFlowing: !!bus && bus.pushedFrames > 0 && bus.consumedFrames > 0, + busUnderflows: bus?.underflowCount ?? null, + busOverflows: bus?.overflowCount ?? null, + }); + const now = Date.now(); + if (snapshot !== lastSnapshot || now - lastLogged > 30000) { + lastSnapshot = snapshot; + lastLogged = now; + console.log(`[asio-diag] engine: ${snapshot}`); + } + } catch (e: any) { + console.warn(`[asio-diag] snapshot failed: ${e.message}`); + } + }, 2000); + } + // ── Device Management ────────────────────────────────────────────────── ipcMain.handle('audio:getDeviceTypes', () => { diff --git a/src/main/preload.ts b/src/main/preload.ts index ca5bd2a..3051064 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -212,6 +212,10 @@ const feedBackDesktopApi = { ? ipcRenderer.invoke('audio:probeDeviceOptions', inputTypeOrType, inputName, outputTypeOrOutputName) : ipcRenderer.invoke('audio:probeDeviceOptions', inputTypeOrType, inputName, outputTypeOrOutputName, outputName), getCurrentDevice: () => ipcRenderer.invoke('audio:getCurrentDevice'), + // True when the app runs with --debug / --verbose / SLOPSMITH_DEBUG. + // The static bundle gates its verbose [asio-diag] routing lines on + // this so normal runs don't spam the console. + debugEnabled: (): Promise => ipcRenderer.invoke('debug:isEnabled'), setDeviceType: (typeName: string) => ipcRenderer.invoke('audio:setDeviceType', typeName), setOutputDeviceType: (typeName: string) => ipcRenderer.invoke('audio:setOutputDeviceType', typeName), setDevice: (( From d069414300666378e7dab84d98955cf45ecbbcd9 Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Sat, 11 Jul 2026 00:18:10 +0200 Subject: [PATCH 2/2] fix(diag): compute busFlowing from per-poll counter deltas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cumulative pushed/consumed never regress, so '> 0' stayed true forever after the first frame — a stalled renderer bus (one of the states this diagnostic exists to expose) would still report flowing. First sample after enable reports false (no baseline yet). Addresses CodeRabbit review on #95. Co-Authored-By: Claude Fable 5 --- src/main/audio-bridge.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/audio-bridge.ts b/src/main/audio-bridge.ts index 093b13f..4740243 100644 --- a/src/main/audio-bridge.ts +++ b/src/main/audio-bridge.ts @@ -341,12 +341,27 @@ export function initAudioBridge(): void { if (audio && isDebugEnabled()) { let lastSnapshot = ''; let lastLogged = 0; + // busFlowing must reflect the CURRENT poll interval: cumulative + // pushed/consumed counters never regress, so "> 0" would stay true + // forever after the first frame — masking a stalled bus, which is + // one of the states this diagnostic exists to expose. + let prevBusCounts: { pushed: number; consumed: number } | null = null; setInterval(() => { try { if (!audio) return; const running = !!audio.isAudioRunning?.(); const dev = running ? audio.getCurrentDevice?.() : null; const bus = audio.getRendererBusMetrics?.() ?? null; + let busFlowing = false; + if (bus) { + if (prevBusCounts) { + busFlowing = bus.pushedFrames > prevBusCounts.pushed + && bus.consumedFrames > prevBusCounts.consumed; + } + prevBusCounts = { pushed: bus.pushedFrames, consumed: bus.consumedFrames }; + } else { + prevBusCounts = null; + } const snapshot = JSON.stringify({ running, inputType: dev?.inputType ?? '', @@ -357,9 +372,8 @@ export function initAudioBridge(): void { backingPlaying: !!audio.isBackingPlaying?.(), streamOutputActive: !!audio.isStreamOutputActive?.(), busEnabled: bus?.enabled ?? null, - // pushed/consumed prove frames are flowing; deltas matter, - // absolute counts churn — bucket to "moving or not". - busFlowing: !!bus && bus.pushedFrames > 0 && bus.consumedFrames > 0, + // per-poll delta ("moved this interval"), not cumulative. + busFlowing, busUnderflows: bus?.underflowCount ?? null, busOverflows: bus?.overflowCount ?? null, });