feat: voice print
This commit is contained in:
parent
8b4ca133fd
commit
89136fba32
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import styles from "./voice-print.module.scss";
|
import styles from "./voice-print.module.scss";
|
||||||
|
|
||||||
interface VoicePrintProps {
|
interface VoicePrintProps {
|
||||||
|
@ -8,76 +8,92 @@ interface VoicePrintProps {
|
||||||
|
|
||||||
export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
|
export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const [history, setHistory] = useState<number[][]>([]);
|
const historyRef = useRef<number[][]>([]); // 存储历史频率数据,用于平滑处理
|
||||||
const historyLengthRef = useRef(10); // 保存10帧历史数据
|
const historyLengthRef = useRef(10); // 历史数据保留帧数,影响平滑程度
|
||||||
|
const animationFrameRef = useRef<number>(); // 用于管理动画帧
|
||||||
|
const currentFrequenciesRef = useRef<Uint8Array>(); // 当前频率数据的引用
|
||||||
|
const amplitudeMultiplier = useRef(1.5); // 波形振幅倍数,控制波形高度
|
||||||
|
|
||||||
|
// 更新频率数据的副作用
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!frequencies || !isActive) {
|
||||||
|
historyRef.current = [];
|
||||||
|
currentFrequenciesRef.current = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFrequenciesRef.current = frequencies;
|
||||||
|
const freqArray = Array.from(frequencies);
|
||||||
|
const newHistory = [...historyRef.current, freqArray];
|
||||||
|
if (newHistory.length > historyLengthRef.current) {
|
||||||
|
newHistory.shift();
|
||||||
|
}
|
||||||
|
historyRef.current = newHistory;
|
||||||
|
}, [frequencies, isActive]);
|
||||||
|
|
||||||
|
// 渲染函数:负责绘制声纹动画
|
||||||
|
const render = () => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
const frequencies = currentFrequenciesRef.current;
|
||||||
|
|
||||||
|
if (!canvas || !frequencies || !isActive) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
// 设置canvas尺寸
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
canvas.width = canvas.offsetWidth * dpr;
|
|
||||||
canvas.height = canvas.offsetHeight * dpr;
|
|
||||||
ctx.scale(dpr, dpr);
|
|
||||||
|
|
||||||
// 清空画布
|
// 清空画布
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
if (!frequencies || !isActive) {
|
|
||||||
setHistory([]); // 重置历史数据
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新历史数据
|
|
||||||
const freqArray = Array.from(frequencies);
|
|
||||||
setHistory((prev) => {
|
|
||||||
const newHistory = [...prev, freqArray];
|
|
||||||
if (newHistory.length > historyLengthRef.current) {
|
|
||||||
newHistory.shift();
|
|
||||||
}
|
|
||||||
return newHistory;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绘制声纹
|
|
||||||
const points: [number, number][] = [];
|
const points: [number, number][] = [];
|
||||||
const centerY = canvas.height / 2;
|
const centerY = canvas.height / 2;
|
||||||
const width = canvas.width;
|
const width = canvas.width;
|
||||||
const sliceWidth = width / (frequencies.length - 1);
|
|
||||||
|
|
||||||
// 绘制主波形
|
// 频率采样处理
|
||||||
|
// 将输入的频率数据重采样为128个点,减少计算量并保持显示效果
|
||||||
|
const frequencyStep = Math.ceil(frequencies.length / 128); // 计算采样间隔
|
||||||
|
const effectiveFrequencies = Array.from(
|
||||||
|
{ length: 128 },
|
||||||
|
(_, i) => frequencies[i * frequencyStep] || 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算每个频率点在画布上的水平间距
|
||||||
|
const sliceWidth = width / (effectiveFrequencies.length - 1);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(0, centerY);
|
ctx.moveTo(0, centerY);
|
||||||
|
|
||||||
// 使用历史数据计算平均值实现平滑效果
|
// 遍历采样后的频率数据,计算并绘制波形
|
||||||
for (let i = 0; i < frequencies.length; i++) {
|
for (let i = 0; i < effectiveFrequencies.length; i++) {
|
||||||
const x = i * sliceWidth;
|
const x = i * sliceWidth;
|
||||||
let avgFrequency = frequencies[i];
|
let avgFrequency = effectiveFrequencies[i];
|
||||||
|
|
||||||
// 计算历史数据的平均值
|
// 使用历史数据进行平滑处理
|
||||||
if (history.length > 0) {
|
// 当前值权重为2,历史数据权重为1,实现平滑过渡
|
||||||
const historicalValues = history.map((h) => h[i] || 0);
|
if (historyRef.current.length > 0) {
|
||||||
|
const historicalValues = historyRef.current.map(
|
||||||
|
(h) => h[i * frequencyStep] || 0,
|
||||||
|
);
|
||||||
avgFrequency =
|
avgFrequency =
|
||||||
(avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
|
(avgFrequency * 2 + historicalValues.reduce((a, b) => a + b, 0)) /
|
||||||
(history.length + 1);
|
(historyRef.current.length + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用三角函数使波形更自然
|
// 波形计算
|
||||||
const normalized = avgFrequency / 255.0;
|
const normalized = Math.pow(avgFrequency / 255.0, 1.1); // 使用幂函数增强对比度
|
||||||
const height = normalized * (canvas.height / 2);
|
const height =
|
||||||
const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
|
normalized * (canvas.height / 2) * amplitudeMultiplier.current;
|
||||||
|
// 使用正弦函数创建波动效果,i * 0.15控制波形密度,Date.now() * 0.003控制波动速度
|
||||||
|
const y = centerY + height * Math.sin(i * 0.15 + Date.now() * 0.003);
|
||||||
|
|
||||||
points.push([x, y]);
|
points.push([x, y]);
|
||||||
|
|
||||||
|
// 使用贝塞尔曲线绘制平滑波形
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
ctx.moveTo(x, y);
|
ctx.moveTo(x, y);
|
||||||
} else {
|
} else {
|
||||||
// 使用贝塞尔曲线使波形更平滑
|
|
||||||
const prevPoint = points[i - 1];
|
const prevPoint = points[i - 1];
|
||||||
const midX = (prevPoint[0] + x) / 2;
|
const midX = (prevPoint[0] + x) / 2;
|
||||||
|
// 二次贝塞尔曲线,使用中点作为控制点
|
||||||
ctx.quadraticCurveTo(
|
ctx.quadraticCurveTo(
|
||||||
prevPoint[0],
|
prevPoint[0],
|
||||||
prevPoint[1],
|
prevPoint[1],
|
||||||
|
@ -87,7 +103,7 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制对称的下半部分
|
// 绘制对称的下半部分波形,创建镜像效果
|
||||||
for (let i = points.length - 1; i >= 0; i--) {
|
for (let i = points.length - 1; i >= 0; i--) {
|
||||||
const [x, y] = points[i];
|
const [x, y] = points[i];
|
||||||
const symmetricY = centerY - (y - centerY);
|
const symmetricY = centerY - (y - centerY);
|
||||||
|
@ -107,15 +123,40 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
|
||||||
|
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
// 设置渐变色和透明度
|
// 创建水平渐变效果
|
||||||
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||||
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
|
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)"); // 左侧颜色
|
||||||
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
|
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)"); // 中间颜色
|
||||||
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
|
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)"); // 右侧颜色
|
||||||
|
|
||||||
ctx.fillStyle = gradient;
|
ctx.fillStyle = gradient;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}, [frequencies, isActive, history]);
|
|
||||||
|
animationFrameRef.current = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化canvas和动画循环
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
// 处理高DPI显示器
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = canvas.offsetWidth * dpr;
|
||||||
|
canvas.height = canvas.offsetHeight * dpr;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
render();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (animationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["voice-print"]}>
|
<div className={styles["voice-print"]}>
|
||||||
|
|
Loading…
Reference in New Issue