Before attempting to integrate osciloscope properely. STILL BROKEN NOW

This commit is contained in:
2025-12-20 15:34:07 +01:00
parent 6fa754a1eb
commit 26584ea848
5 changed files with 1420 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
import { useState, useRef, useCallback, useEffect } from 'react';
interface AudioAnalyzerState {
isActive: boolean;
error: string | null;
source: 'microphone' | 'file' | null;
fileName: string | null;
isPlaying: boolean;
}
export const useAudioAnalyzer = () => {
const [state, setState] = useState<AudioAnalyzerState>({
isActive: false,
error: null,
source: null,
fileName: null,
isPlaying: false,
});
const audioContextRef = useRef<AudioContext | null>(null);
const analyzerLeftRef = useRef<AnalyserNode | null>(null);
const analyzerRightRef = useRef<AnalyserNode | null>(null);
const sourceRef = useRef<MediaStreamAudioSourceNode | MediaElementAudioSourceNode | null>(null);
const splitterRef = useRef<ChannelSplitterNode | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const analysisGainNodeRef = useRef<GainNode | null>(null);
const audioElementRef = useRef<HTMLAudioElement | null>(null);
const gainValueRef = useRef<number>(3); // Default higher gain for analysis sensitivity only
const getTimeDomainData = useCallback(() => {
if (!analyzerLeftRef.current) return null;
const bufferLength = analyzerLeftRef.current.fftSize;
const dataArray = new Uint8Array(bufferLength);
analyzerLeftRef.current.getByteTimeDomainData(dataArray);
return dataArray;
}, []);
const getStereoData = useCallback(() => {
if (!analyzerLeftRef.current || !analyzerRightRef.current) return null;
const bufferLength = analyzerLeftRef.current.fftSize;
const leftData = new Uint8Array(bufferLength);
const rightData = new Uint8Array(bufferLength);
analyzerLeftRef.current.getByteTimeDomainData(leftData);
analyzerRightRef.current.getByteTimeDomainData(rightData);
return { left: leftData, right: rightData };
}, []);
const setGain = useCallback((value: number) => {
gainValueRef.current = value;
if (analysisGainNodeRef.current) {
analysisGainNodeRef.current.gain.value = value;
}
}, []);
const setupAnalyzers = useCallback((audioContext: AudioContext) => {
// Create gain node for analysis sensitivity (does NOT affect audio output)
analysisGainNodeRef.current = audioContext.createGain();
analysisGainNodeRef.current.gain.value = gainValueRef.current;
// Create channel splitter for stereo
splitterRef.current = audioContext.createChannelSplitter(2);
// Create analyzers for each channel
analyzerLeftRef.current = audioContext.createAnalyser();
analyzerRightRef.current = audioContext.createAnalyser();
// Configure analyzers for higher sensitivity
const fftSize = 2048;
analyzerLeftRef.current.fftSize = fftSize;
analyzerRightRef.current.fftSize = fftSize;
analyzerLeftRef.current.smoothingTimeConstant = 0.5;
analyzerRightRef.current.smoothingTimeConstant = 0.5;
analyzerLeftRef.current.minDecibels = -90;
analyzerRightRef.current.minDecibels = -90;
analyzerLeftRef.current.maxDecibels = -10;
analyzerRightRef.current.maxDecibels = -10;
}, []);
const startMicrophone = useCallback(async () => {
try {
setState(prev => ({ ...prev, isActive: false, error: null }));
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
},
});
streamRef.current = stream;
audioContextRef.current = new AudioContext();
setupAnalyzers(audioContextRef.current);
// Create source from microphone
const micSource = audioContextRef.current.createMediaStreamSource(stream);
sourceRef.current = micSource;
// Connect: source -> analysisGain -> splitter -> analyzers
// (microphone doesn't need output, just analysis)
micSource.connect(analysisGainNodeRef.current!);
analysisGainNodeRef.current!.connect(splitterRef.current!);
splitterRef.current!.connect(analyzerLeftRef.current!, 0);
splitterRef.current!.connect(analyzerRightRef.current!, 1);
setState({
isActive: true,
error: null,
source: 'microphone',
fileName: null,
isPlaying: true
});
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to access microphone';
setState(prev => ({ ...prev, isActive: false, error: message }));
}
}, [setupAnalyzers]);
const loadAudioFile = useCallback(async (file: File) => {
try {
// Stop any existing audio
stop();
setState(prev => ({ ...prev, isActive: false, error: null }));
// Create audio element
const audioElement = new Audio();
audioElement.src = URL.createObjectURL(file);
audioElement.loop = true;
audioElementRef.current = audioElement;
audioContextRef.current = new AudioContext();
setupAnalyzers(audioContextRef.current);
// Create source from audio element
const audioSource = audioContextRef.current.createMediaElementSource(audioElement);
sourceRef.current = audioSource;
// For files: source -> destination (clean audio output)
// source -> analysisGain -> splitter -> analyzers (boosted for visualization)
audioSource.connect(audioContextRef.current.destination);
audioSource.connect(analysisGainNodeRef.current!);
analysisGainNodeRef.current!.connect(splitterRef.current!);
splitterRef.current!.connect(analyzerLeftRef.current!, 0);
splitterRef.current!.connect(analyzerRightRef.current!, 1);
// Start playing
await audioElement.play();
setState({
isActive: true,
error: null,
source: 'file',
fileName: file.name,
isPlaying: true
});
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load audio file';
setState(prev => ({ ...prev, isActive: false, error: message }));
}
}, [setupAnalyzers]);
const togglePlayPause = useCallback(() => {
if (!audioElementRef.current) return;
if (audioElementRef.current.paused) {
audioElementRef.current.play();
setState(prev => ({ ...prev, isPlaying: true }));
} else {
audioElementRef.current.pause();
setState(prev => ({ ...prev, isPlaying: false }));
}
}, []);
const stop = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
if (audioElementRef.current) {
audioElementRef.current.pause();
audioElementRef.current.src = '';
audioElementRef.current = null;
}
if (sourceRef.current) {
sourceRef.current.disconnect();
sourceRef.current = null;
}
if (analysisGainNodeRef.current) {
analysisGainNodeRef.current.disconnect();
analysisGainNodeRef.current = null;
}
if (splitterRef.current) {
splitterRef.current.disconnect();
splitterRef.current = null;
}
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}
analyzerLeftRef.current = null;
analyzerRightRef.current = null;
setState({
isActive: false,
error: null,
source: null,
fileName: null,
isPlaying: false
});
}, []);
useEffect(() => {
return () => {
stop();
};
}, [stop]);
const getAudioElement = useCallback(() => {
return audioElementRef.current;
}, []);
return {
...state,
startMicrophone,
loadAudioFile,
togglePlayPause,
stop,
setGain,
getTimeDomainData,
getStereoData,
getAudioElement,
};
};