diff --git a/app/components/voice-print/voice-print.tsx b/app/components/voice-print/voice-print.tsx index de21da5cf..9dd72758e 100644 --- a/app/components/voice-print/voice-print.tsx +++ b/app/components/voice-print/voice-print.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import styles from "./voice-print.module.scss"; interface VoicePrintProps { @@ -8,76 +8,92 @@ interface VoicePrintProps { export function VoicePrint({ frequencies, isActive }: VoicePrintProps) { const canvasRef = useRef(null); - const [history, setHistory] = useState([]); - const historyLengthRef = useRef(10); // 保存10帧历史数据 + 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(); + } + historyRef.current = newHistory; + }, [frequencies, isActive]); + + // 渲染函数:负责绘制声纹动画 + const render = () => { const canvas = canvasRef.current; - if (!canvas) return; + const frequencies = currentFrequenciesRef.current; + + if (!canvas || !frequencies || !isActive) return; const ctx = canvas.getContext("2d"); 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); - 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 centerY = canvas.height / 2; 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.moveTo(0, centerY); - // 使用历史数据计算平均值实现平滑效果 - for (let i = 0; i < frequencies.length; i++) { + // 遍历采样后的频率数据,计算并绘制波形 + for (let i = 0; i < effectiveFrequencies.length; i++) { const x = i * sliceWidth; - let avgFrequency = frequencies[i]; + let avgFrequency = effectiveFrequencies[i]; - // 计算历史数据的平均值 - if (history.length > 0) { - const historicalValues = history.map((h) => h[i] || 0); + // 使用历史数据进行平滑处理 + // 当前值权重为2,历史数据权重为1,实现平滑过渡 + if (historyRef.current.length > 0) { + const historicalValues = historyRef.current.map( + (h) => h[i * frequencyStep] || 0, + ); avgFrequency = - (avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) / - (history.length + 1); + (avgFrequency * 2 + historicalValues.reduce((a, b) => a + b, 0)) / + (historyRef.current.length + 2); } - // 使用三角函数使波形更自然 - const normalized = avgFrequency / 255.0; - const height = normalized * (canvas.height / 2); - const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002); + // 波形计算 + 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], @@ -87,7 +103,7 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) { } } - // 绘制对称的下半部分 + // 绘制对称的下半部分波形,创建镜像效果 for (let i = points.length - 1; i >= 0; i--) { const [x, y] = points[i]; const symmetricY = centerY - (y - centerY); @@ -107,15 +123,40 @@ export function VoicePrint({ frequencies, isActive }: VoicePrintProps) { 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)"); + 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(); - }, [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 (