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.
package/README.md ADDED
@@ -0,0 +1,816 @@
1
+ # Audio Mixer Engine
2
+
3
+ A part-centric JavaScript audio library designed for audio mixer applications and mixer interfaces. Provides individual audio outputs per musical part, enabling advanced mixing workflows with per-part gain control, level monitoring, and analysis capabilities.
4
+
5
+ ## Architecture Overview
6
+
7
+ This library follows a **part-centric mixer design** where each musical part (soprano, alto, tenor, bass, piano, etc.) gets its own:
8
+ - Individual audio output node (`AudioNode`)
9
+ - Independent volume and instrument control
10
+ - Dedicated channel handle for note playback
11
+ - Separate audio analysis capabilities
12
+
13
+ **Key Design Philosophy**: Rather than mixing audio internally, the library provides individual part outputs that external mixer code can connect to gain controls, analyzers, effects, and routing - enabling sophisticated mixing interfaces.
14
+
15
+ ## Features
16
+
17
+ - **Part-Centric Architecture**: Each musical part gets independent control and output
18
+ - **Individual Audio Outputs**: Access `AudioNode` outputs for each part for external mixing
19
+ - **Dual Volume Control**: Internal MIDI volume + external gain node control
20
+ - **Real-time Analysis**: Connect part outputs to analyzers for level metering and visualization
21
+ - **MIDI Processing**: Parse and analyze MIDI files with part identification
22
+ - **Audio Synthesis**: SpessaSynth-based engine with soundfont support
23
+ - **Musical Navigation**: Beat mapping with bar-based navigation and real-time position tracking
24
+ - **Advanced Playback**: Metronome, lead-in, and configurable startup timing for perfect synchronization
25
+ - **Event-Driven**: Modern mitt-based event system with structured event data
26
+ - **Master Volume Control**: Global volume management with emergency stop functionality
27
+ - **Interface Compliance**: Full IAudioMixerEngine interface for seamless mixer integration
28
+ - **Mixer Integration**: Designed for solo/mute, gain control, and routing workflows
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install audio-mixer-engine
34
+ ```
35
+
36
+ ## Quick Start - Basic Playback
37
+
38
+ ```javascript
39
+ import {
40
+ SpessaSynthAudioEngine,
41
+ MidiParser,
42
+ MidiPlayer
43
+ } from 'audio-mixer-engine';
44
+
45
+ // Initialize audio context and engine
46
+ const audioContext = new AudioContext();
47
+ const audioEngine = new SpessaSynthAudioEngine(audioContext);
48
+
49
+ // Load soundfont and parse MIDI
50
+ await audioEngine.initialize('/path/to/soundfont.sf2');
51
+ const parser = new MidiParser();
52
+ const midiData = await parser.parse(midiArrayBuffer);
53
+
54
+ // Create player with optional instrument mapping
55
+ // Note: MIDI program changes from files are used by default
56
+ // instrumentMap only needed to override or for parts without program changes
57
+ const instrumentMap = {
58
+ soprano: { instrument: 'choir_aahs', volume: 1.0 }, // Overrides MIDI program
59
+ alto: { instrument: 'choir_aahs', volume: 1.0 }, // Overrides MIDI program
60
+ piano: { instrument: 'piano', volume: 0.8 } // Overrides MIDI program
61
+ };
62
+
63
+ // Optional structure metadata for advanced beat mapping
64
+ const structureMetadata = {
65
+ sections: [{ startBar: 1, endBar: 8 }],
66
+ order: [{ section: 0 }]
67
+ };
68
+
69
+ const player = new MidiPlayer(audioEngine, instrumentMap, midiData, structureMetadata);
70
+
71
+ // Set up event listeners
72
+ player.on('timeupdate', ({ currentTime }) => {
73
+ console.log(`Playing: ${currentTime.toFixed(2)}s`);
74
+ });
75
+
76
+ player.on('barChanged', ({ bar, beat, time }) => {
77
+ console.log(`Bar ${bar}, Beat ${beat} at ${time.toFixed(2)}s`);
78
+ });
79
+
80
+ player.play();
81
+ ```
82
+
83
+ ## Mixer Integration Pattern
84
+
85
+ ```javascript
86
+ // Create parts manually for mixing interface
87
+ const sopranoChannel = audioEngine.createChannel('soprano', {
88
+ instrument: 'choir_aahs',
89
+ initialVolume: 1.0
90
+ });
91
+
92
+ // Get individual output node for external mixing
93
+ const sopranoOutput = sopranoChannel.getOutputNode();
94
+
95
+ // Set up mixer chain: Part Output -> External Gain -> Analyzer -> Master
96
+ const sopranoGain = audioContext.createGain();
97
+ const sopranoAnalyzer = audioContext.createAnalyser();
98
+
99
+ sopranoOutput.connect(sopranoGain);
100
+ sopranoGain.connect(sopranoAnalyzer);
101
+ sopranoAnalyzer.connect(masterGain); // External master gain
102
+
103
+ // External mixer controls
104
+ sopranoGain.gain.value = 0.5; // 50% volume
105
+ sopranoAnalyzer.getByteFrequencyData(dataArray); // Level metering
106
+
107
+ // Play notes on individual parts
108
+ sopranoChannel.noteOn(60, 100); // C4 at velocity 100
109
+ sopranoChannel.noteOff(60);
110
+ ```
111
+
112
+ ## Audio Routing Architecture
113
+
114
+ The library implements a sophisticated audio routing system designed for mixer applications:
115
+
116
+ ```
117
+ MIDI File → Parser → Player → AudioEngine → Individual Part Outputs
118
+
119
+ External Mixer Interface: Part Gain → Analyzer → Solo/Mute → Master → Destination
120
+ ```
121
+
122
+ ### Key Audio Routing Concepts
123
+
124
+ 1. **Individual Part Outputs**: Each musical part provides an `AudioNode` output via `channelHandle.getOutputNode()`
125
+ 2. **External Volume Control**: Mixer interfaces control gain via external `GainNode` instances connected to part outputs
126
+ 3. **Internal vs External Volume**:
127
+ - Internal: MIDI volume controlled via `channelHandle.setVolume()` (affects synthesis)
128
+ - External: Mixer volume controlled via external gain nodes (affects final output)
129
+ 4. **Analysis Integration**: Connect part outputs to `AnalyserNode` for real-time level metering and visualization
130
+
131
+ ## API Reference
132
+
133
+ ### ChannelHandle Methods
134
+
135
+ ```javascript
136
+ // Get output node for mixer connection
137
+ const outputNode = channelHandle.getOutputNode();
138
+
139
+ // Note control
140
+ channelHandle.noteOn(pitch, velocity);
141
+ channelHandle.noteOff(pitch);
142
+ channelHandle.allNotesOff();
143
+
144
+ // Scheduled note control
145
+ const eventId = channelHandle.playNote(startTime, pitch, velocity, duration);
146
+
147
+ // Internal synthesis controls
148
+ await channelHandle.setInstrument('choir_aahs');
149
+ channelHandle.setVolume(0.8); // Internal MIDI volume
150
+
151
+ // Channel info
152
+ const partId = channelHandle.getPartId();
153
+ const isActive = channelHandle.isActive();
154
+ ```
155
+
156
+ ### MidiPlayer Methods
157
+
158
+ ```javascript
159
+ // Transport controls
160
+ player.play();
161
+ player.pause();
162
+ player.stop();
163
+ player.skipToTime(30.5);
164
+ player.setPlaybackSpeed(1.5);
165
+
166
+ // Musical navigation
167
+ player.setBar(4, 0); // Jump to bar 4, repeat 0
168
+ const time = player.getTimeFromBar(2); // Get time for bar 2
169
+ const barInfo = player.getBarFromTime(15.3); // Get bar info at time
170
+
171
+ // Emergency stop
172
+ player.allSoundsOff();
173
+
174
+ // Part access for mixer integration
175
+ const sopranoOutput = player.getPartOutput('soprano');
176
+ const sopranoChannel = player.getPartChannel('soprano');
177
+
178
+ // REQUIRED: Set up external mixer routing for all parts
179
+ const masterGain = audioContext.createGain();
180
+ masterGain.connect(audioContext.destination);
181
+
182
+ const sopranoGain = audioContext.createGain();
183
+ const sopranoAnalyzer = audioContext.createAnalyser();
184
+
185
+ sopranoOutput.connect(sopranoGain);
186
+ sopranoGain.connect(sopranoAnalyzer);
187
+ sopranoAnalyzer.connect(masterGain); // External master gain
188
+
189
+ // Control volume/mute/solo via external gain nodes
190
+ sopranoGain.gain.value = 0.5; // 50% volume
191
+ sopranoGain.gain.value = 0; // Mute
192
+ masterGain.gain.value = 0.8; // Master volume
193
+
194
+ // Solo functionality: mute all other parts' external gain nodes
195
+ // (Implementation depends on your specific mixer setup)
196
+
197
+ // Level monitoring
198
+ sopranoAnalyzer.getByteFrequencyData(dataArray);
199
+
200
+ // Event handling
201
+ player.on('timeupdate', ({ currentTime }) => { /* */ });
202
+ player.on('ended', ({ finalTime }) => { /* */ });
203
+ player.on('barChanged', ({ bar, beat, repeat, time }) => { /* */ });
204
+ ```
205
+
206
+ ### PlaybackManager Methods
207
+
208
+ ```javascript
209
+ // Advanced playback with metronome, lead-in, and startup timing
210
+ const playbackManager = new PlaybackManager(midiPlayer, {
211
+ metronome: { enabled: true, volume: 0.7 },
212
+ leadIn: { enabled: true, bars: 2 },
213
+ startup: { delayMs: 25 }
214
+ });
215
+
216
+ // Transport controls (wraps MidiPlayer)
217
+ await playbackManager.play({ leadIn: true, metronome: true });
218
+ playbackManager.pause();
219
+ playbackManager.resume();
220
+ playbackManager.stop();
221
+
222
+ // Metronome configuration
223
+ playbackManager.setMetronomeEnabled(true);
224
+ playbackManager.setMetronomeSettings({ volume: 0.8, tickInstrument: 115 });
225
+ const metronomeSettings = playbackManager.getMetronomeSettings();
226
+
227
+ // Lead-in configuration
228
+ playbackManager.setLeadInEnabled(true);
229
+ playbackManager.setLeadInBars(3);
230
+ const leadInSettings = playbackManager.getLeadInSettings();
231
+
232
+ // Startup timing configuration
233
+ playbackManager.setStartupDelay(50); // 50ms delay
234
+ const startupSettings = playbackManager.getStartupSettings();
235
+
236
+ // State and timing
237
+ const state = playbackManager.getState(); // 'stopped', 'lead-in', 'playing', 'paused'
238
+ const isPlaying = playbackManager.isPlaying();
239
+ const currentTime = playbackManager.getCurrentTime();
240
+
241
+ // Enhanced event handling
242
+ playbackManager.on('leadInStarted', ({ bars, totalBeats, startupDelayMs }) => {});
243
+ playbackManager.on('leadInEnded', () => {});
244
+ playbackManager.on('beatChanged', ({ bar, beat, isLeadIn, time }) => {});
245
+ playbackManager.on('startupSettingsChanged', ({ delayMs }) => {});
246
+ ```
247
+
248
+ ### Audio Engine Management
249
+
250
+ ```javascript
251
+ // Engine lifecycle
252
+ await audioEngine.initialize('/path/to/soundfont.sf2');
253
+ const isReady = audioEngine.isInitialized;
254
+
255
+ // Engine controls
256
+ audioEngine.setMasterVolume(0.7);
257
+ const masterVol = audioEngine.getMasterVolume();
258
+ audioEngine.allSoundsOff();
259
+
260
+ // Channel creation
261
+ const channelHandle = audioEngine.createChannel('partId', {
262
+ instrument: 'piano', // Can be string name or MIDI program number (0-127)
263
+ initialVolume: 1.0
264
+ });
265
+
266
+ // Engine lifecycle
267
+ const activeChannels = audioEngine.getActiveChannels();
268
+ audioEngine.destroy(); // Cleanup all resources
269
+ ```
270
+
271
+ ## Mixer-Oriented Design vs Traditional Audio Libraries
272
+
273
+ Unlike traditional audio libraries that output mixed audio directly to speakers, this library is designed specifically for **mixer interfaces** and **interactive practice applications**:
274
+
275
+ ### Traditional Audio Library Pattern:
276
+ ```javascript
277
+ // Traditional libraries mix everything internally
278
+ player.setVolume(0.5); // Global volume only
279
+ player.play(); // Mixed audio to speakers
280
+ // ❌ No individual part control
281
+ // ❌ No real-time level analysis per part
282
+ // ❌ No external routing flexibility
283
+ ```
284
+
285
+ ### Audio Mixer Engine Pattern:
286
+ ```javascript
287
+ // Part-centric with individual outputs for mixer control
288
+ const sopranoChannel = engine.createChannel('soprano');
289
+ const sopranoOutput = sopranoChannel.getOutputNode(); // ← Key difference!
290
+
291
+ // External mixer has full control
292
+ sopranoOutput.connect(mixerGainNode);
293
+ sopranoOutput.connect(levelAnalyzer);
294
+ // ✅ Individual part control
295
+ // ✅ Real-time analysis per part
296
+ // ✅ Full routing flexibility
297
+ ```
298
+
299
+ ## Complete Mixer Example
300
+
301
+ ```javascript
302
+ import { SpessaSynthAudioEngine } from 'audio-mixer-engine';
303
+
304
+ class AudioMixer {
305
+ constructor(audioContext) {
306
+ this.audioContext = audioContext;
307
+ this.audioEngine = new SpessaSynthAudioEngine(audioContext);
308
+ this.parts = new Map(); // partId -> { channel, gain, analyzer, muted, soloed }
309
+ this.masterGain = audioContext.createGain();
310
+ this.masterGain.connect(audioContext.destination);
311
+ }
312
+
313
+ async initialize(soundfontPath) {
314
+ await this.audioEngine.initialize(soundfontPath);
315
+ }
316
+
317
+ addPart(partId, instrument = 'piano') {
318
+ // Create channel for this part
319
+ const channel = this.audioEngine.createChannel(partId, { instrument });
320
+
321
+ // Set up mixer chain for this part
322
+ const gain = this.audioContext.createGain();
323
+ const analyzer = this.audioContext.createAnalyser();
324
+ analyzer.fftSize = 256;
325
+
326
+ // Audio routing: Part → Gain → Analyzer → Master → Destination
327
+ const partOutput = channel.getOutputNode();
328
+ partOutput.connect(gain);
329
+ gain.connect(analyzer);
330
+ analyzer.connect(this.masterGain);
331
+
332
+ // Track part in mixer
333
+ this.parts.set(partId, {
334
+ channel,
335
+ gain,
336
+ analyzer,
337
+ muted: false,
338
+ soloed: false
339
+ });
340
+
341
+ return { channel, gain, analyzer };
342
+ }
343
+
344
+ // Mixer controls
345
+ setPartVolume(partId, volume) {
346
+ const part = this.parts.get(partId);
347
+ if (part) part.gain.gain.value = volume;
348
+ }
349
+
350
+ mutePart(partId) {
351
+ const part = this.parts.get(partId);
352
+ if (part) {
353
+ part.muted = !part.muted;
354
+ part.gain.gain.value = part.muted ? 0 : 1;
355
+ }
356
+ }
357
+
358
+ soloPart(partId) {
359
+ // Toggle solo state
360
+ const part = this.parts.get(partId);
361
+ if (!part) return;
362
+
363
+ part.soloed = !part.soloed;
364
+
365
+ // Apply solo logic: if any parts are soloed, mute non-soloed parts
366
+ const hasAnysoloed = Array.from(this.parts.values()).some(p => p.soloed);
367
+
368
+ this.parts.forEach((partData, id) => {
369
+ if (hasAnysoloed) {
370
+ partData.gain.gain.value = partData.soloed ? 1 : 0;
371
+ } else {
372
+ partData.gain.gain.value = partData.muted ? 0 : 1;
373
+ }
374
+ });
375
+ }
376
+
377
+ // Real-time level monitoring
378
+ getPartLevel(partId) {
379
+ const part = this.parts.get(partId);
380
+ if (!part) return 0;
381
+
382
+ const bufferLength = part.analyzer.frequencyBinCount;
383
+ const dataArray = new Uint8Array(bufferLength);
384
+ part.analyzer.getByteFrequencyData(dataArray);
385
+
386
+ // Calculate RMS level
387
+ let sum = 0;
388
+ for (let i = 0; i < bufferLength; i++) {
389
+ sum += dataArray[i] * dataArray[i];
390
+ }
391
+ return Math.sqrt(sum / bufferLength) / 255; // Normalized 0-1
392
+ }
393
+
394
+ // Play notes on specific parts
395
+ playNote(partId, pitch, velocity = 100) {
396
+ const part = this.parts.get(partId);
397
+ if (part) {
398
+ part.channel.noteOn(pitch, velocity);
399
+ }
400
+ }
401
+
402
+ stopNote(partId, pitch) {
403
+ const part = this.parts.get(partId);
404
+ if (part) {
405
+ part.channel.noteOff(pitch);
406
+ }
407
+ }
408
+ }
409
+
410
+ // Usage
411
+ const mixer = new AudioMixer(audioContext);
412
+ await mixer.initialize('soundfont.sf2');
413
+
414
+ // Add parts to mixer
415
+ mixer.addPart('soprano', 'choir_aahs');
416
+ mixer.addPart('piano', 'piano');
417
+
418
+ // Mixer controls
419
+ mixer.setPartVolume('soprano', 0.7);
420
+ mixer.mutePart('piano');
421
+ mixer.soloPart('soprano');
422
+
423
+ // Play and monitor
424
+ mixer.playNote('soprano', 60, 100); // C4 at velocity 100
425
+ const level = mixer.getPartLevel('soprano'); // Get real-time level
426
+ ```
427
+
428
+ ## Common Mixer Patterns
429
+
430
+ ### Pattern 1: Practice App with Part Isolation
431
+ ```javascript
432
+ // Enable users to practice by isolating their part
433
+ const practiceApp = {
434
+ async setupPractice(userPart, otherParts) {
435
+ // Create channels for all parts
436
+ const channels = {};
437
+ for (const part of [userPart, ...otherParts]) {
438
+ channels[part] = await this.createPartWithMixer(part);
439
+ }
440
+
441
+ // Set up practice mode: user part at full volume, others quieter
442
+ channels[userPart].gain.gain.value = 1.0;
443
+ otherParts.forEach(part => {
444
+ channels[part].gain.gain.value = 0.3; // Background level
445
+ });
446
+ }
447
+ };
448
+ ```
449
+
450
+ ### Pattern 2: Live Mixer Interface
451
+ ```javascript
452
+ // Real-time mixer with visual feedback
453
+ class LiveMixer {
454
+ updateLevels() {
455
+ // Update level meters for all parts in real-time
456
+ this.parts.forEach((partData, partId) => {
457
+ const level = this.getPartLevel(partId);
458
+ this.updateLevelMeter(partId, level);
459
+
460
+ // Highlight active parts
461
+ const isActive = level > 0.02;
462
+ this.highlightPart(partId, isActive);
463
+ });
464
+ }
465
+
466
+ // Mixer UI controls map directly to audio nodes
467
+ onVolumeSlider(partId, value) {
468
+ this.parts.get(partId).gain.gain.value = value;
469
+ }
470
+
471
+ onMuteButton(partId) {
472
+ const part = this.parts.get(partId);
473
+ part.muted = !part.muted;
474
+ part.gain.gain.value = part.muted ? 0 : 1;
475
+ }
476
+ }
477
+ ```
478
+
479
+ ### Pattern 3: Recording/Export with Part Separation
480
+ ```javascript
481
+ // Record individual parts or create stems
482
+ class PartRecorder {
483
+ async recordPart(partId, duration) {
484
+ const part = this.parts.get(partId);
485
+
486
+ // Connect part to MediaRecorder via MediaStreamDestination
487
+ const dest = this.audioContext.createMediaStreamDestination();
488
+ part.gain.connect(dest);
489
+
490
+ const recorder = new MediaRecorder(dest.stream);
491
+ // Record just this part's audio...
492
+ }
493
+ }
494
+ ```
495
+
496
+ ## Advanced Usage Examples
497
+
498
+ ### PlaybackManager with Metronome and Lead-in
499
+
500
+ ```javascript
501
+ import { PlaybackManager } from 'audio-mixer-engine';
502
+
503
+ // Wrap MidiPlayer with PlaybackManager for advanced features
504
+ const playbackManager = new PlaybackManager(player, {
505
+ metronome: {
506
+ enabled: true,
507
+ tickInstrument: 115, // Woodblock for regular beats
508
+ accentInstrument: 116, // Taiko drum for downbeats
509
+ volume: 0.7
510
+ },
511
+ leadIn: {
512
+ enabled: true,
513
+ bars: 2 // 2-bar lead-in
514
+ },
515
+ startup: {
516
+ delayMs: 25 // 25ms startup delay for better timing
517
+ }
518
+ });
519
+
520
+ // Enhanced event handling
521
+ playbackManager.on('leadInStarted', ({ bars, totalBeats, startupDelayMs }) => {
522
+ console.log(`Lead-in: ${bars} bars (${totalBeats} beats), ${startupDelayMs}ms startup delay`);
523
+ });
524
+
525
+ playbackManager.on('beatChanged', ({ bar, beat, isLeadIn }) => {
526
+ if (isLeadIn) {
527
+ console.log(`Lead-in: Bar ${bar}, Beat ${beat}`);
528
+ } else {
529
+ console.log(`Playing: Bar ${bar}, Beat ${beat}`);
530
+ }
531
+ });
532
+
533
+ // Runtime configuration
534
+ playbackManager.setStartupDelay(50); // Adjust startup delay
535
+ playbackManager.setMetronomeEnabled(true); // Toggle metronome
536
+ playbackManager.setLeadInBars(1); // Change lead-in length
537
+
538
+ // Start with features
539
+ await playbackManager.play({ leadIn: true, metronome: true });
540
+ ```
541
+
542
+ ### Startup Timing Control
543
+
544
+ The PlaybackManager includes configurable startup delays to ensure perfect timing synchronization between metronome ticks and initial notes:
545
+
546
+ ```javascript
547
+ // Configure startup delay during construction
548
+ const playbackManager = new PlaybackManager(player, {
549
+ startup: {
550
+ delayMs: 25 // Default: 25ms delay before audio starts
551
+ }
552
+ });
553
+
554
+ // Runtime adjustment of startup delay
555
+ playbackManager.setStartupDelay(50); // 50ms delay
556
+
557
+ // Get current settings
558
+ const settings = playbackManager.getStartupSettings();
559
+ console.log(`Current delay: ${settings.delayMs}ms`);
560
+
561
+ // Listen for configuration changes
562
+ playbackManager.on('startupSettingsChanged', ({ delayMs }) => {
563
+ console.log(`Startup delay changed to ${delayMs}ms`);
564
+ });
565
+ ```
566
+
567
+ **Why Startup Delays Matter:**
568
+ - **Timing Synchronization**: Ensures metronome ticks and first notes start together
569
+ - **Scheduling Setup**: Allows audio context and synthesis engines time to prepare
570
+ - **Reduced Audio Glitches**: Prevents initial audio stuttering on some systems
571
+ - **Consistent Timing**: Eliminates variable delays in first few notes
572
+
573
+ **Recommended Values:**
574
+ - `0ms` - No delay (fastest start, may have timing issues)
575
+ - `25ms` - Default (good balance of speed and timing)
576
+ - `50ms` - Conservative (ensures reliable timing on slower systems)
577
+ - `100ms` - High latency systems (older devices or complex audio setups)
578
+
579
+ **Behavior:**
580
+ - The `play()` method returns immediately and state changes to 'playing'
581
+ - Events are emitted immediately (playbackStarted, leadInStarted)
582
+ - Actual audio output begins after the specified delay
583
+ - Works with both direct playback and lead-in modes
584
+
585
+ ### Musical Navigation
586
+
587
+ ```javascript
588
+ // Set up event listener for bar changes
589
+ player.on('barChanged', ({ bar, beat, repeat, time }) => {
590
+ console.log(`Now at Bar ${bar}, Beat ${beat} (${time.toFixed(2)}s)`);
591
+ updateScoreHighlight(bar);
592
+ });
593
+
594
+ // Navigate to specific bars
595
+ player.setBar(5); // Jump to bar 5
596
+ player.setBar(3, 1); // Jump to bar 3, repeat 1
597
+
598
+ // Time/bar conversion
599
+ const bar8Time = player.getTimeFromBar(8); // Get time for bar 8
600
+ const currentBar = player.getBarFromTime(player.getCurrentTime());
601
+ ```
602
+
603
+ ### Event-Driven Mixer Integration
604
+
605
+ ```javascript
606
+ // Set up comprehensive event handling
607
+ player.on('timeupdate', ({ currentTime }) => {
608
+ updateProgressBar(currentTime / player.getTotalDuration());
609
+ });
610
+
611
+ player.on('ended', ({ finalTime }) => {
612
+ console.log(`Playback completed at ${finalTime}s`);
613
+ showPlaybackComplete();
614
+ });
615
+
616
+ player.on('masterVolumeChanged', ({ volume }) => {
617
+ updateMasterVolumeSlider(volume);
618
+ });
619
+
620
+ // Master volume control with events
621
+ player.setMasterVolume(0.8); // Triggers masterVolumeChanged event
622
+ ```
623
+
624
+ ### Structure Metadata Integration
625
+
626
+ ```javascript
627
+ // Advanced beat mapping with song structure
628
+ const structureMetadata = {
629
+ sections: [
630
+ {startBar: 1, endBar: 8, name: "Verse"},
631
+ {startBar: 9, endBar: 16, name: "Chorus"},
632
+ {startBar: 17, endBar: 24, name: "Bridge"}
633
+ ],
634
+ order: [
635
+ {section: 0}, // Verse
636
+ {section: 1, repeat: 2}, // Chorus x2
637
+ {section: 2}, // Bridge
638
+ {section: 1} // Chorus
639
+ ]
640
+ };
641
+
642
+ const player = new MidiPlayer(audioEngine, instrumentMap, parsedData, structureMetadata);
643
+
644
+ // Navigate by song sections
645
+ player.setBar(9); // Jump to Chorus
646
+ player.setBar(1, 1); // Jump to Verse, second time through
647
+ ```
648
+
649
+ ## Use Cases
650
+
651
+ This library is specifically designed for:
652
+
653
+ - **Choir Practice Apps**: Individual part control for learning and practice
654
+ - **Music Education Software**: Part isolation and analysis for teaching
655
+ - **Live Performance Tools**: Real-time mixing during rehearsals or performances
656
+ - **Audio Workstations**: Part-based stems and multi-track recording
657
+ - **Interactive Score Viewers**: Dynamic part highlighting and playback control
658
+ - **Rehearsal Tools**: Director interfaces with part-specific controls
659
+
660
+ **Not intended for**: Simple audio playback, games, or applications that don't need per-part control.
661
+
662
+ ## Updated API (v0.1.0)
663
+
664
+ ### New Event System
665
+ All events now use structured data objects:
666
+ ```javascript
667
+ // Old format (deprecated)
668
+ player.on('timeupdate', (currentTime) => { /* */ });
669
+
670
+ // New format (current)
671
+ player.on('timeupdate', ({ currentTime }) => { /* */ });
672
+ player.on('ended', ({ finalTime }) => { /* */ });
673
+ player.on('barChanged', ({ bar, beat, repeat, time }) => { /* */ });
674
+ ```
675
+
676
+ ### Enhanced Constructor
677
+
678
+ ```javascript
679
+ // Basic usage (backward compatible)
680
+ new MidiPlayer(audioEngine, instrumentMap, parsedData);
681
+
682
+ // With structure metadata for advanced navigation
683
+ new MidiPlayer(audioEngine, instrumentMap, parsedData, structureMetadata);
684
+ ```
685
+
686
+ ### New Methods
687
+ ```javascript
688
+ // Musical navigation
689
+ player.setBar(barNumber, repeat);
690
+ player.getTimeFromBar(barNumber, repeat);
691
+ player.getBarFromTime(timeInSeconds);
692
+
693
+ // Master volume control
694
+ player.setMasterVolume(0.7);
695
+ player.getMasterVolume();
696
+ player.allSoundsOff();
697
+ ```
698
+
699
+ ## Interface Contract
700
+
701
+ This library implements a specific interface contract for integration with UI layers. See [INTERFACE.md](./INTERFACE.md) for the complete specification of methods, events, and data structures that external coordination layers can depend on.
702
+
703
+ ## Core Components
704
+
705
+ ### AudioEngine (Abstract Base)
706
+ Defines the contract for part-centric audio synthesis:
707
+ - **Part-based channel creation**: Each musical part gets its own `ChannelHandle`
708
+ - **Individual output routing**: Provides separate `AudioNode` outputs per part
709
+ - **Master volume control**: Global volume affecting all parts
710
+ - **Resource management**: Clean lifecycle management for channels and audio nodes
711
+
712
+ ### SpessaSynthAudioEngine
713
+ SpessaSynth-based implementation with mixer-focused features:
714
+ - **Individual MIDI channel outputs**: Creates separate audio outputs for each of 16 MIDI channels
715
+ - **External routing support**: Part outputs designed for connection to external gain/analysis nodes
716
+ - **Soundfont-based synthesis**: High-quality GM-compatible audio synthesis
717
+ - **Fallback audio routing**: Graceful degradation when individual outputs aren't available
718
+
719
+ ### ChannelHandle (Abstract Interface)
720
+ Represents one musical part with full control interface:
721
+ - **Output node access**: `getOutputNode()` provides `AudioNode` for mixer connection
722
+ - **Note playback control**: Start/stop notes with velocity and duration control
723
+ - **Instrument management**: Runtime instrument changes per part
724
+ - **Volume control**: Internal MIDI volume (separate from external mixer volume)
725
+
726
+ ### MidiParser
727
+ MIDI file parser:
728
+ - **Part identification**: Automatically identifies separate instrument parts
729
+ - **Program change support**: Preserves MIDI instrument assignments (program numbers 0-127)
730
+ - **Track analysis**: Extracts tempo changes, time signatures, and structural data
731
+
732
+ ### AudioEngineUtils
733
+ Utility functions for instrument handling:
734
+ - **`getInstrumentProgram(instrument)`**: Converts instrument names to MIDI program numbers
735
+ - **`getProgramName(programNumber)`**: Converts MIDI program numbers to display names
736
+ - **Complete MIDI coverage**: Supports all 128 General MIDI instruments
737
+
738
+ ### MidiPlayer
739
+ High-level playback controller with mixer integration:
740
+ - **Part-centric playback**: Routes notes to appropriate channel handles
741
+ - **External mixer support**: Provides `getPartOutput()` for mixer connection
742
+ - **Convenience controls**: Built-in solo/mute/volume methods for rapid prototyping
743
+ - **Precise timing**: Advanced scheduling with tempo change support
744
+
745
+ ### BeatMapper
746
+ Advanced beat mapping for musical analysis:
747
+ - **Beat position calculation**: Maps time positions to musical beats
748
+ - **Tempo change handling**: Accurately tracks tempo variations
749
+ - **Measure boundary detection**: Identifies bar lines and structural elements
750
+
751
+ ## Development
752
+
753
+ ```bash
754
+ # Install dependencies
755
+ npm install
756
+
757
+ # Run tests
758
+ npm test
759
+
760
+ # Run with coverage
761
+ npm run test:coverage
762
+
763
+ # Development server
764
+ npm run dev
765
+ ```
766
+
767
+ ## Testing
768
+
769
+ The library includes comprehensive test suites:
770
+ - Unit tests for all core components
771
+ - MIDI parsing validation
772
+ - Audio engine integration tests
773
+ - Mock audio engine for testing
774
+
775
+ ## Demo
776
+
777
+ Check out the demo files:
778
+ - `demo/part-audio-engine-demo.html` - Interactive browser demo
779
+ - `test-player.js` - Command-line player example
780
+ - `examples/midi-player-demo.js` - Basic usage example
781
+
782
+ ## License
783
+
784
+ MIT License - see LICENSE file for details.
785
+
786
+ ## Browser Compatibility
787
+
788
+ ### Firefox Compatibility Notes
789
+
790
+ This library uses SpessaSynth for audio synthesis, which has specific Firefox compatibility considerations:
791
+
792
+ - **Firefox is recommended** over Chromium-based browsers due to better Web Audio API support and memory handling for large soundfonts
793
+ - **Known Firefox crashes** have been addressed in recent SpessaSynth versions (fixed crash due to Firefox browser bug #1918506)
794
+ - **Mitigation strategies implemented**:
795
+ - AudioWorklet module loading includes timing delays and retry logic to prevent initialization race conditions
796
+ - Soundfont loading includes progress feedback and chunked processing
797
+ - Engine initialization has automatic retry mechanism (3 attempts with exponential backoff)
798
+ - Memory management optimization before SpessaSynth initialization
799
+
800
+ If experiencing intermittent crashes (~20% rate) during initialization in Firefox:
801
+ 1. Ensure you're using the latest SpessaSynth version
802
+ 2. The built-in retry mechanism should automatically handle most timing-related failures
803
+ 3. Consider implementing additional delays if crashes persist in your specific environment
804
+
805
+ ### Other Browsers
806
+ - **Chromium-based browsers** (Chrome, Edge, Brave): May experience audio distortion due to Chromium Web Audio API bugs
807
+ - **Safari**: Not extensively tested but should work with Web Audio API polyfills
808
+
809
+ ## Contributing
810
+
811
+ This library was extracted from a larger choir practice application. Contributions are welcome for:
812
+ - Additional audio engine implementations
813
+ - MIDI parsing improvements
814
+ - Performance optimizations
815
+ - Documentation enhancements
816
+ - Browser compatibility improvements