Repository navigation
fix: propagate GP arpeggio flag through gp2rs.py chord templates - #78
Conversation
GP3/4/5 imports never read the arpeggio "brush" marker off pyguitarpro's BeatEffect, so ChordTemplate.arp was always false for GP-sourced charts and every arpeggiated passage rendered as an ordinary strummed chord. The wire format and downstream consumers (feedpakr's derive_handshapes, highway_3d's arpeggio frame/bracket visualization) already handle arp correctly for loose-folder/XML imports — this was purely a gp2rs.py gap. Adds ChordTemplate.arp and _gp_beat_arpeggio(), which reads the same effect.stroke field _gp_pick_direction() already uses for strum direction, and emits arp="1" on <chordTemplate> for song.py's existing _chord_template_arpeggio_flag() to read back. Closes #57 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe
|
The failing check (CodeFactor, suite 94080923310) reported 4 Bandit B314 findings on Fix (pushed as Verification: Task list (1/7 completed)
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 18 |
| Duplication | -2 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
Five arpeggio regression tests each rebuilt the same two-note chord beat inline. Extracted _arp_chord_beat() so CodeFactor's duplicate- code detector (4 issues found on PR #78) has nothing left to flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe
|
CI status check-in. The
Standing down on it for now. The real CI ( 🤖 Generated with Claude Code https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe Generated by Claude Code |
There was a problem hiding this comment.
Pull Request Overview
This PR implements the propagation of Guitar Pro arpeggio flags into the arrangement XML. While the detection logic for 'brush' flags is sound, the implementation of chord template reuse contains a significant bug. Currently, the deduplication logic only uses fret positions as a key; if a specific chord voicing is used as both a standard strum and an arpeggio, the code incorrectly marks all instances of that voicing as arpeggiated in the XML output. This 'arpeggio leakage' will cause incorrect visual rendering in the game for non-arpeggiated chords. Addressing this requires including the arpeggio state in the template deduplication key.
About this PR
- The current template deduplication strategy assumes that a chord's identity is defined solely by its fret positions. As more performance-specific attributes (like arpeggios, fingering, or techniques) are added to templates, the deduplication key (fret_key) must be expanded to include these attributes to prevent visual or gameplay data from 'bleeding' between different musical expressions of the same voicing.
1 comment outside of the diff
lib/gp2rs.py
line 1040-1059🟡 MEDIUM RISK
This implementation causes 'arpeggio leakage': because chord templates are deduped only by fret positions (thefret_key), marking one beat as an arpeggio will setarp=Trueon the shared template, affecting all other strummed instances of that same voicing in the XML. To support both strummed and arpeggiated versions of the same chord, the arpeggio state must be part of the template's identity.Modify the logic starting at line 1040: calculate
is_arpusing_gp_beat_arpeggio(beat.effect), include it in thefret_keytuple, and passarp=is_arpto theChordTemplateconstructor. Subsequently, remove the manualchord_templates[idx].arp = Trueassignment at lines 1058-1059.
Test suggestions
- Verify _gp_beat_arpeggio detector logic with various stroke and pickStroke combinations.
- Verify that a chord beat with a valid stroke effect triggers the 'arp="1"' attribute in the output XML.
- Verify that a chord beat without a stroke effect (or with a 'none' direction) does not set the arpeggio flag.
- Verify that the arpeggio flag survives a full round-trip from XML to the internal wire representation.
- Verify that a bare pickStroke does not trigger the arpeggio flag even if pick direction is detected.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
Chord templates deduped by fret voicing alone, so a chord shape played both arpeggiated in one passage and plainly strummed elsewhere in the same song shared a single ChordTemplate. Marking that template arp=True for the arpeggiated beat then incorrectly marked every other strum of the same voicing as an arpeggio too. Fold arp status into the dedup key (fret_key) so the two articulations of the same voicing get separate templates, and pass arp on construction instead of mutating a shared template after the fact. Found by Codacy's automated review on PR #78. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe
|
Fixed the arpeggio-leakage bug from Codacy's review (pushed as 🤖 Generated with Claude Code https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe Generated by Claude Code |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run reviews all 3 commits on head c3a0998 (initial review of PR #78).
lib/gp2rs.py— addsChordTemplate.arp, the_gp_beat_arpeggio()detector (any realeffect.stroke.directionon a chord beat marks it as an arpeggio, deliberately without a.pickStrokefallback), setsarpon both the template-create and template-reuse paths inconvert_track, and emitsarp="1"on<chordTemplate>;lib/song.py's existing_chord_template_arpeggio_flag()reads it back unchanged.tests/test_gp2rs.py— 6 new tests pin the detector truth table, the XML emission on both paths, the bare-pickStrokeexclusion, and a full XML →parse_arrangement→chord_template_to_wireround-trip; ~30 existing tests migrateET.fromstring→safe_xml.safe_fromstring, clearing the CodeFactor B314 finding.CHANGELOG.md—[Unreleased]entry recording the fix for #57.
The GP semantics check out: pyguitarpro's GP4/5 model reads BeatStroke(direction, value) as the "beat stroke" flag (0x40), which is GP's brush ≡ arpeggio mark, while pickStroke is the separate plain strum-direction mark — so treating .stroke presence as arp (and excluding .pickStroke) is the right signal, and the "bare pick-stroke stays a strum" exclusion is what keeps ordinary strums un-flagged. The new tests genuinely pin the behavior — 3 of them fail when the detector is temporarily disabled. All CI checks are green, and no DCO gate exists on this repo, so the pending git commit -s sign-off needs no action.
ℹ️ Arp propagation stops at the GP3/4/5 guitar path — GP8 and piano remain uncovered
The changelog scopes the fix to "GP3/4/5 chord beats", and that's exactly what lands: only convert_track calls _gp_beat_arpeggio. Two adjacent import paths still drop the flag.
lib/gp2rs_gpx.py:_gpif_pick_direction()(GP7/8 import) already reads both GPIF markers —<Property name="Brush">and<Arpeggio>Down|Up</Arpeggio>— but maps them only to per-notepickDirection, never to templatearp. GPIF's<Arpeggio>(a "broken chord... notes struck in sequence", per its own docstring) is arguably the closest GP analogue to the RS/feedpakarpsemantics, so the most apt signal sits there unpropagated.convert_piano_trackbuildschord_templatesbut never sets.arp, so brushes on GP piano chords (a common pianism) don't propagate either.
Not a defect in this PR — the delivered scope is complete and tested — but worth an explicit decision in #57 on whether GP8/piano follow-ups are intended, so the gap is tracked rather than rediscovered.
Technical details
# GP8 + piano arp propagation (follow-up scope)
## Affected sites
- lib/gp2rs_gpx.py:_gpif_pick_direction (~line 654) — reads GPIF Brush + <Arpeggio> for pick direction only; never sets a template/chord arp flag.
- lib/gp2rs.py:convert_piano_track (~line 1536) — builds chord_templates (arp stays default False); no _gp_beat_arpeggio call.
## Open questions for the human
- Does #57 intend guitar-only (GP3/4/5), or also GP7/8 (.gpx) and piano paths?
- For GP8, is template `arp` the right target, or should the `<Arpeggio>` (broken-chord) marker be modeled distinctly from Brush?ℹ️ Template dedup spreads one brushed beat across every strum of the same voicing
The site comment in convert_track already documents this: templates dedupe by fret_key and arp is set on the template, so a single brushed beat marks every strum sharing that fingering — including unbrushed ones — as arp downstream. Because the wire arp lives on the template (chord_template_to_wire) and consumers reference it by chord_id, this is inherent to the data model rather than a per-instance fix. Flagged for the record: importing a song that plays one voicing both brushed and plain will render the plain strums with the arpeggio treatment, which may read as a bug report down the line even though it is an accepted, documented tradeoff here.
Technical details
# Template-level arp contamination (documented tradeoff, no change requested)
## Affected sites
- lib/gp2rs.py:~1058 — `chord_templates[idx].arp = True` on the shared (fret_key-deduped) template.
- lib/song.py:chord_template_to_wire — wire `arp` is template-scoped.
## Open questions for the human
- Acceptable to leave as-is given the format's template-level `arp`? (Author's comment says yes; fine by me.)Big Pickle (free) | 𝕏
|
Thanks for the review — both notes were flagged as informational (ℹ️), not blocking:
🤖 Generated with Claude Code https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe Generated by Claude Code |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run covers the commits landed since the prior pullfrog review of c3a0998: commit 3238e65, which closes the chord-template leak.
lib/gp2rs.py—is_arpis now folded into the template dedup key (fret_key = (tuple(frets), is_arp)) and passed intoChordTemplateat construction, replacing the post-hocchord_templates[idx].arp = Truemutation. A voicing played both arpeggiated in one passage and plainly strummed in another now yields two separate templates, each carrying the correctarp; the chord-to-template references and the<chordTemplate arp="1">emission are unchanged and consistent with the new key.tests/test_gp2rs.py— newtest_arpeggio_does_not_leak_to_other_strums_of_same_voicingbuilds an arpeggiated + plain strum of the identical voicing and asserts two templates with distinctchordIds and correctarpeach.CHANGELOG.md—[Unreleased]entry updated to describe the dedup-by-voicing-and-arp behavior.
The leak test genuinely pins the fix: reverted the dedup-key change and it fails with assert 1 == 2. The full touched suite passes (323 tests). The earlier informational observation about GP8 (.gpx) and piano paths not propagating arp remains open as a scope question for #57, but nothing here needs to change before merge.
Big Pickle (free) | 𝕏

What
lib/gp2rs.py(the binary Guitar Pro → arrangement-XML converter, used for GP3/GP4/GP5 import) never read or propagated the arpeggio "brush" flag Guitar Pro authors attach to a chord beat. Every GP-sourced arpeggiated passage therefore imported as an ordinary strummed chord, and the downstream.feedpakhandshape data (handshapes[].arp) was alwaysfalsefor GP imports — even when the source GP file explicitly marked the chord as an arpeggio. The already-implemented downstream consumers (feedpakr'sderive_handshapes(), and thehighway_3dplugin's arpeggio chord-frame/bracket visualization) were simply never fed the flag for GP imports; loose-folder/XML-authored charts already worked correctly.Fix:
ChordTemplate.arp: bool = Falseingp2rs.py._gp_beat_arpeggio(effect), which readseffect.stroke.direction— the same pyguitarpro "brush" field_gp_pick_direction()already reads for strum direction — treating any real (non-none) direction as the arpeggio marker. A bare.pickStroke(no.stroke) stays a plain strum, not an arpeggio.arpon both the create and reuse paths of chord-template construction inconvert_track, independent of whether the beat also carries a chord diagram.arp="1"on<chordTemplate>in_build_xml.lib/song.py's existing_chord_template_arpeggio_flag()already reads this back — no changes needed downstream.Closes #57.
feedpak surface
lib/song.pyalready parses today.Checklist
CHANGELOG.md[Unreleased]updated (user-visible changes)tests/test_gp2rs.py— 6 new tests covering the detector, XML emission, and a full XML →parse_arrangement→ wire round-trip; fulltest_gp2rs.py/test_song.py/test_parse_arrangement.pysuite passes, 322 tests)git commit -s) — not yet applied; flag if this is required before review.Marked as draft since #57's linked PR (#59) was still an empty Copilot stub — happy to convert to ready-for-review once you've had a look.
🤖 Generated with Claude Code
https://claude.ai/code/session_018jAfdcSJGQw9cMnVpjbfQe
Generated by Claude Code