From ffb2776c8c3bacb6c4cda427af9f594829b85444 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Tue, 15 Sep 2026 02:44:52 +0000 Subject: [PATCH] Giuliani studies as own material category; beam-before-draw fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NEW material 'giuliani': figures require a real voicing — the generator assigns cycle positions (bass root + upper chord tones) to DISTINCT strings with valid frets per bar; bars that cannot be voiced are skipped (deterministic), clear error when nothing fits. Patterns: p-i-m, p-i-m-a, p-a-m-i, a-i-m, p-i-m-a-m-i, p-m-a-m-i-m. Honors the chord pool, string restrictions and the tuning; one figure per beat. - 6/8 meter added (numerator 6) across score model, settings, generators; meter selects now offer 4/4, 3/4, 6/8. - Arpeggio category: Giuliani entries replaced with note-sequence broken-chord figures (1-3-2-1, 1-3-2-5(8), 1-5-3-5(8), 1-2-3-5-3) — pure pitch sequences, no voicing claims. - Beam fix (root cause): setBeam now happens BEFORE voice.draw — VexFlow decides flag vs beam at draw time; registering after drawing left both flag and beam. Beams drawn after notes, flags suppressed correctly. --- src/components/PhraseSheetMusic.tsx | 45 +++--- src/components/PhraseTrainer.tsx | 65 ++++++-- src/music/arpeggioGenerator.ts | 48 ++---- src/music/giulianiGenerator.test.ts | 63 ++++++++ src/music/giulianiGenerator.ts | 243 ++++++++++++++++++++++++++++ src/music/melodyGenerator.ts | 4 +- src/music/scaleDrills.ts | 2 +- src/score/model.ts | 2 +- src/types/SettingsTypes.ts | 9 +- 9 files changed, 412 insertions(+), 69 deletions(-) create mode 100644 src/music/giulianiGenerator.test.ts create mode 100644 src/music/giulianiGenerator.ts diff --git a/src/components/PhraseSheetMusic.tsx b/src/components/PhraseSheetMusic.tsx index 994edd6..ca189cf 100644 --- a/src/components/PhraseSheetMusic.tsx +++ b/src/components/PhraseSheetMusic.tsx @@ -324,9 +324,33 @@ export const PhraseSheetMusic: React.FC = ({ formatter.joinVoices(voices.map(v => v.voice)); formatter.format(voices.map(v => v.voice), formatWidth); + // --- Beams: BEFORE drawing notes. setBeam must happen prior to + // voice.draw — VexFlow decides flag vs beam on this.beam at + // draw time; registering late leaves BOTH flag and beam. + const beamPairs: { a: Fragment; b2: Fragment }[] = []; + const beamed = frags.filter(f => (f.isEighth || f.isSixteenth) && f.note instanceof StaveNote); + for (let i = 0; i + 1 < beamed.length; i++) { + const a = beamed[i], b2 = beamed[i + 1]; + if (a.staveKey !== b2.staveKey) continue; + const step = a.isSixteenth ? 120 : 240; + if (b2.start - a.start !== step) continue; + if ((a.ticks !== step) || (b2.ticks !== step)) continue; + const beatA = Math.floor((a.start - measureStart) / 480); + const beatB = Math.floor((b2.start - measureStart) / 480); + if (beatA === beatB) beamPairs.push({ a, b2 }); + } + // Apply beams to notes FIRST (suppresses flags), then draw. + const beams = beamPairs.map(({ a, b2 }) => { + const beam = new Beam([a.note, b2.note], false); + a.note.setBeam(beam); + b2.note.setBeam(beam); + return beam; + }); + treble.setContext(context).draw(); if (bass) bass.setContext(context).draw(); for (const { voice, stave } of voices) voice.draw(context, stave); + for (const beam of beams) beam.setContext(context).draw(); if (isGrand && isFirstOfRow) { const brace = new StaveConnector(treble, bass!); @@ -337,27 +361,6 @@ export const PhraseSheetMusic: React.FC = ({ line.setContext(context).draw(); } - // --- Beams: consecutive eighth/16th pairs within one beat --- - // Note: the Beam must be registered on each note via setBeam, - // otherwise VexFlow still draws flags (beam === undefined in - // shouldDrawFlag) and you get BOTH flag and beam. - const beamed = frags.filter(f => (f.isEighth || f.isSixteenth) && f.note instanceof StaveNote); - for (let i = 0; i + 1 < beamed.length; i++) { - const a = beamed[i], b2 = beamed[i + 1]; - if (a.staveKey !== b2.staveKey) continue; - // consecutive within the same beat, same subdivision size - const step = a.isSixteenth ? 120 : 240; - if (b2.start - a.start !== step) continue; - if ((a.ticks !== step) || (b2.ticks !== step)) continue; - const beatA = Math.floor((a.start - measureStart) / 480); - const beatB = Math.floor((b2.start - measureStart) / 480); - if (beatA === beatB) { - const beam = new Beam([a.note, b2.note], false); - a.note.setBeam(beam); - b2.note.setBeam(beam); - beam.setContext(context).draw(); - } - } } // --- Ties between consecutive fragments of one event (this row) --- diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 8e5bb3f..62b2103 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -20,6 +20,7 @@ import { computePlayableNotes, isFrettedInstrument, positionsWithinWindow } from import { generateMelody } from '../music/melodyGenerator'; import { generateScaleDrill } from '../music/scaleDrills'; import { generateArpeggio, type ArpeggioDegree } from '../music/arpeggioGenerator'; +import { generateGiulianiStudy, GIULIANI_PATTERN_LABELS, type GiulianiPattern } from '../music/giulianiGenerator'; import { isMode, type ModeId } from '../music/scales'; import { ARPEGGIO_DEGREE_LABELS } from '../music/arpeggioGenerator'; import { scoreEvents, type Result, type Score, type ScoreEvent } from '../score/model'; @@ -191,6 +192,19 @@ export const PhraseTrainer = forwardRef( rhythm: phrase.scaleRhythm, }, pool); } + if (phrase.material === 'giuliani') { + 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, + }, pool); + } if (phrase.material === 'arpeggio') { return generateArpeggio({ keyTonic: phrase.keyTonic, @@ -221,6 +235,7 @@ export const PhraseTrainer = forwardRef( phrase.arpeggioDegree, phrase.arpeggioPattern, phrase.arpeggioCoverage, phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioRhythm, phrase.arpeggioChordSelection, melodySeed, + phrase.giulianiPattern, phrase.giulianiBars, melodySeed, pool, importedScore]); const fullScore: Result | null = useMemo(() => { @@ -557,6 +572,7 @@ export const PhraseTrainer = forwardRef( + @@ -576,9 +592,10 @@ export const PhraseTrainer = forwardRef( @@ -624,14 +642,11 @@ export const PhraseTrainer = forwardRef( - - - - - - - - + + + + + @@ -688,14 +703,42 @@ export const PhraseTrainer = forwardRef( )} + {phrase.material === 'giuliani' && ( + <> + + + + + Chords: from the arpeggio chord pool; voicing enforced (bass + upper strings). + + + )} + {phrase.material === 'library' && (