Skip to content

Translate GP6/GP7 note effects#63

Closed
knoguchi wants to merge 8 commits into
Perlence:masterfrom
knoguchi:feature/gpx-effects
Closed

Translate GP6/GP7 note effects#63
knoguchi wants to merge 8 commits into
Perlence:masterfrom
knoguchi:feature/gpx-effects

Conversation

@knoguchi

Copy link
Copy Markdown

Summary

Maps the common GP6/GP7 note and beat effects between the score.gpif XML and the Song model, in both directions. The flag/type encodings follow alphaTab's GpifParser as the reference.

Effects covered:

  • Hammer-on / pull-offHopoOriginNoteEffect.hammer
  • SlidesSlide/Flags bitfield → NoteEffect.slides (shift, legato, out-down/up, into-from-below/above)
  • HarmonicsHarmonic + HarmonicType + HarmonicFret → natural / artificial / pinch / tap / semi
  • FingeringLeftFingering / RightFingering letter codes (P/I/M/A/C) → Fingering
  • AccentuationAccent flag bits → accent (0x08), heavy accent (0x04), staccato (0x01)
  • Beat textFreeTextBeat.text

Tests

  • Effects read from the real sample file round-trip with full Song equality.
  • Each effect branch has a synthetic parse → write → parse test (so write↔read symmetry is verified even for effects absent from the sample file).

All 221 tests pass.

Not yet covered

Bends / whammy bar (point-based, needs careful encoding), grace notes (structural — stored as separate beats), trill / tremolo picking, and chord diagrams (a large separate subsystem). These can follow in another pass.

Note on stacking

This PR builds on #62 (GP6/GP7 read/write). It is branched off that work, so until #62 is merged the diff here also contains #62's commits — please review #62 first. The effect-specific change is the final commit (Translate GP6/GP7 note effects).

Same two caveats as #62 apply: literal-only BCFZ compression, and round-trips verified through PyGuitarPro rather than against the Guitar Pro application.

🤖 Generated with Claude Code

knoguchi and others added 8 commits June 25, 2026 19:53
GP6 and GP7 store the score as a score.gpif XML document inside a
container: GP6 uses a BCFZ-compressed BCFS virtual filesystem, while GP7
is a plain ZIP archive. This adds a reader for both.

- gpx.py: BCFZ bitstream decompression (based on the algorithm from the
  feature/gpx branch contributed by J. Jorgen von Bargen), the BCFS
  sector archive reader, and GP7 ZIP extraction.
- gpif.py: maps the score.gpif XML into the existing Song model,
  resolving the format's cross-referenced bars/voices/beats/notes/rhythms
  lists. Covers song info, tracks, tunings, master bars and time
  signatures, voices, beats, durations and notes.
- io.py: parse() now detects the BCFZ/BCFS/ZIP magic and dispatches to
  the new reader; the binary GP3/4/5 path is unchanged.

Writing GP6/GP7 is not supported. Advanced note/beat effects are not yet
translated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BCFZ payload may end mid-byte, so the last compression token can
require bits beyond the end of the stream. Yield zero padding bits past
end-of-stream instead of raising IndexError, matching the reference
decoder's EOF handling. Add "Dear Song.gpx" which exercises this path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Synthesize a GP7 archive from an existing .gpx score's score.gpif and
assert it parses into an identical Song, covering the ZIP extraction
branch without committing a binary GP7 fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
guitarpro.write can now produce GP6 and GP7 files, completing the
read/write parity the library offers for GP3/4/5.

- gpif.py: GPIFWriter serializes a Song into a score.gpif document,
  hoisting the measure/voice/beat/note tree into the format's flat,
  cross-referenced Bars/Voices/Beats/Notes/Rhythms lists.
- gpx.py: BCFZ compression (literal-run encoding, which is valid BCFZ and
  keeps the encoder linear) and a BCFS image builder; GP7 is written as a
  ZIP archive.
- io.py: write() dispatches to the GPX writer for version (6,0,0)/(7,0,0)
  or a .gpx/.gp extension.

Round-trips parse -> write -> parse with full Song equality. Writes cover
the same subset of the model that reading populates; advanced effects are
not yet serialized. Also tightened voice reading to skip unused (-1)
voice slots so the voice count is deterministic across a round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Buffer non-seekable streams before the magic peek so parse() works on
  pipes and other non-seekable file-like objects.
- Raise GPException (not KeyError) when a GP6 container has no score.gpif.
- Emit one Bar per track in every MasterBar so the Bars reference list has
  a consistent length.
- Drop the internal buildBCFS helper from the module's public __all__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Map the common note and beat effects between the score.gpif XML and the
Song model, using alphaTab's GpifParser as the reference for the flag and
type encodings:

- hammer-on/pull-off (HopoOrigin)
- slides (Slide/Flags bitfield -> SlideType list)
- harmonics: natural, artificial, pinch, tap, semi (Harmonic + HarmonicType
  + HarmonicFret)
- left/right-hand fingering (LeftFingering/RightFingering letter codes)
- accentuation: accent, heavy accent, staccato (Accent flag bits)
- beat text (FreeText)

Effects read from the sample files round-trip with full Song equality, and
each effect branch is covered by a synthetic parse -> write -> parse test.
Bends, grace notes and chord diagrams remain unmapped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The declared uncompressed length is only an upper bound: real GP6 streams
that use back-references end before reaching it, with the final byte
zero-padded. The previous code padded past end-of-stream with zero bits,
which decode to empty literal runs and made decompression loop forever on
any such file (the literal-heavy sample files happened to hit the declared
length exactly and so were unaffected).

Raise at end-of-stream and stop, matching the reference decoder. Verified
against the alphaTab GP6 test corpus: all 35 files now decompress, parse
and round-trip.

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

Copy link
Copy Markdown
Author

Closing for now — this is stacked on #62 and it's premature to propose it before there's any direction on #62. Will revisit if #62 lands. Branch remains available.

@knoguchi knoguchi closed this Jun 26, 2026
@knoguchi knoguchi reopened this Jun 26, 2026
@knoguchi knoguchi closed this Jun 26, 2026
@knoguchi knoguchi deleted the feature/gpx-effects branch June 26, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant