/** * Unified target-note controls, shared by all game modes (v2 controls). * * - Key filter: restrict target notes to a tonic+mode (independent of the * displayed key signature on the staff — optional sync checkbox). * - Strings: pick which strings contribute target notes (fretted instruments). * - Fret window: restrict target notes to a fret range (fretted instruments). * * In Phrase Mode the same controls bind to the shared practice filter * (mirrored into phrase settings), so state stays consistent across modes. */ import { useMemo } from 'react'; import { Key, Guitar, Music, SlidersHorizontal } from 'lucide-react'; import { useSettings } from '../context/useSettings'; import { getPracticeFilter, type PracticeFilter } from '../types/SettingsTypes'; import { MODE_LABELS, TONICS, isMode, keyFor, type ModeId } from '../music/scales'; import { isFrettedInstrument } from '../music/playableRange'; import { TUNINGS } from '../music/Tunings'; /** Conventional string names, low→high, for guitar/bass tunings. */ const GUITAR_STRING_NAMES = ['E6', 'A5', 'D4', 'G3', 'B2', 'E1']; const BASS_STRING_NAMES = ['E4', 'A3', 'D2', 'G1']; function stringNames(tuningId: string, count: number): string[] { const names = tuningId.startsWith('bass') ? BASS_STRING_NAMES : GUITAR_STRING_NAMES; // Align from the low end (index 0 = lowest) if (count === names.length) return names; return Array.from({ length: count }, (_, i) => { const midi = TUNINGS[tuningId]?.strings[i] ?? 0; const pc = midi % 12; const LETTERS = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']; return LETTERS[pc] ?? `S${i + 1}`; }); } /** Display key signature names that correspond to a tonic+mode pair. */ const MAJOR_TONIC_TO_SIGNATURE: Record = { 'C': 'C', 'G': 'G', 'D': 'D', 'A': 'A', 'E': 'E', 'B': 'C#', 'F#': 'C#', 'Db': 'Db', 'Ab': 'Ab', 'Eb': 'Eb', 'Bb': 'Bb', 'F': 'F', }; const MINOR_TONIC_TO_SIGNATURE: Record = { 'C': 'Eb', 'G': 'Bb', 'D': 'F', 'A': 'C', 'E': 'G', 'B': 'D', 'F#': 'A', 'Db': 'Eb', 'Ab': 'Bb', 'Eb': 'Gb', 'Bb': 'Db', 'F': 'Dm', }; export const TargetNoteControls: React.FC = () => { const { settings, updateSettings } = useSettings(); const pf = getPracticeFilter(settings); const setFilter = (updates: Partial) => { updateSettings(s => { const next: typeof s = { ...s, practice: { ...getPracticeFilter(s), ...updates } }; // Optional coupling: display key signature follows the target key. if (updates.keyTonic !== undefined || updates.keyMode !== undefined) { const tonic = updates.keyTonic ?? pf.keyTonic; const mode = updates.keyMode ?? pf.keyMode; if (s.keyFollowsTarget) { const table = mode === 'minor' ? MINOR_TONIC_TO_SIGNATURE : MAJOR_TONIC_TO_SIGNATURE; const sig = table[tonic]; if (sig) next.keySignature = sig; } } return next; }); }; const fretted = useMemo( () => isFrettedInstrument(settings.instrument) && !!TUNINGS[settings.tuningId], [settings.instrument, settings.tuningId], ); const stringCount = TUNINGS[settings.tuningId]?.strings.length ?? 0; const names = stringNames(settings.tuningId, stringCount); const toggleString = (index: number) => { const cur = pf.strings.length > 0 ? [...pf.strings] : Array.from({ length: stringCount }, (_, i) => i); const next = cur.includes(index) ? cur.filter(i => i !== index) : [...cur, index].sort((a, b) => a - b); // Deselecting all = back to "all strings" (empty array) setFilter({ strings: next.length === stringCount ? [] : next }); }; const allSelected = pf.strings.length === 0; const anyActive = pf.keyEnabled || pf.fretWindowEnabled || pf.strings.length > 0; return (
{/* Section header: unified with Tuner/Metronome style */}
{anyActive && ( )}
{/* Key filter */}
{pf.keyEnabled && ( <>
)}
{/* Strings (fretted instruments) */} {fretted && (
{!allSelected &&
{Array.from({ length: stringCount }, (_, i) => { const active = allSelected || pf.strings.includes(i); return ( ); })}
} {pf.strings.length > 0 && ( )}
)} {/* Fret window (fretted instruments) */} {fretted && (
{pf.fretWindowEnabled && (
Min setFilter({ fretMin: Math.max(0, Math.min(24, parseInt(e.target.value) || 0)) })} />
Max setFilter({ fretMax: Math.max(pf.fretMin, Math.min(24, parseInt(e.target.value) || 0)) })} />
)}
)}
); }; // Re-export keyFor for potential external use (avoids unused-import lint) void keyFor;