Files
sheetmusictrainer/src/AppConfig.ts
Jens Mohrmann 079d574b66 Overhaul pitch detection and note matching
- Pick detector per instrument range: ACF2PLUS (robust down to ~40 Hz) for
  normal instruments, YIN only for the whistle register. pitchfinder's YIN
  latches onto tau=2 (~20 kHz garbage) below ~100 Hz and for quiet input;
  the old fixed 4096 window masked this, the smaller window exposed it.
- RMS-normalize analysis input for the YIN path only; ACF2PLUS keeps the raw
  signal so its internal unpitched gate rejects quiet decayed audio instead
  of amplifying noise into confident wrong pitches (tuner stuck on 'a note').
- Adaptive analysis window sized to the instrument's lowest note (~2 periods):
  half the latency, a quarter of the CPU for guitar/piano/voice; bass keeps
  the full window for E1.
- Reject detections implausibly below the instrument floor (mains hum).
- Clear stale pitch readings after 200 ms instead of keeping them forever.
- Shared AudioEngine singleton for all playback (was: two AudioContexts);
  tracks audibility so the app stops hearing its own speaker (ear-training
  self-scoring) and gates matching plus display during playback.
- MatchTracker: grace-period based note hold instead of reset-on-every-flicker
  (YIN flickers during attacks/decays; the old hard reset made the 50 ms
  threshold fire unreliably).
- Display damping in the producer (EMA for cents/level, 3-frame note-name
  hysteresis); display components render directly without CSS transitions.
- Regression tests: synthetic signals through the exact pipeline helpers
  (MatchTracker + pitchDetection test suites).
2026-09-13 18:52:21 +02:00

89 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Global application configuration
*/
// How long (in ms) the target note must be detected to count as correct.
// Protects against spurious single-frame detections.
export const NOTE_MATCH_THRESHOLD_MS = 50;
// Grace period (in ms): brief detection dropouts or wrong frames while holding
// a note do NOT reset the match timer. Pitch detectors (YIN) routinely flicker
// for a frame or two during note attacks and decays; a hard reset on every
// flicker makes the threshold above feel broken ("shows green but won't register").
export const NOTE_MATCH_GRACE_MS = 150;
// How long (in ms) after speaker playback ends that mic-based matching stays
// gated, so the app doesn't "hear itself" (ear-training auto-play, Play Note
// button, virtual instruments).
export const PLAYBACK_GATE_TAIL_MS = 400;
// Safety cap for the "wait for silence" option: if the mic never goes quiet
// (continuously detectable sound in the room), the next reference note plays
// anyway after this delay so the app can't stall indefinitely.
export const REFERENCE_QUIET_MAX_WAIT_MS = 8000;
// Display smoothing (exponential moving average factor, 0-1) applied to the
// cents reading and the audio level in the detection loop (~60 fps). Keeps
// the tuner needle and level meter readable without CSS transitions — a
// transition on a value that changes every frame is interrupted 60x per
// second, which Safari/WebKit handles by leaving the element stuck.
// 0.25 ≈ 80ms effective smoothing: damps jitter but stays very responsive.
export const DISPLAY_EMA_FACTOR = 0.25;
// How many consecutive detection frames a different note must persist before
// the displayed note name switches (~50ms at 60fps). Prevents the note name
// from flickering between adjacent notes when playing right at a semitone
// boundary. Matching is unaffected (it uses the raw midi value).
export const NOTE_DISPLAY_STABLE_FRAMES = 3;
// How long (in ms) the last detected pitch is kept on screen after the signal
// disappears, before the readout is cleared. Prevents flicker between frames
// without leaving stale notes hanging forever.
export const PITCH_READING_HOLD_MS = 200;
// Pitch analysis window (samples @ AudioContext rate).
// The analyzer adapts the window down from BUFFER_SIZE based on the lowest
// frequency that must be detected (~2 periods minimum). Smaller window =
// lower latency and 4x less CPU per analysis. 4096 covers bass E1 (~41 Hz).
export const PITCH_BUFFER_SIZE = 4096;
export const PITCH_MIN_WINDOW = 1024;
// Fallback for the lowest detectable frequency before the app configures
// the actual instrument range. ~70 Hz covers guitar low E with margin.
export const PITCH_DEFAULT_LOWEST_HZ = 70;
// Instruments whose lowest note is at/above this frequency use YIN instead of
// ACF2PLUS: ACF2PLUS picks subharmonics above ~2 kHz, while YIN's weakness
// below ~100 Hz is irrelevant for high registers (e.g. whistle).
export const PITCH_HIGH_REGISTER_HZ = 300;
// Target RMS for normalizing the analysis buffer before pitch detection
// (YIN/high-register path only — see selectDetector in PitchAnalyzer).
// pitchfinder's YIN force-sets yinBuffer[1] = 1 before normalization, so at
// low absolute amplitudes its difference function gets dwarfed and it latches
// onto tau=2 (~20 kHz garbage). A constant operating level fixes this.
export const PITCH_NORMALIZE_TARGET_RMS = 0.3;
// Detected pitches below (instrument floor × this factor) are rejected as
// implausible — e.g. 50/60 Hz mains hum showing up as a confident wrong note
// while a guitar (floor ~78 Hz) is not playing. ~4 semitones of margin.
export const PITCH_BELOW_FLOOR_REJECT_FACTOR = 2 ** (-4 / 12); // ≈ 0.794
// 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,
};