Repository navigation
Description
Summary
lib/gp2rs.py (the binary Guitar Pro → arrangement-XML converter, used for GP3/GP4/GP5 import) never reads or propagates the arpeggio flag that Guitar Pro authors attach to a beat/chord. As a result, every GP-sourced chart imports its arpeggiated passages as ordinary strummed chords, and the downstream .feedpak handshape data (handshapes[].arp) is always false for GP imports — even when the source GP file explicitly marks the chord as an arpeggio.
This is not a request for a new wire-format concept — the arp field already exists and is spec-legal (see "Where this fits" below). It's a bug/gap in one converter: the flag exists in pyguitarpro's beat data, and there's a place downstream ready to receive it, but gp2rs.py never wires the two together.
Where this fits (already spec-legal, no FEP needed)
feedpak-spec/spec/feedpak-v1.md§6.5/§6.6 already definehandshape.arp(boolean — "treat as an arpeggio span rather than a strummed hold") and chord-templatearp.feedback-plugin-feedpakr'sfeedpakr_handshapes.py(derive_handshapes()) already readsarpstraight off the chord template the wire arrangement provides — i.e. the downstream consumer is correctly implemented and just waiting for the flag to actually be set.- The break is entirely upstream, in this repo.
What's happening in lib/gp2rs.py
-
ChordTemplate(around line 111) is defined as:@dataclass class ChordTemplate: name: str frets: list[int] # per string, -1 = unused fingers: list[int] # per string, -1 = unused
There is no
arp/arpeggio field at all. -
Chord templates are built and appended around line 1027 (
chord_templates.append(ChordTemplate(name="", frets=list(frets), fingers=[-1] * width))) with no arpeggio detection anywhere in that code path. -
gp2rs.pydoes already read other per-beat effect data off pyguitarpro'sguitarpro.BeatEffect/guitarpro.NoteEffectobjects in this same function — e.g.:_gp_pick_direction()(line ~274) readseffect.stroke.direction/effect.pickStrokefor strum direction, with detailed comments on which pyguitarpro field GP writes for authored chord strums vs. individual note picks.getattr(eff, "letRing", False)(line ~911) is read and mapped tolink_next.
So the plumbing pattern for "read a pyguitarpro beat/note effect flag → set it on the wire model" is already established in this file; arpeggio just isn't one of the flags being read.
-
The only place in the codebase that currently sets
arp/arpeggioon a chord template or handshape is the legacy arrangement-XML path (RS2014/EOF<handShape>elements with adisplayNameending in-arpor an explicitarpattribute) — i.e. the loose-folder importer, not the binary GP parser. GP-authored arpeggio data specifically is what's being lost.
What needs to happen
- Identify the pyguitarpro field(s) that carry arpeggio/"let ring as arpeggio" semantics for a chord beat. (pyguitarpro exposes GP's
BeatStrokewith a "brush"/arpeggio-style stroke distinct from a simple down/up strum — worth checkingguitarpro.BeatStrokeDirectionand any arpeggio-specific enum/flag alongside it, and cross-checking against how EOF/RS2014 authoring tools decide when to emitarp="1"on a<handShape>, since that's the semantics we're trying to match.) - Add an
arp: bool = Falsefield toChordTemplateingp2rs.py. - In the chord-template construction/enrichment code (~line 1000-1045), set
arp=Truewhen the source GP beat carries that flag — following the existing "enrich from the GP chord diagram, only when it matches the actually-played voicing" guard pattern already used for name/fingers back-fill. - Thread
arpthrough to the arrangement XML output (whereverChordTemplatefields are currently serialized — the fret/finger writing near the XML-emission code, mirroring howlinkNext/pickDirectionattrs are written per-note around line 1277/1316). - Verify
feedpakr_handshapes.py'sderive_handshapes()picks it up correctly with no changes needed on the feedpakr side (per the earlier investigation, it already readstemplates[cid].get('arp')— this should "just work" once gp2rs.py sets the flag). - Add a regression test in
tests/(this repo usespytest,tests/test_*.py, config inpyproject.toml) importing a small fixture GP file with a known authored arpeggio and asserting the resultingChordTemplate.arp/ arrangement-XML output.
Context / mental model
For anyone unfamiliar with the RS/GP arpeggio convention: an arpeggio marker in Rocksmith-style notation is a "hold this chord shape" indicator, not an instruction to strike all strings at once. The shape shown establishes the fretting-hand handshape; only the individually highlighted notes crossing the strike line are actually picked at that moment, in sequence. Rocksmith 2014 arrangements generally represent this via the handshape mechanism (arp flag) rather than a dedicated arpeggio note-type — which is exactly the mechanism feedpak's handshape.arp already models. The gap here is purely that GP-sourced imports never populate it.
Repos involved (for reference, no action needed elsewhere)
get-flashbacks/feedpak-spec— format already supports this (§6.5/§6.6), no changes needed.get-flashbacks/feedback-plugin-feedpakr—feedpakr_handshapes.pyalready correctly consumesarpfrom chord templates, no changes needed.get-flashbacks/feedback(this repo) —lib/gp2rs.pyis where the fix belongs.

Activity
pullfrog commentedon Aug 8, 2026
Implementation Plan — #57: GP import drops arpeggio flag (chord templates never get
arp)Root cause (confirmed)
lib/gp2rs.py(GP3/4/5 path) has no arpeggio plumbing anywhere.ChordTemplate(gp2rs.py:111) lacks anarpfield, chord templates are constructed atgp2rs.py:1027with no arpeggio detection, and_build_xmlemits<chordTemplate>with onlychordName/fretN/fingerN(gp2rs.py:1244-1251). The downstream chain already supports it end-to-end:lib/song.py:76—ChordTemplate.arpeggio: bool = Falsealready exists.lib/song.py:342—chord_template_to_wire()already emits"arp": ct.arpeggioon the wire.lib/song.py:1053—_chord_template_arpeggio_flag()already parsesarp/Arpeggio/arpeggio/Arp(or a-arpdisplayName) off a<chordTemplate>element.ws_highway.py:728ships templates viachord_template_to_wire; feedpakr'sderive_handshapes()consumestemplates[cid].get('arp')(verified by the issue author; the plugin isn't cloned here, so step 9 re-checks it).So the whole fix is upstream: set
arpin gp2rs, emit it on<chordTemplate>, and the flag flows to the wire untouched.Key finding — what pyguitarpro actually exposes (issue step 1)
pyguitarpro's GP3/4/5 beat model has no binary arpeggio flag. The relevant fields on
BeatEffectarestroke(BeatStroke(direction, value)),pickStroke(bareBeatStrokeDirection), andhasRasgueado(flamenco strumming — exclude, not an arpeggio):effect.strokeis the brush — the arpeggio-style chord mark GP's own UI writes when you drag across a chord. Itsvalueis the roll speed (Duration1=hundredTwentyEighth … 6=quarter).effect.pickStrokeis a rare bare up/down strum mark with no brush semantics._gp_pick_direction()(gp2rs.py:274) already keys on exactly this distinction (.strokefirst,.pickStrokefallback), and its comment documents the brush as "arpeggio chord-strum".Detection decision: treat a present
effect.strokewithdirection != BeatStrokeDirection.noneas the arpeggio marker (setsarp), and a bare.pickStrokeas a plain strum (does not setarp). This matches the issue's framing and the existing_gp_pick_directionprecedent — one helper, one signal. Open refinement to validate against real GP files: gate onstroke.value(roll speed) so only slow brushes (≥ eighth,value >= 5) count as arpeggio; presence-based is the recommended first cut, with the speed gate as a follow-up knob if a validation pass shows GP-authored strums being over-tagged.Scope
In scope:
lib/gp2rs.py(GP3/4/5) +lib/song.pyround-trip + tests. The GPX path (lib/gp2rs_gpx.py) is a related-but-separate module; GPIF exposes a genuinely distinct<Arpeggio><Beat>child (seegp2rs_gpx.py:654-686), which is a cleaner signal than GP4/5. Kept as an optional follow-up (Milestone 5) rather than folded into this issue — its detection differs and it needs its own tests.Milestones
M1. Add the field and a detector helper (
lib/gp2rs.py)arp: bool = FalsetoChordTemplate(gp2rs.py:110-114). Mirrorslib/song.py'sChordTemplate.arpeggio. DefaultFalsekeeps the piano path (convert_piano_track,gp2rs.py:1663) and drum path (convert_drum_track,gp2rs.py:1844) byte-for-byte unchanged._gp_beat_arpeggio(effect) -> boolnext to_gp_pick_direction(gp2rs.py:274), same defensivegetattrstyle: returnsTruewheneffect.strokeexists withdirectionpresent and!= BeatStrokeDirection.none;Falseotherwise. Do not fall back to.pickStroke(plain strum ≠ arpeggio). One short docstring noting the brush = GP's arpeggio notation and whyhasRasgueado/pickStrokeare excluded.M2. Set
arpduring chord-template construction (convert_track,gp2rs.py:1006-1034)idx = chord_template_map[fret_key]resolves (both the create and reuse paths), applyct = chord_templates[idx]; if _gp_beat_arpeggio(beat.effect): ct.arp = True.if beat.effect and beat.effect.chord:atgp2rs.py:1048): the brush is a per-beat articulation, not diagram data, so arp must attach even for beats with no diagram. Name/fingers enrichment stays as-is behind the existing guard.fret_key, so one arpeggiated beat marks every strum sharing that voicing — the same template-level model feedpakr consumes, consistent with name/fingers back-fill.M3. Emit
arp="1"in_build_xml(gp2rs.py:1244-1251)<chordTemplate>loop, addif ct.arp: attrs["arp"] = "1"alongsidechordName.lib/song.py:1053reads it back; absent arp → attribute omitted (matches the default-omit convention used forbendIntent/fretFinger/pickDirection). No change tosong.pyneeded.M4. Regression tests (
tests/test_gp2rs.py)Reuse the existing fixture-free
_ct_song/_ct_beat/_ct_note/_ct_chordhelpers (seetest_chord_diagram_fingers_extractedattest_gp2rs.py:1206and the stroke tests at:1259). Cover:effect.stroke = SimpleNamespace(direction=BeatStrokeDirection.down, value=64)→ output XML's<chordTemplate>hasarp="1", and the chord notes still carrypickDirection="0"(both signals coexist).arpattribute (plain strum).pickStroke(up) → noarpattribute,pickDirection="1"still set (brush-vs-strum distinction).strokewithdirection=none→ noarp.song.parse(or the equivalent used by thefground-trip test at:1235) and assertchord_template_to_wire(...)["arp"] is True, proving the flag survives XML → dataclass → wire.test_chord_beat_stroke_field_imports_as_pkdand_gp_pick_directiontests still pass (their mockedeffectshapes must keep working — the helper readsstrokedefensively).M5 (optional, separate follow-up). GPX path (
lib/gp2rs_gpx.py)GPIF has a real
<Arpeggio>direct<Beat>child — a distinct arpeggio articulation vs the<Property name="Brush">strum._gpif_pick_directioncurrently folds both into direction. A follow-up could setarp=Trueon the GPX chord template (built atgp2rs_gpx.py:2029) when<Arpeggio>is present, with its own GPIF fixture tests in thetest_gp2rs_gpx.pystyle (test_gpif_pick_direction_falls_back_to_arpeggioalready shows the fixture shape).Verification (M4 + manual)
python -m pytest tests/test_gp2rs.py -v(sandbox:pip install pytestfirst; skip TestClient files, per repo learnings).<chordTemplate>in existing output gains spuriousarp(default-omit keeps current golden output identical).feedpakr_handshapes.pyat review time to reconfirmtemplates[cid].get('arp')consumption (issue author states it's already correct).Delivery notes
lib/+tests/. Per the fork'score-commit-labelinggate, the PR's commit subjects touchinglib/**need a^(core|hook|sync|fix)(\([^)]*\))?:prefix —fix: gp2rs import arp...orcore:both pass; a bare "Add arp flag…" subject will fail CI.[Unreleased]entry underCHANGELOG.mdper repo convention.pullfrog/57-gp2rs-arpeggio-flag; do not push tomain.Open questions for the implementer
Task list (5/6 completed)
arp#57 and gather contextarpflag flowsBig Pickle(free) | 𝕏