beat-sync
Synchronize video cuts, transitions, and animations to audio beats using beat detection and tempo analysis.
SKILL.mdAutoAnimation/.claude/skills/beat-sync/SKILL.md
GitHubPurpose
Detect beats in audio, estimate BPM, and synchronize visual elements (text, shapes, media) to the beat grid. Two levels of beat sync exist: a service-level system for the timeline (beatSync.ts in services) and an engine-level system for the animation engine (beatSync.ts in engine). The beat detection service uses Web Audio API spectral flux analysis.
Steps
Analyze audio for beats. Call
detectBeatsFromUrl(audioUrl)frombeatDetection.ts. This:- Fetches and decodes the audio into an
AudioBuffer - Computes spectral flux using energy-based analysis (2048 FFT, 512 hop)
- Detects onsets via adaptive thresholding (local average * 1.5, min 100ms gap)
- Estimates BPM using inter-onset interval histogram (60-200 BPM range)
- Builds a phase-aligned beat grid by testing 16 phase offsets against detected onsets
- Computes energy segments (4-second windows) with average and peak energy
- Returns
BeatAnalysiswithbpm,beats[](seconds),onsets[],segments[],duration
- Fetches and decodes the audio into an
Fine-tune detection. Use
redetectWithSensitivity(audioUrl, sensitivity, manualBpm?)to adjust:sensitivity(0-1): Higher = more beats detected (lower onset threshold). Default 0.5.manualBpm: Override auto-detected BPM while keeping phase alignment- The store also supports
beatGridOffsetto shift all beat positions
Configure per-object sync. In
useBeatSyncStore, set upObjectBeatSyncConfigfor each canvas element:objectRef:{ objectType, objectId }-- which element to synceffect: 'scale-pulse', 'opacity-flash', or 'bounce'subdivision: 1 (every beat), 2 (every other), or 4 (every 4th)offset: time offset in secondsintensity: effect strength (0-1)
Apply beat sync keyframes. Call
useBeatSyncStore.getState().applyObjectBeatSync(configs, fps, clipRanges). This:- Converts beat timestamps to frame numbers
- Reads base property values from native stores (text size, shape dimensions, etc.)
- Generates keyframes using
generateObjectBeatKeyframes()frombeatSync.ts(service) - Writes keyframes to
useKeyframeStoretagged with'beat-sync'for easy removal - Stores the FPS used for later recalculation on FPS changes
Engine-level beat sync. The animation engine (
src/engine/beatSync.ts) provides lower-level utilities:detectBeats(audioBuffer, { sensitivity, minInterval })-- simple energy-based beat detectionestimateBPM(beats)-- median-interval BPM estimationgenerateBeatsFromBPM(bpm, durationSeconds, offset)-- generate beats from known BPMgenerateBeatKeyframes(beats, objectRef, config, fps)-- create keyframes with effect types: 'scale-pulse', 'opacity-flash', 'bounce'snapToBeat(frame, bpm, fps)-- snap a frame to nearest beat positionbeatsToFrames(beats, bpm, fps)-- convert musical beats to frame countgetBeatFramesInRange(start, end, bpm, fps, subdivision, offset)-- get all beat frames in a range
Visualize beats. Set
useBeatSyncStore.showBeatMarkers = trueto display beat markers on the timeline. The store also pre-computeswaveformData(downsampled to 400 points) for visualization.Bridge to animation engine.
beatAnalysisToEngineBeats(analysis)insoundDesigner.tsconvertsBeatAnalysisto the engine'sBeat[]format, enriching with strength from energy segments and marking downbeats using BPM-derived measure positions.
Key Files
src/services/beatDetection.ts-- detectBeats(), detectBeatsFromUrl(), redetectWithSensitivity(), computeFrequencyBands(), BeatAnalysis, BeatSegmentsrc/services/beatSync.ts-- snapToNearestBeat(), generatePulseKeyframes(), generateObjectBeatKeyframes(), BeatSyncOptions (service-level)src/engine/beatSync.ts-- detectBeats(), estimateBPM(), generateBeatKeyframes(), snapToBeat(), beatsToFrames(), getBeatFramesInRange(), Beat, BeatSyncConfig (engine-level)src/stores/useBeatSyncStore.ts-- analyzeAudio(), applyObjectBeatSync(), clearObjectBeatSync(), sensitivity/BPM/offset controls, FPS recalculationsrc/services/soundDesigner.ts-- beatAnalysisToEngineBeats() bridge functionsrc/components/panels/BeatSyncPanel.tsx-- Beat sync configuration UI
Common Issues
- BPM detection inaccurate: The histogram-based approach works best for rhythmic music. For ambient/non-rhythmic audio, manually set BPM via
setManualBpm(). - Beat grid misaligned: Use
setBeatGridOffset(seconds)to shift all beats. Small adjustments of 0.05-0.1s often fix phase issues. - Too many/few beats: Adjust sensitivity (0-1). Default 0.5 is balanced. Higher values detect more subtle beats.
- FPS changes break sync: The store subscribes to
usePlaybackStoreand auto-recalculates beat frame positions when FPS changes viarecalculateBeatFramesForFps(). - Keyframe conflicts: Beat-sync keyframes are tagged with
'beat-sync'. CallclearObjectBeatSync(objectRefs)to remove them without affecting manually-placed keyframes. - Two detection systems:
beatDetection.ts(service) is more sophisticated (spectral flux, phase alignment, segments).engine/beatSync.tsis simpler (energy-only) for real-time use. Use the service for analysis, the engine for runtime.
Examples
// Full pipeline via store
const store = useBeatSyncStore.getState()
await store.analyzeAudio(musicAudioUrl)
// store.analysis.bpm, store.analysis.beats, etc.
// Apply to a text overlay
await store.applyObjectBeatSync(
[{
objectRef: { objectType: 'text', objectId: 'text-1' },
effect: 'scale-pulse',
subdivision: 1,
offset: 0,
intensity: 0.7,
}],
30, // fps
{ 'text:text-1': { startFrame: 0, endFrame: 300 } }
)
// Engine-level: snap a frame to beat
import { snapToBeat } from '@/engine/beatSync'
const snapped = snapToBeat(currentFrame, 120, 30) // 120 BPM, 30 fps