@marmooo/midy 0.4.0 → 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/esm/midy.js CHANGED
@@ -26,12 +26,6 @@ class Note {
26
26
  writable: true,
27
27
  value: false
28
28
  });
29
- Object.defineProperty(this, "pending", {
30
- enumerable: true,
31
- configurable: true,
32
- writable: true,
33
- value: true
34
- });
35
29
  Object.defineProperty(this, "bufferSource", {
36
30
  enumerable: true,
37
31
  configurable: true,
@@ -113,6 +107,9 @@ class Note {
113
107
  this.noteNumber = noteNumber;
114
108
  this.velocity = velocity;
115
109
  this.startTime = startTime;
110
+ this.ready = new Promise((resolve) => {
111
+ this.resolveReady = resolve;
112
+ });
116
113
  }
117
114
  }
118
115
  const drumExclusiveClassesByKit = new Array(57);
@@ -158,14 +155,19 @@ const defaultControllerState = {
158
155
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
159
156
  link: { type: 127, defaultValue: 0 },
160
157
  // bankMSB: { type: 128 + 0, defaultValue: 121, },
161
- modulationDepth: { type: 128 + 1, defaultValue: 0 },
162
- portamentoTime: { type: 128 + 5, defaultValue: 0 },
158
+ modulationDepthMSB: { type: 128 + 1, defaultValue: 0 },
159
+ portamentoTimeMSB: { type: 128 + 5, defaultValue: 0 },
163
160
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
164
- volume: { type: 128 + 7, defaultValue: 100 / 127 },
165
- pan: { type: 128 + 10, defaultValue: 64 / 127 },
166
- expression: { type: 128 + 11, defaultValue: 1 },
161
+ volumeMSB: { type: 128 + 7, defaultValue: 100 / 127 },
162
+ panMSB: { type: 128 + 10, defaultValue: 64 / 127 },
163
+ expressionMSB: { type: 128 + 11, defaultValue: 1 },
167
164
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
165
+ modulationDepthLSB: { type: 128 + 33, defaultValue: 0 },
166
+ portamentoTimeLSB: { type: 128 + 37, defaultValue: 0 },
168
167
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
168
+ volumeLSB: { type: 128 + 39, defaultValue: 0 },
169
+ panLSB: { type: 128 + 42, defaultValue: 0 },
170
+ expressionLSB: { type: 128 + 43, defaultValue: 0 },
169
171
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
170
172
  portamento: { type: 128 + 65, defaultValue: 0 },
171
173
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
@@ -178,12 +180,14 @@ const defaultControllerState = {
178
180
  vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
179
181
  vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
180
182
  vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
183
+ portamentoNoteNumber: { type: 128 + 84, defaultValue: 0 },
181
184
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
182
185
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
183
186
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
184
187
  // dataDecrement: { type: 128 + 97, defaultValue: 0 },
185
188
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
186
189
  // rpnMSB: { type: 128 + 101, defaultValue: 127 },
190
+ // rpgMakerLoop: { type: 128 + 111, defaultValue: 0 },
187
191
  // allSoundOff: { type: 128 + 120, defaultValue: 0 },
188
192
  // resetAllControllers: { type: 128 + 121, defaultValue: 0 },
189
193
  // allNotesOff: { type: 128 + 123, defaultValue: 0 },
@@ -243,8 +247,9 @@ const pitchEnvelopeKeys = [
243
247
  "playbackRate",
244
248
  ];
245
249
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
246
- export class Midy {
250
+ export class Midy extends EventTarget {
247
251
  constructor(audioContext) {
252
+ super();
248
253
  Object.defineProperty(this, "mode", {
249
254
  enumerable: true,
250
255
  configurable: true,
@@ -405,6 +410,18 @@ export class Midy {
405
410
  writable: true,
406
411
  value: false
407
412
  });
413
+ Object.defineProperty(this, "loop", {
414
+ enumerable: true,
415
+ configurable: true,
416
+ writable: true,
417
+ value: true
418
+ });
419
+ Object.defineProperty(this, "loopStart", {
420
+ enumerable: true,
421
+ configurable: true,
422
+ writable: true,
423
+ value: 0
424
+ });
408
425
  Object.defineProperty(this, "playPromise", {
409
426
  enumerable: true,
410
427
  configurable: true,
@@ -561,7 +578,7 @@ export class Midy {
561
578
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
562
579
  }
563
580
  createChannelAudioNodes(audioContext) {
564
- const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
581
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
565
582
  const gainL = new GainNode(audioContext, { gain: gainLeft });
566
583
  const gainR = new GainNode(audioContext, { gain: gainRight });
567
584
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -604,10 +621,9 @@ export class Midy {
604
621
  return channels;
605
622
  }
606
623
  async createAudioBuffer(voiceParams) {
607
- const sample = voiceParams.sample;
608
- const sampleStart = voiceParams.start;
609
- const sampleEnd = sample.data.length + voiceParams.end;
610
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
624
+ const { sample, start, end } = voiceParams;
625
+ const sampleEnd = sample.data.length + end;
626
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
611
627
  return audioBuffer;
612
628
  }
613
629
  isLoopDrum(channel, noteNumber) {
@@ -627,7 +643,7 @@ export class Midy {
627
643
  }
628
644
  return bufferSource;
629
645
  }
630
- async scheduleTimelineEvents(scheduleTime, queueIndex) {
646
+ scheduleTimelineEvents(scheduleTime, queueIndex) {
631
647
  const timeOffset = this.resumeTime - this.startTime;
632
648
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
633
649
  const schedulingOffset = this.startDelay - timeOffset;
@@ -639,12 +655,10 @@ export class Midy {
639
655
  const startTime = event.startTime + schedulingOffset;
640
656
  switch (event.type) {
641
657
  case "noteOn":
642
- await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
658
+ this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
643
659
  break;
644
660
  case "noteOff": {
645
- const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
646
- if (notePromise)
647
- this.notePromises.push(notePromise);
661
+ this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
648
662
  break;
649
663
  }
650
664
  case "noteAftertouch":
@@ -688,22 +702,23 @@ export class Midy {
688
702
  }
689
703
  }
690
704
  updateStates(queueIndex, nextQueueIndex) {
705
+ const now = this.audioContext.currentTime;
691
706
  if (nextQueueIndex < queueIndex)
692
707
  queueIndex = 0;
693
708
  for (let i = queueIndex; i < nextQueueIndex; i++) {
694
709
  const event = this.timeline[i];
695
710
  switch (event.type) {
696
711
  case "controller":
697
- this.setControlChange(event.channel, event.controllerType, event.value, 0);
712
+ this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
698
713
  break;
699
714
  case "programChange":
700
- this.setProgramChange(event.channel, event.programNumber, 0);
715
+ this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
701
716
  break;
702
717
  case "pitchBend":
703
- this.setPitchBend(event.channel, event.value + 8192, 0);
718
+ this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
704
719
  break;
705
720
  case "sysEx":
706
- this.handleSysEx(event.data, 0);
721
+ this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
707
722
  }
708
723
  }
709
724
  }
@@ -711,52 +726,95 @@ export class Midy {
711
726
  if (this.audioContext.state === "suspended") {
712
727
  await this.audioContext.resume();
713
728
  }
729
+ const paused = this.isPaused;
714
730
  this.isPlaying = true;
715
731
  this.isPaused = false;
716
732
  this.startTime = this.audioContext.currentTime;
733
+ if (paused) {
734
+ this.dispatchEvent(new Event("resumed"));
735
+ }
736
+ else {
737
+ this.dispatchEvent(new Event("started"));
738
+ }
717
739
  let queueIndex = this.getQueueIndex(this.resumeTime);
718
- let finished = false;
740
+ let exitReason;
719
741
  this.notePromises = [];
720
- while (queueIndex < this.timeline.length) {
742
+ while (true) {
721
743
  const now = this.audioContext.currentTime;
722
744
  if (0 < this.lastActiveSensing &&
723
745
  this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
724
746
  await this.stopNotes(0, true, now);
725
747
  await this.audioContext.suspend();
726
- finished = true;
748
+ exitReason = "aborted";
727
749
  break;
728
750
  }
751
+ if (this.timeline.length <= queueIndex) {
752
+ await this.stopNotes(0, true, now);
753
+ if (this.loop) {
754
+ this.notePromises = [];
755
+ this.resetAllStates();
756
+ this.startTime = this.audioContext.currentTime;
757
+ this.resumeTime = this.loopStart;
758
+ if (0 < this.loopStart) {
759
+ const nextQueueIndex = this.getQueueIndex(this.resumeTime);
760
+ this.updateStates(queueIndex, nextQueueIndex);
761
+ queueIndex = nextQueueIndex;
762
+ }
763
+ else {
764
+ queueIndex = 0;
765
+ }
766
+ this.dispatchEvent(new Event("looped"));
767
+ continue;
768
+ }
769
+ else {
770
+ await this.audioContext.suspend();
771
+ exitReason = "ended";
772
+ break;
773
+ }
774
+ }
729
775
  if (this.isPausing) {
730
776
  await this.stopNotes(0, true, now);
731
777
  await this.audioContext.suspend();
732
778
  this.notePromises = [];
779
+ this.isPausing = false;
780
+ exitReason = "paused";
733
781
  break;
734
782
  }
735
783
  else if (this.isStopping) {
736
784
  await this.stopNotes(0, true, now);
737
785
  await this.audioContext.suspend();
738
- finished = true;
786
+ this.isStopping = false;
787
+ exitReason = "stopped";
739
788
  break;
740
789
  }
741
790
  else if (this.isSeeking) {
742
- await this.stopNotes(0, true, now);
791
+ this.stopNotes(0, true, now);
743
792
  this.startTime = this.audioContext.currentTime;
744
793
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
745
794
  this.updateStates(queueIndex, nextQueueIndex);
746
795
  queueIndex = nextQueueIndex;
747
796
  this.isSeeking = false;
797
+ this.dispatchEvent(new Event("seeked"));
748
798
  continue;
749
799
  }
750
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
800
+ queueIndex = this.scheduleTimelineEvents(now, queueIndex);
751
801
  const waitTime = now + this.noteCheckInterval;
752
802
  await this.scheduleTask(() => { }, waitTime);
753
803
  }
754
- if (finished) {
804
+ if (exitReason !== "paused") {
755
805
  this.notePromises = [];
756
806
  this.resetAllStates();
757
807
  this.lastActiveSensing = 0;
758
808
  }
759
809
  this.isPlaying = false;
810
+ if (exitReason === "paused") {
811
+ this.isPaused = true;
812
+ this.dispatchEvent(new Event("paused"));
813
+ }
814
+ else {
815
+ this.isPaused = false;
816
+ this.dispatchEvent(new Event(exitReason));
817
+ }
760
818
  }
761
819
  ticksToSecond(ticks, secondsPerBeat) {
762
820
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -893,24 +951,20 @@ export class Midy {
893
951
  return;
894
952
  this.isStopping = true;
895
953
  await this.playPromise;
896
- this.isStopping = false;
897
954
  }
898
955
  async pause() {
899
956
  if (!this.isPlaying || this.isPaused)
900
957
  return;
901
958
  const now = this.audioContext.currentTime;
902
- this.resumeTime = now - this.startTime - this.startDelay;
959
+ this.resumeTime = now + this.resumeTime - this.startTime;
903
960
  this.isPausing = true;
904
961
  await this.playPromise;
905
- this.isPausing = false;
906
- this.isPaused = true;
907
962
  }
908
963
  async resume() {
909
964
  if (!this.isPaused)
910
965
  return;
911
966
  this.playPromise = this.playNotes();
912
967
  await this.playPromise;
913
- this.isPaused = false;
914
968
  }
915
969
  seekTo(second) {
916
970
  this.resumeTime = second;
@@ -933,19 +987,23 @@ export class Midy {
933
987
  const now = this.audioContext.currentTime;
934
988
  return now + this.resumeTime - this.startTime;
935
989
  }
936
- processScheduledNotes(channel, callback) {
990
+ async processScheduledNotes(channel, callback) {
937
991
  const scheduledNotes = channel.scheduledNotes;
992
+ const tasks = [];
938
993
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
939
994
  const note = scheduledNotes[i];
940
995
  if (!note)
941
996
  continue;
942
997
  if (note.ending)
943
998
  continue;
944
- callback(note);
999
+ const task = note.ready.then(() => callback(note));
1000
+ tasks.push(task);
945
1001
  }
1002
+ await Promise.all(tasks);
946
1003
  }
947
- processActiveNotes(channel, scheduleTime, callback) {
1004
+ async processActiveNotes(channel, scheduleTime, callback) {
948
1005
  const scheduledNotes = channel.scheduledNotes;
1006
+ const tasks = [];
949
1007
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
950
1008
  const note = scheduledNotes[i];
951
1009
  if (!note)
@@ -954,8 +1012,10 @@ export class Midy {
954
1012
  continue;
955
1013
  if (scheduleTime < note.startTime)
956
1014
  break;
957
- callback(note);
1015
+ const task = note.ready.then(() => callback(note));
1016
+ tasks.push(task);
958
1017
  }
1018
+ await Promise.all(tasks);
959
1019
  }
960
1020
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
961
1021
  const sampleRate = audioContext.sampleRate;
@@ -1143,6 +1203,13 @@ export class Midy {
1143
1203
  const noteDetune = this.calcNoteDetune(channel, note);
1144
1204
  const pitchControl = this.getPitchControl(channel, note);
1145
1205
  const detune = channel.detune + noteDetune + pitchControl;
1206
+ if (channel.portamentoControl) {
1207
+ const state = channel.state;
1208
+ const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1209
+ note.portamentoNoteNumber = portamentoNoteNumber;
1210
+ channel.portamentoControl = false;
1211
+ state.portamentoNoteNumber = 0;
1212
+ }
1146
1213
  if (this.isPortamento(channel, note)) {
1147
1214
  const startTime = note.startTime;
1148
1215
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
@@ -1159,8 +1226,10 @@ export class Midy {
1159
1226
  }
1160
1227
  }
1161
1228
  getPortamentoTime(channel, note) {
1229
+ const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
1230
+ const portamentoTime = portamentoTimeMSB + portamentoTimeLSB / 128;
1162
1231
  const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1163
- const value = Math.ceil(channel.state.portamentoTime * 127);
1232
+ const value = Math.ceil(portamentoTime * 128);
1164
1233
  return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1165
1234
  }
1166
1235
  getPitchIncrementSpeed(value) {
@@ -1424,7 +1493,7 @@ export class Midy {
1424
1493
  if (0 < state.vibratoDepth) {
1425
1494
  this.startVibrato(channel, note, now);
1426
1495
  }
1427
- if (0 < state.modulationDepth) {
1496
+ if (0 < state.modulationDepthMSB + state.modulationDepthLSB) {
1428
1497
  this.startModulation(channel, note, now);
1429
1498
  }
1430
1499
  if (channel.mono && channel.currentBufferSource) {
@@ -1435,7 +1504,13 @@ export class Midy {
1435
1504
  note.filterNode.connect(note.volumeEnvelopeNode);
1436
1505
  this.setChorusSend(channel, note, now);
1437
1506
  this.setReverbSend(channel, note, now);
1438
- note.bufferSource.start(startTime);
1507
+ if (voiceParams.sample.type === "compressed") {
1508
+ const offset = voiceParams.start / audioBuffer.sampleRate;
1509
+ note.bufferSource.start(startTime, offset);
1510
+ }
1511
+ else {
1512
+ note.bufferSource.start(startTime);
1513
+ }
1439
1514
  return note;
1440
1515
  }
1441
1516
  handleExclusiveClass(note, channelNumber, startTime) {
@@ -1520,11 +1595,7 @@ export class Midy {
1520
1595
  return;
1521
1596
  await this.setNoteAudioNode(channel, note, realtime);
1522
1597
  this.setNoteRouting(channelNumber, note, startTime);
1523
- note.pending = false;
1524
- const off = note.offEvent;
1525
- if (off) {
1526
- this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1527
- }
1598
+ note.resolveReady();
1528
1599
  }
1529
1600
  disconnectNote(note) {
1530
1601
  note.bufferSource.disconnect();
@@ -1569,7 +1640,7 @@ export class Midy {
1569
1640
  }, stopTime);
1570
1641
  });
1571
1642
  }
1572
- noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1643
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1573
1644
  const channel = this.channels[channelNumber];
1574
1645
  const state = channel.state;
1575
1646
  if (!force) {
@@ -1588,13 +1659,13 @@ export class Midy {
1588
1659
  if (index < 0)
1589
1660
  return;
1590
1661
  const note = channel.scheduledNotes[index];
1591
- if (note.pending) {
1592
- note.offEvent = { velocity, startTime: endTime };
1593
- return;
1594
- }
1595
1662
  note.ending = true;
1596
1663
  this.setNoteIndex(channel, index);
1597
- this.releaseNote(channel, note, endTime);
1664
+ const promise = note.ready.then(() => {
1665
+ return this.releaseNote(channel, note, endTime);
1666
+ });
1667
+ this.notePromises.push(promise);
1668
+ return promise;
1598
1669
  }
1599
1670
  setNoteIndex(channel, index) {
1600
1671
  let allEnds = true;
@@ -1693,7 +1764,7 @@ export class Midy {
1693
1764
  this.setEffects(channel, note, table, scheduleTime);
1694
1765
  }
1695
1766
  });
1696
- this.applyVoiceParams(channel, 10);
1767
+ this.applyVoiceParams(channel, 10, scheduleTime);
1697
1768
  }
1698
1769
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1699
1770
  const channel = this.channels[channelNumber];
@@ -1726,7 +1797,7 @@ export class Midy {
1726
1797
  this.processActiveNotes(channel, scheduleTime, (note) => {
1727
1798
  this.setEffects(channel, note, table, scheduleTime);
1728
1799
  });
1729
- this.applyVoiceParams(channel, 13);
1800
+ this.applyVoiceParams(channel, 13, scheduleTime);
1730
1801
  }
1731
1802
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1732
1803
  const pitchBend = msb * 128 + lsb;
@@ -1736,7 +1807,8 @@ export class Midy {
1736
1807
  const channel = this.channels[channelNumber];
1737
1808
  if (channel.isDrum)
1738
1809
  return;
1739
- scheduleTime ??= this.audioContext.currentTime;
1810
+ if (!(0 <= scheduleTime))
1811
+ scheduleTime = this.audioContext.currentTime;
1740
1812
  const state = channel.state;
1741
1813
  const prev = state.pitchWheel * 2 - 1;
1742
1814
  const next = (value - 8192) / 8192;
@@ -1747,9 +1819,11 @@ export class Midy {
1747
1819
  }
1748
1820
  setModLfoToPitch(channel, note, scheduleTime) {
1749
1821
  if (note.modulationDepth) {
1822
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1823
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
1750
1824
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1751
1825
  this.getLFOPitchDepth(channel, note);
1752
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1826
+ const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
1753
1827
  const depth = baseDepth * Math.sign(modLfoToPitch);
1754
1828
  note.modulationDepth.gain
1755
1829
  .cancelScheduledValues(scheduleTime)
@@ -1882,7 +1956,8 @@ export class Midy {
1882
1956
  createVoiceParamsHandlers() {
1883
1957
  return {
1884
1958
  modLfoToPitch: (channel, note, scheduleTime) => {
1885
- if (0 < channel.state.modulationDepth) {
1959
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1960
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1886
1961
  this.setModLfoToPitch(channel, note, scheduleTime);
1887
1962
  }
1888
1963
  },
@@ -1892,12 +1967,14 @@ export class Midy {
1892
1967
  }
1893
1968
  },
1894
1969
  modLfoToFilterFc: (channel, note, scheduleTime) => {
1895
- if (0 < channel.state.modulationDepth) {
1970
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1971
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1896
1972
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1897
1973
  }
1898
1974
  },
1899
1975
  modLfoToVolume: (channel, note, scheduleTime) => {
1900
- if (0 < channel.state.modulationDepth) {
1976
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1977
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1901
1978
  this.setModLfoToVolume(channel, note, scheduleTime);
1902
1979
  }
1903
1980
  },
@@ -1908,12 +1985,14 @@ export class Midy {
1908
1985
  this.setReverbSend(channel, note, scheduleTime);
1909
1986
  },
1910
1987
  delayModLFO: (_channel, note, _scheduleTime) => {
1911
- if (0 < channel.state.modulationDepth) {
1988
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1989
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1912
1990
  this.setDelayModLFO(note);
1913
1991
  }
1914
1992
  },
1915
1993
  freqModLFO: (_channel, note, scheduleTime) => {
1916
- if (0 < channel.state.modulationDepth) {
1994
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1995
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1917
1996
  this.setFreqModLFO(note, scheduleTime);
1918
1997
  }
1919
1998
  },
@@ -1982,7 +2061,12 @@ export class Midy {
1982
2061
  handlers[10] = this.setPan;
1983
2062
  handlers[11] = this.setExpression;
1984
2063
  handlers[32] = this.setBankLSB;
2064
+ handlers[33] = this.setModulationDepth;
2065
+ handlers[37] = this.setPortamentoTime;
1985
2066
  handlers[38] = this.dataEntryLSB;
2067
+ handlers[39] = this.setVolume;
2068
+ handlers[42] = this.setPan;
2069
+ handlers[43] = this.setExpression;
1986
2070
  handlers[64] = this.setSustainPedal;
1987
2071
  handlers[65] = this.setPortamento;
1988
2072
  handlers[66] = this.setSostenutoPedal;
@@ -1995,12 +2079,14 @@ export class Midy {
1995
2079
  handlers[76] = this.setVibratoRate;
1996
2080
  handlers[77] = this.setVibratoDepth;
1997
2081
  handlers[78] = this.setVibratoDelay;
2082
+ handlers[84] = this.setPortamentoNoteNumber;
1998
2083
  handlers[91] = this.setReverbSendLevel;
1999
2084
  handlers[93] = this.setChorusSendLevel;
2000
2085
  handlers[96] = this.dataIncrement;
2001
2086
  handlers[97] = this.dataDecrement;
2002
2087
  handlers[100] = this.setRPNLSB;
2003
2088
  handlers[101] = this.setRPNMSB;
2089
+ handlers[111] = this.setRPGMakerLoop;
2004
2090
  handlers[120] = this.allSoundOff;
2005
2091
  handlers[121] = this.resetAllControllers;
2006
2092
  handlers[123] = this.allNotesOff;
@@ -2026,7 +2112,9 @@ export class Midy {
2026
2112
  this.channels[channelNumber].bankMSB = msb;
2027
2113
  }
2028
2114
  updateModulation(channel, scheduleTime) {
2029
- const depth = channel.state.modulationDepth * channel.modulationDepthRange;
2115
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
2116
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
2117
+ const depth = modulationDepth * channel.modulationDepthRange;
2030
2118
  this.processScheduledNotes(channel, (note) => {
2031
2119
  if (note.modulationDepth) {
2032
2120
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
@@ -2036,12 +2124,16 @@ export class Midy {
2036
2124
  }
2037
2125
  });
2038
2126
  }
2039
- setModulationDepth(channelNumber, modulation, scheduleTime) {
2127
+ setModulationDepth(channelNumber, value, scheduleTime) {
2040
2128
  const channel = this.channels[channelNumber];
2041
2129
  if (channel.isDrum)
2042
2130
  return;
2043
- scheduleTime ??= this.audioContext.currentTime;
2044
- channel.state.modulationDepth = modulation / 127;
2131
+ if (!(0 <= scheduleTime))
2132
+ scheduleTime = this.audioContext.currentTime;
2133
+ const state = channel.state;
2134
+ const intPart = Math.trunc(value);
2135
+ state.modulationDepthMSB = intPart / 127;
2136
+ state.modulationDepthLSB = value - intPart;
2045
2137
  this.updateModulation(channel, scheduleTime);
2046
2138
  }
2047
2139
  updatePortamento(channel, scheduleTime) {
@@ -2062,18 +2154,26 @@ export class Midy {
2062
2154
  }
2063
2155
  });
2064
2156
  }
2065
- setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
2157
+ setPortamentoTime(channelNumber, value, scheduleTime) {
2158
+ if (!(0 <= scheduleTime))
2159
+ scheduleTime = this.audioContext.currentTime;
2066
2160
  const channel = this.channels[channelNumber];
2067
- scheduleTime ??= this.audioContext.currentTime;
2068
- channel.state.portamentoTime = portamentoTime / 127;
2161
+ const state = channel.state;
2162
+ const intPart = Math.trunc(value);
2163
+ state.portamentoTimeMSB = intPart / 127;
2164
+ state.portamentoTimeLSB = value - 127;
2069
2165
  if (channel.isDrum)
2070
2166
  return;
2071
2167
  this.updatePortamento(channel, scheduleTime);
2072
2168
  }
2073
- setVolume(channelNumber, volume, scheduleTime) {
2074
- scheduleTime ??= this.audioContext.currentTime;
2169
+ setVolume(channelNumber, value, scheduleTime) {
2170
+ if (!(0 <= scheduleTime))
2171
+ scheduleTime = this.audioContext.currentTime;
2075
2172
  const channel = this.channels[channelNumber];
2076
- channel.state.volume = volume / 127;
2173
+ const state = channel.state;
2174
+ const intPart = Math.trunc(value);
2175
+ state.volumeMSB = intPart / 127;
2176
+ state.volumeLSB = value - intPart;
2077
2177
  if (channel.isDrum) {
2078
2178
  for (let i = 0; i < 128; i++) {
2079
2179
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2084,16 +2184,20 @@ export class Midy {
2084
2184
  }
2085
2185
  }
2086
2186
  panToGain(pan) {
2087
- const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
2187
+ const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
2088
2188
  return {
2089
2189
  gainLeft: Math.cos(theta),
2090
2190
  gainRight: Math.sin(theta),
2091
2191
  };
2092
2192
  }
2093
- setPan(channelNumber, pan, scheduleTime) {
2094
- scheduleTime ??= this.audioContext.currentTime;
2193
+ setPan(channelNumber, value, scheduleTime) {
2194
+ if (!(0 <= scheduleTime))
2195
+ scheduleTime = this.audioContext.currentTime;
2095
2196
  const channel = this.channels[channelNumber];
2096
- channel.state.pan = pan / 127;
2197
+ const state = channel.state;
2198
+ const intPart = Math.trunc(value);
2199
+ state.panMSB = intPart / 127;
2200
+ state.panLSB = value - intPart;
2097
2201
  if (channel.isDrum) {
2098
2202
  for (let i = 0; i < 128; i++) {
2099
2203
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2103,10 +2207,14 @@ export class Midy {
2103
2207
  this.updateChannelVolume(channel, scheduleTime);
2104
2208
  }
2105
2209
  }
2106
- setExpression(channelNumber, expression, scheduleTime) {
2107
- scheduleTime ??= this.audioContext.currentTime;
2210
+ setExpression(channelNumber, value, scheduleTime) {
2211
+ if (!(0 <= scheduleTime))
2212
+ scheduleTime = this.audioContext.currentTime;
2108
2213
  const channel = this.channels[channelNumber];
2109
- channel.state.expression = expression / 127;
2214
+ const state = channel.state;
2215
+ const intPart = Math.trunc(value);
2216
+ state.expressionMSB = intPart / 127;
2217
+ state.expressionLSB = value - intPart;
2110
2218
  this.updateChannelVolume(channel, scheduleTime);
2111
2219
  }
2112
2220
  setBankLSB(channelNumber, lsb) {
@@ -2117,43 +2225,49 @@ export class Midy {
2117
2225
  this.handleRPN(channelNumber, 0, scheduleTime);
2118
2226
  }
2119
2227
  updateChannelVolume(channel, scheduleTime) {
2120
- const state = channel.state;
2121
- const volume = state.volume * state.expression;
2122
- const { gainLeft, gainRight } = this.panToGain(state.pan);
2228
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2229
+ const volume = volumeMSB + volumeLSB / 128;
2230
+ const expression = expressionMSB + expressionLSB / 128;
2231
+ const pan = panMSB + panLSB / 128;
2232
+ const gain = volume * expression;
2233
+ const { gainLeft, gainRight } = this.panToGain(pan);
2123
2234
  channel.gainL.gain
2124
2235
  .cancelScheduledValues(scheduleTime)
2125
- .setValueAtTime(volume * gainLeft, scheduleTime);
2236
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2126
2237
  channel.gainR.gain
2127
2238
  .cancelScheduledValues(scheduleTime)
2128
- .setValueAtTime(volume * gainRight, scheduleTime);
2239
+ .setValueAtTime(gain * gainRight, scheduleTime);
2129
2240
  }
2130
2241
  updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2131
2242
  const gainL = channel.keyBasedGainLs[keyNumber];
2132
2243
  if (!gainL)
2133
2244
  return;
2134
2245
  const gainR = channel.keyBasedGainRs[keyNumber];
2135
- const state = channel.state;
2136
- const defaultVolume = state.volume * state.expression;
2137
- const defaultPan = state.pan;
2246
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2247
+ const volume = volumeMSB + volumeLSB / 128;
2248
+ const expression = expressionMSB + expressionLSB / 128;
2249
+ const defaultGain = volume * expression;
2250
+ const defaultPan = panMSB + panLSB / 128;
2138
2251
  const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2139
- const volume = (0 <= keyBasedVolume)
2140
- ? defaultVolume * keyBasedVolume / 64
2141
- : defaultVolume;
2252
+ const gain = (0 <= keyBasedVolume)
2253
+ ? defaultGain * keyBasedVolume / 64
2254
+ : defaultGain;
2142
2255
  const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2143
2256
  const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2144
2257
  const { gainLeft, gainRight } = this.panToGain(pan);
2145
2258
  gainL.gain
2146
2259
  .cancelScheduledValues(scheduleTime)
2147
- .setValueAtTime(volume * gainLeft, scheduleTime);
2260
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2148
2261
  gainR.gain
2149
2262
  .cancelScheduledValues(scheduleTime)
2150
- .setValueAtTime(volume * gainRight, scheduleTime);
2263
+ .setValueAtTime(gain * gainRight, scheduleTime);
2151
2264
  }
2152
2265
  setSustainPedal(channelNumber, value, scheduleTime) {
2153
2266
  const channel = this.channels[channelNumber];
2154
2267
  if (channel.isDrum)
2155
2268
  return;
2156
- scheduleTime ??= this.audioContext.currentTime;
2269
+ if (!(0 <= scheduleTime))
2270
+ scheduleTime = this.audioContext.currentTime;
2157
2271
  channel.state.sustainPedal = value / 127;
2158
2272
  if (64 <= value) {
2159
2273
  this.processScheduledNotes(channel, (note) => {
@@ -2171,7 +2285,8 @@ export class Midy {
2171
2285
  const channel = this.channels[channelNumber];
2172
2286
  if (channel.isDrum)
2173
2287
  return;
2174
- scheduleTime ??= this.audioContext.currentTime;
2288
+ if (!(0 <= scheduleTime))
2289
+ scheduleTime = this.audioContext.currentTime;
2175
2290
  channel.state.portamento = value / 127;
2176
2291
  this.updatePortamento(channel, scheduleTime);
2177
2292
  }
@@ -2179,7 +2294,8 @@ export class Midy {
2179
2294
  const channel = this.channels[channelNumber];
2180
2295
  if (channel.isDrum)
2181
2296
  return;
2182
- scheduleTime ??= this.audioContext.currentTime;
2297
+ if (!(0 <= scheduleTime))
2298
+ scheduleTime = this.audioContext.currentTime;
2183
2299
  channel.state.sostenutoPedal = value / 127;
2184
2300
  if (64 <= value) {
2185
2301
  const sostenutoNotes = [];
@@ -2200,7 +2316,8 @@ export class Midy {
2200
2316
  if (channel.isDrum)
2201
2317
  return;
2202
2318
  const state = channel.state;
2203
- scheduleTime ??= this.audioContext.currentTime;
2319
+ if (!(0 <= scheduleTime))
2320
+ scheduleTime = this.audioContext.currentTime;
2204
2321
  state.softPedal = softPedal / 127;
2205
2322
  this.processScheduledNotes(channel, (note) => {
2206
2323
  if (this.isPortamento(channel, note)) {
@@ -2217,11 +2334,12 @@ export class Midy {
2217
2334
  const channel = this.channels[channelNumber];
2218
2335
  if (channel.isDrum)
2219
2336
  return;
2220
- scheduleTime ??= this.audioContext.currentTime;
2337
+ if (!(0 <= scheduleTime))
2338
+ scheduleTime = this.audioContext.currentTime;
2221
2339
  const state = channel.state;
2222
2340
  state.filterResonance = ccValue / 127;
2223
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2224
2341
  this.processScheduledNotes(channel, (note) => {
2342
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2225
2343
  const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2226
2344
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2227
2345
  });
@@ -2238,14 +2356,16 @@ export class Midy {
2238
2356
  const channel = this.channels[channelNumber];
2239
2357
  if (channel.isDrum)
2240
2358
  return;
2241
- scheduleTime ??= this.audioContext.currentTime;
2359
+ if (!(0 <= scheduleTime))
2360
+ scheduleTime = this.audioContext.currentTime;
2242
2361
  channel.state.releaseTime = releaseTime / 127;
2243
2362
  }
2244
2363
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2245
2364
  const channel = this.channels[channelNumber];
2246
2365
  if (channel.isDrum)
2247
2366
  return;
2248
- scheduleTime ??= this.audioContext.currentTime;
2367
+ if (!(0 <= scheduleTime))
2368
+ scheduleTime = this.audioContext.currentTime;
2249
2369
  channel.state.attackTime = attackTime / 127;
2250
2370
  this.processScheduledNotes(channel, (note) => {
2251
2371
  if (scheduleTime < note.startTime) {
@@ -2258,7 +2378,8 @@ export class Midy {
2258
2378
  if (channel.isDrum)
2259
2379
  return;
2260
2380
  const state = channel.state;
2261
- scheduleTime ??= this.audioContext.currentTime;
2381
+ if (!(0 <= scheduleTime))
2382
+ scheduleTime = this.audioContext.currentTime;
2262
2383
  state.brightness = brightness / 127;
2263
2384
  this.processScheduledNotes(channel, (note) => {
2264
2385
  if (this.isPortamento(channel, note)) {
@@ -2273,7 +2394,8 @@ export class Midy {
2273
2394
  const channel = this.channels[channelNumber];
2274
2395
  if (channel.isDrum)
2275
2396
  return;
2276
- scheduleTime ??= this.audioContext.currentTime;
2397
+ if (!(0 <= scheduleTime))
2398
+ scheduleTime = this.audioContext.currentTime;
2277
2399
  channel.state.decayTime = dacayTime / 127;
2278
2400
  this.processScheduledNotes(channel, (note) => {
2279
2401
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2283,7 +2405,8 @@ export class Midy {
2283
2405
  const channel = this.channels[channelNumber];
2284
2406
  if (channel.isDrum)
2285
2407
  return;
2286
- scheduleTime ??= this.audioContext.currentTime;
2408
+ if (!(0 <= scheduleTime))
2409
+ scheduleTime = this.audioContext.currentTime;
2287
2410
  channel.state.vibratoRate = vibratoRate / 127;
2288
2411
  if (channel.vibratoDepth <= 0)
2289
2412
  return;
@@ -2295,7 +2418,8 @@ export class Midy {
2295
2418
  const channel = this.channels[channelNumber];
2296
2419
  if (channel.isDrum)
2297
2420
  return;
2298
- scheduleTime ??= this.audioContext.currentTime;
2421
+ if (!(0 <= scheduleTime))
2422
+ scheduleTime = this.audioContext.currentTime;
2299
2423
  const prev = channel.state.vibratoDepth;
2300
2424
  channel.state.vibratoDepth = vibratoDepth / 127;
2301
2425
  if (0 < prev) {
@@ -2313,7 +2437,8 @@ export class Midy {
2313
2437
  const channel = this.channels[channelNumber];
2314
2438
  if (channel.isDrum)
2315
2439
  return;
2316
- scheduleTime ??= this.audioContext.currentTime;
2440
+ if (!(0 <= scheduleTime))
2441
+ scheduleTime = this.audioContext.currentTime;
2317
2442
  channel.state.vibratoDelay = vibratoDelay / 127;
2318
2443
  if (0 < channel.state.vibratoDepth) {
2319
2444
  this.processScheduledNotes(channel, (note) => {
@@ -2321,8 +2446,16 @@ export class Midy {
2321
2446
  });
2322
2447
  }
2323
2448
  }
2449
+ setPortamentoNoteNumber(channelNumber, value, scheduleTime) {
2450
+ if (!(0 <= scheduleTime))
2451
+ scheduleTime = this.audioContext.currentTime;
2452
+ const channel = this.channels[channelNumber];
2453
+ channel.portamentoControl = true;
2454
+ channel.state.portamentoNoteNumber = value / 127;
2455
+ }
2324
2456
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
2325
- scheduleTime ??= this.audioContext.currentTime;
2457
+ if (!(0 <= scheduleTime))
2458
+ scheduleTime = this.audioContext.currentTime;
2326
2459
  const channel = this.channels[channelNumber];
2327
2460
  const state = channel.state;
2328
2461
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2331,7 +2464,8 @@ export class Midy {
2331
2464
  });
2332
2465
  }
2333
2466
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2334
- scheduleTime ??= this.audioContext.currentTime;
2467
+ if (!(0 <= scheduleTime))
2468
+ scheduleTime = this.audioContext.currentTime;
2335
2469
  const channel = this.channels[channelNumber];
2336
2470
  const state = channel.state;
2337
2471
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2391,12 +2525,14 @@ export class Midy {
2391
2525
  }
2392
2526
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2393
2527
  dataIncrement(channelNumber, scheduleTime) {
2394
- scheduleTime ??= this.audioContext.currentTime;
2528
+ if (!(0 <= scheduleTime))
2529
+ scheduleTime = this.audioContext.currentTime;
2395
2530
  this.handleRPN(channelNumber, 1, scheduleTime);
2396
2531
  }
2397
2532
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2398
2533
  dataDecrement(channelNumber, scheduleTime) {
2399
- scheduleTime ??= this.audioContext.currentTime;
2534
+ if (!(0 <= scheduleTime))
2535
+ scheduleTime = this.audioContext.currentTime;
2400
2536
  this.handleRPN(channelNumber, -1, scheduleTime);
2401
2537
  }
2402
2538
  setRPNMSB(channelNumber, value) {
@@ -2419,7 +2555,8 @@ export class Midy {
2419
2555
  const channel = this.channels[channelNumber];
2420
2556
  if (channel.isDrum)
2421
2557
  return;
2422
- scheduleTime ??= this.audioContext.currentTime;
2558
+ if (!(0 <= scheduleTime))
2559
+ scheduleTime = this.audioContext.currentTime;
2423
2560
  const state = channel.state;
2424
2561
  const prev = state.pitchWheelSensitivity;
2425
2562
  const next = value / 12800;
@@ -2439,7 +2576,8 @@ export class Midy {
2439
2576
  const channel = this.channels[channelNumber];
2440
2577
  if (channel.isDrum)
2441
2578
  return;
2442
- scheduleTime ??= this.audioContext.currentTime;
2579
+ if (!(0 <= scheduleTime))
2580
+ scheduleTime = this.audioContext.currentTime;
2443
2581
  const prev = channel.fineTuning;
2444
2582
  const next = value;
2445
2583
  channel.fineTuning = next;
@@ -2456,7 +2594,8 @@ export class Midy {
2456
2594
  const channel = this.channels[channelNumber];
2457
2595
  if (channel.isDrum)
2458
2596
  return;
2459
- scheduleTime ??= this.audioContext.currentTime;
2597
+ if (!(0 <= scheduleTime))
2598
+ scheduleTime = this.audioContext.currentTime;
2460
2599
  const prev = channel.coarseTuning;
2461
2600
  const next = value;
2462
2601
  channel.coarseTuning = next;
@@ -2473,12 +2612,18 @@ export class Midy {
2473
2612
  const channel = this.channels[channelNumber];
2474
2613
  if (channel.isDrum)
2475
2614
  return;
2476
- scheduleTime ??= this.audioContext.currentTime;
2615
+ if (!(0 <= scheduleTime))
2616
+ scheduleTime = this.audioContext.currentTime;
2477
2617
  channel.modulationDepthRange = value;
2478
2618
  this.updateModulation(channel, scheduleTime);
2479
2619
  }
2480
- allSoundOff(channelNumber, _value, scheduleTime) {
2620
+ setRPGMakerLoop(_channelNumber, _value, scheduleTime) {
2481
2621
  scheduleTime ??= this.audioContext.currentTime;
2622
+ this.loopStart = scheduleTime + this.resumeTime - this.startTime;
2623
+ }
2624
+ allSoundOff(channelNumber, _value, scheduleTime) {
2625
+ if (!(0 <= scheduleTime))
2626
+ scheduleTime = this.audioContext.currentTime;
2482
2627
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2483
2628
  }
2484
2629
  resetChannelStates(channelNumber) {
@@ -2508,8 +2653,10 @@ export class Midy {
2508
2653
  "polyphonicKeyPressure",
2509
2654
  "channelPressure",
2510
2655
  "pitchWheel",
2511
- "expression",
2512
- "modulationDepth",
2656
+ "expressionMSB",
2657
+ "expressionLSB",
2658
+ "modulationDepthMSB",
2659
+ "modulationDepthLSB",
2513
2660
  "sustainPedal",
2514
2661
  "portamento",
2515
2662
  "sostenutoPedal",
@@ -2538,7 +2685,8 @@ export class Midy {
2538
2685
  }
2539
2686
  }
2540
2687
  allNotesOff(channelNumber, _value, scheduleTime) {
2541
- scheduleTime ??= this.audioContext.currentTime;
2688
+ if (!(0 <= scheduleTime))
2689
+ scheduleTime = this.audioContext.currentTime;
2542
2690
  return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2543
2691
  }
2544
2692
  omniOff(channelNumber, value, scheduleTime) {
@@ -2590,7 +2738,8 @@ export class Midy {
2590
2738
  }
2591
2739
  }
2592
2740
  GM1SystemOn(scheduleTime) {
2593
- scheduleTime ??= this.audioContext.currentTime;
2741
+ if (!(0 <= scheduleTime))
2742
+ scheduleTime = this.audioContext.currentTime;
2594
2743
  this.mode = "GM1";
2595
2744
  for (let i = 0; i < this.channels.length; i++) {
2596
2745
  this.allSoundOff(i, 0, scheduleTime);
@@ -2603,7 +2752,8 @@ export class Midy {
2603
2752
  this.channels[9].isDrum = true;
2604
2753
  }
2605
2754
  GM2SystemOn(scheduleTime) {
2606
- scheduleTime ??= this.audioContext.currentTime;
2755
+ if (!(0 <= scheduleTime))
2756
+ scheduleTime = this.audioContext.currentTime;
2607
2757
  this.mode = "GM2";
2608
2758
  for (let i = 0; i < this.channels.length; i++) {
2609
2759
  this.allSoundOff(i, 0, scheduleTime);
@@ -2671,7 +2821,8 @@ export class Midy {
2671
2821
  this.setMasterVolume(volume, scheduleTime);
2672
2822
  }
2673
2823
  setMasterVolume(value, scheduleTime) {
2674
- scheduleTime ??= this.audioContext.currentTime;
2824
+ if (!(0 <= scheduleTime))
2825
+ scheduleTime = this.audioContext.currentTime;
2675
2826
  this.masterVolume.gain
2676
2827
  .cancelScheduledValues(scheduleTime)
2677
2828
  .setValueAtTime(value * value, scheduleTime);
@@ -3218,5 +3369,6 @@ Object.defineProperty(Midy, "channelSettings", {
3218
3369
  modulationDepthRange: 50, // cent
3219
3370
  fineTuning: 0, // cent
3220
3371
  coarseTuning: 0, // cent
3372
+ portamentoControl: false,
3221
3373
  }
3222
3374
  });