feat(gp_autosync): piecewise time-warp helpers + refine_sync onset pass - #787
Conversation
📝 WalkthroughWalkthroughAdds a librosa-free piecewise time-warping layer to ChangesGP Autosync Warp Refinement
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant refine_sync
participant bar_start_times
participant build_warp_anchors
participant warp_time
participant AudioOnsets
Caller->>refine_sync: refine_sync(sync, audio_path, gp_path)
refine_sync->>bar_start_times: bar_start_times(gp_path)
bar_start_times-->>refine_sync: exact bar starts
refine_sync->>build_warp_anchors: build_warp_anchors(sync_points, bar_starts)
build_warp_anchors-->>refine_sync: warp anchors
refine_sync->>warp_time: warp_time(bar_time, anchors)
warp_time-->>refine_sync: estimated audio time
refine_sync->>AudioOnsets: onset-phase sweep around estimate
AudioOnsets-->>refine_sync: refined bar time
refine_sync-->>Caller: new GpSyncData with updated audio_offset
Related issues: Suggested labels: enhancement, audio-sync, tests Suggested reviewers: None specified 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR expands lib/gp_autosync.py beyond a single scalar audio_offset by adding librosa-free helpers that build/apply a piecewise score-time → audio-time warp (Songsterr-style), plus a working refine_sync() implementation. It also fixes a GP3/4/5 tick-origin skew affecting tempo-event timing and chroma alignment, and adds targeted, fixture-free tests for the new warp helpers.
Changes:
- Add public warp helper APIs (
bar_start_times,build_warp_anchors,warp_time,warp_song_times,gp_has_expandable_repeats) and implementrefine_sync()inlib/gp_autosync.py. - Normalize GP3/4/5 tick origin (PyGuitarPro tick axis) and factor bar-start integration logic into shared helpers.
- Add new unit tests for the warp helpers and document the feature in
CHANGELOG.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
lib/gp_autosync.py |
Adds warp helper APIs + refine_sync(), fixes GP3/4/5 tick-origin skew, factors shared bar-start logic. |
tests/test_gp_autosync_warp.py |
New fixture-free tests covering warp helpers and refine_sync early-return behavior. |
lib/gp2rs_gpx.py |
Adds a warning comment clarifying single-pass GPIF repeat behavior and its coupling to sync warping. |
CHANGELOG.md |
Documents the new warp helpers and refine_sync() under Unreleased. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
auto_sync computes per-bar sync points but consumers only ever applied the scalar bar-1 audio_offset, so any tempo drift between the recording and the tab's authored tempo accumulated over the song. Add the librosa-free helpers needed to apply the full piecewise mapping: - bar_start_times(gp_path): per-bar score times sharing auto_sync's axis (GPIF bar-resolution map, GP3/4/5 per-tick integration) - build_warp_anchors(points, bar_starts): monotonic (score, audio) anchors - warp_time(t, anchors): piecewise-linear map with edge-slope extrapolation - warp_song_times(song, warp): retime a lib.song.Song in place (notes, sustains, chords, beats, sections, anchors, handshapes, phrase levels, tone changes, tempo overrides) - gp_has_expandable_repeats(gp_path): detects GP3/4/5 repeat/volta/direction markup whose playback expansion auto_sync's as-written points cannot map Also implement refine_sync() — the editor's refine-sync endpoint has imported it since the snapshot but it never existed in lib, so the Refine button 500'd. It densifies the DTW points to every Nth bar and re-times each with a local onset phase sweep (radius clamped under half a beat to avoid one-beat locks, short scoring grid + median residual snap against the first beats). Synthetic click-track validation: ~13ms mean / ~40ms max error from ±180ms coarse input across 117-123 BPM recordings of a 120 BPM tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d18282e to
f5ac916
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_gp_autosync_warp.py (1)
241-270: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor duplication between repeat-detection tests.
test_gp345_repeats_detectedandtest_gp345_plain_song_no_repeatsshare nearly identical song/track/header construction, differing only in whetherisRepeatOpenis set. Could be factored into a small helper (e.g._build_gp345_song(tmp_path, name, repeat_open=False)) to reduce duplication.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_gp_autosync_warp.py` around lines 241 - 270, The two repeat-detection tests duplicate the same Song/Track/MeasureHeader setup in test_gp_autosync_warp.py. Refactor the shared setup into a small helper near test_gp345_repeats_detected and test_gp345_plain_song_no_repeats (for example, a helper that builds the GP345 song and accepts a repeat-open flag), then have both tests call it and only vary whether MeasureHeader.isRepeatOpen is set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_gp_autosync_warp.py`:
- Around line 241-270: The two repeat-detection tests duplicate the same
Song/Track/MeasureHeader setup in test_gp_autosync_warp.py. Refactor the shared
setup into a small helper near test_gp345_repeats_detected and
test_gp345_plain_song_no_repeats (for example, a helper that builds the GP345
song and accepts a repeat-open flag), then have both tests call it and only vary
whether MeasureHeader.isRepeatOpen is set.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 24138a4a-9550-475d-9663-a2f719d0676f
📒 Files selected for processing (4)
CHANGELOG.mdlib/gp2rs_gpx.pylib/gp_autosync.pytests/test_gp_autosync_warp.py
…lures to ValueError, document ImportError Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/gp_autosync.py (1)
395-406: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep the explicit opening tempo when ticks collide
A bar-1
mixTableChangenormalizes to tick0, but the seeded(0, song.tempo)entry is added first and the dedup keeps the first event per tick. That silently drops an authored opening tempo change and leavessong.tempodriving the early timeline. Prefer the later event on duplicate ticks so the explicit tempo wins.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/gp_autosync.py` around lines 395 - 406, The dedup logic in the tempo event merge drops later events at the same tick, which causes the seeded opening tempo to override an authored bar-1 mixTableChange. Update the unique-building logic in the tempo collection flow around events.sort/seen_ticks so duplicate tick entries keep the later event instead of the first, ensuring the explicit opening tempo from the mixTableChange wins over song.tempo.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@lib/gp_autosync.py`:
- Around line 395-406: The dedup logic in the tempo event merge drops later
events at the same tick, which causes the seeded opening tempo to override an
authored bar-1 mixTableChange. Update the unique-building logic in the tempo
collection flow around events.sort/seen_ticks so duplicate tick entries keep the
later event instead of the first, ensuring the explicit opening tempo from the
mixTableChange wins over song.tempo.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b780e33-6d33-43bc-8692-782b2b9e8f86
📒 Files selected for processing (5)
CHANGELOG.mdlib/gp2rs_gpx.pylib/gp_autosync.pystatic/tailwind.min.csstests/test_gp_autosync_warp.py
✅ Files skipped from review due to trivial changes (2)
- lib/gp2rs_gpx.py
- CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/test_gp_autosync_warp.py
What
auto_sync()computes per-bar sync points (chroma DTW), but consumers could only apply the scalar bar-1audio_offset— any tempo difference between the recording and the tab's authored tempo accumulated audibly over the song. This PR adds the librosa-free helpers needed to apply the full Songsterr-style piecewise mapping, plus a workingrefine_sync().New public API in
lib/gp_autosync.pybar_start_times(gp_path)— per-bar score times on the same axis as the sync points (GPIF bar-resolution map for .gp/.gpx, per-tick integration for GP3/4/5)build_warp_anchors(points, bar_starts)— strictly-monotonic(score, audio)anchor pairs, with a slope sanity gate (0.2x–5x) so DTW folds / clamped refine runs can't crush or absurdly stretch a spanwarp_time(t, anchors)— piecewise-linear map, edge-slope extrapolation for count-ins/tailswarp_song_times(song, warp)— retimes alib.song.Songin place (notes+sustains, chords, beats, sections, anchors, handshapes, phrase levels, tone changes, tempo overrides); identity-safe for objects shared between flat lists and phrase levelsgp_has_expandable_repeats(gp_path)— detects GP3/4/5 repeat/volta/direction markup (both jump sourcesfromDirectionand targetsdirection) whose playback expansion the as-written sync points can't map; callers fall back to offset-only syncrefine_sync(sync, audio_path, bars_per_point, gp_path=None)— the editor plugin's refine-sync endpoint has imported this since the snapshot, but it never existed in core (the Refine button 500'd). Densifies the DTW points to every Nth bar and re-times each with a local onset phase sweep: sweep radius clamped under half a beat (no one-beat locks on periodic material), short scoring grid + median residual snap, spurious-lock guard (<3 matched onsets keeps the coarse estimate).Fixes
beat.start, applying every mid-song tempo change a quarter note late and skewing the synthesised chroma against the bar timeline. Both are now origin-normalised._gpif_bar_starts/_gp345_measure_start_ticks(used by bothauto_syncand_extract_sync_points).Validation
Synthetic click-track runs (117.3 / 120.0 / 122.9 BPM recordings of a 120 BPM tab, ±180ms noise on coarse points, 3 seeds): refine_sync lands ~13ms mean / ~40ms max error, offset within ±30ms. Tests:
tests/test_gp_autosync_warp.py(18 fixture-free tests); existingtests/test_gp_audio_sync.py(23) still green.Consumer
Editor plugin PR (got-feedback/feedback-plugin-editor) consumes these helpers in convert-gp; it degrades gracefully to offset-only sync when running against a core without this PR.
Review note
Codex preflight was unavailable (usage limit until Jul 7); an 8-angle multi-agent Claude review ran instead — findings (fromDirection miss, tick origin, slope gate, double-warp identity, CHANGELOG) are fixed in this PR.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes