@xromdev/notation 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1050 @@
1
+ /**
2
+ * Ticks read as NOTATION: note values, dots, ties and bar lines.
3
+ *
4
+ * Nothing here quantises, because nothing has to: one tick is exactly a
5
+ * sixty-fourth note (a sixteenth of a quarter), so every note given already
6
+ * sits on the grid notation is written on. That is what makes music read out
7
+ * of a game cartridge writable at all — the usual failure of notation built
8
+ * from MIDI is a performance that lands a few milliseconds off the beat, and
9
+ * a sound driver gives no performance, only the integers it counts. Anything
10
+ * else has to be quantised to sixty-fourths before it comes here.
11
+ *
12
+ * The rules were made on Battletoads (NES), whose driver counts in exactly
13
+ * these ticks, and checked by ear on its twenty tracks; the examples in these
14
+ * comments are the bars that decided them.
15
+ *
16
+ * Durations come out as VexFlow codes (`w h q 8 16 32 64`), pitches as VexFlow
17
+ * keys (`c#/4`) — the shortest names there are, and what a renderer reads.
18
+ */
19
+ /** Engine ticks in a quarter note. The driver's own grid, not a choice. */
20
+ export const TICKS_PER_QUARTER = 16;
21
+ export const BEATS_PER_MEASURE = 4;
22
+ /**
23
+ * A bar of 4/4, which is what all but one track is written in and what every
24
+ * caller gets by default.
25
+ *
26
+ * Not a constant of the music: Volkmire's Inferno Race is in 2/4, and its
27
+ * sections — the change to D major among them — start on odd multiples of half
28
+ * a 4/4 bar, so no 64-tick bar line can hold them. So everything here takes the
29
+ * bar length it is barring to, and `MEASURE_TICKS` is only the default.
30
+ */
31
+ export const MEASURE_TICKS = TICKS_PER_QUARTER * BEATS_PER_MEASURE;
32
+ /** Quarters in a bar of `measureTicks` ticks. */
33
+ export const beatsIn = (measureTicks) => measureTicks / TICKS_PER_QUARTER;
34
+ /**
35
+ * A bar of so many ticks as a time signature.
36
+ *
37
+ * In the note value that measures it WHOLE: 64 ticks are 4/4 and 32 are 2/4,
38
+ * but an opening bar of 38 is no number of quarters — it is 19 thirty-seconds,
39
+ * and 19/32 is what a reader is shown.
40
+ */
41
+ export function timeSignatureOf(measureTicks,
42
+ /**
43
+ * The note value a bar is counted in, when the reading names one: 2 writes a
44
+ * bar of 128 ticks as 4/2 rather than 8/4. Left out — or not dividing the bar,
45
+ * as an opening bar of its own length may not — it is chosen as below.
46
+ */
47
+ beatValue) {
48
+ if (beatValue) {
49
+ const beat = (TICKS_PER_QUARTER * 4) / beatValue;
50
+ if (Number.isInteger(beat) && measureTicks % beat === 0) {
51
+ return { beats: measureTicks / beat, beatValue };
52
+ }
53
+ }
54
+ for (const beatValue of [4, 8, 16, 32, 64]) {
55
+ const beat = (TICKS_PER_QUARTER * 4) / beatValue;
56
+ if (measureTicks % beat === 0)
57
+ return { beats: measureTicks / beat, beatValue };
58
+ }
59
+ return { beats: measureTicks, beatValue: 64 };
60
+ }
61
+ export const barGrid = (measureTicks = MEASURE_TICKS, firstBarTicks = measureTicks, oddBars = []) => ({
62
+ measureTicks,
63
+ firstBarTicks,
64
+ ...(oddBars.length ? { oddBars: [...oddBars].sort((a, b) => a.bar - b.bar) } : {}),
65
+ });
66
+ const oddBarsOf = (grid) => (grid.oddBars ?? []).filter((odd) => odd.bar >= 1);
67
+ /** The tick a bar begins on. */
68
+ export const barStartOf = (grid, index) => {
69
+ if (index <= 0)
70
+ return 0;
71
+ let start = grid.firstBarTicks + (index - 1) * grid.measureTicks;
72
+ for (const odd of oddBarsOf(grid)) {
73
+ if (odd.bar >= index)
74
+ break;
75
+ start += odd.ticks - grid.measureTicks;
76
+ }
77
+ return start;
78
+ };
79
+ /** How long a bar is: the first and any odd bar have their own length, the rest the metre's. */
80
+ export const barTicksOf = (grid, index) => index === 0
81
+ ? grid.firstBarTicks
82
+ : (oddBarsOf(grid).find((odd) => odd.bar === index)?.ticks ?? grid.measureTicks);
83
+ /** Which bar a tick falls in. Ticks before the piece belong to its first bar. */
84
+ export const barAt = (grid, tick) => {
85
+ if (tick < grid.firstBarTicks)
86
+ return 0;
87
+ let bar = 1;
88
+ let start = grid.firstBarTicks;
89
+ for (const odd of oddBarsOf(grid)) {
90
+ const oddStart = start + (odd.bar - bar) * grid.measureTicks;
91
+ if (tick < oddStart)
92
+ break;
93
+ if (tick < oddStart + odd.ticks)
94
+ return odd.bar;
95
+ bar = odd.bar + 1;
96
+ start = oddStart + odd.ticks;
97
+ }
98
+ return bar + Math.floor((tick - start) / grid.measureTicks);
99
+ };
100
+ /** How many bars a piece of `totalTicks` takes, the last one counted however little it holds. */
101
+ export const barCountOf = (grid, totalTicks) => totalTicks <= grid.firstBarTicks ? 1 : barAt(grid, totalTicks - 1) + 1;
102
+ /** Longest first — `decompose` takes the first that fits its own grid. */
103
+ const VALUES = [
104
+ { ticks: 64, duration: 'w', dots: 0 },
105
+ { ticks: 56, duration: 'h', dots: 2 },
106
+ { ticks: 48, duration: 'h', dots: 1 },
107
+ { ticks: 32, duration: 'h', dots: 0 },
108
+ { ticks: 28, duration: 'q', dots: 2 },
109
+ { ticks: 24, duration: 'q', dots: 1 },
110
+ { ticks: 16, duration: 'q', dots: 0 },
111
+ { ticks: 14, duration: '8', dots: 2 },
112
+ { ticks: 12, duration: '8', dots: 1 },
113
+ { ticks: 8, duration: '8', dots: 0 },
114
+ // Seven ticks are one head, not a dotted sixteenth tied to a sixty-fourth: Level Complete, pulse 2,
115
+ // where the two heads stood so close the tie vanished and one B3 read as struck twice.
116
+ { ticks: 7, duration: '16', dots: 2 },
117
+ { ticks: 6, duration: '16', dots: 1 },
118
+ { ticks: 4, duration: '16', dots: 0 },
119
+ { ticks: 3, duration: '32', dots: 1 },
120
+ { ticks: 2, duration: '32', dots: 0 },
121
+ { ticks: 1, duration: '64', dots: 0 },
122
+ ];
123
+ /** What one written head costs. Dominates everything else: a tie is the ugliest thing on a staff. */
124
+ const PIECE_COST = 100;
125
+ /** A value begun off its own grid — a syncopation. Worth avoiding, never worth a tie. */
126
+ const OFF_GRID_COST = 10;
127
+ /** A value that hides beat three, the middle of a 4/4 bar. The last tie-break. */
128
+ const HIDES_MIDDLE_COST = 1;
129
+ function pieceCost(value, pos, measureTicks) {
130
+ // A dotted value sits on the grid of the value it dots: two thirds of its length with one dot,
131
+ // four sevenths with two.
132
+ const grid = (value.ticks * 2 ** value.dots) / (2 ** (value.dots + 1) - 1);
133
+ const half = measureTicks / 2;
134
+ const hidesMiddle = pos % measureTicks !== 0 && pos < half && pos + value.ticks > half;
135
+ return (PIECE_COST + (pos % grid === 0 ? 0 : OFF_GRID_COST) + (hidesMiddle ? HIDES_MIDDLE_COST : 0));
136
+ }
137
+ const decomposeCache = new Map();
138
+ /**
139
+ * A length as a sum of writable note values, from a position in the bar.
140
+ *
141
+ * The FEWEST heads, and only then the tidiest. The first version was greedy and
142
+ * took a value only when it started on its own grid, which reads well in a
143
+ * rulebook and terribly on a staff: four ticks from the second thirty-second of
144
+ * a beat came out as a dotted thirty-second tied to a sixty-fourth — a
145
+ * sixteenth written as two heads, which nobody writes. Here the head count is
146
+ * minimised outright (a sixteenth begun off the beat is a syncopation, and a
147
+ * syncopation is one head), and starting on the grid and keeping beat three
148
+ * visible only break ties between spellings with the same count.
149
+ */
150
+ export function decompose(position, length, measureTicks = MEASURE_TICKS) {
151
+ return solve(((position % measureTicks) + measureTicks) % measureTicks, length, measureTicks).pieces.slice();
152
+ }
153
+ function solve(pos, left, measureTicks) {
154
+ if (left <= 0)
155
+ return { cost: 0, pieces: [], offGrid: 0 };
156
+ const key = (measureTicks * 4096 + pos) * 4096 + left;
157
+ const cached = decomposeCache.get(key);
158
+ if (cached)
159
+ return cached;
160
+ let best = null;
161
+ // Longest first, and only a strictly cheaper spelling replaces one found
162
+ // earlier — so among equals the long value comes first.
163
+ for (const value of VALUES) {
164
+ if (value.ticks > left)
165
+ continue;
166
+ const rest = solve((pos + value.ticks) % measureTicks, left - value.ticks, measureTicks);
167
+ const own = pieceCost(value, pos, measureTicks);
168
+ const cost = own + rest.cost;
169
+ if (!best || cost < best.cost) {
170
+ const offGrid = rest.offGrid + (own - PIECE_COST >= OFF_GRID_COST ? 1 : 0);
171
+ best = { cost, pieces: [value, ...rest.pieces], offGrid };
172
+ }
173
+ }
174
+ decomposeCache.set(key, best);
175
+ return best;
176
+ }
177
+ /**
178
+ * A voice's notes as a continuous ribbon: rests where it is silent, cut at
179
+ * every bar line, and every piece already a writable value.
180
+ *
181
+ * Monophonic by construction — the driver walks one stream per channel — so an
182
+ * overlap can only mean the previous note is still counted as sounding, and the
183
+ * cursor simply moves on rather than stacking a chord that the hardware cannot
184
+ * play.
185
+ */
186
+ /**
187
+ * The divisions a tuplet may fill: a written value, so the bracket spans
188
+ * something a reader can name. Three heads in the time of two.
189
+ */
190
+ const TUPLET_TOTALS = [8, 16, 32, 64];
191
+ /**
192
+ * Where the music divides a written value into THREE.
193
+ *
194
+ * One tick is a sixty-fourth, so a third of anything is not a whole number and
195
+ * the driver cannot hold a true triplet: it writes the nearest integers it can,
196
+ * and the give-away is a run of three that sums to a written value and divides
197
+ * as evenly as integers allow without dividing evenly. A quarter triplet is
198
+ * 10+11+11 of the half's 32 ticks; a sixteenth triplet is 2+3+3 of the
199
+ * eighth's 8. Spelt by `decompose` instead, each of those becomes two heads
200
+ * under a tie, and a bar of them is unreadable — which is how this was found.
201
+ *
202
+ * The rule is deliberately narrow, and it is checked against the ROM rather
203
+ * than reasoned about: over all twenty tracks it fires exactly twice — title
204
+ * music's bar 39, where pulse 1, pulse 2, the triangle and the drums all take
205
+ * it together, and Clinger Winger's drums, once a bar for five bars. Nothing
206
+ * else in the cartridge matches, so nothing else is re-spelt.
207
+ *
208
+ * Even thirds are NOT tuplets: three heads of the same written value need no
209
+ * bracket, and the ROM has none inside these totals anyway.
210
+ */
211
+ export function tupletGroups(notes, grid = barGrid()) {
212
+ const out = new Map();
213
+ for (let at = 0; at + 2 < notes.length; at++) {
214
+ const run = notes.slice(at, at + 3);
215
+ const ticks = run[0].ticks + run[1].ticks + run[2].ticks;
216
+ if (!TUPLET_TOTALS.includes(ticks))
217
+ continue;
218
+ // Adjacent, and inside one bar: a bracket cannot cross a bar line.
219
+ if (run.some((note, index) => index > 0 && run[index - 1].start + run[index - 1].ticks !== note.start))
220
+ continue;
221
+ const bar = barAt(grid, run[0].start);
222
+ if (run[0].start - barStartOf(grid, bar) > barTicksOf(grid, bar) - ticks)
223
+ continue;
224
+ const lengths = run.map((note) => note.ticks);
225
+ if (Math.max(...lengths) - Math.min(...lengths) > 1)
226
+ continue;
227
+ if (lengths.some((length) => length * 3 === ticks))
228
+ continue;
229
+ // Three in the time of two, so each head is written as half the division.
230
+ const value = VALUES.find((candidate) => candidate.ticks === ticks / 2 && candidate.dots === 0);
231
+ if (!value)
232
+ continue;
233
+ run.forEach((_, index) => out.set(at + index, {
234
+ value,
235
+ span: { num: 3, occupied: 2, first: index === 0, last: index === 2 },
236
+ }));
237
+ at += 2;
238
+ }
239
+ return out;
240
+ }
241
+ /**
242
+ * Spans an exact tuplet may fill, in ticks, in the order they are tried: the beat first, then
243
+ * shorter, then longer. A bracket over a beat reads better than two over its halves — unless one
244
+ * half needs none (`plainHalf`).
245
+ */
246
+ const TUPLET_SPANS = [16, 8, 4, 32, 64];
247
+ /** How many notes a tuplet may divide its span into — every count that is not a power of two. */
248
+ const TUPLET_COUNTS = [3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15];
249
+ /**
250
+ * Each count with the parts its step may be split into: whole steps for every count first, then
251
+ * halves, then quarters — Alien 3 opens a 9:8 of thirty-seconds with four sixty-fourths. A grid
252
+ * that whole steps of some count already write is read as that count: six sixteenths in a quarter
253
+ * are a sextuplet, not a triplet of eighths split in two.
254
+ */
255
+ const TUPLET_GRIDS = [1, 2, 4].flatMap((split) => TUPLET_COUNTS.map((num) => [num, split]));
256
+ /** How close a start must be to a tuplet's grid, in ticks, to stand on it. */
257
+ const ON_GRID = 1e-6;
258
+ /** The notes a tuplet of `num` is written in the time of: the largest power of two below it. */
259
+ export const tupletOccupied = (num) => 2 ** Math.floor(Math.log2(num - 1e-9));
260
+ const near = (value) => Math.abs(value - Math.round(value)) < ON_GRID;
261
+ /**
262
+ * Tuplets of any count, read from notes whose starts and ends divide a written value exactly.
263
+ *
264
+ * A sound driver counts whole ticks, and its triplets are 10 + 11 + 11 (`tupletGroups`). Music from
265
+ * anywhere else can say what it means: five notes from 0 to 16, each 3.2 ticks — a quintuplet of
266
+ * sixteenths in a quarter. A run is read as a tuplet of `num` in the time of `tupletOccupied(num)`
267
+ * when every start and end in it falls on its grid of `span / num`, the run fills `span` from a
268
+ * point that span may start on, and at least one of its edges is off the grid of whole ticks — so a
269
+ * line of ordinary values is never re-read. Inside, notes may be any whole number of the grid's
270
+ * steps — a quarter and an eighth in an eighth triplet — or of a half or a quarter of a step, and
271
+ * the steps no note takes are rests.
272
+ *
273
+ * Whole-tick music never has one: no span here divides by three, five or seven into whole ticks.
274
+ */
275
+ function exactTuplets(notes, grid) {
276
+ const runs = new Map();
277
+ for (let from = 0; from < notes.length; from++) {
278
+ const begin = notes[from].start;
279
+ const bar = barAt(grid, begin + ON_GRID);
280
+ const barStart = barStartOf(grid, bar);
281
+ const barEnd = barStart + barTicksOf(grid, bar);
282
+ let found = null;
283
+ for (const span of TUPLET_SPANS) {
284
+ if (found)
285
+ break;
286
+ // On the grid the span itself keeps — a beat for anything a beat or longer.
287
+ const step = Math.min(span, TICKS_PER_QUARTER);
288
+ for (let start = Math.floor((begin - barStart) / step) * step + barStart; start <= begin + ON_GRID && !found; start += step) {
289
+ if (start + span > barEnd + ON_GRID)
290
+ break;
291
+ for (const [num, split] of TUPLET_GRIDS) {
292
+ const cells = num * split;
293
+ const unit = span / cells;
294
+ const run = [];
295
+ for (let j = from; j < notes.length && notes[j].start < start + span - ON_GRID; j++)
296
+ run.push(notes[j]);
297
+ if (!run.length || Math.abs(run[0].start - begin) > ON_GRID)
298
+ continue;
299
+ // An earlier note that reaches into the span leaves it no clean start.
300
+ if (from > 0 && notes[from - 1].start + notes[from - 1].ticks > start + ON_GRID)
301
+ continue;
302
+ const steps = run.map((note) => [(note.start - start) / unit, (note.start + note.ticks - start) / unit]);
303
+ if (!steps.every(([a, b]) => near(a) && near(b) && b <= cells + ON_GRID && a >= -ON_GRID))
304
+ continue;
305
+ const edges = run.flatMap((note) => [note.start, note.start + note.ticks]);
306
+ if (edges.every((edge) => near(edge)))
307
+ continue;
308
+ // A half of the span that ordinary values write on their own wants a shorter bracket: a
309
+ // sixteenth triplet and an eighth are a triplet over the first half, not a sextuplet.
310
+ const plainHalf = (lo, hi) => run
311
+ .filter((note) => note.start < hi - ON_GRID && note.start + note.ticks > lo + ON_GRID)
312
+ .every((note) => note.start >= lo - ON_GRID &&
313
+ note.start + note.ticks <= hi + ON_GRID &&
314
+ near(note.start) &&
315
+ near(note.start + note.ticks));
316
+ if (span > 4 && (plainHalf(start, start + span / 2) || plainHalf(start + span / 2, start + span)))
317
+ continue;
318
+ // The pieces: notes and the steps between them, each a value one head writes.
319
+ const occupied = tupletOccupied(num);
320
+ const written = span / occupied / split;
321
+ const pieces = [];
322
+ let at = 0;
323
+ for (let k = 0; k < run.length; k++) {
324
+ const a = Math.round(steps[k][0]);
325
+ const b = Math.round(steps[k][1]);
326
+ if (a < at)
327
+ break;
328
+ if (a > at)
329
+ pieces.push({ from: at, length: a - at, midi: null });
330
+ pieces.push({ from: a, length: b - a, midi: run[k].midi, at: run[k].start });
331
+ at = b;
332
+ }
333
+ if (at < cells)
334
+ pieces.push({ from: at, length: cells - at, midi: null });
335
+ const values = pieces.map((piece) => VALUES.find((value) => Math.abs(value.ticks - piece.length * written) < ON_GRID));
336
+ if (pieces.reduce((sum, piece) => sum + piece.length, 0) !== cells || values.some((value) => !value))
337
+ continue;
338
+ // Each piece from where it starts — a note's own start, so its marks find it — to the next.
339
+ const starts = pieces.map((piece, index) => piece.at !== undefined ? piece.at : index === 0 ? start : start + Math.round(piece.from * unit * 1e9) / 1e9);
340
+ const items = pieces.map((piece, index) => {
341
+ const pieceStart = starts[index];
342
+ const pieceEnd = index + 1 < pieces.length ? starts[index + 1] : start + span;
343
+ return {
344
+ start: pieceStart,
345
+ ticks: pieceEnd - pieceStart,
346
+ midi: piece.midi,
347
+ duration: values[index].duration,
348
+ dots: values[index].dots,
349
+ tieStart: false,
350
+ tieStop: false,
351
+ tuplet: { num, occupied, first: index === 0, last: index === pieces.length - 1 },
352
+ };
353
+ });
354
+ found = { to: from + run.length, start, span, items };
355
+ break;
356
+ }
357
+ }
358
+ }
359
+ if (found) {
360
+ runs.set(from, found);
361
+ from = found.to - 1;
362
+ }
363
+ }
364
+ return runs;
365
+ }
366
+ /**
367
+ * The exact tuplets, a run on the same grid that carries on to the beat read as one: two
368
+ * sixteenth triplets that fill a quarter are a sextuplet, as a reader writes six in a beat. Not
369
+ * past the beat — twelve in a half is four triplets.
370
+ */
371
+ function exactTupletsByBeat(notes, grid) {
372
+ const runs = exactTuplets(notes, grid);
373
+ const merged = new Map();
374
+ const ordered = [...runs.entries()].sort((a, b) => a[0] - b[0]);
375
+ for (let i = 0; i < ordered.length; i++) {
376
+ const [from, run] = ordered[i];
377
+ const next = ordered[i + 1];
378
+ const num = run.items[0].tuplet.num;
379
+ const double = num * 2;
380
+ if (next &&
381
+ run.span * 2 <= TICKS_PER_QUARTER &&
382
+ (run.start - barStartOf(grid, barAt(grid, run.start + ON_GRID))) % (run.span * 2) === 0 &&
383
+ TUPLET_COUNTS.includes(double) &&
384
+ next[0] === run.to &&
385
+ Math.abs(next[1].start - (run.start + run.span)) < ON_GRID &&
386
+ next[1].span === run.span &&
387
+ next[1].items[0].tuplet.num === num) {
388
+ const items = [...run.items, ...next[1].items].map((item, index, all) => ({
389
+ ...item,
390
+ tuplet: { num: double, occupied: tupletOccupied(double), first: index === 0, last: index === all.length - 1 },
391
+ }));
392
+ // The written value stays: the bracket counts the same steps, twice as many of them.
393
+ merged.set(from, { to: next[1].to, start: run.start, span: run.span * 2, items });
394
+ i++;
395
+ continue;
396
+ }
397
+ merged.set(from, run);
398
+ }
399
+ return merged;
400
+ }
401
+ export function staffItems(notes, totalTicks, grid = barGrid()) {
402
+ const items = [];
403
+ const ordered = [...notes].sort((a, b) => a.start - b.start);
404
+ let cursor = 0;
405
+ const push = (start, length, midi) => {
406
+ // Off the tick grid and in no tuplet — a run that does not fill its bracket: the nearest ticks.
407
+ let position = Math.round(start);
408
+ let left = Math.round(start + length) - position;
409
+ let first = true;
410
+ while (left > 0) {
411
+ const bar = barAt(grid, position);
412
+ const barStart = barStartOf(grid, bar);
413
+ const barTicks = barTicksOf(grid, bar);
414
+ const chunk = Math.min(left, barStart + barTicks - position);
415
+ const carriesOn = chunk < left;
416
+ const pieces = decompose(position - barStart, chunk, barTicks);
417
+ let at = position;
418
+ pieces.forEach((piece, index) => {
419
+ items.push({
420
+ start: at,
421
+ ticks: piece.ticks,
422
+ midi,
423
+ duration: piece.duration,
424
+ dots: piece.dots,
425
+ // A head is tied to the one before it when it is a continuation:
426
+ // of an earlier bar's remainder, or of the piece beside it.
427
+ tieStop: midi !== null && (!first || index > 0),
428
+ tieStart: midi !== null && (carriesOn || index < pieces.length - 1),
429
+ });
430
+ at += piece.ticks;
431
+ });
432
+ position += chunk;
433
+ left -= chunk;
434
+ first = false;
435
+ }
436
+ };
437
+ const tuplets = tupletGroups(ordered, grid);
438
+ // Exact tuplets only where some note is off the whole-tick grid: whole-tick music has none.
439
+ const exact = ordered.some((note) => !Number.isInteger(note.start) || !Number.isInteger(note.ticks))
440
+ ? exactTupletsByBeat(ordered, grid)
441
+ : null;
442
+ let skipTo = -1;
443
+ ordered.forEach((note, index) => {
444
+ if (index < skipTo)
445
+ return;
446
+ const run = exact?.get(index);
447
+ if (run) {
448
+ if (run.start > cursor)
449
+ push(cursor, run.start - cursor, null);
450
+ items.push(...run.items);
451
+ cursor = run.start + run.span;
452
+ skipTo = run.to;
453
+ return;
454
+ }
455
+ if (note.start > cursor)
456
+ push(cursor, note.start - cursor, null);
457
+ cursor = Math.max(cursor, note.start);
458
+ const length = Math.min(note.ticks, Math.max(0, totalTicks - cursor));
459
+ if (length <= 0)
460
+ return;
461
+ // A tuplet head is written whole, at the value the bracket says: its ticks
462
+ // are not a written value and `decompose` would tie it into scraps.
463
+ const head = tuplets.get(index);
464
+ if (head) {
465
+ items.push({
466
+ start: cursor,
467
+ ticks: length,
468
+ midi: note.midi,
469
+ duration: head.value.duration,
470
+ dots: 0,
471
+ tieStart: false,
472
+ tieStop: false,
473
+ tuplet: head.span,
474
+ });
475
+ cursor += length;
476
+ return;
477
+ }
478
+ push(cursor, length, note.midi);
479
+ cursor += length;
480
+ });
481
+ if (cursor < totalTicks)
482
+ push(cursor, totalTicks - cursor, null);
483
+ return items;
484
+ }
485
+ /**
486
+ * The ribbon cut into bars, each one filled to the bar line.
487
+ *
488
+ * A bar short of its four beats is not a bar — a formatter given one spaces it
489
+ * as though the missing time did not exist, and the staff stops lining up with
490
+ * the music beside it — so whatever is left at the end is padded with rests.
491
+ */
492
+ export function staffMeasures(items, totalTicks, grid = barGrid()) {
493
+ const count = Math.max(1, barCountOf(grid, totalTicks));
494
+ const measures = [];
495
+ // `staffItems` hands the ribbon over in order, and then each bar is the next
496
+ // run of it: one walk instead of a filter over the whole piece per bar.
497
+ let sorted = true;
498
+ for (let at = 1; at < items.length && sorted; at++)
499
+ sorted = items[at - 1].start <= items[at].start;
500
+ let next = 0;
501
+ for (let index = 0; index < count; index++) {
502
+ const start = barStartOf(grid, index);
503
+ const barTicks = barTicksOf(grid, index);
504
+ let inBar;
505
+ if (sorted) {
506
+ while (next < items.length && items[next].start < start)
507
+ next++;
508
+ inBar = [];
509
+ while (next < items.length && items[next].start < start + barTicks)
510
+ inBar.push(items[next++]);
511
+ }
512
+ else {
513
+ inBar = items.filter((item) => item.start >= start && item.start < start + barTicks);
514
+ }
515
+ const filled = inBar.reduce((sum, item) => sum + item.ticks, 0);
516
+ // Filled to within a hair is full: a tuplet's steps are sums of fractions.
517
+ if (barTicks - filled > ON_GRID) {
518
+ let at = start + filled;
519
+ for (const piece of decompose(filled, barTicks - filled, barTicks)) {
520
+ inBar.push({
521
+ start: at,
522
+ ticks: piece.ticks,
523
+ midi: null,
524
+ duration: piece.duration,
525
+ dots: piece.dots,
526
+ tieStart: false,
527
+ tieStop: false,
528
+ });
529
+ at += piece.ticks;
530
+ }
531
+ }
532
+ measures.push({ index, start, items: inBar });
533
+ }
534
+ return measures;
535
+ }
536
+ const STEPS = ['c', 'c', 'd', 'd', 'e', 'f', 'f', 'g', 'g', 'a', 'a', 'b'];
537
+ const FLAT_STEPS = ['c', 'd', 'd', 'e', 'e', 'f', 'g', 'g', 'a', 'a', 'b', 'b'];
538
+ const BLACK = [false, true, false, true, false, false, true, false, true, false, true, false];
539
+ /** The white keys with a flat spelling — B as C-flat, E as F-flat — by pitch class. */
540
+ const WHITE_FLATS = { 4: 'f', 11: 'c' };
541
+ /**
542
+ * A MIDI number as a VexFlow key — `c#/4` for 61, `db/4` in a key that flats D.
543
+ *
544
+ * A black key is spelt the way the KEY SIGNATURE has it when the signature
545
+ * alters either of its letters, and as a sharp otherwise. A flat key spelt in
546
+ * sharps prints D-sharp against a signature that flattens E — a sign on nearly
547
+ * every note — which is what rat race looked like before flats could be written.
548
+ * Spelling EVERY black key flat in a flat key was the next mistake: Volkmire's
549
+ * Inferno, in E-flat, climbs F to F-sharp, and printing that as G-flat put a
550
+ * flat on a note a reader took for a B-flat already in the key.
551
+ *
552
+ * Two white keys have a flat spelling of their own: B is C-flat and E is F-flat. They take it only
553
+ * in a key that flattens that letter — six flats for C-flat. Rat race is in E-flat minor, whose
554
+ * sixth degree is C-flat; written as B it printed a natural against the B-flat of the signature.
555
+ */
556
+ export function staffKey(midi, key) {
557
+ const index = ((midi % 12) + 12) % 12;
558
+ const octave = Math.floor(midi / 12) - 1;
559
+ const flatWhite = WHITE_FLATS[index];
560
+ if (flatWhite && key?.accidental === 'b' && key.letters.includes(flatWhite)) {
561
+ // C-flat sits in the octave above its B: C-flat 4 is B3.
562
+ return `${flatWhite}b/${index === 11 ? octave + 1 : octave}`;
563
+ }
564
+ if (!BLACK[index])
565
+ return `${STEPS[index]}/${octave}`;
566
+ const flat = (key?.accidental === 'b' && key.letters.includes(FLAT_STEPS[index])) ||
567
+ (key?.spelt?.includes(FLAT_STEPS[index]) ?? false);
568
+ return flat ? `${FLAT_STEPS[index]}b/${octave}` : `${STEPS[index]}#/${octave}`;
569
+ }
570
+ /** Letters by diatonic step, and the steps an interval of n semitones spans when spelt plainly. */
571
+ const LETTER_STEP = { c: 0, d: 1, e: 2, f: 3, g: 4, a: 5, b: 6 };
572
+ const PLAIN_STEPS = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6];
573
+ /** The other spelling of a black key: `c#/4` ↔ `db/4`. Null for a white key. */
574
+ function otherSpelling(midi, spelt) {
575
+ const index = ((midi % 12) + 12) % 12;
576
+ if (!BLACK[index])
577
+ return null;
578
+ const octave = Math.floor(midi / 12) - 1;
579
+ return spelt.includes('#') ? `${FLAT_STEPS[index]}b/${octave}` : `${STEPS[index]}#/${octave}`;
580
+ }
581
+ /** Diatonic position of a VexFlow key: letter step plus seven a an octave. */
582
+ const stepOf = (key) => {
583
+ const [name, octave] = key.split('/');
584
+ return LETTER_STEP[name[0]] + 7 * Number(octave);
585
+ };
586
+ /**
587
+ * The heads of a chord as VexFlow keys, each black key spelt so the chord reads as intervals do.
588
+ *
589
+ * `staffKey` spells one note against the key, and for a line that is right: Volkmire's Inferno, in
590
+ * E-flat, climbs F to F-sharp. A chord is read the other way — by the intervals between its heads.
591
+ * Paganini's Caprice 17, in E-flat, strikes E-flat with G-flat: written E-flat and F-sharp, that is
592
+ * an augmented second where a minor third sounds. So for the black keys of a chord both spellings
593
+ * are tried, and the one whose intervals span the letters their size asks for wins — a third across
594
+ * three letters, a second across two; where the intervals cannot tell, `staffKey` decides. In the
595
+ * order given.
596
+ */
597
+ export function chordKeys(midis, key) {
598
+ const plain = midis.map((midi) => staffKey(midi, key));
599
+ const choices = midis.map((midi, i) => {
600
+ const other = otherSpelling(midi, plain[i]);
601
+ return other ? [plain[i], other] : [plain[i]];
602
+ });
603
+ const cost = (keys) => {
604
+ let wrong = 0;
605
+ for (let i = 0; i < keys.length; i++) {
606
+ for (let j = i + 1; j < keys.length; j++) {
607
+ const [lo, hi] = midis[i] <= midis[j] ? [i, j] : [j, i];
608
+ const semitones = midis[hi] - midis[lo];
609
+ const steps = stepOf(keys[hi]) - stepOf(keys[lo]);
610
+ const expected = PLAIN_STEPS[semitones % 12] + 7 * Math.floor(semitones / 12);
611
+ if (steps !== expected)
612
+ wrong++;
613
+ }
614
+ }
615
+ return wrong;
616
+ };
617
+ let best = plain;
618
+ let bestCost = cost(plain);
619
+ const walk = (at, picked) => {
620
+ if (at === choices.length) {
621
+ const c = cost(picked);
622
+ if (c < bestCost) {
623
+ bestCost = c;
624
+ best = picked.slice();
625
+ }
626
+ return;
627
+ }
628
+ for (const option of choices[at])
629
+ walk(at + 1, [...picked, option]);
630
+ };
631
+ if (choices.some((options) => options.length > 1))
632
+ walk(0, []);
633
+ return best;
634
+ }
635
+ const SHARP_ORDER = ['f', 'c', 'g', 'd', 'a', 'e', 'b'];
636
+ const FLAT_ORDER = ['b', 'e', 'a', 'd', 'g', 'c', 'f'];
637
+ /** Signed number of sharps (+) or flats (−) by VexFlow major key name. */
638
+ const KEY_ALTERATIONS = {
639
+ C: 0,
640
+ G: 1,
641
+ D: 2,
642
+ A: 3,
643
+ E: 4,
644
+ B: 5,
645
+ 'F#': 6,
646
+ 'C#': 7,
647
+ F: -1,
648
+ Bb: -2,
649
+ Eb: -3,
650
+ Ab: -4,
651
+ Db: -5,
652
+ Gb: -6,
653
+ Cb: -7,
654
+ };
655
+ /**
656
+ * A key signature by its VexFlow name (`G` one sharp, `Eb` three flats).
657
+ * A name it does not know is C — written with every sign at its note rather
658
+ * than guessed at.
659
+ */
660
+ export function keyOf(name) {
661
+ const [signature, ...flats] = (name ?? '').split('+');
662
+ const count = signature ? (KEY_ALTERATIONS[signature] ?? 0) : 0;
663
+ const key = count < 0
664
+ ? { accidental: 'b', letters: FLAT_ORDER.slice(0, -count) }
665
+ : { accidental: '#', letters: SHARP_ORDER.slice(0, count) };
666
+ return flats.length ? { ...key, spelt: flats.map((flat) => flat[0].toLowerCase()) } : key;
667
+ }
668
+ /**
669
+ * The signature part of a key name, the one VexFlow is given — `F` of `F+Db+Ab`.
670
+ *
671
+ * What follows a `+` is spelling, not signature: black keys to write as those flats although the
672
+ * key does not flatten their letters. Turbo tunnel, in D minor with one flat, falls D, D-flat, C in
673
+ * the bass; the rule for black keys wrote the middle chord as C-sharp and G-sharp.
674
+ */
675
+ export const signatureName = (name) => name?.split('+')[0];
676
+ /**
677
+ * Treble or bass for a voice, from where it actually sits.
678
+ *
679
+ * The median rather than the mean: one low pickup note must not drag a melody
680
+ * onto ledger lines below the bass staff. The boundary is middle C's octave —
681
+ * a voice whose middle is below A3 spends more of its time under the treble
682
+ * staff than in it.
683
+ */
684
+ export function chooseClef(notes) {
685
+ if (notes.length === 0)
686
+ return 'treble';
687
+ const pitches = notes.map((note) => note.midi).sort((a, b) => a - b);
688
+ const median = pitches[Math.floor(pitches.length / 2)];
689
+ return median < 57 ? 'bass' : 'treble';
690
+ }
691
+ /**
692
+ * How hard a voice is to read from a given bar line: its heads, plus one for
693
+ * every head that starts off its own grid.
694
+ *
695
+ * The measureTicks `voicePhase` minimises. A run of ticks written as
696
+ * one note on the beat costs 1. The syncopation counts as much as a tie, and
697
+ * that is deliberate: `decompose` writes an off-beat run as ONE head, so by
698
+ * heads alone every bar line would look equally good, and the search would
699
+ * happily put the whole piece a sixteenth off the beat.
700
+ */
701
+ export function notationCost(notes, offset, measureTicks = MEASURE_TICKS) {
702
+ let pieces = 0;
703
+ for (const note of notes) {
704
+ // A tuplet's note is off the tick grid, and counts at the whole ticks nearest it: left out,
705
+ // a voice of Alien 3's 9:8 figures was read from its few plain notes, which said 3 ticks late.
706
+ const start = Math.round(note.start);
707
+ // + 2 bars so a shift past the first note still lands on a positive tick.
708
+ // Measured on the plain grid: what it weighs is the phase of a voice
709
+ // against the beat, which the opening bar's own length does not change.
710
+ let position = start - offset + measureTicks * 2;
711
+ let left = Math.max(1, Math.round(note.start + note.ticks) - start);
712
+ while (left > 0) {
713
+ const inBar = position % measureTicks;
714
+ const chunk = Math.min(left, measureTicks - inBar);
715
+ pieces += spellingCost(inBar, chunk, measureTicks);
716
+ position += chunk;
717
+ left -= chunk;
718
+ }
719
+ }
720
+ return pieces;
721
+ }
722
+ /**
723
+ * `solve`'s heads plus off-grid heads for a run inside one bar, from a flat
724
+ * table per bar length. `voicePhase` asks this for every piece of every note,
725
+ * four times over, and a `Map` keyed by a number past 2^31 is the slow part of
726
+ * that — so whole-tick runs are looked up by index instead.
727
+ */
728
+ const costTables = new Map();
729
+ function spellingCost(inBar, chunk, measureTicks) {
730
+ if (!Number.isInteger(measureTicks) ||
731
+ !Number.isInteger(inBar) ||
732
+ !Number.isInteger(chunk) ||
733
+ inBar < 0) {
734
+ const spelling = solve(inBar, chunk, measureTicks);
735
+ return spelling.pieces.length + spelling.offGrid;
736
+ }
737
+ let table = costTables.get(measureTicks);
738
+ if (!table) {
739
+ table = new Int32Array(measureTicks * (measureTicks + 1));
740
+ costTables.set(measureTicks, table);
741
+ }
742
+ const index = inBar * (measureTicks + 1) + chunk;
743
+ let cost = table[index];
744
+ if (cost === 0) {
745
+ const spelling = solve(inBar, chunk, measureTicks);
746
+ // Stored one up, so 0 can mean "not worked out yet".
747
+ cost = spelling.pieces.length + spelling.offGrid + 1;
748
+ table[index] = cost;
749
+ }
750
+ return cost - 1;
751
+ }
752
+ /**
753
+ * How many ticks one channel is written behind the beat. Never more than
754
+ * `MAX_PHASE`.
755
+ *
756
+ * The delay is AUTHORED, not the driver's. Nearly every track opens a channel
757
+ * or two with an explicit `$80` rest one sixty-fourth long, and Intruder
758
+ * Excluder puts a three-tick rest ahead of pulse 2's copy of pulse 1's bass
759
+ * line — a two-tick echo, some 60ms, and audible as one. (An earlier version of
760
+ * this comment called it the cost of setup commands; the stream dump says
761
+ * otherwise.)
762
+ *
763
+ * On a staff, though, it is the difference between a quarter note and a chain
764
+ * of tied scraps, so the notes are written on the beat and the delay is
765
+ * STATED: a staff should label every voice it moved, and not move its playhead.
766
+ * Longer echoes (Surf City's six ticks) are past `MAX_PHASE` and are written as
767
+ * they sound.
768
+ */
769
+ export const MAX_PHASE = 3;
770
+ export function voicePhase(notes, offset, measureTicks = MEASURE_TICKS) {
771
+ let best = 0;
772
+ let bestCost = Infinity;
773
+ for (let phase = 0; phase <= MAX_PHASE; phase++) {
774
+ const cost = notationCost(notes, offset + phase, measureTicks);
775
+ if (cost < bestCost) {
776
+ bestCost = cost;
777
+ best = phase;
778
+ }
779
+ }
780
+ return best;
781
+ }
782
+ /**
783
+ * A voice moved `by` ticks along the staff's clock, cut at tick 0.
784
+ *
785
+ * Taking a channel's delay out can carry its first sliver before the music
786
+ * starts: ragnarok's canyon opens the noise channel with a one-tick hit on
787
+ * tick 0 and then plays everything a tick late. Refusing the phase for the sake
788
+ * of that sliver — which is what a guard in `voicePhase` once did — left every
789
+ * drum on an odd tick, and each of those gave the formatter a column of
790
+ * its own: a gap between the first two sixteenths of pulse 1 in every bar. So
791
+ * the phase is taken, and what lands before tick 0 is cut off there.
792
+ */
793
+ export function shiftNotes(notes, by) {
794
+ const out = [];
795
+ for (const note of notes) {
796
+ const start = note.start + by;
797
+ const ticks = start < 0 ? note.ticks + start : note.ticks;
798
+ if (ticks > 0)
799
+ out.push({ ...note, start: Math.max(0, start), ticks });
800
+ }
801
+ return out;
802
+ }
803
+ /*
804
+ * Where the bar line falls: at the track's FIRST TICK. There is no search.
805
+ *
806
+ * There used to be — a search tried all 64 bar lines and kept the one that
807
+ * wrote the music with the fewest heads, on the reasoning a transcriber uses by
808
+ * ear. It is blind to exactly the mistake that matters: this music repeats in
809
+ * whole-bar patterns on a sixteenth grid, so a bar line an eighth or two off
810
+ * writes it with almost the same number of heads. Ragnarok's canyon came out at
811
+ * 17 (1060 heads) against 1 (1064), and every bar of the score opened two
812
+ * eighths early — heard wrong at once, while the numbers called it a win. Nine
813
+ * of the twenty tracks were moved off tick 0 that way, most by a margin under a
814
+ * percent.
815
+ *
816
+ * Tick 0 is what the evidence says instead: every channel of a track starts
817
+ * there, the repeat points sit on ticks 1..4 in most tracks, and the MusicXML
818
+ * export has always barred from tick 0, and the two agree. A channel's own authored delay (at most `MAX_PHASE` ticks) is still
819
+ * taken out per voice by `voicePhase`.
820
+ */
821
+ /**
822
+ * Ticks to ADD to a piece's own clock to get the staff's, for a bar line
823
+ * `offset` ticks in.
824
+ *
825
+ * Less than a bar, so a piece barred off its first tick gets a pickup bar
826
+ * rather than notes before tick zero — and nothing at all for offset 0, which
827
+ * is what every track uses: an empty bar in front of the music says the music
828
+ * starts a bar late.
829
+ */
830
+ export function staffShift(offset, measureTicks = MEASURE_TICKS) {
831
+ return (measureTicks - (offset % measureTicks)) % measureTicks;
832
+ }
833
+ /**
834
+ * How a piece is barred: where its bar lines fall, and what its notes are moved
835
+ * by to sit behind them.
836
+ *
837
+ * Two readings of the same pickup. Left alone it gets a bar of its own, padded
838
+ * in front with rests, and the music starts part way into bar one. `merged`
839
+ * instead makes the OPENING BAR longer by the pickup and moves nothing: the run
840
+ * and the note it lands on are one bar of 19/32, and the metre takes over after
841
+ * it. The second reads better whenever the pickup is not a whole phrase — a bar
842
+ * line through the middle of a run is worse than an irregular first bar.
843
+ */
844
+ export function barring(offset, measureTicks = MEASURE_TICKS, merged = false,
845
+ /** Bars later in the piece with a length of their own — see `BarGrid`. */
846
+ oddBars = []) {
847
+ const pickup = ((offset % measureTicks) + measureTicks) % measureTicks;
848
+ return merged
849
+ ? { grid: barGrid(measureTicks, measureTicks + pickup, oddBars), shift: 0 }
850
+ : {
851
+ grid: barGrid(measureTicks, measureTicks, oddBars),
852
+ shift: staffShift(offset, measureTicks),
853
+ };
854
+ }
855
+ /**
856
+ * The outer LINES of each staff, as MIDI numbers: E4..F5 and G2..A3.
857
+ *
858
+ * Not the centres — what matters for room is where the ledger lines start.
859
+ */
860
+ export const STAFF_SPAN = {
861
+ treble: [64, 77],
862
+ bass: [43, 57],
863
+ };
864
+ /**
865
+ * The pitch each staff is centred on: B4 on the treble, D3 on the bass. Kept
866
+ * for callers that want the middle line; the clef is not chosen by it.
867
+ */
868
+ export const CLEF_CENTRE = { treble: 71, bass: 50 };
869
+ /**
870
+ * How much better the other clef has to be before a bar switches, in semitones
871
+ * of the ladder it saves.
872
+ *
873
+ * Without it a line hovering between the two staves changes clef every other
874
+ * bar, which is unreadable in a way that ledger lines are not.
875
+ */
876
+ export const CLEF_HYSTERESIS = 3;
877
+ /**
878
+ * How much better a clef change PART WAY through a bar has to be than the best
879
+ * clef for the whole bar, in semitones of the ladder it saves.
880
+ *
881
+ * Higher than `CLEF_HYSTERESIS`, because a change inside a bar is louder. At 3,
882
+ * a line that crosses between the staves on every beat changed clef inside
883
+ * bar after bar — Ragnarok's canyon went treble, bass, treble, bass through
884
+ * bars 5 to 9 — for ladders that were only a few rungs shorter. Measured over
885
+ * the twenty tracks' voices: at 3, 92 changes inside bars; at 6, the bar that
886
+ * drops an octave still splits and the see-saw is gone.
887
+ */
888
+ export const CLEF_SPLIT_MARGIN = 6;
889
+ /**
890
+ * What a bar costs on one clef: the TALLEST ladder of ledger lines in it, with
891
+ * the average as a tie-break.
892
+ *
893
+ * The tallest, not the average, and that is the whole rule. Averaging is what a
894
+ * first version did, and it reads the bar as a crowd: a bar of eight low notes
895
+ * and two very high ones votes for the low clef, and the two high notes then
896
+ * get a fourteen-rung ladder that runs off the top of the strip. What decides
897
+ * whether a bar can be READ is its worst excursion — one note an octave out is
898
+ * two ledger lines and fine, one note two octaves out is unreadable however
899
+ * many of its neighbours sit comfortably.
900
+ *
901
+ * Measured over the twenty tracks, choosing by the worst excursion instead of
902
+ * the average halves the bars carrying a ladder over 18 semitones (37 → 17) and
903
+ * takes fewer clef changes to do it (129 → 112).
904
+ */
905
+ /** How far a pitch sits off a staff, in semitones: 0 on it. */
906
+ function outside(midi, clef) {
907
+ const [low, high] = STAFF_SPAN[clef];
908
+ return Math.max(0, midi - high, low - midi);
909
+ }
910
+ function clefCost(pitches, clef) {
911
+ const worst = pitches.reduce((max, midi) => Math.max(max, outside(midi, clef)), 0);
912
+ const mean = pitches.reduce((sum, midi) => sum + outside(midi, clef), 0) / pitches.length;
913
+ // The average never outweighs a semitone of the worst ladder; it only settles
914
+ // bars the worst excursion calls a draw.
915
+ return worst + mean / 100;
916
+ }
917
+ /**
918
+ * A clef per bar — a real score changes clef, and for this music it has to.
919
+ *
920
+ * These channels are not instruments with a range: one voice carries the bass
921
+ * for eight bars and then a melody three octaves up, and on one fixed clef a
922
+ * TENTH of all the notes in the cartridge land more than an octave off the
923
+ * staff — a ladder of ledger lines, most of it clipped by the strip's edge.
924
+ * Chosen per bar the same figure is 1.35%, at a cost of one clef change every
925
+ * twelve bars. What a bar costs on each clef is `clefCost`.
926
+ *
927
+ * A bar with nothing in it keeps the running clef rather than voting: a rest is
928
+ * not evidence about register, and letting silence switch the clef puts the
929
+ * change in the wrong bar.
930
+ *
931
+ * A bar can also change clef PART WAY THROUGH, once, where the music drops or
932
+ * climbs into the other register. Chosen for the bar as a whole, a register
933
+ * change inside it is paid for by every note on the wrong side of it: bar 5 of
934
+ * Ragnarok's canyon is eleven E4s and then five E3s, and the whole bar went to
935
+ * the bass clef — eleven heads on ledger lines to spare five. A split is taken
936
+ * when it beats the best whole-bar clef by `CLEF_SPLIT_MARGIN`, and only from
937
+ * the clef the bar opens in, so a bar never carries two changes.
938
+ */
939
+ export function clefPlan(bars, start = 'treble') {
940
+ const plan = [];
941
+ let current = null;
942
+ for (const notes of bars) {
943
+ if (notes.length === 0) {
944
+ plan.push({ clef: current ?? start });
945
+ continue;
946
+ }
947
+ const pitches = notes.map((note) => note.midi);
948
+ const bass = clefCost(pitches, 'bass');
949
+ const treble = clefCost(pitches, 'treble');
950
+ let want = bass <= treble ? 'bass' : 'treble';
951
+ if (current && want !== current && Math.abs(bass - treble) < CLEF_HYSTERESIS)
952
+ want = current;
953
+ const from = current ?? start;
954
+ const split = bestSplit(notes, from);
955
+ if (split && split.cost + CLEF_SPLIT_MARGIN <= clefCost(pitches, want)) {
956
+ current = otherClef(from);
957
+ plan.push({ clef: from, change: { tick: split.tick, clef: current } });
958
+ }
959
+ else {
960
+ current = want;
961
+ plan.push({ clef: want });
962
+ }
963
+ }
964
+ return plan;
965
+ }
966
+ /** The clef a note starting on `tick` of the bar is written in. */
967
+ export function clefAt(bar, tick) {
968
+ return bar.change && tick >= bar.change.tick ? bar.change.clef : bar.clef;
969
+ }
970
+ /** The clef a bar hands on to the next one. */
971
+ export function clefAfter(bar) {
972
+ return bar.change?.clef ?? bar.clef;
973
+ }
974
+ function otherClef(clef) {
975
+ return clef === 'treble' ? 'bass' : 'treble';
976
+ }
977
+ /**
978
+ * The cheapest place to leave `from` for the other clef inside a bar, and what
979
+ * the worse half of the bar then costs.
980
+ *
981
+ * Only at a note: the change goes in front of the first head it is for. Never
982
+ * at the first one — a bar that is all in the other clef changes at its bar
983
+ * line. On a tie the LATER place wins, so the clef stands next to the notes
984
+ * that need it rather than in front of ones that were fine either way.
985
+ */
986
+ function bestSplit(notes, from) {
987
+ // Each half's `clefCost` from running totals rather than a filter per place:
988
+ // the worst ladder and the sum of the ladders before it, and from it on.
989
+ const ordered = [...notes].sort((a, b) => a.tick - b.tick);
990
+ const n = ordered.length;
991
+ const to = otherClef(from);
992
+ const worstBefore = new Float64Array(n + 1);
993
+ const sumBefore = new Float64Array(n + 1);
994
+ for (let i = 0; i < n; i++) {
995
+ const out = outside(ordered[i].midi, from);
996
+ worstBefore[i + 1] = Math.max(worstBefore[i], out);
997
+ sumBefore[i + 1] = sumBefore[i] + out;
998
+ }
999
+ const worstAfter = new Float64Array(n + 1);
1000
+ const sumAfter = new Float64Array(n + 1);
1001
+ for (let i = n - 1; i >= 0; i--) {
1002
+ const out = outside(ordered[i].midi, to);
1003
+ worstAfter[i] = Math.max(worstAfter[i + 1], out);
1004
+ sumAfter[i] = sumAfter[i + 1] + out;
1005
+ }
1006
+ let best = null;
1007
+ // Never at the first note: from the first head that starts later on.
1008
+ for (let at = 1; at < n; at++) {
1009
+ if (ordered[at].tick === ordered[at - 1].tick)
1010
+ continue;
1011
+ const cost = Math.max(worstBefore[at] + sumBefore[at] / at / 100, worstAfter[at] + sumAfter[at] / (n - at) / 100);
1012
+ if (!best || cost <= best.cost)
1013
+ best = { tick: ordered[at].tick, cost };
1014
+ }
1015
+ return best;
1016
+ }
1017
+ /** A pitch's distance outside its staff, in DIATONIC steps — one ledger position each. */
1018
+ function stepsOutside(midi, clef) {
1019
+ const [low, high] = STAFF_SPAN[clef];
1020
+ const semitones = (value) => Math.max(0, Math.round((value * 7) / 12));
1021
+ return { above: semitones(midi - high), below: semitones(low - midi) };
1022
+ }
1023
+ /**
1024
+ * How far this music reaches above and below the staff, in ledger positions.
1025
+ *
1026
+ * What it is FOR: a strip has a fixed amount of room around the staff, and
1027
+ * splitting it evenly suits almost no voice. A bass line spends the whole track
1028
+ * under the staff and a lead over it — measured across the twenty tracks, only
1029
+ * a third of the voices fit an even split, while the same room divided in
1030
+ * proportion to where the notes actually go fits nearly all of them. The staff
1031
+ * is put where the music needs it rather than in the middle.
1032
+ *
1033
+ * Read over the WHOLE piece, not the bars on screen: a staff that slid up and
1034
+ * down as the melody moved would be unreadable, and would take the bar lines
1035
+ * and the pinned clef with it.
1036
+ */
1037
+ export function staffRoom(bars, clefs) {
1038
+ let above = 0;
1039
+ let below = 0;
1040
+ bars.forEach((notes, index) => {
1041
+ const bar = clefs[index] ?? { clef: 'treble' };
1042
+ for (const { tick, midi } of notes) {
1043
+ const out = stepsOutside(midi, clefAt(bar, tick));
1044
+ above = Math.max(above, out.above);
1045
+ below = Math.max(below, out.below);
1046
+ }
1047
+ });
1048
+ return { above, below };
1049
+ }
1050
+ //# sourceMappingURL=notation.js.map