@marmooo/midy 0.2.6 → 0.2.8

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.
@@ -358,6 +358,7 @@ class MidyGMLite {
358
358
  state: new ControllerState(),
359
359
  ...this.setChannelAudioNodes(audioContext),
360
360
  scheduledNotes: new SparseMap(128),
361
+ sustainNotes: [],
361
362
  };
362
363
  });
363
364
  return channels;
@@ -415,7 +416,7 @@ class MidyGMLite {
415
416
  }
416
417
  /* falls through */
417
418
  case "noteOff": {
418
- const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, startTime);
419
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
419
420
  if (notePromise) {
420
421
  this.notePromises.push(notePromise);
421
422
  }
@@ -462,10 +463,11 @@ class MidyGMLite {
462
463
  resolve();
463
464
  return;
464
465
  }
465
- const t = this.audioContext.currentTime + offset;
466
+ const now = this.audioContext.currentTime;
467
+ const t = now + offset;
466
468
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
467
469
  if (this.isPausing) {
468
- await this.stopNotes(0, true);
470
+ await this.stopNotes(0, true, now);
469
471
  this.notePromises = [];
470
472
  resolve();
471
473
  this.isPausing = false;
@@ -473,7 +475,7 @@ class MidyGMLite {
473
475
  return;
474
476
  }
475
477
  else if (this.isStopping) {
476
- await this.stopNotes(0, true);
478
+ await this.stopNotes(0, true, now);
477
479
  this.notePromises = [];
478
480
  this.exclusiveClassMap.clear();
479
481
  this.audioBufferCache.clear();
@@ -483,7 +485,7 @@ class MidyGMLite {
483
485
  return;
484
486
  }
485
487
  else if (this.isSeeking) {
486
- this.stopNotes(0, true);
488
+ this.stopNotes(0, true, now);
487
489
  this.exclusiveClassMap.clear();
488
490
  this.startTime = this.audioContext.currentTime;
489
491
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -492,7 +494,6 @@ class MidyGMLite {
492
494
  await schedulePlayback();
493
495
  }
494
496
  else {
495
- const now = this.audioContext.currentTime;
496
497
  const waitTime = now + this.noteCheckInterval;
497
498
  await this.scheduleTask(() => { }, waitTime);
498
499
  await schedulePlayback();
@@ -576,24 +577,21 @@ class MidyGMLite {
576
577
  }
577
578
  return { instruments, timeline };
578
579
  }
579
- async stopChannelNotes(channelNumber, velocity, force) {
580
- const now = this.audioContext.currentTime;
580
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
581
581
  const channel = this.channels[channelNumber];
582
- channel.scheduledNotes.forEach((noteList) => {
583
- for (let i = 0; i < noteList.length; i++) {
584
- const note = noteList[i];
585
- if (!note)
586
- continue;
587
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, force);
588
- this.notePromises.push(promise);
589
- }
582
+ const promises = [];
583
+ this.processScheduledNotes(channel, (note) => {
584
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
585
+ this.notePromises.push(promise);
586
+ promises.push(promise);
590
587
  });
591
588
  channel.scheduledNotes.clear();
592
- await Promise.all(this.notePromises);
589
+ return Promise.all(promises);
593
590
  }
594
- stopNotes(velocity, force) {
591
+ stopNotes(velocity, force, scheduleTime) {
592
+ const promises = [];
595
593
  for (let i = 0; i < this.channels.length; i++) {
596
- this.stopChannelNotes(i, velocity, force);
594
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
597
595
  }
598
596
  return Promise.all(this.notePromises);
599
597
  }
@@ -641,34 +639,32 @@ class MidyGMLite {
641
639
  const now = this.audioContext.currentTime;
642
640
  return this.resumeTime + now - this.startTime - this.startDelay;
643
641
  }
644
- processScheduledNotes(channel, scheduleTime, callback) {
642
+ processScheduledNotes(channel, callback) {
645
643
  channel.scheduledNotes.forEach((noteList) => {
646
644
  for (let i = 0; i < noteList.length; i++) {
647
645
  const note = noteList[i];
648
646
  if (!note)
649
647
  continue;
650
- if (scheduleTime < note.startTime)
651
- continue;
652
648
  callback(note);
653
649
  }
654
650
  });
655
651
  }
656
- getActiveNotes(channel, time) {
652
+ getActiveNotes(channel, scheduleTime) {
657
653
  const activeNotes = new SparseMap(128);
658
654
  channel.scheduledNotes.forEach((noteList) => {
659
- const activeNote = this.getActiveNote(noteList, time);
655
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
660
656
  if (activeNote) {
661
657
  activeNotes.set(activeNote.noteNumber, activeNote);
662
658
  }
663
659
  });
664
660
  return activeNotes;
665
661
  }
666
- getActiveNote(noteList, time) {
662
+ getActiveNote(noteList, scheduleTime) {
667
663
  for (let i = noteList.length - 1; i >= 0; i--) {
668
664
  const note = noteList[i];
669
665
  if (!note)
670
666
  return;
671
- if (time < note.startTime)
667
+ if (scheduleTime < note.startTime)
672
668
  continue;
673
669
  return (note.ending) ? null : note;
674
670
  }
@@ -691,24 +687,17 @@ class MidyGMLite {
691
687
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
692
688
  return pitchWheel * pitchWheelSensitivity;
693
689
  }
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
- }
690
+ updateChannelDetune(channel, scheduleTime) {
691
+ this.processScheduledNotes(channel, (note) => {
692
+ this.updateDetune(channel, note, scheduleTime);
702
693
  });
703
694
  }
704
- updateDetune(channel, note) {
705
- const now = this.audioContext.currentTime;
695
+ updateDetune(channel, note, scheduleTime) {
706
696
  note.bufferSource.detune
707
- .cancelScheduledValues(now)
708
- .setValueAtTime(channel.detune, now);
697
+ .cancelScheduledValues(scheduleTime)
698
+ .setValueAtTime(channel.detune, scheduleTime);
709
699
  }
710
- setVolumeEnvelope(note) {
711
- const now = this.audioContext.currentTime;
700
+ setVolumeEnvelope(note, scheduleTime) {
712
701
  const { voiceParams, startTime } = note;
713
702
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
714
703
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
@@ -717,7 +706,7 @@ class MidyGMLite {
717
706
  const volHold = volAttack + voiceParams.volHold;
718
707
  const volDecay = volHold + voiceParams.volDecay;
719
708
  note.volumeEnvelopeNode.gain
720
- .cancelScheduledValues(now)
709
+ .cancelScheduledValues(scheduleTime)
721
710
  .setValueAtTime(0, startTime)
722
711
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
723
712
  .exponentialRampToValueAtTime(attackVolume, volAttack)
@@ -725,7 +714,6 @@ class MidyGMLite {
725
714
  .linearRampToValueAtTime(sustainVolume, volDecay);
726
715
  }
727
716
  setPitchEnvelope(note, scheduleTime) {
728
- scheduleTime ??= this.audioContext.currentTime;
729
717
  const { voiceParams } = note;
730
718
  const baseRate = voiceParams.playbackRate;
731
719
  note.bufferSource.playbackRate
@@ -752,8 +740,7 @@ class MidyGMLite {
752
740
  const maxFrequency = 20000; // max Hz of initialFilterFc
753
741
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
754
742
  }
755
- setFilterEnvelope(note) {
756
- const now = this.audioContext.currentTime;
743
+ setFilterEnvelope(note, scheduleTime) {
757
744
  const { voiceParams, startTime } = note;
758
745
  const baseFreq = this.centToHz(voiceParams.initialFilterFc);
759
746
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
@@ -767,14 +754,14 @@ class MidyGMLite {
767
754
  const modHold = modAttack + voiceParams.modHold;
768
755
  const modDecay = modHold + voiceParams.modDecay;
769
756
  note.filterNode.frequency
770
- .cancelScheduledValues(now)
757
+ .cancelScheduledValues(scheduleTime)
771
758
  .setValueAtTime(adjustedBaseFreq, startTime)
772
759
  .setValueAtTime(adjustedBaseFreq, modDelay)
773
760
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
774
761
  .setValueAtTime(adjustedPeekFreq, modHold)
775
762
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
776
763
  }
777
- startModulation(channel, note, startTime) {
764
+ startModulation(channel, note, scheduleTime) {
778
765
  const { voiceParams } = note;
779
766
  note.modulationLFO = new OscillatorNode(this.audioContext, {
780
767
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -783,10 +770,10 @@ class MidyGMLite {
783
770
  gain: voiceParams.modLfoToFilterFc,
784
771
  });
785
772
  note.modulationDepth = new GainNode(this.audioContext);
786
- this.setModLfoToPitch(channel, note);
773
+ this.setModLfoToPitch(channel, note, scheduleTime);
787
774
  note.volumeDepth = new GainNode(this.audioContext);
788
- this.setModLfoToVolume(note);
789
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
775
+ this.setModLfoToVolume(note, scheduleTime);
776
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
790
777
  note.modulationLFO.connect(note.filterDepth);
791
778
  note.filterDepth.connect(note.filterNode.frequency);
792
779
  note.modulationLFO.connect(note.modulationDepth);
@@ -813,6 +800,7 @@ class MidyGMLite {
813
800
  }
814
801
  }
815
802
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
803
+ const now = this.audioContext.currentTime;
816
804
  const state = channel.state;
817
805
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
818
806
  const voiceParams = voice.getAllParams(controllerState);
@@ -824,11 +812,11 @@ class MidyGMLite {
824
812
  type: "lowpass",
825
813
  Q: voiceParams.initialFilterQ / 10, // dB
826
814
  });
827
- this.setVolumeEnvelope(note);
828
- this.setFilterEnvelope(note);
829
- this.setPitchEnvelope(note);
815
+ this.setVolumeEnvelope(note, now);
816
+ this.setFilterEnvelope(note, now);
817
+ this.setPitchEnvelope(note, now);
830
818
  if (0 < state.modulationDepth) {
831
- this.startModulation(channel, note, startTime);
819
+ this.startModulation(channel, note, now);
832
820
  }
833
821
  note.bufferSource.connect(note.filterNode);
834
822
  note.filterNode.connect(note.volumeEnvelopeNode);
@@ -849,15 +837,17 @@ class MidyGMLite {
849
837
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
850
838
  note.volumeEnvelopeNode.connect(channel.gainL);
851
839
  note.volumeEnvelopeNode.connect(channel.gainR);
840
+ if (0.5 <= channel.state.sustainPedal) {
841
+ channel.sustainNotes.push(note);
842
+ }
852
843
  const exclusiveClass = note.voiceParams.exclusiveClass;
853
844
  if (exclusiveClass !== 0) {
854
845
  if (this.exclusiveClassMap.has(exclusiveClass)) {
855
846
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
856
847
  const [prevNote, prevChannelNumber] = prevEntry;
857
848
  if (!prevNote.ending) {
858
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
859
- startTime, undefined, // portamentoNoteNumber
860
- true);
849
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
850
+ startTime, true);
861
851
  }
862
852
  }
863
853
  this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
@@ -870,9 +860,9 @@ class MidyGMLite {
870
860
  scheduledNotes.set(noteNumber, [note]);
871
861
  }
872
862
  }
873
- noteOn(channelNumber, noteNumber, velocity) {
874
- const now = this.audioContext.currentTime;
875
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
863
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
864
+ scheduleTime ??= this.audioContext.currentTime;
865
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
876
866
  }
877
867
  stopNote(endTime, stopTime, scheduledNotes, index) {
878
868
  const note = scheduledNotes[index];
@@ -899,9 +889,9 @@ class MidyGMLite {
899
889
  note.bufferSource.stop(stopTime);
900
890
  });
901
891
  }
902
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
892
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
903
893
  const channel = this.channels[channelNumber];
904
- if (!force && 0.5 < channel.state.sustainPedal)
894
+ if (!force && 0.5 <= channel.state.sustainPedal)
905
895
  return;
906
896
  if (!channel.scheduledNotes.has(noteNumber))
907
897
  return;
@@ -921,127 +911,119 @@ class MidyGMLite {
921
911
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
922
912
  }
923
913
  }
924
- releaseNote(channelNumber, noteNumber, velocity) {
925
- const now = this.audioContext.currentTime;
926
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
914
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
915
+ scheduleTime ??= this.audioContext.currentTime;
916
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
927
917
  }
928
- releaseSustainPedal(channelNumber, halfVelocity) {
918
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
929
919
  const velocity = halfVelocity * 2;
930
920
  const channel = this.channels[channelNumber];
931
921
  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
- }
942
- });
922
+ for (let i = 0; i < channel.sustainNotes.length; i++) {
923
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
924
+ promises.push(promise);
925
+ }
926
+ channel.sustainNotes = [];
943
927
  return promises;
944
928
  }
945
- handleMIDIMessage(statusByte, data1, data2) {
929
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
946
930
  const channelNumber = statusByte & 0x0F;
947
931
  const messageType = statusByte & 0xF0;
948
932
  switch (messageType) {
949
933
  case 0x80:
950
- return this.releaseNote(channelNumber, data1, data2);
934
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
951
935
  case 0x90:
952
- return this.noteOn(channelNumber, data1, data2);
936
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
953
937
  case 0xB0:
954
- return this.handleControlChange(channelNumber, data1, data2);
938
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
955
939
  case 0xC0:
956
- return this.handleProgramChange(channelNumber, data1);
940
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
957
941
  case 0xE0:
958
- return this.handlePitchBendMessage(channelNumber, data1, data2);
942
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
959
943
  default:
960
944
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
961
945
  }
962
946
  }
963
- handleProgramChange(channelNumber, program) {
947
+ handleProgramChange(channelNumber, program, _scheduleTime) {
964
948
  const channel = this.channels[channelNumber];
965
949
  channel.program = program;
966
950
  }
967
- handlePitchBendMessage(channelNumber, lsb, msb) {
951
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
968
952
  const pitchBend = msb * 128 + lsb;
969
- this.setPitchBend(channelNumber, pitchBend);
953
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
970
954
  }
971
- setPitchBend(channelNumber, value) {
955
+ setPitchBend(channelNumber, value, scheduleTime) {
956
+ scheduleTime ??= this.audioContext.currentTime;
972
957
  const channel = this.channels[channelNumber];
973
958
  const state = channel.state;
974
959
  const prev = state.pitchWheel * 2 - 1;
975
960
  const next = (value - 8192) / 8192;
976
961
  state.pitchWheel = value / 16383;
977
962
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
978
- this.updateChannelDetune(channel);
979
- this.applyVoiceParams(channel, 14);
963
+ this.updateChannelDetune(channel, scheduleTime);
964
+ this.applyVoiceParams(channel, 14, scheduleTime);
980
965
  }
981
- setModLfoToPitch(channel, note) {
982
- const now = this.audioContext.currentTime;
966
+ setModLfoToPitch(channel, note, scheduleTime) {
983
967
  const modLfoToPitch = note.voiceParams.modLfoToPitch;
984
968
  const baseDepth = Math.abs(modLfoToPitch) +
985
969
  channel.state.modulationDepth;
986
970
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
987
971
  note.modulationDepth.gain
988
- .cancelScheduledValues(now)
989
- .setValueAtTime(modulationDepth, now);
972
+ .cancelScheduledValues(scheduleTime)
973
+ .setValueAtTime(modulationDepth, scheduleTime);
990
974
  }
991
- setModLfoToFilterFc(note) {
992
- const now = this.audioContext.currentTime;
975
+ setModLfoToFilterFc(note, scheduleTime) {
993
976
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
994
977
  note.filterDepth.gain
995
- .cancelScheduledValues(now)
996
- .setValueAtTime(modLfoToFilterFc, now);
978
+ .cancelScheduledValues(scheduleTime)
979
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
997
980
  }
998
- setModLfoToVolume(note) {
999
- const now = this.audioContext.currentTime;
981
+ setModLfoToVolume(note, scheduleTime) {
1000
982
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1001
983
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1002
984
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1003
985
  note.volumeDepth.gain
1004
- .cancelScheduledValues(now)
1005
- .setValueAtTime(volumeDepth, now);
986
+ .cancelScheduledValues(scheduleTime)
987
+ .setValueAtTime(volumeDepth, scheduleTime);
1006
988
  }
1007
- setDelayModLFO(note) {
1008
- const now = this.audioContext.currentTime;
989
+ setDelayModLFO(note, scheduleTime) {
1009
990
  const startTime = note.startTime;
1010
- if (startTime < now)
991
+ if (startTime < scheduleTime)
1011
992
  return;
1012
- note.modulationLFO.stop(now);
993
+ note.modulationLFO.stop(scheduleTime);
1013
994
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1014
995
  note.modulationLFO.connect(note.filterDepth);
1015
996
  }
1016
- setFreqModLFO(note) {
1017
- const now = this.audioContext.currentTime;
997
+ setFreqModLFO(note, scheduleTime) {
1018
998
  const freqModLFO = note.voiceParams.freqModLFO;
1019
999
  note.modulationLFO.frequency
1020
- .cancelScheduledValues(now)
1021
- .setValueAtTime(freqModLFO, now);
1000
+ .cancelScheduledValues(scheduleTime)
1001
+ .setValueAtTime(freqModLFO, scheduleTime);
1022
1002
  }
1023
1003
  createVoiceParamsHandlers() {
1024
1004
  return {
1025
- modLfoToPitch: (channel, note, _prevValue) => {
1005
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1026
1006
  if (0 < channel.state.modulationDepth) {
1027
- this.setModLfoToPitch(channel, note);
1007
+ this.setModLfoToPitch(channel, note, scheduleTime);
1028
1008
  }
1029
1009
  },
1030
- vibLfoToPitch: (_channel, _note, _prevValue) => { },
1031
- modLfoToFilterFc: (channel, note, _prevValue) => {
1032
- if (0 < channel.state.modulationDepth)
1033
- this.setModLfoToFilterFc(note);
1010
+ vibLfoToPitch: (_channel, _note, _prevValue, _scheduleTime) => { },
1011
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1012
+ if (0 < channel.state.modulationDepth) {
1013
+ this.setModLfoToFilterFc(note, scheduleTime);
1014
+ }
1034
1015
  },
1035
- modLfoToVolume: (channel, note, _prevValue) => {
1036
- if (0 < channel.state.modulationDepth)
1037
- this.setModLfoToVolume(note);
1016
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1017
+ if (0 < channel.state.modulationDepth) {
1018
+ this.setModLfoToVolume(note, scheduleTime);
1019
+ }
1038
1020
  },
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) => { },
1021
+ chorusEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1022
+ reverbEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1023
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1024
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1025
+ delayVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1026
+ freqVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1045
1027
  };
1046
1028
  }
1047
1029
  getControllerState(channel, noteNumber, velocity) {
@@ -1051,49 +1033,44 @@ class MidyGMLite {
1051
1033
  state[3] = noteNumber / 127;
1052
1034
  return state;
1053
1035
  }
1054
- applyVoiceParams(channel, controllerType) {
1055
- channel.scheduledNotes.forEach((noteList) => {
1056
- for (let i = 0; i < noteList.length; i++) {
1057
- const note = noteList[i];
1058
- if (!note)
1036
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1037
+ this.processScheduledNotes(channel, (note) => {
1038
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1039
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
1040
+ let appliedFilterEnvelope = false;
1041
+ let appliedVolumeEnvelope = false;
1042
+ for (const [key, value] of Object.entries(voiceParams)) {
1043
+ const prevValue = note.voiceParams[key];
1044
+ if (value === prevValue)
1059
1045
  continue;
1060
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1061
- const voiceParams = note.voice.getParams(controllerType, controllerState);
1062
- let appliedFilterEnvelope = false;
1063
- let appliedVolumeEnvelope = false;
1064
- for (const [key, value] of Object.entries(voiceParams)) {
1065
- const prevValue = note.voiceParams[key];
1066
- if (value === prevValue)
1046
+ note.voiceParams[key] = value;
1047
+ if (key in this.voiceParamsHandlers) {
1048
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1049
+ }
1050
+ else if (filterEnvelopeKeySet.has(key)) {
1051
+ if (appliedFilterEnvelope)
1067
1052
  continue;
1068
- note.voiceParams[key] = value;
1069
- if (key in this.voiceParamsHandlers) {
1070
- this.voiceParamsHandlers[key](channel, note, prevValue);
1071
- }
1072
- else if (filterEnvelopeKeySet.has(key)) {
1073
- if (appliedFilterEnvelope)
1074
- continue;
1075
- appliedFilterEnvelope = true;
1076
- const noteVoiceParams = note.voiceParams;
1077
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1078
- const key = filterEnvelopeKeys[i];
1079
- if (key in voiceParams)
1080
- noteVoiceParams[key] = voiceParams[key];
1081
- }
1082
- this.setFilterEnvelope(note);
1083
- this.setPitchEnvelope(note);
1053
+ appliedFilterEnvelope = true;
1054
+ const noteVoiceParams = note.voiceParams;
1055
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1056
+ const key = filterEnvelopeKeys[i];
1057
+ if (key in voiceParams)
1058
+ noteVoiceParams[key] = voiceParams[key];
1084
1059
  }
1085
- else if (volumeEnvelopeKeySet.has(key)) {
1086
- if (appliedVolumeEnvelope)
1087
- continue;
1088
- appliedVolumeEnvelope = true;
1089
- const noteVoiceParams = note.voiceParams;
1090
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1091
- const key = volumeEnvelopeKeys[i];
1092
- if (key in voiceParams)
1093
- noteVoiceParams[key] = voiceParams[key];
1094
- }
1095
- this.setVolumeEnvelope(channel, note);
1060
+ this.setFilterEnvelope(note, scheduleTime);
1061
+ this.setPitchEnvelope(note, scheduleTime);
1062
+ }
1063
+ else if (volumeEnvelopeKeySet.has(key)) {
1064
+ if (appliedVolumeEnvelope)
1065
+ continue;
1066
+ appliedVolumeEnvelope = true;
1067
+ const noteVoiceParams = note.voiceParams;
1068
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1069
+ const key = volumeEnvelopeKeys[i];
1070
+ if (key in voiceParams)
1071
+ noteVoiceParams[key] = voiceParams[key];
1096
1072
  }
1073
+ this.setVolumeEnvelope(note, scheduleTime);
1097
1074
  }
1098
1075
  }
1099
1076
  });
@@ -1114,12 +1091,12 @@ class MidyGMLite {
1114
1091
  123: this.allNotesOff,
1115
1092
  };
1116
1093
  }
1117
- handleControlChange(channelNumber, controllerType, value, startTime) {
1094
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1118
1095
  const handler = this.controlChangeHandlers[controllerType];
1119
1096
  if (handler) {
1120
- handler.call(this, channelNumber, value, startTime);
1097
+ handler.call(this, channelNumber, value, scheduleTime);
1121
1098
  const channel = this.channels[channelNumber];
1122
- this.applyVoiceParams(channel, controllerType + 128);
1099
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1123
1100
  }
1124
1101
  else {
1125
1102
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1128,7 +1105,7 @@ class MidyGMLite {
1128
1105
  updateModulation(channel, scheduleTime) {
1129
1106
  scheduleTime ??= this.audioContext.currentTime;
1130
1107
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1131
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1108
+ this.processScheduledNotes(channel, (note) => {
1132
1109
  if (note.modulationDepth) {
1133
1110
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1134
1111
  }
@@ -1139,11 +1116,13 @@ class MidyGMLite {
1139
1116
  });
1140
1117
  }
1141
1118
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1119
+ scheduleTime ??= this.audioContext.currentTime;
1142
1120
  const channel = this.channels[channelNumber];
1143
1121
  channel.state.modulationDepth = modulation / 127;
1144
1122
  this.updateModulation(channel, scheduleTime);
1145
1123
  }
1146
1124
  setVolume(channelNumber, volume, scheduleTime) {
1125
+ scheduleTime ??= this.audioContext.currentTime;
1147
1126
  const channel = this.channels[channelNumber];
1148
1127
  channel.state.volume = volume / 127;
1149
1128
  this.updateChannelVolume(channel, scheduleTime);
@@ -1156,35 +1135,43 @@ class MidyGMLite {
1156
1135
  };
1157
1136
  }
1158
1137
  setPan(channelNumber, pan, scheduleTime) {
1138
+ scheduleTime ??= this.audioContext.currentTime;
1159
1139
  const channel = this.channels[channelNumber];
1160
1140
  channel.state.pan = pan / 127;
1161
1141
  this.updateChannelVolume(channel, scheduleTime);
1162
1142
  }
1163
1143
  setExpression(channelNumber, expression, scheduleTime) {
1144
+ scheduleTime ??= this.audioContext.currentTime;
1164
1145
  const channel = this.channels[channelNumber];
1165
1146
  channel.state.expression = expression / 127;
1166
1147
  this.updateChannelVolume(channel, scheduleTime);
1167
1148
  }
1168
- dataEntryLSB(channelNumber, value) {
1149
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1169
1150
  this.channels[channelNumber].dataLSB = value;
1170
- this.handleRPN(channelNumber);
1151
+ this.handleRPN(channelNumber, scheduleTime);
1171
1152
  }
1172
1153
  updateChannelVolume(channel, scheduleTime) {
1173
- scheduleTime ??= this.audioContext.currentTime;
1174
1154
  const state = channel.state;
1175
1155
  const volume = state.volume * state.expression;
1176
1156
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1177
1157
  channel.gainL.gain
1178
- .cancelScheduledValues(now)
1158
+ .cancelScheduledValues(scheduleTime)
1179
1159
  .setValueAtTime(volume * gainLeft, scheduleTime);
1180
1160
  channel.gainR.gain
1181
- .cancelScheduledValues(now)
1161
+ .cancelScheduledValues(scheduleTime)
1182
1162
  .setValueAtTime(volume * gainRight, scheduleTime);
1183
1163
  }
1184
- setSustainPedal(channelNumber, value) {
1185
- this.channels[channelNumber].state.sustainPedal = value / 127;
1186
- if (value < 64) {
1187
- this.releaseSustainPedal(channelNumber, value);
1164
+ setSustainPedal(channelNumber, value, scheduleTime) {
1165
+ scheduleTime ??= this.audioContext.currentTime;
1166
+ const channel = this.channels[channelNumber];
1167
+ channel.state.sustainPedal = value / 127;
1168
+ if (64 <= value) {
1169
+ this.processScheduledNotes(channel, (note) => {
1170
+ channel.sustainNotes.push(note);
1171
+ });
1172
+ }
1173
+ else {
1174
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1188
1175
  }
1189
1176
  }
1190
1177
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
@@ -1205,12 +1192,12 @@ class MidyGMLite {
1205
1192
  channel.dataLSB = minLSB;
1206
1193
  }
1207
1194
  }
1208
- handleRPN(channelNumber) {
1195
+ handleRPN(channelNumber, scheduleTime) {
1209
1196
  const channel = this.channels[channelNumber];
1210
1197
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1211
1198
  switch (rpn) {
1212
1199
  case 0:
1213
- this.handlePitchBendRangeRPN(channelNumber);
1200
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1214
1201
  break;
1215
1202
  default:
1216
1203
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
@@ -1222,28 +1209,30 @@ class MidyGMLite {
1222
1209
  setRPNLSB(channelNumber, value) {
1223
1210
  this.channels[channelNumber].rpnLSB = value;
1224
1211
  }
1225
- dataEntryMSB(channelNumber, value) {
1212
+ dataEntryMSB(channelNumber, value, scheduleTime) {
1226
1213
  this.channels[channelNumber].dataMSB = value;
1227
- this.handleRPN(channelNumber);
1214
+ this.handleRPN(channelNumber, scheduleTime);
1228
1215
  }
1229
- handlePitchBendRangeRPN(channelNumber) {
1216
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
1230
1217
  const channel = this.channels[channelNumber];
1231
1218
  this.limitData(channel, 0, 127, 0, 99);
1232
1219
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1233
- this.setPitchBendRange(channelNumber, pitchBendRange);
1220
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1234
1221
  }
1235
- setPitchBendRange(channelNumber, value) {
1222
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1223
+ scheduleTime ??= this.audioContext.currentTime;
1236
1224
  const channel = this.channels[channelNumber];
1237
1225
  const state = channel.state;
1238
1226
  const prev = state.pitchWheelSensitivity;
1239
1227
  const next = value / 128;
1240
1228
  state.pitchWheelSensitivity = next;
1241
1229
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1242
- this.updateChannelDetune(channel);
1243
- this.applyVoiceParams(channel, 16);
1230
+ this.updateChannelDetune(channel, scheduleTime);
1231
+ this.applyVoiceParams(channel, 16, scheduleTime);
1244
1232
  }
1245
- allSoundOff(channelNumber) {
1246
- return this.stopChannelNotes(channelNumber, 0, true);
1233
+ allSoundOff(channelNumber, _value, scheduleTime) {
1234
+ scheduleTime ??= this.audioContext.currentTime;
1235
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1247
1236
  }
1248
1237
  resetAllControllers(channelNumber) {
1249
1238
  const stateTypes = [
@@ -1267,10 +1256,11 @@ class MidyGMLite {
1267
1256
  channel[type] = this.constructor.channelSettings[type];
1268
1257
  }
1269
1258
  }
1270
- allNotesOff(channelNumber) {
1271
- return this.stopChannelNotes(channelNumber, 0, false);
1259
+ allNotesOff(channelNumber, _value, scheduleTime) {
1260
+ scheduleTime ??= this.audioContext.currentTime;
1261
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1272
1262
  }
1273
- handleUniversalNonRealTimeExclusiveMessage(data) {
1263
+ handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
1274
1264
  switch (data[2]) {
1275
1265
  case 9:
1276
1266
  switch (data[3]) {
@@ -1294,12 +1284,12 @@ class MidyGMLite {
1294
1284
  }
1295
1285
  this.channels[9].bank = 128;
1296
1286
  }
1297
- handleUniversalRealTimeExclusiveMessage(data) {
1287
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1298
1288
  switch (data[2]) {
1299
1289
  case 4:
1300
1290
  switch (data[3]) {
1301
1291
  case 1:
1302
- return this.handleMasterVolumeSysEx(data);
1292
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
1303
1293
  default:
1304
1294
  console.warn(`Unsupported Exclusive Message: ${data}`);
1305
1295
  }
@@ -1308,42 +1298,40 @@ class MidyGMLite {
1308
1298
  console.warn(`Unsupported Exclusive Message: ${data}`);
1309
1299
  }
1310
1300
  }
1311
- handleMasterVolumeSysEx(data) {
1301
+ handleMasterVolumeSysEx(data, scheduleTime) {
1312
1302
  const volume = (data[5] * 128 + data[4]) / 16383;
1313
- this.setMasterVolume(volume);
1303
+ this.setMasterVolume(volume, scheduleTime);
1314
1304
  }
1315
- setMasterVolume(volume) {
1305
+ setMasterVolume(volume, scheduleTime) {
1306
+ scheduleTime ??= this.audioContext.currentTime;
1316
1307
  if (volume < 0 && 1 < volume) {
1317
1308
  console.error("Master Volume is out of range");
1318
1309
  }
1319
1310
  else {
1320
- const now = this.audioContext.currentTime;
1321
- this.masterVolume.gain.cancelScheduledValues(now);
1322
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
1311
+ this.masterVolume.gain
1312
+ .cancelScheduledValues(scheduleTime)
1313
+ .setValueAtTime(volume * volume, scheduleTime);
1323
1314
  }
1324
1315
  }
1325
- handleExclusiveMessage(data) {
1326
- console.warn(`Unsupported Exclusive Message: ${data}`);
1327
- }
1328
- handleSysEx(data) {
1316
+ handleSysEx(data, scheduleTime) {
1329
1317
  switch (data[0]) {
1330
1318
  case 126:
1331
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
1319
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
1332
1320
  case 127:
1333
- return this.handleUniversalRealTimeExclusiveMessage(data);
1321
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
1334
1322
  default:
1335
- return this.handleExclusiveMessage(data);
1323
+ console.warn(`Unsupported Exclusive Message: ${data}`);
1336
1324
  }
1337
1325
  }
1338
- scheduleTask(callback, startTime) {
1326
+ scheduleTask(callback, scheduleTime) {
1339
1327
  return new Promise((resolve) => {
1340
1328
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
1341
1329
  bufferSource.onended = () => {
1342
1330
  callback();
1343
1331
  resolve();
1344
1332
  };
1345
- bufferSource.start(startTime);
1346
- bufferSource.stop(startTime);
1333
+ bufferSource.start(scheduleTime);
1334
+ bufferSource.stop(scheduleTime);
1347
1335
  });
1348
1336
  }
1349
1337
  }