audio-mixer-engine 0.1.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,718 @@
1
+ /**
2
+ * MidiParser - A class for parsing MIDI files
3
+ * Extracts parts, bar structure, and metadata
4
+ */
5
+ class MidiParser {
6
+ constructor() {
7
+ // Common part names to detect
8
+ this.partNames = [
9
+ 'soprano', 'alto', 'tenor', 'bass',
10
+ 'treble', 'mezzo', 'baritone',
11
+ 's', 'a', 't', 'b', 'satb'
12
+ ];
13
+
14
+ // Store the parsed data
15
+ this.parsedData = {
16
+ parts: {}, // Will contain separate arrays for each vocal part
17
+ barStructure: [], // Will contain bar timing information
18
+ metadata: {} // Will contain title, composer, etc.
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Main method to parse a MIDI file
24
+ * @param {ArrayBuffer} midiFileBuffer - The MIDI file as an ArrayBuffer
25
+ * @returns {Object} Parsed data with parts, barStructure and metadata
26
+ */
27
+ async parse(midiFileBuffer) {
28
+ try {
29
+ const midi = await this._parseMidiBuffer(midiFileBuffer);
30
+
31
+ // Extract metadata
32
+ this._extractMetadata(midi);
33
+
34
+ // Extract bar structure (time signatures, tempo changes)
35
+ this._extractBarStructure(midi);
36
+
37
+ // Extract parts
38
+ this._extractParts(midi);
39
+
40
+ return this.parsedData;
41
+ } catch (error) {
42
+ console.error('Error parsing MIDI file:', error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Parse the MIDI buffer into a workable format
49
+ * @private
50
+ */
51
+ async _parseMidiBuffer(midiFileBuffer) {
52
+ // Convert ArrayBuffer to Uint8Array
53
+ const data = new Uint8Array(midiFileBuffer);
54
+
55
+ // Check if it's a valid MIDI file
56
+ if (!(data[0] === 0x4D && data[1] === 0x54 && data[2] === 0x68 && data[3] === 0x64)) {
57
+ throw new Error('Not a valid MIDI file');
58
+ }
59
+
60
+ // Parse header chunk
61
+ const headerLength = this._bytesToNumber(data.slice(4, 8));
62
+ const format = this._bytesToNumber(data.slice(8, 10));
63
+ const tracksCount = this._bytesToNumber(data.slice(10, 12));
64
+ const division = this._bytesToNumber(data.slice(12, 14));
65
+
66
+ // MIDI time division (ticks per quarter note or SMPTE)
67
+ const ticksPerBeat = division & 0x8000 ? null : division;
68
+
69
+ const midi = {
70
+ format,
71
+ ticksPerBeat,
72
+ tracks: [],
73
+ duration: 0
74
+ };
75
+
76
+ // Parse each track
77
+ let currentPosition = 8 + headerLength;
78
+ for (let i = 0; i < tracksCount; i++) {
79
+ if (data[currentPosition] === 0x4D && data[currentPosition + 1] === 0x54 &&
80
+ data[currentPosition + 2] === 0x72 && data[currentPosition + 3] === 0x6B) {
81
+
82
+ const trackLength = this._bytesToNumber(data.slice(currentPosition + 4, currentPosition + 8));
83
+ const trackData = data.slice(currentPosition + 8, currentPosition + 8 + trackLength);
84
+
85
+ const track = this._parseTrack(trackData);
86
+ midi.tracks.push(track);
87
+
88
+ currentPosition += 8 + trackLength;
89
+ } else {
90
+ throw new Error(`Invalid track header at position ${currentPosition}`);
91
+ }
92
+ }
93
+
94
+ return midi;
95
+ }
96
+
97
+ /**
98
+ * Parse a single MIDI track
99
+ * @private
100
+ */
101
+ _parseTrack(data) {
102
+ const track = {
103
+ notes: [],
104
+ name: null,
105
+ lyrics: [],
106
+ events: [],
107
+ duration: 0
108
+ };
109
+
110
+ let currentPosition = 0;
111
+ let currentTick = 0;
112
+ let runningStatus = null;
113
+
114
+ while (currentPosition < data.length) {
115
+ // Parse delta time
116
+ let deltaTime = 0;
117
+ let byte = 0;
118
+ // noinspection JSBitwiseOperatorUsage
119
+ do {
120
+ byte = data[currentPosition++];
121
+ deltaTime = (deltaTime << 7) | (byte & 0x7F);
122
+ } while (byte & 0x80);
123
+
124
+ currentTick += deltaTime;
125
+
126
+ // Get event type
127
+ byte = data[currentPosition++];
128
+ let eventType = byte;
129
+
130
+ // Handle running status
131
+ if ((byte & 0x80) === 0) {
132
+ if (runningStatus === null) {
133
+ throw new Error("Running status byte encountered before status byte");
134
+ }
135
+ eventType = runningStatus;
136
+ currentPosition--; // We need to reread the current byte as a data byte
137
+ } else {
138
+ runningStatus = eventType;
139
+ }
140
+
141
+ // Handle different event types
142
+ if (eventType === 0xFF) { // Meta event
143
+ const metaType = data[currentPosition++];
144
+ const metaLength = this._readVariableLengthValue(data, currentPosition);
145
+ currentPosition += metaLength.bytesRead;
146
+ const metaData = data.slice(currentPosition, currentPosition + metaLength.value);
147
+ currentPosition += metaLength.value;
148
+
149
+ // Handle meta events
150
+ switch (metaType) {
151
+ case 0x03: // Track name
152
+ track.name = this._bytesToString(metaData);
153
+ break;
154
+ case 0x01: // Text event
155
+ track.events.push({
156
+ type: 'text',
157
+ text: this._bytesToString(metaData),
158
+ tick: currentTick
159
+ });
160
+ break;
161
+ case 0x05: // Lyrics
162
+ track.lyrics.push({
163
+ text: this._bytesToString(metaData),
164
+ tick: currentTick
165
+ });
166
+ break;
167
+ case 0x51: // Tempo
168
+ const microsecondsPerBeat = this._bytesToNumber(metaData);
169
+ const tempo = Math.round(60000000 / microsecondsPerBeat);
170
+ track.events.push({
171
+ type: 'tempo',
172
+ bpm: tempo,
173
+ tick: currentTick
174
+ });
175
+ break;
176
+ case 0x58: // Time signature
177
+ track.events.push({
178
+ type: 'timeSignature',
179
+ numerator: metaData[0],
180
+ denominator: Math.pow(2, metaData[1]),
181
+ tick: currentTick
182
+ });
183
+ break;
184
+ case 0x2F: // End of track
185
+ track.duration = currentTick;
186
+ break;
187
+ }
188
+ }
189
+ else if ((eventType & 0xF0) === 0x90) { // Note on
190
+ const channel = eventType & 0x0F;
191
+ const noteNumber = data[currentPosition++];
192
+ const velocity = data[currentPosition++];
193
+
194
+ if (velocity > 0) { // Note on with velocity > 0
195
+ track.notes.push({
196
+ type: 'noteOn',
197
+ noteNumber,
198
+ velocity,
199
+ tick: currentTick,
200
+ channel
201
+ });
202
+ } else { // Note on with velocity = 0 is equivalent to note off
203
+ track.notes.push({
204
+ type: 'noteOff',
205
+ noteNumber,
206
+ tick: currentTick,
207
+ channel
208
+ });
209
+ }
210
+ }
211
+ else if ((eventType & 0xF0) === 0x80) { // Note off
212
+ const channel = eventType & 0x0F;
213
+ const noteNumber = data[currentPosition++];
214
+ // noinspection JSUnusedLocalSymbols
215
+ const velocity = data[currentPosition++]; // Release velocity, intentionally ignored (maybe for now)
216
+
217
+ track.notes.push({
218
+ type: 'noteOff',
219
+ noteNumber,
220
+ tick: currentTick,
221
+ channel
222
+ });
223
+ }
224
+ else if (eventType === 0xF0 || eventType === 0xF7) { // SysEx events
225
+ const length = this._readVariableLengthValue(data, currentPosition);
226
+ currentPosition += length.bytesRead + length.value;
227
+ }
228
+ else if ((eventType & 0xF0) === 0xB0) { // Controller change
229
+ const channel = eventType & 0x0F;
230
+ const controllerNumber = data[currentPosition++];
231
+ const value = data[currentPosition++];
232
+
233
+ track.events.push({
234
+ type: 'controller',
235
+ controllerNumber,
236
+ value,
237
+ channel,
238
+ tick: currentTick
239
+ });
240
+ }
241
+ else if ((eventType & 0xF0) === 0xC0) { // Program change
242
+ const channel = eventType & 0x0F;
243
+ const programNumber = data[currentPosition++];
244
+
245
+ track.events.push({
246
+ type: 'programChange',
247
+ programNumber,
248
+ channel,
249
+ tick: currentTick
250
+ });
251
+ }
252
+ else if ((eventType & 0xF0) === 0xD0) { // Channel aftertouch
253
+ const channel = eventType & 0x0F;
254
+ const pressure = data[currentPosition++];
255
+
256
+ track.events.push({
257
+ type: 'channelAftertouch',
258
+ pressure,
259
+ channel,
260
+ tick: currentTick
261
+ });
262
+ }
263
+ else if ((eventType & 0xF0) === 0xE0) { // Pitch bend
264
+ const channel = eventType & 0x0F;
265
+ const lsb = data[currentPosition++];
266
+ const msb = data[currentPosition++];
267
+ const value = ((msb << 7) | lsb) - 8192; // Center value is 8192 (0x2000)
268
+
269
+ track.events.push({
270
+ type: 'pitchBend',
271
+ value,
272
+ channel,
273
+ tick: currentTick
274
+ });
275
+ }
276
+ else if ((eventType & 0xF0) === 0xA0) { // Note aftertouch (poly pressure)
277
+ const channel = eventType & 0x0F;
278
+ const noteNumber = data[currentPosition++];
279
+ const pressure = data[currentPosition++];
280
+
281
+ track.events.push({
282
+ type: 'noteAftertouch',
283
+ noteNumber,
284
+ pressure,
285
+ channel,
286
+ tick: currentTick
287
+ });
288
+ }
289
+ else {
290
+ // Skip unknown event types
291
+ console.warn(`Unknown event type: ${eventType.toString(16)} at position ${currentPosition - 1}`);
292
+ // Try to recover by skipping to the next event
293
+ currentPosition++;
294
+ }
295
+ }
296
+
297
+ return track;
298
+ }
299
+
300
+ /**
301
+ * Extract metadata from the MIDI object
302
+ * @private
303
+ */
304
+ _extractMetadata(midi) {
305
+ const metadata = {
306
+ title: null,
307
+ composer: null,
308
+ partNames: [],
309
+ format: midi.format,
310
+ ticksPerBeat: midi.ticksPerBeat
311
+ };
312
+
313
+ // Look for title, composer and part names in track names and text events
314
+ midi.tracks.forEach((track, index) => {
315
+ // First track with name is often the title
316
+ if (track.name && !metadata.title) {
317
+ metadata.title = track.name;
318
+ }
319
+
320
+ // Check for common metadata in text events
321
+ track.events.filter(e => e.type === 'text').forEach(event => {
322
+ const text = event.text.toLowerCase();
323
+ if ((text.includes('compos') || text.includes('by')) && !metadata.composer) {
324
+ metadata.composer = event.text;
325
+ }
326
+ });
327
+
328
+ // Store track name as potential part name
329
+ if (track.name) {
330
+ // Check if this track name matches a common choir part name
331
+ const trackNameLower = track.name.toLowerCase();
332
+ for (const partName of this.partNames) {
333
+ if (trackNameLower.includes(partName)) {
334
+ metadata.partNames.push({
335
+ index,
336
+ name: track.name
337
+ });
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ });
343
+
344
+ this.parsedData.metadata = metadata;
345
+ }
346
+
347
+ /**
348
+ * Extract bar structure (time signatures, tempo changes)
349
+ * New format: {"sig": [numerator, denominator], "bpm": tempo_or_array}
350
+ * @private
351
+ */
352
+ _extractBarStructure(midi) {
353
+ // Get ticks per beat for calculations
354
+ const ticksPerBeat = midi.ticksPerBeat || 480;
355
+
356
+ // Collect all time signature and tempo changes
357
+ const allEvents = [];
358
+ midi.tracks.forEach(track => {
359
+ track.events.forEach(event => {
360
+ if (event.type === 'timeSignature' || event.type === 'tempo') {
361
+ allEvents.push(event);
362
+ }
363
+ });
364
+ });
365
+
366
+ // Sort events by tick
367
+ allEvents.sort((a, b) => a.tick - b.tick);
368
+
369
+ // Find total duration of the MIDI file (latest note end tick)
370
+ let totalDurationTicks = 0;
371
+ midi.tracks.forEach(track => {
372
+ if (track.notes) {
373
+ track.notes.forEach(note => {
374
+ if (note.type === 'noteOff' && note.tick > totalDurationTicks) {
375
+ totalDurationTicks = note.tick;
376
+ }
377
+ });
378
+ }
379
+ });
380
+
381
+ // If no notes found, use a reasonable default
382
+ if (totalDurationTicks === 0) {
383
+ totalDurationTicks = ticksPerBeat * 8; // Default to 8 beats
384
+ }
385
+
386
+ // Build bar structure based on time signature changes
387
+ const barStructure = [];
388
+
389
+ // Get all time signature changes with their positions
390
+ const timeSignatureChanges = allEvents
391
+ .filter(event => event.type === 'timeSignature')
392
+ .sort((a, b) => a.tick - b.tick);
393
+
394
+ // Start with the default (will be overridden if there is a time signature at tick 0)
395
+ let currentTimeSignature = { numerator: 4, denominator: 4 };
396
+
397
+ let currentTempo = 120; // Default tempo
398
+ let currentTick = 0;
399
+ let timeSignatureIndex = 0;
400
+
401
+ while (currentTick < totalDurationTicks) {
402
+ // Update time signature for the current position
403
+ while (timeSignatureIndex < timeSignatureChanges.length &&
404
+ timeSignatureChanges[timeSignatureIndex].tick <= currentTick) {
405
+ currentTimeSignature = timeSignatureChanges[timeSignatureIndex];
406
+ timeSignatureIndex++;
407
+ }
408
+
409
+ // For anacrusis/pickup bars, the bar should extend to the next time signature change
410
+ // For normal bars, use standard bar length
411
+ let barEndTick;
412
+
413
+ // Regular bar - use standard time signature-based length
414
+ barEndTick = currentTick + (ticksPerBeat * 4 * currentTimeSignature.numerator / currentTimeSignature.denominator);
415
+
416
+ // Find tempo changes within this bar
417
+ const barTempoChanges = allEvents.filter(event =>
418
+ event.type === 'tempo' &&
419
+ event.tick >= currentTick &&
420
+ event.tick < barEndTick
421
+ );
422
+
423
+ // Build tempo array for each beat in the bar
424
+ const beatsInBar = currentTimeSignature.numerator;
425
+ const bpmArray = [];
426
+
427
+ for (let beat = 0; beat < beatsInBar; beat++) {
428
+ const beatStartTick = currentTick + (beat * ticksPerBeat);
429
+
430
+ // Find the effective tempo for this beat
431
+ let effectiveTempo = currentTempo; // Start with current tempo
432
+
433
+ // Check for tempo changes that affect this beat
434
+ for (let i = barTempoChanges.length - 1; i >= 0; i--) {
435
+ const tempoChange = barTempoChanges[i];
436
+ if (tempoChange.tick <= beatStartTick) {
437
+ effectiveTempo = tempoChange.bpm;
438
+ break;
439
+ }
440
+ }
441
+
442
+ // Also check for global tempo changes before this beat
443
+ for (let i = allEvents.length - 1; i >= 0; i--) {
444
+ const event = allEvents[i];
445
+ if (event.type === 'tempo' && event.tick <= beatStartTick) {
446
+ effectiveTempo = event.bpm;
447
+ // Update current tempo for next iterations
448
+ if (event.tick <= currentTick) {
449
+ currentTempo = event.bpm;
450
+ }
451
+ break;
452
+ }
453
+ }
454
+
455
+ // The bpm from the midi is crotchets (quarter notes) per minute, but we want
456
+ // time signature notes per minute, (i.e. if an x/2 signature then minims (half notes) per minute)
457
+ bpmArray.push(effectiveTempo * currentTimeSignature.denominator / 4);
458
+ }
459
+
460
+ // If all beats have the same tempo, use single number; otherwise use array
461
+ const allSameTempo = bpmArray.every(tempo => tempo === bpmArray[0]);
462
+ const bpm = allSameTempo ? bpmArray[0] : bpmArray;
463
+
464
+ barStructure.push({
465
+ sig: [currentTimeSignature.numerator, currentTimeSignature.denominator],
466
+ bpm: bpm
467
+ });
468
+
469
+ // Move to next bar
470
+ currentTick = barEndTick;
471
+ }
472
+
473
+ this.parsedData.barStructure = barStructure;
474
+ }
475
+
476
+ /**
477
+ * Extract notes for each voice part
478
+ * @private
479
+ */
480
+ _extractParts(midi) {
481
+ const parts = {};
482
+ const ticksPerBeat = midi.ticksPerBeat;
483
+
484
+
485
+ // Try to identify vocal parts by track name
486
+ midi.tracks.forEach((track, trackIndex) => {
487
+ if (!track.notes.length) return; // Skip tracks with no notes (e.g., conductor track)
488
+
489
+ let partName = null;
490
+
491
+ // Try to identify part name from track name
492
+ if (track.name) {
493
+ const lowerName = track.name.toLowerCase();
494
+ for (const name of this.partNames) {
495
+ // For single letters, require exact match; for words, allow includes
496
+ if (name.length === 1) {
497
+ if (lowerName === name) {
498
+ partName = name;
499
+ break;
500
+ }
501
+ } else {
502
+ if (lowerName.includes(name)) {
503
+ partName = name;
504
+ break;
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ // If no recognized part name, use track name or track number
511
+ if (!partName) {
512
+ partName = track.name || `Track ${trackIndex + 1}`;
513
+ }
514
+
515
+ // Normalize part name
516
+ if (partName === 's') partName = 'soprano';
517
+ if (partName === 'a') partName = 'alto';
518
+ if (partName === 't') partName = 'tenor';
519
+ if (partName === 'b') partName = 'bass';
520
+
521
+ // Ensure unique part names by appending counter if name already exists
522
+ let uniquePartName = partName;
523
+ let counter = 2; // Start at 2 for first duplicate
524
+ while (parts[uniquePartName]) {
525
+ uniquePartName = `${partName} ${counter}`;
526
+ counter++;
527
+ }
528
+ partName = uniquePartName;
529
+
530
+ // Process notes
531
+ const notes = [];
532
+ const noteOns = {};
533
+
534
+ track.notes.forEach(noteEvent => {
535
+ if (noteEvent.type === 'noteOn') {
536
+ // Store note start information
537
+ noteOns[noteEvent.noteNumber] = {
538
+ tick: noteEvent.tick,
539
+ velocity: noteEvent.velocity
540
+ };
541
+ } else if (noteEvent.type === 'noteOff') {
542
+ // If we have a matching note on, create a complete note
543
+ if (noteOns[noteEvent.noteNumber]) {
544
+ const start = noteOns[noteEvent.noteNumber];
545
+ const duration = noteEvent.tick - start.tick;
546
+
547
+ notes.push({
548
+ pitch: noteEvent.noteNumber,
549
+ name: this._midiNoteToName(noteEvent.noteNumber),
550
+ startTick: start.tick,
551
+ endTick: noteEvent.tick,
552
+ duration,
553
+ // Convert ticks to actual time considering tempo changes
554
+ startTime: this._ticksToTime(start.tick, midi),
555
+ endTime: this._ticksToTime(noteEvent.tick, midi),
556
+ velocity: start.velocity
557
+ });
558
+
559
+ // Remove from active notes
560
+ delete noteOns[noteEvent.noteNumber];
561
+ }
562
+ }
563
+ });
564
+
565
+ // Add any lyrics associated with this track
566
+ const lyrics = track.lyrics.map(lyric => ({
567
+ text: lyric.text,
568
+ tick: lyric.tick,
569
+ time: lyric.tick / ticksPerBeat // Time in quarter notes
570
+ }));
571
+
572
+ // Sort notes by start time
573
+ notes.sort((a, b) => a.startTick - b.startTick);
574
+
575
+ // Extract program changes for this track
576
+ const programChanges = track.events
577
+ .filter(event => event.type === 'programChange')
578
+ .map(event => ({
579
+ programNumber: event.programNumber,
580
+ tick: event.tick,
581
+ time: this._ticksToTime(event.tick, midi)
582
+ }))
583
+ .sort((a, b) => a.tick - b.tick);
584
+
585
+ // Determine default instrument (first program change or 0 for piano)
586
+ const defaultInstrument = programChanges.length > 0 ? programChanges[0].programNumber : 0;
587
+
588
+ parts[partName] = {
589
+ notes,
590
+ lyrics,
591
+ trackIndex,
592
+ programChanges,
593
+ defaultInstrument
594
+ };
595
+ });
596
+
597
+ this.parsedData.parts = parts;
598
+ }
599
+
600
+ /**
601
+ * Convert a MIDI note number to note name
602
+ * @private
603
+ */
604
+ _midiNoteToName(noteNumber) {
605
+ const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
606
+ const octave = Math.floor(noteNumber / 12) - 1;
607
+ const note = notes[noteNumber % 12];
608
+ return `${note}${octave}`;
609
+ }
610
+
611
+ /**
612
+ * Convert bytes to number
613
+ * @private
614
+ */
615
+ _bytesToNumber(bytes) {
616
+ let value = 0;
617
+ for (let i = 0; i < bytes.length; i++) {
618
+ value = (value << 8) | bytes[i];
619
+ }
620
+ return value;
621
+ }
622
+
623
+ /**
624
+ * Convert bytes to string
625
+ * @private
626
+ */
627
+ _bytesToString(bytes) {
628
+ return new TextDecoder().decode(bytes);
629
+ }
630
+
631
+ /**
632
+ * Read variable-length value from MIDI data
633
+ * @private
634
+ */
635
+ _readVariableLengthValue(data, position) {
636
+ let value = 0;
637
+ let byte;
638
+ let bytesRead = 0;
639
+
640
+ // noinspection JSBitwiseOperatorUsage
641
+ do {
642
+ byte = data[position + bytesRead++];
643
+ value = (value << 7) | (byte & 0x7F);
644
+ } while (byte & 0x80);
645
+
646
+ return { value, bytesRead };
647
+ }
648
+
649
+ /**
650
+ * Convert ticks to time in seconds considering tempo changes within bars
651
+ * @private
652
+ */
653
+ _ticksToTime(targetTick, midi) {
654
+ const ticksPerBeat = midi.ticksPerBeat || 480;
655
+
656
+ // Get all tempo events sorted by tick
657
+ const tempoEvents = [];
658
+ midi.tracks.forEach(track => {
659
+ track.events.forEach(event => {
660
+ if (event.type === 'tempo') {
661
+ tempoEvents.push(event);
662
+ }
663
+ });
664
+ });
665
+ tempoEvents.sort((a, b) => a.tick - b.tick);
666
+
667
+ let totalTime = 0;
668
+ let currentTick = 0;
669
+ let currentTempo = 120; // Default tempo
670
+
671
+ // Process tempo changes up to the target tick
672
+ for (const tempoEvent of tempoEvents) {
673
+ if (tempoEvent.tick > targetTick) break;
674
+
675
+ // Add time for the segment from currentTick to this tempo change
676
+ if (tempoEvent.tick > currentTick) {
677
+ const segmentTicks = tempoEvent.tick - currentTick;
678
+ const segmentTime = (segmentTicks / ticksPerBeat) * (60 / currentTempo);
679
+ totalTime += segmentTime;
680
+ currentTick = tempoEvent.tick;
681
+ }
682
+
683
+ // Update current tempo
684
+ currentTempo = tempoEvent.bpm;
685
+ }
686
+
687
+ // Add time for remaining ticks at current tempo
688
+ if (targetTick > currentTick) {
689
+ const remainingTicks = targetTick - currentTick;
690
+ const remainingTime = (remainingTicks / ticksPerBeat) * (60 / currentTempo);
691
+ totalTime += remainingTime;
692
+ }
693
+
694
+ return totalTime;
695
+ }
696
+ }
697
+
698
+ // Export the class
699
+ export default MidiParser;
700
+
701
+ // Example usage:
702
+ // const parser = new MidiParser();
703
+ // const fileInput = document.getElementById('midiFileInput');
704
+ //
705
+ // fileInput.addEventListener('change', async (event) => {
706
+ // const file = event.target.files[0];
707
+ // const arrayBuffer = await file.arrayBuffer();
708
+ //
709
+ // try {
710
+ // const parsedData = await parser.parse(arrayBuffer);
711
+ // console.log('Parsed MIDI data:', parsedData);
712
+ // console.log('Parts:', parsedData.parts);
713
+ // console.log('Bar structure:', parsedData.barStructure);
714
+ // console.log('Metadata:', parsedData.metadata);
715
+ // } catch (error) {
716
+ // console.error('Error parsing MIDI file:', error);
717
+ // }
718
+ // });