Skip to content

feat(audio): loopback feeder mode + static no-cache — all app audio under exclusive/ASIO#877

Merged
byrongamatos merged 13 commits into
mainfrom
fix/asio-all-app-audio
Jul 11, 2026
Merged

feat(audio): loopback feeder mode + static no-cache — all app audio under exclusive/ASIO#877
byrongamatos merged 13 commits into
mainfrom
fix/asio-all-app-audio

Conversation

@OmikronApex

@OmikronApex OmikronApex commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Builds on the merged renderer-bus work (#852/#865). Companion desktop PR: got-feedBack/feedBack-desktop#98.

Loopback feeder mode (new preferred)

One getDisplayMedia frame-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 via suppressLocalAudioPlayback (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; _elCtx was 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.js for days — the routing watcher/feeder never ran and the (old) plugin loader broke module plugins. no-cache keeps 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 --check clean; fix15 portable build smoke-tested cold-launch end-to-end.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added whole-app “loopback” audio capture for exclusive-output sessions.
    • Automatically falls back to element or stem capture when loopback can’t be used.
    • Added page playback muting/unmuting when local playback suppression isn’t supported.
  • Bug Fixes
    • Improved capture recovery after setup failures and prevented silent/stranded audio states after failed engagement attempts.
    • Better handling for cases where audio elements are already in use by another capture path.
  • Chores
    • Updated /static responses to default to no-cache behavior.

OmikronApex and others added 12 commits July 9, 2026 19:33
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>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bd498598-b180-401e-895e-53b23c8c2498

📥 Commits

Reviewing files that changed from the base of the PR and between 8f53ea1 and b5429d3.

📒 Files selected for processing (2)
  • server.py
  • static/app.js

📝 Walkthrough

Walkthrough

The renderer-bus feeder adds whole-app loopback capture through getDisplayMedia, hardens element capture initialization, expands engagement states, adds exclusive-session fallback handling, and extends diagnostics, static caching behavior, and tests.

Changes

Renderer-bus loopback capture and diagnostics

Layer / File(s) Summary
Diagnostics and static delivery
static/app.js, server.py
Desktop error diagnostics and deferred ASIO debug resolution are added, renderer-bus diagnostics include loopback availability, and static responses default to Cache-Control: no-cache.
Capture state and lifecycle
static/app.js
Element capture now commits its context only after complete setup. Loopback capture manages display-media streams, audio contexts, taps, and page muting.
Exclusive-session reevaluation and fallback
static/app.js
Exclusive sessions prefer loopback, record unavailable loopback probes, fall back to stems or element capture, and clean up failed engagements.
Loopback and fallback test coverage
tests/js/renderer_bus_feeder.test.js
Test fixtures and scenarios cover loopback capture, teardown, preference, mute fallback, sticky failures, reevaluation, and collision recovery.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately summarizes the main additions: loopback feeder mode for exclusive/ASIO sessions and no-cache static responses.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/asio-all-app-audio

Comment @coderabbitai help to get the list of available commands.

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>
@OmikronApex

Copy link
Copy Markdown
Contributor Author

Companion desktop PR: got-feedBack/feedBack-desktop#98

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/js/renderer_bus_feeder.test.js (1)

277-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering post-context-creation engage failure.

The suite tests the pre-context failure path (getDisplayMedia rejection) but not a failure after the loopback AudioContext is created (e.g. tap.attach or setRendererBus throwing). That path exercises _teardownLoopback() from the reevaluate catch — the "failed engagement tears down loopback resources" behavior called out in the PR objectives. A test asserting lbCtx.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

📥 Commits

Reviewing files that changed from the base of the PR and between 92c86f5 and 8f53ea1.

📒 Files selected for processing (2)
  • static/app.js
  • tests/js/renderer_bus_feeder.test.js

@byrongamatos
byrongamatos merged commit 5b90470 into main Jul 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants