feat(audio): loopback feeder mode + static no-cache — all app audio under exclusive/ASIO#877
Conversation
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>
…e 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>
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>
main already contains the renderer-bus Phase 2 feeder; conflicts were the [asio-diag] insertions vs identical upstream context — kept ours. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tester-confirmed (2026-07-11 log): song previews and other plugin-private audio bypass the per-surface feeder taps and leak to the default WASAPI device under ASIO output. Also confirmed: the element capture path poisons itself when highway_3d already owns #audio's one-shot MediaElementSource (InvalidStateError with _elCtx assigned pre-throw → TypeError every later tick). - New preferred mode 'loopback': one getDisplayMedia frame-audio capture (desktop main answers with the app's own frame) covers song, previews, and UI sounds for the whole exclusive session — engages even with no song loaded. Local playback silenced via suppressLocalAudioPlayback, page-mute IPC fallback otherwise. - Sticky fallback to the existing stems/element surface modes when capture is unavailable (old desktop main, denied, Docker sphere). - Element capture: assign module state only after the whole chain succeeds; close the context on failure — collision now retries clean. - Failed engage now disables the bus and tears down loopback (no more bus-enabled-with-no-producer stranding). - Tests: 12 (5 new — loopback engage/preference/mute-fallback/sticky fallback, collision retry). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tester-confirmed (2026-07-11 log): song previews and other plugin-private audio bypass the per-surface feeder taps and leak to the default WASAPI device under ASIO output. Also confirmed: the element capture path poisons itself when highway_3d already owns #audio's one-shot MediaElementSource (InvalidStateError with _elCtx assigned pre-throw → TypeError every later tick). - New preferred mode 'loopback': one getDisplayMedia frame-audio capture (desktop main answers with the app's own frame) covers song, previews, and UI sounds for the whole exclusive session — engages even with no song loaded. Local playback silenced via suppressLocalAudioPlayback, page-mute IPC fallback otherwise. - Sticky fallback to the existing stems/element surface modes when capture is unavailable (old desktop main, denied, Docker sphere). - Element capture: assign module state only after the whole chain succeeds; close the context on failure — collision now retries clean. - Failed engage now disables the bus and tears down loopback (no more bus-enabled-with-no-producer stranding). - Tests: 12 (5 new — loopback engage/preference/mute-fallback/sticky fallback, collision retry). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orklet) The loopback context was reused across engages (_lbCtx || new), but teardown only stopped the stream + deactivated the tap — never closing the context or detaching the worklet node. Each exclusive<->shared switch orphaned a live tap worklet on the long-lived context. Use a fresh context per session and close it on disengage. Adds a test asserting the context is closed on teardown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… chain 2026-07-11 tester log showed the routing watcher and renderer-bus feeder never installed (zero [feedpak-route]/[renderer-bus] lines) plus an uncaught SyntaxError with no source location — nothing in the log said why. New: - global error/unhandledrejection tap logging message + filename:line:col (error events carry the location even for parse errors in other scripts) - explicit install / NOT-installed lines for watcher and feeder (incl. loopback capability probe) - DOMException detail (name/message/stack head) in the feeder retry warn — the console-message forward stringified it to [object DOMException] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…: no-cache) Without Cache-Control Chromium's heuristic freshness (10% of file age) serves /static/app.js from disk cache for hours-to-days without revalidating. Desktop consequence: a new build's window ran the previous build's app.js — the 2026-07-11 ASIO investigation traced 'routing watcher never installed' + a stems module-plugin SyntaxError to exactly this (stale loader predating scriptType support). no-cache keeps caching but revalidates via ETag — unchanged files still cost only a 304. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…debug The error tap and install lines from the previous diag commit were unconditional. Now: error/rejection taps check _asioDiagEnabled() at event time; install lines log deferred once the async debugEnabled() resolves true. The NOT-installed anomaly lines stay bridge-gated (window.feedBackDesktop present) instead — a broken bridge can't deliver the debug flag, they fire at most once, and only in the broken state they exist to witness. Docker sphere: fully silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Squash-merged diag/feeder work already in main; conflict was the deferred install-line gating vs main's plain flag resolve — kept ours. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe renderer-bus feeder adds whole-app loopback capture through ChangesRenderer-bus loopback capture and diagnostics
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant RendererBusFeeder
participant DisplayMedia
participant LoopbackAudioContext
participant EngineRendererBus
RendererBusFeeder->>DisplayMedia: request display audio
DisplayMedia-->>RendererBusFeeder: provide captured stream
RendererBusFeeder->>LoopbackAudioContext: create stream source and tap
LoopbackAudioContext->>EngineRendererBus: route app audio
RendererBusFeeder->>EngineRendererBus: disable bus on failure or disengage
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Kept theirs: fresh loopback context per session + close() on teardown (orphaned-worklet fix) and its test. Kept ours: deferred debug-gated install lines, cache-control, decision-vector diag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Companion desktop PR: got-feedBack/feedBack-desktop#98 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/js/renderer_bus_feeder.test.js (1)
277-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering post-context-creation engage failure.
The suite tests the pre-context failure path (
getDisplayMediarejection) but not a failure after the loopbackAudioContextis created (e.g.tap.attachorsetRendererBusthrowing). That path exercises_teardownLoopback()from the reevaluate catch — the "failed engagement tears down loopback resources" behavior called out in the PR objectives. A test assertinglbCtx.closed === true, the stream stopped, and page unmuted after such a failure would lock in that contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/js/renderer_bus_feeder.test.js` around lines 277 - 290, Add a test covering engagement failure after loopback AudioContext creation, such as throwing from tap.attach or setRendererBus, through _reevaluateRendererBus. Assert that _teardownLoopback closes the loopback context, stops the captured stream, and unmutes the page, while preserving the existing fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/js/renderer_bus_feeder.test.js`:
- Around line 277-290: Add a test covering engagement failure after loopback
AudioContext creation, such as throwing from tap.attach or setRendererBus,
through _reevaluateRendererBus. Assert that _teardownLoopback closes the
loopback context, stops the captured stream, and unmutes the page, while
preserving the existing fallback behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2d45febb-5039-4140-952f-9701bf980a8b
📒 Files selected for processing (2)
static/app.jstests/js/renderer_bus_feeder.test.js
Builds on the merged renderer-bus work (#852/#865). Companion desktop PR: got-feedBack/feedBack-desktop#98.
Loopback feeder mode (new preferred)
One
getDisplayMediaframe-audio capture covers every sound the app makes — song, previews, UI — for the whole exclusive/ASIO session (engages even with no song loaded; fixes tester report of previews/UI on the default WASAPI device). Local playback silenced viasuppressLocalAudioPlayback(spike-verified), page-mute IPC fallback otherwise. Sticky fallback to the existing stems/element surface modes when capture is unavailable; Docker sphere untouched.Element-capture poisoning fix (tester log, confirmed)
createMediaElementSource(#audio)collides with highway_3d's one-shot analyser tap;_elCtxwas assigned pre-throw → every later tick died in a TypeError while the song played on the default device. Module state now assigned only on full success; failed engage disables the bus and tears down loopback./static Cache-Control: no-cache
Root cause of the fix14 tester regression: without Cache-Control, Chromium's heuristic freshness served a previous build's
app.jsfor days — the routing watcher/feeder never ran and the (old) plugin loader broke module plugins.no-cachekeeps caching but forces ETag revalidation (unchanged files = 304). Benefits the Docker sphere equally.Diagnostics (gated on desktop --debug via
audio.debugEnabled())Install/NOT-installed tracing for watcher + feeder, global uncaught-error tap with file:line:col, full device object on outputType change, feeder decision vector, sink flips, DOMException details. Docker sphere: fully silent.
Verification
31/31 JS tests (12 feeder — 5 new for loopback + collision retry — and 19 reroute);
node --checkclean; fix15 portable build smoke-tested cold-launch end-to-end.🤖 Generated with Claude Code
Summary by CodeRabbit
/staticresponses to default to no-cache behavior.