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