@huffduff/midi-writer-ts 3.2.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/.editorconfig +24 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +18 -0
- package/.nvmrc +1 -0
- package/.travis.yml +3 -0
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/browser/midiwriter.js +1367 -0
- package/build/index.cjs +1219 -0
- package/build/index.mjs +1217 -0
- package/build/types/abstract-event.d.ts +7 -0
- package/build/types/chunks/chunk.d.ts +5 -0
- package/build/types/chunks/header.d.ts +13 -0
- package/build/types/chunks/track.d.ts +139 -0
- package/build/types/constants.d.ts +16 -0
- package/build/types/main.d.ts +56 -0
- package/build/types/meta-events/copyright-event.d.ts +18 -0
- package/build/types/meta-events/cue-point-event.d.ts +18 -0
- package/build/types/meta-events/end-track-event.d.ts +16 -0
- package/build/types/meta-events/instrument-name-event.d.ts +18 -0
- package/build/types/meta-events/key-signature-event.d.ts +13 -0
- package/build/types/meta-events/lyric-event.d.ts +18 -0
- package/build/types/meta-events/marker-event.d.ts +18 -0
- package/build/types/meta-events/meta-event.d.ts +5 -0
- package/build/types/meta-events/tempo-event.d.ts +20 -0
- package/build/types/meta-events/text-event.d.ts +18 -0
- package/build/types/meta-events/time-signature-event.d.ts +13 -0
- package/build/types/meta-events/track-name-event.d.ts +18 -0
- package/build/types/midi-events/controller-change-event.d.ts +22 -0
- package/build/types/midi-events/midi-event.d.ts +7 -0
- package/build/types/midi-events/note-event.d.ts +31 -0
- package/build/types/midi-events/note-off-event.d.ts +36 -0
- package/build/types/midi-events/note-on-event.d.ts +36 -0
- package/build/types/midi-events/pitch-bend-event.d.ts +17 -0
- package/build/types/midi-events/program-change-event.d.ts +20 -0
- package/build/types/utils.d.ts +105 -0
- package/build/types/vexflow.d.ts +30 -0
- package/build/types/writer.d.ts +45 -0
- package/examples/chopin-prelude-e-minor.js +143 -0
- package/examples/hot-cross-buns.js +24 -0
- package/examples/mauro.giuliani-op.47-main-theme.js +136 -0
- package/examples/notes-by-start-tick.js +45 -0
- package/examples/zelda-main-theme.js +435 -0
- package/jsdoc.json +5 -0
- package/package.json +79 -0
- package/postinstall.js +1 -0
- package/rollup.config.js +22 -0
- package/runkit.js +18 -0
- package/test/main.js +339 -0
- package/test/vexflow.js +165 -0
- package/test/writer.js +59 -0
- package/tsconfig.json +13 -0
- package/typedoc.json +5 -0
package/build/index.cjs
ADDED
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var midi = require('@tonaljs/midi');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MIDI file format constants.
|
|
7
|
+
* @return {Constants}
|
|
8
|
+
*/
|
|
9
|
+
const Constants = {
|
|
10
|
+
VERSION: '3.1.1',
|
|
11
|
+
HEADER_CHUNK_TYPE: [0x4d, 0x54, 0x68, 0x64],
|
|
12
|
+
HEADER_CHUNK_LENGTH: [0x00, 0x00, 0x00, 0x06],
|
|
13
|
+
HEADER_CHUNK_FORMAT0: [0x00, 0x00],
|
|
14
|
+
HEADER_CHUNK_FORMAT1: [0x00, 0x01],
|
|
15
|
+
HEADER_CHUNK_DIVISION: [0x00, 0x80],
|
|
16
|
+
TRACK_CHUNK_TYPE: [0x4d, 0x54, 0x72, 0x6b],
|
|
17
|
+
META_EVENT_ID: 0xFF,
|
|
18
|
+
META_SMTPE_OFFSET: 0x54
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Static utility functions used throughout the library.
|
|
23
|
+
*/
|
|
24
|
+
class Utils {
|
|
25
|
+
/**
|
|
26
|
+
* Gets MidiWriterJS version number.
|
|
27
|
+
* @return {string}
|
|
28
|
+
*/
|
|
29
|
+
static version() {
|
|
30
|
+
return Constants.VERSION;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert a string to an array of bytes
|
|
34
|
+
* @param {string} string
|
|
35
|
+
* @return {array}
|
|
36
|
+
*/
|
|
37
|
+
static stringToBytes(string) {
|
|
38
|
+
return string.split('').map(char => char.charCodeAt(0));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks if argument is a valid number.
|
|
42
|
+
* @param {*} n - Value to check
|
|
43
|
+
* @return {boolean}
|
|
44
|
+
*/
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
static isNumeric(n) {
|
|
47
|
+
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the correct MIDI number for the specified pitch.
|
|
51
|
+
* Uses Tonal Midi - https://github.com/danigb/tonal/tree/master/packages/midi
|
|
52
|
+
* @param {(string|number)} pitch - 'C#4' or midi note code
|
|
53
|
+
* @param {string} middleC
|
|
54
|
+
* @return {number}
|
|
55
|
+
*/
|
|
56
|
+
static getPitch(pitch, middleC = 'C4') {
|
|
57
|
+
return 60 - midi.toMidi(middleC) + midi.toMidi(pitch);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Translates number of ticks to MIDI timestamp format, returning an array of
|
|
61
|
+
* hex strings with the time values. Midi has a very particular time to express time,
|
|
62
|
+
* take a good look at the spec before ever touching this function.
|
|
63
|
+
* Thanks to https://github.com/sergi/jsmidi
|
|
64
|
+
*
|
|
65
|
+
* @param {number} ticks - Number of ticks to be translated
|
|
66
|
+
* @return {array} - Bytes that form the MIDI time value
|
|
67
|
+
*/
|
|
68
|
+
static numberToVariableLength(ticks) {
|
|
69
|
+
ticks = Math.round(ticks);
|
|
70
|
+
let buffer = ticks & 0x7F;
|
|
71
|
+
// eslint-disable-next-line no-cond-assign
|
|
72
|
+
while (ticks = ticks >> 7) {
|
|
73
|
+
buffer <<= 8;
|
|
74
|
+
buffer |= ((ticks & 0x7F) | 0x80);
|
|
75
|
+
}
|
|
76
|
+
const bList = [];
|
|
77
|
+
// eslint-disable-next-line no-constant-condition
|
|
78
|
+
while (true) {
|
|
79
|
+
bList.push(buffer & 0xff);
|
|
80
|
+
if (buffer & 0x80)
|
|
81
|
+
buffer >>= 8;
|
|
82
|
+
else {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return bList;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Counts number of bytes in string
|
|
90
|
+
* @param {string} s
|
|
91
|
+
* @return {number}
|
|
92
|
+
*/
|
|
93
|
+
static stringByteCount(s) {
|
|
94
|
+
return encodeURI(s).split(/%..|./).length - 1;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get an int from an array of bytes.
|
|
98
|
+
* @param {array} bytes
|
|
99
|
+
* @return {number}
|
|
100
|
+
*/
|
|
101
|
+
static numberFromBytes(bytes) {
|
|
102
|
+
let hex = '';
|
|
103
|
+
let stringResult;
|
|
104
|
+
bytes.forEach((byte) => {
|
|
105
|
+
stringResult = byte.toString(16);
|
|
106
|
+
// ensure string is 2 chars
|
|
107
|
+
if (stringResult.length == 1)
|
|
108
|
+
stringResult = "0" + stringResult;
|
|
109
|
+
hex += stringResult;
|
|
110
|
+
});
|
|
111
|
+
return parseInt(hex, 16);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Takes a number and splits it up into an array of bytes. Can be padded by passing a number to bytesNeeded
|
|
115
|
+
* @param {number} number
|
|
116
|
+
* @param {number} bytesNeeded
|
|
117
|
+
* @return {array} - Array of bytes
|
|
118
|
+
*/
|
|
119
|
+
static numberToBytes(number, bytesNeeded) {
|
|
120
|
+
bytesNeeded = bytesNeeded || 1;
|
|
121
|
+
let hexString = number.toString(16);
|
|
122
|
+
if (hexString.length & 1) { // Make sure hex string is even number of chars
|
|
123
|
+
hexString = '0' + hexString;
|
|
124
|
+
}
|
|
125
|
+
// Split hex string into an array of two char elements
|
|
126
|
+
const hexArray = hexString.match(/.{2}/g);
|
|
127
|
+
// Now parse them out as integers
|
|
128
|
+
const intArray = hexArray.map(item => parseInt(item, 16));
|
|
129
|
+
// Prepend empty bytes if we don't have enough
|
|
130
|
+
if (intArray.length < bytesNeeded) {
|
|
131
|
+
while (bytesNeeded - intArray.length > 0) {
|
|
132
|
+
intArray.unshift(0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return intArray;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Converts value to array if needed.
|
|
139
|
+
* @param {any} value
|
|
140
|
+
* @return {array}
|
|
141
|
+
*/
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
+
static toArray(value) {
|
|
144
|
+
if (Array.isArray(value))
|
|
145
|
+
return value;
|
|
146
|
+
return [value];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Converts velocity to value 0-127
|
|
150
|
+
* @param {number} velocity - Velocity value 1-100
|
|
151
|
+
* @return {number}
|
|
152
|
+
*/
|
|
153
|
+
static convertVelocity(velocity) {
|
|
154
|
+
// Max passed value limited to 100
|
|
155
|
+
velocity = velocity > 100 ? 100 : velocity;
|
|
156
|
+
return Math.round(velocity / 100 * 127);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Gets the total number of ticks of a specified duration.
|
|
160
|
+
* Note: type=='note' defaults to quarter note, type==='rest' defaults to 0
|
|
161
|
+
* @param {(string|array)} duration
|
|
162
|
+
* @return {number}
|
|
163
|
+
*/
|
|
164
|
+
static getTickDuration(duration) {
|
|
165
|
+
if (Array.isArray(duration)) {
|
|
166
|
+
// Recursively execute this method for each item in the array and return the sum of tick durations.
|
|
167
|
+
return duration.map((value) => {
|
|
168
|
+
return Utils.getTickDuration(value);
|
|
169
|
+
}).reduce((a, b) => {
|
|
170
|
+
return a + b;
|
|
171
|
+
}, 0);
|
|
172
|
+
}
|
|
173
|
+
duration = duration.toString();
|
|
174
|
+
if (duration.toLowerCase().charAt(0) === 't') {
|
|
175
|
+
// If duration starts with 't' then the number that follows is an explicit tick count
|
|
176
|
+
const ticks = parseInt(duration.substring(1));
|
|
177
|
+
if (isNaN(ticks) || ticks < 0) {
|
|
178
|
+
throw new Error(duration + ' is not a valid duration.');
|
|
179
|
+
}
|
|
180
|
+
return ticks;
|
|
181
|
+
}
|
|
182
|
+
// Need to apply duration here. Quarter note == Constants.HEADER_CHUNK_DIVISION
|
|
183
|
+
const quarterTicks = Utils.numberFromBytes(Constants.HEADER_CHUNK_DIVISION);
|
|
184
|
+
const tickDuration = quarterTicks * Utils.getDurationMultiplier(duration);
|
|
185
|
+
return Utils.getRoundedIfClose(tickDuration);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Due to rounding errors in JavaScript engines,
|
|
189
|
+
* it's safe to round when we're very close to the actual tick number
|
|
190
|
+
*
|
|
191
|
+
* @static
|
|
192
|
+
* @param {number} tick
|
|
193
|
+
* @return {number}
|
|
194
|
+
*/
|
|
195
|
+
static getRoundedIfClose(tick) {
|
|
196
|
+
const roundedTick = Math.round(tick);
|
|
197
|
+
return Math.abs(roundedTick - tick) < 0.000001 ? roundedTick : tick;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Due to low precision of MIDI,
|
|
201
|
+
* we need to keep track of rounding errors in deltas.
|
|
202
|
+
* This function will calculate the rounding error for a given duration.
|
|
203
|
+
*
|
|
204
|
+
* @static
|
|
205
|
+
* @param {number} tick
|
|
206
|
+
* @return {number}
|
|
207
|
+
*/
|
|
208
|
+
static getPrecisionLoss(tick) {
|
|
209
|
+
const roundedTick = Math.round(tick);
|
|
210
|
+
return roundedTick - tick;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Gets what to multiple ticks/quarter note by to get the specified duration.
|
|
214
|
+
* Note: type=='note' defaults to quarter note, type==='rest' defaults to 0
|
|
215
|
+
* @param {string} duration
|
|
216
|
+
* @return {number}
|
|
217
|
+
*/
|
|
218
|
+
static getDurationMultiplier(duration) {
|
|
219
|
+
// Need to apply duration here.
|
|
220
|
+
// Quarter note == Constants.HEADER_CHUNK_DIVISION ticks.
|
|
221
|
+
if (duration === '0')
|
|
222
|
+
return 0;
|
|
223
|
+
const match = duration.match(/^(?<dotted>d+)?(?<base>\d+)(?:t(?<tuplet>\d*))?/);
|
|
224
|
+
if (match) {
|
|
225
|
+
const base = Number(match.groups.base);
|
|
226
|
+
// 1 or any power of two:
|
|
227
|
+
const isValidBase = base === 1 || ((base & (base - 1)) === 0);
|
|
228
|
+
if (isValidBase) {
|
|
229
|
+
// how much faster or slower is this note compared to a quarter?
|
|
230
|
+
const ratio = base / 4;
|
|
231
|
+
let durationInQuarters = 1 / ratio;
|
|
232
|
+
const { dotted, tuplet } = match.groups;
|
|
233
|
+
if (dotted) {
|
|
234
|
+
const thisManyDots = dotted.length;
|
|
235
|
+
const divisor = Math.pow(2, thisManyDots);
|
|
236
|
+
durationInQuarters = durationInQuarters + (durationInQuarters * ((divisor - 1) / divisor));
|
|
237
|
+
}
|
|
238
|
+
if (typeof tuplet === 'string') {
|
|
239
|
+
const fitInto = durationInQuarters * 2;
|
|
240
|
+
// default to triplet:
|
|
241
|
+
const thisManyNotes = Number(tuplet || '3');
|
|
242
|
+
durationInQuarters = fitInto / thisManyNotes;
|
|
243
|
+
}
|
|
244
|
+
return durationInQuarters;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw new Error(duration + ' is not a valid duration.');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Holds all data for a "controller change" MIDI event
|
|
253
|
+
* @param {object} fields {controllerNumber: integer, controllerValue: integer, delta: integer}
|
|
254
|
+
* @return {ControllerChangeEvent}
|
|
255
|
+
*/
|
|
256
|
+
class ControllerChangeEvent {
|
|
257
|
+
constructor(fields) {
|
|
258
|
+
this.channel = fields.channel - 1 || 0;
|
|
259
|
+
this.controllerValue = fields.controllerValue;
|
|
260
|
+
this.controllerNumber = fields.controllerNumber;
|
|
261
|
+
this.delta = fields.delta || 0x00;
|
|
262
|
+
this.name = 'ControllerChangeEvent';
|
|
263
|
+
this.status = 0xB0;
|
|
264
|
+
this.data = Utils.numberToVariableLength(fields.delta).concat(this.status | this.channel, this.controllerNumber, this.controllerValue);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Object representation of a tempo meta event.
|
|
270
|
+
* @param {object} fields {text: string, delta: integer}
|
|
271
|
+
* @return {CopyrightEvent}
|
|
272
|
+
*/
|
|
273
|
+
class CopyrightEvent {
|
|
274
|
+
constructor(fields) {
|
|
275
|
+
this.delta = fields.delta || 0x00;
|
|
276
|
+
this.name = 'CopyrightEvent';
|
|
277
|
+
this.text = fields.text;
|
|
278
|
+
this.type = 0x02;
|
|
279
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
280
|
+
// Start with zero time delta
|
|
281
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
282
|
+
textBytes);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Object representation of a cue point meta event.
|
|
288
|
+
* @param {object} fields {text: string, delta: integer}
|
|
289
|
+
* @return {CuePointEvent}
|
|
290
|
+
*/
|
|
291
|
+
class CuePointEvent {
|
|
292
|
+
constructor(fields) {
|
|
293
|
+
this.delta = fields.delta || 0x00;
|
|
294
|
+
this.name = 'CuePointEvent';
|
|
295
|
+
this.text = fields.text;
|
|
296
|
+
this.type = 0x07;
|
|
297
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
298
|
+
// Start with zero time delta
|
|
299
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
300
|
+
textBytes);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Object representation of a end track meta event.
|
|
306
|
+
* @param {object} fields {delta: integer}
|
|
307
|
+
* @return {EndTrackEvent}
|
|
308
|
+
*/
|
|
309
|
+
class EndTrackEvent {
|
|
310
|
+
constructor(fields) {
|
|
311
|
+
this.delta = (fields === null || fields === void 0 ? void 0 : fields.delta) || 0x00;
|
|
312
|
+
this.name = 'EndTrackEvent';
|
|
313
|
+
this.type = [0x2F, 0x00];
|
|
314
|
+
// Start with zero time delta
|
|
315
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Object representation of an instrument name meta event.
|
|
321
|
+
* @param {object} fields {text: string, delta: integer}
|
|
322
|
+
* @return {InstrumentNameEvent}
|
|
323
|
+
*/
|
|
324
|
+
class InstrumentNameEvent {
|
|
325
|
+
constructor(fields) {
|
|
326
|
+
this.delta = fields.delta || 0x00;
|
|
327
|
+
this.name = 'InstrumentNameEvent';
|
|
328
|
+
this.text = fields.text;
|
|
329
|
+
this.type = 0x04;
|
|
330
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
331
|
+
// Start with zero time delta
|
|
332
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
333
|
+
textBytes);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Object representation of a key signature meta event.
|
|
339
|
+
* @return {KeySignatureEvent}
|
|
340
|
+
*/
|
|
341
|
+
class KeySignatureEvent {
|
|
342
|
+
constructor(sf, mi) {
|
|
343
|
+
this.name = 'KeySignatureEvent';
|
|
344
|
+
this.type = 0x59;
|
|
345
|
+
let mode = mi || 0;
|
|
346
|
+
sf = sf || 0;
|
|
347
|
+
// Function called with string notation
|
|
348
|
+
if (typeof mi === 'undefined') {
|
|
349
|
+
const fifths = [
|
|
350
|
+
['Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#'],
|
|
351
|
+
['ab', 'eb', 'bb', 'f', 'c', 'g', 'd', 'a', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'a#']
|
|
352
|
+
];
|
|
353
|
+
const _sflen = sf.length;
|
|
354
|
+
let note = sf || 'C';
|
|
355
|
+
if (sf[0] === sf[0].toLowerCase())
|
|
356
|
+
mode = 1;
|
|
357
|
+
if (_sflen > 1) {
|
|
358
|
+
switch (sf.charAt(_sflen - 1)) {
|
|
359
|
+
case 'm':
|
|
360
|
+
mode = 1;
|
|
361
|
+
note = sf.charAt(0).toLowerCase();
|
|
362
|
+
note = note.concat(sf.substring(1, _sflen - 1));
|
|
363
|
+
break;
|
|
364
|
+
case '-':
|
|
365
|
+
mode = 1;
|
|
366
|
+
note = sf.charAt(0).toLowerCase();
|
|
367
|
+
note = note.concat(sf.substring(1, _sflen - 1));
|
|
368
|
+
break;
|
|
369
|
+
case 'M':
|
|
370
|
+
mode = 0;
|
|
371
|
+
note = sf.charAt(0).toUpperCase();
|
|
372
|
+
note = note.concat(sf.substring(1, _sflen - 1));
|
|
373
|
+
break;
|
|
374
|
+
case '+':
|
|
375
|
+
mode = 0;
|
|
376
|
+
note = sf.charAt(0).toUpperCase();
|
|
377
|
+
note = note.concat(sf.substring(1, _sflen - 1));
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const fifthindex = fifths[mode].indexOf(note);
|
|
382
|
+
sf = fifthindex === -1 ? 0 : fifthindex - 7;
|
|
383
|
+
}
|
|
384
|
+
// Start with zero time delta
|
|
385
|
+
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, this.type, [0x02], // Size
|
|
386
|
+
Utils.numberToBytes(sf, 1), // Number of sharp or flats ( < 0 flat; > 0 sharp)
|
|
387
|
+
Utils.numberToBytes(mode, 1));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Object representation of a lyric meta event.
|
|
393
|
+
* @param {object} fields {text: string, delta: integer}
|
|
394
|
+
* @return {LyricEvent}
|
|
395
|
+
*/
|
|
396
|
+
class LyricEvent {
|
|
397
|
+
constructor(fields) {
|
|
398
|
+
this.delta = fields.delta || 0x00;
|
|
399
|
+
this.name = 'LyricEvent';
|
|
400
|
+
this.text = fields.text;
|
|
401
|
+
this.type = 0x05;
|
|
402
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
403
|
+
// Start with zero time delta
|
|
404
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
405
|
+
textBytes);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Object representation of a marker meta event.
|
|
411
|
+
* @param {object} fields {text: string, delta: integer}
|
|
412
|
+
* @return {MarkerEvent}
|
|
413
|
+
*/
|
|
414
|
+
class MarkerEvent {
|
|
415
|
+
constructor(fields) {
|
|
416
|
+
this.delta = fields.delta || 0x00;
|
|
417
|
+
this.name = 'MarkerEvent';
|
|
418
|
+
this.text = fields.text;
|
|
419
|
+
this.type = 0x06;
|
|
420
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
421
|
+
// Start with zero time delta
|
|
422
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
423
|
+
textBytes);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Holds all data for a "note on" MIDI event
|
|
429
|
+
* @param {object} fields {data: []}
|
|
430
|
+
* @return {NoteOnEvent}
|
|
431
|
+
*/
|
|
432
|
+
class NoteOnEvent {
|
|
433
|
+
constructor(fields) {
|
|
434
|
+
this.name = 'NoteOnEvent';
|
|
435
|
+
this.channel = fields.channel || 1;
|
|
436
|
+
this.pitch = fields.pitch;
|
|
437
|
+
this.wait = fields.wait || 0;
|
|
438
|
+
this.velocity = fields.velocity || 50;
|
|
439
|
+
this.tick = fields.tick || null;
|
|
440
|
+
this.delta = null;
|
|
441
|
+
this.data = fields.data;
|
|
442
|
+
this.status = 0x90;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Builds int array for this event.
|
|
446
|
+
* @param {Track} track - parent track
|
|
447
|
+
* @return {NoteOnEvent}
|
|
448
|
+
*/
|
|
449
|
+
buildData(track, precisionDelta, options = {}) {
|
|
450
|
+
this.data = [];
|
|
451
|
+
// Explicitly defined startTick event
|
|
452
|
+
if (this.tick) {
|
|
453
|
+
this.tick = Utils.getRoundedIfClose(this.tick);
|
|
454
|
+
// If this is the first event in the track then use event's starting tick as delta.
|
|
455
|
+
if (track.tickPointer == 0) {
|
|
456
|
+
this.delta = this.tick;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
this.delta = Utils.getTickDuration(this.wait);
|
|
461
|
+
this.tick = Utils.getRoundedIfClose(track.tickPointer + this.delta);
|
|
462
|
+
}
|
|
463
|
+
this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta);
|
|
464
|
+
this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection)
|
|
465
|
+
.concat(this.status | this.channel - 1, Utils.getPitch(this.pitch, options.middleC), Utils.convertVelocity(this.velocity));
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Holds all data for a "note off" MIDI event
|
|
472
|
+
* @param {object} fields {data: []}
|
|
473
|
+
* @return {NoteOffEvent}
|
|
474
|
+
*/
|
|
475
|
+
class NoteOffEvent {
|
|
476
|
+
constructor(fields) {
|
|
477
|
+
this.name = 'NoteOffEvent';
|
|
478
|
+
this.channel = fields.channel || 1;
|
|
479
|
+
this.pitch = fields.pitch;
|
|
480
|
+
this.velocity = fields.velocity || 50;
|
|
481
|
+
this.tick = fields.tick || null;
|
|
482
|
+
this.data = fields.data;
|
|
483
|
+
this.delta = fields.delta || Utils.getTickDuration(fields.duration);
|
|
484
|
+
this.status = 0x80;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Builds int array for this event.
|
|
488
|
+
* @param {Track} track - parent track
|
|
489
|
+
* @return {NoteOffEvent}
|
|
490
|
+
*/
|
|
491
|
+
buildData(track, precisionDelta, options = {}) {
|
|
492
|
+
if (this.tick === null) {
|
|
493
|
+
this.tick = Utils.getRoundedIfClose(this.delta + track.tickPointer);
|
|
494
|
+
}
|
|
495
|
+
this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta);
|
|
496
|
+
this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection)
|
|
497
|
+
.concat(this.status | this.channel - 1, Utils.getPitch(this.pitch, options.middleC), Utils.convertVelocity(this.velocity));
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Wrapper for noteOnEvent/noteOffEvent objects that builds both events.
|
|
504
|
+
* @param {object} fields - {pitch: '[C4]', duration: '4', wait: '4', velocity: 1-100}
|
|
505
|
+
* @return {NoteEvent}
|
|
506
|
+
*/
|
|
507
|
+
class NoteEvent {
|
|
508
|
+
constructor(fields) {
|
|
509
|
+
this.data = [];
|
|
510
|
+
this.name = 'NoteEvent';
|
|
511
|
+
this.pitch = Utils.toArray(fields.pitch);
|
|
512
|
+
this.channel = fields.channel || 1;
|
|
513
|
+
this.duration = fields.duration || '4';
|
|
514
|
+
this.grace = fields.grace;
|
|
515
|
+
this.repeat = fields.repeat || 1;
|
|
516
|
+
this.sequential = fields.sequential || false;
|
|
517
|
+
this.tick = fields.startTick || fields.tick || null;
|
|
518
|
+
this.velocity = fields.velocity || 50;
|
|
519
|
+
this.wait = fields.wait || 0;
|
|
520
|
+
this.tickDuration = Utils.getTickDuration(this.duration);
|
|
521
|
+
this.restDuration = Utils.getTickDuration(this.wait);
|
|
522
|
+
this.events = []; // Hold actual NoteOn/NoteOff events
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Builds int array for this event.
|
|
526
|
+
* @return {NoteEvent}
|
|
527
|
+
*/
|
|
528
|
+
buildData() {
|
|
529
|
+
// Reset data array
|
|
530
|
+
this.data = [];
|
|
531
|
+
// Apply grace note(s) and subtract ticks (currently 1 tick per grace note) from tickDuration so net value is the same
|
|
532
|
+
if (this.grace) {
|
|
533
|
+
const graceDuration = 1;
|
|
534
|
+
this.grace = Utils.toArray(this.grace);
|
|
535
|
+
this.grace.forEach(() => {
|
|
536
|
+
const noteEvent = new NoteEvent({ pitch: this.grace, duration: 'T' + graceDuration });
|
|
537
|
+
this.data = this.data.concat(noteEvent.data);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
// fields.pitch could be an array of pitches.
|
|
541
|
+
// If so create note events for each and apply the same duration.
|
|
542
|
+
// By default this is a chord if it's an array of notes that requires one NoteOnEvent.
|
|
543
|
+
// If this.sequential === true then it's a sequential string of notes that requires separate NoteOnEvents.
|
|
544
|
+
if (!this.sequential) {
|
|
545
|
+
// Handle repeat
|
|
546
|
+
for (let j = 0; j < this.repeat; j++) {
|
|
547
|
+
// Note on
|
|
548
|
+
this.pitch.forEach((p, i) => {
|
|
549
|
+
let noteOnNew;
|
|
550
|
+
if (i == 0) {
|
|
551
|
+
noteOnNew = new NoteOnEvent({
|
|
552
|
+
channel: this.channel,
|
|
553
|
+
wait: this.wait,
|
|
554
|
+
delta: Utils.getTickDuration(this.wait),
|
|
555
|
+
velocity: this.velocity,
|
|
556
|
+
pitch: p,
|
|
557
|
+
tick: this.tick,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
// Running status (can ommit the note on status)
|
|
562
|
+
//noteOn = new NoteOnEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
|
|
563
|
+
noteOnNew = new NoteOnEvent({
|
|
564
|
+
channel: this.channel,
|
|
565
|
+
wait: 0,
|
|
566
|
+
delta: 0,
|
|
567
|
+
velocity: this.velocity,
|
|
568
|
+
pitch: p,
|
|
569
|
+
tick: this.tick,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
this.events.push(noteOnNew);
|
|
573
|
+
});
|
|
574
|
+
// Note off
|
|
575
|
+
this.pitch.forEach((p, i) => {
|
|
576
|
+
let noteOffNew;
|
|
577
|
+
if (i == 0) {
|
|
578
|
+
//noteOff = new NoteOffEvent({data: Utils.numberToVariableLength(tickDuration).concat(this.getNoteOffStatus(), Utils.getPitch(p), Utils.convertVelocity(this.velocity))});
|
|
579
|
+
noteOffNew = new NoteOffEvent({
|
|
580
|
+
channel: this.channel,
|
|
581
|
+
duration: this.duration,
|
|
582
|
+
velocity: this.velocity,
|
|
583
|
+
pitch: p,
|
|
584
|
+
tick: this.tick !== null ? Utils.getTickDuration(this.duration) + this.tick : null,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
// Running status (can omit the note off status)
|
|
589
|
+
//noteOff = new NoteOffEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
|
|
590
|
+
noteOffNew = new NoteOffEvent({
|
|
591
|
+
channel: this.channel,
|
|
592
|
+
duration: 0,
|
|
593
|
+
velocity: this.velocity,
|
|
594
|
+
pitch: p,
|
|
595
|
+
tick: this.tick !== null ? Utils.getTickDuration(this.duration) + this.tick : null,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
this.events.push(noteOffNew);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
// Handle repeat
|
|
604
|
+
for (let j = 0; j < this.repeat; j++) {
|
|
605
|
+
this.pitch.forEach((p, i) => {
|
|
606
|
+
const noteOnNew = new NoteOnEvent({
|
|
607
|
+
channel: this.channel,
|
|
608
|
+
wait: (i > 0 ? 0 : this.wait),
|
|
609
|
+
delta: (i > 0 ? 0 : Utils.getTickDuration(this.wait)),
|
|
610
|
+
velocity: this.velocity,
|
|
611
|
+
pitch: p,
|
|
612
|
+
tick: this.tick,
|
|
613
|
+
});
|
|
614
|
+
const noteOffNew = new NoteOffEvent({
|
|
615
|
+
channel: this.channel,
|
|
616
|
+
duration: this.duration,
|
|
617
|
+
velocity: this.velocity,
|
|
618
|
+
pitch: p,
|
|
619
|
+
});
|
|
620
|
+
this.events.push(noteOnNew, noteOffNew);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return this;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Holds all data for a "Pitch Bend" MIDI event
|
|
630
|
+
* [ -1.0, 0, 1.0 ] -> [ 0, 8192, 16383]
|
|
631
|
+
* @param {object} fields { bend : float, channel : int, delta: int }
|
|
632
|
+
* @return {PitchBendEvent}
|
|
633
|
+
*/
|
|
634
|
+
class PitchBendEvent {
|
|
635
|
+
constructor(fields) {
|
|
636
|
+
this.channel = fields.channel || 0;
|
|
637
|
+
this.delta = fields.delta || 0x00;
|
|
638
|
+
this.name = 'PitchBendEvent';
|
|
639
|
+
this.status = 0xE0;
|
|
640
|
+
const bend14 = this.scale14bits(fields.bend);
|
|
641
|
+
const lsbValue = bend14 & 0x7f;
|
|
642
|
+
const msbValue = (bend14 >> 7) & 0x7f;
|
|
643
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, lsbValue, msbValue);
|
|
644
|
+
}
|
|
645
|
+
scale14bits(zeroOne) {
|
|
646
|
+
if (zeroOne <= 0) {
|
|
647
|
+
return Math.floor(16384 * (zeroOne + 1) / 2);
|
|
648
|
+
}
|
|
649
|
+
return Math.floor(16383 * (zeroOne + 1) / 2);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Holds all data for a "program change" MIDI event
|
|
655
|
+
* @param {object} fields {instrument: integer, delta: integer}
|
|
656
|
+
* @return {ProgramChangeEvent}
|
|
657
|
+
*/
|
|
658
|
+
class ProgramChangeEvent {
|
|
659
|
+
constructor(fields) {
|
|
660
|
+
this.channel = fields.channel || 0;
|
|
661
|
+
this.delta = fields.delta || 0x00;
|
|
662
|
+
this.instrument = fields.instrument;
|
|
663
|
+
this.status = 0xC0;
|
|
664
|
+
this.name = 'ProgramChangeEvent';
|
|
665
|
+
// delta time defaults to 0.
|
|
666
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, this.instrument);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Object representation of a tempo meta event.
|
|
672
|
+
* @param {object} fields {bpm: integer, delta: integer}
|
|
673
|
+
* @return {TempoEvent}
|
|
674
|
+
*/
|
|
675
|
+
class TempoEvent {
|
|
676
|
+
constructor(fields) {
|
|
677
|
+
this.bpm = fields.bpm;
|
|
678
|
+
this.delta = fields.delta || 0x00;
|
|
679
|
+
this.tick = fields.tick;
|
|
680
|
+
this.name = 'TempoEvent';
|
|
681
|
+
this.type = 0x51;
|
|
682
|
+
const tempo = Math.round(60000000 / this.bpm);
|
|
683
|
+
// Start with zero time delta
|
|
684
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, [0x03], // Size
|
|
685
|
+
Utils.numberToBytes(tempo, 3));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Object representation of a tempo meta event.
|
|
691
|
+
* @param {object} fields {text: string, delta: integer}
|
|
692
|
+
* @return {TextEvent}
|
|
693
|
+
*/
|
|
694
|
+
class TextEvent {
|
|
695
|
+
constructor(fields) {
|
|
696
|
+
this.delta = fields.delta || 0x00;
|
|
697
|
+
this.text = fields.text;
|
|
698
|
+
this.name = 'TextEvent';
|
|
699
|
+
this.type = 0x01;
|
|
700
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
701
|
+
// Start with zero time delta
|
|
702
|
+
this.data = Utils.numberToVariableLength(fields.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
703
|
+
textBytes);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Object representation of a time signature meta event.
|
|
709
|
+
* @return {TimeSignatureEvent}
|
|
710
|
+
*/
|
|
711
|
+
class TimeSignatureEvent {
|
|
712
|
+
constructor(numerator, denominator, midiclockspertick, notespermidiclock) {
|
|
713
|
+
this.name = 'TimeSignatureEvent';
|
|
714
|
+
this.type = 0x58;
|
|
715
|
+
// Start with zero time delta
|
|
716
|
+
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, this.type, [0x04], // Size
|
|
717
|
+
Utils.numberToBytes(numerator, 1), // Numerator, 1 bytes
|
|
718
|
+
Utils.numberToBytes(Math.log2(denominator), 1), // Denominator is expressed as pow of 2, 1 bytes
|
|
719
|
+
Utils.numberToBytes(midiclockspertick || 24, 1), // MIDI Clocks per tick, 1 bytes
|
|
720
|
+
Utils.numberToBytes(notespermidiclock || 8, 1));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Object representation of a tempo meta event.
|
|
726
|
+
* @param {object} fields {text: string, delta: integer}
|
|
727
|
+
* @return {TrackNameEvent}
|
|
728
|
+
*/
|
|
729
|
+
class TrackNameEvent {
|
|
730
|
+
constructor(fields) {
|
|
731
|
+
this.delta = fields.delta || 0x00;
|
|
732
|
+
this.name = 'TrackNameEvent';
|
|
733
|
+
this.text = fields.text;
|
|
734
|
+
this.type = 0x03;
|
|
735
|
+
const textBytes = Utils.stringToBytes(this.text);
|
|
736
|
+
// Start with zero time delta
|
|
737
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
|
|
738
|
+
textBytes);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Holds all data for a track.
|
|
744
|
+
* @param {object} fields {type: number, data: array, size: array, events: array}
|
|
745
|
+
* @return {Track}
|
|
746
|
+
*/
|
|
747
|
+
class Track {
|
|
748
|
+
constructor() {
|
|
749
|
+
this.type = Constants.TRACK_CHUNK_TYPE;
|
|
750
|
+
this.data = [];
|
|
751
|
+
this.size = [];
|
|
752
|
+
this.events = [];
|
|
753
|
+
this.explicitTickEvents = [];
|
|
754
|
+
// If there are any events with an explicit tick defined then we will create a "sub" track for those
|
|
755
|
+
// and merge them in and the end.
|
|
756
|
+
this.tickPointer = 0; // Each time an event is added this will increase
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Adds any event type to the track.
|
|
760
|
+
* Events without a specific startTick property are assumed to be added in order of how they should output.
|
|
761
|
+
* Events with a specific startTick property are set aside for now will be merged in during build process.
|
|
762
|
+
*
|
|
763
|
+
* TODO: Don't put startTick events in their own array. Just lump everything together and sort it out during buildData();
|
|
764
|
+
* @param {(NoteEvent|ProgramChangeEvent)} events - Event object or array of Event objects.
|
|
765
|
+
* @param {Function} mapFunction - Callback which can be used to apply specific properties to all events.
|
|
766
|
+
* @return {Track}
|
|
767
|
+
*/
|
|
768
|
+
addEvent(events, mapFunction) {
|
|
769
|
+
Utils.toArray(events).forEach((event, i) => {
|
|
770
|
+
if (event instanceof NoteEvent) {
|
|
771
|
+
// Handle map function if provided
|
|
772
|
+
if (typeof mapFunction === 'function') {
|
|
773
|
+
const properties = mapFunction(i, event);
|
|
774
|
+
if (typeof properties === 'object') {
|
|
775
|
+
Object.assign(event, properties);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// If this note event has an explicit startTick then we need to set aside for now
|
|
779
|
+
if (event.tick !== null) {
|
|
780
|
+
this.explicitTickEvents.push(event);
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
// Push each on/off event to track's event stack
|
|
784
|
+
event.buildData().events.forEach((e) => this.events.push(e));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
this.events.push(event);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
return this;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Builds int array of all events.
|
|
795
|
+
* @param {object} options
|
|
796
|
+
* @return {Track}
|
|
797
|
+
*/
|
|
798
|
+
buildData(options = {}) {
|
|
799
|
+
// Reset
|
|
800
|
+
this.data = [];
|
|
801
|
+
this.size = [];
|
|
802
|
+
this.tickPointer = 0;
|
|
803
|
+
let precisionLoss = 0;
|
|
804
|
+
this.events.forEach((event) => {
|
|
805
|
+
// Build event & add to total tick duration
|
|
806
|
+
if (event instanceof NoteOnEvent || event instanceof NoteOffEvent) {
|
|
807
|
+
const built = event.buildData(this, precisionLoss, options);
|
|
808
|
+
precisionLoss = Utils.getPrecisionLoss(event.deltaWithPrecisionCorrection || 0);
|
|
809
|
+
this.data = this.data.concat(built.data);
|
|
810
|
+
this.tickPointer = Utils.getRoundedIfClose(event.tick);
|
|
811
|
+
}
|
|
812
|
+
else if (event instanceof TempoEvent) {
|
|
813
|
+
this.tickPointer = Utils.getRoundedIfClose(event.tick);
|
|
814
|
+
this.data = this.data.concat(event.data);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
this.data = this.data.concat(event.data);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
this.mergeExplicitTickEvents();
|
|
821
|
+
// If the last event isn't EndTrackEvent, then tack it onto the data.
|
|
822
|
+
if (!this.events.length || !(this.events[this.events.length - 1] instanceof EndTrackEvent)) {
|
|
823
|
+
this.data = this.data.concat((new EndTrackEvent).data);
|
|
824
|
+
}
|
|
825
|
+
this.size = Utils.numberToBytes(this.data.length, 4); // 4 bytes long
|
|
826
|
+
return this;
|
|
827
|
+
}
|
|
828
|
+
mergeExplicitTickEvents() {
|
|
829
|
+
if (!this.explicitTickEvents.length)
|
|
830
|
+
return;
|
|
831
|
+
// First sort asc list of events by startTick
|
|
832
|
+
this.explicitTickEvents.sort((a, b) => a.tick - b.tick);
|
|
833
|
+
// Now this.explicitTickEvents is in correct order, and so is this.events naturally.
|
|
834
|
+
// For each explicit tick event, splice it into the main list of events and
|
|
835
|
+
// adjust the delta on the following events so they still play normally.
|
|
836
|
+
this.explicitTickEvents.forEach((noteEvent) => {
|
|
837
|
+
// Convert NoteEvent to it's respective NoteOn/NoteOff events
|
|
838
|
+
// Note that as we splice in events the delta for the NoteOff ones will
|
|
839
|
+
// Need to change based on what comes before them after the splice.
|
|
840
|
+
noteEvent.buildData().events.forEach((e) => e.buildData(this));
|
|
841
|
+
// Merge each event individually into this track's event list.
|
|
842
|
+
noteEvent.events.forEach((event) => this.mergeSingleEvent(event));
|
|
843
|
+
});
|
|
844
|
+
// Hacky way to rebuild track with newly spliced events. Need better solution.
|
|
845
|
+
this.explicitTickEvents = [];
|
|
846
|
+
this.buildData();
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Merges another track's events with this track.
|
|
850
|
+
* @param {Track} track
|
|
851
|
+
* @return {Track}
|
|
852
|
+
*/
|
|
853
|
+
mergeTrack(track) {
|
|
854
|
+
// First build this track to populate each event's tick property
|
|
855
|
+
this.buildData();
|
|
856
|
+
// Then build track to be merged so that tick property is populated on all events & merge each event.
|
|
857
|
+
track.buildData().events.forEach((event) => this.mergeSingleEvent(event));
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Merges a single event into this track's list of events based on event.tick property.
|
|
862
|
+
* @param {AbstractEvent} - event
|
|
863
|
+
* @return {Track}
|
|
864
|
+
*/
|
|
865
|
+
mergeSingleEvent(event) {
|
|
866
|
+
// There are no events yet, so just add it in.
|
|
867
|
+
if (!this.events.length) {
|
|
868
|
+
this.addEvent(event);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
// Find index of existing event we need to follow with
|
|
872
|
+
let lastEventIndex;
|
|
873
|
+
for (let i = 0; i < this.events.length; i++) {
|
|
874
|
+
if (this.events[i].tick > event.tick)
|
|
875
|
+
break;
|
|
876
|
+
lastEventIndex = i;
|
|
877
|
+
}
|
|
878
|
+
const splicedEventIndex = lastEventIndex + 1;
|
|
879
|
+
// Need to adjust the delta of this event to ensure it falls on the correct tick.
|
|
880
|
+
event.delta = event.tick - this.events[lastEventIndex].tick;
|
|
881
|
+
// Splice this event at lastEventIndex + 1
|
|
882
|
+
this.events.splice(splicedEventIndex, 0, event);
|
|
883
|
+
// Now adjust delta of all following events
|
|
884
|
+
for (let i = splicedEventIndex + 1; i < this.events.length; i++) {
|
|
885
|
+
// Since each existing event should have a tick value at this point we just need to
|
|
886
|
+
// adjust delta to that the event still falls on the correct tick.
|
|
887
|
+
this.events[i].delta = this.events[i].tick - this.events[i - 1].tick;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Removes all events matching specified type.
|
|
892
|
+
* @param {string} eventName - Event type
|
|
893
|
+
* @return {Track}
|
|
894
|
+
*/
|
|
895
|
+
removeEventsByName(eventName) {
|
|
896
|
+
this.events.forEach((event, index) => {
|
|
897
|
+
if (event.name === eventName) {
|
|
898
|
+
this.events.splice(index, 1);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
return this;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Sets tempo of the MIDI file.
|
|
905
|
+
* @param {number} bpm - Tempo in beats per minute.
|
|
906
|
+
* @param {number} tick - Start tick.
|
|
907
|
+
* @return {Track}
|
|
908
|
+
*/
|
|
909
|
+
setTempo(bpm, tick = 0) {
|
|
910
|
+
return this.addEvent(new TempoEvent({ bpm, tick }));
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Sets time signature.
|
|
914
|
+
* @param {number} numerator - Top number of the time signature.
|
|
915
|
+
* @param {number} denominator - Bottom number of the time signature.
|
|
916
|
+
* @param {number} midiclockspertick - Defaults to 24.
|
|
917
|
+
* @param {number} notespermidiclock - Defaults to 8.
|
|
918
|
+
* @return {Track}
|
|
919
|
+
*/
|
|
920
|
+
setTimeSignature(numerator, denominator, midiclockspertick, notespermidiclock) {
|
|
921
|
+
return this.addEvent(new TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock));
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Sets key signature.
|
|
925
|
+
* @param {*} sf -
|
|
926
|
+
* @param {*} mi -
|
|
927
|
+
* @return {Track}
|
|
928
|
+
*/
|
|
929
|
+
setKeySignature(sf, mi) {
|
|
930
|
+
return this.addEvent(new KeySignatureEvent(sf, mi));
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Adds text to MIDI file.
|
|
934
|
+
* @param {string} text - Text to add.
|
|
935
|
+
* @return {Track}
|
|
936
|
+
*/
|
|
937
|
+
addText(text) {
|
|
938
|
+
return this.addEvent(new TextEvent({ text }));
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Adds copyright to MIDI file.
|
|
942
|
+
* @param {string} text - Text of copyright line.
|
|
943
|
+
* @return {Track}
|
|
944
|
+
*/
|
|
945
|
+
addCopyright(text) {
|
|
946
|
+
return this.addEvent(new CopyrightEvent({ text }));
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Adds Sequence/Track Name.
|
|
950
|
+
* @param {string} text - Text of track name.
|
|
951
|
+
* @return {Track}
|
|
952
|
+
*/
|
|
953
|
+
addTrackName(text) {
|
|
954
|
+
return this.addEvent(new TrackNameEvent({ text }));
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Sets instrument name of track.
|
|
958
|
+
* @param {string} text - Name of instrument.
|
|
959
|
+
* @return {Track}
|
|
960
|
+
*/
|
|
961
|
+
addInstrumentName(text) {
|
|
962
|
+
return this.addEvent(new InstrumentNameEvent({ text }));
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Adds marker to MIDI file.
|
|
966
|
+
* @param {string} text - Marker text.
|
|
967
|
+
* @return {Track}
|
|
968
|
+
*/
|
|
969
|
+
addMarker(text) {
|
|
970
|
+
return this.addEvent(new MarkerEvent({ text }));
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Adds cue point to MIDI file.
|
|
974
|
+
* @param {string} text - Text of cue point.
|
|
975
|
+
* @return {Track}
|
|
976
|
+
*/
|
|
977
|
+
addCuePoint(text) {
|
|
978
|
+
return this.addEvent(new CuePointEvent({ text }));
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Adds lyric to MIDI file.
|
|
982
|
+
* @param {string} text - Lyric text to add.
|
|
983
|
+
* @return {Track}
|
|
984
|
+
*/
|
|
985
|
+
addLyric(text) {
|
|
986
|
+
return this.addEvent(new LyricEvent({ text }));
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Channel mode messages
|
|
990
|
+
* @return {Track}
|
|
991
|
+
*/
|
|
992
|
+
polyModeOn() {
|
|
993
|
+
const event = new NoteOnEvent({ data: [0x00, 0xB0, 0x7E, 0x00] });
|
|
994
|
+
return this.addEvent(event);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Sets a pitch bend.
|
|
998
|
+
* @param {float} bend - Bend value ranging [-1,1], zero meaning no bend.
|
|
999
|
+
* @return {Track}
|
|
1000
|
+
*/
|
|
1001
|
+
setPitchBend(bend) {
|
|
1002
|
+
return this.addEvent(new PitchBendEvent({ bend }));
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Adds a controller change event
|
|
1006
|
+
* @param {number} number - Control number.
|
|
1007
|
+
* @param {number} value - Control value.
|
|
1008
|
+
* @param {number} channel - Channel to send controller change event on (1-based).
|
|
1009
|
+
* @param {number} delta - Track tick offset for cc event.
|
|
1010
|
+
* @return {Track}
|
|
1011
|
+
*/
|
|
1012
|
+
controllerChange(number, value, channel, delta) {
|
|
1013
|
+
return this.addEvent(new ControllerChangeEvent({ controllerNumber: number, controllerValue: value, channel: channel, delta: delta }));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
class VexFlow {
|
|
1018
|
+
/**
|
|
1019
|
+
* Support for converting VexFlow voice into MidiWriterJS track
|
|
1020
|
+
* @return MidiWriter.Track object
|
|
1021
|
+
*/
|
|
1022
|
+
trackFromVoice(voice, options = { addRenderedAccidentals: false }) {
|
|
1023
|
+
const track = new Track;
|
|
1024
|
+
let wait = [];
|
|
1025
|
+
voice.tickables.forEach(tickable => {
|
|
1026
|
+
if (tickable.noteType === 'n') {
|
|
1027
|
+
track.addEvent(new NoteEvent({
|
|
1028
|
+
pitch: tickable.keys.map((pitch, index) => this.convertPitch(pitch, index, tickable, options.addRenderedAccidentals)),
|
|
1029
|
+
duration: this.convertDuration(tickable),
|
|
1030
|
+
wait
|
|
1031
|
+
}));
|
|
1032
|
+
// reset wait
|
|
1033
|
+
wait = [];
|
|
1034
|
+
}
|
|
1035
|
+
else if (tickable.noteType === 'r') {
|
|
1036
|
+
// move on to the next tickable and add this to the stack
|
|
1037
|
+
// of the `wait` property for the next note event
|
|
1038
|
+
wait.push(this.convertDuration(tickable));
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
// There may be outstanding rests at the end of the track,
|
|
1042
|
+
// pad with a ghost note (zero duration and velocity), just to capture the wait.
|
|
1043
|
+
if (wait.length > 0) {
|
|
1044
|
+
track.addEvent(new NoteEvent({ pitch: '[c4]', duration: '0', wait, velocity: '0' }));
|
|
1045
|
+
}
|
|
1046
|
+
return track;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Converts VexFlow pitch syntax to MidiWriterJS syntax
|
|
1050
|
+
* @param pitch string
|
|
1051
|
+
* @param index pitch index
|
|
1052
|
+
* @param note struct from Vexflow
|
|
1053
|
+
* @param addRenderedAccidentals adds Vexflow rendered accidentals
|
|
1054
|
+
*/
|
|
1055
|
+
convertPitch(pitch, index, note, addRenderedAccidentals = false) {
|
|
1056
|
+
var _a;
|
|
1057
|
+
// Splits note name from octave
|
|
1058
|
+
const pitchParts = pitch.split('/');
|
|
1059
|
+
// Retrieves accidentals from pitch
|
|
1060
|
+
// Removes natural accidentals since they are not accepted in Tonal Midi
|
|
1061
|
+
let accidentals = pitchParts[0].substring(1).replace('n', '');
|
|
1062
|
+
if (addRenderedAccidentals) {
|
|
1063
|
+
(_a = note.getAccidentals()) === null || _a === void 0 ? void 0 : _a.forEach(accidental => {
|
|
1064
|
+
if (accidental.index === index) {
|
|
1065
|
+
if (accidental.type === 'n') {
|
|
1066
|
+
accidentals = '';
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
accidentals += accidental.type;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
return pitchParts[0][0] + accidentals + pitchParts[1];
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Converts VexFlow duration syntax to MidiWriterJS syntax
|
|
1078
|
+
* @param note struct from VexFlow
|
|
1079
|
+
*/
|
|
1080
|
+
convertDuration(note) {
|
|
1081
|
+
return 'd'.repeat(note.dots) + this.convertBaseDuration(note.duration) + (note.tuplet ? 't' + note.tuplet.num_notes : '');
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Converts VexFlow base duration syntax to MidiWriterJS syntax
|
|
1085
|
+
* @param duration Vexflow duration
|
|
1086
|
+
* @returns MidiWriterJS duration
|
|
1087
|
+
*/
|
|
1088
|
+
convertBaseDuration(duration) {
|
|
1089
|
+
switch (duration) {
|
|
1090
|
+
case 'w':
|
|
1091
|
+
return '1';
|
|
1092
|
+
case 'h':
|
|
1093
|
+
return '2';
|
|
1094
|
+
case 'q':
|
|
1095
|
+
return '4';
|
|
1096
|
+
default:
|
|
1097
|
+
return duration;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Object representation of a header chunk section of a MIDI file.
|
|
1104
|
+
* @param {number} numberOfTracks - Number of tracks
|
|
1105
|
+
* @return {Header}
|
|
1106
|
+
*/
|
|
1107
|
+
class Header {
|
|
1108
|
+
constructor(numberOfTracks) {
|
|
1109
|
+
this.type = Constants.HEADER_CHUNK_TYPE;
|
|
1110
|
+
const trackType = numberOfTracks > 1 ? Constants.HEADER_CHUNK_FORMAT1 : Constants.HEADER_CHUNK_FORMAT0;
|
|
1111
|
+
this.data = trackType.concat(Utils.numberToBytes(numberOfTracks, 2), // two bytes long,
|
|
1112
|
+
Constants.HEADER_CHUNK_DIVISION);
|
|
1113
|
+
this.size = [0, 0, 0, this.data.length];
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Object that puts together tracks and provides methods for file output.
|
|
1119
|
+
* @param {array|Track} tracks - A single {Track} object or an array of {Track} objects.
|
|
1120
|
+
* @param {object} options - {middleC: 'C4'}
|
|
1121
|
+
* @return {Writer}
|
|
1122
|
+
*/
|
|
1123
|
+
class Writer {
|
|
1124
|
+
constructor(tracks, options = {}) {
|
|
1125
|
+
// Ensure tracks is an array
|
|
1126
|
+
this.tracks = Utils.toArray(tracks);
|
|
1127
|
+
this.options = options;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Builds array of data from chunkschunks.
|
|
1131
|
+
* @return {array}
|
|
1132
|
+
*/
|
|
1133
|
+
buildData() {
|
|
1134
|
+
const data = [];
|
|
1135
|
+
data.push(new Header(this.tracks.length));
|
|
1136
|
+
// For each track add final end of track event and build data
|
|
1137
|
+
this.tracks.forEach((track) => {
|
|
1138
|
+
data.push(track.buildData(this.options));
|
|
1139
|
+
});
|
|
1140
|
+
return data;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Builds the file into a Uint8Array
|
|
1144
|
+
* @return {Uint8Array}
|
|
1145
|
+
*/
|
|
1146
|
+
buildFile() {
|
|
1147
|
+
let build = [];
|
|
1148
|
+
// Data consists of chunks which consists of data
|
|
1149
|
+
this.buildData().forEach((d) => build = build.concat(d.type, d.size, d.data));
|
|
1150
|
+
return new Uint8Array(build);
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Convert file buffer to a base64 string. Different methods depending on if browser or node.
|
|
1154
|
+
* @return {string}
|
|
1155
|
+
*/
|
|
1156
|
+
base64() {
|
|
1157
|
+
if (typeof btoa === 'function') {
|
|
1158
|
+
let binary = '';
|
|
1159
|
+
const bytes = this.buildFile();
|
|
1160
|
+
const len = bytes.byteLength;
|
|
1161
|
+
for (let i = 0; i < len; i++) {
|
|
1162
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1163
|
+
}
|
|
1164
|
+
return btoa(binary);
|
|
1165
|
+
}
|
|
1166
|
+
return Buffer.from(this.buildFile()).toString('base64');
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Get the data URI.
|
|
1170
|
+
* @return {string}
|
|
1171
|
+
*/
|
|
1172
|
+
dataUri() {
|
|
1173
|
+
return 'data:audio/midi;base64,' + this.base64();
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Set option on instantiated Writer.
|
|
1177
|
+
* @param {string} key
|
|
1178
|
+
* @param {any} value
|
|
1179
|
+
* @return {Writer}
|
|
1180
|
+
*/
|
|
1181
|
+
setOption(key, value) {
|
|
1182
|
+
this.options[key] = value;
|
|
1183
|
+
return this;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Output to stdout
|
|
1187
|
+
* @return {string}
|
|
1188
|
+
*/
|
|
1189
|
+
stdout() {
|
|
1190
|
+
return process.stdout.write(Buffer.from(this.buildFile()));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
var main = {
|
|
1195
|
+
Constants,
|
|
1196
|
+
ControllerChangeEvent,
|
|
1197
|
+
CopyrightEvent,
|
|
1198
|
+
CuePointEvent,
|
|
1199
|
+
EndTrackEvent,
|
|
1200
|
+
InstrumentNameEvent,
|
|
1201
|
+
KeySignatureEvent,
|
|
1202
|
+
LyricEvent,
|
|
1203
|
+
MarkerEvent,
|
|
1204
|
+
NoteOnEvent,
|
|
1205
|
+
NoteOffEvent,
|
|
1206
|
+
NoteEvent,
|
|
1207
|
+
PitchBendEvent,
|
|
1208
|
+
ProgramChangeEvent,
|
|
1209
|
+
TempoEvent,
|
|
1210
|
+
TextEvent,
|
|
1211
|
+
TimeSignatureEvent,
|
|
1212
|
+
Track,
|
|
1213
|
+
TrackNameEvent,
|
|
1214
|
+
Utils,
|
|
1215
|
+
VexFlow,
|
|
1216
|
+
Writer
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
module.exports = main;
|