libadlmidi-js 1.2.0 → 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 -65
  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 +465 -21
  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 -56
  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,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
@@ -231,6 +202,29 @@ export class AdlMidi {
231
202
  this.#messageHandlers.get(type)?.add(wrappedHandler);
232
203
  }
233
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
+
234
228
  /**
235
229
  * Send a message to the processor
236
230
  * @param {Object} msg - Message to send
@@ -369,7 +363,7 @@ export class AdlMidi {
369
363
  * @param {ArrayBuffer} arrayBuffer - Bank file data
370
364
  * @returns {Promise<void>}
371
365
  */
372
- async loadBank(arrayBuffer) {
366
+ async loadBankData(arrayBuffer) {
373
367
  return new Promise((resolve, reject) => {
374
368
  this.#onceMessage('bankLoaded', /** @param {{success: boolean, error?: string}} msg */(msg) => {
375
369
  if (msg.success) {
@@ -379,7 +373,7 @@ export class AdlMidi {
379
373
  }
380
374
  });
381
375
 
382
- this.#send({ type: 'loadBank', data: arrayBuffer });
376
+ this.#send({ type: 'loadBankData', data: arrayBuffer });
383
377
  });
384
378
  }
385
379
 
@@ -472,6 +466,19 @@ export class AdlMidi {
472
466
  });
473
467
  }
474
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
+
475
482
  /**
476
483
  * Enable/disable scaling of modulators by volume
477
484
  * @param {boolean} enabled
@@ -531,35 +538,61 @@ export class AdlMidi {
531
538
  }
532
539
 
533
540
  /**
534
- * Set the volume model
541
+ * Set the volume range model
535
542
  * @param {number} model - Volume model number
536
543
  */
537
- setVolumeModel(model) {
538
- this.#send({ type: 'setVolumeModel', model });
544
+ setVolumeRangeModel(model) {
545
+ this.#send({ type: 'setVolumeRangeModel', model });
539
546
  }
540
547
 
541
548
  /**
542
- * Enable/disable rhythm mode (percussion)
549
+ * Enable/disable soft stereo panning
543
550
  * @param {boolean} enabled
544
551
  */
545
- setPercussionMode(enabled) {
546
- this.#send({ type: 'setPercMode', enabled });
552
+ setSoftPanEnabled(enabled) {
553
+ this.#send({ type: 'setSoftPanEnabled', enabled });
547
554
  }
548
555
 
549
556
  /**
550
557
  * Enable/disable deep vibrato
551
558
  * @param {boolean} enabled
552
559
  */
553
- setVibrato(enabled) {
554
- 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
+ });
555
575
  }
556
576
 
557
577
  /**
558
578
  * Enable/disable deep tremolo
559
579
  * @param {boolean} enabled
560
580
  */
561
- setTremolo(enabled) {
562
- 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
+ });
563
596
  }
564
597
 
565
598
  /**
@@ -577,7 +610,7 @@ export class AdlMidi {
577
610
  * - nuked profile: NUKED only
578
611
  * - dosbox profile: DOSBOX only
579
612
  * - light profile: NUKED, DOSBOX
580
- * - full profile: NUKED, DOSBOX, OPAL, JAVA, ESFMU, YMFM_OPL2, YMFM_OPL3
613
+ * - full profile: NUKED, DOSBOX, OPAL, JAVA, ESFMu, YMFM_OPL2, YMFM_OPL3
581
614
  *
582
615
  * @param {number} emulator - Emulator ID from the Emulator enum
583
616
  * @returns {Promise<void>} Resolves when emulator is switched, rejects if unavailable
@@ -614,6 +647,19 @@ export class AdlMidi {
614
647
  });
615
648
  }
616
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
+
617
663
  /**
618
664
  * Get the version string of the linked libADLMIDI library
619
665
  * @returns {Promise<string>}
@@ -670,12 +716,12 @@ export class AdlMidi {
670
716
  * Get the volume range model
671
717
  * @returns {Promise<number>}
672
718
  */
673
- async getVolumeModel() {
719
+ async getVolumeRangeModel() {
674
720
  return new Promise((resolve) => {
675
- this.#onceMessage('volumeModel', /** @param {{model: number}} msg */(msg) => {
721
+ this.#onceMessage('volumeRangeModel', /** @param {{model: number}} msg */(msg) => {
676
722
  resolve(msg.model);
677
723
  });
678
- this.#send({ type: 'getVolumeModel' });
724
+ this.#send({ type: 'getVolumeRangeModel' });
679
725
  });
680
726
  }
681
727
 
@@ -696,6 +742,119 @@ export class AdlMidi {
696
742
  });
697
743
  }
698
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
+
699
858
  /**
700
859
  * Reset the synthesizer
701
860
  * @returns {void}
@@ -751,6 +910,47 @@ export class AdlMidi {
751
910
  });
752
911
  }
753
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
+
754
954
  /**
755
955
  * Start or resume MIDI file playback
756
956
  * @returns {void}
@@ -781,8 +981,126 @@ export class AdlMidi {
781
981
  * @param {boolean} enabled - Whether to loop
782
982
  * @returns {void}
783
983
  */
784
- setLoop(enabled) {
785
- 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
+ });
786
1104
  }
787
1105
 
788
1106
  /**