mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 01:24:33 +02:00
More detailed note detection settings
This commit is contained in:
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) */}
|
||||||
|
|||||||
@@ -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%' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user