Files
led2/.planning/phases/04-timeline-editor/04-03-SUMMARY.md

7.8 KiB

phase, plan, subsystem, tags, dependency_graph, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags dependency_graph tech-stack key-files key-decisions patterns-established requirements-completed duration completed
04-timeline-editor 03 frontend-canvas
canvas
timeline
beat-detection
snap-to-beat
daw-ui
librosa
requires provides affects
phase provides
04-timeline-editor/04-01 TimelineCanvas class with beat mark rendering, coordinate helpers, render loop
phase provides
04-timeline-editor/04-02 Block interactions with snapToBeat() wired into move/resize/drop operations
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
04-04 (playback engine reads beats from timeline.beatGrid for sync)
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
created modified
lightsync/frontend/timeline/beats.js
lightsync/frontend/timeline/timeline.js
lightsync/frontend/app.js
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
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
TL-07
TL-08
SYNC-01
SYNC-02
2min 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