import WaveSurfer from "wavesurfer.js"; import { usePlayerStore } from "../stores/playerStore"; class AudioService { private static instance: AudioService; private wavesurfer: WaveSurfer | null = null; private currentUrl: string | null = null; private isReady = false; private lastTimeUpdate = 0; private constructor() {} public static getInstance(): AudioService { if (!this.instance) { this.instance = new AudioService(); } return this.instance; } // For use in tests only public static resetInstance(): void { this.instance = undefined as any; } public async initialize(container: HTMLElement, url: string): Promise { if (!container) throw new Error('Container element is required'); if (!url) throw new Error('Valid audio URL is required'); // Reuse the existing instance when the URL hasn't changed if (this.currentUrl === url && this.wavesurfer) return; // Tear down the previous instance before creating a new one if (this.wavesurfer) this.destroyWaveSurfer(); const ws = WaveSurfer.create({ container, waveColor: "rgba(255,255,255,0.09)", progressColor: "#c8861a", cursorColor: "#e8a22a", barWidth: 2, barRadius: 2, height: 104, normalize: true, autoplay: false, }); this.wavesurfer = ws; this.currentUrl = url; this.setupEventHandlers(ws); await new Promise((resolve, reject) => { const onReady = async () => { const duration = ws.getDuration(); if (duration > 0) { usePlayerStore.getState().setDuration(duration); this.isReady = true; resolve(); } else { reject(new Error('Audio loaded but duration is 0')); } }; ws.on('ready', () => { onReady().catch(reject); }); ws.on('error', (err) => reject(err instanceof Error ? err : new Error(String(err)))); ws.load(url); }); } private setupEventHandlers(ws: WaveSurfer): void { ws.on('play', () => usePlayerStore.getState().batchUpdate({ isPlaying: true })); ws.on('pause', () => usePlayerStore.getState().batchUpdate({ isPlaying: false })); ws.on('finish', () => usePlayerStore.getState().batchUpdate({ isPlaying: false })); ws.on('audioprocess', (time) => { const now = Date.now(); if (now - this.lastTimeUpdate >= 250) { usePlayerStore.getState().batchUpdate({ currentTime: time }); this.lastTimeUpdate = now; } }); } public async play(songId: string | null = null, bandId: string | null = null): Promise { if (!this.wavesurfer || !this.isReady) { console.warn('[AudioService] play() called before ready'); return; } await this.wavesurfer.play(); if (songId && bandId) { usePlayerStore.getState().setCurrentPlayingSong(songId, bandId); } } public pause(): void { this.wavesurfer?.pause(); } public seekTo(time: number): void { if (this.wavesurfer && this.isReady && isFinite(time)) { this.wavesurfer.setTime(time); } } public getCurrentTime(): number { return this.wavesurfer?.getCurrentTime() ?? 0; } public getDuration(): number { return this.wavesurfer?.getDuration() ?? 0; } public isPlaying(): boolean { return this.wavesurfer?.isPlaying() ?? false; } public isWaveformReady(): boolean { return this.isReady && !!this.wavesurfer; } // Aliases kept for callers until Phase 3 cleans them up public isReadyForPlayback(): boolean { return this.isWaveformReady(); } public canAttemptPlayback(): boolean { return this.isWaveformReady(); } public cleanup(): void { this.destroyWaveSurfer(); const store = usePlayerStore.getState(); store.setCurrentPlayingSong(null, null); store.batchUpdate({ isPlaying: false, currentTime: 0 }); } private destroyWaveSurfer(): void { if (!this.wavesurfer) return; try { if (this.wavesurfer.isPlaying()) this.wavesurfer.pause(); this.wavesurfer.unAll(); this.wavesurfer.destroy(); } catch (err) { console.error('[AudioService] Error destroying WaveSurfer:', err); } this.wavesurfer = null; this.currentUrl = null; this.isReady = false; } } export const audioService = AudioService.getInstance(); export { AudioService };