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 +139 -844
- package/dist/audio-mixer-engine.cjs.js +1 -1
- package/dist/audio-mixer-engine.es.js +1170 -1147
- package/package.json +1 -1
- package/src/index.js +15 -1
- package/src/lib/lightweight-audio-engine.js +5 -4
- package/src/lib/playback-manager.js +60 -33
package/README.md
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
# Audio Mixer Engine
|
|
2
2
|
|
|
3
|
-
A part-centric JavaScript audio library
|
|
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-
|
|
18
|
-
- **
|
|
19
|
-
- **Dual
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
42
|
+
// Load MIDI file
|
|
60
43
|
await manager.load(midiArrayBuffer);
|
|
61
44
|
|
|
62
45
|
// Set up event listeners
|
|
63
|
-
manager.on('timeupdate', ({currentTime}) =>
|
|
64
|
-
|
|
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
|
|
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
|
|
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
|
|
62
|
+
// Start playback
|
|
85
63
|
await manager.play({ leadIn: true, metronome: true });
|
|
86
64
|
```
|
|
87
65
|
|
|
88
|
-
##
|
|
66
|
+
## Key Concepts
|
|
89
67
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
await audioEngine.initialize('./soundfont.sf3');
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Progress Event Format
|
|
80
|
+
### PlaybackManager
|
|
113
81
|
|
|
114
82
|
```javascript
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
113
|
+
delayBetweenParts: 0.4,
|
|
114
|
+
duration: 0.6,
|
|
115
|
+
velocity: 110
|
|
145
116
|
});
|
|
146
117
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
125
|
+
### MidiPlayer
|
|
169
126
|
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
140
|
+
// Part access
|
|
141
|
+
const outputNode = player.getPartOutput('soprano');
|
|
142
|
+
const channelHandle = player.getPartChannel('soprano');
|
|
179
143
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
|
154
|
+
### ChannelHandle
|
|
190
155
|
|
|
191
156
|
```javascript
|
|
192
|
-
//
|
|
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
|
|
165
|
+
// Scheduled notes
|
|
201
166
|
const eventId = channelHandle.playNote(startTime, pitch, velocity, duration);
|
|
202
167
|
|
|
203
|
-
//
|
|
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
|
-
//
|
|
172
|
+
// Info
|
|
208
173
|
const partId = channelHandle.getPartId();
|
|
209
174
|
const isActive = channelHandle.isActive();
|
|
210
175
|
```
|
|
211
176
|
|
|
212
|
-
###
|
|
177
|
+
### AudioEngine
|
|
213
178
|
|
|
214
179
|
```javascript
|
|
215
|
-
//
|
|
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
|
-
//
|
|
342
|
-
audioEngine.
|
|
343
|
-
|
|
344
|
-
|
|
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',
|
|
191
|
+
instrument: 'piano',
|
|
349
192
|
initialVolume: 1.0
|
|
350
193
|
});
|
|
351
194
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
audioEngine.
|
|
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
|
-
##
|
|
201
|
+
## Mixer Example
|
|
386
202
|
|
|
387
203
|
```javascript
|
|
388
|
-
|
|
389
|
-
|
|
204
|
+
// Typical mixer integration
|
|
390
205
|
class AudioMixer {
|
|
391
206
|
constructor(audioContext) {
|
|
392
207
|
this.audioContext = audioContext;
|
|
393
|
-
this.
|
|
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
|
|
401
|
-
await
|
|
213
|
+
async loadMidiFile(manager, midiBuffer) {
|
|
214
|
+
await manager.load(midiBuffer);
|
|
402
215
|
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
221
|
+
outputNode.connect(gain);
|
|
222
|
+
gain.connect(analyzer);
|
|
223
|
+
analyzer.connect(this.masterGain);
|
|
414
224
|
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
479
|
-
|
|
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 <
|
|
244
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
488
245
|
sum += dataArray[i] * dataArray[i];
|
|
489
246
|
}
|
|
490
|
-
return Math.sqrt(sum /
|
|
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
|
-
##
|
|
252
|
+
## Additional Documentation
|
|
527
253
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
##
|
|
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
|
|
970
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
984
|
-
-
|
|
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
|