@xromdev/notation 0.0.0-stage → 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksandr Maksymov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,160 @@
1
- # Temporary Holding Version
1
+ # @xromdev/notation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Notes in ticks, read as a score — **note values, ties, tuplets, bar lines, key
4
+ spelling, clefs, the grand staff and dynamics, with no DOM and no renderer.**
5
+
6
+ Give it notes as integers — where each starts and how long it lasts, in
7
+ sixty-fourth notes — and it tells you how to write them:
8
+
9
+ - **Note values:** each length is written with the fewest heads. A note that
10
+ crosses a bar line is tied over it, and a syncopation is one head rather
11
+ than a sixteenth tied into scraps. Off-grid runs of three that a sound
12
+ driver could only approximate (10 + 11 + 11 ticks) are written as triplets.
13
+ - **Bar lines:** any metre, an opening bar of its own length (a 19/32 pickup
14
+ bar in front of 2/4), and odd bars later in the piece. A channel's own
15
+ delay of a tick or three is taken out, so its notes sit on the beat.
16
+ - **Pitch spelling** in the key and through key changes. Black keys are
17
+ written the way the signature has them, and C♭ and F♭ appear where the key
18
+ flattens those letters.
19
+ - **Clefs:** chosen per bar, by a part's worst ledger-line ladder rather than
20
+ its average. A bar that drops into the other register changes clef partway
21
+ through.
22
+ - **A grand staff:** a part that needs two staves is split by beam group, and
23
+ a beam crosses between the staves the way piano music does.
24
+ - **Percussion:** a percussion staff for the NES noise channel, its sixteen
25
+ rates mapped onto the drum lines.
26
+ - **Dynamics, bends and accents:**
27
+ - dynamics read from volume levels;
28
+ - a figure whose level falls or rises note by note (4 3 2 1 in sixteenths)
29
+ written as its level and a hairpin, the next one like it as `sim.` with
30
+ dashes for as long as it holds;
31
+ - slides, bends and vibrato read from pitch curves;
32
+ - grace notes and echoes in brackets;
33
+ - accents and beams that follow them, in a passage you name.
34
+
35
+ Every rule was made on the music of Battletoads (NES), whose sound driver
36
+ counts in exactly these ticks, and checked by ear across its twenty tracks.
37
+ The comments in the source name the bars that decided each one.
38
+
39
+ The output is a model for whatever draws it. Durations come out as VexFlow
40
+ codes (`w h q 8 16 32 64` plus dots) and pitches as VexFlow keys (`f#/4`), so
41
+ [VexFlow](https://www.vexflow.com) can draw it as it stands.
42
+
43
+ **See it:** the score pages at [xrom.dev](https://xrom.dev).
44
+
45
+ ## Install
46
+
47
+ ```sh
48
+ npm install @xromdev/notation
49
+ ```
50
+
51
+ ## A score
52
+
53
+ ```ts
54
+ import { buildScore, keyOf, staffKey, noiseKey } from '@xromdev/notation';
55
+
56
+ // 16 ticks to a quarter: 16 is a quarter, 8 an eighth, 64 a bar of 4/4.
57
+ const lead = [
58
+ { start: 0, ticks: 16, midi: 64, volume: 12 },
59
+ { start: 16, ticks: 8, midi: 66, volume: 12 },
60
+ { start: 24, ticks: 8, midi: 67, volume: 8 },
61
+ { start: 32, ticks: 32, midi: 71, volume: 12 },
62
+ ];
63
+ const drums = [
64
+ { start: 0, ticks: 8, period: 15 }, // NES noise rates: 15 kick
65
+ { start: 16, ticks: 8, period: 7 }, // 7 snare
66
+ ];
67
+
68
+ const score = buildScore([{ id: 'lead', notes: lead }], drums, 64, {
69
+ measureTicks: 64, // 4/4
70
+ });
71
+
72
+ const key = keyOf('G');
73
+ for (const stave of score.staves) {
74
+ for (const bar of stave.bars) {
75
+ for (const item of bar) {
76
+ const pitch =
77
+ item.midi === null ? 'rest'
78
+ : stave.kind === 'percussion' ? noiseKey(item.midi)
79
+ : staffKey(item.midi, key);
80
+ console.log(stave.kind, pitch, item.duration + '.'.repeat(item.dots));
81
+ }
82
+ }
83
+ }
84
+ // treble e/4 q · f#/4 8 · g/4 8 · b/4 h
85
+ // percussion e/4 8 · rest 8 · d/5 8 · rest 8 · rest h
86
+ console.log(score.dynamics.lead); // [{ tick: 0, dynamic: 'f' }]
87
+ ```
88
+
89
+ `ScoreModel` holds:
90
+ - `staves` — each with its bars, already filled to the bar line. A grand
91
+ staff also carries `voiceBars` (the part as one voice across both staves).
92
+ - `grid` and `shift` — where the bar lines fall.
93
+ - `barCount`.
94
+ - `dynamics` and `phases` per part. `phases` is the delay taken out of each part.
95
+ A dynamic is `{ tick, dynamic }`, and on a shaped figure also `words`
96
+ (`'dim.'` or `'cresc.'` — a hairpin; `'sim.'` — the word and dashes, no
97
+ level printed), `until` (the tick of the last note the hairpin or the dashes
98
+ cover) and `bare` (a hairpin from a level already in force: print no letter).
99
+
100
+ Each item is a head or a rest, with:
101
+ - `start`, `ticks`, `duration`, `dots`;
102
+ - ties (`tieStart`, `tieStop`) and `tuplet`;
103
+ - and, where they apply: `effect`, `grace`, `accent`, `echo`, `volume`,
104
+ `staff` and `beamGroup`.
105
+
106
+ ### Options
107
+
108
+ | option | default | |
109
+ |---|---|---|
110
+ | `offset` | 0 | The tick the bar lines are counted from. |
111
+ | `measureTicks` | 64 | Ticks in a bar: 64 is 4/4, 32 is 2/4, 48 is 3/4. |
112
+ | `mergePickup` | false | Put a pickup into a longer opening bar instead of a bar of rests. |
113
+ | `oddBars` | `[]` | `{ bar, ticks }` — later bars with a length of their own. |
114
+ | `accentBeams` | none | `{ from, to }` ticks: mark accents there, and beam by them. |
115
+
116
+ A part's notes may also carry:
117
+ - `volume` (0..15), from which the dynamics and accents are read;
118
+ - `bend` — a pitch curve as `[position 0..1, semitones]` pairs;
119
+ - `grace` — MIDI numbers played in front of the note;
120
+ - `echo` — a quieter repeat, written in brackets.
121
+
122
+ ## One staff
123
+
124
+ The pieces under `buildScore`, for a single strip of music:
125
+
126
+ ```ts
127
+ import { barring, voicePhase, shiftNotes, staffItems, staffMeasures, clefPlan } from '@xromdev/notation';
128
+
129
+ const { grid, shift } = barring(offset, 64);
130
+ const phase = voicePhase(notes, offset, 64);
131
+ const end = totalTicks + shift;
132
+ const bars = staffMeasures(staffItems(shiftNotes(notes, shift - phase), end, grid), end, grid);
133
+ ```
134
+
135
+ `clefPlan` gives each bar's clef, and `staffRoom` how far the music reaches
136
+ above and below the staff. `decompose(position, length)` spells a single run
137
+ of ticks. `keyOf`, `staffKey` and `barKeys` handle keys and key changes.
138
+
139
+ ## Speed
140
+
141
+ Measured on 46 real pieces (Battletoads, Battletoads & Double Dragon,
142
+ Alien 3, RoboCop 1 and 3, Terminator 2) on a laptop:
143
+
144
+ | | per piece |
145
+ |---|---|
146
+ | A whole score (`buildScore`) | ~0.9 ms |
147
+ | Each channel's single staff | ~0.6 ms |
148
+
149
+ Both are computed once per piece, not per frame. About 6 KB minified and gzipped.
150
+
151
+ ## Development
152
+
153
+ ```sh
154
+ npm install
155
+ npm test
156
+ ```
157
+
158
+ ## License
159
+
160
+ MIT © Oleksandr Maksymov
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @xromdev/notation — notes in ticks, read as a score.
3
+ */
4
+ export * from './notation.js';
5
+ export * from './score.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @xromdev/notation — notes in ticks, read as a score.
3
+ */
4
+ export * from './notation.js';
5
+ export * from './score.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC","sourcesContent":["/**\n * @xromdev/notation — notes in ticks, read as a score.\n */\n\nexport * from './notation.js';\nexport * from './score.js';\n"]}
@@ -0,0 +1,414 @@
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 declare const TICKS_PER_QUARTER = 16;
21
+ export declare 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 declare const MEASURE_TICKS: number;
32
+ /** Quarters in a bar of `measureTicks` ticks. */
33
+ export declare const beatsIn: (measureTicks: number) => number;
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 declare function timeSignatureOf(measureTicks: number,
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?: number): {
48
+ beats: number;
49
+ beatValue: number;
50
+ };
51
+ /**
52
+ * Where the bar lines of a piece fall: every bar `measureTicks` long, except
53
+ * the first.
54
+ *
55
+ * The opening bar has its own length because a pickup written as a bar of rests
56
+ * is a lie about the music: Volkmire's Inferno Race opens with a run and a held
57
+ * note that together are six ticks longer than two of its 2/4 bars, and a
58
+ * reader meets that as one bar of 19/32 and then 2/4 throughout, rather than as
59
+ * a bar of rests, a bar line through the run, and a note tied over it.
60
+ *
61
+ * `firstBarTicks` equal to `measureTicks` is the ordinary case, and then the
62
+ * grid is the plain multiples every other piece uses.
63
+ *
64
+ * `oddBars` are bars later in the piece with a length of their own — a bar of
65
+ * 2/4 in a piece in 4/4. Arctic Caverns has one: after six bars of 4/4 the bass
66
+ * runs up E F# G A B C D E in 32 ticks, and everything after that run, every
67
+ * section and the loop's end, sits half a bar past the 4/4 grid.
68
+ */
69
+ export interface BarGrid {
70
+ measureTicks: number;
71
+ firstBarTicks: number;
72
+ /** Bars after the first with their own length, by bar index, in order. */
73
+ oddBars?: readonly OddBar[];
74
+ }
75
+ /** A bar after the first whose length is not the metre's — see `BarGrid`. */
76
+ export interface OddBar {
77
+ /** The bar's index, from 0 — never the first bar, which has `firstBarTicks`. */
78
+ bar: number;
79
+ ticks: number;
80
+ }
81
+ export declare const barGrid: (measureTicks?: number, firstBarTicks?: number, oddBars?: readonly OddBar[]) => BarGrid;
82
+ /** The tick a bar begins on. */
83
+ export declare const barStartOf: (grid: BarGrid, index: number) => number;
84
+ /** How long a bar is: the first and any odd bar have their own length, the rest the metre's. */
85
+ export declare const barTicksOf: (grid: BarGrid, index: number) => number;
86
+ /** Which bar a tick falls in. Ticks before the piece belong to its first bar. */
87
+ export declare const barAt: (grid: BarGrid, tick: number) => number;
88
+ /** How many bars a piece of `totalTicks` takes, the last one counted however little it holds. */
89
+ export declare const barCountOf: (grid: BarGrid, totalTicks: number) => number;
90
+ /** One writable note value: how long it is, what it is called, how many dots. */
91
+ export interface NoteValue {
92
+ ticks: number;
93
+ /** VexFlow duration code. */
94
+ duration: string;
95
+ dots: number;
96
+ }
97
+ /**
98
+ * A length as a sum of writable note values, from a position in the bar.
99
+ *
100
+ * The FEWEST heads, and only then the tidiest. The first version was greedy and
101
+ * took a value only when it started on its own grid, which reads well in a
102
+ * rulebook and terribly on a staff: four ticks from the second thirty-second of
103
+ * a beat came out as a dotted thirty-second tied to a sixty-fourth — a
104
+ * sixteenth written as two heads, which nobody writes. Here the head count is
105
+ * minimised outright (a sixteenth begun off the beat is a syncopation, and a
106
+ * syncopation is one head), and starting on the grid and keeping beat three
107
+ * visible only break ties between spellings with the same count.
108
+ */
109
+ export declare function decompose(position: number, length: number, measureTicks?: number): NoteValue[];
110
+ /** A note of one voice, in the integers the ROM holds. */
111
+ export interface TickNote {
112
+ /** Ticks from the start of the piece. */
113
+ start: number;
114
+ ticks: number;
115
+ /** MIDI number. */
116
+ midi: number;
117
+ }
118
+ /**
119
+ * One head of a tuplet: three notes written in the time of two.
120
+ *
121
+ * `num` heads in the time of `occupied`, and the bracket is drawn over the run
122
+ * from `first` to `last` — VexFlow needs the group, not the single head.
123
+ */
124
+ export interface TupletSpan {
125
+ num: number;
126
+ occupied: number;
127
+ first: boolean;
128
+ last: boolean;
129
+ }
130
+ /** One head or rest on the staff — already a writable value. */
131
+ export interface StaffItem {
132
+ start: number;
133
+ ticks: number;
134
+ /** null is a rest. */
135
+ midi: number | null;
136
+ duration: string;
137
+ dots: number;
138
+ /** Set on each head of a tuplet — see `tupletGroups`. */
139
+ tuplet?: TupletSpan;
140
+ /**
141
+ * Still the same sounding note as the head before it / after it.
142
+ *
143
+ * A tie is the difference between one note and two, and one crossing a bar
144
+ * line is drawn as two half curves that meet at it.
145
+ */
146
+ tieStart: boolean;
147
+ tieStop: boolean;
148
+ }
149
+ export interface StaffMeasure {
150
+ /** Bar number from the start of the piece, counting from 0. */
151
+ index: number;
152
+ /** Tick the bar starts on. */
153
+ start: number;
154
+ items: StaffItem[];
155
+ }
156
+ /** One head of a tuplet, with the value it is written as. */
157
+ interface TupletHead {
158
+ span: TupletSpan;
159
+ value: NoteValue;
160
+ }
161
+ /**
162
+ * Where the music divides a written value into THREE.
163
+ *
164
+ * One tick is a sixty-fourth, so a third of anything is not a whole number and
165
+ * the driver cannot hold a true triplet: it writes the nearest integers it can,
166
+ * and the give-away is a run of three that sums to a written value and divides
167
+ * as evenly as integers allow without dividing evenly. A quarter triplet is
168
+ * 10+11+11 of the half's 32 ticks; a sixteenth triplet is 2+3+3 of the
169
+ * eighth's 8. Spelt by `decompose` instead, each of those becomes two heads
170
+ * under a tie, and a bar of them is unreadable — which is how this was found.
171
+ *
172
+ * The rule is deliberately narrow, and it is checked against the ROM rather
173
+ * than reasoned about: over all twenty tracks it fires exactly twice — title
174
+ * music's bar 39, where pulse 1, pulse 2, the triangle and the drums all take
175
+ * it together, and Clinger Winger's drums, once a bar for five bars. Nothing
176
+ * else in the cartridge matches, so nothing else is re-spelt.
177
+ *
178
+ * Even thirds are NOT tuplets: three heads of the same written value need no
179
+ * bracket, and the ROM has none inside these totals anyway.
180
+ */
181
+ export declare function tupletGroups(notes: readonly TickNote[], grid?: BarGrid): Map<number, TupletHead>;
182
+ /** The notes a tuplet of `num` is written in the time of: the largest power of two below it. */
183
+ export declare const tupletOccupied: (num: number) => number;
184
+ export declare function staffItems(notes: readonly TickNote[], totalTicks: number, grid?: BarGrid): StaffItem[];
185
+ /**
186
+ * The ribbon cut into bars, each one filled to the bar line.
187
+ *
188
+ * A bar short of its four beats is not a bar — a formatter given one spaces it
189
+ * as though the missing time did not exist, and the staff stops lining up with
190
+ * the music beside it — so whatever is left at the end is padded with rests.
191
+ */
192
+ export declare function staffMeasures(items: readonly StaffItem[], totalTicks: number, grid?: BarGrid): StaffMeasure[];
193
+ /**
194
+ * A MIDI number as a VexFlow key — `c#/4` for 61, `db/4` in a key that flats D.
195
+ *
196
+ * A black key is spelt the way the KEY SIGNATURE has it when the signature
197
+ * alters either of its letters, and as a sharp otherwise. A flat key spelt in
198
+ * sharps prints D-sharp against a signature that flattens E — a sign on nearly
199
+ * every note — which is what rat race looked like before flats could be written.
200
+ * Spelling EVERY black key flat in a flat key was the next mistake: Volkmire's
201
+ * Inferno, in E-flat, climbs F to F-sharp, and printing that as G-flat put a
202
+ * flat on a note a reader took for a B-flat already in the key.
203
+ *
204
+ * Two white keys have a flat spelling of their own: B is C-flat and E is F-flat. They take it only
205
+ * in a key that flattens that letter — six flats for C-flat. Rat race is in E-flat minor, whose
206
+ * sixth degree is C-flat; written as B it printed a natural against the B-flat of the signature.
207
+ */
208
+ export declare function staffKey(midi: number, key?: KeySignature): string;
209
+ /**
210
+ * The heads of a chord as VexFlow keys, each black key spelt so the chord reads as intervals do.
211
+ *
212
+ * `staffKey` spells one note against the key, and for a line that is right: Volkmire's Inferno, in
213
+ * E-flat, climbs F to F-sharp. A chord is read the other way — by the intervals between its heads.
214
+ * Paganini's Caprice 17, in E-flat, strikes E-flat with G-flat: written E-flat and F-sharp, that is
215
+ * an augmented second where a minor third sounds. So for the black keys of a chord both spellings
216
+ * are tried, and the one whose intervals span the letters their size asks for wins — a third across
217
+ * three letters, a second across two; where the intervals cannot tell, `staffKey` decides. In the
218
+ * order given.
219
+ */
220
+ export declare function chordKeys(midis: readonly number[], key?: KeySignature): string[];
221
+ /** What a key signature does: which letters it alters, and which way. */
222
+ export interface KeySignature {
223
+ /** `#` for a sharp key, `b` for a flat one. C has neither and no letters. */
224
+ accidental: '#' | 'b';
225
+ letters: readonly string[];
226
+ /** Letters whose black key is written flat although the key leaves the letter alone — see `keyOf`. */
227
+ spelt?: readonly string[];
228
+ }
229
+ /**
230
+ * A key signature by its VexFlow name (`G` one sharp, `Eb` three flats).
231
+ * A name it does not know is C — written with every sign at its note rather
232
+ * than guessed at.
233
+ */
234
+ export declare function keyOf(name?: string): KeySignature;
235
+ /**
236
+ * The signature part of a key name, the one VexFlow is given — `F` of `F+Db+Ab`.
237
+ *
238
+ * What follows a `+` is spelling, not signature: black keys to write as those flats although the
239
+ * key does not flatten their letters. Turbo tunnel, in D minor with one flat, falls D, D-flat, C in
240
+ * the bass; the rule for black keys wrote the middle chord as C-sharp and G-sharp.
241
+ */
242
+ export declare const signatureName: (name?: string) => string | undefined;
243
+ /**
244
+ * Treble or bass for a voice, from where it actually sits.
245
+ *
246
+ * The median rather than the mean: one low pickup note must not drag a melody
247
+ * onto ledger lines below the bass staff. The boundary is middle C's octave —
248
+ * a voice whose middle is below A3 spends more of its time under the treble
249
+ * staff than in it.
250
+ */
251
+ export declare function chooseClef(notes: readonly {
252
+ midi: number;
253
+ }[]): 'treble' | 'bass';
254
+ /**
255
+ * How hard a voice is to read from a given bar line: its heads, plus one for
256
+ * every head that starts off its own grid.
257
+ *
258
+ * The measureTicks `voicePhase` minimises. A run of ticks written as
259
+ * one note on the beat costs 1. The syncopation counts as much as a tie, and
260
+ * that is deliberate: `decompose` writes an off-beat run as ONE head, so by
261
+ * heads alone every bar line would look equally good, and the search would
262
+ * happily put the whole piece a sixteenth off the beat.
263
+ */
264
+ export declare function notationCost(notes: readonly TickNote[], offset: number, measureTicks?: number): number;
265
+ /**
266
+ * How many ticks one channel is written behind the beat. Never more than
267
+ * `MAX_PHASE`.
268
+ *
269
+ * The delay is AUTHORED, not the driver's. Nearly every track opens a channel
270
+ * or two with an explicit `$80` rest one sixty-fourth long, and Intruder
271
+ * Excluder puts a three-tick rest ahead of pulse 2's copy of pulse 1's bass
272
+ * line — a two-tick echo, some 60ms, and audible as one. (An earlier version of
273
+ * this comment called it the cost of setup commands; the stream dump says
274
+ * otherwise.)
275
+ *
276
+ * On a staff, though, it is the difference between a quarter note and a chain
277
+ * of tied scraps, so the notes are written on the beat and the delay is
278
+ * STATED: a staff should label every voice it moved, and not move its playhead.
279
+ * Longer echoes (Surf City's six ticks) are past `MAX_PHASE` and are written as
280
+ * they sound.
281
+ */
282
+ export declare const MAX_PHASE = 3;
283
+ export declare function voicePhase(notes: readonly TickNote[], offset: number, measureTicks?: number): number;
284
+ /**
285
+ * A voice moved `by` ticks along the staff's clock, cut at tick 0.
286
+ *
287
+ * Taking a channel's delay out can carry its first sliver before the music
288
+ * starts: ragnarok's canyon opens the noise channel with a one-tick hit on
289
+ * tick 0 and then plays everything a tick late. Refusing the phase for the sake
290
+ * of that sliver — which is what a guard in `voicePhase` once did — left every
291
+ * drum on an odd tick, and each of those gave the formatter a column of
292
+ * its own: a gap between the first two sixteenths of pulse 1 in every bar. So
293
+ * the phase is taken, and what lands before tick 0 is cut off there.
294
+ */
295
+ export declare function shiftNotes<T extends TickNote>(notes: readonly T[], by: number): T[];
296
+ /**
297
+ * Ticks to ADD to a piece's own clock to get the staff's, for a bar line
298
+ * `offset` ticks in.
299
+ *
300
+ * Less than a bar, so a piece barred off its first tick gets a pickup bar
301
+ * rather than notes before tick zero — and nothing at all for offset 0, which
302
+ * is what every track uses: an empty bar in front of the music says the music
303
+ * starts a bar late.
304
+ */
305
+ export declare function staffShift(offset: number, measureTicks?: number): number;
306
+ /**
307
+ * How a piece is barred: where its bar lines fall, and what its notes are moved
308
+ * by to sit behind them.
309
+ *
310
+ * Two readings of the same pickup. Left alone it gets a bar of its own, padded
311
+ * in front with rests, and the music starts part way into bar one. `merged`
312
+ * instead makes the OPENING BAR longer by the pickup and moves nothing: the run
313
+ * and the note it lands on are one bar of 19/32, and the metre takes over after
314
+ * it. The second reads better whenever the pickup is not a whole phrase — a bar
315
+ * line through the middle of a run is worse than an irregular first bar.
316
+ */
317
+ export declare function barring(offset: number, measureTicks?: number, merged?: boolean,
318
+ /** Bars later in the piece with a length of their own — see `BarGrid`. */
319
+ oddBars?: readonly OddBar[]): {
320
+ grid: BarGrid;
321
+ shift: number;
322
+ };
323
+ export type Clef = 'treble' | 'bass';
324
+ /**
325
+ * The outer LINES of each staff, as MIDI numbers: E4..F5 and G2..A3.
326
+ *
327
+ * Not the centres — what matters for room is where the ledger lines start.
328
+ */
329
+ export declare const STAFF_SPAN: Record<Clef, readonly [number, number]>;
330
+ /**
331
+ * The pitch each staff is centred on: B4 on the treble, D3 on the bass. Kept
332
+ * for callers that want the middle line; the clef is not chosen by it.
333
+ */
334
+ export declare const CLEF_CENTRE: Record<Clef, number>;
335
+ /**
336
+ * How much better the other clef has to be before a bar switches, in semitones
337
+ * of the ladder it saves.
338
+ *
339
+ * Without it a line hovering between the two staves changes clef every other
340
+ * bar, which is unreadable in a way that ledger lines are not.
341
+ */
342
+ export declare const CLEF_HYSTERESIS = 3;
343
+ /**
344
+ * How much better a clef change PART WAY through a bar has to be than the best
345
+ * clef for the whole bar, in semitones of the ladder it saves.
346
+ *
347
+ * Higher than `CLEF_HYSTERESIS`, because a change inside a bar is louder. At 3,
348
+ * a line that crosses between the staves on every beat changed clef inside
349
+ * bar after bar — Ragnarok's canyon went treble, bass, treble, bass through
350
+ * bars 5 to 9 — for ladders that were only a few rungs shorter. Measured over
351
+ * the twenty tracks' voices: at 3, 92 changes inside bars; at 6, the bar that
352
+ * drops an octave still splits and the see-saw is gone.
353
+ */
354
+ export declare const CLEF_SPLIT_MARGIN = 6;
355
+ /**
356
+ * A clef per bar — a real score changes clef, and for this music it has to.
357
+ *
358
+ * These channels are not instruments with a range: one voice carries the bass
359
+ * for eight bars and then a melody three octaves up, and on one fixed clef a
360
+ * TENTH of all the notes in the cartridge land more than an octave off the
361
+ * staff — a ladder of ledger lines, most of it clipped by the strip's edge.
362
+ * Chosen per bar the same figure is 1.35%, at a cost of one clef change every
363
+ * twelve bars. What a bar costs on each clef is `clefCost`.
364
+ *
365
+ * A bar with nothing in it keeps the running clef rather than voting: a rest is
366
+ * not evidence about register, and letting silence switch the clef puts the
367
+ * change in the wrong bar.
368
+ *
369
+ * A bar can also change clef PART WAY THROUGH, once, where the music drops or
370
+ * climbs into the other register. Chosen for the bar as a whole, a register
371
+ * change inside it is paid for by every note on the wrong side of it: bar 5 of
372
+ * Ragnarok's canyon is eleven E4s and then five E3s, and the whole bar went to
373
+ * the bass clef — eleven heads on ledger lines to spare five. A split is taken
374
+ * when it beats the best whole-bar clef by `CLEF_SPLIT_MARGIN`, and only from
375
+ * the clef the bar opens in, so a bar never carries two changes.
376
+ */
377
+ export declare function clefPlan(bars: readonly (readonly BarNote[])[], start?: Clef): BarClef[];
378
+ /** A note as the clef reads it: the tick of the bar it starts on, and its pitch. */
379
+ export interface BarNote {
380
+ tick: number;
381
+ midi: number;
382
+ }
383
+ /** The clef one bar opens in, and the one change inside it, if it has one. */
384
+ export interface BarClef {
385
+ clef: Clef;
386
+ /** From this tick of the bar on, the staff reads in `change.clef`. */
387
+ change?: {
388
+ tick: number;
389
+ clef: Clef;
390
+ };
391
+ }
392
+ /** The clef a note starting on `tick` of the bar is written in. */
393
+ export declare function clefAt(bar: BarClef, tick: number): Clef;
394
+ /** The clef a bar hands on to the next one. */
395
+ export declare function clefAfter(bar: BarClef): Clef;
396
+ /**
397
+ * How far this music reaches above and below the staff, in ledger positions.
398
+ *
399
+ * What it is FOR: a strip has a fixed amount of room around the staff, and
400
+ * splitting it evenly suits almost no voice. A bass line spends the whole track
401
+ * under the staff and a lead over it — measured across the twenty tracks, only
402
+ * a third of the voices fit an even split, while the same room divided in
403
+ * proportion to where the notes actually go fits nearly all of them. The staff
404
+ * is put where the music needs it rather than in the middle.
405
+ *
406
+ * Read over the WHOLE piece, not the bars on screen: a staff that slid up and
407
+ * down as the melody moved would be unreadable, and would take the bar lines
408
+ * and the pinned clef with it.
409
+ */
410
+ export declare function staffRoom(bars: readonly (readonly BarNote[])[], clefs: readonly BarClef[]): {
411
+ above: number;
412
+ below: number;
413
+ };
414
+ export {};