Restore GP6/7/8 tremolo picking on import (#572)

* Map GP7/8 tremolo picking on import

GP7/8 (GPIF) encodes tremolo picking as a beat-level <Tremolo> element,
which the importer ignored — so tremolo was silently dropped on .gp import,
while note vibrato and the GP3-5 path were unaffected. Read the beat-level
<Tremolo> and set the note tremolo flag across the beat, independent of
vibrato (a note can carry both).

Signed-off-by: Sin <deathlysin@outlook.com>

* test: cover GP6/7/8 tremolo-picking import

Extract the beat-level <Tremolo> detection into a pure _beat_has_tremolo
helper (mirroring the tested _note_has_vibrato) so it's unit-testable in
this suite's fixture-free style, then add:

- 4 unit tests on _beat_has_tremolo: direct <Tremolo> child detected
  (rate-agnostic), absent -> False, direct-child-only (nested Tremolo
  ignored), independent of the VibratoWTremBar whammy property.
- 1 end-to-end test driving convert_file via a crafted GPIF (monkeypatched
  _load_gpif): a tremolo-picked beat's note serializes tremolo="1" while a
  plain beat stays "0".

Both the detection and integration tests fail without the fix; full GP
suite 238 passed. Refactor is behavior-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Sin <deathlysin@outlook.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sin
2026-06-22 21:14:43 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Byron Gamatos
parent 32127bc70b
commit 37aedd4251
2 changed files with 109 additions and 0 deletions
+25
View File
@@ -722,6 +722,20 @@ def _note_has_vibrato(note_el: ET.Element, prop_map: dict) -> bool:
return 'Vibrato' in prop_map or note_el.find('Vibrato') is not None
def _beat_has_tremolo(beat_el: ET.Element) -> bool:
"""True if a GP7/GP8 beat carries tremolo picking.
GPIF encodes tremolo picking as a DIRECT beat-level
``<Tremolo>1/8</Tremolo>`` child of ``<Beat>`` (the value is the rate). The
RS note model has a single boolean tremolo flag with no rate, so the rate is
intentionally ignored — any tremolo-picked beat maps to note tremolo across
it. Matched as a direct child (not ``.//``) so it is never confused with the
whammy-bar ``VibratoWTremBar`` Property, a separate beat-level effect
handled elsewhere.
"""
return beat_el.find('Tremolo') is not None
# ---------------------------------------------------------------------------
# list_tracks — mirrors gp2rs.list_tracks interface
# ---------------------------------------------------------------------------
@@ -1696,6 +1710,17 @@ def convert_file(
for _bn in beat_rs_notes:
_bn.vibrato = True
# Tremolo picking: GP7/GP8 encodes the rate as a
# beat-level <Tremolo>1/8</Tremolo> child. The note
# model has a single tremolo flag (no rate), so map
# any tremolo-picked beat to note tremolo across it.
# Independent of vibrato above — a note can carry
# both. (Beat-level <Tremolo>, not the whammy
# VibratoWTremBar Property, which is handled above.)
if _beat_has_tremolo(beat_el):
for _bn in beat_rs_notes:
_bn.tremolo = True
if len(beat_rs_notes) == 1:
rs_notes.append(beat_rs_notes[0])
elif len(beat_rs_notes) > 1: