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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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