From 9dc95e3787861fa98c98ab816610be193be21d1c Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 14 Sep 2026 19:20:15 +0000 Subject: [PATCH] Metronome always-visible controls, Giuliani patterns, pool fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Metronome widget: BPM/slider row and pendulum always visible (dimmed when off); the toggle gates sound + animation only. BPM/Timer mode switch restored for single-note auto-advance. - Giuliani right-hand figures as arpeggio patterns: p-i-m, p-i-m-a, p-a-m-i, m-a-m-i, a-m-i — cycle [root, third, fifth, octave] per figure with octave register shift per repetition (120-studies feel). - Pool fix: fretted instruments without active filters now get the full 0-12 fretboard in ALL modes (legacy difficulty ranges no longer clamp the pool since the Note Set dropdown was removed). - Tests 174/174. --- src/components/MetronomeWidget.tsx | 171 ++++++++++++++++++----------- src/components/PhraseTrainer.tsx | 50 ++++----- src/music/arpeggioGenerator.ts | 54 ++++++++- src/music/targetPool.ts | 7 +- 4 files changed, 181 insertions(+), 101 deletions(-) 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( + + + + + + +