@karaplay/file-coder 1.5.0 → 1.5.2
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/DEMO_ENHANCED.md +207 -134
- package/DOCUMENTATION_INDEX.md +317 -0
- package/EMK_REFERENCE_DATA.json +190 -0
- package/EMK_SONGS_INFO.md +336 -0
- package/EMK_TEST_SUITE_README.md +456 -0
- package/EMK_TEST_SUITE_SUMMARY.txt +197 -0
- package/README.md +90 -0
- package/RELEASE_v1.5.1.md +190 -0
- package/RELEASE_v1.5.2.md +238 -0
- package/SONG_LIST.txt +268 -0
- package/TEMPO_TRICKS_SUMMARY.md +240 -0
- package/demo-libs/KarFile.js +391 -0
- package/demo-libs/MIDIEvents.js +325 -0
- package/demo-libs/MIDIFile.js +450 -0
- package/demo-libs/MIDIFileHeader.js +144 -0
- package/demo-libs/MIDIFileTrack.js +111 -0
- package/demo-libs/TextEncoding.js +275 -0
- package/demo-libs/UTF8.js +151 -0
- package/demo-server.js +78 -1
- package/demo-simple.html +287 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -1
- package/dist/kar-validator.d.ts +66 -0
- package/dist/kar-validator.js +152 -0
- package/dist/ncntokar.browser.js +13 -1
- package/dist/ncntokar.js +13 -1
- package/package.json +4 -1
- package/verify-emk-reference.js +230 -0
- package/analyze-emk-cursor.js +0 -169
- package/analyze-emk-simple.js +0 -124
- package/check-real-duration.js +0 -69
- package/temp/test_output.kar +0 -0
- package/test-all-emk-durations.js +0 -109
- package/test-convert-001.js +0 -130
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MIDIFile : Read (and soon edit) a MIDI file in a given ArrayBuffer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MIDIFileHeader = require("./MIDIFileHeader");
|
|
8
|
+
const MIDIFileTrack = require("./MIDIFileTrack");
|
|
9
|
+
const MIDIEvents = require("./MIDIEvents");
|
|
10
|
+
const UTF8 = require("./UTF8");
|
|
11
|
+
const TextEncoding = require("./TextEncoding");
|
|
12
|
+
class MIDIFile {
|
|
13
|
+
constructor(buffer, strictMode) {
|
|
14
|
+
let track;
|
|
15
|
+
let curIndex;
|
|
16
|
+
let i;
|
|
17
|
+
let j;
|
|
18
|
+
let arrayBuffer;
|
|
19
|
+
// If not buffer given, creating a new MIDI file
|
|
20
|
+
if (!buffer) {
|
|
21
|
+
// Creating the content
|
|
22
|
+
this.header = new MIDIFileHeader();
|
|
23
|
+
this.tracks = [new MIDIFileTrack()];
|
|
24
|
+
// if a buffer is provided, parsing him
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Convert Node.js Buffer to ArrayBuffer if needed
|
|
28
|
+
if (Buffer.isBuffer(buffer)) {
|
|
29
|
+
// Convert Node.js Buffer to ArrayBuffer
|
|
30
|
+
arrayBuffer = new Uint8Array(buffer).buffer;
|
|
31
|
+
}
|
|
32
|
+
else if (buffer instanceof Uint8Array) {
|
|
33
|
+
const slice = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
34
|
+
arrayBuffer = slice instanceof ArrayBuffer ? slice : new Uint8Array(slice).buffer;
|
|
35
|
+
}
|
|
36
|
+
else if (buffer instanceof ArrayBuffer) {
|
|
37
|
+
arrayBuffer = buffer;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new Error('Invalid buffer received. Expected Buffer, ArrayBuffer, or Uint8Array.');
|
|
41
|
+
}
|
|
42
|
+
// Minimum MIDI file size is a headerChunk size (14bytes)
|
|
43
|
+
// and an empty track (8+3bytes)
|
|
44
|
+
if (25 > arrayBuffer.byteLength) {
|
|
45
|
+
throw new Error('A buffer of a valid MIDI file must have, at least, a' +
|
|
46
|
+
' size of 25bytes.');
|
|
47
|
+
}
|
|
48
|
+
// Reading header
|
|
49
|
+
this.header = new MIDIFileHeader(arrayBuffer, strictMode);
|
|
50
|
+
this.tracks = [];
|
|
51
|
+
curIndex = MIDIFileHeader.HEADER_LENGTH;
|
|
52
|
+
// Reading tracks
|
|
53
|
+
for (i = 0, j = this.header.getTracksCount(); i < j; i++) {
|
|
54
|
+
// Testing the buffer length
|
|
55
|
+
if (strictMode && curIndex >= arrayBuffer.byteLength - 1) {
|
|
56
|
+
throw new Error('Couldn\'t find datas corresponding to the track #' + i + '.');
|
|
57
|
+
}
|
|
58
|
+
// Creating the track object
|
|
59
|
+
track = new MIDIFileTrack(arrayBuffer, curIndex, strictMode);
|
|
60
|
+
this.tracks.push(track);
|
|
61
|
+
// Updating index to the track end
|
|
62
|
+
curIndex += track.getTrackLength() + 8;
|
|
63
|
+
}
|
|
64
|
+
// Testing integrity : curIndex should be at the end of the buffer
|
|
65
|
+
if (strictMode && curIndex !== arrayBuffer.byteLength) {
|
|
66
|
+
throw new Error('It seems that the buffer contains too much datas.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Events reading helpers
|
|
71
|
+
getEvents(type, subtype) {
|
|
72
|
+
let events;
|
|
73
|
+
let event;
|
|
74
|
+
let playTime = 0;
|
|
75
|
+
const filteredEvents = [];
|
|
76
|
+
const format = this.header.getFormat();
|
|
77
|
+
let tickResolution = this.header.getTickResolution();
|
|
78
|
+
let i;
|
|
79
|
+
let j;
|
|
80
|
+
let trackParsers;
|
|
81
|
+
let smallestDelta;
|
|
82
|
+
// Reading events
|
|
83
|
+
// if the read is sequential
|
|
84
|
+
if (1 !== format || 1 === this.tracks.length) {
|
|
85
|
+
for (i = 0, j = this.tracks.length; i < j; i++) {
|
|
86
|
+
// reset playtime if format is 2
|
|
87
|
+
playTime = (2 === format && playTime ? playTime : 0);
|
|
88
|
+
events = MIDIEvents.createParser(this.tracks[i].getTrackContent(), 0, false);
|
|
89
|
+
// loooping through events
|
|
90
|
+
event = events.next();
|
|
91
|
+
while (event) {
|
|
92
|
+
playTime += event.delta ? (event.delta * tickResolution) / 1000 : 0;
|
|
93
|
+
if (event.type === MIDIEvents.EVENT_META) {
|
|
94
|
+
// tempo change events
|
|
95
|
+
if (event.subtype === MIDIEvents.EVENT_META_SET_TEMPO) {
|
|
96
|
+
tickResolution = this.header.getTickResolution(event.tempo);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// push the asked events
|
|
100
|
+
if (((!type) || event.type === type) &&
|
|
101
|
+
((!subtype) || (event.subtype && event.subtype === subtype))) {
|
|
102
|
+
event.playTime = playTime;
|
|
103
|
+
filteredEvents.push(event);
|
|
104
|
+
}
|
|
105
|
+
event = events.next();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// the read is concurrent
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
trackParsers = [];
|
|
112
|
+
smallestDelta = -1;
|
|
113
|
+
// Creating parsers
|
|
114
|
+
for (i = 0, j = this.tracks.length; i < j; i++) {
|
|
115
|
+
trackParsers[i] = {
|
|
116
|
+
parser: MIDIEvents.createParser(this.tracks[i].getTrackContent(), 0, false),
|
|
117
|
+
curEvent: null
|
|
118
|
+
};
|
|
119
|
+
trackParsers[i].curEvent = trackParsers[i].parser.next();
|
|
120
|
+
}
|
|
121
|
+
// Filling events
|
|
122
|
+
do {
|
|
123
|
+
smallestDelta = -1;
|
|
124
|
+
// finding the smallest event
|
|
125
|
+
for (i = 0, j = trackParsers.length; i < j; i++) {
|
|
126
|
+
if (trackParsers[i].curEvent) {
|
|
127
|
+
if (-1 === smallestDelta || trackParsers[i].curEvent.delta <
|
|
128
|
+
trackParsers[smallestDelta].curEvent.delta) {
|
|
129
|
+
smallestDelta = i;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (-1 !== smallestDelta) {
|
|
134
|
+
// removing the delta of previous events
|
|
135
|
+
for (i = 0, j = trackParsers.length; i < j; i++) {
|
|
136
|
+
if (i !== smallestDelta && trackParsers[i].curEvent) {
|
|
137
|
+
trackParsers[i].curEvent.delta -= trackParsers[smallestDelta].curEvent.delta;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// filling values
|
|
141
|
+
event = trackParsers[smallestDelta].curEvent;
|
|
142
|
+
playTime += (event.delta ? (event.delta * tickResolution) / 1000 : 0);
|
|
143
|
+
if (event.type === MIDIEvents.EVENT_META) {
|
|
144
|
+
// tempo change events
|
|
145
|
+
if (event.subtype === MIDIEvents.EVENT_META_SET_TEMPO) {
|
|
146
|
+
tickResolution = this.header.getTickResolution(event.tempo);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// push midi events
|
|
150
|
+
if (((!type) || event.type === type) &&
|
|
151
|
+
((!subtype) || (event.subtype && event.subtype === subtype))) {
|
|
152
|
+
event.playTime = playTime;
|
|
153
|
+
event.track = smallestDelta;
|
|
154
|
+
filteredEvents.push(event);
|
|
155
|
+
}
|
|
156
|
+
// getting next event
|
|
157
|
+
trackParsers[smallestDelta].curEvent = trackParsers[smallestDelta].parser.next();
|
|
158
|
+
}
|
|
159
|
+
} while (-1 !== smallestDelta);
|
|
160
|
+
}
|
|
161
|
+
return filteredEvents;
|
|
162
|
+
}
|
|
163
|
+
getMidiEvents() {
|
|
164
|
+
return this.getEvents(MIDIEvents.EVENT_MIDI);
|
|
165
|
+
}
|
|
166
|
+
getLyrics() {
|
|
167
|
+
const events = this.getEvents(MIDIEvents.EVENT_META);
|
|
168
|
+
let texts = [];
|
|
169
|
+
const lyrics = [];
|
|
170
|
+
let event;
|
|
171
|
+
let i;
|
|
172
|
+
let j;
|
|
173
|
+
for (i = 0, j = events.length; i < j; i++) {
|
|
174
|
+
event = events[i];
|
|
175
|
+
// Lyrics
|
|
176
|
+
if (event.subtype === MIDIEvents.EVENT_META_LYRICS) {
|
|
177
|
+
lyrics.push(event);
|
|
178
|
+
// Texts
|
|
179
|
+
}
|
|
180
|
+
else if (event.subtype === MIDIEvents.EVENT_META_TEXT) {
|
|
181
|
+
// Ignore special texts
|
|
182
|
+
if (event.data && event.data.length > 0 && '@' === String.fromCharCode(event.data[0])) {
|
|
183
|
+
if (event.data.length > 1 && 'T' === String.fromCharCode(event.data[1])) {
|
|
184
|
+
// console.log('Title : ' + event.text.substring(2));
|
|
185
|
+
}
|
|
186
|
+
else if (event.data.length > 1 && 'I' === String.fromCharCode(event.data[1])) {
|
|
187
|
+
// console.log('Info : ' + event.text.substring(2));
|
|
188
|
+
}
|
|
189
|
+
else if (event.data.length > 1 && 'L' === String.fromCharCode(event.data[1])) {
|
|
190
|
+
// console.log('Lang : ' + event.text.substring(2));
|
|
191
|
+
}
|
|
192
|
+
// karaoke text follows, remove all previous text
|
|
193
|
+
}
|
|
194
|
+
else if (event.data && 0 === String.fromCharCode.apply(String, event.data).indexOf('words')) {
|
|
195
|
+
texts.length = 0;
|
|
196
|
+
// console.log('Word marker found');
|
|
197
|
+
// Karaoke texts
|
|
198
|
+
// If playtime is greater than 0
|
|
199
|
+
}
|
|
200
|
+
else if (event.playTime && 0 !== event.playTime) {
|
|
201
|
+
texts.push(event);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Choosing the right lyrics
|
|
206
|
+
if (2 < lyrics.length) {
|
|
207
|
+
texts = lyrics;
|
|
208
|
+
}
|
|
209
|
+
else if (!texts.length) {
|
|
210
|
+
texts = [];
|
|
211
|
+
}
|
|
212
|
+
// Convert texts with fallback encoding chain: TIS-620 → Windows-874 → UTF-8
|
|
213
|
+
// Get TextEncoding from global scope if not available locally
|
|
214
|
+
let textEncoder = TextEncoding;
|
|
215
|
+
if (!textEncoder && typeof globalThis.window !== 'undefined') {
|
|
216
|
+
textEncoder = globalThis.window.TextEncoding;
|
|
217
|
+
}
|
|
218
|
+
if (!textEncoder && typeof globalThis !== 'undefined') {
|
|
219
|
+
textEncoder = globalThis.TextEncoding;
|
|
220
|
+
}
|
|
221
|
+
texts.forEach(function (event) {
|
|
222
|
+
if (textEncoder && textEncoder.decodeWithFallback) {
|
|
223
|
+
event.text = textEncoder.decodeWithFallback(event.data, 0, event.length);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Fallback: try to decode as UTF-8 or use simple character mapping
|
|
227
|
+
try {
|
|
228
|
+
let utf8Decoder = UTF8;
|
|
229
|
+
if (!utf8Decoder && typeof globalThis.window !== 'undefined') {
|
|
230
|
+
utf8Decoder = globalThis.window.UTF8;
|
|
231
|
+
}
|
|
232
|
+
if (!utf8Decoder && typeof globalThis !== 'undefined') {
|
|
233
|
+
utf8Decoder = globalThis.UTF8;
|
|
234
|
+
}
|
|
235
|
+
if (utf8Decoder && utf8Decoder.getStringFromBytes) {
|
|
236
|
+
event.text = utf8Decoder.getStringFromBytes(event.data, 0, event.length, false);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// Ultimate fallback: simple character mapping
|
|
240
|
+
let text = '';
|
|
241
|
+
for (let i = 0; i < event.length && i < event.data.length; i++) {
|
|
242
|
+
text += String.fromCharCode(event.data[i]);
|
|
243
|
+
}
|
|
244
|
+
event.text = text;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
// If all fails, use simple character mapping
|
|
249
|
+
let text = '';
|
|
250
|
+
for (let i = 0; i < event.length && i < event.data.length; i++) {
|
|
251
|
+
text += String.fromCharCode(event.data[i]);
|
|
252
|
+
}
|
|
253
|
+
event.text = text;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return texts;
|
|
258
|
+
}
|
|
259
|
+
// Basic events reading
|
|
260
|
+
getTrackEvents(index) {
|
|
261
|
+
let event;
|
|
262
|
+
const events = [];
|
|
263
|
+
let parser;
|
|
264
|
+
if (index > this.tracks.length || 0 > index) {
|
|
265
|
+
throw new Error('Invalid track index (' + index + ')');
|
|
266
|
+
}
|
|
267
|
+
parser = MIDIEvents.createParser(this.tracks[index].getTrackContent(), 0, false);
|
|
268
|
+
event = parser.next();
|
|
269
|
+
do {
|
|
270
|
+
if (event) {
|
|
271
|
+
events.push(event);
|
|
272
|
+
event = parser.next();
|
|
273
|
+
}
|
|
274
|
+
} while (event);
|
|
275
|
+
return events;
|
|
276
|
+
}
|
|
277
|
+
// Retrieve the content in a buffer
|
|
278
|
+
getContent() {
|
|
279
|
+
let bufferLength;
|
|
280
|
+
let destination;
|
|
281
|
+
let origin;
|
|
282
|
+
let i;
|
|
283
|
+
let j;
|
|
284
|
+
let k;
|
|
285
|
+
let l;
|
|
286
|
+
let m;
|
|
287
|
+
let n;
|
|
288
|
+
// Calculating the buffer content
|
|
289
|
+
// - initialize with the header length
|
|
290
|
+
bufferLength = MIDIFileHeader.HEADER_LENGTH;
|
|
291
|
+
// - add tracks length
|
|
292
|
+
for (i = 0, j = this.tracks.length; i < j; i++) {
|
|
293
|
+
bufferLength += this.tracks[i].getTrackLength() + 8;
|
|
294
|
+
}
|
|
295
|
+
// Creating the destination buffer
|
|
296
|
+
destination = new Uint8Array(bufferLength);
|
|
297
|
+
// Adding header
|
|
298
|
+
origin = new Uint8Array(this.header.datas.buffer, this.header.datas.byteOffset, MIDIFileHeader.HEADER_LENGTH);
|
|
299
|
+
for (i = 0, j = MIDIFileHeader.HEADER_LENGTH; i < j; i++) {
|
|
300
|
+
destination[i] = origin[i];
|
|
301
|
+
}
|
|
302
|
+
// Adding tracks
|
|
303
|
+
for (k = 0, l = this.tracks.length; k < l; k++) {
|
|
304
|
+
origin = new Uint8Array(this.tracks[k].datas.buffer, this.tracks[k].datas.byteOffset, this.tracks[k].datas.byteLength);
|
|
305
|
+
for (m = 0, n = this.tracks[k].datas.byteLength; m < n; m++) {
|
|
306
|
+
destination[i++] = origin[m];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return destination.buffer;
|
|
310
|
+
}
|
|
311
|
+
// parseSong method for MIDIPlayer compatibility
|
|
312
|
+
parseSong() {
|
|
313
|
+
const song = {
|
|
314
|
+
duration: 0,
|
|
315
|
+
tracks: [],
|
|
316
|
+
beats: []
|
|
317
|
+
};
|
|
318
|
+
const events = this.getMidiEvents();
|
|
319
|
+
for (let i = 0; i < events.length; i++) {
|
|
320
|
+
if (song.duration < events[i].playTime / 1000) {
|
|
321
|
+
song.duration = events[i].playTime / 1000;
|
|
322
|
+
}
|
|
323
|
+
if (events[i].subtype === MIDIEvents.EVENT_MIDI_NOTE_ON) {
|
|
324
|
+
if (events[i].channel === 9) {
|
|
325
|
+
if (events[i].param1 >= 35 && events[i].param1 <= 81) {
|
|
326
|
+
this.startDrum(events[i], song);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
console.log('wrong drum', events[i]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
if (events[i].param1 >= 0 && events[i].param1 <= 127) {
|
|
334
|
+
this.startNote(events[i], song);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.log('wrong tone', events[i]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
if (events[i].subtype === MIDIEvents.EVENT_MIDI_NOTE_OFF) {
|
|
343
|
+
if (events[i].channel !== 9) {
|
|
344
|
+
this.closeNote(events[i], song);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
if (events[i].subtype === MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE) {
|
|
349
|
+
if (events[i].channel !== 9) {
|
|
350
|
+
const track = this.takeTrack(events[i].channel, song);
|
|
351
|
+
track.program = events[i].param1;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
console.log('skip program for drums');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
if (events[i].subtype === MIDIEvents.EVENT_MIDI_CONTROLLER) {
|
|
359
|
+
if (events[i].param1 === 7) {
|
|
360
|
+
if (events[i].channel !== 9) {
|
|
361
|
+
const track = this.takeTrack(events[i].channel, song);
|
|
362
|
+
track.volume = events[i].param2 / 127 || 0.000001;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
if (events[i].subtype === MIDIEvents.EVENT_MIDI_PITCH_BEND) {
|
|
368
|
+
this.addSlide(events[i], song);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log('unknown', events[i].channel, events[i]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return song;
|
|
379
|
+
}
|
|
380
|
+
startNote(event, song) {
|
|
381
|
+
const track = this.takeTrack(event.channel, song);
|
|
382
|
+
track.notes.push({
|
|
383
|
+
when: event.playTime / 1000,
|
|
384
|
+
pitch: event.param1,
|
|
385
|
+
duration: 0.0000001,
|
|
386
|
+
slides: []
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
closeNote(event, song) {
|
|
390
|
+
const track = this.takeTrack(event.channel, song);
|
|
391
|
+
for (let i = 0; i < track.notes.length; i++) {
|
|
392
|
+
if (track.notes[i].duration === 0.0000001 &&
|
|
393
|
+
track.notes[i].pitch === event.param1 &&
|
|
394
|
+
track.notes[i].when < event.playTime / 1000) {
|
|
395
|
+
track.notes[i].duration = event.playTime / 1000 - track.notes[i].when;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
addSlide(event, song) {
|
|
401
|
+
const track = this.takeTrack(event.channel, song);
|
|
402
|
+
for (let i = 0; i < track.notes.length; i++) {
|
|
403
|
+
if (track.notes[i].duration === 0.0000001 &&
|
|
404
|
+
track.notes[i].when < event.playTime / 1000) {
|
|
405
|
+
track.notes[i].slides.push({
|
|
406
|
+
pitch: track.notes[i].pitch + (event.param2 - 64) / 6,
|
|
407
|
+
when: event.playTime / 1000 - track.notes[i].when
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
startDrum(event, song) {
|
|
413
|
+
const beat = this.takeBeat(event.param1, song);
|
|
414
|
+
beat.notes.push({
|
|
415
|
+
when: event.playTime / 1000
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
takeTrack(n, song) {
|
|
419
|
+
for (let i = 0; i < song.tracks.length; i++) {
|
|
420
|
+
if (song.tracks[i].n === n) {
|
|
421
|
+
return song.tracks[i];
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const track = {
|
|
425
|
+
n: n,
|
|
426
|
+
notes: [],
|
|
427
|
+
volume: 1,
|
|
428
|
+
program: 0
|
|
429
|
+
};
|
|
430
|
+
song.tracks.push(track);
|
|
431
|
+
return track;
|
|
432
|
+
}
|
|
433
|
+
takeBeat(n, song) {
|
|
434
|
+
for (let i = 0; i < song.beats.length; i++) {
|
|
435
|
+
if (song.beats[i].n === n) {
|
|
436
|
+
return song.beats[i];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const beat = {
|
|
440
|
+
n: n,
|
|
441
|
+
notes: [],
|
|
442
|
+
volume: 1
|
|
443
|
+
};
|
|
444
|
+
song.beats.push(beat);
|
|
445
|
+
return beat;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
MIDIFile.Header = MIDIFileHeader;
|
|
449
|
+
MIDIFile.Track = MIDIFileTrack;
|
|
450
|
+
module.exports = MIDIFile;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MIDIFileHeader : Read and edit a MIDI header chunk in a given ArrayBuffer
|
|
4
|
+
*/
|
|
5
|
+
class MIDIFileHeader {
|
|
6
|
+
constructor(buffer, _strictMode) {
|
|
7
|
+
let a;
|
|
8
|
+
// No buffer creating him
|
|
9
|
+
if (!buffer) {
|
|
10
|
+
a = new Uint8Array(MIDIFileHeader.HEADER_LENGTH);
|
|
11
|
+
// Adding the header id (MThd)
|
|
12
|
+
a[0] = 0x4D;
|
|
13
|
+
a[1] = 0x54;
|
|
14
|
+
a[2] = 0x68;
|
|
15
|
+
a[3] = 0x64;
|
|
16
|
+
// Adding the header chunk size
|
|
17
|
+
a[4] = 0x00;
|
|
18
|
+
a[5] = 0x00;
|
|
19
|
+
a[6] = 0x00;
|
|
20
|
+
a[7] = 0x06;
|
|
21
|
+
// Adding the file format (1 here cause it's the most commonly used)
|
|
22
|
+
a[8] = 0x00;
|
|
23
|
+
a[9] = 0x01;
|
|
24
|
+
// Adding the track count (1 cause it's a new file)
|
|
25
|
+
a[10] = 0x00;
|
|
26
|
+
a[11] = 0x01;
|
|
27
|
+
// Adding the time division (192 ticks per beat)
|
|
28
|
+
a[12] = 0x00;
|
|
29
|
+
a[13] = 0xC0;
|
|
30
|
+
// saving the buffer
|
|
31
|
+
this.datas = new DataView(a.buffer, 0, MIDIFileHeader.HEADER_LENGTH);
|
|
32
|
+
// Parsing the given buffer
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (!(buffer instanceof ArrayBuffer)) {
|
|
36
|
+
throw new Error('Invalid buffer received.');
|
|
37
|
+
}
|
|
38
|
+
this.datas = new DataView(buffer, 0, MIDIFileHeader.HEADER_LENGTH);
|
|
39
|
+
// Reading MIDI header chunk
|
|
40
|
+
if (!('M' === String.fromCharCode(this.datas.getUint8(0)) &&
|
|
41
|
+
'T' === String.fromCharCode(this.datas.getUint8(1)) &&
|
|
42
|
+
'h' === String.fromCharCode(this.datas.getUint8(2)) &&
|
|
43
|
+
'd' === String.fromCharCode(this.datas.getUint8(3)))) {
|
|
44
|
+
throw new Error('Invalid MIDIFileHeader : MThd prefix not found');
|
|
45
|
+
}
|
|
46
|
+
// Reading chunk length
|
|
47
|
+
if (6 !== this.datas.getUint32(4)) {
|
|
48
|
+
throw new Error('Invalid MIDIFileHeader : Chunk length must be 6');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// MIDI file format
|
|
53
|
+
getFormat() {
|
|
54
|
+
const format = this.datas.getUint16(8);
|
|
55
|
+
if (0 !== format && 1 !== format && 2 !== format) {
|
|
56
|
+
throw new Error('Invalid MIDI file : MIDI format (' + format + '),' +
|
|
57
|
+
' format can be 0, 1 or 2 only.');
|
|
58
|
+
}
|
|
59
|
+
return format;
|
|
60
|
+
}
|
|
61
|
+
setFormat(format) {
|
|
62
|
+
if (0 !== format && 1 !== format && 2 !== format) {
|
|
63
|
+
throw new Error('Invalid MIDI format given (' + format + '),' +
|
|
64
|
+
' format can be 0, 1 or 2 only.');
|
|
65
|
+
}
|
|
66
|
+
this.datas.setUint16(8, format);
|
|
67
|
+
}
|
|
68
|
+
// Number of tracks
|
|
69
|
+
getTracksCount() {
|
|
70
|
+
return this.datas.getUint16(10);
|
|
71
|
+
}
|
|
72
|
+
setTracksCount(n) {
|
|
73
|
+
this.datas.setUint16(10, n);
|
|
74
|
+
}
|
|
75
|
+
// Tick compute
|
|
76
|
+
getTickResolution(tempo) {
|
|
77
|
+
// Frames per seconds
|
|
78
|
+
if (this.datas.getUint16(12) & 0x8000) {
|
|
79
|
+
return 1000000 / (this.getSMPTEFrames() * this.getTicksPerFrame());
|
|
80
|
+
// Ticks per beat
|
|
81
|
+
}
|
|
82
|
+
// Default MIDI tempo is 120bpm, 500ms per beat
|
|
83
|
+
tempo = tempo || 500000;
|
|
84
|
+
return tempo / this.getTicksPerBeat();
|
|
85
|
+
}
|
|
86
|
+
// Time division type
|
|
87
|
+
getTimeDivision() {
|
|
88
|
+
if (this.datas.getUint16(12) & 0x8000) {
|
|
89
|
+
return MIDIFileHeader.FRAMES_PER_SECONDS;
|
|
90
|
+
}
|
|
91
|
+
return MIDIFileHeader.TICKS_PER_BEAT;
|
|
92
|
+
}
|
|
93
|
+
// Ticks per beat
|
|
94
|
+
getTicksPerBeat() {
|
|
95
|
+
const divisionWord = this.datas.getUint16(12);
|
|
96
|
+
if (divisionWord & 0x8000) {
|
|
97
|
+
throw new Error('Time division is not expressed as ticks per beat.');
|
|
98
|
+
}
|
|
99
|
+
return divisionWord;
|
|
100
|
+
}
|
|
101
|
+
setTicksPerBeat(ticksPerBeat) {
|
|
102
|
+
this.datas.setUint16(12, ticksPerBeat & 0x7FFF);
|
|
103
|
+
}
|
|
104
|
+
// Frames per seconds
|
|
105
|
+
getSMPTEFrames() {
|
|
106
|
+
const divisionWord = this.datas.getUint16(12);
|
|
107
|
+
let smpteFrames;
|
|
108
|
+
if (!(divisionWord & 0x8000)) {
|
|
109
|
+
throw new Error('Time division is not expressed as frames per seconds.');
|
|
110
|
+
}
|
|
111
|
+
smpteFrames = divisionWord & 0x7F00;
|
|
112
|
+
if (-1 === [24, 25, 29, 30].indexOf(smpteFrames)) {
|
|
113
|
+
throw new Error('Invalid SMPTE frames value (' + smpteFrames + ').');
|
|
114
|
+
}
|
|
115
|
+
return 29 === smpteFrames ? 29.97 : smpteFrames;
|
|
116
|
+
}
|
|
117
|
+
getTicksPerFrame() {
|
|
118
|
+
const divisionWord = this.datas.getUint16(12);
|
|
119
|
+
if (!(divisionWord & 0x8000)) {
|
|
120
|
+
throw new Error('Time division is not expressed as frames per seconds.');
|
|
121
|
+
}
|
|
122
|
+
return divisionWord & 0x00FF;
|
|
123
|
+
}
|
|
124
|
+
setSMTPEDivision(smpteFrames, ticksPerFrame) {
|
|
125
|
+
let frames = smpteFrames;
|
|
126
|
+
if (29.97 === frames) {
|
|
127
|
+
frames = 29;
|
|
128
|
+
}
|
|
129
|
+
if (-1 === [24, 25, 29, 30].indexOf(frames)) {
|
|
130
|
+
throw new Error('Invalid SMPTE frames value given (' + frames + ').');
|
|
131
|
+
}
|
|
132
|
+
if (0 > ticksPerFrame || 0xFF < ticksPerFrame) {
|
|
133
|
+
throw new Error('Invalid ticks per frame value given (' + frames + ').');
|
|
134
|
+
}
|
|
135
|
+
this.datas.setUint8(12, 0x80 | frames);
|
|
136
|
+
this.datas.setUint8(13, ticksPerFrame);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
module.exports = MIDIFileHeader;
|
|
140
|
+
// Static constants
|
|
141
|
+
MIDIFileHeader.HEADER_LENGTH = 14;
|
|
142
|
+
MIDIFileHeader.FRAMES_PER_SECONDS = 1;
|
|
143
|
+
MIDIFileHeader.TICKS_PER_BEAT = 2;
|
|
144
|
+
module.exports = MIDIFileHeader;
|