mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
fix(song): make bass detection instrument-type-aware, not name-only (#1019)
ship-ci / ci (push) Waiting to run
ship-ci / ci (push) Waiting to run
Editor now authors an arrangement's instrument as first-class data (a manifest 'type' field). Core dropped it: the sloppak loader never read 'type', and 'is this a bass?' was defined three different ways across call sites (name-only in note_pitch_midi and the highway scale-degree path; path_bass+name in bass selection; name-only in arrangement_string_count). So an authored type=bass chart not named 'bass' got 6-string lane counts and guitar open-string MIDI. - Add optional Arrangement.type; sloppak load_song lifts the manifest type onto it - Add arrangement_is_bass(arr) = type=='bass' OR path_bass OR 'bass' in name (None/whitespace safe), and route string count, note_pitch_midi, the highway scale-degree base, and bass-player selection through it - Back-compat: no bass signal -> unchanged 6-string / guitar behavior Companion to editor #335 (first-class instrument type). Scale degrees are display-only and never feed a grader. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
605dbdfd25
commit
e0270e5c30
@@ -329,6 +329,31 @@ def test_note_pitch_midi_bass_uses_bass_base():
|
||||
assert note_pitch_midi(bass, Note(time=0, string=0, fret=0)) == 28
|
||||
|
||||
|
||||
def test_note_pitch_midi_authored_bass_type_uses_bass_base():
|
||||
"""Editor PR #335: a bass authored via `type` on an arrangement whose NAME
|
||||
doesn't say "bass". arrangement_string_count now returns 4 for it, so the
|
||||
open-string base MUST also be the bass base (low E1 = 28), not the guitar
|
||||
octave (40). Pre-fix note_pitch_midi keyed is_bass off the name only, so
|
||||
this returned 40 (4 lanes on a guitar octave — the exact inconsistency)."""
|
||||
bass = Arrangement(
|
||||
name="Low End", type="bass",
|
||||
tuning=[0, 0, 0, 0],
|
||||
notes=[Note(time=float(i), string=i, fret=0) for i in range(4)],
|
||||
)
|
||||
assert note_pitch_midi(bass, Note(time=0, string=0, fret=0)) == 28
|
||||
|
||||
|
||||
def test_note_pitch_midi_path_bass_flag_uses_bass_base():
|
||||
"""Same guarantee via the archive <arrangementProperties> pathBass flag on a
|
||||
non-"bass"-named arrangement: bass base (28), not guitar (40)."""
|
||||
bass = Arrangement(
|
||||
name="Low End", path_bass=True,
|
||||
tuning=[0, 0, 0, 0],
|
||||
notes=[Note(time=float(i), string=i, fret=0) for i in range(4)],
|
||||
)
|
||||
assert note_pitch_midi(bass, Note(time=0, string=0, fret=0)) == 28
|
||||
|
||||
|
||||
def test_note_pitch_midi_out_of_range_string_is_none():
|
||||
arr = Arrangement(name="Lead", tuning=[0, 0, 0, 0, 0, 0])
|
||||
assert note_pitch_midi(arr, Note(time=0, string=9, fret=0)) is None
|
||||
@@ -1153,6 +1178,60 @@ def test_string_count_ignores_rs_padded_tuning_for_bass():
|
||||
assert arrangement_string_count(arr) == 4
|
||||
|
||||
|
||||
def test_string_count_4_for_path_bass_flag_without_bass_in_name():
|
||||
# Archive/DLC bass whose manifest ArrangementName isn't "Bass" but whose
|
||||
# <arrangementProperties> pathBass flag is set. Notes on 0..3, tuning
|
||||
# padded to the arrangement-XML length of 6. Pre-fix, name_based forced
|
||||
# 6 (name has no "bass"), so this returned 6 despite the authoritative
|
||||
# instrument flag saying bass.
|
||||
arr = Arrangement(
|
||||
name="Low End",
|
||||
path_bass=True,
|
||||
tuning=[0, 0, 0, 0, 0, 0],
|
||||
notes=[Note(time=float(i), string=i, fret=0) for i in range(4)],
|
||||
)
|
||||
assert arrangement_string_count(arr) == 4
|
||||
|
||||
|
||||
def test_string_count_4_for_authored_bass_type_without_bass_in_name():
|
||||
# Editor PR #335: an instrument `type` authored as bass on an arrangement
|
||||
# whose NAME does not contain "bass" (the sloppak loader lifts the manifest
|
||||
# `type` onto Arrangement.type). Notes on 0..3, tuning padded to 6.
|
||||
# The editor lays out 4 lanes off the type; core must agree.
|
||||
arr = Arrangement(
|
||||
name="Low End",
|
||||
type="bass",
|
||||
tuning=[0, 0, 0, 0, 0, 0],
|
||||
notes=[Note(time=float(i), string=i, fret=0) for i in range(4)],
|
||||
)
|
||||
assert arrangement_string_count(arr) == 4
|
||||
|
||||
|
||||
def test_string_count_6_for_authored_guitar_type_no_regression():
|
||||
# A non-bass authored type on a generic name still resolves to the
|
||||
# canonical 6 — the type signal only pulls DOWN to 4 for bass.
|
||||
arr = Arrangement(
|
||||
name="Track 1",
|
||||
type="guitar",
|
||||
notes=[Note(time=float(i), string=i, fret=0) for i in range(5)],
|
||||
)
|
||||
assert arrangement_string_count(arr) == 6
|
||||
|
||||
|
||||
def test_arrangement_is_bass_signal_safety():
|
||||
# The manifest `type` is lifted onto arr.type verbatim; the helper must be
|
||||
# safe against the messy shapes a hand-edited/loose source can produce.
|
||||
from song import arrangement_is_bass
|
||||
assert arrangement_is_bass(Arrangement(name="Low End", type="bass"))
|
||||
assert arrangement_is_bass(Arrangement(name="Low End", type=" BASS ")) # ws/case
|
||||
assert arrangement_is_bass(Arrangement(name="Low End", path_bass=True))
|
||||
assert arrangement_is_bass(Arrangement(name="Slap Bass")) # legacy name
|
||||
# Non-bass / absent signals stay False (back-compat: no bass signal → 6).
|
||||
assert not arrangement_is_bass(Arrangement(name="Lead", type=""))
|
||||
assert not arrangement_is_bass(Arrangement(name="Rhythm", type="guitar"))
|
||||
assert not arrangement_is_bass(Arrangement(name="", type=""))
|
||||
|
||||
|
||||
# ── compute_smart_names ───────────────────────────────────────────────────────
|
||||
|
||||
def _sarr(path_lead=False, path_rhythm=False, path_bass=False,
|
||||
|
||||
Reference in New Issue
Block a user