docs(04-03): complete BeatGrid plan summary and update state

This commit is contained in:
Claude
2026-04-06 23:49:44 +00:00
parent 287fe16a56
commit d1965dcefc
4 changed files with 160 additions and 12 deletions

View File

@@ -0,0 +1,145 @@
---
phase: 04-timeline-editor
plan: 03
subsystem: frontend-canvas
tags: [canvas, timeline, beat-detection, snap-to-beat, daw-ui, librosa]
dependency_graph:
requires:
- phase: 04-timeline-editor/04-01
provides: TimelineCanvas class with beat mark rendering, coordinate helpers, render loop
- phase: 04-timeline-editor/04-02
provides: Block interactions with snapToBeat() wired into move/resize/drop operations
provides:
- BeatGrid class with load(), getCalibratedBeats(), getCalibratedOnsets(), snap(), hasBeats getter
- Beat mark rendering via calibrated beat positions (cyan lines full height)
- Onset tick rendering (short 8px ticks at top of track area, when onset data available)
- Loading indicator DETECTING BEATS... shown during async beat analysis
- Error state display when beat detection fails
- BPM label update in header after beat analysis completes
affects:
- 04-04 (playback engine reads beats from timeline.beatGrid for sync)
tech-stack:
added: []
patterns:
- "BeatGrid module pattern: raw times stored unmodified, calibration applied only at display/snap time — calibrationOffset is never baked into stored data"
- "Getter/setter proxy pattern for backward compatibility: TimelineCanvas.snapEnabled and .calibrationOffset proxy to beatGrid without breaking existing app.js wiring"
- "loading/error state management in data loader class (BeatGrid.loading, BeatGrid.error) — UI reads state each render frame, no imperative show/hide"
key-files:
created:
- lightsync/frontend/timeline/beats.js
modified:
- lightsync/frontend/timeline/timeline.js
- lightsync/frontend/app.js
key-decisions:
- "BeatGrid stores raw beat_times unmodified — calibrationOffset is subtracted only in getCalibratedBeats()/getCalibratedOnsets() — ensures offset slider works without corrupting base data"
- "API field normalization in BeatGrid.load(): supports beats/beat_times and tempo/tempo_bpm variants — the existing API returns 'beats' and 'tempo' (not the plan's 'beat_times'/'tempo_bpm')"
- "onset_times gracefully absent: onsets.length=0 skips onset tick rendering silently — current API does not return onset data, future API extension will just work"
patterns-established:
- "BeatGrid.snap() threshold: 0.1s (100ms) — consistent with existing SNAP_THRESHOLD constant in timeline.js"
- "Canvas text alignment reset: ctx.textAlign restored to 'left' after centered text renders — prevents layout corruption for subsequent draw calls"
requirements-completed: [TL-07, TL-08, SYNC-01, SYNC-02]
duration: 2min
completed: 2026-04-06
---
# Phase 04 Plan 03: BeatGrid Module + Beat Rendering Summary
**BeatGrid ES module encapsulating librosa beat data with calibration offset management, loading/error states, and snap-to-beat logic — extracted from TimelineCanvas for clean separation of concerns.**
## Performance
- **Duration:** ~2 min
- **Started:** 2026-04-06T23:47:10Z
- **Completed:** 2026-04-06T23:49:30Z
- **Tasks:** 1
- **Files modified:** 3 (1 created, 2 modified)
## Accomplishments
- `BeatGrid` class in `beats.js`: async `load()` fetching from `/api/audio/beats`, `getCalibratedBeats()`, `getCalibratedOnsets()`, `snap()` with 100ms threshold, `loading`/`error` state, `hasBeats` getter
- Beat mark rendering refactored to use `beatGrid.getCalibratedBeats()` — calibration offset applied at render time, raw data never mutated
- Loading state indicator "DETECTING BEATS..." displayed in canvas during async analysis
- Error state displayed when beat detection fails
- BPM label updates in header toolbar after beat analysis resolves via `.then()`
- Getters/setters on TimelineCanvas proxy `snapEnabled` and `calibrationOffset` to beatGrid — existing app.js toolbar wiring works without changes
- `snapToBeat()` delegates to `beatGrid.snap()` — block placement, move, and resize all continue to snap correctly
## Task Commits
1. **Task 1: BeatGrid module + enhanced timeline integration** - `287fe16` (feat)
## Files Created/Modified
- `lightsync/frontend/timeline/beats.js` — BeatGrid class (load, getCalibratedBeats, getCalibratedOnsets, snap, hasBeats, loading/error state)
- `lightsync/frontend/timeline/timeline.js` — Imports BeatGrid, creates this.beatGrid in constructor, render() uses beatGrid for beat marks + loading indicator, snapToBeat() delegates to beatGrid.snap(), getters/setters for calibrationOffset/snapEnabled
- `lightsync/frontend/app.js` — loadBeats() chained with .then() to update BPM label from beatGrid.tempoBpm
## Decisions Made
| Decision | Rationale |
|----------|-----------|
| API field normalization in load() | Existing API returns `beats`/`tempo` not `beat_times`/`tempo_bpm` as plan expected — BeatGrid.load() handles both variants with fallback |
| onset_times silently absent | Current API has no onset_times field — getCalibratedOnsets() returns [] and rendering skips onset ticks — future API extension will work automatically |
| textAlign reset after centered text | Drawing loading/error text centered then resetting to 'left' prevents layout corruption for subsequent draw calls in same render frame |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] API field name mismatch for tempo and beat times**
- **Found during:** Task 1 implementation (reading audio.py + beats.py)
- **Issue:** Plan's BeatGrid code expected `data.beat_times` and `data.tempo_bpm` but the actual `/api/audio/beats` endpoint returns `{"beats": [...], "tempo": float}` (confirmed in `lightsync/audio/beats.py`)
- **Fix:** BeatGrid.load() uses `data.beats || data.beat_times` for beat times and `data.tempo_bpm || data.tempo` for BPM — handles both field name variants
- **Files modified:** `lightsync/frontend/timeline/beats.js`
- **Committed in:** 287fe16
**2. [Rule 2 - Missing Critical] onset_times not in API response**
- **Found during:** Task 1 implementation
- **Issue:** Plan called for rendering onset marks from `onset_times` but the API has no such field — would silently break if array access attempted on undefined
- **Fix:** `this.onsetTimes = data.onset_times || []` — fallback to empty array; onset tick rendering guarded by `if (onsets.length > 0)` — no errors, graceful no-op
- **Files modified:** `lightsync/frontend/timeline/beats.js`, `lightsync/frontend/timeline/timeline.js`
- **Committed in:** 287fe16
---
**Total deviations:** 2 auto-fixed (1 Rule 1 - API field names, 1 Rule 2 - missing null guard)
**Impact on plan:** Both fixes necessary for correctness. No scope creep. Core functionality (beat marks, snap, loading state, BPM display) works as designed.
## Issues Encountered
None beyond the field name deviations documented above.
## Known Stubs
None — BeatGrid loads live data from `/api/audio/beats`. Onset ticks are gracefully absent (no onset data in current API) but will render automatically when API is extended.
## Self-Check: PASSED
- `lightsync/frontend/timeline/beats.js` exists: YES (287fe16)
- `export class BeatGrid` in beats.js: YES
- `getCalibratedBeats`, `getCalibratedOnsets`, `snap()`, `SNAP_THRESHOLD`: YES
- `import { BeatGrid }` in timeline.js: YES
- `this.beatGrid = new BeatGrid()` in constructor: YES
- `DETECTING BEATS...` in render(): YES
- `getCalibratedBeats` and `getCalibratedOnsets` used in render(): YES
- `get snapEnabled`, `set snapEnabled`, `get calibrationOffset`, `set calibrationOffset`: YES
- `loadBeats()` delegates to `beatGrid.load()`: YES
- Docker build: SUCCESS (no errors)
## Next Phase Readiness
- BeatGrid is available as `timeline.beatGrid` — playback engine (Plan 04-04) can read `timeline.beatGrid.getCalibratedBeats()` for live sync
- Beat snap works for all block operations (place, move, resize) via beatGrid.snap()
- Calibration offset and snap toggle continue working through existing app.js toolbar wiring
- No blockers
---
*Phase: 04-timeline-editor*
*Completed: 2026-04-06*