import React, { useRef, useEffect, useState } from 'react'; import { getAnalyserData } from '../engine/audioEngine.js'; // Zoom levels: how many samples to display (from a 2048-sample buffer) // Fewer samples = zoomed in (more detail), more samples = zoomed out (more time visible) const ZOOM_LEVELS = [64, 128, 256, 512, 1024, 2048]; const DEFAULT_ZOOM = 2; // index → 256 samples export default function ScopeDisplay({ moduleId }) { const canvasRef = useRef(null); const rafRef = useRef(null); const [zoomIdx, setZoomIdx] = useState(DEFAULT_ZOOM); const zoomRef = useRef(ZOOM_LEVELS[DEFAULT_ZOOM]); // Keep ref in sync so the draw loop picks it up without re-creating the effect useEffect(() => { zoomRef.current = ZOOM_LEVELS[zoomIdx]; }, [zoomIdx]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const w = canvas.width = 160; const h = canvas.height = 60; let frameCount = 0; const draw = () => { frameCount++; rafRef.current = requestAnimationFrame(draw); // Throttle to ~30fps to reduce main thread pressure during playback if (frameCount % 2 !== 0) return; ctx.fillStyle = '#050510'; ctx.fillRect(0, 0, w, h); // Grid lines ctx.strokeStyle = '#151530'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2); ctx.moveTo(0, h / 4); ctx.lineTo(w, h / 4); ctx.moveTo(0, h * 3 / 4); ctx.lineTo(w, h * 3 / 4); for (let x = w / 4; x < w; x += w / 4) { ctx.moveTo(x, 0); ctx.lineTo(x, h); } ctx.stroke(); const data = getAnalyserData(moduleId); if (data && data.length > 0) { const samplesToShow = zoomRef.current; // Center the window in the buffer const offset = Math.max(0, Math.floor((data.length - samplesToShow) / 2)); const end = Math.min(data.length, offset + samplesToShow); ctx.strokeStyle = '#00e5ff'; ctx.lineWidth = 1.5; ctx.beginPath(); const count = end - offset; const step = w / count; for (let i = 0; i < count; i++) { const y = h / 2 + data[offset + i] * h / 2 * -1; if (i === 0) ctx.moveTo(0, y); else ctx.lineTo(i * step, y); } ctx.stroke(); } }; draw(); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [moduleId]); const canZoomIn = zoomIdx > 0; const canZoomOut = zoomIdx < ZOOM_LEVELS.length - 1; return (