Files
audio-ui/lib/hooks/useAudioPlayer.ts
Sebastian Krüger 10d2921147 fix: proper seeking behavior with optional auto-play
Complete rewrite of seeking logic to support both scrubbing and
click-to-play functionality with proper state management.

Changes:
1. Added autoPlay parameter to seek() methods across the stack
2. Waveform behavior:
   - Click and drag → scrubs WITHOUT auto-play during drag
   - Mouse up after drag → auto-plays from release position
   - This allows smooth scrubbing while dragging
3. Timeline slider behavior:
   - onChange → seeks WITHOUT auto-play (smooth dragging)
   - onMouseUp/onTouchEnd → auto-plays from final position
4. Transport button state now correctly syncs with playback state

Technical implementation:
- player.seek(time, autoPlay) - autoPlay defaults to false
- If autoPlay=true OR was already playing → continues playback
- If autoPlay=false AND wasn't playing → just seeks (isPaused=true)
- useAudioPlayer.seek() now reads actual player state after seeking

User experience:
✓ Click on waveform → music plays from that position
✓ Drag on waveform → scrubs smoothly, plays on release
✓ Drag timeline slider → scrubs smoothly, plays on release
✓ Transport buttons show correct state (Play/Pause)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 17:30:11 +01:00

217 lines
5.1 KiB
TypeScript

'use client';
import * as React from 'react';
import { AudioPlayer } from '@/lib/audio/player';
import { decodeAudioFile, formatDuration } from '@/lib/audio/decoder';
export interface UseAudioPlayerReturn {
// File management
loadFile: (file: File) => Promise<void>;
loadBuffer: (buffer: AudioBuffer, name?: string) => void;
clearFile: () => void;
// Playback controls
play: () => Promise<void>;
pause: () => void;
stop: () => void;
seek: (time: number, autoPlay?: boolean) => Promise<void>;
// Volume control
setVolume: (volume: number) => void;
// State
isPlaying: boolean;
isPaused: boolean;
currentTime: number;
duration: number;
volume: number;
audioBuffer: AudioBuffer | null;
fileName: string | null;
isLoading: boolean;
error: string | null;
// Formatted values
currentTimeFormatted: string;
durationFormatted: string;
}
export function useAudioPlayer(): UseAudioPlayerReturn {
const [player, setPlayer] = React.useState<AudioPlayer | null>(null);
const [isPlaying, setIsPlaying] = React.useState(false);
const [isPaused, setIsPaused] = React.useState(false);
const [currentTime, setCurrentTime] = React.useState(0);
const [duration, setDuration] = React.useState(0);
const [volume, setVolumeState] = React.useState(1);
const [audioBuffer, setAudioBuffer] = React.useState<AudioBuffer | null>(null);
const [fileName, setFileName] = React.useState<string | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
// Initialize player on client side only
React.useEffect(() => {
if (typeof window !== 'undefined') {
setPlayer(new AudioPlayer());
}
}, []);
// Update current time while playing
React.useEffect(() => {
if (!isPlaying || !player) return;
const interval = setInterval(() => {
const state = player.getState();
setCurrentTime(state.currentTime);
// Stop when reaching the end
if (state.currentTime >= state.duration) {
setIsPlaying(false);
setIsPaused(false);
setCurrentTime(0);
}
}, 50); // Update 20 times per second
return () => clearInterval(interval);
}, [isPlaying, player]);
const loadFile = React.useCallback(
async (file: File) => {
if (!player) return;
setIsLoading(true);
setError(null);
try {
const buffer = await decodeAudioFile(file);
player.loadBuffer(buffer);
setAudioBuffer(buffer);
setFileName(file.name);
setDuration(buffer.duration);
setCurrentTime(0);
setIsPlaying(false);
setIsPaused(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load audio file');
console.error('Error loading audio file:', err);
} finally {
setIsLoading(false);
}
},
[player]
);
const loadBuffer = React.useCallback(
(buffer: AudioBuffer, name?: string) => {
if (!player) return;
player.loadBuffer(buffer);
setAudioBuffer(buffer);
if (name) setFileName(name);
setDuration(buffer.duration);
setCurrentTime(0);
setIsPlaying(false);
setIsPaused(false);
},
[player]
);
const clearFile = React.useCallback(() => {
if (!player) return;
player.stop();
setAudioBuffer(null);
setFileName(null);
setDuration(0);
setCurrentTime(0);
setIsPlaying(false);
setIsPaused(false);
setError(null);
}, [player]);
const play = React.useCallback(async () => {
if (!player) return;
try {
await player.play();
setIsPlaying(true);
setIsPaused(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to play audio');
console.error('Error playing audio:', err);
}
}, [player]);
const pause = React.useCallback(() => {
if (!player) return;
player.pause();
setIsPlaying(false);
setIsPaused(true);
}, [player]);
const stop = React.useCallback(() => {
if (!player) return;
player.stop();
setIsPlaying(false);
setIsPaused(false);
setCurrentTime(0);
}, [player]);
const seek = React.useCallback(
async (time: number, autoPlay: boolean = false) => {
if (!player) return;
await player.seek(time, autoPlay);
setCurrentTime(time);
// Update state based on what actually happened
const state = player.getState();
setIsPlaying(state.isPlaying);
setIsPaused(state.isPaused);
},
[player]
);
const setVolume = React.useCallback(
(vol: number) => {
if (!player) return;
player.setVolume(vol);
setVolumeState(vol);
},
[player]
);
// Cleanup on unmount
React.useEffect(() => {
return () => {
if (player) {
player.dispose();
}
};
}, [player]);
return {
loadFile,
loadBuffer,
clearFile,
play,
pause,
stop,
seek,
setVolume,
isPlaying,
isPaused,
currentTime,
duration,
volume,
audioBuffer,
fileName,
isLoading,
error,
currentTimeFormatted: formatDuration(currentTime),
durationFormatted: formatDuration(duration),
};
}