midi-audio-player 1.1.2 → 2.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ ## Changelog
2
+
3
+ ### [2.0.0] - Unreleased
4
+
5
+ #### Changed
6
+ * Complete refactor
7
+
8
+
9
+ ### [1.1.2] - 2026-05-10 00:19:40
10
+
11
+ #### Changed
12
+ * load() method now public
13
+
14
+
15
+ ### [1.1.1] - 2026-05-09 19:45:23
16
+
17
+ #### Added
18
+ * index.js in the root
19
+
20
+
21
+ ### [1.1.0] - 2026-05-09 17:16:45
22
+
23
+ #### Added
24
+ * Complete refactor
25
+ * Optimized WebAudioFont handling
26
+ * Change instrument option to preset
package/README.md CHANGED
@@ -1,86 +1,472 @@
1
- # midi-audio-player
2
-
3
- A lightweight JavaScript MIDI audio player built on top of the Web Audio API using WebAudioFont. This package enables playback of MIDI files directly in the browser with minimal setup and no heavy dependencies.
4
-
5
- ## Features
6
-
7
- * MIDI file playback in modern browsers
8
- * Built on the Web Audio API
9
- * Uses WebAudioFont for preset rendering
10
- * Lightweight and dependency-minimal
11
- * Simple programmatic API
12
- * CLI tool for preset/font handling
13
-
14
- ## Installation
15
-
16
- ```bash
17
- npm install midi-audio-player
18
- ```
19
-
20
- ## Usage
21
-
22
- ### Basic Example
23
-
24
- ```js
25
- import MidiAudioPlayer from 'midi-audio-player';
26
-
27
- const response = await fetch('/examples/data/iwillsurvive.mid');
28
- const buffer = await response.arrayBuffer();
29
-
30
- const player = new MidiAudioPlayer({
31
- preset: presetData,
32
- volume: 0.02,
33
- onEndFile: async () => await this.playNextSong()
34
- });
35
-
36
- player.play(buffer);
37
- ```
38
-
39
- ### Control Playback
40
-
41
- ```js
42
- player.play();
43
- player.pause();
44
- player.stop();
45
- ```
46
-
47
- ### Working with AudioContext
48
-
49
- Due to browser autoplay restrictions, you should ensure that your AudioContext is resumed after a user interaction.
50
-
51
- ## CLI
52
-
53
- This package provides a CLI tool for downloading and converting WebAudioFont assets. You need to provide a WebAudioFont ID and the json file for the destination.
54
-
55
- ```bash
56
- webaudiofont 0000_Chaos_sf2_file dest/preset.json
57
- ```
58
-
59
- You can find presets here: [WebAudioFont](https://github.com/surikov/webaudiofont#catalog-of-instruments)
60
-
61
- ## Browser Compatibility
62
-
63
- Requires a modern browser with support for:
64
-
65
- * Web Audio API
66
- * ES Modules
67
-
68
- ## Limitations
69
-
70
- * First playback may be delayed if the AudioContext is not initialized properly
71
- * Depends on WebAudioFont instrument quality and availability
72
- * Not intended for high-fidelity or professional audio rendering
73
-
74
-
75
- ## License
76
-
77
- [LICENCE](LICENSE)
78
-
79
- ## Author
80
-
81
- Maxime Larrivée-Roy
82
-
83
- ## Repository
84
-
85
- [https://github.com/ZmotriN/midi-audio-player](https://github.com/ZmotriN/midi-audio-player)
86
-
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)