Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **`lib/gp2rs.py` now propagates the Guitar Pro arpeggio/handshape flag on import** (#57). GP3/4/5 chord beats never set `arp` on their `ChordTemplate`, so every GP-sourced arpeggiated passage imported as an ordinary strummed chord and the downstream `.feedpak` handshape data (`handshapes[].arp`) was always `false` — even when the source GP file explicitly marked the chord as an arpeggio. The already-implemented downstream consumers (feedpakr's `derive_handshapes()`, the `highway_3d` plugin's arpeggio frame/bracket visualization) were simply never fed the flag for GP imports. Added a `ChordTemplate.arp` field and a `_gp_beat_arpeggio()` detector that reads the same `effect.stroke` "brush" field `_gp_pick_direction()` already uses for strum direction, treating a present, non-`none` stroke as the arpeggio marker (a bare `.pickStroke` stays a plain strum, not an arpeggio). Threaded through to the `<chordTemplate arp="1">` XML attribute, which `lib/song.py`'s existing `_chord_template_arpeggio_flag()` already reads back — no downstream changes needed. Chord templates dedupe by fret voicing **and** arp status together, so a voicing played both arpeggiated in one passage and plainly strummed in another gets two separate templates instead of one shared template "leaking" the arpeggio flag onto the unrelated strum (caught in review on PR #78).
- **keys_highway_3d now respects the mastery/difficulty slider** (#67). The plugin fetches `notation_measures` over its own private `/ws/highway` connection and never consulted the host's difficulty-filtered note stream, so lowering the mastery percentage had no visible effect — the highway always showed the full chart. Notation carries no per-note difficulty tag of its own (it's a structurally separate representation from the tab/gameplay note stream — see feedback-plugin-difficulty-ladder#90), so this correlates each flattened notation event's onset against the chart's phrase windows (now also streamed to this plugin, chunked like `notation_measures`) and the render bundle's ALREADY mastery-filtered tab content (`bundle.notes`/`bundle.chords`): a phrase is "playable" when the tab has any filtered note/chord in that phrase's `[start_time, end_time)` window, and a notation event is kept when its onset falls inside a playable phrase. Recomputes (and rebuilds note meshes + resets scoring) whenever `bundle.mastery` changes; a fixed-difficulty chart with no phrase data is unaffected.

### Changed
Expand Down
30 changes: 29 additions & 1 deletion lib/gp2rs.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class ChordTemplate:
name: str
frets: list[int] # per string, -1 = unused
fingers: list[int] # per string, -1 = unused
arp: bool = False # arpeggio/handshape marker — see _gp_beat_arpeggio


@dataclass
Expand Down Expand Up @@ -302,6 +303,22 @@ def _gp_pick_direction(effect) -> int:
return -1


def _gp_beat_arpeggio(effect) -> bool:
"""Read a chord beat's arpeggio marker off a pyguitarpro BeatEffect.

`effect.stroke` (BeatStroke(direction, value)) is the "brush" GP's own UI
writes when you drag across a chord to arpeggiate it — see
`_gp_pick_direction` above for the same field used for strum direction.
Unlike pick direction, arpeggio does NOT fall back to `.pickStroke`: a
bare pick-stroke mark is a plain up/down strum, not an arpeggio brush.
"""
if effect is None:
return False
stroke = getattr(effect, "stroke", None)
direction = getattr(stroke, "direction", None) if stroke is not None else None
return direction is not None and direction != guitarpro.BeatStrokeDirection.none


def _pick_direction_xml_attrs(n: "RsNote") -> dict:
"""Optional `pickDirection` XML attribute for a <note>/<chordNote>, default-
omitted (!= -1 only). `_parse_note` (lib/song.py) reads it back as `pkd` on
Expand Down Expand Up @@ -1020,14 +1037,23 @@ def convert_track(
for n in beat_notes:
if 0 <= n.string < width:
frets[n.string] = n.fret
fret_key = tuple(frets)
# Arpeggio is part of the template's identity, not a
# backfilled attribute: the same voicing can be strummed
# in one passage and arpeggiated in another, and each
# must render correctly on its own. Folding is_arp into
# the dedup key (rather than mutating a shared template's
# `arp` after the fact) keeps those two uses from
# "leaking" into each other via one shared ChordTemplate.
is_arp = _gp_beat_arpeggio(beat.effect)
fret_key = (tuple(frets), is_arp)

if fret_key not in chord_template_map:
idx = len(chord_templates)
chord_templates.append(ChordTemplate(
name="",
frets=list(frets),
fingers=[-1] * width,
arp=is_arp,
))
chord_template_map[fret_key] = idx
else:
Expand Down Expand Up @@ -1248,6 +1274,8 @@ def _build_xml(
for i in range(width):
attrs[f"fret{i}"] = str(ct.frets[i] if i < len(ct.frets) else -1)
attrs[f"finger{i}"] = str(ct.fingers[i] if i < len(ct.fingers) else -1)
if ct.arp:
attrs["arp"] = "1"
ET.SubElement(ct_el, "chordTemplate", **attrs)

# Single difficulty level with all notes
Expand Down
Loading
Loading