@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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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
  }
@@ -691,24 +693,17 @@ class MidyGMLite {
691
693
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
692
694
  return pitchWheel * pitchWheelSensitivity;
693
695
  }
694
- updateChannelDetune(channel) {
695
- channel.scheduledNotes.forEach((noteList) => {
696
- for (let i = 0; i < noteList.length; i++) {
697
- const note = noteList[i];
698
- if (!note)
699
- continue;
700
- this.updateDetune(channel, note);
701
- }
696
+ updateChannelDetune(channel, scheduleTime) {
697
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
698
+ this.updateDetune(channel, note, scheduleTime);
702
699
  });
703
700
  }
704
- updateDetune(channel, note) {
705
- const now = this.audioContext.currentTime;
701
+ updateDetune(channel, note, scheduleTime) {
706
702
  note.bufferSource.detune
707
- .cancelScheduledValues(now)
708
- .setValueAtTime(channel.detune, now);
703
+ .cancelScheduledValues(scheduleTime)
704
+ .setValueAtTime(channel.detune, scheduleTime);
709
705
  }
710
- setVolumeEnvelope(note) {
711
- const now = this.audioContext.currentTime;
706
+ setVolumeEnvelope(note, scheduleTime) {
712
707
  const { voiceParams, startTime } = note;
713
708
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
714
709
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
@@ -717,7 +712,7 @@ class MidyGMLite {
717
712
  const volHold = volAttack + voiceParams.volHold;
718
713
  const volDecay = volHold + voiceParams.volDecay;
719
714
  note.volumeEnvelopeNode.gain
720
- .cancelScheduledValues(now)
715
+ .cancelScheduledValues(scheduleTime)
721
716
  .setValueAtTime(0, startTime)
722
717
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
723
718
  .exponentialRampToValueAtTime(attackVolume, volAttack)
@@ -725,7 +720,6 @@ class MidyGMLite {
725
720
  .linearRampToValueAtTime(sustainVolume, volDecay);
726
721
  }
727
722
  setPitchEnvelope(note, scheduleTime) {
728
- scheduleTime ??= this.audioContext.currentTime;
729
723
  const { voiceParams } = note;
730
724
  const baseRate = voiceParams.playbackRate;
731
725
  note.bufferSource.playbackRate
@@ -752,8 +746,7 @@ class MidyGMLite {
752
746
  const maxFrequency = 20000; // max Hz of initialFilterFc
753
747
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
754
748
  }
755
- setFilterEnvelope(note) {
756
- const now = this.audioContext.currentTime;
749
+ setFilterEnvelope(note, scheduleTime) {
757
750
  const { voiceParams, startTime } = note;
758
751
  const baseFreq = this.centToHz(voiceParams.initialFilterFc);
759
752
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
@@ -767,14 +760,14 @@ class MidyGMLite {
767
760
  const modHold = modAttack + voiceParams.modHold;
768
761
  const modDecay = modHold + voiceParams.modDecay;
769
762
  note.filterNode.frequency
770
- .cancelScheduledValues(now)
763
+ .cancelScheduledValues(scheduleTime)
771
764
  .setValueAtTime(adjustedBaseFreq, startTime)
772
765
  .setValueAtTime(adjustedBaseFreq, modDelay)
773
766
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
774
767
  .setValueAtTime(adjustedPeekFreq, modHold)
775
768
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
776
769
  }
777
- startModulation(channel, note, startTime) {
770
+ startModulation(channel, note, scheduleTime) {
778
771
  const { voiceParams } = note;
779
772
  note.modulationLFO = new OscillatorNode(this.audioContext, {
780
773
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -783,10 +776,10 @@ class MidyGMLite {
783
776
  gain: voiceParams.modLfoToFilterFc,
784
777
  });
785
778
  note.modulationDepth = new GainNode(this.audioContext);
786
- this.setModLfoToPitch(channel, note);
779
+ this.setModLfoToPitch(channel, note, scheduleTime);
787
780
  note.volumeDepth = new GainNode(this.audioContext);
788
- this.setModLfoToVolume(note);
789
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
781
+ this.setModLfoToVolume(note, scheduleTime);
782
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
790
783
  note.modulationLFO.connect(note.filterDepth);
791
784
  note.filterDepth.connect(note.filterNode.frequency);
792
785
  note.modulationLFO.connect(note.modulationDepth);
@@ -813,6 +806,7 @@ class MidyGMLite {
813
806
  }
814
807
  }
815
808
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
809
+ const now = this.audioContext.currentTime;
816
810
  const state = channel.state;
817
811
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
818
812
  const voiceParams = voice.getAllParams(controllerState);
@@ -824,11 +818,11 @@ class MidyGMLite {
824
818
  type: "lowpass",
825
819
  Q: voiceParams.initialFilterQ / 10, // dB
826
820
  });
827
- this.setVolumeEnvelope(note);
828
- this.setFilterEnvelope(note);
829
- this.setPitchEnvelope(note);
821
+ this.setVolumeEnvelope(note, now);
822
+ this.setFilterEnvelope(note, now);
823
+ this.setPitchEnvelope(note, now);
830
824
  if (0 < state.modulationDepth) {
831
- this.startModulation(channel, note, startTime);
825
+ this.startModulation(channel, note, now);
832
826
  }
833
827
  note.bufferSource.connect(note.filterNode);
834
828
  note.filterNode.connect(note.volumeEnvelopeNode);
@@ -855,7 +849,7 @@ class MidyGMLite {
855
849
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
856
850
  const [prevNote, prevChannelNumber] = prevEntry;
857
851
  if (!prevNote.ending) {
858
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
852
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
859
853
  startTime, undefined, // portamentoNoteNumber
860
854
  true);
861
855
  }
@@ -870,9 +864,9 @@ class MidyGMLite {
870
864
  scheduledNotes.set(noteNumber, [note]);
871
865
  }
872
866
  }
873
- noteOn(channelNumber, noteNumber, velocity) {
874
- const now = this.audioContext.currentTime;
875
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
867
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
868
+ scheduleTime ??= this.audioContext.currentTime;
869
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
876
870
  }
877
871
  stopNote(endTime, stopTime, scheduledNotes, index) {
878
872
  const note = scheduledNotes[index];
@@ -899,7 +893,7 @@ class MidyGMLite {
899
893
  note.bufferSource.stop(stopTime);
900
894
  });
901
895
  }
902
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
896
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
903
897
  const channel = this.channels[channelNumber];
904
898
  if (!force && 0.5 < channel.state.sustainPedal)
905
899
  return;
@@ -921,127 +915,119 @@ class MidyGMLite {
921
915
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
922
916
  }
923
917
  }
924
- releaseNote(channelNumber, noteNumber, velocity) {
925
- const now = this.audioContext.currentTime;
926
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
918
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
919
+ scheduleTime ??= this.audioContext.currentTime;
920
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
927
921
  }
928
- releaseSustainPedal(channelNumber, halfVelocity) {
922
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
929
923
  const velocity = halfVelocity * 2;
930
924
  const channel = this.channels[channelNumber];
931
925
  const promises = [];
932
- channel.state.sustainPedal = halfVelocity;
933
- channel.scheduledNotes.forEach((noteList) => {
934
- for (let i = 0; i < noteList.length; i++) {
935
- const note = noteList[i];
936
- if (!note)
937
- continue;
938
- const { noteNumber } = note;
939
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
940
- promises.push(promise);
941
- }
926
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
927
+ const { noteNumber } = note;
928
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
929
+ promises.push(promise);
942
930
  });
943
931
  return promises;
944
932
  }
945
- handleMIDIMessage(statusByte, data1, data2) {
933
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
946
934
  const channelNumber = statusByte & 0x0F;
947
935
  const messageType = statusByte & 0xF0;
948
936
  switch (messageType) {
949
937
  case 0x80:
950
- return this.releaseNote(channelNumber, data1, data2);
938
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
951
939
  case 0x90:
952
- return this.noteOn(channelNumber, data1, data2);
940
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
953
941
  case 0xB0:
954
- return this.handleControlChange(channelNumber, data1, data2);
942
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
955
943
  case 0xC0:
956
- return this.handleProgramChange(channelNumber, data1);
944
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
957
945
  case 0xE0:
958
- return this.handlePitchBendMessage(channelNumber, data1, data2);
946
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
959
947
  default:
960
948
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
961
949
  }
962
950
  }
963
- handleProgramChange(channelNumber, program) {
951
+ handleProgramChange(channelNumber, program, _scheduleTime) {
964
952
  const channel = this.channels[channelNumber];
965
953
  channel.program = program;
966
954
  }
967
- handlePitchBendMessage(channelNumber, lsb, msb) {
955
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
968
956
  const pitchBend = msb * 128 + lsb;
969
- this.setPitchBend(channelNumber, pitchBend);
957
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
970
958
  }
971
- setPitchBend(channelNumber, value) {
959
+ setPitchBend(channelNumber, value, scheduleTime) {
960
+ scheduleTime ??= this.audioContext.currentTime;
972
961
  const channel = this.channels[channelNumber];
973
962
  const state = channel.state;
974
963
  const prev = state.pitchWheel * 2 - 1;
975
964
  const next = (value - 8192) / 8192;
976
965
  state.pitchWheel = value / 16383;
977
966
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
978
- this.updateChannelDetune(channel);
979
- this.applyVoiceParams(channel, 14);
967
+ this.updateChannelDetune(channel, scheduleTime);
968
+ this.applyVoiceParams(channel, 14, scheduleTime);
980
969
  }
981
- setModLfoToPitch(channel, note) {
982
- const now = this.audioContext.currentTime;
970
+ setModLfoToPitch(channel, note, scheduleTime) {
983
971
  const modLfoToPitch = note.voiceParams.modLfoToPitch;
984
972
  const baseDepth = Math.abs(modLfoToPitch) +
985
973
  channel.state.modulationDepth;
986
974
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
987
975
  note.modulationDepth.gain
988
- .cancelScheduledValues(now)
989
- .setValueAtTime(modulationDepth, now);
976
+ .cancelScheduledValues(scheduleTime)
977
+ .setValueAtTime(modulationDepth, scheduleTime);
990
978
  }
991
- setModLfoToFilterFc(note) {
992
- const now = this.audioContext.currentTime;
979
+ setModLfoToFilterFc(note, scheduleTime) {
993
980
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
994
981
  note.filterDepth.gain
995
- .cancelScheduledValues(now)
996
- .setValueAtTime(modLfoToFilterFc, now);
982
+ .cancelScheduledValues(scheduleTime)
983
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
997
984
  }
998
- setModLfoToVolume(note) {
999
- const now = this.audioContext.currentTime;
985
+ setModLfoToVolume(note, scheduleTime) {
1000
986
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1001
987
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1002
988
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1003
989
  note.volumeDepth.gain
1004
- .cancelScheduledValues(now)
1005
- .setValueAtTime(volumeDepth, now);
990
+ .cancelScheduledValues(scheduleTime)
991
+ .setValueAtTime(volumeDepth, scheduleTime);
1006
992
  }
1007
- setDelayModLFO(note) {
1008
- const now = this.audioContext.currentTime;
993
+ setDelayModLFO(note, scheduleTime) {
1009
994
  const startTime = note.startTime;
1010
- if (startTime < now)
995
+ if (startTime < scheduleTime)
1011
996
  return;
1012
- note.modulationLFO.stop(now);
997
+ note.modulationLFO.stop(scheduleTime);
1013
998
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1014
999
  note.modulationLFO.connect(note.filterDepth);
1015
1000
  }
1016
- setFreqModLFO(note) {
1017
- const now = this.audioContext.currentTime;
1001
+ setFreqModLFO(note, scheduleTime) {
1018
1002
  const freqModLFO = note.voiceParams.freqModLFO;
1019
1003
  note.modulationLFO.frequency
1020
- .cancelScheduledValues(now)
1021
- .setValueAtTime(freqModLFO, now);
1004
+ .cancelScheduledValues(scheduleTime)
1005
+ .setValueAtTime(freqModLFO, scheduleTime);
1022
1006
  }
1023
1007
  createVoiceParamsHandlers() {
1024
1008
  return {
1025
- modLfoToPitch: (channel, note, _prevValue) => {
1009
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1026
1010
  if (0 < channel.state.modulationDepth) {
1027
- this.setModLfoToPitch(channel, note);
1011
+ this.setModLfoToPitch(channel, note, scheduleTime);
1028
1012
  }
1029
1013
  },
1030
- vibLfoToPitch: (_channel, _note, _prevValue) => { },
1031
- modLfoToFilterFc: (channel, note, _prevValue) => {
1032
- if (0 < channel.state.modulationDepth)
1033
- this.setModLfoToFilterFc(note);
1014
+ vibLfoToPitch: (_channel, _note, _prevValue, _scheduleTime) => { },
1015
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1016
+ if (0 < channel.state.modulationDepth) {
1017
+ this.setModLfoToFilterFc(note, scheduleTime);
1018
+ }
1034
1019
  },
1035
- modLfoToVolume: (channel, note, _prevValue) => {
1036
- if (0 < channel.state.modulationDepth)
1037
- this.setModLfoToVolume(note);
1020
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1021
+ if (0 < channel.state.modulationDepth) {
1022
+ this.setModLfoToVolume(note, scheduleTime);
1023
+ }
1038
1024
  },
1039
- chorusEffectsSend: (_channel, _note, _prevValue) => { },
1040
- reverbEffectsSend: (_channel, _note, _prevValue) => { },
1041
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1042
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1043
- delayVibLFO: (_channel, _note, _prevValue) => { },
1044
- freqVibLFO: (_channel, _note, _prevValue) => { },
1025
+ chorusEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1026
+ reverbEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1027
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1028
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1029
+ delayVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1030
+ freqVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1045
1031
  };
1046
1032
  }
1047
1033
  getControllerState(channel, noteNumber, velocity) {
@@ -1051,7 +1037,7 @@ class MidyGMLite {
1051
1037
  state[3] = noteNumber / 127;
1052
1038
  return state;
1053
1039
  }
1054
- applyVoiceParams(channel, controllerType) {
1040
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1055
1041
  channel.scheduledNotes.forEach((noteList) => {
1056
1042
  for (let i = 0; i < noteList.length; i++) {
1057
1043
  const note = noteList[i];
@@ -1067,7 +1053,7 @@ class MidyGMLite {
1067
1053
  continue;
1068
1054
  note.voiceParams[key] = value;
1069
1055
  if (key in this.voiceParamsHandlers) {
1070
- this.voiceParamsHandlers[key](channel, note, prevValue);
1056
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1071
1057
  }
1072
1058
  else if (filterEnvelopeKeySet.has(key)) {
1073
1059
  if (appliedFilterEnvelope)
@@ -1079,8 +1065,8 @@ class MidyGMLite {
1079
1065
  if (key in voiceParams)
1080
1066
  noteVoiceParams[key] = voiceParams[key];
1081
1067
  }
1082
- this.setFilterEnvelope(note);
1083
- this.setPitchEnvelope(note);
1068
+ this.setFilterEnvelope(note, scheduleTime);
1069
+ this.setPitchEnvelope(note, scheduleTime);
1084
1070
  }
1085
1071
  else if (volumeEnvelopeKeySet.has(key)) {
1086
1072
  if (appliedVolumeEnvelope)
@@ -1092,7 +1078,7 @@ class MidyGMLite {
1092
1078
  if (key in voiceParams)
1093
1079
  noteVoiceParams[key] = voiceParams[key];
1094
1080
  }
1095
- this.setVolumeEnvelope(channel, note);
1081
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1096
1082
  }
1097
1083
  }
1098
1084
  }
@@ -1114,12 +1100,12 @@ class MidyGMLite {
1114
1100
  123: this.allNotesOff,
1115
1101
  };
1116
1102
  }
1117
- handleControlChange(channelNumber, controllerType, value, startTime) {
1103
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1118
1104
  const handler = this.controlChangeHandlers[controllerType];
1119
1105
  if (handler) {
1120
- handler.call(this, channelNumber, value, startTime);
1106
+ handler.call(this, channelNumber, value, scheduleTime);
1121
1107
  const channel = this.channels[channelNumber];
1122
- this.applyVoiceParams(channel, controllerType + 128);
1108
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1123
1109
  }
1124
1110
  else {
1125
1111
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1165,26 +1151,26 @@ class MidyGMLite {
1165
1151
  channel.state.expression = expression / 127;
1166
1152
  this.updateChannelVolume(channel, scheduleTime);
1167
1153
  }
1168
- dataEntryLSB(channelNumber, value) {
1154
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1169
1155
  this.channels[channelNumber].dataLSB = value;
1170
- this.handleRPN(channelNumber);
1156
+ this.handleRPN(channelNumber, scheduleTime);
1171
1157
  }
1172
1158
  updateChannelVolume(channel, scheduleTime) {
1173
- scheduleTime ??= this.audioContext.currentTime;
1174
1159
  const state = channel.state;
1175
1160
  const volume = state.volume * state.expression;
1176
1161
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1177
1162
  channel.gainL.gain
1178
- .cancelScheduledValues(now)
1163
+ .cancelScheduledValues(scheduleTime)
1179
1164
  .setValueAtTime(volume * gainLeft, scheduleTime);
1180
1165
  channel.gainR.gain
1181
- .cancelScheduledValues(now)
1166
+ .cancelScheduledValues(scheduleTime)
1182
1167
  .setValueAtTime(volume * gainRight, scheduleTime);
1183
1168
  }
1184
- setSustainPedal(channelNumber, value) {
1169
+ setSustainPedal(channelNumber, value, scheduleTime) {
1170
+ scheduleTime ??= this.audioContext.currentTime;
1185
1171
  this.channels[channelNumber].state.sustainPedal = value / 127;
1186
1172
  if (value < 64) {
1187
- this.releaseSustainPedal(channelNumber, value);
1173
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1188
1174
  }
1189
1175
  }
1190
1176
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
@@ -1205,12 +1191,12 @@ class MidyGMLite {
1205
1191
  channel.dataLSB = minLSB;
1206
1192
  }
1207
1193
  }
1208
- handleRPN(channelNumber) {
1194
+ handleRPN(channelNumber, scheduleTime) {
1209
1195
  const channel = this.channels[channelNumber];
1210
1196
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1211
1197
  switch (rpn) {
1212
1198
  case 0:
1213
- this.handlePitchBendRangeRPN(channelNumber);
1199
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1214
1200
  break;
1215
1201
  default:
1216
1202
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
@@ -1222,28 +1208,30 @@ class MidyGMLite {
1222
1208
  setRPNLSB(channelNumber, value) {
1223
1209
  this.channels[channelNumber].rpnLSB = value;
1224
1210
  }
1225
- dataEntryMSB(channelNumber, value) {
1211
+ dataEntryMSB(channelNumber, value, scheduleTime) {
1226
1212
  this.channels[channelNumber].dataMSB = value;
1227
- this.handleRPN(channelNumber);
1213
+ this.handleRPN(channelNumber, scheduleTime);
1228
1214
  }
1229
- handlePitchBendRangeRPN(channelNumber) {
1215
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
1230
1216
  const channel = this.channels[channelNumber];
1231
1217
  this.limitData(channel, 0, 127, 0, 99);
1232
1218
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1233
- this.setPitchBendRange(channelNumber, pitchBendRange);
1219
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1234
1220
  }
1235
- setPitchBendRange(channelNumber, value) {
1221
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1222
+ scheduleTime ??= this.audioContext.currentTime;
1236
1223
  const channel = this.channels[channelNumber];
1237
1224
  const state = channel.state;
1238
1225
  const prev = state.pitchWheelSensitivity;
1239
1226
  const next = value / 128;
1240
1227
  state.pitchWheelSensitivity = next;
1241
1228
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1242
- this.updateChannelDetune(channel);
1243
- this.applyVoiceParams(channel, 16);
1229
+ this.updateChannelDetune(channel, scheduleTime);
1230
+ this.applyVoiceParams(channel, 16, scheduleTime);
1244
1231
  }
1245
- allSoundOff(channelNumber) {
1246
- return this.stopChannelNotes(channelNumber, 0, true);
1232
+ allSoundOff(channelNumber, _value, scheduleTime) {
1233
+ scheduleTime ??= this.audioContext.currentTime;
1234
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1247
1235
  }
1248
1236
  resetAllControllers(channelNumber) {
1249
1237
  const stateTypes = [
@@ -1267,10 +1255,11 @@ class MidyGMLite {
1267
1255
  channel[type] = this.constructor.channelSettings[type];
1268
1256
  }
1269
1257
  }
1270
- allNotesOff(channelNumber) {
1271
- return this.stopChannelNotes(channelNumber, 0, false);
1258
+ allNotesOff(channelNumber, _value, scheduleTime) {
1259
+ scheduleTime ??= this.audioContext.currentTime;
1260
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1272
1261
  }
1273
- handleUniversalNonRealTimeExclusiveMessage(data) {
1262
+ handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
1274
1263
  switch (data[2]) {
1275
1264
  case 9:
1276
1265
  switch (data[3]) {
@@ -1294,12 +1283,12 @@ class MidyGMLite {
1294
1283
  }
1295
1284
  this.channels[9].bank = 128;
1296
1285
  }
1297
- handleUniversalRealTimeExclusiveMessage(data) {
1286
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1298
1287
  switch (data[2]) {
1299
1288
  case 4:
1300
1289
  switch (data[3]) {
1301
1290
  case 1:
1302
- return this.handleMasterVolumeSysEx(data);
1291
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
1303
1292
  default:
1304
1293
  console.warn(`Unsupported Exclusive Message: ${data}`);
1305
1294
  }
@@ -1308,42 +1297,40 @@ class MidyGMLite {
1308
1297
  console.warn(`Unsupported Exclusive Message: ${data}`);
1309
1298
  }
1310
1299
  }
1311
- handleMasterVolumeSysEx(data) {
1300
+ handleMasterVolumeSysEx(data, scheduleTime) {
1312
1301
  const volume = (data[5] * 128 + data[4]) / 16383;
1313
- this.setMasterVolume(volume);
1302
+ this.setMasterVolume(volume, scheduleTime);
1314
1303
  }
1315
- setMasterVolume(volume) {
1304
+ setMasterVolume(volume, scheduleTime) {
1305
+ scheduleTime ??= this.audioContext.currentTime;
1316
1306
  if (volume < 0 && 1 < volume) {
1317
1307
  console.error("Master Volume is out of range");
1318
1308
  }
1319
1309
  else {
1320
- const now = this.audioContext.currentTime;
1321
- this.masterVolume.gain.cancelScheduledValues(now);
1322
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
1310
+ this.masterVolume.gain
1311
+ .cancelScheduledValues(scheduleTime)
1312
+ .setValueAtTime(volume * volume, scheduleTime);
1323
1313
  }
1324
1314
  }
1325
- handleExclusiveMessage(data) {
1326
- console.warn(`Unsupported Exclusive Message: ${data}`);
1327
- }
1328
- handleSysEx(data) {
1315
+ handleSysEx(data, scheduleTime) {
1329
1316
  switch (data[0]) {
1330
1317
  case 126:
1331
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
1318
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
1332
1319
  case 127:
1333
- return this.handleUniversalRealTimeExclusiveMessage(data);
1320
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
1334
1321
  default:
1335
- return this.handleExclusiveMessage(data);
1322
+ console.warn(`Unsupported Exclusive Message: ${data}`);
1336
1323
  }
1337
1324
  }
1338
- scheduleTask(callback, startTime) {
1325
+ scheduleTask(callback, scheduleTime) {
1339
1326
  return new Promise((resolve) => {
1340
1327
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
1341
1328
  bufferSource.onended = () => {
1342
1329
  callback();
1343
1330
  resolve();
1344
1331
  };
1345
- bufferSource.start(startTime);
1346
- bufferSource.stop(startTime);
1332
+ bufferSource.start(scheduleTime);
1333
+ bufferSource.stop(scheduleTime);
1347
1334
  });
1348
1335
  }
1349
1336
  }