108 lines
2.8 KiB
TypeScript
108 lines
2.8 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
export interface AudioLevelMeterOptions {
|
|
enabled: boolean;
|
|
deviceId?: string;
|
|
smoothingFactor?: number;
|
|
}
|
|
|
|
export function useAudioLevelMeter(options: AudioLevelMeterOptions) {
|
|
const [level, setLevel] = useState(0);
|
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
const streamRef = useRef<MediaStream | null>(null);
|
|
const animationFrameRef = useRef<number | null>(null);
|
|
|
|
const cleanup = useCallback(() => {
|
|
if (animationFrameRef.current) {
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
animationFrameRef.current = null;
|
|
}
|
|
if (streamRef.current) {
|
|
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
streamRef.current = null;
|
|
}
|
|
if (audioContextRef.current) {
|
|
audioContextRef.current.close();
|
|
audioContextRef.current = null;
|
|
}
|
|
analyserRef.current = null;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!options.enabled) {
|
|
cleanup();
|
|
setLevel(0);
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
|
|
const startMonitoring = async () => {
|
|
try {
|
|
const constraints: MediaStreamConstraints = {
|
|
audio: options.deviceId ? { deviceId: { exact: options.deviceId } } : true,
|
|
video: false,
|
|
};
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
if (!mounted) {
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
return;
|
|
}
|
|
|
|
streamRef.current = stream;
|
|
|
|
const audioContext = new AudioContext();
|
|
if (audioContext.state === "suspended") {
|
|
await audioContext.resume();
|
|
}
|
|
audioContextRef.current = audioContext;
|
|
|
|
const analyser = audioContext.createAnalyser();
|
|
analyser.fftSize = 256;
|
|
analyser.smoothingTimeConstant = options.smoothingFactor ?? 0.8;
|
|
analyserRef.current = analyser;
|
|
|
|
const source = audioContext.createMediaStreamSource(stream);
|
|
source.connect(analyser);
|
|
|
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
|
|
const updateLevel = () => {
|
|
if (!mounted || !analyserRef.current) return;
|
|
|
|
analyser.getByteFrequencyData(dataArray);
|
|
|
|
let sum = 0;
|
|
for (let i = 0; i < dataArray.length; i++) {
|
|
sum += dataArray[i] * dataArray[i];
|
|
}
|
|
const rms = Math.sqrt(sum / dataArray.length);
|
|
const normalizedLevel = Math.min(100, (rms / 255) * 100 * 2);
|
|
|
|
setLevel(normalizedLevel);
|
|
animationFrameRef.current = requestAnimationFrame(updateLevel);
|
|
};
|
|
|
|
updateLevel();
|
|
} catch (err) {
|
|
console.error("Error starting audio level monitoring:", err);
|
|
if (mounted) {
|
|
setLevel(0);
|
|
}
|
|
}
|
|
};
|
|
|
|
startMonitoring();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
cleanup();
|
|
};
|
|
}, [options.enabled, options.deviceId, options.smoothingFactor, cleanup]);
|
|
|
|
return { level };
|
|
}
|