mirror3/src/components/VoiceAssistant copy.tsx
2025-03-07 02:22:25 +08:00

240 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef, useCallback } from "react";
interface VoiceAssistantProps {
greeting: string;
}
// 性能优化配置
const ANALYSER_FFT_SIZE = 128; // 降低FFT大小提升性能
const VOLUME_SENSITIVITY = 1.5;
const SMOOTHING_FACTOR = 0.7; // 增加平滑系数减少突变
const BAR_COUNT = 12; // 固定柱状图数量
const VoiceAssistant = ({ greeting }: VoiceAssistantProps) => {
// 状态管理
const [isListening, setIsListening] = useState(false);
const [error, setError] = useState<string | null>(null);
// DOM元素引用
const barsRef = useRef<HTMLDivElement>(null);
// 音频处理相关引用
const mediaStreamRef = useRef<MediaStream | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const animationFrameRef = useRef<number | null>(null);
// 重用数据数组减少内存分配
const dataArrayRef = useRef<Uint8Array | null>(null);
const lastValuesRef = useRef<number[]>(new Array(BAR_COUNT).fill(10));
// 初始化音频处理使用useCallback避免重复创建
const initAudioPipeline = useCallback(async () => {
try {
mediaStreamRef.current = await navigator.mediaDevices.getUserMedia({
audio: {
noiseSuppression: true,
echoCancellation: true,
autoGainControl: false,
},
});
const AudioContextClass =
window.AudioContext || (window as any).webkitAudioContext;
audioContextRef.current = new AudioContextClass({
latencyHint: "balanced", // 平衡延迟和性能
});
analyserRef.current = audioContextRef.current.createAnalyser();
analyserRef.current.fftSize = ANALYSER_FFT_SIZE;
analyserRef.current.smoothingTimeConstant = SMOOTHING_FACTOR;
// 初始化数据数组
dataArrayRef.current = new Uint8Array(
analyserRef.current.frequencyBinCount
);
const source = audioContextRef.current.createMediaStreamSource(
mediaStreamRef.current
);
source.connect(analyserRef.current);
startVisualization();
} catch (err) {
console.error("音频初始化失败:", err);
setIsListening(false);
}
}, []);
// 优化后的可视化逻辑直接操作DOM
const startVisualization = useCallback(() => {
if (!analyserRef.current || !dataArrayRef.current) {
console.error('Audio analyzer not initialized');
return;
}
// 获取频率数据缓冲区长度
const bufferLength = analyserRef.current.frequencyBinCount;
// 初始化时创建数据数组(安全校验)
if (!dataArrayRef.current || dataArrayRef.current.length !== bufferLength) {
dataArrayRef.current = new Uint8Array(bufferLength);
}
// 定义动画帧回调
const updateBars = () => {
// 1. 获取最新频率数据
analyserRef.current!.getByteFrequencyData(dataArrayRef.current!);
// 2. 性能优化批量DOM操作
const bars = barsRef.current?.children;
if (!bars) return;
// 3. 使用现代循环代替Array.from提升性能
for (let i = 0; i < bars.length; i++) {
const bar = bars[i] as HTMLElement;
// 4. 优化数据采样策略前1/2频谱
const dataIndex = Math.floor((i / BAR_COUNT) * (bufferLength / 2));
const rawValue = (dataArrayRef.current![dataIndex] / 255) * 100 * VOLUME_SENSITIVITY;
// 5. 应用指数平滑滤波
const smoothValue = Math.min(100, Math.max(10,
rawValue * 0.6 + lastValuesRef.current[i] * 0.4
));
lastValuesRef.current[i] = smoothValue;
// 6. 复合样式更新(减少重排)
bar.style.cssText = `
height: ${smoothValue}%;
transform: scaleY(${0.8 + (smoothValue / 100) * 0.6});
animation-delay: ${i * 0.1}s;
`;
}
// 7. 使用绑定this的requestAnimationFrame
animationFrameRef.current = requestAnimationFrame(updateBars.bind(this));
};
// 8. 启动动画前取消已有帧
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(updateBars);
// 9. 返回清理函数
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, []); // 10. 移除不必要的依赖项
// 切换监听状态
const toggleListening = useCallback(async () => {
if (isListening) {
mediaStreamRef.current?.getTracks().forEach((track) => track.stop());
audioContextRef.current?.close();
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
} else {
await initAudioPipeline();
}
setIsListening((prev) => !prev);
}, [isListening, initAudioPipeline]);
// 清理资源
useEffect(() => {
return () => {
if (isListening) {
mediaStreamRef.current?.getTracks().forEach((track) => track.stop());
audioContextRef.current?.close();
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
}
};
}, [isListening]);
return (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-full px-4">
{/* 问候语 */}
<h1 className="text-6xl font-light mb-8 drop-shadow-glow">{greeting}</h1>
{/* 优化后的音频波形可视化 */}
<div className="relative inline-block">
<button
onClick={toggleListening}
className={[
"group relative flex h-20 items-end gap-1.5 rounded-3xl p-6",
"bg-gradient-to-b from-black/80 to-gray-900/90",
"shadow-[0_0_20px_0_rgba(34,211,238,0.2)] hover:shadow-[0_0_30px_0_rgba(109,213,237,0.3)]",
"transition-shadow duration-300 ease-out",
isListening ? "ring-2 ring-cyan-400/20" : "",
].join(" ")}
style={{
// 禁用will-change优化经测试反而降低性能
backdropFilter: "blur(12px)", // 直接使用CSS属性
WebkitBackdropFilter: "blur(12px)",
}}
>
{/* 优化后的柱状图容器 */}
<div ref={barsRef} className="flex h-full w-full items-end gap-1.5">
{[...Array(BAR_COUNT)].map((_, index) => (
<div
key={index}
className={[
"w-2.5 rounded-full",
"bg-gradient-to-t from-cyan-400/90 via-blue-400/90 to-purple-500/90",
"transition-transform duration-150 ease-out",
].join(" ")}
style={{
willChange: "height, transform", // 提示浏览器优化
boxShadow: "0 0 8px -2px rgba(52,211,254,0.4)",
height: "10%", // 初始高度
}}
/>
))}
</div>
</button>
</div>
{/* 底部状态信息 */}
<div className="mt-8 text-xs text-gray-500 space-y-1">
<p>"魔镜魔镜"</p>
<div className="flex items-center justify-center gap-1.5">
<div className="relative flex items-center">
{/* 呼吸圆点指示器 */}
<div
className={`w-2 h-2 rounded-full ${
isListening ? "bg-green-400 animate-breath" : "bg-gray-400"
}`}
/>
{/* 扩散波纹效果 */}
{isListening && (
<div className="absolute inset-0 rounded-full bg-green-400/20 animate-ping" />
)}
</div>
<span>: {isListening ? "监听中" : "待机"}</span>
</div>
</div>
{/* 错误提示 */}
{error && (
<div className="mt-6 text-red-400/90 text-lg animate-fade-in">
{error}
<button
onClick={() => setError(null)}
className="ml-3 text-gray-400 hover:text-gray-300 transition-colors"
>
×
</button>
</div>
)}
</div>
);
};
export default VoiceAssistant;