feat(core): route the highway chart to the selected instrument's part (working-tuning PR 2) (#659)

When a song loads without an explicit arrangement, highway_ws now reads the
player's selected `instrument` from config.json (the same file it already reads
for the default-arrangement preference) and picks the arrangement that matches:
bass -> the Bass part. Guitar — and any unknown/future instrument (drums, keys)
— falls through to the existing preference/most-notes default, which already
lands on a guitar part.

Previously the instrument selector only fed the tuner, so a bass player was
handed the default Lead/guitar chart, and the working-tuning coverage check then
compared a 4-string bass against a 6-string part (always "can't cover"). This is
the instrument->chart routing the working-tuning series leans on.

Server-only (every launch path flows through the WS, so no client change). An
explicit arrangement request always wins, so only the default part chosen on
load changes. Tests: tests/test_highway_ws_instrument_routing.py (bass->Bass,
guitar->default, explicit-wins) — 3 new, existing highway WS tests still green.


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChrisBeWithYou
2026-07-01 08:58:50 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 491039a12d
commit c4bb58233d
3 changed files with 243 additions and 3 deletions
+51 -3
View File
@@ -7842,15 +7842,63 @@ async def highway_ws(websocket: WebSocket, filename: str, arrangement: int = -1,
if 0 <= arrangement < len(song.arrangements):
best = arrangement
else:
# Check user's default arrangement preference
# Read the user's config once: their selected instrument (route the chart
# to the matching part) and their default-arrangement preference.
pref = ""
sel_instrument = ""
config_file = CONFIG_DIR / "config.json"
if config_file.exists():
try:
pref = json.loads(config_file.read_text(encoding="utf-8")).get("default_arrangement", "")
_cfg = json.loads(config_file.read_text(encoding="utf-8"))
pref = _cfg.get("default_arrangement", "")
sel_instrument = (_cfg.get("instrument", "") or "")
except Exception:
pass
if pref:
# Instrument routing: load the part that matches the selected instrument so
# "your instrument" and "the chart you play" line up. The default ordering
# is Lead/guitar-first, so without this a bass player gets handed a guitar
# chart (and any tune-check then compares a 4-string bass against a 6-string
# part). Currently routes bass -> a Bass arrangement; guitar — and any
# unknown/future instrument (drums, keys) — falls through to the
# preference/most-notes logic below, which already lands on a guitar part.
# Drums/keys get their own match when those arrangement types + selector
# entries land. Only applies when no explicit arrangement was requested, so
# a manual arrangement switch is always respected.
if sel_instrument.lower() == "bass":
# Candidate bass parts, preferring the structured pathBass flag; the
# normalized smart name (itself pathBass-derived) and raw name are
# fallbacks for sources without the flag.
bass_idxs = [
i
for i, a in enumerate(song.arrangements)
if getattr(a, "path_bass", False)
or (smart_names[i] or "").lower().startswith("bass")
or "bass" in (getattr(a, "name", "") or "").lower()
]
if bass_idxs:
# Among the bass parts: (1) honor the saved default-arrangement
# preference if it names one of them (so a bass player who prefers
# "Bass 2"/"Alt. Bass" keeps it), (2) else the canonical main "Bass",
# (3) else the first bass part in order.
pref_bass = -1
if pref:
for i in bass_idxs:
nm = (smart_names[i] if naming_mode == "smart" and i < len(smart_names)
else getattr(song.arrangements[i], "name", ""))
if nm == pref:
pref_bass = i
break
if pref_bass >= 0:
best = pref_bass
else:
best = next(
(i for i in bass_idxs
if (smart_names[i] if i < len(smart_names) else "") == "Bass"),
bass_idxs[0],
)
# User's default arrangement preference (only when instrument routing did not
# already resolve a part — i.e. guitar, or a bass player with no bass part).
if best < 0 and pref:
if naming_mode == "smart":
best = _pick_smart_arrangement(song.arrangements, smart_names, pref)
else: