From ee7abe3dc997ab93526b1ca9c4bb22884ee9706f Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Sat, 11 Jul 2026 00:08:33 +0200 Subject: [PATCH] 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: ((