libadlmidi-js 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/core.d.ts +186 -4
  2. package/dist/fm_banks/manifest.json +1 -1
  3. package/dist/libadlmidi.d.ts +143 -63
  4. package/dist/libadlmidi.dosbox.browser.js +1 -1
  5. package/dist/libadlmidi.dosbox.browser.wasm +0 -0
  6. package/dist/libadlmidi.dosbox.core.js +1 -1
  7. package/dist/libadlmidi.dosbox.core.wasm +0 -0
  8. package/dist/libadlmidi.dosbox.js +0 -0
  9. package/dist/libadlmidi.dosbox.processor.js +242 -74
  10. package/dist/libadlmidi.dosbox.slim.browser.js +1 -1
  11. package/dist/libadlmidi.dosbox.slim.browser.wasm +0 -0
  12. package/dist/libadlmidi.dosbox.slim.core.js +1 -1
  13. package/dist/libadlmidi.dosbox.slim.core.wasm +0 -0
  14. package/dist/libadlmidi.dosbox.slim.js +0 -0
  15. package/dist/libadlmidi.dosbox.slim.processor.js +242 -74
  16. package/dist/libadlmidi.full.browser.js +1 -1
  17. package/dist/libadlmidi.full.browser.wasm +0 -0
  18. package/dist/libadlmidi.full.core.js +1 -1
  19. package/dist/libadlmidi.full.core.wasm +0 -0
  20. package/dist/libadlmidi.full.js +0 -0
  21. package/dist/libadlmidi.full.processor.js +242 -74
  22. package/dist/libadlmidi.full.slim.browser.js +1 -1
  23. package/dist/libadlmidi.full.slim.browser.wasm +0 -0
  24. package/dist/libadlmidi.full.slim.core.js +1 -1
  25. package/dist/libadlmidi.full.slim.core.wasm +0 -0
  26. package/dist/libadlmidi.full.slim.js +0 -0
  27. package/dist/libadlmidi.full.slim.processor.js +242 -74
  28. package/dist/libadlmidi.js +468 -22
  29. package/dist/libadlmidi.js.map +3 -3
  30. package/dist/libadlmidi.light.browser.js +1 -1
  31. package/dist/libadlmidi.light.browser.wasm +0 -0
  32. package/dist/libadlmidi.light.core.js +1 -1
  33. package/dist/libadlmidi.light.core.wasm +0 -0
  34. package/dist/libadlmidi.light.js +0 -0
  35. package/dist/libadlmidi.light.processor.js +242 -74
  36. package/dist/libadlmidi.light.slim.browser.js +1 -1
  37. package/dist/libadlmidi.light.slim.browser.wasm +0 -0
  38. package/dist/libadlmidi.light.slim.core.js +1 -1
  39. package/dist/libadlmidi.light.slim.core.wasm +0 -0
  40. package/dist/libadlmidi.light.slim.js +0 -0
  41. package/dist/libadlmidi.light.slim.processor.js +242 -74
  42. package/dist/libadlmidi.nuked.browser.js +1 -1
  43. package/dist/libadlmidi.nuked.browser.wasm +0 -0
  44. package/dist/libadlmidi.nuked.core.js +1 -1
  45. package/dist/libadlmidi.nuked.core.wasm +0 -0
  46. package/dist/libadlmidi.nuked.js +0 -0
  47. package/dist/libadlmidi.nuked.processor.js +242 -74
  48. package/dist/libadlmidi.nuked.slim.browser.js +1 -1
  49. package/dist/libadlmidi.nuked.slim.browser.wasm +0 -0
  50. package/dist/libadlmidi.nuked.slim.core.js +1 -1
  51. package/dist/libadlmidi.nuked.slim.core.wasm +0 -0
  52. package/dist/libadlmidi.nuked.slim.js +0 -0
  53. package/dist/libadlmidi.nuked.slim.processor.js +242 -74
  54. package/dist/profiles/dosbox.d.ts +1 -0
  55. package/dist/profiles/dosbox.slim.d.ts +1 -0
  56. package/dist/profiles/full.d.ts +1 -0
  57. package/dist/profiles/full.slim.d.ts +1 -0
  58. package/dist/profiles/light.d.ts +1 -0
  59. package/dist/profiles/light.slim.d.ts +1 -0
  60. package/dist/profiles/nuked.d.ts +1 -0
  61. package/dist/profiles/nuked.slim.d.ts +1 -0
  62. package/dist/utils/constants.d.ts +59 -0
  63. package/package.json +1 -1
  64. package/src/core.js +352 -4
  65. package/src/libadlmidi.js +374 -54
  66. package/src/processor.js +204 -12
  67. package/src/profiles/dosbox.js +7 -4
  68. package/src/profiles/dosbox.slim.js +7 -4
  69. package/src/profiles/full.js +7 -4
  70. package/src/profiles/full.slim.js +7 -4
  71. package/src/profiles/light.js +7 -4
  72. package/src/profiles/light.slim.js +7 -4
  73. package/src/profiles/nuked.js +7 -4
  74. package/src/profiles/nuked.slim.js +7 -4
  75. package/src/utils/constants.js +51 -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,43 +76,13 @@
73
76
  * @property {boolean} [deepTremolo] - Enable deep tremolo
74
77
  */
75
78
 
76
- /**
77
- * Available 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
- });
107
-
108
79
  export class AdlMidi {
109
80
  /** @type {boolean} */
110
81
  #ready = false;
111
82
  /** @type {Map<string, Set<Function>>} */
112
83
  #messageHandlers = new Map();
84
+ /** @type {number} */
85
+ #nextRequestId = 0;
113
86
 
114
87
  /**
115
88
  * Create a new AdlMidi instance
@@ -229,6 +202,29 @@ export class AdlMidi {
229
202
  this.#messageHandlers.get(type)?.add(wrappedHandler);
230
203
  }
231
204
 
205
+ /**
206
+ * Register a one-time handler correlated by request ID.
207
+ * Allows concurrent operations of the same type without reply misrouting.
208
+ * @param {string} type - Message type
209
+ * @param {number} reqId - Request ID to match against
210
+ * @param {Function} handler - Handler function
211
+ */
212
+ #onceCorrelatedMessage(type, reqId, handler) {
213
+ if (!this.#messageHandlers.has(type)) {
214
+ this.#messageHandlers.set(type, new Set());
215
+ }
216
+
217
+ /** @param {{reqId?: number}} msg */
218
+ const filteredHandler = (msg) => {
219
+ if (msg.reqId === reqId) {
220
+ this.#messageHandlers.get(type)?.delete(filteredHandler);
221
+ handler(msg);
222
+ }
223
+ };
224
+
225
+ this.#messageHandlers.get(type)?.add(filteredHandler);
226
+ }
227
+
232
228
  /**
233
229
  * Send a message to the processor
234
230
  * @param {Object} msg - Message to send
@@ -367,7 +363,7 @@ export class AdlMidi {
367
363
  * @param {ArrayBuffer} arrayBuffer - Bank file data
368
364
  * @returns {Promise<void>}
369
365
  */
370
- async loadBank(arrayBuffer) {
366
+ async loadBankData(arrayBuffer) {
371
367
  return new Promise((resolve, reject) => {
372
368
  this.#onceMessage('bankLoaded', /** @param {{success: boolean, error?: string}} msg */(msg) => {
373
369
  if (msg.success) {
@@ -377,7 +373,7 @@ export class AdlMidi {
377
373
  }
378
374
  });
379
375
 
380
- this.#send({ type: 'loadBank', data: arrayBuffer });
376
+ this.#send({ type: 'loadBankData', data: arrayBuffer });
381
377
  });
382
378
  }
383
379
 
@@ -470,6 +466,19 @@ export class AdlMidi {
470
466
  });
471
467
  }
472
468
 
469
+ /**
470
+ * Get the number of 4-operator channels obtained
471
+ * @returns {Promise<number>}
472
+ */
473
+ async getNumFourOpChannelsObtained() {
474
+ return new Promise((resolve) => {
475
+ this.#onceMessage('numFourOpChannelsObtained', /** @param {{channels: number}} msg */(msg) => {
476
+ resolve(msg.channels);
477
+ });
478
+ this.#send({ type: 'getNumFourOpChannelsObtained' });
479
+ });
480
+ }
481
+
473
482
  /**
474
483
  * Enable/disable scaling of modulators by volume
475
484
  * @param {boolean} enabled
@@ -529,35 +538,61 @@ export class AdlMidi {
529
538
  }
530
539
 
531
540
  /**
532
- * Set the volume model
541
+ * Set the volume range model
533
542
  * @param {number} model - Volume model number
534
543
  */
535
- setVolumeModel(model) {
536
- this.#send({ type: 'setVolumeModel', model });
544
+ setVolumeRangeModel(model) {
545
+ this.#send({ type: 'setVolumeRangeModel', model });
537
546
  }
538
547
 
539
548
  /**
540
- * Enable/disable rhythm mode (percussion)
549
+ * Enable/disable soft stereo panning
541
550
  * @param {boolean} enabled
542
551
  */
543
- setPercussionMode(enabled) {
544
- this.#send({ type: 'setPercMode', enabled });
552
+ setSoftPanEnabled(enabled) {
553
+ this.#send({ type: 'setSoftPanEnabled', enabled });
545
554
  }
546
555
 
547
556
  /**
548
557
  * Enable/disable deep vibrato
549
558
  * @param {boolean} enabled
550
559
  */
551
- setVibrato(enabled) {
552
- this.#send({ type: 'setVibrato', enabled });
560
+ setDeepVibrato(enabled) {
561
+ this.#send({ type: 'setDeepVibrato', enabled });
562
+ }
563
+
564
+ /**
565
+ * Get deep vibrato state
566
+ * @returns {Promise<boolean>}
567
+ */
568
+ async getDeepVibrato() {
569
+ return new Promise((resolve) => {
570
+ this.#onceMessage('deepVibrato', /** @param {{enabled: boolean}} msg */(msg) => {
571
+ resolve(msg.enabled);
572
+ });
573
+ this.#send({ type: 'getDeepVibrato' });
574
+ });
553
575
  }
554
576
 
555
577
  /**
556
578
  * Enable/disable deep tremolo
557
579
  * @param {boolean} enabled
558
580
  */
559
- setTremolo(enabled) {
560
- this.#send({ type: 'setTremolo', enabled });
581
+ setDeepTremolo(enabled) {
582
+ this.#send({ type: 'setDeepTremolo', enabled });
583
+ }
584
+
585
+ /**
586
+ * Get deep tremolo state
587
+ * @returns {Promise<boolean>}
588
+ */
589
+ async getDeepTremolo() {
590
+ return new Promise((resolve) => {
591
+ this.#onceMessage('deepTremolo', /** @param {{enabled: boolean}} msg */(msg) => {
592
+ resolve(msg.enabled);
593
+ });
594
+ this.#send({ type: 'getDeepTremolo' });
595
+ });
561
596
  }
562
597
 
563
598
  /**
@@ -575,7 +610,7 @@ export class AdlMidi {
575
610
  * - nuked profile: NUKED only
576
611
  * - dosbox profile: DOSBOX only
577
612
  * - light profile: NUKED, DOSBOX
578
- * - full profile: NUKED, DOSBOX, OPAL, JAVA, ESFMU, YMFM_OPL2, YMFM_OPL3
613
+ * - full profile: NUKED, DOSBOX, OPAL, JAVA, ESFMu, YMFM_OPL2, YMFM_OPL3
579
614
  *
580
615
  * @param {number} emulator - Emulator ID from the Emulator enum
581
616
  * @returns {Promise<void>} Resolves when emulator is switched, rejects if unavailable
@@ -612,6 +647,19 @@ export class AdlMidi {
612
647
  });
613
648
  }
614
649
 
650
+ /**
651
+ * Get the last error info for the player instance
652
+ * @returns {Promise<string>}
653
+ */
654
+ async getErrorInfo() {
655
+ return new Promise((resolve) => {
656
+ this.#onceMessage('errorInfo', /** @param {{info: string}} msg */(msg) => {
657
+ resolve(msg.info);
658
+ });
659
+ this.#send({ type: 'getErrorInfo' });
660
+ });
661
+ }
662
+
615
663
  /**
616
664
  * Get the version string of the linked libADLMIDI library
617
665
  * @returns {Promise<string>}
@@ -668,12 +716,12 @@ export class AdlMidi {
668
716
  * Get the volume range model
669
717
  * @returns {Promise<number>}
670
718
  */
671
- async getVolumeModel() {
719
+ async getVolumeRangeModel() {
672
720
  return new Promise((resolve) => {
673
- this.#onceMessage('volumeModel', /** @param {{model: number}} msg */(msg) => {
721
+ this.#onceMessage('volumeRangeModel', /** @param {{model: number}} msg */(msg) => {
674
722
  resolve(msg.model);
675
723
  });
676
- this.#send({ type: 'getVolumeModel' });
724
+ this.#send({ type: 'getVolumeRangeModel' });
677
725
  });
678
726
  }
679
727
 
@@ -694,6 +742,119 @@ export class AdlMidi {
694
742
  });
695
743
  }
696
744
 
745
+ // ================== Bank Management API ==================
746
+
747
+ /**
748
+ * Reserve a number of banks
749
+ * @param {number} count - Number of banks to reserve
750
+ * @returns {Promise<void>} Resolves on success, rejects on failure
751
+ */
752
+ async reserveBanks(count) {
753
+ const reqId = this.#nextRequestId++;
754
+ return new Promise((resolve, reject) => {
755
+ this.#onceCorrelatedMessage('banksReserved', reqId, /** @param {{success: boolean}} msg */(msg) => {
756
+ if (msg.success) {
757
+ resolve();
758
+ } else {
759
+ reject(new Error('Failed to reserve banks'));
760
+ }
761
+ });
762
+ this.#send({ type: 'reserveBanks', count, reqId });
763
+ });
764
+ }
765
+
766
+ /**
767
+ * Get the bank ID for a given bank identifier
768
+ * @param {BankId} bankId - Bank identifier
769
+ * @returns {Promise<{percussive: number, msb: number, lsb: number}|null>} Bank ID or null if not found
770
+ */
771
+ async getBankId(bankId) {
772
+ const reqId = this.#nextRequestId++;
773
+ return new Promise((resolve) => {
774
+ this.#onceCorrelatedMessage('bankId', reqId, /** @param {{id: {percussive: number, msb: number, lsb: number}|null}} msg */(msg) => {
775
+ resolve(msg.id);
776
+ });
777
+ this.#send({ type: 'getBankId', bankId, reqId });
778
+ });
779
+ }
780
+
781
+ /**
782
+ * Remove a bank by its identifier
783
+ * @param {BankId} bankId - Bank identifier
784
+ * @returns {Promise<void>} Resolves on success, rejects on failure
785
+ */
786
+ async removeBank(bankId) {
787
+ const reqId = this.#nextRequestId++;
788
+ return new Promise((resolve, reject) => {
789
+ this.#onceCorrelatedMessage('bankRemoved', reqId, /** @param {{success: boolean}} msg */(msg) => {
790
+ if (msg.success) {
791
+ resolve();
792
+ } else {
793
+ reject(new Error('Failed to remove bank'));
794
+ }
795
+ });
796
+ this.#send({ type: 'removeBank', bankId, reqId });
797
+ });
798
+ }
799
+
800
+ /**
801
+ * Load an embedded bank into a custom bank slot
802
+ * @param {BankId} bankId - Target bank identifier
803
+ * @param {number} num - Embedded bank number to load
804
+ * @returns {Promise<void>} Resolves on success, rejects on failure
805
+ */
806
+ async loadEmbeddedBank(bankId, num) {
807
+ const reqId = this.#nextRequestId++;
808
+ return new Promise((resolve, reject) => {
809
+ this.#onceCorrelatedMessage('embeddedBankLoaded', reqId, /** @param {{success: boolean}} msg */(msg) => {
810
+ if (msg.success) {
811
+ resolve();
812
+ } else {
813
+ reject(new Error('Failed to load embedded bank'));
814
+ }
815
+ });
816
+ this.#send({ type: 'loadEmbeddedBank', bankId, num, reqId });
817
+ });
818
+ }
819
+
820
+ // ================== SysEx API ==================
821
+
822
+ /**
823
+ * Send a System Exclusive (SysEx) message
824
+ * @param {Uint8Array|ArrayBuffer} data - SysEx message data
825
+ * @returns {Promise<void>} Resolves on success, rejects on failure
826
+ */
827
+ async systemExclusive(data) {
828
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
829
+ const reqId = this.#nextRequestId++;
830
+ return new Promise((resolve, reject) => {
831
+ this.#onceCorrelatedMessage('systemExclusiveSent', reqId, /** @param {{success: boolean}} msg */(msg) => {
832
+ if (msg.success) {
833
+ resolve();
834
+ } else {
835
+ reject(new Error('Failed to send system exclusive message'));
836
+ }
837
+ });
838
+ this.#send({ type: 'systemExclusive', data: Array.from(bytes), reqId });
839
+ });
840
+ }
841
+
842
+ // ================== Debug / Diagnostics API ==================
843
+
844
+ /**
845
+ * Describe the current state of all channels (debug utility)
846
+ * @returns {Promise<{text: string, attr: Uint8Array}>} Channel state text and raw per-channel attribute bytes
847
+ */
848
+ async describeChannels() {
849
+ const reqId = this.#nextRequestId++;
850
+ return new Promise((resolve) => {
851
+ this.#onceCorrelatedMessage('channelsDescribed', reqId, /** @param {{text: string, attr: number[]}} msg */(msg) => {
852
+ resolve({ text: msg.text, attr: new Uint8Array(msg.attr) });
853
+ });
854
+ this.#send({ type: 'describeChannels', reqId });
855
+ });
856
+ }
857
+
697
858
  /**
698
859
  * Reset the synthesizer
699
860
  * @returns {void}
@@ -749,6 +910,47 @@ export class AdlMidi {
749
910
  });
750
911
  }
751
912
 
913
+ /**
914
+ * Get the number of track titles in the loaded MIDI file
915
+ * @returns {Promise<number>}
916
+ */
917
+ async getTrackTitleCount() {
918
+ return new Promise((resolve) => {
919
+ this.#onceMessage('trackTitleCount', /** @param {{count: number}} msg */(msg) => {
920
+ resolve(msg.count);
921
+ });
922
+ this.#send({ type: 'getTrackTitleCount' });
923
+ });
924
+ }
925
+
926
+ /**
927
+ * Get a track title by index
928
+ * @param {number} index - Track title index
929
+ * @returns {Promise<string>}
930
+ */
931
+ async getTrackTitle(index) {
932
+ const reqId = this.#nextRequestId++;
933
+ return new Promise((resolve) => {
934
+ this.#onceCorrelatedMessage('trackTitle', reqId, /** @param {{title: string}} msg */(msg) => {
935
+ resolve(msg.title);
936
+ });
937
+ this.#send({ type: 'getTrackTitle', index, reqId });
938
+ });
939
+ }
940
+
941
+ /**
942
+ * Get the number of MIDI markers in the loaded file
943
+ * @returns {Promise<number>}
944
+ */
945
+ async getMarkerCount() {
946
+ return new Promise((resolve) => {
947
+ this.#onceMessage('markerCount', /** @param {{count: number}} msg */(msg) => {
948
+ resolve(msg.count);
949
+ });
950
+ this.#send({ type: 'getMarkerCount' });
951
+ });
952
+ }
953
+
752
954
  /**
753
955
  * Start or resume MIDI file playback
754
956
  * @returns {void}
@@ -779,8 +981,126 @@ export class AdlMidi {
779
981
  * @param {boolean} enabled - Whether to loop
780
982
  * @returns {void}
781
983
  */
782
- setLoop(enabled) {
783
- this.#send({ type: 'setLoop', enabled });
984
+ setLoopEnabled(enabled) {
985
+ this.#send({ type: 'setLoopEnabled', enabled });
986
+ }
987
+
988
+ /**
989
+ * Set the number of loop repetitions
990
+ * @param {number} count - Loop count (-1 = infinite, 0 = no loops, 1+ = number of loops)
991
+ */
992
+ setLoopCount(count) {
993
+ this.#send({ type: 'setLoopCount', count });
994
+ }
995
+
996
+ /**
997
+ * Enable/disable loop hooks only mode
998
+ * @param {boolean} enabled
999
+ */
1000
+ setLoopHooksOnly(enabled) {
1001
+ this.#send({ type: 'setLoopHooksOnly', enabled });
1002
+ }
1003
+
1004
+ /**
1005
+ * Get the loop start time in seconds
1006
+ * @returns {Promise<number>}
1007
+ */
1008
+ async getLoopStartTime() {
1009
+ return new Promise((resolve) => {
1010
+ this.#onceMessage('loopStartTime', /** @param {{time: number}} msg */(msg) => {
1011
+ resolve(msg.time);
1012
+ });
1013
+ this.#send({ type: 'getLoopStartTime' });
1014
+ });
1015
+ }
1016
+
1017
+ /**
1018
+ * Get the loop end time in seconds
1019
+ * @returns {Promise<number>}
1020
+ */
1021
+ async getLoopEndTime() {
1022
+ return new Promise((resolve) => {
1023
+ this.#onceMessage('loopEndTime', /** @param {{time: number}} msg */(msg) => {
1024
+ resolve(msg.time);
1025
+ });
1026
+ this.#send({ type: 'getLoopEndTime' });
1027
+ });
1028
+ }
1029
+
1030
+ /**
1031
+ * Select a song number for multi-song MIDI files
1032
+ * @param {number} num - Song number (0-based)
1033
+ */
1034
+ selectSongNum(num) {
1035
+ this.#send({ type: 'selectSongNum', num });
1036
+ }
1037
+
1038
+ /**
1039
+ * Get the number of songs in the loaded MIDI file
1040
+ * @returns {Promise<number>}
1041
+ */
1042
+ async getSongsCount() {
1043
+ return new Promise((resolve) => {
1044
+ this.#onceMessage('songsCount', /** @param {{count: number}} msg */(msg) => {
1045
+ resolve(msg.count);
1046
+ });
1047
+ this.#send({ type: 'getSongsCount' });
1048
+ });
1049
+ }
1050
+
1051
+ /**
1052
+ * Get the number of tracks in the loaded MIDI file
1053
+ * @returns {Promise<number>}
1054
+ */
1055
+ async getTrackCount() {
1056
+ return new Promise((resolve) => {
1057
+ this.#onceMessage('trackCount', /** @param {{count: number}} msg */(msg) => {
1058
+ resolve(msg.count);
1059
+ });
1060
+ this.#send({ type: 'getTrackCount' });
1061
+ });
1062
+ }
1063
+
1064
+ /**
1065
+ * Set track options (enable, mute, or solo)
1066
+ * Use the TrackOption enum: TrackOption.ON (1), TrackOption.OFF (2), TrackOption.SOLO (3).
1067
+ * Note: Passing 0 is a silent no-op that resolves without changing state.
1068
+ * @param {number} track - Track index
1069
+ * @param {number} options - Track option from TrackOption enum
1070
+ * @returns {Promise<void>} Resolves on success, rejects on failure
1071
+ */
1072
+ async setTrackOptions(track, options) {
1073
+ const reqId = this.#nextRequestId++;
1074
+ return new Promise((resolve, reject) => {
1075
+ this.#onceCorrelatedMessage('trackOptionsSet', reqId, /** @param {{success: boolean}} msg */(msg) => {
1076
+ if (msg.success) {
1077
+ resolve();
1078
+ } else {
1079
+ reject(new Error(`Failed to set track options for track ${track}`));
1080
+ }
1081
+ });
1082
+ this.#send({ type: 'setTrackOptions', track, options, reqId });
1083
+ });
1084
+ }
1085
+
1086
+ /**
1087
+ * Enable or disable a MIDI channel
1088
+ * @param {number} channel - MIDI channel (0-15)
1089
+ * @param {boolean} enabled - Whether to enable the channel
1090
+ * @returns {Promise<void>} Resolves on success, rejects on failure
1091
+ */
1092
+ async setChannelEnabled(channel, enabled) {
1093
+ const reqId = this.#nextRequestId++;
1094
+ return new Promise((resolve, reject) => {
1095
+ this.#onceCorrelatedMessage('channelEnabledSet', reqId, /** @param {{success: boolean}} msg */(msg) => {
1096
+ if (msg.success) {
1097
+ resolve();
1098
+ } else {
1099
+ reject(new Error(`Failed to set channel ${channel} enabled state`));
1100
+ }
1101
+ });
1102
+ this.#send({ type: 'setChannelEnabled', channel, enabled, reqId });
1103
+ });
784
1104
  }
785
1105
 
786
1106
  /**