import { useRef, useEffect, useState } from 'react' const SETTINGS = [ { section: 'Chord Detection', items: [ { key: 'chromaSmooth', label: 'Chroma Smoothing', min: 1, max: 20, step: 1, desc: 'Frames to average for chord chroma. More = smoother but slower to react to chord changes.', }, { key: 'chordVoteThreshold', label: 'Chord Vote Threshold', min: 1, max: 8, step: 1, desc: 'Consecutive identical detections required to confirm a chord. Higher = more stable, slower.', }, { key: 'chordMinScore', label: 'Chord Min Score', min: 0.10, max: 0.80, step: 0.01, desc: 'Minimum coverage score to accept a chord match. Lower = more chord types detected (may add false positives).', }, ], }, { section: 'Key Detection', items: [ { key: 'noteHistorySize', label: 'Note History Size', min: 100, max: 12000, step: 100, desc: 'Pitch readings kept for key detection. ~2000 ≈ 1 min, 12000 ≈ whole session. Larger = more stable key.', }, { key: 'keyVoteWindow', label: 'Key Vote Window', min: 4, max: 30, step: 1, desc: 'Rolling window of key votes. Larger = more inertia — key changes need sustained evidence.', }, { key: 'keyVoteThreshold', label: 'Key Vote Threshold', min: 1, max: 30, step: 1, desc: 'Votes needed within the window to confirm a key. Higher = stricter consensus required.', }, { key: 'chordNoteBoost', label: 'Chord Note Boost', min: 0, max: 10, step: 1, desc: 'Times confirmed chord tones are injected into note history. Higher = chords dominate over transient melody notes.', }, ], }, { section: 'Audio Input', items: [ { key: 'minClarity', label: 'Min Pitch Clarity', min: 0.50, max: 0.99, step: 0.01, desc: 'Autocorrelation clarity threshold to accept a pitch reading. Higher = only clean, in-tune notes count.', }, { key: 'minVolume', label: 'Min Volume (RMS)', min: 0.001, max: 0.05, step: 0.001, desc: 'Minimum signal level before processing. Increase to cut through room noise.', }, ], }, ] export default function Settings({ config, onChange, onClose, onReset, monoColor, onMonoColorChange }) { // Snapshot on mount so Cancel can restore const savedConfig = useRef(config) const savedMono = useRef(monoColor) function handleCancel() { Object.entries(savedConfig.current).forEach(([k, v]) => onChange(k, v)) onMonoColorChange(savedMono.current) onClose() } function DeviceSelector({ config, onChange }) { const [devices, setDevices] = useState([]) async function refresh() { try { if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return const list = await navigator.mediaDevices.enumerateDevices() setDevices(list.filter(d => d.kind === 'audioinput')) } catch (e) { console.warn('enumerateDevices failed', e) } } useEffect(() => { refresh() }, []) return (
If device labels are empty, grant microphone permission first and hit Refresh.
) } return (

Detection Settings

Tune chord and key recognition sensitivity in real time

{/* ── Display ── */}

Display

Mono Color Mode

Use a single purple palette instead of purple + amber for note tiers.

{SETTINGS.map(section => (

{section.section}

{section.items.map(item => (
{Number.isInteger(config[item.key]) ? config[item.key] : config[item.key].toFixed(item.step < 0.01 ? 3 : 2)}
{ const val = item.step < 1 ? parseFloat(e.target.value) : parseInt(e.target.value, 10) onChange(item.key, val) }} className="w-full accent-purple-500" />
{item.min} {item.desc} {item.max}
))}
))} {/* Audio device selector */}

Microphone

) }