"use client"; import { useEffect, useRef } from "react"; // Configuration constants for the audio analyzer const AUDIO_CONFIG = { FFT_SIZE: 512, SMOOTHING: 0.8, MIN_BAR_HEIGHT: 2, MIN_BAR_WIDTH: 2, BAR_SPACING: 1, COLOR: { MIN_INTENSITY: 100, // Minimum gray value (darker) MAX_INTENSITY: 255, // Maximum gray value (brighter) INTENSITY_RANGE: 155, // MAX_INTENSITY - MIN_INTENSITY }, } as const; interface AudioVisualizerProps { stream: MediaStream | null; isRecording: boolean; onClick: () => void; } export function AudioVisualizer({ stream, isRecording, onClick, }: AudioVisualizerProps) { // Refs for managing audio context and animation const canvasRef = useRef(null); const audioContextRef = useRef(null); const analyserRef = useRef(null); const animationFrameRef = useRef(); const containerRef = useRef(null); // Cleanup function to stop visualization and close audio context const cleanup = () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } if (audioContextRef.current) { audioContextRef.current.close(); } }; // Cleanup on unmount useEffect(() => { return cleanup; }, []); // Start or stop visualization based on recording state useEffect(() => { if (stream && isRecording) { startVisualization(); } else { cleanup(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [stream, isRecording]); // Handle window resize useEffect(() => { const handleResize = () => { if (canvasRef.current && containerRef.current) { const container = containerRef.current; const canvas = canvasRef.current; const dpr = window.devicePixelRatio || 1; // Set canvas size based on container and device pixel ratio const rect = container.getBoundingClientRect(); // Account for the 2px total margin (1px on each side) canvas.width = (rect.width - 2) * dpr; canvas.height = (rect.height - 2) * dpr; // Scale canvas CSS size to match container minus margins canvas.style.width = `${rect.width - 2}px`; canvas.style.height = `${rect.height - 2}px`; } }; window.addEventListener("resize", handleResize); // Initial setup handleResize(); return () => window.removeEventListener("resize", handleResize); }, []); // Initialize audio context and start visualization const startVisualization = async () => { try { const audioContext = new AudioContext(); audioContextRef.current = audioContext; const analyser = audioContext.createAnalyser(); analyser.fftSize = AUDIO_CONFIG.FFT_SIZE; analyser.smoothingTimeConstant = AUDIO_CONFIG.SMOOTHING; analyserRef.current = analyser; const source = audioContext.createMediaStreamSource(stream!); source.connect(analyser); draw(); } catch (error) { console.error("Error starting visualization:", error); } }; // Calculate the color intensity based on bar height const getBarColor = (normalizedHeight: number) => { const intensity = Math.floor(normalizedHeight * AUDIO_CONFIG.COLOR.INTENSITY_RANGE) + AUDIO_CONFIG.COLOR.MIN_INTENSITY; return `rgb(${intensity}, ${intensity}, ${intensity})`; }; // Draw a single bar of the visualizer const drawBar = ( ctx: CanvasRenderingContext2D, x: number, centerY: number, width: number, height: number, color: string ) => { ctx.fillStyle = color; // Draw upper bar (above center) ctx.fillRect(x, centerY - height, width, height); // Draw lower bar (below center) ctx.fillRect(x, centerY, width, height); }; // Main drawing function const draw = () => { if (!isRecording) return; const canvas = canvasRef.current; const ctx = canvas?.getContext("2d"); if (!canvas || !ctx || !analyserRef.current) return; const dpr = window.devicePixelRatio || 1; ctx.scale(dpr, dpr); const analyser = analyserRef.current; const bufferLength = analyser.frequencyBinCount; const frequencyData = new Uint8Array(bufferLength); const drawFrame = () => { animationFrameRef.current = requestAnimationFrame(drawFrame); // Get current frequency data analyser.getByteFrequencyData(frequencyData); // Clear canvas - use CSS pixels for clearing ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr); // Calculate dimensions in CSS pixels const barWidth = Math.max( AUDIO_CONFIG.MIN_BAR_WIDTH, canvas.width / dpr / bufferLength - AUDIO_CONFIG.BAR_SPACING ); const centerY = canvas.height / dpr / 2; let x = 0; // Draw each frequency bar for (let i = 0; i < bufferLength; i++) { const normalizedHeight = frequencyData[i] / 255; // Convert to 0-1 range const barHeight = Math.max( AUDIO_CONFIG.MIN_BAR_HEIGHT, normalizedHeight * centerY ); drawBar( ctx, x, centerY, barWidth, barHeight, getBarColor(normalizedHeight) ); x += barWidth + AUDIO_CONFIG.BAR_SPACING; } }; drawFrame(); }; return (
); }