feat: implement global volume and mute controls

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-11-18 07:20:29 +01:00
parent 5817598c48
commit 2f8718626c
2 changed files with 31 additions and 7 deletions

View File

@@ -19,6 +19,7 @@ export function AudioEditor() {
const [importDialogOpen, setImportDialogOpen] = React.useState(false); const [importDialogOpen, setImportDialogOpen] = React.useState(false);
const [selectedTrackId, setSelectedTrackId] = React.useState<string | null>(null); const [selectedTrackId, setSelectedTrackId] = React.useState<string | null>(null);
const [zoom, setZoom] = React.useState(1); const [zoom, setZoom] = React.useState(1);
const [masterVolume, setMasterVolume] = React.useState(0.8);
const { addToast } = useToast(); const { addToast } = useToast();
@@ -41,7 +42,7 @@ export function AudioEditor() {
stop, stop,
seek, seek,
togglePlayPause, togglePlayPause,
} = useMultiTrackPlayer(tracks); } = useMultiTrackPlayer(tracks, masterVolume);
// Effect chain (for selected track) // Effect chain (for selected track)
const { const {
@@ -290,12 +291,12 @@ export function AudioEditor() {
isPaused={!isPlaying} isPaused={!isPlaying}
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
volume={1} volume={masterVolume}
onPlay={play} onPlay={play}
onPause={pause} onPause={pause}
onStop={stop} onStop={stop}
onSeek={seek} onSeek={seek}
onVolumeChange={() => {}} onVolumeChange={setMasterVolume}
currentTimeFormatted={formatDuration(currentTime)} currentTimeFormatted={formatDuration(currentTime)}
durationFormatted={formatDuration(duration)} durationFormatted={formatDuration(duration)}
/> />

View File

@@ -9,7 +9,7 @@ export interface MultiTrackPlayerState {
duration: number; duration: number;
} }
export function useMultiTrackPlayer(tracks: Track[]) { export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
@@ -18,6 +18,7 @@ export function useMultiTrackPlayer(tracks: Track[]) {
const sourceNodesRef = useRef<AudioBufferSourceNode[]>([]); const sourceNodesRef = useRef<AudioBufferSourceNode[]>([]);
const gainNodesRef = useRef<GainNode[]>([]); const gainNodesRef = useRef<GainNode[]>([]);
const panNodesRef = useRef<StereoPannerNode[]>([]); const panNodesRef = useRef<StereoPannerNode[]>([]);
const masterGainNodeRef = useRef<GainNode | null>(null);
const startTimeRef = useRef<number>(0); const startTimeRef = useRef<number>(0);
const pausedAtRef = useRef<number>(0); const pausedAtRef = useRef<number>(0);
const animationFrameRef = useRef<number | null>(null); const animationFrameRef = useRef<number | null>(null);
@@ -71,11 +72,20 @@ export function useMultiTrackPlayer(tracks: Track[]) {
}); });
gainNodesRef.current.forEach(node => node.disconnect()); gainNodesRef.current.forEach(node => node.disconnect());
panNodesRef.current.forEach(node => node.disconnect()); panNodesRef.current.forEach(node => node.disconnect());
if (masterGainNodeRef.current) {
masterGainNodeRef.current.disconnect();
}
sourceNodesRef.current = []; sourceNodesRef.current = [];
gainNodesRef.current = []; gainNodesRef.current = [];
panNodesRef.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 // Create audio graph for each track
for (const track of tracks) { for (const track of tracks) {
if (!track.audioBuffer) continue; if (!track.audioBuffer) continue;
@@ -93,10 +103,10 @@ export function useMultiTrackPlayer(tracks: Track[]) {
// Set pan // Set pan
panNode.pan.setValueAtTime(track.pan, audioContext.currentTime); panNode.pan.setValueAtTime(track.pan, audioContext.currentTime);
// Connect: source -> gain -> pan -> destination // Connect: source -> gain -> pan -> master gain -> destination
source.connect(gainNode); source.connect(gainNode);
gainNode.connect(panNode); gainNode.connect(panNode);
panNode.connect(audioContext.destination); panNode.connect(masterGain);
// Start playback from current position // Start playback from current position
source.start(0, pausedAtRef.current); source.start(0, pausedAtRef.current);
@@ -119,7 +129,7 @@ export function useMultiTrackPlayer(tracks: Track[]) {
startTimeRef.current = audioContext.currentTime; startTimeRef.current = audioContext.currentTime;
setIsPlaying(true); setIsPlaying(true);
updatePlaybackPosition(); updatePlaybackPosition();
}, [tracks, duration, updatePlaybackPosition]); }, [tracks, duration, masterVolume, updatePlaybackPosition]);
const pause = useCallback(() => { const pause = useCallback(() => {
if (!audioContextRef.current || !isPlaying) return; if (!audioContextRef.current || !isPlaying) return;
@@ -200,6 +210,16 @@ export function useMultiTrackPlayer(tracks: Track[]) {
}); });
}, [tracks, isPlaying]); }, [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 // Cleanup on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -216,6 +236,9 @@ export function useMultiTrackPlayer(tracks: Track[]) {
}); });
gainNodesRef.current.forEach(node => node.disconnect()); gainNodesRef.current.forEach(node => node.disconnect());
panNodesRef.current.forEach(node => node.disconnect()); panNodesRef.current.forEach(node => node.disconnect());
if (masterGainNodeRef.current) {
masterGainNodeRef.current.disconnect();
}
}; };
}, []); }, []);