diff --git a/src/App.tsx b/src/App.tsx index 3e72345..6c80290 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,7 +26,6 @@ const NOTE_MATCH_THRESHOLD_MS = 300; // How long to convert hold note to confirm function App() { const [listening, setListening] = useState(false); - const { pitchData, error } = usePitchDetector(listening); const { playNote } = useAudioPlayer(); const [targetMidi, setTargetMidi] = useState(60); // Start with C4 @@ -55,9 +54,12 @@ function App() { autoPlayVolume: 0.5, virtualGuitarVolume: 0.5, virtualGuitarMute: false, + micSensitivity: 0.5, disableAnimation: false }); + const { pitchData, error } = usePitchDetector(listening, settings.micSensitivity); + const [matchStartTime, setMatchStartTime] = useState(null); const [feedbackMessage, setFeedbackMessage] = useState(""); const [revealed, setRevealed] = useState(false); diff --git a/src/audio/PitchAnalyzer.ts b/src/audio/PitchAnalyzer.ts index f8ed40b..2a80dda 100644 --- a/src/audio/PitchAnalyzer.ts +++ b/src/audio/PitchAnalyzer.ts @@ -13,6 +13,28 @@ export class PitchAnalyzer { this.buffer = new Float32Array(2048); // Standard size } + private sensitivityThreshold = 0.03; // Default + + /** + * Set sensitivity from 0.0 (least sensitive) to 1.0 (most sensitive). + * Maps to RMS threshold: + * 0.0 -> 0.1 (Requires loud input) + * 0.5 -> 0.03 (Default) + * 1.0 -> 0.005 (Very sensitive) + */ + setSensitivity(value: number) { + // Clamp value 0-1 + const v = Math.max(0, Math.min(1, value)); + + // Linear interpolation or something that feels right + // Let's do a simple mapping: + // 1.0 -> 0.002 + // 0.0 -> 0.1 + // linear: 0.1 - (0.098 * v) roughly + + this.sensitivityThreshold = 0.1 - (0.095 * v); + } + async start(): Promise { if (this.audioContext) return; @@ -58,12 +80,13 @@ export class PitchAnalyzer { } rms = Math.sqrt(rms / this.buffer.length); - if (rms < 0.05) return null; // Silence threshold increased to 0.05 for robustness because 0.01 picked up noise + if (rms < this.sensitivityThreshold) return null; const pitch = this.detector(this.buffer); - // Widen range for Bass (E1 ~41Hz) and Whistle (C8 ~4186Hz) - if (pitch && (pitch < 30 || pitch > 5000)) return null; + // Widen range for Bass (E1 ~41Hz, but A0 is 27.5Hz) and Whistle (C8 ~4186Hz, Harmonics go higher) + // Range 25Hz - 8000Hz covers Piano A0 to well above highest fundamental + if (pitch && (pitch < 25 || pitch > 8000)) return null; return pitch; } diff --git a/src/components/Controls.tsx b/src/components/Controls.tsx index 0c9b182..c0b5a49 100644 --- a/src/components/Controls.tsx +++ b/src/components/Controls.tsx @@ -32,6 +32,7 @@ export interface AppSettings { autoPlaySightReading?: boolean; autoPlayVolume?: number; virtualGuitarVolume?: number; + micSensitivity?: number; // 0.0 to 1.0 (0=least sensitive, 1=most) virtualGuitarMute?: boolean; disableAnimation?: boolean; } diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 1aadf7d..147d387 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -111,6 +111,31 @@ export const SettingsModal: React.FC = ({ isOpen, onClose, s ? "Automatically plays the note audio (Required for Ear Training)." : "Automatically plays the note audio when a new note appears."}

+ +
+ + {/* Mic Sensitivity */} +
+ +
+ Low + onUpdateSettings({ ...settings, micSensitivity: parseFloat(e.target.value) })} + style={{ flex: 1 }} + /> + High +
+

+ Adjust if notes are not detected (increase) or if background noise triggers notes (decrease). +

+
diff --git a/src/hooks/usePitchDetector.ts b/src/hooks/usePitchDetector.ts index c798005..ba1e65f 100644 --- a/src/hooks/usePitchDetector.ts +++ b/src/hooks/usePitchDetector.ts @@ -10,16 +10,27 @@ interface PitchData { clarity: number; // Placeholder for now, maybe uses probability if YIN exposes it } -export function usePitchDetector(active: boolean) { +export function usePitchDetector(active: boolean, sensitivity: number = 0.5) { const analyzerRef = useRef(null); const [pitchData, setPitchData] = useState(null); const [isListening, setIsListening] = useState(false); const [error, setError] = useState(null); const animationRef = useRef(null); + // Update sensitivity when it changes + useEffect(() => { + if (analyzerRef.current) { + analyzerRef.current.setSensitivity(sensitivity); + } + }, [sensitivity]); + const updatePitch = useCallback(() => { if (!analyzerRef.current) return; + // Ensure sensitivity is set on start + // (Though the effect above handles updates, safely re-asserting here doesn't hurt, + // but let's trust the effect and the init sequence). + const freq = analyzerRef.current.getPitch(); if (freq) { const midi = frequencyToMidi(freq); @@ -46,6 +57,7 @@ export function usePitchDetector(active: boolean) { if (active) { if (!analyzerRef.current) { analyzerRef.current = new PitchAnalyzer(); + analyzerRef.current.setSensitivity(sensitivity); } analyzerRef.current.start()