@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.
@@ -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;