- 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.
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
"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;
|
||
}
|