Phrase mode: always-eighths arpeggio/Giuliani, auto meter, custom patterns, alternating bass

- 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.
This commit is contained in:
OpenClaw
2026-09-15 06:29:11 +00:00
parent 0b85f24797
commit 8ecb7f2de2
7 changed files with 561 additions and 190 deletions

View File

@@ -109,7 +109,7 @@ export const PhraseSheetMusic: React.FC<PhraseSheetMusicProps> = ({
// 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';