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