@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.
- package/README.md +208 -106
- package/{ESMFEventType.d.ts → dist/ESMFEventType.d.ts} +26 -40
- package/dist/ESMFEventType.js +11 -0
- 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 +40 -25
- package/dist/midi10-smf-types.js +0 -1
- package/dist/project.d.ts +4 -2
- 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 +9 -8
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* @file src/midi10-smf-types.ts
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
import type { ESMFEventType } from "./ESMFEventType.ts";
|
|
14
14
|
export interface MidiHeader {
|
|
15
15
|
format: 0 | 1 | 2;
|
|
16
16
|
numTracks: number;
|
|
@@ -20,22 +20,20 @@ export interface MidiHeader {
|
|
|
20
20
|
ticksPerBeat?: number;
|
|
21
21
|
}
|
|
22
22
|
export type TextEventTypesUnion = ESMFEventType.text | ESMFEventType.copyrightNotice | ESMFEventType.trackName | ESMFEventType.instrumentName | ESMFEventType.lyrics | ESMFEventType.marker | ESMFEventType.cuePoint;
|
|
23
|
-
export interface MidiBaseEvent<
|
|
23
|
+
export interface MidiBaseEvent<GEventType> {
|
|
24
24
|
/**
|
|
25
25
|
* serialize/deserialize by `writeVarInt/readVarInt`
|
|
26
26
|
* + byte range are __`1 to 4`__ (unsigned integer)
|
|
27
27
|
*/
|
|
28
28
|
deltaTime: number;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Numeric event discriminator shared by channel, system-exclusive, and meta events.
|
|
31
|
+
* SHOULD be `ESMFEventType`.
|
|
31
32
|
* @see {@link ESMFEventType}
|
|
32
33
|
*/
|
|
33
|
-
type:
|
|
34
|
+
type: GEventType;
|
|
34
35
|
}
|
|
35
|
-
export
|
|
36
|
-
meta?: true;
|
|
37
|
-
}
|
|
38
|
-
export type MidiEndOfTrackEvent = MidiMetaEvent<ESMFEventType.endOfTrack>;
|
|
36
|
+
export type MidiEndOfTrackEvent = MidiBaseEvent<ESMFEventType.endOfTrack>;
|
|
39
37
|
export type FrameRate = 24 | 25 | 29.97 | 30;
|
|
40
38
|
export interface MidiSmpteOffsetMixins {
|
|
41
39
|
frameRate: FrameRate;
|
|
@@ -45,7 +43,7 @@ export interface MidiSmpteOffsetMixins {
|
|
|
45
43
|
frame: number;
|
|
46
44
|
subFrame: number;
|
|
47
45
|
}
|
|
48
|
-
export type MidiSmpteOffsetEvent =
|
|
46
|
+
export type MidiSmpteOffsetEvent = MidiBaseEvent<ESMFEventType.smpteOffset> & MidiSmpteOffsetMixins;
|
|
49
47
|
export interface MidiTimeSignatureMixins {
|
|
50
48
|
numerator: number;
|
|
51
49
|
denominator: number;
|
|
@@ -54,47 +52,47 @@ export interface MidiTimeSignatureMixins {
|
|
|
54
52
|
/** @todo whats this? */
|
|
55
53
|
thirtyseconds: number;
|
|
56
54
|
}
|
|
57
|
-
export type MidiTimeSignatureEvent =
|
|
55
|
+
export type MidiTimeSignatureEvent = MidiBaseEvent<ESMFEventType.timeSignature> & MidiTimeSignatureMixins;
|
|
58
56
|
export interface MidiKeySignatureMixins {
|
|
59
57
|
key: number;
|
|
60
58
|
scale: number;
|
|
61
59
|
}
|
|
62
|
-
export type MidiKeySignatureEvent =
|
|
60
|
+
export type MidiKeySignatureEvent = MidiBaseEvent<ESMFEventType.keySignature> & MidiKeySignatureMixins;
|
|
63
61
|
export interface MidiDataMixins {
|
|
64
62
|
data: ArrayLike<number>;
|
|
65
63
|
}
|
|
66
64
|
export type MidiSysExEvent = MidiBaseEvent<ESMFEventType.sysEx> & MidiDataMixins;
|
|
67
65
|
export type MidiEndSysExEvent = MidiBaseEvent<ESMFEventType.endSysEx> & MidiDataMixins;
|
|
68
|
-
export type MidiSequencerSpecificEvent =
|
|
66
|
+
export type MidiSequencerSpecificEvent = MidiBaseEvent<ESMFEventType.sequencerSpecific> & MidiDataMixins;
|
|
69
67
|
export interface MidiUnknownMixins {
|
|
70
68
|
data: ArrayLike<number>;
|
|
71
69
|
metatypeByte: number;
|
|
72
70
|
}
|
|
73
|
-
export type MidiUnknownEvent =
|
|
71
|
+
export type MidiUnknownEvent = MidiBaseEvent<ESMFEventType.unknownMeta> & MidiUnknownMixins;
|
|
74
72
|
export interface MidiNumberMixins {
|
|
75
73
|
number: number;
|
|
76
74
|
}
|
|
77
|
-
export type MidiSequenceNumberEvent =
|
|
75
|
+
export type MidiSequenceNumberEvent = MidiBaseEvent<ESMFEventType.sequenceNumber> & MidiNumberMixins;
|
|
78
76
|
export interface MidiTextMixins {
|
|
79
77
|
text: string;
|
|
80
78
|
}
|
|
81
|
-
export type MidiTextEvent =
|
|
82
|
-
export type MidiLyricsEvent =
|
|
83
|
-
export type MidiMarkerEvent =
|
|
84
|
-
export type MidiCuePointEvent =
|
|
85
|
-
export type MidiTrackNameEvent =
|
|
86
|
-
export type MidiInstrumentNameEvent =
|
|
87
|
-
export type MidiCopyrightNoticeEvent =
|
|
88
|
-
export type MidiSetTempoEvent =
|
|
79
|
+
export type MidiTextEvent = MidiBaseEvent<ESMFEventType.text> & MidiTextMixins;
|
|
80
|
+
export type MidiLyricsEvent = MidiBaseEvent<ESMFEventType.lyrics> & MidiTextMixins;
|
|
81
|
+
export type MidiMarkerEvent = MidiBaseEvent<ESMFEventType.marker> & MidiTextMixins;
|
|
82
|
+
export type MidiCuePointEvent = MidiBaseEvent<ESMFEventType.cuePoint> & MidiTextMixins;
|
|
83
|
+
export type MidiTrackNameEvent = MidiBaseEvent<ESMFEventType.trackName> & MidiTextMixins;
|
|
84
|
+
export type MidiInstrumentNameEvent = MidiBaseEvent<ESMFEventType.instrumentName> & MidiTextMixins;
|
|
85
|
+
export type MidiCopyrightNoticeEvent = MidiBaseEvent<ESMFEventType.copyrightNotice> & MidiTextMixins;
|
|
86
|
+
export type MidiSetTempoEvent = MidiBaseEvent<ESMFEventType.setTempo> & {
|
|
89
87
|
/**
|
|
90
88
|
* __`unsigned 24-bit integer`__
|
|
91
89
|
*/
|
|
92
90
|
microsecondsPerBeat: number;
|
|
93
91
|
};
|
|
94
|
-
export type MidiPortPrefixEvent =
|
|
92
|
+
export type MidiPortPrefixEvent = MidiBaseEvent<ESMFEventType.portPrefix> & {
|
|
95
93
|
port: number;
|
|
96
94
|
};
|
|
97
|
-
export type MidiChannelPrefixEvent =
|
|
95
|
+
export type MidiChannelPrefixEvent = MidiBaseEvent<ESMFEventType.channelPrefix> & {
|
|
98
96
|
channel: number;
|
|
99
97
|
};
|
|
100
98
|
export interface MidiNoteMixins {
|
|
@@ -121,7 +119,7 @@ export interface MidiControllerMixins {
|
|
|
121
119
|
*/
|
|
122
120
|
value: number;
|
|
123
121
|
}
|
|
124
|
-
export interface MidiChannelEvent<
|
|
122
|
+
export interface MidiChannelEvent<GEventType> extends MidiBaseEvent<GEventType> {
|
|
125
123
|
running?: true;
|
|
126
124
|
channel: number;
|
|
127
125
|
}
|
|
@@ -168,6 +166,23 @@ export type MidiEvent =
|
|
|
168
166
|
| MidiCopyrightNoticeEvent
|
|
169
167
|
| MidiChannelAftertouchEvent
|
|
170
168
|
| MidiSequencerSpecificEvent;
|
|
169
|
+
/**
|
|
170
|
+
* Common Utilities
|
|
171
|
+
*/
|
|
172
|
+
export type WithAbsoluteTime = {
|
|
173
|
+
/**
|
|
174
|
+
* `absoluteTime` is same as `ticks`
|
|
175
|
+
*/
|
|
176
|
+
absoluteTime: number;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* MIDI Event Derivation
|
|
180
|
+
*/
|
|
181
|
+
export type MidiEventWithAbsTime<GEvent extends MidiEvent = MidiEvent> = GEvent & WithAbsoluteTime;
|
|
182
|
+
/**
|
|
183
|
+
* MIDI Event Derivation
|
|
184
|
+
*/
|
|
185
|
+
export type TTrackEvents<GEvent extends MidiEvent = MidiEvent> = Array<MidiEventWithAbsTime<GEvent>>;
|
|
171
186
|
export interface MidiData {
|
|
172
187
|
header: MidiHeader;
|
|
173
188
|
tracks: MidiEvent[][];
|
package/dist/midi10-smf-types.js
CHANGED
package/dist/project.d.ts
CHANGED
|
@@ -136,7 +136,9 @@ export type TTrackInfo<T extends Record<string, any>> = {
|
|
|
136
136
|
/**
|
|
137
137
|
* MIDI clips indexed by {@linkcode TClipSummary.id TClipSummary.id}
|
|
138
138
|
*/
|
|
139
|
-
clips:
|
|
139
|
+
clips: {
|
|
140
|
+
[uuidLike: string]: TClipSummary<T>;
|
|
141
|
+
};
|
|
140
142
|
/**
|
|
141
143
|
* @since v0.10.0
|
|
142
144
|
*/
|
|
@@ -163,7 +165,7 @@ export type TParsedAbletonLiveSet<T extends Record<string, any>> = {
|
|
|
163
165
|
*
|
|
164
166
|
* + Currently contains only MIDI tracks.
|
|
165
167
|
*/
|
|
166
|
-
tracks: TTrackInfo<T
|
|
168
|
+
tracks: Array<TTrackInfo<T>>;
|
|
167
169
|
/**
|
|
168
170
|
* Tempo automation events extracted from the master track.
|
|
169
171
|
*
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
import type { MidiData } from "./midi10-smf-types.ts";
|
|
13
|
+
/**
|
|
14
|
+
* Parse a Standard MIDI File into numeric {@link ESMFEventType} events.
|
|
15
|
+
*
|
|
16
|
+
* The Uint8Array fast path parses all chunks through a shared cursor and does
|
|
17
|
+
* not copy complete track payloads. Other array-like inputs are normalized once.
|
|
18
|
+
*
|
|
19
|
+
* @param {ArrayLike<number> | ArrayBuffer} input
|
|
20
|
+
* @returns {MidiData}
|
|
21
|
+
*/
|
|
22
|
+
export declare const parseMidi: (input: ArrayLike<number> | ArrayBuffer) => MidiData;
|
|
@@ -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;
|