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