mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-15 13:17:25 +00:00
feat(core): consume song_timeline tempos + time_signatures + per-chart tempos (feedpak 1.2.0) (#529)
feedpak 1.2.0 added song-level `tempos` + `time_signatures` to song_timeline.json
and a per-chart `tempos` override on arrangements (§6.10). Core stored the raw
song_timeline dict but never consumed the maps, and didn't read per-chart tempos.
- song.py: shared `sanitize_tempos([{time,bpm}])` (finite non-bool time, finite
bpm>0, sorted); `Arrangement.tempos` field wired through arrangement_to_wire
(omitted when None/empty per §6.10) / arrangement_from_wire.
- sloppak.py: `_sanitize_time_signatures([{time,ts:[num,den]}])`;
LoadedSloppak.tempos / .time_signatures, loaded from song_timeline.json
INDEPENDENTLY of beats/sections (all are optional in 1.2.0).
- server.py: stream `tempos` + `time_signatures` highway-WS messages; the active
arrangement's per-chart `tempos` overrides the song-level map for that chart.
Renderer/UI surfacing is a thin follow-up; this lands the data plumbing.
Codex-reviewed: clean (no findings). +9 tests (sanitizers, per-chart wire
round-trip + omit-when-absent, song-level load/sanitize/absent + maps-without-
beats). 90 song/sloppak tests pass. (Pre-existing unrelated failure:
test_diagnostics_redact, fails on clean main too.)
Closes #526. 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
e64378da78
commit
587fbbea81
@@ -148,6 +148,50 @@ def test_song_timeline_absent_when_path_escapes_sloppak(tmp_path: Path):
|
||||
assert loaded.song_timeline is None
|
||||
|
||||
|
||||
# ── tempos + time_signatures (feedpak 1.2.0) ─────────────────────────────────
|
||||
|
||||
def test_song_timeline_tempos_and_time_signatures_loaded(tmp_path: Path):
|
||||
payload = {
|
||||
"version": 1, "beats": [], "sections": [],
|
||||
"tempos": [{"time": 0.0, "bpm": 120}, {"time": 4.0, "bpm": 90}],
|
||||
"time_signatures": [{"time": 0.0, "ts": [4, 4]}, {"time": 8.0, "ts": [6, 8]}],
|
||||
}
|
||||
pak = _write_dir_sloppak(tmp_path, {"song_timeline": "song_timeline.json"}, payload)
|
||||
loaded = _load(pak, tmp_path)
|
||||
assert loaded.tempos == [{"time": 0.0, "bpm": 120.0}, {"time": 4.0, "bpm": 90.0}]
|
||||
assert loaded.time_signatures == [{"time": 0.0, "ts": [4, 4]},
|
||||
{"time": 8.0, "ts": [6, 8]}]
|
||||
|
||||
|
||||
def test_song_timeline_maps_absent_when_not_provided(tmp_path: Path):
|
||||
payload = {"version": 1, "beats": [], "sections": []}
|
||||
pak = _write_dir_sloppak(tmp_path, {"song_timeline": "song_timeline.json"}, payload)
|
||||
loaded = _load(pak, tmp_path)
|
||||
assert loaded.tempos is None and loaded.time_signatures is None
|
||||
|
||||
|
||||
def test_song_timeline_maps_sanitized(tmp_path: Path):
|
||||
payload = {
|
||||
"version": 1, "beats": [], "sections": [],
|
||||
"tempos": [{"time": 1.0, "bpm": 0}, {"time": 0.0, "bpm": 100}], # bpm 0 dropped + sorted
|
||||
"time_signatures": [{"time": 0.0, "ts": [4, 4, 4]}, # 3-long dropped
|
||||
{"time": 2.0, "ts": [3, 4]}],
|
||||
}
|
||||
pak = _write_dir_sloppak(tmp_path, {"song_timeline": "song_timeline.json"}, payload)
|
||||
loaded = _load(pak, tmp_path)
|
||||
assert loaded.tempos == [{"time": 0.0, "bpm": 100.0}]
|
||||
assert loaded.time_signatures == [{"time": 2.0, "ts": [3, 4]}]
|
||||
|
||||
|
||||
def test_song_timeline_maps_load_even_without_beats_or_sections(tmp_path: Path):
|
||||
# tempos/time_signatures are independent of beats/sections — a payload that
|
||||
# omits beats (invalid for the override path) must still surface the maps.
|
||||
payload = {"version": 1, "tempos": [{"time": 0.0, "bpm": 100}]}
|
||||
pak = _write_dir_sloppak(tmp_path, {"song_timeline": "song_timeline.json"}, payload)
|
||||
loaded = _load(pak, tmp_path)
|
||||
assert loaded.tempos == [{"time": 0.0, "bpm": 100.0}]
|
||||
|
||||
|
||||
def test_song_timeline_absent_when_path_is_absolute(tmp_path: Path):
|
||||
pak = _write_dir_sloppak(tmp_path, {"song_timeline": "/etc/passwd"}, None)
|
||||
loaded = _load(pak, tmp_path)
|
||||
|
||||
Reference in New Issue
Block a user