@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.
@@ -415,7 +415,7 @@ class MidyGM1 {
415
415
  }
416
416
  /* falls through */
417
417
  case "noteOff": {
418
- const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, startTime);
418
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime);
419
419
  if (notePromise) {
420
420
  this.notePromises.push(notePromise);
421
421
  }
@@ -462,10 +462,11 @@ class MidyGM1 {
462
462
  resolve();
463
463
  return;
464
464
  }
465
- const t = this.audioContext.currentTime + offset;
465
+ const now = this.audioContext.currentTime;
466
+ const t = now + offset;
466
467
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
467
468
  if (this.isPausing) {
468
- await this.stopNotes(0, true);
469
+ await this.stopNotes(0, true, now);
469
470
  this.notePromises = [];
470
471
  resolve();
471
472
  this.isPausing = false;
@@ -473,7 +474,7 @@ class MidyGM1 {
473
474
  return;
474
475
  }
475
476
  else if (this.isStopping) {
476
- await this.stopNotes(0, true);
477
+ await this.stopNotes(0, true, now);
477
478
  this.notePromises = [];
478
479
  this.exclusiveClassMap.clear();
479
480
  this.audioBufferCache.clear();
@@ -483,7 +484,7 @@ class MidyGM1 {
483
484
  return;
484
485
  }
485
486
  else if (this.isSeeking) {
486
- this.stopNotes(0, true);
487
+ this.stopNotes(0, true, now);
487
488
  this.exclusiveClassMap.clear();
488
489
  this.startTime = this.audioContext.currentTime;
489
490
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -492,7 +493,6 @@ class MidyGM1 {
492
493
  await schedulePlayback();
493
494
  }
494
495
  else {
495
- const now = this.audioContext.currentTime;
496
496
  const waitTime = now + this.noteCheckInterval;
497
497
  await this.scheduleTask(() => { }, waitTime);
498
498
  await schedulePlayback();
@@ -576,24 +576,26 @@ class MidyGM1 {
576
576
  }
577
577
  return { instruments, timeline };
578
578
  }
579
- async stopChannelNotes(channelNumber, velocity, force) {
580
- const now = this.audioContext.currentTime;
579
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
581
580
  const channel = this.channels[channelNumber];
581
+ const promises = [];
582
582
  channel.scheduledNotes.forEach((noteList) => {
583
583
  for (let i = 0; i < noteList.length; i++) {
584
584
  const note = noteList[i];
585
585
  if (!note)
586
586
  continue;
587
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, force);
587
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
588
588
  this.notePromises.push(promise);
589
+ promises.push(promise);
589
590
  }
590
591
  });
591
592
  channel.scheduledNotes.clear();
592
- await Promise.all(this.notePromises);
593
+ return Promise.all(promises);
593
594
  }
594
- stopNotes(velocity, force) {
595
+ stopNotes(velocity, force, scheduleTime) {
596
+ const promises = [];
595
597
  for (let i = 0; i < this.channels.length; i++) {
596
- this.stopChannelNotes(i, velocity, force);
598
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
597
599
  }
598
600
  return Promise.all(this.notePromises);
599
601
  }
@@ -653,22 +655,22 @@ class MidyGM1 {
653
655
  }
654
656
  });
655
657
  }
656
- getActiveNotes(channel, time) {
658
+ getActiveNotes(channel, scheduleTime) {
657
659
  const activeNotes = new SparseMap(128);
658
660
  channel.scheduledNotes.forEach((noteList) => {
659
- const activeNote = this.getActiveNote(noteList, time);
661
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
660
662
  if (activeNote) {
661
663
  activeNotes.set(activeNote.noteNumber, activeNote);
662
664
  }
663
665
  });
664
666
  return activeNotes;
665
667
  }
666
- getActiveNote(noteList, time) {
668
+ getActiveNote(noteList, scheduleTime) {
667
669
  for (let i = noteList.length - 1; i >= 0; i--) {
668
670
  const note = noteList[i];
669
671
  if (!note)
670
672
  return;
671
- if (time < note.startTime)
673
+ if (scheduleTime < note.startTime)
672
674
  continue;
673
675
  return (note.ending) ? null : note;
674
676
  }
@@ -693,24 +695,17 @@ class MidyGM1 {
693
695
  const pitch = pitchWheel * pitchWheelSensitivity;
694
696
  return tuning + pitch;
695
697
  }
696
- updateChannelDetune(channel) {
697
- channel.scheduledNotes.forEach((noteList) => {
698
- for (let i = 0; i < noteList.length; i++) {
699
- const note = noteList[i];
700
- if (!note)
701
- continue;
702
- this.updateDetune(channel, note);
703
- }
698
+ updateChannelDetune(channel, scheduleTime) {
699
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
700
+ this.updateDetune(channel, note, scheduleTime);
704
701
  });
705
702
  }
706
- updateDetune(channel, note) {
707
- const now = this.audioContext.currentTime;
703
+ updateDetune(channel, note, scheduleTime) {
708
704
  note.bufferSource.detune
709
- .cancelScheduledValues(now)
710
- .setValueAtTime(channel.detune, now);
705
+ .cancelScheduledValues(scheduleTime)
706
+ .setValueAtTime(channel.detune, scheduleTime);
711
707
  }
712
- setVolumeEnvelope(note) {
713
- const now = this.audioContext.currentTime;
708
+ setVolumeEnvelope(note, scheduleTime) {
714
709
  const { voiceParams, startTime } = note;
715
710
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
716
711
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
@@ -719,7 +714,7 @@ class MidyGM1 {
719
714
  const volHold = volAttack + voiceParams.volHold;
720
715
  const volDecay = volHold + voiceParams.volDecay;
721
716
  note.volumeEnvelopeNode.gain
722
- .cancelScheduledValues(now)
717
+ .cancelScheduledValues(scheduleTime)
723
718
  .setValueAtTime(0, startTime)
724
719
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
725
720
  .exponentialRampToValueAtTime(attackVolume, volAttack)
@@ -727,7 +722,6 @@ class MidyGM1 {
727
722
  .linearRampToValueAtTime(sustainVolume, volDecay);
728
723
  }
729
724
  setPitchEnvelope(note, scheduleTime) {
730
- scheduleTime ??= this.audioContext.currentTime;
731
725
  const { voiceParams } = note;
732
726
  const baseRate = voiceParams.playbackRate;
733
727
  note.bufferSource.playbackRate
@@ -754,8 +748,7 @@ class MidyGM1 {
754
748
  const maxFrequency = 20000; // max Hz of initialFilterFc
755
749
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
756
750
  }
757
- setFilterEnvelope(note) {
758
- const now = this.audioContext.currentTime;
751
+ setFilterEnvelope(note, scheduleTime) {
759
752
  const { voiceParams, startTime } = note;
760
753
  const baseFreq = this.centToHz(voiceParams.initialFilterFc);
761
754
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
@@ -769,14 +762,14 @@ class MidyGM1 {
769
762
  const modHold = modAttack + voiceParams.modHold;
770
763
  const modDecay = modHold + voiceParams.modDecay;
771
764
  note.filterNode.frequency
772
- .cancelScheduledValues(now)
765
+ .cancelScheduledValues(scheduleTime)
773
766
  .setValueAtTime(adjustedBaseFreq, startTime)
774
767
  .setValueAtTime(adjustedBaseFreq, modDelay)
775
768
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
776
769
  .setValueAtTime(adjustedPeekFreq, modHold)
777
770
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
778
771
  }
779
- startModulation(channel, note, startTime) {
772
+ startModulation(channel, note, scheduleTime) {
780
773
  const { voiceParams } = note;
781
774
  note.modulationLFO = new OscillatorNode(this.audioContext, {
782
775
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -785,10 +778,10 @@ class MidyGM1 {
785
778
  gain: voiceParams.modLfoToFilterFc,
786
779
  });
787
780
  note.modulationDepth = new GainNode(this.audioContext);
788
- this.setModLfoToPitch(channel, note);
781
+ this.setModLfoToPitch(channel, note, scheduleTime);
789
782
  note.volumeDepth = new GainNode(this.audioContext);
790
- this.setModLfoToVolume(note);
791
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
783
+ this.setModLfoToVolume(note, scheduleTime);
784
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
792
785
  note.modulationLFO.connect(note.filterDepth);
793
786
  note.filterDepth.connect(note.filterNode.frequency);
794
787
  note.modulationLFO.connect(note.modulationDepth);
@@ -815,6 +808,7 @@ class MidyGM1 {
815
808
  }
816
809
  }
817
810
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
811
+ const now = this.audioContext.currentTime;
818
812
  const state = channel.state;
819
813
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
820
814
  const voiceParams = voice.getAllParams(controllerState);
@@ -826,11 +820,11 @@ class MidyGM1 {
826
820
  type: "lowpass",
827
821
  Q: voiceParams.initialFilterQ / 10, // dB
828
822
  });
829
- this.setVolumeEnvelope(note);
830
- this.setFilterEnvelope(note);
831
- this.setPitchEnvelope(note);
823
+ this.setVolumeEnvelope(note, now);
824
+ this.setFilterEnvelope(note, now);
825
+ this.setPitchEnvelope(note, now);
832
826
  if (0 < state.modulationDepth) {
833
- this.startModulation(channel, note, startTime);
827
+ this.startModulation(channel, note, now);
834
828
  }
835
829
  note.bufferSource.connect(note.filterNode);
836
830
  note.filterNode.connect(note.volumeEnvelopeNode);
@@ -857,7 +851,7 @@ class MidyGM1 {
857
851
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
858
852
  const [prevNote, prevChannelNumber] = prevEntry;
859
853
  if (!prevNote.ending) {
860
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
854
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
861
855
  startTime, undefined, // portamentoNoteNumber
862
856
  true);
863
857
  }
@@ -872,9 +866,9 @@ class MidyGM1 {
872
866
  scheduledNotes.set(noteNumber, [note]);
873
867
  }
874
868
  }
875
- noteOn(channelNumber, noteNumber, velocity) {
876
- const now = this.audioContext.currentTime;
877
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
869
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
870
+ scheduleTime ??= this.audioContext.currentTime;
871
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
878
872
  }
879
873
  stopNote(endTime, stopTime, scheduledNotes, index) {
880
874
  const note = scheduledNotes[index];
@@ -901,7 +895,7 @@ class MidyGM1 {
901
895
  note.bufferSource.stop(stopTime);
902
896
  });
903
897
  }
904
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
898
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
905
899
  const channel = this.channels[channelNumber];
906
900
  if (!force && 0.5 < channel.state.sustainPedal)
907
901
  return;
@@ -923,127 +917,119 @@ class MidyGM1 {
923
917
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
924
918
  }
925
919
  }
926
- releaseNote(channelNumber, noteNumber, velocity) {
927
- const now = this.audioContext.currentTime;
928
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
920
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
921
+ scheduleTime ??= this.audioContext.currentTime;
922
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
929
923
  }
930
- releaseSustainPedal(channelNumber, halfVelocity) {
924
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
931
925
  const velocity = halfVelocity * 2;
932
926
  const channel = this.channels[channelNumber];
933
927
  const promises = [];
934
- channel.state.sustainPedal = halfVelocity;
935
- channel.scheduledNotes.forEach((noteList) => {
936
- for (let i = 0; i < noteList.length; i++) {
937
- const note = noteList[i];
938
- if (!note)
939
- continue;
940
- const { noteNumber } = note;
941
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
942
- promises.push(promise);
943
- }
928
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
929
+ const { noteNumber } = note;
930
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
931
+ promises.push(promise);
944
932
  });
945
933
  return promises;
946
934
  }
947
- handleMIDIMessage(statusByte, data1, data2) {
935
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
948
936
  const channelNumber = statusByte & 0x0F;
949
937
  const messageType = statusByte & 0xF0;
950
938
  switch (messageType) {
951
939
  case 0x80:
952
- return this.releaseNote(channelNumber, data1, data2);
940
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
953
941
  case 0x90:
954
- return this.noteOn(channelNumber, data1, data2);
942
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
955
943
  case 0xB0:
956
- return this.handleControlChange(channelNumber, data1, data2);
944
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
957
945
  case 0xC0:
958
- return this.handleProgramChange(channelNumber, data1);
946
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
959
947
  case 0xE0:
960
- return this.handlePitchBendMessage(channelNumber, data1, data2);
948
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
961
949
  default:
962
950
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
963
951
  }
964
952
  }
965
- handleProgramChange(channelNumber, program) {
953
+ handleProgramChange(channelNumber, program, _scheduleTime) {
966
954
  const channel = this.channels[channelNumber];
967
955
  channel.program = program;
968
956
  }
969
- handlePitchBendMessage(channelNumber, lsb, msb) {
957
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
970
958
  const pitchBend = msb * 128 + lsb;
971
- this.setPitchBend(channelNumber, pitchBend);
959
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
972
960
  }
973
- setPitchBend(channelNumber, value) {
961
+ setPitchBend(channelNumber, value, scheduleTime) {
962
+ scheduleTime ??= this.audioContext.currentTime;
974
963
  const channel = this.channels[channelNumber];
975
964
  const state = channel.state;
976
965
  const prev = state.pitchWheel * 2 - 1;
977
966
  const next = (value - 8192) / 8192;
978
967
  state.pitchWheel = value / 16383;
979
968
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
980
- this.updateChannelDetune(channel);
981
- this.applyVoiceParams(channel, 14);
969
+ this.updateChannelDetune(channel, scheduleTime);
970
+ this.applyVoiceParams(channel, 14, scheduleTime);
982
971
  }
983
- setModLfoToPitch(channel, note) {
984
- const now = this.audioContext.currentTime;
972
+ setModLfoToPitch(channel, note, scheduleTime) {
985
973
  const modLfoToPitch = note.voiceParams.modLfoToPitch;
986
974
  const baseDepth = Math.abs(modLfoToPitch) +
987
975
  channel.state.modulationDepth;
988
976
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
989
977
  note.modulationDepth.gain
990
- .cancelScheduledValues(now)
991
- .setValueAtTime(modulationDepth, now);
978
+ .cancelScheduledValues(scheduleTime)
979
+ .setValueAtTime(modulationDepth, scheduleTime);
992
980
  }
993
- setModLfoToFilterFc(note) {
994
- const now = this.audioContext.currentTime;
981
+ setModLfoToFilterFc(note, scheduleTime) {
995
982
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
996
983
  note.filterDepth.gain
997
- .cancelScheduledValues(now)
998
- .setValueAtTime(modLfoToFilterFc, now);
984
+ .cancelScheduledValues(scheduleTime)
985
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
999
986
  }
1000
- setModLfoToVolume(note) {
1001
- const now = this.audioContext.currentTime;
987
+ setModLfoToVolume(note, scheduleTime) {
1002
988
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1003
989
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1004
990
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1005
991
  note.volumeDepth.gain
1006
- .cancelScheduledValues(now)
1007
- .setValueAtTime(volumeDepth, now);
992
+ .cancelScheduledValues(scheduleTime)
993
+ .setValueAtTime(volumeDepth, scheduleTime);
1008
994
  }
1009
- setDelayModLFO(note) {
1010
- const now = this.audioContext.currentTime;
995
+ setDelayModLFO(note, scheduleTime) {
1011
996
  const startTime = note.startTime;
1012
- if (startTime < now)
997
+ if (startTime < scheduleTime)
1013
998
  return;
1014
- note.modulationLFO.stop(now);
999
+ note.modulationLFO.stop(scheduleTime);
1015
1000
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1016
1001
  note.modulationLFO.connect(note.filterDepth);
1017
1002
  }
1018
- setFreqModLFO(note) {
1019
- const now = this.audioContext.currentTime;
1003
+ setFreqModLFO(note, scheduleTime) {
1020
1004
  const freqModLFO = note.voiceParams.freqModLFO;
1021
1005
  note.modulationLFO.frequency
1022
- .cancelScheduledValues(now)
1023
- .setValueAtTime(freqModLFO, now);
1006
+ .cancelScheduledValues(scheduleTime)
1007
+ .setValueAtTime(freqModLFO, scheduleTime);
1024
1008
  }
1025
1009
  createVoiceParamsHandlers() {
1026
1010
  return {
1027
- modLfoToPitch: (channel, note, _prevValue) => {
1011
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1028
1012
  if (0 < channel.state.modulationDepth) {
1029
- this.setModLfoToPitch(channel, note);
1013
+ this.setModLfoToPitch(channel, note, scheduleTime);
1030
1014
  }
1031
1015
  },
1032
- vibLfoToPitch: (_channel, _note, _prevValue) => { },
1033
- modLfoToFilterFc: (channel, note, _prevValue) => {
1034
- if (0 < channel.state.modulationDepth)
1035
- this.setModLfoToFilterFc(note);
1016
+ vibLfoToPitch: (_channel, _note, _prevValue, _scheduleTime) => { },
1017
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1018
+ if (0 < channel.state.modulationDepth) {
1019
+ this.setModLfoToFilterFc(note, scheduleTime);
1020
+ }
1036
1021
  },
1037
- modLfoToVolume: (channel, note, _prevValue) => {
1038
- if (0 < channel.state.modulationDepth)
1039
- this.setModLfoToVolume(note);
1022
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1023
+ if (0 < channel.state.modulationDepth) {
1024
+ this.setModLfoToVolume(note, scheduleTime);
1025
+ }
1040
1026
  },
1041
- chorusEffectsSend: (_channel, _note, _prevValue) => { },
1042
- reverbEffectsSend: (_channel, _note, _prevValue) => { },
1043
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1044
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1045
- delayVibLFO: (_channel, _note, _prevValue) => { },
1046
- freqVibLFO: (_channel, _note, _prevValue) => { },
1027
+ chorusEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1028
+ reverbEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1029
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1030
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1031
+ delayVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1032
+ freqVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1047
1033
  };
1048
1034
  }
1049
1035
  getControllerState(channel, noteNumber, velocity) {
@@ -1053,7 +1039,7 @@ class MidyGM1 {
1053
1039
  state[3] = noteNumber / 127;
1054
1040
  return state;
1055
1041
  }
1056
- applyVoiceParams(channel, controllerType) {
1042
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1057
1043
  channel.scheduledNotes.forEach((noteList) => {
1058
1044
  for (let i = 0; i < noteList.length; i++) {
1059
1045
  const note = noteList[i];
@@ -1069,7 +1055,7 @@ class MidyGM1 {
1069
1055
  continue;
1070
1056
  note.voiceParams[key] = value;
1071
1057
  if (key in this.voiceParamsHandlers) {
1072
- this.voiceParamsHandlers[key](channel, note, prevValue);
1058
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1073
1059
  }
1074
1060
  else if (filterEnvelopeKeySet.has(key)) {
1075
1061
  if (appliedFilterEnvelope)
@@ -1081,8 +1067,8 @@ class MidyGM1 {
1081
1067
  if (key in voiceParams)
1082
1068
  noteVoiceParams[key] = voiceParams[key];
1083
1069
  }
1084
- this.setFilterEnvelope(note);
1085
- this.setPitchEnvelope(note);
1070
+ this.setFilterEnvelope(note, scheduleTime);
1071
+ this.setPitchEnvelope(note, scheduleTime);
1086
1072
  }
1087
1073
  else if (volumeEnvelopeKeySet.has(key)) {
1088
1074
  if (appliedVolumeEnvelope)
@@ -1094,7 +1080,7 @@ class MidyGM1 {
1094
1080
  if (key in voiceParams)
1095
1081
  noteVoiceParams[key] = voiceParams[key];
1096
1082
  }
1097
- this.setVolumeEnvelope(channel, note);
1083
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1098
1084
  }
1099
1085
  }
1100
1086
  }
@@ -1116,12 +1102,12 @@ class MidyGM1 {
1116
1102
  123: this.allNotesOff,
1117
1103
  };
1118
1104
  }
1119
- handleControlChange(channelNumber, controllerType, value, startTime) {
1105
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1120
1106
  const handler = this.controlChangeHandlers[controllerType];
1121
1107
  if (handler) {
1122
- handler.call(this, channelNumber, value, startTime);
1108
+ handler.call(this, channelNumber, value, scheduleTime);
1123
1109
  const channel = this.channels[channelNumber];
1124
- this.applyVoiceParams(channel, controllerType + 128);
1110
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1125
1111
  }
1126
1112
  else {
1127
1113
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1167,26 +1153,26 @@ class MidyGM1 {
1167
1153
  channel.state.expression = expression / 127;
1168
1154
  this.updateChannelVolume(channel, scheduleTime);
1169
1155
  }
1170
- dataEntryLSB(channelNumber, value) {
1156
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1171
1157
  this.channels[channelNumber].dataLSB = value;
1172
- this.handleRPN(channelNumber, 0);
1158
+ this.handleRPN(channelNumber, scheduleTime);
1173
1159
  }
1174
1160
  updateChannelVolume(channel, scheduleTime) {
1175
- scheduleTime ??= this.audioContext.currentTime;
1176
1161
  const state = channel.state;
1177
1162
  const volume = state.volume * state.expression;
1178
1163
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1179
1164
  channel.gainL.gain
1180
- .cancelScheduledValues(now)
1165
+ .cancelScheduledValues(scheduleTime)
1181
1166
  .setValueAtTime(volume * gainLeft, scheduleTime);
1182
1167
  channel.gainR.gain
1183
- .cancelScheduledValues(now)
1168
+ .cancelScheduledValues(scheduleTime)
1184
1169
  .setValueAtTime(volume * gainRight, scheduleTime);
1185
1170
  }
1186
- setSustainPedal(channelNumber, value) {
1171
+ setSustainPedal(channelNumber, value, scheduleTime) {
1172
+ scheduleTime ??= this.audioContext.currentTime;
1187
1173
  this.channels[channelNumber].state.sustainPedal = value / 127;
1188
1174
  if (value < 64) {
1189
- this.releaseSustainPedal(channelNumber, value);
1175
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1190
1176
  }
1191
1177
  }
1192
1178
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
@@ -1215,12 +1201,12 @@ class MidyGM1 {
1215
1201
  channel.dataMSB = minMSB;
1216
1202
  }
1217
1203
  }
1218
- handleRPN(channelNumber) {
1204
+ handleRPN(channelNumber, scheduleTime) {
1219
1205
  const channel = this.channels[channelNumber];
1220
1206
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1221
1207
  switch (rpn) {
1222
1208
  case 0:
1223
- this.handlePitchBendRangeRPN(channelNumber);
1209
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1224
1210
  break;
1225
1211
  case 1:
1226
1212
  this.handleFineTuningRPN(channelNumber);
@@ -1238,25 +1224,26 @@ class MidyGM1 {
1238
1224
  setRPNLSB(channelNumber, value) {
1239
1225
  this.channels[channelNumber].rpnLSB = value;
1240
1226
  }
1241
- dataEntryMSB(channelNumber, value) {
1227
+ dataEntryMSB(channelNumber, value, scheduleTime) {
1242
1228
  this.channels[channelNumber].dataMSB = value;
1243
- this.handleRPN(channelNumber);
1229
+ this.handleRPN(channelNumber, scheduleTime);
1244
1230
  }
1245
- handlePitchBendRangeRPN(channelNumber) {
1231
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
1246
1232
  const channel = this.channels[channelNumber];
1247
1233
  this.limitData(channel, 0, 127, 0, 99);
1248
1234
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1249
- this.setPitchBendRange(channelNumber, pitchBendRange);
1235
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1250
1236
  }
1251
- setPitchBendRange(channelNumber, value) {
1237
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1238
+ scheduleTime ??= this.audioContext.currentTime;
1252
1239
  const channel = this.channels[channelNumber];
1253
1240
  const state = channel.state;
1254
1241
  const prev = state.pitchWheelSensitivity;
1255
1242
  const next = value / 128;
1256
1243
  state.pitchWheelSensitivity = next;
1257
1244
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1258
- this.updateChannelDetune(channel);
1259
- this.applyVoiceParams(channel, 16);
1245
+ this.updateChannelDetune(channel, scheduleTime);
1246
+ this.applyVoiceParams(channel, 16, scheduleTime);
1260
1247
  }
1261
1248
  handleFineTuningRPN(channelNumber) {
1262
1249
  const channel = this.channels[channelNumber];
@@ -1286,8 +1273,9 @@ class MidyGM1 {
1286
1273
  channel.detune += next - prev;
1287
1274
  this.updateChannelDetune(channel);
1288
1275
  }
1289
- allSoundOff(channelNumber) {
1290
- return this.stopChannelNotes(channelNumber, 0, true);
1276
+ allSoundOff(channelNumber, _value, scheduleTime) {
1277
+ scheduleTime ??= this.audioContext.currentTime;
1278
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1291
1279
  }
1292
1280
  resetAllControllers(channelNumber) {
1293
1281
  const stateTypes = [
@@ -1311,10 +1299,11 @@ class MidyGM1 {
1311
1299
  channel[type] = this.constructor.channelSettings[type];
1312
1300
  }
1313
1301
  }
1314
- allNotesOff(channelNumber) {
1315
- return this.stopChannelNotes(channelNumber, 0, false);
1302
+ allNotesOff(channelNumber, _value, scheduleTime) {
1303
+ scheduleTime ??= this.audioContext.currentTime;
1304
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1316
1305
  }
1317
- handleUniversalNonRealTimeExclusiveMessage(data) {
1306
+ handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
1318
1307
  switch (data[2]) {
1319
1308
  case 9:
1320
1309
  switch (data[3]) {
@@ -1338,12 +1327,12 @@ class MidyGM1 {
1338
1327
  }
1339
1328
  this.channels[9].bank = 128;
1340
1329
  }
1341
- handleUniversalRealTimeExclusiveMessage(data) {
1330
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1342
1331
  switch (data[2]) {
1343
1332
  case 4:
1344
1333
  switch (data[3]) {
1345
1334
  case 1:
1346
- return this.handleMasterVolumeSysEx(data);
1335
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
1347
1336
  default:
1348
1337
  console.warn(`Unsupported Exclusive Message: ${data}`);
1349
1338
  }
@@ -1352,42 +1341,40 @@ class MidyGM1 {
1352
1341
  console.warn(`Unsupported Exclusive Message: ${data}`);
1353
1342
  }
1354
1343
  }
1355
- handleMasterVolumeSysEx(data) {
1344
+ handleMasterVolumeSysEx(data, scheduleTime) {
1356
1345
  const volume = (data[5] * 128 + data[4]) / 16383;
1357
- this.setMasterVolume(volume);
1346
+ this.setMasterVolume(volume, scheduleTime);
1358
1347
  }
1359
- setMasterVolume(volume) {
1348
+ setMasterVolume(volume, scheduleTime) {
1349
+ scheduleTime ??= this.audioContext.currentTime;
1360
1350
  if (volume < 0 && 1 < volume) {
1361
1351
  console.error("Master Volume is out of range");
1362
1352
  }
1363
1353
  else {
1364
- const now = this.audioContext.currentTime;
1365
- this.masterVolume.gain.cancelScheduledValues(now);
1366
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
1354
+ this.masterVolume.gain
1355
+ .cancelScheduledValues(scheduleTime)
1356
+ .setValueAtTime(volume * volume, scheduleTime);
1367
1357
  }
1368
1358
  }
1369
- handleExclusiveMessage(data) {
1370
- console.warn(`Unsupported Exclusive Message: ${data}`);
1371
- }
1372
- handleSysEx(data) {
1359
+ handleSysEx(data, scheduleTime) {
1373
1360
  switch (data[0]) {
1374
1361
  case 126:
1375
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
1362
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
1376
1363
  case 127:
1377
- return this.handleUniversalRealTimeExclusiveMessage(data);
1364
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
1378
1365
  default:
1379
- return this.handleExclusiveMessage(data);
1366
+ console.warn(`Unsupported Exclusive Message: ${data}`);
1380
1367
  }
1381
1368
  }
1382
- scheduleTask(callback, startTime) {
1369
+ scheduleTask(callback, scheduleTime) {
1383
1370
  return new Promise((resolve) => {
1384
1371
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
1385
1372
  bufferSource.onended = () => {
1386
1373
  callback();
1387
1374
  resolve();
1388
1375
  };
1389
- bufferSource.start(startTime);
1390
- bufferSource.stop(startTime);
1376
+ bufferSource.start(scheduleTime);
1377
+ bufferSource.stop(scheduleTime);
1391
1378
  });
1392
1379
  }
1393
1380
  }