"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(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; }