AIvoices/frontend-nextjs/app/components/Playground/Visualizer.tsx
2025-04-08 14:05:27 +01:00

211 lines
7.1 KiB
TypeScript

import React, { useEffect, useRef } from "react";
const interpolateColor = (
startColor: number[],
endColor: number[],
factor: number
): number[] => {
const result = [];
for (let i = 0; i < startColor.length; i++) {
result[i] = Math.round(
startColor[i] + factor * (endColor[i] - startColor[i])
);
}
return result;
};
interface VisualizerProps {
stream?: MediaStream;
audioBuffer?: AudioBuffer;
}
const Visualizer: React.FC<VisualizerProps> = ({ stream, audioBuffer }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const dataArrayRef = useRef<Uint8Array | null>(null);
const barHeightsRef = useRef<number[]>([]);
useEffect(() => {
let bufferSource: AudioBufferSourceNode | null = null;
let mediaStreamSource: MediaStreamAudioSourceNode | null = null;
if (stream || audioBuffer) {
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
const analyser = audioContext.createAnalyser();
analyser.fftSize = 128;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
barHeightsRef.current = new Array(bufferLength).fill(0);
if (stream) {
mediaStreamSource =
audioContext.createMediaStreamSource(stream);
mediaStreamSource.connect(analyser);
}
if (audioBuffer) {
bufferSource = audioContext.createBufferSource();
bufferSource.buffer = audioBuffer;
bufferSource.connect(analyser);
bufferSource.start();
}
audioContextRef.current = audioContext;
analyserRef.current = analyser;
dataArrayRef.current = dataArray;
draw();
}
return () => {
if (bufferSource) {
bufferSource.stop();
}
if (mediaStreamSource) {
mediaStreamSource.disconnect();
}
if (analyserRef.current) {
analyserRef.current.disconnect();
}
if (
audioContextRef.current &&
audioContextRef.current.state !== "closed"
) {
audioContextRef.current.close();
}
};
}, [stream, audioBuffer]);
const draw = () => {
const canvas = canvasRef.current;
if (!canvas || !analyserRef.current || !dataArrayRef.current) return;
const context = canvas.getContext("2d");
if (!context) return;
const devicePixelRatio = window.devicePixelRatio || 1;
const width = canvas.clientWidth * devicePixelRatio;
const height = canvas.clientHeight * devicePixelRatio;
const centerY = height / 2; // Center of the canvas
canvas.width = width;
canvas.height = height;
const barWidth = Math.floor(
(width / analyserRef.current.frequencyBinCount) * 2
);
const barGap = 6; // Adjust this value to increase the gap between bars
let x = 0;
requestAnimationFrame(draw);
analyserRef.current.getByteFrequencyData(dataArrayRef.current);
context.clearRect(0, 0, width, height);
const startColor = [19, 239, 147];
const endColor = [20, 154, 251];
const smoothingFactor = 0.05; // Adjust this value for smoother transitions
const cornerRadius = 3; // Adjust this value to change the chamfer (corner radius)
for (let i = 0; i < dataArrayRef.current.length; i++) {
const value = dataArrayRef.current[i];
const targetHeight = (value / 255) * (height / 2); // Adjust to half height for centering
barHeightsRef.current[i] +=
(targetHeight - barHeightsRef.current[i]) * smoothingFactor;
const barHeight = Math.floor(barHeightsRef.current[i]);
const interpolationFactor = value / 255;
const color = interpolateColor(
startColor,
endColor,
interpolationFactor
);
context.fillStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.8)`;
if (barHeight === 0) {
// Draw a small circle (point) at the center when barHeight is 0
const pointRadius = 2; // Adjust the radius of the point
context.beginPath();
context.arc(
x + barWidth / 2,
centerY,
pointRadius,
0,
Math.PI * 2
);
context.closePath();
context.fill();
} else if (barHeight < cornerRadius) {
// Draw a circle
context.beginPath();
context.arc(
x + barWidth / 2,
centerY,
barHeight,
0,
Math.PI * 2
);
context.closePath();
context.fill();
} else {
// Draw the rounded rectangle with top and bottom rounded corners
context.beginPath();
context.moveTo(x, centerY - barHeight);
context.lineTo(x, centerY - barHeight + cornerRadius);
context.quadraticCurveTo(
x,
centerY - barHeight,
x + cornerRadius,
centerY - barHeight
);
context.lineTo(
x + barWidth - cornerRadius,
centerY - barHeight
);
context.quadraticCurveTo(
x + barWidth,
centerY - barHeight,
x + barWidth,
centerY - barHeight + cornerRadius
);
context.lineTo(
x + barWidth,
centerY + barHeight - cornerRadius
);
context.quadraticCurveTo(
x + barWidth,
centerY + barHeight,
x + barWidth - cornerRadius,
centerY + barHeight
);
context.lineTo(x + cornerRadius, centerY + barHeight);
context.quadraticCurveTo(
x,
centerY + barHeight,
x,
centerY + barHeight - cornerRadius
);
context.lineTo(x, centerY - barHeight);
context.closePath();
context.fill();
}
x += barWidth + barGap; // Increase the gap between bars
}
};
return (
<div className="flex justify-center items-center">
<canvas
ref={canvasRef}
className="sm:w-[160px] w-[110px] h-[24px]"
></canvas>
</div>
);
};
export default Visualizer;