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' && (