From b79341bff96e191cf67b7fd3b40e009f4291f82b Mon Sep 17 00:00:00 2001 From: kaizenman <15638776+kaizenman@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:32:52 +0200 Subject: [PATCH] gp3/gp4: extract note accent flags (0x02 heavy, 0x40 normal) `GP3File.readNote` never read flag bits 0x02 (heavy accent) and 0x40 (normal accent) from the note flags byte, even though the docstring documented them. GP4 inherited this broken read path; only GP5 read them correctly. The writer side was asymmetric too: `GP3File.packNoteFlags` emitted 0x02 but not 0x40, while GP4 overrode it to add 0x40 only. Round-tripping a note with `accentuatedNote=True` through PGP silently dropped the flag. This change: * reads both flags in `GP3File.readNote` (inherited by GP4) * writes flag 0x40 in `GP3File.packNoteFlags` * drops the now-redundant override in `GP4File.packNoteFlags` * adds a round-trip regression test for all three formats Cross-checked against alphaTab's `Gp3To5Importer.ts:1203-1207` which extracts both flags universally. Closes #1 --- src/guitarpro/gp3.py | 4 ++++ src/guitarpro/gp4.py | 2 -- tests/test_conversion.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/guitarpro/gp3.py b/src/guitarpro/gp3.py index 23e49f7..6e54a10 100644 --- a/src/guitarpro/gp3.py +++ b/src/guitarpro/gp3.py @@ -899,7 +899,9 @@ def readNote(self, note, guitarString, track): """ flags = self.readU8() note.string = guitarString.number + note.effect.heavyAccentuatedNote = bool(flags & 0x02) note.effect.ghostNote = bool(flags & 0x04) + note.effect.accentuatedNote = bool(flags & 0x40) if flags & 0x20: note.type = gp.NoteType(self.readU8()) if flags & 0x01: @@ -1443,6 +1445,8 @@ def packNoteFlags(self, note): if note.velocity != gp.Velocities.default: flags |= 0x10 flags |= 0x20 + if note.effect.accentuatedNote: + flags |= 0x40 return flags def writeNoteEffects(self, note): diff --git a/src/guitarpro/gp4.py b/src/guitarpro/gp4.py index 78bc305..31a6814 100644 --- a/src/guitarpro/gp4.py +++ b/src/guitarpro/gp4.py @@ -655,8 +655,6 @@ def writeNote(self, note): def packNoteFlags(self, note): flags = super().packNoteFlags(note) - if note.effect.accentuatedNote: - flags |= 0x40 if note.effect.isFingering: flags |= 0x80 return flags diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 19b4649..e6115a7 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -126,6 +126,37 @@ def testChord(tmpdir, caplog, filename): assert song == song2 +@pytest.mark.parametrize('filename', ['Effects.gp3', 'Effects.gp4', 'Effects.gp5']) +def testNoteAccentuationRoundtrip(tmpdir, filename): + """Regression test for GP3/GP4 note accent flag extraction. + + `GP3File.readNote` historically did not extract flag bits 0x02 (heavy + accent) and 0x40 (normal accent), while `writeNote` *does* write them. + GP4 inherits the broken read path; only GP5 read them correctly. + + This caused a reader/writer asymmetry: accent flags were silently + dropped on every parse, even though PGP could write them. + """ + filepath = LOCATION / filename + song = gp.parse(filepath) + note = song.tracks[0].measures[0].voices[0].beats[0].notes[0] + + note.effect.accentuatedNote = True + note.effect.heavyAccentuatedNote = True + + destpath = str(tmpdir.join(filename)) + gp.write(song, destpath) + song2 = gp.parse(destpath) + note2 = song2.tracks[0].measures[0].voices[0].beats[0].notes[0] + + assert note2.effect.accentuatedNote is True, ( + f'{filename}: accentuatedNote not preserved on round-trip' + ) + assert note2.effect.heavyAccentuatedNote is True, ( + f'{filename}: heavyAccentuatedNote not preserved on round-trip' + ) + + @pytest.mark.parametrize('version', ['gp3', 'gp4', 'gp5']) def testReadErrorAnnotation(version): def writeToBytesIO(song):