Implemented remaining Phase 10 analysis tools: **Phase Correlation Meter (10.3)** - Real-time stereo phase correlation display - Pearson correlation coefficient calculation - Color-coded indicator (-1 to +1 scale) - Visual feedback: Mono-like, Good Stereo, Wide Stereo, Phase Issues **LUFS Loudness Meter (10.3)** - Momentary, Short-term, and Integrated LUFS measurements - Simplified K-weighting approximation - Vertical bar display with -70 to 0 LUFS range - -23 LUFS broadcast standard reference line - Real-time history tracking (10 seconds) **Audio Statistics (10.4)** - Project info: track count, duration, sample rate, channels, bit depth - Level analysis: peak, RMS, dynamic range, headroom - Real-time buffer analysis from all tracks - Color-coded warnings for clipping and low headroom **Integration** - Added 5-button toggle in master column (FFT, SPEC, PHS, LUFS, INFO) - All analyzers share consistent 192px width layout - Theme-aware styling for light/dark modes - Compact button labels for space efficiency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
'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<HTMLCanvasElement>(null);
|
|
const animationFrameRef = React.useRef<number | undefined>(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 (
|
|
<div className={cn('w-full h-full bg-card/50 border-2 border-accent/50 rounded-lg p-2', className)}>
|
|
<div className="text-[10px] font-bold text-accent uppercase tracking-wider mb-2">
|
|
Phase Correlation
|
|
</div>
|
|
<div className="w-full h-[calc(100%-24px)] rounded bg-muted/30 flex flex-col items-center justify-center">
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-16 rounded"
|
|
/>
|
|
<div className="text-[9px] text-muted-foreground mt-2 text-center px-2">
|
|
{correlation > 0.9 ? 'Mono-like' :
|
|
correlation > 0.5 ? 'Good Stereo' :
|
|
correlation > -0.5 ? 'Wide Stereo' :
|
|
'Phase Issues'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|