From 263b6f6b6966fa91923f07505246d47855a53aea Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 14 Sep 2026 18:44:14 +0000 Subject: [PATCH] Arpeggio sequences + auto-start on first note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sequence mode: bars (2/4/6/8) with one chord per bar, chosen by functional rules (I->IV/V/vi...), diatonic cycle, or random (no immediate repeats); deterministic via melody seed. Chord symbols per bar in the score model (chordSymbols) rendered above the staff via setSection. - Rhythm: quarters or eighths (cycling up/down through the pattern). - Single-chord mode unchanged (whole path, overflow bars if needed). - Auto-start: 'Start on first note (no count-in)' — step pace begins the run as soon as the mic detects the first note; no start press needed. - 'New' button re-rolls the progression (new seed). - Tests 174/174. --- src/components/PhraseSheetMusic.tsx | 8 ++ src/components/PhraseTrainer.tsx | 42 ++++++- src/hooks/usePhraseTrainer.ts | 16 ++- src/music/arpeggioGenerator.test.ts | 47 ++++++++ src/music/arpeggioGenerator.ts | 165 ++++++++++++++++++++++++---- src/score/model.ts | 3 + src/types/SettingsTypes.ts | 11 ++ 7 files changed, 265 insertions(+), 27 deletions(-) diff --git a/src/components/PhraseSheetMusic.tsx b/src/components/PhraseSheetMusic.tsx index d74ec50..c85ab60 100644 --- a/src/components/PhraseSheetMusic.tsx +++ b/src/components/PhraseSheetMusic.tsx @@ -272,6 +272,14 @@ export const PhraseSheetMusic: React.FC = ({ if (r === 0) treble.addTimeSignature(`${score.meter.numerator}/${score.meter.denominator}`); treble.setMeasure(measure.number); } + // Chord symbol above the staff (bar-indexed), drawn via the + // stave's annotation area so it shifts with the clef offset. + const chordSym = score.chordSymbols?.[m]; + if (chordSym) { + try { + treble.setSection(chordSym, 0); + } catch { /* courtesy label — never fatal */ } + } let bass: Stave | null = null; if (isGrand) { bass = new Stave(x, y + 130, barWidth); diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 9f8ff30..49c16ab 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -210,6 +210,10 @@ export const PhraseTrainer = forwardRef( pattern: phrase.arpeggioPattern, coverage: phrase.arpeggioCoverage, meter: { numerator: phrase.meterNumerator, denominator: 4 }, + bars: phrase.arpeggioBars, + progression: phrase.arpeggioProgression, + rhythm: phrase.arpeggioRhythm, + seed: melodySeed, }, pool); } if (phrase.material === 'library') { @@ -225,6 +229,7 @@ export const PhraseTrainer = forwardRef( }, [phrase.material, phrase.keyTonic, phrase.keyMode, phrase.bars, phrase.meterNumerator, phrase.rhythmLevel, phrase.scaleDirection, phrase.scaleCoverage, phrase.libraryId, phrase.arpeggioDegree, phrase.arpeggioPattern, phrase.arpeggioCoverage, + phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioRhythm, melodySeed, melodySeed, pool, importedScore]); const fullScore: Result | null = useMemo(() => { @@ -252,6 +257,7 @@ export const PhraseTrainer = forwardRef( clickSound: phrase.clickSound, inputMode: phrase.inputMode, previewVolume: settings.autoPlayVolume ?? 0.4, + autoStartOnNote: phrase.autoStartOnNote, }, listening, micError, @@ -310,7 +316,7 @@ export const PhraseTrainer = forwardRef( }, [scoreOk, fixedMaterial, pool, fullScore]); const nextAction = useCallback(() => { - if (phrase.material === 'melody') { + if (phrase.material === 'melody' || (phrase.material === 'arpeggio' && phrase.arpeggioBars > 1)) { setMelodySeed(s => (s + 1) % 999983); } else if (fixedMaterial && scoreOk) { const total = fullScore!.value.measures.length; @@ -499,7 +505,7 @@ export const PhraseTrainer = forwardRef( Retry - {(phrase.material === 'melody' || fixedMaterial) && ( + {(phrase.material === 'melody' || phrase.material === 'arpeggio' || fixedMaterial) && ( - {(phrase.material === 'melody' || fixedMaterial) && ( + {(phrase.material === 'melody' || phrase.material === 'arpeggio' || fixedMaterial) && (