import { useRef, useEffect } from 'react' // Map a frequency (Hz) to a bin index in the 256-bin log spectrum (40–4000 Hz) function freqToBin(freq) { return Math.round(255 * Math.log(freq / 40) / Math.log(4000 / 40)) } function bandMax(spectrum, lo, hi) { if (!spectrum) return 0 const a = freqToBin(lo) const b = Math.min(freqToBin(hi), spectrum.length - 1) let max = 0 for (let i = a; i <= b; i++) if (spectrum[i] > max) max = spectrum[i] return max } const BANDS = [ { label: 'Kick', lo: 40, hi: 120, color: '#ef4444' }, { label: 'Snare', lo: 120, hi: 300, color: '#f59e0b' }, { label: 'Mid', lo: 300, hi: 1000, color: '#22c55e' }, { label: 'Presence', lo: 1000, hi: 4000, color: '#60a5fa' }, ] const TIMELINE_MS = 4000 // onset timeline window const RMS_HISTORY = 180 // ~3s at 60fps export default function DrumView({ waveform, bpm }) { const rmsHistRef = useRef([]) const beatCanvasRef = useRef(null) const rmsCanvasRef = useRef(null) const spectrum = waveform?.spectrum ?? null const bandLevels = BANDS.map(b => bandMax(spectrum, b.lo, b.hi)) // Accumulate RMS history useEffect(() => { if (waveform == null) return const h = rmsHistRef.current h.push(Math.min(waveform.rms * 10, 1)) if (h.length > RMS_HISTORY) h.shift() }, [waveform]) // Draw onset / beat timeline useEffect(() => { const canvas = beatCanvasRef.current if (!canvas) return const ctx = canvas.getContext('2d') const W = canvas.width, H = canvas.height ctx.fillStyle = '#0a0a0a' ctx.fillRect(0, 0, W, H) const onsets = waveform?.onsets ?? [] const now = performance.now() // Beat grid aligned to the most recent onset if (bpm) { const beatMs = 60000 / bpm const numBeats = Math.ceil(TIMELINE_MS / beatMs) + 1 const latest = onsets[onsets.length - 1] const phase = latest != null ? (now - latest) % beatMs : 0 for (let b = 0; b <= numBeats; b++) { const ageMs = b * beatMs - phase if (ageMs < 0 || ageMs > TIMELINE_MS) continue const x = W * (1 - ageMs / TIMELINE_MS) ctx.strokeStyle = 'rgba(255,255,255,0.07)' ctx.lineWidth = 1 ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke() } } // Onset dots + vertical tails const recent = onsets.filter(t => now - t <= TIMELINE_MS) for (const t of recent) { const age = now - t const x = W * (1 - age / TIMELINE_MS) const alpha = Math.pow(1 - age / TIMELINE_MS, 0.4) ctx.strokeStyle = `rgba(168,85,247,${(alpha * 0.35).toFixed(2)})` ctx.lineWidth = 1 ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke() ctx.fillStyle = `rgba(168,85,247,${alpha.toFixed(2)})` ctx.beginPath(); ctx.arc(x, H / 2, 5, 0, Math.PI * 2); ctx.fill() } // "Now" edge ctx.strokeStyle = 'rgba(255,255,255,0.18)' ctx.lineWidth = 2 ctx.beginPath(); ctx.moveTo(W - 1, 0); ctx.lineTo(W - 1, H); ctx.stroke() }, [waveform, bpm]) // Draw RMS envelope useEffect(() => { const canvas = rmsCanvasRef.current if (!canvas) return const ctx = canvas.getContext('2d') const W = canvas.width, H = canvas.height const h = rmsHistRef.current ctx.fillStyle = '#0a0a0a' ctx.fillRect(0, 0, W, H) if (h.length < 2) return const barW = W / RMS_HISTORY for (let i = 0; i < h.length; i++) { const x = W * (i / RMS_HISTORY) const barH = h[i] * H const v = Math.round(h[i] * 160 + 60) ctx.fillStyle = `rgb(${v},30,${v})` ctx.fillRect(x, H - barH, Math.max(barW - 0.5, 1), barH) } }, [waveform]) const noData = !waveform return (
{/* Band meters */}

Frequency Bands

{BANDS.map((b, i) => (
{noData ? (
) : (
)}
{b.label}
))}
{/* Onset timeline */}

Onset Timeline{bpm ? ` · ${bpm} BPM` : ''} ← 4 seconds

{noData && (

Start listening to see hits

)}
{/* Volume envelope */}

Volume Envelope ← ~3 seconds

) }