mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 13:53:55 +01:00
Mobile clipping fix + 6/8 render crash fix (root cause) + more variants
- Clipping: min width per bar raised (16ths 420px, 8ths 330px, quarters 240px) and format width reserves more room (clef/key 100px, barline margin 24px) — narrow screens now wrap into more rows with real margins instead of clipping the last bar. - 6/8 crash ROOT CAUSE: Giuliani figures whose length didn't divide the bar into renderable note values produced 96-tick durations (32nd-ish) that durationCode can't spell -> renderer threw -> white screen. Giuliani generator now snaps figure subdivision to renderable durations (per 2/3/4 divisions, quarter-note walk as last resort); pimamim in 6/8 renders as a walking quarter figure. - 6/8 melody templates: fixed sums (were 2640/3120 in 4 templates — those filtered out, silently degrading to the fallback). - More variants: arpeggio figures 1-3-5-3, 1-2-1-3-2-1, 1-5-4-5-3-5, 1-3-2-5-3-2; Giuliani patterns p-i-a-i, p-m-a-m-i, p-i-m-a-m-i (waltz), p-i-m-a-i-a. - Tests 179/179.
This commit is contained in:
@@ -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 ? 320 : densestTick <= 240 ? 260 : 210;
|
||||
const minWidthPerBar = densestTick <= 120 ? 420 : 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';
|
||||
@@ -324,7 +324,7 @@ export const PhraseSheetMusic: React.FC<PhraseSheetMusicProps> = ({
|
||||
voices.push(buildVoice(treble, () => true));
|
||||
}
|
||||
|
||||
const formatWidth = Math.max(60, barWidth - (isFirstOfRow ? 92 : 16));
|
||||
const formatWidth = Math.max(60, barWidth - (isFirstOfRow ? 100 : 24));
|
||||
const formatter = new Formatter();
|
||||
formatter.joinVoices(voices.map(v => v.voice));
|
||||
formatter.format(voices.map(v => v.voice), formatWidth);
|
||||
|
||||
@@ -46,7 +46,9 @@ describe('generateGiulianiStudy', () => {
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
const ev = scoreEvents(r.value);
|
||||
expect(ev.length).toBe(4 * 4 * 6);
|
||||
// 4/4: figure falls back to quarter-note walk (6-note figure over 4
|
||||
// beats is not renderable in even subdivisions) — 4 bars × 4 quarters
|
||||
expect(ev.length).toBe(16);
|
||||
expect(validateScore(r.value)).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -204,33 +204,53 @@ export function generateGiulianiStudy(
|
||||
|
||||
// Rhythm: one note per slot (figure repeats); one figure per beat.
|
||||
const figure = FIGURES[config.pattern];
|
||||
// Each figure repetition occupies one beat; repetitions per bar = meter.numerator.
|
||||
const notesPerBar = config.meter.numerator * figure.length;
|
||||
const noteDur = full / notesPerBar;
|
||||
// Renderable note durations only (durationCode subset). Figures with a
|
||||
// length that divides neither 4 nor 3 evenly would produce exotic ticks
|
||||
// (e.g. 96 = 32nd triplets) that the renderer can't spell — snap those
|
||||
// combinations to a safe layout instead of crashing the renderer.
|
||||
const RENDERABLE_NOTE_TICKS = [120, 240, 360, 480, 720, 960];
|
||||
let notesPerBar = config.meter.numerator * figure.length;
|
||||
let noteDur = full / notesPerBar;
|
||||
if (!RENDERABLE_NOTE_TICKS.includes(noteDur)) {
|
||||
// Fall back: one figure per TWO beats (halves the notes per bar),
|
||||
// then per bar, then quarters as last resort.
|
||||
for (const div of [2, 3, 4]) {
|
||||
const cand = full / div;
|
||||
if (cand % figure.length === 0 && RENDERABLE_NOTE_TICKS.includes(cand / figure.length)) {
|
||||
notesPerBar = div * figure.length;
|
||||
noteDur = cand / figure.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!RENDERABLE_NOTE_TICKS.includes(noteDur)) {
|
||||
// Final fallback: quarter-note walk through the figure (cycling).
|
||||
notesPerBar = config.meter.numerator;
|
||||
noteDur = PPQ;
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
for (let b = 0; b < usableBars.length; b++) {
|
||||
const { degreeIndex, placement } = usableBars[b];
|
||||
if (!placement) continue;
|
||||
chordSymbols.push(chordNameFor(key, degreeIndex));
|
||||
for (let beat = 0; beat < config.meter.numerator; beat++) {
|
||||
for (let f = 0; f < figure.length; f++) {
|
||||
const slot = figure[f];
|
||||
const note = placement[slot];
|
||||
if (!note) continue;
|
||||
// spelling via key degree of this pc
|
||||
const pc = ((note.midi % 12) + 12) % 12;
|
||||
const d = key.degrees.find(deg => degreePc(deg) === pc);
|
||||
if (!d) continue;
|
||||
const octave = (note.midi - pc) / 12 - 1;
|
||||
events.push({
|
||||
id: `g${b}-${beat}-${f}`,
|
||||
startTick: cursor,
|
||||
durationTicks: noteDur,
|
||||
pitch: { midi: note.midi, step: d.step, alter: d.alter, octave },
|
||||
});
|
||||
cursor += noteDur;
|
||||
}
|
||||
for (let n = 0; n < notesPerBar; n++) {
|
||||
const f = n % figure.length;
|
||||
const slot = figure[f];
|
||||
const note = placement[slot];
|
||||
if (!note) continue;
|
||||
// spelling via key degree of this pc
|
||||
const pc = ((note.midi % 12) + 12) % 12;
|
||||
const d = key.degrees.find(deg => degreePc(deg) === pc);
|
||||
if (!d) continue;
|
||||
const octave = (note.midi - pc) / 12 - 1;
|
||||
events.push({
|
||||
id: `g${b}-${n}`,
|
||||
startTick: cursor,
|
||||
durationTicks: noteDur,
|
||||
pitch: { midi: note.midi, step: d.step, alter: d.alter, octave },
|
||||
});
|
||||
cursor += noteDur;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const TEMPLATES_4_L3: number[][] = [
|
||||
];
|
||||
// 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],
|
||||
[240, 240, 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],
|
||||
@@ -75,12 +75,11 @@ const TEMPLATES_6_L1: number[][] = [
|
||||
[1440, 480, 480, 480],
|
||||
];
|
||||
const TEMPLATES_6_L2: number[][] = [
|
||||
[480, 480, 120, 120, 120, 120, 240, 240, 480, 480],
|
||||
[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],
|
||||
];
|
||||
[480, 240, 240, 480, 240, 240, 240, 240, 240, 240],
|
||||
[480, 240, 240, 480, 240, 240, 240, 240, 240, 240],
|
||||
[480, 240, 240, 480, 240, 240, 240, 240, 240, 240],];
|
||||
const TEMPLATES_3_L1: number[][] = [
|
||||
[480, 480, 480],
|
||||
[960, 480],
|
||||
|
||||
Reference in New Issue
Block a user