@marmooo/midy 0.4.1 → 0.4.2

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/script/midy.js CHANGED
@@ -29,12 +29,6 @@ class Note {
29
29
  writable: true,
30
30
  value: false
31
31
  });
32
- Object.defineProperty(this, "pending", {
33
- enumerable: true,
34
- configurable: true,
35
- writable: true,
36
- value: true
37
- });
38
32
  Object.defineProperty(this, "bufferSource", {
39
33
  enumerable: true,
40
34
  configurable: true,
@@ -116,6 +110,9 @@ class Note {
116
110
  this.noteNumber = noteNumber;
117
111
  this.velocity = velocity;
118
112
  this.startTime = startTime;
113
+ this.ready = new Promise((resolve) => {
114
+ this.resolveReady = resolve;
115
+ });
119
116
  }
120
117
  }
121
118
  const drumExclusiveClassesByKit = new Array(57);
@@ -193,6 +190,7 @@ const defaultControllerState = {
193
190
  // dataDecrement: { type: 128 + 97, defaultValue: 0 },
194
191
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
195
192
  // rpnMSB: { type: 128 + 101, defaultValue: 127 },
193
+ // rpgMakerLoop: { type: 128 + 111, defaultValue: 0 },
196
194
  // allSoundOff: { type: 128 + 120, defaultValue: 0 },
197
195
  // resetAllControllers: { type: 128 + 121, defaultValue: 0 },
198
196
  // allNotesOff: { type: 128 + 123, defaultValue: 0 },
@@ -252,8 +250,9 @@ const pitchEnvelopeKeys = [
252
250
  "playbackRate",
253
251
  ];
254
252
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
255
- class Midy {
253
+ class Midy extends EventTarget {
256
254
  constructor(audioContext) {
255
+ super();
257
256
  Object.defineProperty(this, "mode", {
258
257
  enumerable: true,
259
258
  configurable: true,
@@ -414,6 +413,18 @@ class Midy {
414
413
  writable: true,
415
414
  value: false
416
415
  });
416
+ Object.defineProperty(this, "loop", {
417
+ enumerable: true,
418
+ configurable: true,
419
+ writable: true,
420
+ value: true
421
+ });
422
+ Object.defineProperty(this, "loopStart", {
423
+ enumerable: true,
424
+ configurable: true,
425
+ writable: true,
426
+ value: 0
427
+ });
417
428
  Object.defineProperty(this, "playPromise", {
418
429
  enumerable: true,
419
430
  configurable: true,
@@ -635,7 +646,7 @@ class Midy {
635
646
  }
636
647
  return bufferSource;
637
648
  }
638
- async scheduleTimelineEvents(scheduleTime, queueIndex) {
649
+ scheduleTimelineEvents(scheduleTime, queueIndex) {
639
650
  const timeOffset = this.resumeTime - this.startTime;
640
651
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
641
652
  const schedulingOffset = this.startDelay - timeOffset;
@@ -647,7 +658,7 @@ class Midy {
647
658
  const startTime = event.startTime + schedulingOffset;
648
659
  switch (event.type) {
649
660
  case "noteOn":
650
- await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
661
+ this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
651
662
  break;
652
663
  case "noteOff": {
653
664
  this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
@@ -694,22 +705,23 @@ class Midy {
694
705
  }
695
706
  }
696
707
  updateStates(queueIndex, nextQueueIndex) {
708
+ const now = this.audioContext.currentTime;
697
709
  if (nextQueueIndex < queueIndex)
698
710
  queueIndex = 0;
699
711
  for (let i = queueIndex; i < nextQueueIndex; i++) {
700
712
  const event = this.timeline[i];
701
713
  switch (event.type) {
702
714
  case "controller":
703
- this.setControlChange(event.channel, event.controllerType, event.value, 0);
715
+ this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
704
716
  break;
705
717
  case "programChange":
706
- this.setProgramChange(event.channel, event.programNumber, 0);
718
+ this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
707
719
  break;
708
720
  case "pitchBend":
709
- this.setPitchBend(event.channel, event.value + 8192, 0);
721
+ this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
710
722
  break;
711
723
  case "sysEx":
712
- this.handleSysEx(event.data, 0);
724
+ this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
713
725
  }
714
726
  }
715
727
  }
@@ -717,58 +729,95 @@ class Midy {
717
729
  if (this.audioContext.state === "suspended") {
718
730
  await this.audioContext.resume();
719
731
  }
732
+ const paused = this.isPaused;
720
733
  this.isPlaying = true;
721
734
  this.isPaused = false;
722
735
  this.startTime = this.audioContext.currentTime;
736
+ if (paused) {
737
+ this.dispatchEvent(new Event("resumed"));
738
+ }
739
+ else {
740
+ this.dispatchEvent(new Event("started"));
741
+ }
723
742
  let queueIndex = this.getQueueIndex(this.resumeTime);
724
- let finished = false;
743
+ let exitReason;
725
744
  this.notePromises = [];
726
- while (queueIndex < this.timeline.length) {
745
+ while (true) {
727
746
  const now = this.audioContext.currentTime;
728
747
  if (0 < this.lastActiveSensing &&
729
748
  this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
730
749
  await this.stopNotes(0, true, now);
731
750
  await this.audioContext.suspend();
732
- finished = true;
751
+ exitReason = "aborted";
733
752
  break;
734
753
  }
754
+ if (this.timeline.length <= queueIndex) {
755
+ await this.stopNotes(0, true, now);
756
+ if (this.loop) {
757
+ this.notePromises = [];
758
+ this.resetAllStates();
759
+ this.startTime = this.audioContext.currentTime;
760
+ this.resumeTime = this.loopStart;
761
+ if (0 < this.loopStart) {
762
+ const nextQueueIndex = this.getQueueIndex(this.resumeTime);
763
+ this.updateStates(queueIndex, nextQueueIndex);
764
+ queueIndex = nextQueueIndex;
765
+ }
766
+ else {
767
+ queueIndex = 0;
768
+ }
769
+ this.dispatchEvent(new Event("looped"));
770
+ continue;
771
+ }
772
+ else {
773
+ await this.audioContext.suspend();
774
+ exitReason = "ended";
775
+ break;
776
+ }
777
+ }
735
778
  if (this.isPausing) {
736
779
  await this.stopNotes(0, true, now);
737
780
  await this.audioContext.suspend();
738
781
  this.notePromises = [];
782
+ this.isPausing = false;
783
+ exitReason = "paused";
739
784
  break;
740
785
  }
741
786
  else if (this.isStopping) {
742
787
  await this.stopNotes(0, true, now);
743
788
  await this.audioContext.suspend();
744
- finished = true;
789
+ this.isStopping = false;
790
+ exitReason = "stopped";
745
791
  break;
746
792
  }
747
793
  else if (this.isSeeking) {
748
- await this.stopNotes(0, true, now);
794
+ this.stopNotes(0, true, now);
749
795
  this.startTime = this.audioContext.currentTime;
750
796
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
751
797
  this.updateStates(queueIndex, nextQueueIndex);
752
798
  queueIndex = nextQueueIndex;
753
799
  this.isSeeking = false;
800
+ this.dispatchEvent(new Event("seeked"));
754
801
  continue;
755
802
  }
756
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
803
+ queueIndex = this.scheduleTimelineEvents(now, queueIndex);
757
804
  const waitTime = now + this.noteCheckInterval;
758
805
  await this.scheduleTask(() => { }, waitTime);
759
806
  }
760
- if (this.timeline.length <= queueIndex) {
761
- const now = this.audioContext.currentTime;
762
- await this.stopNotes(0, true, now);
763
- await this.audioContext.suspend();
764
- finished = true;
765
- }
766
- if (finished) {
807
+ if (exitReason !== "paused") {
767
808
  this.notePromises = [];
768
809
  this.resetAllStates();
769
810
  this.lastActiveSensing = 0;
770
811
  }
771
812
  this.isPlaying = false;
813
+ if (exitReason === "paused") {
814
+ this.isPaused = true;
815
+ this.dispatchEvent(new Event("paused"));
816
+ }
817
+ else {
818
+ this.isPaused = false;
819
+ this.dispatchEvent(new Event(exitReason));
820
+ }
772
821
  }
773
822
  ticksToSecond(ticks, secondsPerBeat) {
774
823
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -905,24 +954,20 @@ class Midy {
905
954
  return;
906
955
  this.isStopping = true;
907
956
  await this.playPromise;
908
- this.isStopping = false;
909
957
  }
910
958
  async pause() {
911
959
  if (!this.isPlaying || this.isPaused)
912
960
  return;
913
961
  const now = this.audioContext.currentTime;
914
- this.resumeTime = now - this.startTime - this.startDelay;
962
+ this.resumeTime = now + this.resumeTime - this.startTime;
915
963
  this.isPausing = true;
916
964
  await this.playPromise;
917
- this.isPausing = false;
918
- this.isPaused = true;
919
965
  }
920
966
  async resume() {
921
967
  if (!this.isPaused)
922
968
  return;
923
969
  this.playPromise = this.playNotes();
924
970
  await this.playPromise;
925
- this.isPaused = false;
926
971
  }
927
972
  seekTo(second) {
928
973
  this.resumeTime = second;
@@ -945,19 +990,23 @@ class Midy {
945
990
  const now = this.audioContext.currentTime;
946
991
  return now + this.resumeTime - this.startTime;
947
992
  }
948
- processScheduledNotes(channel, callback) {
993
+ async processScheduledNotes(channel, callback) {
949
994
  const scheduledNotes = channel.scheduledNotes;
995
+ const tasks = [];
950
996
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
951
997
  const note = scheduledNotes[i];
952
998
  if (!note)
953
999
  continue;
954
1000
  if (note.ending)
955
1001
  continue;
956
- callback(note);
1002
+ const task = note.ready.then(() => callback(note));
1003
+ tasks.push(task);
957
1004
  }
1005
+ await Promise.all(tasks);
958
1006
  }
959
- processActiveNotes(channel, scheduleTime, callback) {
1007
+ async processActiveNotes(channel, scheduleTime, callback) {
960
1008
  const scheduledNotes = channel.scheduledNotes;
1009
+ const tasks = [];
961
1010
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
962
1011
  const note = scheduledNotes[i];
963
1012
  if (!note)
@@ -966,8 +1015,10 @@ class Midy {
966
1015
  continue;
967
1016
  if (scheduleTime < note.startTime)
968
1017
  break;
969
- callback(note);
1018
+ const task = note.ready.then(() => callback(note));
1019
+ tasks.push(task);
970
1020
  }
1021
+ await Promise.all(tasks);
971
1022
  }
972
1023
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
973
1024
  const sampleRate = audioContext.sampleRate;
@@ -1547,11 +1598,7 @@ class Midy {
1547
1598
  return;
1548
1599
  await this.setNoteAudioNode(channel, note, realtime);
1549
1600
  this.setNoteRouting(channelNumber, note, startTime);
1550
- note.pending = false;
1551
- const off = note.offEvent;
1552
- if (off) {
1553
- this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1554
- }
1601
+ note.resolveReady();
1555
1602
  }
1556
1603
  disconnectNote(note) {
1557
1604
  note.bufferSource.disconnect();
@@ -1596,7 +1643,7 @@ class Midy {
1596
1643
  }, stopTime);
1597
1644
  });
1598
1645
  }
1599
- noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1646
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1600
1647
  const channel = this.channels[channelNumber];
1601
1648
  const state = channel.state;
1602
1649
  if (!force) {
@@ -1615,13 +1662,11 @@ class Midy {
1615
1662
  if (index < 0)
1616
1663
  return;
1617
1664
  const note = channel.scheduledNotes[index];
1618
- if (note.pending) {
1619
- note.offEvent = { velocity, startTime: endTime };
1620
- return;
1621
- }
1622
1665
  note.ending = true;
1623
1666
  this.setNoteIndex(channel, index);
1624
- const promise = this.releaseNote(channel, note, endTime);
1667
+ const promise = note.ready.then(() => {
1668
+ return this.releaseNote(channel, note, endTime);
1669
+ });
1625
1670
  this.notePromises.push(promise);
1626
1671
  return promise;
1627
1672
  }
@@ -1722,7 +1767,7 @@ class Midy {
1722
1767
  this.setEffects(channel, note, table, scheduleTime);
1723
1768
  }
1724
1769
  });
1725
- this.applyVoiceParams(channel, 10);
1770
+ this.applyVoiceParams(channel, 10, scheduleTime);
1726
1771
  }
1727
1772
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1728
1773
  const channel = this.channels[channelNumber];
@@ -1755,7 +1800,7 @@ class Midy {
1755
1800
  this.processActiveNotes(channel, scheduleTime, (note) => {
1756
1801
  this.setEffects(channel, note, table, scheduleTime);
1757
1802
  });
1758
- this.applyVoiceParams(channel, 13);
1803
+ this.applyVoiceParams(channel, 13, scheduleTime);
1759
1804
  }
1760
1805
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1761
1806
  const pitchBend = msb * 128 + lsb;
@@ -1765,7 +1810,8 @@ class Midy {
1765
1810
  const channel = this.channels[channelNumber];
1766
1811
  if (channel.isDrum)
1767
1812
  return;
1768
- scheduleTime ??= this.audioContext.currentTime;
1813
+ if (!(0 <= scheduleTime))
1814
+ scheduleTime = this.audioContext.currentTime;
1769
1815
  const state = channel.state;
1770
1816
  const prev = state.pitchWheel * 2 - 1;
1771
1817
  const next = (value - 8192) / 8192;
@@ -2043,6 +2089,7 @@ class Midy {
2043
2089
  handlers[97] = this.dataDecrement;
2044
2090
  handlers[100] = this.setRPNLSB;
2045
2091
  handlers[101] = this.setRPNMSB;
2092
+ handlers[111] = this.setRPGMakerLoop;
2046
2093
  handlers[120] = this.allSoundOff;
2047
2094
  handlers[121] = this.resetAllControllers;
2048
2095
  handlers[123] = this.allNotesOff;
@@ -2084,7 +2131,8 @@ class Midy {
2084
2131
  const channel = this.channels[channelNumber];
2085
2132
  if (channel.isDrum)
2086
2133
  return;
2087
- scheduleTime ??= this.audioContext.currentTime;
2134
+ if (!(0 <= scheduleTime))
2135
+ scheduleTime = this.audioContext.currentTime;
2088
2136
  const state = channel.state;
2089
2137
  const intPart = Math.trunc(value);
2090
2138
  state.modulationDepthMSB = intPart / 127;
@@ -2110,7 +2158,8 @@ class Midy {
2110
2158
  });
2111
2159
  }
2112
2160
  setPortamentoTime(channelNumber, value, scheduleTime) {
2113
- scheduleTime ??= this.audioContext.currentTime;
2161
+ if (!(0 <= scheduleTime))
2162
+ scheduleTime = this.audioContext.currentTime;
2114
2163
  const channel = this.channels[channelNumber];
2115
2164
  const state = channel.state;
2116
2165
  const intPart = Math.trunc(value);
@@ -2121,7 +2170,8 @@ class Midy {
2121
2170
  this.updatePortamento(channel, scheduleTime);
2122
2171
  }
2123
2172
  setVolume(channelNumber, value, scheduleTime) {
2124
- scheduleTime ??= this.audioContext.currentTime;
2173
+ if (!(0 <= scheduleTime))
2174
+ scheduleTime = this.audioContext.currentTime;
2125
2175
  const channel = this.channels[channelNumber];
2126
2176
  const state = channel.state;
2127
2177
  const intPart = Math.trunc(value);
@@ -2144,7 +2194,8 @@ class Midy {
2144
2194
  };
2145
2195
  }
2146
2196
  setPan(channelNumber, value, scheduleTime) {
2147
- scheduleTime ??= this.audioContext.currentTime;
2197
+ if (!(0 <= scheduleTime))
2198
+ scheduleTime = this.audioContext.currentTime;
2148
2199
  const channel = this.channels[channelNumber];
2149
2200
  const state = channel.state;
2150
2201
  const intPart = Math.trunc(value);
@@ -2160,7 +2211,8 @@ class Midy {
2160
2211
  }
2161
2212
  }
2162
2213
  setExpression(channelNumber, value, scheduleTime) {
2163
- scheduleTime ??= this.audioContext.currentTime;
2214
+ if (!(0 <= scheduleTime))
2215
+ scheduleTime = this.audioContext.currentTime;
2164
2216
  const channel = this.channels[channelNumber];
2165
2217
  const state = channel.state;
2166
2218
  const intPart = Math.trunc(value);
@@ -2217,7 +2269,8 @@ class Midy {
2217
2269
  const channel = this.channels[channelNumber];
2218
2270
  if (channel.isDrum)
2219
2271
  return;
2220
- scheduleTime ??= this.audioContext.currentTime;
2272
+ if (!(0 <= scheduleTime))
2273
+ scheduleTime = this.audioContext.currentTime;
2221
2274
  channel.state.sustainPedal = value / 127;
2222
2275
  if (64 <= value) {
2223
2276
  this.processScheduledNotes(channel, (note) => {
@@ -2235,7 +2288,8 @@ class Midy {
2235
2288
  const channel = this.channels[channelNumber];
2236
2289
  if (channel.isDrum)
2237
2290
  return;
2238
- scheduleTime ??= this.audioContext.currentTime;
2291
+ if (!(0 <= scheduleTime))
2292
+ scheduleTime = this.audioContext.currentTime;
2239
2293
  channel.state.portamento = value / 127;
2240
2294
  this.updatePortamento(channel, scheduleTime);
2241
2295
  }
@@ -2243,7 +2297,8 @@ class Midy {
2243
2297
  const channel = this.channels[channelNumber];
2244
2298
  if (channel.isDrum)
2245
2299
  return;
2246
- scheduleTime ??= this.audioContext.currentTime;
2300
+ if (!(0 <= scheduleTime))
2301
+ scheduleTime = this.audioContext.currentTime;
2247
2302
  channel.state.sostenutoPedal = value / 127;
2248
2303
  if (64 <= value) {
2249
2304
  const sostenutoNotes = [];
@@ -2264,7 +2319,8 @@ class Midy {
2264
2319
  if (channel.isDrum)
2265
2320
  return;
2266
2321
  const state = channel.state;
2267
- scheduleTime ??= this.audioContext.currentTime;
2322
+ if (!(0 <= scheduleTime))
2323
+ scheduleTime = this.audioContext.currentTime;
2268
2324
  state.softPedal = softPedal / 127;
2269
2325
  this.processScheduledNotes(channel, (note) => {
2270
2326
  if (this.isPortamento(channel, note)) {
@@ -2281,7 +2337,8 @@ class Midy {
2281
2337
  const channel = this.channels[channelNumber];
2282
2338
  if (channel.isDrum)
2283
2339
  return;
2284
- scheduleTime ??= this.audioContext.currentTime;
2340
+ if (!(0 <= scheduleTime))
2341
+ scheduleTime = this.audioContext.currentTime;
2285
2342
  const state = channel.state;
2286
2343
  state.filterResonance = ccValue / 127;
2287
2344
  this.processScheduledNotes(channel, (note) => {
@@ -2302,14 +2359,16 @@ class Midy {
2302
2359
  const channel = this.channels[channelNumber];
2303
2360
  if (channel.isDrum)
2304
2361
  return;
2305
- scheduleTime ??= this.audioContext.currentTime;
2362
+ if (!(0 <= scheduleTime))
2363
+ scheduleTime = this.audioContext.currentTime;
2306
2364
  channel.state.releaseTime = releaseTime / 127;
2307
2365
  }
2308
2366
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2309
2367
  const channel = this.channels[channelNumber];
2310
2368
  if (channel.isDrum)
2311
2369
  return;
2312
- scheduleTime ??= this.audioContext.currentTime;
2370
+ if (!(0 <= scheduleTime))
2371
+ scheduleTime = this.audioContext.currentTime;
2313
2372
  channel.state.attackTime = attackTime / 127;
2314
2373
  this.processScheduledNotes(channel, (note) => {
2315
2374
  if (scheduleTime < note.startTime) {
@@ -2322,7 +2381,8 @@ class Midy {
2322
2381
  if (channel.isDrum)
2323
2382
  return;
2324
2383
  const state = channel.state;
2325
- scheduleTime ??= this.audioContext.currentTime;
2384
+ if (!(0 <= scheduleTime))
2385
+ scheduleTime = this.audioContext.currentTime;
2326
2386
  state.brightness = brightness / 127;
2327
2387
  this.processScheduledNotes(channel, (note) => {
2328
2388
  if (this.isPortamento(channel, note)) {
@@ -2337,7 +2397,8 @@ class Midy {
2337
2397
  const channel = this.channels[channelNumber];
2338
2398
  if (channel.isDrum)
2339
2399
  return;
2340
- scheduleTime ??= this.audioContext.currentTime;
2400
+ if (!(0 <= scheduleTime))
2401
+ scheduleTime = this.audioContext.currentTime;
2341
2402
  channel.state.decayTime = dacayTime / 127;
2342
2403
  this.processScheduledNotes(channel, (note) => {
2343
2404
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2347,7 +2408,8 @@ class Midy {
2347
2408
  const channel = this.channels[channelNumber];
2348
2409
  if (channel.isDrum)
2349
2410
  return;
2350
- scheduleTime ??= this.audioContext.currentTime;
2411
+ if (!(0 <= scheduleTime))
2412
+ scheduleTime = this.audioContext.currentTime;
2351
2413
  channel.state.vibratoRate = vibratoRate / 127;
2352
2414
  if (channel.vibratoDepth <= 0)
2353
2415
  return;
@@ -2359,7 +2421,8 @@ class Midy {
2359
2421
  const channel = this.channels[channelNumber];
2360
2422
  if (channel.isDrum)
2361
2423
  return;
2362
- scheduleTime ??= this.audioContext.currentTime;
2424
+ if (!(0 <= scheduleTime))
2425
+ scheduleTime = this.audioContext.currentTime;
2363
2426
  const prev = channel.state.vibratoDepth;
2364
2427
  channel.state.vibratoDepth = vibratoDepth / 127;
2365
2428
  if (0 < prev) {
@@ -2377,7 +2440,8 @@ class Midy {
2377
2440
  const channel = this.channels[channelNumber];
2378
2441
  if (channel.isDrum)
2379
2442
  return;
2380
- scheduleTime ??= this.audioContext.currentTime;
2443
+ if (!(0 <= scheduleTime))
2444
+ scheduleTime = this.audioContext.currentTime;
2381
2445
  channel.state.vibratoDelay = vibratoDelay / 127;
2382
2446
  if (0 < channel.state.vibratoDepth) {
2383
2447
  this.processScheduledNotes(channel, (note) => {
@@ -2386,13 +2450,15 @@ class Midy {
2386
2450
  }
2387
2451
  }
2388
2452
  setPortamentoNoteNumber(channelNumber, value, scheduleTime) {
2389
- scheduleTime ??= this.audioContext.currentTime;
2453
+ if (!(0 <= scheduleTime))
2454
+ scheduleTime = this.audioContext.currentTime;
2390
2455
  const channel = this.channels[channelNumber];
2391
2456
  channel.portamentoControl = true;
2392
2457
  channel.state.portamentoNoteNumber = value / 127;
2393
2458
  }
2394
2459
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
2395
- scheduleTime ??= this.audioContext.currentTime;
2460
+ if (!(0 <= scheduleTime))
2461
+ scheduleTime = this.audioContext.currentTime;
2396
2462
  const channel = this.channels[channelNumber];
2397
2463
  const state = channel.state;
2398
2464
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2401,7 +2467,8 @@ class Midy {
2401
2467
  });
2402
2468
  }
2403
2469
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2404
- scheduleTime ??= this.audioContext.currentTime;
2470
+ if (!(0 <= scheduleTime))
2471
+ scheduleTime = this.audioContext.currentTime;
2405
2472
  const channel = this.channels[channelNumber];
2406
2473
  const state = channel.state;
2407
2474
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2461,12 +2528,14 @@ class Midy {
2461
2528
  }
2462
2529
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2463
2530
  dataIncrement(channelNumber, scheduleTime) {
2464
- scheduleTime ??= this.audioContext.currentTime;
2531
+ if (!(0 <= scheduleTime))
2532
+ scheduleTime = this.audioContext.currentTime;
2465
2533
  this.handleRPN(channelNumber, 1, scheduleTime);
2466
2534
  }
2467
2535
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2468
2536
  dataDecrement(channelNumber, scheduleTime) {
2469
- scheduleTime ??= this.audioContext.currentTime;
2537
+ if (!(0 <= scheduleTime))
2538
+ scheduleTime = this.audioContext.currentTime;
2470
2539
  this.handleRPN(channelNumber, -1, scheduleTime);
2471
2540
  }
2472
2541
  setRPNMSB(channelNumber, value) {
@@ -2489,7 +2558,8 @@ class Midy {
2489
2558
  const channel = this.channels[channelNumber];
2490
2559
  if (channel.isDrum)
2491
2560
  return;
2492
- scheduleTime ??= this.audioContext.currentTime;
2561
+ if (!(0 <= scheduleTime))
2562
+ scheduleTime = this.audioContext.currentTime;
2493
2563
  const state = channel.state;
2494
2564
  const prev = state.pitchWheelSensitivity;
2495
2565
  const next = value / 12800;
@@ -2509,7 +2579,8 @@ class Midy {
2509
2579
  const channel = this.channels[channelNumber];
2510
2580
  if (channel.isDrum)
2511
2581
  return;
2512
- scheduleTime ??= this.audioContext.currentTime;
2582
+ if (!(0 <= scheduleTime))
2583
+ scheduleTime = this.audioContext.currentTime;
2513
2584
  const prev = channel.fineTuning;
2514
2585
  const next = value;
2515
2586
  channel.fineTuning = next;
@@ -2526,7 +2597,8 @@ class Midy {
2526
2597
  const channel = this.channels[channelNumber];
2527
2598
  if (channel.isDrum)
2528
2599
  return;
2529
- scheduleTime ??= this.audioContext.currentTime;
2600
+ if (!(0 <= scheduleTime))
2601
+ scheduleTime = this.audioContext.currentTime;
2530
2602
  const prev = channel.coarseTuning;
2531
2603
  const next = value;
2532
2604
  channel.coarseTuning = next;
@@ -2543,12 +2615,18 @@ class Midy {
2543
2615
  const channel = this.channels[channelNumber];
2544
2616
  if (channel.isDrum)
2545
2617
  return;
2546
- scheduleTime ??= this.audioContext.currentTime;
2618
+ if (!(0 <= scheduleTime))
2619
+ scheduleTime = this.audioContext.currentTime;
2547
2620
  channel.modulationDepthRange = value;
2548
2621
  this.updateModulation(channel, scheduleTime);
2549
2622
  }
2550
- allSoundOff(channelNumber, _value, scheduleTime) {
2623
+ setRPGMakerLoop(_channelNumber, _value, scheduleTime) {
2551
2624
  scheduleTime ??= this.audioContext.currentTime;
2625
+ this.loopStart = scheduleTime + this.resumeTime - this.startTime;
2626
+ }
2627
+ allSoundOff(channelNumber, _value, scheduleTime) {
2628
+ if (!(0 <= scheduleTime))
2629
+ scheduleTime = this.audioContext.currentTime;
2552
2630
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2553
2631
  }
2554
2632
  resetChannelStates(channelNumber) {
@@ -2610,7 +2688,8 @@ class Midy {
2610
2688
  }
2611
2689
  }
2612
2690
  allNotesOff(channelNumber, _value, scheduleTime) {
2613
- scheduleTime ??= this.audioContext.currentTime;
2691
+ if (!(0 <= scheduleTime))
2692
+ scheduleTime = this.audioContext.currentTime;
2614
2693
  return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2615
2694
  }
2616
2695
  omniOff(channelNumber, value, scheduleTime) {
@@ -2662,7 +2741,8 @@ class Midy {
2662
2741
  }
2663
2742
  }
2664
2743
  GM1SystemOn(scheduleTime) {
2665
- scheduleTime ??= this.audioContext.currentTime;
2744
+ if (!(0 <= scheduleTime))
2745
+ scheduleTime = this.audioContext.currentTime;
2666
2746
  this.mode = "GM1";
2667
2747
  for (let i = 0; i < this.channels.length; i++) {
2668
2748
  this.allSoundOff(i, 0, scheduleTime);
@@ -2675,7 +2755,8 @@ class Midy {
2675
2755
  this.channels[9].isDrum = true;
2676
2756
  }
2677
2757
  GM2SystemOn(scheduleTime) {
2678
- scheduleTime ??= this.audioContext.currentTime;
2758
+ if (!(0 <= scheduleTime))
2759
+ scheduleTime = this.audioContext.currentTime;
2679
2760
  this.mode = "GM2";
2680
2761
  for (let i = 0; i < this.channels.length; i++) {
2681
2762
  this.allSoundOff(i, 0, scheduleTime);
@@ -2743,7 +2824,8 @@ class Midy {
2743
2824
  this.setMasterVolume(volume, scheduleTime);
2744
2825
  }
2745
2826
  setMasterVolume(value, scheduleTime) {
2746
- scheduleTime ??= this.audioContext.currentTime;
2827
+ if (!(0 <= scheduleTime))
2828
+ scheduleTime = this.audioContext.currentTime;
2747
2829
  this.masterVolume.gain
2748
2830
  .cancelScheduledValues(scheduleTime)
2749
2831
  .setValueAtTime(value * value, scheduleTime);