/** * AudioPlayer Component * Inline audio player for TTS content with play/pause, progress, * speed control, download, and duration display. * * Follows PDA-friendly design: no aggressive colors, calm interface. */ import { useState, useRef, useEffect, useCallback } from "react"; import type { ReactElement } from "react"; /** Playback speed options */ const SPEED_OPTIONS = [1, 1.5, 2, 0.5] as const; export interface AudioPlayerProps { /** URL of the audio to play (blob URL or HTTP URL). If null, nothing renders. */ src: string | null; /** Whether to auto-play when src changes */ autoPlay?: boolean; /** Callback when play state changes */ onPlayStateChange?: (isPlaying: boolean) => void; /** Optional className for the container */ className?: string; } /** * Format seconds into M:SS display */ function formatTime(seconds: number): string { if (!isFinite(seconds) || seconds < 0) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${String(mins)}:${String(secs).padStart(2, "0")}`; } /** * AudioPlayer displays an inline audio player with controls for * play/pause, progress tracking, speed adjustment, and download. */ export function AudioPlayer({ src, autoPlay = false, onPlayStateChange, className = "", }: AudioPlayerProps): ReactElement | null { const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [speedIndex, setSpeedIndex] = useState(0); const audioRef = useRef(null); /** * Set up audio element when src changes */ useEffect((): (() => void) | undefined => { if (!src) return undefined; const audio = new Audio(src); audioRef.current = audio; const onLoadedMetadata = (): void => { if (isFinite(audio.duration)) { setDuration(audio.duration); } }; const onTimeUpdate = (): void => { setCurrentTime(audio.currentTime); }; const onEnded = (): void => { setIsPlaying(false); setCurrentTime(0); onPlayStateChange?.(false); }; audio.addEventListener("loadedmetadata", onLoadedMetadata); audio.addEventListener("timeupdate", onTimeUpdate); audio.addEventListener("ended", onEnded); if (autoPlay) { void audio.play().then(() => { setIsPlaying(true); onPlayStateChange?.(true); }); } return (): void => { audio.pause(); audio.removeEventListener("loadedmetadata", onLoadedMetadata); audio.removeEventListener("timeupdate", onTimeUpdate); audio.removeEventListener("ended", onEnded); audioRef.current = null; }; }, [src, autoPlay, onPlayStateChange]); /** * Toggle play/pause */ const togglePlayPause = useCallback(async (): Promise => { const audio = audioRef.current; if (!audio) return; if (isPlaying) { audio.pause(); setIsPlaying(false); onPlayStateChange?.(false); } else { await audio.play(); setIsPlaying(true); onPlayStateChange?.(true); } }, [isPlaying, onPlayStateChange]); /** * Cycle through speed options */ const cycleSpeed = useCallback((): void => { const nextIndex = (speedIndex + 1) % SPEED_OPTIONS.length; setSpeedIndex(nextIndex); const audio = audioRef.current; if (audio) { audio.playbackRate = SPEED_OPTIONS[nextIndex] ?? 1; } }, [speedIndex]); /** * Handle progress bar click for seeking */ const handleProgressClick = useCallback( (event: React.MouseEvent): void => { const audio = audioRef.current; if (!audio || !duration) return; const rect = event.currentTarget.getBoundingClientRect(); const clickX = event.clientX - rect.left; const fraction = clickX / rect.width; audio.currentTime = fraction * duration; setCurrentTime(audio.currentTime); }, [duration] ); /** * Handle download */ const handleDownload = useCallback((): void => { if (!src) return; const link = document.createElement("a"); link.href = src; link.download = "speech-audio.mp3"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }, [src]); // Don't render if no source if (!src) return null; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const currentSpeed = SPEED_OPTIONS[speedIndex] ?? 1; return (
{/* Play/Pause Button */} {/* Time Display */} {formatTime(currentTime)} {duration > 0 && / {formatTime(duration)}} {/* Progress Bar */}
{/* Speed Control */} {/* Download Button */}
); } export default AudioPlayer;