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.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"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;
|
||||
}
|
||||
Reference in New Issue
Block a user