More detailed note detection settings

This commit is contained in:
2026-01-01 19:45:16 +01:00
parent a785d188e9
commit d8602e0bd8
5 changed files with 84 additions and 32 deletions

View File

@@ -3,4 +3,23 @@
*/ */
// How long (in ms) a note must be held to be accepted as correct // 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,
};

View File

@@ -1,4 +1,5 @@
import { YIN } from "pitchfinder"; import { YIN } from "pitchfinder";
import { MIC_SENSITIVITY_DB_RANGE, AUDIO_CONSTRAINTS } from "../AppConfig";
export interface MicrophoneDebugInfo { export interface MicrophoneDebugInfo {
audioContextState: AudioContextState | 'inactive'; audioContextState: AudioContextState | 'inactive';
@@ -34,17 +35,15 @@ export class PitchAnalyzer {
/** /**
* Set sensitivity from 0.0 (least sensitive) to 1.0 (most sensitive). * Set sensitivity from 0.0 (least sensitive) to 1.0 (most sensitive).
* Maps to approximate dB Thresholds: * Maps to approximate dB Thresholds based on AppConfig
* 0.0 -> -20 dB (0.1 RMS) - Requires loud input
* 1.0 -> -60 dB (0.001 RMS) - Very sensitive, picks up background noise
*/ */
setSensitivity(value: number) { setSensitivity(value: number) {
// Clamp value 0-1 // Clamp value 0-1
const v = Math.max(0, Math.min(1, value)); const v = Math.max(0, Math.min(1, value));
// Linear map to dB: -20dB to -60dB // Linear map to dB using Config Range
// High sensitivity (1.0) = Lower Threshold (-60dB) const { min, max } = MIC_SENSITIVITY_DB_RANGE;
const db = -20 - (v * 40); const db = min + (v * (max - min));
// Convert dB to RMS amplitude // Convert dB to RMS amplitude
this.sensitivityThreshold = Math.pow(10, db / 20); this.sensitivityThreshold = Math.pow(10, db / 20);
@@ -68,13 +67,9 @@ export class PitchAnalyzer {
this.detector = YIN({ sampleRate: this.audioContext.sampleRate }); this.detector = YIN({ sampleRate: this.audioContext.sampleRate });
try { try {
// Use more explicit audio constraints for better Android compatibility // Use configured audio constraints
const constraints: MediaStreamConstraints = { const constraints: MediaStreamConstraints = {
audio: { audio: AUDIO_CONSTRAINTS
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
}
}; };
this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints); this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState, useMemo } from 'react';
import type { Tuning, FretPosition } from '../music/Tunings'; import { getFretboardPositions, type Tuning, type FretPosition } from '../music/Tunings';
import { getNoteDetails } from '../music/NoteUtils'; import { getNoteDetails } from '../music/NoteUtils';
interface FretboardProps { interface FretboardProps {
@@ -27,6 +27,14 @@ export function Fretboard({
}: FretboardProps) { }: FretboardProps) {
const [hoverPos, setHoverPos] = useState<{ stringIndex: number, fret: number } | null>(null); 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 // Config
const numStrings = tuning.strings.length; const numStrings = tuning.strings.length;
// Visual params // Visual params
@@ -199,12 +207,29 @@ export function Fretboard({
{/* Hover Highlight (Ghost Dot) */} {/* Hover Highlight (Ghost Dot) */}
{interactive && hoverPos && ( {interactive && hoverPos && (
(() => { <g style={{ pointerEvents: 'none' }}>
const cx = getNoteX(hoverPos.fret); {/* Alternate Positions */}
const cy = getStringY(hoverPos.stringIndex); {alternatePositions.map((p, idx) => {
const cx = getNoteX(p.fret);
const cy = getStringY(p.stringIndex);
return (
<circle
key={`alt-${idx}`}
cx={cx}
cy={cy}
r={6}
fill="var(--color-primary)"
fillOpacity={0.4}
stroke="none"
/>
);
})}
return ( {/* Main Hover Selection */}
<g style={{ pointerEvents: 'none' }}> {(() => {
const cx = getNoteX(hoverPos.fret);
const cy = getStringY(hoverPos.stringIndex);
return (
<circle <circle
cx={cx} cx={cx}
cy={cy} cy={cy}
@@ -213,9 +238,9 @@ export function Fretboard({
stroke="#888" stroke="#888"
strokeWidth={2} strokeWidth={2}
/> />
</g> );
); })()}
})() </g>
)} )}
{/* Interaction Overlay (Invisible hit targets) */} {/* Interaction Overlay (Invisible hit targets) */}

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { X, Volume2, VolumeX, ChevronDown, ChevronUp, Mic, MicOff, Sun, Moon, Monitor } from 'lucide-react'; import { X, Volume2, VolumeX, ChevronDown, ChevronUp, Mic, MicOff, Sun, Moon, Monitor } from 'lucide-react';
import type { AppSettings } from '../types/SettingsTypes'; import type { AppSettings } from '../types/SettingsTypes';
import { MIC_SENSITIVITY_DB_RANGE, MIC_DEFAULT_SENSITIVITY } from '../AppConfig';
import type { MicrophoneDebugInfo } from '../hooks/usePitchDetector'; import type { MicrophoneDebugInfo } from '../hooks/usePitchDetector';
interface SettingsModalProps { interface SettingsModalProps {
@@ -18,9 +19,14 @@ interface SettingsModalProps {
const LevelMeter: React.FC<{ level: number; isActive: boolean }> = ({ level, isActive }) => { const LevelMeter: React.FC<{ level: number; isActive: boolean }> = ({ level, isActive }) => {
// Convert RMS to dB for display, then normalize to 0-100% // Convert RMS to dB for display, then normalize to 0-100%
// RMS of 0.001 = -60dB, RMS of 1.0 = 0dB // RMS of 0.001 = -60dB, RMS of 1.0 = 0dB
const db = level > 0 ? 20 * Math.log10(level) : -100; const db = level > 0 ? 20 * Math.log10(level) : -120;
// Map -60dB to 0dB => 0% to 100%
const percentage = Math.max(0, Math.min(100, ((db + 60) / 60) * 100)); // 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 ( return (
<div style={{ <div style={{
@@ -46,7 +52,7 @@ const LevelMeter: React.FC<{ level: number; isActive: boolean }> = ({ level, isA
}} /> }} />
</div> </div>
<span style={{ fontSize: '11px', opacity: 0.7, minWidth: '45px', textAlign: 'right' }}> <span style={{ fontSize: '11px', opacity: 0.7, minWidth: '45px', textAlign: 'right' }}>
{isActive ? `${Math.round(db)} dB` : '— dB'} {isActive && db > -120 ? `${Math.round(db)} dB` : '— dB'}
</span> </span>
</div> </div>
); );
@@ -236,18 +242,23 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
</label> </label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', opacity: 0.7 }}> <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', opacity: 0.7 }}>
<span>Low (-20dB)</span> <span>Low ({MIC_SENSITIVITY_DB_RANGE.min}dB)</span>
<span style={{ color: '#4cc9f0' }}> <span style={{ color: '#4cc9f0' }}>
{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
</span> </span>
<span>High (-60dB)</span> <span>High ({MIC_SENSITIVITY_DB_RANGE.max}dB)</span>
</div> </div>
<input <input
type="range" type="range"
min="0" min="0"
max="1" max="1"
step="0.05" step="0.05"
value={settings.micSensitivity ?? 0.5} value={settings.micSensitivity ?? MIC_DEFAULT_SENSITIVITY}
onChange={(e) => onUpdateSettings({ ...settings, micSensitivity: parseFloat(e.target.value) })} onChange={(e) => onUpdateSettings({ ...settings, micSensitivity: parseFloat(e.target.value) })}
style={{ width: '100%' }} style={{ width: '100%' }}
/> />

View File

@@ -10,9 +10,11 @@ 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
} }
import { MIC_DEFAULT_SENSITIVITY } from '../AppConfig';
export type { MicrophoneDebugInfo }; 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<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);