From bd22951874a7d9de03850f3da79d7ed668eebc8a Mon Sep 17 00:00:00 2001 From: zagatozee <33841901+zagatozee@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:40:40 +0100 Subject: [PATCH] feed the rabbit Updated the docstring for _load_gpif to clarify supported file formats and improved error messages for unrecognized containers.L (cherry picked from commit 1866c72e53339a10d6f184d21c2261e9f0b21395) --- lib/gp2rs_gpx.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/gp2rs_gpx.py b/lib/gp2rs_gpx.py index 3e1f8d1..dc37a22 100644 --- a/lib/gp2rs_gpx.py +++ b/lib/gp2rs_gpx.py @@ -133,15 +133,35 @@ def _parse_bcfs(bcfs: bytes) -> dict: def _load_gpif(gp_path: str) -> ET.Element: - """Load and parse score.gpif from a .gpx file.""" + """Load and parse score.gpif from a .gpx (GP6) or .gp (GP7/GP8) file. + + GP6 (.gpx): custom BCFZ/BCFS binary container holding score.gpif. + GP7/GP8 (.gp): standard ZIP archive holding Content/score.gpif. + Both formats use the same GPIF XML schema inside, so a single parser + handles all versions once the outer container is unpacked. + """ with open(gp_path, 'rb') as fh: raw = fh.read() + + # GP7/GP8: ZIP container (PK magic) + if raw[:2] == b'PK': + import zipfile + import io as _io + with zipfile.ZipFile(_io.BytesIO(raw)) as zf: + if 'Content/score.gpif' not in zf.namelist(): + raise ValueError("Content/score.gpif not found in GP7/GP8 ZIP container") + return ET.fromstring(zf.read('Content/score.gpif')) + + # GP6 (.gpx): BCFZ compressed or raw BCFS if raw[:4] == b'BCFZ': bcfs = _decompress_bcfz(raw) elif raw[:4] == b'BCFS': bcfs = raw else: - raise ValueError(f"Not a GPX file (magic: {raw[:4]!r})") + raise ValueError( + f"Unrecognised Guitar Pro container (magic: {raw[:4]!r}). " + f"Supported: .gpx (GP6, BCFZ/BCFS) and .gp (GP7/GP8, ZIP)." + ) fs = _parse_bcfs(bcfs) if 'score.gpif' not in fs: raise ValueError("score.gpif not found in GPX container") @@ -589,7 +609,8 @@ def list_tracks(gp_path: str) -> list[dict]: and not t['string_pitches'] # no string tuning = not guitar-family and 32 <= t['midi_program'] <= 39 ) or ( - t['string_pitches'] + not t['is_drums'] # explicit guard: drums can have low string pitches + and t['string_pitches'] and max(t['string_pitches']) <= 48 # bass top string ≤ C3 ) ) @@ -1400,7 +1421,7 @@ def convert_file( ) # Staff assignment for keys/piano (slopsmith staff schema). # Derived from voice position as authored in the GP tab — - # not inferred from pitch — preserving hand crossings exactly. + # not inferred from pitch — preserving hand crossings. # Voice 0 = treble/RH staff (staff=1), # Voice 1+ = bass/LH staff (staff=0). if is_keys: @@ -1589,7 +1610,7 @@ def convert_file( if _lh_bid != '-1' and _lh_bid: _lh_bar = bars_by_id.get(_lh_bid) if _lh_bar is not None: - for _lh_voice_pos, _lh_vid in enumerate(_lh_bar.findtext('Voices', '').split()): + for _lh_vid in _lh_bar.findtext('Voices', '').split(): if _lh_vid == '-1': continue _lh_voice = voices_dict.get(_lh_vid) @@ -1639,8 +1660,10 @@ def convert_file( fret=_lh_midi % 24, sustain=_lh_dur if _lh_dur > 0.2 else 0.0, ) - # Preserve authored voice-position staff assignment. - _lh_rn.staff = 1 if _lh_voice_pos == 0 else 0 + # LH track notes are always bass clef (staff=0). + # The LH track as a whole IS the left hand — + # voice position within the LH track is irrelevant. + _lh_rn.staff = 0 _lh_notes.append(_lh_rn) _lh_last_per_key[_lh_midi] = _lh_rn _lh_vt += _lh_dur