From 2f8718626ce608c4cdd3db8a69486cb86fa1ac01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 18 Nov 2025 07:20:29 +0100 Subject: [PATCH] feat: implement global volume and mute controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add masterVolume state to AudioEditor (default 0.8) - Pass masterVolume to useMultiTrackPlayer hook - Create master gain node in audio graph - Connect all tracks through master gain before destination - Update master gain in real-time when volume changes - Wire up PlaybackControls volume slider and mute button - Clean up master gain node on unmount Fixes global volume and mute controls not working in transport controls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/editor/AudioEditor.tsx | 7 ++++--- lib/hooks/useMultiTrackPlayer.ts | 31 +++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index a1fea09..c14c3dd 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -19,6 +19,7 @@ export function AudioEditor() { const [importDialogOpen, setImportDialogOpen] = React.useState(false); const [selectedTrackId, setSelectedTrackId] = React.useState(null); const [zoom, setZoom] = React.useState(1); + const [masterVolume, setMasterVolume] = React.useState(0.8); const { addToast } = useToast(); @@ -41,7 +42,7 @@ export function AudioEditor() { stop, seek, togglePlayPause, - } = useMultiTrackPlayer(tracks); + } = useMultiTrackPlayer(tracks, masterVolume); // Effect chain (for selected track) const { @@ -290,12 +291,12 @@ export function AudioEditor() { isPaused={!isPlaying} currentTime={currentTime} duration={duration} - volume={1} + volume={masterVolume} onPlay={play} onPause={pause} onStop={stop} onSeek={seek} - onVolumeChange={() => {}} + onVolumeChange={setMasterVolume} currentTimeFormatted={formatDuration(currentTime)} durationFormatted={formatDuration(duration)} /> diff --git a/lib/hooks/useMultiTrackPlayer.ts b/lib/hooks/useMultiTrackPlayer.ts index 90a707c..332c101 100644 --- a/lib/hooks/useMultiTrackPlayer.ts +++ b/lib/hooks/useMultiTrackPlayer.ts @@ -9,7 +9,7 @@ export interface MultiTrackPlayerState { duration: number; } -export function useMultiTrackPlayer(tracks: Track[]) { +export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) { const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); @@ -18,6 +18,7 @@ export function useMultiTrackPlayer(tracks: Track[]) { const sourceNodesRef = useRef([]); const gainNodesRef = useRef([]); const panNodesRef = useRef([]); + const masterGainNodeRef = useRef(null); const startTimeRef = useRef(0); const pausedAtRef = useRef(0); const animationFrameRef = useRef(null); @@ -71,11 +72,20 @@ export function useMultiTrackPlayer(tracks: Track[]) { }); gainNodesRef.current.forEach(node => node.disconnect()); panNodesRef.current.forEach(node => node.disconnect()); + if (masterGainNodeRef.current) { + masterGainNodeRef.current.disconnect(); + } sourceNodesRef.current = []; gainNodesRef.current = []; panNodesRef.current = []; + // Create master gain node + const masterGain = audioContext.createGain(); + masterGain.gain.setValueAtTime(masterVolume, audioContext.currentTime); + masterGain.connect(audioContext.destination); + masterGainNodeRef.current = masterGain; + // Create audio graph for each track for (const track of tracks) { if (!track.audioBuffer) continue; @@ -93,10 +103,10 @@ export function useMultiTrackPlayer(tracks: Track[]) { // Set pan panNode.pan.setValueAtTime(track.pan, audioContext.currentTime); - // Connect: source -> gain -> pan -> destination + // Connect: source -> gain -> pan -> master gain -> destination source.connect(gainNode); gainNode.connect(panNode); - panNode.connect(audioContext.destination); + panNode.connect(masterGain); // Start playback from current position source.start(0, pausedAtRef.current); @@ -119,7 +129,7 @@ export function useMultiTrackPlayer(tracks: Track[]) { startTimeRef.current = audioContext.currentTime; setIsPlaying(true); updatePlaybackPosition(); - }, [tracks, duration, updatePlaybackPosition]); + }, [tracks, duration, masterVolume, updatePlaybackPosition]); const pause = useCallback(() => { if (!audioContextRef.current || !isPlaying) return; @@ -200,6 +210,16 @@ export function useMultiTrackPlayer(tracks: Track[]) { }); }, [tracks, isPlaying]); + // Update master volume when it changes + useEffect(() => { + if (!isPlaying || !audioContextRef.current || !masterGainNodeRef.current) return; + + masterGainNodeRef.current.gain.setValueAtTime( + masterVolume, + audioContextRef.current.currentTime + ); + }, [masterVolume, isPlaying]); + // Cleanup on unmount useEffect(() => { return () => { @@ -216,6 +236,9 @@ export function useMultiTrackPlayer(tracks: Track[]) { }); gainNodesRef.current.forEach(node => node.disconnect()); panNodesRef.current.forEach(node => node.disconnect()); + if (masterGainNodeRef.current) { + masterGainNodeRef.current.disconnect(); + } }; }, []);