docs(04-03): complete BeatGrid plan summary and update state
This commit is contained in:
145
.planning/phases/04-timeline-editor/04-03-SUMMARY.md
Normal file
145
.planning/phases/04-timeline-editor/04-03-SUMMARY.md
Normal 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*
|
||||
Reference in New Issue
Block a user