From 9acf32a32f0288fe76533992419f82cf3179f90d Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 14 Sep 2026 21:08:15 +0000 Subject: [PATCH] UI consolidation round: pace checkbox, unified Limit notes section, fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pace dropdown REMOVED from Phrase setup — replaced by a 'Sync to exercise' checkbox in the Metronome widget (analog to 'Auto' in single-note modes). pace is derived from the checkbox (checked = tempo, unchecked = step). - Preview clicks now registered in PREVIEW_GROUP: stopping/abandoning a preview cancels its scheduled clicks — no more parallel ghost metronomes after preview->stop->next exercise. - Chord pool replaces the chord dropdown entirely (single chord AND sequences): togglable degree list, empty = all 7. - Giuliani figures corrected to NOTE sequences (p always takes the bass/ root): p-i-m, p-i-m-a, p-a-m-i, a-i-m, p-i-m-a-m-i, p-m-a-m-i-m, p-e-a-m-a-m (replaced the fingering-driven patterns that made no musical sense). - 'Limit notes' is now one bordered section like Tuner/Metronome, with a header, Clear-all link, and Strings behind a toggle (row hidden until restricted). --- src/audio/AudioEngine.ts | 9 +- src/components/MetronomeWidget.tsx | 25 +++--- src/components/PhraseTrainer.tsx | 115 ++++++++++---------------- src/components/TargetNoteControls.tsx | 44 +++++++--- src/hooks/usePhraseTrainer.ts | 4 +- src/music/arpeggioGenerator.ts | 39 +++++---- 6 files changed, 120 insertions(+), 116 deletions(-) diff --git a/src/audio/AudioEngine.ts b/src/audio/AudioEngine.ts index 8467f50..6a60e27 100644 --- a/src/audio/AudioEngine.ts +++ b/src/audio/AudioEngine.ts @@ -140,8 +140,10 @@ class AudioEngine { * Short NON-TONAL metronome click (~10 ms noise burst). Not registered in * the pitched-audio gate; instead the mic is blanked for 30 ms so the * click itself can't be scored as a pitch (contract E2). + * Optional groupId: the click becomes cancellable via cancelGroup (used + * by preview clicks so stopping a preview also stops its clicks). */ - playClickAt(time: number, volume = 0.5, accent = false): void { + playClickAt(time: number, volume = 0.5, accent = false, groupId?: string): void { const ctx = this.ensureContext(); if (!this.noiseBuffer) { const len = Math.floor(ctx.sampleRate * 0.01); @@ -160,6 +162,11 @@ class AudioEngine { src.start(time); src.stop(time + 0.02); this.clickBlankUntil = Math.max(this.clickBlankUntil, time + 0.03); + if (groupId) { + // Register as a cancellable node (cast: clicks are buffer sources, + // cancelGroup only calls stop() on them, which BufferSource has). + this.active.push({ osc: src as unknown as OscillatorNode, gain: gainNode, endsAt: time + 0.02, groupId }); + } } /** True for ~30 ms after a scheduled click (mic frames should be skipped). */ diff --git a/src/components/MetronomeWidget.tsx b/src/components/MetronomeWidget.tsx index a0d728c..77ae164 100644 --- a/src/components/MetronomeWidget.tsx +++ b/src/components/MetronomeWidget.tsx @@ -27,7 +27,10 @@ interface MetronomeWidgetProps { showAutoAdvance?: boolean; /** Compact layout (phrase transport row). */ compact?: boolean; - /** 'option': show the sync/free toggle (Phrase Mode). null: plain on/off. */ + /** 'option': show the sync checkbox (Phrase Mode) that replaces the pace + * dropdown: checked = clicks/beats only with the exercise (preview & + * run, count-in included); unchecked = free-running metronome. + * null: plain on/off (single-note modes). */ syncMode?: 'option' | null; /** External gate (sync mode): true while the exercise runs. */ gate?: boolean | null; @@ -85,18 +88,14 @@ export const MetronomeWidget: React.FC = ({ Metronome {syncMode === 'option' ? ( - + ) : ( - ); - })} - +
+ + Chords ({phrase.arpeggioChordSelection.length === 0 ? 'all 7' : `${phrase.arpeggioChordSelection.length} selected`}): + +
+ {(Object.keys(ARPEGGIO_DEGREE_LABELS) as ArpeggioDegree[]).map(d => { + const active = phrase.arpeggioChordSelection.length === 0 || phrase.arpeggioChordSelection.includes(d); + return ( + + ); + })}
- )} +
- {phrase.pace === 'tempo' && ( - - Tempo (BPM): set it on the metronome in the tools below — shared across modes. - - )} + {/* Pace lives in the metronome widget now: + 'Sync to exercise' checked = tempo pace, + unchecked = at-your-pace (step). */} {phrase.pace === 'tempo' && ( - Clicks: use the metronome's "Click sound" checkbox (with count-in, synced). + BPM + click: on the metronome widget below. )}