From 233f5280e8e96b4beb83c02da65a431f6603df6e Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Thu, 9 Jul 2026 00:17:43 -0500 Subject: [PATCH] fix(gp2rs): write beat times at 6-decimal precision The editor/timeline derives per-bar BPM from beat spans (bpm = beats*60/span), which amplifies rounding: at millisecond (3-decimal) precision a constant-tempo GP import (e.g. 140 BPM) shows a spurious per-bar "tempo drift" of ~0.05-0.7 BPM because most bar lengths don't land on a ms boundary (worse for fast/odd meters). gp2rs computes these beat times exactly from the GP tempo map, so the only precision loss is the ebeat/startBeat format string. Writing them at 6 decimals (microseconds) makes the derived tempo match GP's authored value. Verified on GP5 imports (Highway to Hell 116, Equivalence 140, Living After Midnight 138): the derived per-bar BPM collapses from two drifting values to the single authored constant. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_013JgxKh99UAeQqmhzSc73tv Signed-off-by: ChrisBeWithYou --- lib/gp2rs.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/gp2rs.py b/lib/gp2rs.py index dcbbd59..525d542 100644 --- a/lib/gp2rs.py +++ b/lib/gp2rs.py @@ -1114,7 +1114,7 @@ def _build_xml( ET.SubElement(root, "arrangement").text = arrangement ET.SubElement(root, "offset").text = f"{audio_offset:.3f}" ET.SubElement(root, "songLength").text = f"{song_length:.3f}" - ET.SubElement(root, "startBeat").text = f"{beats[0].time:.3f}" if beats else "0.000" + ET.SubElement(root, "startBeat").text = f"{beats[0].time:.6f}" if beats else "0.000000" ET.SubElement(root, "averageTempo").text = str(tempo) ET.SubElement(root, "artistName").text = artist ET.SubElement(root, "albumName").text = album @@ -1139,10 +1139,17 @@ def _build_xml( tuning_el.set(f"string{i}", str(tuning[i] if i < len(tuning) else 0)) ET.SubElement(root, "capo").text = "0" - # Ebeats + # Ebeats — write beat times at MICROSECOND (6-decimal) precision, not + # millisecond (3-decimal). The editor/timeline DERIVES per-bar BPM from beat + # spans (bpm = beats·60/span), which amplifies any rounding: at 3 decimals a + # constant-tempo GP (e.g. 140) shows a spurious ±0.05–0.7 BPM per-bar drift + # (worse for fast/odd meters) because most bar lengths don't land on a ms + # boundary. gp2rs computes these times exactly from the GP tempo map, so the + # only loss is this format string — 6 decimals makes the derived tempo match + # GP's authored value. (Everything else stays at :.3f; only beats drive tempo.) ebeats = ET.SubElement(root, "ebeats", count=str(len(beats))) for b in beats: - ET.SubElement(ebeats, "ebeat", time=f"{b.time:.3f}", measure=str(b.measure)) + ET.SubElement(ebeats, "ebeat", time=f"{b.time:.6f}", measure=str(b.measure)) # Sections sections_el = ET.SubElement(root, "sections", count=str(len(sections)))