@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/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);
@@ -161,14 +158,19 @@ const defaultControllerState = {
161
158
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
162
159
  link: { type: 127, defaultValue: 0 },
163
160
  // bankMSB: { type: 128 + 0, defaultValue: 121, },
164
- modulationDepth: { type: 128 + 1, defaultValue: 0 },
165
- portamentoTime: { type: 128 + 5, defaultValue: 0 },
161
+ modulationDepthMSB: { type: 128 + 1, defaultValue: 0 },
162
+ portamentoTimeMSB: { type: 128 + 5, defaultValue: 0 },
166
163
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
167
- volume: { type: 128 + 7, defaultValue: 100 / 127 },
168
- pan: { type: 128 + 10, defaultValue: 64 / 127 },
169
- expression: { type: 128 + 11, defaultValue: 1 },
164
+ volumeMSB: { type: 128 + 7, defaultValue: 100 / 127 },
165
+ panMSB: { type: 128 + 10, defaultValue: 64 / 127 },
166
+ expressionMSB: { type: 128 + 11, defaultValue: 1 },
170
167
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
168
+ modulationDepthLSB: { type: 128 + 33, defaultValue: 0 },
169
+ portamentoTimeLSB: { type: 128 + 37, defaultValue: 0 },
171
170
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
171
+ volumeLSB: { type: 128 + 39, defaultValue: 0 },
172
+ panLSB: { type: 128 + 42, defaultValue: 0 },
173
+ expressionLSB: { type: 128 + 43, defaultValue: 0 },
172
174
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
173
175
  portamento: { type: 128 + 65, defaultValue: 0 },
174
176
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
@@ -181,12 +183,14 @@ const defaultControllerState = {
181
183
  vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
182
184
  vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
183
185
  vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
186
+ portamentoNoteNumber: { type: 128 + 84, defaultValue: 0 },
184
187
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
185
188
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
186
189
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
187
190
  // dataDecrement: { type: 128 + 97, defaultValue: 0 },
188
191
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
189
192
  // rpnMSB: { type: 128 + 101, defaultValue: 127 },
193
+ // rpgMakerLoop: { type: 128 + 111, defaultValue: 0 },
190
194
  // allSoundOff: { type: 128 + 120, defaultValue: 0 },
191
195
  // resetAllControllers: { type: 128 + 121, defaultValue: 0 },
192
196
  // allNotesOff: { type: 128 + 123, defaultValue: 0 },
@@ -246,8 +250,9 @@ const pitchEnvelopeKeys = [
246
250
  "playbackRate",
247
251
  ];
248
252
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
249
- class Midy {
253
+ class Midy extends EventTarget {
250
254
  constructor(audioContext) {
255
+ super();
251
256
  Object.defineProperty(this, "mode", {
252
257
  enumerable: true,
253
258
  configurable: true,
@@ -408,6 +413,18 @@ class Midy {
408
413
  writable: true,
409
414
  value: false
410
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
+ });
411
428
  Object.defineProperty(this, "playPromise", {
412
429
  enumerable: true,
413
430
  configurable: true,
@@ -564,7 +581,7 @@ class Midy {
564
581
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
565
582
  }
566
583
  createChannelAudioNodes(audioContext) {
567
- const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
584
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
568
585
  const gainL = new GainNode(audioContext, { gain: gainLeft });
569
586
  const gainR = new GainNode(audioContext, { gain: gainRight });
570
587
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -607,10 +624,9 @@ class Midy {
607
624
  return channels;
608
625
  }
609
626
  async createAudioBuffer(voiceParams) {
610
- const sample = voiceParams.sample;
611
- const sampleStart = voiceParams.start;
612
- const sampleEnd = sample.data.length + voiceParams.end;
613
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
627
+ const { sample, start, end } = voiceParams;
628
+ const sampleEnd = sample.data.length + end;
629
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
614
630
  return audioBuffer;
615
631
  }
616
632
  isLoopDrum(channel, noteNumber) {
@@ -630,7 +646,7 @@ class Midy {
630
646
  }
631
647
  return bufferSource;
632
648
  }
633
- async scheduleTimelineEvents(scheduleTime, queueIndex) {
649
+ scheduleTimelineEvents(scheduleTime, queueIndex) {
634
650
  const timeOffset = this.resumeTime - this.startTime;
635
651
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
636
652
  const schedulingOffset = this.startDelay - timeOffset;
@@ -642,12 +658,10 @@ class Midy {
642
658
  const startTime = event.startTime + schedulingOffset;
643
659
  switch (event.type) {
644
660
  case "noteOn":
645
- await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
661
+ this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
646
662
  break;
647
663
  case "noteOff": {
648
- const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
649
- if (notePromise)
650
- this.notePromises.push(notePromise);
664
+ this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
651
665
  break;
652
666
  }
653
667
  case "noteAftertouch":
@@ -691,22 +705,23 @@ class Midy {
691
705
  }
692
706
  }
693
707
  updateStates(queueIndex, nextQueueIndex) {
708
+ const now = this.audioContext.currentTime;
694
709
  if (nextQueueIndex < queueIndex)
695
710
  queueIndex = 0;
696
711
  for (let i = queueIndex; i < nextQueueIndex; i++) {
697
712
  const event = this.timeline[i];
698
713
  switch (event.type) {
699
714
  case "controller":
700
- this.setControlChange(event.channel, event.controllerType, event.value, 0);
715
+ this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
701
716
  break;
702
717
  case "programChange":
703
- this.setProgramChange(event.channel, event.programNumber, 0);
718
+ this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
704
719
  break;
705
720
  case "pitchBend":
706
- this.setPitchBend(event.channel, event.value + 8192, 0);
721
+ this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
707
722
  break;
708
723
  case "sysEx":
709
- this.handleSysEx(event.data, 0);
724
+ this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
710
725
  }
711
726
  }
712
727
  }
@@ -714,52 +729,95 @@ class Midy {
714
729
  if (this.audioContext.state === "suspended") {
715
730
  await this.audioContext.resume();
716
731
  }
732
+ const paused = this.isPaused;
717
733
  this.isPlaying = true;
718
734
  this.isPaused = false;
719
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
+ }
720
742
  let queueIndex = this.getQueueIndex(this.resumeTime);
721
- let finished = false;
743
+ let exitReason;
722
744
  this.notePromises = [];
723
- while (queueIndex < this.timeline.length) {
745
+ while (true) {
724
746
  const now = this.audioContext.currentTime;
725
747
  if (0 < this.lastActiveSensing &&
726
748
  this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
727
749
  await this.stopNotes(0, true, now);
728
750
  await this.audioContext.suspend();
729
- finished = true;
751
+ exitReason = "aborted";
730
752
  break;
731
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
+ }
732
778
  if (this.isPausing) {
733
779
  await this.stopNotes(0, true, now);
734
780
  await this.audioContext.suspend();
735
781
  this.notePromises = [];
782
+ this.isPausing = false;
783
+ exitReason = "paused";
736
784
  break;
737
785
  }
738
786
  else if (this.isStopping) {
739
787
  await this.stopNotes(0, true, now);
740
788
  await this.audioContext.suspend();
741
- finished = true;
789
+ this.isStopping = false;
790
+ exitReason = "stopped";
742
791
  break;
743
792
  }
744
793
  else if (this.isSeeking) {
745
- await this.stopNotes(0, true, now);
794
+ this.stopNotes(0, true, now);
746
795
  this.startTime = this.audioContext.currentTime;
747
796
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
748
797
  this.updateStates(queueIndex, nextQueueIndex);
749
798
  queueIndex = nextQueueIndex;
750
799
  this.isSeeking = false;
800
+ this.dispatchEvent(new Event("seeked"));
751
801
  continue;
752
802
  }
753
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
803
+ queueIndex = this.scheduleTimelineEvents(now, queueIndex);
754
804
  const waitTime = now + this.noteCheckInterval;
755
805
  await this.scheduleTask(() => { }, waitTime);
756
806
  }
757
- if (finished) {
807
+ if (exitReason !== "paused") {
758
808
  this.notePromises = [];
759
809
  this.resetAllStates();
760
810
  this.lastActiveSensing = 0;
761
811
  }
762
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
+ }
763
821
  }
764
822
  ticksToSecond(ticks, secondsPerBeat) {
765
823
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -896,24 +954,20 @@ class Midy {
896
954
  return;
897
955
  this.isStopping = true;
898
956
  await this.playPromise;
899
- this.isStopping = false;
900
957
  }
901
958
  async pause() {
902
959
  if (!this.isPlaying || this.isPaused)
903
960
  return;
904
961
  const now = this.audioContext.currentTime;
905
- this.resumeTime = now - this.startTime - this.startDelay;
962
+ this.resumeTime = now + this.resumeTime - this.startTime;
906
963
  this.isPausing = true;
907
964
  await this.playPromise;
908
- this.isPausing = false;
909
- this.isPaused = true;
910
965
  }
911
966
  async resume() {
912
967
  if (!this.isPaused)
913
968
  return;
914
969
  this.playPromise = this.playNotes();
915
970
  await this.playPromise;
916
- this.isPaused = false;
917
971
  }
918
972
  seekTo(second) {
919
973
  this.resumeTime = second;
@@ -936,19 +990,23 @@ class Midy {
936
990
  const now = this.audioContext.currentTime;
937
991
  return now + this.resumeTime - this.startTime;
938
992
  }
939
- processScheduledNotes(channel, callback) {
993
+ async processScheduledNotes(channel, callback) {
940
994
  const scheduledNotes = channel.scheduledNotes;
995
+ const tasks = [];
941
996
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
942
997
  const note = scheduledNotes[i];
943
998
  if (!note)
944
999
  continue;
945
1000
  if (note.ending)
946
1001
  continue;
947
- callback(note);
1002
+ const task = note.ready.then(() => callback(note));
1003
+ tasks.push(task);
948
1004
  }
1005
+ await Promise.all(tasks);
949
1006
  }
950
- processActiveNotes(channel, scheduleTime, callback) {
1007
+ async processActiveNotes(channel, scheduleTime, callback) {
951
1008
  const scheduledNotes = channel.scheduledNotes;
1009
+ const tasks = [];
952
1010
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
953
1011
  const note = scheduledNotes[i];
954
1012
  if (!note)
@@ -957,8 +1015,10 @@ class Midy {
957
1015
  continue;
958
1016
  if (scheduleTime < note.startTime)
959
1017
  break;
960
- callback(note);
1018
+ const task = note.ready.then(() => callback(note));
1019
+ tasks.push(task);
961
1020
  }
1021
+ await Promise.all(tasks);
962
1022
  }
963
1023
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
964
1024
  const sampleRate = audioContext.sampleRate;
@@ -1146,6 +1206,13 @@ class Midy {
1146
1206
  const noteDetune = this.calcNoteDetune(channel, note);
1147
1207
  const pitchControl = this.getPitchControl(channel, note);
1148
1208
  const detune = channel.detune + noteDetune + pitchControl;
1209
+ if (channel.portamentoControl) {
1210
+ const state = channel.state;
1211
+ const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1212
+ note.portamentoNoteNumber = portamentoNoteNumber;
1213
+ channel.portamentoControl = false;
1214
+ state.portamentoNoteNumber = 0;
1215
+ }
1149
1216
  if (this.isPortamento(channel, note)) {
1150
1217
  const startTime = note.startTime;
1151
1218
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
@@ -1162,8 +1229,10 @@ class Midy {
1162
1229
  }
1163
1230
  }
1164
1231
  getPortamentoTime(channel, note) {
1232
+ const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
1233
+ const portamentoTime = portamentoTimeMSB + portamentoTimeLSB / 128;
1165
1234
  const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1166
- const value = Math.ceil(channel.state.portamentoTime * 127);
1235
+ const value = Math.ceil(portamentoTime * 128);
1167
1236
  return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1168
1237
  }
1169
1238
  getPitchIncrementSpeed(value) {
@@ -1427,7 +1496,7 @@ class Midy {
1427
1496
  if (0 < state.vibratoDepth) {
1428
1497
  this.startVibrato(channel, note, now);
1429
1498
  }
1430
- if (0 < state.modulationDepth) {
1499
+ if (0 < state.modulationDepthMSB + state.modulationDepthLSB) {
1431
1500
  this.startModulation(channel, note, now);
1432
1501
  }
1433
1502
  if (channel.mono && channel.currentBufferSource) {
@@ -1438,7 +1507,13 @@ class Midy {
1438
1507
  note.filterNode.connect(note.volumeEnvelopeNode);
1439
1508
  this.setChorusSend(channel, note, now);
1440
1509
  this.setReverbSend(channel, note, now);
1441
- note.bufferSource.start(startTime);
1510
+ if (voiceParams.sample.type === "compressed") {
1511
+ const offset = voiceParams.start / audioBuffer.sampleRate;
1512
+ note.bufferSource.start(startTime, offset);
1513
+ }
1514
+ else {
1515
+ note.bufferSource.start(startTime);
1516
+ }
1442
1517
  return note;
1443
1518
  }
1444
1519
  handleExclusiveClass(note, channelNumber, startTime) {
@@ -1523,11 +1598,7 @@ class Midy {
1523
1598
  return;
1524
1599
  await this.setNoteAudioNode(channel, note, realtime);
1525
1600
  this.setNoteRouting(channelNumber, note, startTime);
1526
- note.pending = false;
1527
- const off = note.offEvent;
1528
- if (off) {
1529
- this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1530
- }
1601
+ note.resolveReady();
1531
1602
  }
1532
1603
  disconnectNote(note) {
1533
1604
  note.bufferSource.disconnect();
@@ -1572,7 +1643,7 @@ class Midy {
1572
1643
  }, stopTime);
1573
1644
  });
1574
1645
  }
1575
- noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1646
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1576
1647
  const channel = this.channels[channelNumber];
1577
1648
  const state = channel.state;
1578
1649
  if (!force) {
@@ -1591,13 +1662,13 @@ class Midy {
1591
1662
  if (index < 0)
1592
1663
  return;
1593
1664
  const note = channel.scheduledNotes[index];
1594
- if (note.pending) {
1595
- note.offEvent = { velocity, startTime: endTime };
1596
- return;
1597
- }
1598
1665
  note.ending = true;
1599
1666
  this.setNoteIndex(channel, index);
1600
- this.releaseNote(channel, note, endTime);
1667
+ const promise = note.ready.then(() => {
1668
+ return this.releaseNote(channel, note, endTime);
1669
+ });
1670
+ this.notePromises.push(promise);
1671
+ return promise;
1601
1672
  }
1602
1673
  setNoteIndex(channel, index) {
1603
1674
  let allEnds = true;
@@ -1696,7 +1767,7 @@ class Midy {
1696
1767
  this.setEffects(channel, note, table, scheduleTime);
1697
1768
  }
1698
1769
  });
1699
- this.applyVoiceParams(channel, 10);
1770
+ this.applyVoiceParams(channel, 10, scheduleTime);
1700
1771
  }
1701
1772
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1702
1773
  const channel = this.channels[channelNumber];
@@ -1729,7 +1800,7 @@ class Midy {
1729
1800
  this.processActiveNotes(channel, scheduleTime, (note) => {
1730
1801
  this.setEffects(channel, note, table, scheduleTime);
1731
1802
  });
1732
- this.applyVoiceParams(channel, 13);
1803
+ this.applyVoiceParams(channel, 13, scheduleTime);
1733
1804
  }
1734
1805
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1735
1806
  const pitchBend = msb * 128 + lsb;
@@ -1739,7 +1810,8 @@ class Midy {
1739
1810
  const channel = this.channels[channelNumber];
1740
1811
  if (channel.isDrum)
1741
1812
  return;
1742
- scheduleTime ??= this.audioContext.currentTime;
1813
+ if (!(0 <= scheduleTime))
1814
+ scheduleTime = this.audioContext.currentTime;
1743
1815
  const state = channel.state;
1744
1816
  const prev = state.pitchWheel * 2 - 1;
1745
1817
  const next = (value - 8192) / 8192;
@@ -1750,9 +1822,11 @@ class Midy {
1750
1822
  }
1751
1823
  setModLfoToPitch(channel, note, scheduleTime) {
1752
1824
  if (note.modulationDepth) {
1825
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1826
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
1753
1827
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1754
1828
  this.getLFOPitchDepth(channel, note);
1755
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1829
+ const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
1756
1830
  const depth = baseDepth * Math.sign(modLfoToPitch);
1757
1831
  note.modulationDepth.gain
1758
1832
  .cancelScheduledValues(scheduleTime)
@@ -1885,7 +1959,8 @@ class Midy {
1885
1959
  createVoiceParamsHandlers() {
1886
1960
  return {
1887
1961
  modLfoToPitch: (channel, note, scheduleTime) => {
1888
- if (0 < channel.state.modulationDepth) {
1962
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1963
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1889
1964
  this.setModLfoToPitch(channel, note, scheduleTime);
1890
1965
  }
1891
1966
  },
@@ -1895,12 +1970,14 @@ class Midy {
1895
1970
  }
1896
1971
  },
1897
1972
  modLfoToFilterFc: (channel, note, scheduleTime) => {
1898
- if (0 < channel.state.modulationDepth) {
1973
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1974
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1899
1975
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1900
1976
  }
1901
1977
  },
1902
1978
  modLfoToVolume: (channel, note, scheduleTime) => {
1903
- if (0 < channel.state.modulationDepth) {
1979
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1980
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1904
1981
  this.setModLfoToVolume(channel, note, scheduleTime);
1905
1982
  }
1906
1983
  },
@@ -1911,12 +1988,14 @@ class Midy {
1911
1988
  this.setReverbSend(channel, note, scheduleTime);
1912
1989
  },
1913
1990
  delayModLFO: (_channel, note, _scheduleTime) => {
1914
- if (0 < channel.state.modulationDepth) {
1991
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1992
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1915
1993
  this.setDelayModLFO(note);
1916
1994
  }
1917
1995
  },
1918
1996
  freqModLFO: (_channel, note, scheduleTime) => {
1919
- if (0 < channel.state.modulationDepth) {
1997
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1998
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1920
1999
  this.setFreqModLFO(note, scheduleTime);
1921
2000
  }
1922
2001
  },
@@ -1985,7 +2064,12 @@ class Midy {
1985
2064
  handlers[10] = this.setPan;
1986
2065
  handlers[11] = this.setExpression;
1987
2066
  handlers[32] = this.setBankLSB;
2067
+ handlers[33] = this.setModulationDepth;
2068
+ handlers[37] = this.setPortamentoTime;
1988
2069
  handlers[38] = this.dataEntryLSB;
2070
+ handlers[39] = this.setVolume;
2071
+ handlers[42] = this.setPan;
2072
+ handlers[43] = this.setExpression;
1989
2073
  handlers[64] = this.setSustainPedal;
1990
2074
  handlers[65] = this.setPortamento;
1991
2075
  handlers[66] = this.setSostenutoPedal;
@@ -1998,12 +2082,14 @@ class Midy {
1998
2082
  handlers[76] = this.setVibratoRate;
1999
2083
  handlers[77] = this.setVibratoDepth;
2000
2084
  handlers[78] = this.setVibratoDelay;
2085
+ handlers[84] = this.setPortamentoNoteNumber;
2001
2086
  handlers[91] = this.setReverbSendLevel;
2002
2087
  handlers[93] = this.setChorusSendLevel;
2003
2088
  handlers[96] = this.dataIncrement;
2004
2089
  handlers[97] = this.dataDecrement;
2005
2090
  handlers[100] = this.setRPNLSB;
2006
2091
  handlers[101] = this.setRPNMSB;
2092
+ handlers[111] = this.setRPGMakerLoop;
2007
2093
  handlers[120] = this.allSoundOff;
2008
2094
  handlers[121] = this.resetAllControllers;
2009
2095
  handlers[123] = this.allNotesOff;
@@ -2029,7 +2115,9 @@ class Midy {
2029
2115
  this.channels[channelNumber].bankMSB = msb;
2030
2116
  }
2031
2117
  updateModulation(channel, scheduleTime) {
2032
- const depth = channel.state.modulationDepth * channel.modulationDepthRange;
2118
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
2119
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
2120
+ const depth = modulationDepth * channel.modulationDepthRange;
2033
2121
  this.processScheduledNotes(channel, (note) => {
2034
2122
  if (note.modulationDepth) {
2035
2123
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
@@ -2039,12 +2127,16 @@ class Midy {
2039
2127
  }
2040
2128
  });
2041
2129
  }
2042
- setModulationDepth(channelNumber, modulation, scheduleTime) {
2130
+ setModulationDepth(channelNumber, value, scheduleTime) {
2043
2131
  const channel = this.channels[channelNumber];
2044
2132
  if (channel.isDrum)
2045
2133
  return;
2046
- scheduleTime ??= this.audioContext.currentTime;
2047
- channel.state.modulationDepth = modulation / 127;
2134
+ if (!(0 <= scheduleTime))
2135
+ scheduleTime = this.audioContext.currentTime;
2136
+ const state = channel.state;
2137
+ const intPart = Math.trunc(value);
2138
+ state.modulationDepthMSB = intPart / 127;
2139
+ state.modulationDepthLSB = value - intPart;
2048
2140
  this.updateModulation(channel, scheduleTime);
2049
2141
  }
2050
2142
  updatePortamento(channel, scheduleTime) {
@@ -2065,18 +2157,26 @@ class Midy {
2065
2157
  }
2066
2158
  });
2067
2159
  }
2068
- setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
2160
+ setPortamentoTime(channelNumber, value, scheduleTime) {
2161
+ if (!(0 <= scheduleTime))
2162
+ scheduleTime = this.audioContext.currentTime;
2069
2163
  const channel = this.channels[channelNumber];
2070
- scheduleTime ??= this.audioContext.currentTime;
2071
- channel.state.portamentoTime = portamentoTime / 127;
2164
+ const state = channel.state;
2165
+ const intPart = Math.trunc(value);
2166
+ state.portamentoTimeMSB = intPart / 127;
2167
+ state.portamentoTimeLSB = value - 127;
2072
2168
  if (channel.isDrum)
2073
2169
  return;
2074
2170
  this.updatePortamento(channel, scheduleTime);
2075
2171
  }
2076
- setVolume(channelNumber, volume, scheduleTime) {
2077
- scheduleTime ??= this.audioContext.currentTime;
2172
+ setVolume(channelNumber, value, scheduleTime) {
2173
+ if (!(0 <= scheduleTime))
2174
+ scheduleTime = this.audioContext.currentTime;
2078
2175
  const channel = this.channels[channelNumber];
2079
- channel.state.volume = volume / 127;
2176
+ const state = channel.state;
2177
+ const intPart = Math.trunc(value);
2178
+ state.volumeMSB = intPart / 127;
2179
+ state.volumeLSB = value - intPart;
2080
2180
  if (channel.isDrum) {
2081
2181
  for (let i = 0; i < 128; i++) {
2082
2182
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2087,16 +2187,20 @@ class Midy {
2087
2187
  }
2088
2188
  }
2089
2189
  panToGain(pan) {
2090
- const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
2190
+ const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
2091
2191
  return {
2092
2192
  gainLeft: Math.cos(theta),
2093
2193
  gainRight: Math.sin(theta),
2094
2194
  };
2095
2195
  }
2096
- setPan(channelNumber, pan, scheduleTime) {
2097
- scheduleTime ??= this.audioContext.currentTime;
2196
+ setPan(channelNumber, value, scheduleTime) {
2197
+ if (!(0 <= scheduleTime))
2198
+ scheduleTime = this.audioContext.currentTime;
2098
2199
  const channel = this.channels[channelNumber];
2099
- channel.state.pan = pan / 127;
2200
+ const state = channel.state;
2201
+ const intPart = Math.trunc(value);
2202
+ state.panMSB = intPart / 127;
2203
+ state.panLSB = value - intPart;
2100
2204
  if (channel.isDrum) {
2101
2205
  for (let i = 0; i < 128; i++) {
2102
2206
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2106,10 +2210,14 @@ class Midy {
2106
2210
  this.updateChannelVolume(channel, scheduleTime);
2107
2211
  }
2108
2212
  }
2109
- setExpression(channelNumber, expression, scheduleTime) {
2110
- scheduleTime ??= this.audioContext.currentTime;
2213
+ setExpression(channelNumber, value, scheduleTime) {
2214
+ if (!(0 <= scheduleTime))
2215
+ scheduleTime = this.audioContext.currentTime;
2111
2216
  const channel = this.channels[channelNumber];
2112
- channel.state.expression = expression / 127;
2217
+ const state = channel.state;
2218
+ const intPart = Math.trunc(value);
2219
+ state.expressionMSB = intPart / 127;
2220
+ state.expressionLSB = value - intPart;
2113
2221
  this.updateChannelVolume(channel, scheduleTime);
2114
2222
  }
2115
2223
  setBankLSB(channelNumber, lsb) {
@@ -2120,43 +2228,49 @@ class Midy {
2120
2228
  this.handleRPN(channelNumber, 0, scheduleTime);
2121
2229
  }
2122
2230
  updateChannelVolume(channel, scheduleTime) {
2123
- const state = channel.state;
2124
- const volume = state.volume * state.expression;
2125
- const { gainLeft, gainRight } = this.panToGain(state.pan);
2231
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2232
+ const volume = volumeMSB + volumeLSB / 128;
2233
+ const expression = expressionMSB + expressionLSB / 128;
2234
+ const pan = panMSB + panLSB / 128;
2235
+ const gain = volume * expression;
2236
+ const { gainLeft, gainRight } = this.panToGain(pan);
2126
2237
  channel.gainL.gain
2127
2238
  .cancelScheduledValues(scheduleTime)
2128
- .setValueAtTime(volume * gainLeft, scheduleTime);
2239
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2129
2240
  channel.gainR.gain
2130
2241
  .cancelScheduledValues(scheduleTime)
2131
- .setValueAtTime(volume * gainRight, scheduleTime);
2242
+ .setValueAtTime(gain * gainRight, scheduleTime);
2132
2243
  }
2133
2244
  updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2134
2245
  const gainL = channel.keyBasedGainLs[keyNumber];
2135
2246
  if (!gainL)
2136
2247
  return;
2137
2248
  const gainR = channel.keyBasedGainRs[keyNumber];
2138
- const state = channel.state;
2139
- const defaultVolume = state.volume * state.expression;
2140
- const defaultPan = state.pan;
2249
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2250
+ const volume = volumeMSB + volumeLSB / 128;
2251
+ const expression = expressionMSB + expressionLSB / 128;
2252
+ const defaultGain = volume * expression;
2253
+ const defaultPan = panMSB + panLSB / 128;
2141
2254
  const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2142
- const volume = (0 <= keyBasedVolume)
2143
- ? defaultVolume * keyBasedVolume / 64
2144
- : defaultVolume;
2255
+ const gain = (0 <= keyBasedVolume)
2256
+ ? defaultGain * keyBasedVolume / 64
2257
+ : defaultGain;
2145
2258
  const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2146
2259
  const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2147
2260
  const { gainLeft, gainRight } = this.panToGain(pan);
2148
2261
  gainL.gain
2149
2262
  .cancelScheduledValues(scheduleTime)
2150
- .setValueAtTime(volume * gainLeft, scheduleTime);
2263
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2151
2264
  gainR.gain
2152
2265
  .cancelScheduledValues(scheduleTime)
2153
- .setValueAtTime(volume * gainRight, scheduleTime);
2266
+ .setValueAtTime(gain * gainRight, scheduleTime);
2154
2267
  }
2155
2268
  setSustainPedal(channelNumber, value, scheduleTime) {
2156
2269
  const channel = this.channels[channelNumber];
2157
2270
  if (channel.isDrum)
2158
2271
  return;
2159
- scheduleTime ??= this.audioContext.currentTime;
2272
+ if (!(0 <= scheduleTime))
2273
+ scheduleTime = this.audioContext.currentTime;
2160
2274
  channel.state.sustainPedal = value / 127;
2161
2275
  if (64 <= value) {
2162
2276
  this.processScheduledNotes(channel, (note) => {
@@ -2174,7 +2288,8 @@ class Midy {
2174
2288
  const channel = this.channels[channelNumber];
2175
2289
  if (channel.isDrum)
2176
2290
  return;
2177
- scheduleTime ??= this.audioContext.currentTime;
2291
+ if (!(0 <= scheduleTime))
2292
+ scheduleTime = this.audioContext.currentTime;
2178
2293
  channel.state.portamento = value / 127;
2179
2294
  this.updatePortamento(channel, scheduleTime);
2180
2295
  }
@@ -2182,7 +2297,8 @@ class Midy {
2182
2297
  const channel = this.channels[channelNumber];
2183
2298
  if (channel.isDrum)
2184
2299
  return;
2185
- scheduleTime ??= this.audioContext.currentTime;
2300
+ if (!(0 <= scheduleTime))
2301
+ scheduleTime = this.audioContext.currentTime;
2186
2302
  channel.state.sostenutoPedal = value / 127;
2187
2303
  if (64 <= value) {
2188
2304
  const sostenutoNotes = [];
@@ -2203,7 +2319,8 @@ class Midy {
2203
2319
  if (channel.isDrum)
2204
2320
  return;
2205
2321
  const state = channel.state;
2206
- scheduleTime ??= this.audioContext.currentTime;
2322
+ if (!(0 <= scheduleTime))
2323
+ scheduleTime = this.audioContext.currentTime;
2207
2324
  state.softPedal = softPedal / 127;
2208
2325
  this.processScheduledNotes(channel, (note) => {
2209
2326
  if (this.isPortamento(channel, note)) {
@@ -2220,11 +2337,12 @@ class Midy {
2220
2337
  const channel = this.channels[channelNumber];
2221
2338
  if (channel.isDrum)
2222
2339
  return;
2223
- scheduleTime ??= this.audioContext.currentTime;
2340
+ if (!(0 <= scheduleTime))
2341
+ scheduleTime = this.audioContext.currentTime;
2224
2342
  const state = channel.state;
2225
2343
  state.filterResonance = ccValue / 127;
2226
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2227
2344
  this.processScheduledNotes(channel, (note) => {
2345
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2228
2346
  const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2229
2347
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2230
2348
  });
@@ -2241,14 +2359,16 @@ class Midy {
2241
2359
  const channel = this.channels[channelNumber];
2242
2360
  if (channel.isDrum)
2243
2361
  return;
2244
- scheduleTime ??= this.audioContext.currentTime;
2362
+ if (!(0 <= scheduleTime))
2363
+ scheduleTime = this.audioContext.currentTime;
2245
2364
  channel.state.releaseTime = releaseTime / 127;
2246
2365
  }
2247
2366
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2248
2367
  const channel = this.channels[channelNumber];
2249
2368
  if (channel.isDrum)
2250
2369
  return;
2251
- scheduleTime ??= this.audioContext.currentTime;
2370
+ if (!(0 <= scheduleTime))
2371
+ scheduleTime = this.audioContext.currentTime;
2252
2372
  channel.state.attackTime = attackTime / 127;
2253
2373
  this.processScheduledNotes(channel, (note) => {
2254
2374
  if (scheduleTime < note.startTime) {
@@ -2261,7 +2381,8 @@ class Midy {
2261
2381
  if (channel.isDrum)
2262
2382
  return;
2263
2383
  const state = channel.state;
2264
- scheduleTime ??= this.audioContext.currentTime;
2384
+ if (!(0 <= scheduleTime))
2385
+ scheduleTime = this.audioContext.currentTime;
2265
2386
  state.brightness = brightness / 127;
2266
2387
  this.processScheduledNotes(channel, (note) => {
2267
2388
  if (this.isPortamento(channel, note)) {
@@ -2276,7 +2397,8 @@ class Midy {
2276
2397
  const channel = this.channels[channelNumber];
2277
2398
  if (channel.isDrum)
2278
2399
  return;
2279
- scheduleTime ??= this.audioContext.currentTime;
2400
+ if (!(0 <= scheduleTime))
2401
+ scheduleTime = this.audioContext.currentTime;
2280
2402
  channel.state.decayTime = dacayTime / 127;
2281
2403
  this.processScheduledNotes(channel, (note) => {
2282
2404
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2286,7 +2408,8 @@ class Midy {
2286
2408
  const channel = this.channels[channelNumber];
2287
2409
  if (channel.isDrum)
2288
2410
  return;
2289
- scheduleTime ??= this.audioContext.currentTime;
2411
+ if (!(0 <= scheduleTime))
2412
+ scheduleTime = this.audioContext.currentTime;
2290
2413
  channel.state.vibratoRate = vibratoRate / 127;
2291
2414
  if (channel.vibratoDepth <= 0)
2292
2415
  return;
@@ -2298,7 +2421,8 @@ class Midy {
2298
2421
  const channel = this.channels[channelNumber];
2299
2422
  if (channel.isDrum)
2300
2423
  return;
2301
- scheduleTime ??= this.audioContext.currentTime;
2424
+ if (!(0 <= scheduleTime))
2425
+ scheduleTime = this.audioContext.currentTime;
2302
2426
  const prev = channel.state.vibratoDepth;
2303
2427
  channel.state.vibratoDepth = vibratoDepth / 127;
2304
2428
  if (0 < prev) {
@@ -2316,7 +2440,8 @@ class Midy {
2316
2440
  const channel = this.channels[channelNumber];
2317
2441
  if (channel.isDrum)
2318
2442
  return;
2319
- scheduleTime ??= this.audioContext.currentTime;
2443
+ if (!(0 <= scheduleTime))
2444
+ scheduleTime = this.audioContext.currentTime;
2320
2445
  channel.state.vibratoDelay = vibratoDelay / 127;
2321
2446
  if (0 < channel.state.vibratoDepth) {
2322
2447
  this.processScheduledNotes(channel, (note) => {
@@ -2324,8 +2449,16 @@ class Midy {
2324
2449
  });
2325
2450
  }
2326
2451
  }
2452
+ setPortamentoNoteNumber(channelNumber, value, scheduleTime) {
2453
+ if (!(0 <= scheduleTime))
2454
+ scheduleTime = this.audioContext.currentTime;
2455
+ const channel = this.channels[channelNumber];
2456
+ channel.portamentoControl = true;
2457
+ channel.state.portamentoNoteNumber = value / 127;
2458
+ }
2327
2459
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
2328
- scheduleTime ??= this.audioContext.currentTime;
2460
+ if (!(0 <= scheduleTime))
2461
+ scheduleTime = this.audioContext.currentTime;
2329
2462
  const channel = this.channels[channelNumber];
2330
2463
  const state = channel.state;
2331
2464
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2334,7 +2467,8 @@ class Midy {
2334
2467
  });
2335
2468
  }
2336
2469
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2337
- scheduleTime ??= this.audioContext.currentTime;
2470
+ if (!(0 <= scheduleTime))
2471
+ scheduleTime = this.audioContext.currentTime;
2338
2472
  const channel = this.channels[channelNumber];
2339
2473
  const state = channel.state;
2340
2474
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2394,12 +2528,14 @@ class Midy {
2394
2528
  }
2395
2529
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2396
2530
  dataIncrement(channelNumber, scheduleTime) {
2397
- scheduleTime ??= this.audioContext.currentTime;
2531
+ if (!(0 <= scheduleTime))
2532
+ scheduleTime = this.audioContext.currentTime;
2398
2533
  this.handleRPN(channelNumber, 1, scheduleTime);
2399
2534
  }
2400
2535
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2401
2536
  dataDecrement(channelNumber, scheduleTime) {
2402
- scheduleTime ??= this.audioContext.currentTime;
2537
+ if (!(0 <= scheduleTime))
2538
+ scheduleTime = this.audioContext.currentTime;
2403
2539
  this.handleRPN(channelNumber, -1, scheduleTime);
2404
2540
  }
2405
2541
  setRPNMSB(channelNumber, value) {
@@ -2422,7 +2558,8 @@ class Midy {
2422
2558
  const channel = this.channels[channelNumber];
2423
2559
  if (channel.isDrum)
2424
2560
  return;
2425
- scheduleTime ??= this.audioContext.currentTime;
2561
+ if (!(0 <= scheduleTime))
2562
+ scheduleTime = this.audioContext.currentTime;
2426
2563
  const state = channel.state;
2427
2564
  const prev = state.pitchWheelSensitivity;
2428
2565
  const next = value / 12800;
@@ -2442,7 +2579,8 @@ class Midy {
2442
2579
  const channel = this.channels[channelNumber];
2443
2580
  if (channel.isDrum)
2444
2581
  return;
2445
- scheduleTime ??= this.audioContext.currentTime;
2582
+ if (!(0 <= scheduleTime))
2583
+ scheduleTime = this.audioContext.currentTime;
2446
2584
  const prev = channel.fineTuning;
2447
2585
  const next = value;
2448
2586
  channel.fineTuning = next;
@@ -2459,7 +2597,8 @@ class Midy {
2459
2597
  const channel = this.channels[channelNumber];
2460
2598
  if (channel.isDrum)
2461
2599
  return;
2462
- scheduleTime ??= this.audioContext.currentTime;
2600
+ if (!(0 <= scheduleTime))
2601
+ scheduleTime = this.audioContext.currentTime;
2463
2602
  const prev = channel.coarseTuning;
2464
2603
  const next = value;
2465
2604
  channel.coarseTuning = next;
@@ -2476,12 +2615,18 @@ class Midy {
2476
2615
  const channel = this.channels[channelNumber];
2477
2616
  if (channel.isDrum)
2478
2617
  return;
2479
- scheduleTime ??= this.audioContext.currentTime;
2618
+ if (!(0 <= scheduleTime))
2619
+ scheduleTime = this.audioContext.currentTime;
2480
2620
  channel.modulationDepthRange = value;
2481
2621
  this.updateModulation(channel, scheduleTime);
2482
2622
  }
2483
- allSoundOff(channelNumber, _value, scheduleTime) {
2623
+ setRPGMakerLoop(_channelNumber, _value, scheduleTime) {
2484
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;
2485
2630
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2486
2631
  }
2487
2632
  resetChannelStates(channelNumber) {
@@ -2511,8 +2656,10 @@ class Midy {
2511
2656
  "polyphonicKeyPressure",
2512
2657
  "channelPressure",
2513
2658
  "pitchWheel",
2514
- "expression",
2515
- "modulationDepth",
2659
+ "expressionMSB",
2660
+ "expressionLSB",
2661
+ "modulationDepthMSB",
2662
+ "modulationDepthLSB",
2516
2663
  "sustainPedal",
2517
2664
  "portamento",
2518
2665
  "sostenutoPedal",
@@ -2541,7 +2688,8 @@ class Midy {
2541
2688
  }
2542
2689
  }
2543
2690
  allNotesOff(channelNumber, _value, scheduleTime) {
2544
- scheduleTime ??= this.audioContext.currentTime;
2691
+ if (!(0 <= scheduleTime))
2692
+ scheduleTime = this.audioContext.currentTime;
2545
2693
  return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2546
2694
  }
2547
2695
  omniOff(channelNumber, value, scheduleTime) {
@@ -2593,7 +2741,8 @@ class Midy {
2593
2741
  }
2594
2742
  }
2595
2743
  GM1SystemOn(scheduleTime) {
2596
- scheduleTime ??= this.audioContext.currentTime;
2744
+ if (!(0 <= scheduleTime))
2745
+ scheduleTime = this.audioContext.currentTime;
2597
2746
  this.mode = "GM1";
2598
2747
  for (let i = 0; i < this.channels.length; i++) {
2599
2748
  this.allSoundOff(i, 0, scheduleTime);
@@ -2606,7 +2755,8 @@ class Midy {
2606
2755
  this.channels[9].isDrum = true;
2607
2756
  }
2608
2757
  GM2SystemOn(scheduleTime) {
2609
- scheduleTime ??= this.audioContext.currentTime;
2758
+ if (!(0 <= scheduleTime))
2759
+ scheduleTime = this.audioContext.currentTime;
2610
2760
  this.mode = "GM2";
2611
2761
  for (let i = 0; i < this.channels.length; i++) {
2612
2762
  this.allSoundOff(i, 0, scheduleTime);
@@ -2674,7 +2824,8 @@ class Midy {
2674
2824
  this.setMasterVolume(volume, scheduleTime);
2675
2825
  }
2676
2826
  setMasterVolume(value, scheduleTime) {
2677
- scheduleTime ??= this.audioContext.currentTime;
2827
+ if (!(0 <= scheduleTime))
2828
+ scheduleTime = this.audioContext.currentTime;
2678
2829
  this.masterVolume.gain
2679
2830
  .cancelScheduledValues(scheduleTime)
2680
2831
  .setValueAtTime(value * value, scheduleTime);
@@ -3222,5 +3373,6 @@ Object.defineProperty(Midy, "channelSettings", {
3222
3373
  modulationDepthRange: 50, // cent
3223
3374
  fineTuning: 0, // cent
3224
3375
  coarseTuning: 0, // cent
3376
+ portamentoControl: false,
3225
3377
  }
3226
3378
  });