webui: set a song's BPM and GAP from the sheet #34

Merged
branderson merged 3 commits from grid-edit into sheet-shortcuts 2026-10-14 23:17:46 -07:00
Collaborator

Summary generated by LLM agent

Stacked on #33, which is on #31 and #30. Merge from the top of the chain down: #33's chain first, then this one.

The beat grid came from the detected tempo (or the BPM override), with GAP always pinned to the first note. It didn't always line up with the music, so snapping in the editor didn't land on the song's beats. This lets BPM and GAP be set from the sheet.

The GRID bar

A strip above the toolbar:

  • Fields: BPM, with ×2 / ÷2 for a tempo detected at double or half speed, and GAP in ms, with −10 / +10.
  • ALIGN TO NOTE: moves GAP by the least amount that puts the selected note's start on a beat.
  • Live preview: typing or a button redraws the grid at once. "Not applied" shows until APPLY charts it.
  • CANCEL / RESET: CANCEL drops the trial. RESET goes back to songtrace's choice: your BPM override, or the detected tempo, with GAP at the first note.
  • Note editing pauses while a trial grid is pending, so notes never snap to a grid that's then cancelled.
  • Unsaved lines: APPLY and RESET are refused while the current line has unsaved changes, since applying reloads the line.

How it's kept

  • Stored with the edits: the grid lives in edits.json next to the line edits, so every rebuild keeps it, and lyric changes don't affect it. A grid set on its own keeps the file alive.
  • The grid stage uses it in place of its own choice. It logs "using the BPM and GAP set in the editor", and records its own choice in tempo.json for RESET.
  • Older songs: a song built before tempo.json existed has its chart's BPM recorded when a grid is first set, so RESET returns to the original tempo rather than the edited one. The browser test caught that.
  • Every chart rewrite uses the same rule, line saves included: the editor's grid if set, otherwise the pipeline's tempo with GAP at the first note.
  • GAP stays valid: a GAP after the first note would put notes on negative beats, which players reject, so to_ultrastar moves it back by whole beats. That keeps the grid lines where they were set.
  • Notes keep their timing: they're stored in seconds and re-snap to the new grid.

API: PUT /api/jobs/{id}/grid with {bpm, gap_ms} (BPM above 0 and at most 2000, GAP 0 or more), and DELETE to reset. Both rewrite the charts in place and return the review, which now also has grid_edited and auto_bpm.

Testing

  • New tests:
    • edits: validation; the grid surviving lyric re-keying; the file kept for a grid alone.
    • grid: explicit GAP; a late GAP moved back by whole beats with the grid kept in place; the stage using the editor's grid and recording its own.
    • Web: set, then a line save keeping it; reset to the recorded tempo and to an older song's original tempo; bad values refused.
  • In Chromium on the seeded 300 BPM song:
    • ÷2 previewed and paused editing; CANCEL restored everything.
    • Typed 280 / 1990, then APPLY: the server's charts updated.
    • ALIGN TO NOTE left the selected note within 0.43ms of a beat (GAP is whole milliseconds).
    • APPLY with an unsaved line was refused; the line saved afterwards kept the grid.
    • RESET returned to 300 BPM with GAP at the first note.
    • No script errors.
  • Full suite passes, except test_version (needs the package installed).
**Summary generated by LLM agent** Stacked on #33, which is on #31 and #30. Merge from the top of the chain down: #33's chain first, then this one. The beat grid came from the detected tempo (or the BPM override), with GAP always pinned to the first note. It didn't always line up with the music, so snapping in the editor didn't land on the song's beats. This lets BPM and GAP be set from the sheet. ## The GRID bar A strip above the toolbar: - **Fields:** BPM, with ×2 / ÷2 for a tempo detected at double or half speed, and GAP in ms, with −10 / +10. - **ALIGN TO NOTE:** moves GAP by the least amount that puts the selected note's start on a beat. - **Live preview:** typing or a button redraws the grid at once. "Not applied" shows until APPLY charts it. - **CANCEL / RESET:** CANCEL drops the trial. RESET goes back to songtrace's choice: your BPM override, or the detected tempo, with GAP at the first note. - **Note editing pauses while a trial grid is pending,** so notes never snap to a grid that's then cancelled. - **Unsaved lines:** APPLY and RESET are refused while the current line has unsaved changes, since applying reloads the line. ## How it's kept - **Stored with the edits:** the grid lives in `edits.json` next to the line edits, so every rebuild keeps it, and lyric changes don't affect it. A grid set on its own keeps the file alive. - **The grid stage uses it** in place of its own choice. It logs "using the BPM and GAP set in the editor", and records its own choice in `tempo.json` for RESET. - **Older songs:** a song built before `tempo.json` existed has its chart's BPM recorded when a grid is first set, so RESET returns to the original tempo rather than the edited one. The browser test caught that. - **Every chart rewrite uses the same rule,** line saves included: the editor's grid if set, otherwise the pipeline's tempo with GAP at the first note. - **GAP stays valid:** a GAP after the first note would put notes on negative beats, which players reject, so `to_ultrastar` moves it back by whole beats. That keeps the grid lines where they were set. - **Notes keep their timing:** they're stored in seconds and re-snap to the new grid. API: `PUT /api/jobs/{id}/grid` with `{bpm, gap_ms}` (BPM above 0 and at most 2000, GAP 0 or more), and `DELETE` to reset. Both rewrite the charts in place and return the review, which now also has `grid_edited` and `auto_bpm`. ## Testing - New tests: - `edits`: validation; the grid surviving lyric re-keying; the file kept for a grid alone. - `grid`: explicit GAP; a late GAP moved back by whole beats with the grid kept in place; the stage using the editor's grid and recording its own. - Web: set, then a line save keeping it; reset to the recorded tempo and to an older song's original tempo; bad values refused. - In Chromium on the seeded 300 BPM song: - ÷2 previewed and paused editing; CANCEL restored everything. - Typed 280 / 1990, then APPLY: the server's charts updated. - ALIGN TO NOTE left the selected note within 0.43ms of a beat (GAP is whole milliseconds). - APPLY with an unsaved line was refused; the line saved afterwards kept the grid. - RESET returned to 300 BPM with GAP at the first note. - No script errors. - Full suite passes, except `test_version` (needs the package installed).
dejima-agent added 1 commit 2026-10-13 16:08:40 -07:00
webui: set a song's BPM and GAP from the sheet
tests / test (pull_request_target) Successful in 3m52s
ae5ca3465b
The beat grid came from the detected tempo (or the BPM override) and a GAP
pinned to the first note, so it did not always line up with the music,
and snapping did not land on its beats. A GRID bar on the sheet now sets
both, redrawing the grid live as values are tried, with x2 and /2 for a
tempo detected at double or half speed and ALIGN TO NOTE to move GAP the
least that puts the selected note on a beat. Note editing pauses while a
trial grid is unapplied, so notes never snap to a grid that is then
cancelled.

The grid is stored with the song's edits, so every rebuild keeps it, and
grid charts with it in place of its own choice; it records that choice in
tempo.json for RESET to return to. A GAP after the first note moves back
by whole beats, as notes on negative beats are rejected by players. PUT
and DELETE /api/jobs/{id}/grid set and reset it, rewriting the charts in
place.
branderson added 2 commits 2026-10-14 18:54:24 -07:00
webui: move line breaks on the sheet
tests / test (pull_request_target) Successful in 4m18s
ce44fb3d79
Line breaks were always placed by songtrace, midway between lines and
recomputed around edited ones, so a break could not be put where a singer
breathes. The sheet now draws the breaks either side of a line as markers
that drag along the beat grid, kept between the lines they separate, and
double-click back to automatic. Break moves share the line's undo history
and SAVE LINE, which saves the notes only if they changed, so moving a
break never locks either line's notes.

Moved breaks are stored in edits.json keyed by the line each comes before,
follow their lines when the lyrics change, and win over the computed break
on every rebuild, clamped so a break never lands inside a line's notes.
The review lists each break with that line and whether it was moved, and
PUT /api/jobs/{id}/breaks takes {line: seconds | null}. merge now passes
untouched notes through as they are rather than rebuilding them, which
added float noise to their durations.
Reviewed-on: #35
Reviewed-by: branderson <brad@branderson.io>
branderson approved these changes 2026-10-14 23:17:37 -07:00
branderson merged commit 264354df09 into sheet-shortcuts 2026-10-14 23:17:46 -07:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: branderson/songtrace#34