@marmooo/midy 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/midy.js CHANGED
@@ -66,31 +66,37 @@ class Note {
66
66
  writable: true,
67
67
  value: void 0
68
68
  });
69
+ Object.defineProperty(this, "filterDepth", {
70
+ enumerable: true,
71
+ configurable: true,
72
+ writable: true,
73
+ value: void 0
74
+ });
69
75
  Object.defineProperty(this, "volumeEnvelopeNode", {
70
76
  enumerable: true,
71
77
  configurable: true,
72
78
  writable: true,
73
79
  value: void 0
74
80
  });
75
- Object.defineProperty(this, "volumeNode", {
81
+ Object.defineProperty(this, "volumeDepth", {
76
82
  enumerable: true,
77
83
  configurable: true,
78
84
  writable: true,
79
85
  value: void 0
80
86
  });
81
- Object.defineProperty(this, "gainL", {
87
+ Object.defineProperty(this, "volumeNode", {
82
88
  enumerable: true,
83
89
  configurable: true,
84
90
  writable: true,
85
91
  value: void 0
86
92
  });
87
- Object.defineProperty(this, "gainR", {
93
+ Object.defineProperty(this, "gainL", {
88
94
  enumerable: true,
89
95
  configurable: true,
90
96
  writable: true,
91
97
  value: void 0
92
98
  });
93
- Object.defineProperty(this, "volumeDepth", {
99
+ Object.defineProperty(this, "gainR", {
94
100
  enumerable: true,
95
101
  configurable: true,
96
102
  writable: true,
@@ -499,6 +505,10 @@ export class Midy {
499
505
  ...this.setChannelAudioNodes(audioContext),
500
506
  scheduledNotes: new SparseMap(128),
501
507
  sostenutoNotes: new SparseMap(128),
508
+ scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
509
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
510
+ polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
511
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
502
512
  };
503
513
  });
504
514
  return channels;
@@ -565,10 +575,11 @@ export class Midy {
565
575
  const event = this.timeline[queueIndex];
566
576
  if (event.startTime > t + this.lookAhead)
567
577
  break;
578
+ const startTime = event.startTime + this.startDelay - offset;
568
579
  switch (event.type) {
569
580
  case "noteOn":
570
581
  if (event.velocity !== 0) {
571
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
582
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
572
583
  break;
573
584
  }
574
585
  /* falls through */
@@ -576,29 +587,30 @@ export class Midy {
576
587
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
577
588
  if (portamentoTarget)
578
589
  portamentoTarget.portamento = true;
579
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
590
+ const notePromise = this.scheduleNoteOff(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, false, // force
591
+ portamentoTarget?.noteNumber);
580
592
  if (notePromise) {
581
593
  this.notePromises.push(notePromise);
582
594
  }
583
595
  break;
584
596
  }
585
597
  case "noteAftertouch":
586
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount);
598
+ this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
587
599
  break;
588
600
  case "controller":
589
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
601
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
590
602
  break;
591
603
  case "programChange":
592
- this.handleProgramChange(event.channel, event.programNumber);
604
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
593
605
  break;
594
606
  case "channelAftertouch":
595
- this.handleChannelPressure(event.channel, event.amount);
607
+ this.handleChannelPressure(event.channel, event.amount, startTime);
596
608
  break;
597
609
  case "pitchBend":
598
- this.setPitchBend(event.channel, event.value + 8192);
610
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
599
611
  break;
600
612
  case "sysEx":
601
- this.handleSysEx(event.data);
613
+ this.handleSysEx(event.data, startTime);
602
614
  }
603
615
  queueIndex++;
604
616
  }
@@ -629,10 +641,11 @@ export class Midy {
629
641
  resolve();
630
642
  return;
631
643
  }
632
- const t = this.audioContext.currentTime + offset;
644
+ const now = this.audioContext.currentTime;
645
+ const t = now + offset;
633
646
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
634
647
  if (this.isPausing) {
635
- await this.stopNotes(0, true);
648
+ await this.stopNotes(0, true, now);
636
649
  this.notePromises = [];
637
650
  resolve();
638
651
  this.isPausing = false;
@@ -640,7 +653,7 @@ export class Midy {
640
653
  return;
641
654
  }
642
655
  else if (this.isStopping) {
643
- await this.stopNotes(0, true);
656
+ await this.stopNotes(0, true, now);
644
657
  this.notePromises = [];
645
658
  this.exclusiveClassMap.clear();
646
659
  this.audioBufferCache.clear();
@@ -650,7 +663,7 @@ export class Midy {
650
663
  return;
651
664
  }
652
665
  else if (this.isSeeking) {
653
- this.stopNotes(0, true);
666
+ this.stopNotes(0, true, now);
654
667
  this.exclusiveClassMap.clear();
655
668
  this.startTime = this.audioContext.currentTime;
656
669
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -659,7 +672,6 @@ export class Midy {
659
672
  await schedulePlayback();
660
673
  }
661
674
  else {
662
- const now = this.audioContext.currentTime;
663
675
  const waitTime = now + this.noteCheckInterval;
664
676
  await this.scheduleTask(() => { }, waitTime);
665
677
  await schedulePlayback();
@@ -779,25 +791,26 @@ export class Midy {
779
791
  }
780
792
  return { instruments, timeline };
781
793
  }
782
- async stopChannelNotes(channelNumber, velocity, force) {
783
- const now = this.audioContext.currentTime;
794
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
784
795
  const channel = this.channels[channelNumber];
796
+ const promises = [];
785
797
  channel.scheduledNotes.forEach((noteList) => {
786
798
  for (let i = 0; i < noteList.length; i++) {
787
799
  const note = noteList[i];
788
800
  if (!note)
789
801
  continue;
790
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
791
- force);
802
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
792
803
  this.notePromises.push(promise);
804
+ promises.push(promise);
793
805
  }
794
806
  });
795
807
  channel.scheduledNotes.clear();
796
- await Promise.all(this.notePromises);
808
+ return Promise.all(promises);
797
809
  }
798
- stopNotes(velocity, force) {
810
+ stopNotes(velocity, force, scheduleTime) {
811
+ const promises = [];
799
812
  for (let i = 0; i < this.channels.length; i++) {
800
- this.stopChannelNotes(i, velocity, force);
813
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
801
814
  }
802
815
  return Promise.all(this.notePromises);
803
816
  }
@@ -845,22 +858,34 @@ export class Midy {
845
858
  const now = this.audioContext.currentTime;
846
859
  return this.resumeTime + now - this.startTime - this.startDelay;
847
860
  }
848
- getActiveNotes(channel, time) {
861
+ processScheduledNotes(channel, scheduleTime, callback) {
862
+ channel.scheduledNotes.forEach((noteList) => {
863
+ for (let i = 0; i < noteList.length; i++) {
864
+ const note = noteList[i];
865
+ if (!note)
866
+ continue;
867
+ if (scheduleTime < note.startTime)
868
+ continue;
869
+ callback(note);
870
+ }
871
+ });
872
+ }
873
+ getActiveNotes(channel, scheduleTime) {
849
874
  const activeNotes = new SparseMap(128);
850
875
  channel.scheduledNotes.forEach((noteList) => {
851
- const activeNote = this.getActiveNote(noteList, time);
876
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
852
877
  if (activeNote) {
853
878
  activeNotes.set(activeNote.noteNumber, activeNote);
854
879
  }
855
880
  });
856
881
  return activeNotes;
857
882
  }
858
- getActiveNote(noteList, time) {
883
+ getActiveNote(noteList, scheduleTime) {
859
884
  for (let i = noteList.length - 1; i >= 0; i--) {
860
885
  const note = noteList[i];
861
886
  if (!note)
862
887
  return;
863
- if (time < note.startTime)
888
+ if (scheduleTime < note.startTime)
864
889
  continue;
865
890
  return (note.ending) ? null : note;
866
891
  }
@@ -1020,74 +1045,66 @@ export class Midy {
1020
1045
  calcNoteDetune(channel, note) {
1021
1046
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1022
1047
  }
1023
- updateChannelDetune(channel) {
1024
- channel.scheduledNotes.forEach((noteList) => {
1025
- for (let i = 0; i < noteList.length; i++) {
1026
- const note = noteList[i];
1027
- if (!note)
1028
- continue;
1029
- this.updateDetune(channel, note, 0);
1030
- }
1048
+ updateChannelDetune(channel, scheduleTime) {
1049
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1050
+ this.updateDetune(channel, note, scheduleTime);
1031
1051
  });
1032
1052
  }
1033
- updateDetune(channel, note, pressure) {
1034
- const now = this.audioContext.currentTime;
1053
+ updateDetune(channel, note, scheduleTime) {
1035
1054
  const noteDetune = this.calcNoteDetune(channel, note);
1036
- const detune = channel.detune + noteDetune + pressure;
1055
+ const pitchControl = this.getPitchControl(channel, note);
1056
+ const detune = channel.detune + noteDetune + pitchControl;
1037
1057
  note.bufferSource.detune
1038
- .cancelScheduledValues(now)
1039
- .setValueAtTime(detune, now);
1058
+ .cancelScheduledValues(scheduleTime)
1059
+ .setValueAtTime(detune, scheduleTime);
1040
1060
  }
1041
1061
  getPortamentoTime(channel) {
1042
1062
  const factor = 5 * Math.log(10) / 127;
1043
1063
  const time = channel.state.portamentoTime;
1044
1064
  return Math.log(time) / factor;
1045
1065
  }
1046
- setPortamentoStartVolumeEnvelope(channel, note) {
1047
- const now = this.audioContext.currentTime;
1066
+ setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1048
1067
  const { voiceParams, startTime } = note;
1049
1068
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1050
1069
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1051
1070
  const volDelay = startTime + voiceParams.volDelay;
1052
1071
  const portamentoTime = volDelay + this.getPortamentoTime(channel);
1053
1072
  note.volumeEnvelopeNode.gain
1054
- .cancelScheduledValues(now)
1073
+ .cancelScheduledValues(scheduleTime)
1055
1074
  .setValueAtTime(0, volDelay)
1056
1075
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
1057
1076
  }
1058
- setVolumeEnvelope(channel, note, pressure) {
1059
- const now = this.audioContext.currentTime;
1077
+ setVolumeEnvelope(channel, note, scheduleTime) {
1060
1078
  const state = channel.state;
1061
1079
  const { voiceParams, startTime } = note;
1062
1080
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1063
- (1 + pressure);
1081
+ (1 + this.getAmplitudeControl(channel, note));
1064
1082
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1065
1083
  const volDelay = startTime + voiceParams.volDelay;
1066
1084
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1067
1085
  const volHold = volAttack + voiceParams.volHold;
1068
1086
  const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
1069
1087
  note.volumeEnvelopeNode.gain
1070
- .cancelScheduledValues(now)
1088
+ .cancelScheduledValues(scheduleTime)
1071
1089
  .setValueAtTime(0, startTime)
1072
1090
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
1073
1091
  .exponentialRampToValueAtTime(attackVolume, volAttack)
1074
1092
  .setValueAtTime(attackVolume, volHold)
1075
1093
  .linearRampToValueAtTime(sustainVolume, volDecay);
1076
1094
  }
1077
- setPitchEnvelope(note) {
1078
- const now = this.audioContext.currentTime;
1095
+ setPitchEnvelope(note, scheduleTime) {
1079
1096
  const { voiceParams } = note;
1080
1097
  const baseRate = voiceParams.playbackRate;
1081
1098
  note.bufferSource.playbackRate
1082
- .cancelScheduledValues(now)
1083
- .setValueAtTime(baseRate, now);
1099
+ .cancelScheduledValues(scheduleTime)
1100
+ .setValueAtTime(baseRate, scheduleTime);
1084
1101
  const modEnvToPitch = voiceParams.modEnvToPitch;
1085
1102
  if (modEnvToPitch === 0)
1086
1103
  return;
1087
1104
  const basePitch = this.rateToCent(baseRate);
1088
1105
  const peekPitch = basePitch + modEnvToPitch;
1089
1106
  const peekRate = this.centToRate(peekPitch);
1090
- const modDelay = startTime + voiceParams.modDelay;
1107
+ const modDelay = note.startTime + voiceParams.modDelay;
1091
1108
  const modAttack = modDelay + voiceParams.modAttack;
1092
1109
  const modHold = modAttack + voiceParams.modHold;
1093
1110
  const modDecay = modHold + voiceParams.modDecay;
@@ -1102,8 +1119,7 @@ export class Midy {
1102
1119
  const maxFrequency = 20000; // max Hz of initialFilterFc
1103
1120
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1104
1121
  }
1105
- setPortamentoStartFilterEnvelope(channel, note) {
1106
- const now = this.audioContext.currentTime;
1122
+ setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1107
1123
  const state = channel.state;
1108
1124
  const { voiceParams, noteNumber, startTime } = note;
1109
1125
  const softPedalFactor = 1 -
@@ -1119,18 +1135,18 @@ export class Midy {
1119
1135
  const portamentoTime = startTime + this.getPortamentoTime(channel);
1120
1136
  const modDelay = startTime + voiceParams.modDelay;
1121
1137
  note.filterNode.frequency
1122
- .cancelScheduledValues(now)
1138
+ .cancelScheduledValues(scheduleTime)
1123
1139
  .setValueAtTime(adjustedBaseFreq, startTime)
1124
1140
  .setValueAtTime(adjustedBaseFreq, modDelay)
1125
1141
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1126
1142
  }
1127
- setFilterEnvelope(channel, note, pressure) {
1128
- const now = this.audioContext.currentTime;
1143
+ setFilterEnvelope(channel, note, scheduleTime) {
1129
1144
  const state = channel.state;
1130
1145
  const { voiceParams, noteNumber, startTime } = note;
1131
1146
  const softPedalFactor = 1 -
1132
1147
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1133
- const baseCent = voiceParams.initialFilterFc + pressure;
1148
+ const baseCent = voiceParams.initialFilterFc +
1149
+ this.getFilterCutoffControl(channel, note);
1134
1150
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1135
1151
  state.brightness * 2;
1136
1152
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
@@ -1145,14 +1161,14 @@ export class Midy {
1145
1161
  const modHold = modAttack + voiceParams.modHold;
1146
1162
  const modDecay = modHold + voiceParams.modDecay;
1147
1163
  note.filterNode.frequency
1148
- .cancelScheduledValues(now)
1164
+ .cancelScheduledValues(scheduleTime)
1149
1165
  .setValueAtTime(adjustedBaseFreq, startTime)
1150
1166
  .setValueAtTime(adjustedBaseFreq, modDelay)
1151
1167
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1152
1168
  .setValueAtTime(adjustedPeekFreq, modHold)
1153
1169
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
1154
1170
  }
1155
- startModulation(channel, note, startTime) {
1171
+ startModulation(channel, note, scheduleTime) {
1156
1172
  const { voiceParams } = note;
1157
1173
  note.modulationLFO = new OscillatorNode(this.audioContext, {
1158
1174
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -1161,10 +1177,10 @@ export class Midy {
1161
1177
  gain: voiceParams.modLfoToFilterFc,
1162
1178
  });
1163
1179
  note.modulationDepth = new GainNode(this.audioContext);
1164
- this.setModLfoToPitch(channel, note, 0);
1180
+ this.setModLfoToPitch(channel, note, scheduleTime);
1165
1181
  note.volumeDepth = new GainNode(this.audioContext);
1166
- this.setModLfoToVolume(note, 0);
1167
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1182
+ this.setModLfoToVolume(channel, note, scheduleTime);
1183
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1168
1184
  note.modulationLFO.connect(note.filterDepth);
1169
1185
  note.filterDepth.connect(note.filterNode.frequency);
1170
1186
  note.modulationLFO.connect(note.modulationDepth);
@@ -1172,15 +1188,15 @@ export class Midy {
1172
1188
  note.modulationLFO.connect(note.volumeDepth);
1173
1189
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1174
1190
  }
1175
- startVibrato(channel, note, startTime) {
1191
+ startVibrato(channel, note, scheduleTime) {
1176
1192
  const { voiceParams } = note;
1177
1193
  const state = channel.state;
1178
1194
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1179
1195
  frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1180
1196
  });
1181
- note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1197
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1182
1198
  note.vibratoDepth = new GainNode(this.audioContext);
1183
- this.setVibLfoToPitch(channel, note);
1199
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1184
1200
  note.vibratoLFO.connect(note.vibratoDepth);
1185
1201
  note.vibratoDepth.connect(note.bufferSource.detune);
1186
1202
  }
@@ -1203,6 +1219,7 @@ export class Midy {
1203
1219
  }
1204
1220
  }
1205
1221
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1222
+ const now = this.audioContext.currentTime;
1206
1223
  const state = channel.state;
1207
1224
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1208
1225
  const voiceParams = voice.getAllParams(controllerState);
@@ -1219,20 +1236,20 @@ export class Midy {
1219
1236
  });
1220
1237
  if (portamento) {
1221
1238
  note.portamento = true;
1222
- this.setPortamentoStartVolumeEnvelope(channel, note);
1223
- this.setPortamentoStartFilterEnvelope(channel, note);
1239
+ this.setPortamentoStartVolumeEnvelope(channel, note, now);
1240
+ this.setPortamentoStartFilterEnvelope(channel, note, now);
1224
1241
  }
1225
1242
  else {
1226
1243
  note.portamento = false;
1227
- this.setVolumeEnvelope(channel, note, 0);
1228
- this.setFilterEnvelope(channel, note, 0);
1244
+ this.setVolumeEnvelope(channel, note, now);
1245
+ this.setFilterEnvelope(channel, note, now);
1229
1246
  }
1230
1247
  if (0 < state.vibratoDepth) {
1231
- this.startVibrato(channel, note, startTime);
1248
+ this.startVibrato(channel, note, now);
1232
1249
  }
1233
- this.setPitchEnvelope(note);
1250
+ this.setPitchEnvelope(note, now);
1234
1251
  if (0 < state.modulationDepth) {
1235
- this.startModulation(channel, note, startTime);
1252
+ this.startModulation(channel, note, now);
1236
1253
  }
1237
1254
  if (this.mono && channel.currentBufferSource) {
1238
1255
  channel.currentBufferSource.stop(startTime);
@@ -1244,10 +1261,10 @@ export class Midy {
1244
1261
  note.volumeNode.connect(note.gainL);
1245
1262
  note.volumeNode.connect(note.gainR);
1246
1263
  if (0 < channel.chorusSendLevel) {
1247
- this.setChorusEffectsSend(channel, note, 0);
1264
+ this.setChorusEffectsSend(channel, note, 0, now);
1248
1265
  }
1249
1266
  if (0 < channel.reverbSendLevel) {
1250
- this.setReverbEffectsSend(channel, note, 0);
1267
+ this.setReverbEffectsSend(channel, note, 0, now);
1251
1268
  }
1252
1269
  note.bufferSource.start(startTime);
1253
1270
  return note;
@@ -1284,9 +1301,9 @@ export class Midy {
1284
1301
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1285
1302
  const [prevNote, prevChannelNumber] = prevEntry;
1286
1303
  if (!prevNote.ending) {
1287
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1288
- startTime, undefined, // portamentoNoteNumber
1289
- true);
1304
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1305
+ startTime, true, // force
1306
+ undefined);
1290
1307
  }
1291
1308
  }
1292
1309
  this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
@@ -1299,9 +1316,9 @@ export class Midy {
1299
1316
  scheduledNotes.set(noteNumber, [note]);
1300
1317
  }
1301
1318
  }
1302
- noteOn(channelNumber, noteNumber, velocity, portamento) {
1303
- const now = this.audioContext.currentTime;
1304
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1319
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1320
+ scheduleTime ??= this.audioContext.currentTime;
1321
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1305
1322
  }
1306
1323
  stopNote(endTime, stopTime, scheduledNotes, index) {
1307
1324
  const note = scheduledNotes[index];
@@ -1341,7 +1358,7 @@ export class Midy {
1341
1358
  note.bufferSource.stop(stopTime);
1342
1359
  });
1343
1360
  }
1344
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
1361
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1345
1362
  const channel = this.channels[channelNumber];
1346
1363
  const state = channel.state;
1347
1364
  if (!force) {
@@ -1381,24 +1398,19 @@ export class Midy {
1381
1398
  }
1382
1399
  }
1383
1400
  }
1384
- releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1385
- const now = this.audioContext.currentTime;
1386
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1401
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1402
+ scheduleTime ??= this.audioContext.currentTime;
1403
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1404
+ undefined);
1387
1405
  }
1388
- releaseSustainPedal(channelNumber, halfVelocity) {
1406
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1389
1407
  const velocity = halfVelocity * 2;
1390
1408
  const channel = this.channels[channelNumber];
1391
1409
  const promises = [];
1392
- channel.state.sustainPedal = halfVelocity;
1393
- channel.scheduledNotes.forEach((noteList) => {
1394
- for (let i = 0; i < noteList.length; i++) {
1395
- const note = noteList[i];
1396
- if (!note)
1397
- continue;
1398
- const { noteNumber } = note;
1399
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1400
- promises.push(promise);
1401
- }
1410
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1411
+ const { noteNumber } = note;
1412
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
1413
+ promises.push(promise);
1402
1414
  });
1403
1415
  return promises;
1404
1416
  }
@@ -1409,53 +1421,51 @@ export class Midy {
1409
1421
  channel.state.sostenutoPedal = 0;
1410
1422
  channel.sostenutoNotes.forEach((activeNote) => {
1411
1423
  const { noteNumber } = activeNote;
1412
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
1424
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
1413
1425
  promises.push(promise);
1414
1426
  });
1415
1427
  channel.sostenutoNotes.clear();
1416
1428
  return promises;
1417
1429
  }
1418
- handleMIDIMessage(statusByte, data1, data2) {
1430
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1419
1431
  const channelNumber = omni ? 0 : statusByte & 0x0F;
1420
1432
  const messageType = statusByte & 0xF0;
1421
1433
  switch (messageType) {
1422
1434
  case 0x80:
1423
- return this.releaseNote(channelNumber, data1, data2);
1435
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
1424
1436
  case 0x90:
1425
- return this.noteOn(channelNumber, data1, data2);
1437
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
1426
1438
  case 0xA0:
1427
- return this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
1439
+ return this.handlePolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1428
1440
  case 0xB0:
1429
- return this.handleControlChange(channelNumber, data1, data2);
1441
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1430
1442
  case 0xC0:
1431
- return this.handleProgramChange(channelNumber, data1);
1443
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
1432
1444
  case 0xD0:
1433
- return this.handleChannelPressure(channelNumber, data1);
1445
+ return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1434
1446
  case 0xE0:
1435
- return this.handlePitchBendMessage(channelNumber, data1, data2);
1447
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1436
1448
  default:
1437
1449
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1438
1450
  }
1439
1451
  }
1440
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
1441
- const now = this.audioContext.currentTime;
1452
+ handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1442
1453
  const channel = this.channels[channelNumber];
1443
1454
  channel.state.polyphonicKeyPressure = pressure / 127;
1444
1455
  const table = channel.polyphonicKeyPressureTable;
1445
- const activeNotes = this.getActiveNotes(channel, now);
1456
+ const activeNotes = this.getActiveNotes(channel, scheduleTime);
1446
1457
  if (activeNotes.has(noteNumber)) {
1447
1458
  const note = activeNotes.get(noteNumber);
1448
- this.applyDestinationSettings(channel, note, table);
1459
+ this.setControllerParameters(channel, note, table);
1449
1460
  }
1450
1461
  // this.applyVoiceParams(channel, 10);
1451
1462
  }
1452
- handleProgramChange(channelNumber, program) {
1463
+ handleProgramChange(channelNumber, program, _scheduleTime) {
1453
1464
  const channel = this.channels[channelNumber];
1454
1465
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1455
1466
  channel.program = program;
1456
1467
  }
1457
- handleChannelPressure(channelNumber, value) {
1458
- const now = this.audioContext.currentTime;
1468
+ handleChannelPressure(channelNumber, value, scheduleTime) {
1459
1469
  const channel = this.channels[channelNumber];
1460
1470
  const prev = channel.state.channelPressure;
1461
1471
  const next = value / 127;
@@ -1465,69 +1475,68 @@ export class Midy {
1465
1475
  channel.detune += pressureDepth * (next - prev);
1466
1476
  }
1467
1477
  const table = channel.channelPressureTable;
1468
- this.getActiveNotes(channel, now).forEach((note) => {
1469
- this.applyDestinationSettings(channel, note, table);
1478
+ this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1479
+ this.setControllerParameters(channel, note, table);
1470
1480
  });
1471
1481
  // this.applyVoiceParams(channel, 13);
1472
1482
  }
1473
- handlePitchBendMessage(channelNumber, lsb, msb) {
1483
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1474
1484
  const pitchBend = msb * 128 + lsb;
1475
- this.setPitchBend(channelNumber, pitchBend);
1485
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1476
1486
  }
1477
- setPitchBend(channelNumber, value) {
1487
+ setPitchBend(channelNumber, value, scheduleTime) {
1488
+ scheduleTime ??= this.audioContext.currentTime;
1478
1489
  const channel = this.channels[channelNumber];
1479
1490
  const state = channel.state;
1480
1491
  const prev = state.pitchWheel * 2 - 1;
1481
1492
  const next = (value - 8192) / 8192;
1482
1493
  state.pitchWheel = value / 16383;
1483
1494
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1484
- this.updateChannelDetune(channel);
1485
- this.applyVoiceParams(channel, 14);
1495
+ this.updateChannelDetune(channel, scheduleTime);
1496
+ this.applyVoiceParams(channel, 14, scheduleTime);
1486
1497
  }
1487
- setModLfoToPitch(channel, note, pressure) {
1488
- const now = this.audioContext.currentTime;
1489
- const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1498
+ setModLfoToPitch(channel, note, scheduleTime) {
1499
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1500
+ this.getLFOPitchDepth(channel, note);
1490
1501
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1491
1502
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1492
1503
  note.modulationDepth.gain
1493
- .cancelScheduledValues(now)
1494
- .setValueAtTime(modulationDepth, now);
1504
+ .cancelScheduledValues(scheduleTime)
1505
+ .setValueAtTime(modulationDepth, scheduleTime);
1495
1506
  }
1496
- setVibLfoToPitch(channel, note) {
1497
- const now = this.audioContext.currentTime;
1507
+ setVibLfoToPitch(channel, note, scheduleTime) {
1498
1508
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1499
1509
  const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1500
1510
  2;
1501
1511
  const vibratoDepthSign = 0 < vibLfoToPitch;
1502
1512
  note.vibratoDepth.gain
1503
- .cancelScheduledValues(now)
1504
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1513
+ .cancelScheduledValues(scheduleTime)
1514
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, scheduleTime);
1505
1515
  }
1506
- setModLfoToFilterFc(note, pressure) {
1507
- const now = this.audioContext.currentTime;
1508
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1516
+ setModLfoToFilterFc(channel, note, scheduleTime) {
1517
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1518
+ this.getLFOFilterDepth(channel, note);
1509
1519
  note.filterDepth.gain
1510
- .cancelScheduledValues(now)
1511
- .setValueAtTime(modLfoToFilterFc, now);
1520
+ .cancelScheduledValues(scheduleTime)
1521
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
1512
1522
  }
1513
- setModLfoToVolume(note, pressure) {
1514
- const now = this.audioContext.currentTime;
1523
+ setModLfoToVolume(channel, note, scheduleTime) {
1515
1524
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1516
1525
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1517
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1526
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1527
+ (1 + this.getLFOAmplitudeDepth(channel, note));
1518
1528
  note.volumeDepth.gain
1519
- .cancelScheduledValues(now)
1520
- .setValueAtTime(volumeDepth, now);
1529
+ .cancelScheduledValues(scheduleTime)
1530
+ .setValueAtTime(volumeDepth, scheduleTime);
1521
1531
  }
1522
- setReverbEffectsSend(channel, note, prevValue) {
1532
+ setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1523
1533
  if (0 < prevValue) {
1524
1534
  if (0 < note.voiceParams.reverbEffectsSend) {
1525
- const now = this.audioContext.currentTime;
1526
1535
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1527
1536
  const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1528
1537
  note.reverbEffectsSend.gain
1529
- .cancelScheduledValues(now)
1530
- .setValueAtTime(value, now);
1538
+ .cancelScheduledValues(scheduleTime)
1539
+ .setValueAtTime(value, scheduleTime);
1531
1540
  }
1532
1541
  else {
1533
1542
  note.reverbEffectsSend.disconnect();
@@ -1545,15 +1554,14 @@ export class Midy {
1545
1554
  }
1546
1555
  }
1547
1556
  }
1548
- setChorusEffectsSend(channel, note, prevValue) {
1557
+ setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1549
1558
  if (0 < prevValue) {
1550
1559
  if (0 < note.voiceParams.chorusEffectsSend) {
1551
- const now = this.audioContext.currentTime;
1552
1560
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1553
1561
  const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1554
1562
  note.chorusEffectsSend.gain
1555
- .cancelScheduledValues(now)
1556
- .setValueAtTime(value, now);
1563
+ .cancelScheduledValues(scheduleTime)
1564
+ .setValueAtTime(value, scheduleTime);
1557
1565
  }
1558
1566
  else {
1559
1567
  note.chorusEffectsSend.disconnect();
@@ -1571,75 +1579,71 @@ export class Midy {
1571
1579
  }
1572
1580
  }
1573
1581
  }
1574
- setDelayModLFO(note) {
1575
- const now = this.audioContext.currentTime;
1582
+ setDelayModLFO(note, scheduleTime) {
1576
1583
  const startTime = note.startTime;
1577
- if (startTime < now)
1584
+ if (startTime < scheduleTime)
1578
1585
  return;
1579
- note.modulationLFO.stop(now);
1586
+ note.modulationLFO.stop(scheduleTime);
1580
1587
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1581
1588
  note.modulationLFO.connect(note.filterDepth);
1582
1589
  }
1583
- setFreqModLFO(note) {
1584
- const now = this.audioContext.currentTime;
1590
+ setFreqModLFO(note, scheduleTime) {
1585
1591
  const freqModLFO = note.voiceParams.freqModLFO;
1586
1592
  note.modulationLFO.frequency
1587
- .cancelScheduledValues(now)
1588
- .setValueAtTime(freqModLFO, now);
1593
+ .cancelScheduledValues(scheduleTime)
1594
+ .setValueAtTime(freqModLFO, scheduleTime);
1589
1595
  }
1590
- setFreqVibLFO(channel, note) {
1591
- const now = this.audioContext.currentTime;
1596
+ setFreqVibLFO(channel, note, scheduleTime) {
1592
1597
  const freqVibLFO = note.voiceParams.freqVibLFO;
1593
1598
  note.vibratoLFO.frequency
1594
- .cancelScheduledValues(now)
1595
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1599
+ .cancelScheduledValues(scheduleTime)
1600
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, scheduleTime);
1596
1601
  }
1597
1602
  createVoiceParamsHandlers() {
1598
1603
  return {
1599
- modLfoToPitch: (channel, note, _prevValue) => {
1604
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1600
1605
  if (0 < channel.state.modulationDepth) {
1601
- this.setModLfoToPitch(channel, note, 0);
1606
+ this.setModLfoToPitch(channel, note, scheduleTime);
1602
1607
  }
1603
1608
  },
1604
- vibLfoToPitch: (channel, note, _prevValue) => {
1609
+ vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1605
1610
  if (0 < channel.state.vibratoDepth) {
1606
- this.setVibLfoToPitch(channel, note);
1611
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1607
1612
  }
1608
1613
  },
1609
- modLfoToFilterFc: (channel, note, _prevValue) => {
1614
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1610
1615
  if (0 < channel.state.modulationDepth) {
1611
- this.setModLfoToFilterFc(note, 0);
1616
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
1612
1617
  }
1613
1618
  },
1614
- modLfoToVolume: (channel, note, _prevValue) => {
1619
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1615
1620
  if (0 < channel.state.modulationDepth) {
1616
- this.setModLfoToVolume(note, 0);
1621
+ this.setModLfoToVolume(channel, note, scheduleTime);
1617
1622
  }
1618
1623
  },
1619
- chorusEffectsSend: (channel, note, prevValue) => {
1620
- this.setChorusEffectsSend(channel, note, prevValue);
1624
+ chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1625
+ this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1621
1626
  },
1622
- reverbEffectsSend: (channel, note, prevValue) => {
1623
- this.setReverbEffectsSend(channel, note, prevValue);
1627
+ reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1628
+ this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1624
1629
  },
1625
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1626
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1627
- delayVibLFO: (channel, note, prevValue) => {
1630
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1631
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1632
+ delayVibLFO: (channel, note, prevValue, scheduleTime) => {
1628
1633
  if (0 < channel.state.vibratoDepth) {
1629
- const now = this.audioContext.currentTime;
1630
1634
  const vibratoDelay = channel.state.vibratoDelay * 2;
1631
1635
  const prevStartTime = note.startTime + prevValue * vibratoDelay;
1632
- if (now < prevStartTime)
1636
+ if (scheduleTime < prevStartTime)
1633
1637
  return;
1634
1638
  const value = note.voiceParams.delayVibLFO;
1635
1639
  const startTime = note.startTime + value * vibratoDelay;
1636
- note.vibratoLFO.stop(now);
1640
+ note.vibratoLFO.stop(scheduleTime);
1637
1641
  note.vibratoLFO.start(startTime);
1638
1642
  }
1639
1643
  },
1640
- freqVibLFO: (channel, note, _prevValue) => {
1644
+ freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1641
1645
  if (0 < channel.state.vibratoDepth) {
1642
- this.setFreqVibLFO(channel, note);
1646
+ this.setFreqVibLFO(channel, note, scheduleTime);
1643
1647
  }
1644
1648
  },
1645
1649
  };
@@ -1651,7 +1655,7 @@ export class Midy {
1651
1655
  state[3] = noteNumber / 127;
1652
1656
  return state;
1653
1657
  }
1654
- applyVoiceParams(channel, controllerType) {
1658
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1655
1659
  channel.scheduledNotes.forEach((noteList) => {
1656
1660
  for (let i = 0; i < noteList.length; i++) {
1657
1661
  const note = noteList[i];
@@ -1667,7 +1671,7 @@ export class Midy {
1667
1671
  continue;
1668
1672
  note.voiceParams[key] = value;
1669
1673
  if (key in this.voiceParamsHandlers) {
1670
- this.voiceParamsHandlers[key](channel, note, prevValue);
1674
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1671
1675
  }
1672
1676
  else if (filterEnvelopeKeySet.has(key)) {
1673
1677
  if (appliedFilterEnvelope)
@@ -1680,12 +1684,12 @@ export class Midy {
1680
1684
  noteVoiceParams[key] = voiceParams[key];
1681
1685
  }
1682
1686
  if (note.portamento) {
1683
- this.setPortamentoStartFilterEnvelope(channel, note);
1687
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1684
1688
  }
1685
1689
  else {
1686
- this.setFilterEnvelope(channel, note, 0);
1690
+ this.setFilterEnvelope(channel, note, scheduleTime);
1687
1691
  }
1688
- this.setPitchEnvelope(note);
1692
+ this.setPitchEnvelope(note, scheduleTime);
1689
1693
  }
1690
1694
  else if (volumeEnvelopeKeySet.has(key)) {
1691
1695
  if (appliedVolumeEnvelope)
@@ -1697,7 +1701,7 @@ export class Midy {
1697
1701
  if (key in voiceParams)
1698
1702
  noteVoiceParams[key] = voiceParams[key];
1699
1703
  }
1700
- this.setVolumeEnvelope(channel, note, 0);
1704
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1701
1705
  }
1702
1706
  }
1703
1707
  }
@@ -1741,12 +1745,12 @@ export class Midy {
1741
1745
  127: this.polyOn,
1742
1746
  };
1743
1747
  }
1744
- handleControlChange(channelNumber, controllerType, value) {
1748
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1745
1749
  const handler = this.controlChangeHandlers[controllerType];
1746
1750
  if (handler) {
1747
- handler.call(this, channelNumber, value);
1751
+ handler.call(this, channelNumber, value, scheduleTime);
1748
1752
  const channel = this.channels[channelNumber];
1749
- this.applyVoiceParams(channel, controllerType + 128);
1753
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1750
1754
  this.applyControlTable(channel, controllerType);
1751
1755
  }
1752
1756
  else {
@@ -1756,55 +1760,45 @@ export class Midy {
1756
1760
  setBankMSB(channelNumber, msb) {
1757
1761
  this.channels[channelNumber].bankMSB = msb;
1758
1762
  }
1759
- updateModulation(channel) {
1760
- const now = this.audioContext.currentTime;
1763
+ updateModulation(channel, scheduleTime) {
1764
+ scheduleTime ??= this.audioContext.currentTime;
1761
1765
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1762
- channel.scheduledNotes.forEach((noteList) => {
1763
- for (let i = 0; i < noteList.length; i++) {
1764
- const note = noteList[i];
1765
- if (!note)
1766
- continue;
1767
- if (note.modulationDepth) {
1768
- note.modulationDepth.gain.setValueAtTime(depth, now);
1769
- }
1770
- else {
1771
- this.setPitchEnvelope(note);
1772
- this.startModulation(channel, note, now);
1773
- }
1766
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1767
+ if (note.modulationDepth) {
1768
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1769
+ }
1770
+ else {
1771
+ this.setPitchEnvelope(note, scheduleTime);
1772
+ this.startModulation(channel, note, scheduleTime);
1774
1773
  }
1775
1774
  });
1776
1775
  }
1777
- setModulationDepth(channelNumber, modulation) {
1776
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1778
1777
  const channel = this.channels[channelNumber];
1779
1778
  channel.state.modulationDepth = modulation / 127;
1780
- this.updateModulation(channel);
1779
+ this.updateModulation(channel, scheduleTime);
1781
1780
  }
1782
1781
  setPortamentoTime(channelNumber, portamentoTime) {
1783
1782
  const channel = this.channels[channelNumber];
1784
1783
  const factor = 5 * Math.log(10) / 127;
1785
1784
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1786
1785
  }
1787
- setKeyBasedVolume(channel) {
1788
- const now = this.audioContext.currentTime;
1789
- channel.scheduledNotes.forEach((noteList) => {
1790
- for (let i = 0; i < noteList.length; i++) {
1791
- const note = noteList[i];
1792
- if (!note)
1793
- continue;
1794
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1795
- if (keyBasedValue === 0)
1796
- continue;
1786
+ setKeyBasedVolume(channel, scheduleTime) {
1787
+ scheduleTime ??= this.audioContext.currentTime;
1788
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1789
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1790
+ if (keyBasedValue !== 0) {
1797
1791
  note.volumeNode.gain
1798
- .cancelScheduledValues(now)
1799
- .setValueAtTime(1 + keyBasedValue, now);
1792
+ .cancelScheduledValues(scheduleTime)
1793
+ .setValueAtTime(1 + keyBasedValue, scheduleTime);
1800
1794
  }
1801
1795
  });
1802
1796
  }
1803
- setVolume(channelNumber, volume) {
1797
+ setVolume(channelNumber, volume, scheduleTime) {
1804
1798
  const channel = this.channels[channelNumber];
1805
1799
  channel.state.volume = volume / 127;
1806
- this.updateChannelVolume(channel);
1807
- this.setKeyBasedVolume(channel);
1800
+ this.updateChannelVolume(channel, scheduleTime);
1801
+ this.setKeyBasedVolume(channel, scheduleTime);
1808
1802
  }
1809
1803
  panToGain(pan) {
1810
1804
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1813,82 +1807,75 @@ export class Midy {
1813
1807
  gainRight: Math.sin(theta),
1814
1808
  };
1815
1809
  }
1816
- setKeyBasedPan(channel) {
1817
- const now = this.audioContext.currentTime;
1818
- channel.scheduledNotes.forEach((noteList) => {
1819
- for (let i = 0; i < noteList.length; i++) {
1820
- const note = noteList[i];
1821
- if (!note)
1822
- continue;
1823
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1824
- if (keyBasedValue === 0)
1825
- continue;
1810
+ setKeyBasedPan(channel, scheduleTime) {
1811
+ scheduleTime ??= this.audioContext.currentTime;
1812
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1813
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1814
+ if (keyBasedValue !== 0) {
1826
1815
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1827
1816
  note.gainL.gain
1828
- .cancelScheduledValues(now)
1829
- .setValueAtTime(gainLeft, now);
1817
+ .cancelScheduledValues(scheduleTime)
1818
+ .setValueAtTime(gainLeft, scheduleTime);
1830
1819
  note.gainR.gain
1831
- .cancelScheduledValues(now)
1832
- .setValueAtTime(gainRight, now);
1820
+ .cancelScheduledValues(scheduleTime)
1821
+ .setValueAtTime(gainRight, scheduleTime);
1833
1822
  }
1834
1823
  });
1835
1824
  }
1836
- setPan(channelNumber, pan) {
1825
+ setPan(channelNumber, pan, scheduleTime) {
1837
1826
  const channel = this.channels[channelNumber];
1838
1827
  channel.state.pan = pan / 127;
1839
- this.updateChannelVolume(channel);
1840
- this.setKeyBasedPan(channel);
1828
+ this.updateChannelVolume(channel, scheduleTime);
1829
+ this.setKeyBasedPan(channel, scheduleTime);
1841
1830
  }
1842
- setExpression(channelNumber, expression) {
1831
+ setExpression(channelNumber, expression, scheduleTime) {
1843
1832
  const channel = this.channels[channelNumber];
1844
1833
  channel.state.expression = expression / 127;
1845
- this.updateChannelVolume(channel);
1834
+ this.updateChannelVolume(channel, scheduleTime);
1846
1835
  }
1847
1836
  setBankLSB(channelNumber, lsb) {
1848
1837
  this.channels[channelNumber].bankLSB = lsb;
1849
1838
  }
1850
- dataEntryLSB(channelNumber, value) {
1839
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1851
1840
  this.channels[channelNumber].dataLSB = value;
1852
- this.handleRPN(channelNumber, 0);
1841
+ this.handleRPN(channelNumber, scheduleTime);
1853
1842
  }
1854
- updateChannelVolume(channel) {
1855
- const now = this.audioContext.currentTime;
1843
+ updateChannelVolume(channel, scheduleTime) {
1856
1844
  const state = channel.state;
1857
1845
  const volume = state.volume * state.expression;
1858
1846
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1859
1847
  channel.gainL.gain
1860
- .cancelScheduledValues(now)
1861
- .setValueAtTime(volume * gainLeft, now);
1848
+ .cancelScheduledValues(scheduleTime)
1849
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1862
1850
  channel.gainR.gain
1863
- .cancelScheduledValues(now)
1864
- .setValueAtTime(volume * gainRight, now);
1851
+ .cancelScheduledValues(scheduleTime)
1852
+ .setValueAtTime(volume * gainRight, scheduleTime);
1865
1853
  }
1866
- setSustainPedal(channelNumber, value) {
1854
+ setSustainPedal(channelNumber, value, scheduleTime) {
1855
+ scheduleTime ??= this.audioContext.currentTime;
1867
1856
  this.channels[channelNumber].state.sustainPedal = value / 127;
1868
1857
  if (value < 64) {
1869
- this.releaseSustainPedal(channelNumber, value);
1858
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1870
1859
  }
1871
1860
  }
1872
1861
  setPortamento(channelNumber, value) {
1873
1862
  this.channels[channelNumber].state.portamento = value / 127;
1874
1863
  }
1875
- setSostenutoPedal(channelNumber, value) {
1864
+ setSostenutoPedal(channelNumber, value, scheduleTime) {
1876
1865
  const channel = this.channels[channelNumber];
1877
1866
  channel.state.sostenutoPedal = value / 127;
1878
1867
  if (64 <= value) {
1879
- const now = this.audioContext.currentTime;
1880
- channel.sostenutoNotes = this.getActiveNotes(channel, now);
1868
+ channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
1881
1869
  }
1882
1870
  else {
1883
1871
  this.releaseSostenutoPedal(channelNumber, value);
1884
1872
  }
1885
1873
  }
1886
- setSoftPedal(channelNumber, softPedal) {
1874
+ setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1887
1875
  const channel = this.channels[channelNumber];
1888
1876
  channel.state.softPedal = softPedal / 127;
1889
1877
  }
1890
- setFilterResonance(channelNumber, filterResonance) {
1891
- const now = this.audioContext.currentTime;
1878
+ setFilterResonance(channelNumber, filterResonance, scheduleTime) {
1892
1879
  const channel = this.channels[channelNumber];
1893
1880
  const state = channel.state;
1894
1881
  state.filterResonance = filterResonance / 64;
@@ -1898,16 +1885,15 @@ export class Midy {
1898
1885
  if (!note)
1899
1886
  continue;
1900
1887
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
1901
- note.filterNode.Q.setValueAtTime(Q, now);
1888
+ note.filterNode.Q.setValueAtTime(Q, scheduleTime);
1902
1889
  }
1903
1890
  });
1904
1891
  }
1905
- setReleaseTime(channelNumber, releaseTime) {
1892
+ setReleaseTime(channelNumber, releaseTime, _scheduleTime) {
1906
1893
  const channel = this.channels[channelNumber];
1907
1894
  channel.state.releaseTime = releaseTime / 64;
1908
1895
  }
1909
- setAttackTime(channelNumber, attackTime) {
1910
- const now = this.audioContext.currentTime;
1896
+ setAttackTime(channelNumber, attackTime, scheduleTime) {
1911
1897
  const channel = this.channels[channelNumber];
1912
1898
  channel.state.attackTime = attackTime / 64;
1913
1899
  channel.scheduledNotes.forEach((noteList) => {
@@ -1915,13 +1901,13 @@ export class Midy {
1915
1901
  const note = noteList[i];
1916
1902
  if (!note)
1917
1903
  continue;
1918
- if (note.startTime < now)
1904
+ if (note.startTime < scheduleTime)
1919
1905
  continue;
1920
- this.setVolumeEnvelope(channel, note, 0);
1906
+ this.setVolumeEnvelope(channel, note);
1921
1907
  }
1922
1908
  });
1923
1909
  }
1924
- setBrightness(channelNumber, brightness) {
1910
+ setBrightness(channelNumber, brightness, scheduleTime) {
1925
1911
  const channel = this.channels[channelNumber];
1926
1912
  channel.state.brightness = brightness / 64;
1927
1913
  channel.scheduledNotes.forEach((noteList) => {
@@ -1930,15 +1916,15 @@ export class Midy {
1930
1916
  if (!note)
1931
1917
  continue;
1932
1918
  if (note.portamento) {
1933
- this.setPortamentoStartFilterEnvelope(channel, note);
1919
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1934
1920
  }
1935
1921
  else {
1936
- this.setFilterEnvelope(channel, note, 0);
1922
+ this.setFilterEnvelope(channel, note);
1937
1923
  }
1938
1924
  }
1939
1925
  });
1940
1926
  }
1941
- setDecayTime(channelNumber, dacayTime) {
1927
+ setDecayTime(channelNumber, dacayTime, scheduleTime) {
1942
1928
  const channel = this.channels[channelNumber];
1943
1929
  channel.state.decayTime = dacayTime / 64;
1944
1930
  channel.scheduledNotes.forEach((noteList) => {
@@ -1946,11 +1932,11 @@ export class Midy {
1946
1932
  const note = noteList[i];
1947
1933
  if (!note)
1948
1934
  continue;
1949
- this.setVolumeEnvelope(channel, note, 0);
1935
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1950
1936
  }
1951
1937
  });
1952
1938
  }
1953
- setVibratoRate(channelNumber, vibratoRate) {
1939
+ setVibratoRate(channelNumber, vibratoRate, scheduleTime) {
1954
1940
  const channel = this.channels[channelNumber];
1955
1941
  channel.state.vibratoRate = vibratoRate / 64;
1956
1942
  if (channel.vibratoDepth <= 0)
@@ -1960,11 +1946,11 @@ export class Midy {
1960
1946
  const note = noteList[i];
1961
1947
  if (!note)
1962
1948
  continue;
1963
- this.setVibLfoToPitch(channel, note);
1949
+ this.setVibLfoToPitch(channel, note, scheduleTime);
1964
1950
  }
1965
1951
  });
1966
1952
  }
1967
- setVibratoDepth(channelNumber, vibratoDepth) {
1953
+ setVibratoDepth(channelNumber, vibratoDepth, scheduleTime) {
1968
1954
  const channel = this.channels[channelNumber];
1969
1955
  const prev = channel.state.vibratoDepth;
1970
1956
  channel.state.vibratoDepth = vibratoDepth / 64;
@@ -1974,7 +1960,7 @@ export class Midy {
1974
1960
  const note = noteList[i];
1975
1961
  if (!note)
1976
1962
  continue;
1977
- this.setFreqVibLFO(channel, note);
1963
+ this.setFreqVibLFO(channel, note, scheduleTime);
1978
1964
  }
1979
1965
  });
1980
1966
  }
@@ -1984,7 +1970,7 @@ export class Midy {
1984
1970
  const note = noteList[i];
1985
1971
  if (!note)
1986
1972
  continue;
1987
- this.startVibrato(channel, note, note.startTime);
1973
+ this.startVibrato(channel, note, scheduleTime);
1988
1974
  }
1989
1975
  });
1990
1976
  }
@@ -1998,21 +1984,21 @@ export class Midy {
1998
1984
  const note = noteList[i];
1999
1985
  if (!note)
2000
1986
  continue;
2001
- this.startVibrato(channel, note, note.startTime);
1987
+ this.startVibrato(channel, note, scheduleTime);
2002
1988
  }
2003
1989
  });
2004
1990
  }
2005
1991
  }
2006
- setReverbSendLevel(channelNumber, reverbSendLevel) {
1992
+ setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
2007
1993
  const channel = this.channels[channelNumber];
2008
1994
  const state = channel.state;
2009
1995
  const reverbEffect = this.reverbEffect;
2010
1996
  if (0 < state.reverbSendLevel) {
2011
1997
  if (0 < reverbSendLevel) {
2012
- const now = this.audioContext.currentTime;
2013
1998
  state.reverbSendLevel = reverbSendLevel / 127;
2014
- reverbEffect.input.gain.cancelScheduledValues(now);
2015
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1999
+ reverbEffect.input.gain
2000
+ .cancelScheduledValues(scheduleTime)
2001
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
2016
2002
  }
2017
2003
  else {
2018
2004
  channel.scheduledNotes.forEach((noteList) => {
@@ -2029,31 +2015,31 @@ export class Midy {
2029
2015
  }
2030
2016
  else {
2031
2017
  if (0 < reverbSendLevel) {
2032
- const now = this.audioContext.currentTime;
2033
2018
  channel.scheduledNotes.forEach((noteList) => {
2034
2019
  for (let i = 0; i < noteList.length; i++) {
2035
2020
  const note = noteList[i];
2036
2021
  if (!note)
2037
2022
  continue;
2038
- this.setReverbEffectsSend(channel, note, 0);
2023
+ this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2039
2024
  }
2040
2025
  });
2041
2026
  state.reverbSendLevel = reverbSendLevel / 127;
2042
- reverbEffect.input.gain.cancelScheduledValues(now);
2043
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
2027
+ reverbEffect.input.gain
2028
+ .cancelScheduledValues(scheduleTime)
2029
+ .setValueAtTime(state.reverbSendLevel, scheduleTime);
2044
2030
  }
2045
2031
  }
2046
2032
  }
2047
- setChorusSendLevel(channelNumber, chorusSendLevel) {
2033
+ setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2048
2034
  const channel = this.channels[channelNumber];
2049
2035
  const state = channel.state;
2050
2036
  const chorusEffect = this.chorusEffect;
2051
2037
  if (0 < state.chorusSendLevel) {
2052
2038
  if (0 < chorusSendLevel) {
2053
- const now = this.audioContext.currentTime;
2054
2039
  state.chorusSendLevel = chorusSendLevel / 127;
2055
- chorusEffect.input.gain.cancelScheduledValues(now);
2056
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
2040
+ chorusEffect.input.gain
2041
+ .cancelScheduledValues(scheduleTime)
2042
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
2057
2043
  }
2058
2044
  else {
2059
2045
  channel.scheduledNotes.forEach((noteList) => {
@@ -2070,18 +2056,18 @@ export class Midy {
2070
2056
  }
2071
2057
  else {
2072
2058
  if (0 < chorusSendLevel) {
2073
- const now = this.audioContext.currentTime;
2074
2059
  channel.scheduledNotes.forEach((noteList) => {
2075
2060
  for (let i = 0; i < noteList.length; i++) {
2076
2061
  const note = noteList[i];
2077
2062
  if (!note)
2078
2063
  continue;
2079
- this.setChorusEffectsSend(channel, note, 0);
2064
+ this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2080
2065
  }
2081
2066
  });
2082
2067
  state.chorusSendLevel = chorusSendLevel / 127;
2083
- chorusEffect.input.gain.cancelScheduledValues(now);
2084
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
2068
+ chorusEffect.input.gain
2069
+ .cancelScheduledValues(scheduleTime)
2070
+ .setValueAtTime(state.chorusSendLevel, scheduleTime);
2085
2071
  }
2086
2072
  }
2087
2073
  }
@@ -2111,13 +2097,13 @@ export class Midy {
2111
2097
  channel.dataMSB = minMSB;
2112
2098
  }
2113
2099
  }
2114
- handleRPN(channelNumber, value) {
2100
+ handleRPN(channelNumber, value, scheduleTime) {
2115
2101
  const channel = this.channels[channelNumber];
2116
2102
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
2117
2103
  switch (rpn) {
2118
2104
  case 0:
2119
2105
  channel.dataLSB += value;
2120
- this.handlePitchBendRangeRPN(channelNumber);
2106
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
2121
2107
  break;
2122
2108
  case 1:
2123
2109
  channel.dataLSB += value;
@@ -2149,25 +2135,26 @@ export class Midy {
2149
2135
  setRPNLSB(channelNumber, value) {
2150
2136
  this.channels[channelNumber].rpnLSB = value;
2151
2137
  }
2152
- dataEntryMSB(channelNumber, value) {
2138
+ dataEntryMSB(channelNumber, value, scheduleTime) {
2153
2139
  this.channels[channelNumber].dataMSB = value;
2154
- this.handleRPN(channelNumber, 0);
2140
+ this.handleRPN(channelNumber, scheduleTime);
2155
2141
  }
2156
- handlePitchBendRangeRPN(channelNumber) {
2142
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2157
2143
  const channel = this.channels[channelNumber];
2158
2144
  this.limitData(channel, 0, 127, 0, 99);
2159
2145
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2160
- this.setPitchBendRange(channelNumber, pitchBendRange);
2146
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2161
2147
  }
2162
- setPitchBendRange(channelNumber, value) {
2148
+ setPitchBendRange(channelNumber, value, scheduleTime) {
2149
+ scheduleTime ??= this.audioContext.currentTime;
2163
2150
  const channel = this.channels[channelNumber];
2164
2151
  const state = channel.state;
2165
2152
  const prev = state.pitchWheelSensitivity;
2166
2153
  const next = value / 128;
2167
2154
  state.pitchWheelSensitivity = next;
2168
2155
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2169
- this.updateChannelDetune(channel);
2170
- this.applyVoiceParams(channel, 16);
2156
+ this.updateChannelDetune(channel, scheduleTime);
2157
+ this.applyVoiceParams(channel, 16, scheduleTime);
2171
2158
  }
2172
2159
  handleFineTuningRPN(channelNumber) {
2173
2160
  const channel = this.channels[channelNumber];
@@ -2208,8 +2195,9 @@ export class Midy {
2208
2195
  channel.modulationDepthRange = modulationDepthRange;
2209
2196
  this.updateModulation(channel);
2210
2197
  }
2211
- allSoundOff(channelNumber) {
2212
- return this.stopChannelNotes(channelNumber, 0, true);
2198
+ allSoundOff(channelNumber, _value, scheduleTime) {
2199
+ scheduleTime ??= this.audioContext.currentTime;
2200
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2213
2201
  }
2214
2202
  resetAllControllers(channelNumber) {
2215
2203
  const stateTypes = [
@@ -2237,8 +2225,9 @@ export class Midy {
2237
2225
  channel[type] = this.constructor.channelSettings[type];
2238
2226
  }
2239
2227
  }
2240
- allNotesOff(channelNumber) {
2241
- return this.stopChannelNotes(channelNumber, 0, false);
2228
+ allNotesOff(channelNumber, _value, scheduleTime) {
2229
+ scheduleTime ??= this.audioContext.currentTime;
2230
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2242
2231
  }
2243
2232
  omniOff() {
2244
2233
  this.omni = false;
@@ -2252,16 +2241,16 @@ export class Midy {
2252
2241
  polyOn() {
2253
2242
  this.mono = false;
2254
2243
  }
2255
- handleUniversalNonRealTimeExclusiveMessage(data) {
2244
+ handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
2256
2245
  switch (data[2]) {
2257
2246
  case 8:
2258
2247
  switch (data[3]) {
2259
2248
  case 8:
2260
2249
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2261
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2250
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false, scheduleTime);
2262
2251
  case 9:
2263
2252
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2264
- return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false);
2253
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false, scheduleTime);
2265
2254
  default:
2266
2255
  console.warn(`Unsupported Exclusive Message: ${data}`);
2267
2256
  }
@@ -2304,18 +2293,18 @@ export class Midy {
2304
2293
  this.channels[9].bankMSB = 120;
2305
2294
  this.channels[9].bank = 120 * 128;
2306
2295
  }
2307
- handleUniversalRealTimeExclusiveMessage(data) {
2296
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2308
2297
  switch (data[2]) {
2309
2298
  case 4:
2310
2299
  switch (data[3]) {
2311
2300
  case 1:
2312
- return this.handleMasterVolumeSysEx(data);
2301
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
2313
2302
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2314
- return this.handleMasterFineTuningSysEx(data);
2303
+ return this.handleMasterFineTuningSysEx(data, scheduleTime);
2315
2304
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2316
- return this.handleMasterCoarseTuningSysEx(data);
2305
+ return this.handleMasterCoarseTuningSysEx(data, scheduleTime);
2317
2306
  case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2318
- return this.handleGlobalParameterControlSysEx(data);
2307
+ return this.handleGlobalParameterControlSysEx(data, scheduleTime);
2319
2308
  default:
2320
2309
  console.warn(`Unsupported Exclusive Message: ${data}`);
2321
2310
  }
@@ -2323,10 +2312,10 @@ export class Midy {
2323
2312
  case 8:
2324
2313
  switch (data[3]) {
2325
2314
  case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2326
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true);
2315
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true, scheduleTime);
2327
2316
  case 9:
2328
2317
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2329
- return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true);
2318
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true, scheduleTime);
2330
2319
  default:
2331
2320
  console.warn(`Unsupported Exclusive Message: ${data}`);
2332
2321
  }
@@ -2346,7 +2335,7 @@ export class Midy {
2346
2335
  case 10:
2347
2336
  switch (data[3]) {
2348
2337
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2349
- return this.handleKeyBasedInstrumentControlSysEx(data);
2338
+ return this.handleKeyBasedInstrumentControlSysEx(data, scheduleTime);
2350
2339
  default:
2351
2340
  console.warn(`Unsupported Exclusive Message: ${data}`);
2352
2341
  }
@@ -2355,49 +2344,50 @@ export class Midy {
2355
2344
  console.warn(`Unsupported Exclusive Message: ${data}`);
2356
2345
  }
2357
2346
  }
2358
- handleMasterVolumeSysEx(data) {
2347
+ handleMasterVolumeSysEx(data, scheduleTime) {
2359
2348
  const volume = (data[5] * 128 + data[4]) / 16383;
2360
- this.setMasterVolume(volume);
2349
+ this.setMasterVolume(volume, scheduleTime);
2361
2350
  }
2362
- setMasterVolume(volume) {
2351
+ setMasterVolume(volume, scheduleTime) {
2352
+ scheduleTime ??= this.audioContext.currentTime;
2363
2353
  if (volume < 0 && 1 < volume) {
2364
2354
  console.error("Master Volume is out of range");
2365
2355
  }
2366
2356
  else {
2367
- const now = this.audioContext.currentTime;
2368
- this.masterVolume.gain.cancelScheduledValues(now);
2369
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
2357
+ this.masterVolume.gain
2358
+ .cancelScheduledValues(scheduleTime)
2359
+ .setValueAtTime(volume * volume, scheduleTime);
2370
2360
  }
2371
2361
  }
2372
- handleMasterFineTuningSysEx(data) {
2362
+ handleMasterFineTuningSysEx(data, scheduleTime) {
2373
2363
  const fineTuning = data[5] * 128 + data[4];
2374
- this.setMasterFineTuning(fineTuning);
2364
+ this.setMasterFineTuning(fineTuning, scheduleTime);
2375
2365
  }
2376
- setMasterFineTuning(value) {
2366
+ setMasterFineTuning(value, scheduleTime) {
2377
2367
  const prev = this.masterFineTuning;
2378
2368
  const next = (value - 8192) / 8.192; // cent
2379
2369
  this.masterFineTuning = next;
2380
2370
  channel.detune += next - prev;
2381
- this.updateChannelDetune(channel);
2371
+ this.updateChannelDetune(channel, scheduleTime);
2382
2372
  }
2383
- handleMasterCoarseTuningSysEx(data) {
2373
+ handleMasterCoarseTuningSysEx(data, scheduleTime) {
2384
2374
  const coarseTuning = data[4];
2385
- this.setMasterCoarseTuning(coarseTuning);
2375
+ this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2386
2376
  }
2387
- setMasterCoarseTuning(value) {
2377
+ setMasterCoarseTuning(value, scheduleTime) {
2388
2378
  const prev = this.masterCoarseTuning;
2389
2379
  const next = (value - 64) * 100; // cent
2390
2380
  this.masterCoarseTuning = next;
2391
2381
  channel.detune += next - prev;
2392
- this.updateChannelDetune(channel);
2382
+ this.updateChannelDetune(channel, scheduleTime);
2393
2383
  }
2394
- handleGlobalParameterControlSysEx(data) {
2384
+ handleGlobalParameterControlSysEx(data, scheduleTime) {
2395
2385
  if (data[7] === 1) {
2396
2386
  switch (data[8]) {
2397
2387
  case 1:
2398
2388
  return this.handleReverbParameterSysEx(data);
2399
2389
  case 2:
2400
- return this.handleChorusParameterSysEx(data);
2390
+ return this.handleChorusParameterSysEx(data, scheduleTime);
2401
2391
  default:
2402
2392
  console.warn(`Unsupported Global Parameter Control Message: ${data}`);
2403
2393
  }
@@ -2476,88 +2466,84 @@ export class Midy {
2476
2466
  calcDelay(rt60, feedback) {
2477
2467
  return -rt60 * Math.log10(feedback) / 3;
2478
2468
  }
2479
- handleChorusParameterSysEx(data) {
2469
+ handleChorusParameterSysEx(data, scheduleTime) {
2480
2470
  switch (data[9]) {
2481
2471
  case 0:
2482
- return this.setChorusType(data[10]);
2472
+ return this.setChorusType(data[10], scheduleTime);
2483
2473
  case 1:
2484
- return this.setChorusModRate(data[10]);
2474
+ return this.setChorusModRate(data[10], scheduleTime);
2485
2475
  case 2:
2486
- return this.setChorusModDepth(data[10]);
2476
+ return this.setChorusModDepth(data[10], scheduleTime);
2487
2477
  case 3:
2488
- return this.setChorusFeedback(data[10]);
2478
+ return this.setChorusFeedback(data[10], scheduleTime);
2489
2479
  case 4:
2490
- return this.setChorusSendToReverb(data[10]);
2480
+ return this.setChorusSendToReverb(data[10], scheduleTime);
2491
2481
  }
2492
2482
  }
2493
- setChorusType(type) {
2483
+ setChorusType(type, scheduleTime) {
2494
2484
  switch (type) {
2495
2485
  case 0:
2496
- return this.setChorusParameter(3, 5, 0, 0);
2486
+ return this.setChorusParameter(3, 5, 0, 0, scheduleTime);
2497
2487
  case 1:
2498
- return this.setChorusParameter(9, 19, 5, 0);
2488
+ return this.setChorusParameter(9, 19, 5, 0, scheduleTime);
2499
2489
  case 2:
2500
- return this.setChorusParameter(3, 19, 8, 0);
2490
+ return this.setChorusParameter(3, 19, 8, 0, scheduleTime);
2501
2491
  case 3:
2502
- return this.setChorusParameter(9, 16, 16, 0);
2492
+ return this.setChorusParameter(9, 16, 16, 0, scheduleTime);
2503
2493
  case 4:
2504
- return this.setChorusParameter(2, 24, 64, 0);
2494
+ return this.setChorusParameter(2, 24, 64, 0, scheduleTime);
2505
2495
  case 5:
2506
- return this.setChorusParameter(1, 5, 112, 0);
2496
+ return this.setChorusParameter(1, 5, 112, 0, scheduleTime);
2507
2497
  default:
2508
2498
  console.warn(`Unsupported Chorus Type: ${type}`);
2509
2499
  }
2510
2500
  }
2511
- setChorusParameter(modRate, modDepth, feedback, sendToReverb) {
2512
- this.setChorusModRate(modRate);
2513
- this.setChorusModDepth(modDepth);
2514
- this.setChorusFeedback(feedback);
2515
- this.setChorusSendToReverb(sendToReverb);
2501
+ setChorusParameter(modRate, modDepth, feedback, sendToReverb, scheduleTime) {
2502
+ this.setChorusModRate(modRate, scheduleTime);
2503
+ this.setChorusModDepth(modDepth, scheduleTime);
2504
+ this.setChorusFeedback(feedback, scheduleTime);
2505
+ this.setChorusSendToReverb(sendToReverb, scheduleTime);
2516
2506
  }
2517
- setChorusModRate(value) {
2518
- const now = this.audioContext.currentTime;
2507
+ setChorusModRate(value, scheduleTime) {
2519
2508
  const modRate = this.getChorusModRate(value);
2520
2509
  this.chorus.modRate = modRate;
2521
- this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
2510
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, scheduleTime);
2522
2511
  }
2523
2512
  getChorusModRate(value) {
2524
2513
  return value * 0.122; // Hz
2525
2514
  }
2526
- setChorusModDepth(value) {
2527
- const now = this.audioContext.currentTime;
2515
+ setChorusModDepth(value, scheduleTime) {
2528
2516
  const modDepth = this.getChorusModDepth(value);
2529
2517
  this.chorus.modDepth = modDepth;
2530
2518
  this.chorusEffect.lfoGain.gain
2531
- .cancelScheduledValues(now)
2532
- .setValueAtTime(modDepth / 2, now);
2519
+ .cancelScheduledValues(scheduleTime)
2520
+ .setValueAtTime(modDepth / 2, scheduleTime);
2533
2521
  }
2534
2522
  getChorusModDepth(value) {
2535
2523
  return (value + 1) / 3200; // second
2536
2524
  }
2537
- setChorusFeedback(value) {
2538
- const now = this.audioContext.currentTime;
2525
+ setChorusFeedback(value, scheduleTime) {
2539
2526
  const feedback = this.getChorusFeedback(value);
2540
2527
  this.chorus.feedback = feedback;
2541
2528
  const chorusEffect = this.chorusEffect;
2542
2529
  for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
2543
2530
  chorusEffect.feedbackGains[i].gain
2544
- .cancelScheduledValues(now)
2545
- .setValueAtTime(feedback, now);
2531
+ .cancelScheduledValues(scheduleTime)
2532
+ .setValueAtTime(feedback, scheduleTime);
2546
2533
  }
2547
2534
  }
2548
2535
  getChorusFeedback(value) {
2549
2536
  return value * 0.00763;
2550
2537
  }
2551
- setChorusSendToReverb(value) {
2538
+ setChorusSendToReverb(value, scheduleTime) {
2552
2539
  const sendToReverb = this.getChorusSendToReverb(value);
2553
2540
  const sendGain = this.chorusEffect.sendGain;
2554
2541
  if (0 < this.chorus.sendToReverb) {
2555
2542
  this.chorus.sendToReverb = sendToReverb;
2556
2543
  if (0 < sendToReverb) {
2557
- const now = this.audioContext.currentTime;
2558
2544
  sendGain.gain
2559
- .cancelScheduledValues(now)
2560
- .setValueAtTime(sendToReverb, now);
2545
+ .cancelScheduledValues(scheduleTime)
2546
+ .setValueAtTime(sendToReverb, scheduleTime);
2561
2547
  }
2562
2548
  else {
2563
2549
  sendGain.disconnect();
@@ -2566,11 +2552,10 @@ export class Midy {
2566
2552
  else {
2567
2553
  this.chorus.sendToReverb = sendToReverb;
2568
2554
  if (0 < sendToReverb) {
2569
- const now = this.audioContext.currentTime;
2570
2555
  sendGain.connect(this.reverbEffect.input);
2571
2556
  sendGain.gain
2572
- .cancelScheduledValues(now)
2573
- .setValueAtTime(sendToReverb, now);
2557
+ .cancelScheduledValues(scheduleTime)
2558
+ .setValueAtTime(sendToReverb, scheduleTime);
2574
2559
  }
2575
2560
  }
2576
2561
  }
@@ -2596,7 +2581,7 @@ export class Midy {
2596
2581
  }
2597
2582
  return bitmap;
2598
2583
  }
2599
- handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2584
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime, scheduleTime) {
2600
2585
  if (data.length < 19) {
2601
2586
  console.error("Data length is too short");
2602
2587
  return;
@@ -2611,10 +2596,10 @@ export class Midy {
2611
2596
  channel.scaleOctaveTuningTable[j] = centValue;
2612
2597
  }
2613
2598
  if (realtime)
2614
- this.updateChannelDetune(channel);
2599
+ this.updateChannelDetune(channel, scheduleTime);
2615
2600
  }
2616
2601
  }
2617
- handleScaleOctaveTuning2ByteFormatSysEx(data, realtime) {
2602
+ handleScaleOctaveTuning2ByteFormatSysEx(data, realtime, scheduleTime) {
2618
2603
  if (data.length < 31) {
2619
2604
  console.error("Data length is too short");
2620
2605
  return;
@@ -2633,66 +2618,66 @@ export class Midy {
2633
2618
  channel.scaleOctaveTuningTable[j] = centValue;
2634
2619
  }
2635
2620
  if (realtime)
2636
- this.updateChannelDetune(channel);
2637
- }
2638
- }
2639
- applyDestinationSettings(channel, note, table) {
2640
- if (table[0] !== 64) {
2641
- const polyphonicKeyPressure = (0 < note.pressure)
2642
- ? channel.polyphonicKeyPressureTable[0] * note.pressure
2643
- : 0;
2644
- const pressure = (polyphonicKeyPressure - 64) / 37.5; // 2400 / 64;
2645
- this.updateDetune(channel, note, pressure);
2646
- }
2621
+ this.updateChannelDetune(channel, scheduleTime);
2622
+ }
2623
+ }
2624
+ getPitchControl(channel, note) {
2625
+ const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2626
+ note.pressure;
2627
+ return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2628
+ }
2629
+ getFilterCutoffControl(channel, note) {
2630
+ const channelPressure = (channel.channelPressureTable[1] - 64) *
2631
+ channel.state.channelPressure;
2632
+ const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2633
+ note.pressure;
2634
+ return (channelPressure + polyphonicKeyPressure) * 15;
2635
+ }
2636
+ getAmplitudeControl(channel, note) {
2637
+ const channelPressure = channel.channelPressureTable[2] *
2638
+ channel.state.channelPressure;
2639
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2640
+ note.pressure;
2641
+ return (channelPressure + polyphonicKeyPressure) / 128;
2642
+ }
2643
+ getLFOPitchDepth(channel, note) {
2644
+ const channelPressure = channel.channelPressureTable[3] *
2645
+ channel.state.channelPressure;
2646
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2647
+ note.pressure;
2648
+ return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2649
+ }
2650
+ getLFOFilterDepth(channel, note) {
2651
+ const channelPressure = channel.channelPressureTable[4] *
2652
+ channel.state.channelPressure;
2653
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2654
+ note.pressure;
2655
+ return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2656
+ }
2657
+ getLFOAmplitudeDepth(channel, note) {
2658
+ const channelPressure = channel.channelPressureTable[5] *
2659
+ channel.state.channelPressure;
2660
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2661
+ note.pressure;
2662
+ return (channelPressure + polyphonicKeyPressure) / 254;
2663
+ }
2664
+ setControllerParameters(channel, note, table) {
2665
+ if (table[0] !== 64)
2666
+ this.updateDetune(channel, note);
2647
2667
  if (!note.portamento) {
2648
- if (table[1] !== 64) {
2649
- const channelPressure = channel.channelPressureTable[1] *
2650
- channel.state.channelPressure;
2651
- const polyphonicKeyPressure = (0 < note.pressure)
2652
- ? channel.polyphonicKeyPressureTable[1] * note.pressure
2653
- : 0;
2654
- const pressure = (channelPressure + polyphonicKeyPressure - 128) * 15;
2655
- this.setFilterEnvelope(channel, note, pressure);
2656
- }
2657
- if (table[2] !== 64) {
2658
- const channelPressure = channel.channelPressureTable[2] *
2659
- channel.state.channelPressure;
2660
- const polyphonicKeyPressure = (0 < note.pressure)
2661
- ? channel.polyphonicKeyPressureTable[2] * note.pressure
2662
- : 0;
2663
- const pressure = (channelPressure + polyphonicKeyPressure) / 128;
2664
- this.setVolumeEnvelope(channel, note, pressure);
2665
- }
2666
- }
2667
- if (table[3] !== 0) {
2668
- const channelPressure = channel.channelPressureTable[3] *
2669
- channel.state.channelPressure;
2670
- const polyphonicKeyPressure = (0 < note.pressure)
2671
- ? channel.polyphonicKeyPressureTable[3] * note.pressure
2672
- : 0;
2673
- const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 600;
2674
- this.setModLfoToPitch(channel, note, pressure);
2675
- }
2676
- if (table[4] !== 0) {
2677
- const channelPressure = channel.channelPressureTable[4] *
2678
- channel.state.channelPressure;
2679
- const polyphonicKeyPressure = (0 < note.pressure)
2680
- ? channel.polyphonicKeyPressureTable[4] * note.pressure
2681
- : 0;
2682
- const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2683
- this.setModLfoToFilterFc(note, pressure);
2684
- }
2685
- if (table[5] !== 0) {
2686
- const channelPressure = channel.channelPressureTable[5] *
2687
- channel.state.channelPressure;
2688
- const polyphonicKeyPressure = (0 < note.pressure)
2689
- ? channel.polyphonicKeyPressureTable[5] * note.pressure
2690
- : 0;
2691
- const pressure = (channelPressure + polyphonicKeyPressure) / 254;
2692
- this.setModLfoToVolume(note, pressure);
2693
- }
2694
- }
2695
- handleChannelPressureSysEx(data, tableName) {
2668
+ if (table[1] !== 64)
2669
+ this.setFilterEnvelope(channel, note);
2670
+ if (table[2] !== 64)
2671
+ this.setVolumeEnvelope(channel, note);
2672
+ }
2673
+ if (table[3] !== 0)
2674
+ this.setModLfoToPitch(channel, note);
2675
+ if (table[4] !== 0)
2676
+ this.setModLfoToFilterFc(channel, note);
2677
+ if (table[5] !== 0)
2678
+ this.setModLfoToVolume(channel, note);
2679
+ }
2680
+ handlePressureSysEx(data, tableName) {
2696
2681
  const channelNumber = data[4];
2697
2682
  const table = this.channels[channelNumber][tableName];
2698
2683
  for (let i = 5; i < data.length - 1; i += 2) {
@@ -2721,7 +2706,7 @@ export class Midy {
2721
2706
  const note = noteList[i];
2722
2707
  if (!note)
2723
2708
  continue;
2724
- this.applyDestinationSettings(channel, note, table);
2709
+ this.setControllerParameters(channel, note, table);
2725
2710
  }
2726
2711
  });
2727
2712
  }
@@ -2740,7 +2725,7 @@ export class Midy {
2740
2725
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2741
2726
  return (controlValue + 64) / 64;
2742
2727
  }
2743
- handleKeyBasedInstrumentControlSysEx(data) {
2728
+ handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2744
2729
  const channelNumber = data[4];
2745
2730
  const keyNumber = data[5];
2746
2731
  const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
@@ -2750,30 +2735,27 @@ export class Midy {
2750
2735
  const index = keyNumber * 128 + controllerType;
2751
2736
  table[index] = value - 64;
2752
2737
  }
2753
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2754
- }
2755
- handleExclusiveMessage(data) {
2756
- console.warn(`Unsupported Exclusive Message: ${data}`);
2738
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2757
2739
  }
2758
- handleSysEx(data) {
2740
+ handleSysEx(data, scheduleTime) {
2759
2741
  switch (data[0]) {
2760
2742
  case 126:
2761
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
2743
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
2762
2744
  case 127:
2763
- return this.handleUniversalRealTimeExclusiveMessage(data);
2745
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
2764
2746
  default:
2765
- return this.handleExclusiveMessage(data);
2747
+ console.warn(`Unsupported Exclusive Message: ${data}`);
2766
2748
  }
2767
2749
  }
2768
- scheduleTask(callback, startTime) {
2750
+ scheduleTask(callback, scheduleTime) {
2769
2751
  return new Promise((resolve) => {
2770
2752
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
2771
2753
  bufferSource.onended = () => {
2772
2754
  callback();
2773
2755
  resolve();
2774
2756
  };
2775
- bufferSource.start(startTime);
2776
- bufferSource.stop(startTime);
2757
+ bufferSource.start(scheduleTime);
2758
+ bufferSource.stop(scheduleTime);
2777
2759
  });
2778
2760
  }
2779
2761
  }
@@ -2784,10 +2766,6 @@ Object.defineProperty(Midy, "channelSettings", {
2784
2766
  value: {
2785
2767
  currentBufferSource: null,
2786
2768
  detune: 0,
2787
- scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
2788
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2789
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2790
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2791
2769
  program: 0,
2792
2770
  bank: 121 * 128,
2793
2771
  bankMSB: 121,