voice-print.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { useEffect, useRef, useCallback } 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. // Canvas引用,用于获取绘图上下文
  9. const canvasRef = useRef<HTMLCanvasElement>(null);
  10. // 存储历史频率数据,用于平滑处理
  11. const historyRef = useRef<number[][]>([]);
  12. // 控制保留的历史数据帧数,影响平滑度
  13. const historyLengthRef = useRef(10);
  14. // 存储动画帧ID,用于清理
  15. const animationFrameRef = useRef<number>();
  16. /**
  17. * 更新频率历史数据
  18. * 使用FIFO队列维护固定长度的历史记录
  19. */
  20. const updateHistory = useCallback((freqArray: number[]) => {
  21. historyRef.current.push(freqArray);
  22. if (historyRef.current.length > historyLengthRef.current) {
  23. historyRef.current.shift();
  24. }
  25. }, []);
  26. useEffect(() => {
  27. const canvas = canvasRef.current;
  28. if (!canvas) return;
  29. const ctx = canvas.getContext("2d");
  30. if (!ctx) return;
  31. /**
  32. * 处理高DPI屏幕显示
  33. * 根据设备像素比例调整canvas实际渲染分辨率
  34. */
  35. const dpr = window.devicePixelRatio || 1;
  36. canvas.width = canvas.offsetWidth * dpr;
  37. canvas.height = canvas.offsetHeight * dpr;
  38. ctx.scale(dpr, dpr);
  39. /**
  40. * 主要绘制函数
  41. * 使用requestAnimationFrame实现平滑动画
  42. * 包含以下步骤:
  43. * 1. 清空画布
  44. * 2. 更新历史数据
  45. * 3. 计算波形点
  46. * 4. 绘制上下对称的声纹
  47. */
  48. const draw = () => {
  49. // 清空画布
  50. ctx.clearRect(0, 0, canvas.width, canvas.height);
  51. if (!frequencies || !isActive) {
  52. historyRef.current = [];
  53. return;
  54. }
  55. const freqArray = Array.from(frequencies);
  56. updateHistory(freqArray);
  57. // 绘制声纹
  58. const points: [number, number][] = [];
  59. const centerY = canvas.height / 2;
  60. const width = canvas.width;
  61. const sliceWidth = width / (frequencies.length - 1);
  62. // 绘制主波形
  63. ctx.beginPath();
  64. ctx.moveTo(0, centerY);
  65. /**
  66. * 声纹绘制算法:
  67. * 1. 使用历史数据平均值实现平滑过渡
  68. * 2. 通过正弦函数添加自然波动
  69. * 3. 使用贝塞尔曲线连接点,使曲线更平滑
  70. * 4. 绘制对称部分形成完整声纹
  71. */
  72. for (let i = 0; i < frequencies.length; i++) {
  73. const x = i * sliceWidth;
  74. let avgFrequency = frequencies[i];
  75. /**
  76. * 波形平滑处理:
  77. * 1. 收集历史数据中对应位置的频率值
  78. * 2. 计算当前值与历史值的加权平均
  79. * 3. 根据平均值计算实际显示高度
  80. */
  81. if (historyRef.current.length > 0) {
  82. const historicalValues = historyRef.current.map((h) => h[i] || 0);
  83. avgFrequency =
  84. (avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
  85. (historyRef.current.length + 1);
  86. }
  87. /**
  88. * 波形变换:
  89. * 1. 归一化频率值到0-1范围
  90. * 2. 添加时间相关的正弦变换
  91. * 3. 使用贝塞尔曲线平滑连接点
  92. */
  93. const normalized = avgFrequency / 255.0;
  94. const height = normalized * (canvas.height / 2);
  95. const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
  96. points.push([x, y]);
  97. if (i === 0) {
  98. ctx.moveTo(x, y);
  99. } else {
  100. // 使用贝塞尔曲线使波形更平滑
  101. const prevPoint = points[i - 1];
  102. const midX = (prevPoint[0] + x) / 2;
  103. ctx.quadraticCurveTo(
  104. prevPoint[0],
  105. prevPoint[1],
  106. midX,
  107. (prevPoint[1] + y) / 2,
  108. );
  109. }
  110. }
  111. // 绘制对称的下半部分
  112. for (let i = points.length - 1; i >= 0; i--) {
  113. const [x, y] = points[i];
  114. const symmetricY = centerY - (y - centerY);
  115. if (i === points.length - 1) {
  116. ctx.lineTo(x, symmetricY);
  117. } else {
  118. const nextPoint = points[i + 1];
  119. const midX = (nextPoint[0] + x) / 2;
  120. ctx.quadraticCurveTo(
  121. nextPoint[0],
  122. centerY - (nextPoint[1] - centerY),
  123. midX,
  124. centerY - ((nextPoint[1] + y) / 2 - centerY),
  125. );
  126. }
  127. }
  128. ctx.closePath();
  129. /**
  130. * 渐变效果:
  131. * 从左到右应用三色渐变,带透明度
  132. * 使用蓝色系配色提升视觉效果
  133. */
  134. const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
  135. gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
  136. gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
  137. gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
  138. ctx.fillStyle = gradient;
  139. ctx.fill();
  140. animationFrameRef.current = requestAnimationFrame(draw);
  141. };
  142. // 启动动画循环
  143. draw();
  144. // 清理函数:在组件卸载时取消动画
  145. return () => {
  146. if (animationFrameRef.current) {
  147. cancelAnimationFrame(animationFrameRef.current);
  148. }
  149. };
  150. }, [frequencies, isActive, updateHistory]);
  151. return (
  152. <div className={styles["voice-print"]}>
  153. <canvas ref={canvasRef} />
  154. </div>
  155. );
  156. }