@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 +21 -0
- package/README.md +159 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/notation.d.ts +414 -0
- package/dist/notation.js +1050 -0
- package/dist/notation.js.map +1 -0
- package/dist/score.d.ts +475 -0
- package/dist/score.js +926 -0
- package/dist/score.js.map +1 -0
- package/package.json +57 -4
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
|
-
#
|
|
1
|
+
# @xromdev/notation
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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 {};
|