Files
DogDetector/src/hooks/useAudioAnalyzer.ts

109 lines
3.0 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react';
export interface AudioAnalyzerState {
isInitialized: boolean;
isStreaming: boolean;
error: string | null;
}
export const useAudioAnalyzer = (fftSize: number = 8192) => {
const [state, setState] = useState<AudioAnalyzerState>({
isInitialized: false,
isStreaming: false,
error: null,
});
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
const init = useCallback(async () => {
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error("Microphone API not available (requires HTTPS)");
}
const AudioContextCtor = window.AudioContext || (window as any).webkitAudioContext;
const ctx = new AudioContextCtor({ sampleRate: 48000 });
// iOS requires AudioContext to be resumed synchronously during user interaction
if (ctx.state === 'suspended') {
await ctx.resume();
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false,
},
});
const analyser = ctx.createAnalyser();
analyser.fftSize = fftSize;
analyser.smoothingTimeConstant = 0.3;
const source = ctx.createMediaStreamSource(stream);
source.connect(analyser);
audioContextRef.current = ctx;
analyserRef.current = analyser;
streamRef.current = stream;
sourceRef.current = source;
setState({
isInitialized: true,
isStreaming: true,
error: null,
});
return true;
} catch (err: any) {
console.error('Failed to init audio context', err);
setState(s => ({ ...s, error: err.message || 'Microphone access denied' }));
return false;
}
}, [fftSize]);
const stop = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
if (audioContextRef.current) {
audioContextRef.current.close();
}
setState({
isInitialized: false,
isStreaming: false,
error: null,
});
}, []);
useEffect(() => {
return () => {
stop();
};
}, [stop]);
// Expose a method to grab the latest frequency data
const getFrequencyData = useCallback(() => {
if (!analyserRef.current || !audioContextRef.current) return null;
const analyser = analyserRef.current;
// Uint8Array for performance. Values 0-255.
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
return {
data: dataArray,
sampleRate: audioContextRef.current.sampleRate,
fftSize: analyser.fftSize,
binCount: analyser.frequencyBinCount
};
}, []);
return { ...state, init, stop, getFrequencyData };
};