audio-mixer-engine 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,14 @@
1
1
  # Audio Mixer Engine
2
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.
3
+ A part-centric JavaScript audio library for mixer applications. Provides individual audio outputs per musical part (soprano, alto, piano, etc.) enabling per-part gain control, level monitoring, and flexible routing.
14
4
 
15
5
  ## Features
16
6
 
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
- - **Initialization Progress Tracking**: Real-time progress events with download progress bars for soundfont loading
7
+ - **Part-centric audio**: Individual `AudioNode` outputs per part with separate gain/analysis routing
8
+ - **MIDI processing**: Parse files, beat/bar mapping, tempo changes, metadata extraction
9
+ - **Dual audio engines**: SpessaSynth (soundfont, ~10-50MB) or Lightweight (samples, ~574KB bundle)
10
+ - **Playback control**: Metronome, lead-in, seeking, speed changes, bar navigation, real-time events
11
+ - **Mixer-ready**: Designed for solo/mute, routing, and level monitoring workflows
30
12
 
31
13
  ## Installation
32
14
 
@@ -34,162 +16,145 @@ This library follows a **part-centric mixer design** where each musical part (so
34
16
  npm install audio-mixer-engine
35
17
  ```
36
18
 
37
- ## Quick Start - Basic Playback
19
+ ### Audio Engine Options
20
+
21
+ - **SpessaSynthAudioEngine**: Soundfont-based synthesis, requires separate soundfont file (~10-50MB)
22
+ - **LightweightAudioEngine**: Sample-based with smaller bundle (~574KB). See [LIGHTWEIGHT_ENGINE.md](./LIGHTWEIGHT_ENGINE.md) for setup.
23
+
24
+ ## Quick Start
38
25
 
39
26
  ```javascript
40
- import {
41
- SpessaSynthAudioEngine,
42
- PlaybackManager
43
- } from 'audio-mixer-engine';
27
+ import { SpessaSynthAudioEngine, PlaybackManager } from 'audio-mixer-engine';
44
28
 
45
- // Initialize audio context
46
29
  const ac = new AudioContext();
47
30
 
48
- // Setup the audio engine - use the spessasynth engine with a soundfont
31
+ // Initialize audio engine
49
32
  const audioEngine = new SpessaSynthAudioEngine(ac);
50
33
  await audioEngine.initialize('/path/to/soundfont.sf2');
51
34
 
52
- // Create PlaybackManager with optional configuration
35
+ // Create PlaybackManager
53
36
  const manager = new PlaybackManager(audioEngine, {
54
37
  metronome: { enabled: true, volume: 0.7 },
55
38
  leadIn: { enabled: true, bars: 1 },
56
39
  startup: { delayMs: 25 }
57
40
  });
58
41
 
59
- // Load MIDI data - this parses the file and creates the player automatically
42
+ // Load MIDI file
60
43
  await manager.load(midiArrayBuffer);
61
44
 
62
45
  // Set up event listeners
63
- manager.on('timeupdate', ({currentTime}) => {
64
- console.log(`Playing: ${currentTime.toFixed(2)}s`);
65
- });
66
-
67
- manager.on('beatChanged', ({bar, beat, time}) => {
68
- console.log(`Bar ${bar}, Beat ${beat} at ${time.toFixed(2)}s`);
69
- });
46
+ manager.on('timeupdate', ({currentTime}) => console.log(currentTime));
47
+ manager.on('beatChanged', ({bar, beat}) => console.log(`Bar ${bar}, Beat ${beat}`));
70
48
 
71
- // Setup audio routing - REQUIRED for audio output
49
+ // Setup audio routing (REQUIRED for audio output)
72
50
  // Metronome
73
51
  const metGain = ac.createGain();
74
52
  metGain.gain.value = 0.5;
75
53
  manager.getMetronomeOutput().connect(metGain).connect(ac.destination);
76
54
 
77
- // Part outputs using iterator pattern
55
+ // Part outputs
78
56
  for (const [partName, outputNode] of manager.getPartOutputs()) {
79
57
  const gain = ac.createGain();
80
58
  gain.gain.value = 0.75;
81
59
  outputNode.connect(gain).connect(ac.destination);
82
60
  }
83
61
 
84
- // Start playback with lead-in metronome ticks
62
+ // Start playback
85
63
  await manager.play({ leadIn: true, metronome: true });
86
64
  ```
87
65
 
88
- ## Initialization Progress Tracking
66
+ ## Key Concepts
89
67
 
90
- The library provides real-time progress events during initialization, particularly useful for showing progress bars during soundfont downloads which can take 20+ seconds over the internet.
68
+ **Part-centric design**: Each musical part gets an independent `ChannelHandle` with its own `AudioNode` output. Your application connects these outputs to gain controls, analyzers, and effects.
91
69
 
92
- ```javascript
93
- import SpessaSynthAudioEngine from 'audio-mixer-engine';
70
+ **External routing required**: The library provides individual part outputs but doesn't mix them internally. You must connect part outputs to `AudioContext.destination` (see Quick Start example).
94
71
 
95
- const audioEngine = new SpessaSynthAudioEngine(audioContext);
72
+ **Dual volume control**:
73
+ - Internal: `channelHandle.setVolume()` - affects MIDI synthesis
74
+ - External: Your gain nodes - affects final output and enables solo/mute
96
75
 
97
- // Listen for progress events
98
- audioEngine.on('initProgress', (event) => {
99
- console.log(`[${event.stage}] ${event.message}`);
76
+ **PlaybackManager vs MidiPlayer**: `PlaybackManager` adds metronome, lead-in, and convenience methods. `MidiPlayer` provides lower-level playback control.
100
77
 
101
- // Show progress bar for soundfont download
102
- if (event.progress !== undefined) {
103
- progressBar.style.width = `${event.progress * 100}%`;
104
- statusText.textContent = event.message;
105
- }
106
- });
78
+ ## API Overview
107
79
 
108
- // Initialize with soundfont
109
- await audioEngine.initialize('./soundfont.sf3');
110
- ```
111
-
112
- ### Progress Event Format
80
+ ### PlaybackManager
113
81
 
114
82
  ```javascript
115
- {
116
- stage: 'loading-soundfont', // Stage identifier
117
- message: 'Downloading soundfont: 45% (234 KB / 520 KB)', // Human-readable message
118
- progress: 0.45 // Optional: 0.0-1.0 for stages with progress bars
119
- }
120
- ```
121
-
122
- ### Initialization Stages
123
-
124
- | Stage | Description | Has Progress | Typical Duration |
125
- |-------|-------------|--------------|------------------|
126
- | `importing` | Loading SpessaSynth library | No | ~20ms local, ~1.5s remote |
127
- | `loading-soundfont` | **Downloading soundfont** | **Yes** | ~2s local, **~20s remote** |
128
- | `loading-worklet` | Loading audio worklet | No | ~100ms local, ~2s remote |
129
- | `creating-synth` | Setting up audio channels | No | ~60ms |
130
- | `loading-soundbank` | Loading soundbank | No | ~100ms |
131
- | `finalizing` | Finalizing setup | No | <10ms |
132
- | `ready` | Complete | No | N/A |
133
-
134
- **See [INIT_PROGRESS.md](./INIT_PROGRESS.md) for complete documentation, examples, and best practices.**
135
-
136
- **Demo:** `demo/initialization-progress-demo.html` shows a working implementation with visual progress indicators.
83
+ // Lifecycle
84
+ await manager.load(midiArrayBuffer, metadata, instrumentMap);
85
+ manager.reset();
137
86
 
138
- ## Mixer Integration Pattern
139
-
140
- ```javascript
141
- // Create parts manually for mixing interface
142
- const partChannel = audioEngine.createChannel('soprano', {
87
+ // Transport
88
+ await manager.play({ leadIn: true, metronome: true });
89
+ manager.pause();
90
+ manager.resume();
91
+ manager.stop();
92
+
93
+ // Configuration
94
+ manager.setMetronomeEnabled(true);
95
+ manager.setMetronomeSettings({ volume: 0.8 });
96
+ manager.setLeadInEnabled(true);
97
+ manager.setLeadInBars(2);
98
+ manager.setStartupDelay(50);
99
+
100
+ // Audio routing
101
+ const metronomeOutput = manager.getMetronomeOutput();
102
+ for (const [partName, outputNode] of manager.getPartOutputs()) { /* ... */ }
103
+ const partNames = manager.getPartNames();
104
+
105
+ // State
106
+ const state = manager.getState(); // 'reset'|'ready'|'stopped'|'lead-in'|'playing'|'paused'
107
+ const isPlaying = manager.isPlaying();
108
+ const currentTime = manager.getCurrentTime();
109
+
110
+ // Preview next notes (pitch reference for singers)
111
+ const result = manager.previewNextNotes({
143
112
  instrument: 'piano',
144
- initialVolume: 1.0
113
+ delayBetweenParts: 0.4,
114
+ duration: 0.6,
115
+ velocity: 110
145
116
  });
146
117
 
147
- // Get individual output node for external mixing
148
- const partOutput = partChannel.getOutputNode();
149
-
150
- // Set up mixer chain: Part Output -> External Gain -> Analyzer -> Master
151
- const partGain = audioContext.createGain();
152
- const partAnalyzer = audioContext.createAnalyser();
153
-
154
- partOutput.connect(partGain);
155
- partGain.connect(partAnalyzer);
156
- partAnalyzer.connect(masterGain); // External master gain
157
-
158
- // External mixer controls
159
- partGain.gain.value = 0.5; // 50% volume
160
- partAnalyzer.getByteFrequencyData(dataArray); // Level metering
161
-
162
- // Play a note
163
- const ac = audioEngine.audioContext;
164
- // In half a second play a C4 (pitch=60) at velocity of 100, and will last half a second
165
- partChannel.playNote(ac.currentTime + 0.5, 60, 100, 0.5)
118
+ // Events
119
+ manager.on('timeupdate', ({currentTime}) => {});
120
+ manager.on('beatChanged', ({bar, beat, isLeadIn, time}) => {});
121
+ manager.on('leadInStarted', ({bars, totalBeats, startupDelayMs}) => {});
122
+ manager.on('leadInEnded', () => {});
166
123
  ```
167
124
 
168
- ## Audio Routing Architecture
125
+ ### MidiPlayer
169
126
 
170
- The library implements a sophisticated audio routing system designed for mixer applications:
127
+ ```javascript
128
+ // Transport
129
+ player.play();
130
+ player.pause();
131
+ player.stop();
132
+ player.skipToTime(30.5);
133
+ player.setPlaybackSpeed(1.5);
171
134
 
172
- ```
173
- MIDI File Parser Player AudioEngine → Individual Part Outputs
174
-
175
- External Mixer Interface: Part Gain → Analyzer → Solo/Mute → Master → Destination
176
- ```
135
+ // Musical navigation
136
+ player.setBar(4, 1); // Jump to bar 4, repeat 1
137
+ const time = player.getTimeFromBar(2);
138
+ const barInfo = player.getBeatFromTime(15.3);
177
139
 
178
- ### Key Audio Routing Concepts
140
+ // Part access
141
+ const outputNode = player.getPartOutput('soprano');
142
+ const channelHandle = player.getPartChannel('soprano');
179
143
 
180
- 1. **Individual Part Outputs**: Each musical part provides an `AudioNode` output via `channelHandle.getOutputNode()`
181
- 2. **External Volume Control**: Mixer interfaces control gain via external `GainNode` instances connected to part outputs
182
- 3. **Internal vs External Volume**:
183
- - Internal: MIDI volume controlled via `channelHandle.setVolume()` (affects synthesis)
184
- - External: Mixer volume controlled via external gain nodes (affects final output)
185
- 4. **Analysis Integration**: Connect part outputs to `AnalyserNode` for real-time level metering and visualization
144
+ // Controls
145
+ player.setMasterVolume(0.7);
146
+ player.allSoundsOff();
186
147
 
187
- ## API Reference
148
+ // Events
149
+ player.on('timeupdate', ({currentTime}) => {});
150
+ player.on('ended', ({finalTime}) => {});
151
+ player.on('barChanged', ({bar, beat, repeat, time}) => {});
152
+ ```
188
153
 
189
- ### ChannelHandle Methods
154
+ ### ChannelHandle
190
155
 
191
156
  ```javascript
192
- // Get output node for mixer connection
157
+ // Output routing
193
158
  const outputNode = channelHandle.getOutputNode();
194
159
 
195
160
  // Note control
@@ -197,792 +162,122 @@ channelHandle.noteOn(pitch, velocity);
197
162
  channelHandle.noteOff(pitch);
198
163
  channelHandle.allNotesOff();
199
164
 
200
- // Scheduled note control
165
+ // Scheduled notes
201
166
  const eventId = channelHandle.playNote(startTime, pitch, velocity, duration);
202
167
 
203
- // Internal synthesis controls
204
- await channelHandle.setInstrument('choir_aahs');
168
+ // Configuration
169
+ await channelHandle.setInstrument('choir_aahs'); // String name or MIDI program number
205
170
  channelHandle.setVolume(0.8); // Internal MIDI volume
206
171
 
207
- // Channel info
172
+ // Info
208
173
  const partId = channelHandle.getPartId();
209
174
  const isActive = channelHandle.isActive();
210
175
  ```
211
176
 
212
- ### MidiPlayer Methods
177
+ ### AudioEngine
213
178
 
214
179
  ```javascript
215
- // Transport controls
216
- player.play();
217
- player.pause();
218
- player.stop();
219
- player.skipToTime(30.5);
220
- player.setPlaybackSpeed(1.5);
221
-
222
- // Musical navigation
223
- player.setBar(4, 1); // Jump to bar 4, repeat 1
224
- const time = player.getTimeFromBar(2); // Get time for bar 2
225
- const barInfo = player.getBeatFromTime(15.3); // Get beat info at time (includes bar, beat, time signature)
226
-
227
- // Emergency stop
228
- player.allSoundsOff();
229
-
230
- // Part access for mixer integration
231
- const sopranoOutput = player.getPartOutput('soprano');
232
- const sopranoChannel = player.getPartChannel('soprano');
233
-
234
- // REQUIRED: Set up external mixer routing for all parts
235
- const masterGain = audioContext.createGain();
236
- masterGain.connect(audioContext.destination);
237
-
238
- const sopranoGain = audioContext.createGain();
239
- const sopranoAnalyzer = audioContext.createAnalyser();
240
-
241
- sopranoOutput.connect(sopranoGain);
242
- sopranoGain.connect(sopranoAnalyzer);
243
- sopranoAnalyzer.connect(masterGain); // External master gain
244
-
245
- // Control volume/mute/solo via external gain nodes
246
- sopranoGain.gain.value = 0.5; // 50% volume
247
- sopranoGain.gain.value = 0; // Mute
248
- masterGain.gain.value = 0.8; // Master volume
249
-
250
- // Solo functionality: mute all other parts' external gain nodes
251
- // (Implementation depends on your specific mixer setup)
252
-
253
- // Level monitoring
254
- sopranoAnalyzer.getByteFrequencyData(dataArray);
255
-
256
- // Event handling
257
- player.on('timeupdate', ({currentTime}) => { /* */
258
- });
259
- player.on('ended', ({finalTime}) => { /* */
260
- });
261
- player.on('barChanged', ({bar, beat, repeat, time}) => { /* */
262
- });
263
- ```
264
-
265
- ### PlaybackManager Methods
266
-
267
- ```javascript
268
- // Create PlaybackManager with audio engine and optional configuration
269
- const playbackManager = new PlaybackManager(audioEngine, {
270
- metronome: { enabled: true, volume: 0.7, tickInstrument: 115, accentInstrument: 116 },
271
- leadIn: { enabled: true, bars: 2 },
272
- startup: { delayMs: 25 }
273
- });
274
-
275
- // Load MIDI data - parses and creates player automatically
276
- await playbackManager.load(midiArrayBuffer);
277
- // Or with metadata and custom instrument mapping
278
- await playbackManager.load(midiArrayBuffer, structureMetadata, instrumentMap);
279
-
280
- // Reset and load new MIDI file
281
- playbackManager.reset();
282
- await playbackManager.load(newMidiArrayBuffer);
283
-
284
- // Transport controls
285
- await playbackManager.play({ leadIn: true, metronome: true });
286
- playbackManager.pause();
287
- playbackManager.resume();
288
- playbackManager.stop();
289
-
290
- // Metronome configuration
291
- playbackManager.setMetronomeEnabled(true);
292
- playbackManager.setMetronomeSettings({ volume: 0.8, tickInstrument: 115 });
293
- const metronomeSettings = playbackManager.getMetronomeSettings();
294
-
295
- // Lead-in configuration
296
- playbackManager.setLeadInEnabled(true);
297
- playbackManager.setLeadInBars(3);
298
- const leadInSettings = playbackManager.getLeadInSettings();
299
-
300
- // Startup timing configuration
301
- playbackManager.setStartupDelay(50); // 50ms delay
302
- const startupSettings = playbackManager.getStartupSettings();
303
-
304
- // State and timing
305
- const state = playbackManager.getState(); // 'reset', 'ready', 'stopped', 'lead-in', 'playing', 'paused'
306
- const isPlaying = playbackManager.isPlaying();
307
- const currentTime = playbackManager.getCurrentTime();
308
-
309
- // Audio routing access
310
- const metronomeOutput = playbackManager.getMetronomeOutput();
311
- for (const [partName, outputNode] of playbackManager.getPartOutputs()) {
312
- // Set up external routing for each part
313
- }
314
- const partNames = playbackManager.getPartNames(); // Array of part names
315
-
316
- // Preview next notes (useful for pitch reference before singing)
317
- const result = playbackManager.previewNextNotes({
318
- instrument: 'piano', // Optional instrument override for all parts
319
- delayBetweenParts: 0.4, // Time between each part (seconds)
320
- duration: 0.6, // Duration of each note (seconds)
321
- velocity: 110, // Velocity for all notes
322
- partOrder: ['soprano', 'alto'] // Optional custom part order
323
- });
324
- // Returns: { parts: [{partName, pitch, startTime}, ...], totalDuration }
325
- // Automatically skips muted parts
326
-
327
- // Enhanced event handling
328
- playbackManager.on('leadInStarted', ({ bars, totalBeats, startupDelayMs }) => {});
329
- playbackManager.on('leadInEnded', () => {});
330
- playbackManager.on('beatChanged', ({ bar, beat, isLeadIn, time }) => {});
331
- playbackManager.on('startupSettingsChanged', ({ delayMs }) => {});
332
- ```
333
-
334
- ### Audio Engine Management
335
-
336
- ```javascript
337
- // Engine lifecycle
180
+ // Initialization
338
181
  await audioEngine.initialize('/path/to/soundfont.sf2');
339
182
  const isReady = audioEngine.isInitialized;
340
183
 
341
- // Engine controls
342
- audioEngine.setMasterVolume(0.7);
343
- const masterVol = audioEngine.getMasterVolume();
344
- audioEngine.allSoundsOff();
184
+ // Progress tracking (SpessaSynth only)
185
+ audioEngine.on('initProgress', ({stage, message, progress}) => {
186
+ // Show progress bar for soundfont downloads (20+ seconds)
187
+ });
345
188
 
346
189
  // Channel creation
347
190
  const channelHandle = audioEngine.createChannel('partId', {
348
- instrument: 'piano', // Can be string name or MIDI program number (0-127)
191
+ instrument: 'piano',
349
192
  initialVolume: 1.0
350
193
  });
351
194
 
352
- // Engine lifecycle
353
- const activeChannels = audioEngine.getActiveChannels();
354
- audioEngine.destroy(); // Cleanup all resources
355
- ```
356
-
357
- ## Mixer-Oriented Design vs Traditional Audio Libraries
358
-
359
- Unlike traditional audio libraries that output mixed audio directly to speakers, this library is designed specifically for **mixer interfaces** and **interactive practice applications**:
360
-
361
- ### Traditional Audio Library Pattern:
362
- ```javascript
363
- // Traditional libraries mix everything internally
364
- player.setVolume(0.5); // Global volume only
365
- player.play(); // Mixed audio to speakers
366
- // ❌ No individual part control
367
- // ❌ No real-time level analysis per part
368
- // ❌ No external routing flexibility
369
- ```
370
-
371
- ### Audio Mixer Engine Pattern:
372
- ```javascript
373
- // Part-centric with individual outputs for mixer control
374
- const sopranoChannel = engine.createChannel('soprano');
375
- const sopranoOutput = sopranoChannel.getOutputNode(); // ← Key difference!
376
-
377
- // External mixer has full control
378
- sopranoOutput.connect(mixerGainNode);
379
- sopranoOutput.connect(levelAnalyzer);
380
- // ✅ Individual part control
381
- // ✅ Real-time analysis per part
382
- // ✅ Full routing flexibility
195
+ // Controls
196
+ audioEngine.setMasterVolume(0.7);
197
+ audioEngine.allSoundsOff();
198
+ audioEngine.destroy();
383
199
  ```
384
200
 
385
- ## Complete Mixer Example
201
+ ## Mixer Example
386
202
 
387
203
  ```javascript
388
- import { SpessaSynthAudioEngine, PlaybackManager } from 'audio-mixer-engine';
389
-
204
+ // Typical mixer integration
390
205
  class AudioMixer {
391
206
  constructor(audioContext) {
392
207
  this.audioContext = audioContext;
393
- this.audioEngine = new SpessaSynthAudioEngine(audioContext);
394
- this.playbackManager = null;
395
- this.parts = new Map(); // partId -> { gain, analyzer, muted, soloed }
208
+ this.parts = new Map(); // partId -> { gain, analyzer }
396
209
  this.masterGain = audioContext.createGain();
397
210
  this.masterGain.connect(audioContext.destination);
398
211
  }
399
212
 
400
- async initialize(soundfontPath) {
401
- await this.audioEngine.initialize(soundfontPath);
213
+ async loadMidiFile(manager, midiBuffer) {
214
+ await manager.load(midiBuffer);
402
215
 
403
- // Create PlaybackManager with configuration
404
- this.playbackManager = new PlaybackManager(this.audioEngine, {
405
- metronome: { enabled: false, volume: 0.7 },
406
- leadIn: { enabled: false, bars: 1 },
407
- startup: { delayMs: 25 }
408
- });
409
- }
216
+ // Set up routing for each part
217
+ for (const [partName, outputNode] of manager.getPartOutputs()) {
218
+ const gain = this.audioContext.createGain();
219
+ const analyzer = this.audioContext.createAnalyser();
410
220
 
411
- async loadMidiFile(midiArrayBuffer, metadata = null) {
412
- // Load MIDI data into PlaybackManager
413
- await this.playbackManager.load(midiArrayBuffer, metadata);
221
+ outputNode.connect(gain);
222
+ gain.connect(analyzer);
223
+ analyzer.connect(this.masterGain);
414
224
 
415
- // Set up mixer routing for all parts
416
- for (const [partName, outputNode] of this.playbackManager.getPartOutputs()) {
417
- this.addPartToMixer(partName, outputNode);
225
+ this.parts.set(partName, { gain, analyzer });
418
226
  }
419
227
  }
420
228
 
421
- addPartToMixer(partId, partOutput) {
422
- // Set up mixer chain for this part
423
- const gain = this.audioContext.createGain();
424
- const analyzer = this.audioContext.createAnalyser();
425
- analyzer.fftSize = 256;
426
-
427
- // Audio routing: Part → Gain → Analyzer → Master → Destination
428
- partOutput.connect(gain);
429
- gain.connect(analyzer);
430
- analyzer.connect(this.masterGain);
431
-
432
- // Track part in mixer
433
- this.parts.set(partId, {
434
- gain,
435
- analyzer,
436
- muted: false,
437
- soloed: false
438
- });
439
-
440
- return { gain, analyzer };
441
- }
442
-
443
- // Mixer controls
444
229
  setPartVolume(partId, volume) {
445
- const part = this.parts.get(partId);
446
- if (part) part.gain.gain.value = volume;
230
+ this.parts.get(partId).gain.gain.value = volume;
447
231
  }
448
232
 
449
233
  mutePart(partId) {
450
- const part = this.parts.get(partId);
451
- if (part) {
452
- part.muted = !part.muted;
453
- part.gain.gain.value = part.muted ? 0 : 1;
454
- }
234
+ this.parts.get(partId).gain.gain.value = 0;
455
235
  }
456
236
 
457
- soloPart(partId) {
458
- // Toggle solo state
459
- const part = this.parts.get(partId);
460
- if (!part) return;
461
-
462
- part.soloed = !part.soloed;
463
-
464
- // Apply solo logic: if any parts are soloed, mute non-soloed parts
465
- const hasAnySoloed = Array.from(this.parts.values()).some(p => p.soloed);
466
-
467
- this.parts.forEach((partData, id) => {
468
- if (hasAnySoloed) {
469
- partData.gain.gain.value = partData.soloed ? 1 : 0;
470
- } else {
471
- partData.gain.gain.value = partData.muted ? 0 : 1;
472
- }
473
- });
474
- }
475
-
476
- // Real-time level monitoring
477
237
  getPartLevel(partId) {
478
- const part = this.parts.get(partId);
479
- if (!part) return 0;
480
-
481
- const bufferLength = part.analyzer.frequencyBinCount;
482
- const dataArray = new Uint8Array(bufferLength);
483
- part.analyzer.getByteFrequencyData(dataArray);
238
+ const { analyzer } = this.parts.get(partId);
239
+ const dataArray = new Uint8Array(analyzer.frequencyBinCount);
240
+ analyzer.getByteFrequencyData(dataArray);
484
241
 
485
242
  // Calculate RMS level
486
243
  let sum = 0;
487
- for (let i = 0; i < bufferLength; i++) {
244
+ for (let i = 0; i < dataArray.length; i++) {
488
245
  sum += dataArray[i] * dataArray[i];
489
246
  }
490
- return Math.sqrt(sum / bufferLength) / 255; // Normalized 0-1
491
- }
492
-
493
- // Playback controls (delegated to PlaybackManager)
494
- async play(options = {}) {
495
- await this.playbackManager.play(options);
496
- }
497
-
498
- pause() {
499
- this.playbackManager.pause();
500
- }
501
-
502
- stop() {
503
- this.playbackManager.stop();
247
+ return Math.sqrt(sum / dataArray.length) / 255;
504
248
  }
505
249
  }
506
-
507
- // Usage
508
- const mixer = new AudioMixer(audioContext);
509
- await mixer.initialize('soundfont.sf2');
510
-
511
- // Load MIDI file
512
- await mixer.loadMidiFile(midiArrayBuffer);
513
-
514
- // Mixer controls
515
- mixer.setPartVolume('soprano', 0.7);
516
- mixer.mutePart('piano');
517
- mixer.soloPart('soprano');
518
-
519
- // Playback
520
- await mixer.play({ leadIn: true, metronome: true });
521
-
522
- // Monitor levels
523
- const level = mixer.getPartLevel('soprano'); // Get real-time level
524
250
  ```
525
251
 
526
- ## Common Mixer Patterns
252
+ ## Additional Documentation
527
253
 
528
- ### Pattern 1: Practice App with Part Isolation
529
- ```javascript
530
- // Enable users to practice by isolating their part
531
- const practiceApp = {
532
- async setupPractice(userPart, otherParts) {
533
- // Create channels for all parts
534
- const channels = {};
535
- for (const part of [userPart, ...otherParts]) {
536
- channels[part] = await this.createPartWithMixer(part);
537
- }
538
-
539
- // Set up practice mode: user part at full volume, others quieter
540
- channels[userPart].gain.gain.value = 1.0;
541
- otherParts.forEach(part => {
542
- channels[part].gain.gain.value = 0.3; // Background level
543
- });
544
- },
545
-
546
- // Preview starting pitch before singing
547
- playStartingPitch() {
548
- // Preview next notes using piano for clear pitch reference
549
- const result = this.playbackManager.previewNextNotes({
550
- instrument: 'piano', // Clear pitch reference
551
- delayBetweenParts: 0.4, // Sequence parts left-to-right
552
- duration: 0.6, // 600ms per note
553
- velocity: 110 // Clear and audible
554
- });
555
-
556
- // Shows which parts will play (automatically skips muted parts)
557
- console.log(`Playing pitches for: ${result.parts.map(p => p.partName).join(', ')}`);
558
- }
559
- };
560
- ```
561
-
562
- ### Pattern 2: Live Mixer Interface
563
- ```javascript
564
- // Real-time mixer with visual feedback
565
- class LiveMixer {
566
- updateLevels() {
567
- // Update level meters for all parts in real-time
568
- this.parts.forEach((partData, partId) => {
569
- const level = this.getPartLevel(partId);
570
- this.updateLevelMeter(partId, level);
571
-
572
- // Highlight active parts
573
- const isActive = level > 0.02;
574
- this.highlightPart(partId, isActive);
575
- });
576
- }
577
-
578
- // Mixer UI controls map directly to audio nodes
579
- onVolumeSlider(partId, value) {
580
- this.parts.get(partId).gain.gain.value = value;
581
- }
582
-
583
- onMuteButton(partId) {
584
- const part = this.parts.get(partId);
585
- part.muted = !part.muted;
586
- part.gain.gain.value = part.muted ? 0 : 1;
587
- }
588
- }
589
- ```
590
-
591
- ### Pattern 3: Recording/Export with Part Separation
592
- ```javascript
593
- // Record individual parts or create stems
594
- class PartRecorder {
595
- async recordPart(partId, duration) {
596
- const part = this.parts.get(partId);
597
-
598
- // Connect part to MediaRecorder via MediaStreamDestination
599
- const dest = this.audioContext.createMediaStreamDestination();
600
- part.gain.connect(dest);
601
-
602
- const recorder = new MediaRecorder(dest.stream);
603
- // Record just this part's audio...
604
- }
605
- }
606
- ```
607
-
608
- ## Advanced Usage Examples
609
-
610
- ### Preview Next Notes (Pitch Reference)
611
-
612
- For choir practice apps, singers often need to hear their starting pitch before singing:
613
-
614
- ```javascript
615
- // Pause playback and preview the next notes
616
- playbackManager.pause();
617
-
618
- // Play next note for all unmuted parts in sequence
619
- const result = playbackManager.previewNextNotes({
620
- instrument: 'piano', // Use piano for clear pitch reference
621
- delayBetweenParts: 0.4, // 400ms between each part
622
- duration: 0.6, // 600ms note duration
623
- velocity: 110 // Clear, audible level
624
- });
625
-
626
- // Result shows which parts were previewed
627
- console.log(`Previewed notes for: ${result.parts.map(p => p.partName).join(', ')}`);
628
- // Example output: {
629
- // parts: [
630
- // { partName: 'soprano', pitch: 64, startTime: 0.01 },
631
- // { partName: 'alto', pitch: 60, startTime: 0.41 }
632
- // ],
633
- // totalDuration: 0.8
634
- // }
635
-
636
- // Automatically skips muted parts (checked via partOutput.gain.value === 0)
637
- // Notes play through each part's channel, so level indicators will respond
638
- // Can specify custom part order: partOrder: ['soprano', 'alto', 'tenor', 'bass']
639
- ```
640
-
641
- **How it works:**
642
- 1. **MidiPlayer.getAllNextNotes()** - Finds the next note for each part after current time
643
- 2. **ChannelHandle.playPreviewNote()** - Plays a single note with optional instrument override
644
- 3. **PlaybackManager.previewNextNotes()** - Orchestrates sequential playback of all parts
645
-
646
- **Key features:**
647
- - Plays through actual part channels (level meters show activity)
648
- - Automatically skips muted parts
649
- - Supports instrument override (e.g., piano instead of choir_aahs)
650
- - Configurable timing, duration, and velocity
651
- - Custom part ordering support
652
-
653
- ### PlaybackManager with Metronome and Lead-in
654
-
655
- ```javascript
656
- import { SpessaSynthAudioEngine, PlaybackManager } from 'audio-mixer-engine';
657
-
658
- // Initialize audio engine
659
- const audioEngine = new SpessaSynthAudioEngine(audioContext);
660
- await audioEngine.initialize('/path/to/soundfont.sf2');
661
-
662
- // Create PlaybackManager with advanced features
663
- const playbackManager = new PlaybackManager(audioEngine, {
664
- metronome: {
665
- enabled: true,
666
- tickInstrument: 115, // Woodblock for regular beats
667
- accentInstrument: 116, // Taiko drum for downbeats
668
- volume: 0.7
669
- },
670
- leadIn: {
671
- enabled: true,
672
- bars: 2 // 2-bar lead-in
673
- },
674
- startup: {
675
- delayMs: 25 // 25ms startup delay for better timing
676
- }
677
- });
678
-
679
- // Load MIDI data
680
- await playbackManager.load(midiArrayBuffer);
681
-
682
- // Enhanced event handling
683
- playbackManager.on('leadInStarted', ({ bars, totalBeats, startupDelayMs }) => {
684
- console.log(`Lead-in: ${bars} bars (${totalBeats} beats), ${startupDelayMs}ms startup delay`);
685
- });
686
-
687
- playbackManager.on('beatChanged', ({ bar, beat, isLeadIn }) => {
688
- if (isLeadIn) {
689
- console.log(`Lead-in: Bar ${bar}, Beat ${beat}`);
690
- } else {
691
- console.log(`Playing: Bar ${bar}, Beat ${beat}`);
692
- }
693
- });
694
-
695
- // Runtime configuration
696
- playbackManager.setStartupDelay(50); // Adjust startup delay
697
- playbackManager.setMetronomeEnabled(true); // Toggle metronome
698
- playbackManager.setLeadInBars(1); // Change lead-in length
699
-
700
- // Start with features
701
- await playbackManager.play({ leadIn: true, metronome: true });
702
- ```
703
-
704
- ### Startup Timing Control
705
-
706
- The PlaybackManager includes configurable startup delays to ensure perfect timing synchronization between metronome ticks and initial notes:
707
-
708
- ```javascript
709
- // Configure startup delay during construction
710
- const playbackManager = new PlaybackManager(audioEngine, {
711
- startup: {
712
- delayMs: 25 // Default: 25ms delay before audio starts
713
- }
714
- });
715
-
716
- // Runtime adjustment of startup delay
717
- playbackManager.setStartupDelay(50); // 50ms delay
718
-
719
- // Get current settings
720
- const settings = playbackManager.getStartupSettings();
721
- console.log(`Current delay: ${settings.delayMs}ms`);
722
-
723
- // Listen for configuration changes
724
- playbackManager.on('startupSettingsChanged', ({ delayMs }) => {
725
- console.log(`Startup delay changed to ${delayMs}ms`);
726
- });
727
- ```
728
-
729
- **Why Startup Delays Matter:**
730
- - **Timing Synchronization**: Ensures metronome ticks and first notes start together
731
- - **Scheduling Setup**: Allows audio context and synthesis engines time to prepare
732
- - **Reduced Audio Glitches**: Prevents initial audio stuttering on some systems
733
- - **Consistent Timing**: Eliminates variable delays in first few notes
734
-
735
- **Recommended Values:**
736
- - `0ms` - No delay (fastest start, may have timing issues)
737
- - `25ms` - Default (good balance of speed and timing)
738
- - `50ms` - Conservative (ensures reliable timing on slower systems)
739
- - `100ms` - High latency systems (older devices or complex audio setups)
740
-
741
- **Behavior:**
742
- - The `play()` method returns immediately and state changes to 'playing'
743
- - Events are emitted immediately (playbackStarted, leadInStarted)
744
- - Actual audio output begins after the specified delay
745
- - Works with both direct playback and lead-in modes
746
-
747
- ### Musical Navigation
748
-
749
- ```javascript
750
- // Set up event listener for bar changes
751
- player.on('barChanged', ({bar, beat, repeat, time}) => {
752
- console.log(`Now at Bar ${bar}, Beat ${beat} (${time.toFixed(2)}s)`);
753
- updateScoreHighlight(bar);
754
- });
755
-
756
- // Navigate to specific bars
757
- player.setBar(5); // Jump to bar 5
758
- player.setBar(3, 1); // Jump to bar 3, repeat 1
759
-
760
- // Time/bar conversion
761
- const bar8Time = player.getTimeFromBar(8); // Get time for bar 8
762
- const currentBar = player.getBeatFromTime(player.getCurrentTime());
763
- ```
764
-
765
- ### Event-Driven Mixer Integration
766
-
767
- ```javascript
768
- // Set up comprehensive event handling
769
- player.on('timeupdate', ({ currentTime }) => {
770
- updateProgressBar(currentTime / player.getTotalDuration());
771
- });
772
-
773
- player.on('ended', ({ finalTime }) => {
774
- console.log(`Playback completed at ${finalTime}s`);
775
- showPlaybackComplete();
776
- });
777
-
778
- player.on('masterVolumeChanged', ({ volume }) => {
779
- updateMasterVolumeSlider(volume);
780
- });
781
-
782
- // Master volume control with events
783
- player.setMasterVolume(0.8); // Triggers masterVolumeChanged event
784
- ```
785
-
786
- ### Structure Metadata Integration
787
-
788
- ```javascript
789
- // Advanced beat mapping with song structure
790
- const structureMetadata = {
791
- sections: [
792
- {startBar: 1, endBar: 8, name: "Verse"},
793
- {startBar: 9, endBar: 16, name: "Chorus"},
794
- {startBar: 17, endBar: 24, name: "Bridge"}
795
- ],
796
- order: [
797
- {section: 0}, // Verse
798
- {section: 1, repeat: 2}, // Chorus x2
799
- {section: 2}, // Bridge
800
- {section: 1} // Chorus
801
- ]
802
- };
803
-
804
- const player = new MidiPlayer(audioEngine, parsedData, instrumentMap, structureMetadata);
805
-
806
- // Navigate by song sections
807
- player.setBar(9); // Jump to Chorus
808
- player.setBar(1, 1); // Jump to Verse, second time through
809
- ```
810
-
811
- ## Metadata and Score Configuration
812
-
813
- The library supports comprehensive metadata to override MIDI file information and configure playback behavior. See [METADATA.md](./METADATA.md) for the complete metadata specification including:
814
-
815
- - **Score metadata overrides**: Title, composer, arranger, copyright
816
- - **Part configuration**: Custom part names, track mapping, instrument overrides
817
- - **Playback modifiers**: Semitone shift, playback speed adjustment
818
- - **Structure metadata**: Non-linear playback with sections and repeats (see also [BEATMAPPING.md](./BEATMAPPING.md))
819
-
820
- ```javascript
821
- // Example: Load with metadata overrides
822
- const metadata = {
823
- title: "Amazing Grace",
824
- composer: "John Newton",
825
- semitoneShift: -2, // Transpose down 2 semitones
826
- playbackSpeed: 0.8, // Slow to 80% speed
827
- parts: {
828
- "soprano": { trackIndex: 0, instrument: 52 },
829
- "alto": { trackIndex: 1, instrument: 52 }
830
- },
831
- sections: [...], // Structure for non-linear playback
832
- order: [...]
833
- };
834
-
835
- await playbackManager.load(midiBuffer, metadata);
836
- ```
837
-
838
- ## Use Cases
839
-
840
- This library is specifically designed for:
841
-
842
- - **Choir Practice Apps**: Individual part control for learning and practice
843
- - **Music Education Software**: Part isolation and analysis for teaching
844
- - **Live Performance Tools**: Real-time mixing during rehearsals or performances
845
- - **Audio Workstations**: Part-based stems and multi-track recording
846
- - **Interactive Score Viewers**: Dynamic part highlighting and playback control
847
- - **Rehearsal Tools**: Director interfaces with part-specific controls
848
-
849
- **Not intended for**: Simple audio playback, games, or applications that don't need per-part control.
850
-
851
- ## Updated API (v0.1.0)
852
-
853
- ### New Event System
854
- All events now use structured data objects:
855
- ```javascript
856
- // Old format (deprecated)
857
- player.on('timeupdate', (currentTime) => { /* */ });
858
-
859
- // New format (current)
860
- player.on('timeupdate', ({ currentTime }) => { /* */ });
861
- player.on('ended', ({ finalTime }) => { /* */ });
862
- player.on('barChanged', ({ bar, beat, repeat, time }) => { /* */ });
863
- ```
864
-
865
- ### Enhanced Constructor
866
-
867
- ```javascript
868
- // Basic usage (backward compatible)
869
- new MidiPlayer(audioEngine, parsedData, instrumentMap);
870
-
871
- // With structure metadata for advanced navigation
872
- new MidiPlayer(audioEngine, parsedData, instrumentMap, structureMetadata);
873
- ```
874
-
875
- ### New Methods
876
-
877
- ```javascript
878
- // Musical navigation
879
- player.setBar(barNumber, repeat);
880
- player.getTimeFromBar(barNumber, repeat);
881
- player.getBeatFromTime(timeInSeconds);
882
-
883
- // Master volume control
884
- player.setMasterVolume(0.7);
885
- player.getMasterVolume();
886
- player.allSoundsOff();
887
- ```
888
-
889
- ## Interface Contract
890
-
891
- 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.
892
-
893
- ## Core Components
894
-
895
- ### AudioEngine (Abstract Base)
896
- Defines the contract for part-centric audio synthesis:
897
- - **Part-based channel creation**: Each musical part gets its own `ChannelHandle`
898
- - **Individual output routing**: Provides separate `AudioNode` outputs per part
899
- - **Master volume control**: Global volume affecting all parts
900
- - **Resource management**: Clean lifecycle management for channels and audio nodes
901
-
902
- ### SpessaSynthAudioEngine
903
- SpessaSynth-based implementation with mixer-focused features:
904
- - **Individual MIDI channel outputs**: Creates separate audio outputs for each of 16 MIDI channels
905
- - **External routing support**: Part outputs designed for connection to external gain/analysis nodes
906
- - **Soundfont-based synthesis**: High-quality GM-compatible audio synthesis
907
- - **Fallback audio routing**: Graceful degradation when individual outputs aren't available
908
-
909
- ### ChannelHandle (Abstract Interface)
910
- Represents one musical part with full control interface:
911
- - **Output node access**: `getOutputNode()` provides `AudioNode` for mixer connection
912
- - **Note playback control**: Start/stop notes with velocity and duration control
913
- - **Instrument management**: Runtime instrument changes per part
914
- - **Volume control**: Internal MIDI volume (separate from external mixer volume)
915
-
916
- ### MidiParser
917
- MIDI file parser:
918
- - **Part identification**: Automatically identifies separate instrument parts
919
- - **Program change support**: Preserves MIDI instrument assignments (program numbers 0-127)
920
- - **Track analysis**: Extracts tempo changes, time signatures, and structural data
921
-
922
- ### AudioEngineUtils
923
- Utility functions for instrument handling:
924
- - **`getInstrumentProgram(instrument)`**: Converts instrument names to MIDI program numbers
925
- - **`getProgramName(programNumber)`**: Converts MIDI program numbers to display names
926
- - **Complete MIDI coverage**: Supports all 128 General MIDI instruments
927
-
928
- ### MidiPlayer
929
- High-level playback controller with mixer integration:
930
- - **Part-centric playback**: Routes notes to appropriate channel handles
931
- - **External mixer support**: Provides `getPartOutput()` for mixer connection
932
- - **Convenience controls**: Built-in solo/mute/volume methods for rapid prototyping
933
- - **Precise timing**: Advanced scheduling with tempo change support
934
-
935
- ### BeatMapper
936
- Advanced beat mapping for musical analysis:
937
- - **Beat position calculation**: Maps time positions to musical beats
938
- - **Tempo change handling**: Accurately tracks tempo variations
939
- - **Measure boundary detection**: Identifies bar lines and structural elements
254
+ - **[LIGHTWEIGHT_ENGINE.md](./LIGHTWEIGHT_ENGINE.md)** - Lightweight engine setup and sample file configuration
255
+ - **[METADATA.md](./METADATA.md)** - Score metadata, part configuration, and playback modifiers
256
+ - **[BEATMAPPING.md](./BEATMAPPING.md)** - Beat mapping and non-linear playback structures
257
+ - **[INTERFACE.md](./INTERFACE.md)** - Complete interface contract for UI integration
258
+ - **[INIT_PROGRESS.md](./INIT_PROGRESS.md)** - Initialization progress tracking and best practices
940
259
 
941
260
  ## Development
942
261
 
943
262
  ```bash
944
- # Install dependencies
945
263
  npm install
946
-
947
- # Run tests
948
264
  npm test
949
-
950
- # Run with coverage
951
265
  npm run test:coverage
952
-
953
- # Development server
954
266
  npm run dev
267
+ npm run build
955
268
  ```
956
269
 
957
- ## Testing
270
+ ## Examples
958
271
 
959
- The library includes comprehensive test suites:
960
- - Unit tests for all core components
961
- - MIDI parsing validation
962
- - Audio engine integration tests
963
- - Mock audio engine for testing
964
-
965
- ## Demo
966
-
967
- Check out the demo files:
968
272
  - `demo/part-audio-engine-demo.html` - Interactive browser demo
969
- - `demo/initialization-progress-demo.html` - Progress tracking demo with timing breakdown
970
- - `test-player.js` - Command-line player example
971
- - `examples/midi-player-demo.js` - Basic usage example
273
+ - `demo/initialization-progress-demo.html` - Progress tracking demo
274
+ - `examples/midi-player-demo.js` - Basic usage
972
275
 
973
276
  ## License
974
277
 
975
278
  MIT License - see LICENSE file for details.
976
279
 
977
- ### Browsers
978
- - **Chromium-based browsers** (Chrome, Edge, Brave): May experience audio distortion due to Chromium Web Audio API bugs
979
- - **Safari**: Not extensively tested but should work with Web Audio API polyfills
980
-
981
- ## Contributing
280
+ ## Browser Compatibility
982
281
 
983
- This library was extracted from a larger choir practice application. Contributions are welcome for:
984
- - Additional audio engine implementations
985
- - MIDI parsing improvements
986
- - Performance optimizations
987
- - Documentation enhancements
988
- - Browser compatibility improvements
282
+ - **Chromium browsers** (Chrome, Edge, Brave): May experience audio distortion due to Web Audio API bugs
283
+ - **Safari**: Should work with standard Web Audio API support