import { useEffect, useRef, useState } from "react"; // Custom audio player with bar-waveform viz and click-to-seek. // Pattern from victor/ace-step-jam; we render N bars pulled from decoded audio buffer peaks. const NUM_BARS = 80; export default function Waveform({ src, duration }) { const audioRef = useRef(null); const [peaks, setPeaks] = useState(null); const [playing, setPlaying] = useState(false); const [progress, setProgress] = useState(0); // Decode audio to extract bar peaks useEffect(() => { if (!src) return; let cancelled = false; (async () => { try { const res = await fetch(src); const buf = await res.arrayBuffer(); const ctx = new (window.AudioContext || window.webkitAudioContext)(); const audio = await ctx.decodeAudioData(buf.slice(0)); const channel = audio.getChannelData(0); const samplesPerBar = Math.floor(channel.length / NUM_BARS); const out = new Float32Array(NUM_BARS); let globalMax = 0; for (let b = 0; b < NUM_BARS; b++) { let max = 0; const start = b * samplesPerBar; const end = Math.min(start + samplesPerBar, channel.length); for (let i = start; i < end; i++) { const v = Math.abs(channel[i]); if (Number.isFinite(v) && v > max) max = v; } out[b] = max; if (max > globalMax) globalMax = max; } // Normalize — if silent or NaN, fall back to flat low bars const peak = Number.isFinite(globalMax) && globalMax > 1e-5 ? globalMax : 1; for (let i = 0; i < NUM_BARS; i++) { const n = out[i] / peak; out[i] = Number.isFinite(n) ? Math.max(0.05, Math.min(1, n)) : 0.05; } if (!cancelled) setPeaks(out); ctx.close?.(); } catch (e) { console.warn("waveform decode failed:", e); // Still show fallback bars so UI isn't broken if (!cancelled) setPeaks(new Float32Array(NUM_BARS).fill(0.1)); } })(); return () => { cancelled = true; }; }, [src]); useEffect(() => { const a = audioRef.current; if (!a) return; const onTime = () => setProgress(a.duration ? a.currentTime / a.duration : 0); const onEnd = () => setPlaying(false); a.addEventListener("timeupdate", onTime); a.addEventListener("ended", onEnd); return () => { a.removeEventListener("timeupdate", onTime); a.removeEventListener("ended", onEnd); }; }, [src]); const toggle = () => { const a = audioRef.current; if (!a) return; if (a.paused) { a.play(); setPlaying(true); } else { a.pause(); setPlaying(false); } }; const seek = (e) => { const a = audioRef.current; if (!a || !a.duration) return; const rect = e.currentTarget.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; a.currentTime = Math.max(0, Math.min(1, x)) * a.duration; setProgress(x); }; return (