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. )}