libadlmidi-js 1.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.
Files changed (86) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +126 -0
  3. package/dist/core.d.ts +276 -0
  4. package/dist/fm_banks/ail/MonopolyDeluxe.wopl +0 -0
  5. package/dist/fm_banks/ail/master_of_magic.wopl +0 -0
  6. package/dist/fm_banks/manifest.json +60 -0
  7. package/dist/fm_banks/wopl/Apogee-IMF-90.wopl +0 -0
  8. package/dist/fm_banks/wopl/DMXOPL3-by-sneakernets-GS.wopl +0 -0
  9. package/dist/fm_banks/wopl/GM-By-J.A.Nguyen-and-Wohlstand.wopl +0 -0
  10. package/dist/fm_banks/wopl/Wohlstand's-modded-FatMan.wopl +0 -0
  11. package/dist/fm_banks/wopl/fatman-2op.wopl +0 -0
  12. package/dist/fm_banks/wopl/fatman-4op.wopl +0 -0
  13. package/dist/fm_banks/wopl/msadlib.wopl +0 -0
  14. package/dist/libadlmidi.d.ts +453 -0
  15. package/dist/libadlmidi.dosbox.browser.js +2 -0
  16. package/dist/libadlmidi.dosbox.browser.wasm +0 -0
  17. package/dist/libadlmidi.dosbox.core.js +2 -0
  18. package/dist/libadlmidi.dosbox.core.wasm +0 -0
  19. package/dist/libadlmidi.dosbox.js +0 -0
  20. package/dist/libadlmidi.dosbox.processor.js +3226 -0
  21. package/dist/libadlmidi.dosbox.slim.browser.js +2 -0
  22. package/dist/libadlmidi.dosbox.slim.browser.wasm +0 -0
  23. package/dist/libadlmidi.dosbox.slim.core.js +2 -0
  24. package/dist/libadlmidi.dosbox.slim.core.wasm +0 -0
  25. package/dist/libadlmidi.dosbox.slim.js +0 -0
  26. package/dist/libadlmidi.dosbox.slim.processor.js +3226 -0
  27. package/dist/libadlmidi.full.browser.js +2 -0
  28. package/dist/libadlmidi.full.browser.wasm +0 -0
  29. package/dist/libadlmidi.full.core.js +2 -0
  30. package/dist/libadlmidi.full.core.wasm +0 -0
  31. package/dist/libadlmidi.full.js +0 -0
  32. package/dist/libadlmidi.full.processor.js +3226 -0
  33. package/dist/libadlmidi.full.slim.browser.js +2 -0
  34. package/dist/libadlmidi.full.slim.browser.wasm +0 -0
  35. package/dist/libadlmidi.full.slim.core.js +2 -0
  36. package/dist/libadlmidi.full.slim.core.wasm +0 -0
  37. package/dist/libadlmidi.full.slim.js +0 -0
  38. package/dist/libadlmidi.full.slim.processor.js +3226 -0
  39. package/dist/libadlmidi.js +445 -0
  40. package/dist/libadlmidi.js.map +7 -0
  41. package/dist/libadlmidi.light.browser.js +2 -0
  42. package/dist/libadlmidi.light.browser.wasm +0 -0
  43. package/dist/libadlmidi.light.core.js +2 -0
  44. package/dist/libadlmidi.light.core.wasm +0 -0
  45. package/dist/libadlmidi.light.js +0 -0
  46. package/dist/libadlmidi.light.processor.js +3226 -0
  47. package/dist/libadlmidi.light.slim.browser.js +2 -0
  48. package/dist/libadlmidi.light.slim.browser.wasm +0 -0
  49. package/dist/libadlmidi.light.slim.core.js +2 -0
  50. package/dist/libadlmidi.light.slim.core.wasm +0 -0
  51. package/dist/libadlmidi.light.slim.js +0 -0
  52. package/dist/libadlmidi.light.slim.processor.js +3226 -0
  53. package/dist/libadlmidi.nuked.browser.js +2 -0
  54. package/dist/libadlmidi.nuked.browser.wasm +0 -0
  55. package/dist/libadlmidi.nuked.core.js +2 -0
  56. package/dist/libadlmidi.nuked.core.wasm +0 -0
  57. package/dist/libadlmidi.nuked.js +0 -0
  58. package/dist/libadlmidi.nuked.processor.js +3226 -0
  59. package/dist/libadlmidi.nuked.slim.browser.js +2 -0
  60. package/dist/libadlmidi.nuked.slim.browser.wasm +0 -0
  61. package/dist/libadlmidi.nuked.slim.core.js +2 -0
  62. package/dist/libadlmidi.nuked.slim.core.wasm +0 -0
  63. package/dist/libadlmidi.nuked.slim.js +0 -0
  64. package/dist/libadlmidi.nuked.slim.processor.js +3226 -0
  65. package/dist/profiles/dosbox.d.ts +49 -0
  66. package/dist/profiles/dosbox.slim.d.ts +49 -0
  67. package/dist/profiles/full.d.ts +49 -0
  68. package/dist/profiles/full.slim.d.ts +49 -0
  69. package/dist/profiles/light.d.ts +49 -0
  70. package/dist/profiles/light.slim.d.ts +49 -0
  71. package/dist/profiles/nuked.d.ts +49 -0
  72. package/dist/profiles/nuked.slim.d.ts +49 -0
  73. package/dist/utils/struct.d.ts +209 -0
  74. package/package.json +103 -0
  75. package/src/core.js +591 -0
  76. package/src/libadlmidi.js +524 -0
  77. package/src/processor.js +517 -0
  78. package/src/profiles/dosbox.js +82 -0
  79. package/src/profiles/dosbox.slim.js +82 -0
  80. package/src/profiles/full.js +82 -0
  81. package/src/profiles/full.slim.js +82 -0
  82. package/src/profiles/light.js +82 -0
  83. package/src/profiles/light.slim.js +82 -0
  84. package/src/profiles/nuked.js +82 -0
  85. package/src/profiles/nuked.slim.js +82 -0
  86. package/src/utils/struct.js +288 -0
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "libadlmidi-js",
3
+ "version": "1.0.0",
4
+ "libadlmidi": {
5
+ "version": "1.6.2",
6
+ "commit": "221a19d62bb643aac7ceb344712efd09dd88ea86"
7
+ },
8
+ "description": "WebAssembly build of libADLMIDI - OPL3/FM synthesis for the browser",
9
+ "main": "dist/libadlmidi.js",
10
+ "type": "module",
11
+ "exports": {
12
+ ".": "./dist/libadlmidi.js",
13
+ "./core": {
14
+ "import": "./src/core.js",
15
+ "types": "./dist/core.d.ts"
16
+ },
17
+ "./structs": {
18
+ "import": "./src/utils/struct.js",
19
+ "types": "./dist/utils/struct.d.ts"
20
+ },
21
+ "./nuked": {
22
+ "import": "./src/profiles/nuked.js"
23
+ },
24
+ "./nuked/slim": {
25
+ "import": "./src/profiles/nuked.slim.js"
26
+ },
27
+ "./dosbox": {
28
+ "import": "./src/profiles/dosbox.js"
29
+ },
30
+ "./dosbox/slim": {
31
+ "import": "./src/profiles/dosbox.slim.js"
32
+ },
33
+ "./light": {
34
+ "import": "./src/profiles/light.js"
35
+ },
36
+ "./light/slim": {
37
+ "import": "./src/profiles/light.slim.js"
38
+ },
39
+ "./full": {
40
+ "import": "./src/profiles/full.js"
41
+ },
42
+ "./full/slim": {
43
+ "import": "./src/profiles/full.slim.js"
44
+ },
45
+ "./dist/*": "./dist/*"
46
+ },
47
+ "scripts": {
48
+ "build:wasm": "bash build.sh all",
49
+ "build:wasm:nuked": "bash build.sh nuked",
50
+ "build:wasm:dosbox": "bash build.sh dosbox",
51
+ "build:wasm:light": "bash build.sh light",
52
+ "build:wasm:full": "bash build.sh full",
53
+ "build:js": "node bundle.js",
54
+ "build:types": "tsc",
55
+ "build:banks": "node scripts/copy-banks.js",
56
+ "build": "npm run build:wasm && npm run build:js && npm run build:types && npm run build:banks",
57
+ "clean": "rm -rf build-* dist/*.js dist/*.wasm dist/*.d.ts dist/*.map dist/fm_banks",
58
+ "test": "npm run build:types && vitest run",
59
+ "test:watch": "vitest",
60
+ "test:browser": "playwright test",
61
+ "test:bundler": "cd tests/bundler-smoke && npm install && npm test",
62
+ "test:all": "npm test && npm run test:browser && npm run test:bundler",
63
+ "typecheck": "tsc --noEmit",
64
+ "lint": "eslint .",
65
+ "lint:fix": "eslint . --fix",
66
+ "check": "npm run lint && npm run typecheck && npm run sync-version -- --check",
67
+ "sync-version": "node scripts/sync-libadlmidi-version.js",
68
+ "prepare": "command -v pre-commit >/dev/null && pre-commit install || echo 'pre-commit not found, run: pipx install pre-commit'"
69
+ },
70
+ "types": "dist/libadlmidi.d.ts",
71
+ "keywords": [
72
+ "midi",
73
+ "opl3",
74
+ "fm-synthesis",
75
+ "adlib",
76
+ "audio",
77
+ "webassembly",
78
+ "wasm",
79
+ "audioworklet"
80
+ ],
81
+ "author": "Tony Gies <tgies@tgies.net>",
82
+ "license": "LGPL-3.0",
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "https://github.com/libadlmidi-js/libadlmidi-js.git"
86
+ },
87
+ "devDependencies": {
88
+ "@eslint/js": "^9.39.2",
89
+ "@playwright/test": "^1.57.0",
90
+ "@types/node": "^25.0.3",
91
+ "esbuild": "^0.27.2",
92
+ "eslint": "^9.39.2",
93
+ "globals": "^16.5.0",
94
+ "playwright": "^1.57.0",
95
+ "typescript": "^5.9.3",
96
+ "vitest": "^4.0.16"
97
+ },
98
+ "files": [
99
+ "dist/",
100
+ "src/",
101
+ "README.md"
102
+ ]
103
+ }
package/src/core.js ADDED
@@ -0,0 +1,591 @@
1
+ /**
2
+ * AdlMidiCore - Low-level platform-agnostic synthesis interface
3
+ *
4
+ * Provides direct access to libADLMIDI WASM for use cases that don't need
5
+ * WebAudio/AudioWorklet integration (e.g., Node.js batch rendering,
6
+ * custom audio backends, game engines).
7
+ *
8
+ * @module core
9
+ */
10
+
11
+ import {
12
+ SIZEOF_ADL_INSTRUMENT,
13
+ SIZEOF_ADL_BANK_ID,
14
+ decodeInstrument,
15
+ encodeInstrument,
16
+ } from './utils/struct.js';
17
+
18
+ /**
19
+ * Low-level OPL3 synthesis interface.
20
+ *
21
+ * Unlike AdlMidi (which manages AudioWorklet), AdlMidiCore works directly
22
+ * with the WASM module and returns raw audio samples.
23
+ *
24
+ * @example
25
+ * ```javascript
26
+ * import { AdlMidiCore } from 'libadlmidi-js/core';
27
+ *
28
+ * // Load with specific WASM profile
29
+ * const synth = await AdlMidiCore.create({
30
+ * corePath: './node_modules/libadlmidi-js/dist/libadlmidi.nuked.core.js'
31
+ * });
32
+ *
33
+ * await synth.init(44100);
34
+ * synth.setBank(72);
35
+ * synth.noteOn(0, 60, 100);
36
+ *
37
+ * // Generate audio
38
+ * const samples = synth.generate(4096); // Float32Array, stereo interleaved
39
+ *
40
+ * synth.close();
41
+ * ```
42
+ */
43
+ export class AdlMidiCore {
44
+ /**
45
+ * Create a new AdlMidiCore instance.
46
+ *
47
+ * @param {Object} options
48
+ * @param {string} options.corePath - Path to the .core.js WASM loader module
49
+ * @param {ArrayBuffer} [options.wasmBinary] - Pre-loaded WASM binary (optional)
50
+ * @returns {Promise<AdlMidiCore>}
51
+ */
52
+ static async create(options) {
53
+ if (!options?.corePath) {
54
+ throw new Error('AdlMidiCore.create requires corePath option');
55
+ }
56
+
57
+ const core = new AdlMidiCore();
58
+
59
+ // Dynamically import the WASM loader module
60
+ const { default: createADLMIDI } = await import(options.corePath);
61
+
62
+ // Initialize the Emscripten module
63
+ const moduleConfig = options.wasmBinary
64
+ ? { wasmBinary: options.wasmBinary }
65
+ : undefined;
66
+
67
+ core._module = await createADLMIDI(moduleConfig);
68
+ core._player = null;
69
+ core._sampleRate = 44100;
70
+ core._audioBuffer = null;
71
+ core._audioBufferPtr = null;
72
+
73
+ return core;
74
+ }
75
+
76
+ constructor() {
77
+ /** @private @type {any} */
78
+ this._module = null;
79
+ /** @private @type {number|null} */
80
+ this._player = null;
81
+ /** @private @type {number} */
82
+ this._sampleRate = 44100;
83
+ /** @private @type {Float32Array|null} */
84
+ this._audioBuffer = null;
85
+ /** @private @type {number|null} */
86
+ this._audioBufferPtr = null;
87
+ }
88
+
89
+ /**
90
+ * Initialize the synthesizer.
91
+ *
92
+ * @param {number} [sampleRate=44100] - Audio sample rate
93
+ * @returns {boolean} True if successful
94
+ */
95
+ init(sampleRate = 44100) {
96
+ if (!this._module) {
97
+ throw new Error('AdlMidiCore not initialized - use AdlMidiCore.create()');
98
+ }
99
+
100
+ this._sampleRate = sampleRate;
101
+ this._player = this._module._adl_init(sampleRate);
102
+
103
+ if (!this._player) {
104
+ throw new Error('Failed to initialize ADL MIDI player');
105
+ }
106
+
107
+ return true;
108
+ }
109
+
110
+ /**
111
+ * Close the synthesizer and free resources.
112
+ */
113
+ close() {
114
+ if (this._audioBufferPtr) {
115
+ this._module._free(this._audioBufferPtr);
116
+ this._audioBufferPtr = null;
117
+ this._audioBuffer = null;
118
+ }
119
+
120
+ if (this._player) {
121
+ this._module._adl_close(this._player);
122
+ this._player = null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Reset the synthesizer state (stop all notes).
128
+ */
129
+ reset() {
130
+ this._ensurePlayer();
131
+ this._module._adl_rt_resetState(this._player);
132
+ }
133
+
134
+ /**
135
+ * Panic - immediately stop all notes.
136
+ */
137
+ panic() {
138
+ this._ensurePlayer();
139
+ this._module._adl_panic(this._player);
140
+ }
141
+
142
+ // =========================================================================
143
+ // Configuration
144
+ // =========================================================================
145
+
146
+ /**
147
+ * Set the FM bank by index.
148
+ *
149
+ * @param {number} bank - Bank index (0-72+ depending on build)
150
+ * @returns {boolean} True if successful
151
+ */
152
+ setBank(bank) {
153
+ this._ensurePlayer();
154
+ return this._module._adl_setBank(this._player, bank) === 0;
155
+ }
156
+
157
+ /**
158
+ * Load a custom WOPL bank from data.
159
+ *
160
+ * @param {ArrayBuffer|Uint8Array} data - WOPL bank data
161
+ * @returns {boolean} True if successful
162
+ */
163
+ loadBankData(data) {
164
+ this._ensurePlayer();
165
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
166
+
167
+ const ptr = this._module._malloc(bytes.length);
168
+ this._module.HEAPU8.set(bytes, ptr);
169
+
170
+ const result = this._module._adl_openBankData(this._player, ptr, bytes.length);
171
+
172
+ this._module._free(ptr);
173
+ return result === 0;
174
+ }
175
+
176
+ /**
177
+ * Get the number of available embedded banks.
178
+ *
179
+ * @returns {number} Number of banks
180
+ */
181
+ getBankCount() {
182
+ return this._module._adl_getBanksCount();
183
+ }
184
+
185
+ /**
186
+ * Set the number of emulated OPL3 chips.
187
+ *
188
+ * @param {number} count - Number of chips (1-100)
189
+ * @returns {boolean} True if successful
190
+ */
191
+ setNumChips(count) {
192
+ this._ensurePlayer();
193
+ return this._module._adl_setNumChips(this._player, count) === 0;
194
+ }
195
+
196
+ /**
197
+ * Enable/disable soft stereo panning.
198
+ *
199
+ * @param {boolean} enabled
200
+ */
201
+ setSoftPan(enabled) {
202
+ this._ensurePlayer();
203
+ this._module._adl_setSoftPanEnabled(this._player, enabled ? 1 : 0);
204
+ }
205
+
206
+ /**
207
+ * Enable/disable deep vibrato.
208
+ *
209
+ * @param {boolean} enabled
210
+ */
211
+ setDeepVibrato(enabled) {
212
+ this._ensurePlayer();
213
+ this._module._adl_setHVibrato(this._player, enabled ? 1 : 0);
214
+ }
215
+
216
+ /**
217
+ * Enable/disable deep tremolo.
218
+ *
219
+ * @param {boolean} enabled
220
+ */
221
+ setDeepTremolo(enabled) {
222
+ this._ensurePlayer();
223
+ this._module._adl_setHTremolo(this._player, enabled ? 1 : 0);
224
+ }
225
+
226
+ /**
227
+ * Switch OPL3 emulator (if multiple are compiled in).
228
+ *
229
+ * @param {number} emulator - Emulator ID (0=Nuked, 2=DosBox, etc.)
230
+ * @returns {boolean} True if successful
231
+ */
232
+ switchEmulator(emulator) {
233
+ this._ensurePlayer();
234
+ return this._module._adl_switchEmulator(this._player, emulator) === 0;
235
+ }
236
+
237
+ /**
238
+ * Get the current chip emulator name.
239
+ *
240
+ * @returns {string} Emulator name
241
+ */
242
+ getEmulatorName() {
243
+ this._ensurePlayer();
244
+ const ptr = this._module._adl_chipEmulatorName(this._player);
245
+ return this._module.UTF8ToString(ptr);
246
+ }
247
+
248
+ // =========================================================================
249
+ // Real-time Synthesis
250
+ // =========================================================================
251
+
252
+ /**
253
+ * Trigger a note on.
254
+ *
255
+ * @param {number} channel - MIDI channel (0-15)
256
+ * @param {number} note - MIDI note (0-127)
257
+ * @param {number} velocity - Velocity (0-127)
258
+ */
259
+ noteOn(channel, note, velocity) {
260
+ this._ensurePlayer();
261
+ this._module._adl_rt_noteOn(this._player, channel, note, velocity);
262
+ }
263
+
264
+ /**
265
+ * Trigger a note off.
266
+ *
267
+ * @param {number} channel - MIDI channel (0-15)
268
+ * @param {number} note - MIDI note (0-127)
269
+ */
270
+ noteOff(channel, note) {
271
+ this._ensurePlayer();
272
+ this._module._adl_rt_noteOff(this._player, channel, note);
273
+ }
274
+
275
+ /**
276
+ * Send a pitch bend message.
277
+ *
278
+ * @param {number} channel - MIDI channel (0-15)
279
+ * @param {number} value - Pitch bend value (0-16383, 8192 = center)
280
+ */
281
+ pitchBend(channel, value) {
282
+ this._ensurePlayer();
283
+ this._module._adl_rt_pitchBend(this._player, channel, value);
284
+ }
285
+
286
+ /**
287
+ * Send a controller change message.
288
+ *
289
+ * @param {number} channel - MIDI channel (0-15)
290
+ * @param {number} controller - Controller number (0-127)
291
+ * @param {number} value - Controller value (0-127)
292
+ */
293
+ controllerChange(channel, controller, value) {
294
+ this._ensurePlayer();
295
+ this._module._adl_rt_controllerChange(this._player, channel, controller, value);
296
+ }
297
+
298
+ /**
299
+ * Send a program change message.
300
+ *
301
+ * @param {number} channel - MIDI channel (0-15)
302
+ * @param {number} program - Program number (0-127)
303
+ */
304
+ programChange(channel, program) {
305
+ this._ensurePlayer();
306
+ this._module._adl_rt_patchChange(this._player, channel, program);
307
+ }
308
+
309
+ /**
310
+ * Generate audio samples (real-time synthesis).
311
+ *
312
+ * @param {number} frames - Number of stereo frames to generate
313
+ * @returns {Float32Array} Stereo interleaved audio samples (-1 to +1)
314
+ */
315
+ generate(frames) {
316
+ this._ensurePlayer();
317
+
318
+ const samples = frames * 2; // Stereo
319
+ const bytes = samples * 2; // Int16
320
+
321
+ // Allocate/reuse buffer
322
+ if (!this._audioBufferPtr || !this._audioBuffer || this._audioBuffer.length < samples) {
323
+ if (this._audioBufferPtr) {
324
+ this._module._free(this._audioBufferPtr);
325
+ }
326
+ this._audioBufferPtr = this._module._malloc(bytes);
327
+ this._audioBuffer = new Float32Array(samples);
328
+ }
329
+
330
+ // Generate audio (Int16 output)
331
+ this._module._adl_generate(this._player, samples, this._audioBufferPtr);
332
+
333
+ // Convert Int16 to Float32
334
+ const heap16 = this._module.HEAP16;
335
+ const offset = /** @type {number} */ (this._audioBufferPtr) >> 1; // Byte offset to Int16 offset
336
+ for (let i = 0; i < samples; i++) {
337
+ /** @type {Float32Array} */ (this._audioBuffer)[i] = heap16[offset + i] / 32768;
338
+ }
339
+
340
+ return /** @type {Float32Array} */ (this._audioBuffer).slice(0, samples);
341
+ }
342
+
343
+ // =========================================================================
344
+ // MIDI File Playback
345
+ // =========================================================================
346
+
347
+ /**
348
+ * Load a MIDI file from data.
349
+ *
350
+ * @param {ArrayBuffer|Uint8Array} data - MIDI file data
351
+ * @returns {boolean} True if successful
352
+ */
353
+ loadMidi(data) {
354
+ this._ensurePlayer();
355
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
356
+
357
+ const ptr = this._module._malloc(bytes.length);
358
+ this._module.HEAPU8.set(bytes, ptr);
359
+
360
+ const result = this._module._adl_openData(this._player, ptr, bytes.length);
361
+
362
+ this._module._free(ptr);
363
+ return result === 0;
364
+ }
365
+
366
+ /**
367
+ * Play MIDI file and generate audio.
368
+ *
369
+ * @param {number} frames - Number of stereo frames to generate
370
+ * @returns {Float32Array} Stereo interleaved audio samples (-1 to +1)
371
+ */
372
+ play(frames) {
373
+ this._ensurePlayer();
374
+
375
+ const samples = frames * 2;
376
+ const bytes = samples * 2;
377
+
378
+ if (!this._audioBufferPtr || !this._audioBuffer || this._audioBuffer.length < samples) {
379
+ if (this._audioBufferPtr) {
380
+ this._module._free(this._audioBufferPtr);
381
+ }
382
+ this._audioBufferPtr = this._module._malloc(bytes);
383
+ this._audioBuffer = new Float32Array(samples);
384
+ }
385
+
386
+ this._module._adl_play(this._player, samples, this._audioBufferPtr);
387
+
388
+ const heap16 = this._module.HEAP16;
389
+ const offset = /** @type {number} */ (this._audioBufferPtr) >> 1;
390
+ for (let i = 0; i < samples; i++) {
391
+ /** @type {Float32Array} */ (this._audioBuffer)[i] = heap16[offset + i] / 32768;
392
+ }
393
+
394
+ return /** @type {Float32Array} */ (this._audioBuffer).slice(0, samples);
395
+ }
396
+
397
+ /**
398
+ * Get the current playback position in seconds.
399
+ *
400
+ * @returns {number} Position in seconds
401
+ */
402
+ get position() {
403
+ this._ensurePlayer();
404
+ return this._module._adl_positionTell(this._player);
405
+ }
406
+
407
+ /**
408
+ * Get the total duration in seconds.
409
+ *
410
+ * @returns {number} Duration in seconds
411
+ */
412
+ get duration() {
413
+ this._ensurePlayer();
414
+ return this._module._adl_totalTimeLength(this._player);
415
+ }
416
+
417
+ /**
418
+ * Seek to a position.
419
+ *
420
+ * @param {number} seconds - Position in seconds
421
+ */
422
+ seek(seconds) {
423
+ this._ensurePlayer();
424
+ this._module._adl_positionSeek(this._player, seconds);
425
+ }
426
+
427
+ /**
428
+ * Rewind to the beginning.
429
+ */
430
+ rewind() {
431
+ this._ensurePlayer();
432
+ this._module._adl_positionRewind(this._player);
433
+ }
434
+
435
+ /**
436
+ * Check if playback has reached the end.
437
+ *
438
+ * @returns {boolean} True if at end
439
+ */
440
+ get atEnd() {
441
+ this._ensurePlayer();
442
+ return this._module._adl_atEnd(this._player) !== 0;
443
+ }
444
+
445
+ /**
446
+ * Enable/disable looping.
447
+ *
448
+ * @param {boolean} enabled
449
+ */
450
+ setLooping(enabled) {
451
+ this._ensurePlayer();
452
+ this._module._adl_setLoopEnabled(this._player, enabled ? 1 : 0);
453
+ }
454
+
455
+ /**
456
+ * Set playback tempo multiplier.
457
+ *
458
+ * @param {number} tempo - Tempo multiplier (1.0 = normal)
459
+ */
460
+ setTempo(tempo) {
461
+ this._ensurePlayer();
462
+ this._module._adl_setTempo(this._player, tempo);
463
+ }
464
+
465
+ // =========================================================================
466
+ // Instrument Access
467
+ // =========================================================================
468
+
469
+ /**
470
+ * Get an instrument from a bank.
471
+ *
472
+ * @param {Object} bankId - Bank identifier
473
+ * @param {number} bankId.percussive - 0 for melodic, 1 for percussion
474
+ * @param {number} bankId.msb - Bank MSB
475
+ * @param {number} bankId.lsb - Bank LSB
476
+ * @param {number} program - Program number (0-127)
477
+ * @returns {import('./utils/struct.js').Instrument|null} Instrument or null if not found
478
+ */
479
+ getInstrument(bankId, program) {
480
+ this._ensurePlayer();
481
+
482
+ // Allocate bank ID struct
483
+ const bankIdPtr = this._module._malloc(SIZEOF_ADL_BANK_ID);
484
+ this._module.HEAPU8[bankIdPtr] = bankId.percussive || 0;
485
+ this._module.HEAPU8[bankIdPtr + 1] = bankId.msb || 0;
486
+ this._module.HEAPU8[bankIdPtr + 2] = bankId.lsb || 0;
487
+
488
+ // Allocate instrument struct
489
+ const instPtr = this._module._malloc(SIZEOF_ADL_INSTRUMENT);
490
+
491
+ const result = this._module._adl_getInstrument(
492
+ this._player,
493
+ bankIdPtr,
494
+ program,
495
+ instPtr
496
+ );
497
+
498
+ let instrument = null;
499
+ if (result === 0) {
500
+ const bytes = this._module.HEAPU8.slice(instPtr, instPtr + SIZEOF_ADL_INSTRUMENT);
501
+ instrument = decodeInstrument(bytes);
502
+ }
503
+
504
+ this._module._free(bankIdPtr);
505
+ this._module._free(instPtr);
506
+
507
+ return instrument;
508
+ }
509
+
510
+ /**
511
+ * Set an instrument in a bank.
512
+ *
513
+ * @param {Object} bankId - Bank identifier
514
+ * @param {number} bankId.percussive - 0 for melodic, 1 for percussion
515
+ * @param {number} bankId.msb - Bank MSB
516
+ * @param {number} bankId.lsb - Bank LSB
517
+ * @param {number} program - Program number (0-127)
518
+ * @param {import('./utils/struct.js').Instrument} instrument - Instrument to set
519
+ * @returns {boolean} True if successful
520
+ */
521
+ setInstrument(bankId, program, instrument) {
522
+ this._ensurePlayer();
523
+
524
+ // Allocate bank ID struct
525
+ const bankIdPtr = this._module._malloc(SIZEOF_ADL_BANK_ID);
526
+ this._module.HEAPU8[bankIdPtr] = bankId.percussive || 0;
527
+ this._module.HEAPU8[bankIdPtr + 1] = bankId.msb || 0;
528
+ this._module.HEAPU8[bankIdPtr + 2] = bankId.lsb || 0;
529
+
530
+ // Encode and write instrument
531
+ const bytes = encodeInstrument(instrument);
532
+ const instPtr = this._module._malloc(SIZEOF_ADL_INSTRUMENT);
533
+ this._module.HEAPU8.set(bytes, instPtr);
534
+
535
+ const result = this._module._adl_setInstrument(
536
+ this._player,
537
+ bankIdPtr,
538
+ program,
539
+ instPtr
540
+ );
541
+
542
+ this._module._free(bankIdPtr);
543
+ this._module._free(instPtr);
544
+
545
+ return result === 0;
546
+ }
547
+
548
+ // =========================================================================
549
+ // Direct Module Access
550
+ // =========================================================================
551
+
552
+ /**
553
+ * Get the raw Emscripten module for advanced usage.
554
+ *
555
+ * @returns {Object} The raw WASM module
556
+ */
557
+ get module() {
558
+ return this._module;
559
+ }
560
+
561
+ /**
562
+ * Get the raw player pointer for advanced usage.
563
+ *
564
+ * @returns {number|null} Player pointer
565
+ */
566
+ get player() {
567
+ return this._player;
568
+ }
569
+
570
+ /**
571
+ * Get the sample rate.
572
+ *
573
+ * @returns {number} Sample rate in Hz
574
+ */
575
+ get sampleRate() {
576
+ return this._sampleRate;
577
+ }
578
+
579
+ // =========================================================================
580
+ // Private
581
+ // =========================================================================
582
+
583
+ /** @private */
584
+ _ensurePlayer() {
585
+ if (!this._player) {
586
+ throw new Error('Synthesizer not initialized - call init() first');
587
+ }
588
+ }
589
+ }
590
+
591
+ export default AdlMidiCore;