Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ Thumbs.db
*.swp
.idea/
plugins/support_creators
/library
/static/sloppak_cache
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
`<EmbeddedFilePath>` is matched on filename stem so a format variant of the
same recording can win (an `.ogg` beside the declared `.mp3` is copied out
Expand Down
12 changes: 9 additions & 3 deletions lib/gp2rs_gpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
8 changes: 8 additions & 0 deletions lib/routers/ws_highway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]
Expand All @@ -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).
Expand Down
8 changes: 8 additions & 0 deletions lib/song.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion plugins/highway_3d/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugins/highway_3d/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
14 changes: 12 additions & 2 deletions plugins/highway_3d/screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -16662,6 +16666,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);
};
Expand Down
2 changes: 2 additions & 0 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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());
});
Expand Down
6 changes: 6 additions & 0 deletions static/js/transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down