Skip to content

Slice 13 — Manual BPM/offset correction (#14) - #33

Merged
cbaucom merged 1 commit into
mainfrom
feat/14-manual-bpm-offset-correction
Jun 11, 2026
Merged

cbaucom merged 1 commit into
mainfrom
feat/14-manual-bpm-offset-correction

Conversation

@cbaucom

@cbaucom cbaucom commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Closes #14

Approach

Two pure functions added to the Beat Grid module, plus a schema field for persistence.

tapToBpm(tapTimestampsMs) — takes ≥8 tap timestamps (ms from track start), computes inter-tap intervals, takes their median (robust against one mis-tap), and derives BPM + first-beat offset. Requires ≥8 taps to match the AC minimum; throws on non-positive medianMs (duplicate/stuck timestamps).

manualBeatGridFromBpm(bpm, firstBeatOffsetSecs) — numeric-entry path that constructs a BeatGrid directly. Throws on bpm ≤ 0. Produces results identical to tapToBpm with perfectly-spaced taps at the same tempo (AC3 by construction).

resolveEffectiveBeatGrid(manual, cache) — manual ?? cache priority logic: manual override always wins; re-analysis (cache update) never evicts it; clearing is explicit (pass undefined).

SlideshowJson.manualBeatGrid — schema field for persistence across project reopens. Wiring into slidesToJson and the editor shell is intentional follow-on work (the UI detection slice).

Acceptance criteria

  • Given at least 8 taps, when applied, derived BPM/offset replace the detected grid and plan re-nudges — tapToBpm produces a BeatGrid accepted by plan(..., beatGrid) from the prior slice
  • Given a manual correction, when the project reopens or the track is re-analyzed, manual values still win until explicitly cleared — resolveEffectiveBeatGrid + SlideshowJson.manualBeatGrid schema field; persistence wiring is a follow-up
  • Given numeric entry of BPM and offset, when applied, behavior is identical to tapping — manualBeatGridFromBpm produces the same BPM/interval/offset as tapToBpm with perfect taps (tested directly)

Code review findings fixed

  • Guard raised from ≥2 to ≥8 taps (AC minimum)
  • medianMs ≤ 0 guard added (duplicate timestamps → throw instead of Infinity BPM)
  • bpm ≤ 0 guard added in manualBeatGridFromBpm (prevents Infinity beatIntervalSecs)

Follow-ups

  • Wire tapToBpm / manualBeatGridFromBpm into the editor UI (tap button + BPM/offset fields)
  • Persist manualBeatGrid through slidesToJson and read it back in the Project Store
  • Expose "clear manual override" action in the UI

- tapToBpm: median inter-tap interval → BPM + first-beat offset; robust
  against single mis-taps via median (not mean)
- manualBeatGridFromBpm: numeric-entry path producing an identical BeatGrid
  so AC3 (numeric == tapping) holds by construction
- resolveEffectiveBeatGrid: manual override wins over auto-detected cache;
  re-analysis never evicts the manual value — only explicit clear (undefined)
  does; satisfies AC2 persistence requirement
- SlideshowJson.manualBeatGrid added to schema for persistence across reopens
- All exports wired through beat-grid/index.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cbaucom
cbaucom merged commit 7fd20f5 into main Jun 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slice 13 — Manual BPM/offset correction

1 participant