voice-print.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { useEffect, useRef, useState } 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 [history, setHistory] = useState<number[][]>([]);
  10. const historyLengthRef = useRef(10); // 保存10帧历史数据
  11. useEffect(() => {
  12. const canvas = canvasRef.current;
  13. if (!canvas) return;
  14. const ctx = canvas.getContext("2d");
  15. if (!ctx) return;
  16. // 设置canvas尺寸
  17. const dpr = window.devicePixelRatio || 1;
  18. canvas.width = canvas.offsetWidth * dpr;
  19. canvas.height = canvas.offsetHeight * dpr;
  20. ctx.scale(dpr, dpr);
  21. // 清空画布
  22. ctx.clearRect(0, 0, canvas.width, canvas.height);
  23. if (!frequencies || !isActive) {
  24. setHistory([]); // 重置历史数据
  25. return;
  26. }
  27. // 更新历史数据
  28. const freqArray = Array.from(frequencies);
  29. setHistory((prev) => {
  30. const newHistory = [...prev, freqArray];
  31. if (newHistory.length > historyLengthRef.current) {
  32. newHistory.shift();
  33. }
  34. return newHistory;
  35. });
  36. // 绘制声纹
  37. const points: [number, number][] = [];
  38. const centerY = canvas.height / 2;
  39. const width = canvas.width;
  40. const sliceWidth = width / (frequencies.length - 1);
  41. // 绘制主波形
  42. ctx.beginPath();
  43. ctx.moveTo(0, centerY);
  44. // 使用历史数据计算平均值实现平滑效果
  45. for (let i = 0; i < frequencies.length; i++) {
  46. const x = i * sliceWidth;
  47. let avgFrequency = frequencies[i];
  48. // 计算历史数据的平均值
  49. if (history.length > 0) {
  50. const historicalValues = history.map((h) => h[i] || 0);
  51. avgFrequency =
  52. (avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
  53. (history.length + 1);
  54. }
  55. // 使用三角函数使波形更自然
  56. const normalized = avgFrequency / 255.0;
  57. const height = normalized * (canvas.height / 2);
  58. const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
  59. points.push([x, y]);
  60. if (i === 0) {
  61. ctx.moveTo(x, y);
  62. } else {
  63. // 使用贝塞尔曲线使波形更平滑
  64. const prevPoint = points[i - 1];
  65. const midX = (prevPoint[0] + x) / 2;
  66. ctx.quadraticCurveTo(
  67. prevPoint[0],
  68. prevPoint[1],
  69. midX,
  70. (prevPoint[1] + y) / 2,
  71. );
  72. }
  73. }
  74. // 绘制对称的下半部分
  75. for (let i = points.length - 1; i >= 0; i--) {
  76. const [x, y] = points[i];
  77. const symmetricY = centerY - (y - centerY);
  78. if (i === points.length - 1) {
  79. ctx.lineTo(x, symmetricY);
  80. } else {
  81. const nextPoint = points[i + 1];
  82. const midX = (nextPoint[0] + x) / 2;
  83. ctx.quadraticCurveTo(
  84. nextPoint[0],
  85. centerY - (nextPoint[1] - centerY),
  86. midX,
  87. centerY - ((nextPoint[1] + y) / 2 - centerY),
  88. );
  89. }
  90. }
  91. ctx.closePath();
  92. // 设置渐变色和透明度
  93. const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
  94. gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
  95. gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
  96. gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
  97. ctx.fillStyle = gradient;
  98. ctx.fill();
  99. }, [frequencies, isActive, history]);
  100. return (
  101. <div className={styles["voice-print"]}>
  102. <canvas ref={canvasRef} />
  103. </div>
  104. );
  105. }