NextChat-U/app/components/voice-print/voice-print.tsx

181 lines
5.3 KiB
TypeScript
Raw Normal View History

2024-11-08 23:39:17 +09:00
import { useEffect, useRef, useCallback } from "react";
2024-11-08 23:02:31 +09:00
import styles from "./voice-print.module.scss";
interface VoicePrintProps {
frequencies?: Uint8Array;
isActive?: boolean;
}
export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
2024-11-08 23:39:17 +09:00
// Canvas引用用于获取绘图上下文
2024-11-08 23:02:31 +09:00
const canvasRef = useRef<HTMLCanvasElement>(null);
2024-11-08 23:39:17 +09:00
// 存储历史频率数据,用于平滑处理
const historyRef = useRef<number[][]>([]);
// 控制保留的历史数据帧数,影响平滑度
const historyLengthRef = useRef(10);
// 存储动画帧ID用于清理
const animationFrameRef = useRef<number>();
/**
*
* 使FIFO队列维护固定长度的历史记录
*/
const updateHistory = useCallback((freqArray: number[]) => {
historyRef.current.push(freqArray);
if (historyRef.current.length > historyLengthRef.current) {
historyRef.current.shift();
2024-11-08 23:18:39 +09:00
}
2024-11-08 23:39:17 +09:00
}, []);
2024-11-08 23:18:39 +09:00
2024-11-08 23:39:17 +09:00
useEffect(() => {
2024-11-08 23:02:31 +09:00
const canvas = canvasRef.current;
2024-11-08 23:39:17 +09:00
if (!canvas) return;
2024-11-08 23:02:31 +09:00
const ctx = canvas.getContext("2d");
if (!ctx) return;
2024-11-08 23:39:17 +09:00
/**
* DPI屏幕显示
* canvas实际渲染分辨率
*/
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
2024-11-08 23:02:31 +09:00
2024-11-08 23:39:17 +09:00
/**
*
* 使requestAnimationFrame实现平滑动画
*
* 1.
* 2.
* 3.
* 4.
*/
const draw = () => {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!frequencies || !isActive) {
historyRef.current = [];
return;
2024-11-08 23:02:31 +09:00
}
2024-11-08 23:39:17 +09:00
const freqArray = Array.from(frequencies);
updateHistory(freqArray);
// 绘制声纹
const points: [number, number][] = [];
const centerY = canvas.height / 2;
const width = canvas.width;
const sliceWidth = width / (frequencies.length - 1);
// 绘制主波形
ctx.beginPath();
ctx.moveTo(0, centerY);
/**
*
* 1. 使
* 2.
* 3. 使线使线
* 4.
*/
for (let i = 0; i < frequencies.length; i++) {
const x = i * sliceWidth;
let avgFrequency = frequencies[i];
/**
*
* 1.
* 2.
* 3.
*/
if (historyRef.current.length > 0) {
const historicalValues = historyRef.current.map((h) => h[i] || 0);
avgFrequency =
(avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
(historyRef.current.length + 1);
}
/**
*
* 1. 0-1
* 2.
* 3. 使线
*/
const normalized = avgFrequency / 255.0;
const height = normalized * (canvas.height / 2);
const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
points.push([x, y]);
if (i === 0) {
ctx.moveTo(x, y);
} else {
// 使用贝塞尔曲线使波形更平滑
const prevPoint = points[i - 1];
const midX = (prevPoint[0] + x) / 2;
ctx.quadraticCurveTo(
prevPoint[0],
prevPoint[1],
midX,
(prevPoint[1] + y) / 2,
);
}
2024-11-08 23:02:31 +09:00
}
2024-11-08 23:39:17 +09:00
// 绘制对称的下半部分
for (let i = points.length - 1; i >= 0; i--) {
const [x, y] = points[i];
const symmetricY = centerY - (y - centerY);
if (i === points.length - 1) {
ctx.lineTo(x, symmetricY);
} else {
const nextPoint = points[i + 1];
const midX = (nextPoint[0] + x) / 2;
ctx.quadraticCurveTo(
nextPoint[0],
centerY - (nextPoint[1] - centerY),
midX,
centerY - ((nextPoint[1] + y) / 2 - centerY),
);
}
}
2024-11-08 23:02:31 +09:00
2024-11-08 23:39:17 +09:00
ctx.closePath();
2024-11-08 23:18:39 +09:00
2024-11-08 23:39:17 +09:00
/**
*
*
* 使
*/
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
2024-11-08 23:18:39 +09:00
2024-11-08 23:39:17 +09:00
ctx.fillStyle = gradient;
ctx.fill();
2024-11-08 23:18:39 +09:00
2024-11-08 23:39:17 +09:00
animationFrameRef.current = requestAnimationFrame(draw);
};
2024-11-08 23:18:39 +09:00
2024-11-08 23:39:17 +09:00
// 启动动画循环
draw();
2024-11-08 23:18:39 +09:00
2024-11-08 23:39:17 +09:00
// 清理函数:在组件卸载时取消动画
2024-11-08 23:18:39 +09:00
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
2024-11-08 23:39:17 +09:00
}, [frequencies, isActive, updateHistory]);
2024-11-08 23:02:31 +09:00
return (
<div className={styles["voice-print"]}>
<canvas ref={canvasRef} />
</div>
);
}