libadlmidi-js 1.2.0 → 2.1.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/README.md +8 -5
- package/dist/core.d.ts +191 -4
- package/dist/fm_banks/manifest.json +1 -1
- package/dist/libadlmidi.d.ts +146 -66
- package/dist/libadlmidi.dosbox.browser.js +1 -1
- package/dist/libadlmidi.dosbox.browser.wasm +0 -0
- package/dist/libadlmidi.dosbox.core.js +1 -1
- package/dist/libadlmidi.dosbox.core.wasm +0 -0
- package/dist/libadlmidi.dosbox.js +0 -0
- package/dist/libadlmidi.dosbox.processor.js +247 -74
- package/dist/libadlmidi.dosbox.slim.browser.js +1 -1
- package/dist/libadlmidi.dosbox.slim.browser.wasm +0 -0
- package/dist/libadlmidi.dosbox.slim.core.js +1 -1
- package/dist/libadlmidi.dosbox.slim.core.wasm +0 -0
- package/dist/libadlmidi.dosbox.slim.js +0 -0
- package/dist/libadlmidi.dosbox.slim.processor.js +247 -74
- package/dist/libadlmidi.full.browser.js +1 -1
- package/dist/libadlmidi.full.browser.wasm +0 -0
- package/dist/libadlmidi.full.core.js +1 -1
- package/dist/libadlmidi.full.core.wasm +0 -0
- package/dist/libadlmidi.full.js +0 -0
- package/dist/libadlmidi.full.processor.js +247 -74
- package/dist/libadlmidi.full.slim.browser.js +1 -1
- package/dist/libadlmidi.full.slim.browser.wasm +0 -0
- package/dist/libadlmidi.full.slim.core.js +1 -1
- package/dist/libadlmidi.full.slim.core.wasm +0 -0
- package/dist/libadlmidi.full.slim.js +0 -0
- package/dist/libadlmidi.full.slim.processor.js +247 -74
- package/dist/libadlmidi.js +473 -24
- package/dist/libadlmidi.js.map +3 -3
- package/dist/libadlmidi.light.browser.js +1 -1
- package/dist/libadlmidi.light.browser.wasm +0 -0
- package/dist/libadlmidi.light.core.js +1 -1
- package/dist/libadlmidi.light.core.wasm +0 -0
- package/dist/libadlmidi.light.js +0 -0
- package/dist/libadlmidi.light.processor.js +247 -74
- package/dist/libadlmidi.light.slim.browser.js +1 -1
- package/dist/libadlmidi.light.slim.browser.wasm +0 -0
- package/dist/libadlmidi.light.slim.core.js +1 -1
- package/dist/libadlmidi.light.slim.core.wasm +0 -0
- package/dist/libadlmidi.light.slim.js +0 -0
- package/dist/libadlmidi.light.slim.processor.js +247 -74
- package/dist/libadlmidi.nuked.browser.js +1 -1
- package/dist/libadlmidi.nuked.browser.wasm +0 -0
- package/dist/libadlmidi.nuked.core.js +1 -1
- package/dist/libadlmidi.nuked.core.wasm +0 -0
- package/dist/libadlmidi.nuked.js +0 -0
- package/dist/libadlmidi.nuked.processor.js +247 -74
- package/dist/libadlmidi.nuked.slim.browser.js +1 -1
- package/dist/libadlmidi.nuked.slim.browser.wasm +0 -0
- package/dist/libadlmidi.nuked.slim.core.js +1 -1
- package/dist/libadlmidi.nuked.slim.core.wasm +0 -0
- package/dist/libadlmidi.nuked.slim.js +0 -0
- package/dist/libadlmidi.nuked.slim.processor.js +247 -74
- package/dist/profiles/dosbox.d.ts +7 -2
- package/dist/profiles/dosbox.slim.d.ts +7 -2
- package/dist/profiles/full.d.ts +7 -2
- package/dist/profiles/full.slim.d.ts +7 -2
- package/dist/profiles/light.d.ts +7 -2
- package/dist/profiles/light.slim.d.ts +7 -2
- package/dist/profiles/nuked.d.ts +7 -2
- package/dist/profiles/nuked.slim.d.ts +7 -2
- package/dist/utils/constants.d.ts +61 -0
- package/package.json +30 -9
- package/src/core.js +361 -4
- package/src/libadlmidi.js +379 -58
- package/src/processor.js +210 -12
- package/src/profiles/dosbox.js +20 -10
- package/src/profiles/dosbox.slim.js +20 -10
- package/src/profiles/full.js +21 -11
- package/src/profiles/full.slim.js +21 -11
- package/src/profiles/light.js +21 -11
- package/src/profiles/light.slim.js +21 -11
- package/src/profiles/nuked.js +21 -11
- package/src/profiles/nuked.slim.js +21 -11
- package/src/utils/constants.js +53 -0
package/src/libadlmidi.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* libADLMIDI-JS - Main Thread Interface
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* High-level API for real-time OPL3 FM synthesis in the browser.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```javascript
|
|
8
8
|
* import { AdlMidi } from 'libadlmidi-js';
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* const synth = new AdlMidi();
|
|
11
11
|
* await synth.init('/path/to/processor.js');
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* synth.noteOn(0, 60, 100); // Middle C on channel 0
|
|
14
14
|
* synth.noteOff(0, 60);
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { Emulator, TrackOption } from './utils/constants.js';
|
|
19
|
+
export { Emulator, TrackOption };
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* Bank identifier for instrument access
|
|
20
23
|
* @typedef {Object} BankId
|
|
@@ -73,45 +76,13 @@
|
|
|
73
76
|
* @property {boolean} [deepTremolo] - Enable deep tremolo
|
|
74
77
|
*/
|
|
75
78
|
|
|
76
|
-
/**
|
|
77
|
-
* Available OPL2/OPL3 emulator cores.
|
|
78
|
-
* Use with switchEmulator() to change the synthesis engine at runtime.
|
|
79
|
-
* Note: Only emulators compiled into the current profile are available.
|
|
80
|
-
* @readonly
|
|
81
|
-
* @enum {number}
|
|
82
|
-
*/
|
|
83
|
-
export const Emulator = Object.freeze({
|
|
84
|
-
/** Nuked OPL3 v1.8 - Most accurate, higher CPU usage */
|
|
85
|
-
NUKED: 0,
|
|
86
|
-
/** Nuked OPL3 v1.7.4 - Slightly older version */
|
|
87
|
-
NUKED_174: 1,
|
|
88
|
-
/** DosBox OPL3 - Good accuracy, lower CPU usage */
|
|
89
|
-
DOSBOX: 2,
|
|
90
|
-
/** Opal - Reality Adlib Tracker emulator */
|
|
91
|
-
OPAL: 3,
|
|
92
|
-
/** Java OPL3 - Port of emu8950 */
|
|
93
|
-
JAVA: 4,
|
|
94
|
-
/** ESFMu - ESFM chip emulator */
|
|
95
|
-
ESFMU: 5,
|
|
96
|
-
/** MAME OPL2 */
|
|
97
|
-
MAME_OPL2: 6,
|
|
98
|
-
/** YMFM OPL2 */
|
|
99
|
-
YMFM_OPL2: 7,
|
|
100
|
-
/** YMFM OPL3 */
|
|
101
|
-
YMFM_OPL3: 8,
|
|
102
|
-
/** Nuked OPL2 LLE - Transistor-level emulation */
|
|
103
|
-
NUKED_OPL2_LLE: 9,
|
|
104
|
-
/** Nuked OPL3 LLE - Transistor-level emulation */
|
|
105
|
-
NUKED_OPL3_LLE: 10,
|
|
106
|
-
/** Nuked OPL2 Lite - Lightweight OPL2 emulation for AdLib-era music */
|
|
107
|
-
NUKED_OPL2_LITE: 11,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
79
|
export class AdlMidi {
|
|
111
80
|
/** @type {boolean} */
|
|
112
81
|
#ready = false;
|
|
113
82
|
/** @type {Map<string, Set<Function>>} */
|
|
114
83
|
#messageHandlers = new Map();
|
|
84
|
+
/** @type {number} */
|
|
85
|
+
#nextRequestId = 0;
|
|
115
86
|
|
|
116
87
|
/**
|
|
117
88
|
* Create a new AdlMidi instance
|
|
@@ -143,9 +114,11 @@ export class AdlMidi {
|
|
|
143
114
|
* @param {string} processorUrl - URL to the bundled processor JavaScript file
|
|
144
115
|
* @param {string | null} [wasmUrl=null] - Optional URL to the .wasm file for split builds.
|
|
145
116
|
* If not provided, assumes bundled version with embedded WASM.
|
|
117
|
+
* @param {object} [defaultSettings={}] - Initial synth settings applied before ready.
|
|
118
|
+
* Profile wrappers use this to set a default emulator.
|
|
146
119
|
* @returns {Promise<void>}
|
|
147
120
|
*/
|
|
148
|
-
async init(processorUrl, wasmUrl = null) {
|
|
121
|
+
async init(processorUrl, wasmUrl = null, defaultSettings = {}) {
|
|
149
122
|
if (!this.ctx) {
|
|
150
123
|
this.ctx = new AudioContext({ sampleRate: 44100 });
|
|
151
124
|
}
|
|
@@ -172,7 +145,8 @@ export class AdlMidi {
|
|
|
172
145
|
this.node = new AudioWorkletNode(this.ctx, 'adl-midi-processor', {
|
|
173
146
|
processorOptions: {
|
|
174
147
|
sampleRate: this.ctx.sampleRate,
|
|
175
|
-
wasmBinary: wasmBinary // null for bundled, ArrayBuffer for split
|
|
148
|
+
wasmBinary: wasmBinary, // null for bundled, ArrayBuffer for split
|
|
149
|
+
settings: defaultSettings
|
|
176
150
|
}
|
|
177
151
|
});
|
|
178
152
|
|
|
@@ -231,6 +205,29 @@ export class AdlMidi {
|
|
|
231
205
|
this.#messageHandlers.get(type)?.add(wrappedHandler);
|
|
232
206
|
}
|
|
233
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Register a one-time handler correlated by request ID.
|
|
210
|
+
* Allows concurrent operations of the same type without reply misrouting.
|
|
211
|
+
* @param {string} type - Message type
|
|
212
|
+
* @param {number} reqId - Request ID to match against
|
|
213
|
+
* @param {Function} handler - Handler function
|
|
214
|
+
*/
|
|
215
|
+
#onceCorrelatedMessage(type, reqId, handler) {
|
|
216
|
+
if (!this.#messageHandlers.has(type)) {
|
|
217
|
+
this.#messageHandlers.set(type, new Set());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @param {{reqId?: number}} msg */
|
|
221
|
+
const filteredHandler = (msg) => {
|
|
222
|
+
if (msg.reqId === reqId) {
|
|
223
|
+
this.#messageHandlers.get(type)?.delete(filteredHandler);
|
|
224
|
+
handler(msg);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
this.#messageHandlers.get(type)?.add(filteredHandler);
|
|
229
|
+
}
|
|
230
|
+
|
|
234
231
|
/**
|
|
235
232
|
* Send a message to the processor
|
|
236
233
|
* @param {Object} msg - Message to send
|
|
@@ -369,7 +366,7 @@ export class AdlMidi {
|
|
|
369
366
|
* @param {ArrayBuffer} arrayBuffer - Bank file data
|
|
370
367
|
* @returns {Promise<void>}
|
|
371
368
|
*/
|
|
372
|
-
async
|
|
369
|
+
async loadBankData(arrayBuffer) {
|
|
373
370
|
return new Promise((resolve, reject) => {
|
|
374
371
|
this.#onceMessage('bankLoaded', /** @param {{success: boolean, error?: string}} msg */(msg) => {
|
|
375
372
|
if (msg.success) {
|
|
@@ -379,7 +376,7 @@ export class AdlMidi {
|
|
|
379
376
|
}
|
|
380
377
|
});
|
|
381
378
|
|
|
382
|
-
this.#send({ type: '
|
|
379
|
+
this.#send({ type: 'loadBankData', data: arrayBuffer });
|
|
383
380
|
});
|
|
384
381
|
}
|
|
385
382
|
|
|
@@ -472,6 +469,19 @@ export class AdlMidi {
|
|
|
472
469
|
});
|
|
473
470
|
}
|
|
474
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Get the number of 4-operator channels obtained
|
|
474
|
+
* @returns {Promise<number>}
|
|
475
|
+
*/
|
|
476
|
+
async getNumFourOpChannelsObtained() {
|
|
477
|
+
return new Promise((resolve) => {
|
|
478
|
+
this.#onceMessage('numFourOpChannelsObtained', /** @param {{channels: number}} msg */(msg) => {
|
|
479
|
+
resolve(msg.channels);
|
|
480
|
+
});
|
|
481
|
+
this.#send({ type: 'getNumFourOpChannelsObtained' });
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
475
485
|
/**
|
|
476
486
|
* Enable/disable scaling of modulators by volume
|
|
477
487
|
* @param {boolean} enabled
|
|
@@ -531,35 +541,61 @@ export class AdlMidi {
|
|
|
531
541
|
}
|
|
532
542
|
|
|
533
543
|
/**
|
|
534
|
-
* Set the volume model
|
|
544
|
+
* Set the volume range model
|
|
535
545
|
* @param {number} model - Volume model number
|
|
536
546
|
*/
|
|
537
|
-
|
|
538
|
-
this.#send({ type: '
|
|
547
|
+
setVolumeRangeModel(model) {
|
|
548
|
+
this.#send({ type: 'setVolumeRangeModel', model });
|
|
539
549
|
}
|
|
540
550
|
|
|
541
551
|
/**
|
|
542
|
-
* Enable/disable
|
|
552
|
+
* Enable/disable soft stereo panning
|
|
543
553
|
* @param {boolean} enabled
|
|
544
554
|
*/
|
|
545
|
-
|
|
546
|
-
this.#send({ type: '
|
|
555
|
+
setSoftPanEnabled(enabled) {
|
|
556
|
+
this.#send({ type: 'setSoftPanEnabled', enabled });
|
|
547
557
|
}
|
|
548
558
|
|
|
549
559
|
/**
|
|
550
560
|
* Enable/disable deep vibrato
|
|
551
561
|
* @param {boolean} enabled
|
|
552
562
|
*/
|
|
553
|
-
|
|
554
|
-
this.#send({ type: '
|
|
563
|
+
setDeepVibrato(enabled) {
|
|
564
|
+
this.#send({ type: 'setDeepVibrato', enabled });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get deep vibrato state
|
|
569
|
+
* @returns {Promise<boolean>}
|
|
570
|
+
*/
|
|
571
|
+
async getDeepVibrato() {
|
|
572
|
+
return new Promise((resolve) => {
|
|
573
|
+
this.#onceMessage('deepVibrato', /** @param {{enabled: boolean}} msg */(msg) => {
|
|
574
|
+
resolve(msg.enabled);
|
|
575
|
+
});
|
|
576
|
+
this.#send({ type: 'getDeepVibrato' });
|
|
577
|
+
});
|
|
555
578
|
}
|
|
556
579
|
|
|
557
580
|
/**
|
|
558
581
|
* Enable/disable deep tremolo
|
|
559
582
|
* @param {boolean} enabled
|
|
560
583
|
*/
|
|
561
|
-
|
|
562
|
-
this.#send({ type: '
|
|
584
|
+
setDeepTremolo(enabled) {
|
|
585
|
+
this.#send({ type: 'setDeepTremolo', enabled });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get deep tremolo state
|
|
590
|
+
* @returns {Promise<boolean>}
|
|
591
|
+
*/
|
|
592
|
+
async getDeepTremolo() {
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
this.#onceMessage('deepTremolo', /** @param {{enabled: boolean}} msg */(msg) => {
|
|
595
|
+
resolve(msg.enabled);
|
|
596
|
+
});
|
|
597
|
+
this.#send({ type: 'getDeepTremolo' });
|
|
598
|
+
});
|
|
563
599
|
}
|
|
564
600
|
|
|
565
601
|
/**
|
|
@@ -577,7 +613,7 @@ export class AdlMidi {
|
|
|
577
613
|
* - nuked profile: NUKED only
|
|
578
614
|
* - dosbox profile: DOSBOX only
|
|
579
615
|
* - light profile: NUKED, DOSBOX
|
|
580
|
-
* - full profile: NUKED, DOSBOX, OPAL, JAVA,
|
|
616
|
+
* - full profile: NUKED, DOSBOX, OPAL, JAVA, ESFMu, YMFM_OPL2, YMFM_OPL3
|
|
581
617
|
*
|
|
582
618
|
* @param {number} emulator - Emulator ID from the Emulator enum
|
|
583
619
|
* @returns {Promise<void>} Resolves when emulator is switched, rejects if unavailable
|
|
@@ -614,6 +650,19 @@ export class AdlMidi {
|
|
|
614
650
|
});
|
|
615
651
|
}
|
|
616
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Get the last error info for the player instance
|
|
655
|
+
* @returns {Promise<string>}
|
|
656
|
+
*/
|
|
657
|
+
async getErrorInfo() {
|
|
658
|
+
return new Promise((resolve) => {
|
|
659
|
+
this.#onceMessage('errorInfo', /** @param {{info: string}} msg */(msg) => {
|
|
660
|
+
resolve(msg.info);
|
|
661
|
+
});
|
|
662
|
+
this.#send({ type: 'getErrorInfo' });
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
617
666
|
/**
|
|
618
667
|
* Get the version string of the linked libADLMIDI library
|
|
619
668
|
* @returns {Promise<string>}
|
|
@@ -670,12 +719,12 @@ export class AdlMidi {
|
|
|
670
719
|
* Get the volume range model
|
|
671
720
|
* @returns {Promise<number>}
|
|
672
721
|
*/
|
|
673
|
-
async
|
|
722
|
+
async getVolumeRangeModel() {
|
|
674
723
|
return new Promise((resolve) => {
|
|
675
|
-
this.#onceMessage('
|
|
724
|
+
this.#onceMessage('volumeRangeModel', /** @param {{model: number}} msg */(msg) => {
|
|
676
725
|
resolve(msg.model);
|
|
677
726
|
});
|
|
678
|
-
this.#send({ type: '
|
|
727
|
+
this.#send({ type: 'getVolumeRangeModel' });
|
|
679
728
|
});
|
|
680
729
|
}
|
|
681
730
|
|
|
@@ -696,6 +745,119 @@ export class AdlMidi {
|
|
|
696
745
|
});
|
|
697
746
|
}
|
|
698
747
|
|
|
748
|
+
// ================== Bank Management API ==================
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Reserve a number of banks
|
|
752
|
+
* @param {number} count - Number of banks to reserve
|
|
753
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
754
|
+
*/
|
|
755
|
+
async reserveBanks(count) {
|
|
756
|
+
const reqId = this.#nextRequestId++;
|
|
757
|
+
return new Promise((resolve, reject) => {
|
|
758
|
+
this.#onceCorrelatedMessage('banksReserved', reqId, /** @param {{success: boolean}} msg */(msg) => {
|
|
759
|
+
if (msg.success) {
|
|
760
|
+
resolve();
|
|
761
|
+
} else {
|
|
762
|
+
reject(new Error('Failed to reserve banks'));
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
this.#send({ type: 'reserveBanks', count, reqId });
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Get the bank ID for a given bank identifier
|
|
771
|
+
* @param {BankId} bankId - Bank identifier
|
|
772
|
+
* @returns {Promise<{percussive: number, msb: number, lsb: number}|null>} Bank ID or null if not found
|
|
773
|
+
*/
|
|
774
|
+
async getBankId(bankId) {
|
|
775
|
+
const reqId = this.#nextRequestId++;
|
|
776
|
+
return new Promise((resolve) => {
|
|
777
|
+
this.#onceCorrelatedMessage('bankId', reqId, /** @param {{id: {percussive: number, msb: number, lsb: number}|null}} msg */(msg) => {
|
|
778
|
+
resolve(msg.id);
|
|
779
|
+
});
|
|
780
|
+
this.#send({ type: 'getBankId', bankId, reqId });
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Remove a bank by its identifier
|
|
786
|
+
* @param {BankId} bankId - Bank identifier
|
|
787
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
788
|
+
*/
|
|
789
|
+
async removeBank(bankId) {
|
|
790
|
+
const reqId = this.#nextRequestId++;
|
|
791
|
+
return new Promise((resolve, reject) => {
|
|
792
|
+
this.#onceCorrelatedMessage('bankRemoved', reqId, /** @param {{success: boolean}} msg */(msg) => {
|
|
793
|
+
if (msg.success) {
|
|
794
|
+
resolve();
|
|
795
|
+
} else {
|
|
796
|
+
reject(new Error('Failed to remove bank'));
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
this.#send({ type: 'removeBank', bankId, reqId });
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Load an embedded bank into a custom bank slot
|
|
805
|
+
* @param {BankId} bankId - Target bank identifier
|
|
806
|
+
* @param {number} num - Embedded bank number to load
|
|
807
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
808
|
+
*/
|
|
809
|
+
async loadEmbeddedBank(bankId, num) {
|
|
810
|
+
const reqId = this.#nextRequestId++;
|
|
811
|
+
return new Promise((resolve, reject) => {
|
|
812
|
+
this.#onceCorrelatedMessage('embeddedBankLoaded', reqId, /** @param {{success: boolean}} msg */(msg) => {
|
|
813
|
+
if (msg.success) {
|
|
814
|
+
resolve();
|
|
815
|
+
} else {
|
|
816
|
+
reject(new Error('Failed to load embedded bank'));
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
this.#send({ type: 'loadEmbeddedBank', bankId, num, reqId });
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ================== SysEx API ==================
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Send a System Exclusive (SysEx) message
|
|
827
|
+
* @param {Uint8Array|ArrayBuffer} data - SysEx message data
|
|
828
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
829
|
+
*/
|
|
830
|
+
async systemExclusive(data) {
|
|
831
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
832
|
+
const reqId = this.#nextRequestId++;
|
|
833
|
+
return new Promise((resolve, reject) => {
|
|
834
|
+
this.#onceCorrelatedMessage('systemExclusiveSent', reqId, /** @param {{success: boolean}} msg */(msg) => {
|
|
835
|
+
if (msg.success) {
|
|
836
|
+
resolve();
|
|
837
|
+
} else {
|
|
838
|
+
reject(new Error('Failed to send system exclusive message'));
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
this.#send({ type: 'systemExclusive', data: Array.from(bytes), reqId });
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ================== Debug / Diagnostics API ==================
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Describe the current state of all channels (debug utility)
|
|
849
|
+
* @returns {Promise<{text: string, attr: Uint8Array}>} Channel state text and raw per-channel attribute bytes
|
|
850
|
+
*/
|
|
851
|
+
async describeChannels() {
|
|
852
|
+
const reqId = this.#nextRequestId++;
|
|
853
|
+
return new Promise((resolve) => {
|
|
854
|
+
this.#onceCorrelatedMessage('channelsDescribed', reqId, /** @param {{text: string, attr: number[]}} msg */(msg) => {
|
|
855
|
+
resolve({ text: msg.text, attr: new Uint8Array(msg.attr) });
|
|
856
|
+
});
|
|
857
|
+
this.#send({ type: 'describeChannels', reqId });
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
699
861
|
/**
|
|
700
862
|
* Reset the synthesizer
|
|
701
863
|
* @returns {void}
|
|
@@ -751,6 +913,47 @@ export class AdlMidi {
|
|
|
751
913
|
});
|
|
752
914
|
}
|
|
753
915
|
|
|
916
|
+
/**
|
|
917
|
+
* Get the number of track titles in the loaded MIDI file
|
|
918
|
+
* @returns {Promise<number>}
|
|
919
|
+
*/
|
|
920
|
+
async getTrackTitleCount() {
|
|
921
|
+
return new Promise((resolve) => {
|
|
922
|
+
this.#onceMessage('trackTitleCount', /** @param {{count: number}} msg */(msg) => {
|
|
923
|
+
resolve(msg.count);
|
|
924
|
+
});
|
|
925
|
+
this.#send({ type: 'getTrackTitleCount' });
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Get a track title by index
|
|
931
|
+
* @param {number} index - Track title index
|
|
932
|
+
* @returns {Promise<string>}
|
|
933
|
+
*/
|
|
934
|
+
async getTrackTitle(index) {
|
|
935
|
+
const reqId = this.#nextRequestId++;
|
|
936
|
+
return new Promise((resolve) => {
|
|
937
|
+
this.#onceCorrelatedMessage('trackTitle', reqId, /** @param {{title: string}} msg */(msg) => {
|
|
938
|
+
resolve(msg.title);
|
|
939
|
+
});
|
|
940
|
+
this.#send({ type: 'getTrackTitle', index, reqId });
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Get the number of MIDI markers in the loaded file
|
|
946
|
+
* @returns {Promise<number>}
|
|
947
|
+
*/
|
|
948
|
+
async getMarkerCount() {
|
|
949
|
+
return new Promise((resolve) => {
|
|
950
|
+
this.#onceMessage('markerCount', /** @param {{count: number}} msg */(msg) => {
|
|
951
|
+
resolve(msg.count);
|
|
952
|
+
});
|
|
953
|
+
this.#send({ type: 'getMarkerCount' });
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
754
957
|
/**
|
|
755
958
|
* Start or resume MIDI file playback
|
|
756
959
|
* @returns {void}
|
|
@@ -781,8 +984,126 @@ export class AdlMidi {
|
|
|
781
984
|
* @param {boolean} enabled - Whether to loop
|
|
782
985
|
* @returns {void}
|
|
783
986
|
*/
|
|
784
|
-
|
|
785
|
-
this.#send({ type: '
|
|
987
|
+
setLoopEnabled(enabled) {
|
|
988
|
+
this.#send({ type: 'setLoopEnabled', enabled });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Set the number of loop repetitions
|
|
993
|
+
* @param {number} count - Loop count (-1 = infinite, 0 = no loops, 1+ = number of loops)
|
|
994
|
+
*/
|
|
995
|
+
setLoopCount(count) {
|
|
996
|
+
this.#send({ type: 'setLoopCount', count });
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Enable/disable loop hooks only mode
|
|
1001
|
+
* @param {boolean} enabled
|
|
1002
|
+
*/
|
|
1003
|
+
setLoopHooksOnly(enabled) {
|
|
1004
|
+
this.#send({ type: 'setLoopHooksOnly', enabled });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Get the loop start time in seconds
|
|
1009
|
+
* @returns {Promise<number>}
|
|
1010
|
+
*/
|
|
1011
|
+
async getLoopStartTime() {
|
|
1012
|
+
return new Promise((resolve) => {
|
|
1013
|
+
this.#onceMessage('loopStartTime', /** @param {{time: number}} msg */(msg) => {
|
|
1014
|
+
resolve(msg.time);
|
|
1015
|
+
});
|
|
1016
|
+
this.#send({ type: 'getLoopStartTime' });
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Get the loop end time in seconds
|
|
1022
|
+
* @returns {Promise<number>}
|
|
1023
|
+
*/
|
|
1024
|
+
async getLoopEndTime() {
|
|
1025
|
+
return new Promise((resolve) => {
|
|
1026
|
+
this.#onceMessage('loopEndTime', /** @param {{time: number}} msg */(msg) => {
|
|
1027
|
+
resolve(msg.time);
|
|
1028
|
+
});
|
|
1029
|
+
this.#send({ type: 'getLoopEndTime' });
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Select a song number for multi-song MIDI files
|
|
1035
|
+
* @param {number} num - Song number (0-based)
|
|
1036
|
+
*/
|
|
1037
|
+
selectSongNum(num) {
|
|
1038
|
+
this.#send({ type: 'selectSongNum', num });
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Get the number of songs in the loaded MIDI file
|
|
1043
|
+
* @returns {Promise<number>}
|
|
1044
|
+
*/
|
|
1045
|
+
async getSongsCount() {
|
|
1046
|
+
return new Promise((resolve) => {
|
|
1047
|
+
this.#onceMessage('songsCount', /** @param {{count: number}} msg */(msg) => {
|
|
1048
|
+
resolve(msg.count);
|
|
1049
|
+
});
|
|
1050
|
+
this.#send({ type: 'getSongsCount' });
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Get the number of tracks in the loaded MIDI file
|
|
1056
|
+
* @returns {Promise<number>}
|
|
1057
|
+
*/
|
|
1058
|
+
async getTrackCount() {
|
|
1059
|
+
return new Promise((resolve) => {
|
|
1060
|
+
this.#onceMessage('trackCount', /** @param {{count: number}} msg */(msg) => {
|
|
1061
|
+
resolve(msg.count);
|
|
1062
|
+
});
|
|
1063
|
+
this.#send({ type: 'getTrackCount' });
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Set track options (enable, mute, or solo)
|
|
1069
|
+
* Use the TrackOption enum: TrackOption.ON (1), TrackOption.OFF (2), TrackOption.SOLO (3).
|
|
1070
|
+
* Note: Passing 0 is a silent no-op that resolves without changing state.
|
|
1071
|
+
* @param {number} track - Track index
|
|
1072
|
+
* @param {number} options - Track option from TrackOption enum
|
|
1073
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
1074
|
+
*/
|
|
1075
|
+
async setTrackOptions(track, options) {
|
|
1076
|
+
const reqId = this.#nextRequestId++;
|
|
1077
|
+
return new Promise((resolve, reject) => {
|
|
1078
|
+
this.#onceCorrelatedMessage('trackOptionsSet', reqId, /** @param {{success: boolean}} msg */(msg) => {
|
|
1079
|
+
if (msg.success) {
|
|
1080
|
+
resolve();
|
|
1081
|
+
} else {
|
|
1082
|
+
reject(new Error(`Failed to set track options for track ${track}`));
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
this.#send({ type: 'setTrackOptions', track, options, reqId });
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Enable or disable a MIDI channel
|
|
1091
|
+
* @param {number} channel - MIDI channel (0-15)
|
|
1092
|
+
* @param {boolean} enabled - Whether to enable the channel
|
|
1093
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
1094
|
+
*/
|
|
1095
|
+
async setChannelEnabled(channel, enabled) {
|
|
1096
|
+
const reqId = this.#nextRequestId++;
|
|
1097
|
+
return new Promise((resolve, reject) => {
|
|
1098
|
+
this.#onceCorrelatedMessage('channelEnabledSet', reqId, /** @param {{success: boolean}} msg */(msg) => {
|
|
1099
|
+
if (msg.success) {
|
|
1100
|
+
resolve();
|
|
1101
|
+
} else {
|
|
1102
|
+
reject(new Error(`Failed to set channel ${channel} enabled state`));
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
this.#send({ type: 'setChannelEnabled', channel, enabled, reqId });
|
|
1106
|
+
});
|
|
786
1107
|
}
|
|
787
1108
|
|
|
788
1109
|
/**
|