feat(diag): --debug ASIO routing diagnostics in static bundle (#852)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex 2026-07-11 00:42:41 +02:00 committed by GitHub
parent 4cc8fa3b4d
commit 76159c16cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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 !== '<no-getCurrentDevice>') {
@ -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();