feat(core): teaching marks fg/ch/sd — wire + GP import + sd derivation (§6.2.2) - #536
Merged
Conversation
…n (§6.2.2) Add the three OPTIONAL per-note feedpak 1.5.0 teaching marks — fg (fret-hand finger), ch (strum-group key), sd (scale degree) — to the Note model and wire format, mirroring the bend-shape work (#531). These are DISPLAY/TEACHING ONLY: nothing in the scoring / note-verification path reads them. - lib/song.py: Note.fret_finger / strum_group / scale_degree, default-omitted on the wire (fg/ch/sd) and decoded via _wire_int_optional; _parse_note reads the GP-written fretFinger XML attr. Pure helpers key_to_tonic_pc (§7.7 key name -> tonic pitch class) + scale_degree_for_pitch, plus base_open_string_midis / pitch_from_base / note_pitch_midi (tuning offsets + capo + fret -> MIDI, mirroring app.js _TUNING_BASE_MIDI). - lib/gp2rs.py: GP5 note.effect.leftHandFinger -> fg (RsNote field + fretFinger XML attr), reusing the chord Fingering value convention. - lib/gp2rs_gpx.py: GP8/GPIF per-note <LeftFingering> (p-i-m-a-c letter codes, verified against real GP8 exports) -> fg. - server.py highway_ws: derive sd for notes + chord notes from the active keys.json key + sounding pitch when the author didn't author one (author value wins); base hoisted out of the per-note loop. Tests: round-trip + omit-when-default + malformed-tolerance for fg/ch/sd; key_to_tonic_pc + scale_degree_for_pitch + note_pitch_midi (standard/drop-D/ capo/bass) units; GP5 leftHandFinger and GP8 <LeftFingering> import. Part of #334 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 20, 2026
This was referenced Jun 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #334
PR 1 of 3 for the teaching marks epic (feedpak 1.5.0 §6.2.2). Mirrors the bend-shape precedent (#531/#532). The three marks are display/teaching only — nothing in the scoring /
NoteVerifierpath readsfg/ch/sd.What this adds
Three OPTIONAL per-note fields, all default-omitted on the wire:
fgfret-hand finger (-1unset,0thumb …4pinky)chstrum-group key (>= -1; notes sharing a value>= 0are one gesture,pkdgives direction)sdscale degree (0..11chromatic offset above the active key's tonic; author value wins, otherwise derived)Core wire (
lib/song.py)Note.fret_finger/strum_group/scale_degree; emit (fg/ch/sd) and decode via the existing_wire_int_optionalhelper. Lossless round-trip._parse_notereads the GP-writtenfretFingerchart-XML attribute.key_to_tonic_pc(parse a keys.json key name like"Em"→ tonic pitch class),scale_degree_for_pitch, andbase_open_string_midis/pitch_from_base/note_pitch_midi(tuning offsets + capo + fret → MIDI, mirroringapp.js_TUNING_BASE_MIDI).GP import →
fglib/gp2rs.py):note.effect.leftHandFinger(Fingering enum) →fg, reusing the chord-path value convention; carried as afretFingerXML attr.lib/gp2rs_gpx.py): per-note<LeftFingering>(classical p-i-m-a-c letter codes, verified against real GP8 exports) →fg.sdderivation (server.pyhighway_ws)Where notes, tuning and the
keys.jsontimeline converge: for each note/chord-note, if the author didn't setsdand a key is active at the note's time, derive it from the sounding pitch. Base hoisted out of the per-note loop.Tested locally
pytest tests/test_song.py tests/test_gp2rs.py tests/test_gp2rs_gpx.py→ 292 passed. (Pre-existing unrelatedtest_diagnostics_redact.pyfailure on cleanmainis untouched / not in this run.)GP8_Test.gpemitsfretFinger="2"/"1"for the M/I notes and omits theOpennote.arr.tuningis offsets from standard, not absolute MIDI — now fixed viabase_open_string_midis/pitch_from_base; re-review clean.Notes / follow-ups
sdderivation only fires when akeys.jsonis present and the key name parses; missing key / unparseable name / no tuning entry →sdleft unset (never fabricated).🤖 Generated with Claude Code