@marmooo/midy 0.2.6 → 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.
@@ -582,7 +582,8 @@ class MidyGM2 {
582
582
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
583
583
  if (portamentoTarget)
584
584
  portamentoTarget.portamento = true;
585
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, portamentoTarget?.noteNumber, false);
585
+ const notePromise = this.scheduleNoteOff(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, false, // force
586
+ portamentoTarget?.noteNumber);
586
587
  if (notePromise) {
587
588
  this.notePromises.push(notePromise);
588
589
  }
@@ -632,10 +633,11 @@ class MidyGM2 {
632
633
  resolve();
633
634
  return;
634
635
  }
635
- const t = this.audioContext.currentTime + offset;
636
+ const now = this.audioContext.currentTime;
637
+ const t = now + offset;
636
638
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
637
639
  if (this.isPausing) {
638
- await this.stopNotes(0, true);
640
+ await this.stopNotes(0, true, now);
639
641
  this.notePromises = [];
640
642
  resolve();
641
643
  this.isPausing = false;
@@ -643,7 +645,7 @@ class MidyGM2 {
643
645
  return;
644
646
  }
645
647
  else if (this.isStopping) {
646
- await this.stopNotes(0, true);
648
+ await this.stopNotes(0, true, now);
647
649
  this.notePromises = [];
648
650
  this.exclusiveClassMap.clear();
649
651
  this.audioBufferCache.clear();
@@ -653,7 +655,7 @@ class MidyGM2 {
653
655
  return;
654
656
  }
655
657
  else if (this.isSeeking) {
656
- this.stopNotes(0, true);
658
+ this.stopNotes(0, true, now);
657
659
  this.exclusiveClassMap.clear();
658
660
  this.startTime = this.audioContext.currentTime;
659
661
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -662,7 +664,6 @@ class MidyGM2 {
662
664
  await schedulePlayback();
663
665
  }
664
666
  else {
665
- const now = this.audioContext.currentTime;
666
667
  const waitTime = now + this.noteCheckInterval;
667
668
  await this.scheduleTask(() => { }, waitTime);
668
669
  await schedulePlayback();
@@ -782,25 +783,26 @@ class MidyGM2 {
782
783
  }
783
784
  return { instruments, timeline };
784
785
  }
785
- async stopChannelNotes(channelNumber, velocity, force) {
786
- const now = this.audioContext.currentTime;
786
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
787
787
  const channel = this.channels[channelNumber];
788
+ const promises = [];
788
789
  channel.scheduledNotes.forEach((noteList) => {
789
790
  for (let i = 0; i < noteList.length; i++) {
790
791
  const note = noteList[i];
791
792
  if (!note)
792
793
  continue;
793
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
794
- force);
794
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
795
795
  this.notePromises.push(promise);
796
+ promises.push(promise);
796
797
  }
797
798
  });
798
799
  channel.scheduledNotes.clear();
799
- await Promise.all(this.notePromises);
800
+ return Promise.all(promises);
800
801
  }
801
- stopNotes(velocity, force) {
802
+ stopNotes(velocity, force, scheduleTime) {
803
+ const promises = [];
802
804
  for (let i = 0; i < this.channels.length; i++) {
803
- this.stopChannelNotes(i, velocity, force);
805
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
804
806
  }
805
807
  return Promise.all(this.notePromises);
806
808
  }
@@ -860,22 +862,22 @@ class MidyGM2 {
860
862
  }
861
863
  });
862
864
  }
863
- getActiveNotes(channel, time) {
865
+ getActiveNotes(channel, scheduleTime) {
864
866
  const activeNotes = new SparseMap(128);
865
867
  channel.scheduledNotes.forEach((noteList) => {
866
- const activeNote = this.getActiveNote(noteList, time);
868
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
867
869
  if (activeNote) {
868
870
  activeNotes.set(activeNote.noteNumber, activeNote);
869
871
  }
870
872
  });
871
873
  return activeNotes;
872
874
  }
873
- getActiveNote(noteList, time) {
875
+ getActiveNote(noteList, scheduleTime) {
874
876
  for (let i = noteList.length - 1; i >= 0; i--) {
875
877
  const note = noteList[i];
876
878
  if (!note)
877
879
  return;
878
- if (time < note.startTime)
880
+ if (scheduleTime < note.startTime)
879
881
  continue;
880
882
  return (note.ending) ? null : note;
881
883
  }
@@ -1035,43 +1037,35 @@ class MidyGM2 {
1035
1037
  calcNoteDetune(channel, note) {
1036
1038
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1037
1039
  }
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
- }
1040
+ updateChannelDetune(channel, scheduleTime) {
1041
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1042
+ this.updateDetune(channel, note, scheduleTime);
1046
1043
  });
1047
1044
  }
1048
- updateDetune(channel, note) {
1049
- const now = this.audioContext.currentTime;
1045
+ updateDetune(channel, note, scheduleTime) {
1050
1046
  const noteDetune = this.calcNoteDetune(channel, note);
1051
1047
  const detune = channel.detune + noteDetune;
1052
1048
  note.bufferSource.detune
1053
- .cancelScheduledValues(now)
1054
- .setValueAtTime(detune, now);
1049
+ .cancelScheduledValues(scheduleTime)
1050
+ .setValueAtTime(detune, scheduleTime);
1055
1051
  }
1056
1052
  getPortamentoTime(channel) {
1057
1053
  const factor = 5 * Math.log(10) / 127;
1058
1054
  const time = channel.state.portamentoTime;
1059
1055
  return Math.log(time) / factor;
1060
1056
  }
1061
- setPortamentoStartVolumeEnvelope(channel, note) {
1062
- const now = this.audioContext.currentTime;
1057
+ setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1063
1058
  const { voiceParams, startTime } = note;
1064
1059
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1065
1060
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1066
1061
  const volDelay = startTime + voiceParams.volDelay;
1067
1062
  const portamentoTime = volDelay + this.getPortamentoTime(channel);
1068
1063
  note.volumeEnvelopeNode.gain
1069
- .cancelScheduledValues(now)
1064
+ .cancelScheduledValues(scheduleTime)
1070
1065
  .setValueAtTime(0, volDelay)
1071
1066
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
1072
1067
  }
1073
- setVolumeEnvelope(channel, note) {
1074
- const now = this.audioContext.currentTime;
1068
+ setVolumeEnvelope(channel, note, scheduleTime) {
1075
1069
  const { voiceParams, startTime } = note;
1076
1070
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1077
1071
  (1 + this.getAmplitudeControl(channel));
@@ -1081,7 +1075,7 @@ class MidyGM2 {
1081
1075
  const volHold = volAttack + voiceParams.volHold;
1082
1076
  const volDecay = volHold + voiceParams.volDecay;
1083
1077
  note.volumeEnvelopeNode.gain
1084
- .cancelScheduledValues(now)
1078
+ .cancelScheduledValues(scheduleTime)
1085
1079
  .setValueAtTime(0, startTime)
1086
1080
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
1087
1081
  .exponentialRampToValueAtTime(attackVolume, volAttack)
@@ -1089,7 +1083,6 @@ class MidyGM2 {
1089
1083
  .linearRampToValueAtTime(sustainVolume, volDecay);
1090
1084
  }
1091
1085
  setPitchEnvelope(note, scheduleTime) {
1092
- scheduleTime ??= this.audioContext.currentTime;
1093
1086
  const { voiceParams } = note;
1094
1087
  const baseRate = voiceParams.playbackRate;
1095
1088
  note.bufferSource.playbackRate
@@ -1116,8 +1109,7 @@ class MidyGM2 {
1116
1109
  const maxFrequency = 20000; // max Hz of initialFilterFc
1117
1110
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1118
1111
  }
1119
- setPortamentoStartFilterEnvelope(channel, note) {
1120
- const now = this.audioContext.currentTime;
1112
+ setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1121
1113
  const state = channel.state;
1122
1114
  const { voiceParams, noteNumber, startTime } = note;
1123
1115
  const softPedalFactor = 1 -
@@ -1132,13 +1124,12 @@ class MidyGM2 {
1132
1124
  const portamentoTime = startTime + this.getPortamentoTime(channel);
1133
1125
  const modDelay = startTime + voiceParams.modDelay;
1134
1126
  note.filterNode.frequency
1135
- .cancelScheduledValues(now)
1127
+ .cancelScheduledValues(scheduleTime)
1136
1128
  .setValueAtTime(adjustedBaseFreq, startTime)
1137
1129
  .setValueAtTime(adjustedBaseFreq, modDelay)
1138
1130
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1139
1131
  }
1140
- setFilterEnvelope(channel, note) {
1141
- const now = this.audioContext.currentTime;
1132
+ setFilterEnvelope(channel, note, scheduleTime) {
1142
1133
  const state = channel.state;
1143
1134
  const { voiceParams, noteNumber, startTime } = note;
1144
1135
  const softPedalFactor = 1 -
@@ -1158,14 +1149,14 @@ class MidyGM2 {
1158
1149
  const modHold = modAttack + voiceParams.modHold;
1159
1150
  const modDecay = modHold + voiceParams.modDecay;
1160
1151
  note.filterNode.frequency
1161
- .cancelScheduledValues(now)
1152
+ .cancelScheduledValues(scheduleTime)
1162
1153
  .setValueAtTime(adjustedBaseFreq, startTime)
1163
1154
  .setValueAtTime(adjustedBaseFreq, modDelay)
1164
1155
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1165
1156
  .setValueAtTime(adjustedPeekFreq, modHold)
1166
1157
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
1167
1158
  }
1168
- startModulation(channel, note, startTime) {
1159
+ startModulation(channel, note, scheduleTime) {
1169
1160
  const { voiceParams } = note;
1170
1161
  note.modulationLFO = new OscillatorNode(this.audioContext, {
1171
1162
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -1174,10 +1165,10 @@ class MidyGM2 {
1174
1165
  gain: voiceParams.modLfoToFilterFc,
1175
1166
  });
1176
1167
  note.modulationDepth = new GainNode(this.audioContext);
1177
- this.setModLfoToPitch(channel, note);
1168
+ this.setModLfoToPitch(channel, note, scheduleTime);
1178
1169
  note.volumeDepth = new GainNode(this.audioContext);
1179
- this.setModLfoToVolume(channel, note);
1180
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1170
+ this.setModLfoToVolume(note, scheduleTime);
1171
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1181
1172
  note.modulationLFO.connect(note.filterDepth);
1182
1173
  note.filterDepth.connect(note.filterNode.frequency);
1183
1174
  note.modulationLFO.connect(note.modulationDepth);
@@ -1185,15 +1176,15 @@ class MidyGM2 {
1185
1176
  note.modulationLFO.connect(note.volumeDepth);
1186
1177
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1187
1178
  }
1188
- startVibrato(channel, note, startTime) {
1179
+ startVibrato(channel, note, scheduleTime) {
1189
1180
  const { voiceParams } = note;
1190
1181
  const state = channel.state;
1191
1182
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1192
1183
  frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1193
1184
  });
1194
- note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1185
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1195
1186
  note.vibratoDepth = new GainNode(this.audioContext);
1196
- this.setVibLfoToPitch(channel, note);
1187
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1197
1188
  note.vibratoLFO.connect(note.vibratoDepth);
1198
1189
  note.vibratoDepth.connect(note.bufferSource.detune);
1199
1190
  }
@@ -1216,6 +1207,7 @@ class MidyGM2 {
1216
1207
  }
1217
1208
  }
1218
1209
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1210
+ const now = this.audioContext.currentTime;
1219
1211
  const state = channel.state;
1220
1212
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1221
1213
  const voiceParams = voice.getAllParams(controllerState);
@@ -1232,20 +1224,20 @@ class MidyGM2 {
1232
1224
  });
1233
1225
  if (portamento) {
1234
1226
  note.portamento = true;
1235
- this.setPortamentoStartVolumeEnvelope(channel, note);
1236
- this.setPortamentoStartFilterEnvelope(channel, note);
1227
+ this.setPortamentoStartVolumeEnvelope(channel, note, now);
1228
+ this.setPortamentoStartFilterEnvelope(channel, note, now);
1237
1229
  }
1238
1230
  else {
1239
1231
  note.portamento = false;
1240
- this.setVolumeEnvelope(channel, note);
1241
- this.setFilterEnvelope(channel, note);
1232
+ this.setVolumeEnvelope(channel, note, now);
1233
+ this.setFilterEnvelope(channel, note, now);
1242
1234
  }
1243
1235
  if (0 < state.vibratoDepth) {
1244
- this.startVibrato(channel, note, startTime);
1236
+ this.startVibrato(channel, note, now);
1245
1237
  }
1246
- this.setPitchEnvelope(note);
1238
+ this.setPitchEnvelope(note, now);
1247
1239
  if (0 < state.modulationDepth) {
1248
- this.startModulation(channel, note, startTime);
1240
+ this.startModulation(channel, note, now);
1249
1241
  }
1250
1242
  if (this.mono && channel.currentBufferSource) {
1251
1243
  channel.currentBufferSource.stop(startTime);
@@ -1257,10 +1249,10 @@ class MidyGM2 {
1257
1249
  note.volumeNode.connect(note.gainL);
1258
1250
  note.volumeNode.connect(note.gainR);
1259
1251
  if (0 < channel.chorusSendLevel) {
1260
- this.setChorusEffectsSend(channel, note, 0);
1252
+ this.setChorusEffectsSend(channel, note, 0, now);
1261
1253
  }
1262
1254
  if (0 < channel.reverbSendLevel) {
1263
- this.setReverbEffectsSend(channel, note, 0);
1255
+ this.setReverbEffectsSend(channel, note, 0, now);
1264
1256
  }
1265
1257
  note.bufferSource.start(startTime);
1266
1258
  return note;
@@ -1297,9 +1289,9 @@ class MidyGM2 {
1297
1289
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1298
1290
  const [prevNote, prevChannelNumber] = prevEntry;
1299
1291
  if (!prevNote.ending) {
1300
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1301
- startTime, undefined, // portamentoNoteNumber
1302
- true);
1292
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1293
+ startTime, true, // force
1294
+ undefined);
1303
1295
  }
1304
1296
  }
1305
1297
  this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
@@ -1312,9 +1304,9 @@ class MidyGM2 {
1312
1304
  scheduledNotes.set(noteNumber, [note]);
1313
1305
  }
1314
1306
  }
1315
- noteOn(channelNumber, noteNumber, velocity, portamento) {
1316
- const now = this.audioContext.currentTime;
1317
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1307
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1308
+ scheduleTime ??= this.audioContext.currentTime;
1309
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1318
1310
  }
1319
1311
  stopNote(endTime, stopTime, scheduledNotes, index) {
1320
1312
  const note = scheduledNotes[index];
@@ -1354,7 +1346,7 @@ class MidyGM2 {
1354
1346
  note.bufferSource.stop(stopTime);
1355
1347
  });
1356
1348
  }
1357
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
1349
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1358
1350
  const channel = this.channels[channelNumber];
1359
1351
  const state = channel.state;
1360
1352
  if (!force) {
@@ -1393,24 +1385,19 @@ class MidyGM2 {
1393
1385
  }
1394
1386
  }
1395
1387
  }
1396
- releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1397
- const now = this.audioContext.currentTime;
1398
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1388
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1389
+ scheduleTime ??= this.audioContext.currentTime;
1390
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1391
+ undefined);
1399
1392
  }
1400
- releaseSustainPedal(channelNumber, halfVelocity) {
1393
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1401
1394
  const velocity = halfVelocity * 2;
1402
1395
  const channel = this.channels[channelNumber];
1403
1396
  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
- }
1397
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1398
+ const { noteNumber } = note;
1399
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
1400
+ promises.push(promise);
1414
1401
  });
1415
1402
  return promises;
1416
1403
  }
@@ -1421,40 +1408,38 @@ class MidyGM2 {
1421
1408
  channel.state.sostenutoPedal = 0;
1422
1409
  channel.sostenutoNotes.forEach((activeNote) => {
1423
1410
  const { noteNumber } = activeNote;
1424
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1411
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
1425
1412
  promises.push(promise);
1426
1413
  });
1427
1414
  channel.sostenutoNotes.clear();
1428
1415
  return promises;
1429
1416
  }
1430
- handleMIDIMessage(statusByte, data1, data2) {
1417
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1431
1418
  const channelNumber = omni ? 0 : statusByte & 0x0F;
1432
1419
  const messageType = statusByte & 0xF0;
1433
1420
  switch (messageType) {
1434
1421
  case 0x80:
1435
- return this.releaseNote(channelNumber, data1, data2);
1422
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
1436
1423
  case 0x90:
1437
- return this.noteOn(channelNumber, data1, data2);
1424
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
1438
1425
  case 0xB0:
1439
- return this.handleControlChange(channelNumber, data1, data2);
1426
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1440
1427
  case 0xC0:
1441
- return this.handleProgramChange(channelNumber, data1);
1428
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
1442
1429
  case 0xD0:
1443
- return this.handleChannelPressure(channelNumber, data1);
1430
+ return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1444
1431
  case 0xE0:
1445
- return this.handlePitchBendMessage(channelNumber, data1, data2);
1432
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1446
1433
  default:
1447
1434
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1448
1435
  }
1449
1436
  }
1450
- handleProgramChange(channelNumber, program) {
1437
+ handleProgramChange(channelNumber, program, _scheduleTime) {
1451
1438
  const channel = this.channels[channelNumber];
1452
1439
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1453
1440
  channel.program = program;
1454
1441
  }
1455
- handleChannelPressure(channelNumber, value, startTime) {
1456
- if (!startTime)
1457
- startTime = this.audioContext.currentTime;
1442
+ handleChannelPressure(channelNumber, value, scheduleTime) {
1458
1443
  const channel = this.channels[channelNumber];
1459
1444
  const prev = channel.state.channelPressure;
1460
1445
  const next = value / 127;
@@ -1464,72 +1449,68 @@ class MidyGM2 {
1464
1449
  channel.detune += pressureDepth * (next - prev);
1465
1450
  }
1466
1451
  const table = channel.channelPressureTable;
1467
- this.getActiveNotes(channel, startTime).forEach((note) => {
1452
+ this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1468
1453
  this.setControllerParameters(channel, note, table);
1469
1454
  });
1470
1455
  // this.applyVoiceParams(channel, 13);
1471
1456
  }
1472
- handlePitchBendMessage(channelNumber, lsb, msb) {
1457
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1473
1458
  const pitchBend = msb * 128 + lsb;
1474
- this.setPitchBend(channelNumber, pitchBend);
1459
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1475
1460
  }
1476
- setPitchBend(channelNumber, value) {
1461
+ setPitchBend(channelNumber, value, scheduleTime) {
1462
+ scheduleTime ??= this.audioContext.currentTime;
1477
1463
  const channel = this.channels[channelNumber];
1478
1464
  const state = channel.state;
1479
1465
  const prev = state.pitchWheel * 2 - 1;
1480
1466
  const next = (value - 8192) / 8192;
1481
1467
  state.pitchWheel = value / 16383;
1482
1468
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1483
- this.updateChannelDetune(channel);
1484
- this.applyVoiceParams(channel, 14);
1469
+ this.updateChannelDetune(channel, scheduleTime);
1470
+ this.applyVoiceParams(channel, 14, scheduleTime);
1485
1471
  }
1486
- setModLfoToPitch(channel, note) {
1487
- const now = this.audioContext.currentTime;
1472
+ setModLfoToPitch(channel, note, scheduleTime) {
1488
1473
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1489
1474
  this.getLFOPitchDepth(channel);
1490
1475
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1491
1476
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1492
1477
  note.modulationDepth.gain
1493
- .cancelScheduledValues(now)
1494
- .setValueAtTime(modulationDepth, now);
1478
+ .cancelScheduledValues(scheduleTime)
1479
+ .setValueAtTime(modulationDepth, scheduleTime);
1495
1480
  }
1496
- setVibLfoToPitch(channel, note) {
1497
- const now = this.audioContext.currentTime;
1481
+ setVibLfoToPitch(channel, note, scheduleTime) {
1498
1482
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1499
1483
  const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1500
1484
  2;
1501
1485
  const vibratoDepthSign = 0 < vibLfoToPitch;
1502
1486
  note.vibratoDepth.gain
1503
- .cancelScheduledValues(now)
1504
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1487
+ .cancelScheduledValues(scheduleTime)
1488
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, scheduleTime);
1505
1489
  }
1506
- setModLfoToFilterFc(channel, note) {
1507
- const now = this.audioContext.currentTime;
1490
+ setModLfoToFilterFc(channel, note, scheduleTime) {
1508
1491
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1509
1492
  this.getLFOFilterDepth(channel);
1510
1493
  note.filterDepth.gain
1511
- .cancelScheduledValues(now)
1512
- .setValueAtTime(modLfoToFilterFc, now);
1494
+ .cancelScheduledValues(scheduleTime)
1495
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
1513
1496
  }
1514
- setModLfoToVolume(channel, note) {
1515
- const now = this.audioContext.currentTime;
1497
+ setModLfoToVolume(channel, note, scheduleTime) {
1516
1498
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1517
1499
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1518
1500
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1519
1501
  (1 + this.getLFOAmplitudeDepth(channel));
1520
1502
  note.volumeDepth.gain
1521
- .cancelScheduledValues(now)
1522
- .setValueAtTime(volumeDepth, now);
1503
+ .cancelScheduledValues(scheduleTime)
1504
+ .setValueAtTime(volumeDepth, scheduleTime);
1523
1505
  }
1524
- setReverbEffectsSend(channel, note, prevValue) {
1506
+ setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1525
1507
  if (0 < prevValue) {
1526
1508
  if (0 < note.voiceParams.reverbEffectsSend) {
1527
- const now = this.audioContext.currentTime;
1528
1509
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1529
1510
  const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1530
1511
  note.reverbEffectsSend.gain
1531
- .cancelScheduledValues(now)
1532
- .setValueAtTime(value, now);
1512
+ .cancelScheduledValues(scheduleTime)
1513
+ .setValueAtTime(value, scheduleTime);
1533
1514
  }
1534
1515
  else {
1535
1516
  note.reverbEffectsSend.disconnect();
@@ -1547,15 +1528,14 @@ class MidyGM2 {
1547
1528
  }
1548
1529
  }
1549
1530
  }
1550
- setChorusEffectsSend(channel, note, prevValue) {
1531
+ setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1551
1532
  if (0 < prevValue) {
1552
1533
  if (0 < note.voiceParams.chorusEffectsSend) {
1553
- const now = this.audioContext.currentTime;
1554
1534
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1555
1535
  const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1556
1536
  note.chorusEffectsSend.gain
1557
- .cancelScheduledValues(now)
1558
- .setValueAtTime(value, now);
1537
+ .cancelScheduledValues(scheduleTime)
1538
+ .setValueAtTime(value, scheduleTime);
1559
1539
  }
1560
1540
  else {
1561
1541
  note.chorusEffectsSend.disconnect();
@@ -1573,75 +1553,71 @@ class MidyGM2 {
1573
1553
  }
1574
1554
  }
1575
1555
  }
1576
- setDelayModLFO(note) {
1577
- const now = this.audioContext.currentTime;
1556
+ setDelayModLFO(note, scheduleTime) {
1578
1557
  const startTime = note.startTime;
1579
- if (startTime < now)
1558
+ if (startTime < scheduleTime)
1580
1559
  return;
1581
- note.modulationLFO.stop(now);
1560
+ note.modulationLFO.stop(scheduleTime);
1582
1561
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1583
1562
  note.modulationLFO.connect(note.filterDepth);
1584
1563
  }
1585
- setFreqModLFO(note) {
1586
- const now = this.audioContext.currentTime;
1564
+ setFreqModLFO(note, scheduleTime) {
1587
1565
  const freqModLFO = note.voiceParams.freqModLFO;
1588
1566
  note.modulationLFO.frequency
1589
- .cancelScheduledValues(now)
1590
- .setValueAtTime(freqModLFO, now);
1567
+ .cancelScheduledValues(scheduleTime)
1568
+ .setValueAtTime(freqModLFO, scheduleTime);
1591
1569
  }
1592
- setFreqVibLFO(channel, note) {
1593
- const now = this.audioContext.currentTime;
1570
+ setFreqVibLFO(channel, note, scheduleTime) {
1594
1571
  const freqVibLFO = note.voiceParams.freqVibLFO;
1595
1572
  note.vibratoLFO.frequency
1596
- .cancelScheduledValues(now)
1597
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1573
+ .cancelScheduledValues(scheduleTime)
1574
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, scheduleTime);
1598
1575
  }
1599
1576
  createVoiceParamsHandlers() {
1600
1577
  return {
1601
- modLfoToPitch: (channel, note, _prevValue) => {
1578
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1602
1579
  if (0 < channel.state.modulationDepth) {
1603
- this.setModLfoToPitch(channel, note);
1580
+ this.setModLfoToPitch(channel, note, scheduleTime);
1604
1581
  }
1605
1582
  },
1606
- vibLfoToPitch: (channel, note, _prevValue) => {
1583
+ vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1607
1584
  if (0 < channel.state.vibratoDepth) {
1608
- this.setVibLfoToPitch(channel, note);
1585
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1609
1586
  }
1610
1587
  },
1611
- modLfoToFilterFc: (channel, note, _prevValue) => {
1588
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1612
1589
  if (0 < channel.state.modulationDepth) {
1613
- this.setModLfoToFilterFc(channel, note);
1590
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
1614
1591
  }
1615
1592
  },
1616
- modLfoToVolume: (channel, note, _prevValue) => {
1593
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1617
1594
  if (0 < channel.state.modulationDepth) {
1618
- this.setModLfoToVolume(channel, note);
1595
+ this.setModLfoToVolume(channel, note, scheduleTime);
1619
1596
  }
1620
1597
  },
1621
- chorusEffectsSend: (channel, note, prevValue) => {
1622
- this.setChorusEffectsSend(channel, note, prevValue);
1598
+ chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1599
+ this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1623
1600
  },
1624
- reverbEffectsSend: (channel, note, prevValue) => {
1625
- this.setReverbEffectsSend(channel, note, prevValue);
1601
+ reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1602
+ this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1626
1603
  },
1627
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1628
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1629
- delayVibLFO: (channel, note, prevValue) => {
1604
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1605
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1606
+ delayVibLFO: (channel, note, prevValue, scheduleTime) => {
1630
1607
  if (0 < channel.state.vibratoDepth) {
1631
- const now = this.audioContext.currentTime;
1632
1608
  const vibratoDelay = channel.state.vibratoDelay * 2;
1633
1609
  const prevStartTime = note.startTime + prevValue * vibratoDelay;
1634
- if (now < prevStartTime)
1610
+ if (scheduleTime < prevStartTime)
1635
1611
  return;
1636
1612
  const value = note.voiceParams.delayVibLFO;
1637
1613
  const startTime = note.startTime + value * vibratoDelay;
1638
- note.vibratoLFO.stop(now);
1614
+ note.vibratoLFO.stop(scheduleTime);
1639
1615
  note.vibratoLFO.start(startTime);
1640
1616
  }
1641
1617
  },
1642
- freqVibLFO: (channel, note, _prevValue) => {
1618
+ freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1643
1619
  if (0 < channel.state.vibratoDepth) {
1644
- this.setFreqVibLFO(channel, note);
1620
+ this.setFreqVibLFO(channel, note, scheduleTime);
1645
1621
  }
1646
1622
  },
1647
1623
  };
@@ -1653,7 +1629,7 @@ class MidyGM2 {
1653
1629
  state[3] = noteNumber / 127;
1654
1630
  return state;
1655
1631
  }
1656
- applyVoiceParams(channel, controllerType) {
1632
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1657
1633
  channel.scheduledNotes.forEach((noteList) => {
1658
1634
  for (let i = 0; i < noteList.length; i++) {
1659
1635
  const note = noteList[i];
@@ -1669,7 +1645,7 @@ class MidyGM2 {
1669
1645
  continue;
1670
1646
  note.voiceParams[key] = value;
1671
1647
  if (key in this.voiceParamsHandlers) {
1672
- this.voiceParamsHandlers[key](channel, note, prevValue);
1648
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1673
1649
  }
1674
1650
  else if (filterEnvelopeKeySet.has(key)) {
1675
1651
  if (appliedFilterEnvelope)
@@ -1682,12 +1658,12 @@ class MidyGM2 {
1682
1658
  noteVoiceParams[key] = voiceParams[key];
1683
1659
  }
1684
1660
  if (note.portamento) {
1685
- this.setPortamentoStartFilterEnvelope(channel, note);
1661
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1686
1662
  }
1687
1663
  else {
1688
- this.setFilterEnvelope(channel, note);
1664
+ this.setFilterEnvelope(channel, note, scheduleTime);
1689
1665
  }
1690
- this.setPitchEnvelope(note);
1666
+ this.setPitchEnvelope(note, scheduleTime);
1691
1667
  }
1692
1668
  else if (volumeEnvelopeKeySet.has(key)) {
1693
1669
  if (appliedVolumeEnvelope)
@@ -1699,7 +1675,7 @@ class MidyGM2 {
1699
1675
  if (key in voiceParams)
1700
1676
  noteVoiceParams[key] = voiceParams[key];
1701
1677
  }
1702
- this.setVolumeEnvelope(channel, note);
1678
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1703
1679
  }
1704
1680
  }
1705
1681
  }
@@ -1733,12 +1709,12 @@ class MidyGM2 {
1733
1709
  127: this.polyOn,
1734
1710
  };
1735
1711
  }
1736
- handleControlChange(channelNumber, controllerType, value, startTime) {
1712
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1737
1713
  const handler = this.controlChangeHandlers[controllerType];
1738
1714
  if (handler) {
1739
- handler.call(this, channelNumber, value, startTime);
1715
+ handler.call(this, channelNumber, value, scheduleTime);
1740
1716
  const channel = this.channels[channelNumber];
1741
- this.applyVoiceParams(channel, controllerType + 128);
1717
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1742
1718
  this.applyControlTable(channel, controllerType);
1743
1719
  }
1744
1720
  else {
@@ -1824,56 +1800,55 @@ class MidyGM2 {
1824
1800
  setBankLSB(channelNumber, lsb) {
1825
1801
  this.channels[channelNumber].bankLSB = lsb;
1826
1802
  }
1827
- dataEntryLSB(channelNumber, value) {
1803
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1828
1804
  this.channels[channelNumber].dataLSB = value;
1829
- this.handleRPN(channelNumber);
1805
+ this.handleRPN(channelNumber, scheduleTime);
1830
1806
  }
1831
- updateChannelVolume(channel) {
1832
- const now = this.audioContext.currentTime;
1807
+ updateChannelVolume(channel, scheduleTime) {
1833
1808
  const state = channel.state;
1834
1809
  const volume = state.volume * state.expression;
1835
1810
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1836
1811
  channel.gainL.gain
1837
- .cancelScheduledValues(now)
1838
- .setValueAtTime(volume * gainLeft, now);
1812
+ .cancelScheduledValues(scheduleTime)
1813
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1839
1814
  channel.gainR.gain
1840
- .cancelScheduledValues(now)
1841
- .setValueAtTime(volume * gainRight, now);
1815
+ .cancelScheduledValues(scheduleTime)
1816
+ .setValueAtTime(volume * gainRight, scheduleTime);
1842
1817
  }
1843
- setSustainPedal(channelNumber, value) {
1818
+ setSustainPedal(channelNumber, value, scheduleTime) {
1819
+ scheduleTime ??= this.audioContext.currentTime;
1844
1820
  this.channels[channelNumber].state.sustainPedal = value / 127;
1845
1821
  if (value < 64) {
1846
- this.releaseSustainPedal(channelNumber, value);
1822
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1847
1823
  }
1848
1824
  }
1849
1825
  setPortamento(channelNumber, value) {
1850
1826
  this.channels[channelNumber].state.portamento = value / 127;
1851
1827
  }
1852
- setSostenutoPedal(channelNumber, value) {
1828
+ setSostenutoPedal(channelNumber, value, scheduleTime) {
1853
1829
  const channel = this.channels[channelNumber];
1854
1830
  channel.state.sostenutoPedal = value / 127;
1855
1831
  if (64 <= value) {
1856
- const now = this.audioContext.currentTime;
1857
- channel.sostenutoNotes = this.getActiveNotes(channel, now);
1832
+ channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
1858
1833
  }
1859
1834
  else {
1860
1835
  this.releaseSostenutoPedal(channelNumber, value);
1861
1836
  }
1862
1837
  }
1863
- setSoftPedal(channelNumber, softPedal) {
1838
+ setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1864
1839
  const channel = this.channels[channelNumber];
1865
1840
  channel.state.softPedal = softPedal / 127;
1866
1841
  }
1867
- setReverbSendLevel(channelNumber, reverbSendLevel) {
1842
+ setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
1868
1843
  const channel = this.channels[channelNumber];
1869
1844
  const state = channel.state;
1870
1845
  const reverbEffect = this.reverbEffect;
1871
1846
  if (0 < state.reverbSendLevel) {
1872
1847
  if (0 < reverbSendLevel) {
1873
- const now = this.audioContext.currentTime;
1874
1848
  state.reverbSendLevel = reverbSendLevel / 127;
1875
- reverbEffect.input.gain.cancelScheduledValues(now);
1876
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1849
+ reverbEffect.input.gain
1850
+ .cancelScheduledValues(scheduleTime)
1851
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
1877
1852
  }
1878
1853
  else {
1879
1854
  channel.scheduledNotes.forEach((noteList) => {
@@ -1890,31 +1865,31 @@ class MidyGM2 {
1890
1865
  }
1891
1866
  else {
1892
1867
  if (0 < reverbSendLevel) {
1893
- const now = this.audioContext.currentTime;
1894
1868
  channel.scheduledNotes.forEach((noteList) => {
1895
1869
  for (let i = 0; i < noteList.length; i++) {
1896
1870
  const note = noteList[i];
1897
1871
  if (!note)
1898
1872
  continue;
1899
- this.setReverbEffectsSend(channel, note, 0);
1873
+ this.setReverbEffectsSend(channel, note, 0, scheduleTime);
1900
1874
  }
1901
1875
  });
1902
1876
  state.reverbSendLevel = reverbSendLevel / 127;
1903
- reverbEffect.input.gain.cancelScheduledValues(now);
1904
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1877
+ reverbEffect.input.gain
1878
+ .cancelScheduledValues(scheduleTime)
1879
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
1905
1880
  }
1906
1881
  }
1907
1882
  }
1908
- setChorusSendLevel(channelNumber, chorusSendLevel) {
1883
+ setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
1909
1884
  const channel = this.channels[channelNumber];
1910
1885
  const state = channel.state;
1911
1886
  const chorusEffect = this.chorusEffect;
1912
1887
  if (0 < state.chorusSendLevel) {
1913
1888
  if (0 < chorusSendLevel) {
1914
- const now = this.audioContext.currentTime;
1915
1889
  state.chorusSendLevel = chorusSendLevel / 127;
1916
- chorusEffect.input.gain.cancelScheduledValues(now);
1917
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1890
+ chorusEffect.input.gain
1891
+ .cancelScheduledValues(scheduleTime)
1892
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
1918
1893
  }
1919
1894
  else {
1920
1895
  channel.scheduledNotes.forEach((noteList) => {
@@ -1931,18 +1906,18 @@ class MidyGM2 {
1931
1906
  }
1932
1907
  else {
1933
1908
  if (0 < chorusSendLevel) {
1934
- const now = this.audioContext.currentTime;
1935
1909
  channel.scheduledNotes.forEach((noteList) => {
1936
1910
  for (let i = 0; i < noteList.length; i++) {
1937
1911
  const note = noteList[i];
1938
1912
  if (!note)
1939
1913
  continue;
1940
- this.setChorusEffectsSend(channel, note, 0);
1914
+ this.setChorusEffectsSend(channel, note, 0, scheduleTime);
1941
1915
  }
1942
1916
  });
1943
1917
  state.chorusSendLevel = chorusSendLevel / 127;
1944
- chorusEffect.input.gain.cancelScheduledValues(now);
1945
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1918
+ chorusEffect.input.gain
1919
+ .cancelScheduledValues(scheduleTime)
1920
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
1946
1921
  }
1947
1922
  }
1948
1923
  }
@@ -1972,12 +1947,12 @@ class MidyGM2 {
1972
1947
  channel.dataMSB = minMSB;
1973
1948
  }
1974
1949
  }
1975
- handleRPN(channelNumber) {
1950
+ handleRPN(channelNumber, scheduleTime) {
1976
1951
  const channel = this.channels[channelNumber];
1977
1952
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1978
1953
  switch (rpn) {
1979
1954
  case 0:
1980
- this.handlePitchBendRangeRPN(channelNumber);
1955
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1981
1956
  break;
1982
1957
  case 1:
1983
1958
  this.handleFineTuningRPN(channelNumber);
@@ -1998,25 +1973,26 @@ class MidyGM2 {
1998
1973
  setRPNLSB(channelNumber, value) {
1999
1974
  this.channels[channelNumber].rpnLSB = value;
2000
1975
  }
2001
- dataEntryMSB(channelNumber, value) {
1976
+ dataEntryMSB(channelNumber, value, scheduleTime) {
2002
1977
  this.channels[channelNumber].dataMSB = value;
2003
- this.handleRPN(channelNumber);
1978
+ this.handleRPN(channelNumber, scheduleTime);
2004
1979
  }
2005
- handlePitchBendRangeRPN(channelNumber) {
1980
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2006
1981
  const channel = this.channels[channelNumber];
2007
1982
  this.limitData(channel, 0, 127, 0, 99);
2008
1983
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2009
- this.setPitchBendRange(channelNumber, pitchBendRange);
1984
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2010
1985
  }
2011
- setPitchBendRange(channelNumber, value) {
1986
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1987
+ scheduleTime ??= this.audioContext.currentTime;
2012
1988
  const channel = this.channels[channelNumber];
2013
1989
  const state = channel.state;
2014
1990
  const prev = state.pitchWheelSensitivity;
2015
1991
  const next = value / 128;
2016
1992
  state.pitchWheelSensitivity = next;
2017
1993
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2018
- this.updateChannelDetune(channel);
2019
- this.applyVoiceParams(channel, 16);
1994
+ this.updateChannelDetune(channel, scheduleTime);
1995
+ this.applyVoiceParams(channel, 16, scheduleTime);
2020
1996
  }
2021
1997
  handleFineTuningRPN(channelNumber) {
2022
1998
  const channel = this.channels[channelNumber];
@@ -2057,8 +2033,9 @@ class MidyGM2 {
2057
2033
  channel.modulationDepthRange = modulationDepthRange;
2058
2034
  this.updateModulation(channel);
2059
2035
  }
2060
- allSoundOff(channelNumber) {
2061
- return this.stopChannelNotes(channelNumber, 0, true);
2036
+ allSoundOff(channelNumber, _value, scheduleTime) {
2037
+ scheduleTime ??= this.audioContext.currentTime;
2038
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2062
2039
  }
2063
2040
  resetAllControllers(channelNumber) {
2064
2041
  const stateTypes = [
@@ -2086,8 +2063,9 @@ class MidyGM2 {
2086
2063
  channel[type] = this.constructor.channelSettings[type];
2087
2064
  }
2088
2065
  }
2089
- allNotesOff(channelNumber) {
2090
- return this.stopChannelNotes(channelNumber, 0, false);
2066
+ allNotesOff(channelNumber, _value, scheduleTime) {
2067
+ scheduleTime ??= this.audioContext.currentTime;
2068
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2091
2069
  }
2092
2070
  omniOff() {
2093
2071
  this.omni = false;
@@ -2101,13 +2079,13 @@ class MidyGM2 {
2101
2079
  polyOn() {
2102
2080
  this.mono = false;
2103
2081
  }
2104
- handleUniversalNonRealTimeExclusiveMessage(data) {
2082
+ handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
2105
2083
  switch (data[2]) {
2106
2084
  case 8:
2107
2085
  switch (data[3]) {
2108
2086
  case 8:
2109
2087
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2110
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2088
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false, scheduleTime);
2111
2089
  default:
2112
2090
  console.warn(`Unsupported Exclusive Message: ${data}`);
2113
2091
  }
@@ -2150,18 +2128,18 @@ class MidyGM2 {
2150
2128
  this.channels[9].bankMSB = 120;
2151
2129
  this.channels[9].bank = 120 * 128;
2152
2130
  }
2153
- handleUniversalRealTimeExclusiveMessage(data) {
2131
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2154
2132
  switch (data[2]) {
2155
2133
  case 4:
2156
2134
  switch (data[3]) {
2157
2135
  case 1:
2158
- return this.handleMasterVolumeSysEx(data);
2136
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
2159
2137
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2160
- return this.handleMasterFineTuningSysEx(data);
2138
+ return this.handleMasterFineTuningSysEx(data, scheduleTime);
2161
2139
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2162
- return this.handleMasterCoarseTuningSysEx(data);
2140
+ return this.handleMasterCoarseTuningSysEx(data, scheduleTime);
2163
2141
  case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2164
- return this.handleGlobalParameterControlSysEx(data);
2142
+ return this.handleGlobalParameterControlSysEx(data, scheduleTime);
2165
2143
  default:
2166
2144
  console.warn(`Unsupported Exclusive Message: ${data}`);
2167
2145
  }
@@ -2179,7 +2157,7 @@ class MidyGM2 {
2179
2157
  case 10:
2180
2158
  switch (data[3]) {
2181
2159
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2182
- return this.handleKeyBasedInstrumentControlSysEx(data);
2160
+ return this.handleKeyBasedInstrumentControlSysEx(data, scheduleTime);
2183
2161
  default:
2184
2162
  console.warn(`Unsupported Exclusive Message: ${data}`);
2185
2163
  }
@@ -2188,49 +2166,50 @@ class MidyGM2 {
2188
2166
  console.warn(`Unsupported Exclusive Message: ${data}`);
2189
2167
  }
2190
2168
  }
2191
- handleMasterVolumeSysEx(data) {
2169
+ handleMasterVolumeSysEx(data, scheduleTime) {
2192
2170
  const volume = (data[5] * 128 + data[4]) / 16383;
2193
- this.setMasterVolume(volume);
2171
+ this.setMasterVolume(volume, scheduleTime);
2194
2172
  }
2195
- setMasterVolume(volume) {
2173
+ setMasterVolume(volume, scheduleTime) {
2174
+ scheduleTime ??= this.audioContext.currentTime;
2196
2175
  if (volume < 0 && 1 < volume) {
2197
2176
  console.error("Master Volume is out of range");
2198
2177
  }
2199
2178
  else {
2200
- const now = this.audioContext.currentTime;
2201
- this.masterVolume.gain.cancelScheduledValues(now);
2202
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
2179
+ this.masterVolume.gain
2180
+ .cancelScheduledValues(scheduleTime)
2181
+ .setValueAtTime(volume * volume, scheduleTime);
2203
2182
  }
2204
2183
  }
2205
- handleMasterFineTuningSysEx(data) {
2184
+ handleMasterFineTuningSysEx(data, scheduleTime) {
2206
2185
  const fineTuning = data[5] * 128 + data[4];
2207
- this.setMasterFineTuning(fineTuning);
2186
+ this.setMasterFineTuning(fineTuning, scheduleTime);
2208
2187
  }
2209
- setMasterFineTuning(value) {
2188
+ setMasterFineTuning(value, scheduleTime) {
2210
2189
  const prev = this.masterFineTuning;
2211
2190
  const next = (value - 8192) / 8.192; // cent
2212
2191
  this.masterFineTuning = next;
2213
2192
  channel.detune += next - prev;
2214
- this.updateChannelDetune(channel);
2193
+ this.updateChannelDetune(channel, scheduleTime);
2215
2194
  }
2216
- handleMasterCoarseTuningSysEx(data) {
2195
+ handleMasterCoarseTuningSysEx(data, scheduleTime) {
2217
2196
  const coarseTuning = data[4];
2218
- this.setMasterCoarseTuning(coarseTuning);
2197
+ this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2219
2198
  }
2220
- setMasterCoarseTuning(value) {
2199
+ setMasterCoarseTuning(value, scheduleTime) {
2221
2200
  const prev = this.masterCoarseTuning;
2222
2201
  const next = (value - 64) * 100; // cent
2223
2202
  this.masterCoarseTuning = next;
2224
2203
  channel.detune += next - prev;
2225
- this.updateChannelDetune(channel);
2204
+ this.updateChannelDetune(channel, scheduleTime);
2226
2205
  }
2227
- handleGlobalParameterControlSysEx(data) {
2206
+ handleGlobalParameterControlSysEx(data, scheduleTime) {
2228
2207
  if (data[7] === 1) {
2229
2208
  switch (data[8]) {
2230
2209
  case 1:
2231
2210
  return this.handleReverbParameterSysEx(data);
2232
2211
  case 2:
2233
- return this.handleChorusParameterSysEx(data);
2212
+ return this.handleChorusParameterSysEx(data, scheduleTime);
2234
2213
  default:
2235
2214
  console.warn(`Unsupported Global Parameter Control Message: ${data}`);
2236
2215
  }
@@ -2309,88 +2288,84 @@ class MidyGM2 {
2309
2288
  calcDelay(rt60, feedback) {
2310
2289
  return -rt60 * Math.log10(feedback) / 3;
2311
2290
  }
2312
- handleChorusParameterSysEx(data) {
2291
+ handleChorusParameterSysEx(data, scheduleTime) {
2313
2292
  switch (data[9]) {
2314
2293
  case 0:
2315
- return this.setChorusType(data[10]);
2294
+ return this.setChorusType(data[10], scheduleTime);
2316
2295
  case 1:
2317
- return this.setChorusModRate(data[10]);
2296
+ return this.setChorusModRate(data[10], scheduleTime);
2318
2297
  case 2:
2319
- return this.setChorusModDepth(data[10]);
2298
+ return this.setChorusModDepth(data[10], scheduleTime);
2320
2299
  case 3:
2321
- return this.setChorusFeedback(data[10]);
2300
+ return this.setChorusFeedback(data[10], scheduleTime);
2322
2301
  case 4:
2323
- return this.setChorusSendToReverb(data[10]);
2302
+ return this.setChorusSendToReverb(data[10], scheduleTime);
2324
2303
  }
2325
2304
  }
2326
- setChorusType(type) {
2305
+ setChorusType(type, scheduleTime) {
2327
2306
  switch (type) {
2328
2307
  case 0:
2329
- return this.setChorusParameter(3, 5, 0, 0);
2308
+ return this.setChorusParameter(3, 5, 0, 0, scheduleTime);
2330
2309
  case 1:
2331
- return this.setChorusParameter(9, 19, 5, 0);
2310
+ return this.setChorusParameter(9, 19, 5, 0, scheduleTime);
2332
2311
  case 2:
2333
- return this.setChorusParameter(3, 19, 8, 0);
2312
+ return this.setChorusParameter(3, 19, 8, 0, scheduleTime);
2334
2313
  case 3:
2335
- return this.setChorusParameter(9, 16, 16, 0);
2314
+ return this.setChorusParameter(9, 16, 16, 0, scheduleTime);
2336
2315
  case 4:
2337
- return this.setChorusParameter(2, 24, 64, 0);
2316
+ return this.setChorusParameter(2, 24, 64, 0, scheduleTime);
2338
2317
  case 5:
2339
- return this.setChorusParameter(1, 5, 112, 0);
2318
+ return this.setChorusParameter(1, 5, 112, 0, scheduleTime);
2340
2319
  default:
2341
2320
  console.warn(`Unsupported Chorus Type: ${type}`);
2342
2321
  }
2343
2322
  }
2344
- setChorusParameter(modRate, modDepth, feedback, sendToReverb) {
2345
- this.setChorusModRate(modRate);
2346
- this.setChorusModDepth(modDepth);
2347
- this.setChorusFeedback(feedback);
2348
- this.setChorusSendToReverb(sendToReverb);
2323
+ setChorusParameter(modRate, modDepth, feedback, sendToReverb, scheduleTime) {
2324
+ this.setChorusModRate(modRate, scheduleTime);
2325
+ this.setChorusModDepth(modDepth, scheduleTime);
2326
+ this.setChorusFeedback(feedback, scheduleTime);
2327
+ this.setChorusSendToReverb(sendToReverb, scheduleTime);
2349
2328
  }
2350
- setChorusModRate(value) {
2351
- const now = this.audioContext.currentTime;
2329
+ setChorusModRate(value, scheduleTime) {
2352
2330
  const modRate = this.getChorusModRate(value);
2353
2331
  this.chorus.modRate = modRate;
2354
- this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
2332
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, scheduleTime);
2355
2333
  }
2356
2334
  getChorusModRate(value) {
2357
2335
  return value * 0.122; // Hz
2358
2336
  }
2359
- setChorusModDepth(value) {
2360
- const now = this.audioContext.currentTime;
2337
+ setChorusModDepth(value, scheduleTime) {
2361
2338
  const modDepth = this.getChorusModDepth(value);
2362
2339
  this.chorus.modDepth = modDepth;
2363
2340
  this.chorusEffect.lfoGain.gain
2364
- .cancelScheduledValues(now)
2365
- .setValueAtTime(modDepth / 2, now);
2341
+ .cancelScheduledValues(scheduleTime)
2342
+ .setValueAtTime(modDepth / 2, scheduleTime);
2366
2343
  }
2367
2344
  getChorusModDepth(value) {
2368
2345
  return (value + 1) / 3200; // second
2369
2346
  }
2370
- setChorusFeedback(value) {
2371
- const now = this.audioContext.currentTime;
2347
+ setChorusFeedback(value, scheduleTime) {
2372
2348
  const feedback = this.getChorusFeedback(value);
2373
2349
  this.chorus.feedback = feedback;
2374
2350
  const chorusEffect = this.chorusEffect;
2375
2351
  for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
2376
2352
  chorusEffect.feedbackGains[i].gain
2377
- .cancelScheduledValues(now)
2378
- .setValueAtTime(feedback, now);
2353
+ .cancelScheduledValues(scheduleTime)
2354
+ .setValueAtTime(feedback, scheduleTime);
2379
2355
  }
2380
2356
  }
2381
2357
  getChorusFeedback(value) {
2382
2358
  return value * 0.00763;
2383
2359
  }
2384
- setChorusSendToReverb(value) {
2360
+ setChorusSendToReverb(value, scheduleTime) {
2385
2361
  const sendToReverb = this.getChorusSendToReverb(value);
2386
2362
  const sendGain = this.chorusEffect.sendGain;
2387
2363
  if (0 < this.chorus.sendToReverb) {
2388
2364
  this.chorus.sendToReverb = sendToReverb;
2389
2365
  if (0 < sendToReverb) {
2390
- const now = this.audioContext.currentTime;
2391
2366
  sendGain.gain
2392
- .cancelScheduledValues(now)
2393
- .setValueAtTime(sendToReverb, now);
2367
+ .cancelScheduledValues(scheduleTime)
2368
+ .setValueAtTime(sendToReverb, scheduleTime);
2394
2369
  }
2395
2370
  else {
2396
2371
  sendGain.disconnect();
@@ -2399,11 +2374,10 @@ class MidyGM2 {
2399
2374
  else {
2400
2375
  this.chorus.sendToReverb = sendToReverb;
2401
2376
  if (0 < sendToReverb) {
2402
- const now = this.audioContext.currentTime;
2403
2377
  sendGain.connect(this.reverbEffect.input);
2404
2378
  sendGain.gain
2405
- .cancelScheduledValues(now)
2406
- .setValueAtTime(sendToReverb, now);
2379
+ .cancelScheduledValues(scheduleTime)
2380
+ .setValueAtTime(sendToReverb, scheduleTime);
2407
2381
  }
2408
2382
  }
2409
2383
  }
@@ -2429,7 +2403,7 @@ class MidyGM2 {
2429
2403
  }
2430
2404
  return bitmap;
2431
2405
  }
2432
- handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2406
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime, scheduleTime) {
2433
2407
  if (data.length < 19) {
2434
2408
  console.error("Data length is too short");
2435
2409
  return;
@@ -2444,7 +2418,7 @@ class MidyGM2 {
2444
2418
  channel.scaleOctaveTuningTable[j] = centValue;
2445
2419
  }
2446
2420
  if (realtime)
2447
- this.updateChannelDetune(channel);
2421
+ this.updateChannelDetune(channel, scheduleTime);
2448
2422
  }
2449
2423
  }
2450
2424
  getFilterCutoffControl(channel) {
@@ -2488,7 +2462,7 @@ class MidyGM2 {
2488
2462
  if (table[5] !== 0)
2489
2463
  this.setModLfoToVolume(channel, note);
2490
2464
  }
2491
- handleChannelPressureSysEx(data, tableName) {
2465
+ handlePressureSysEx(data, tableName) {
2492
2466
  const channelNumber = data[4];
2493
2467
  const table = this.channels[channelNumber][tableName];
2494
2468
  for (let i = 5; i < data.length - 1; i += 2) {
@@ -2536,7 +2510,7 @@ class MidyGM2 {
2536
2510
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2537
2511
  return (controlValue + 64) / 64;
2538
2512
  }
2539
- handleKeyBasedInstrumentControlSysEx(data) {
2513
+ handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2540
2514
  const channelNumber = data[4];
2541
2515
  const keyNumber = data[5];
2542
2516
  const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
@@ -2546,30 +2520,27 @@ class MidyGM2 {
2546
2520
  const index = keyNumber * 128 + controllerType;
2547
2521
  table[index] = value - 64;
2548
2522
  }
2549
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2523
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2550
2524
  }
2551
- handleExclusiveMessage(data) {
2552
- console.warn(`Unsupported Exclusive Message: ${data}`);
2553
- }
2554
- handleSysEx(data) {
2525
+ handleSysEx(data, scheduleTime) {
2555
2526
  switch (data[0]) {
2556
2527
  case 126:
2557
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
2528
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
2558
2529
  case 127:
2559
- return this.handleUniversalRealTimeExclusiveMessage(data);
2530
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
2560
2531
  default:
2561
- return this.handleExclusiveMessage(data);
2532
+ console.warn(`Unsupported Exclusive Message: ${data}`);
2562
2533
  }
2563
2534
  }
2564
- scheduleTask(callback, startTime) {
2535
+ scheduleTask(callback, scheduleTime) {
2565
2536
  return new Promise((resolve) => {
2566
2537
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
2567
2538
  bufferSource.onended = () => {
2568
2539
  callback();
2569
2540
  resolve();
2570
2541
  };
2571
- bufferSource.start(startTime);
2572
- bufferSource.stop(startTime);
2542
+ bufferSource.start(scheduleTime);
2543
+ bufferSource.stop(scheduleTime);
2573
2544
  });
2574
2545
  }
2575
2546
  }