import { useEffect, useState, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; interface WaveformDisplayProps { trackId: string; currentTime: number; duration: number; onSeek?: (time: number) => void; className?: string; height?: number; barWidth?: number; barGap?: number; activeColor?: string; inactiveColor?: string; } interface WaveformDataResponse { data: number[]; length: number; sample_rate: number; samples_per_pixel: number; } function normalizeWaveformData(raw: number[], targetBars: number): number[] { if (raw.length === 0) return Array(targetBars).fill(0); const step = raw.length / targetBars; const normalized: number[] = []; let maxVal = 0; for (let i = 0; i < targetBars; i++) { const start = Math.floor(i * step); const end = Math.floor((i + 1) * step); let sum = 0; let count = 0; for (let j = start; j < end && j < raw.length; j++) { sum += Math.abs(raw[j]); count++; } const avg = count > 0 ? sum / count : 0; normalized.push(avg); if (avg > maxVal) maxVal = avg; } if (maxVal > 0) { return normalized.map((v) => v / maxVal); } return normalized; } export function WaveformDisplay({ trackId, currentTime, duration, onSeek, className, height = 48, barWidth = 2, barGap = 1, }: WaveformDisplayProps) { const containerRef = useRef(null); const [bars, setBars] = useState([]); const [barCount, setBarCount] = useState(100); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(false); useEffect(() => { const container = containerRef.current; if (!container) return; const observer = new ResizeObserver((entries) => { const width = entries[0]?.contentRect.width ?? 300; setBarCount(Math.floor(width / (barWidth + barGap))); }); observer.observe(container); return () => observer.disconnect(); }, [barWidth, barGap]); useEffect(() => { if (!trackId || barCount === 0) return; let cancelled = false; setIsLoading(true); setError(false); const apiBase = import.meta.env.VITE_API_URL || '/api/v1'; fetch(`${apiBase}/tracks/${trackId}/waveform`, { credentials: 'include', }) .then((res) => { if (!res.ok) throw new Error('Waveform not available'); return res.json(); }) .then((data: WaveformDataResponse) => { if (cancelled) return; setBars(normalizeWaveformData(data.data, barCount)); setIsLoading(false); }) .catch(() => { if (cancelled) return; setError(true); setIsLoading(false); const placeholder = Array.from({ length: barCount }, (_, i) => 0.3 + 0.4 * Math.abs(Math.sin(i * 0.15)) ); setBars(placeholder); }); return () => { cancelled = true; }; }, [trackId, barCount]); const progress = duration > 0 ? currentTime / duration : 0; const handleClick = useCallback( (e: React.MouseEvent) => { if (!onSeek || !containerRef.current || duration === 0) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const pct = x / rect.width; onSeek(pct * duration); }, [onSeek, duration] ); return (
{isLoading && bars.length === 0 ? (
{Array.from({ length: barCount || 50 }).map((_, i) => (
))}
) : ( bars.map((value, i) => { const barProgress = i / bars.length; const isActive = barProgress <= progress; return (
); }) )}
); }