'use client'; import * as React from 'react'; import { cn } from '@/lib/utils/cn'; export interface PhaseCorrelationMeterProps { analyserNode: AnalyserNode | null; className?: string; } export function PhaseCorrelationMeter({ analyserNode, className }: PhaseCorrelationMeterProps) { const canvasRef = React.useRef(null); const animationFrameRef = React.useRef(undefined); const [correlation, setCorrelation] = React.useState(0); 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 audioContext = analyserNode.context as AudioContext; const bufferLength = analyserNode.fftSize; const dataArrayL = new Float32Array(bufferLength); const dataArrayR = new Float32Array(bufferLength); // Create a splitter to get L/R channels const splitter = audioContext.createChannelSplitter(2); const analyserL = audioContext.createAnalyser(); const analyserR = audioContext.createAnalyser(); analyserL.fftSize = bufferLength; analyserR.fftSize = bufferLength; // Try to connect to the analyser node's source // Note: This is a simplified approach - ideally we'd get the source node try { analyserNode.connect(splitter); splitter.connect(analyserL, 0); splitter.connect(analyserR, 1); } catch (e) { // If connection fails, just show static display } const draw = () => { animationFrameRef.current = requestAnimationFrame(draw); try { analyserL.getFloatTimeDomainData(dataArrayL); analyserR.getFloatTimeDomainData(dataArrayR); // Calculate phase correlation (Pearson correlation coefficient) let sumL = 0, sumR = 0, sumLR = 0, sumL2 = 0, sumR2 = 0; const n = bufferLength; for (let i = 0; i < n; i++) { sumL += dataArrayL[i]; sumR += dataArrayR[i]; sumLR += dataArrayL[i] * dataArrayR[i]; sumL2 += dataArrayL[i] * dataArrayL[i]; sumR2 += dataArrayR[i] * dataArrayR[i]; } const meanL = sumL / n; const meanR = sumR / n; const covLR = (sumLR / n) - (meanL * meanR); const varL = (sumL2 / n) - (meanL * meanL); const varR = (sumR2 / n) - (meanR * meanR); let r = 0; if (varL > 0 && varR > 0) { r = covLR / Math.sqrt(varL * varR); r = Math.max(-1, Math.min(1, r)); // Clamp to [-1, 1] } setCorrelation(r); // Clear canvas const bgColor = getComputedStyle(canvas.parentElement!).backgroundColor; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, rect.width, rect.height); // Draw scale background const centerY = rect.height / 2; const barHeight = 20; // Draw scale markers ctx.fillStyle = 'rgba(128, 128, 128, 0.2)'; ctx.fillRect(0, centerY - barHeight / 2, rect.width, barHeight); // Draw center line (0) ctx.strokeStyle = 'rgba(128, 128, 128, 0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(rect.width / 2, centerY - barHeight / 2 - 5); ctx.lineTo(rect.width / 2, centerY + barHeight / 2 + 5); ctx.stroke(); // Draw correlation indicator const x = ((r + 1) / 2) * rect.width; // Color based on correlation value let color; if (r > 0.9) { color = '#10b981'; // Green - good correlation (mono-ish) } else if (r > 0.5) { color = '#84cc16'; // Lime - moderate correlation } else if (r > -0.5) { color = '#eab308'; // Yellow - decorrelated (good stereo) } else if (r > -0.9) { color = '#f97316'; // Orange - negative correlation } else { color = '#ef4444'; // Red - phase issues } ctx.fillStyle = color; ctx.fillRect(x - 2, centerY - barHeight / 2, 4, barHeight); // Draw labels ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.font = '9px monospace'; ctx.textAlign = 'left'; ctx.fillText('-1', 2, centerY - barHeight / 2 - 8); ctx.textAlign = 'center'; ctx.fillText('0', rect.width / 2, centerY - barHeight / 2 - 8); ctx.textAlign = 'right'; ctx.fillText('+1', rect.width - 2, centerY - barHeight / 2 - 8); // Draw correlation value ctx.textAlign = 'center'; ctx.font = 'bold 11px monospace'; ctx.fillText(r.toFixed(3), rect.width / 2, centerY + barHeight / 2 + 15); } catch (e) { // Silently handle errors } }; draw(); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } try { splitter.disconnect(); analyserL.disconnect(); analyserR.disconnect(); } catch (e) { // Ignore disconnection errors } }; }, [analyserNode]); return (
Phase Correlation
{correlation > 0.9 ? 'Mono-like' : correlation > 0.5 ? 'Good Stereo' : correlation > -0.5 ? 'Wide Stereo' : 'Phase Issues'}
); }