From 304a050e26fd6218e2eca56a1fb25f0fbbc96e25 Mon Sep 17 00:00:00 2001 From: carochacs <79524656+carochacs@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:32:19 -0600 Subject: [PATCH 1/4] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4fe31983..17589e76 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ Thumbs.db *.swp .idea/ plugins/support_creators +/library +/static/sloppak_cache From 3627fb5b80ad429f74a83b2f23017044022b5794 Mon Sep 17 00:00:00 2001 From: carochacs <79524656+carochacs@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:14:01 -0600 Subject: [PATCH 2/4] Add arrangement type to WS and fix keyboard detection Adds explicit `type` field to WebSocket song_info and arrangements, allowing viz auto-selection to match on real instrument type instead of name-sniffing. Fixes misclassification of keyboard arrangements (e.g., GP imports with piano parts labeled 'Combo') by checking manifest `type` before arrangement name patterns. Reorders GP track classification to yield keyboard parts before assuming guitar. Also adds `_stemsRerouteInProgress` guards matching `_juceRerouteInProgress` to prevent spurious play/pause events during stems plugin Web-Audio takeover. --- lib/gp2rs_gpx.py | 12 +++++++++--- lib/routers/ws_highway.py | 8 ++++++++ lib/song.py | 8 ++++++++ plugins/highway_3d/screen.js | 6 ++++++ static/app.js | 2 ++ static/js/transport.js | 6 ++++++ 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/gp2rs_gpx.py b/lib/gp2rs_gpx.py index 75d854a2..361beaeb 100644 --- a/lib/gp2rs_gpx.py +++ b/lib/gp2rs_gpx.py @@ -2469,12 +2469,20 @@ def _auto_select_gpx(tracks: list[dict]) -> tuple[list[int], dict[int, str]]: is_bass = (t['string_pitches'] and max(t['string_pitches']) <= 48) \ or t['midi_program'] in BASS_PROGS - is_guitar = bool(t['string_pitches']) and not is_bass is_keys = (not t['string_pitches'] and t['midi_program'] in KEYS_PROGS) \ or any(kw in name_l for kw in ('piano', 'keys', 'organ')) + # GP6+ often notates piano/keys parts on a fretted string template, so + # string_pitches alone can't distinguish a keyboard part from a real + # guitar. Check is_keys (which also matches on name/program) before + # is_guitar, so a track explicitly named "Keys ..." isn't swept into + # the unhinted-guitar Lead/Rhythm/Combo bucket just because it has + # string tuning data (feedBack: "Combo" mislabeled piano arrangement). + is_guitar = bool(t['string_pitches']) and not is_bass and not is_keys if is_bass: selected.append((i, 'bass')) + elif is_keys: + selected.append((i, 'keys')) elif is_guitar: # Honor "lead"/"rhythm" in the GP track name so two guitars keep # the author's roles instead of being labelled by appearance order @@ -2485,8 +2493,6 @@ def _auto_select_gpx(tracks: list[dict]) -> tuple[list[int], dict[int, str]]: selected.append((i, 'guitar_rhythm')) else: selected.append((i, 'guitar')) - elif is_keys: - selected.append((i, 'keys')) if not selected: for i, t in enumerate(tracks): diff --git a/lib/routers/ws_highway.py b/lib/routers/ws_highway.py index 61cb65b6..0ebecff3 100644 --- a/lib/routers/ws_highway.py +++ b/lib/routers/ws_highway.py @@ -496,6 +496,11 @@ def _evict_audio_cache(): "name": a.name, "smart_name": smart_names[i], "notes": len(a.notes) + sum(len(c.notes) for c in a.chords), + # Manifest `type` (sloppak.py:942) — authoritative instrument + # classification, independent of the display name. Lets viz + # auto-selection (e.g. the piano viz's matchesArrangement) + # match on real type instead of name-sniffing. + "type": (a.type or "").strip().lower() if isinstance(a.type, str) else "", } for i, a in enumerate(song.arrangements) ] @@ -508,6 +513,9 @@ def _evict_audio_cache(): "arrangement": arr.name, "arrangement_smart_name": smart_names[best], "arrangement_index": best, + # Named distinctly from the top-level "type" (WS message + # discriminator, = "song_info") to avoid colliding with it. + "arrangement_type": (arr.type or "").strip().lower() if isinstance(arr.type, str) else "", # Echo the resolved naming mode so highway.js doesn't have to # re-read localStorage (which can be unavailable / disagree with # app.js's in-memory cache when storage writes fail). diff --git a/lib/song.py b/lib/song.py index 3527d112..d154f86c 100644 --- a/lib/song.py +++ b/lib/song.py @@ -803,6 +803,14 @@ def _resolve(a: Arrangement) -> tuple[str | None, bool]: return "path_rhythm", bool(a.bonus_arr) if a.path_bass: return "path_bass", bool(a.bonus_arr) + # The manifest `type` field (sloppak.py:942) is authoritative when + # present — trust it over name-sniffing. A "keys"/"vocals"/"drums" + # arrangement is never part of the Lead/Rhythm/Bass grouping, even + # if its display name happens to collide with the name-fallback + # table below (e.g. a keys arrangement literally named "Combo"). + arr_type = (a.type or "").strip().lower() if isinstance(a.type, str) else "" + if arr_type in ("keys", "vocals", "drums"): + return None, bool(a.bonus_arr) name = a.name if isinstance(a.name, str) else "" entry = _NAME_FALLBACK.get(name.strip().lower()) if entry is None: diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index 44b6775b..855c6f2b 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -16662,6 +16662,12 @@ // arrangements that merely contain these as substrings (e.g. a // "BasslineKeys" arrangement would otherwise match `bass`). window.feedBackViz_highway_3d.matchesArrangement = function (songInfo) { + // Manifest `type: keys` is authoritative and independent of the + // display name — a keys arrangement literally named "Combo" (GP + // import quirk) would otherwise match the /combo/ keyword below + // and steal the song from the piano/keys viz. Yield whenever the + // active arrangement's real type says keys. + if (songInfo && songInfo.arrangement_type === 'keys') return false; const arr = (songInfo && songInfo.arrangement) || ''; return /\b(?:lead|rhythm|bass|combo|guitar)\b/i.test(arr); }; diff --git a/static/app.js b/static/app.js index 6aef021d..76b90f94 100644 --- a/static/app.js +++ b/static/app.js @@ -1001,6 +1001,7 @@ audio.addEventListener('play', () => { // migration step — playback genuinely continues, so don't emit song:play or // flip feedBack.isPlaying (the watcher keeps the canonical state itself). if (window._juceRerouteInProgress) return; + if (window._stemsRerouteInProgress) return; window.feedBack.isPlaying = true; const payload = _songEventPayload(); window.feedBack.emit('song:play', payload); @@ -1011,6 +1012,7 @@ audio.addEventListener('pause', () => { // Same as above: suppress the song:pause emitted by a reroute's deliberate // audio.pause() — the migration is transparent to plugin play-state. if (window._juceRerouteInProgress) return; + if (window._stemsRerouteInProgress) return; window.feedBack.isPlaying = false; window.feedBack.emit('song:pause', _songEventPayload()); }); diff --git a/static/js/transport.js b/static/js/transport.js index 0da16018..f698a947 100644 --- a/static/js/transport.js +++ b/static/js/transport.js @@ -358,6 +358,12 @@ export async function togglePlay() { // leave the button showing Play while the song keeps playing — the // "two clicks to pause on the first song after a fresh load" bug. if (window._juceRerouteInProgress) return; + // Same shape of race, HTML5 -> stems-plugin Web-Audio takeover + // (get-flashbacks/feedBack#39): the stems plugin deliberately + // pauses the core element while it builds its own multi-stem + // transport, then dispatches a synthetic 'play' once that + // transport actually starts. Don't stomp the button in between. + if (window._stemsRerouteInProgress) return; console.error('[app] audio.play() rejected:', err); S.isPlaying = false; setPlayButtonState(false); From 94a8f6f2999b78012fcfbe39cc92a3d13cc3a3b8 Mon Sep 17 00:00:00 2001 From: carochacs <79524656+carochacs@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:01:10 -0600 Subject: [PATCH 3/4] fix(highway_3d): chord diagram no longer mirrors on Invert drawChordDiagram() was passed inverted: _invertedCached at both call sites, flipping its column order (high-e/low-E swapped) whenever the highway's Invert toggle was on. The diagram's orientation should be fixed regardless of that toggle, so both sites now pass inverted: false. Note: plugins/highway_3d/CLAUDE.md had documented the mirroring as this overlay's contract, but that line traces only to a single squashed "Clean release snapshot" commit with no surviving design rationale -- treated here as an inaccurate description of a bug, not a protected feature, and updated accordingly. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 10 ++++++++++ plugins/highway_3d/CLAUDE.md | 2 +- plugins/highway_3d/screen.js | 8 ++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52105d66..af6358ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -293,6 +293,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 engine (`app.js`, `highway.js`, `playSong`, `showScreen`, the capability registry). ### Fixed +- **highway_3d chord diagram no longer mirrors on Invert.** The top-left chord + diagram overlay (`drawChordDiagram()`) was flipping its column order + (high-e/low-E swapped) whenever the highway's Invert toggle was on, passed + through as `inverted: _invertedCached` at both call sites. The diagram's + orientation should be fixed regardless of that toggle, so both call sites + now pass `inverted: false`. Note: `plugins/highway_3d/CLAUDE.md` had + documented the mirroring as this overlay's contract, but that line traces + only to a single squashed "Clean release snapshot" commit with no + surviving design rationale — treated here as an inaccurate description of + a bug, not a protected feature, and updated accordingly. - **GP8 asset resolution honours the directory the registry named.** `` is matched on filename stem so a format variant of the same recording can win (an `.ogg` beside the declared `.mp3` is copied out diff --git a/plugins/highway_3d/CLAUDE.md b/plugins/highway_3d/CLAUDE.md index 97890b20..39016e34 100644 --- a/plugins/highway_3d/CLAUDE.md +++ b/plugins/highway_3d/CLAUDE.md @@ -124,7 +124,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks ### Lyrics & overlays - **Lyrics overlay** → `drawLyrics()`. 2D canvas, top centre, semi-transparent rounded background, syllable-level highlighting (current syllable in white, played in muted, upcoming in dim). -- **Chord diagram overlay** → `drawChordDiagram()` (see "Chords" above). 2D canvas, top-left, fades over the 0.55 s linger window. Respects `inverted` (column 0 is high-e when inverted, low-E otherwise). +- **Chord diagram overlay** → `drawChordDiagram()` (see "Chords" above). 2D canvas, top-left, fades over the 0.55 s linger window. `drawChordDiagram()` still accepts an `inverted` param (column 0 is high-e when inverted, low-E otherwise), but the two call sites always pass `inverted: false` — the diagram's orientation is fixed and does not mirror when the highway's own Invert toggle is on. - **The `lyricsCanvas`** is created in `initScene()` with `z-index:1`, appended to `wrap` **after** `ren.domElement` — this is the empirically-correct stacking order for all browsers/contexts (including splitscreen panels with `position:relative; overflow:hidden`). Don't reorder; see Pitfall #5. ### Splitscreen diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index 855c6f2b..f8de71c1 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -16544,7 +16544,9 @@ ? Math.min(1.0, Math.max(0, (bundle.currentTime - _diagPrev.t) / DIAG_ENTRANCE_S)) : 1.0, canvasW: lyricsCanvas.width, canvasH: lyricsCanvas.height, - inverted: _invertedCached, + // Chord diagram orientation is fixed regardless of the + // highway's own Invert toggle. + inverted: false, sizeSlider: chordDiagramSize, position: chordDiagramPosition, nStr: _diagPrev.nStr ?? nStr, lyricsBottom, @@ -16558,7 +16560,9 @@ opacity: Math.max(0, 1 + (_diagChord.t - bundle.currentTime) / DIAG_LINGER_S), entranceT: _diagEntranceT, canvasW: lyricsCanvas.width, canvasH: lyricsCanvas.height, - inverted: _invertedCached, + // Chord diagram orientation is fixed regardless of the + // highway's own Invert toggle. + inverted: false, sizeSlider: chordDiagramSize, position: chordDiagramPosition, nStr: _diagChord.nStr ?? nStr, lyricsBottom, From 97cdefedd8489d8a9b0cce218cdfd651652a8781 Mon Sep 17 00:00:00 2001 From: carochacs <79524656+carochacs@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:03:53 -0600 Subject: [PATCH 4/4] Bump 3D Highway plugin version Update the bundled `highway_3d` plugin version from 3.34.1 to 3.34.2. --- plugins/highway_3d/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/highway_3d/plugin.json b/plugins/highway_3d/plugin.json index 755ed1be..7d97f246 100644 --- a/plugins/highway_3d/plugin.json +++ b/plugins/highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "highway_3d", "name": "3D Highway", - "version": "3.34.1", + "version": "3.34.2", "type": "visualization", "bundled": true, "script": "screen.js",