From a965dde58512d4c0b38fbdda54da14030e720db4 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 14 Sep 2026 16:41:49 +0000 Subject: [PATCH] Click behavior per pace: continuous in step, run-synced (incl. preview) in tempo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step pace ('at your pace'): metronome ticks continuously when the user switches it on — phraseRunBus gate no longer applies (clock stays free). - Tempo pace ('sync to metronome'): gate now covers preview too; preview schedules quarter-note clicks along with the notes (accented each bar) so the rhythm is audible while listening. --- src/hooks/usePhraseTrainer.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/hooks/usePhraseTrainer.ts b/src/hooks/usePhraseTrainer.ts index 6f59168..c25ea2c 100644 --- a/src/hooks/usePhraseTrainer.ts +++ b/src/hooks/usePhraseTrainer.ts @@ -120,9 +120,12 @@ export function usePhraseTrainer( const setPhaseBoth = useCallback((p: PhrasePhase) => { phaseRef.current = p; setPhase(p); - // Expose run state so the shared metronome can gate its click/pendulum: - // only running during count-in and playing, never idle/preview/paused. - phraseRunBus.set(p === 'countIn' || p === 'playing'); + // Gate the shared metronome only in 'sync to metronome' (tempo) pace: + // there the run owns the clock (clicks during count-in/playing only). + // In 'at your pace' (step) the metronome ticks continuously when the + // user has it switched on — like the single-note modes. + const ownsClock = paceRef.current === 'tempo'; + phraseRunBus.set(ownsClock && (p === 'countIn' || p === 'playing' || p === 'preview')); }, []); useEffect(() => { paceRef.current = config.pace; }, [config.pace]); @@ -493,6 +496,15 @@ export function usePhraseTrainer( if (!e.pitch) continue; audioEngine.scheduleNote(e.pitch.midi, t0 + (e.startTick / PPQ) * spb, (e.durationTicks / PPQ) * spb, vol, PREVIEW_GROUP); } + // Tempo pace: click along with the preview so the rhythm is + // audible while listening (quarter beats over the full span). + if (paceRef.current === 'tempo' && configRef.current.clickSound) { + const totalTicks = events.reduce((a, e) => Math.max(a, e.startTick + e.durationTicks), 0); + const beats = Math.floor(totalTicks / PPQ); + for (let i = 0; i < beats; i++) { + audioEngine.playClickAt(t0 + i * spb, CLICK_VOLUME, i % 4 === 0); + } + } setPhaseBoth('preview'); const totalSec = (events.reduce((a, e) => Math.max(a, e.startTick + e.durationTicks), 0) / PPQ) * spb; previewTimerRef.current = setTimeout(() => {