mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(core): teaching marks fg/ch/sd — wire + GP import + sd derivation (§6.2.2) (#536)
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 got-feedback/feedback#334 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
a858617d71
commit
6ee5da3d8b
@@ -457,6 +457,31 @@ _GPIF_FINGER_MAP = {
|
||||
'pinky': 4, 'little': 4,
|
||||
}
|
||||
|
||||
# Per-note <LeftFingering> teaching mark (§6.2.2). Unlike the chord-diagram
|
||||
# <Position finger=".."> path above, GPIF stores a single note's fret-hand
|
||||
# finger as a direct <Note> child element with the classical p-i-m-a-c letter
|
||||
# codes (verified against GP8 exports), mapped to the same RS finger integers
|
||||
# (open = -1, thumb = 0, index = 1, middle = 2, annular/ring = 3, little = 4).
|
||||
_GPIF_LEFT_FINGERING_MAP = {
|
||||
'open': -1, 'none': -1, '': -1,
|
||||
'p': 0, 'thumb': 0,
|
||||
'i': 1, 'index': 1,
|
||||
'm': 2, 'middle': 2,
|
||||
'a': 3, 'annular': 3, 'ring': 3,
|
||||
'c': 4, 'little': 4, 'pinky': 4,
|
||||
}
|
||||
|
||||
|
||||
def _gpif_left_fingering(note_el) -> int:
|
||||
"""Read a GPIF <Note>'s fret-hand finger (<LeftFingering>) -> RS finger int.
|
||||
|
||||
Returns -1 (unset) when absent or unrecognised — never fabricates a finger.
|
||||
Teaching mark only (§6.2.2); never used for grading."""
|
||||
raw = (note_el.findtext('LeftFingering') or '').strip().lower()
|
||||
if not raw:
|
||||
return -1
|
||||
return _GPIF_LEFT_FINGERING_MAP.get(raw, -1)
|
||||
|
||||
|
||||
def _rs_string_order(string_pitches: list[int]) -> dict[int, int]:
|
||||
"""Map each GPIF string index → RS string index (0 = lowest pitch).
|
||||
@@ -1600,6 +1625,11 @@ def convert_file(
|
||||
rn.vibrato = True
|
||||
if 'LeftHandTapping' in _tp or 'Tapped' in _tp:
|
||||
rn.tap = True
|
||||
# Fret-hand fingering -> fg teaching mark
|
||||
# (§6.2.2). <LeftFingering> is a direct <Note>
|
||||
# child, not a <Property>, so read it off
|
||||
# note_el rather than the property map.
|
||||
rn.fret_finger = _gpif_left_fingering(note_el)
|
||||
if 'HarmonicType' in _tp:
|
||||
_ht = (_tp['HarmonicType'].findtext('HType')
|
||||
or '').strip().lower()
|
||||
|
||||
Reference in New Issue
Block a user