mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
mic sensitivity setting
This commit is contained in:
@@ -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<number>(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<number | null>(null);
|
||||
const [feedbackMessage, setFeedbackMessage] = useState<string>("");
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 when a new note appears."}
|
||||
</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>
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
// 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()
|
||||
|
||||
Reference in New Issue
Block a user