- color-scheme per theme so native controls (selects, checkboxes, number
inputs, sliders) render dark-native instead of black-on-black.
- Themed .control-select text color; new .control-input class replaces
hardcoded white-on-black inline input styles.
- Tuner widget stays mounted when enabled: idle state (dimmed, needle parked,
'—') instead of appearing/disappearing. Fully theme-var based (was
hardcoded white — invisible in light mode).
- Remove CSS transitions on per-frame values (tuner needle, level meter):
transitions interrupted 60x/sec are left stuck by WebKit/Safari; the
display EMA from the detection loop replaces them.
- VexFlow notation: resolve CSS var() to concrete colors (SVG attributes
can't resolve var(), silently fell back to black) and override VexFlow's
hardcoded #444 ledger-line default; re-render on theme changes
(useLayoutEffect so children read the updated attribute in time).
- Themed switch toggles (were hardcoded #ddd / white — glowed in dark mode).
- Fixed-height pitch readout so the layout stops jumping when a note starts
or stops sounding.
- Split SettingsContext into context/Provider/hook files (fast-refresh);
memoized provider functions; typed webkitAudioContext; prefer-const;
removed 'as any' casts. ESLint now passes clean.
- 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).