import { create } from "zustand"; interface ActiveTrack { src: string; title: string; } interface AudioPlayerState { track: ActiveTrack | null; audioEl: HTMLAudioElement | null; isPlaying: boolean; volume: number; playbackRate: number; duration: number; position: number; playTrack: (track: ActiveTrack) => Promise; togglePlay: () => Promise; seekTo: (seconds: number) => void; seekBy: (secondsDelta: number) => void; setVolume: (volume: number) => void; setPlaybackRate: (rate: number) => void; cyclePlaybackRate: () => void; stop: () => void; } let globalAudioEl: HTMLAudioElement | null = null; let isBound = false; const PLAYBACK_RATE_KEY = "bm_audio_playback_rate"; function getInitialPlaybackRate(): number { if (typeof window === "undefined") { return 1; } const raw = Number(window.localStorage.getItem(PLAYBACK_RATE_KEY) || "1"); if (raw === 1 || raw === 1.5 || raw === 2) { return raw; } return 1; } function ensureAudio(): HTMLAudioElement { if (globalAudioEl) { return globalAudioEl; } globalAudioEl = new Audio(); globalAudioEl.preload = "metadata"; return globalAudioEl; } function getPlayableDuration(audio: HTMLAudioElement): number { if (Number.isFinite(audio.duration) && audio.duration > 0) { return audio.duration; } try { if (audio.seekable && audio.seekable.length > 0) { const end = audio.seekable.end(audio.seekable.length - 1); if (Number.isFinite(end) && end > 0) { return end; } } } catch { return 0; } return 0; } export const useAudioPlayerStore = create((set, get) => ({ track: null, audioEl: ensureAudio(), isPlaying: false, volume: 1, playbackRate: getInitialPlaybackRate(), duration: 0, position: 0, playTrack: async (track) => { const audio = ensureAudio(); if (!isBound) { audio.addEventListener("timeupdate", () => { set({ position: audio.currentTime || 0, duration: getPlayableDuration(audio) }); }); audio.addEventListener("loadedmetadata", () => { set({ duration: getPlayableDuration(audio) }); }); audio.addEventListener("durationchange", () => { set({ duration: getPlayableDuration(audio) }); }); audio.addEventListener("play", () => set({ isPlaying: true })); audio.addEventListener("pause", () => set({ isPlaying: false })); audio.addEventListener("ended", () => set({ isPlaying: false, position: 0 })); isBound = true; } const current = get().track; if (current?.src !== track.src) { audio.pause(); audio.currentTime = 0; audio.src = track.src; set({ track, position: 0, duration: 0 }); } else { set({ track }); } audio.volume = get().volume; audio.playbackRate = get().playbackRate; try { await audio.play(); set({ isPlaying: true, audioEl: audio }); } catch { set({ isPlaying: false, audioEl: audio }); } }, togglePlay: async () => { const audio = ensureAudio(); if (!audio.src) { return; } if (!audio) { return; } if (audio.paused) { await audio.play(); set({ isPlaying: true }); return; } audio.pause(); set({ isPlaying: false }); }, seekTo: (seconds) => { const audio = ensureAudio(); if (!audio.src) { return; } const target = Math.max(0, Math.min(Number.isFinite(audio.duration) ? audio.duration : seconds, seconds)); audio.currentTime = target; set({ position: target }); }, seekBy: (secondsDelta) => { const audio = ensureAudio(); if (!audio.src) { return; } const duration = Number.isFinite(audio.duration) ? audio.duration : Number.MAX_SAFE_INTEGER; const target = Math.max(0, Math.min(duration, (audio.currentTime || 0) + secondsDelta)); audio.currentTime = target; set({ position: target }); }, setVolume: (volume) => { const audio = ensureAudio(); const normalized = Math.max(0, Math.min(1, volume)); audio.volume = normalized; set({ volume: normalized }); }, setPlaybackRate: (rate) => { const normalized = rate === 2 ? 2 : rate === 1.5 ? 1.5 : 1; const audio = ensureAudio(); audio.playbackRate = normalized; if (typeof window !== "undefined") { window.localStorage.setItem(PLAYBACK_RATE_KEY, String(normalized)); } set({ playbackRate: normalized }); }, cyclePlaybackRate: () => { const current = get().playbackRate; const next = current === 1 ? 1.5 : current === 1.5 ? 2 : 1; get().setPlaybackRate(next); }, stop: () => { const audio = ensureAudio(); if (audio) { audio.pause(); audio.currentTime = 0; } set({ audioEl: audio, track: null, isPlaying: false, position: 0, duration: 0 }); }, }));