@marmooo/midy 0.2.5 → 0.2.7

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.
@@ -69,31 +69,37 @@ class Note {
69
69
  writable: true,
70
70
  value: void 0
71
71
  });
72
+ Object.defineProperty(this, "filterDepth", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: void 0
77
+ });
72
78
  Object.defineProperty(this, "volumeEnvelopeNode", {
73
79
  enumerable: true,
74
80
  configurable: true,
75
81
  writable: true,
76
82
  value: void 0
77
83
  });
78
- Object.defineProperty(this, "volumeNode", {
84
+ Object.defineProperty(this, "volumeDepth", {
79
85
  enumerable: true,
80
86
  configurable: true,
81
87
  writable: true,
82
88
  value: void 0
83
89
  });
84
- Object.defineProperty(this, "gainL", {
90
+ Object.defineProperty(this, "volumeNode", {
85
91
  enumerable: true,
86
92
  configurable: true,
87
93
  writable: true,
88
94
  value: void 0
89
95
  });
90
- Object.defineProperty(this, "gainR", {
96
+ Object.defineProperty(this, "gainL", {
91
97
  enumerable: true,
92
98
  configurable: true,
93
99
  writable: true,
94
100
  value: void 0
95
101
  });
96
- Object.defineProperty(this, "volumeDepth", {
102
+ Object.defineProperty(this, "gainR", {
97
103
  enumerable: true,
98
104
  configurable: true,
99
105
  writable: true,
@@ -495,6 +501,9 @@ class MidyGM2 {
495
501
  ...this.setChannelAudioNodes(audioContext),
496
502
  scheduledNotes: new SparseMap(128),
497
503
  sostenutoNotes: new SparseMap(128),
504
+ scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
505
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
506
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
498
507
  };
499
508
  });
500
509
  return channels;
@@ -561,10 +570,11 @@ class MidyGM2 {
561
570
  const event = this.timeline[queueIndex];
562
571
  if (event.startTime > t + this.lookAhead)
563
572
  break;
573
+ const startTime = event.startTime + this.startDelay - offset;
564
574
  switch (event.type) {
565
575
  case "noteOn":
566
576
  if (event.velocity !== 0) {
567
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
577
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
568
578
  break;
569
579
  }
570
580
  /* falls through */
@@ -572,26 +582,27 @@ class MidyGM2 {
572
582
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
573
583
  if (portamentoTarget)
574
584
  portamentoTarget.portamento = true;
575
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
585
+ const notePromise = this.scheduleNoteOff(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, false, // force
586
+ portamentoTarget?.noteNumber);
576
587
  if (notePromise) {
577
588
  this.notePromises.push(notePromise);
578
589
  }
579
590
  break;
580
591
  }
581
592
  case "controller":
582
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
593
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
583
594
  break;
584
595
  case "programChange":
585
- this.handleProgramChange(event.channel, event.programNumber);
596
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
586
597
  break;
587
598
  case "channelAftertouch":
588
- this.handleChannelPressure(event.channel, event.amount);
599
+ this.handleChannelPressure(event.channel, event.amount, startTime);
589
600
  break;
590
601
  case "pitchBend":
591
- this.setPitchBend(event.channel, event.value + 8192);
602
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
592
603
  break;
593
604
  case "sysEx":
594
- this.handleSysEx(event.data);
605
+ this.handleSysEx(event.data, startTime);
595
606
  }
596
607
  queueIndex++;
597
608
  }
@@ -622,10 +633,11 @@ class MidyGM2 {
622
633
  resolve();
623
634
  return;
624
635
  }
625
- const t = this.audioContext.currentTime + offset;
636
+ const now = this.audioContext.currentTime;
637
+ const t = now + offset;
626
638
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
627
639
  if (this.isPausing) {
628
- await this.stopNotes(0, true);
640
+ await this.stopNotes(0, true, now);
629
641
  this.notePromises = [];
630
642
  resolve();
631
643
  this.isPausing = false;
@@ -633,7 +645,7 @@ class MidyGM2 {
633
645
  return;
634
646
  }
635
647
  else if (this.isStopping) {
636
- await this.stopNotes(0, true);
648
+ await this.stopNotes(0, true, now);
637
649
  this.notePromises = [];
638
650
  this.exclusiveClassMap.clear();
639
651
  this.audioBufferCache.clear();
@@ -643,7 +655,7 @@ class MidyGM2 {
643
655
  return;
644
656
  }
645
657
  else if (this.isSeeking) {
646
- this.stopNotes(0, true);
658
+ this.stopNotes(0, true, now);
647
659
  this.exclusiveClassMap.clear();
648
660
  this.startTime = this.audioContext.currentTime;
649
661
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -652,7 +664,6 @@ class MidyGM2 {
652
664
  await schedulePlayback();
653
665
  }
654
666
  else {
655
- const now = this.audioContext.currentTime;
656
667
  const waitTime = now + this.noteCheckInterval;
657
668
  await this.scheduleTask(() => { }, waitTime);
658
669
  await schedulePlayback();
@@ -772,25 +783,26 @@ class MidyGM2 {
772
783
  }
773
784
  return { instruments, timeline };
774
785
  }
775
- async stopChannelNotes(channelNumber, velocity, force) {
776
- const now = this.audioContext.currentTime;
786
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
777
787
  const channel = this.channels[channelNumber];
788
+ const promises = [];
778
789
  channel.scheduledNotes.forEach((noteList) => {
779
790
  for (let i = 0; i < noteList.length; i++) {
780
791
  const note = noteList[i];
781
792
  if (!note)
782
793
  continue;
783
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
784
- force);
794
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
785
795
  this.notePromises.push(promise);
796
+ promises.push(promise);
786
797
  }
787
798
  });
788
799
  channel.scheduledNotes.clear();
789
- await Promise.all(this.notePromises);
800
+ return Promise.all(promises);
790
801
  }
791
- stopNotes(velocity, force) {
802
+ stopNotes(velocity, force, scheduleTime) {
803
+ const promises = [];
792
804
  for (let i = 0; i < this.channels.length; i++) {
793
- this.stopChannelNotes(i, velocity, force);
805
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
794
806
  }
795
807
  return Promise.all(this.notePromises);
796
808
  }
@@ -838,22 +850,34 @@ class MidyGM2 {
838
850
  const now = this.audioContext.currentTime;
839
851
  return this.resumeTime + now - this.startTime - this.startDelay;
840
852
  }
841
- getActiveNotes(channel, time) {
853
+ processScheduledNotes(channel, scheduleTime, callback) {
854
+ channel.scheduledNotes.forEach((noteList) => {
855
+ for (let i = 0; i < noteList.length; i++) {
856
+ const note = noteList[i];
857
+ if (!note)
858
+ continue;
859
+ if (scheduleTime < note.startTime)
860
+ continue;
861
+ callback(note);
862
+ }
863
+ });
864
+ }
865
+ getActiveNotes(channel, scheduleTime) {
842
866
  const activeNotes = new SparseMap(128);
843
867
  channel.scheduledNotes.forEach((noteList) => {
844
- const activeNote = this.getActiveNote(noteList, time);
868
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
845
869
  if (activeNote) {
846
870
  activeNotes.set(activeNote.noteNumber, activeNote);
847
871
  }
848
872
  });
849
873
  return activeNotes;
850
874
  }
851
- getActiveNote(noteList, time) {
875
+ getActiveNote(noteList, scheduleTime) {
852
876
  for (let i = noteList.length - 1; i >= 0; i--) {
853
877
  const note = noteList[i];
854
878
  if (!note)
855
879
  return;
856
- if (time < note.startTime)
880
+ if (scheduleTime < note.startTime)
857
881
  continue;
858
882
  return (note.ending) ? null : note;
859
883
  }
@@ -1013,73 +1037,64 @@ class MidyGM2 {
1013
1037
  calcNoteDetune(channel, note) {
1014
1038
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1015
1039
  }
1016
- updateChannelDetune(channel) {
1017
- channel.scheduledNotes.forEach((noteList) => {
1018
- for (let i = 0; i < noteList.length; i++) {
1019
- const note = noteList[i];
1020
- if (!note)
1021
- continue;
1022
- this.updateDetune(channel, note, 0);
1023
- }
1040
+ updateChannelDetune(channel, scheduleTime) {
1041
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1042
+ this.updateDetune(channel, note, scheduleTime);
1024
1043
  });
1025
1044
  }
1026
- updateDetune(channel, note, pressure) {
1027
- const now = this.audioContext.currentTime;
1045
+ updateDetune(channel, note, scheduleTime) {
1028
1046
  const noteDetune = this.calcNoteDetune(channel, note);
1029
- const detune = channel.detune + noteDetune + pressure;
1047
+ const detune = channel.detune + noteDetune;
1030
1048
  note.bufferSource.detune
1031
- .cancelScheduledValues(now)
1032
- .setValueAtTime(detune, now);
1049
+ .cancelScheduledValues(scheduleTime)
1050
+ .setValueAtTime(detune, scheduleTime);
1033
1051
  }
1034
1052
  getPortamentoTime(channel) {
1035
1053
  const factor = 5 * Math.log(10) / 127;
1036
1054
  const time = channel.state.portamentoTime;
1037
1055
  return Math.log(time) / factor;
1038
1056
  }
1039
- setPortamentoStartVolumeEnvelope(channel, note) {
1040
- const now = this.audioContext.currentTime;
1057
+ setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1041
1058
  const { voiceParams, startTime } = note;
1042
1059
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1043
1060
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1044
1061
  const volDelay = startTime + voiceParams.volDelay;
1045
1062
  const portamentoTime = volDelay + this.getPortamentoTime(channel);
1046
1063
  note.volumeEnvelopeNode.gain
1047
- .cancelScheduledValues(now)
1064
+ .cancelScheduledValues(scheduleTime)
1048
1065
  .setValueAtTime(0, volDelay)
1049
1066
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
1050
1067
  }
1051
- setVolumeEnvelope(note, pressure) {
1052
- const now = this.audioContext.currentTime;
1068
+ setVolumeEnvelope(channel, note, scheduleTime) {
1053
1069
  const { voiceParams, startTime } = note;
1054
1070
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1055
- (1 + pressure);
1071
+ (1 + this.getAmplitudeControl(channel));
1056
1072
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1057
1073
  const volDelay = startTime + voiceParams.volDelay;
1058
1074
  const volAttack = volDelay + voiceParams.volAttack;
1059
1075
  const volHold = volAttack + voiceParams.volHold;
1060
1076
  const volDecay = volHold + voiceParams.volDecay;
1061
1077
  note.volumeEnvelopeNode.gain
1062
- .cancelScheduledValues(now)
1078
+ .cancelScheduledValues(scheduleTime)
1063
1079
  .setValueAtTime(0, startTime)
1064
1080
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
1065
1081
  .exponentialRampToValueAtTime(attackVolume, volAttack)
1066
1082
  .setValueAtTime(attackVolume, volHold)
1067
1083
  .linearRampToValueAtTime(sustainVolume, volDecay);
1068
1084
  }
1069
- setPitchEnvelope(note) {
1070
- const now = this.audioContext.currentTime;
1085
+ setPitchEnvelope(note, scheduleTime) {
1071
1086
  const { voiceParams } = note;
1072
1087
  const baseRate = voiceParams.playbackRate;
1073
1088
  note.bufferSource.playbackRate
1074
- .cancelScheduledValues(now)
1075
- .setValueAtTime(baseRate, now);
1089
+ .cancelScheduledValues(scheduleTime)
1090
+ .setValueAtTime(baseRate, scheduleTime);
1076
1091
  const modEnvToPitch = voiceParams.modEnvToPitch;
1077
1092
  if (modEnvToPitch === 0)
1078
1093
  return;
1079
1094
  const basePitch = this.rateToCent(baseRate);
1080
1095
  const peekPitch = basePitch + modEnvToPitch;
1081
1096
  const peekRate = this.centToRate(peekPitch);
1082
- const modDelay = startTime + voiceParams.modDelay;
1097
+ const modDelay = note.startTime + voiceParams.modDelay;
1083
1098
  const modAttack = modDelay + voiceParams.modAttack;
1084
1099
  const modHold = modAttack + voiceParams.modHold;
1085
1100
  const modDecay = modHold + voiceParams.modDecay;
@@ -1094,8 +1109,7 @@ class MidyGM2 {
1094
1109
  const maxFrequency = 20000; // max Hz of initialFilterFc
1095
1110
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1096
1111
  }
1097
- setPortamentoStartFilterEnvelope(channel, note) {
1098
- const now = this.audioContext.currentTime;
1112
+ setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1099
1113
  const state = channel.state;
1100
1114
  const { voiceParams, noteNumber, startTime } = note;
1101
1115
  const softPedalFactor = 1 -
@@ -1110,18 +1124,18 @@ class MidyGM2 {
1110
1124
  const portamentoTime = startTime + this.getPortamentoTime(channel);
1111
1125
  const modDelay = startTime + voiceParams.modDelay;
1112
1126
  note.filterNode.frequency
1113
- .cancelScheduledValues(now)
1127
+ .cancelScheduledValues(scheduleTime)
1114
1128
  .setValueAtTime(adjustedBaseFreq, startTime)
1115
1129
  .setValueAtTime(adjustedBaseFreq, modDelay)
1116
1130
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1117
1131
  }
1118
- setFilterEnvelope(channel, note, pressure) {
1119
- const now = this.audioContext.currentTime;
1132
+ setFilterEnvelope(channel, note, scheduleTime) {
1120
1133
  const state = channel.state;
1121
1134
  const { voiceParams, noteNumber, startTime } = note;
1122
1135
  const softPedalFactor = 1 -
1123
1136
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1124
- const baseCent = voiceParams.initialFilterFc + pressure;
1137
+ const baseCent = voiceParams.initialFilterFc +
1138
+ this.getFilterCutoffControl(channel);
1125
1139
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1126
1140
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1127
1141
  softPedalFactor;
@@ -1135,14 +1149,14 @@ class MidyGM2 {
1135
1149
  const modHold = modAttack + voiceParams.modHold;
1136
1150
  const modDecay = modHold + voiceParams.modDecay;
1137
1151
  note.filterNode.frequency
1138
- .cancelScheduledValues(now)
1152
+ .cancelScheduledValues(scheduleTime)
1139
1153
  .setValueAtTime(adjustedBaseFreq, startTime)
1140
1154
  .setValueAtTime(adjustedBaseFreq, modDelay)
1141
1155
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1142
1156
  .setValueAtTime(adjustedPeekFreq, modHold)
1143
1157
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
1144
1158
  }
1145
- startModulation(channel, note, startTime) {
1159
+ startModulation(channel, note, scheduleTime) {
1146
1160
  const { voiceParams } = note;
1147
1161
  note.modulationLFO = new OscillatorNode(this.audioContext, {
1148
1162
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -1151,10 +1165,10 @@ class MidyGM2 {
1151
1165
  gain: voiceParams.modLfoToFilterFc,
1152
1166
  });
1153
1167
  note.modulationDepth = new GainNode(this.audioContext);
1154
- this.setModLfoToPitch(channel, note, 0);
1168
+ this.setModLfoToPitch(channel, note, scheduleTime);
1155
1169
  note.volumeDepth = new GainNode(this.audioContext);
1156
- this.setModLfoToVolume(note, 0);
1157
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1170
+ this.setModLfoToVolume(note, scheduleTime);
1171
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1158
1172
  note.modulationLFO.connect(note.filterDepth);
1159
1173
  note.filterDepth.connect(note.filterNode.frequency);
1160
1174
  note.modulationLFO.connect(note.modulationDepth);
@@ -1162,15 +1176,15 @@ class MidyGM2 {
1162
1176
  note.modulationLFO.connect(note.volumeDepth);
1163
1177
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1164
1178
  }
1165
- startVibrato(channel, note, startTime) {
1179
+ startVibrato(channel, note, scheduleTime) {
1166
1180
  const { voiceParams } = note;
1167
1181
  const state = channel.state;
1168
1182
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1169
1183
  frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1170
1184
  });
1171
- note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1185
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1172
1186
  note.vibratoDepth = new GainNode(this.audioContext);
1173
- this.setVibLfoToPitch(channel, note);
1187
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1174
1188
  note.vibratoLFO.connect(note.vibratoDepth);
1175
1189
  note.vibratoDepth.connect(note.bufferSource.detune);
1176
1190
  }
@@ -1193,6 +1207,7 @@ class MidyGM2 {
1193
1207
  }
1194
1208
  }
1195
1209
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1210
+ const now = this.audioContext.currentTime;
1196
1211
  const state = channel.state;
1197
1212
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1198
1213
  const voiceParams = voice.getAllParams(controllerState);
@@ -1209,20 +1224,20 @@ class MidyGM2 {
1209
1224
  });
1210
1225
  if (portamento) {
1211
1226
  note.portamento = true;
1212
- this.setPortamentoStartVolumeEnvelope(channel, note);
1213
- this.setPortamentoStartFilterEnvelope(channel, note);
1227
+ this.setPortamentoStartVolumeEnvelope(channel, note, now);
1228
+ this.setPortamentoStartFilterEnvelope(channel, note, now);
1214
1229
  }
1215
1230
  else {
1216
1231
  note.portamento = false;
1217
- this.setVolumeEnvelope(note, 0);
1218
- this.setFilterEnvelope(channel, note, 0);
1232
+ this.setVolumeEnvelope(channel, note, now);
1233
+ this.setFilterEnvelope(channel, note, now);
1219
1234
  }
1220
1235
  if (0 < state.vibratoDepth) {
1221
- this.startVibrato(channel, note, startTime);
1236
+ this.startVibrato(channel, note, now);
1222
1237
  }
1223
- this.setPitchEnvelope(note);
1238
+ this.setPitchEnvelope(note, now);
1224
1239
  if (0 < state.modulationDepth) {
1225
- this.startModulation(channel, note, startTime);
1240
+ this.startModulation(channel, note, now);
1226
1241
  }
1227
1242
  if (this.mono && channel.currentBufferSource) {
1228
1243
  channel.currentBufferSource.stop(startTime);
@@ -1234,10 +1249,10 @@ class MidyGM2 {
1234
1249
  note.volumeNode.connect(note.gainL);
1235
1250
  note.volumeNode.connect(note.gainR);
1236
1251
  if (0 < channel.chorusSendLevel) {
1237
- this.setChorusEffectsSend(channel, note, 0);
1252
+ this.setChorusEffectsSend(channel, note, 0, now);
1238
1253
  }
1239
1254
  if (0 < channel.reverbSendLevel) {
1240
- this.setReverbEffectsSend(channel, note, 0);
1255
+ this.setReverbEffectsSend(channel, note, 0, now);
1241
1256
  }
1242
1257
  note.bufferSource.start(startTime);
1243
1258
  return note;
@@ -1274,9 +1289,9 @@ class MidyGM2 {
1274
1289
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1275
1290
  const [prevNote, prevChannelNumber] = prevEntry;
1276
1291
  if (!prevNote.ending) {
1277
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1278
- startTime, undefined, // portamentoNoteNumber
1279
- true);
1292
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1293
+ startTime, true, // force
1294
+ undefined);
1280
1295
  }
1281
1296
  }
1282
1297
  this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
@@ -1289,9 +1304,9 @@ class MidyGM2 {
1289
1304
  scheduledNotes.set(noteNumber, [note]);
1290
1305
  }
1291
1306
  }
1292
- noteOn(channelNumber, noteNumber, velocity, portamento) {
1293
- const now = this.audioContext.currentTime;
1294
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1307
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1308
+ scheduleTime ??= this.audioContext.currentTime;
1309
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1295
1310
  }
1296
1311
  stopNote(endTime, stopTime, scheduledNotes, index) {
1297
1312
  const note = scheduledNotes[index];
@@ -1331,7 +1346,7 @@ class MidyGM2 {
1331
1346
  note.bufferSource.stop(stopTime);
1332
1347
  });
1333
1348
  }
1334
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
1349
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1335
1350
  const channel = this.channels[channelNumber];
1336
1351
  const state = channel.state;
1337
1352
  if (!force) {
@@ -1370,24 +1385,19 @@ class MidyGM2 {
1370
1385
  }
1371
1386
  }
1372
1387
  }
1373
- releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1374
- const now = this.audioContext.currentTime;
1375
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1388
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1389
+ scheduleTime ??= this.audioContext.currentTime;
1390
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1391
+ undefined);
1376
1392
  }
1377
- releaseSustainPedal(channelNumber, halfVelocity) {
1393
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1378
1394
  const velocity = halfVelocity * 2;
1379
1395
  const channel = this.channels[channelNumber];
1380
1396
  const promises = [];
1381
- channel.state.sustainPedal = halfVelocity;
1382
- channel.scheduledNotes.forEach((noteList) => {
1383
- for (let i = 0; i < noteList.length; i++) {
1384
- const note = noteList[i];
1385
- if (!note)
1386
- continue;
1387
- const { noteNumber } = note;
1388
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1389
- promises.push(promise);
1390
- }
1397
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1398
+ const { noteNumber } = note;
1399
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
1400
+ promises.push(promise);
1391
1401
  });
1392
1402
  return promises;
1393
1403
  }
@@ -1398,39 +1408,38 @@ class MidyGM2 {
1398
1408
  channel.state.sostenutoPedal = 0;
1399
1409
  channel.sostenutoNotes.forEach((activeNote) => {
1400
1410
  const { noteNumber } = activeNote;
1401
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1411
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
1402
1412
  promises.push(promise);
1403
1413
  });
1404
1414
  channel.sostenutoNotes.clear();
1405
1415
  return promises;
1406
1416
  }
1407
- handleMIDIMessage(statusByte, data1, data2) {
1417
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1408
1418
  const channelNumber = omni ? 0 : statusByte & 0x0F;
1409
1419
  const messageType = statusByte & 0xF0;
1410
1420
  switch (messageType) {
1411
1421
  case 0x80:
1412
- return this.releaseNote(channelNumber, data1, data2);
1422
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
1413
1423
  case 0x90:
1414
- return this.noteOn(channelNumber, data1, data2);
1424
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
1415
1425
  case 0xB0:
1416
- return this.handleControlChange(channelNumber, data1, data2);
1426
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1417
1427
  case 0xC0:
1418
- return this.handleProgramChange(channelNumber, data1);
1428
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
1419
1429
  case 0xD0:
1420
- return this.handleChannelPressure(channelNumber, data1);
1430
+ return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1421
1431
  case 0xE0:
1422
- return this.handlePitchBendMessage(channelNumber, data1, data2);
1432
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1423
1433
  default:
1424
1434
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1425
1435
  }
1426
1436
  }
1427
- handleProgramChange(channelNumber, program) {
1437
+ handleProgramChange(channelNumber, program, _scheduleTime) {
1428
1438
  const channel = this.channels[channelNumber];
1429
1439
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1430
1440
  channel.program = program;
1431
1441
  }
1432
- handleChannelPressure(channelNumber, value) {
1433
- const now = this.audioContext.currentTime;
1442
+ handleChannelPressure(channelNumber, value, scheduleTime) {
1434
1443
  const channel = this.channels[channelNumber];
1435
1444
  const prev = channel.state.channelPressure;
1436
1445
  const next = value / 127;
@@ -1440,69 +1449,68 @@ class MidyGM2 {
1440
1449
  channel.detune += pressureDepth * (next - prev);
1441
1450
  }
1442
1451
  const table = channel.channelPressureTable;
1443
- this.getActiveNotes(channel, now).forEach((note) => {
1444
- this.applyDestinationSettings(channel, note, table);
1452
+ this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1453
+ this.setControllerParameters(channel, note, table);
1445
1454
  });
1446
1455
  // this.applyVoiceParams(channel, 13);
1447
1456
  }
1448
- handlePitchBendMessage(channelNumber, lsb, msb) {
1457
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1449
1458
  const pitchBend = msb * 128 + lsb;
1450
- this.setPitchBend(channelNumber, pitchBend);
1459
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1451
1460
  }
1452
- setPitchBend(channelNumber, value) {
1461
+ setPitchBend(channelNumber, value, scheduleTime) {
1462
+ scheduleTime ??= this.audioContext.currentTime;
1453
1463
  const channel = this.channels[channelNumber];
1454
1464
  const state = channel.state;
1455
1465
  const prev = state.pitchWheel * 2 - 1;
1456
1466
  const next = (value - 8192) / 8192;
1457
1467
  state.pitchWheel = value / 16383;
1458
1468
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1459
- this.updateChannelDetune(channel);
1460
- this.applyVoiceParams(channel, 14);
1469
+ this.updateChannelDetune(channel, scheduleTime);
1470
+ this.applyVoiceParams(channel, 14, scheduleTime);
1461
1471
  }
1462
- setModLfoToPitch(channel, note, pressure) {
1463
- const now = this.audioContext.currentTime;
1464
- const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1472
+ setModLfoToPitch(channel, note, scheduleTime) {
1473
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1474
+ this.getLFOPitchDepth(channel);
1465
1475
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1466
1476
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1467
1477
  note.modulationDepth.gain
1468
- .cancelScheduledValues(now)
1469
- .setValueAtTime(modulationDepth, now);
1478
+ .cancelScheduledValues(scheduleTime)
1479
+ .setValueAtTime(modulationDepth, scheduleTime);
1470
1480
  }
1471
- setVibLfoToPitch(channel, note) {
1472
- const now = this.audioContext.currentTime;
1481
+ setVibLfoToPitch(channel, note, scheduleTime) {
1473
1482
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1474
1483
  const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1475
1484
  2;
1476
1485
  const vibratoDepthSign = 0 < vibLfoToPitch;
1477
1486
  note.vibratoDepth.gain
1478
- .cancelScheduledValues(now)
1479
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1487
+ .cancelScheduledValues(scheduleTime)
1488
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, scheduleTime);
1480
1489
  }
1481
- setModLfoToFilterFc(note, pressure) {
1482
- const now = this.audioContext.currentTime;
1483
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1490
+ setModLfoToFilterFc(channel, note, scheduleTime) {
1491
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1492
+ this.getLFOFilterDepth(channel);
1484
1493
  note.filterDepth.gain
1485
- .cancelScheduledValues(now)
1486
- .setValueAtTime(modLfoToFilterFc, now);
1494
+ .cancelScheduledValues(scheduleTime)
1495
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
1487
1496
  }
1488
- setModLfoToVolume(note, pressure) {
1489
- const now = this.audioContext.currentTime;
1497
+ setModLfoToVolume(channel, note, scheduleTime) {
1490
1498
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1491
1499
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1492
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1500
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1501
+ (1 + this.getLFOAmplitudeDepth(channel));
1493
1502
  note.volumeDepth.gain
1494
- .cancelScheduledValues(now)
1495
- .setValueAtTime(volumeDepth, now);
1503
+ .cancelScheduledValues(scheduleTime)
1504
+ .setValueAtTime(volumeDepth, scheduleTime);
1496
1505
  }
1497
- setReverbEffectsSend(channel, note, prevValue) {
1506
+ setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1498
1507
  if (0 < prevValue) {
1499
1508
  if (0 < note.voiceParams.reverbEffectsSend) {
1500
- const now = this.audioContext.currentTime;
1501
1509
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1502
1510
  const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1503
1511
  note.reverbEffectsSend.gain
1504
- .cancelScheduledValues(now)
1505
- .setValueAtTime(value, now);
1512
+ .cancelScheduledValues(scheduleTime)
1513
+ .setValueAtTime(value, scheduleTime);
1506
1514
  }
1507
1515
  else {
1508
1516
  note.reverbEffectsSend.disconnect();
@@ -1520,15 +1528,14 @@ class MidyGM2 {
1520
1528
  }
1521
1529
  }
1522
1530
  }
1523
- setChorusEffectsSend(channel, note, prevValue) {
1531
+ setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1524
1532
  if (0 < prevValue) {
1525
1533
  if (0 < note.voiceParams.chorusEffectsSend) {
1526
- const now = this.audioContext.currentTime;
1527
1534
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1528
1535
  const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1529
1536
  note.chorusEffectsSend.gain
1530
- .cancelScheduledValues(now)
1531
- .setValueAtTime(value, now);
1537
+ .cancelScheduledValues(scheduleTime)
1538
+ .setValueAtTime(value, scheduleTime);
1532
1539
  }
1533
1540
  else {
1534
1541
  note.chorusEffectsSend.disconnect();
@@ -1546,75 +1553,71 @@ class MidyGM2 {
1546
1553
  }
1547
1554
  }
1548
1555
  }
1549
- setDelayModLFO(note) {
1550
- const now = this.audioContext.currentTime;
1556
+ setDelayModLFO(note, scheduleTime) {
1551
1557
  const startTime = note.startTime;
1552
- if (startTime < now)
1558
+ if (startTime < scheduleTime)
1553
1559
  return;
1554
- note.modulationLFO.stop(now);
1560
+ note.modulationLFO.stop(scheduleTime);
1555
1561
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1556
1562
  note.modulationLFO.connect(note.filterDepth);
1557
1563
  }
1558
- setFreqModLFO(note) {
1559
- const now = this.audioContext.currentTime;
1564
+ setFreqModLFO(note, scheduleTime) {
1560
1565
  const freqModLFO = note.voiceParams.freqModLFO;
1561
1566
  note.modulationLFO.frequency
1562
- .cancelScheduledValues(now)
1563
- .setValueAtTime(freqModLFO, now);
1567
+ .cancelScheduledValues(scheduleTime)
1568
+ .setValueAtTime(freqModLFO, scheduleTime);
1564
1569
  }
1565
- setFreqVibLFO(channel, note) {
1566
- const now = this.audioContext.currentTime;
1570
+ setFreqVibLFO(channel, note, scheduleTime) {
1567
1571
  const freqVibLFO = note.voiceParams.freqVibLFO;
1568
1572
  note.vibratoLFO.frequency
1569
- .cancelScheduledValues(now)
1570
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1573
+ .cancelScheduledValues(scheduleTime)
1574
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, scheduleTime);
1571
1575
  }
1572
1576
  createVoiceParamsHandlers() {
1573
1577
  return {
1574
- modLfoToPitch: (channel, note, _prevValue) => {
1578
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1575
1579
  if (0 < channel.state.modulationDepth) {
1576
- this.setModLfoToPitch(channel, note, 0);
1580
+ this.setModLfoToPitch(channel, note, scheduleTime);
1577
1581
  }
1578
1582
  },
1579
- vibLfoToPitch: (channel, note, _prevValue) => {
1583
+ vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1580
1584
  if (0 < channel.state.vibratoDepth) {
1581
- this.setVibLfoToPitch(channel, note);
1585
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1582
1586
  }
1583
1587
  },
1584
- modLfoToFilterFc: (channel, note, _prevValue) => {
1588
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1585
1589
  if (0 < channel.state.modulationDepth) {
1586
- this.setModLfoToFilterFc(note, 0);
1590
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
1587
1591
  }
1588
1592
  },
1589
- modLfoToVolume: (channel, note, _prevValue) => {
1593
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1590
1594
  if (0 < channel.state.modulationDepth) {
1591
- this.setModLfoToVolume(note, 0);
1595
+ this.setModLfoToVolume(channel, note, scheduleTime);
1592
1596
  }
1593
1597
  },
1594
- chorusEffectsSend: (channel, note, prevValue) => {
1595
- this.setChorusEffectsSend(channel, note, prevValue);
1598
+ chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1599
+ this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1596
1600
  },
1597
- reverbEffectsSend: (channel, note, prevValue) => {
1598
- this.setReverbEffectsSend(channel, note, prevValue);
1601
+ reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1602
+ this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1599
1603
  },
1600
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1601
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1602
- delayVibLFO: (channel, note, prevValue) => {
1604
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1605
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1606
+ delayVibLFO: (channel, note, prevValue, scheduleTime) => {
1603
1607
  if (0 < channel.state.vibratoDepth) {
1604
- const now = this.audioContext.currentTime;
1605
1608
  const vibratoDelay = channel.state.vibratoDelay * 2;
1606
1609
  const prevStartTime = note.startTime + prevValue * vibratoDelay;
1607
- if (now < prevStartTime)
1610
+ if (scheduleTime < prevStartTime)
1608
1611
  return;
1609
1612
  const value = note.voiceParams.delayVibLFO;
1610
1613
  const startTime = note.startTime + value * vibratoDelay;
1611
- note.vibratoLFO.stop(now);
1614
+ note.vibratoLFO.stop(scheduleTime);
1612
1615
  note.vibratoLFO.start(startTime);
1613
1616
  }
1614
1617
  },
1615
- freqVibLFO: (channel, note, _prevValue) => {
1618
+ freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1616
1619
  if (0 < channel.state.vibratoDepth) {
1617
- this.setFreqVibLFO(channel, note);
1620
+ this.setFreqVibLFO(channel, note, scheduleTime);
1618
1621
  }
1619
1622
  },
1620
1623
  };
@@ -1626,7 +1629,7 @@ class MidyGM2 {
1626
1629
  state[3] = noteNumber / 127;
1627
1630
  return state;
1628
1631
  }
1629
- applyVoiceParams(channel, controllerType) {
1632
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1630
1633
  channel.scheduledNotes.forEach((noteList) => {
1631
1634
  for (let i = 0; i < noteList.length; i++) {
1632
1635
  const note = noteList[i];
@@ -1642,7 +1645,7 @@ class MidyGM2 {
1642
1645
  continue;
1643
1646
  note.voiceParams[key] = value;
1644
1647
  if (key in this.voiceParamsHandlers) {
1645
- this.voiceParamsHandlers[key](channel, note, prevValue);
1648
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1646
1649
  }
1647
1650
  else if (filterEnvelopeKeySet.has(key)) {
1648
1651
  if (appliedFilterEnvelope)
@@ -1655,12 +1658,12 @@ class MidyGM2 {
1655
1658
  noteVoiceParams[key] = voiceParams[key];
1656
1659
  }
1657
1660
  if (note.portamento) {
1658
- this.setPortamentoStartFilterEnvelope(channel, note);
1661
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1659
1662
  }
1660
1663
  else {
1661
- this.setFilterEnvelope(channel, note, 0);
1664
+ this.setFilterEnvelope(channel, note, scheduleTime);
1662
1665
  }
1663
- this.setPitchEnvelope(note);
1666
+ this.setPitchEnvelope(note, scheduleTime);
1664
1667
  }
1665
1668
  else if (volumeEnvelopeKeySet.has(key)) {
1666
1669
  if (appliedVolumeEnvelope)
@@ -1672,7 +1675,7 @@ class MidyGM2 {
1672
1675
  if (key in voiceParams)
1673
1676
  noteVoiceParams[key] = voiceParams[key];
1674
1677
  }
1675
- this.setVolumeEnvelope(note, 0);
1678
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1676
1679
  }
1677
1680
  }
1678
1681
  }
@@ -1706,12 +1709,12 @@ class MidyGM2 {
1706
1709
  127: this.polyOn,
1707
1710
  };
1708
1711
  }
1709
- handleControlChange(channelNumber, controllerType, value) {
1712
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1710
1713
  const handler = this.controlChangeHandlers[controllerType];
1711
1714
  if (handler) {
1712
- handler.call(this, channelNumber, value);
1715
+ handler.call(this, channelNumber, value, scheduleTime);
1713
1716
  const channel = this.channels[channelNumber];
1714
- this.applyVoiceParams(channel, controllerType + 128);
1717
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1715
1718
  this.applyControlTable(channel, controllerType);
1716
1719
  }
1717
1720
  else {
@@ -1721,55 +1724,45 @@ class MidyGM2 {
1721
1724
  setBankMSB(channelNumber, msb) {
1722
1725
  this.channels[channelNumber].bankMSB = msb;
1723
1726
  }
1724
- updateModulation(channel) {
1725
- const now = this.audioContext.currentTime;
1727
+ updateModulation(channel, scheduleTime) {
1728
+ scheduleTime ??= this.audioContext.currentTime;
1726
1729
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1727
- channel.scheduledNotes.forEach((noteList) => {
1728
- for (let i = 0; i < noteList.length; i++) {
1729
- const note = noteList[i];
1730
- if (!note)
1731
- continue;
1732
- if (note.modulationDepth) {
1733
- note.modulationDepth.gain.setValueAtTime(depth, now);
1734
- }
1735
- else {
1736
- this.setPitchEnvelope(note);
1737
- this.startModulation(channel, note, now);
1738
- }
1730
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1731
+ if (note.modulationDepth) {
1732
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1733
+ }
1734
+ else {
1735
+ this.setPitchEnvelope(note, scheduleTime);
1736
+ this.startModulation(channel, note, scheduleTime);
1739
1737
  }
1740
1738
  });
1741
1739
  }
1742
- setModulationDepth(channelNumber, modulation) {
1740
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1743
1741
  const channel = this.channels[channelNumber];
1744
1742
  channel.state.modulationDepth = modulation / 127;
1745
- this.updateModulation(channel);
1743
+ this.updateModulation(channel, scheduleTime);
1746
1744
  }
1747
1745
  setPortamentoTime(channelNumber, portamentoTime) {
1748
1746
  const channel = this.channels[channelNumber];
1749
1747
  const factor = 5 * Math.log(10) / 127;
1750
1748
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1751
1749
  }
1752
- setKeyBasedVolume(channel) {
1753
- const now = this.audioContext.currentTime;
1754
- channel.scheduledNotes.forEach((noteList) => {
1755
- for (let i = 0; i < noteList.length; i++) {
1756
- const note = noteList[i];
1757
- if (!note)
1758
- continue;
1759
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1760
- if (keyBasedValue === 0)
1761
- continue;
1750
+ setKeyBasedVolume(channel, scheduleTime) {
1751
+ scheduleTime ??= this.audioContext.currentTime;
1752
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1753
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1754
+ if (keyBasedValue !== 0) {
1762
1755
  note.volumeNode.gain
1763
- .cancelScheduledValues(now)
1764
- .setValueAtTime(1 + keyBasedValue, now);
1756
+ .cancelScheduledValues(scheduleTime)
1757
+ .setValueAtTime(1 + keyBasedValue, scheduleTime);
1765
1758
  }
1766
1759
  });
1767
1760
  }
1768
- setVolume(channelNumber, volume) {
1761
+ setVolume(channelNumber, volume, scheduleTime) {
1769
1762
  const channel = this.channels[channelNumber];
1770
1763
  channel.state.volume = volume / 127;
1771
- this.updateChannelVolume(channel);
1772
- this.setKeyBasedVolume(channel);
1764
+ this.updateChannelVolume(channel, scheduleTime);
1765
+ this.setKeyBasedVolume(channel, scheduleTime);
1773
1766
  }
1774
1767
  panToGain(pan) {
1775
1768
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1778,90 +1771,84 @@ class MidyGM2 {
1778
1771
  gainRight: Math.sin(theta),
1779
1772
  };
1780
1773
  }
1781
- setKeyBasedPan(channel) {
1782
- const now = this.audioContext.currentTime;
1783
- channel.scheduledNotes.forEach((noteList) => {
1784
- for (let i = 0; i < noteList.length; i++) {
1785
- const note = noteList[i];
1786
- if (!note)
1787
- continue;
1788
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1789
- if (keyBasedValue === 0)
1790
- continue;
1774
+ setKeyBasedPan(channel, scheduleTime) {
1775
+ scheduleTime ??= this.audioContext.currentTime;
1776
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1777
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1778
+ if (keyBasedValue !== 0) {
1791
1779
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1792
1780
  note.gainL.gain
1793
- .cancelScheduledValues(now)
1794
- .setValueAtTime(gainLeft, now);
1781
+ .cancelScheduledValues(scheduleTime)
1782
+ .setValueAtTime(gainLeft, scheduleTime);
1795
1783
  note.gainR.gain
1796
- .cancelScheduledValues(now)
1797
- .setValueAtTime(gainRight, now);
1784
+ .cancelScheduledValues(scheduleTime)
1785
+ .setValueAtTime(gainRight, scheduleTime);
1798
1786
  }
1799
1787
  });
1800
1788
  }
1801
- setPan(channelNumber, pan) {
1789
+ setPan(channelNumber, pan, scheduleTime) {
1802
1790
  const channel = this.channels[channelNumber];
1803
1791
  channel.state.pan = pan / 127;
1804
- this.updateChannelVolume(channel);
1805
- this.setKeyBasedPan(channel);
1792
+ this.updateChannelVolume(channel, scheduleTime);
1793
+ this.setKeyBasedPan(channel, scheduleTime);
1806
1794
  }
1807
- setExpression(channelNumber, expression) {
1795
+ setExpression(channelNumber, expression, scheduleTime) {
1808
1796
  const channel = this.channels[channelNumber];
1809
1797
  channel.state.expression = expression / 127;
1810
- this.updateChannelVolume(channel);
1798
+ this.updateChannelVolume(channel, scheduleTime);
1811
1799
  }
1812
1800
  setBankLSB(channelNumber, lsb) {
1813
1801
  this.channels[channelNumber].bankLSB = lsb;
1814
1802
  }
1815
- dataEntryLSB(channelNumber, value) {
1803
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1816
1804
  this.channels[channelNumber].dataLSB = value;
1817
- this.handleRPN(channelNumber);
1805
+ this.handleRPN(channelNumber, scheduleTime);
1818
1806
  }
1819
- updateChannelVolume(channel) {
1820
- const now = this.audioContext.currentTime;
1807
+ updateChannelVolume(channel, scheduleTime) {
1821
1808
  const state = channel.state;
1822
1809
  const volume = state.volume * state.expression;
1823
1810
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1824
1811
  channel.gainL.gain
1825
- .cancelScheduledValues(now)
1826
- .setValueAtTime(volume * gainLeft, now);
1812
+ .cancelScheduledValues(scheduleTime)
1813
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1827
1814
  channel.gainR.gain
1828
- .cancelScheduledValues(now)
1829
- .setValueAtTime(volume * gainRight, now);
1815
+ .cancelScheduledValues(scheduleTime)
1816
+ .setValueAtTime(volume * gainRight, scheduleTime);
1830
1817
  }
1831
- setSustainPedal(channelNumber, value) {
1818
+ setSustainPedal(channelNumber, value, scheduleTime) {
1819
+ scheduleTime ??= this.audioContext.currentTime;
1832
1820
  this.channels[channelNumber].state.sustainPedal = value / 127;
1833
1821
  if (value < 64) {
1834
- this.releaseSustainPedal(channelNumber, value);
1822
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1835
1823
  }
1836
1824
  }
1837
1825
  setPortamento(channelNumber, value) {
1838
1826
  this.channels[channelNumber].state.portamento = value / 127;
1839
1827
  }
1840
- setSostenutoPedal(channelNumber, value) {
1828
+ setSostenutoPedal(channelNumber, value, scheduleTime) {
1841
1829
  const channel = this.channels[channelNumber];
1842
1830
  channel.state.sostenutoPedal = value / 127;
1843
1831
  if (64 <= value) {
1844
- const now = this.audioContext.currentTime;
1845
- channel.sostenutoNotes = this.getActiveNotes(channel, now);
1832
+ channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
1846
1833
  }
1847
1834
  else {
1848
1835
  this.releaseSostenutoPedal(channelNumber, value);
1849
1836
  }
1850
1837
  }
1851
- setSoftPedal(channelNumber, softPedal) {
1838
+ setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1852
1839
  const channel = this.channels[channelNumber];
1853
1840
  channel.state.softPedal = softPedal / 127;
1854
1841
  }
1855
- setReverbSendLevel(channelNumber, reverbSendLevel) {
1842
+ setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
1856
1843
  const channel = this.channels[channelNumber];
1857
1844
  const state = channel.state;
1858
1845
  const reverbEffect = this.reverbEffect;
1859
1846
  if (0 < state.reverbSendLevel) {
1860
1847
  if (0 < reverbSendLevel) {
1861
- const now = this.audioContext.currentTime;
1862
1848
  state.reverbSendLevel = reverbSendLevel / 127;
1863
- reverbEffect.input.gain.cancelScheduledValues(now);
1864
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1849
+ reverbEffect.input.gain
1850
+ .cancelScheduledValues(scheduleTime)
1851
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
1865
1852
  }
1866
1853
  else {
1867
1854
  channel.scheduledNotes.forEach((noteList) => {
@@ -1878,31 +1865,31 @@ class MidyGM2 {
1878
1865
  }
1879
1866
  else {
1880
1867
  if (0 < reverbSendLevel) {
1881
- const now = this.audioContext.currentTime;
1882
1868
  channel.scheduledNotes.forEach((noteList) => {
1883
1869
  for (let i = 0; i < noteList.length; i++) {
1884
1870
  const note = noteList[i];
1885
1871
  if (!note)
1886
1872
  continue;
1887
- this.setReverbEffectsSend(channel, note, 0);
1873
+ this.setReverbEffectsSend(channel, note, 0, scheduleTime);
1888
1874
  }
1889
1875
  });
1890
1876
  state.reverbSendLevel = reverbSendLevel / 127;
1891
- reverbEffect.input.gain.cancelScheduledValues(now);
1892
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1877
+ reverbEffect.input.gain
1878
+ .cancelScheduledValues(scheduleTime)
1879
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
1893
1880
  }
1894
1881
  }
1895
1882
  }
1896
- setChorusSendLevel(channelNumber, chorusSendLevel) {
1883
+ setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
1897
1884
  const channel = this.channels[channelNumber];
1898
1885
  const state = channel.state;
1899
1886
  const chorusEffect = this.chorusEffect;
1900
1887
  if (0 < state.chorusSendLevel) {
1901
1888
  if (0 < chorusSendLevel) {
1902
- const now = this.audioContext.currentTime;
1903
1889
  state.chorusSendLevel = chorusSendLevel / 127;
1904
- chorusEffect.input.gain.cancelScheduledValues(now);
1905
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1890
+ chorusEffect.input.gain
1891
+ .cancelScheduledValues(scheduleTime)
1892
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
1906
1893
  }
1907
1894
  else {
1908
1895
  channel.scheduledNotes.forEach((noteList) => {
@@ -1919,18 +1906,18 @@ class MidyGM2 {
1919
1906
  }
1920
1907
  else {
1921
1908
  if (0 < chorusSendLevel) {
1922
- const now = this.audioContext.currentTime;
1923
1909
  channel.scheduledNotes.forEach((noteList) => {
1924
1910
  for (let i = 0; i < noteList.length; i++) {
1925
1911
  const note = noteList[i];
1926
1912
  if (!note)
1927
1913
  continue;
1928
- this.setChorusEffectsSend(channel, note, 0);
1914
+ this.setChorusEffectsSend(channel, note, 0, scheduleTime);
1929
1915
  }
1930
1916
  });
1931
1917
  state.chorusSendLevel = chorusSendLevel / 127;
1932
- chorusEffect.input.gain.cancelScheduledValues(now);
1933
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1918
+ chorusEffect.input.gain
1919
+ .cancelScheduledValues(scheduleTime)
1920
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
1934
1921
  }
1935
1922
  }
1936
1923
  }
@@ -1960,12 +1947,12 @@ class MidyGM2 {
1960
1947
  channel.dataMSB = minMSB;
1961
1948
  }
1962
1949
  }
1963
- handleRPN(channelNumber) {
1950
+ handleRPN(channelNumber, scheduleTime) {
1964
1951
  const channel = this.channels[channelNumber];
1965
1952
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1966
1953
  switch (rpn) {
1967
1954
  case 0:
1968
- this.handlePitchBendRangeRPN(channelNumber);
1955
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1969
1956
  break;
1970
1957
  case 1:
1971
1958
  this.handleFineTuningRPN(channelNumber);
@@ -1986,25 +1973,26 @@ class MidyGM2 {
1986
1973
  setRPNLSB(channelNumber, value) {
1987
1974
  this.channels[channelNumber].rpnLSB = value;
1988
1975
  }
1989
- dataEntryMSB(channelNumber, value) {
1976
+ dataEntryMSB(channelNumber, value, scheduleTime) {
1990
1977
  this.channels[channelNumber].dataMSB = value;
1991
- this.handleRPN(channelNumber);
1978
+ this.handleRPN(channelNumber, scheduleTime);
1992
1979
  }
1993
- handlePitchBendRangeRPN(channelNumber) {
1980
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
1994
1981
  const channel = this.channels[channelNumber];
1995
1982
  this.limitData(channel, 0, 127, 0, 99);
1996
1983
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1997
- this.setPitchBendRange(channelNumber, pitchBendRange);
1984
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1998
1985
  }
1999
- setPitchBendRange(channelNumber, value) {
1986
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1987
+ scheduleTime ??= this.audioContext.currentTime;
2000
1988
  const channel = this.channels[channelNumber];
2001
1989
  const state = channel.state;
2002
1990
  const prev = state.pitchWheelSensitivity;
2003
1991
  const next = value / 128;
2004
1992
  state.pitchWheelSensitivity = next;
2005
1993
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2006
- this.updateChannelDetune(channel);
2007
- this.applyVoiceParams(channel, 16);
1994
+ this.updateChannelDetune(channel, scheduleTime);
1995
+ this.applyVoiceParams(channel, 16, scheduleTime);
2008
1996
  }
2009
1997
  handleFineTuningRPN(channelNumber) {
2010
1998
  const channel = this.channels[channelNumber];
@@ -2045,8 +2033,9 @@ class MidyGM2 {
2045
2033
  channel.modulationDepthRange = modulationDepthRange;
2046
2034
  this.updateModulation(channel);
2047
2035
  }
2048
- allSoundOff(channelNumber) {
2049
- return this.stopChannelNotes(channelNumber, 0, true);
2036
+ allSoundOff(channelNumber, _value, scheduleTime) {
2037
+ scheduleTime ??= this.audioContext.currentTime;
2038
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2050
2039
  }
2051
2040
  resetAllControllers(channelNumber) {
2052
2041
  const stateTypes = [
@@ -2074,8 +2063,9 @@ class MidyGM2 {
2074
2063
  channel[type] = this.constructor.channelSettings[type];
2075
2064
  }
2076
2065
  }
2077
- allNotesOff(channelNumber) {
2078
- return this.stopChannelNotes(channelNumber, 0, false);
2066
+ allNotesOff(channelNumber, _value, scheduleTime) {
2067
+ scheduleTime ??= this.audioContext.currentTime;
2068
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2079
2069
  }
2080
2070
  omniOff() {
2081
2071
  this.omni = false;
@@ -2089,13 +2079,13 @@ class MidyGM2 {
2089
2079
  polyOn() {
2090
2080
  this.mono = false;
2091
2081
  }
2092
- handleUniversalNonRealTimeExclusiveMessage(data) {
2082
+ handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
2093
2083
  switch (data[2]) {
2094
2084
  case 8:
2095
2085
  switch (data[3]) {
2096
2086
  case 8:
2097
2087
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2098
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2088
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false, scheduleTime);
2099
2089
  default:
2100
2090
  console.warn(`Unsupported Exclusive Message: ${data}`);
2101
2091
  }
@@ -2138,18 +2128,18 @@ class MidyGM2 {
2138
2128
  this.channels[9].bankMSB = 120;
2139
2129
  this.channels[9].bank = 120 * 128;
2140
2130
  }
2141
- handleUniversalRealTimeExclusiveMessage(data) {
2131
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2142
2132
  switch (data[2]) {
2143
2133
  case 4:
2144
2134
  switch (data[3]) {
2145
2135
  case 1:
2146
- return this.handleMasterVolumeSysEx(data);
2136
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
2147
2137
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2148
- return this.handleMasterFineTuningSysEx(data);
2138
+ return this.handleMasterFineTuningSysEx(data, scheduleTime);
2149
2139
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2150
- return this.handleMasterCoarseTuningSysEx(data);
2140
+ return this.handleMasterCoarseTuningSysEx(data, scheduleTime);
2151
2141
  case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2152
- return this.handleGlobalParameterControlSysEx(data);
2142
+ return this.handleGlobalParameterControlSysEx(data, scheduleTime);
2153
2143
  default:
2154
2144
  console.warn(`Unsupported Exclusive Message: ${data}`);
2155
2145
  }
@@ -2167,7 +2157,7 @@ class MidyGM2 {
2167
2157
  case 10:
2168
2158
  switch (data[3]) {
2169
2159
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2170
- return this.handleKeyBasedInstrumentControlSysEx(data);
2160
+ return this.handleKeyBasedInstrumentControlSysEx(data, scheduleTime);
2171
2161
  default:
2172
2162
  console.warn(`Unsupported Exclusive Message: ${data}`);
2173
2163
  }
@@ -2176,49 +2166,50 @@ class MidyGM2 {
2176
2166
  console.warn(`Unsupported Exclusive Message: ${data}`);
2177
2167
  }
2178
2168
  }
2179
- handleMasterVolumeSysEx(data) {
2169
+ handleMasterVolumeSysEx(data, scheduleTime) {
2180
2170
  const volume = (data[5] * 128 + data[4]) / 16383;
2181
- this.setMasterVolume(volume);
2171
+ this.setMasterVolume(volume, scheduleTime);
2182
2172
  }
2183
- setMasterVolume(volume) {
2173
+ setMasterVolume(volume, scheduleTime) {
2174
+ scheduleTime ??= this.audioContext.currentTime;
2184
2175
  if (volume < 0 && 1 < volume) {
2185
2176
  console.error("Master Volume is out of range");
2186
2177
  }
2187
2178
  else {
2188
- const now = this.audioContext.currentTime;
2189
- this.masterVolume.gain.cancelScheduledValues(now);
2190
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
2179
+ this.masterVolume.gain
2180
+ .cancelScheduledValues(scheduleTime)
2181
+ .setValueAtTime(volume * volume, scheduleTime);
2191
2182
  }
2192
2183
  }
2193
- handleMasterFineTuningSysEx(data) {
2184
+ handleMasterFineTuningSysEx(data, scheduleTime) {
2194
2185
  const fineTuning = data[5] * 128 + data[4];
2195
- this.setMasterFineTuning(fineTuning);
2186
+ this.setMasterFineTuning(fineTuning, scheduleTime);
2196
2187
  }
2197
- setMasterFineTuning(value) {
2188
+ setMasterFineTuning(value, scheduleTime) {
2198
2189
  const prev = this.masterFineTuning;
2199
2190
  const next = (value - 8192) / 8.192; // cent
2200
2191
  this.masterFineTuning = next;
2201
2192
  channel.detune += next - prev;
2202
- this.updateChannelDetune(channel);
2193
+ this.updateChannelDetune(channel, scheduleTime);
2203
2194
  }
2204
- handleMasterCoarseTuningSysEx(data) {
2195
+ handleMasterCoarseTuningSysEx(data, scheduleTime) {
2205
2196
  const coarseTuning = data[4];
2206
- this.setMasterCoarseTuning(coarseTuning);
2197
+ this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2207
2198
  }
2208
- setMasterCoarseTuning(value) {
2199
+ setMasterCoarseTuning(value, scheduleTime) {
2209
2200
  const prev = this.masterCoarseTuning;
2210
2201
  const next = (value - 64) * 100; // cent
2211
2202
  this.masterCoarseTuning = next;
2212
2203
  channel.detune += next - prev;
2213
- this.updateChannelDetune(channel);
2204
+ this.updateChannelDetune(channel, scheduleTime);
2214
2205
  }
2215
- handleGlobalParameterControlSysEx(data) {
2206
+ handleGlobalParameterControlSysEx(data, scheduleTime) {
2216
2207
  if (data[7] === 1) {
2217
2208
  switch (data[8]) {
2218
2209
  case 1:
2219
2210
  return this.handleReverbParameterSysEx(data);
2220
2211
  case 2:
2221
- return this.handleChorusParameterSysEx(data);
2212
+ return this.handleChorusParameterSysEx(data, scheduleTime);
2222
2213
  default:
2223
2214
  console.warn(`Unsupported Global Parameter Control Message: ${data}`);
2224
2215
  }
@@ -2297,88 +2288,84 @@ class MidyGM2 {
2297
2288
  calcDelay(rt60, feedback) {
2298
2289
  return -rt60 * Math.log10(feedback) / 3;
2299
2290
  }
2300
- handleChorusParameterSysEx(data) {
2291
+ handleChorusParameterSysEx(data, scheduleTime) {
2301
2292
  switch (data[9]) {
2302
2293
  case 0:
2303
- return this.setChorusType(data[10]);
2294
+ return this.setChorusType(data[10], scheduleTime);
2304
2295
  case 1:
2305
- return this.setChorusModRate(data[10]);
2296
+ return this.setChorusModRate(data[10], scheduleTime);
2306
2297
  case 2:
2307
- return this.setChorusModDepth(data[10]);
2298
+ return this.setChorusModDepth(data[10], scheduleTime);
2308
2299
  case 3:
2309
- return this.setChorusFeedback(data[10]);
2300
+ return this.setChorusFeedback(data[10], scheduleTime);
2310
2301
  case 4:
2311
- return this.setChorusSendToReverb(data[10]);
2302
+ return this.setChorusSendToReverb(data[10], scheduleTime);
2312
2303
  }
2313
2304
  }
2314
- setChorusType(type) {
2305
+ setChorusType(type, scheduleTime) {
2315
2306
  switch (type) {
2316
2307
  case 0:
2317
- return this.setChorusParameter(3, 5, 0, 0);
2308
+ return this.setChorusParameter(3, 5, 0, 0, scheduleTime);
2318
2309
  case 1:
2319
- return this.setChorusParameter(9, 19, 5, 0);
2310
+ return this.setChorusParameter(9, 19, 5, 0, scheduleTime);
2320
2311
  case 2:
2321
- return this.setChorusParameter(3, 19, 8, 0);
2312
+ return this.setChorusParameter(3, 19, 8, 0, scheduleTime);
2322
2313
  case 3:
2323
- return this.setChorusParameter(9, 16, 16, 0);
2314
+ return this.setChorusParameter(9, 16, 16, 0, scheduleTime);
2324
2315
  case 4:
2325
- return this.setChorusParameter(2, 24, 64, 0);
2316
+ return this.setChorusParameter(2, 24, 64, 0, scheduleTime);
2326
2317
  case 5:
2327
- return this.setChorusParameter(1, 5, 112, 0);
2318
+ return this.setChorusParameter(1, 5, 112, 0, scheduleTime);
2328
2319
  default:
2329
2320
  console.warn(`Unsupported Chorus Type: ${type}`);
2330
2321
  }
2331
2322
  }
2332
- setChorusParameter(modRate, modDepth, feedback, sendToReverb) {
2333
- this.setChorusModRate(modRate);
2334
- this.setChorusModDepth(modDepth);
2335
- this.setChorusFeedback(feedback);
2336
- this.setChorusSendToReverb(sendToReverb);
2323
+ setChorusParameter(modRate, modDepth, feedback, sendToReverb, scheduleTime) {
2324
+ this.setChorusModRate(modRate, scheduleTime);
2325
+ this.setChorusModDepth(modDepth, scheduleTime);
2326
+ this.setChorusFeedback(feedback, scheduleTime);
2327
+ this.setChorusSendToReverb(sendToReverb, scheduleTime);
2337
2328
  }
2338
- setChorusModRate(value) {
2339
- const now = this.audioContext.currentTime;
2329
+ setChorusModRate(value, scheduleTime) {
2340
2330
  const modRate = this.getChorusModRate(value);
2341
2331
  this.chorus.modRate = modRate;
2342
- this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
2332
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, scheduleTime);
2343
2333
  }
2344
2334
  getChorusModRate(value) {
2345
2335
  return value * 0.122; // Hz
2346
2336
  }
2347
- setChorusModDepth(value) {
2348
- const now = this.audioContext.currentTime;
2337
+ setChorusModDepth(value, scheduleTime) {
2349
2338
  const modDepth = this.getChorusModDepth(value);
2350
2339
  this.chorus.modDepth = modDepth;
2351
2340
  this.chorusEffect.lfoGain.gain
2352
- .cancelScheduledValues(now)
2353
- .setValueAtTime(modDepth / 2, now);
2341
+ .cancelScheduledValues(scheduleTime)
2342
+ .setValueAtTime(modDepth / 2, scheduleTime);
2354
2343
  }
2355
2344
  getChorusModDepth(value) {
2356
2345
  return (value + 1) / 3200; // second
2357
2346
  }
2358
- setChorusFeedback(value) {
2359
- const now = this.audioContext.currentTime;
2347
+ setChorusFeedback(value, scheduleTime) {
2360
2348
  const feedback = this.getChorusFeedback(value);
2361
2349
  this.chorus.feedback = feedback;
2362
2350
  const chorusEffect = this.chorusEffect;
2363
2351
  for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
2364
2352
  chorusEffect.feedbackGains[i].gain
2365
- .cancelScheduledValues(now)
2366
- .setValueAtTime(feedback, now);
2353
+ .cancelScheduledValues(scheduleTime)
2354
+ .setValueAtTime(feedback, scheduleTime);
2367
2355
  }
2368
2356
  }
2369
2357
  getChorusFeedback(value) {
2370
2358
  return value * 0.00763;
2371
2359
  }
2372
- setChorusSendToReverb(value) {
2360
+ setChorusSendToReverb(value, scheduleTime) {
2373
2361
  const sendToReverb = this.getChorusSendToReverb(value);
2374
2362
  const sendGain = this.chorusEffect.sendGain;
2375
2363
  if (0 < this.chorus.sendToReverb) {
2376
2364
  this.chorus.sendToReverb = sendToReverb;
2377
2365
  if (0 < sendToReverb) {
2378
- const now = this.audioContext.currentTime;
2379
2366
  sendGain.gain
2380
- .cancelScheduledValues(now)
2381
- .setValueAtTime(sendToReverb, now);
2367
+ .cancelScheduledValues(scheduleTime)
2368
+ .setValueAtTime(sendToReverb, scheduleTime);
2382
2369
  }
2383
2370
  else {
2384
2371
  sendGain.disconnect();
@@ -2387,11 +2374,10 @@ class MidyGM2 {
2387
2374
  else {
2388
2375
  this.chorus.sendToReverb = sendToReverb;
2389
2376
  if (0 < sendToReverb) {
2390
- const now = this.audioContext.currentTime;
2391
2377
  sendGain.connect(this.reverbEffect.input);
2392
2378
  sendGain.gain
2393
- .cancelScheduledValues(now)
2394
- .setValueAtTime(sendToReverb, now);
2379
+ .cancelScheduledValues(scheduleTime)
2380
+ .setValueAtTime(sendToReverb, scheduleTime);
2395
2381
  }
2396
2382
  }
2397
2383
  }
@@ -2417,7 +2403,7 @@ class MidyGM2 {
2417
2403
  }
2418
2404
  return bitmap;
2419
2405
  }
2420
- handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2406
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime, scheduleTime) {
2421
2407
  if (data.length < 19) {
2422
2408
  console.error("Data length is too short");
2423
2409
  return;
@@ -2432,47 +2418,51 @@ class MidyGM2 {
2432
2418
  channel.scaleOctaveTuningTable[j] = centValue;
2433
2419
  }
2434
2420
  if (realtime)
2435
- this.updateChannelDetune(channel);
2421
+ this.updateChannelDetune(channel, scheduleTime);
2436
2422
  }
2437
2423
  }
2438
- applyDestinationSettings(channel, note, table) {
2439
- if (table[0] !== 64) {
2440
- this.updateDetune(channel, note, 0);
2441
- }
2442
- if (!note.portamento) {
2443
- if (table[1] !== 64) {
2444
- const channelPressure = channel.channelPressureTable[1] *
2445
- channel.state.channelPressure;
2446
- const pressure = (channelPressure - 64) * 15;
2447
- this.setFilterEnvelope(channel, note, pressure);
2448
- }
2449
- if (table[2] !== 64) {
2450
- const channelPressure = channel.channelPressureTable[2] *
2451
- channel.state.channelPressure;
2452
- const pressure = channelPressure / 64;
2453
- this.setVolumeEnvelope(note, pressure);
2454
- }
2455
- }
2456
- if (table[3] !== 0) {
2457
- const channelPressure = channel.channelPressureTable[3] *
2458
- channel.state.channelPressure;
2459
- const pressure = channelPressure / 127 * 600;
2460
- this.setModLfoToPitch(channel, note, pressure);
2461
- }
2462
- if (table[4] !== 0) {
2463
- const channelPressure = channel.channelPressureTable[4] *
2464
- channel.state.channelPressure;
2465
- const pressure = channelPressure / 127 * 2400;
2466
- this.setModLfoToFilterFc(note, pressure);
2467
- }
2468
- if (table[5] !== 0) {
2469
- const channelPressure = channel.channelPressureTable[5] *
2470
- channel.state.channelPressure;
2471
- const pressure = channelPressure / 127;
2472
- this.setModLfoToVolume(note, pressure);
2473
- }
2424
+ getFilterCutoffControl(channel) {
2425
+ const channelPressure = (channel.channelPressureTable[1] - 64) *
2426
+ channel.state.channelPressure;
2427
+ return channelPressure * 15;
2474
2428
  }
2475
- handleChannelPressureSysEx(data, tableName) {
2429
+ getAmplitudeControl(channel) {
2430
+ const channelPressure = channel.channelPressureTable[2] *
2431
+ channel.state.channelPressure;
2432
+ return channelPressure / 64;
2433
+ }
2434
+ getLFOPitchDepth(channel) {
2435
+ const channelPressure = channel.channelPressureTable[3] *
2436
+ channel.state.channelPressure;
2437
+ return channelPressure / 127 * 600;
2438
+ }
2439
+ getLFOFilterDepth(channel) {
2440
+ const channelPressure = channel.channelPressureTable[4] *
2441
+ channel.state.channelPressure;
2442
+ return channelPressure / 127 * 2400;
2443
+ }
2444
+ getLFOAmplitudeDepth(channel) {
2445
+ const channelPressure = channel.channelPressureTable[5] *
2446
+ channel.state.channelPressure;
2447
+ return channelPressure / 127;
2448
+ }
2449
+ setControllerParameters(channel, note, table) {
2450
+ if (table[0] !== 64)
2451
+ this.updateDetune(channel, note);
2452
+ if (!note.portamento) {
2453
+ if (table[1] !== 64)
2454
+ this.setFilterEnvelope(channel, note);
2455
+ if (table[2] !== 64)
2456
+ this.setVolumeEnvelope(channel, note);
2457
+ }
2458
+ if (table[3] !== 0)
2459
+ this.setModLfoToPitch(channel, note);
2460
+ if (table[4] !== 0)
2461
+ this.setModLfoToFilterFc(channel, note);
2462
+ if (table[5] !== 0)
2463
+ this.setModLfoToVolume(channel, note);
2464
+ }
2465
+ handlePressureSysEx(data, tableName) {
2476
2466
  const channelNumber = data[4];
2477
2467
  const table = this.channels[channelNumber][tableName];
2478
2468
  for (let i = 5; i < data.length - 1; i += 2) {
@@ -2501,7 +2491,7 @@ class MidyGM2 {
2501
2491
  const note = noteList[i];
2502
2492
  if (!note)
2503
2493
  continue;
2504
- this.applyDestinationSettings(channel, note, table);
2494
+ this.setControllerParameters(channel, note, table);
2505
2495
  }
2506
2496
  });
2507
2497
  }
@@ -2520,7 +2510,7 @@ class MidyGM2 {
2520
2510
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2521
2511
  return (controlValue + 64) / 64;
2522
2512
  }
2523
- handleKeyBasedInstrumentControlSysEx(data) {
2513
+ handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2524
2514
  const channelNumber = data[4];
2525
2515
  const keyNumber = data[5];
2526
2516
  const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
@@ -2530,30 +2520,27 @@ class MidyGM2 {
2530
2520
  const index = keyNumber * 128 + controllerType;
2531
2521
  table[index] = value - 64;
2532
2522
  }
2533
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2534
- }
2535
- handleExclusiveMessage(data) {
2536
- console.warn(`Unsupported Exclusive Message: ${data}`);
2523
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2537
2524
  }
2538
- handleSysEx(data) {
2525
+ handleSysEx(data, scheduleTime) {
2539
2526
  switch (data[0]) {
2540
2527
  case 126:
2541
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
2528
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
2542
2529
  case 127:
2543
- return this.handleUniversalRealTimeExclusiveMessage(data);
2530
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
2544
2531
  default:
2545
- return this.handleExclusiveMessage(data);
2532
+ console.warn(`Unsupported Exclusive Message: ${data}`);
2546
2533
  }
2547
2534
  }
2548
- scheduleTask(callback, startTime) {
2535
+ scheduleTask(callback, scheduleTime) {
2549
2536
  return new Promise((resolve) => {
2550
2537
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
2551
2538
  bufferSource.onended = () => {
2552
2539
  callback();
2553
2540
  resolve();
2554
2541
  };
2555
- bufferSource.start(startTime);
2556
- bufferSource.stop(startTime);
2542
+ bufferSource.start(scheduleTime);
2543
+ bufferSource.stop(scheduleTime);
2557
2544
  });
2558
2545
  }
2559
2546
  }
@@ -2565,9 +2552,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2565
2552
  value: {
2566
2553
  currentBufferSource: null,
2567
2554
  detune: 0,
2568
- scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
2569
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2570
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2571
2555
  program: 0,
2572
2556
  bank: 121 * 128,
2573
2557
  bankMSB: 121,