mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 19:29:33 +00:00
feat(song): per-note keys hand assignment (hand) on the Note wire (#990)
The editor's keys LH/RH hand arc needs a per-note hand assignment
('lh'/'rh', from a MusicXML grand staff import today, hand-editable
later) to survive a sloppak save → reload: the editor already emits it
on the wire, but note_from_wire dropped unknown keys, so the field died
on every reopen.
- Note gains `hand: str | None = None` (None = unassigned; the
heuristic hand split keeps owning unassigned notes). Distinct from
`right_hand` (the bass plucking finger) — hence the spelled-out
`hand` wire key, since `rh` is taken.
- note_to_wire emits it default-omitted and validates on emit; older
readers ignore it (feedpak: unknown note keys are permitted).
- note_from_wire decodes it as a strict enum — anything but 'lh'/'rh'
(junk, wrong case, bools) falls back to unassigned rather than
poisoning downstream hand-split / hands-separate practice logic.
Groundwork consumers land separately: notation_lift.split_hands
respecting per-note overrides, and the editor's hand surface.
Editor counterpart: feedBack-plugin-editor #299.
Tests: three new wire round-trip tests in tests/test_song.py (literal
key, default-omitted, junk rejection both directions), matching the
teaching-marks test style.
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
ChrisBeWithYou
Claude Opus 4.8
parent
1745b13ba7
commit
00fce2772d
@@ -249,6 +249,34 @@ def test_note_teaching_marks_tolerate_malformed_optional_ints():
|
||||
assert n.scale_degree == -1
|
||||
|
||||
|
||||
# ── Keys hand assignment ─────────────────────────────────────────────────────
|
||||
|
||||
def test_note_hand_round_trips_under_literal_key():
|
||||
"""The keys hand assignment survives the wire as the literal `hand` key
|
||||
(spelled out — `rh` is taken by right_hand, the bass plucking finger)."""
|
||||
for hand in ("lh", "rh"):
|
||||
n = Note(time=0.0, string=2, fret=12, hand=hand)
|
||||
wire = note_to_wire(n)
|
||||
assert wire["hand"] == hand
|
||||
assert note_from_wire(wire) == n
|
||||
|
||||
|
||||
def test_note_hand_omitted_when_unassigned():
|
||||
wire = note_to_wire(Note(time=0.0, string=0, fret=0))
|
||||
assert "hand" not in wire
|
||||
assert note_from_wire(wire).hand is None
|
||||
|
||||
|
||||
def test_note_hand_junk_never_emitted_and_decodes_to_unassigned():
|
||||
"""Emit side validates ('LH', True, … stay off the wire); decode side is a
|
||||
strict enum so a hand-edited pack can't poison hand-split logic."""
|
||||
for junk in ("LH", "left", "", True, 1, ["lh"]):
|
||||
assert "hand" not in note_to_wire(
|
||||
Note(time=0.0, string=0, fret=0, hand=junk))
|
||||
assert note_from_wire(
|
||||
{"t": 0.0, "s": 0, "f": 0, "hand": junk}).hand is None
|
||||
|
||||
|
||||
# ── Scale-degree derivation helpers (§6.2.2 / §7.7) ──────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("key,pc", [
|
||||
|
||||
Reference in New Issue
Block a user