From d33e772fa592c24e4adc03f127c887c9e4727913 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 8 Nov 2024 22:39:17 +0800 Subject: [PATCH] feat: voice print --- app/components/voice-print/voice-print.tsx | 268 +++++++++++---------- 1 file changed, 141 insertions(+), 127 deletions(-) diff --git a/app/components/voice-print/voice-print.tsx b/app/components/voice-print/voice-print.tsx index 9dd72758e30..793210c1930 100644 --- a/app/components/voice-print/voice-print.tsx +++ b/app/components/voice-print/voice-print.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useCallback } from "react"; import styles from "./voice-print.module.scss"; interface VoicePrintProps { @@ -7,156 +7,170 @@ interface VoicePrintProps { } export function VoicePrint({ frequencies, isActive }: VoicePrintProps) { + // Canvas引用,用于获取绘图上下文 const canvasRef = useRef(null); - const historyRef = useRef([]); // 存储历史频率数据,用于平滑处理 - const historyLengthRef = useRef(10); // 历史数据保留帧数,影响平滑程度 - const animationFrameRef = useRef(); // 用于管理动画帧 - const currentFrequenciesRef = useRef(); // 当前频率数据的引用 - const amplitudeMultiplier = useRef(1.5); // 波形振幅倍数,控制波形高度 - - // 更新频率数据的副作用 - 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(); + // 存储历史频率数据,用于平滑处理 + const historyRef = useRef([]); + // 控制保留的历史数据帧数,影响平滑度 + const historyLengthRef = useRef(10); + // 存储动画帧ID,用于清理 + const animationFrameRef = useRef(); + + /** + * 更新频率历史数据 + * 使用FIFO队列维护固定长度的历史记录 + */ + const updateHistory = useCallback((freqArray: number[]) => { + historyRef.current.push(freqArray); + if (historyRef.current.length > historyLengthRef.current) { + historyRef.current.shift(); } - historyRef.current = newHistory; - }, [frequencies, isActive]); + }, []); - // 渲染函数:负责绘制声纹动画 - const render = () => { + useEffect(() => { const canvas = canvasRef.current; - const frequencies = currentFrequenciesRef.current; - - if (!canvas || !frequencies || !isActive) return; + if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; - // 清空画布 - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const points: [number, number][] = []; - const centerY = canvas.height / 2; - const width = canvas.width; - - // 频率采样处理 - // 将输入的频率数据重采样为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.moveTo(0, centerY); - - // 遍历采样后的频率数据,计算并绘制波形 - for (let i = 0; i < effectiveFrequencies.length; i++) { - const x = i * sliceWidth; - let avgFrequency = effectiveFrequencies[i]; - - // 使用历史数据进行平滑处理 - // 当前值权重为2,历史数据权重为1,实现平滑过渡 - if (historyRef.current.length > 0) { - const historicalValues = historyRef.current.map( - (h) => h[i * frequencyStep] || 0, - ); - avgFrequency = - (avgFrequency * 2 + historicalValues.reduce((a, b) => a + b, 0)) / - (historyRef.current.length + 2); - } + /** + * 处理高DPI屏幕显示 + * 根据设备像素比例调整canvas实际渲染分辨率 + */ + const dpr = window.devicePixelRatio || 1; + canvas.width = canvas.offsetWidth * dpr; + canvas.height = canvas.offsetHeight * dpr; + ctx.scale(dpr, dpr); - // 波形计算 - const normalized = Math.pow(avgFrequency / 255.0, 1.1); // 使用幂函数增强对比度 - const height = - 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]); - - // 使用贝塞尔曲线绘制平滑波形 - 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, - ); + /** + * 主要绘制函数 + * 使用requestAnimationFrame实现平滑动画 + * 包含以下步骤: + * 1. 清空画布 + * 2. 更新历史数据 + * 3. 计算波形点 + * 4. 绘制上下对称的声纹 + */ + const draw = () => { + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (!frequencies || !isActive) { + historyRef.current = []; + return; } - } - // 绘制对称的下半部分波形,创建镜像效果 - 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), - ); + 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, + ); + } } - } - 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)"); // 右侧颜色 + // 绘制对称的下半部分 + 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.fillStyle = gradient; - ctx.fill(); + ctx.closePath(); - animationFrameRef.current = requestAnimationFrame(render); - }; + /** + * 渐变效果: + * 从左到右应用三色渐变,带透明度 + * 使用蓝色系配色提升视觉效果 + */ + 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)"); - // 初始化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; + ctx.fillStyle = gradient; + ctx.fill(); - const ctx = canvas.getContext("2d"); - if (!ctx) return; - ctx.scale(dpr, dpr); + animationFrameRef.current = requestAnimationFrame(draw); + }; - render(); + // 启动动画循环 + draw(); + // 清理函数:在组件卸载时取消动画 return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; - }, []); + }, [frequencies, isActive, updateHistory]); return (