From a7a1d34c11bd0c272436d1f2b8f54e7a39fd5f3f Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Tue, 15 Sep 2026 03:04:29 +0000 Subject: [PATCH] Mobile width fix, 6/8 crash fix, more arpeggio+Giuliani variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhraseSheetMusic: min width per bar scales with notation density (16ths 320px, 8ths 260px, quarters 210px) — narrow screens wrap into more rows instead of clipping notes off-screen. - 6/8 crash fixed at the root: melody templatesFor had no 6/8 templates (empty pool -> pick on empty array). Added 6/8 template sets for levels 1/2 plus an empty-pool fallback guard so generators can never pick from an empty array again (any meter/level combination stays playable). - More arpeggio broken-chord figures: 1-3-5-3, 1-2-1-3-2-1, 1-5-4-5-3-5, 1-3-2-5-3-2. - More Giuliani patterns: p-i-a-i (wide), p-m-a-m-i, p-i-m-a-m-i (waltz feel), p-i-m-a-i-a. - Tests 179/179. --- src/components/PhraseSheetMusic.tsx | 7 +++++- src/components/PhraseTrainer.tsx | 4 ++++ src/music/arpeggioGenerator.ts | 14 ++++++++++- src/music/giulianiGenerator.ts | 24 ++++++++++++++----- src/music/melodyGenerator.ts | 37 +++++++++++++++++++++++++---- 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/components/PhraseSheetMusic.tsx b/src/components/PhraseSheetMusic.tsx index ca189cf..641315a 100644 --- a/src/components/PhraseSheetMusic.tsx +++ b/src/components/PhraseSheetMusic.tsx @@ -105,7 +105,12 @@ export const PhraseSheetMusic: React.FC = ({ const events = scoreEvents(score); const totalBars = score.measures.length; - const barsPerRow = Math.max(1, Math.min(4, Math.floor((width - 40) / 210))); + // Min width per bar grows with notation density (eighths/16ths need more + // 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 ? 320 : densestTick <= 240 ? 260 : 210; + 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'; const rowHeight = isGrand ? 250 : 140; diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 62b2103..7d54d93 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -647,6 +647,10 @@ export const PhraseTrainer = forwardRef( + + + + diff --git a/src/music/arpeggioGenerator.ts b/src/music/arpeggioGenerator.ts index 787faad..b8bf432 100644 --- a/src/music/arpeggioGenerator.ts +++ b/src/music/arpeggioGenerator.ts @@ -21,7 +21,11 @@ export type ArpeggioPattern = | '1321' // root-fifth-third-root | '1325' // root-fifth-third-octave | '1535' // root-octave-fifth-octave - | '12353'; // root-third-fifth-octave-fifth + | '12353' // root-third-fifth-octave-fifth + | '121321' // rolling thirds (root-third-root-fifth-third-root) + | '1353' // root-third-fifth-third + | '15453' // root-octave-fifth-octave-fifth-octave + | '132532';// long broken-chord wave export type ArpeggioCoverage = 'one-octave' | 'two-octave'; /** How the chord per bar is chosen in sequence mode. */ export type ArpeggioProgression = 'random' | 'functional' | 'diatonic-cycle'; @@ -61,6 +65,10 @@ export const PATTERN_LABELS: Record = { '1325': '1-3-2-5(8)', '1535': '1-5-3-5(8)', '12353': '1-2-3-5-3', + '121321': '1-2-1-3-2-1', + '1353': '1-3-5-3', + '15453': '1-5-4-5-3-5', + '132532': '1-3-2-5-3-2', }; /** @@ -77,6 +85,10 @@ const BROKEN_FIGURES: Record = { '1325': [0, 2, 1, 3], '1535': [0, 3, 2, 3], '12353': [0, 1, 2, 3, 2], + '121321': [0, 1, 0, 2, 1, 0], + '1353': [0, 1, 2, 1], + '15453': [0, 3, 2, 3, 2, 3], + '132532': [0, 2, 1, 3, 2, 1], }; /** pc of a spelled degree (step letter + alter). */ diff --git a/src/music/giulianiGenerator.ts b/src/music/giulianiGenerator.ts index 0ccbbcf..34d7a37 100644 --- a/src/music/giulianiGenerator.ts +++ b/src/music/giulianiGenerator.ts @@ -17,12 +17,16 @@ import { TUNINGS, type Tuning } from '../music/Tunings'; import { makeRng } from './melodyGenerator'; export type GiulianiPattern = - | 'pim' // Studies 1–20 feel: bass + two upper voices - | 'pima' // Studies 21–40: bass + three upper voices - | 'pami' // bass + descending upper voices - | 'aim' // upper voices first - | 'pimamim'// six-note rolling - | 'pmamim';// asymmetric + | 'pim' // Studies 1–20 feel: bass + two upper voices + | 'pima' // Studies 21–40: bass + three upper voices + | 'pami' // bass + descending upper voices + | 'aim' // upper voices first + | 'pimamim' // six-note rolling + | 'pmamim' // asymmetric + | 'piai' // bass + octave-third (wide spacing) + | 'pmami' // bass + fifth-octave-third + | 'pimami' // bass-third-fifth-third (waltz-like) + | 'pimaia'; // bass with two upper-voice pairs export const GIULIANI_PATTERN_LABELS: Record = { pim: 'p-i-m (3 voices)', @@ -31,6 +35,10 @@ export const GIULIANI_PATTERN_LABELS: Record = { aim: 'a-i-m', pimamim: 'p-i-m-a-m-i', pmamim: 'p-m-a-m-i-m', + piai: 'p-i-a-i (wide)', + pmami: 'p-m-a-m-i', + pimami: 'p-i-m-a-m-i (waltz)', + pimaia: 'p-i-m-a-i-a', }; /** Figure voice slots per repetition (indices into the cycle [root, third, fifth, octave]). */ @@ -41,6 +49,10 @@ const FIGURES: Record = { aim: [0, 3, 1], pimamim: [0, 1, 2, 3, 2, 1], pmamim: [0, 2, 3, 2, 1, 2], + piai: [0, 1, 3, 1], + pmami: [0, 2, 3, 2, 1], + pimami: [0, 1, 2, 3, 2], + pimaia: [0, 1, 2, 3, 1, 3], }; export interface GiulianiConfig { diff --git a/src/music/melodyGenerator.ts b/src/music/melodyGenerator.ts index 3fd1e2e..564bf32 100644 --- a/src/music/melodyGenerator.ts +++ b/src/music/melodyGenerator.ts @@ -65,6 +65,22 @@ const TEMPLATES_4_L3: number[][] = [ [480, 480, 360, 120, 480], [720, 120, 120, 120, 120, 480], ]; +// 6/8 templates (6 quarter-ticks = 2880 per bar; 8th-note oriented). +const TEMPLATES_6_L1: number[][] = [ + [240, 240, 240, 240, 240, 240, 480, 480], + [480, 480, 240, 240, 240, 240, 480, 480], + [240, 240, 480, 480, 480, 240, 240, 240, 240], + [960, 480, 480, 480, 480], + [480, 480, 480, 480, 960], + [1440, 480, 480, 480], +]; +const TEMPLATES_6_L2: number[][] = [ + [480, 480, 120, 120, 120, 120, 240, 240, 480, 480], + [120, 120, 120, 120, 480, 480, 240, 240, 240, 240, 240, 240], + [720, 240, 240, 480, 480, 240, 240, 240, 240], + [360, 120, 480, 720, 480, 240, 240, 240, 240], + [480, 240, 240, 480, 240, 240, 480, 240, 240, 240], +]; const TEMPLATES_3_L1: number[][] = [ [480, 480, 480], [960, 480], @@ -87,10 +103,23 @@ const TEMPLATES_3_L3: number[][] = [ ]; function templatesFor(meter: { numerator: 3 | 4 | 6 }, level: 1 | 2 | 3): number[][] { - const base = meter.numerator === 4 - ? (level === 1 ? TEMPLATES_4_L1 : level === 2 ? [...TEMPLATES_4_L1, ...TEMPLATES_4_L2] : [...TEMPLATES_4_L1, ...TEMPLATES_4_L2, ...TEMPLATES_4_L3]) - : (level === 1 ? TEMPLATES_3_L1 : level === 2 ? [...TEMPLATES_3_L1, ...TEMPLATES_3_L2] : [...TEMPLATES_3_L1, ...TEMPLATES_3_L2, ...TEMPLATES_3_L3]); - return base.filter(t => t.reduce((a, b) => a + b, 0) === meter.numerator * PPQ); + let base: number[][]; + if (meter.numerator === 4) { + base = level === 1 ? TEMPLATES_4_L1 : level === 2 ? [...TEMPLATES_4_L1, ...TEMPLATES_4_L2] : [...TEMPLATES_4_L1, ...TEMPLATES_4_L2, ...TEMPLATES_4_L3]; + } else if (meter.numerator === 6) { + base = level === 1 ? TEMPLATES_6_L1 : [...TEMPLATES_6_L1, ...TEMPLATES_6_L2]; + } else { + base = level === 1 ? TEMPLATES_3_L1 : level === 2 ? [...TEMPLATES_3_L1, ...TEMPLATES_3_L2] : [...TEMPLATES_3_L1, ...TEMPLATES_3_L2, ...TEMPLATES_3_L3]; + } + const out = base.filter(t => t.reduce((a, b) => a + b, 0) === meter.numerator * PPQ); + // Never return an empty pool — fall back to the closest available level + // so generator.pick on an empty array can never crash the app. + if (out.length === 0) { + const all = [...TEMPLATES_4_L1, ...TEMPLATES_4_L2, ...TEMPLATES_3_L1, ...TEMPLATES_3_L2, ...TEMPLATES_6_L1]; + const anyFit = all.filter(t => t.reduce((a, b) => a + b, 0) === meter.numerator * PPQ); + return anyFit.length > 0 ? anyFit : [[meter.numerator * PPQ]]; + } + return out; } /** Motion weights by semitone distance (soft preference, contract C3). */