voice-print.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { useEffect, useRef } from "react";
  2. import styles from "./voice-print.module.scss";
  3. interface VoicePrintProps {
  4. frequencies?: Uint8Array;
  5. isActive?: boolean;
  6. }
  7. export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
  8. const canvasRef = useRef<HTMLCanvasElement>(null);
  9. const historyRef = useRef<number[][]>([]); // 存储历史频率数据,用于平滑处理
  10. const historyLengthRef = useRef(10); // 历史数据保留帧数,影响平滑程度
  11. const animationFrameRef = useRef<number>(); // 用于管理动画帧
  12. const currentFrequenciesRef = useRef<Uint8Array>(); // 当前频率数据的引用
  13. const amplitudeMultiplier = useRef(1.5); // 波形振幅倍数,控制波形高度
  14. // 更新频率数据的副作用
  15. useEffect(() => {
  16. if (!frequencies || !isActive) {
  17. historyRef.current = [];
  18. currentFrequenciesRef.current = undefined;
  19. return;
  20. }
  21. currentFrequenciesRef.current = frequencies;
  22. const freqArray = Array.from(frequencies);
  23. const newHistory = [...historyRef.current, freqArray];
  24. if (newHistory.length > historyLengthRef.current) {
  25. newHistory.shift();
  26. }
  27. historyRef.current = newHistory;
  28. }, [frequencies, isActive]);
  29. // 渲染函数:负责绘制声纹动画
  30. const render = () => {
  31. const canvas = canvasRef.current;
  32. const frequencies = currentFrequenciesRef.current;
  33. if (!canvas || !frequencies || !isActive) return;
  34. const ctx = canvas.getContext("2d");
  35. if (!ctx) return;
  36. // 清空画布
  37. ctx.clearRect(0, 0, canvas.width, canvas.height);
  38. const points: [number, number][] = [];
  39. const centerY = canvas.height / 2;
  40. const width = canvas.width;
  41. // 频率采样处理
  42. // 将输入的频率数据重采样为128个点,减少计算量并保持显示效果
  43. const frequencyStep = Math.ceil(frequencies.length / 128); // 计算采样间隔
  44. const effectiveFrequencies = Array.from(
  45. { length: 128 },
  46. (_, i) => frequencies[i * frequencyStep] || 0,
  47. );
  48. // 计算每个频率点在画布上的水平间距
  49. const sliceWidth = width / (effectiveFrequencies.length - 1);
  50. ctx.beginPath();
  51. ctx.moveTo(0, centerY);
  52. // 遍历采样后的频率数据,计算并绘制波形
  53. for (let i = 0; i < effectiveFrequencies.length; i++) {
  54. const x = i * sliceWidth;
  55. let avgFrequency = effectiveFrequencies[i];
  56. // 使用历史数据进行平滑处理
  57. // 当前值权重为2,历史数据权重为1,实现平滑过渡
  58. if (historyRef.current.length > 0) {
  59. const historicalValues = historyRef.current.map(
  60. (h) => h[i * frequencyStep] || 0,
  61. );
  62. avgFrequency =
  63. (avgFrequency * 2 + historicalValues.reduce((a, b) => a + b, 0)) /
  64. (historyRef.current.length + 2);
  65. }
  66. // 波形计算
  67. const normalized = Math.pow(avgFrequency / 255.0, 1.1); // 使用幂函数增强对比度
  68. const height =
  69. normalized * (canvas.height / 2) * amplitudeMultiplier.current;
  70. // 使用正弦函数创建波动效果,i * 0.15控制波形密度,Date.now() * 0.003控制波动速度
  71. const y = centerY + height * Math.sin(i * 0.15 + Date.now() * 0.003);
  72. points.push([x, y]);
  73. // 使用贝塞尔曲线绘制平滑波形
  74. if (i === 0) {
  75. ctx.moveTo(x, y);
  76. } else {
  77. const prevPoint = points[i - 1];
  78. const midX = (prevPoint[0] + x) / 2;
  79. // 二次贝塞尔曲线,使用中点作为控制点
  80. ctx.quadraticCurveTo(
  81. prevPoint[0],
  82. prevPoint[1],
  83. midX,
  84. (prevPoint[1] + y) / 2,
  85. );
  86. }
  87. }
  88. // 绘制对称的下半部分波形,创建镜像效果
  89. for (let i = points.length - 1; i >= 0; i--) {
  90. const [x, y] = points[i];
  91. const symmetricY = centerY - (y - centerY);
  92. if (i === points.length - 1) {
  93. ctx.lineTo(x, symmetricY);
  94. } else {
  95. const nextPoint = points[i + 1];
  96. const midX = (nextPoint[0] + x) / 2;
  97. ctx.quadraticCurveTo(
  98. nextPoint[0],
  99. centerY - (nextPoint[1] - centerY),
  100. midX,
  101. centerY - ((nextPoint[1] + y) / 2 - centerY),
  102. );
  103. }
  104. }
  105. ctx.closePath();
  106. // 创建水平渐变效果
  107. const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
  108. gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)"); // 左侧颜色
  109. gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)"); // 中间颜色
  110. gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)"); // 右侧颜色
  111. ctx.fillStyle = gradient;
  112. ctx.fill();
  113. animationFrameRef.current = requestAnimationFrame(render);
  114. };
  115. // 初始化canvas和动画循环
  116. useEffect(() => {
  117. const canvas = canvasRef.current;
  118. if (!canvas) return;
  119. // 处理高DPI显示器
  120. const dpr = window.devicePixelRatio || 1;
  121. canvas.width = canvas.offsetWidth * dpr;
  122. canvas.height = canvas.offsetHeight * dpr;
  123. const ctx = canvas.getContext("2d");
  124. if (!ctx) return;
  125. ctx.scale(dpr, dpr);
  126. render();
  127. return () => {
  128. if (animationFrameRef.current) {
  129. cancelAnimationFrame(animationFrameRef.current);
  130. }
  131. };
  132. }, []);
  133. return (
  134. <div className={styles["voice-print"]}>
  135. <canvas ref={canvasRef} />
  136. </div>
  137. );
  138. }