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,1669 @@
1
+ class k {
2
+ /**
3
+ * Create a new AudioEngine instance
4
+ * @param {AudioContext} audioContext - Web Audio API context
5
+ * @param {Object} options - Engine-specific options
6
+ */
7
+ constructor(t, e = {}) {
8
+ if (new.target === k)
9
+ throw new Error("AudioEngine is abstract and cannot be instantiated directly");
10
+ this.audioContext = t, this.options = e, this.isInitialized = !1, this.channels = /* @__PURE__ */ new WeakMap(), this.activeChannels = /* @__PURE__ */ new Set();
11
+ }
12
+ /**
13
+ * Initialize the audio engine - load soundfont and set up synthesis
14
+ * @param {string|ArrayBuffer} soundfontData - Path to soundfont or binary data
15
+ * @returns {Promise<void>}
16
+ */
17
+ async initialize(t) {
18
+ throw new Error("initialize() must be implemented by subclass");
19
+ }
20
+ /**
21
+ * Create a new channel for a musical part
22
+ * @param {string} partId - Unique identifier for this part (e.g., 'soprano', 'piano')
23
+ * @param {Object} options - Channel configuration
24
+ * @param {string|number} [options.instrument] - Initial instrument
25
+ * @param {number} [options.initialVolume=1.0] - Initial volume (0.0-1.0)
26
+ * @returns {ChannelHandle} Handle object for controlling this channel
27
+ */
28
+ createChannel(t, e = {}) {
29
+ throw new Error("createChannel() must be implemented by subclass");
30
+ }
31
+ /**
32
+ * Stop all notes on all channels
33
+ */
34
+ allSoundsOff() {
35
+ throw new Error("allSoundsOff() must be implemented by subclass");
36
+ }
37
+ /**
38
+ * Get list of all active channel handles
39
+ * @returns {Array<ChannelHandle>} Array of active channel handles
40
+ */
41
+ getActiveChannels() {
42
+ return Array.from(this.activeChannels);
43
+ }
44
+ /**
45
+ * Clean up all resources and disconnect audio nodes
46
+ */
47
+ destroy() {
48
+ this.allSoundsOff(), this.activeChannels.clear(), this.isInitialized = !1;
49
+ }
50
+ /**
51
+ * Validate that the engine is initialized
52
+ * @protected
53
+ */
54
+ _validateInitialized() {
55
+ if (!this.isInitialized)
56
+ throw new Error("AudioEngine not initialized. Call initialize() first.");
57
+ }
58
+ /**
59
+ * Register a new channel handle (called by subclasses)
60
+ * @param {ChannelHandle} handle - Channel handle to register
61
+ * @protected
62
+ */
63
+ _registerChannel(t) {
64
+ this.activeChannels.add(t);
65
+ }
66
+ /**
67
+ * Unregister a channel handle (called when channel is destroyed)
68
+ * @param {ChannelHandle} handle - Channel handle to unregister
69
+ * @protected
70
+ */
71
+ _unregisterChannel(t) {
72
+ this.activeChannels.delete(t), this.channels.delete(t);
73
+ }
74
+ }
75
+ class v {
76
+ constructor(t, e, s = {}) {
77
+ if (new.target === v)
78
+ throw new Error("ChannelHandle is abstract and cannot be instantiated directly");
79
+ this.engine = t, this.partId = e, this.options = { initialVolume: 1, ...s }, this.isDestroyed = !1, this.noteRefCounts = /* @__PURE__ */ new Map(), this.scheduledEvents = /* @__PURE__ */ new Map(), this.activeNotes = /* @__PURE__ */ new Set();
80
+ }
81
+ /**
82
+ * Get the output audio node for this channel
83
+ * This node can be connected to gain controls, analyzers, etc.
84
+ * @returns {AudioNode} Output node (typically a GainNode)
85
+ */
86
+ getOutputNode() {
87
+ throw new Error("getOutputNode() must be implemented by subclass");
88
+ }
89
+ /**
90
+ * Start a note with reference counting (handles overlaps automatically)
91
+ * @param {number} pitch - MIDI pitch (0-127)
92
+ * @param {number} velocity - Note velocity (0-127)
93
+ */
94
+ noteOn(t, e) {
95
+ this._validateActive();
96
+ const s = this.noteRefCounts.get(t) || 0;
97
+ this.noteRefCounts.set(t, s + 1), this._actualNoteOn(t, e), s === 0 && this.activeNotes.add(t);
98
+ }
99
+ /**
100
+ * Stop a note with reference counting (only stops when count reaches 0)
101
+ * @param {number} pitch - MIDI pitch (0-127)
102
+ */
103
+ noteOff(t) {
104
+ this._validateActive();
105
+ const e = this.noteRefCounts.get(t) || 0;
106
+ if (e <= 0)
107
+ return;
108
+ const s = e - 1;
109
+ this.noteRefCounts.set(t, s), s === 0 && (this._actualNoteOff(t), this.activeNotes.delete(t), this.noteRefCounts.delete(t));
110
+ }
111
+ /**
112
+ * Schedule a note with automatic timing adjustment
113
+ * @param {number} startTime - Absolute audio context time when note should start
114
+ * @param {number} pitch - MIDI pitch (0-127)
115
+ * @param {number} velocity - Note velocity (0-127)
116
+ * @param {number} duration - Note duration in seconds
117
+ * @returns {string} Event ID for cancellation
118
+ */
119
+ playNote(t, e, s, i) {
120
+ this._validateActive();
121
+ const r = this.engine.audioContext.currentTime, n = `${this.partId}_${t}_${e}_${Date.now()}`;
122
+ let l = t, o = i;
123
+ if (t < r) {
124
+ const f = r - t;
125
+ l = r, o = Math.max(0, i - f);
126
+ }
127
+ if (o <= 0)
128
+ return n;
129
+ const a = Math.max(0, (l - r) * 1e3), c = setTimeout(() => {
130
+ this.noteOn(e, s), this.scheduledEvents.delete(`${n}_on`);
131
+ }, a), h = a + o * 1e3, m = setTimeout(() => {
132
+ this.noteOff(e), this.scheduledEvents.delete(`${n}_off`);
133
+ }, h);
134
+ return this.scheduledEvents.set(`${n}_on`, c), this.scheduledEvents.set(`${n}_off`, m), n;
135
+ }
136
+ /**
137
+ * Stop all notes on this channel
138
+ */
139
+ allNotesOff() {
140
+ this._validateActive(), this.scheduledEvents.forEach((t) => {
141
+ clearTimeout(t);
142
+ }), this.scheduledEvents.clear(), this.activeNotes.forEach((t) => {
143
+ this._actualNoteOff(t);
144
+ }), this.noteRefCounts.clear(), this.activeNotes.clear();
145
+ }
146
+ /**
147
+ * Actually start a note (implemented by subclass)
148
+ * @param {number} pitch - MIDI pitch (0-127)
149
+ * @param {number} velocity - Note velocity (0-127)
150
+ * @protected
151
+ */
152
+ _actualNoteOn(t, e) {
153
+ throw new Error("_actualNoteOn() must be implemented by subclass");
154
+ }
155
+ /**
156
+ * Actually stop a note (implemented by subclass)
157
+ * @param {number} pitch - MIDI pitch (0-127)
158
+ * @protected
159
+ */
160
+ _actualNoteOff(t) {
161
+ throw new Error("_actualNoteOff() must be implemented by subclass");
162
+ }
163
+ /**
164
+ * Change the instrument for this channel
165
+ * @param {string|number} instrument - Instrument name or program number
166
+ * @returns {Promise<void>}
167
+ */
168
+ async setInstrument(t) {
169
+ throw new Error("setInstrument() must be implemented by subclass");
170
+ }
171
+ /**
172
+ * Get current instrument for this channel
173
+ * @returns {string|number} Current instrument
174
+ */
175
+ getInstrument() {
176
+ throw new Error("getInstrument() must be implemented by subclass");
177
+ }
178
+ /**
179
+ * Set volume for this channel (affects the internal channel volume)
180
+ * Note: External volume control should use the output node from getOutputNode()
181
+ * @param {number} volume - Volume level (0.0-1.0)
182
+ */
183
+ setVolume(t) {
184
+ throw new Error("setVolume() must be implemented by subclass");
185
+ }
186
+ /**
187
+ * Get current volume for this channel
188
+ * @returns {number} Current volume (0.0-1.0)
189
+ */
190
+ getVolume() {
191
+ throw new Error("getVolume() must be implemented by subclass");
192
+ }
193
+ /**
194
+ * Get the part ID for this channel
195
+ * @returns {string} Part identifier
196
+ */
197
+ getPartId() {
198
+ return this.partId;
199
+ }
200
+ /**
201
+ * Check if this channel is still active
202
+ * @returns {boolean} True if channel is active
203
+ */
204
+ isActive() {
205
+ return !this.isDestroyed && this.engine.isInitialized && this.engine.activeChannels.has(this);
206
+ }
207
+ /**
208
+ * Destroy this channel and clean up resources
209
+ */
210
+ destroy() {
211
+ if (!this.isDestroyed) {
212
+ this.allNotesOff();
213
+ const t = this.getOutputNode();
214
+ t && t.disconnect(), this.noteRefCounts.clear(), this.scheduledEvents.clear(), this.activeNotes.clear(), this.engine._unregisterChannel(this), this.isDestroyed = !0;
215
+ }
216
+ }
217
+ /**
218
+ * Validate that this channel is still active
219
+ * @protected
220
+ */
221
+ _validateActive() {
222
+ if (this.isDestroyed)
223
+ throw new Error("Channel has been destroyed");
224
+ if (!this.engine.isInitialized)
225
+ throw new Error("AudioEngine is not initialized");
226
+ }
227
+ }
228
+ class P {
229
+ /**
230
+ * Map common instrument names to MIDI program numbers
231
+ * @param {string|number} instrument - Instrument name or program number
232
+ * @returns {number} MIDI program number
233
+ */
234
+ static getInstrumentProgram(t) {
235
+ if (typeof t == "number") return t;
236
+ const s = {
237
+ // Piano family (0-7)
238
+ piano: 0,
239
+ bright_piano: 1,
240
+ electric_grand: 2,
241
+ honky_tonk: 3,
242
+ electric_piano_1: 4,
243
+ electric_piano_2: 5,
244
+ harpsichord: 6,
245
+ clavinet: 7,
246
+ // Chromatic percussion (8-15)
247
+ celesta: 8,
248
+ glockenspiel: 9,
249
+ music_box: 10,
250
+ vibraphone: 11,
251
+ marimba: 12,
252
+ xylophone: 13,
253
+ tubular_bells: 14,
254
+ dulcimer: 15,
255
+ // Organ (16-23)
256
+ drawbar_organ: 16,
257
+ percussive_organ: 17,
258
+ rock_organ: 18,
259
+ church_organ: 19,
260
+ reed_organ: 20,
261
+ accordion: 21,
262
+ harmonica: 22,
263
+ tango_accordion: 23,
264
+ organ: 19,
265
+ // Guitar (24-31)
266
+ nylon_guitar: 24,
267
+ steel_guitar: 25,
268
+ electric_guitar_jazz: 26,
269
+ electric_guitar_clean: 27,
270
+ electric_guitar_muted: 28,
271
+ overdriven_guitar: 29,
272
+ distortion_guitar: 30,
273
+ guitar_harmonics: 31,
274
+ guitar: 24,
275
+ // Bass (32-39)
276
+ acoustic_bass: 32,
277
+ electric_bass_finger: 33,
278
+ electric_bass_pick: 34,
279
+ fretless_bass: 35,
280
+ slap_bass_1: 36,
281
+ slap_bass_2: 37,
282
+ synth_bass_1: 38,
283
+ synth_bass_2: 39,
284
+ bass: 32,
285
+ // Strings (40-47)
286
+ violin: 40,
287
+ viola: 41,
288
+ cello: 42,
289
+ contrabass: 43,
290
+ tremolo_strings: 44,
291
+ pizzicato_strings: 45,
292
+ orchestral_harp: 46,
293
+ timpani: 47,
294
+ strings: 48,
295
+ strings_ensemble: 48,
296
+ // Ensemble (48-55)
297
+ slow_strings: 49,
298
+ synth_strings_1: 50,
299
+ synth_strings_2: 51,
300
+ choir_aahs: 52,
301
+ voice_oohs: 53,
302
+ synth_voice: 54,
303
+ orchestra_hit: 55,
304
+ // Brass (56-63)
305
+ trumpet: 56,
306
+ trombone: 57,
307
+ tuba: 58,
308
+ muted_trumpet: 59,
309
+ french_horn: 60,
310
+ brass_section: 61,
311
+ synth_brass_1: 62,
312
+ synth_brass_2: 63,
313
+ // Reed (64-71)
314
+ soprano_sax: 64,
315
+ alto_sax: 65,
316
+ tenor_sax: 66,
317
+ baritone_sax: 67,
318
+ oboe: 68,
319
+ english_horn: 69,
320
+ bassoon: 70,
321
+ clarinet: 71,
322
+ saxophone: 64,
323
+ // Pipe (72-79)
324
+ piccolo: 72,
325
+ flute: 73,
326
+ recorder: 74,
327
+ pan_flute: 75,
328
+ blown_bottle: 76,
329
+ shakuhachi: 77,
330
+ whistle: 78,
331
+ ocarina: 79,
332
+ // Synth lead (80-87)
333
+ lead_1_square: 80,
334
+ lead_2_sawtooth: 81,
335
+ lead_3_calliope: 82,
336
+ lead_4_chiff: 83,
337
+ lead_5_charang: 84,
338
+ lead_6_voice: 85,
339
+ lead_7_fifths: 86,
340
+ lead_8_bass: 87,
341
+ // Synth pad (88-95)
342
+ pad_1_new_age: 88,
343
+ pad_2_warm: 89,
344
+ pad_3_polysynth: 90,
345
+ pad_4_choir: 91,
346
+ pad_5_bowed: 92,
347
+ pad_6_metallic: 93,
348
+ pad_7_halo: 94,
349
+ pad_8_sweep: 95,
350
+ // Synth effects (96-103)
351
+ fx_1_rain: 96,
352
+ fx_2_soundtrack: 97,
353
+ fx_3_crystal: 98,
354
+ fx_4_atmosphere: 99,
355
+ fx_5_brightness: 100,
356
+ fx_6_goblins: 101,
357
+ fx_7_echoes: 102,
358
+ fx_8_sci_fi: 103,
359
+ // Ethnic (104-111)
360
+ sitar: 104,
361
+ banjo: 105,
362
+ shamisen: 106,
363
+ koto: 107,
364
+ kalimba: 108,
365
+ bag_pipe: 109,
366
+ fiddle: 110,
367
+ shanai: 111,
368
+ // Percussive (112-119)
369
+ tinkle_bell: 112,
370
+ agogo: 113,
371
+ steel_drums: 114,
372
+ woodblock: 115,
373
+ taiko_drum: 116,
374
+ melodic_tom: 117,
375
+ synth_drum: 118,
376
+ reverse_cymbal: 119,
377
+ // Sound effects (120-127)
378
+ guitar_fret_noise: 120,
379
+ breath_noise: 121,
380
+ seashore: 122,
381
+ bird_tweet: 123,
382
+ telephone_ring: 124,
383
+ helicopter: 125,
384
+ applause: 126,
385
+ gunshot: 127
386
+ }[t.toLowerCase()];
387
+ return s !== void 0 ? s : 0;
388
+ }
389
+ /**
390
+ * Get instrument name from MIDI program number (for display purposes)
391
+ * @param {number} programNumber - MIDI program number (0-127)
392
+ * @returns {string} Instrument name or fallback
393
+ */
394
+ static getProgramName(t) {
395
+ return [
396
+ // Piano family (0-7)
397
+ "Piano",
398
+ "Bright Piano",
399
+ "Electric Grand",
400
+ "Honky-tonk Piano",
401
+ "Electric Piano 1",
402
+ "Electric Piano 2",
403
+ "Harpsichord",
404
+ "Clavinet",
405
+ // Chromatic percussion (8-15)
406
+ "Celesta",
407
+ "Glockenspiel",
408
+ "Music Box",
409
+ "Vibraphone",
410
+ "Marimba",
411
+ "Xylophone",
412
+ "Tubular Bells",
413
+ "Dulcimer",
414
+ // Organ (16-23)
415
+ "Drawbar Organ",
416
+ "Percussive Organ",
417
+ "Rock Organ",
418
+ "Church Organ",
419
+ "Reed Organ",
420
+ "Accordion",
421
+ "Harmonica",
422
+ "Tango Accordion",
423
+ // Guitar (24-31)
424
+ "Nylon Guitar",
425
+ "Steel Guitar",
426
+ "Electric Guitar (jazz)",
427
+ "Electric Guitar (clean)",
428
+ "Electric Guitar (muted)",
429
+ "Overdriven Guitar",
430
+ "Distortion Guitar",
431
+ "Guitar Harmonics",
432
+ // Bass (32-39)
433
+ "Acoustic Bass",
434
+ "Electric Bass (finger)",
435
+ "Electric Bass (pick)",
436
+ "Fretless Bass",
437
+ "Slap Bass 1",
438
+ "Slap Bass 2",
439
+ "Synth Bass 1",
440
+ "Synth Bass 2",
441
+ // Strings (40-47)
442
+ "Violin",
443
+ "Viola",
444
+ "Cello",
445
+ "Contrabass",
446
+ "Tremolo Strings",
447
+ "Pizzicato Strings",
448
+ "Orchestral Harp",
449
+ "Timpani",
450
+ // Ensemble (48-55)
451
+ "String Ensemble 1",
452
+ "String Ensemble 2",
453
+ "Synth Strings 1",
454
+ "Synth Strings 2",
455
+ "Choir Aahs",
456
+ "Voice Oohs",
457
+ "Synth Voice",
458
+ "Orchestra Hit",
459
+ // Brass (56-63)
460
+ "Trumpet",
461
+ "Trombone",
462
+ "Tuba",
463
+ "Muted Trumpet",
464
+ "French Horn",
465
+ "Brass Section",
466
+ "Synth Brass 1",
467
+ "Synth Brass 2",
468
+ // Reed (64-71)
469
+ "Soprano Sax",
470
+ "Alto Sax",
471
+ "Tenor Sax",
472
+ "Baritone Sax",
473
+ "Oboe",
474
+ "English Horn",
475
+ "Bassoon",
476
+ "Clarinet",
477
+ // Pipe (72-79)
478
+ "Piccolo",
479
+ "Flute",
480
+ "Recorder",
481
+ "Pan Flute",
482
+ "Blown Bottle",
483
+ "Shakuhachi",
484
+ "Whistle",
485
+ "Ocarina",
486
+ // Synth lead (80-87)
487
+ "Lead 1 (square)",
488
+ "Lead 2 (sawtooth)",
489
+ "Lead 3 (calliope)",
490
+ "Lead 4 (chiff)",
491
+ "Lead 5 (charang)",
492
+ "Lead 6 (voice)",
493
+ "Lead 7 (fifths)",
494
+ "Lead 8 (bass + lead)",
495
+ // Synth pad (88-95)
496
+ "Pad 1 (new age)",
497
+ "Pad 2 (warm)",
498
+ "Pad 3 (polysynth)",
499
+ "Pad 4 (choir)",
500
+ "Pad 5 (bowed)",
501
+ "Pad 6 (metallic)",
502
+ "Pad 7 (halo)",
503
+ "Pad 8 (sweep)",
504
+ // Synth effects (96-103)
505
+ "FX 1 (rain)",
506
+ "FX 2 (soundtrack)",
507
+ "FX 3 (crystal)",
508
+ "FX 4 (atmosphere)",
509
+ "FX 5 (brightness)",
510
+ "FX 6 (goblins)",
511
+ "FX 7 (echoes)",
512
+ "FX 8 (sci-fi)",
513
+ // Ethnic (104-111)
514
+ "Sitar",
515
+ "Banjo",
516
+ "Shamisen",
517
+ "Koto",
518
+ "Kalimba",
519
+ "Bag pipe",
520
+ "Fiddle",
521
+ "Shanai",
522
+ // Percussive (112-119)
523
+ "Tinkle Bell",
524
+ "Agogo",
525
+ "Steel Drums",
526
+ "Woodblock",
527
+ "Taiko Drum",
528
+ "Melodic Tom",
529
+ "Synth Drum",
530
+ "Reverse Cymbal",
531
+ // Sound effects (120-127)
532
+ "Guitar Fret Noise",
533
+ "Breath Noise",
534
+ "Seashore",
535
+ "Bird Tweet",
536
+ "Telephone Ring",
537
+ "Helicopter",
538
+ "Applause",
539
+ "Gunshot"
540
+ ][t] || `Program ${t}`;
541
+ }
542
+ /**
543
+ * Generate a unique note ID
544
+ * @param {number} channel - MIDI channel
545
+ * @param {number} pitch - MIDI pitch
546
+ * @param {number} startTime - Start time
547
+ * @returns {string} Unique note ID
548
+ */
549
+ static generateNoteId(t, e, s) {
550
+ return `${t}_${e}_${Math.round(s)}`;
551
+ }
552
+ }
553
+ class x extends v {
554
+ constructor(t, e, s, i = {}) {
555
+ super(t, e, i), this.midiChannel = s, this.currentVolume = i.initialVolume || 1, this.currentInstrument = i.instrument || "piano", this.outputGain = null, this._setupOutputNode(), this.setVolume(this.currentVolume), i.instrument && this.setInstrument(i.instrument);
556
+ }
557
+ getOutputNode() {
558
+ return this.outputGain;
559
+ }
560
+ /**
561
+ * Actually start a note in the synthesizer
562
+ * @param {number} pitch - MIDI pitch (0-127)
563
+ * @param {number} velocity - Note velocity (0-127)
564
+ * @protected
565
+ */
566
+ _actualNoteOn(t, e) {
567
+ const s = this.engine._getSynthesizer();
568
+ if (s && s.noteOn) {
569
+ const i = Math.round(e * this.currentVolume);
570
+ s.noteOn(this.midiChannel, t, i);
571
+ }
572
+ }
573
+ /**
574
+ * Actually stop a note in the synthesizer
575
+ * @param {number} pitch - MIDI pitch (0-127)
576
+ * @protected
577
+ */
578
+ _actualNoteOff(t) {
579
+ const e = this.engine._getSynthesizer();
580
+ e && e.noteOff && e.noteOff(this.midiChannel, t);
581
+ }
582
+ async setInstrument(t) {
583
+ this._validateActive();
584
+ const e = P.getInstrumentProgram(t);
585
+ this.currentInstrument = t;
586
+ const s = this.engine._getSynthesizer();
587
+ s && s.programChange ? s.programChange(this.midiChannel, e) : console.warn("Cannot set instrument: synthesizer not available or no programChange method");
588
+ }
589
+ getInstrument() {
590
+ return this.currentInstrument;
591
+ }
592
+ setVolume(t) {
593
+ this._validateActive(), t = Math.max(0, Math.min(1, t)), this.currentVolume = t;
594
+ const e = Math.round(t * 127), s = this.engine._getSynthesizer();
595
+ s && s.controllerChange && s.controllerChange(this.midiChannel, 7, e);
596
+ }
597
+ getVolume() {
598
+ return this.currentVolume;
599
+ }
600
+ /**
601
+ * Get the MIDI channel number used by this handle
602
+ * @returns {number} MIDI channel (0-15)
603
+ */
604
+ getMidiChannel() {
605
+ return this.midiChannel;
606
+ }
607
+ /**
608
+ * Get count of active notes
609
+ * @returns {number} Number of active notes
610
+ */
611
+ getActiveNoteCount() {
612
+ return this.activeNotes.size;
613
+ }
614
+ destroy() {
615
+ if (!this.isDestroyed) {
616
+ const t = this.engine._getIndividualOutput(this.midiChannel);
617
+ this.outputGain && this.outputGain !== t && this.outputGain.disconnect(), this.outputGain = null;
618
+ }
619
+ super.destroy();
620
+ }
621
+ /**
622
+ * Set up the output gain node for this channel
623
+ * This connects to the individual synthesizer output for this MIDI channel
624
+ * @private
625
+ */
626
+ _setupOutputNode() {
627
+ const t = this.engine._getIndividualOutput(this.midiChannel);
628
+ t ? this.outputGain = t : (console.warn(`No individual output available for MIDI channel ${this.midiChannel}, using fallback`), this.outputGain = this.engine.audioContext.createGain(), this.outputGain.gain.value = this.currentVolume);
629
+ }
630
+ }
631
+ class B extends k {
632
+ constructor(t, e = {}) {
633
+ super(t, e), this.synthesizer = null, this.soundfont = null, this.channelCounter = 0, this.partToMidiChannel = /* @__PURE__ */ new Map(), this.midiChannelToPart = /* @__PURE__ */ new Map(), this.individualOutputs = [];
634
+ }
635
+ async initialize(t) {
636
+ try {
637
+ const { Synthetizer: e } = await import("spessasynth_lib");
638
+ let s;
639
+ if (typeof t == "string")
640
+ console.log("Loading soundfont from path:", t), s = await this._loadSoundfontFromPath(t), console.log("Soundfont loaded successfully, size:", s.byteLength, "bytes");
641
+ else if (t instanceof ArrayBuffer)
642
+ s = t;
643
+ else
644
+ throw new Error("Invalid soundfont data type. Expected string path or ArrayBuffer.");
645
+ await this._loadAudioWorkletSafely(), this._setupIndividualOutputs(), this.dummyTarget = this.audioContext.createGain(), await new Promise((i) => setTimeout(i, 50)), this.synthesizer = new e(this.dummyTarget, s), this._connectIndividualOutputs(), this.isInitialized = !0;
646
+ } catch (e) {
647
+ throw console.error("Failed to initialize SpessaSynthAudioEngine:", e), e;
648
+ }
649
+ }
650
+ createChannel(t, e = {}) {
651
+ if (this._validateInitialized(), this.partToMidiChannel.has(t))
652
+ throw new Error(`Channel for part '${t}' already exists`);
653
+ const s = this.channelCounter;
654
+ if (s >= 16)
655
+ throw new Error("Maximum number of MIDI channels (16) exceeded");
656
+ this.channelCounter++, this.partToMidiChannel.set(t, s), this.midiChannelToPart.set(s, t);
657
+ const i = new x(
658
+ this,
659
+ t,
660
+ s,
661
+ e
662
+ );
663
+ return this._registerChannel(i), e.instrument && i.setInstrument(e.instrument), i;
664
+ }
665
+ allSoundsOff() {
666
+ this.synthesizer && this.midiChannelToPart.forEach((t, e) => {
667
+ this.synthesizer.controllerChange && this.synthesizer.controllerChange(e, 120, 0);
668
+ });
669
+ }
670
+ /**
671
+ * Clear all channels (for loading new scores)
672
+ */
673
+ clearAllChannels() {
674
+ this.allSoundsOff(), this.partToMidiChannel.clear(), this.midiChannelToPart.clear(), this.channelCounter = 0;
675
+ }
676
+ destroy() {
677
+ this.allSoundsOff(), this.synthesizer && typeof this.synthesizer.disconnect == "function" && this.synthesizer.disconnect(), this.individualOutputs.forEach((t) => {
678
+ t && t.disconnect && t.disconnect();
679
+ }), this.individualOutputs = [], this.dummyTarget && (this.dummyTarget.disconnect(), this.dummyTarget = null), this.partToMidiChannel.clear(), this.midiChannelToPart.clear(), this.channelCounter = 0, super.destroy(), this.synthesizer = null, this.soundfont = null;
680
+ }
681
+ /**
682
+ * Get the MIDI channel number for a part
683
+ * @param {string} partId - Part identifier
684
+ * @returns {number|null} MIDI channel number or null if not found
685
+ */
686
+ getMidiChannelForPart(t) {
687
+ return this.partToMidiChannel.has(t) ? this.partToMidiChannel.get(t) : null;
688
+ }
689
+ /**
690
+ * Get access to the underlying synthesizer for channel handles
691
+ * @returns {Object} SpessaSynth synthesizer instance
692
+ * @protected
693
+ */
694
+ _getSynthesizer() {
695
+ return this.synthesizer;
696
+ }
697
+ /**
698
+ * Get the individual output node for a specific MIDI channel
699
+ * @param {number} midiChannel - MIDI channel number (0-15)
700
+ * @returns {AudioNode|null} Individual output node or null if not available
701
+ * @protected
702
+ */
703
+ _getIndividualOutput(t) {
704
+ return t >= 0 && t < this.individualOutputs.length ? this.individualOutputs[t] : null;
705
+ }
706
+ /**
707
+ * Set up individual output nodes for each MIDI channel
708
+ * @private
709
+ */
710
+ _setupIndividualOutputs() {
711
+ this.individualOutputs = [];
712
+ for (let t = 0; t < 16; t++) {
713
+ const e = this.audioContext.createGain();
714
+ e.gain.value = 1, this.individualOutputs.push(e);
715
+ }
716
+ }
717
+ /**
718
+ * Connect synthesizer individual outputs to channel-specific gain nodes
719
+ * @private
720
+ */
721
+ _connectIndividualOutputs() {
722
+ try {
723
+ this.synthesizer && this.synthesizer.connectIndividualOutputs ? this.synthesizer.connectIndividualOutputs(this.individualOutputs) : console.warn("Synthesizer does not support individual outputs, using master output only");
724
+ } catch (t) {
725
+ console.warn("Failed to connect individual outputs:", t.message), console.warn("Falling back to master output routing");
726
+ }
727
+ }
728
+ /**
729
+ * Load soundfont from file path or URL
730
+ * @param {string} path - Path to soundfont file
731
+ * @returns {Promise<ArrayBuffer>} Soundfont data
732
+ * @private
733
+ */
734
+ async _loadSoundfontFromPath(t) {
735
+ if (typeof window < "u") {
736
+ const e = await fetch(t);
737
+ if (!e.ok)
738
+ throw new Error(`Failed to load soundfont: ${e.status} ${e.statusText}`);
739
+ return await e.arrayBuffer();
740
+ } else {
741
+ const e = await Promise.resolve().then(() => C), s = await Promise.resolve().then(() => C), i = s.isAbsolute(t) ? t : s.resolve(process.cwd(), t);
742
+ return e.readFileSync(i).buffer;
743
+ }
744
+ }
745
+ /**
746
+ * Load AudioWorklet with aggressive Firefox compatibility measures
747
+ * @private
748
+ */
749
+ async _loadAudioWorkletSafely() {
750
+ const t = "/node_modules/spessasynth_lib/synthetizer/worklet_processor.min.js", s = typeof navigator < "u" && navigator.userAgent.toLowerCase().includes("firefox");
751
+ for (let i = 1; i <= 5; i++)
752
+ try {
753
+ if (s) {
754
+ const r = i * 200;
755
+ await new Promise((n) => setTimeout(n, r)), "gc" in window && typeof window.gc == "function" && window.gc(), this.audioContext.state === "suspended" && await this.audioContext.resume();
756
+ }
757
+ console.log(`Attempting to load AudioWorklet (attempt ${i}/5)`), await this.audioContext.audioWorklet.addModule(t), console.log(`AudioWorklet loaded successfully on attempt ${i}`);
758
+ return;
759
+ } catch (r) {
760
+ if (console.warn(`AudioWorklet loading failed (attempt ${i}/5):`, r.message), i === 5)
761
+ throw new Error(`AudioWorklet failed after 5 attempts: ${r.message}`);
762
+ const n = s ? i * 1e3 : i * 500;
763
+ await new Promise((l) => setTimeout(l, n));
764
+ }
765
+ }
766
+ }
767
+ class E {
768
+ constructor() {
769
+ this.partNames = [
770
+ "soprano",
771
+ "alto",
772
+ "tenor",
773
+ "bass",
774
+ "treble",
775
+ "mezzo",
776
+ "baritone",
777
+ "s",
778
+ "a",
779
+ "t",
780
+ "b",
781
+ "satb"
782
+ ], this.parsedData = {
783
+ parts: {},
784
+ // Will contain separate arrays for each vocal part
785
+ barStructure: [],
786
+ // Will contain bar timing information
787
+ metadata: {}
788
+ // Will contain title, composer, etc.
789
+ };
790
+ }
791
+ /**
792
+ * Main method to parse a MIDI file
793
+ * @param {ArrayBuffer} midiFileBuffer - The MIDI file as an ArrayBuffer
794
+ * @returns {Object} Parsed data with parts, barStructure and metadata
795
+ */
796
+ async parse(t) {
797
+ try {
798
+ const e = await this._parseMidiBuffer(t);
799
+ return this._extractMetadata(e), this._extractBarStructure(e), this._extractParts(e), this.parsedData;
800
+ } catch (e) {
801
+ throw console.error("Error parsing MIDI file:", e), e;
802
+ }
803
+ }
804
+ /**
805
+ * Parse the MIDI buffer into a workable format
806
+ * @private
807
+ */
808
+ async _parseMidiBuffer(t) {
809
+ const e = new Uint8Array(t);
810
+ if (!(e[0] === 77 && e[1] === 84 && e[2] === 104 && e[3] === 100))
811
+ throw new Error("Not a valid MIDI file");
812
+ const s = this._bytesToNumber(e.slice(4, 8)), i = this._bytesToNumber(e.slice(8, 10)), r = this._bytesToNumber(e.slice(10, 12)), n = this._bytesToNumber(e.slice(12, 14)), l = n & 32768 ? null : n, o = {
813
+ format: i,
814
+ ticksPerBeat: l,
815
+ tracks: [],
816
+ duration: 0
817
+ };
818
+ let a = 8 + s;
819
+ for (let c = 0; c < r; c++)
820
+ if (e[a] === 77 && e[a + 1] === 84 && e[a + 2] === 114 && e[a + 3] === 107) {
821
+ const h = this._bytesToNumber(e.slice(a + 4, a + 8)), m = e.slice(a + 8, a + 8 + h), f = this._parseTrack(m);
822
+ o.tracks.push(f), a += 8 + h;
823
+ } else
824
+ throw new Error(`Invalid track header at position ${a}`);
825
+ return o;
826
+ }
827
+ /**
828
+ * Parse a single MIDI track
829
+ * @private
830
+ */
831
+ _parseTrack(t) {
832
+ const e = {
833
+ notes: [],
834
+ name: null,
835
+ lyrics: [],
836
+ events: [],
837
+ duration: 0
838
+ };
839
+ let s = 0, i = 0, r = null;
840
+ for (; s < t.length; ) {
841
+ let n = 0, l = 0;
842
+ do
843
+ l = t[s++], n = n << 7 | l & 127;
844
+ while (l & 128);
845
+ i += n, l = t[s++];
846
+ let o = l;
847
+ if ((l & 128) === 0) {
848
+ if (r === null)
849
+ throw new Error("Running status byte encountered before status byte");
850
+ o = r, s--;
851
+ } else
852
+ r = o;
853
+ if (o === 255) {
854
+ const a = t[s++], c = this._readVariableLengthValue(t, s);
855
+ s += c.bytesRead;
856
+ const h = t.slice(s, s + c.value);
857
+ switch (s += c.value, a) {
858
+ case 3:
859
+ e.name = this._bytesToString(h);
860
+ break;
861
+ case 1:
862
+ e.events.push({
863
+ type: "text",
864
+ text: this._bytesToString(h),
865
+ tick: i
866
+ });
867
+ break;
868
+ case 5:
869
+ e.lyrics.push({
870
+ text: this._bytesToString(h),
871
+ tick: i
872
+ });
873
+ break;
874
+ case 81:
875
+ const m = this._bytesToNumber(h), f = Math.round(6e7 / m);
876
+ e.events.push({
877
+ type: "tempo",
878
+ bpm: f,
879
+ tick: i
880
+ });
881
+ break;
882
+ case 88:
883
+ e.events.push({
884
+ type: "timeSignature",
885
+ numerator: h[0],
886
+ denominator: Math.pow(2, h[1]),
887
+ tick: i
888
+ });
889
+ break;
890
+ case 47:
891
+ e.duration = i;
892
+ break;
893
+ }
894
+ } else if ((o & 240) === 144) {
895
+ const a = o & 15, c = t[s++], h = t[s++];
896
+ h > 0 ? e.notes.push({
897
+ type: "noteOn",
898
+ noteNumber: c,
899
+ velocity: h,
900
+ tick: i,
901
+ channel: a
902
+ }) : e.notes.push({
903
+ type: "noteOff",
904
+ noteNumber: c,
905
+ tick: i,
906
+ channel: a
907
+ });
908
+ } else if ((o & 240) === 128) {
909
+ const a = o & 15, c = t[s++];
910
+ t[s++], e.notes.push({
911
+ type: "noteOff",
912
+ noteNumber: c,
913
+ tick: i,
914
+ channel: a
915
+ });
916
+ } else if (o === 240 || o === 247) {
917
+ const a = this._readVariableLengthValue(t, s);
918
+ s += a.bytesRead + a.value;
919
+ } else if ((o & 240) === 176) {
920
+ const a = o & 15, c = t[s++], h = t[s++];
921
+ e.events.push({
922
+ type: "controller",
923
+ controllerNumber: c,
924
+ value: h,
925
+ channel: a,
926
+ tick: i
927
+ });
928
+ } else if ((o & 240) === 192) {
929
+ const a = o & 15, c = t[s++];
930
+ e.events.push({
931
+ type: "programChange",
932
+ programNumber: c,
933
+ channel: a,
934
+ tick: i
935
+ });
936
+ } else if ((o & 240) === 208) {
937
+ const a = o & 15, c = t[s++];
938
+ e.events.push({
939
+ type: "channelAftertouch",
940
+ pressure: c,
941
+ channel: a,
942
+ tick: i
943
+ });
944
+ } else if ((o & 240) === 224) {
945
+ const a = o & 15, c = t[s++], m = (t[s++] << 7 | c) - 8192;
946
+ e.events.push({
947
+ type: "pitchBend",
948
+ value: m,
949
+ channel: a,
950
+ tick: i
951
+ });
952
+ } else if ((o & 240) === 160) {
953
+ const a = o & 15, c = t[s++], h = t[s++];
954
+ e.events.push({
955
+ type: "noteAftertouch",
956
+ noteNumber: c,
957
+ pressure: h,
958
+ channel: a,
959
+ tick: i
960
+ });
961
+ } else
962
+ console.warn(`Unknown event type: ${o.toString(16)} at position ${s - 1}`), s++;
963
+ }
964
+ return e;
965
+ }
966
+ /**
967
+ * Extract metadata from the MIDI object
968
+ * @private
969
+ */
970
+ _extractMetadata(t) {
971
+ const e = {
972
+ title: null,
973
+ composer: null,
974
+ partNames: [],
975
+ format: t.format,
976
+ ticksPerBeat: t.ticksPerBeat
977
+ };
978
+ t.tracks.forEach((s, i) => {
979
+ if (s.name && !e.title && (e.title = s.name), s.events.filter((r) => r.type === "text").forEach((r) => {
980
+ const n = r.text.toLowerCase();
981
+ (n.includes("compos") || n.includes("by")) && !e.composer && (e.composer = r.text);
982
+ }), s.name) {
983
+ const r = s.name.toLowerCase();
984
+ for (const n of this.partNames)
985
+ if (r.includes(n)) {
986
+ e.partNames.push({
987
+ index: i,
988
+ name: s.name
989
+ });
990
+ break;
991
+ }
992
+ }
993
+ }), this.parsedData.metadata = e;
994
+ }
995
+ /**
996
+ * Extract bar structure (time signatures, tempo changes)
997
+ * New format: {"sig": [numerator, denominator], "bpm": tempo_or_array}
998
+ * @private
999
+ */
1000
+ _extractBarStructure(t) {
1001
+ const e = t.ticksPerBeat || 480, s = [];
1002
+ t.tracks.forEach((h) => {
1003
+ h.events.forEach((m) => {
1004
+ (m.type === "timeSignature" || m.type === "tempo") && s.push(m);
1005
+ });
1006
+ }), s.sort((h, m) => h.tick - m.tick);
1007
+ let i = 0;
1008
+ t.tracks.forEach((h) => {
1009
+ h.notes && h.notes.forEach((m) => {
1010
+ m.type === "noteOff" && m.tick > i && (i = m.tick);
1011
+ });
1012
+ }), i === 0 && (i = e * 8);
1013
+ const r = [], n = s.filter((h) => h.type === "timeSignature").sort((h, m) => h.tick - m.tick);
1014
+ let l = { numerator: 4, denominator: 4 }, o = 120, a = 0, c = 0;
1015
+ for (; a < i; ) {
1016
+ for (; c < n.length && n[c].tick <= a; )
1017
+ l = n[c], c++;
1018
+ let h;
1019
+ h = a + e * 4 * l.numerator / l.denominator;
1020
+ const m = s.filter(
1021
+ (g) => g.type === "tempo" && g.tick >= a && g.tick < h
1022
+ ), f = l.numerator, u = [];
1023
+ for (let g = 0; g < f; g++) {
1024
+ const y = a + g * e;
1025
+ let w = o;
1026
+ for (let b = m.length - 1; b >= 0; b--) {
1027
+ const T = m[b];
1028
+ if (T.tick <= y) {
1029
+ w = T.bpm;
1030
+ break;
1031
+ }
1032
+ }
1033
+ for (let b = s.length - 1; b >= 0; b--) {
1034
+ const T = s[b];
1035
+ if (T.type === "tempo" && T.tick <= y) {
1036
+ w = T.bpm, T.tick <= a && (o = T.bpm);
1037
+ break;
1038
+ }
1039
+ }
1040
+ u.push(w * l.denominator / 4);
1041
+ }
1042
+ const _ = u.every((g) => g === u[0]) ? u[0] : u;
1043
+ r.push({
1044
+ sig: [l.numerator, l.denominator],
1045
+ bpm: _
1046
+ }), a = h;
1047
+ }
1048
+ this.parsedData.barStructure = r;
1049
+ }
1050
+ /**
1051
+ * Extract notes for each voice part
1052
+ * @private
1053
+ */
1054
+ _extractParts(t) {
1055
+ const e = {}, s = t.ticksPerBeat;
1056
+ t.tracks.forEach((i, r) => {
1057
+ if (!i.notes.length) return;
1058
+ let n = null;
1059
+ if (i.name) {
1060
+ const u = i.name.toLowerCase();
1061
+ for (const d of this.partNames)
1062
+ if (d.length === 1) {
1063
+ if (u === d) {
1064
+ n = d;
1065
+ break;
1066
+ }
1067
+ } else if (u.includes(d)) {
1068
+ n = d;
1069
+ break;
1070
+ }
1071
+ }
1072
+ n || (n = i.name || `Track ${r + 1}`), n === "s" && (n = "soprano"), n === "a" && (n = "alto"), n === "t" && (n = "tenor"), n === "b" && (n = "bass");
1073
+ let l = n, o = 2;
1074
+ for (; e[l]; )
1075
+ l = `${n} ${o}`, o++;
1076
+ n = l;
1077
+ const a = [], c = {};
1078
+ i.notes.forEach((u) => {
1079
+ if (u.type === "noteOn")
1080
+ c[u.noteNumber] = {
1081
+ tick: u.tick,
1082
+ velocity: u.velocity
1083
+ };
1084
+ else if (u.type === "noteOff" && c[u.noteNumber]) {
1085
+ const d = c[u.noteNumber], _ = u.tick - d.tick;
1086
+ a.push({
1087
+ pitch: u.noteNumber,
1088
+ name: this._midiNoteToName(u.noteNumber),
1089
+ startTick: d.tick,
1090
+ endTick: u.tick,
1091
+ duration: _,
1092
+ // Convert ticks to actual time considering tempo changes
1093
+ startTime: this._ticksToTime(d.tick, t),
1094
+ endTime: this._ticksToTime(u.tick, t),
1095
+ velocity: d.velocity
1096
+ }), delete c[u.noteNumber];
1097
+ }
1098
+ });
1099
+ const h = i.lyrics.map((u) => ({
1100
+ text: u.text,
1101
+ tick: u.tick,
1102
+ time: u.tick / s
1103
+ // Time in quarter notes
1104
+ }));
1105
+ a.sort((u, d) => u.startTick - d.startTick);
1106
+ const m = i.events.filter((u) => u.type === "programChange").map((u) => ({
1107
+ programNumber: u.programNumber,
1108
+ tick: u.tick,
1109
+ time: this._ticksToTime(u.tick, t)
1110
+ })).sort((u, d) => u.tick - d.tick), f = m.length > 0 ? m[0].programNumber : 0;
1111
+ e[n] = {
1112
+ notes: a,
1113
+ lyrics: h,
1114
+ trackIndex: r,
1115
+ programChanges: m,
1116
+ defaultInstrument: f
1117
+ };
1118
+ }), this.parsedData.parts = e;
1119
+ }
1120
+ /**
1121
+ * Convert a MIDI note number to note name
1122
+ * @private
1123
+ */
1124
+ _midiNoteToName(t) {
1125
+ const e = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"], s = Math.floor(t / 12) - 1;
1126
+ return `${e[t % 12]}${s}`;
1127
+ }
1128
+ /**
1129
+ * Convert bytes to number
1130
+ * @private
1131
+ */
1132
+ _bytesToNumber(t) {
1133
+ let e = 0;
1134
+ for (let s = 0; s < t.length; s++)
1135
+ e = e << 8 | t[s];
1136
+ return e;
1137
+ }
1138
+ /**
1139
+ * Convert bytes to string
1140
+ * @private
1141
+ */
1142
+ _bytesToString(t) {
1143
+ return new TextDecoder().decode(t);
1144
+ }
1145
+ /**
1146
+ * Read variable-length value from MIDI data
1147
+ * @private
1148
+ */
1149
+ _readVariableLengthValue(t, e) {
1150
+ let s = 0, i, r = 0;
1151
+ do
1152
+ i = t[e + r++], s = s << 7 | i & 127;
1153
+ while (i & 128);
1154
+ return { value: s, bytesRead: r };
1155
+ }
1156
+ /**
1157
+ * Convert ticks to time in seconds considering tempo changes within bars
1158
+ * @private
1159
+ */
1160
+ _ticksToTime(t, e) {
1161
+ const s = e.ticksPerBeat || 480, i = [];
1162
+ e.tracks.forEach((o) => {
1163
+ o.events.forEach((a) => {
1164
+ a.type === "tempo" && i.push(a);
1165
+ });
1166
+ }), i.sort((o, a) => o.tick - a.tick);
1167
+ let r = 0, n = 0, l = 120;
1168
+ for (const o of i) {
1169
+ if (o.tick > t) break;
1170
+ if (o.tick > n) {
1171
+ const c = (o.tick - n) / s * (60 / l);
1172
+ r += c, n = o.tick;
1173
+ }
1174
+ l = o.bpm;
1175
+ }
1176
+ if (t > n) {
1177
+ const a = (t - n) / s * (60 / l);
1178
+ r += a;
1179
+ }
1180
+ return r;
1181
+ }
1182
+ }
1183
+ class S {
1184
+ constructor() {
1185
+ this.barOrder = [], this.beatTable = [];
1186
+ }
1187
+ /**
1188
+ * Main method to generate beat mapping from parsed MIDI and structure metadata
1189
+ * @param {Object} parsedMidiData - Output from MidiParser
1190
+ * @param {Object} structureMetadata - Score structure with sections and order
1191
+ * @returns {Array} Beat table with time, repeat, bar, and beat information
1192
+ */
1193
+ mapBeats(t, e) {
1194
+ try {
1195
+ return this.barOrder = this.generateBarOrder(e.sections, e.order), this.beatTable = this.generateBeatTable(this.barOrder, t.barStructure), this.beatTable;
1196
+ } catch (s) {
1197
+ throw console.error("Error mapping beats:", s), s;
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Generate ordered sequence of bars with repeat information
1202
+ * @param {Array} sections - Section definitions from structure metadata
1203
+ * @param {Array} order - Play order from structure metadata
1204
+ * @returns {Array} Bar order with bar numbers and repeat counts
1205
+ */
1206
+ generateBarOrder(t, e) {
1207
+ const s = [], i = {};
1208
+ for (const r of e) {
1209
+ const n = t[r.section];
1210
+ if (!n)
1211
+ throw new Error(`Invalid section index: ${r.section}`);
1212
+ const l = r.section;
1213
+ i[l] || (i[l] = 0), i[l]++;
1214
+ const o = i[l], a = r.from !== void 0 ? r.from : this._getSectionStartBar(t, r.section), c = r.to !== void 0 ? r.to : n.to, h = r.as || 1;
1215
+ for (let m = a; m <= c; m++)
1216
+ this._shouldPlayBar(n, m, h) && s.push({
1217
+ barNumber: m,
1218
+ repeat: o,
1219
+ sectionIndex: r.section,
1220
+ voltaTime: h
1221
+ });
1222
+ }
1223
+ return s;
1224
+ }
1225
+ /**
1226
+ * Generate beat table from bar order and MIDI bar structure
1227
+ * @param {Array} barOrder - Bar sequence with repeat information
1228
+ * @param {Array} barStructure - MIDI bar structure with timing info
1229
+ * @returns {Array} Beat table with time, repeat, bar, and beat information
1230
+ */
1231
+ generateBeatTable(t, e) {
1232
+ const s = [];
1233
+ let i = 0;
1234
+ for (const r of t) {
1235
+ const n = r.barNumber, l = e[n];
1236
+ if (!l)
1237
+ throw new Error(`No bar structure data found for bar ${n}`);
1238
+ const { sig: [o, a], bpm: c } = l, h = 60 / c;
1239
+ for (let m = 1; m <= o; m++)
1240
+ s.push({
1241
+ time: i,
1242
+ repeat: r.repeat,
1243
+ bar: n,
1244
+ beat: m
1245
+ }), i += h;
1246
+ }
1247
+ return s;
1248
+ }
1249
+ /**
1250
+ * Determine the starting bar for a section
1251
+ * @private
1252
+ */
1253
+ _getSectionStartBar(t, e) {
1254
+ return t[e].pickup !== void 0 ? 0 : e === 0 ? 1 : t[e - 1].to + 1;
1255
+ }
1256
+ /**
1257
+ * Check if a bar should be played based on volta information
1258
+ * @private
1259
+ */
1260
+ _shouldPlayBar(t, e, s) {
1261
+ if (!t.voltas)
1262
+ return !0;
1263
+ const i = t.voltas.indexOf(e);
1264
+ return i === -1 ? !0 : i + 1 === s;
1265
+ }
1266
+ }
1267
+ function O(p) {
1268
+ return { all: p = p || /* @__PURE__ */ new Map(), on: function(t, e) {
1269
+ var s = p.get(t);
1270
+ s ? s.push(e) : p.set(t, [e]);
1271
+ }, off: function(t, e) {
1272
+ var s = p.get(t);
1273
+ s && (e ? s.splice(s.indexOf(e) >>> 0, 1) : p.set(t, []));
1274
+ }, emit: function(t, e) {
1275
+ var s = p.get(t);
1276
+ s && s.slice().map(function(i) {
1277
+ i(e);
1278
+ }), (s = p.get("*")) && s.slice().map(function(i) {
1279
+ i(t, e);
1280
+ });
1281
+ } };
1282
+ }
1283
+ class N {
1284
+ /**
1285
+ * Create a new MidiPlayer instance
1286
+ * @param {AudioEngine} audioEngine - Initialized audio engine instance
1287
+ * @param {Object} instrumentMap - Mapping of part names to instrument configurations
1288
+ * @param {Object} parsedMidiData - Output from MidiParser
1289
+ * @param {Object} [structureMetadata] - Optional score structure for beat mapping
1290
+ */
1291
+ constructor(t, e, s, i = null) {
1292
+ if (!t || !t.isInitialized)
1293
+ throw new Error("Initialized AudioEngine is required");
1294
+ if (!s)
1295
+ throw new Error("Parsed MIDI data is required");
1296
+ this.audioEngine = t, this.instrumentMap = e || {}, this.parsedData = s, this._isPlaying = !1, this._currentTime = 0, this._totalDuration = 0, this.playbackSpeed = 1, this.partChannels = /* @__PURE__ */ new Map(), this.partOutputs = /* @__PURE__ */ new Map(), this.playbackStartTime = 0, this.lookAheadTime = 0.05, this.scheduleInterval = null, this.partNotePointers = /* @__PURE__ */ new Map(), this.partProgramPointers = /* @__PURE__ */ new Map(), this.eventBus = O(), this.beatMapper = new S(), this.beats = [], this._setupPartChannels(), this._calculateTotalDuration(), this._resetNotePointers(), this._resetProgramPointers(), i ? this.beats = this.beatMapper.mapBeats(s, i) : this.beats = this._generateSimpleBeatMapping();
1297
+ }
1298
+ // ========================================
1299
+ // PUBLIC API METHODS (unchanged interface)
1300
+ // ========================================
1301
+ /**
1302
+ * Start playback from current position
1303
+ */
1304
+ play() {
1305
+ this._isPlaying || (this._isPlaying = !0, this.playbackStartTime = this.audioEngine.audioContext.currentTime - this._currentTime / this.playbackSpeed, this._resetNotePointers(), this._resetProgramPointers(), this._schedulePlayback(), this._startTimeUpdateLoop());
1306
+ }
1307
+ /**
1308
+ * Start playback at a specific audio context time
1309
+ * @param {number} startTime - Audio context time when playback should begin
1310
+ */
1311
+ playAt(t) {
1312
+ this._isPlaying || (this._isPlaying = !0, this.playbackStartTime = t - this._currentTime / this.playbackSpeed, this._resetNotePointers(), this._resetProgramPointers(), this._schedulePlayback(), this._startTimeUpdateLoop());
1313
+ }
1314
+ /**
1315
+ * Pause playback (resumable)
1316
+ */
1317
+ pause() {
1318
+ this._isPlaying && (this._isPlaying = !1, this._stopScheduling(), this._stopTimeUpdateLoop());
1319
+ }
1320
+ /**
1321
+ * Stop playback and reset to beginning
1322
+ */
1323
+ stop() {
1324
+ this._isPlaying = !1, this._currentTime = 0, this._stopScheduling(), this._stopTimeUpdateLoop(), this._resetNotePointers(), this._resetProgramPointers();
1325
+ }
1326
+ /**
1327
+ * Skip to a specific time in seconds
1328
+ * @param {number} seconds - Time to skip to
1329
+ */
1330
+ skipToTime(t) {
1331
+ t = Math.max(0, Math.min(t, this._totalDuration));
1332
+ const e = this._isPlaying;
1333
+ e && this.pause(), this._currentTime = t, this._resetNotePointers(), this._resetProgramPointers(), e && this.play();
1334
+ }
1335
+ /**
1336
+ * Set playback speed
1337
+ * @param {number} speed - Speed multiplier (1.0 = normal)
1338
+ */
1339
+ setPlaybackSpeed(t) {
1340
+ if (t <= 0)
1341
+ throw new Error("Playback speed must be greater than 0");
1342
+ const e = this._isPlaying;
1343
+ e && this.pause(), this.playbackSpeed = t, e && this.play();
1344
+ }
1345
+ /**
1346
+ * Navigate to a specific bar position
1347
+ * @param {number} barNumber - Bar number (1-based)
1348
+ * @param {number} repeat - Repeat section number (0-based)
1349
+ */
1350
+ setBar(t, e = 0) {
1351
+ const s = this.getTimeFromBar(t, e);
1352
+ s !== null && (this.skipToTime(s), this._emitEvent("barChanged", { bar: t, beat: 1, repeat: e, time: s }));
1353
+ }
1354
+ /**
1355
+ * Get time position for a specific bar
1356
+ * @param {number} barNumber - Bar number (1-based)
1357
+ * @param {number} repeat - Repeat section number (0-based)
1358
+ * @returns {number|null} Time in seconds or null if not found
1359
+ */
1360
+ getTimeFromBar(t, e = 0) {
1361
+ const s = this.beats.find(
1362
+ (i) => i.bar === t && i.beat === 1 && i.repeat === e
1363
+ );
1364
+ return s ? s.time : null;
1365
+ }
1366
+ /**
1367
+ * Get bar information for a specific time position
1368
+ * @param {number} time - Time in seconds
1369
+ * @returns {Object|null} Bar info object or null
1370
+ */
1371
+ getBarFromTime(t) {
1372
+ if (!this.beats.length) return null;
1373
+ let e = null;
1374
+ for (let s = this.beats.length - 1; s >= 0; s--)
1375
+ if (this.beats[s].time <= t) {
1376
+ e = this.beats[s];
1377
+ break;
1378
+ }
1379
+ return e;
1380
+ }
1381
+ /**
1382
+ * Stop all sounds immediately
1383
+ */
1384
+ allSoundsOff() {
1385
+ this.audioEngine.allSoundsOff();
1386
+ }
1387
+ /**
1388
+ * Get the output node for a specific part for external mixer routing
1389
+ *
1390
+ * IMPORTANT: You MUST connect this output to external gain/analyzer nodes:
1391
+ *
1392
+ * @example
1393
+ * const partOutput = player.getPartOutput('soprano');
1394
+ * const externalGain = audioContext.createGain();
1395
+ * const analyzer = audioContext.createAnalyser();
1396
+ *
1397
+ * partOutput.connect(externalGain);
1398
+ * externalGain.connect(analyzer);
1399
+ * analyzer.connect(masterGain); // External master gain
1400
+ *
1401
+ * // Now control volume via external gain:
1402
+ * externalGain.gain.value = 0.5; // 50% volume
1403
+ *
1404
+ * @param {string} partName - Name of the part (e.g., 'soprano', 'alto')
1405
+ * @returns {AudioNode|null} Output gain node for this part
1406
+ */
1407
+ getPartOutput(t) {
1408
+ return this.partOutputs.get(t) || null;
1409
+ }
1410
+ /**
1411
+ * Get the channel handle for a specific part (for direct note control)
1412
+ * @param {string} partName - Name of the part
1413
+ * @returns {ChannelHandle|null} Channel handle for this part
1414
+ */
1415
+ getPartChannel(t) {
1416
+ return this.partChannels.get(t) || null;
1417
+ }
1418
+ // ========================================
1419
+ // STATE ACCESS METHODS (unchanged)
1420
+ // ========================================
1421
+ getCurrentTime() {
1422
+ if (this._isPlaying) {
1423
+ const t = (this.audioEngine.audioContext.currentTime - this.playbackStartTime) * this.playbackSpeed;
1424
+ this._currentTime = Math.min(t, this._totalDuration);
1425
+ }
1426
+ return this._currentTime;
1427
+ }
1428
+ getTotalDuration() {
1429
+ return this._totalDuration;
1430
+ }
1431
+ isPlaying() {
1432
+ return this._isPlaying;
1433
+ }
1434
+ // ========================================
1435
+ // EVENT HANDLING (mitt-based)
1436
+ // ========================================
1437
+ on(t, e) {
1438
+ this.eventBus.on(t, e);
1439
+ }
1440
+ off(t, e) {
1441
+ this.eventBus.off(t, e);
1442
+ }
1443
+ // ========================================
1444
+ // PART-CENTRIC IMPLEMENTATION METHODS
1445
+ // ========================================
1446
+ /**
1447
+ * Set up channel handles for each musical part
1448
+ * @private
1449
+ */
1450
+ _setupPartChannels() {
1451
+ Object.keys(this.parsedData.parts).forEach((t) => {
1452
+ const e = this.parsedData.parts[t], s = this.instrumentMap[t] || {}, i = s.instrument !== void 0 ? s.instrument : e.defaultInstrument !== void 0 ? e.defaultInstrument : 0;
1453
+ try {
1454
+ const r = this.audioEngine.createChannel(t, {
1455
+ instrument: i,
1456
+ initialVolume: s.volume || 1
1457
+ });
1458
+ this.partChannels.set(t, r);
1459
+ const n = this.audioEngine.audioContext.createGain();
1460
+ n.gain.value = 1;
1461
+ const l = r.getOutputNode();
1462
+ l && l.connect(n), this.partOutputs.set(t, n);
1463
+ } catch (r) {
1464
+ console.error(`Failed to create channel for part '${t}':`, r), this._emitEvent("error", r);
1465
+ }
1466
+ });
1467
+ }
1468
+ /**
1469
+ * Calculate total duration from parsed MIDI data
1470
+ * @private
1471
+ */
1472
+ _calculateTotalDuration() {
1473
+ let t = 0;
1474
+ Object.values(this.parsedData.parts).forEach((e) => {
1475
+ e.notes.forEach((s) => {
1476
+ s.endTime > t && (t = s.endTime);
1477
+ });
1478
+ }), this._totalDuration = t;
1479
+ }
1480
+ /**
1481
+ * Schedule all notes for playback using simplified approach
1482
+ * @private
1483
+ */
1484
+ _schedulePlayback() {
1485
+ this._stopScheduling(), this._startScheduleLoop();
1486
+ }
1487
+ /**
1488
+ * Start the simplified scheduling loop
1489
+ * @private
1490
+ */
1491
+ _startScheduleLoop() {
1492
+ this.scheduleInterval || (this.scheduleInterval = setInterval(() => {
1493
+ if (!this._isPlaying) return;
1494
+ const e = (this.audioEngine.audioContext.currentTime - this.playbackStartTime) * this.playbackSpeed, s = e + this.lookAheadTime;
1495
+ for (const [i, r] of this.partChannels) {
1496
+ const n = this.parsedData.parts[i];
1497
+ if (n) {
1498
+ if (n.programChanges && n.programChanges.length > 0) {
1499
+ let l = this.partProgramPointers.get(i) || 0;
1500
+ const o = n.programChanges;
1501
+ for (; l < o.length && o[l].time < e; )
1502
+ l++;
1503
+ for (; l < o.length && o[l].time <= s; ) {
1504
+ const a = o[l];
1505
+ r.setInstrument(a.programNumber), l++;
1506
+ }
1507
+ this.partProgramPointers.set(i, l);
1508
+ }
1509
+ if (n.notes) {
1510
+ let l = this.partNotePointers.get(i) || 0;
1511
+ const o = n.notes;
1512
+ for (; l < o.length && o[l].endTime < e; )
1513
+ l++;
1514
+ for (; l < o.length && o[l].startTime <= s; ) {
1515
+ const a = o[l];
1516
+ if (a.endTime - a.startTime >= 0.01) {
1517
+ const c = this.playbackStartTime + a.startTime / this.playbackSpeed, h = (a.endTime - a.startTime) / this.playbackSpeed;
1518
+ r.playNote(c, a.pitch, a.velocity, h);
1519
+ }
1520
+ l++;
1521
+ }
1522
+ this.partNotePointers.set(i, l);
1523
+ }
1524
+ }
1525
+ }
1526
+ }, 50));
1527
+ }
1528
+ /**
1529
+ * Reset note pointers for all parts based on current playback time
1530
+ * @private
1531
+ */
1532
+ _resetNotePointers() {
1533
+ const t = this._currentTime;
1534
+ for (const [e] of this.partChannels) {
1535
+ const s = this.parsedData.parts[e];
1536
+ if (!s || !s.notes) continue;
1537
+ let i = 0;
1538
+ for (; i < s.notes.length && s.notes[i].endTime < t; )
1539
+ i++;
1540
+ this.partNotePointers.set(e, i);
1541
+ }
1542
+ }
1543
+ /**
1544
+ * Reset program change pointers for all parts based on current playback time
1545
+ * @private
1546
+ */
1547
+ _resetProgramPointers() {
1548
+ const t = this._currentTime;
1549
+ for (const [e, s] of this.partChannels) {
1550
+ const i = this.parsedData.parts[e];
1551
+ if (!i || !i.programChanges) {
1552
+ this.partProgramPointers.set(e, 0);
1553
+ continue;
1554
+ }
1555
+ let r = 0, n = i.defaultInstrument;
1556
+ for (; r < i.programChanges.length && i.programChanges[r].time <= t; )
1557
+ n = i.programChanges[r].programNumber, r++;
1558
+ s.setInstrument(n), this.partProgramPointers.set(e, r);
1559
+ }
1560
+ }
1561
+ /**
1562
+ * Stop scheduling and all channel notes
1563
+ * @private
1564
+ */
1565
+ _stopScheduling() {
1566
+ this.scheduleInterval && (clearInterval(this.scheduleInterval), this.scheduleInterval = null), this.partChannels.forEach((t) => {
1567
+ t.isActive() && t.allNotesOff();
1568
+ });
1569
+ }
1570
+ /**
1571
+ * Start the time update loop for firing timeupdate events
1572
+ * @private
1573
+ */
1574
+ _startTimeUpdateLoop() {
1575
+ this.timeUpdateInterval = setInterval(() => {
1576
+ const t = this.getCurrentTime();
1577
+ this._emitEvent("timeupdate", { currentTime: t });
1578
+ const e = this.getBarFromTime(t);
1579
+ e && this._emitEvent("barChanged", e), t >= this._totalDuration && (this.stop(), this._emitEvent("ended", { finalTime: t }));
1580
+ }, 100);
1581
+ }
1582
+ /**
1583
+ * Stop the time update loop
1584
+ * @private
1585
+ */
1586
+ _stopTimeUpdateLoop() {
1587
+ this.timeUpdateInterval && (clearInterval(this.timeUpdateInterval), this.timeUpdateInterval = null);
1588
+ }
1589
+ /**
1590
+ * Emit an event to all registered listeners
1591
+ * @param {string} eventType - Type of event
1592
+ * @param {any} data - Data to pass to listeners
1593
+ * @private
1594
+ */
1595
+ _emitEvent(t, e) {
1596
+ (this.eventBus.all.get(t) || []).forEach((i) => {
1597
+ try {
1598
+ i(e);
1599
+ } catch (r) {
1600
+ console.error(`Error in ${t} event listener:`, r);
1601
+ }
1602
+ });
1603
+ }
1604
+ /**
1605
+ * Generate simple beat mapping from MIDI tempo changes when no structure metadata provided
1606
+ * @returns {Array} Array of beat mapping objects
1607
+ * @private
1608
+ */
1609
+ _generateSimpleBeatMapping() {
1610
+ const t = [], e = this.parsedData.tempoChanges || [], s = 120, i = this.parsedData.barStructure || [], r = i[0] || { sig: [4, 4], bpm: s }, n = r.sig[0], a = 60 / (Array.isArray(r.bpm) ? r.bpm[0] : r.bpm) * n;
1611
+ if (e.length === 0) {
1612
+ const m = Math.max(1, Math.ceil(this._totalDuration / a));
1613
+ let f = 0;
1614
+ for (let u = 1; u <= m; u++) {
1615
+ const d = Math.max(0, u - 1), _ = i[d] || r, g = _.sig[0], y = Array.isArray(_.bpm) ? _.bpm[0] : _.bpm, w = 60 / y;
1616
+ for (let b = 1; b <= g; b++)
1617
+ t.push({
1618
+ bar: u,
1619
+ beat: b,
1620
+ repeat: 0,
1621
+ tempo: y,
1622
+ time: f,
1623
+ timeSig: g
1624
+ }), f += w;
1625
+ }
1626
+ return t;
1627
+ }
1628
+ let c = 0, h = 1;
1629
+ for (let m = 0; m < e.length; m++) {
1630
+ const f = e[m], u = e[m + 1], d = u ? u.time : this._totalDuration, _ = f.tempo || s, g = 60 / _;
1631
+ for (; c < d; ) {
1632
+ for (let y = 1; y <= n && c < d; y++)
1633
+ t.push({
1634
+ bar: h,
1635
+ beat: y,
1636
+ repeat: 0,
1637
+ tempo: _,
1638
+ time: c,
1639
+ timeSig: n
1640
+ }), c += g;
1641
+ h++;
1642
+ }
1643
+ }
1644
+ return t;
1645
+ }
1646
+ /**
1647
+ * Clean up resources and destroy the player
1648
+ */
1649
+ destroy() {
1650
+ this.stop(), this.partChannels.forEach((t) => {
1651
+ t.destroy();
1652
+ }), this.partChannels.clear(), this.partOutputs.forEach((t) => {
1653
+ t.disconnect();
1654
+ }), this.partOutputs.clear(), this.partNotePointers.clear(), this.partProgramPointers.clear(), this.eventBus.all.clear();
1655
+ }
1656
+ }
1657
+ const C = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1658
+ __proto__: null
1659
+ }, Symbol.toStringTag, { value: "Module" }));
1660
+ export {
1661
+ k as AudioEngine,
1662
+ P as AudioEngineUtils,
1663
+ S as BeatMapper,
1664
+ v as ChannelHandle,
1665
+ E as MidiParser,
1666
+ N as MidiPlayer,
1667
+ B as SpessaSynthAudioEngine,
1668
+ x as SpessaSynthChannelHandle
1669
+ };