Spaces:
Sleeping
Sleeping
import { useEffect, useRef, useCallback } from "react"; | |
import styles from "./voice-print.module.scss"; | |
interface VoicePrintProps { | |
frequencies?: Uint8Array; | |
isActive?: boolean; | |
} | |
export function VoicePrint({ frequencies, isActive }: VoicePrintProps) { | |
// Canvas引用,用于获取绘图上下文 | |
const canvasRef = useRef<HTMLCanvasElement>(null); | |
// 存储历史频率数据,用于平滑处理 | |
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(); | |
} | |
}, []); | |
useEffect(() => { | |
const canvas = canvasRef.current; | |
if (!canvas) return; | |
const ctx = canvas.getContext("2d"); | |
if (!ctx) return; | |
/** | |
* 处理高DPI屏幕显示 | |
* 根据设备像素比例调整canvas实际渲染分辨率 | |
*/ | |
const dpr = window.devicePixelRatio || 1; | |
canvas.width = canvas.offsetWidth * dpr; | |
canvas.height = canvas.offsetHeight * dpr; | |
ctx.scale(dpr, dpr); | |
/** | |
* 主要绘制函数 | |
* 使用requestAnimationFrame实现平滑动画 | |
* 包含以下步骤: | |
* 1. 清空画布 | |
* 2. 更新历史数据 | |
* 3. 计算波形点 | |
* 4. 绘制上下对称的声纹 | |
*/ | |
const draw = () => { | |
// 清空画布 | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
if (!frequencies || !isActive) { | |
historyRef.current = []; | |
return; | |
} | |
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, | |
); | |
} | |
} | |
// 绘制对称的下半部分 | |
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), | |
); | |
} | |
} | |
ctx.closePath(); | |
/** | |
* 渐变效果: | |
* 从左到右应用三色渐变,带透明度 | |
* 使用蓝色系配色提升视觉效果 | |
*/ | |
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)"); | |
ctx.fillStyle = gradient; | |
ctx.fill(); | |
animationFrameRef.current = requestAnimationFrame(draw); | |
}; | |
// 启动动画循环 | |
draw(); | |
// 清理函数:在组件卸载时取消动画 | |
return () => { | |
if (animationFrameRef.current) { | |
cancelAnimationFrame(animationFrameRef.current); | |
} | |
}; | |
}, [frequencies, isActive, updateHistory]); | |
return ( | |
<div className={styles["voice-print"]}> | |
<canvas ref={canvasRef} /> | |
</div> | |
); | |
} | |