Skip to content

Exclusive-mode output: feedpak/sloppak song audio is silent (stems bypass the native backing funnel) #89

Description

@ChrisBeWithYou

Handoff → @OmikronApex. Follow-up to the WASAPI distortion hunt, picking up the item you scoped out of #86. The docs/native-song-playback-plan.md you referenced never landed on any branch, so this reconstructs the plan from the code — root cause is pinned to a specific gate, with both a cheap-interim and a proper fix. Findings gathered by @ChrisBeWithYou.

Context / status

This tracks the one remaining known issue from the WASAPI distortion hunt (tester: MajorMokoto, RTC + Komplete Audio; reproduced by @OmikronApex). The distortion / digital-static cluster is already fixed and awaiting merge:

#86 explicitly scopes this out: "song audio silent with exclusive output — song playback runs through the renderer; plan exists separately."

Symptom

Exclusive output mode: guitar monitoring is clean, but feedpak song audio is silent (playback pauses / won't advance). Console shows:

The AudioContext encountered an error from the audio device or the WebAudio renderer.

Shared-mode WASAPI works. Tester repro'd on a feedpak (Accept - Metal Heart).

Root cause (confirmed in code)

Device-ownership conflict. The native JUCE engine (guitar in/out + VST/NAM) opens the device in exclusive mode, so it holds the output device exclusively. Chromium's WebAudio / HTML5 <audio> — the renderer path song playback normally uses — then cannot open that same output device → the AudioContext error above → silence.

Why loose-folder songs survive but feedpak songs don't. The core already has a native-backing funnel that migrates song audio onto the engine's own backing transport when the engine is running:

  • static/app.js_installJuceEngineRoutingWatcher() / window.jucePlayerloadBackingTrack / startBacking / seekBacking (desktop src/main/audio-bridge.tsAudioEngine::loadBackingTrack, a JUCE AudioTransportSource mixed into the engine output — so it plays fine under exclusive mode).

But feedpak/sloppak songs are gated out of that funnel:

  • static/highway.js:3414const isAudioUrl = msg.audio_url.startsWith('/audio/');window._currentSongAudio = { url: msg.audio_url, juceEligible: isAudioUrl };
  • static/app.js:5122-5123// Only /audio/ songs are JUCE-routable; sloppak stems stay on HTML5. if (!songAudio || !songAudio.juceEligible) return;

Loose-folder songs are served at /audio/… (single mixed file) → juceEligible = true → routed through JUCE → survive exclusive mode. Sloppak/feedpak songs are served at /api/sloppak/…juceEligible = false → stay on HTML5/WebAudio → die in exclusive output. feedpak is the primary format, so most real songs hit this.

Why stems were excluded (two reasons):

  1. /api/sloppak/… stem URLs aren't resolvable to a single local file via /api/audio-local-path (what loadBackingTrack needs).
  2. More fundamentally, stems need per-stem mixing — the stems plugin mutes the core <audio> and drives its own WebAudio graph with per-stem faders (static/audio-mixer.js:142 "Multi-stem sloppak: the stems plugin mutes the core element … drive that master instead"). A single-file loadBackingTrack transport can't reproduce a multi-stem mix.

Fix options

Option A — native single-mix fallback (small, interim). feedpak always ships stems/full.ogg (per feedpak-spec). Expose a resolvable local path for it and mark the full-mix juceEligible, so in exclusive mode the funnel loads that single mixed file through JUCE. Cost: per-stem mixing is unavailable in exclusive mode only (acceptable degradation — the user gets song audio back). Smallest change; unblocks the common case.

Option B — mix the WebAudio master into the engine output bus (proper, general). Feed the renderer's song/stem master into the engine's output bus so it mixes with guitar and rides out the exclusively-held device. Reuse the existing stream-bus / backing-mix plumbing (setStreamBus(includeBacking, includeGuitar, gain) at src/audio/NodeAddon.cpp:1371, backing mix bus + getBackingLevel). Added latency lands only on song audio (~10–20ms per @OmikronApex's estimate) — the guitar-monitoring path is untouched, and backing latency isn't perceptually critical. Works for all songs including full stem mixing. This is the real fix; shares infrastructure with #48 (streamer mix outputs).

Rejected:

  • Exclusive input + shared output — tester confirmed unplayable latency (split-mode output ring floor).
  • WASAPI loopback / OS capture — Windows 10+ only, heavier; keep as last resort.

Key pointers

  • static/highway.js:3414juceEligible = audio_url.startsWith('/audio/') (the gate)
  • static/app.js:5119-5148_reevaluateJuceRouting() (the reroute watcher; the juceEligible early-return)
  • static/app.js:4880-5010_switchHtml5ToJuce() (HTML5→JUCE migration; the reject/fallback path)
  • static/audio-mixer.js:142-147 — stems plugin owns the master when a multi-stem sloppak is loaded
  • src/audio/AudioEngine.cpp:1248 loadBackingTrack + src/main/audio-bridge.ts:1192 bridge — the native transport the funnel already targets
  • src/audio/NodeAddon.cpp:1371 setStreamBus — the mix-bus Option B would extend

Related / secondary observations from the session (triage separately)

Metadata

Metadata

Assignees

Labels

audioAudio engine, latency, routing, VST/NAMbugSomething isn't workingpriority:highMust fix / ship for next release

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions