Metronome rewrite: single sound checkbox, clean click architecture

- MetronomeWidget rebuilt cleanly: ONE 'Click sound' checkbox (the earlier
  restructure left duplicated option rows behind — neither worked). Widget
  body always visible (dimmed when disarmed); toggle arms sound+pendulum.
- Click decision unified: 'sync to metronome' derives phrase clickSound
  from the widget's sound checkbox (no separate phrase click option);
  'at your pace' clicks when the metronome is armed.
- Chord selection: abwählbare Liste (arpeggioChordSelection) restricts
  progression chords; first bar draws a random chord FROM THE SELECTION
  (fixed first chord repeated the exercise identically).
- Scales: rhythm setting (quarters/eighths) shared with arpeggios;
  drill id includes rhythm to invalidate cached scores.
- Beam fix: register Beam via note.setBeam so VexFlow suppresses flags
  (eighths/16ths previously drew BOTH beam and flag).
This commit is contained in:
OpenClaw
2026-09-14 19:46:23 +00:00
parent 9dc95e3787
commit ffb94cc93f
6 changed files with 93 additions and 97 deletions

View File

@@ -82,13 +82,14 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
// (single source of truth across modes; the phrase-local BPM control
// was replaced by the common metronome widget in Controls).
phrase.bpm = settings.rhythm.bpm;
// "Sync to metronome": the phrase run IS the click source while it
// plays (scheduler-driven, with count-in) — the metronome widget only
// mirrors the beats visually and is muted in Phrase Mode, so clicks
// can never double. The separate-click checkbox stays as an explicit
// opt-in for a click even in "at your pace" mode.
const metronomeActive = settings.rhythm.active;
phrase.clickSound = phrase.pace === 'tempo' ? true : phrase.clickSound && metronomeActive;
// "Sync to metronome" (tempo): the phrase run owns the clock and the
// click decision comes from the METRONOME widget's sound checkbox —
// one place, no separate phrase click option. "At your pace" (step):
// the widget clicks freely when armed (metronomeActive); a legacy
// phrase.clickSound=true also allows clicks without the metronome.
phrase.clickSound = phrase.pace === 'tempo'
? settings.rhythm.sound
: settings.rhythm.active || phrase.clickSound;
const setPhrase = useCallback((updates: Partial<PhraseSettings>) => {
updateSettings(s => {
const next = { ...s, phrase: { ...getPhraseSettings(s), ...updates } };
@@ -183,6 +184,7 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
direction: phrase.scaleDirection,
coverage: phrase.scaleCoverage,
meter: { numerator: phrase.meterNumerator, denominator: 4 },
rhythm: phrase.scaleRhythm,
}, pool);
}
if (phrase.material === 'arpeggio') {
@@ -210,7 +212,7 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
}
return null;
}, [phrase.material, phrase.keyTonic, phrase.keyMode, phrase.bars, phrase.meterNumerator,
phrase.rhythmLevel, phrase.scaleDirection, phrase.scaleCoverage, phrase.libraryId,
phrase.rhythmLevel, phrase.scaleDirection, phrase.scaleCoverage, phrase.scaleRhythm, phrase.libraryId,
phrase.arpeggioDegree, phrase.arpeggioPattern, phrase.arpeggioCoverage,
phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioRhythm, melodySeed,
melodySeed, pool, importedScore]);
@@ -756,19 +758,9 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
</label>
{phrase.pace === 'tempo' && (
<span style={{ fontSize: '11px', opacity: 0.7, alignSelf: 'center' }}>
Clicks come from the run (with count-in); the metronome widget mirrors the beats.
Clicks: use the metronome's "Click sound" checkbox (with count-in, synced).
</span>
)}
{phrase.pace === 'step' && (
<label className="phrase-check">
<input
type="checkbox"
checked={phrase.clickSound}
onChange={e => setPhrase({ clickSound: e.target.checked })}
/>
Metronome click while practicing (requires metronome on)
</label>
)}
<label className="phrase-check">
<input
type="checkbox"