From 71271387ab554630eebcd96b2acc72c30dbe2f75 Mon Sep 17 00:00:00 2001 From: Jens Mohrmann Date: Sun, 20 Sep 2026 00:27:59 +0200 Subject: [PATCH] Fix double metronome click in single-note modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useGameLogic's own scheduler and the footer widget's scheduler both played audible clicks while rhythm.active — assist->learn-notes switch made the second one mount. Now the widget is the single click source in non-zen modes; useGameLogic's scheduler is a silent tick source for auto-advance (full sound only in zen mode, where the widget isn't rendered). Also: widget honors Timer mode (60/seconds) like the trainer, and useMetronome skips audio nodes at volume 0. --- src/components/MetronomeWidget.tsx | 8 ++++++-- src/hooks/useGameLogic.ts | 6 +++++- src/hooks/useMetronome.ts | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/MetronomeWidget.tsx b/src/components/MetronomeWidget.tsx index e344866..ed57a6c 100644 --- a/src/components/MetronomeWidget.tsx +++ b/src/components/MetronomeWidget.tsx @@ -65,8 +65,12 @@ export const MetronomeWidget: React.FC = ({ // Free/plain: the widget's own scheduler runs whenever armed. const running = isSync ? runActive : armed; + // Timer mode = one beat per N seconds: effective BPM 60/seconds so + // clicks and pendulum match the trainer's auto-advance cadence. + const effectiveBpm = rhythm.mode === 'bpm' ? rhythm.bpm : 60 / Math.max(1, rhythm.seconds); + const { restart } = useMetronome({ - bpm: rhythm.bpm, + bpm: effectiveBpm, volume: rhythm.sound ? rhythm.volume : 0, playing: running && !isSync, onTick: handleTick, @@ -82,7 +86,7 @@ export const MetronomeWidget: React.FC = ({ }); }, [isSync, runActive]); - const beatMs = 60000 / Math.max(1, rhythm.bpm); + const beatMs = 60000 / Math.max(1, effectiveBpm); return (
diff --git a/src/hooks/useGameLogic.ts b/src/hooks/useGameLogic.ts index 75e27ba..bf74f2c 100644 --- a/src/hooks/useGameLogic.ts +++ b/src/hooks/useGameLogic.ts @@ -85,7 +85,11 @@ export const useGameLogic = ( const { restart: restartMetronome } = useMetronome({ bpm: effectiveBpm, - volume: settings.rhythm.sound ? settings.rhythm.volume : 0, + // Audible clicks come from the shared footer widget (Controls) in + // non-zen single-note modes — this scheduler is only the tick source + // for auto-advance. Sounding it here too doubles every beat. In zen + // mode the widget isn't rendered, so this scheduler IS the click. + volume: settings.zenMode && settings.rhythm.sound ? settings.rhythm.volume : 0, playing: settings.rhythm.active, onTick: handleTick }); diff --git a/src/hooks/useMetronome.ts b/src/hooks/useMetronome.ts index 72a8240..b3304d7 100644 --- a/src/hooks/useMetronome.ts +++ b/src/hooks/useMetronome.ts @@ -34,6 +34,8 @@ export function useMetronome({ bpm, volume, playing, onTick }: MetronomeOptions) const playClick = useCallback((time: number) => { if (!audioContext.current) return; + onTickRef.current(currentBeat.current); + if (volume <= 0) return; // silent tick source — no audio nodes needed const osc = audioContext.current.createOscillator(); const gainNode = audioContext.current.createGain();