Skip to content

feat(audio): route feedpak full-mix natively under WASAPI-exclusive output (Phase 1) - #824

Merged
byrongamatos merged 1 commit into
mainfrom
feat/feedpak-exclusive-native-routing
Jul 9, 2026
Merged

byrongamatos merged 1 commit into
mainfrom
feat/feedpak-exclusive-native-routing

Conversation

@OmikronApex

@OmikronApex OmikronApex commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Song playback runs through the Electron renderer (WebAudio / <audio>). When the desktop engine opens the output endpoint in WASAPI exclusive mode (or via most ASIO drivers), Windows silences every other client on the endpoint — including our own renderer, so songs go silent while guitar monitoring keeps working. Root cause + fix options in #89; this implements Slice 1 (Phase 1 of the native-song-playback plan).

What this does

  • Exclusive-output detection (renderer, shared static code): the share mode is the JUCE output device type — getCurrentDevice().outputType === 'Windows Audio (Exclusive Mode)' || 'ASIO' ("Low Latency Mode" is shared and does not match). Evaluated per watcher tick, so mid-song device switches re-route both directions.
  • Eligibility (highway.js): feedpak full-mix songs are now JUCE-routable, gated on exclusive output. Full-mix covers both real-world pack shapes: stem-less original_audio packs and single-stem packs (stems: [full.ogg] — what packs in the wild actually use). Multi-stem (>1) packs stay on the WebAudio path until Phase 2. In shared mode nothing changes — the gate is load-bearing so the stem mixer keeps working.
  • /api/audio-local-path (server.py): now resolves /api/sloppak/<fn>/file/<rel> URLs to the unpacked cache path, via a _resolve_sloppak_local_file() helper shared with serve_sloppak_file so both endpoints apply identical containment/traversal guards.
  • Diagnostics: [feedpak-route] log lines at each routing decision (transition-gated, not per-tick) — captured by the diagnostics export bundle; already proved out during tester debugging.

Docker sphere

Unaffected: all new behavior is behind the window.feedBackDesktop.audio bridge presence + exclusive-output predicate; pure-browser deployments never hit it.

Tests

  • tests/js/juce_engine_reroute.test.js: +4 cases — exclusive→JUCE, ASIO→JUCE, shared stays HTML5 (incl. Low Latency Mode), mid-song exclusive→shared migrates back.
  • tests/test_audio_local_path.py: +5 cases — sloppak resolution happy path, percent-encoding, rel/filename traversal (403), unconfigured DLC.
  • Full suites green: 948 JS, 25 pytest (audio-local-path + sloppak traversal).
  • Field-verified on tester build fix11 (WASAPI Exclusive, single-stem feedpaks audible; shared mode unchanged).

Companion (desktop repo): pin test for the JUCE type-name strings the predicate depends on.

Refs #89.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for opening local audio paths from packaged content URLs in desktop mode.
    • Improved audio routing behavior so certain packaged full-mix tracks can now use the desktop audio engine when the output device supports it.
  • Bug Fixes

    • Tightened path handling and security checks for packaged file access, including safer decoding and traversal protection.
    • Improved routing stability when switching between audio output modes, with better fallback behavior and logging.

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

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR centralizes sloppak file path resolution into a shared server helper reused by two endpoints, and adds JUCE output-mode detection so feedpak full-mix songs only route to JUCE when the active output is exclusive-style (Exclusive Mode/ASIO), with corresponding test coverage.

Changes

Server sloppak path resolution

Layer / File(s) Summary
Shared resolver helper
server.py
Adds _resolve_sloppak_local_file for containment-enforcing sloppak resolution and refactors serve_sloppak_file to delegate to it.
audio-local-path sloppak support
server.py
Extends audio_local_path to decode and resolve /api/sloppak/.../file/... URLs via the shared helper, returning path or JSON error.
Sloppak resolution tests
tests/test_audio_local_path.py
Adds a DLC-backed fixture and tests for resolution, percent-decoding, traversal rejection (403), and missing DLC (404), replacing a prior generic 400 test.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Exclusive-Output JUCE Routing for Feedpak Full-Mix

Layer / File(s) Summary
Exclusive output detection
static/app.js
Adds _outputIsExclusive probe via juceApi.getCurrentDevice() and exposes window._juceOutputIsExclusive.
Feedpak full-mix eligibility flags
static/highway.js
Computes isAudioUrl/isFeedpakFullMix, stores them in window._currentSongAudio, and gates JUCE routing on output exclusivity for full-mix songs.
Per-tick reroute evaluation
static/app.js
Updates _reevaluateJuceRouting guard/eligibility logic for feedpak full-mix and output exclusivity, clears rejected-URL memo on stop, adds fetch failure logging.
Routing watcher tests
tests/js/juce_engine_reroute.test.js
Adds outputType/getCurrentDevice() support to test sandbox and new tests for JUCE migration across output mode transitions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HighwayJS as highway.js
  participant AppJS as app.js
  participant JuceApi as juceApi
  participant Server as server.py

  HighwayJS->>HighwayJS: compute isFeedpakFullMix on song_info
  HighwayJS->>JuceApi: isAudioRunning()
  HighwayJS->>AppJS: read window._juceOutputIsExclusive
  AppJS->>JuceApi: getCurrentDevice()
  JuceApi-->>AppJS: outputType
  AppJS-->>HighwayJS: exclusive boolean
  alt exclusive and running
    HighwayJS->>AppJS: proceed with JUCE routing
    AppJS->>Server: GET /api/audio-local-path (sloppak URL)
    Server->>Server: _resolve_sloppak_local_file(filename, rel_path)
    Server-->>AppJS: resolved local path or error
    AppJS->>JuceApi: loadBackingTrack(localPath)
  else not exclusive
    HighwayJS->>AppJS: stay on HTML5 playback
  end
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: native routing of feedpak full-mix playback under exclusive desktop audio output.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 feat/feedpak-exclusive-native-routing

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


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

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

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@server.py`:
- Around line 13946-13956: Update the sloppak URL parsing in the
`/api/audio-local-path` handling so it splits on the first `/file/` delimiter
instead of using the current greedy `re.fullmatch` capture in the `slop_match`
block. Adjust the regex or parsing logic so `_resolve_sloppak_local_file`
receives the correct pak name and relative path even when the in-pack path
contains `file/`, and add a regression test like
`test_sloppak_url_rel_path_can_contain_file_segment` to verify
`/api/sloppak/song.sloppak/file/stems/file/full.ogg` resolves successfully.
- Around line 12965-12975: The cache and path validation in the sloppak serving
flow is too strict on junctioned subfolders and can return stale content from
get_cached_source_dir(). Update the request path logic around
resolved.relative_to(dlc.resolve()) and the src lookup so it stays DLC-relative,
always routes through resolve_source_dir() on each request, and avoids serving
an unpacked directory without revalidating it. Also narrow the broad except in
this block to only the expected resolver failure cases.

In `@static/app.js`:
- Around line 4948-4953: The audio-local-path fetch path in the browser route
treats every non-OK response as transient, so persistent 4xx lookup failures
keep retrying on every poll. Update the logic around the `/api/audio-local-path`
handling so deterministic client errors are marked as a hard reject after HTML5
playback is restored, and only true transient failures continue to be retried.
Use the existing `catch` and the `rejected` memoization flow in `static/app.js`
to locate and wire the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 351550c5-c511-4238-8664-d3e4e744210c

📥 Commits

Reviewing files that changed from the base of the PR and between 950e348 and ec63235.

📒 Files selected for processing (5)
  • server.py
  • static/app.js
  • static/highway.js
  • tests/js/juce_engine_reroute.test.js
  • tests/test_audio_local_path.py

Comment thread server.py
Comment on lines 12965 to +12975
try:
filename = resolved.relative_to(dlc.resolve()).as_posix()
except ValueError:
# safe_join already proved containment, so this is unreachable in
# practice; fail closed rather than fall back to the raw param.
return JSONResponse({"error": "forbidden"}, 403)
# safe_join already proved containment; fail closed regardless.
return ("forbidden", 403)
src = sloppak_mod.get_cached_source_dir(filename)
if src is None:
try:
src = sloppak_mod.resolve_source_dir(filename, dlc, SLOPPAK_CACHE_DIR)
except Exception:
return JSONResponse({"error": "not found"}, 404)
return ("not found", 404)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the resolver/cache contracts without executing repository code.
rg -n -C4 'def _resolve_dlc_path|def _resolve_sloppak_local_file|def resolve_source_dir|def get_cached_source_dir|test_junction_subfolder_allowed' server.py lib tests

Repository: got-feedBack/feedBack

Length of output: 2738


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== server.py around _resolve_sloppak_local_file =="
sed -n '12944,12995p' server.py | cat -n

echo
echo "== lib/sloppak.py resolve_source_dir and cache helper =="
sed -n '136,205p' lib/sloppak.py | cat -n

echo
echo "== tests/test_dlc_junction.py relevant test =="
sed -n '1,140p' tests/test_dlc_junction.py | cat -n

Repository: got-feedBack/feedBack

Length of output: 9860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== all sloppak cache callers =="
rg -n -C3 '_source_cache|get_cached_source_dir|resolve_source_dir\(' lib server.py tests

echo
echo "== sloppak-related file serving paths in server.py =="
rg -n -C3 'sloppak_mod\.(is_sloppak|get_cached_source_dir|resolve_source_dir|_source_cache)' server.py

Repository: got-feedBack/feedBack

Length of output: 13371


Avoid caching past the resolver

resolved.relative_to(dlc.resolve()) rejects valid junctioned DLC subfolders, and get_cached_source_dir() can return an unpacked directory without rechecking mtime/size, so replaced or removed sloppaks can serve stale files. Keep the cache key DLC-relative and go through resolve_source_dir() on each request; narrow the broad except while here.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 12974-12974: Do not catch blind exception: Exception

(BLE001)

🤖 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 `@server.py` around lines 12965 - 12975, The cache and path validation in the
sloppak serving flow is too strict on junctioned subfolders and can return stale
content from get_cached_source_dir(). Update the request path logic around
resolved.relative_to(dlc.resolve()) and the src lookup so it stays DLC-relative,
always routes through resolve_source_dir() on each request, and avoids serving
an unpacked directory without revalidating it. Also narrow the broad except in
this block to only the expected resolver failure cases.

Source: Linters/SAST tools

Comment thread server.py
Comment on lines +13946 to +13956
slop_match = re.fullmatch(r"/api/sloppak/([^?#]+)/file/([^?#]+)", url)
if slop_match:
from urllib.parse import unquote

result = _resolve_sloppak_local_file(
unquote(slop_match.group(1)), unquote(slop_match.group(2))
)
if isinstance(result, tuple):
error, status = result
return JSONResponse({"error": error}, status_code=status)
return JSONResponse({"path": str(result)})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Split sloppak URLs at the first /file/ delimiter.

Line 13946’s greedy first capture misparses valid in-pack paths such as stems/file/full.ogg as part of the sloppak filename, returning 404 for an existing file.

Proposed fix and regression test
-    slop_match = re.fullmatch(r"/api/sloppak/([^?#]+)/file/([^?#]+)", url)
+    slop_match = re.fullmatch(r"/api/sloppak/([^?#]+?)/file/([^?#]+)", url)
def test_sloppak_url_rel_path_can_contain_file_segment(dlc_client):
    tc, _server, dlc = dlc_client
    pak = _make_sloppak(dlc)
    nested = pak / "stems" / "file"
    nested.mkdir()
    (nested / "full.ogg").write_bytes(b"OggS-fake")

    r = tc.get(
        "/api/audio-local-path",
        params={"url": "/api/sloppak/song.sloppak/file/stems/file/full.ogg"},
    )

    assert r.status_code == 200, r.text
    assert r.json()["path"] == str((nested / "full.ogg").resolve())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
slop_match = re.fullmatch(r"/api/sloppak/([^?#]+)/file/([^?#]+)", url)
if slop_match:
from urllib.parse import unquote
result = _resolve_sloppak_local_file(
unquote(slop_match.group(1)), unquote(slop_match.group(2))
)
if isinstance(result, tuple):
error, status = result
return JSONResponse({"error": error}, status_code=status)
return JSONResponse({"path": str(result)})
slop_match = re.fullmatch(r"/api/sloppak/([^?#]+?)/file/([^?#]+)", url)
if slop_match:
from urllib.parse import unquote
result = _resolve_sloppak_local_file(
unquote(slop_match.group(1)), unquote(slop_match.group(2))
)
if isinstance(result, tuple):
error, status = result
return JSONResponse({"error": error}, status_code=status)
return JSONResponse({"path": str(result)})
🤖 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 `@server.py` around lines 13946 - 13956, Update the sloppak URL parsing in the
`/api/audio-local-path` handling so it splits on the first `/file/` delimiter
instead of using the current greedy `re.fullmatch` capture in the `slop_match`
block. Adjust the regex or parsing logic so `_resolve_sloppak_local_file`
receives the correct pak name and relative path even when the in-pack path
contains `file/`, and add a regression test like
`test_sloppak_url_rel_path_can_contain_file_segment` to verify
`/api/sloppak/song.sloppak/file/stems/file/full.ogg` resolves successfully.

Comment thread static/app.js
Comment on lines +4948 to +4953
if (!res.ok) {
console.warn('[feedpak-route] audio-local-path HTTP', res.status, 'for', url);
throw new Error('HTTP ' + res.status);
}
const { path } = await res.json();
console.log('[feedpak-route] audio-local-path resolved:', (typeof path === 'string' && path.split(/[\\/]/).pop()) || '<missing>');

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid retrying persistent local-path lookup failures every poll.

Line 4950 turns every non-OK /api/audio-local-path response into a transient throw. Since only 'rejected' is memoized later, a persistent 400/403/404 will pause/resume HTML5 and log again every 350ms. Mark deterministic 4xx lookup failures as a hard reject after restoring HTML5 playback.

Proposed direction
             if (!res.ok) {
                 console.warn('[feedpak-route] audio-local-path HTTP', res.status, 'for', url);
-                throw new Error('HTTP ' + res.status);
+                const err = new Error('HTTP ' + res.status);
+                err.hardReject = res.status >= 400 && res.status < 500;
+                throw err;
             }

Then in the existing catch, after restoring/reporting the browser route:

-            throw err;
+            if (err && err.hardReject) return 'rejected';
+            throw err;
🤖 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 `@static/app.js` around lines 4948 - 4953, The audio-local-path fetch path in
the browser route treats every non-OK response as transient, so persistent 4xx
lookup failures keep retrying on every poll. Update the logic around the
`/api/audio-local-path` handling so deterministic client errors are marked as a
hard reject after HTML5 playback is restored, and only true transient failures
continue to be retried. Use the existing `catch` and the `rejected` memoization
flow in `static/app.js` to locate and wire the fix.

@byrongamatos
byrongamatos merged commit 1b31780 into main Jul 9, 2026
5 checks passed
byrongamatos pushed a commit that referenced this pull request Jul 9, 2026
…r exclusive mode (Phase 2) (#828)

* 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>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
byrongamatos pushed a commit that referenced this pull request Jul 10, 2026
* 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>
byrongamatos added a commit that referenced this pull request Jul 11, 2026
…nder exclusive/ASIO (#877)

* 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>

* feat(audio): loopback feeder mode — all app audio under exclusive/ASIO

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>

* feat(audio): loopback feeder mode — all app audio under exclusive/ASIO

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>

* fix(audio): close loopback capture context on teardown (release tap worklet)

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>

* feat(diag): install-time + uncaught-error diagnostics for the reroute 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>

* fix(static): force conditional revalidation on /static (Cache-Control: 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>

* fix(diag): gate install-time + uncaught-error [asio-diag] lines on --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>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
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