Frontend: Scene Timeline Playback
Guidelines for implementing smooth, performant playback in the Scene Editor timeline. Covers RAF architecture, audio sync, and common pitfalls.
Critical Rule: Single RAF Loop Architecture
NEVER create multiple independent requestAnimationFrame loops for the same feature.
The Scene Editor uses a single RAF loop in PageScene_Timeline.tsx that:
- •Advances time based on
performance.now()delta - •Updates
timelineTimeRef.current(the source of truth) - •Updates playhead DOM directly (no React re-renders)
- •Updates time display DOM directly
Why Multiple RAF Loops Are Bad
// ❌ BAD: Multiple independent RAF loops
// Each AudioLane running its own loop
useEffect(() => {
const tick = () => {
syncAudio(timelineTimeRef.current);
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
}, []);
// Problems:
// 1. Loops fire at different times within the same frame
// 2. Unsynchronized reads from shared state (timelineTimeRef)
// 3. Accumulated CPU overhead (N tracks = N+1 loops)
// 4. Can cause jitter and timing conflicts
// ✅ GOOD: Single RAF loop with callbacks/events
// Main loop notifies subsystems
const tick = (timestamp: number) => {
const newTime = calculateNewTime(timestamp);
timelineTimeRef.current = newTime;
// Option A: Direct call to audio manager
audioManager.syncToTime(newTime);
// Option B: Event-based notification
eventBus.emit("timelineTime", newTime);
updatePlayheadDOM(newTime);
rafRef.current = requestAnimationFrame(tick);
};
Reference Implementation
The main playback loop is in:
- •
PageScene_Timeline.tsx:22-138-usePageScene_Timeline_Playbackhook
Key refs used:
- •
timelineTimeRef- Current playback time (source of truth during playback) - •
playheadRef- DOM element for direct manipulation - •
pxPerSecRef,scrollOffsetPxRef- Cached zoom/scroll values
Audio Synchronization Guidelines
Problem: HTML5 Audio Timing is Imprecise
- •
<audio>element uses browser's audio clock (independent fromperformance.now()) - •Setting
audio.currentTimeis a blocking seek operation that causes frame drops - •Clocks naturally drift apart over time
Anti-patterns to Avoid
// ❌ BAD: Checking drift every frame and seeking
const tick = () => {
const drift = Math.abs(audio.currentTime - expectedOffset);
if (drift > 0.05) {
// 50ms threshold
audio.currentTime = expectedOffset; // CAUSES JITTER!
}
rafRef.current = requestAnimationFrame(tick);
};
// ❌ BAD: Multiple RAF loops for audio sync
// Each track has its own loop
useEffect(() => {
const tick = () => {
clips.forEach((clip) => checkAndSync(clip));
rafRef.current = requestAnimationFrame(tick);
};
// ...
}, []);
Recommended Approaches
Option A: Fire-and-Forget (Simple)
// Start audio at correct offset, let it play naturally
// Accept minor drift as acceptable tradeoff
if (!clip.isPlaying && isInClip) {
clip.audio.currentTime = audioOffset;
clip.audio.play();
clip.isPlaying = true;
}
// NO drift correction during playback
Option B: Infrequent Drift Correction
// Check drift less frequently (every 30 frames ≈ 0.5s)
// Use larger threshold (200-300ms)
frameCountRef.current++;
if (frameCountRef.current % 30 === 0) {
const drift = Math.abs(audio.currentTime - expectedOffset);
if (drift > 0.2) {
// 200ms threshold
audio.currentTime = expectedOffset;
}
}
Option C: Web Audio API (Professional-grade) - IMPLEMENTED
The Scene Editor uses Web Audio API for audio playback:
// WebAudioManager singleton (webAudioManager.ts) // - AudioContext for precise timing // - AudioBuffer cache per file // - GainNode per clip for instant volume changes // - AudioBufferSourceNode per playback (fire-and-forget) // Main RAF loop calls syncPlayback with current clip data webAudioManager.syncPlayback(audioClipsDataRef.current, newTime, true); // Key files: // - webAudioManager.ts - Singleton service // - useAudioPlayback_PageScene_Timeline.ts - Centralized hook // - PageScene_Timeline.tsx - Integration point (line 117)
Integration Points
When adding new features that need time sync:
- •Read from
timelineTimeRef.current- Don't create your own time tracking - •Don't start new RAF loops - Hook into the existing playback system
- •For audio: Consider Web Audio API for professional timing needs
- •For animations: Use CSS animations or integrate with main RAF loop
Implementation Status
Playhead jitter during audio playback - RESOLVED (2025-12-18)
Root cause was multiple RAF loops (per-track) + HTML5 audio drift correction. Fixed by migrating to Web Audio API with single RAF loop architecture.
See docs/spark/frontend/my-vite-app/audio-drag-drop-spec.md Section 15 for historical context.
Related Skills
- •frontend-provider-context - Provider patterns used by timeline state
- •frontend-fiber-canvas - 3D canvas integration that also uses timeline time