@jeffy-g/live-midi-types 0.10.0 → 0.11.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.
@@ -0,0 +1,473 @@
1
+ /*!
2
+ * Original Source: midi-file (https://github.com/carter-thaxton/midi-file/blob/master/lib/midi-writer.js)
3
+ * Original License: MIT License
4
+ *
5
+ * This file is a reimplementation of the original midi-writer.js with performance optimizations.
6
+ * The core logic is preserved, but the Writer class is replaced with a more efficient
7
+ * implementation that pre-calculates the buffer size and writes directly to a Uint8Array.
8
+ *
9
+ * Copyright (c) 2025 jeffy-g
10
+ * Released under the MIT license.
11
+ */
12
+ /**
13
+ * @file src/smf-writer.ts
14
+ */
15
+ /**
16
+ * @import {
17
+ * MidiData,
18
+ * MidiEvent,
19
+ * MidiHeader,
20
+ * MidiChannelEvent,
21
+ * MidiWriteOption,
22
+ * } from "./midi10-smf-types.ts";
23
+ */
24
+ /** cache object */
25
+ const encoder = new TextEncoder();
26
+ /**
27
+ * A highly efficient writer that pre-allocates a buffer and writes data directly
28
+ * into it using offsets. This avoids the massive overhead of Array.push/concat.
29
+ *
30
+ * @see {@link https://sites.google.com/site/yyagisite/material/smfspec?authuser=0 SMF (Standard MIDI Files) の構造}
31
+ */
32
+ class FastWriter {
33
+ /**
34
+ * @param {number} size
35
+ */
36
+ constructor(size) {
37
+ this.writeInt8 = this.writeUInt8;
38
+ this.buffer = new Uint8Array(size);
39
+ this.view = new DataView(this.buffer.buffer);
40
+ this.offset = 0;
41
+ }
42
+ /**
43
+ * @param {number} value
44
+ */
45
+ writeUInt8(value) {
46
+ this.view.setUint8(this.offset, value);
47
+ this.offset += 1;
48
+ }
49
+ /**
50
+ * @param {number} value
51
+ */
52
+ writeUInt16(value) {
53
+ this.view.setUint16(this.offset, value, false);
54
+ this.offset += 2;
55
+ }
56
+ /**
57
+ * @param {number} value
58
+ */
59
+ writeUInt24(value) {
60
+ const view = this.view,
61
+ offset = this.offset;
62
+ view.setUint8(offset, (value >> 16) & 0xff);
63
+ view.setUint8(offset + 1, (value >> 8) & 0xff);
64
+ view.setUint8(offset + 2, value & 0xff);
65
+ this.offset += 3;
66
+ }
67
+ /**
68
+ * @param {number} value
69
+ */
70
+ writeUInt32(value) {
71
+ this.view.setUint32(this.offset, value, false);
72
+ this.offset += 4;
73
+ }
74
+ /**
75
+ * @param {ArrayLike<number>} data
76
+ */
77
+ writeBytes(data) {
78
+ this.buffer.set(data, this.offset);
79
+ this.offset += data.length;
80
+ }
81
+ /**
82
+ * @param {string} str
83
+ */
84
+ writeString(str) {
85
+ this.writeBytes(encoder.encode(str));
86
+ }
87
+ /**
88
+ * @param {number} value
89
+ */
90
+ writeVarInt(value) {
91
+ const buf = this.buffer;
92
+ const offset = this.offset;
93
+ if (value <= 0x7f) {
94
+ buf[offset] = value;
95
+ this.offset = offset + 1;
96
+ return;
97
+ }
98
+ let v = value;
99
+ const bytes = [];
100
+ bytes.push(v & 0x7f);
101
+ v >>>= 7;
102
+ while (v) {
103
+ bytes.push((v & 0x7f) | 0x80);
104
+ v >>>= 7;
105
+ }
106
+ v = bytes.length;
107
+ this.offset = offset + v;
108
+ let i = 0;
109
+ while (v--) {
110
+ buf[offset + v] = bytes[i++];
111
+ }
112
+ }
113
+ /**
114
+ * @param {string} id
115
+ * @param {Uint8Array} data
116
+ */
117
+ writeChunk(id, data) {
118
+ this.writeString(id);
119
+ this.writeUInt32(data.length);
120
+ this.writeBytes(data);
121
+ }
122
+ /**
123
+ * @param {number} msPerBeat microsecondsPerBeat
124
+ */
125
+ writeSetTempo(msPerBeat) {
126
+ var view = this.view;
127
+ var offset = this.offset;
128
+ view.setUint8(offset, 0xff);
129
+ view.setUint8(offset + 1, 0x51);
130
+ view.setUint8(offset + 2, 3);
131
+ view.setUint8(offset + 3, (msPerBeat >> 16) & 0xff);
132
+ view.setUint8(offset + 4, (msPerBeat >> 8) & 0xff);
133
+ view.setUint8(offset + 5, msPerBeat & 0xff);
134
+ this.offset = offset + 6;
135
+ }
136
+ }
137
+ /**
138
+ * @param {number} type
139
+ * @throws {Error} Unrecognized event type
140
+ */
141
+ const throwInvalidType = (type) => {
142
+ throw new Error(`Unrecognized event type: ${type}`);
143
+ };
144
+ /**
145
+ * Returns the number of bytes required to encode a non-negative integer
146
+ * as a MIDI 1.0 Variable-Length Quantity (VLQ).
147
+ *
148
+ * VLQ encoding uses 7 bits of payload per byte; the MSB of each byte
149
+ * is a continuation flag (1 = more bytes follow, 0 = final byte).
150
+ * The MIDI 1.0 spec caps values at 4 bytes → max encodable value is
151
+ * `0x0FFF_FFFF` (268,435,455 / 28 bits).
152
+ *
153
+ * ```
154
+ * value range encoded bytes
155
+ * ─────────────────────────── ─────────────
156
+ * 0x0000_0000 – 0x0000_007F 1
157
+ * 0x0000_0080 – 0x0000_3FFF 2
158
+ * 0x0000_4000 – 0x001F_FFFF 3
159
+ * 0x0020_0000 – 0x0FFF_FFFF 4
160
+ * ```
161
+ *
162
+ * @see MIDI 1.0 Detailed Specification §2 — "Variable-Length Quantities"
163
+ *
164
+ * @param {number} value - A non-negative integer to encode as VLQ.
165
+ * @returns Byte count needed to hold the VLQ-encoded value (1–4).
166
+ * @throws {RangeError} If `value` is negative or exceeds `0x0FFF_FFFF`.
167
+ *
168
+ * @example
169
+ * getVarIntSize(0x0000_0000); // → 1 (MIDI delta-time of 0)
170
+ * getVarIntSize(0x0000_007F); // → 1
171
+ * getVarIntSize(0x0000_0080); // → 2
172
+ * getVarIntSize(0x0FFF_FFFF); // → 4 (spec maximum)
173
+ */
174
+ export function getVarIntSize(value) {
175
+ if (value < 0) throw new RangeError(`VLQ value must be ≥ 0 (got ${value})`);
176
+ if (value <= 127) return 1;
177
+ if (value <= 16383) return 2;
178
+ if (value <= 2097151) return 3;
179
+ if (value <= 268435455) return 4;
180
+ throw new RangeError(`VLQ value 0x${value.toString(16)} exceeds MIDI 1.0 maximum (0x0FFF_FFFF)`);
181
+ }
182
+ /**
183
+ * @template GType
184
+ * @typedef {GType & { bytes: Uint8Array }} TInternalTextEvent
185
+ */
186
+ /** @type {(event: MidiEvent) => number} */
187
+ // @ts-expect- error `useByte9ForNoteOff` is unused...
188
+ function calculateEventSize(event /* , useByte9ForNoteOff?: boolean */) {
189
+ let size = getVarIntSize(event.deltaTime);
190
+ /** @type {number} */
191
+ let temp;
192
+ switch (event.type) {
193
+ case 0 /* ESMFEventType.sequenceNumber */:
194
+ size += 5;
195
+ break;
196
+ case 1 /* ESMFEventType.text */:
197
+ case 2 /* ESMFEventType.copyrightNotice */:
198
+ case 3 /* ESMFEventType.trackName */:
199
+ case 4 /* ESMFEventType.instrumentName */:
200
+ case 5 /* ESMFEventType.lyrics */:
201
+ case 6 /* ESMFEventType.marker */:
202
+ case 7 /* ESMFEventType.cuePoint */: {
203
+ const u8data = encoder.encode(event.text);
204
+ const u8dataLen = u8data.length;
205
+ size += 2 + getVarIntSize(u8dataLen) + u8dataLen;
206
+ /** @type {TInternalTextEvent<typeof event>} */ (event).bytes = u8data;
207
+ break;
208
+ }
209
+ case 8 /* ESMFEventType.noteOff */:
210
+ case 9 /* ESMFEventType.noteOn */:
211
+ case 10 /* ESMFEventType.noteAftertouch */:
212
+ case 11 /* ESMFEventType.controller */:
213
+ case 14 /* ESMFEventType.pitchBend */:
214
+ size += 3 - +!!event.running;
215
+ break;
216
+ case 12 /* ESMFEventType.programChange */:
217
+ case 13 /* ESMFEventType.channelAftertouch */:
218
+ size += 2 - +!!event.running;
219
+ break;
220
+ case 32 /* ESMFEventType.channelPrefix */:
221
+ case 33 /* ESMFEventType.portPrefix */:
222
+ size += 4;
223
+ break;
224
+ case 47 /* ESMFEventType.endOfTrack */:
225
+ size += 3;
226
+ break;
227
+ case 81 /* ESMFEventType.setTempo */:
228
+ size += 6;
229
+ break;
230
+ case 84 /* ESMFEventType.smpteOffset */:
231
+ size += 8;
232
+ break;
233
+ case 88 /* ESMFEventType.timeSignature */:
234
+ size += 7;
235
+ break;
236
+ case 89 /* ESMFEventType.keySignature */:
237
+ size += 5;
238
+ break;
239
+ case -1 /* ESMFEventType.unknownMeta */:
240
+ case 127 /* ESMFEventType.sequencerSpecific */:
241
+ size += 2 + getVarIntSize((temp = event.data.length)) + temp;
242
+ break;
243
+ case 240 /* ESMFEventType.sysEx */:
244
+ case 247 /* ESMFEventType.endSysEx */:
245
+ size += 1 + getVarIntSize((temp = event.data.length)) + temp;
246
+ break;
247
+ default:
248
+ throwInvalidType(/** @type {MidiEvent} */ (event).type);
249
+ }
250
+ return size;
251
+ }
252
+ /**
253
+ * @param {MidiEvent[]} track
254
+ */
255
+ function calculateTrackSize(track) {
256
+ let size = 8;
257
+ const trackLen = track.length;
258
+ for (let idx = 0; idx < trackLen;) {
259
+ size += calculateEventSize(track[idx++]);
260
+ }
261
+ return size;
262
+ }
263
+ /**
264
+ * @param {MidiData} data
265
+ */
266
+ function calculateMidiSize(data) {
267
+ let size = 14;
268
+ const trackEvents = data.tracks;
269
+ for (let idx = 0, trackEventsLen = trackEvents.length; idx < trackEventsLen;) {
270
+ size += calculateTrackSize(trackEvents[idx++]);
271
+ }
272
+ return size;
273
+ }
274
+ /**
275
+ * @param {FastWriter} w
276
+ * @param {MidiHeader} header
277
+ * @param {number} numTracks
278
+ */
279
+ function writeHeader(w, header, numTracks) {
280
+ const format = header.format ?? 1;
281
+ let timeDivision = 128;
282
+ if (header.timeDivision) {
283
+ timeDivision = header.timeDivision;
284
+ } else if (header.ticksPerFrame && header.framesPerSecond) {
285
+ timeDivision = (-(header.framesPerSecond & 0xff) << 8) | (header.ticksPerFrame & 0xff);
286
+ } else if (header.ticksPerBeat) {
287
+ timeDivision = header.ticksPerBeat & 0x7fff;
288
+ }
289
+ const h = new FastWriter(6);
290
+ h.writeUInt16(format);
291
+ h.writeUInt16(numTracks);
292
+ h.writeUInt16(timeDivision);
293
+ w.writeChunk("MThd", h.buffer);
294
+ }
295
+ const FRAME_RATES = { 24: 0x00, 25: 0x20, 29.97: 0x40, 30: 0x60 };
296
+ /** @type {(w: FastWriter, event: MidiEvent, lastEventTypeByte: number | undefined, useByte9ForNoteOff?: boolean) => number | undefined} */
297
+ function writeEvent(w, event, lastEventTypeByte, useByte9ForNoteOff) {
298
+ /** @type {number=} */
299
+ let eventTypeByte;
300
+ w.writeVarInt(event.deltaTime);
301
+ switch (event.type) {
302
+ case 0 /* ESMFEventType.sequenceNumber */:
303
+ w.writeUInt8(0xff);
304
+ w.writeUInt8(0x00);
305
+ w.writeVarInt(2);
306
+ w.writeUInt16(event.number);
307
+ break;
308
+ case 1 /* ESMFEventType.text */:
309
+ case 2 /* ESMFEventType.copyrightNotice */:
310
+ case 3 /* ESMFEventType.trackName */:
311
+ case 4 /* ESMFEventType.instrumentName */:
312
+ case 5 /* ESMFEventType.lyrics */:
313
+ case 6 /* ESMFEventType.marker */:
314
+ case 7 /* ESMFEventType.cuePoint */: {
315
+ w.writeUInt8(0xff);
316
+ w.writeUInt8(event.type);
317
+ const textBytes = /** @type {TInternalTextEvent<typeof event>} */ (event).bytes;
318
+ w.writeVarInt(textBytes.length);
319
+ w.writeBytes(textBytes);
320
+ break;
321
+ }
322
+ case 8 /* ESMFEventType.noteOff */:
323
+ const noteMSB = (useByte9ForNoteOff !== false && event.byte9) || (useByte9ForNoteOff && event.velocity === 0) ? 0x90 : 0x80;
324
+ eventTypeByte = noteMSB | event.channel;
325
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
326
+ w.writeUInt8(event.noteNumber);
327
+ w.writeUInt8(event.velocity);
328
+ break;
329
+ case 9 /* ESMFEventType.noteOn */:
330
+ eventTypeByte = 0x90 | event.channel;
331
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
332
+ w.writeUInt8(event.noteNumber);
333
+ w.writeUInt8(event.velocity);
334
+ break;
335
+ case 10 /* ESMFEventType.noteAftertouch */:
336
+ eventTypeByte = 0xa0 | event.channel;
337
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
338
+ w.writeUInt8(event.noteNumber);
339
+ w.writeUInt8(event.amount);
340
+ break;
341
+ case 11 /* ESMFEventType.controller */:
342
+ eventTypeByte = 0xb0 | event.channel;
343
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
344
+ w.writeUInt8(event.controllerType);
345
+ w.writeUInt8(event.value);
346
+ break;
347
+ case 12 /* ESMFEventType.programChange */:
348
+ eventTypeByte = 0xc0 | event.channel;
349
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
350
+ w.writeUInt8(event.programNumber);
351
+ break;
352
+ case 13 /* ESMFEventType.channelAftertouch */:
353
+ eventTypeByte = 0xd0 | event.channel;
354
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
355
+ w.writeUInt8(event.amount);
356
+ break;
357
+ case 14 /* ESMFEventType.pitchBend */:
358
+ eventTypeByte = 0xe0 | event.channel;
359
+ if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte);
360
+ const value14 = 0x2000 + event.value;
361
+ w.writeUInt8(value14 & 0x7f);
362
+ w.writeUInt8((value14 >> 7) & 0x7f);
363
+ break;
364
+ case 32 /* ESMFEventType.channelPrefix */:
365
+ w.writeUInt8(0xff);
366
+ w.writeUInt8(0x20);
367
+ w.writeVarInt(1);
368
+ w.writeUInt8(event.channel);
369
+ break;
370
+ case 33 /* ESMFEventType.portPrefix */:
371
+ w.writeUInt8(0xff);
372
+ w.writeUInt8(0x21);
373
+ w.writeVarInt(1);
374
+ w.writeUInt8(event.port);
375
+ break;
376
+ case 47 /* ESMFEventType.endOfTrack */:
377
+ w.writeUInt8(0xff);
378
+ w.writeUInt8(0x2f);
379
+ w.writeVarInt(0);
380
+ break;
381
+ case 81 /* ESMFEventType.setTempo */:
382
+ w.writeSetTempo(event.microsecondsPerBeat);
383
+ break;
384
+ case 84 /* ESMFEventType.smpteOffset */:
385
+ w.writeUInt8(0xff);
386
+ w.writeUInt8(0x54);
387
+ w.writeVarInt(5);
388
+ const hourByte = (event.hour & 0x1f) | FRAME_RATES[event.frameRate];
389
+ w.writeUInt8(hourByte);
390
+ w.writeUInt8(event.min);
391
+ w.writeUInt8(event.sec);
392
+ w.writeUInt8(event.frame);
393
+ w.writeUInt8(event.subFrame);
394
+ break;
395
+ case 88 /* ESMFEventType.timeSignature */:
396
+ w.writeUInt8(0xff);
397
+ w.writeUInt8(0x58);
398
+ w.writeVarInt(4);
399
+ w.writeUInt8(event.numerator);
400
+ const denominator = Math.floor(Math.log(event.denominator) / Math.LN2) & 0xff;
401
+ w.writeUInt8(denominator);
402
+ w.writeUInt8(event.metronome);
403
+ w.writeUInt8(event.thirtyseconds || 8);
404
+ break;
405
+ case 89 /* ESMFEventType.keySignature */:
406
+ w.writeUInt8(0xff);
407
+ w.writeUInt8(0x59);
408
+ w.writeVarInt(2);
409
+ w.writeInt8(event.key);
410
+ w.writeUInt8(event.scale);
411
+ break;
412
+ case 127 /* ESMFEventType.sequencerSpecific */:
413
+ w.writeUInt8(0xff);
414
+ w.writeUInt8(0x7f);
415
+ w.writeVarInt(event.data.length);
416
+ w.writeBytes(event.data);
417
+ break;
418
+ case -1 /* ESMFEventType.unknownMeta */:
419
+ if (event.metatypeByte != null) {
420
+ w.writeUInt8(0xff);
421
+ w.writeUInt8(event.metatypeByte);
422
+ w.writeVarInt(event.data.length);
423
+ w.writeBytes(event.data);
424
+ }
425
+ break;
426
+ case 240 /* ESMFEventType.sysEx */:
427
+ w.writeUInt8(0xf0);
428
+ w.writeVarInt(event.data.length);
429
+ w.writeBytes(event.data);
430
+ break;
431
+ case 247 /* ESMFEventType.endSysEx */:
432
+ w.writeUInt8(0xf7);
433
+ w.writeVarInt(event.data.length);
434
+ w.writeBytes(event.data);
435
+ break;
436
+ default:
437
+ throwInvalidType(/** @type {MidiEvent} */ (event).type);
438
+ }
439
+ return eventTypeByte;
440
+ }
441
+ /** @type {(writer: FastWriter, track: MidiEvent[], opts: MidiWriteOption) => void} */
442
+ function writeTrack(writer, track, opts) {
443
+ writer.writeString("MTrk");
444
+ writer.writeUInt32(calculateTrackSize(track) - 8);
445
+ let eventTypeByte;
446
+ for (let idx = 0, L = track.length; idx < L;) {
447
+ const midiEvent = track[idx++];
448
+ if (opts.running === false || (!opts.running && !(/** @type {MidiChannelEvent<unknown>} */ (midiEvent).running))) {
449
+ eventTypeByte = undefined;
450
+ }
451
+ eventTypeByte = writeEvent(writer, midiEvent, eventTypeByte, opts.useByte9ForNoteOff);
452
+ }
453
+ }
454
+ /**
455
+ * Writes MIDI data to a Uint8Array.
456
+ * @param {MidiData} data The MIDI data to write.
457
+ * @param {MidiWriteOption=} opts Options for writing the MIDI file.
458
+ * @returns A Uint8Array containing the MIDI file data.
459
+ */
460
+ export function writeMidi(data, opts = {}) {
461
+ if (typeof data !== "object") {
462
+ throw new Error("Invalid MIDI data");
463
+ }
464
+ const header = data.header || {};
465
+ const tracks = data.tracks || [];
466
+ const totalSize = calculateMidiSize(data);
467
+ const w = new FastWriter(totalSize);
468
+ writeHeader(w, header, tracks.length);
469
+ for (const track of tracks) {
470
+ writeTrack(w, track, opts);
471
+ }
472
+ return w.buffer;
473
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffy-g/live-midi-types",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Common TypeScript types for Ableton Live MIDI clip and event data.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,18 +16,19 @@
16
16
  "require": "./dist/midi10-smf-types.js",
17
17
  "types": "./dist/midi10-smf-types.d.ts"
18
18
  },
19
- "./smf-event-type": {
20
- "import": "./dist/smf-event-type.js",
21
- "require": "./dist/smf-event-type.js",
22
- "types": "./dist/smf-event-type.d.ts"
19
+ "./midi10-smf-io": {
20
+ "types": "./dist/midi10-smf-io.d.ts",
21
+ "import": "./dist/midi10-smf-io.js",
22
+ "default": "./dist/midi10-smf-io.js"
23
23
  },
24
24
  "./ESMFEventType": {
25
- "types": "./ESMFEventType.d.ts"
25
+ "types": "./dist/ESMFEventType.d.ts",
26
+ "import": "./dist/smf-event-type.js",
27
+ "default": "./dist/smf-event-type.js"
26
28
  }
27
29
  },
28
30
  "files": [
29
- "dist",
30
- "ESMFEventType.d.ts"
31
+ "dist"
31
32
  ],
32
33
  "author": "jeffy-g <hirotom1107@gmail.com>",
33
34
  "license": "MIT",