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 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-11 00:08:33 +02:00
co-authored by Claude Fable 5
parent 09617aa417
commit ee7abe3dc9
2 changed files with 54 additions and 0 deletions
+50
View File
@@ -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', () => {
+4
View File
@@ -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<boolean> => ipcRenderer.invoke('debug:isEnabled'),
setDeviceType: (typeName: string) => ipcRenderer.invoke('audio:setDeviceType', typeName),
setOutputDeviceType: (typeName: string) => ipcRenderer.invoke('audio:setOutputDeviceType', typeName),
setDevice: ((