87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import * as React from 'react';
|
||
|
|
import { cn } from '@/lib/utils/cn';
|
||
|
|
|
||
|
|
export interface FrequencyAnalyzerProps {
|
||
|
|
analyserNode: AnalyserNode | null;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function FrequencyAnalyzer({ analyserNode, className }: FrequencyAnalyzerProps) {
|
||
|
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||
|
|
const animationFrameRef = React.useRef<number>();
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
const draw = () => {
|
||
|
|
animationFrameRef.current = requestAnimationFrame(draw);
|
||
|
|
|
||
|
|
analyserNode.getByteFrequencyData(dataArray);
|
||
|
|
|
||
|
|
// Clear canvas
|
||
|
|
ctx.fillStyle = 'rgb(15, 15, 15)';
|
||
|
|
ctx.fillRect(0, 0, rect.width, rect.height);
|
||
|
|
|
||
|
|
const barWidth = rect.width / bufferLength;
|
||
|
|
let x = 0;
|
||
|
|
|
||
|
|
for (let i = 0; i < bufferLength; i++) {
|
||
|
|
const barHeight = (dataArray[i] / 255) * rect.height;
|
||
|
|
|
||
|
|
// Color gradient based on frequency
|
||
|
|
const hue = (i / bufferLength) * 120; // 0 (red) to 120 (green)
|
||
|
|
ctx.fillStyle = `hsl(${180 + hue}, 70%, 50%)`;
|
||
|
|
|
||
|
|
ctx.fillRect(x, rect.height - barHeight, barWidth, barHeight);
|
||
|
|
x += barWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw frequency labels
|
||
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
||
|
|
ctx.font = '10px monospace';
|
||
|
|
ctx.textAlign = 'left';
|
||
|
|
ctx.fillText('20Hz', 5, rect.height - 5);
|
||
|
|
ctx.textAlign = 'center';
|
||
|
|
ctx.fillText('1kHz', rect.width / 2, rect.height - 5);
|
||
|
|
ctx.textAlign = 'right';
|
||
|
|
ctx.fillText('20kHz', rect.width - 5, rect.height - 5);
|
||
|
|
};
|
||
|
|
|
||
|
|
draw();
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
if (animationFrameRef.current) {
|
||
|
|
cancelAnimationFrame(animationFrameRef.current);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, [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">
|
||
|
|
Frequency Analyzer
|
||
|
|
</div>
|
||
|
|
<canvas
|
||
|
|
ref={canvasRef}
|
||
|
|
className="w-full h-[calc(100%-24px)] rounded"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|