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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/audio-mixer-engine.cjs.js +1 -0
- package/dist/audio-mixer-engine.es.js +1669 -0
- package/package.json +54 -0
- package/src/assets/stick-4cs.mp3 +0 -0
- package/src/assets/stick-4d.mp3 +0 -0
- package/src/index.js +18 -0
- package/src/lib/audio-engine.js +526 -0
- package/src/lib/beat-mapper.js +155 -0
- package/src/lib/midi-parser.js +718 -0
- package/src/lib/midi-player.js +700 -0
- package/src/lib/playback-manager.js +1257 -0
- package/src/lib/spessasynth-audio-engine.js +310 -0
- package/src/lib/spessasynth-channel-handle.js +151 -0
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
|