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,700 @@
1
+ import mitt from 'mitt';
2
+ import BeatMapper from './beat-mapper.js';
3
+
4
+ /**
5
+ * Modern MidiPlayer - Part-centric MIDI player
6
+ *
7
+ * IMPORTANT: This class provides MIDI playback and individual part outputs.
8
+ * For volume/mute/solo controls, you MUST set up external audio routing:
9
+ *
10
+ * @example
11
+ * // 1. Create player
12
+ * const player = new MidiPlayer(engine, instrumentMap, midiData);
13
+ *
14
+ * // 2. Set up external routing for each part (REQUIRED for audio output)
15
+ * const partOutput = player.getPartOutput('soprano');
16
+ * const externalGain = audioContext.createGain();
17
+ * const masterGain = audioContext.createGain();
18
+ *
19
+ * partOutput.connect(externalGain);
20
+ * externalGain.connect(masterGain);
21
+ * masterGain.connect(audioContext.destination);
22
+ *
23
+ * // 3. Control volume via external gain node
24
+ * externalGain.gain.value = 0.5; // 50% volume
25
+ *
26
+ * Uses AudioEngine abstraction for synthesis
27
+ */
28
+ export default class MidiPlayer {
29
+ /**
30
+ * Create a new MidiPlayer instance
31
+ * @param {AudioEngine} audioEngine - Initialized audio engine instance
32
+ * @param {Object} instrumentMap - Mapping of part names to instrument configurations
33
+ * @param {Object} parsedMidiData - Output from MidiParser
34
+ * @param {Object} [structureMetadata] - Optional score structure for beat mapping
35
+ */
36
+ constructor(audioEngine, instrumentMap, parsedMidiData, structureMetadata = null) {
37
+ // Validate required parameters
38
+ if (!audioEngine || !audioEngine.isInitialized) {
39
+ throw new Error('Initialized AudioEngine is required');
40
+ }
41
+ if (!parsedMidiData) {
42
+ throw new Error('Parsed MIDI data is required');
43
+ }
44
+
45
+ // Core dependencies
46
+ this.audioEngine = audioEngine;
47
+ this.instrumentMap = instrumentMap || {};
48
+ this.parsedData = parsedMidiData;
49
+
50
+ // Initialize state
51
+ this._isPlaying = false;
52
+ this._currentTime = 0;
53
+ this._totalDuration = 0;
54
+ this.playbackSpeed = 1.0;
55
+
56
+ // Channel management - part-centric approach
57
+ this.partChannels = new Map(); // partName -> ChannelHandle
58
+ this.partOutputs = new Map(); // partName -> GainNode (for external control)
59
+
60
+ // Scheduling and timing
61
+ this.playbackStartTime = 0;
62
+ this.lookAheadTime = 0.05; // 50ms lookahead for scheduling
63
+ this.scheduleInterval = null;
64
+ this.partNotePointers = new Map(); // partName -> next note index to consider
65
+ this.partProgramPointers = new Map(); // partName -> next program change index to consider
66
+
67
+ // Event handling with mitt
68
+ this.eventBus = mitt();
69
+
70
+ // Beat mapping
71
+ this.beatMapper = new BeatMapper();
72
+ this.beats = []; // Will be populated from structure metadata or fallback
73
+
74
+ // Set up audio routing and calculate duration
75
+ this._setupPartChannels();
76
+ this._calculateTotalDuration();
77
+
78
+ // Initialize note and program change pointers
79
+ this._resetNotePointers();
80
+ this._resetProgramPointers();
81
+
82
+ // Generate beat mapping if structure provided
83
+ if (structureMetadata) {
84
+ this.beats = this.beatMapper.mapBeats(parsedMidiData, structureMetadata);
85
+ } else {
86
+ // Generate simple beat mapping from parsed data tempo changes
87
+ this.beats = this._generateSimpleBeatMapping();
88
+ }
89
+ }
90
+
91
+ // ========================================
92
+ // PUBLIC API METHODS (unchanged interface)
93
+ // ========================================
94
+
95
+ /**
96
+ * Start playback from current position
97
+ */
98
+ play() {
99
+ if (this._isPlaying) return;
100
+
101
+ this._isPlaying = true;
102
+ this.playbackStartTime = this.audioEngine.audioContext.currentTime - (this._currentTime / this.playbackSpeed);
103
+
104
+ // Reset note and program change pointers based on current time
105
+ this._resetNotePointers();
106
+ this._resetProgramPointers();
107
+
108
+ this._schedulePlayback();
109
+ this._startTimeUpdateLoop();
110
+ }
111
+
112
+ /**
113
+ * Start playback at a specific audio context time
114
+ * @param {number} startTime - Audio context time when playback should begin
115
+ */
116
+ playAt(startTime) {
117
+ if (this._isPlaying) return;
118
+
119
+ this._isPlaying = true;
120
+ this.playbackStartTime = startTime - (this._currentTime / this.playbackSpeed);
121
+
122
+ // Reset note and program change pointers based on current time
123
+ this._resetNotePointers();
124
+ this._resetProgramPointers();
125
+
126
+ this._schedulePlayback();
127
+ this._startTimeUpdateLoop();
128
+ }
129
+
130
+ /**
131
+ * Pause playback (resumable)
132
+ */
133
+ pause() {
134
+ if (!this._isPlaying) return;
135
+
136
+ this._isPlaying = false;
137
+ this._stopScheduling();
138
+ this._stopTimeUpdateLoop();
139
+ }
140
+
141
+ /**
142
+ * Stop playback and reset to beginning
143
+ */
144
+ stop() {
145
+ this._isPlaying = false;
146
+ this._currentTime = 0;
147
+ this._stopScheduling();
148
+ this._stopTimeUpdateLoop();
149
+ this._resetNotePointers();
150
+ this._resetProgramPointers();
151
+ }
152
+
153
+ /**
154
+ * Skip to a specific time in seconds
155
+ * @param {number} seconds - Time to skip to
156
+ */
157
+ skipToTime(seconds) {
158
+ seconds = Math.max(0, Math.min(seconds, this._totalDuration));
159
+
160
+ const wasPlaying = this._isPlaying;
161
+ if (wasPlaying) {
162
+ this.pause();
163
+ }
164
+
165
+ this._currentTime = seconds;
166
+ this._resetNotePointers();
167
+ this._resetProgramPointers();
168
+
169
+ if (wasPlaying) {
170
+ this.play();
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Set playback speed
176
+ * @param {number} speed - Speed multiplier (1.0 = normal)
177
+ */
178
+ setPlaybackSpeed(speed) {
179
+ if (speed <= 0) {
180
+ throw new Error('Playback speed must be greater than 0');
181
+ }
182
+
183
+ const wasPlaying = this._isPlaying;
184
+
185
+ if (wasPlaying) {
186
+ this.pause();
187
+ }
188
+
189
+ this.playbackSpeed = speed;
190
+
191
+ if (wasPlaying) {
192
+ this.play();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Navigate to a specific bar position
198
+ * @param {number} barNumber - Bar number (1-based)
199
+ * @param {number} repeat - Repeat section number (0-based)
200
+ */
201
+ setBar(barNumber, repeat = 0) {
202
+ const time = this.getTimeFromBar(barNumber, repeat);
203
+ if (time !== null) {
204
+ this.skipToTime(time);
205
+ this._emitEvent('barChanged', { bar: barNumber, beat: 1, repeat, time });
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Get time position for a specific bar
211
+ * @param {number} barNumber - Bar number (1-based)
212
+ * @param {number} repeat - Repeat section number (0-based)
213
+ * @returns {number|null} Time in seconds or null if not found
214
+ */
215
+ getTimeFromBar(barNumber, repeat = 0) {
216
+ const beat = this.beats.find(b =>
217
+ b.bar === barNumber && b.beat === 1 && b.repeat === repeat
218
+ );
219
+ return beat ? beat.time : null;
220
+ }
221
+
222
+ /**
223
+ * Get bar information for a specific time position
224
+ * @param {number} time - Time in seconds
225
+ * @returns {Object|null} Bar info object or null
226
+ */
227
+ getBarFromTime(time) {
228
+ if (!this.beats.length) return null;
229
+
230
+ let currentBeat = null;
231
+ for (let i = this.beats.length - 1; i >= 0; i--) {
232
+ if (this.beats[i].time <= time) {
233
+ currentBeat = this.beats[i];
234
+ break;
235
+ }
236
+ }
237
+ return currentBeat;
238
+ }
239
+
240
+
241
+ /**
242
+ * Stop all sounds immediately
243
+ */
244
+ allSoundsOff() {
245
+ this.audioEngine.allSoundsOff();
246
+ }
247
+
248
+ /**
249
+ * Get the output node for a specific part for external mixer routing
250
+ *
251
+ * IMPORTANT: You MUST connect this output to external gain/analyzer nodes:
252
+ *
253
+ * @example
254
+ * const partOutput = player.getPartOutput('soprano');
255
+ * const externalGain = audioContext.createGain();
256
+ * const analyzer = audioContext.createAnalyser();
257
+ *
258
+ * partOutput.connect(externalGain);
259
+ * externalGain.connect(analyzer);
260
+ * analyzer.connect(masterGain); // External master gain
261
+ *
262
+ * // Now control volume via external gain:
263
+ * externalGain.gain.value = 0.5; // 50% volume
264
+ *
265
+ * @param {string} partName - Name of the part (e.g., 'soprano', 'alto')
266
+ * @returns {AudioNode|null} Output gain node for this part
267
+ */
268
+ getPartOutput(partName) {
269
+ return this.partOutputs.get(partName) || null;
270
+ }
271
+
272
+ /**
273
+ * Get the channel handle for a specific part (for direct note control)
274
+ * @param {string} partName - Name of the part
275
+ * @returns {ChannelHandle|null} Channel handle for this part
276
+ */
277
+ getPartChannel(partName) {
278
+ return this.partChannels.get(partName) || null;
279
+ }
280
+
281
+
282
+ // ========================================
283
+ // STATE ACCESS METHODS (unchanged)
284
+ // ========================================
285
+
286
+ getCurrentTime() {
287
+ if (this._isPlaying) {
288
+ const elapsed = (this.audioEngine.audioContext.currentTime - this.playbackStartTime) * this.playbackSpeed;
289
+ this._currentTime = Math.min(elapsed, this._totalDuration);
290
+ }
291
+ return this._currentTime;
292
+ }
293
+
294
+ getTotalDuration() {
295
+ return this._totalDuration;
296
+ }
297
+
298
+ isPlaying() {
299
+ return this._isPlaying;
300
+ }
301
+
302
+ // ========================================
303
+ // EVENT HANDLING (mitt-based)
304
+ // ========================================
305
+
306
+ on(event, callback) {
307
+ this.eventBus.on(event, callback);
308
+ }
309
+
310
+ off(event, callback) {
311
+ this.eventBus.off(event, callback);
312
+ }
313
+
314
+ // ========================================
315
+ // PART-CENTRIC IMPLEMENTATION METHODS
316
+ // ========================================
317
+
318
+ /**
319
+ * Set up channel handles for each musical part
320
+ * @private
321
+ */
322
+ _setupPartChannels() {
323
+ Object.keys(this.parsedData.parts).forEach(partName => {
324
+ // Get instrument configuration for this part
325
+ const partData = this.parsedData.parts[partName];
326
+ const instrumentConfig = this.instrumentMap[partName] || {};
327
+
328
+ // Use MIDI program change if available, otherwise fall back to instrumentConfig or default
329
+ const instrument = instrumentConfig.instrument !== undefined
330
+ ? instrumentConfig.instrument
331
+ : (partData.defaultInstrument !== undefined ? partData.defaultInstrument : 0);
332
+
333
+ try {
334
+ // Create channel handle through audio engine
335
+ const channelHandle = this.audioEngine.createChannel(partName, {
336
+ instrument: instrument,
337
+ initialVolume: instrumentConfig.volume || 1.0
338
+ });
339
+
340
+ this.partChannels.set(partName, channelHandle);
341
+
342
+ // Create external gain node for mixer control
343
+ const partGain = this.audioEngine.audioContext.createGain();
344
+ partGain.gain.value = 1.0;
345
+
346
+ // Connect: ChannelHandle -> PartGain
347
+ // Note: The actual audio routing depends on the specific AudioEngine implementation
348
+ // This provides the interface for external volume control, solo/mute, and analysis
349
+ const channelOutput = channelHandle.getOutputNode();
350
+ if (channelOutput) {
351
+ channelOutput.connect(partGain);
352
+ }
353
+
354
+ this.partOutputs.set(partName, partGain);
355
+
356
+ } catch (error) {
357
+ console.error(`Failed to create channel for part '${partName}':`, error);
358
+ this._emitEvent('error', error);
359
+ }
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Calculate total duration from parsed MIDI data
365
+ * @private
366
+ */
367
+ _calculateTotalDuration() {
368
+ let maxEndTime = 0;
369
+
370
+ Object.values(this.parsedData.parts).forEach(part => {
371
+ part.notes.forEach(note => {
372
+ if (note.endTime > maxEndTime) {
373
+ maxEndTime = note.endTime;
374
+ }
375
+ });
376
+ });
377
+
378
+ this._totalDuration = maxEndTime;
379
+ }
380
+
381
+ /**
382
+ * Schedule all notes for playback using simplified approach
383
+ * @private
384
+ */
385
+ _schedulePlayback() {
386
+ // Stop any existing scheduling
387
+ this._stopScheduling();
388
+
389
+ // Start the simplified scheduling loop
390
+ this._startScheduleLoop();
391
+ }
392
+
393
+ /**
394
+ * Start the simplified scheduling loop
395
+ * @private
396
+ */
397
+ _startScheduleLoop() {
398
+ if (this.scheduleInterval) return;
399
+
400
+ this.scheduleInterval = setInterval(() => {
401
+ if (!this._isPlaying) return;
402
+
403
+ const currentTime = this.audioEngine.audioContext.currentTime;
404
+ const playbackTime = (currentTime - this.playbackStartTime) * this.playbackSpeed;
405
+ const scheduleAheadTime = playbackTime + this.lookAheadTime;
406
+
407
+ // Simple scheduling: advance pointers and schedule upcoming events
408
+ for (const [partName, channelHandle] of this.partChannels) {
409
+ const partData = this.parsedData.parts[partName];
410
+ if (!partData) continue;
411
+
412
+ // Handle program changes
413
+ if (partData.programChanges && partData.programChanges.length > 0) {
414
+ let programIndex = this.partProgramPointers.get(partName) || 0;
415
+ const programChanges = partData.programChanges;
416
+
417
+ // Skip program changes that have already passed
418
+ while (programIndex < programChanges.length && programChanges[programIndex].time < playbackTime) {
419
+ programIndex++;
420
+ }
421
+
422
+ // Apply program changes that should happen within the lookahead window
423
+ while (programIndex < programChanges.length && programChanges[programIndex].time <= scheduleAheadTime) {
424
+ const programChange = programChanges[programIndex];
425
+ channelHandle.setInstrument(programChange.programNumber);
426
+ programIndex++;
427
+ }
428
+
429
+ // Update pointer for this part
430
+ this.partProgramPointers.set(partName, programIndex);
431
+ }
432
+
433
+ // Handle notes
434
+ if (partData.notes) {
435
+ let noteIndex = this.partNotePointers.get(partName) || 0;
436
+ const notes = partData.notes;
437
+
438
+ // Skip notes that have already ended
439
+ while (noteIndex < notes.length && notes[noteIndex].endTime < playbackTime) {
440
+ noteIndex++;
441
+ }
442
+
443
+ // Schedule notes that should start within the lookahead window
444
+ while (noteIndex < notes.length && notes[noteIndex].startTime <= scheduleAheadTime) {
445
+ const note = notes[noteIndex];
446
+
447
+ // Skip zero-duration notes
448
+ if (note.endTime - note.startTime >= 0.01) {
449
+ const audioStartTime = this.playbackStartTime + (note.startTime / this.playbackSpeed);
450
+ const audioDuration = (note.endTime - note.startTime) / this.playbackSpeed;
451
+
452
+ channelHandle.playNote(audioStartTime, note.pitch, note.velocity, audioDuration);
453
+ }
454
+
455
+ noteIndex++;
456
+ }
457
+
458
+ // Update pointer for this part
459
+ this.partNotePointers.set(partName, noteIndex);
460
+ }
461
+ }
462
+ }, 50); // Schedule every 50ms - less frequent since we're doing less work
463
+ }
464
+
465
+ /**
466
+ * Reset note pointers for all parts based on current playback time
467
+ * @private
468
+ */
469
+ _resetNotePointers() {
470
+ const currentTime = this._currentTime;
471
+
472
+ for (const [partName] of this.partChannels) {
473
+ const partData = this.parsedData.parts[partName];
474
+ if (!partData || !partData.notes) continue;
475
+
476
+ // Find the first note that hasn't ended yet
477
+ let noteIndex = 0;
478
+ while (noteIndex < partData.notes.length && partData.notes[noteIndex].endTime < currentTime) {
479
+ noteIndex++;
480
+ }
481
+
482
+ this.partNotePointers.set(partName, noteIndex);
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Reset program change pointers for all parts based on current playback time
488
+ * @private
489
+ */
490
+ _resetProgramPointers() {
491
+ const currentTime = this._currentTime;
492
+
493
+ for (const [partName, channelHandle] of this.partChannels) {
494
+ const partData = this.parsedData.parts[partName];
495
+ if (!partData || !partData.programChanges) {
496
+ this.partProgramPointers.set(partName, 0);
497
+ continue;
498
+ }
499
+
500
+ // Find the most recent program change before current time
501
+ let programIndex = 0;
502
+ let lastActiveProgram = partData.defaultInstrument;
503
+
504
+ while (programIndex < partData.programChanges.length &&
505
+ partData.programChanges[programIndex].time <= currentTime) {
506
+ lastActiveProgram = partData.programChanges[programIndex].programNumber;
507
+ programIndex++;
508
+ }
509
+
510
+ // Set the current instrument to the most recent program
511
+ channelHandle.setInstrument(lastActiveProgram);
512
+
513
+ this.partProgramPointers.set(partName, programIndex);
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Stop scheduling and all channel notes
519
+ * @private
520
+ */
521
+ _stopScheduling() {
522
+ // Stop the scheduling loop
523
+ if (this.scheduleInterval) {
524
+ clearInterval(this.scheduleInterval);
525
+ this.scheduleInterval = null;
526
+ }
527
+
528
+ // Stop all notes on all channels (channels handle their own cleanup)
529
+ this.partChannels.forEach(channelHandle => {
530
+ if (channelHandle.isActive()) {
531
+ channelHandle.allNotesOff();
532
+ }
533
+ });
534
+ }
535
+
536
+ /**
537
+ * Start the time update loop for firing timeupdate events
538
+ * @private
539
+ */
540
+ _startTimeUpdateLoop() {
541
+ this.timeUpdateInterval = setInterval(() => {
542
+ const currentTime = this.getCurrentTime();
543
+
544
+ // Emit timeupdate event
545
+ this._emitEvent('timeupdate', { currentTime });
546
+
547
+ // Emit barChanged event if beat mapping is available
548
+ const barInfo = this.getBarFromTime(currentTime);
549
+ if (barInfo) {
550
+ this._emitEvent('barChanged', barInfo);
551
+ }
552
+
553
+ // Check for end of playback
554
+ if (currentTime >= this._totalDuration) {
555
+ this.stop();
556
+ this._emitEvent('ended', { finalTime: currentTime });
557
+ }
558
+ }, 100); // Update every 100ms
559
+ }
560
+
561
+ /**
562
+ * Stop the time update loop
563
+ * @private
564
+ */
565
+ _stopTimeUpdateLoop() {
566
+ if (this.timeUpdateInterval) {
567
+ clearInterval(this.timeUpdateInterval);
568
+ this.timeUpdateInterval = null;
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Emit an event to all registered listeners
574
+ * @param {string} eventType - Type of event
575
+ * @param {any} data - Data to pass to listeners
576
+ * @private
577
+ */
578
+ _emitEvent(eventType, data) {
579
+ // Get all listeners for this event type and call them individually
580
+ // to ensure one failing listener doesn't prevent others from being called
581
+ const listeners = this.eventBus.all.get(eventType) || [];
582
+ listeners.forEach(listener => {
583
+ try {
584
+ listener(data);
585
+ } catch (error) {
586
+ console.error(`Error in ${eventType} event listener:`, error);
587
+ }
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Generate simple beat mapping from MIDI tempo changes when no structure metadata provided
593
+ * @returns {Array} Array of beat mapping objects
594
+ * @private
595
+ */
596
+ _generateSimpleBeatMapping() {
597
+ // Generate basic beat mapping from MIDI tempo changes
598
+ const beats = [];
599
+ const tempoChanges = this.parsedData.tempoChanges || [];
600
+ const defaultTempo = 120;
601
+
602
+ // Use actual time signature from barStructure instead of hardcoding 4
603
+ const barStructure = this.parsedData.barStructure || [];
604
+ const firstBarData = barStructure[0] || { sig: [4, 4], bpm: defaultTempo };
605
+ const beatsPerBar = firstBarData.sig[0];
606
+ const tempo = Array.isArray(firstBarData.bpm) ? firstBarData.bpm[0] : firstBarData.bpm;
607
+ const secondsPerBeat = 60 / tempo;
608
+ const secondsPerBar = secondsPerBeat * beatsPerBar;
609
+
610
+ if (tempoChanges.length === 0) {
611
+ // No tempo data, create mapping based on total duration and barStructure
612
+ const numBars = Math.max(1, Math.ceil(this._totalDuration / secondsPerBar));
613
+ let currentTime = 0;
614
+
615
+ for (let bar = 1; bar <= numBars; bar++) {
616
+ // Get time signature for this specific bar (not just the first bar)
617
+ const barIndex = Math.max(0, bar - 1);
618
+ const currentBarData = barStructure[barIndex] || firstBarData;
619
+ const currentBeatsPerBar = currentBarData.sig[0];
620
+ const currentTempo = Array.isArray(currentBarData.bpm) ? currentBarData.bpm[0] : currentBarData.bpm;
621
+ const currentSecondsPerBeat = 60 / currentTempo;
622
+
623
+ for (let beat = 1; beat <= currentBeatsPerBar; beat++) {
624
+ beats.push({
625
+ bar,
626
+ beat,
627
+ repeat: 0,
628
+ tempo: currentTempo,
629
+ time: currentTime,
630
+ timeSig: currentBeatsPerBar
631
+ });
632
+
633
+ currentTime += currentSecondsPerBeat;
634
+ }
635
+ }
636
+
637
+ return beats;
638
+ }
639
+
640
+ // Convert tempo changes to beat array with all beats
641
+ // This is a simplified implementation - full beat mapping requires BeatMapper
642
+ let currentTime = 0;
643
+ let currentBar = 1;
644
+
645
+ for (let i = 0; i < tempoChanges.length; i++) {
646
+ const change = tempoChanges[i];
647
+ const nextChange = tempoChanges[i + 1];
648
+ const endTime = nextChange ? nextChange.time : this._totalDuration;
649
+ const currentBpm = change.tempo || defaultTempo;
650
+ const currentSecondsPerBeat = 60 / currentBpm;
651
+
652
+ // Generate beats from this tempo change to the next
653
+ while (currentTime < endTime) {
654
+ for (let beat = 1; beat <= beatsPerBar && currentTime < endTime; beat++) {
655
+ beats.push({
656
+ bar: currentBar,
657
+ beat,
658
+ repeat: 0,
659
+ tempo: currentBpm,
660
+ time: currentTime,
661
+ timeSig: beatsPerBar
662
+ });
663
+
664
+ currentTime += currentSecondsPerBeat;
665
+ }
666
+ currentBar++;
667
+ }
668
+ }
669
+
670
+ return beats;
671
+ }
672
+
673
+ /**
674
+ * Clean up resources and destroy the player
675
+ */
676
+ destroy() {
677
+ this.stop();
678
+
679
+ // Destroy all channel handles
680
+ this.partChannels.forEach(channelHandle => {
681
+ channelHandle.destroy();
682
+ });
683
+ this.partChannels.clear();
684
+
685
+ // Disconnect part outputs
686
+ this.partOutputs.forEach(gainNode => {
687
+ gainNode.disconnect();
688
+ });
689
+ this.partOutputs.clear();
690
+
691
+ // Clear note and program change pointers
692
+ this.partNotePointers.clear();
693
+ this.partProgramPointers.clear();
694
+
695
+ // Clear event bus
696
+ this.eventBus.all.clear();
697
+
698
+ // Note: We don't destroy the audioEngine as it might be shared
699
+ }
700
+ }