Files
ai-video-fullstack/frontend/src/hooks/use-audio-analyser.ts
T
Xin Wang b3fbfac5df Implement audio visualizers and refactor AssistantPage
- Introduce three new audio visualizer components: AuraVisualizer, SpectrumVisualizer, and WaveVisualizer, enhancing the audio interaction experience.
- Replace the deprecated VoiceVisualizer with the new visualizers, ensuring a cohesive visual language across components.
- Update the AssistantPage to support dynamic visualization style switching, improving user engagement during audio interactions.
- Refactor DebugVoicePanel to accommodate the new visualizer props and enhance the overall debugging interface.
2026-06-09 16:28:45 +08:00

91 lines
2.6 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.
"use client";
import * as React from "react";
export type UseAudioAnalyserOptions = {
/** 是否激活:true 时采集麦克风,false 时不连接(用于静态呼吸态) */
active?: boolean;
/** 外部分析器;提供后直接复用,不再申请麦克风 */
analyser?: AnalyserNode | null;
/** 外部音频流;提供后用它构建分析器,而不调用 getUserMedia */
stream?: MediaStream | null;
fftSize?: number;
smoothingTimeConstant?: number;
onError?: (error: unknown) => void;
};
/**
* 三种可视化共用的音频接入逻辑:优先用外部 analyser / stream,
* 否则在 active 时申请麦克风。返回一个始终指向当前 AnalyserNode 的 ref。
*/
export function useAudioAnalyser({
active = false,
analyser = null,
stream = null,
fftSize = 512,
smoothingTimeConstant = 0.7,
onError,
}: UseAudioAnalyserOptions) {
const analyserRef = React.useRef<AnalyserNode | null>(null);
const onErrorRef = React.useRef(onError);
React.useEffect(() => {
onErrorRef.current = onError;
}, [onError]);
React.useEffect(() => {
let cancelled = false;
let ctx: AudioContext | null = null;
let micStream: MediaStream | null = null;
async function setup() {
if (analyser) {
analyserRef.current = analyser;
return;
}
if (!active) {
analyserRef.current = null;
return;
}
try {
const AudioCtor =
window.AudioContext ||
(window as unknown as { webkitAudioContext: typeof AudioContext })
.webkitAudioContext;
ctx = new AudioCtor();
let source: MediaStreamAudioSourceNode;
if (stream) {
source = ctx.createMediaStreamSource(stream);
} else {
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
if (cancelled) {
micStream.getTracks().forEach((track) => track.stop());
return;
}
source = ctx.createMediaStreamSource(micStream);
}
const node = ctx.createAnalyser();
node.fftSize = fftSize;
node.smoothingTimeConstant = smoothingTimeConstant;
source.connect(node);
analyserRef.current = node;
} catch (error) {
if (!cancelled) onErrorRef.current?.(error);
}
}
setup();
return () => {
cancelled = true;
analyserRef.current = analyser ?? null;
micStream?.getTracks().forEach((track) => track.stop());
ctx?.close().catch(() => {});
};
}, [active, analyser, stream, fftSize, smoothingTimeConstant]);
return analyserRef;
}