midi-audio-player 2.0.1 → 2.0.3

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,472 +1,475 @@
1
- ![logo](https://webaudiofonts.com/images/logo.svg)
2
-
3
- # midi-audio-player
4
-
5
- **Real MIDI playback in the browser — powered by Web Audio API and WebAudioFont.**
6
-
7
- [See full feature demo here](https://webaudiofonts.com/demo/)
8
-
9
- No server. No heavy runtime. Just load a `.mid` or `.kar` file and play it — with reverb, a 10-band EQ, per-channel volume control, karaoke support, and over 3,000 instrument presets.
10
-
11
- ```ts
12
- const player = new MidiAudioPlayer({ volume: 0.8, reverb: 0.2, eqPreset: 'jazz' });
13
- player.on('endOfFile', () => playAnotherSong());
14
- await player.play('https://example.com/song.mid');
15
- ```
16
-
17
- ---
18
-
19
- ## Features
20
-
21
- - Full General MIDI playback via WebAudioFont instrument presets
22
- - 3,000+ free instrument presets — piano, strings, brass, synths, drums, and more
23
- - Convolution reverb with adjustable wet/dry mix
24
- - 10-band parametric EQ with named presets
25
- - Per-channel volume control
26
- - Karaoke mode parses MIDI text/lyric events and emits timed HTML frames
27
- - Vocal channel auto-detection and muting
28
- - Auto-repair for corrupted MIDI files
29
- - Leading/trailing silence trimming
30
- - SVG waveform generation
31
- - Real-time amplitude metering
32
- - Full MIDI protocol support (pitch bend, controllers, program change, etc.)
33
- - Works with URLs, `ArrayBuffer`, and Base64 input
34
- - IndexedDB preset cache — presets are only downloaded once
35
- - Bring your own preset endpoint (self-hosted sf2-json deployment)
36
- - ESM-native, compatible with bundlers and vanilla browser environments
37
-
38
- ---
39
-
40
- ## Installation
41
-
42
- ```bash
43
- npm install midi-audio-player
44
- ```
45
-
46
- Or via CDN (UMD):
47
-
48
- ```html
49
- <script src="https://cdn.jsdelivr.net/npm/midi-audio-player/dist/midi-audio-player.min.js"></script>
50
- ```
51
-
52
- ---
53
-
54
- ## Quick Start
55
-
56
- ```ts
57
- import MidiAudioPlayer from 'midi-audio-player';
58
-
59
- const player = new MidiAudioPlayer({
60
- volume: 0.7,
61
- reverb: 0.25,
62
- });
63
-
64
- player.on('computed', ({ title, duration }) => {
65
- console.log(`"${title}" — ${duration.toFixed(1)}s`);
66
- });
67
-
68
- player.on('presetsLoaded', () => {
69
- console.log('All instruments ready.');
70
- });
71
-
72
- player.on('endOfFile', () => player.stop());
73
-
74
- await player.play('https://example.com/song.mid');
75
- ```
76
-
77
- > **Note:** `MidiAudioPlayer` must be instantiated in response to a user gesture (click, keydown, etc.), as browsers require user activation before creating a `Web Audio` context.
78
-
79
- ---
80
-
81
- ## API Reference
82
-
83
- ### `new MidiAudioPlayer(opts?)`
84
-
85
- Creates a new player instance and initializes the full Web Audio signal chain.
86
-
87
- | Option | Type | Default | Description |
88
- |---|---|---|---|
89
- | `endpoint` | `string` | `"https://webaudiofonts.github.io/presets/"` | Base URL of the preset endpoint. Must expose `catalog.json` and individual preset JSON files. [See custom endpoint](#custom-preset-endpoint). |
90
- | `volume` | `number` | `0.6` | Master output volume `[0, 1]`. Applied with a logarithmic curve. |
91
- | `reverb` | `number` | `0.3` | Convolution reverb wet level `[0, 1]`. `0` = dry, `1` = full wet. |
92
- | `localCache` | `boolean` | `true` | Cache the catalog in `sessionStorage` and presets in `IndexedDB`. |
93
- | `presetRandom` | `boolean` | `false` | Pick a random preset for each MIDI program instead of the first available. |
94
- | `karaoke` | `boolean` | `false` | Enable karaoke mode. Parses MIDI lyric events and emits timed HTML frames. |
95
- | `karaokeDelay` | `number` | `0` | Advance karaoke frames by this many seconds (lyrics appear earlier). |
96
- | `muteExpression` | `boolean` | `false` | Auto-detect and mute the vocal channel. Requires `karaoke: true`. |
97
- | `maxCharPerLine` | `number` | `48` | Max characters per karaoke line before forcing a line break. |
98
- | `eqPreset` | `EQPresetName` | `"flat"` | EQ preset applied on instantiation. |
99
- | `preferred` | `string[]` | `[]` | Ordered list of preferred sound bank suffixes (e.g. `["FluidR3_GM"]`). |
100
- | `presets` | `string[]` | `[]` | Preset IDs to pre-register in the program map, overriding auto-selection. |
101
-
102
- ---
103
-
104
- ### Getters / Setters
105
-
106
- | Property | Type | Description |
107
- |---|---|---|
108
- | `volume` | `number` (get/set) | Master output volume `[0, 1]`. |
109
- | `reverb` | `number` (get/set) | Reverb wet mix `[0, 1]`. |
110
- | `muteExpression` | `boolean` (get/set) | Enable/disable vocal channel muting. |
111
- | `eq` | `EQGains` (get) | Current EQ band gains as a frequency → dB map. |
112
- | `eqFrequencies` | `number[]` (get) | The 10 fixed EQ frequencies: `[32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]`. |
113
- | `channels` | `Record<string, object>` (get) | Active `WebAudioFontPlayer` instances, keyed by MIDI channel number. |
114
- | `channelStates` | `Record<string, boolean>` (get) | Snapshot of which channels are currently playing notes. |
115
- | `catalog` | `Promise<Catalog>` (get) | Resolves the full preset catalog. Equivalent to `getCatalog()`. |
116
-
117
- ---
118
-
119
- ### EQ
120
-
121
- #### `getEQ() → EQGains`
122
-
123
- Returns the current gain (in dB) for all 10 EQ bands.
124
-
125
- ```ts
126
- const gains = player.getEQ();
127
- console.log(gains[32]); // e.g. 6
128
- ```
129
-
130
- #### `setEQ(gains: EQGains) void`
131
-
132
- Updates one or more EQ bands. Unspecified bands are unchanged.
133
-
134
- ```ts
135
- player.setEQ({ 32: 6, 64: 4, 1024: -2 });
136
- ```
137
-
138
- #### `setEQPreset(name: EQPresetName) void`
139
-
140
- Applies a named EQ preset across all 10 bands.
141
-
142
- Available presets: `flat`, `bass`, `treble`, `vocal`, `loudness`, `classical`, `jazz`, `electronic`.
143
-
144
- ```ts
145
- player.setEQPreset('classical');
146
- ```
147
-
148
- #### `setChannelVolume(channel: number, volume: number) → void`
149
-
150
- Overrides the volume multiplier for a specific MIDI channel. Takes effect immediately.
151
-
152
- ```ts
153
- player.setChannelVolume(10, 0); // Mute drums
154
- player.setChannelVolume(1, 0.5); // Half volume on channel 1
155
- ```
156
-
157
- ---
158
-
159
- ### Preset Catalog
160
-
161
- #### `getCatalog() → Promise<Catalog>`
162
-
163
- Downloads and returns the full WebAudioFont preset catalog. Cached in `sessionStorage` after the first fetch.
164
-
165
- ```ts
166
- const catalog = await player.getCatalog();
167
- console.log(catalog.categories.length);
168
- ```
169
-
170
- #### `getCategories() → Promise<CatalogCategory[]>`
171
-
172
- Returns all top-level instrument categories from the catalog.
173
-
174
- ```ts
175
- const categories = await player.getCategories();
176
- categories.forEach(c => console.log(c.name));
177
- ```
178
-
179
- #### `findPreset(id: string) → Promise<PresetInfo | null>`
180
-
181
- Finds a preset by its string ID and returns its full metadata, including `category`, `instrument`, and `program`.
182
-
183
- ```ts
184
- const info = await player.findPreset('0000_FluidR3');
185
- // { id: '0000_...', category: 'Piano', instrument: 'Acoustic Grand Piano', program: 1 }
186
- ```
187
-
188
- #### `loadPreset(presetId: string, channel: number) Promise<void>`
189
-
190
- Replaces the instrument on a specific MIDI channel at runtime.
191
-
192
- ```ts
193
- await player.loadPreset('0000_Steinway', 1);
194
- ```
195
-
196
- ---
197
-
198
- ### Song Loading
199
-
200
- #### `load(content, setup?) → Promise<void>`
201
-
202
- Loads a MIDI file and prepares the audio engine for playback.
203
-
204
- **`content`** accepts:
205
- - A URL string fetched automatically
206
- - An `ArrayBuffer` — e.g. from a `<input type="file">`
207
- - A Base64-encoded MIDI string
208
-
209
- **`setup`** (optional) accepts a `SongSetup` object or a URL to a JSON file,
210
- to restore previously saved preset/volume overrides.
211
-
212
- The loading sequence is:
213
- 1. Stop current playback and clear note registry
214
- 2. Parse and repair the MIDI buffer if necessary
215
- 3. Resolve and download all required instrument presets
216
- 4. Trim leading and trailing silence
217
- 5. Generate karaoke frames (if enabled)
218
- 6. Emit `computed` early, then `presetsLoaded` when audio is fully ready
219
-
220
- ```ts
221
- // From URL
222
- await player.load('https://example.com/song.mid');
223
-
224
- // From file input
225
- const buffer = await file.arrayBuffer();
226
- await player.load(buffer);
227
-
228
- // With a saved setup
229
- await player.load('song.mid', savedSetup);
230
- ```
231
-
232
- #### `getSongSetup() → Promise<SongSetup>`
233
-
234
- Returns a serializable snapshot of the current channel configuration (active preset IDs + per-channel volumes).
235
- Save and pass back to `load()` to reproduce the same instrument mapping.
236
-
237
- ```ts
238
- const setup = await player.getSongSetup();
239
- localStorage.setItem('setup', JSON.stringify(setup));
240
-
241
- // Later:
242
- await player.load('song.mid', JSON.parse(localStorage.getItem('setup')));
243
- ```
244
-
245
- #### `getTrainingPresets() → Promise<string[]>`
246
-
247
- Returns the preset IDs currently registered in the internal program-to-preset override map.
248
-
249
- ---
250
-
251
- ### Playback Control
252
-
253
- #### `play(content?) → Promise<boolean>`
254
-
255
- Starts or resumes playback. If `content` is provided, loads it first.
256
-
257
- Returns `true` on success, `false` if the Web Audio context could not be resumed (browser autoplay restriction).
258
-
259
- ```ts
260
- await player.play(); // Resume
261
- await player.play('https://example.com/song.mid'); // Load and play
262
- ```
263
-
264
- #### `pause() Promise<void>`
265
-
266
- Pauses at the current position. Kills the reverb tail. Resume with `play()`.
267
-
268
- #### `stop() → Promise<MidiAudioPlayer>`
269
-
270
- Stops playback and resets to the beginning. Returns the player instance for chaining.
271
-
272
- #### `skipToSeconds(seconds: number) → Promise<MidiAudioPlayer>`
273
-
274
- Seeks to the specified position in seconds. Works during playback and while paused.
275
- Resumes playback automatically if it was active.
276
-
277
- ```ts
278
- await player.skipToSeconds(45.0);
279
- ```
280
-
281
- ---
282
-
283
- ### Metering & Visualization
284
-
285
- #### `getRealTimeVolume() → number`
286
-
287
- Returns the current normalized output amplitude `[0, 1]`, computed from the `AnalyserNode`.
288
- Useful for driving VU meters or visualizers in a `requestAnimationFrame` loop.
289
-
290
- ```ts
291
- function tick() {
292
- meter.style.width = `${player.getRealTimeVolume() * 100}%`;
293
- requestAnimationFrame(tick);
294
- }
295
- tick();
296
- ```
297
-
298
- #### `getSongTimeRemaining() → number`
299
-
300
- Returns the remaining playback time in seconds.
301
-
302
- #### `generateWaveformSVG(samples?) → Promise<string>`
303
-
304
- Generates an SVG waveform for the currently loaded song, based on note velocities
305
- and expression/volume controller events.
306
-
307
- The returned `<svg>` element has the class `midiaudioplayer-waveform`. Its `<path>` carries no default stroke or fill — apply via CSS.
308
-
309
- ```ts
310
- const svg = await player.generateWaveformSVG(800);
311
- document.getElementById('waveform').innerHTML = svg;
312
- ```
313
-
314
- ```css
315
- .midiaudioplayer-waveform path {
316
- stroke: #6ee7b7;
317
- stroke-width: 1.5;
318
- fill: rgba(110, 231, 183, 0.15);
319
- }
320
- ```
321
-
322
- | Parameter | Type | Default | Description |
323
- |---|---|---|---|
324
- | `samples` | `number` | `1000` | Number of horizontal data points. Higher = finer detail. |
325
-
326
- ---
327
-
328
- ### Lifecycle
329
-
330
- #### `close() → Promise<void>`
331
-
332
- Closes all channel players and the Web Audio context, releasing hardware resources.
333
- The instance cannot be reused after this call.
334
-
335
- ---
336
-
337
- ### Events — `player.on(event, handler)`
338
-
339
- #### `logs`
340
-
341
- Emitted throughout the loading and playback lifecycle with internal status messages.
342
-
343
- ```ts
344
- player.on('logs', (message) => console.log('[midi]', message));
345
- ```
346
-
347
- #### `computed`
348
-
349
- Emitted after MIDI parsing completes, before presets finish downloading.
350
- Use this to update your UI with song metadata immediately.
351
-
352
- ```ts
353
- player.on('computed', ({ title, duration, channels, karaoke }) => {
354
- titleEl.textContent = title || 'Untitled';
355
- durationEl.textContent = `${duration.toFixed(1)}s`;
356
- });
357
- ```
358
-
359
- **Payload:**
360
-
361
- | Field | Type | Description |
362
- |---|---|---|
363
- | `title` | `string` | Song title from MIDI metadata, or empty string. |
364
- | `karaoke` | `boolean` | Whether the file contains parseable lyric data. |
365
- | `tempo` | `number` | Initial tempo in BPM. |
366
- | `division` | `number` | Ticks per quarter-note (PPQ). |
367
- | `duration` | `number` | Total duration in seconds. |
368
- | `sampleRate` | `number` | Web Audio context sample rate. |
369
- | `totalTicks` | `number` | Total MIDI ticks in the song. |
370
- | `totalEvents` | `number` | Total MIDI events across all tracks. |
371
- | `channels` | `Record<string, number>` | Map of channel number → GM program number. |
372
-
373
- #### `presetsLoaded`
374
-
375
- Emitted once all instrument presets have been downloaded and the player is fully ready.
376
-
377
- ```ts
378
- player.on('presetsLoaded', () => playButton.disabled = false);
379
- ```
380
-
381
- #### `endOfFile`
382
-
383
- Emitted when playback reaches the end of the MIDI file.
384
-
385
- ```ts
386
- player.on('endOfFile', () => playNextSong());
387
- ```
388
-
389
- #### `karaoke`
390
-
391
- Emitted for each timed lyric frame when karaoke mode is enabled.
392
- Inject `html` directly into a DOM element. The HTML uses predictable CSS classes:
393
-
394
- | Class | Description |
395
- |---|---|
396
- | `.karaoke-playing` | Syllable currently being sung |
397
- | `.karaoke-played` | Syllables already past |
398
- | `.karaoke-coming` | Upcoming syllables |
399
- | `.karaoke-clear` | Empty frame silence between paragraphs |
400
- | `.karaoke-intro` | Emitted before song start or after `stop()` |
401
- | `.karaoke-title` | Emitted when a title is detected in the MIDI metadata |
402
-
403
- ```ts
404
- const lyricsEl = document.getElementById('lyrics');
405
- player.on('karaoke', ({ html }) => { lyricsEl.innerHTML = html; });
406
- ```
407
-
408
- **Payload:**
409
-
410
- | Field | Type | Description |
411
- |---|---|---|
412
- | `type` | `'lyric' \| 'clear' \| 'intro' \| 'title'` | Frame type. |
413
- | `tick` | `number` | MIDI tick at which this frame should appear. |
414
- | `html` | `string` | HTML string ready for DOM injection. |
415
- | `title` | `string?` | Present only when `type === "title"`. |
416
-
417
- #### `channelState`
418
-
419
- Emitted whenever any MIDI channel transitions between active (sustaining notes) and idle.
420
- Fires only on actual state changes.
421
-
422
- ```ts
423
- player.on('channelState', (states) => {
424
- // { "1": true, "2": false, "10": true }
425
- Object.entries(states).forEach(([ch, active]) => {
426
- document.getElementById(`ch-${ch}`)?.classList.toggle('active', active);
427
- });
428
- });
429
- ```
430
-
431
- ---
432
-
433
- ## Custom Preset Endpoint
434
-
435
- By default, presets are served from the official WebAudioFont CDN.
436
- If you want to self-host — to reduce latency, restrict the available preset set,
437
- or curate your own instrument library — you can deploy the
438
- [sf2-json template](https://github.com/WebAudioFonts/sf2-json) and
439
- point the player at your own server:
440
-
441
- ```ts
442
- const player = new MidiAudioPlayer({
443
- endpoint: 'https://my-cdn.example.com/presets/',
444
- });
445
- ```
446
-
447
- The endpoint must expose:
448
- - `catalog.json` — the full instrument catalog
449
- - `{presetId}.json` — one file per preset
450
-
451
- ---
452
-
453
- ## Browser Compatibility
454
-
455
- Requires a modern browser with:
456
- - [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
457
- - [ES Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
458
- - [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) (optional, for preset caching)
459
-
460
- ---
461
-
462
- ## License
463
-
464
- [MIT](LICENSE)
465
-
466
- ## Author
467
-
468
- Maxime Larrivée-Roy
469
-
470
- ## Repository
471
-
472
- [https://github.com/WebAudioFonts/midi-audio-player](https://github.com/WebAudioFonts/midi-audio-player)
1
+ ![logo](https://webaudiofonts.com/images/logo.svg)
2
+
3
+ # midi-audio-player
4
+
5
+ **Real MIDI playback in the browser — powered by Web Audio API and WebAudioFont.**
6
+
7
+ [See full feature demo here](https://webaudiofonts.com/demo/) / [Explore the full instrument library and listen before you use](https://webaudiofonts.com/catalog/)
8
+
9
+ ![demo](https://webaudiofonts.com/images/demo.webp?2)
10
+
11
+
12
+ No server. No heavy runtime. Just load a `.mid` or `.kar` file and play it — with reverb, a 10-band EQ, per-channel volume control, karaoke support, and over 3,000 instrument presets.
13
+
14
+ ```ts
15
+ const player = new MidiAudioPlayer({ volume: 0.8, reverb: 0.2, eqPreset: 'jazz' });
16
+ player.on('endOfFile', () => playAnotherSong());
17
+ await player.play('https://example.com/song.mid');
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Features
23
+
24
+ - Full General MIDI playback via WebAudioFont instrument presets
25
+ - 3,000+ free instrument presets — piano, strings, brass, synths, drums, and more
26
+ - Convolution reverb with adjustable wet/dry mix
27
+ - 10-band parametric EQ with named presets
28
+ - Per-channel volume control
29
+ - Karaoke mode — parses MIDI text/lyric events and emits timed HTML frames
30
+ - Vocal channel auto-detection and muting
31
+ - Auto-repair for corrupted MIDI files
32
+ - Leading/trailing silence trimming
33
+ - SVG waveform generation
34
+ - Real-time amplitude metering
35
+ - Full MIDI protocol support (pitch bend, controllers, program change, etc.)
36
+ - Works with URLs, `ArrayBuffer`, and Base64 input
37
+ - IndexedDB preset cache — presets are only downloaded once
38
+ - Bring your own preset endpoint (self-hosted sf2-json deployment)
39
+ - ESM-native, compatible with bundlers and vanilla browser environments
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ npm install midi-audio-player
47
+ ```
48
+
49
+ Or via CDN (UMD):
50
+
51
+ ```html
52
+ <script src="https://cdn.jsdelivr.net/npm/midi-audio-player/dist/midi-audio-player.min.js"></script>
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Quick Start
58
+
59
+ ```ts
60
+ import MidiAudioPlayer from 'midi-audio-player';
61
+
62
+ const player = new MidiAudioPlayer({
63
+ volume: 0.7,
64
+ reverb: 0.25,
65
+ });
66
+
67
+ player.on('computed', ({ title, duration }) => {
68
+ console.log(`"${title}" — ${duration.toFixed(1)}s`);
69
+ });
70
+
71
+ player.on('presetsLoaded', () => {
72
+ console.log('All instruments ready.');
73
+ });
74
+
75
+ player.on('endOfFile', () => player.stop());
76
+
77
+ await player.play('https://example.com/song.mid');
78
+ ```
79
+
80
+ > **Note:** `MidiAudioPlayer` must be instantiated in response to a user gesture (click, keydown, etc.), as browsers require user activation before creating a `Web Audio` context.
81
+
82
+ ---
83
+
84
+ ## API Reference
85
+
86
+ ### `new MidiAudioPlayer(opts?)`
87
+
88
+ Creates a new player instance and initializes the full Web Audio signal chain.
89
+
90
+ | Option | Type | Default | Description |
91
+ |---|---|---|---|
92
+ | `endpoint` | `string` | `"https://webaudiofonts.github.io/presets/"` | Base URL of the preset endpoint. Must expose `catalog.json` and individual preset JSON files. [See custom endpoint](#custom-preset-endpoint). |
93
+ | `volume` | `number` | `0.6` | Master output volume `[0, 1]`. Applied with a logarithmic curve. |
94
+ | `reverb` | `number` | `0.3` | Convolution reverb wet level `[0, 1]`. `0` = dry, `1` = full wet. |
95
+ | `localCache` | `boolean` | `true` | Cache the catalog in `sessionStorage` and presets in `IndexedDB`. |
96
+ | `presetRandom` | `boolean` | `false` | Pick a random preset for each MIDI program instead of the first available. |
97
+ | `karaoke` | `boolean` | `false` | Enable karaoke mode. Parses MIDI lyric events and emits timed HTML frames. |
98
+ | `karaokeDelay` | `number` | `0` | Advance karaoke frames by this many seconds (lyrics appear earlier). |
99
+ | `muteExpression` | `boolean` | `false` | Auto-detect and mute the vocal channel. Requires `karaoke: true`. |
100
+ | `maxCharPerLine` | `number` | `48` | Max characters per karaoke line before forcing a line break. |
101
+ | `eqPreset` | `EQPresetName` | `"flat"` | EQ preset applied on instantiation. |
102
+ | `preferred` | `string[]` | `[]` | Ordered list of preferred sound bank suffixes (e.g. `["FluidR3_GM"]`). |
103
+ | `presets` | `string[]` | `[]` | Preset IDs to pre-register in the program map, overriding auto-selection. |
104
+
105
+ ---
106
+
107
+ ### Getters / Setters
108
+
109
+ | Property | Type | Description |
110
+ |---|---|---|
111
+ | `volume` | `number` (get/set) | Master output volume `[0, 1]`. |
112
+ | `reverb` | `number` (get/set) | Reverb wet mix `[0, 1]`. |
113
+ | `muteExpression` | `boolean` (get/set) | Enable/disable vocal channel muting. |
114
+ | `eq` | `EQGains` (get) | Current EQ band gains as a frequency → dB map. |
115
+ | `eqFrequencies` | `number[]` (get) | The 10 fixed EQ frequencies: `[32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]`. |
116
+ | `channels` | `Record<string, object>` (get) | Active `WebAudioFontPlayer` instances, keyed by MIDI channel number. |
117
+ | `channelStates` | `Record<string, boolean>` (get) | Snapshot of which channels are currently playing notes. |
118
+ | `catalog` | `Promise<Catalog>` (get) | Resolves the full preset catalog. Equivalent to `getCatalog()`. |
119
+
120
+ ---
121
+
122
+ ### EQ
123
+
124
+ #### `getEQ() → EQGains`
125
+
126
+ Returns the current gain (in dB) for all 10 EQ bands.
127
+
128
+ ```ts
129
+ const gains = player.getEQ();
130
+ console.log(gains[32]); // e.g. 6
131
+ ```
132
+
133
+ #### `setEQ(gains: EQGains) → void`
134
+
135
+ Updates one or more EQ bands. Unspecified bands are unchanged.
136
+
137
+ ```ts
138
+ player.setEQ({ 32: 6, 64: 4, 1024: -2 });
139
+ ```
140
+
141
+ #### `setEQPreset(name: EQPresetName) → void`
142
+
143
+ Applies a named EQ preset across all 10 bands.
144
+
145
+ Available presets: `flat`, `bass`, `treble`, `vocal`, `loudness`, `classical`, `jazz`, `electronic`.
146
+
147
+ ```ts
148
+ player.setEQPreset('classical');
149
+ ```
150
+
151
+ #### `setChannelVolume(channel: number, volume: number) → void`
152
+
153
+ Overrides the volume multiplier for a specific MIDI channel. Takes effect immediately.
154
+
155
+ ```ts
156
+ player.setChannelVolume(10, 0); // Mute drums
157
+ player.setChannelVolume(1, 0.5); // Half volume on channel 1
158
+ ```
159
+
160
+ ---
161
+
162
+ ### Preset Catalog
163
+
164
+ #### `getCatalog() → Promise<Catalog>`
165
+
166
+ Downloads and returns the full WebAudioFont preset catalog. Cached in `sessionStorage` after the first fetch.
167
+
168
+ ```ts
169
+ const catalog = await player.getCatalog();
170
+ console.log(catalog.categories.length);
171
+ ```
172
+
173
+ #### `getCategories() → Promise<CatalogCategory[]>`
174
+
175
+ Returns all top-level instrument categories from the catalog.
176
+
177
+ ```ts
178
+ const categories = await player.getCategories();
179
+ categories.forEach(c => console.log(c.name));
180
+ ```
181
+
182
+ #### `findPreset(id: string) → Promise<PresetInfo | null>`
183
+
184
+ Finds a preset by its string ID and returns its full metadata, including `category`, `instrument`, and `program`.
185
+
186
+ ```ts
187
+ const info = await player.findPreset('0000_FluidR3');
188
+ // { id: '0000_...', category: 'Piano', instrument: 'Acoustic Grand Piano', program: 1 }
189
+ ```
190
+
191
+ #### `loadPreset(presetId: string, channel: number) → Promise<void>`
192
+
193
+ Replaces the instrument on a specific MIDI channel at runtime.
194
+
195
+ ```ts
196
+ await player.loadPreset('0000_Steinway', 1);
197
+ ```
198
+
199
+ ---
200
+
201
+ ### Song Loading
202
+
203
+ #### `load(content, setup?) → Promise<void>`
204
+
205
+ Loads a MIDI file and prepares the audio engine for playback.
206
+
207
+ **`content`** accepts:
208
+ - A URL string — fetched automatically
209
+ - An `ArrayBuffer` e.g. from a `<input type="file">`
210
+ - A Base64-encoded MIDI string
211
+
212
+ **`setup`** (optional) accepts a `SongSetup` object or a URL to a JSON file,
213
+ to restore previously saved preset/volume overrides.
214
+
215
+ The loading sequence is:
216
+ 1. Stop current playback and clear note registry
217
+ 2. Parse and repair the MIDI buffer if necessary
218
+ 3. Resolve and download all required instrument presets
219
+ 4. Trim leading and trailing silence
220
+ 5. Generate karaoke frames (if enabled)
221
+ 6. Emit `computed` early, then `presetsLoaded` when audio is fully ready
222
+
223
+ ```ts
224
+ // From URL
225
+ await player.load('https://example.com/song.mid');
226
+
227
+ // From file input
228
+ const buffer = await file.arrayBuffer();
229
+ await player.load(buffer);
230
+
231
+ // With a saved setup
232
+ await player.load('song.mid', savedSetup);
233
+ ```
234
+
235
+ #### `getSongSetup() Promise<SongSetup>`
236
+
237
+ Returns a serializable snapshot of the current channel configuration (active preset IDs + per-channel volumes).
238
+ Save and pass back to `load()` to reproduce the same instrument mapping.
239
+
240
+ ```ts
241
+ const setup = await player.getSongSetup();
242
+ localStorage.setItem('setup', JSON.stringify(setup));
243
+
244
+ // Later:
245
+ await player.load('song.mid', JSON.parse(localStorage.getItem('setup')));
246
+ ```
247
+
248
+ #### `getTrainingPresets() → Promise<string[]>`
249
+
250
+ Returns the preset IDs currently registered in the internal program-to-preset override map.
251
+
252
+ ---
253
+
254
+ ### Playback Control
255
+
256
+ #### `play(content?) → Promise<boolean>`
257
+
258
+ Starts or resumes playback. If `content` is provided, loads it first.
259
+
260
+ Returns `true` on success, `false` if the Web Audio context could not be resumed (browser autoplay restriction).
261
+
262
+ ```ts
263
+ await player.play(); // Resume
264
+ await player.play('https://example.com/song.mid'); // Load and play
265
+ ```
266
+
267
+ #### `pause() → Promise<void>`
268
+
269
+ Pauses at the current position. Kills the reverb tail. Resume with `play()`.
270
+
271
+ #### `stop() → Promise<MidiAudioPlayer>`
272
+
273
+ Stops playback and resets to the beginning. Returns the player instance for chaining.
274
+
275
+ #### `skipToSeconds(seconds: number) Promise<MidiAudioPlayer>`
276
+
277
+ Seeks to the specified position in seconds. Works during playback and while paused.
278
+ Resumes playback automatically if it was active.
279
+
280
+ ```ts
281
+ await player.skipToSeconds(45.0);
282
+ ```
283
+
284
+ ---
285
+
286
+ ### Metering & Visualization
287
+
288
+ #### `getRealTimeVolume() number`
289
+
290
+ Returns the current normalized output amplitude `[0, 1]`, computed from the `AnalyserNode`.
291
+ Useful for driving VU meters or visualizers in a `requestAnimationFrame` loop.
292
+
293
+ ```ts
294
+ function tick() {
295
+ meter.style.width = `${player.getRealTimeVolume() * 100}%`;
296
+ requestAnimationFrame(tick);
297
+ }
298
+ tick();
299
+ ```
300
+
301
+ #### `getSongTimeRemaining() → number`
302
+
303
+ Returns the remaining playback time in seconds.
304
+
305
+ #### `generateWaveformSVG(samples?) Promise<string>`
306
+
307
+ Generates an SVG waveform for the currently loaded song, based on note velocities
308
+ and expression/volume controller events.
309
+
310
+ The returned `<svg>` element has the class `midiaudioplayer-waveform`. Its `<path>` carries no default stroke or fill — apply via CSS.
311
+
312
+ ```ts
313
+ const svg = await player.generateWaveformSVG(800);
314
+ document.getElementById('waveform').innerHTML = svg;
315
+ ```
316
+
317
+ ```css
318
+ .midiaudioplayer-waveform path {
319
+ stroke: #6ee7b7;
320
+ stroke-width: 1.5;
321
+ fill: rgba(110, 231, 183, 0.15);
322
+ }
323
+ ```
324
+
325
+ | Parameter | Type | Default | Description |
326
+ |---|---|---|---|
327
+ | `samples` | `number` | `1000` | Number of horizontal data points. Higher = finer detail. |
328
+
329
+ ---
330
+
331
+ ### Lifecycle
332
+
333
+ #### `close() Promise<void>`
334
+
335
+ Closes all channel players and the Web Audio context, releasing hardware resources.
336
+ The instance cannot be reused after this call.
337
+
338
+ ---
339
+
340
+ ### Events — `player.on(event, handler)`
341
+
342
+ #### `logs`
343
+
344
+ Emitted throughout the loading and playback lifecycle with internal status messages.
345
+
346
+ ```ts
347
+ player.on('logs', (message) => console.log('[midi]', message));
348
+ ```
349
+
350
+ #### `computed`
351
+
352
+ Emitted after MIDI parsing completes, before presets finish downloading.
353
+ Use this to update your UI with song metadata immediately.
354
+
355
+ ```ts
356
+ player.on('computed', ({ title, duration, channels, karaoke }) => {
357
+ titleEl.textContent = title || 'Untitled';
358
+ durationEl.textContent = `${duration.toFixed(1)}s`;
359
+ });
360
+ ```
361
+
362
+ **Payload:**
363
+
364
+ | Field | Type | Description |
365
+ |---|---|---|
366
+ | `title` | `string` | Song title from MIDI metadata, or empty string. |
367
+ | `karaoke` | `boolean` | Whether the file contains parseable lyric data. |
368
+ | `tempo` | `number` | Initial tempo in BPM. |
369
+ | `division` | `number` | Ticks per quarter-note (PPQ). |
370
+ | `duration` | `number` | Total duration in seconds. |
371
+ | `sampleRate` | `number` | Web Audio context sample rate. |
372
+ | `totalTicks` | `number` | Total MIDI ticks in the song. |
373
+ | `totalEvents` | `number` | Total MIDI events across all tracks. |
374
+ | `channels` | `Record<string, number>` | Map of channel number → GM program number. |
375
+
376
+ #### `presetsLoaded`
377
+
378
+ Emitted once all instrument presets have been downloaded and the player is fully ready.
379
+
380
+ ```ts
381
+ player.on('presetsLoaded', () => playButton.disabled = false);
382
+ ```
383
+
384
+ #### `endOfFile`
385
+
386
+ Emitted when playback reaches the end of the MIDI file.
387
+
388
+ ```ts
389
+ player.on('endOfFile', () => playNextSong());
390
+ ```
391
+
392
+ #### `karaoke`
393
+
394
+ Emitted for each timed lyric frame when karaoke mode is enabled.
395
+ Inject `html` directly into a DOM element. The HTML uses predictable CSS classes:
396
+
397
+ | Class | Description |
398
+ |---|---|
399
+ | `.karaoke-playing` | Syllable currently being sung |
400
+ | `.karaoke-played` | Syllables already past |
401
+ | `.karaoke-coming` | Upcoming syllables |
402
+ | `.karaoke-clear` | Empty frame — silence between paragraphs |
403
+ | `.karaoke-intro` | Emitted before song start or after `stop()` |
404
+ | `.karaoke-title` | Emitted when a title is detected in the MIDI metadata |
405
+
406
+ ```ts
407
+ const lyricsEl = document.getElementById('lyrics');
408
+ player.on('karaoke', ({ html }) => { lyricsEl.innerHTML = html; });
409
+ ```
410
+
411
+ **Payload:**
412
+
413
+ | Field | Type | Description |
414
+ |---|---|---|
415
+ | `type` | `'lyric' \| 'clear' \| 'intro' \| 'title'` | Frame type. |
416
+ | `tick` | `number` | MIDI tick at which this frame should appear. |
417
+ | `html` | `string` | HTML string ready for DOM injection. |
418
+ | `title` | `string?` | Present only when `type === "title"`. |
419
+
420
+ #### `channelState`
421
+
422
+ Emitted whenever any MIDI channel transitions between active (sustaining notes) and idle.
423
+ Fires only on actual state changes.
424
+
425
+ ```ts
426
+ player.on('channelState', (states) => {
427
+ // { "1": true, "2": false, "10": true }
428
+ Object.entries(states).forEach(([ch, active]) => {
429
+ document.getElementById(`ch-${ch}`)?.classList.toggle('active', active);
430
+ });
431
+ });
432
+ ```
433
+
434
+ ---
435
+
436
+ ## Custom Preset Endpoint
437
+
438
+ By default, presets are served from the official WebAudioFont CDN.
439
+ If you want to self-host to reduce latency, restrict the available preset set,
440
+ or curate your own instrument library — you can deploy the
441
+ [sf2-json template](https://github.com/WebAudioFonts/sf2-json) and
442
+ point the player at your own server:
443
+
444
+ ```ts
445
+ const player = new MidiAudioPlayer({
446
+ endpoint: 'https://my-cdn.example.com/presets/',
447
+ });
448
+ ```
449
+
450
+ The endpoint must expose:
451
+ - `catalog.json` — the full instrument catalog
452
+ - `{presetId}.json` — one file per preset
453
+
454
+ ---
455
+
456
+ ## Browser Compatibility
457
+
458
+ Requires a modern browser with:
459
+ - [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
460
+ - [ES Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
461
+ - [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) (optional, for preset caching)
462
+
463
+ ---
464
+
465
+ ## License
466
+
467
+ [MIT](LICENSE)
468
+
469
+ ## Author
470
+
471
+ Maxime Larrivée-Roy
472
+
473
+ ## Repository
474
+
475
+ [https://github.com/WebAudioFonts/midi-audio-player](https://github.com/WebAudioFonts/midi-audio-player)