From a157172e3d580c359b0737bb5a5b05a51276bf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 18 Nov 2025 15:12:42 +0100 Subject: [PATCH] fix: resolve playback level monitoring closure issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed playback level meters staying at 0% by resolving React closure issue in the monitoring loop - same pattern as the recording fix. The Problem: - monitorPlaybackLevels callback checked stale `isPlaying` state - Animation loop would run once and never continue - Dependency on isPlaying caused callback recreation on every state change The Solution: - Added isMonitoringLevelsRef to track state independent of React - Removed isPlaying dependency from callback (now has empty deps []) - Set ref to true when starting playback - Set ref to false when pausing, stopping, or ending playback - Animation loop checks ref instead of stale closure state Monitoring State Management: - Start: play() sets isMonitoringLevelsRef.current = true - Stop: pause(), stop(), onended, and cleanup set it to false - Loop: continues while ref is true, stops when false This ensures the requestAnimationFrame loop runs continuously during playback and calculates real-time RMS levels for all tracks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/hooks/useMultiTrackPlayer.ts | 34 +++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/hooks/useMultiTrackPlayer.ts b/lib/hooks/useMultiTrackPlayer.ts index 836b92a..1226691 100644 --- a/lib/hooks/useMultiTrackPlayer.ts +++ b/lib/hooks/useMultiTrackPlayer.ts @@ -32,6 +32,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { const pausedAtRef = useRef(0); const animationFrameRef = useRef(null); const levelMonitorFrameRef = useRef(null); + const isMonitoringLevelsRef = useRef(false); const tracksRef = useRef(tracks); // Always keep latest tracks // Keep tracksRef in sync with tracks prop @@ -52,7 +53,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { // Monitor playback levels for all tracks const monitorPlaybackLevels = useCallback(() => { - if (!isPlaying || analyserNodesRef.current.length === 0) return; + if (!isMonitoringLevelsRef.current || analyserNodesRef.current.length === 0) return; const levels: Record = {}; @@ -75,10 +76,8 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { setTrackLevels(levels); - if (isPlaying) { - levelMonitorFrameRef.current = requestAnimationFrame(monitorPlaybackLevels); - } - }, [isPlaying]); + levelMonitorFrameRef.current = requestAnimationFrame(monitorPlaybackLevels); + }, []); const updatePlaybackPosition = useCallback(() => { if (!audioContextRef.current) return; @@ -88,12 +87,18 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { if (newTime >= duration) { setIsPlaying(false); + isMonitoringLevelsRef.current = false; setCurrentTime(0); pausedAtRef.current = 0; + setTrackLevels({}); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } + if (levelMonitorFrameRef.current) { + cancelAnimationFrame(levelMonitorFrameRef.current); + levelMonitorFrameRef.current = null; + } return; } @@ -185,8 +190,10 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { source.onended = () => { if (pausedAtRef.current + (audioContext.currentTime - startTimeRef.current) >= duration) { setIsPlaying(false); + isMonitoringLevelsRef.current = false; setCurrentTime(0); pausedAtRef.current = 0; + setTrackLevels({}); } }; } @@ -194,6 +201,9 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { startTimeRef.current = audioContext.currentTime; setIsPlaying(true); updatePlaybackPosition(); + + // Start level monitoring + isMonitoringLevelsRef.current = true; monitorPlaybackLevels(); }, [tracks, duration, masterVolume, updatePlaybackPosition, monitorPlaybackLevels]); @@ -217,6 +227,9 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { setIsPlaying(false); + // Stop level monitoring + isMonitoringLevelsRef.current = false; + if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; @@ -421,8 +434,10 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { source.onended = () => { if (pausedAtRef.current + (audioContext.currentTime - startTimeRef.current) >= duration) { setIsPlaying(false); + isMonitoringLevelsRef.current = false; setCurrentTime(0); pausedAtRef.current = 0; + setTrackLevels({}); } }; } @@ -430,6 +445,9 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { startTimeRef.current = audioContext.currentTime; setIsPlaying(true); + // Start level monitoring + isMonitoringLevelsRef.current = true; + // Start animation frame for position updates const updatePosition = () => { if (!audioContextRef.current) return; @@ -439,12 +457,17 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { if (newTime >= duration) { setIsPlaying(false); + isMonitoringLevelsRef.current = false; setCurrentTime(0); pausedAtRef.current = 0; if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } + if (levelMonitorFrameRef.current) { + cancelAnimationFrame(levelMonitorFrameRef.current); + levelMonitorFrameRef.current = null; + } return; } @@ -513,6 +536,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { // Cleanup on unmount useEffect(() => { return () => { + isMonitoringLevelsRef.current = false; if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); }