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.
Files changed (76) hide show
  1. package/README.md +8 -5
  2. package/dist/core.d.ts +191 -4
  3. package/dist/fm_banks/manifest.json +1 -1
  4. package/dist/libadlmidi.d.ts +146 -66
  5. package/dist/libadlmidi.dosbox.browser.js +1 -1
  6. package/dist/libadlmidi.dosbox.browser.wasm +0 -0
  7. package/dist/libadlmidi.dosbox.core.js +1 -1
  8. package/dist/libadlmidi.dosbox.core.wasm +0 -0
  9. package/dist/libadlmidi.dosbox.js +0 -0
  10. package/dist/libadlmidi.dosbox.processor.js +247 -74
  11. package/dist/libadlmidi.dosbox.slim.browser.js +1 -1
  12. package/dist/libadlmidi.dosbox.slim.browser.wasm +0 -0
  13. package/dist/libadlmidi.dosbox.slim.core.js +1 -1
  14. package/dist/libadlmidi.dosbox.slim.core.wasm +0 -0
  15. package/dist/libadlmidi.dosbox.slim.js +0 -0
  16. package/dist/libadlmidi.dosbox.slim.processor.js +247 -74
  17. package/dist/libadlmidi.full.browser.js +1 -1
  18. package/dist/libadlmidi.full.browser.wasm +0 -0
  19. package/dist/libadlmidi.full.core.js +1 -1
  20. package/dist/libadlmidi.full.core.wasm +0 -0
  21. package/dist/libadlmidi.full.js +0 -0
  22. package/dist/libadlmidi.full.processor.js +247 -74
  23. package/dist/libadlmidi.full.slim.browser.js +1 -1
  24. package/dist/libadlmidi.full.slim.browser.wasm +0 -0
  25. package/dist/libadlmidi.full.slim.core.js +1 -1
  26. package/dist/libadlmidi.full.slim.core.wasm +0 -0
  27. package/dist/libadlmidi.full.slim.js +0 -0
  28. package/dist/libadlmidi.full.slim.processor.js +247 -74
  29. package/dist/libadlmidi.js +473 -24
  30. package/dist/libadlmidi.js.map +3 -3
  31. package/dist/libadlmidi.light.browser.js +1 -1
  32. package/dist/libadlmidi.light.browser.wasm +0 -0
  33. package/dist/libadlmidi.light.core.js +1 -1
  34. package/dist/libadlmidi.light.core.wasm +0 -0
  35. package/dist/libadlmidi.light.js +0 -0
  36. package/dist/libadlmidi.light.processor.js +247 -74
  37. package/dist/libadlmidi.light.slim.browser.js +1 -1
  38. package/dist/libadlmidi.light.slim.browser.wasm +0 -0
  39. package/dist/libadlmidi.light.slim.core.js +1 -1
  40. package/dist/libadlmidi.light.slim.core.wasm +0 -0
  41. package/dist/libadlmidi.light.slim.js +0 -0
  42. package/dist/libadlmidi.light.slim.processor.js +247 -74
  43. package/dist/libadlmidi.nuked.browser.js +1 -1
  44. package/dist/libadlmidi.nuked.browser.wasm +0 -0
  45. package/dist/libadlmidi.nuked.core.js +1 -1
  46. package/dist/libadlmidi.nuked.core.wasm +0 -0
  47. package/dist/libadlmidi.nuked.js +0 -0
  48. package/dist/libadlmidi.nuked.processor.js +247 -74
  49. package/dist/libadlmidi.nuked.slim.browser.js +1 -1
  50. package/dist/libadlmidi.nuked.slim.browser.wasm +0 -0
  51. package/dist/libadlmidi.nuked.slim.core.js +1 -1
  52. package/dist/libadlmidi.nuked.slim.core.wasm +0 -0
  53. package/dist/libadlmidi.nuked.slim.js +0 -0
  54. package/dist/libadlmidi.nuked.slim.processor.js +247 -74
  55. package/dist/profiles/dosbox.d.ts +7 -2
  56. package/dist/profiles/dosbox.slim.d.ts +7 -2
  57. package/dist/profiles/full.d.ts +7 -2
  58. package/dist/profiles/full.slim.d.ts +7 -2
  59. package/dist/profiles/light.d.ts +7 -2
  60. package/dist/profiles/light.slim.d.ts +7 -2
  61. package/dist/profiles/nuked.d.ts +7 -2
  62. package/dist/profiles/nuked.slim.d.ts +7 -2
  63. package/dist/utils/constants.d.ts +61 -0
  64. package/package.json +30 -9
  65. package/src/core.js +361 -4
  66. package/src/libadlmidi.js +379 -58
  67. package/src/processor.js +210 -12
  68. package/src/profiles/dosbox.js +20 -10
  69. package/src/profiles/dosbox.slim.js +20 -10
  70. package/src/profiles/full.js +21 -11
  71. package/src/profiles/full.slim.js +21 -11
  72. package/src/profiles/light.js +21 -11
  73. package/src/profiles/light.slim.js +21 -11
  74. package/src/profiles/nuked.js +21 -11
  75. package/src/profiles/nuked.slim.js +21 -11
  76. 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 loadBank(arrayBuffer) {
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: 'loadBank', data: arrayBuffer });
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
- setVolumeModel(model) {
538
- this.#send({ type: 'setVolumeModel', model });
547
+ setVolumeRangeModel(model) {
548
+ this.#send({ type: 'setVolumeRangeModel', model });
539
549
  }
540
550
 
541
551
  /**
542
- * Enable/disable rhythm mode (percussion)
552
+ * Enable/disable soft stereo panning
543
553
  * @param {boolean} enabled
544
554
  */
545
- setPercussionMode(enabled) {
546
- this.#send({ type: 'setPercMode', enabled });
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
- setVibrato(enabled) {
554
- this.#send({ type: 'setVibrato', enabled });
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
- setTremolo(enabled) {
562
- this.#send({ type: 'setTremolo', enabled });
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, ESFMU, YMFM_OPL2, YMFM_OPL3
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 getVolumeModel() {
722
+ async getVolumeRangeModel() {
674
723
  return new Promise((resolve) => {
675
- this.#onceMessage('volumeModel', /** @param {{model: number}} msg */(msg) => {
724
+ this.#onceMessage('volumeRangeModel', /** @param {{model: number}} msg */(msg) => {
676
725
  resolve(msg.model);
677
726
  });
678
- this.#send({ type: 'getVolumeModel' });
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
- setLoop(enabled) {
785
- this.#send({ type: 'setLoop', enabled });
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
  /**