mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
import { PitchAnalyzer } from '../audio/PitchAnalyzer';
|
|
import { frequencyToMidi, getNoteDetails, getCentDifference } from '../music/NoteUtils';
|
|
|
|
interface PitchData {
|
|
frequency: number;
|
|
midi: number;
|
|
note: string;
|
|
cents: number;
|
|
clarity: number; // Placeholder for now, maybe uses probability if YIN exposes it
|
|
}
|
|
|
|
export function usePitchDetector(active: boolean) {
|
|
const analyzerRef = useRef<PitchAnalyzer | null>(null);
|
|
const [pitchData, setPitchData] = useState<PitchData | null>(null);
|
|
const [isListening, setIsListening] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const animationRef = useRef<number | null>(null);
|
|
|
|
const updatePitch = useCallback(() => {
|
|
if (!analyzerRef.current) return;
|
|
|
|
const freq = analyzerRef.current.getPitch();
|
|
if (freq) {
|
|
const midi = frequencyToMidi(freq);
|
|
const { scientific } = getNoteDetails(midi);
|
|
|
|
// Calculate cents off purely for display if needed,
|
|
// but for training we usually care if midi matches.
|
|
|
|
const cents = getCentDifference(freq, midi);
|
|
|
|
setPitchData({
|
|
frequency: freq,
|
|
midi,
|
|
note: scientific,
|
|
cents,
|
|
clarity: 1
|
|
});
|
|
}
|
|
|
|
animationRef.current = requestAnimationFrame(updatePitch);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (active) {
|
|
if (!analyzerRef.current) {
|
|
analyzerRef.current = new PitchAnalyzer();
|
|
}
|
|
|
|
analyzerRef.current.start()
|
|
.then(() => {
|
|
setIsListening(true);
|
|
updatePitch();
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
setError("Could not access microphone.");
|
|
setIsListening(false);
|
|
});
|
|
} else {
|
|
if (analyzerRef.current) {
|
|
analyzerRef.current.stop();
|
|
analyzerRef.current = null;
|
|
}
|
|
setIsListening(false);
|
|
if (animationRef.current) cancelAnimationFrame(animationRef.current);
|
|
}
|
|
|
|
return () => {
|
|
if (animationRef.current) cancelAnimationFrame(animationRef.current);
|
|
if (analyzerRef.current) {
|
|
analyzerRef.current.stop();
|
|
}
|
|
};
|
|
}, [active, updatePitch]);
|
|
|
|
return { pitchData, isListening, error };
|
|
}
|