diff --git a/src/AppConfig.ts b/src/AppConfig.ts index 4f85e7a..48b0b25 100644 --- a/src/AppConfig.ts +++ b/src/AppConfig.ts @@ -3,4 +3,23 @@ */ // How long (in ms) a note must be held to be accepted as correct -export const NOTE_MATCH_THRESHOLD_MS = 100; +// How long (in ms) a note must be held to be accepted as correct +export const NOTE_MATCH_THRESHOLD_MS = 50; + +// Microphone Sensitivity Configuration +// Sensitivity Scale: 0.0 (Low) to 1.0 (High) +// Linear mapping to dB thresholds +export const MIC_SENSITIVITY_DB_RANGE = { + min: -10, // 0.0 Sensitivity: Loudest input required (e.g. loud singing/instrument) + max: -100 // 1.0 Sensitivity: Quietest input accepted (mic floor) +}; + +export const MIC_DEFAULT_SENSITIVITY = 0.75; // Bias towards high sensitivity for ease of use + +// Audio Constraints for getUserMedia +// Disabled processing is generally better for musical pitch detection +export const AUDIO_CONSTRAINTS = { + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false, +}; diff --git a/src/audio/PitchAnalyzer.ts b/src/audio/PitchAnalyzer.ts index 54c0dce..153f11b 100644 --- a/src/audio/PitchAnalyzer.ts +++ b/src/audio/PitchAnalyzer.ts @@ -1,4 +1,5 @@ import { YIN } from "pitchfinder"; +import { MIC_SENSITIVITY_DB_RANGE, AUDIO_CONSTRAINTS } from "../AppConfig"; export interface MicrophoneDebugInfo { audioContextState: AudioContextState | 'inactive'; @@ -34,17 +35,15 @@ export class PitchAnalyzer { /** * Set sensitivity from 0.0 (least sensitive) to 1.0 (most sensitive). - * Maps to approximate dB Thresholds: - * 0.0 -> -20 dB (0.1 RMS) - Requires loud input - * 1.0 -> -60 dB (0.001 RMS) - Very sensitive, picks up background noise + * Maps to approximate dB Thresholds based on AppConfig */ setSensitivity(value: number) { // Clamp value 0-1 const v = Math.max(0, Math.min(1, value)); - // Linear map to dB: -20dB to -60dB - // High sensitivity (1.0) = Lower Threshold (-60dB) - const db = -20 - (v * 40); + // Linear map to dB using Config Range + const { min, max } = MIC_SENSITIVITY_DB_RANGE; + const db = min + (v * (max - min)); // Convert dB to RMS amplitude this.sensitivityThreshold = Math.pow(10, db / 20); @@ -68,13 +67,9 @@ export class PitchAnalyzer { this.detector = YIN({ sampleRate: this.audioContext.sampleRate }); try { - // Use more explicit audio constraints for better Android compatibility + // Use configured audio constraints const constraints: MediaStreamConstraints = { - audio: { - echoCancellation: false, - noiseSuppression: false, - autoGainControl: false, - } + audio: AUDIO_CONSTRAINTS }; this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints); diff --git a/src/components/Fretboard.tsx b/src/components/Fretboard.tsx index e1103d8..dec4f1b 100644 --- a/src/components/Fretboard.tsx +++ b/src/components/Fretboard.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import type { Tuning, FretPosition } from '../music/Tunings'; +import { useState, useMemo } from 'react'; +import { getFretboardPositions, type Tuning, type FretPosition } from '../music/Tunings'; import { getNoteDetails } from '../music/NoteUtils'; interface FretboardProps { @@ -27,6 +27,14 @@ export function Fretboard({ }: FretboardProps) { const [hoverPos, setHoverPos] = useState<{ stringIndex: number, fret: number } | null>(null); + const alternatePositions = useMemo(() => { + if (!showHints || !hoverPos) return []; + const midi = tuning.strings[hoverPos.stringIndex] + hoverPos.fret; + return getFretboardPositions(midi, tuning, maxFrets).filter( + p => p.stringIndex !== hoverPos.stringIndex || p.fret !== hoverPos.fret + ); + }, [hoverPos, tuning, maxFrets, showHints]); + // Config const numStrings = tuning.strings.length; // Visual params @@ -199,12 +207,29 @@ export function Fretboard({ {/* Hover Highlight (Ghost Dot) */} {interactive && hoverPos && ( - (() => { - const cx = getNoteX(hoverPos.fret); - const cy = getStringY(hoverPos.stringIndex); + + {/* Alternate Positions */} + {alternatePositions.map((p, idx) => { + const cx = getNoteX(p.fret); + const cy = getStringY(p.stringIndex); + return ( + + ); + })} - return ( - + {/* Main Hover Selection */} + {(() => { + const cx = getNoteX(hoverPos.fret); + const cy = getStringY(hoverPos.stringIndex); + return ( - - ); - })() + ); + })()} + )} {/* Interaction Overlay (Invisible hit targets) */} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 2db45cc..9c52e21 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { X, Volume2, VolumeX, ChevronDown, ChevronUp, Mic, MicOff, Sun, Moon, Monitor } from 'lucide-react'; import type { AppSettings } from '../types/SettingsTypes'; +import { MIC_SENSITIVITY_DB_RANGE, MIC_DEFAULT_SENSITIVITY } from '../AppConfig'; import type { MicrophoneDebugInfo } from '../hooks/usePitchDetector'; interface SettingsModalProps { @@ -18,9 +19,14 @@ interface SettingsModalProps { const LevelMeter: React.FC<{ level: number; isActive: boolean }> = ({ level, isActive }) => { // Convert RMS to dB for display, then normalize to 0-100% // RMS of 0.001 = -60dB, RMS of 1.0 = 0dB - const db = level > 0 ? 20 * Math.log10(level) : -100; - // Map -60dB to 0dB => 0% to 100% - const percentage = Math.max(0, Math.min(100, ((db + 60) / 60) * 100)); + const db = level > 0 ? 20 * Math.log10(level) : -120; + + // Map full range from -100dB to 0dB => 0% to 100% + // This ensures even very quiet inputs (-80 or -90dB) show on the meter + const minDb = MIC_SENSITIVITY_DB_RANGE.max; // e.g. -100 + const maxDb = 0; // 0dB is full scale + + const percentage = Math.max(0, Math.min(100, ((db - minDb) / (maxDb - minDb)) * 100)); return (
= ({ level, isA }} />
- {isActive ? `${Math.round(db)} dB` : '— dB'} + {isActive && db > -120 ? `${Math.round(db)} dB` : '— dB'} ); @@ -236,18 +242,23 @@ export const SettingsModal: React.FC = ({
- Low (-20dB) + Low ({MIC_SENSITIVITY_DB_RANGE.min}dB) - {Math.round(-20 - ((settings.micSensitivity ?? 0.5) * 40))} dB + {/* Display current estimated threshold in dB */} + {Math.round( + MIC_SENSITIVITY_DB_RANGE.min + + ((settings.micSensitivity ?? MIC_DEFAULT_SENSITIVITY) * + (MIC_SENSITIVITY_DB_RANGE.max - MIC_SENSITIVITY_DB_RANGE.min)) + )} dB - High (-60dB) + High ({MIC_SENSITIVITY_DB_RANGE.max}dB)
onUpdateSettings({ ...settings, micSensitivity: parseFloat(e.target.value) })} style={{ width: '100%' }} /> diff --git a/src/hooks/usePitchDetector.ts b/src/hooks/usePitchDetector.ts index 969542e..7c31b5e 100644 --- a/src/hooks/usePitchDetector.ts +++ b/src/hooks/usePitchDetector.ts @@ -10,9 +10,11 @@ interface PitchData { clarity: number; // Placeholder for now, maybe uses probability if YIN exposes it } +import { MIC_DEFAULT_SENSITIVITY } from '../AppConfig'; + export type { MicrophoneDebugInfo }; -export function usePitchDetector(active: boolean, sensitivity: number = 0.5) { +export function usePitchDetector(active: boolean, sensitivity: number = MIC_DEFAULT_SENSITIVITY) { const analyzerRef = useRef(null); const [pitchData, setPitchData] = useState(null); const [isListening, setIsListening] = useState(false);