Metronome/phrase click unification: run-driven beats, gated widget

- Phrase Mode (sync to metronome): the run's scheduler is the single click
  source (with count-in); the metronome widget is muted and its pendulum
  mirrors the run's beats via the new phraseBeatBus (audio-timestamp
  scheduled). No more double clicks; clicks only while the run is active
  (phraseRunBus gates the widget's own ticking).
- Schedule entries now carry continuous beat index + accent flag (downbeats
  accented, count-in beat 1 accented).
- 'At your pace': optional click via the metronome toggle (kept explicit).
- Metronome widget: gate prop; in gated mode it listens to the beat bus
  instead of running its own scheduler.
This commit is contained in:
OpenClaw
2026-09-14 16:33:39 +00:00
parent 57a6a654a0
commit 2ed1a88184
7 changed files with 108 additions and 14 deletions

View File

@@ -1,3 +1,4 @@
import React, { useEffect, useState } from 'react';
import { Guitar, Music, Gauge } from 'lucide-react';
import { TUNINGS, INSTRUMENT_TUNINGS } from '../music/Tunings';
import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs';
@@ -5,7 +6,7 @@ import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs';
import { TuningMeter } from './TuningMeter';
import { TargetNoteControls } from './TargetNoteControls';
import { MetronomeWidget } from './MetronomeWidget';
import { phraseRunBus } from '../hooks/phraseRunBus';
import { useSettings } from '../context/useSettings';
import { type Difficulty, type RhythmSettings } from '../types/SettingsTypes';
@@ -19,6 +20,10 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
const { settings, updateSettings } = useSettings();
// Alias to minimize refactor, or just use updateSettings.
const onUpdateSettings = updateSettings;
// Phrase run gate: the metronome only ticks while the phrase trainer is
// counting in / playing (armed by the user toggle, fired by the run).
const [phraseRunActive, setPhraseRunActive] = useState(false);
useEffect(() => phraseRunBus.subscribe(setPhraseRunActive), []);
const handleTuningChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onUpdateSettings({ ...settings, tuningId: e.target.value });
};
@@ -176,6 +181,7 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
rhythm={settings.rhythm}
onUpdate={updateRhythm}
showAutoAdvance={!inPhraseMode}
gate={inPhraseMode ? phraseRunActive : null}
/>
</div>