mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-15 21:27:14 +00:00
feat(core): per-note bend shape (bt + bnv) — wire + GP import (#531)
Implements feedpak spec §6.2.1 (feedpak 1.4.0) per-note bend shape on the
core side:
- `bn` stays the bend's peak magnitude in semitones (unchanged).
- `bt` — bend intent (0 up, 1 release, 2 pre-bend, 3 pre-bend-release,
4 round-trip), default 0, default-omitted on the wire.
- `bnv` — time-stamped bend curve [{t: seconds-from-onset, v: semitones}],
authoritative when present; default-omitted. Older readers ignore both.
Wire (lib/song.py): Note.bend_intent/bend_values; note_to_wire emits bt/bnv
only when set; note_from_wire reads them via _sanitize_bend_curve (drops
malformed entries, empty -> None never []). _parse_note reads them from the
GP-import XML (bendIntent attr + bendValues JSON) so GP curves survive
import -> XML -> wire -> highway.
GP5 (lib/gp2rs.py): _gp_bend_shape maps pyguitarpro BendPoints to a bnv
curve — semitones = value/2.0 (consistent with the existing scalar bn),
t = position/12 * duration — and _bend_intent_from_values derives bt from
the shape. Emitted for <note> and <chordNote> via the shared _build_xml.
GP8 (lib/gp2rs_gpx.py): _gpx_bend_shape builds a 3-point curve from the
GPIF origin/middle/destination value+offset Properties (value/divisor
semitones, offset/100 * sustain seconds), reusing the shared _build_xml.
GPIF offset Property names should be confirmed against a real GP8 export.
Tests cover wire round-trip + default-omit + sanitization, GP5 unit/time
mapping + intent classification end-to-end through the XML, and the GP8
curve builder.
Part of got-feedback/feedback#334
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8382139ca
commit
e33df9a720
@@ -31,6 +31,7 @@ from gp2rs_gpx import (
|
||||
_collect_tone_events,
|
||||
_inject_tones,
|
||||
_resolve_pending_slides,
|
||||
_gpx_bend_shape,
|
||||
)
|
||||
from gp2rs import RsNote
|
||||
|
||||
@@ -55,6 +56,50 @@ def test_safe_filename_stem(name, expected):
|
||||
assert ".." not in out
|
||||
|
||||
|
||||
# ── _gpx_bend_shape (bn / bt / bnv, §6.2.1) ─────────────────────────────────
|
||||
|
||||
def _bend_props(**vals):
|
||||
"""Build a GPIF property map {name: <Property> element} for the given
|
||||
bend Float values, e.g. _bend_props(BendOriginValue=0, BendMiddleValue=100)."""
|
||||
tp = {}
|
||||
for name, num in vals.items():
|
||||
tp[name] = ET.fromstring(
|
||||
f'<Property name="{name}"><Float>{num}</Float></Property>')
|
||||
return tp
|
||||
|
||||
|
||||
def test_gpx_bend_shape_round_trip_curve():
|
||||
"""origin/middle/destination value+offset → 3-point bnv; value/divisor=semis."""
|
||||
tp = _bend_props(
|
||||
BendOriginValue=0, BendOriginOffset=0,
|
||||
BendMiddleValue=100, BendMiddleOffset1=50, # 100/50 = 2 semitones
|
||||
BendDestinationValue=0, BendDestinationOffset=100,
|
||||
)
|
||||
peak, intent, curve = _gpx_bend_shape(tp, divisor=50.0, sustain=1.0)
|
||||
assert peak == 2.0
|
||||
assert intent == 4 # round-trip (up then back down)
|
||||
assert curve == [
|
||||
{"t": 0.0, "v": 0.0}, {"t": 0.5, "v": 2.0}, {"t": 1.0, "v": 0.0}]
|
||||
|
||||
|
||||
def test_gpx_bend_shape_falls_back_to_even_spacing_without_offsets():
|
||||
tp = _bend_props(BendOriginValue=0, BendDestinationValue=100) # no offsets
|
||||
peak, intent, curve = _gpx_bend_shape(tp, divisor=50.0, sustain=1.0)
|
||||
assert peak == 2.0
|
||||
assert intent == 0 # plain up
|
||||
# origin defaults to 0%, destination to 100%.
|
||||
assert curve == [{"t": 0.0, "v": 0.0}, {"t": 1.0, "v": 2.0}]
|
||||
|
||||
|
||||
def test_gpx_bend_shape_no_props_and_zero_length():
|
||||
assert _gpx_bend_shape({}, divisor=50.0, sustain=1.0) == (0.0, 0, None)
|
||||
# Peak + intent still derived for a zero-length note, but no curve.
|
||||
peak, intent, curve = _gpx_bend_shape(
|
||||
_bend_props(BendOriginValue=0, BendDestinationValue=100),
|
||||
divisor=50.0, sustain=0.0)
|
||||
assert peak == 2.0 and intent == 0 and curve is None
|
||||
|
||||
|
||||
# ── _decompress_bcfz / _parse_bcfs input guards ─────────────────────────────
|
||||
|
||||
def test_decompress_bcfz_rejects_bad_magic():
|
||||
|
||||
Reference in New Issue
Block a user