mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
Mobile width fix, 6/8 crash fix, more arpeggio+Giuliani variants
- 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.
This commit is contained in:
@@ -105,7 +105,12 @@ export const PhraseSheetMusic: React.FC<PhraseSheetMusicProps> = ({
|
|||||||
|
|
||||||
const events = scoreEvents(score);
|
const events = scoreEvents(score);
|
||||||
const totalBars = score.measures.length;
|
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 rowCount = Math.max(1, Math.ceil(totalBars / barsPerRow));
|
||||||
const isGrand = clef === 'grand';
|
const isGrand = clef === 'grand';
|
||||||
const rowHeight = isGrand ? 250 : 140;
|
const rowHeight = isGrand ? 250 : 140;
|
||||||
|
|||||||
@@ -647,6 +647,10 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
<option value="1325">1-3-2-5(8)</option>
|
<option value="1325">1-3-2-5(8)</option>
|
||||||
<option value="1535">1-5-3-5(8)</option>
|
<option value="1535">1-5-3-5(8)</option>
|
||||||
<option value="12353">1-2-3-5-3</option>
|
<option value="12353">1-2-3-5-3</option>
|
||||||
|
<option value="1353">1-3-5-3</option>
|
||||||
|
<option value="121321">1-2-1-3-2-1</option>
|
||||||
|
<option value="15453">1-5-4-5-3-5</option>
|
||||||
|
<option value="132532">1-3-2-5-3-2</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export type ArpeggioPattern =
|
|||||||
| '1321' // root-fifth-third-root
|
| '1321' // root-fifth-third-root
|
||||||
| '1325' // root-fifth-third-octave
|
| '1325' // root-fifth-third-octave
|
||||||
| '1535' // root-octave-fifth-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';
|
export type ArpeggioCoverage = 'one-octave' | 'two-octave';
|
||||||
/** How the chord per bar is chosen in sequence mode. */
|
/** How the chord per bar is chosen in sequence mode. */
|
||||||
export type ArpeggioProgression = 'random' | 'functional' | 'diatonic-cycle';
|
export type ArpeggioProgression = 'random' | 'functional' | 'diatonic-cycle';
|
||||||
@@ -61,6 +65,10 @@ export const PATTERN_LABELS: Record<ArpeggioPattern, string> = {
|
|||||||
'1325': '1-3-2-5(8)',
|
'1325': '1-3-2-5(8)',
|
||||||
'1535': '1-5-3-5(8)',
|
'1535': '1-5-3-5(8)',
|
||||||
'12353': '1-2-3-5-3',
|
'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<string, number[]> = {
|
|||||||
'1325': [0, 2, 1, 3],
|
'1325': [0, 2, 1, 3],
|
||||||
'1535': [0, 3, 2, 3],
|
'1535': [0, 3, 2, 3],
|
||||||
'12353': [0, 1, 2, 3, 2],
|
'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). */
|
/** pc of a spelled degree (step letter + alter). */
|
||||||
|
|||||||
@@ -17,12 +17,16 @@ import { TUNINGS, type Tuning } from '../music/Tunings';
|
|||||||
import { makeRng } from './melodyGenerator';
|
import { makeRng } from './melodyGenerator';
|
||||||
|
|
||||||
export type GiulianiPattern =
|
export type GiulianiPattern =
|
||||||
| 'pim' // Studies 1–20 feel: bass + two upper voices
|
| 'pim' // Studies 1–20 feel: bass + two upper voices
|
||||||
| 'pima' // Studies 21–40: bass + three upper voices
|
| 'pima' // Studies 21–40: bass + three upper voices
|
||||||
| 'pami' // bass + descending upper voices
|
| 'pami' // bass + descending upper voices
|
||||||
| 'aim' // upper voices first
|
| 'aim' // upper voices first
|
||||||
| 'pimamim'// six-note rolling
|
| 'pimamim' // six-note rolling
|
||||||
| 'pmamim';// asymmetric
|
| '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<GiulianiPattern, string> = {
|
export const GIULIANI_PATTERN_LABELS: Record<GiulianiPattern, string> = {
|
||||||
pim: 'p-i-m (3 voices)',
|
pim: 'p-i-m (3 voices)',
|
||||||
@@ -31,6 +35,10 @@ export const GIULIANI_PATTERN_LABELS: Record<GiulianiPattern, string> = {
|
|||||||
aim: 'a-i-m',
|
aim: 'a-i-m',
|
||||||
pimamim: 'p-i-m-a-m-i',
|
pimamim: 'p-i-m-a-m-i',
|
||||||
pmamim: 'p-m-a-m-i-m',
|
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]). */
|
/** Figure voice slots per repetition (indices into the cycle [root, third, fifth, octave]). */
|
||||||
@@ -41,6 +49,10 @@ const FIGURES: Record<GiulianiPattern, number[]> = {
|
|||||||
aim: [0, 3, 1],
|
aim: [0, 3, 1],
|
||||||
pimamim: [0, 1, 2, 3, 2, 1],
|
pimamim: [0, 1, 2, 3, 2, 1],
|
||||||
pmamim: [0, 2, 3, 2, 1, 2],
|
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 {
|
export interface GiulianiConfig {
|
||||||
|
|||||||
@@ -65,6 +65,22 @@ const TEMPLATES_4_L3: number[][] = [
|
|||||||
[480, 480, 360, 120, 480],
|
[480, 480, 360, 120, 480],
|
||||||
[720, 120, 120, 120, 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[][] = [
|
const TEMPLATES_3_L1: number[][] = [
|
||||||
[480, 480, 480],
|
[480, 480, 480],
|
||||||
[960, 480],
|
[960, 480],
|
||||||
@@ -87,10 +103,23 @@ const TEMPLATES_3_L3: number[][] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function templatesFor(meter: { numerator: 3 | 4 | 6 }, level: 1 | 2 | 3): number[][] {
|
function templatesFor(meter: { numerator: 3 | 4 | 6 }, level: 1 | 2 | 3): number[][] {
|
||||||
const base = meter.numerator === 4
|
let base: number[][];
|
||||||
? (level === 1 ? TEMPLATES_4_L1 : level === 2 ? [...TEMPLATES_4_L1, ...TEMPLATES_4_L2] : [...TEMPLATES_4_L1, ...TEMPLATES_4_L2, ...TEMPLATES_4_L3])
|
if (meter.numerator === 4) {
|
||||||
: (level === 1 ? TEMPLATES_3_L1 : level === 2 ? [...TEMPLATES_3_L1, ...TEMPLATES_3_L2] : [...TEMPLATES_3_L1, ...TEMPLATES_3_L2, ...TEMPLATES_3_L3]);
|
base = level === 1 ? TEMPLATES_4_L1 : level === 2 ? [...TEMPLATES_4_L1, ...TEMPLATES_4_L2] : [...TEMPLATES_4_L1, ...TEMPLATES_4_L2, ...TEMPLATES_4_L3];
|
||||||
return base.filter(t => t.reduce((a, b) => a + b, 0) === meter.numerator * PPQ);
|
} 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). */
|
/** Motion weights by semitone distance (soft preference, contract C3). */
|
||||||
|
|||||||
Reference in New Issue
Block a user