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.
- package/LICENSE +165 -0
- package/README.md +126 -0
- package/dist/core.d.ts +276 -0
- package/dist/fm_banks/ail/MonopolyDeluxe.wopl +0 -0
- package/dist/fm_banks/ail/master_of_magic.wopl +0 -0
- package/dist/fm_banks/manifest.json +60 -0
- package/dist/fm_banks/wopl/Apogee-IMF-90.wopl +0 -0
- package/dist/fm_banks/wopl/DMXOPL3-by-sneakernets-GS.wopl +0 -0
- package/dist/fm_banks/wopl/GM-By-J.A.Nguyen-and-Wohlstand.wopl +0 -0
- package/dist/fm_banks/wopl/Wohlstand's-modded-FatMan.wopl +0 -0
- package/dist/fm_banks/wopl/fatman-2op.wopl +0 -0
- package/dist/fm_banks/wopl/fatman-4op.wopl +0 -0
- package/dist/fm_banks/wopl/msadlib.wopl +0 -0
- package/dist/libadlmidi.d.ts +453 -0
- package/dist/libadlmidi.dosbox.browser.js +2 -0
- package/dist/libadlmidi.dosbox.browser.wasm +0 -0
- package/dist/libadlmidi.dosbox.core.js +2 -0
- package/dist/libadlmidi.dosbox.core.wasm +0 -0
- package/dist/libadlmidi.dosbox.js +0 -0
- package/dist/libadlmidi.dosbox.processor.js +3226 -0
- package/dist/libadlmidi.dosbox.slim.browser.js +2 -0
- package/dist/libadlmidi.dosbox.slim.browser.wasm +0 -0
- package/dist/libadlmidi.dosbox.slim.core.js +2 -0
- package/dist/libadlmidi.dosbox.slim.core.wasm +0 -0
- package/dist/libadlmidi.dosbox.slim.js +0 -0
- package/dist/libadlmidi.dosbox.slim.processor.js +3226 -0
- package/dist/libadlmidi.full.browser.js +2 -0
- package/dist/libadlmidi.full.browser.wasm +0 -0
- package/dist/libadlmidi.full.core.js +2 -0
- package/dist/libadlmidi.full.core.wasm +0 -0
- package/dist/libadlmidi.full.js +0 -0
- package/dist/libadlmidi.full.processor.js +3226 -0
- package/dist/libadlmidi.full.slim.browser.js +2 -0
- package/dist/libadlmidi.full.slim.browser.wasm +0 -0
- package/dist/libadlmidi.full.slim.core.js +2 -0
- package/dist/libadlmidi.full.slim.core.wasm +0 -0
- package/dist/libadlmidi.full.slim.js +0 -0
- package/dist/libadlmidi.full.slim.processor.js +3226 -0
- package/dist/libadlmidi.js +445 -0
- package/dist/libadlmidi.js.map +7 -0
- package/dist/libadlmidi.light.browser.js +2 -0
- package/dist/libadlmidi.light.browser.wasm +0 -0
- package/dist/libadlmidi.light.core.js +2 -0
- package/dist/libadlmidi.light.core.wasm +0 -0
- package/dist/libadlmidi.light.js +0 -0
- package/dist/libadlmidi.light.processor.js +3226 -0
- package/dist/libadlmidi.light.slim.browser.js +2 -0
- package/dist/libadlmidi.light.slim.browser.wasm +0 -0
- package/dist/libadlmidi.light.slim.core.js +2 -0
- package/dist/libadlmidi.light.slim.core.wasm +0 -0
- package/dist/libadlmidi.light.slim.js +0 -0
- package/dist/libadlmidi.light.slim.processor.js +3226 -0
- package/dist/libadlmidi.nuked.browser.js +2 -0
- package/dist/libadlmidi.nuked.browser.wasm +0 -0
- package/dist/libadlmidi.nuked.core.js +2 -0
- package/dist/libadlmidi.nuked.core.wasm +0 -0
- package/dist/libadlmidi.nuked.js +0 -0
- package/dist/libadlmidi.nuked.processor.js +3226 -0
- package/dist/libadlmidi.nuked.slim.browser.js +2 -0
- package/dist/libadlmidi.nuked.slim.browser.wasm +0 -0
- package/dist/libadlmidi.nuked.slim.core.js +2 -0
- package/dist/libadlmidi.nuked.slim.core.wasm +0 -0
- package/dist/libadlmidi.nuked.slim.js +0 -0
- package/dist/libadlmidi.nuked.slim.processor.js +3226 -0
- package/dist/profiles/dosbox.d.ts +49 -0
- package/dist/profiles/dosbox.slim.d.ts +49 -0
- package/dist/profiles/full.d.ts +49 -0
- package/dist/profiles/full.slim.d.ts +49 -0
- package/dist/profiles/light.d.ts +49 -0
- package/dist/profiles/light.slim.d.ts +49 -0
- package/dist/profiles/nuked.d.ts +49 -0
- package/dist/profiles/nuked.slim.d.ts +49 -0
- package/dist/utils/struct.d.ts +209 -0
- package/package.json +103 -0
- package/src/core.js +591 -0
- package/src/libadlmidi.js +524 -0
- package/src/processor.js +517 -0
- package/src/profiles/dosbox.js +82 -0
- package/src/profiles/dosbox.slim.js +82 -0
- package/src/profiles/full.js +82 -0
- package/src/profiles/full.slim.js +82 -0
- package/src/profiles/light.js +82 -0
- package/src/profiles/light.slim.js +82 -0
- package/src/profiles/nuked.js +82 -0
- package/src/profiles/nuked.slim.js +82 -0
- 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;
|