@jeffy-g/live-midi-types 0.10.1 → 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.
- package/README.md +208 -106
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/midi-clip.d.ts +6 -4
- package/dist/midi10-smf-io.d.ts +12 -0
- package/dist/midi10-smf-io.js +12 -0
- package/dist/midi10-smf-types.d.ts +39 -24
- package/dist/project.d.ts +3 -1
- package/dist/smf-parser.d.ts +22 -0
- package/dist/smf-parser.js +287 -0
- package/dist/smf-writer.d.ts +53 -0
- package/dist/smf-writer.js +473 -0
- package/package.json +6 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Original Source: midi-file (https://github.com/carter-thaxton/midi-file/blob/master/lib/midi-parser.js)
|
|
3
|
+
* Original License: MIT License
|
|
4
|
+
*
|
|
5
|
+
* This file is a numeric-event reimplementation optimized for Uint8Array input.
|
|
6
|
+
* Copyright (c) 2026 jeffy-g
|
|
7
|
+
* Released under the MIT license.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @file src/smf-parser.ts
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @import {
|
|
14
|
+
* FrameRate,
|
|
15
|
+
* MidiData,
|
|
16
|
+
* MidiEvent,
|
|
17
|
+
* MidiHeader,
|
|
18
|
+
* } from "./midi10-smf-types";
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* ```
|
|
22
|
+
* // "MThd"
|
|
23
|
+
* String.fromCharCode(0x4d, 0x54, 0x68, 0x64) === "MThd"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
const MTHD = 0x4d546864;
|
|
27
|
+
/**
|
|
28
|
+
* ```
|
|
29
|
+
* // "MTrk"
|
|
30
|
+
* String.fromCharCode(0x4d, 0x54, 0x72, 0x6b) === "MTrk"
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
const MTRK = 0x4d54726b;
|
|
34
|
+
/** @type {FrameRate[]} */
|
|
35
|
+
const FRAME_RATES = [24, 25, 29.97, 30];
|
|
36
|
+
const decorder = new TextDecoder();
|
|
37
|
+
class SmfReader {
|
|
38
|
+
/**
|
|
39
|
+
* @param {Uint8Array} data
|
|
40
|
+
*/
|
|
41
|
+
constructor(data) {
|
|
42
|
+
this.pos = 0;
|
|
43
|
+
this.data = data;
|
|
44
|
+
}
|
|
45
|
+
readUInt8() {
|
|
46
|
+
return this.data[this.pos++];
|
|
47
|
+
}
|
|
48
|
+
readInt8() {
|
|
49
|
+
const value = this.data[this.pos++];
|
|
50
|
+
return value & 0x80 ? value - 0x100 : value;
|
|
51
|
+
}
|
|
52
|
+
readUInt16() {
|
|
53
|
+
const data = this.data;
|
|
54
|
+
const pos = this.pos;
|
|
55
|
+
this.pos = pos + 2;
|
|
56
|
+
return (data[pos] << 8) | data[pos + 1];
|
|
57
|
+
}
|
|
58
|
+
readUInt24() {
|
|
59
|
+
const data = this.data;
|
|
60
|
+
const pos = this.pos;
|
|
61
|
+
this.pos = pos + 3;
|
|
62
|
+
return (data[pos] << 16) | (data[pos + 1] << 8) | data[pos + 2];
|
|
63
|
+
}
|
|
64
|
+
readUInt32() {
|
|
65
|
+
const data = this.data;
|
|
66
|
+
const pos = this.pos;
|
|
67
|
+
this.pos = pos + 4;
|
|
68
|
+
return (data[pos] * 0x1000000 + (data[pos + 1] << 16) + (data[pos + 2] << 8) + data[pos + 3]) >>> 0;
|
|
69
|
+
}
|
|
70
|
+
readVarInt() {
|
|
71
|
+
const data = this.data;
|
|
72
|
+
let position = this.pos;
|
|
73
|
+
let result = 0;
|
|
74
|
+
for (let count = 0; count++ < 4;) {
|
|
75
|
+
const byte = data[position++];
|
|
76
|
+
result = (result << 7) | (byte & 0x7f);
|
|
77
|
+
if ((byte & 0x80) === 0) {
|
|
78
|
+
this.pos = position;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw new Error("MIDI variable-length integer exceeds four bytes");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* @param {number} length
|
|
86
|
+
*/
|
|
87
|
+
readBytes(length) {
|
|
88
|
+
const start = this.pos;
|
|
89
|
+
const end = start + length;
|
|
90
|
+
this.pos = end;
|
|
91
|
+
return this.data.slice(start, end);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* @param {number} length
|
|
95
|
+
*/
|
|
96
|
+
readString(length) {
|
|
97
|
+
const start = this.pos;
|
|
98
|
+
const end = start + length;
|
|
99
|
+
this.pos = end;
|
|
100
|
+
return decorder.decode(this.data.slice(start, end));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** @type {(id: number) => string} */
|
|
104
|
+
const chunkIdToString = (id) => String.fromCharCode(id >>> 24, (id >>> 16) & 0xff, (id >>> 8) & 0xff, id & 0xff);
|
|
105
|
+
/** @type {(reader: SmfReader, magicNumber: number) => number} */
|
|
106
|
+
const readChunkEnd = (reader, magicNumber /* , expectedName: string */) => {
|
|
107
|
+
const id = reader.readUInt32();
|
|
108
|
+
const length = reader.readUInt32();
|
|
109
|
+
if (id !== magicNumber) throw new Error(`Expected MIDI "${chunkIdToString(magicNumber)}" chunk, got "${chunkIdToString(id)}"`);
|
|
110
|
+
const end = reader.pos + length;
|
|
111
|
+
if (end > reader.data.length) throw new Error(`MIDI "${chunkIdToString(magicNumber)}" chunk exceeds the input length`);
|
|
112
|
+
return end;
|
|
113
|
+
};
|
|
114
|
+
/** @type {(reader: SmfReader) => MidiHeader} */
|
|
115
|
+
const parseHeader = (reader /* , end: number */) => {
|
|
116
|
+
const format = reader.readUInt16();
|
|
117
|
+
if (format > 2) throw new Error(`Unsupported MIDI format: ${format}`);
|
|
118
|
+
const numTracks = reader.readUInt16();
|
|
119
|
+
const timeDivision = reader.readUInt16();
|
|
120
|
+
const header = /** @type {MidiHeader} */ ({
|
|
121
|
+
format: format,
|
|
122
|
+
numTracks,
|
|
123
|
+
});
|
|
124
|
+
if (timeDivision & 0x8000) {
|
|
125
|
+
header.framesPerSecond = 0x100 - (timeDivision >> 8);
|
|
126
|
+
header.ticksPerFrame = timeDivision & 0xff;
|
|
127
|
+
} else {
|
|
128
|
+
header.ticksPerBeat = timeDivision;
|
|
129
|
+
}
|
|
130
|
+
return header;
|
|
131
|
+
};
|
|
132
|
+
/** @type {(reader: SmfReader, trackEnd: number, deltaTime: number, eventTypeByte: number) => MidiEvent} */
|
|
133
|
+
const readSystemEvent = (reader, trackEnd, deltaTime, eventTypeByte) => {
|
|
134
|
+
if (eventTypeByte === 0xff) {
|
|
135
|
+
/** read `ESMFEventType` */
|
|
136
|
+
const type = reader.readUInt8();
|
|
137
|
+
const length = reader.readVarInt();
|
|
138
|
+
if (reader.pos + length > trackEnd) throw new Error("MIDI meta event exceeds its track chunk");
|
|
139
|
+
switch (type) {
|
|
140
|
+
case 0 /* ESMFEventType.sequenceNumber */:
|
|
141
|
+
return { deltaTime, type, number: reader.readUInt16() };
|
|
142
|
+
case 1 /* ESMFEventType.text */:
|
|
143
|
+
case 2 /* ESMFEventType.copyrightNotice */:
|
|
144
|
+
case 3 /* ESMFEventType.trackName */:
|
|
145
|
+
case 4 /* ESMFEventType.instrumentName */:
|
|
146
|
+
case 5 /* ESMFEventType.lyrics */:
|
|
147
|
+
case 6 /* ESMFEventType.marker */:
|
|
148
|
+
case 7 /* ESMFEventType.cuePoint */:
|
|
149
|
+
return { deltaTime, type, text: reader.readString(length) };
|
|
150
|
+
case 32 /* ESMFEventType.channelPrefix */:
|
|
151
|
+
return { deltaTime, type, channel: reader.readUInt8() };
|
|
152
|
+
case 33 /* ESMFEventType.portPrefix */:
|
|
153
|
+
return { deltaTime, type, port: reader.readUInt8() };
|
|
154
|
+
case 47 /* ESMFEventType.endOfTrack */:
|
|
155
|
+
return { deltaTime, type };
|
|
156
|
+
case 81 /* ESMFEventType.setTempo */:
|
|
157
|
+
return { deltaTime, type, microsecondsPerBeat: reader.readUInt24() };
|
|
158
|
+
case 84 /* ESMFEventType.smpteOffset */: {
|
|
159
|
+
const hourByte = reader.readUInt8();
|
|
160
|
+
return {
|
|
161
|
+
deltaTime,
|
|
162
|
+
type,
|
|
163
|
+
frameRate: FRAME_RATES[(hourByte >> 5) & 0x03],
|
|
164
|
+
hour: hourByte & 0x1f,
|
|
165
|
+
min: reader.readUInt8(),
|
|
166
|
+
sec: reader.readUInt8(),
|
|
167
|
+
frame: reader.readUInt8(),
|
|
168
|
+
subFrame: reader.readUInt8(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case 88 /* ESMFEventType.timeSignature */: {
|
|
172
|
+
return {
|
|
173
|
+
deltaTime,
|
|
174
|
+
type,
|
|
175
|
+
numerator: reader.readUInt8(),
|
|
176
|
+
denominator: 1 << reader.readUInt8(),
|
|
177
|
+
metronome: length === 4 ? reader.readUInt8() : 0x24,
|
|
178
|
+
thirtyseconds: length === 4 ? reader.readUInt8() : 0x08,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
case 89 /* ESMFEventType.keySignature */:
|
|
182
|
+
return { deltaTime, type, key: reader.readInt8(), scale: reader.readUInt8() };
|
|
183
|
+
case 127 /* ESMFEventType.sequencerSpecific */:
|
|
184
|
+
return { deltaTime, type, data: reader.readBytes(length) };
|
|
185
|
+
default:
|
|
186
|
+
return { deltaTime, type: -1 /* ESMFEventType.unknownMeta */, metatypeByte: type, data: reader.readBytes(length) };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (eventTypeByte === 240 /* ESMFEventType.sysEx */ || eventTypeByte === 247 /* ESMFEventType.endSysEx */) {
|
|
190
|
+
const length = reader.readVarInt();
|
|
191
|
+
return {
|
|
192
|
+
deltaTime,
|
|
193
|
+
type: eventTypeByte,
|
|
194
|
+
data: reader.readBytes(length),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`Unrecognized MIDI event type byte: ${eventTypeByte}`);
|
|
198
|
+
};
|
|
199
|
+
/** @type {(reader: SmfReader, trackEnd: number) => MidiEvent[]} */
|
|
200
|
+
const parseTrack = (reader, trackEnd) => {
|
|
201
|
+
/** @type {MidiEvent[]} */
|
|
202
|
+
const events = [];
|
|
203
|
+
const data = reader.data;
|
|
204
|
+
let lastEventTypeByte = null;
|
|
205
|
+
while (reader.pos < trackEnd) {
|
|
206
|
+
const deltaTime = reader.readVarInt();
|
|
207
|
+
let eventTypeByte = data[reader.pos++];
|
|
208
|
+
if ((eventTypeByte & 0xf0) === 0xf0) {
|
|
209
|
+
events.push(readSystemEvent(reader, trackEnd, deltaTime, eventTypeByte));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
/** @type {number} */
|
|
213
|
+
let eventValue;
|
|
214
|
+
/** @type {number} */
|
|
215
|
+
let running;
|
|
216
|
+
if ((eventTypeByte & 0x80) === 0) {
|
|
217
|
+
if (lastEventTypeByte === null) throw new Error("Running status encountered before a channel status byte");
|
|
218
|
+
eventValue = eventTypeByte;
|
|
219
|
+
eventTypeByte = lastEventTypeByte;
|
|
220
|
+
running = 1;
|
|
221
|
+
} else {
|
|
222
|
+
eventValue = data[reader.pos++];
|
|
223
|
+
lastEventTypeByte = eventTypeByte;
|
|
224
|
+
running = 0;
|
|
225
|
+
}
|
|
226
|
+
const type = eventTypeByte >> 4;
|
|
227
|
+
const channel = eventTypeByte & 0x0f;
|
|
228
|
+
/** @type {MidiEvent} */
|
|
229
|
+
let event;
|
|
230
|
+
switch (type) {
|
|
231
|
+
case 8 /* ESMFEventType.noteOff */:
|
|
232
|
+
event = { deltaTime, type, channel, noteNumber: eventValue, velocity: data[reader.pos++] };
|
|
233
|
+
break;
|
|
234
|
+
case 9 /* ESMFEventType.noteOn */: {
|
|
235
|
+
const velocity = data[reader.pos++];
|
|
236
|
+
if (velocity === 0) event = { deltaTime, type: 8 /* ESMFEventType.noteOff */, channel, noteNumber: eventValue, velocity, byte9: true };
|
|
237
|
+
else event = { deltaTime, type, channel, noteNumber: eventValue, velocity };
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 10 /* ESMFEventType.noteAftertouch */:
|
|
241
|
+
event = { deltaTime, type, channel, noteNumber: eventValue, amount: data[reader.pos++] };
|
|
242
|
+
break;
|
|
243
|
+
case 11 /* ESMFEventType.controller */:
|
|
244
|
+
event = { deltaTime, type, channel, controllerType: eventValue, value: data[reader.pos++] };
|
|
245
|
+
break;
|
|
246
|
+
case 12 /* ESMFEventType.programChange */:
|
|
247
|
+
event = { deltaTime, type, channel, programNumber: eventValue };
|
|
248
|
+
break;
|
|
249
|
+
case 13 /* ESMFEventType.channelAftertouch */:
|
|
250
|
+
event = { deltaTime, type, channel, amount: eventValue };
|
|
251
|
+
break;
|
|
252
|
+
case 14 /* ESMFEventType.pitchBend */:
|
|
253
|
+
event = { deltaTime, type, channel, value: eventValue + (data[reader.pos++] << 7) - 0x2000 };
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
throw new Error(`Unrecognized MIDI channel event type: ${type}`);
|
|
257
|
+
}
|
|
258
|
+
if (reader.pos > trackEnd) throw new Error("MIDI channel event exceeds its track chunk");
|
|
259
|
+
if (running) event.running = true;
|
|
260
|
+
events.push(event);
|
|
261
|
+
}
|
|
262
|
+
return events;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Parse a Standard MIDI File into numeric {@link ESMFEventType} events.
|
|
266
|
+
*
|
|
267
|
+
* The Uint8Array fast path parses all chunks through a shared cursor and does
|
|
268
|
+
* not copy complete track payloads. Other array-like inputs are normalized once.
|
|
269
|
+
*
|
|
270
|
+
* @param {ArrayLike<number> | ArrayBuffer} input
|
|
271
|
+
* @returns {MidiData}
|
|
272
|
+
*/
|
|
273
|
+
export const parseMidi = (input) => {
|
|
274
|
+
const data = input instanceof Uint8Array ? input : input instanceof ArrayBuffer ? new Uint8Array(input) : Uint8Array.from(input);
|
|
275
|
+
const reader = new SmfReader(data);
|
|
276
|
+
const headerEnd = readChunkEnd(reader, MTHD /* , "MThd" */);
|
|
277
|
+
const header = parseHeader(reader /* , headerEnd */);
|
|
278
|
+
reader.pos = headerEnd;
|
|
279
|
+
/** @type {MidiEvent[][]} */
|
|
280
|
+
const tracks = [];
|
|
281
|
+
for (let i = 0, L = header.numTracks; i++ < L;) {
|
|
282
|
+
const trackEnd = readChunkEnd(reader, MTRK /* , "MTrk" */);
|
|
283
|
+
tracks.push(parseTrack(reader, trackEnd));
|
|
284
|
+
reader.pos = trackEnd;
|
|
285
|
+
}
|
|
286
|
+
return { header, tracks };
|
|
287
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import type { MidiData, MidiWriteOption } from "./midi10-smf-types.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Returns the number of bytes required to encode a non-negative integer
|
|
18
|
+
* as a MIDI 1.0 Variable-Length Quantity (VLQ).
|
|
19
|
+
*
|
|
20
|
+
* VLQ encoding uses 7 bits of payload per byte; the MSB of each byte
|
|
21
|
+
* is a continuation flag (1 = more bytes follow, 0 = final byte).
|
|
22
|
+
* The MIDI 1.0 spec caps values at 4 bytes → max encodable value is
|
|
23
|
+
* `0x0FFF_FFFF` (268,435,455 / 28 bits).
|
|
24
|
+
*
|
|
25
|
+
* ```
|
|
26
|
+
* value range encoded bytes
|
|
27
|
+
* ─────────────────────────── ─────────────
|
|
28
|
+
* 0x0000_0000 – 0x0000_007F 1
|
|
29
|
+
* 0x0000_0080 – 0x0000_3FFF 2
|
|
30
|
+
* 0x0000_4000 – 0x001F_FFFF 3
|
|
31
|
+
* 0x0020_0000 – 0x0FFF_FFFF 4
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @see MIDI 1.0 Detailed Specification §2 — "Variable-Length Quantities"
|
|
35
|
+
*
|
|
36
|
+
* @param {number} value - A non-negative integer to encode as VLQ.
|
|
37
|
+
* @returns Byte count needed to hold the VLQ-encoded value (1–4).
|
|
38
|
+
* @throws {RangeError} If `value` is negative or exceeds `0x0FFF_FFFF`.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* getVarIntSize(0x0000_0000); // → 1 (MIDI delta-time of 0)
|
|
42
|
+
* getVarIntSize(0x0000_007F); // → 1
|
|
43
|
+
* getVarIntSize(0x0000_0080); // → 2
|
|
44
|
+
* getVarIntSize(0x0FFF_FFFF); // → 4 (spec maximum)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getVarIntSize(value: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Writes MIDI data to a Uint8Array.
|
|
49
|
+
* @param {MidiData} data The MIDI data to write.
|
|
50
|
+
* @param {MidiWriteOption=} opts Options for writing the MIDI file.
|
|
51
|
+
* @returns A Uint8Array containing the MIDI file data.
|
|
52
|
+
*/
|
|
53
|
+
export declare function writeMidi(data: MidiData, opts?: MidiWriteOption): Uint8Array;
|