'use client'; import * as React from 'react'; import { cn } from '@/lib/utils/cn'; export interface SpectrogramProps { analyserNode: AnalyserNode | null; className?: string; } export function Spectrogram({ analyserNode, className }: SpectrogramProps) { const canvasRef = React.useRef(null); const animationFrameRef = React.useRef(); const spectrogramDataRef = React.useRef(null); React.useEffect(() => { if (!analyserNode || !canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); const bufferLength = analyserNode.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); // Get background color from computed styles const bgColorStr = getComputedStyle(canvas.parentElement!).backgroundColor; const bgMatch = bgColorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); const bgR = bgMatch ? parseInt(bgMatch[1]) : 240; const bgG = bgMatch ? parseInt(bgMatch[2]) : 240; const bgB = bgMatch ? parseInt(bgMatch[3]) : 240; // Initialize spectrogram data spectrogramDataRef.current = ctx.createImageData(rect.width, rect.height); const draw = () => { animationFrameRef.current = requestAnimationFrame(draw); analyserNode.getByteFrequencyData(dataArray); if (!spectrogramDataRef.current) return; const imageData = spectrogramDataRef.current; // Shift existing data to the left by 1 pixel for (let y = 0; y < rect.height; y++) { for (let x = 0; x < rect.width - 1; x++) { const sourceIndex = ((y * rect.width) + x + 1) * 4; const targetIndex = ((y * rect.width) + x) * 4; imageData.data[targetIndex] = imageData.data[sourceIndex]; imageData.data[targetIndex + 1] = imageData.data[sourceIndex + 1]; imageData.data[targetIndex + 2] = imageData.data[sourceIndex + 2]; imageData.data[targetIndex + 3] = 255; } } // Add new column on the right const x = rect.width - 1; for (let y = 0; y < rect.height; y++) { // Map frequency bins to canvas height (inverted) const freqIndex = Math.floor((1 - y / rect.height) * bufferLength); const value = dataArray[freqIndex]; // Color mapping: background color (0) -> blue -> cyan -> green -> yellow -> red (255) let r, g, b; if (value < 64) { // Background to blue const t = value / 64; r = Math.round(bgR * (1 - t)); g = Math.round(bgG * (1 - t)); b = Math.round(bgB * (1 - t) + 255 * t); } else if (value < 128) { // Blue to cyan r = 0; g = (value - 64) * 4; b = 255; } else if (value < 192) { // Cyan to green r = 0; g = 255; b = 255 - (value - 128) * 4; } else { // Green to yellow to red r = (value - 192) * 4; g = 255; b = 0; } const index = ((y * rect.width) + x) * 4; imageData.data[index] = r; imageData.data[index + 1] = g; imageData.data[index + 2] = b; imageData.data[index + 3] = 255; } // Draw the spectrogram ctx.putImageData(imageData, 0, 0); // Draw frequency labels ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.fillText('20kHz', 5, 12); ctx.fillText('1kHz', 5, rect.height / 2); ctx.fillText('20Hz', 5, rect.height - 5); }; draw(); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [analyserNode]); return (
Spectrogram
); }