Skip to content

feat(song-info): publish the playable stem list so stems can preload (fixes the 698ms freeze) - #972

Merged
byrongamatos merged 2 commits into
mainfrom
feat/song-info-playable-stems
Jul 14, 2026
Merged

byrongamatos merged 2 commits into
mainfrom
feat/song-info-playable-stems

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

The stems plugin could only learn its stem list from the highway's WS ready — which arrives once the highway is already up. So it fetched, decoded, and then handed every stem's PCM to its audio worklet, copying the whole song, with the player on screen.

For a 4-minute 6-stem pack that is over half a gigabyte of memcpy, in one frame, on the main thread. Measured on a real load:

frame times (ms): ... 10.3, 9.3, 10.2, 10.3, 9.7, 697.9, 6.7 ...
                                              ^^^^^ right as the credits card appears
hottest: buildGraphFromBuffers [stems/src/main.js]

That is the "the video pauses when the author appears" report — the venue video's texture simply isn't drawn for 0.7s.

GET /api/song/{f}?stems=1 now returns the same list ([{id, url, default}] + full_mix_url), so the plugin can start the whole load at song:loading, before the highway and the venue are drawn, where a stalled frame costs nothing. Nothing about the work changes; only when.

Opt-in via the query param, so the library's metadata calls — the hot path — pay nothing. Deliberately not cached: the metadata cache is a fixed-column table, and widening it would mean a schema migration plus a stale row for every already-scanned song, to cache what is a plain manifest read on an already-unpacked pack.

The safety property

REST and the WS must publish the same list. If they disagreed, the plugin would preload a graph and then throw it away and rebuild — strictly worse than not preloading. So both now resolve default through one shared helper (stem_default_on, extracted from load_song), and the test rebuilds the WS's payload from load_song and requires the REST helper to produce the identical list — pinned against each other, not against a snapshot.

Also pinned: the mixdown is lifted out of the stem list (spec §5.3 — listing full beside the instruments would play the whole song on top of the stems) while staying reachable as full_mix_url; a single-full pack keeps it as its only playable stem; an unreadable pack yields an empty list rather than failing the request.

Full suite 2608 passed. Consumed by feedBack-plugin-stems.

Summary by CodeRabbit

  • New Features
    • Added an opt-in stems parameter to GET /api/song/{filename} to return playable stem details (id, url, default) along with full_mix_url when available, matching the existing WebSocket “ready” payload behavior.
  • Bug Fixes
    • Improved normalization of stem default values (including common off variants) for consistent playable-stem metadata.
    • Corrupt or unreadable song packs now return an empty stems list with full_mix_url: None instead of failing.

The stems plugin could only learn its stem list from the highway's WS `ready`,
which arrives once the highway is already up. So it fetched, decoded, and then
handed every stem's PCM to its audio worklet — copying the WHOLE SONG — with the
player already on screen.

For a 4-minute 6-stem pack that is over half a GIGABYTE of memcpy, in one frame,
on the main thread. Measured on a real load: a 698 ms frame, right as the
song-credits card appeared, with the venue video visibly stopping. That is the
"the video pauses when the author appears" report.

GET /api/song/{f}?stems=1 now returns the same list — [{id, url, default}] plus
full_mix_url — so the plugin can start the whole load at `song:loading`, before
the highway (and the venue) is drawn, where a stalled frame costs nothing.
Nothing about the work changes; only WHEN.

Opt-in via the query param so the library's own metadata calls — the hot path —
pay nothing. Deliberately NOT stored in the metadata cache: that is a
fixed-column table, and widening it would mean a schema migration plus a stale
row for every song already scanned, to cache something that is a plain manifest
read on an already-unpacked pack.

The safety property: REST and the WS must publish the SAME list. If they
disagreed the plugin would preload a graph and then throw it away and rebuild —
strictly worse than not preloading. So both now resolve `default` through one
shared helper (stem_default_on, extracted from load_song), and a test rebuilds
the WS's payload from load_song and requires the REST helper to produce the
identical list, rather than pinning either against a snapshot.

Also pinned: the mixdown is lifted OUT of the stem list (spec 5.3 — `full` is
not a layer; listing it beside the instruments would play the whole song on top
of the stems) while staying reachable as full_mix_url, a single-`full` pack keeps
it as its only playable stem, and an unreadable pack yields an empty list rather
than failing the request. Full suite 2608 passed.

Consumed by feedBack-plugin-stems (preloadSong).
@coderabbitai

coderabbitai Bot commented Jul 14, 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: 66c51648-92a2-4b61-9c5e-bc1d669bcc85

📥 Commits

Reviewing files that changed from the base of the PR and between b7b28de and 98a2702.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • lib/routers/song.py
  • lib/sloppak.py
  • tests/test_song_info_stems.py

📝 Walkthrough

Walkthrough

The song-info endpoint optionally returns playable stem metadata and a full-mix URL. Shared default parsing and metadata loading align REST responses with WebSocket-built song data, with tests covering partitioning, legacy packs, parity, and corrupt packs.

Changes

Playable stems

Layer / File(s) Summary
Normalize stem metadata
lib/sloppak.py
Shared default parsing is used when load_song() builds normalized stem descriptors.
Add optional song-info stems response
lib/routers/song.py, CHANGELOG.md
The song-info route accepts stems=1, augments cached and freshly extracted metadata, and documents the response shape.
Validate REST and WS stem parity
tests/test_song_info_stems.py
Tests cover default handling, reserved and legacy full-mix behavior, REST/WS parity, and corrupt-pack handling.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant get_song_info
  participant _with_stems
  participant _playable_stems_payload
  participant sloppak.load_song
  Client->>get_song_info: Request with stems=1
  get_song_info->>_with_stems: Augment song metadata
  _with_stems->>_playable_stems_payload: Extract stems payload
  _playable_stems_payload->>sloppak.load_song: Load sloppak
  sloppak.load_song-->>_playable_stems_payload: Return normalized stems
  _playable_stems_payload-->>_with_stems: Return playable and full-mix URLs
  _with_stems-->>Client: Return augmented song metadata
Loading

Possibly related PRs

Suggested reviewers: topkoa, chrisbewithyou

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, but it does not follow the required template sections and omits the checklist and feedpak surface confirmation. Add the template headings: What, feedpak surface, and Checklist, and explicitly state whether feedpak read/write behavior changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: opt-in playable stems for song info to support preloading.
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/song-info-playable-stems

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: 2

🤖 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 `@lib/routers/song.py`:
- Around line 874-881: Update CHANGELOG.md’s [Unreleased] section to document
the new public `stems` query parameter supported by `get_song_info`, including
that `?stems=1` returns playable stem metadata and URLs.

In `@lib/sloppak.py`:
- Around line 1316-1322: The full_mix_file mapping in extract_meta must match
load_song’s legacy fallback behavior. Reuse the existing full-stem resolution
and validation path, including deprecated original_audio when no reserved full
stem exists, so REST and WS expose the same full-mix URL; add a fixture covering
a pack that requires this fallback.
🪄 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: 7d26464b-2e03-45e8-91eb-8a66517ccce4

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0e3c5 and b7b28de.

📒 Files selected for processing (3)
  • lib/routers/song.py
  • lib/sloppak.py
  • tests/test_song_info_stems.py

Comment thread lib/routers/song.py
Comment thread lib/sloppak.py Outdated
Comment on lines +1316 to +1322
# The PLAYABLE stems (id/file/default), partitioned exactly as load_song
# does — the mixdown lifted out, never a layer. get_song_info turns these
# into URLs so the stems plugin can start fetching and decoding on
# `song:loading`, instead of waiting for the highway's WS `ready`.
"stems": instrument_stems,
# The mixdown, when the pack carries one (spec §5.3). Same reason.
"full_mix_file": (_full or {}).get("file") or None,

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 | 🟠 Major | ⚡ Quick win

Preserve legacy full-mix parity.

load_song() falls back to deprecated original_audio when no reserved full stem exists, but extract_meta() returns None here. Those packs therefore expose a WS full_mix_url but no REST full_mix_url. Reuse the same legacy resolution/validation path and add a parity fixture for it.

🤖 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 `@lib/sloppak.py` around lines 1316 - 1322, The full_mix_file mapping in
extract_meta must match load_song’s legacy fallback behavior. Reuse the existing
full-stem resolution and validation path, including deprecated original_audio
when no reserved full stem exists, so REST and WS expose the same full-mix URL;
add a fixture covering a pack that requires this fallback.

…ent it

CodeRabbit caught a real bug, and it would have hit most real libraries.

load_song() falls back to the DEPRECATED `original_audio:` key when a pack has no
reserved `full` stem — which is every pack written before feedpak 1.15.0. My
payload rebuilt the full-mix rule from extract_meta and returned None for those:
REST would say "no full mix" while the WS said there was one.

Worse than a wrong field: the plugin would preload a graph WITHOUT the pristine
mix and — because the stem signature still matched — never rebuild. Unity
playback would silently downgrade to the lossy stem recombination.

That is exactly the drift this PR claims to prevent, and my test had a hole: I
only covered packs that carry a `full` stem.

So stop reimplementing. The payload now calls load_song, whose LoadedSloppak
already carries the partitioned stems and the resolved full mix, and builds the
URLs exactly as ws_highway does. Drift is now impossible by construction rather
than by agreement. extract_meta is reverted to its original shape (it never
needed to change), and the shared stem_default_on helper stays as the one place
`default: off` is resolved.

Tests rewritten to compare against load_song — the WS's own function — for a
reserved-`full` pack, a LEGACY original_audio pack (the case that was broken), and
a single-`full` pack. Also documents the `?stems=1` contract in CHANGELOG.md.
Full suite green.
@byrongamatos

Copy link
Copy Markdown
Contributor Author

@coderabbitai the legacy full-mix finding is addressed in 98a2702, and it was a real bug that would have hit most real libraries.

You were right: load_song() falls back to the deprecated original_audio: key when a pack has no reserved full stem — which is every pack written before feedpak 1.15.0. My payload reimplemented the full-mix rule from extract_meta and returned None for those, so REST would say "no full mix" while the WS said there was one.

Worse than a wrong field: the stem signature would still match, so the plugin would preload a graph without the pristine mix and never rebuild — unity playback silently downgrading to the lossy stem recombination. That is exactly the drift this PR claims to prevent, and my test had a hole (I only covered packs carrying a full stem).

The fix is structural rather than another special case: _playable_stems_payload now calls load_song itself — the same function the WS uses — whose LoadedSloppak already carries the partitioned stems and the resolved full mix, and builds the URLs exactly as ws_highway does. Drift is now impossible by construction instead of by agreement. extract_meta is reverted to its original shape (it never needed to change).

Tests rewritten to compare against load_song for a reserved-full pack, a legacy original_audio pack (the case that was broken), and a single-full pack.

@byrongamatos
byrongamatos merged commit 939c982 into main Jul 14, 2026
6 checks passed
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cd /repo 2>/dev/null || true
git log --oneline -5 2>/dev/null
git show 98a2702 --stat 2>/dev/null
git show 98a2702 -- lib/routers/song.py 2>/dev/null | head -150

Length of output: 8743


@byrongamatos Confirmed — I pulled 98a2702 and the fix is exactly right.

_playable_stems_payload now delegates entirely to sloppak_mod.load_song(filename, dlc, appstate.sloppak_cache_dir) and builds stems/full_mix_url straight off loaded.stems / loaded.full_mix, the same LoadedSloppak the WS path consumes. That closes the exact gap I flagged: REST can no longer diverge from the WS on the original_audio: legacy fallback, since there's only one code path resolving the full mix now instead of two independently-maintained rules.

Good catch on your end tracing the blast radius too — a matching stem signature with a silently-missing pristine mix is a nasty failure mode precisely because nothing would look wrong until someone A/B'd the audio. Delegating to load_song instead of re-deriving the rule is the right structural fix, and reverting extract_meta back to its original shape keeps the blast radius of this PR minimal. Thanks for the thorough writeup and the legacy/single-full test coverage.

(๑˃̵ᴗ˂̵)و good fix

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.

1 participant