diff --git a/src/components/MetronomeWidget.tsx b/src/components/MetronomeWidget.tsx index b4cd0e9..cdb7029 100644 --- a/src/components/MetronomeWidget.tsx +++ b/src/components/MetronomeWidget.tsx @@ -87,76 +87,87 @@ export const MetronomeWidget: React.FC = ({ - {rhythm.active && ( -
- {/* Linear pendulum — same element, left transitions each beat */} + {/* Always visible: pendulum + BPM; the toggle gates sound + animation */} +
+ {/* Linear pendulum — same element, left transitions each beat */} +
-
-
+ /> +
- {rhythm.mode === 'bpm' || compact ? ( -
-
- - onUpdate({ bpm: Math.max(1, parseInt(e.target.value) || 60) })} - className="control-input" - style={{ width: '48px', textAlign: 'center', padding: '2px' }} - /> - - onUpdate({ bpm: Number(e.target.value) })} - style={{ flex: 1 }} - /> -
-
- {getTempoMarking(rhythm.bpm)} -
+
+ {!compact && ( +
+ +
+ )} + {rhythm.mode === 'bpm' ? ( + <> + + onUpdate({ bpm: Math.max(1, parseInt(e.target.value) || 60) })} + className="control-input" + style={{ width: '48px', textAlign: 'center', padding: '2px' }} + /> + + onUpdate({ bpm: Number(e.target.value) })} + style={{ flex: 1 }} + /> + ) : ( -
+ <> {rhythm.seconds}s = ({ onChange={(e) => onUpdate({ seconds: Number(e.target.value) })} style={{ flex: 1 }} /> -
+ )} +
+ {rhythm.mode === 'bpm' && ( +
+ {getTempoMarking(rhythm.bpm)} +
+ )} + +
+ {showAutoAdvance ? ( + + ) : } + +
+
{showAutoAdvance ? ( @@ -189,9 +228,7 @@ export const MetronomeWidget: React.FC = ({ /> Sound -
- )}
); }; diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 49c16ab..538f76c 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -16,7 +16,7 @@ import { usePhraseTrainer, type PhraseTrainerApi, type PhrasePhase } from '../ho import { getPhraseSettings, getPracticeFilter, type PhraseSettings, type PracticeFilter } from '../types/SettingsTypes'; import { rawFrameCount } from '../hooks/rawFrameBus'; import { audioEngine } from '../audio/AudioEngine'; -import { computePlayableNotes, isFrettedInstrument, positionsWithinWindow, fretWindowNotes } from '../music/playableRange'; +import { computePlayableNotes, isFrettedInstrument, positionsWithinWindow } from '../music/playableRange'; import { generateMelody } from '../music/melodyGenerator'; import { generateScaleDrill } from '../music/scaleDrills'; import { generateArpeggio, type ArpeggioDegree } from '../music/arpeggioGenerator'; @@ -109,39 +109,22 @@ export const PhraseTrainer = forwardRef( // ---- Playable pool ---------------------------------------------------- const pool = useMemo(() => { - // Unified filter: strings ∩ fret window take precedence for fretted - // instruments (same semantics as the single-note modes). - if (fretted && currentTuning && (pf.strings.length > 0 || pf.fretWindowEnabled)) { + // Unified filter: strings ∩ fret window, computed identically to + // the single-note modes (full 0–12 fretboard when no filter is + // active — the legacy difficulty ranges no longer apply). + if (fretted && currentTuning) { const all = currentTuning.strings; const selected = pf.strings.length > 0 ? all.filter((_, i) => pf.strings.includes(i)) : all; - if (selected.length > 0) { - if (pf.fretWindowEnabled) { - const lo = Math.max(0, Math.min(24, pf.fretMin)); - const hi = Math.max(lo, Math.min(24, pf.fretMax)); - return selected.flatMap(open => { - const notes: number[] = []; - for (let fret = lo; fret <= hi; fret++) notes.push(open + fret); - return notes; - }).sort((a, b) => a - b); - } - // Strings only: use their full range - const maxFret = Math.max(...all.map(open => { - let f = 0; - while (open + f <= 127) f++; - return Math.min(f - 1, 24); - })); - return selected.flatMap(open => { - const notes: number[] = []; - for (let fret = 0; fret <= maxFret; fret++) notes.push(open + fret); - return notes; - }).sort((a, b) => a - b); - } - return []; - } - if (phrase.fretWindowEnabled && fretted && currentTuning) { - return fretWindowNotes(currentTuning, phrase.fretMin, phrase.fretMax); + if (selected.length === 0) return []; + const lo = pf.fretWindowEnabled ? Math.max(0, Math.min(24, pf.fretMin)) : 0; + const hi = pf.fretWindowEnabled ? Math.max(lo, Math.min(24, pf.fretMax)) : 12; + return selected.flatMap(open => { + const notes: number[] = []; + for (let fret = lo; fret <= hi; fret++) notes.push(open + fret); + return notes; + }).sort((a, b) => a - b); } return computePlayableNotes({ instrumentId: settings.instrument, @@ -640,6 +623,13 @@ export const PhraseTrainer = forwardRef( + + + + + + +