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:
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user