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();