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