feat: add playback speed control (0.25x - 2x)
Implemented variable playback speed functionality: - Added playbackRate state and ref to useMultiTrackPlayer (0.25x - 2x range) - Applied playback rate to AudioBufferSourceNode.playbackRate - Updated timing calculations to account for playback rate - Real-time playback speed adjustment for active playback - Dropdown UI control in PlaybackControls with preset speeds - Integrated changePlaybackRate function through AudioEditor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -300,6 +300,8 @@ export function AudioEditor() {
|
|||||||
toggleLoop,
|
toggleLoop,
|
||||||
setLoopPoints,
|
setLoopPoints,
|
||||||
setLoopFromSelection,
|
setLoopFromSelection,
|
||||||
|
playbackRate,
|
||||||
|
changePlaybackRate,
|
||||||
} = useMultiTrackPlayer(tracks, masterVolume, handleAutomationRecording);
|
} = useMultiTrackPlayer(tracks, masterVolume, handleAutomationRecording);
|
||||||
|
|
||||||
// Reset latch triggered state when playback stops
|
// Reset latch triggered state when playback stops
|
||||||
@@ -1983,6 +1985,8 @@ export function AudioEditor() {
|
|||||||
loopEnd={loopEnd}
|
loopEnd={loopEnd}
|
||||||
onToggleLoop={toggleLoop}
|
onToggleLoop={toggleLoop}
|
||||||
onSetLoopPoints={setLoopPoints}
|
onSetLoopPoints={setLoopPoints}
|
||||||
|
playbackRate={playbackRate}
|
||||||
|
onPlaybackRateChange={changePlaybackRate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface PlaybackControlsProps {
|
|||||||
loopEnd?: number;
|
loopEnd?: number;
|
||||||
onToggleLoop?: () => void;
|
onToggleLoop?: () => void;
|
||||||
onSetLoopPoints?: (start: number, end: number) => void;
|
onSetLoopPoints?: (start: number, end: number) => void;
|
||||||
|
playbackRate?: number;
|
||||||
|
onPlaybackRateChange?: (rate: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlaybackControls({
|
export function PlaybackControls({
|
||||||
@@ -69,6 +71,8 @@ export function PlaybackControls({
|
|||||||
loopEnd = 0,
|
loopEnd = 0,
|
||||||
onToggleLoop,
|
onToggleLoop,
|
||||||
onSetLoopPoints,
|
onSetLoopPoints,
|
||||||
|
playbackRate = 1.0,
|
||||||
|
onPlaybackRateChange,
|
||||||
}: PlaybackControlsProps) {
|
}: PlaybackControlsProps) {
|
||||||
const handlePlayPause = () => {
|
const handlePlayPause = () => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@@ -276,6 +280,26 @@ export function PlaybackControls({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Playback Speed Control */}
|
||||||
|
{onPlaybackRateChange && (
|
||||||
|
<div className="flex items-center gap-1 border-l border-border pl-2 ml-1">
|
||||||
|
<select
|
||||||
|
value={playbackRate}
|
||||||
|
onChange={(e) => onPlaybackRateChange(parseFloat(e.target.value))}
|
||||||
|
className="h-7 px-2 py-0 bg-background border border-border rounded text-xs cursor-pointer hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
title="Playback Speed"
|
||||||
|
>
|
||||||
|
<option value={0.25}>0.25x</option>
|
||||||
|
<option value={0.5}>0.5x</option>
|
||||||
|
<option value={0.75}>0.75x</option>
|
||||||
|
<option value={1.0}>1x</option>
|
||||||
|
<option value={1.25}>1.25x</option>
|
||||||
|
<option value={1.5}>1.5x</option>
|
||||||
|
<option value={2.0}>2x</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface MultiTrackPlayerState {
|
|||||||
loopEnabled: boolean;
|
loopEnabled: boolean;
|
||||||
loopStart: number;
|
loopStart: number;
|
||||||
loopEnd: number;
|
loopEnd: number;
|
||||||
|
playbackRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrackLevel {
|
export interface TrackLevel {
|
||||||
@@ -38,6 +39,7 @@ export function useMultiTrackPlayer(
|
|||||||
const [loopEnabled, setLoopEnabled] = useState(false);
|
const [loopEnabled, setLoopEnabled] = useState(false);
|
||||||
const [loopStart, setLoopStart] = useState(0);
|
const [loopStart, setLoopStart] = useState(0);
|
||||||
const [loopEnd, setLoopEnd] = useState(0);
|
const [loopEnd, setLoopEnd] = useState(0);
|
||||||
|
const [playbackRate, setPlaybackRate] = useState(1.0);
|
||||||
|
|
||||||
const audioContextRef = useRef<AudioContext | null>(null);
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
const sourceNodesRef = useRef<AudioBufferSourceNode[]>([]);
|
const sourceNodesRef = useRef<AudioBufferSourceNode[]>([]);
|
||||||
@@ -60,6 +62,7 @@ export function useMultiTrackPlayer(
|
|||||||
const loopEnabledRef = useRef<boolean>(false);
|
const loopEnabledRef = useRef<boolean>(false);
|
||||||
const loopStartRef = useRef<number>(0);
|
const loopStartRef = useRef<number>(0);
|
||||||
const loopEndRef = useRef<number>(0);
|
const loopEndRef = useRef<number>(0);
|
||||||
|
const playbackRateRef = useRef<number>(1.0);
|
||||||
|
|
||||||
// Keep tracksRef in sync with tracks prop
|
// Keep tracksRef in sync with tracks prop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,6 +76,11 @@ export function useMultiTrackPlayer(
|
|||||||
loopEndRef.current = loopEnd;
|
loopEndRef.current = loopEnd;
|
||||||
}, [loopEnabled, loopStart, loopEnd]);
|
}, [loopEnabled, loopStart, loopEnd]);
|
||||||
|
|
||||||
|
// Keep playbackRate ref in sync with state
|
||||||
|
useEffect(() => {
|
||||||
|
playbackRateRef.current = playbackRate;
|
||||||
|
}, [playbackRate]);
|
||||||
|
|
||||||
// Keep onRecordAutomationRef in sync
|
// Keep onRecordAutomationRef in sync
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onRecordAutomationRef.current = onRecordAutomation;
|
onRecordAutomationRef.current = onRecordAutomation;
|
||||||
@@ -313,7 +321,7 @@ export function useMultiTrackPlayer(
|
|||||||
const updatePlaybackPosition = useCallback(() => {
|
const updatePlaybackPosition = useCallback(() => {
|
||||||
if (!audioContextRef.current) return;
|
if (!audioContextRef.current) return;
|
||||||
|
|
||||||
const elapsed = audioContextRef.current.currentTime - startTimeRef.current;
|
const elapsed = (audioContextRef.current.currentTime - startTimeRef.current) * playbackRateRef.current;
|
||||||
const newTime = pausedAtRef.current + elapsed;
|
const newTime = pausedAtRef.current + elapsed;
|
||||||
|
|
||||||
// Check if loop is enabled and we've reached the loop end
|
// Check if loop is enabled and we've reached the loop end
|
||||||
@@ -346,6 +354,7 @@ export function useMultiTrackPlayer(
|
|||||||
|
|
||||||
const source = audioContext.createBufferSource();
|
const source = audioContext.createBufferSource();
|
||||||
source.buffer = track.audioBuffer;
|
source.buffer = track.audioBuffer;
|
||||||
|
source.playbackRate.value = playbackRateRef.current;
|
||||||
|
|
||||||
// Connect to existing nodes (gain, pan, effects are still connected)
|
// Connect to existing nodes (gain, pan, effects are still connected)
|
||||||
const trackIndex = tracks.indexOf(track);
|
const trackIndex = tracks.indexOf(track);
|
||||||
@@ -465,6 +474,9 @@ export function useMultiTrackPlayer(
|
|||||||
outputNode.connect(masterGain);
|
outputNode.connect(masterGain);
|
||||||
console.log('[MultiTrackPlayer] Effect output connected with', effectNodes.length, 'effect nodes');
|
console.log('[MultiTrackPlayer] Effect output connected with', effectNodes.length, 'effect nodes');
|
||||||
|
|
||||||
|
// Set playback rate
|
||||||
|
source.playbackRate.value = playbackRateRef.current;
|
||||||
|
|
||||||
// Start playback from current position
|
// Start playback from current position
|
||||||
source.start(0, pausedAtRef.current);
|
source.start(0, pausedAtRef.current);
|
||||||
|
|
||||||
@@ -902,6 +914,17 @@ export function useMultiTrackPlayer(
|
|||||||
}
|
}
|
||||||
}, [setLoopPoints]);
|
}, [setLoopPoints]);
|
||||||
|
|
||||||
|
const changePlaybackRate = useCallback((rate: number) => {
|
||||||
|
// Clamp rate between 0.25x and 2x
|
||||||
|
const clampedRate = Math.max(0.25, Math.min(2.0, rate));
|
||||||
|
setPlaybackRate(clampedRate);
|
||||||
|
|
||||||
|
// Update playback rate on all active source nodes
|
||||||
|
sourceNodesRef.current.forEach(source => {
|
||||||
|
source.playbackRate.value = clampedRate;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
currentTime,
|
currentTime,
|
||||||
@@ -923,5 +946,7 @@ export function useMultiTrackPlayer(
|
|||||||
toggleLoop,
|
toggleLoop,
|
||||||
setLoopPoints,
|
setLoopPoints,
|
||||||
setLoopFromSelection,
|
setLoopFromSelection,
|
||||||
|
playbackRate,
|
||||||
|
changePlaybackRate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user