Files
audio-ui/lib/audio/player.ts
Sebastian Krüger 19bf7dc68a fix: seek now auto-starts playback for intuitive interaction
Changed seek behavior to match user expectations:
- Clicking on waveform now immediately starts playing from that position
- Dragging timeline slider starts playback at the selected position
- No need to click Play button after seeking

Previous behavior:
- seek() only continued playback if music was already playing
- Otherwise it just set isPaused = true
- User had to seek AND THEN click play button

New behavior:
- seek() always starts playback automatically
- Click anywhere → music plays from that position
- Much more intuitive UX matching common audio editors

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

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

186 lines
4.0 KiB
TypeScript

/**
* Audio playback controller
*/
import { getAudioContext, resumeAudioContext } from './context';
export class AudioPlayer {
private audioContext: AudioContext;
private audioBuffer: AudioBuffer | null = null;
private sourceNode: AudioBufferSourceNode | null = null;
private gainNode: GainNode;
private startTime: number = 0;
private pauseTime: number = 0;
private isPlaying: boolean = false;
private isPaused: boolean = false;
constructor() {
this.audioContext = getAudioContext();
this.gainNode = this.audioContext.createGain();
this.gainNode.connect(this.audioContext.destination);
}
/**
* Load an audio buffer for playback
*/
loadBuffer(buffer: AudioBuffer): void {
this.stop();
this.audioBuffer = buffer;
this.pauseTime = 0;
}
/**
* Start playback from current position
*/
async play(startOffset: number = 0): Promise<void> {
if (!this.audioBuffer) {
throw new Error('No audio buffer loaded');
}
// Resume audio context if needed
await resumeAudioContext();
// Calculate start offset BEFORE stopping (since stop() resets pauseTime)
const offset = this.isPaused ? this.pauseTime : startOffset;
// Stop any existing playback
this.stop();
// Create new source node
this.sourceNode = this.audioContext.createBufferSource();
this.sourceNode.buffer = this.audioBuffer;
this.sourceNode.connect(this.gainNode);
// Set start time
this.startTime = this.audioContext.currentTime - offset;
// Start playback
this.sourceNode.start(0, offset);
this.isPlaying = true;
this.isPaused = false;
// Handle playback end
this.sourceNode.onended = () => {
if (this.isPlaying) {
this.isPlaying = false;
this.isPaused = false;
this.pauseTime = 0;
}
};
}
/**
* Pause playback
*/
pause(): void {
if (!this.isPlaying) return;
this.pauseTime = this.getCurrentTime();
this.stop();
this.isPaused = true;
}
/**
* Stop playback
*/
stop(): void {
if (this.sourceNode) {
try {
this.sourceNode.stop();
} catch (error) {
// Ignore errors if already stopped
}
this.sourceNode.disconnect();
this.sourceNode = null;
}
this.isPlaying = false;
this.isPaused = false;
this.pauseTime = 0;
this.startTime = 0;
}
/**
* Get current playback time in seconds
*/
getCurrentTime(): number {
if (!this.audioBuffer) return 0;
if (this.isPlaying) {
const currentTime = this.audioContext.currentTime - this.startTime;
return Math.min(currentTime, this.audioBuffer.duration);
}
return this.pauseTime;
}
/**
* Seek to a specific time and start playback
*/
async seek(time: number): Promise<void> {
if (!this.audioBuffer) return;
const clampedTime = Math.max(0, Math.min(time, this.audioBuffer.duration));
this.stop();
this.pauseTime = clampedTime;
// Always start playback after seeking
await this.play(clampedTime);
}
/**
* Set playback volume (0 to 1)
*/
setVolume(volume: number): void {
const clampedVolume = Math.max(0, Math.min(1, volume));
this.gainNode.gain.setValueAtTime(clampedVolume, this.audioContext.currentTime);
}
/**
* Get current volume
*/
getVolume(): number {
return this.gainNode.gain.value;
}
/**
* Get playback state
*/
getState(): {
isPlaying: boolean;
isPaused: boolean;
currentTime: number;
duration: number;
} {
return {
isPlaying: this.isPlaying,
isPaused: this.isPaused,
currentTime: this.getCurrentTime(),
duration: this.audioBuffer?.duration ?? 0,
};
}
/**
* Get audio buffer
*/
getBuffer(): AudioBuffer | null {
return this.audioBuffer;
}
/**
* Check if audio is loaded
*/
hasBuffer(): boolean {
return this.audioBuffer !== null;
}
/**
* Cleanup resources
*/
dispose(): void {
this.stop();
this.gainNode.disconnect();
this.audioBuffer = null;
}
}