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