From 76159c16cd299d5d7903a90397f40fe291f1807d Mon Sep 17 00:00:00 2001 From: OmikronApex <45161725+OmikronApex@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:42:41 +0200 Subject: [PATCH] feat(diag): --debug ASIO routing diagnostics in static bundle (#852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(audio): route feedpak full-mix natively under exclusive output Song playback runs through the renderer, which WASAPI-exclusive (and ASIO) output silences. Route single-mix feedpaks (stem-less original_audio packs AND single-stem packs) onto the engine's backing transport when the output device type is exclusive-style, and migrate back to HTML5 when it isn't. Extends /api/audio-local-path to resolve /api/sloppak/.../file/... URLs via the same containment guards as serve_sloppak_file. Multi-stem packs stay on the WebAudio path (Phase 2). Includes [feedpak-route] transition-gated diagnostics logging. Co-Authored-By: Claude Fable 5 * feat(audio): renderer-bus feeder — mix renderer song audio into engine output (Phase 2) Under exclusive-style output the native backing transport (Phase 1, #824) carries loose /audio/ songs and feedpak full-mixes, but not the stems plugin's multi-stem WebAudio graph or tracks JUCE rejected. The feeder taps the renderer-side master with an AudioWorklet, re-points the owning AudioContext at a null sink so it keeps rendering without a device, and pushes ~10 ms chunks over IPC into the desktop engine's renderer bus (feedBack-desktop#90 follow-up). Inert in the Docker sphere and in shared mode. Validated by the fix12 tester spike: null-sink rendering works, clocks hold, no overflow. Co-Authored-By: Claude Fable 5 * feat(diag): --debug ASIO routing diagnostics in static bundle Gated on window.feedBackDesktop.audio.debugEnabled() (desktop --debug); inert in the Docker sphere and normal desktop runs. - [asio-diag] getCurrentDevice= full device object on outputType change (catches ASIO drivers reporting a non-'ASIO' type name) - [asio-diag] renderer-bus: full feeder decision vector, change-gated (running/exclusive/stems/juceMode/elementSong/want/mode) - [asio-diag] setSink: every sink flip with ctx state + rate Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- static/app.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/static/app.js b/static/app.js index 3287bcb..d5d33ef 100644 --- a/static/app.js +++ b/static/app.js @@ -4877,6 +4877,15 @@ window.jucePlayer = jucePlayer; // value change (this runs on a 350ms poll — logging every tick would // flood the diagnostics buffer). let _loggedOutputType; + // [asio-diag] verbose diagnostics, gated on --debug (preload exposes + // audio.debugEnabled). Resolved once at install; until it resolves the + // flag stays false and verbose lines are skipped. Shared with the + // renderer-bus feeder below via window._asioDiagEnabled. + let _asioDiag = false; + if (typeof juceApi.debugEnabled === 'function') { + juceApi.debugEnabled().then((v) => { _asioDiag = !!v; }).catch(() => {}); + } + window._asioDiagEnabled = () => _asioDiag; async function _outputIsExclusive() { if (typeof juceApi.getCurrentDevice !== 'function') { if (_loggedOutputType !== '') { @@ -4892,6 +4901,15 @@ window.jucePlayer = jucePlayer; if (t !== _loggedOutputType) { _loggedOutputType = t; console.log('[feedpak-route] outputType=', JSON.stringify(t), '→ exclusive=', excl); + // [asio-diag] full device object on every type change — shows + // the exact strings the predicate saw (inputType vs outputType, + // device names, duplex), so a driver reporting a non-'ASIO' + // type name is visible in tester logs. + if (_asioDiag) { + try { + console.log('[asio-diag] getCurrentDevice=', JSON.stringify(dev)); + } catch (_) { /* circular/hostile object — skip */ } + } } return excl; } catch (e) { @@ -5376,6 +5394,13 @@ window.jucePlayer = jucePlayer; if (typeof ctx.setSinkId !== 'function') throw new Error('setSinkId unsupported'); await ctx.setSinkId(exclusive ? { type: 'none' } : ''); if (ctx.state !== 'running') await ctx.resume().catch(() => {}); + // [asio-diag] a context left on the default sink while the bus is + // engaged is exactly the "song on the wrong device" symptom — record + // every successful sink flip (failures throw and are logged upstream). + if (window._asioDiagEnabled?.()) { + console.log('[asio-diag] setSink:', exclusive ? 'null-sink' : 'default', + 'state=', ctx.state, 'rate=', ctx.sampleRate); + } } async function _disengage() { @@ -5446,6 +5471,24 @@ window.jucePlayer = jucePlayer; else if (elementSong) want = 'element'; } + // [asio-diag] full decision vector, change-gated (500ms poll — + // steady state must not flood the buffer). This is the feeder-side + // counterpart of the watcher's [feedpak-route] decision line: it + // shows WHY the bus did or didn't engage (exclusive predicate, + // stems graph presence, native transport ownership, element song). + if (window._asioDiagEnabled?.()) { + const d = 'running=' + running + ' exclusive=' + exclusive + + ' stems=' + !!stems + ' songAudio=' + !!songAudio + + ' juceMode=' + !!window._juceMode + + ' elementSong=' + elementSong + + ' want=' + want + ' mode=' + _mode; + if (d !== window._lastRendererBusDecision) { + window._lastRendererBusDecision = d; + console.log('[asio-diag] renderer-bus:', d); + } + } + + const stemsGraphChanged = _mode === 'stems' && stems !== _stemsGraph; if (want !== _mode || stemsGraphChanged) { await _disengage();