@marmooo/midy 0.2.6 → 0.2.8

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.
@@ -287,18 +287,6 @@ class MidyGM2 {
287
287
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
288
288
  }
289
289
  });
290
- Object.defineProperty(this, "mono", {
291
- enumerable: true,
292
- configurable: true,
293
- writable: true,
294
- value: false
295
- }); // CC#124, CC#125
296
- Object.defineProperty(this, "omni", {
297
- enumerable: true,
298
- configurable: true,
299
- writable: true,
300
- value: false
301
- }); // CC#126, CC#127
302
290
  Object.defineProperty(this, "noteCheckInterval", {
303
291
  enumerable: true,
304
292
  configurable: true,
@@ -500,6 +488,7 @@ class MidyGM2 {
500
488
  controlTable: this.initControlTable(),
501
489
  ...this.setChannelAudioNodes(audioContext),
502
490
  scheduledNotes: new SparseMap(128),
491
+ sustainNotes: [],
503
492
  sostenutoNotes: new SparseMap(128),
504
493
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
505
494
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
@@ -582,14 +571,15 @@ class MidyGM2 {
582
571
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
583
572
  if (portamentoTarget)
584
573
  portamentoTarget.portamento = true;
585
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, portamentoTarget?.noteNumber, false);
574
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
575
+ portamentoTarget?.noteNumber);
586
576
  if (notePromise) {
587
577
  this.notePromises.push(notePromise);
588
578
  }
589
579
  break;
590
580
  }
591
581
  case "controller":
592
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
582
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
593
583
  break;
594
584
  case "programChange":
595
585
  this.handleProgramChange(event.channel, event.programNumber, startTime);
@@ -632,10 +622,11 @@ class MidyGM2 {
632
622
  resolve();
633
623
  return;
634
624
  }
635
- const t = this.audioContext.currentTime + offset;
625
+ const now = this.audioContext.currentTime;
626
+ const t = now + offset;
636
627
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
637
628
  if (this.isPausing) {
638
- await this.stopNotes(0, true);
629
+ await this.stopNotes(0, true, now);
639
630
  this.notePromises = [];
640
631
  resolve();
641
632
  this.isPausing = false;
@@ -643,7 +634,7 @@ class MidyGM2 {
643
634
  return;
644
635
  }
645
636
  else if (this.isStopping) {
646
- await this.stopNotes(0, true);
637
+ await this.stopNotes(0, true, now);
647
638
  this.notePromises = [];
648
639
  this.exclusiveClassMap.clear();
649
640
  this.audioBufferCache.clear();
@@ -653,7 +644,7 @@ class MidyGM2 {
653
644
  return;
654
645
  }
655
646
  else if (this.isSeeking) {
656
- this.stopNotes(0, true);
647
+ this.stopNotes(0, true, now);
657
648
  this.exclusiveClassMap.clear();
658
649
  this.startTime = this.audioContext.currentTime;
659
650
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -662,7 +653,6 @@ class MidyGM2 {
662
653
  await schedulePlayback();
663
654
  }
664
655
  else {
665
- const now = this.audioContext.currentTime;
666
656
  const waitTime = now + this.noteCheckInterval;
667
657
  await this.scheduleTask(() => { }, waitTime);
668
658
  await schedulePlayback();
@@ -782,25 +772,21 @@ class MidyGM2 {
782
772
  }
783
773
  return { instruments, timeline };
784
774
  }
785
- async stopChannelNotes(channelNumber, velocity, force) {
786
- const now = this.audioContext.currentTime;
775
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
787
776
  const channel = this.channels[channelNumber];
788
- channel.scheduledNotes.forEach((noteList) => {
789
- for (let i = 0; i < noteList.length; i++) {
790
- const note = noteList[i];
791
- if (!note)
792
- continue;
793
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
794
- force);
795
- this.notePromises.push(promise);
796
- }
777
+ const promises = [];
778
+ this.processScheduledNotes(channel, (note) => {
779
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
780
+ this.notePromises.push(promise);
781
+ promises.push(promise);
797
782
  });
798
783
  channel.scheduledNotes.clear();
799
- await Promise.all(this.notePromises);
784
+ return Promise.all(promises);
800
785
  }
801
- stopNotes(velocity, force) {
786
+ stopNotes(velocity, force, scheduleTime) {
787
+ const promises = [];
802
788
  for (let i = 0; i < this.channels.length; i++) {
803
- this.stopChannelNotes(i, velocity, force);
789
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
804
790
  }
805
791
  return Promise.all(this.notePromises);
806
792
  }
@@ -848,34 +834,32 @@ class MidyGM2 {
848
834
  const now = this.audioContext.currentTime;
849
835
  return this.resumeTime + now - this.startTime - this.startDelay;
850
836
  }
851
- processScheduledNotes(channel, scheduleTime, callback) {
837
+ processScheduledNotes(channel, callback) {
852
838
  channel.scheduledNotes.forEach((noteList) => {
853
839
  for (let i = 0; i < noteList.length; i++) {
854
840
  const note = noteList[i];
855
841
  if (!note)
856
842
  continue;
857
- if (scheduleTime < note.startTime)
858
- continue;
859
843
  callback(note);
860
844
  }
861
845
  });
862
846
  }
863
- getActiveNotes(channel, time) {
847
+ getActiveNotes(channel, scheduleTime) {
864
848
  const activeNotes = new SparseMap(128);
865
849
  channel.scheduledNotes.forEach((noteList) => {
866
- const activeNote = this.getActiveNote(noteList, time);
850
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
867
851
  if (activeNote) {
868
852
  activeNotes.set(activeNote.noteNumber, activeNote);
869
853
  }
870
854
  });
871
855
  return activeNotes;
872
856
  }
873
- getActiveNote(noteList, time) {
857
+ getActiveNote(noteList, scheduleTime) {
874
858
  for (let i = noteList.length - 1; i >= 0; i--) {
875
859
  const note = noteList[i];
876
860
  if (!note)
877
861
  return;
878
- if (time < note.startTime)
862
+ if (scheduleTime < note.startTime)
879
863
  continue;
880
864
  return (note.ending) ? null : note;
881
865
  }
@@ -1035,43 +1019,35 @@ class MidyGM2 {
1035
1019
  calcNoteDetune(channel, note) {
1036
1020
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1037
1021
  }
1038
- updateChannelDetune(channel) {
1039
- channel.scheduledNotes.forEach((noteList) => {
1040
- for (let i = 0; i < noteList.length; i++) {
1041
- const note = noteList[i];
1042
- if (!note)
1043
- continue;
1044
- this.updateDetune(channel, note);
1045
- }
1022
+ updateChannelDetune(channel, scheduleTime) {
1023
+ this.processScheduledNotes(channel, (note) => {
1024
+ this.updateDetune(channel, note, scheduleTime);
1046
1025
  });
1047
1026
  }
1048
- updateDetune(channel, note) {
1049
- const now = this.audioContext.currentTime;
1027
+ updateDetune(channel, note, scheduleTime) {
1050
1028
  const noteDetune = this.calcNoteDetune(channel, note);
1051
1029
  const detune = channel.detune + noteDetune;
1052
1030
  note.bufferSource.detune
1053
- .cancelScheduledValues(now)
1054
- .setValueAtTime(detune, now);
1031
+ .cancelScheduledValues(scheduleTime)
1032
+ .setValueAtTime(detune, scheduleTime);
1055
1033
  }
1056
1034
  getPortamentoTime(channel) {
1057
1035
  const factor = 5 * Math.log(10) / 127;
1058
1036
  const time = channel.state.portamentoTime;
1059
1037
  return Math.log(time) / factor;
1060
1038
  }
1061
- setPortamentoStartVolumeEnvelope(channel, note) {
1062
- const now = this.audioContext.currentTime;
1039
+ setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1063
1040
  const { voiceParams, startTime } = note;
1064
1041
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1065
1042
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1066
1043
  const volDelay = startTime + voiceParams.volDelay;
1067
1044
  const portamentoTime = volDelay + this.getPortamentoTime(channel);
1068
1045
  note.volumeEnvelopeNode.gain
1069
- .cancelScheduledValues(now)
1046
+ .cancelScheduledValues(scheduleTime)
1070
1047
  .setValueAtTime(0, volDelay)
1071
1048
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
1072
1049
  }
1073
- setVolumeEnvelope(channel, note) {
1074
- const now = this.audioContext.currentTime;
1050
+ setVolumeEnvelope(channel, note, scheduleTime) {
1075
1051
  const { voiceParams, startTime } = note;
1076
1052
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1077
1053
  (1 + this.getAmplitudeControl(channel));
@@ -1081,7 +1057,7 @@ class MidyGM2 {
1081
1057
  const volHold = volAttack + voiceParams.volHold;
1082
1058
  const volDecay = volHold + voiceParams.volDecay;
1083
1059
  note.volumeEnvelopeNode.gain
1084
- .cancelScheduledValues(now)
1060
+ .cancelScheduledValues(scheduleTime)
1085
1061
  .setValueAtTime(0, startTime)
1086
1062
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
1087
1063
  .exponentialRampToValueAtTime(attackVolume, volAttack)
@@ -1089,7 +1065,6 @@ class MidyGM2 {
1089
1065
  .linearRampToValueAtTime(sustainVolume, volDecay);
1090
1066
  }
1091
1067
  setPitchEnvelope(note, scheduleTime) {
1092
- scheduleTime ??= this.audioContext.currentTime;
1093
1068
  const { voiceParams } = note;
1094
1069
  const baseRate = voiceParams.playbackRate;
1095
1070
  note.bufferSource.playbackRate
@@ -1116,8 +1091,7 @@ class MidyGM2 {
1116
1091
  const maxFrequency = 20000; // max Hz of initialFilterFc
1117
1092
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1118
1093
  }
1119
- setPortamentoStartFilterEnvelope(channel, note) {
1120
- const now = this.audioContext.currentTime;
1094
+ setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1121
1095
  const state = channel.state;
1122
1096
  const { voiceParams, noteNumber, startTime } = note;
1123
1097
  const softPedalFactor = 1 -
@@ -1132,13 +1106,12 @@ class MidyGM2 {
1132
1106
  const portamentoTime = startTime + this.getPortamentoTime(channel);
1133
1107
  const modDelay = startTime + voiceParams.modDelay;
1134
1108
  note.filterNode.frequency
1135
- .cancelScheduledValues(now)
1109
+ .cancelScheduledValues(scheduleTime)
1136
1110
  .setValueAtTime(adjustedBaseFreq, startTime)
1137
1111
  .setValueAtTime(adjustedBaseFreq, modDelay)
1138
1112
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1139
1113
  }
1140
- setFilterEnvelope(channel, note) {
1141
- const now = this.audioContext.currentTime;
1114
+ setFilterEnvelope(channel, note, scheduleTime) {
1142
1115
  const state = channel.state;
1143
1116
  const { voiceParams, noteNumber, startTime } = note;
1144
1117
  const softPedalFactor = 1 -
@@ -1158,14 +1131,14 @@ class MidyGM2 {
1158
1131
  const modHold = modAttack + voiceParams.modHold;
1159
1132
  const modDecay = modHold + voiceParams.modDecay;
1160
1133
  note.filterNode.frequency
1161
- .cancelScheduledValues(now)
1134
+ .cancelScheduledValues(scheduleTime)
1162
1135
  .setValueAtTime(adjustedBaseFreq, startTime)
1163
1136
  .setValueAtTime(adjustedBaseFreq, modDelay)
1164
1137
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1165
1138
  .setValueAtTime(adjustedPeekFreq, modHold)
1166
1139
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
1167
1140
  }
1168
- startModulation(channel, note, startTime) {
1141
+ startModulation(channel, note, scheduleTime) {
1169
1142
  const { voiceParams } = note;
1170
1143
  note.modulationLFO = new OscillatorNode(this.audioContext, {
1171
1144
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -1174,10 +1147,10 @@ class MidyGM2 {
1174
1147
  gain: voiceParams.modLfoToFilterFc,
1175
1148
  });
1176
1149
  note.modulationDepth = new GainNode(this.audioContext);
1177
- this.setModLfoToPitch(channel, note);
1150
+ this.setModLfoToPitch(channel, note, scheduleTime);
1178
1151
  note.volumeDepth = new GainNode(this.audioContext);
1179
- this.setModLfoToVolume(channel, note);
1180
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1152
+ this.setModLfoToVolume(note, scheduleTime);
1153
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1181
1154
  note.modulationLFO.connect(note.filterDepth);
1182
1155
  note.filterDepth.connect(note.filterNode.frequency);
1183
1156
  note.modulationLFO.connect(note.modulationDepth);
@@ -1185,15 +1158,15 @@ class MidyGM2 {
1185
1158
  note.modulationLFO.connect(note.volumeDepth);
1186
1159
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1187
1160
  }
1188
- startVibrato(channel, note, startTime) {
1161
+ startVibrato(channel, note, scheduleTime) {
1189
1162
  const { voiceParams } = note;
1190
1163
  const state = channel.state;
1191
1164
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1192
1165
  frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1193
1166
  });
1194
- note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1167
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1195
1168
  note.vibratoDepth = new GainNode(this.audioContext);
1196
- this.setVibLfoToPitch(channel, note);
1169
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1197
1170
  note.vibratoLFO.connect(note.vibratoDepth);
1198
1171
  note.vibratoDepth.connect(note.bufferSource.detune);
1199
1172
  }
@@ -1216,6 +1189,7 @@ class MidyGM2 {
1216
1189
  }
1217
1190
  }
1218
1191
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1192
+ const now = this.audioContext.currentTime;
1219
1193
  const state = channel.state;
1220
1194
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1221
1195
  const voiceParams = voice.getAllParams(controllerState);
@@ -1232,22 +1206,22 @@ class MidyGM2 {
1232
1206
  });
1233
1207
  if (portamento) {
1234
1208
  note.portamento = true;
1235
- this.setPortamentoStartVolumeEnvelope(channel, note);
1236
- this.setPortamentoStartFilterEnvelope(channel, note);
1209
+ this.setPortamentoStartVolumeEnvelope(channel, note, now);
1210
+ this.setPortamentoStartFilterEnvelope(channel, note, now);
1237
1211
  }
1238
1212
  else {
1239
1213
  note.portamento = false;
1240
- this.setVolumeEnvelope(channel, note);
1241
- this.setFilterEnvelope(channel, note);
1214
+ this.setVolumeEnvelope(channel, note, now);
1215
+ this.setFilterEnvelope(channel, note, now);
1242
1216
  }
1243
1217
  if (0 < state.vibratoDepth) {
1244
- this.startVibrato(channel, note, startTime);
1218
+ this.startVibrato(channel, note, now);
1245
1219
  }
1246
- this.setPitchEnvelope(note);
1220
+ this.setPitchEnvelope(note, now);
1247
1221
  if (0 < state.modulationDepth) {
1248
- this.startModulation(channel, note, startTime);
1222
+ this.startModulation(channel, note, now);
1249
1223
  }
1250
- if (this.mono && channel.currentBufferSource) {
1224
+ if (channel.mono && channel.currentBufferSource) {
1251
1225
  channel.currentBufferSource.stop(startTime);
1252
1226
  channel.currentBufferSource = note.bufferSource;
1253
1227
  }
@@ -1257,10 +1231,10 @@ class MidyGM2 {
1257
1231
  note.volumeNode.connect(note.gainL);
1258
1232
  note.volumeNode.connect(note.gainR);
1259
1233
  if (0 < channel.chorusSendLevel) {
1260
- this.setChorusEffectsSend(channel, note, 0);
1234
+ this.setChorusEffectsSend(channel, note, 0, now);
1261
1235
  }
1262
1236
  if (0 < channel.reverbSendLevel) {
1263
- this.setReverbEffectsSend(channel, note, 0);
1237
+ this.setReverbEffectsSend(channel, note, 0, now);
1264
1238
  }
1265
1239
  note.bufferSource.start(startTime);
1266
1240
  return note;
@@ -1288,8 +1262,8 @@ class MidyGM2 {
1288
1262
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1289
1263
  note.gainL.connect(channel.gainL);
1290
1264
  note.gainR.connect(channel.gainR);
1291
- if (channel.state.sostenutoPedal) {
1292
- channel.sostenutoNotes.set(noteNumber, note);
1265
+ if (0.5 <= channel.state.sustainPedal) {
1266
+ channel.sustainNotes.push(note);
1293
1267
  }
1294
1268
  const exclusiveClass = note.voiceParams.exclusiveClass;
1295
1269
  if (exclusiveClass !== 0) {
@@ -1297,9 +1271,9 @@ class MidyGM2 {
1297
1271
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1298
1272
  const [prevNote, prevChannelNumber] = prevEntry;
1299
1273
  if (!prevNote.ending) {
1300
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1301
- startTime, undefined, // portamentoNoteNumber
1302
- true);
1274
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1275
+ startTime, true, // force
1276
+ undefined);
1303
1277
  }
1304
1278
  }
1305
1279
  this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
@@ -1312,9 +1286,9 @@ class MidyGM2 {
1312
1286
  scheduledNotes.set(noteNumber, [note]);
1313
1287
  }
1314
1288
  }
1315
- noteOn(channelNumber, noteNumber, velocity, portamento) {
1316
- const now = this.audioContext.currentTime;
1317
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1289
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1290
+ scheduleTime ??= this.audioContext.currentTime;
1291
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1318
1292
  }
1319
1293
  stopNote(endTime, stopTime, scheduledNotes, index) {
1320
1294
  const note = scheduledNotes[index];
@@ -1354,11 +1328,11 @@ class MidyGM2 {
1354
1328
  note.bufferSource.stop(stopTime);
1355
1329
  });
1356
1330
  }
1357
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
1331
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1358
1332
  const channel = this.channels[channelNumber];
1359
1333
  const state = channel.state;
1360
1334
  if (!force) {
1361
- if (0.5 < state.sustainPedal)
1335
+ if (0.5 <= state.sustainPedal)
1362
1336
  return;
1363
1337
  if (channel.sostenutoNotes.has(noteNumber))
1364
1338
  return;
@@ -1393,68 +1367,60 @@ class MidyGM2 {
1393
1367
  }
1394
1368
  }
1395
1369
  }
1396
- releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1397
- const now = this.audioContext.currentTime;
1398
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1370
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1371
+ scheduleTime ??= this.audioContext.currentTime;
1372
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1373
+ undefined);
1399
1374
  }
1400
- releaseSustainPedal(channelNumber, halfVelocity) {
1375
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1401
1376
  const velocity = halfVelocity * 2;
1402
1377
  const channel = this.channels[channelNumber];
1403
1378
  const promises = [];
1404
- channel.state.sustainPedal = halfVelocity;
1405
- channel.scheduledNotes.forEach((noteList) => {
1406
- for (let i = 0; i < noteList.length; i++) {
1407
- const note = noteList[i];
1408
- if (!note)
1409
- continue;
1410
- const { noteNumber } = note;
1411
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1412
- promises.push(promise);
1413
- }
1414
- });
1379
+ for (let i = 0; i < channel.sustainNotes.length; i++) {
1380
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1381
+ promises.push(promise);
1382
+ }
1383
+ channel.sustainNotes = [];
1415
1384
  return promises;
1416
1385
  }
1417
- releaseSostenutoPedal(channelNumber, halfVelocity) {
1386
+ releaseSostenutoPedal(channelNumber, halfVelocity, scheduleTime) {
1418
1387
  const velocity = halfVelocity * 2;
1419
1388
  const channel = this.channels[channelNumber];
1420
1389
  const promises = [];
1421
1390
  channel.state.sostenutoPedal = 0;
1422
- channel.sostenutoNotes.forEach((activeNote) => {
1423
- const { noteNumber } = activeNote;
1424
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1391
+ channel.sostenutoNotes.forEach((note) => {
1392
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1425
1393
  promises.push(promise);
1426
1394
  });
1427
1395
  channel.sostenutoNotes.clear();
1428
1396
  return promises;
1429
1397
  }
1430
- handleMIDIMessage(statusByte, data1, data2) {
1431
- const channelNumber = omni ? 0 : statusByte & 0x0F;
1398
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1399
+ const channelNumber = statusByte & 0x0F;
1432
1400
  const messageType = statusByte & 0xF0;
1433
1401
  switch (messageType) {
1434
1402
  case 0x80:
1435
- return this.releaseNote(channelNumber, data1, data2);
1403
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
1436
1404
  case 0x90:
1437
- return this.noteOn(channelNumber, data1, data2);
1405
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
1438
1406
  case 0xB0:
1439
- return this.handleControlChange(channelNumber, data1, data2);
1407
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1440
1408
  case 0xC0:
1441
- return this.handleProgramChange(channelNumber, data1);
1409
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
1442
1410
  case 0xD0:
1443
- return this.handleChannelPressure(channelNumber, data1);
1411
+ return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1444
1412
  case 0xE0:
1445
- return this.handlePitchBendMessage(channelNumber, data1, data2);
1413
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1446
1414
  default:
1447
1415
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1448
1416
  }
1449
1417
  }
1450
- handleProgramChange(channelNumber, program) {
1418
+ handleProgramChange(channelNumber, program, _scheduleTime) {
1451
1419
  const channel = this.channels[channelNumber];
1452
1420
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1453
1421
  channel.program = program;
1454
1422
  }
1455
- handleChannelPressure(channelNumber, value, startTime) {
1456
- if (!startTime)
1457
- startTime = this.audioContext.currentTime;
1423
+ handleChannelPressure(channelNumber, value, scheduleTime) {
1458
1424
  const channel = this.channels[channelNumber];
1459
1425
  const prev = channel.state.channelPressure;
1460
1426
  const next = value / 127;
@@ -1464,72 +1430,68 @@ class MidyGM2 {
1464
1430
  channel.detune += pressureDepth * (next - prev);
1465
1431
  }
1466
1432
  const table = channel.channelPressureTable;
1467
- this.getActiveNotes(channel, startTime).forEach((note) => {
1433
+ this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1468
1434
  this.setControllerParameters(channel, note, table);
1469
1435
  });
1470
1436
  // this.applyVoiceParams(channel, 13);
1471
1437
  }
1472
- handlePitchBendMessage(channelNumber, lsb, msb) {
1438
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1473
1439
  const pitchBend = msb * 128 + lsb;
1474
- this.setPitchBend(channelNumber, pitchBend);
1440
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1475
1441
  }
1476
- setPitchBend(channelNumber, value) {
1442
+ setPitchBend(channelNumber, value, scheduleTime) {
1443
+ scheduleTime ??= this.audioContext.currentTime;
1477
1444
  const channel = this.channels[channelNumber];
1478
1445
  const state = channel.state;
1479
1446
  const prev = state.pitchWheel * 2 - 1;
1480
1447
  const next = (value - 8192) / 8192;
1481
1448
  state.pitchWheel = value / 16383;
1482
1449
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1483
- this.updateChannelDetune(channel);
1484
- this.applyVoiceParams(channel, 14);
1450
+ this.updateChannelDetune(channel, scheduleTime);
1451
+ this.applyVoiceParams(channel, 14, scheduleTime);
1485
1452
  }
1486
- setModLfoToPitch(channel, note) {
1487
- const now = this.audioContext.currentTime;
1453
+ setModLfoToPitch(channel, note, scheduleTime) {
1488
1454
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1489
1455
  this.getLFOPitchDepth(channel);
1490
1456
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1491
1457
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1492
1458
  note.modulationDepth.gain
1493
- .cancelScheduledValues(now)
1494
- .setValueAtTime(modulationDepth, now);
1459
+ .cancelScheduledValues(scheduleTime)
1460
+ .setValueAtTime(modulationDepth, scheduleTime);
1495
1461
  }
1496
- setVibLfoToPitch(channel, note) {
1497
- const now = this.audioContext.currentTime;
1462
+ setVibLfoToPitch(channel, note, scheduleTime) {
1498
1463
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1499
1464
  const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1500
1465
  2;
1501
1466
  const vibratoDepthSign = 0 < vibLfoToPitch;
1502
1467
  note.vibratoDepth.gain
1503
- .cancelScheduledValues(now)
1504
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1468
+ .cancelScheduledValues(scheduleTime)
1469
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, scheduleTime);
1505
1470
  }
1506
- setModLfoToFilterFc(channel, note) {
1507
- const now = this.audioContext.currentTime;
1471
+ setModLfoToFilterFc(channel, note, scheduleTime) {
1508
1472
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1509
1473
  this.getLFOFilterDepth(channel);
1510
1474
  note.filterDepth.gain
1511
- .cancelScheduledValues(now)
1512
- .setValueAtTime(modLfoToFilterFc, now);
1475
+ .cancelScheduledValues(scheduleTime)
1476
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
1513
1477
  }
1514
- setModLfoToVolume(channel, note) {
1515
- const now = this.audioContext.currentTime;
1478
+ setModLfoToVolume(channel, note, scheduleTime) {
1516
1479
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1517
1480
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1518
1481
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1519
1482
  (1 + this.getLFOAmplitudeDepth(channel));
1520
1483
  note.volumeDepth.gain
1521
- .cancelScheduledValues(now)
1522
- .setValueAtTime(volumeDepth, now);
1484
+ .cancelScheduledValues(scheduleTime)
1485
+ .setValueAtTime(volumeDepth, scheduleTime);
1523
1486
  }
1524
- setReverbEffectsSend(channel, note, prevValue) {
1487
+ setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1525
1488
  if (0 < prevValue) {
1526
1489
  if (0 < note.voiceParams.reverbEffectsSend) {
1527
- const now = this.audioContext.currentTime;
1528
1490
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1529
1491
  const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1530
1492
  note.reverbEffectsSend.gain
1531
- .cancelScheduledValues(now)
1532
- .setValueAtTime(value, now);
1493
+ .cancelScheduledValues(scheduleTime)
1494
+ .setValueAtTime(value, scheduleTime);
1533
1495
  }
1534
1496
  else {
1535
1497
  note.reverbEffectsSend.disconnect();
@@ -1547,15 +1509,14 @@ class MidyGM2 {
1547
1509
  }
1548
1510
  }
1549
1511
  }
1550
- setChorusEffectsSend(channel, note, prevValue) {
1512
+ setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1551
1513
  if (0 < prevValue) {
1552
1514
  if (0 < note.voiceParams.chorusEffectsSend) {
1553
- const now = this.audioContext.currentTime;
1554
1515
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1555
1516
  const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1556
1517
  note.chorusEffectsSend.gain
1557
- .cancelScheduledValues(now)
1558
- .setValueAtTime(value, now);
1518
+ .cancelScheduledValues(scheduleTime)
1519
+ .setValueAtTime(value, scheduleTime);
1559
1520
  }
1560
1521
  else {
1561
1522
  note.chorusEffectsSend.disconnect();
@@ -1573,75 +1534,71 @@ class MidyGM2 {
1573
1534
  }
1574
1535
  }
1575
1536
  }
1576
- setDelayModLFO(note) {
1577
- const now = this.audioContext.currentTime;
1537
+ setDelayModLFO(note, scheduleTime) {
1578
1538
  const startTime = note.startTime;
1579
- if (startTime < now)
1539
+ if (startTime < scheduleTime)
1580
1540
  return;
1581
- note.modulationLFO.stop(now);
1541
+ note.modulationLFO.stop(scheduleTime);
1582
1542
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1583
1543
  note.modulationLFO.connect(note.filterDepth);
1584
1544
  }
1585
- setFreqModLFO(note) {
1586
- const now = this.audioContext.currentTime;
1545
+ setFreqModLFO(note, scheduleTime) {
1587
1546
  const freqModLFO = note.voiceParams.freqModLFO;
1588
1547
  note.modulationLFO.frequency
1589
- .cancelScheduledValues(now)
1590
- .setValueAtTime(freqModLFO, now);
1548
+ .cancelScheduledValues(scheduleTime)
1549
+ .setValueAtTime(freqModLFO, scheduleTime);
1591
1550
  }
1592
- setFreqVibLFO(channel, note) {
1593
- const now = this.audioContext.currentTime;
1551
+ setFreqVibLFO(channel, note, scheduleTime) {
1594
1552
  const freqVibLFO = note.voiceParams.freqVibLFO;
1595
1553
  note.vibratoLFO.frequency
1596
- .cancelScheduledValues(now)
1597
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1554
+ .cancelScheduledValues(scheduleTime)
1555
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, scheduleTime);
1598
1556
  }
1599
1557
  createVoiceParamsHandlers() {
1600
1558
  return {
1601
- modLfoToPitch: (channel, note, _prevValue) => {
1559
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1602
1560
  if (0 < channel.state.modulationDepth) {
1603
- this.setModLfoToPitch(channel, note);
1561
+ this.setModLfoToPitch(channel, note, scheduleTime);
1604
1562
  }
1605
1563
  },
1606
- vibLfoToPitch: (channel, note, _prevValue) => {
1564
+ vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1607
1565
  if (0 < channel.state.vibratoDepth) {
1608
- this.setVibLfoToPitch(channel, note);
1566
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1609
1567
  }
1610
1568
  },
1611
- modLfoToFilterFc: (channel, note, _prevValue) => {
1569
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1612
1570
  if (0 < channel.state.modulationDepth) {
1613
- this.setModLfoToFilterFc(channel, note);
1571
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
1614
1572
  }
1615
1573
  },
1616
- modLfoToVolume: (channel, note, _prevValue) => {
1574
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1617
1575
  if (0 < channel.state.modulationDepth) {
1618
- this.setModLfoToVolume(channel, note);
1576
+ this.setModLfoToVolume(channel, note, scheduleTime);
1619
1577
  }
1620
1578
  },
1621
- chorusEffectsSend: (channel, note, prevValue) => {
1622
- this.setChorusEffectsSend(channel, note, prevValue);
1579
+ chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1580
+ this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1623
1581
  },
1624
- reverbEffectsSend: (channel, note, prevValue) => {
1625
- this.setReverbEffectsSend(channel, note, prevValue);
1582
+ reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1583
+ this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1626
1584
  },
1627
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1628
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1629
- delayVibLFO: (channel, note, prevValue) => {
1585
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1586
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1587
+ delayVibLFO: (channel, note, prevValue, scheduleTime) => {
1630
1588
  if (0 < channel.state.vibratoDepth) {
1631
- const now = this.audioContext.currentTime;
1632
1589
  const vibratoDelay = channel.state.vibratoDelay * 2;
1633
1590
  const prevStartTime = note.startTime + prevValue * vibratoDelay;
1634
- if (now < prevStartTime)
1591
+ if (scheduleTime < prevStartTime)
1635
1592
  return;
1636
1593
  const value = note.voiceParams.delayVibLFO;
1637
1594
  const startTime = note.startTime + value * vibratoDelay;
1638
- note.vibratoLFO.stop(now);
1595
+ note.vibratoLFO.stop(scheduleTime);
1639
1596
  note.vibratoLFO.start(startTime);
1640
1597
  }
1641
1598
  },
1642
- freqVibLFO: (channel, note, _prevValue) => {
1599
+ freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1643
1600
  if (0 < channel.state.vibratoDepth) {
1644
- this.setFreqVibLFO(channel, note);
1601
+ this.setFreqVibLFO(channel, note, scheduleTime);
1645
1602
  }
1646
1603
  },
1647
1604
  };
@@ -1653,54 +1610,49 @@ class MidyGM2 {
1653
1610
  state[3] = noteNumber / 127;
1654
1611
  return state;
1655
1612
  }
1656
- applyVoiceParams(channel, controllerType) {
1657
- channel.scheduledNotes.forEach((noteList) => {
1658
- for (let i = 0; i < noteList.length; i++) {
1659
- const note = noteList[i];
1660
- if (!note)
1613
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1614
+ this.processScheduledNotes(channel, (note) => {
1615
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1616
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
1617
+ let appliedFilterEnvelope = false;
1618
+ let appliedVolumeEnvelope = false;
1619
+ for (const [key, value] of Object.entries(voiceParams)) {
1620
+ const prevValue = note.voiceParams[key];
1621
+ if (value === prevValue)
1661
1622
  continue;
1662
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1663
- const voiceParams = note.voice.getParams(controllerType, controllerState);
1664
- let appliedFilterEnvelope = false;
1665
- let appliedVolumeEnvelope = false;
1666
- for (const [key, value] of Object.entries(voiceParams)) {
1667
- const prevValue = note.voiceParams[key];
1668
- if (value === prevValue)
1623
+ note.voiceParams[key] = value;
1624
+ if (key in this.voiceParamsHandlers) {
1625
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1626
+ }
1627
+ else if (filterEnvelopeKeySet.has(key)) {
1628
+ if (appliedFilterEnvelope)
1669
1629
  continue;
1670
- note.voiceParams[key] = value;
1671
- if (key in this.voiceParamsHandlers) {
1672
- this.voiceParamsHandlers[key](channel, note, prevValue);
1630
+ appliedFilterEnvelope = true;
1631
+ const noteVoiceParams = note.voiceParams;
1632
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1633
+ const key = filterEnvelopeKeys[i];
1634
+ if (key in voiceParams)
1635
+ noteVoiceParams[key] = voiceParams[key];
1673
1636
  }
1674
- else if (filterEnvelopeKeySet.has(key)) {
1675
- if (appliedFilterEnvelope)
1676
- continue;
1677
- appliedFilterEnvelope = true;
1678
- const noteVoiceParams = note.voiceParams;
1679
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1680
- const key = filterEnvelopeKeys[i];
1681
- if (key in voiceParams)
1682
- noteVoiceParams[key] = voiceParams[key];
1683
- }
1684
- if (note.portamento) {
1685
- this.setPortamentoStartFilterEnvelope(channel, note);
1686
- }
1687
- else {
1688
- this.setFilterEnvelope(channel, note);
1689
- }
1690
- this.setPitchEnvelope(note);
1637
+ if (note.portamento) {
1638
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1691
1639
  }
1692
- else if (volumeEnvelopeKeySet.has(key)) {
1693
- if (appliedVolumeEnvelope)
1694
- continue;
1695
- appliedVolumeEnvelope = true;
1696
- const noteVoiceParams = note.voiceParams;
1697
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1698
- const key = volumeEnvelopeKeys[i];
1699
- if (key in voiceParams)
1700
- noteVoiceParams[key] = voiceParams[key];
1701
- }
1702
- this.setVolumeEnvelope(channel, note);
1640
+ else {
1641
+ this.setFilterEnvelope(channel, note, scheduleTime);
1642
+ }
1643
+ this.setPitchEnvelope(note, scheduleTime);
1644
+ }
1645
+ else if (volumeEnvelopeKeySet.has(key)) {
1646
+ if (appliedVolumeEnvelope)
1647
+ continue;
1648
+ appliedVolumeEnvelope = true;
1649
+ const noteVoiceParams = note.voiceParams;
1650
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1651
+ const key = volumeEnvelopeKeys[i];
1652
+ if (key in voiceParams)
1653
+ noteVoiceParams[key] = voiceParams[key];
1703
1654
  }
1655
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1704
1656
  }
1705
1657
  }
1706
1658
  });
@@ -1733,12 +1685,12 @@ class MidyGM2 {
1733
1685
  127: this.polyOn,
1734
1686
  };
1735
1687
  }
1736
- handleControlChange(channelNumber, controllerType, value, startTime) {
1688
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1737
1689
  const handler = this.controlChangeHandlers[controllerType];
1738
1690
  if (handler) {
1739
- handler.call(this, channelNumber, value, startTime);
1691
+ handler.call(this, channelNumber, value, scheduleTime);
1740
1692
  const channel = this.channels[channelNumber];
1741
- this.applyVoiceParams(channel, controllerType + 128);
1693
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1742
1694
  this.applyControlTable(channel, controllerType);
1743
1695
  }
1744
1696
  else {
@@ -1749,9 +1701,8 @@ class MidyGM2 {
1749
1701
  this.channels[channelNumber].bankMSB = msb;
1750
1702
  }
1751
1703
  updateModulation(channel, scheduleTime) {
1752
- scheduleTime ??= this.audioContext.currentTime;
1753
1704
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1754
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1705
+ this.processScheduledNotes(channel, (note) => {
1755
1706
  if (note.modulationDepth) {
1756
1707
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1757
1708
  }
@@ -1762,6 +1713,7 @@ class MidyGM2 {
1762
1713
  });
1763
1714
  }
1764
1715
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1716
+ scheduleTime ??= this.audioContext.currentTime;
1765
1717
  const channel = this.channels[channelNumber];
1766
1718
  channel.state.modulationDepth = modulation / 127;
1767
1719
  this.updateModulation(channel, scheduleTime);
@@ -1772,8 +1724,7 @@ class MidyGM2 {
1772
1724
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1773
1725
  }
1774
1726
  setKeyBasedVolume(channel, scheduleTime) {
1775
- scheduleTime ??= this.audioContext.currentTime;
1776
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1727
+ this.processScheduledNotes(channel, (note) => {
1777
1728
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1778
1729
  if (keyBasedValue !== 0) {
1779
1730
  note.volumeNode.gain
@@ -1783,6 +1734,7 @@ class MidyGM2 {
1783
1734
  });
1784
1735
  }
1785
1736
  setVolume(channelNumber, volume, scheduleTime) {
1737
+ scheduleTime ??= this.audioContext.currentTime;
1786
1738
  const channel = this.channels[channelNumber];
1787
1739
  channel.state.volume = volume / 127;
1788
1740
  this.updateChannelVolume(channel, scheduleTime);
@@ -1796,8 +1748,7 @@ class MidyGM2 {
1796
1748
  };
1797
1749
  }
1798
1750
  setKeyBasedPan(channel, scheduleTime) {
1799
- scheduleTime ??= this.audioContext.currentTime;
1800
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1751
+ this.processScheduledNotes(channel, (note) => {
1801
1752
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1802
1753
  if (keyBasedValue !== 0) {
1803
1754
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
@@ -1811,12 +1762,14 @@ class MidyGM2 {
1811
1762
  });
1812
1763
  }
1813
1764
  setPan(channelNumber, pan, scheduleTime) {
1765
+ scheduleTime ??= this.audioContext.currentTime;
1814
1766
  const channel = this.channels[channelNumber];
1815
1767
  channel.state.pan = pan / 127;
1816
1768
  this.updateChannelVolume(channel, scheduleTime);
1817
1769
  this.setKeyBasedPan(channel, scheduleTime);
1818
1770
  }
1819
1771
  setExpression(channelNumber, expression, scheduleTime) {
1772
+ scheduleTime ??= this.audioContext.currentTime;
1820
1773
  const channel = this.channels[channelNumber];
1821
1774
  channel.state.expression = expression / 127;
1822
1775
  this.updateChannelVolume(channel, scheduleTime);
@@ -1824,125 +1777,113 @@ class MidyGM2 {
1824
1777
  setBankLSB(channelNumber, lsb) {
1825
1778
  this.channels[channelNumber].bankLSB = lsb;
1826
1779
  }
1827
- dataEntryLSB(channelNumber, value) {
1780
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1828
1781
  this.channels[channelNumber].dataLSB = value;
1829
- this.handleRPN(channelNumber);
1782
+ this.handleRPN(channelNumber, scheduleTime);
1830
1783
  }
1831
- updateChannelVolume(channel) {
1832
- const now = this.audioContext.currentTime;
1784
+ updateChannelVolume(channel, scheduleTime) {
1833
1785
  const state = channel.state;
1834
1786
  const volume = state.volume * state.expression;
1835
1787
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1836
1788
  channel.gainL.gain
1837
- .cancelScheduledValues(now)
1838
- .setValueAtTime(volume * gainLeft, now);
1789
+ .cancelScheduledValues(scheduleTime)
1790
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1839
1791
  channel.gainR.gain
1840
- .cancelScheduledValues(now)
1841
- .setValueAtTime(volume * gainRight, now);
1792
+ .cancelScheduledValues(scheduleTime)
1793
+ .setValueAtTime(volume * gainRight, scheduleTime);
1842
1794
  }
1843
- setSustainPedal(channelNumber, value) {
1844
- this.channels[channelNumber].state.sustainPedal = value / 127;
1845
- if (value < 64) {
1846
- this.releaseSustainPedal(channelNumber, value);
1795
+ setSustainPedal(channelNumber, value, scheduleTime) {
1796
+ scheduleTime ??= this.audioContext.currentTime;
1797
+ const channel = this.channels[channelNumber];
1798
+ channel.state.sustainPedal = value / 127;
1799
+ if (64 <= value) {
1800
+ this.processScheduledNotes(channel, (note) => {
1801
+ channel.sustainNotes.push(note);
1802
+ });
1803
+ }
1804
+ else {
1805
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1847
1806
  }
1848
1807
  }
1849
1808
  setPortamento(channelNumber, value) {
1850
1809
  this.channels[channelNumber].state.portamento = value / 127;
1851
1810
  }
1852
- setSostenutoPedal(channelNumber, value) {
1811
+ setSostenutoPedal(channelNumber, value, scheduleTime) {
1812
+ scheduleTime ??= this.audioContext.currentTime;
1853
1813
  const channel = this.channels[channelNumber];
1854
1814
  channel.state.sostenutoPedal = value / 127;
1855
1815
  if (64 <= value) {
1856
- const now = this.audioContext.currentTime;
1857
- channel.sostenutoNotes = this.getActiveNotes(channel, now);
1816
+ channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
1858
1817
  }
1859
1818
  else {
1860
- this.releaseSostenutoPedal(channelNumber, value);
1819
+ this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1861
1820
  }
1862
1821
  }
1863
- setSoftPedal(channelNumber, softPedal) {
1822
+ setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1864
1823
  const channel = this.channels[channelNumber];
1865
1824
  channel.state.softPedal = softPedal / 127;
1866
1825
  }
1867
- setReverbSendLevel(channelNumber, reverbSendLevel) {
1826
+ setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
1827
+ scheduleTime ??= this.audioContext.currentTime;
1868
1828
  const channel = this.channels[channelNumber];
1869
1829
  const state = channel.state;
1870
1830
  const reverbEffect = this.reverbEffect;
1871
1831
  if (0 < state.reverbSendLevel) {
1872
1832
  if (0 < reverbSendLevel) {
1873
- const now = this.audioContext.currentTime;
1874
1833
  state.reverbSendLevel = reverbSendLevel / 127;
1875
- reverbEffect.input.gain.cancelScheduledValues(now);
1876
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1834
+ reverbEffect.input.gain
1835
+ .cancelScheduledValues(scheduleTime)
1836
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
1877
1837
  }
1878
1838
  else {
1879
- channel.scheduledNotes.forEach((noteList) => {
1880
- for (let i = 0; i < noteList.length; i++) {
1881
- const note = noteList[i];
1882
- if (!note)
1883
- continue;
1884
- if (note.voiceParams.reverbEffectsSend <= 0)
1885
- continue;
1886
- note.reverbEffectsSend.disconnect();
1887
- }
1839
+ this.processScheduledNotes(channel, (note) => {
1840
+ if (note.voiceParams.reverbEffectsSend <= 0)
1841
+ return false;
1842
+ note.reverbEffectsSend.disconnect();
1888
1843
  });
1889
1844
  }
1890
1845
  }
1891
1846
  else {
1892
1847
  if (0 < reverbSendLevel) {
1893
- const now = this.audioContext.currentTime;
1894
- channel.scheduledNotes.forEach((noteList) => {
1895
- for (let i = 0; i < noteList.length; i++) {
1896
- const note = noteList[i];
1897
- if (!note)
1898
- continue;
1899
- this.setReverbEffectsSend(channel, note, 0);
1900
- }
1848
+ this.processScheduledNotes(channel, (note) => {
1849
+ this.setReverbEffectsSend(channel, note, 0, scheduleTime);
1901
1850
  });
1902
1851
  state.reverbSendLevel = reverbSendLevel / 127;
1903
- reverbEffect.input.gain.cancelScheduledValues(now);
1904
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1852
+ reverbEffect.input.gain
1853
+ .cancelScheduledValues(scheduleTime)
1854
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
1905
1855
  }
1906
1856
  }
1907
1857
  }
1908
- setChorusSendLevel(channelNumber, chorusSendLevel) {
1858
+ setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
1859
+ scheduleTime ??= this.audioContext.currentTime;
1909
1860
  const channel = this.channels[channelNumber];
1910
1861
  const state = channel.state;
1911
1862
  const chorusEffect = this.chorusEffect;
1912
1863
  if (0 < state.chorusSendLevel) {
1913
1864
  if (0 < chorusSendLevel) {
1914
- const now = this.audioContext.currentTime;
1915
1865
  state.chorusSendLevel = chorusSendLevel / 127;
1916
- chorusEffect.input.gain.cancelScheduledValues(now);
1917
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1866
+ chorusEffect.input.gain
1867
+ .cancelScheduledValues(scheduleTime)
1868
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
1918
1869
  }
1919
1870
  else {
1920
- channel.scheduledNotes.forEach((noteList) => {
1921
- for (let i = 0; i < noteList.length; i++) {
1922
- const note = noteList[i];
1923
- if (!note)
1924
- continue;
1925
- if (note.voiceParams.chorusEffectsSend <= 0)
1926
- continue;
1927
- note.chorusEffectsSend.disconnect();
1928
- }
1871
+ this.processScheduledNotes(channel, (note) => {
1872
+ if (note.voiceParams.chorusEffectsSend <= 0)
1873
+ return false;
1874
+ note.chorusEffectsSend.disconnect();
1929
1875
  });
1930
1876
  }
1931
1877
  }
1932
1878
  else {
1933
1879
  if (0 < chorusSendLevel) {
1934
- const now = this.audioContext.currentTime;
1935
- channel.scheduledNotes.forEach((noteList) => {
1936
- for (let i = 0; i < noteList.length; i++) {
1937
- const note = noteList[i];
1938
- if (!note)
1939
- continue;
1940
- this.setChorusEffectsSend(channel, note, 0);
1941
- }
1880
+ this.processScheduledNotes(channel, (note) => {
1881
+ this.setChorusEffectsSend(channel, note, 0, scheduleTime);
1942
1882
  });
1943
1883
  state.chorusSendLevel = chorusSendLevel / 127;
1944
- chorusEffect.input.gain.cancelScheduledValues(now);
1945
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1884
+ chorusEffect.input.gain
1885
+ .cancelScheduledValues(scheduleTime)
1886
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
1946
1887
  }
1947
1888
  }
1948
1889
  }
@@ -1972,21 +1913,21 @@ class MidyGM2 {
1972
1913
  channel.dataMSB = minMSB;
1973
1914
  }
1974
1915
  }
1975
- handleRPN(channelNumber) {
1916
+ handleRPN(channelNumber, scheduleTime) {
1976
1917
  const channel = this.channels[channelNumber];
1977
1918
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1978
1919
  switch (rpn) {
1979
1920
  case 0:
1980
- this.handlePitchBendRangeRPN(channelNumber);
1921
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1981
1922
  break;
1982
1923
  case 1:
1983
- this.handleFineTuningRPN(channelNumber);
1924
+ this.handleFineTuningRPN(channelNumber, scheduleTime);
1984
1925
  break;
1985
1926
  case 2:
1986
- this.handleCoarseTuningRPN(channelNumber);
1927
+ this.handleCoarseTuningRPN(channelNumber, scheduleTime);
1987
1928
  break;
1988
1929
  case 5:
1989
- this.handleModulationDepthRangeRPN(channelNumber);
1930
+ this.handleModulationDepthRangeRPN(channelNumber, scheduleTime);
1990
1931
  break;
1991
1932
  default:
1992
1933
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
@@ -1998,67 +1939,72 @@ class MidyGM2 {
1998
1939
  setRPNLSB(channelNumber, value) {
1999
1940
  this.channels[channelNumber].rpnLSB = value;
2000
1941
  }
2001
- dataEntryMSB(channelNumber, value) {
1942
+ dataEntryMSB(channelNumber, value, scheduleTime) {
2002
1943
  this.channels[channelNumber].dataMSB = value;
2003
- this.handleRPN(channelNumber);
1944
+ this.handleRPN(channelNumber, scheduleTime);
2004
1945
  }
2005
- handlePitchBendRangeRPN(channelNumber) {
1946
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2006
1947
  const channel = this.channels[channelNumber];
2007
1948
  this.limitData(channel, 0, 127, 0, 99);
2008
1949
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2009
- this.setPitchBendRange(channelNumber, pitchBendRange);
1950
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2010
1951
  }
2011
- setPitchBendRange(channelNumber, value) {
1952
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1953
+ scheduleTime ??= this.audioContext.currentTime;
2012
1954
  const channel = this.channels[channelNumber];
2013
1955
  const state = channel.state;
2014
1956
  const prev = state.pitchWheelSensitivity;
2015
1957
  const next = value / 128;
2016
1958
  state.pitchWheelSensitivity = next;
2017
1959
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2018
- this.updateChannelDetune(channel);
2019
- this.applyVoiceParams(channel, 16);
1960
+ this.updateChannelDetune(channel, scheduleTime);
1961
+ this.applyVoiceParams(channel, 16, scheduleTime);
2020
1962
  }
2021
- handleFineTuningRPN(channelNumber) {
1963
+ handleFineTuningRPN(channelNumber, scheduleTime) {
2022
1964
  const channel = this.channels[channelNumber];
2023
1965
  this.limitData(channel, 0, 127, 0, 127);
2024
1966
  const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
2025
- this.setFineTuning(channelNumber, fineTuning);
1967
+ this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2026
1968
  }
2027
- setFineTuning(channelNumber, value) {
1969
+ setFineTuning(channelNumber, value, scheduleTime) {
1970
+ scheduleTime ??= this.audioContext.currentTime;
2028
1971
  const channel = this.channels[channelNumber];
2029
1972
  const prev = channel.fineTuning;
2030
1973
  const next = (value - 8192) / 8.192; // cent
2031
1974
  channel.fineTuning = next;
2032
1975
  channel.detune += next - prev;
2033
- this.updateChannelDetune(channel);
1976
+ this.updateChannelDetune(channel, scheduleTime);
2034
1977
  }
2035
- handleCoarseTuningRPN(channelNumber) {
1978
+ handleCoarseTuningRPN(channelNumber, scheduleTime) {
2036
1979
  const channel = this.channels[channelNumber];
2037
1980
  this.limitDataMSB(channel, 0, 127);
2038
1981
  const coarseTuning = channel.dataMSB;
2039
- this.setCoarseTuning(channelNumber, coarseTuning);
1982
+ this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2040
1983
  }
2041
- setCoarseTuning(channelNumber, value) {
1984
+ setCoarseTuning(channelNumber, value, scheduleTime) {
1985
+ scheduleTime ??= this.audioContext.currentTime;
2042
1986
  const channel = this.channels[channelNumber];
2043
1987
  const prev = channel.coarseTuning;
2044
1988
  const next = (value - 64) * 100; // cent
2045
1989
  channel.coarseTuning = next;
2046
1990
  channel.detune += next - prev;
2047
- this.updateChannelDetune(channel);
1991
+ this.updateChannelDetune(channel, scheduleTime);
2048
1992
  }
2049
- handleModulationDepthRangeRPN(channelNumber) {
1993
+ handleModulationDepthRangeRPN(channelNumber, scheduleTime) {
2050
1994
  const channel = this.channels[channelNumber];
2051
1995
  this.limitData(channel, 0, 127, 0, 127);
2052
1996
  const modulationDepthRange = (dataMSB + dataLSB / 128) * 100;
2053
- this.setModulationDepthRange(channelNumber, modulationDepthRange);
1997
+ this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2054
1998
  }
2055
- setModulationDepthRange(channelNumber, modulationDepthRange) {
1999
+ setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2000
+ scheduleTime ??= this.audioContext.currentTime;
2056
2001
  const channel = this.channels[channelNumber];
2057
2002
  channel.modulationDepthRange = modulationDepthRange;
2058
- this.updateModulation(channel);
2003
+ this.updateModulation(channel, scheduleTime);
2059
2004
  }
2060
- allSoundOff(channelNumber) {
2061
- return this.stopChannelNotes(channelNumber, 0, true);
2005
+ allSoundOff(channelNumber, _value, scheduleTime) {
2006
+ scheduleTime ??= this.audioContext.currentTime;
2007
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2062
2008
  }
2063
2009
  resetAllControllers(channelNumber) {
2064
2010
  const stateTypes = [
@@ -2086,28 +2032,33 @@ class MidyGM2 {
2086
2032
  channel[type] = this.constructor.channelSettings[type];
2087
2033
  }
2088
2034
  }
2089
- allNotesOff(channelNumber) {
2090
- return this.stopChannelNotes(channelNumber, 0, false);
2035
+ allNotesOff(channelNumber, _value, scheduleTime) {
2036
+ scheduleTime ??= this.audioContext.currentTime;
2037
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2091
2038
  }
2092
- omniOff() {
2093
- this.omni = false;
2039
+ omniOff(channelNumber, value, scheduleTime) {
2040
+ this.allNotesOff(channelNumber, value, scheduleTime);
2094
2041
  }
2095
- omniOn() {
2096
- this.omni = true;
2042
+ omniOn(channelNumber, value, scheduleTime) {
2043
+ this.allNotesOff(channelNumber, value, scheduleTime);
2097
2044
  }
2098
- monoOn() {
2099
- this.mono = true;
2045
+ monoOn(channelNumber, value, scheduleTime) {
2046
+ const channel = this.channels[channelNumber];
2047
+ this.allNotesOff(channelNumber, value, scheduleTime);
2048
+ channel.mono = true;
2100
2049
  }
2101
- polyOn() {
2102
- this.mono = false;
2050
+ polyOn(channelNumber, value, scheduleTime) {
2051
+ const channel = this.channels[channelNumber];
2052
+ this.allNotesOff(channelNumber, value, scheduleTime);
2053
+ channel.mono = false;
2103
2054
  }
2104
- handleUniversalNonRealTimeExclusiveMessage(data) {
2055
+ handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
2105
2056
  switch (data[2]) {
2106
2057
  case 8:
2107
2058
  switch (data[3]) {
2108
2059
  case 8:
2109
2060
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2110
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2061
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false, scheduleTime);
2111
2062
  default:
2112
2063
  console.warn(`Unsupported Exclusive Message: ${data}`);
2113
2064
  }
@@ -2150,18 +2101,18 @@ class MidyGM2 {
2150
2101
  this.channels[9].bankMSB = 120;
2151
2102
  this.channels[9].bank = 120 * 128;
2152
2103
  }
2153
- handleUniversalRealTimeExclusiveMessage(data) {
2104
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2154
2105
  switch (data[2]) {
2155
2106
  case 4:
2156
2107
  switch (data[3]) {
2157
2108
  case 1:
2158
- return this.handleMasterVolumeSysEx(data);
2109
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
2159
2110
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2160
- return this.handleMasterFineTuningSysEx(data);
2111
+ return this.handleMasterFineTuningSysEx(data, scheduleTime);
2161
2112
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2162
- return this.handleMasterCoarseTuningSysEx(data);
2113
+ return this.handleMasterCoarseTuningSysEx(data, scheduleTime);
2163
2114
  case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2164
- return this.handleGlobalParameterControlSysEx(data);
2115
+ return this.handleGlobalParameterControlSysEx(data, scheduleTime);
2165
2116
  default:
2166
2117
  console.warn(`Unsupported Exclusive Message: ${data}`);
2167
2118
  }
@@ -2179,7 +2130,7 @@ class MidyGM2 {
2179
2130
  case 10:
2180
2131
  switch (data[3]) {
2181
2132
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2182
- return this.handleKeyBasedInstrumentControlSysEx(data);
2133
+ return this.handleKeyBasedInstrumentControlSysEx(data, scheduleTime);
2183
2134
  default:
2184
2135
  console.warn(`Unsupported Exclusive Message: ${data}`);
2185
2136
  }
@@ -2188,49 +2139,50 @@ class MidyGM2 {
2188
2139
  console.warn(`Unsupported Exclusive Message: ${data}`);
2189
2140
  }
2190
2141
  }
2191
- handleMasterVolumeSysEx(data) {
2142
+ handleMasterVolumeSysEx(data, scheduleTime) {
2192
2143
  const volume = (data[5] * 128 + data[4]) / 16383;
2193
- this.setMasterVolume(volume);
2144
+ this.setMasterVolume(volume, scheduleTime);
2194
2145
  }
2195
- setMasterVolume(volume) {
2146
+ setMasterVolume(volume, scheduleTime) {
2147
+ scheduleTime ??= this.audioContext.currentTime;
2196
2148
  if (volume < 0 && 1 < volume) {
2197
2149
  console.error("Master Volume is out of range");
2198
2150
  }
2199
2151
  else {
2200
- const now = this.audioContext.currentTime;
2201
- this.masterVolume.gain.cancelScheduledValues(now);
2202
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
2152
+ this.masterVolume.gain
2153
+ .cancelScheduledValues(scheduleTime)
2154
+ .setValueAtTime(volume * volume, scheduleTime);
2203
2155
  }
2204
2156
  }
2205
- handleMasterFineTuningSysEx(data) {
2157
+ handleMasterFineTuningSysEx(data, scheduleTime) {
2206
2158
  const fineTuning = data[5] * 128 + data[4];
2207
- this.setMasterFineTuning(fineTuning);
2159
+ this.setMasterFineTuning(fineTuning, scheduleTime);
2208
2160
  }
2209
- setMasterFineTuning(value) {
2161
+ setMasterFineTuning(value, scheduleTime) {
2210
2162
  const prev = this.masterFineTuning;
2211
2163
  const next = (value - 8192) / 8.192; // cent
2212
2164
  this.masterFineTuning = next;
2213
2165
  channel.detune += next - prev;
2214
- this.updateChannelDetune(channel);
2166
+ this.updateChannelDetune(channel, scheduleTime);
2215
2167
  }
2216
- handleMasterCoarseTuningSysEx(data) {
2168
+ handleMasterCoarseTuningSysEx(data, scheduleTime) {
2217
2169
  const coarseTuning = data[4];
2218
- this.setMasterCoarseTuning(coarseTuning);
2170
+ this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2219
2171
  }
2220
- setMasterCoarseTuning(value) {
2172
+ setMasterCoarseTuning(value, scheduleTime) {
2221
2173
  const prev = this.masterCoarseTuning;
2222
2174
  const next = (value - 64) * 100; // cent
2223
2175
  this.masterCoarseTuning = next;
2224
2176
  channel.detune += next - prev;
2225
- this.updateChannelDetune(channel);
2177
+ this.updateChannelDetune(channel, scheduleTime);
2226
2178
  }
2227
- handleGlobalParameterControlSysEx(data) {
2179
+ handleGlobalParameterControlSysEx(data, scheduleTime) {
2228
2180
  if (data[7] === 1) {
2229
2181
  switch (data[8]) {
2230
2182
  case 1:
2231
2183
  return this.handleReverbParameterSysEx(data);
2232
2184
  case 2:
2233
- return this.handleChorusParameterSysEx(data);
2185
+ return this.handleChorusParameterSysEx(data, scheduleTime);
2234
2186
  default:
2235
2187
  console.warn(`Unsupported Global Parameter Control Message: ${data}`);
2236
2188
  }
@@ -2309,88 +2261,84 @@ class MidyGM2 {
2309
2261
  calcDelay(rt60, feedback) {
2310
2262
  return -rt60 * Math.log10(feedback) / 3;
2311
2263
  }
2312
- handleChorusParameterSysEx(data) {
2264
+ handleChorusParameterSysEx(data, scheduleTime) {
2313
2265
  switch (data[9]) {
2314
2266
  case 0:
2315
- return this.setChorusType(data[10]);
2267
+ return this.setChorusType(data[10], scheduleTime);
2316
2268
  case 1:
2317
- return this.setChorusModRate(data[10]);
2269
+ return this.setChorusModRate(data[10], scheduleTime);
2318
2270
  case 2:
2319
- return this.setChorusModDepth(data[10]);
2271
+ return this.setChorusModDepth(data[10], scheduleTime);
2320
2272
  case 3:
2321
- return this.setChorusFeedback(data[10]);
2273
+ return this.setChorusFeedback(data[10], scheduleTime);
2322
2274
  case 4:
2323
- return this.setChorusSendToReverb(data[10]);
2275
+ return this.setChorusSendToReverb(data[10], scheduleTime);
2324
2276
  }
2325
2277
  }
2326
- setChorusType(type) {
2278
+ setChorusType(type, scheduleTime) {
2327
2279
  switch (type) {
2328
2280
  case 0:
2329
- return this.setChorusParameter(3, 5, 0, 0);
2281
+ return this.setChorusParameter(3, 5, 0, 0, scheduleTime);
2330
2282
  case 1:
2331
- return this.setChorusParameter(9, 19, 5, 0);
2283
+ return this.setChorusParameter(9, 19, 5, 0, scheduleTime);
2332
2284
  case 2:
2333
- return this.setChorusParameter(3, 19, 8, 0);
2285
+ return this.setChorusParameter(3, 19, 8, 0, scheduleTime);
2334
2286
  case 3:
2335
- return this.setChorusParameter(9, 16, 16, 0);
2287
+ return this.setChorusParameter(9, 16, 16, 0, scheduleTime);
2336
2288
  case 4:
2337
- return this.setChorusParameter(2, 24, 64, 0);
2289
+ return this.setChorusParameter(2, 24, 64, 0, scheduleTime);
2338
2290
  case 5:
2339
- return this.setChorusParameter(1, 5, 112, 0);
2291
+ return this.setChorusParameter(1, 5, 112, 0, scheduleTime);
2340
2292
  default:
2341
2293
  console.warn(`Unsupported Chorus Type: ${type}`);
2342
2294
  }
2343
2295
  }
2344
- setChorusParameter(modRate, modDepth, feedback, sendToReverb) {
2345
- this.setChorusModRate(modRate);
2346
- this.setChorusModDepth(modDepth);
2347
- this.setChorusFeedback(feedback);
2348
- this.setChorusSendToReverb(sendToReverb);
2296
+ setChorusParameter(modRate, modDepth, feedback, sendToReverb, scheduleTime) {
2297
+ this.setChorusModRate(modRate, scheduleTime);
2298
+ this.setChorusModDepth(modDepth, scheduleTime);
2299
+ this.setChorusFeedback(feedback, scheduleTime);
2300
+ this.setChorusSendToReverb(sendToReverb, scheduleTime);
2349
2301
  }
2350
- setChorusModRate(value) {
2351
- const now = this.audioContext.currentTime;
2302
+ setChorusModRate(value, scheduleTime) {
2352
2303
  const modRate = this.getChorusModRate(value);
2353
2304
  this.chorus.modRate = modRate;
2354
- this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
2305
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, scheduleTime);
2355
2306
  }
2356
2307
  getChorusModRate(value) {
2357
2308
  return value * 0.122; // Hz
2358
2309
  }
2359
- setChorusModDepth(value) {
2360
- const now = this.audioContext.currentTime;
2310
+ setChorusModDepth(value, scheduleTime) {
2361
2311
  const modDepth = this.getChorusModDepth(value);
2362
2312
  this.chorus.modDepth = modDepth;
2363
2313
  this.chorusEffect.lfoGain.gain
2364
- .cancelScheduledValues(now)
2365
- .setValueAtTime(modDepth / 2, now);
2314
+ .cancelScheduledValues(scheduleTime)
2315
+ .setValueAtTime(modDepth / 2, scheduleTime);
2366
2316
  }
2367
2317
  getChorusModDepth(value) {
2368
2318
  return (value + 1) / 3200; // second
2369
2319
  }
2370
- setChorusFeedback(value) {
2371
- const now = this.audioContext.currentTime;
2320
+ setChorusFeedback(value, scheduleTime) {
2372
2321
  const feedback = this.getChorusFeedback(value);
2373
2322
  this.chorus.feedback = feedback;
2374
2323
  const chorusEffect = this.chorusEffect;
2375
2324
  for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
2376
2325
  chorusEffect.feedbackGains[i].gain
2377
- .cancelScheduledValues(now)
2378
- .setValueAtTime(feedback, now);
2326
+ .cancelScheduledValues(scheduleTime)
2327
+ .setValueAtTime(feedback, scheduleTime);
2379
2328
  }
2380
2329
  }
2381
2330
  getChorusFeedback(value) {
2382
2331
  return value * 0.00763;
2383
2332
  }
2384
- setChorusSendToReverb(value) {
2333
+ setChorusSendToReverb(value, scheduleTime) {
2385
2334
  const sendToReverb = this.getChorusSendToReverb(value);
2386
2335
  const sendGain = this.chorusEffect.sendGain;
2387
2336
  if (0 < this.chorus.sendToReverb) {
2388
2337
  this.chorus.sendToReverb = sendToReverb;
2389
2338
  if (0 < sendToReverb) {
2390
- const now = this.audioContext.currentTime;
2391
2339
  sendGain.gain
2392
- .cancelScheduledValues(now)
2393
- .setValueAtTime(sendToReverb, now);
2340
+ .cancelScheduledValues(scheduleTime)
2341
+ .setValueAtTime(sendToReverb, scheduleTime);
2394
2342
  }
2395
2343
  else {
2396
2344
  sendGain.disconnect();
@@ -2399,11 +2347,10 @@ class MidyGM2 {
2399
2347
  else {
2400
2348
  this.chorus.sendToReverb = sendToReverb;
2401
2349
  if (0 < sendToReverb) {
2402
- const now = this.audioContext.currentTime;
2403
2350
  sendGain.connect(this.reverbEffect.input);
2404
2351
  sendGain.gain
2405
- .cancelScheduledValues(now)
2406
- .setValueAtTime(sendToReverb, now);
2352
+ .cancelScheduledValues(scheduleTime)
2353
+ .setValueAtTime(sendToReverb, scheduleTime);
2407
2354
  }
2408
2355
  }
2409
2356
  }
@@ -2429,7 +2376,7 @@ class MidyGM2 {
2429
2376
  }
2430
2377
  return bitmap;
2431
2378
  }
2432
- handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2379
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime, scheduleTime) {
2433
2380
  if (data.length < 19) {
2434
2381
  console.error("Data length is too short");
2435
2382
  return;
@@ -2444,7 +2391,7 @@ class MidyGM2 {
2444
2391
  channel.scaleOctaveTuningTable[j] = centValue;
2445
2392
  }
2446
2393
  if (realtime)
2447
- this.updateChannelDetune(channel);
2394
+ this.updateChannelDetune(channel, scheduleTime);
2448
2395
  }
2449
2396
  }
2450
2397
  getFilterCutoffControl(channel) {
@@ -2488,7 +2435,7 @@ class MidyGM2 {
2488
2435
  if (table[5] !== 0)
2489
2436
  this.setModLfoToVolume(channel, note);
2490
2437
  }
2491
- handleChannelPressureSysEx(data, tableName) {
2438
+ handlePressureSysEx(data, tableName) {
2492
2439
  const channelNumber = data[4];
2493
2440
  const table = this.channels[channelNumber][tableName];
2494
2441
  for (let i = 5; i < data.length - 1; i += 2) {
@@ -2512,13 +2459,8 @@ class MidyGM2 {
2512
2459
  const slotSize = 6;
2513
2460
  const offset = controllerType * slotSize;
2514
2461
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2515
- channel.scheduledNotes.forEach((noteList) => {
2516
- for (let i = 0; i < noteList.length; i++) {
2517
- const note = noteList[i];
2518
- if (!note)
2519
- continue;
2520
- this.setControllerParameters(channel, note, table);
2521
- }
2462
+ this.processScheduledNotes(channel, (note) => {
2463
+ this.setControllerParameters(channel, note, table);
2522
2464
  });
2523
2465
  }
2524
2466
  handleControlChangeSysEx(data) {
@@ -2536,7 +2478,7 @@ class MidyGM2 {
2536
2478
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2537
2479
  return (controlValue + 64) / 64;
2538
2480
  }
2539
- handleKeyBasedInstrumentControlSysEx(data) {
2481
+ handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2540
2482
  const channelNumber = data[4];
2541
2483
  const keyNumber = data[5];
2542
2484
  const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
@@ -2546,30 +2488,27 @@ class MidyGM2 {
2546
2488
  const index = keyNumber * 128 + controllerType;
2547
2489
  table[index] = value - 64;
2548
2490
  }
2549
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2491
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2550
2492
  }
2551
- handleExclusiveMessage(data) {
2552
- console.warn(`Unsupported Exclusive Message: ${data}`);
2553
- }
2554
- handleSysEx(data) {
2493
+ handleSysEx(data, scheduleTime) {
2555
2494
  switch (data[0]) {
2556
2495
  case 126:
2557
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
2496
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
2558
2497
  case 127:
2559
- return this.handleUniversalRealTimeExclusiveMessage(data);
2498
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
2560
2499
  default:
2561
- return this.handleExclusiveMessage(data);
2500
+ console.warn(`Unsupported Exclusive Message: ${data}`);
2562
2501
  }
2563
2502
  }
2564
- scheduleTask(callback, startTime) {
2503
+ scheduleTask(callback, scheduleTime) {
2565
2504
  return new Promise((resolve) => {
2566
2505
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
2567
2506
  bufferSource.onended = () => {
2568
2507
  callback();
2569
2508
  resolve();
2570
2509
  };
2571
- bufferSource.start(startTime);
2572
- bufferSource.stop(startTime);
2510
+ bufferSource.start(scheduleTime);
2511
+ bufferSource.stop(scheduleTime);
2573
2512
  });
2574
2513
  }
2575
2514
  }
@@ -2589,6 +2528,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2589
2528
  dataLSB: 0,
2590
2529
  rpnMSB: 127,
2591
2530
  rpnLSB: 127,
2531
+ mono: false, // CC#124, CC#125
2592
2532
  fineTuning: 0, // cb
2593
2533
  coarseTuning: 0, // cb
2594
2534
  modulationDepthRange: 50, // cent