fix: resolve playback level monitoring closure issue

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-18 15:12:42 +01:00
parent 6fbb677bd2
commit a157172e3d

View File

@@ -32,6 +32,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
const pausedAtRef = useRef<number>(0);
const animationFrameRef = useRef<number | null>(null);
const levelMonitorFrameRef = useRef<number | null>(null);
const isMonitoringLevelsRef = useRef<boolean>(false);
const tracksRef = useRef<Track[]>(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<string, number> = {};
@@ -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);
}