mic sensitivity setting

This commit is contained in:
2025-12-27 18:43:19 +01:00
parent 2ad1fe0ea9
commit 03a5e8e3c1
5 changed files with 68 additions and 5 deletions

View File

@@ -26,7 +26,6 @@ const NOTE_MATCH_THRESHOLD_MS = 300; // How long to convert hold note to confirm
function App() { function App() {
const [listening, setListening] = useState(false); const [listening, setListening] = useState(false);
const { pitchData, error } = usePitchDetector(listening);
const { playNote } = useAudioPlayer(); const { playNote } = useAudioPlayer();
const [targetMidi, setTargetMidi] = useState<number>(60); // Start with C4 const [targetMidi, setTargetMidi] = useState<number>(60); // Start with C4
@@ -55,9 +54,12 @@ function App() {
autoPlayVolume: 0.5, autoPlayVolume: 0.5,
virtualGuitarVolume: 0.5, virtualGuitarVolume: 0.5,
virtualGuitarMute: false, virtualGuitarMute: false,
micSensitivity: 0.5,
disableAnimation: false disableAnimation: false
}); });
const { pitchData, error } = usePitchDetector(listening, settings.micSensitivity);
const [matchStartTime, setMatchStartTime] = useState<number | null>(null); const [matchStartTime, setMatchStartTime] = useState<number | null>(null);
const [feedbackMessage, setFeedbackMessage] = useState<string>(""); const [feedbackMessage, setFeedbackMessage] = useState<string>("");
const [revealed, setRevealed] = useState(false); const [revealed, setRevealed] = useState(false);

View File

@@ -13,6 +13,28 @@ export class PitchAnalyzer {
this.buffer = new Float32Array(2048); // Standard size 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<void> { async start(): Promise<void> {
if (this.audioContext) return; if (this.audioContext) return;
@@ -58,12 +80,13 @@ export class PitchAnalyzer {
} }
rms = Math.sqrt(rms / this.buffer.length); 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); const pitch = this.detector(this.buffer);
// Widen range for Bass (E1 ~41Hz) and Whistle (C8 ~4186Hz) // Widen range for Bass (E1 ~41Hz, but A0 is 27.5Hz) and Whistle (C8 ~4186Hz, Harmonics go higher)
if (pitch && (pitch < 30 || pitch > 5000)) return null; // Range 25Hz - 8000Hz covers Piano A0 to well above highest fundamental
if (pitch && (pitch < 25 || pitch > 8000)) return null;
return pitch; return pitch;
} }

View File

@@ -32,6 +32,7 @@ export interface AppSettings {
autoPlaySightReading?: boolean; autoPlaySightReading?: boolean;
autoPlayVolume?: number; autoPlayVolume?: number;
virtualGuitarVolume?: number; virtualGuitarVolume?: number;
micSensitivity?: number; // 0.0 to 1.0 (0=least sensitive, 1=most)
virtualGuitarMute?: boolean; virtualGuitarMute?: boolean;
disableAnimation?: boolean; disableAnimation?: boolean;
} }

View File

@@ -111,6 +111,31 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, s
? "Automatically plays the note audio (Required for Ear Training)." ? "Automatically plays the note audio (Required for Ear Training)."
: "Automatically plays the note audio when a new note appears."} : "Automatically plays the note audio when a new note appears."}
</p> </p>
<hr style={{ borderColor: 'rgba(255,255,255,0.1)', margin: '24px 0' }} />
{/* Mic Sensitivity */}
<div className="control-group">
<label className="control-label" style={{ marginBottom: '12px', fontSize: '16px' }}>
<span>Microphone Sensitivity</span>
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '12px', opacity: 0.7, minWidth: '30px' }}>Low</span>
<input
type="range"
min="0"
max="1"
step="0.05"
value={settings.micSensitivity ?? 0.5}
onChange={(e) => onUpdateSettings({ ...settings, micSensitivity: parseFloat(e.target.value) })}
style={{ flex: 1 }}
/>
<span style={{ fontSize: '12px', opacity: 0.7, minWidth: '30px' }}>High</span>
</div>
<p style={{ fontSize: '12px', opacity: 0.7, margin: '8px 0 0 0' }}>
Adjust if notes are not detected (increase) or if background noise triggers notes (decrease).
</p>
</div>
</div> </div>
</div> </div>

View File

@@ -10,16 +10,27 @@ interface PitchData {
clarity: number; // Placeholder for now, maybe uses probability if YIN exposes it 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<PitchAnalyzer | null>(null); const analyzerRef = useRef<PitchAnalyzer | null>(null);
const [pitchData, setPitchData] = useState<PitchData | null>(null); const [pitchData, setPitchData] = useState<PitchData | null>(null);
const [isListening, setIsListening] = useState(false); const [isListening, setIsListening] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const animationRef = useRef<number | null>(null); const animationRef = useRef<number | null>(null);
// Update sensitivity when it changes
useEffect(() => {
if (analyzerRef.current) {
analyzerRef.current.setSensitivity(sensitivity);
}
}, [sensitivity]);
const updatePitch = useCallback(() => { const updatePitch = useCallback(() => {
if (!analyzerRef.current) return; 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(); const freq = analyzerRef.current.getPitch();
if (freq) { if (freq) {
const midi = frequencyToMidi(freq); const midi = frequencyToMidi(freq);
@@ -46,6 +57,7 @@ export function usePitchDetector(active: boolean) {
if (active) { if (active) {
if (!analyzerRef.current) { if (!analyzerRef.current) {
analyzerRef.current = new PitchAnalyzer(); analyzerRef.current = new PitchAnalyzer();
analyzerRef.current.setSensitivity(sensitivity);
} }
analyzerRef.current.start() analyzerRef.current.start()