168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
interface AudioActivityConfig {
|
|
threshold?: number; // 音频阈值,默认 30
|
|
sampleRate?: number; // 采样率,默认 100ms
|
|
smoothingTimeConstant?: number; // 平滑常数,默认 0.8
|
|
fftSize?: number; // FFT 大小,默认 1024
|
|
}
|
|
|
|
interface UseAudioActivityDetectionReturn {
|
|
isDetecting: boolean;
|
|
isSpeaking: boolean;
|
|
audioLevel: number;
|
|
startDetection: (stream: MediaStream) => void;
|
|
stopDetection: () => void;
|
|
error: string | null;
|
|
}
|
|
|
|
export function useAudioActivityDetection(
|
|
config: AudioActivityConfig = {}
|
|
): UseAudioActivityDetectionReturn {
|
|
const {
|
|
threshold = 30,
|
|
sampleRate = 100,
|
|
smoothingTimeConstant = 0.8,
|
|
fftSize = 1024,
|
|
} = config;
|
|
|
|
const [isDetecting, setIsDetecting] = useState(false);
|
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
const [audioLevel, setAudioLevel] = useState(0);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
const microphoneRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
|
const animationFrameRef = useRef<number | null>(null);
|
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// 音频分析函数
|
|
const analyzeAudio = useCallback(() => {
|
|
if (!analyserRef.current) return;
|
|
|
|
const analyser = analyserRef.current;
|
|
const bufferLength = analyser.frequencyBinCount;
|
|
const dataArray = new Uint8Array(bufferLength);
|
|
|
|
const checkAudioLevel = () => {
|
|
if (!analyser) return;
|
|
|
|
analyser.getByteFrequencyData(dataArray);
|
|
|
|
// 计算平均音量
|
|
let sum = 0;
|
|
for (let i = 0; i < bufferLength; i++) {
|
|
sum += dataArray[i];
|
|
}
|
|
const average = sum / bufferLength;
|
|
|
|
setAudioLevel(average);
|
|
|
|
// 判断是否在说话
|
|
const currentlySpeaking = average > threshold;
|
|
setIsSpeaking(currentlySpeaking);
|
|
|
|
// 调试日志
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// console.log('Audio Level:', average, 'Speaking:', currentlySpeaking);
|
|
}
|
|
};
|
|
|
|
// 使用定时器而不是 requestAnimationFrame 以确保稳定的采样率
|
|
intervalRef.current = setInterval(checkAudioLevel, sampleRate);
|
|
}, [threshold, sampleRate]);
|
|
|
|
// 开始检测
|
|
const startDetection = useCallback(async (stream: MediaStream) => {
|
|
try {
|
|
setError(null);
|
|
|
|
// 创建音频上下文
|
|
if (!audioContextRef.current) {
|
|
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
}
|
|
|
|
const audioContext = audioContextRef.current;
|
|
|
|
// 确保音频上下文处于运行状态
|
|
if (audioContext.state === 'suspended') {
|
|
await audioContext.resume();
|
|
}
|
|
|
|
// 创建分析器
|
|
const analyser = audioContext.createAnalyser();
|
|
analyser.smoothingTimeConstant = smoothingTimeConstant;
|
|
analyser.fftSize = fftSize;
|
|
|
|
// 创建音频源
|
|
const microphone = audioContext.createMediaStreamSource(stream);
|
|
microphone.connect(analyser);
|
|
|
|
analyserRef.current = analyser;
|
|
microphoneRef.current = microphone;
|
|
|
|
setIsDetecting(true);
|
|
analyzeAudio();
|
|
|
|
console.log('音频活动检测已启动');
|
|
} catch (err) {
|
|
console.error('启动音频检测失败:', err);
|
|
setError(err instanceof Error ? err.message : '启动音频检测失败');
|
|
}
|
|
}, [analyzeAudio, smoothingTimeConstant, fftSize]);
|
|
|
|
// 停止检测
|
|
const stopDetection = useCallback(() => {
|
|
// 清除定时器
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
|
|
// 清除动画帧
|
|
if (animationFrameRef.current) {
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
animationFrameRef.current = null;
|
|
}
|
|
|
|
// 断开音频连接
|
|
if (microphoneRef.current) {
|
|
microphoneRef.current.disconnect();
|
|
microphoneRef.current = null;
|
|
}
|
|
|
|
// 清理分析器
|
|
analyserRef.current = null;
|
|
|
|
// 关闭音频上下文
|
|
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
|
|
audioContextRef.current.close();
|
|
audioContextRef.current = null;
|
|
}
|
|
|
|
setIsDetecting(false);
|
|
setIsSpeaking(false);
|
|
setAudioLevel(0);
|
|
setError(null);
|
|
|
|
console.log('音频活动检测已停止');
|
|
}, []);
|
|
|
|
// 组件卸载时清理
|
|
useEffect(() => {
|
|
return () => {
|
|
stopDetection();
|
|
};
|
|
}, [stopDetection]);
|
|
|
|
return {
|
|
isDetecting,
|
|
isSpeaking,
|
|
audioLevel,
|
|
startDetection,
|
|
stopDetection,
|
|
error,
|
|
};
|
|
}
|