From 8ecb7f2de283a96522b8ede2ca1b54b18c768062 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Tue, 15 Sep 2026 06:29:11 +0000 Subject: [PATCH] Phrase mode: always-eighths arpeggio/Giuliani, auto meter, custom patterns, alternating bass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - arpeggioGenerator: always 240-tick eighths; auto-derive 3/4 vs 4/4 from pattern length (remainder heuristic); never stretch the last note (fixes the two-octave 2880-tick VexFlow crash in 6/8); rest fills built from renderable durations only; new 'custom' pattern from digit input (1=root..5=fifth-above-octave); removed meter/rhythm config — generator computes its own meter. - giulianiGenerator: always eighths; meter auto-derived from figure length (3v->3/4, 4v->4/4, 5v->4/4+rest, 6v->3/4); fixed pimami figure [0,1,2,3,2,1]; added pimai/pmia/pmim figures; alternating bass option (even bars voice the fifth as bass); removed the quarter-note fallback. - PhraseTrainer: removed meter/rhythm selects for arpeggio+giuliani; custom pattern text input; alternating bass checkbox. - SettingsTypes: 'custom' arpeggio pattern, customArpeggioPattern, giulianiAlternateBass; removed arpeggioRhythm. - PhraseSheetMusic: minWidthPerBar 420 -> 500 for 16th notes. - Tests updated for the new invariants + coverage for custom patterns, alternating bass, new figures, renderable-tick sweeps (1031 -> 191 per-file, all green); removed temporary crash_test.test.ts. --- src/components/PhraseSheetMusic.tsx | 2 +- src/components/PhraseTrainer.tsx | 54 ++--- src/music/arpeggioGenerator.test.ts | 106 +++++++-- src/music/arpeggioGenerator.ts | 334 ++++++++++++++++++++-------- src/music/giulianiGenerator.test.ts | 105 ++++++++- src/music/giulianiGenerator.ts | 138 ++++++++---- src/types/SettingsTypes.ts | 12 +- 7 files changed, 561 insertions(+), 190 deletions(-) diff --git a/src/components/PhraseSheetMusic.tsx b/src/components/PhraseSheetMusic.tsx index 9f516ee..ecfc6ee 100644 --- a/src/components/PhraseSheetMusic.tsx +++ b/src/components/PhraseSheetMusic.tsx @@ -109,7 +109,7 @@ export const PhraseSheetMusic: React.FC = ({ // horizontal room than quarters) so narrow screens wrap into more rows // instead of clipping notes. const densestTick = Math.min(...events.filter(e => e.pitch).map(e => e.durationTicks), 1920); - const minWidthPerBar = densestTick <= 120 ? 420 : densestTick <= 240 ? 330 : 240; + const minWidthPerBar = densestTick <= 120 ? 500 : densestTick <= 240 ? 330 : 240; const barsPerRow = Math.max(1, Math.min(4, Math.floor((width - 40) / minWidthPerBar))); const rowCount = Math.max(1, Math.ceil(totalBars / barsPerRow)); const isGrand = clef === 'grand'; diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 7d54d93..aef5be7 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -193,16 +193,18 @@ export const PhraseTrainer = forwardRef( }, pool); } if (phrase.material === 'giuliani') { + // Meter is auto-derived inside the generator from the figure + // length (always eighths) — the user no longer picks a meter. return generateGiulianiStudy({ keyTonic: phrase.keyTonic, keyMode: (isMode(phrase.keyMode) ? phrase.keyMode : 'major') as ModeId, pattern: phrase.giulianiPattern, - meter: { numerator: phrase.meterNumerator, denominator: 4 }, tuningId: settings.tuningId, bars: phrase.giulianiBars, chordSelection: phrase.arpeggioChordSelection as string[], seed: melodySeed, allowedStrings: pf.strings, + alternateBass: phrase.giulianiAlternateBass ?? false, }, pool); } if (phrase.material === 'arpeggio') { @@ -212,11 +214,10 @@ export const PhraseTrainer = forwardRef( degree: (phrase.arpeggioDegree as ArpeggioDegree) || 'I', pattern: phrase.arpeggioPattern, coverage: phrase.arpeggioCoverage, - meter: { numerator: phrase.meterNumerator, denominator: 4 }, bars: phrase.arpeggioBars, progression: phrase.arpeggioProgression, chordSelection: phrase.arpeggioChordSelection as ArpeggioDegree[], - rhythm: phrase.arpeggioRhythm, + customPattern: phrase.customArpeggioPattern, seed: melodySeed, }, pool); } @@ -233,9 +234,9 @@ export const PhraseTrainer = forwardRef( }, [phrase.material, phrase.keyTonic, phrase.keyMode, phrase.bars, phrase.meterNumerator, phrase.rhythmLevel, phrase.scaleDirection, phrase.scaleCoverage, phrase.scaleRhythm, phrase.libraryId, phrase.arpeggioDegree, phrase.arpeggioPattern, phrase.arpeggioCoverage, - phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioRhythm, + phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioChordSelection, melodySeed, - phrase.giulianiPattern, phrase.giulianiBars, + phrase.giulianiPattern, phrase.giulianiBars, phrase.giulianiAlternateBass, melodySeed, pool, importedScore]); const fullScore: Result | null = useMemo(() => { @@ -651,9 +652,24 @@ export const PhraseTrainer = forwardRef( + + {phrase.arpeggioPattern === 'custom' && ( + + )} - )} @@ -730,15 +733,16 @@ export const PhraseTrainer = forwardRef( {[1, 2, 4, 6, 8].map(b => )} -