@marmooo/midy 0.2.5 → 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.
@@ -69,6 +69,12 @@ class Note {
69
69
  writable: true,
70
70
  value: void 0
71
71
  });
72
+ Object.defineProperty(this, "filterDepth", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: void 0
77
+ });
72
78
  Object.defineProperty(this, "volumeEnvelopeNode", {
73
79
  enumerable: true,
74
80
  configurable: true,
@@ -400,31 +406,32 @@ class MidyGMLite {
400
406
  const event = this.timeline[queueIndex];
401
407
  if (event.startTime > t + this.lookAhead)
402
408
  break;
409
+ const startTime = event.startTime + this.startDelay - offset;
403
410
  switch (event.type) {
404
411
  case "noteOn":
405
412
  if (event.velocity !== 0) {
406
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
413
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
407
414
  break;
408
415
  }
409
416
  /* falls through */
410
417
  case "noteOff": {
411
- const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
418
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime);
412
419
  if (notePromise) {
413
420
  this.notePromises.push(notePromise);
414
421
  }
415
422
  break;
416
423
  }
417
424
  case "controller":
418
- this.handleControlChange(event.channel, event.controllerType, event.value);
425
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
419
426
  break;
420
427
  case "programChange":
421
- this.handleProgramChange(event.channel, event.programNumber);
428
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
422
429
  break;
423
430
  case "pitchBend":
424
- this.setPitchBend(event.channel, event.value + 8192);
431
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
425
432
  break;
426
433
  case "sysEx":
427
- this.handleSysEx(event.data);
434
+ this.handleSysEx(event.data, startTime);
428
435
  }
429
436
  queueIndex++;
430
437
  }
@@ -455,10 +462,11 @@ class MidyGMLite {
455
462
  resolve();
456
463
  return;
457
464
  }
458
- const t = this.audioContext.currentTime + offset;
465
+ const now = this.audioContext.currentTime;
466
+ const t = now + offset;
459
467
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
460
468
  if (this.isPausing) {
461
- await this.stopNotes(0, true);
469
+ await this.stopNotes(0, true, now);
462
470
  this.notePromises = [];
463
471
  resolve();
464
472
  this.isPausing = false;
@@ -466,7 +474,7 @@ class MidyGMLite {
466
474
  return;
467
475
  }
468
476
  else if (this.isStopping) {
469
- await this.stopNotes(0, true);
477
+ await this.stopNotes(0, true, now);
470
478
  this.notePromises = [];
471
479
  this.exclusiveClassMap.clear();
472
480
  this.audioBufferCache.clear();
@@ -476,7 +484,7 @@ class MidyGMLite {
476
484
  return;
477
485
  }
478
486
  else if (this.isSeeking) {
479
- this.stopNotes(0, true);
487
+ this.stopNotes(0, true, now);
480
488
  this.exclusiveClassMap.clear();
481
489
  this.startTime = this.audioContext.currentTime;
482
490
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -485,7 +493,6 @@ class MidyGMLite {
485
493
  await schedulePlayback();
486
494
  }
487
495
  else {
488
- const now = this.audioContext.currentTime;
489
496
  const waitTime = now + this.noteCheckInterval;
490
497
  await this.scheduleTask(() => { }, waitTime);
491
498
  await schedulePlayback();
@@ -569,24 +576,26 @@ class MidyGMLite {
569
576
  }
570
577
  return { instruments, timeline };
571
578
  }
572
- async stopChannelNotes(channelNumber, velocity, force) {
573
- const now = this.audioContext.currentTime;
579
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
574
580
  const channel = this.channels[channelNumber];
581
+ const promises = [];
575
582
  channel.scheduledNotes.forEach((noteList) => {
576
583
  for (let i = 0; i < noteList.length; i++) {
577
584
  const note = noteList[i];
578
585
  if (!note)
579
586
  continue;
580
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, force);
587
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
581
588
  this.notePromises.push(promise);
589
+ promises.push(promise);
582
590
  }
583
591
  });
584
592
  channel.scheduledNotes.clear();
585
- await Promise.all(this.notePromises);
593
+ return Promise.all(promises);
586
594
  }
587
- stopNotes(velocity, force) {
595
+ stopNotes(velocity, force, scheduleTime) {
596
+ const promises = [];
588
597
  for (let i = 0; i < this.channels.length; i++) {
589
- this.stopChannelNotes(i, velocity, force);
598
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
590
599
  }
591
600
  return Promise.all(this.notePromises);
592
601
  }
@@ -634,22 +643,34 @@ class MidyGMLite {
634
643
  const now = this.audioContext.currentTime;
635
644
  return this.resumeTime + now - this.startTime - this.startDelay;
636
645
  }
637
- getActiveNotes(channel, time) {
646
+ processScheduledNotes(channel, scheduleTime, callback) {
647
+ channel.scheduledNotes.forEach((noteList) => {
648
+ for (let i = 0; i < noteList.length; i++) {
649
+ const note = noteList[i];
650
+ if (!note)
651
+ continue;
652
+ if (scheduleTime < note.startTime)
653
+ continue;
654
+ callback(note);
655
+ }
656
+ });
657
+ }
658
+ getActiveNotes(channel, scheduleTime) {
638
659
  const activeNotes = new SparseMap(128);
639
660
  channel.scheduledNotes.forEach((noteList) => {
640
- const activeNote = this.getActiveNote(noteList, time);
661
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
641
662
  if (activeNote) {
642
663
  activeNotes.set(activeNote.noteNumber, activeNote);
643
664
  }
644
665
  });
645
666
  return activeNotes;
646
667
  }
647
- getActiveNote(noteList, time) {
668
+ getActiveNote(noteList, scheduleTime) {
648
669
  for (let i = noteList.length - 1; i >= 0; i--) {
649
670
  const note = noteList[i];
650
671
  if (!note)
651
672
  return;
652
- if (time < note.startTime)
673
+ if (scheduleTime < note.startTime)
653
674
  continue;
654
675
  return (note.ending) ? null : note;
655
676
  }
@@ -672,24 +693,17 @@ class MidyGMLite {
672
693
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
673
694
  return pitchWheel * pitchWheelSensitivity;
674
695
  }
675
- updateChannelDetune(channel) {
676
- channel.scheduledNotes.forEach((noteList) => {
677
- for (let i = 0; i < noteList.length; i++) {
678
- const note = noteList[i];
679
- if (!note)
680
- continue;
681
- this.updateDetune(channel, note);
682
- }
696
+ updateChannelDetune(channel, scheduleTime) {
697
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
698
+ this.updateDetune(channel, note, scheduleTime);
683
699
  });
684
700
  }
685
- updateDetune(channel, note) {
686
- const now = this.audioContext.currentTime;
701
+ updateDetune(channel, note, scheduleTime) {
687
702
  note.bufferSource.detune
688
- .cancelScheduledValues(now)
689
- .setValueAtTime(channel.detune, now);
703
+ .cancelScheduledValues(scheduleTime)
704
+ .setValueAtTime(channel.detune, scheduleTime);
690
705
  }
691
- setVolumeEnvelope(note) {
692
- const now = this.audioContext.currentTime;
706
+ setVolumeEnvelope(note, scheduleTime) {
693
707
  const { voiceParams, startTime } = note;
694
708
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
695
709
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
@@ -698,27 +712,26 @@ class MidyGMLite {
698
712
  const volHold = volAttack + voiceParams.volHold;
699
713
  const volDecay = volHold + voiceParams.volDecay;
700
714
  note.volumeEnvelopeNode.gain
701
- .cancelScheduledValues(now)
715
+ .cancelScheduledValues(scheduleTime)
702
716
  .setValueAtTime(0, startTime)
703
717
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
704
718
  .exponentialRampToValueAtTime(attackVolume, volAttack)
705
719
  .setValueAtTime(attackVolume, volHold)
706
720
  .linearRampToValueAtTime(sustainVolume, volDecay);
707
721
  }
708
- setPitchEnvelope(note) {
709
- const now = this.audioContext.currentTime;
722
+ setPitchEnvelope(note, scheduleTime) {
710
723
  const { voiceParams } = note;
711
724
  const baseRate = voiceParams.playbackRate;
712
725
  note.bufferSource.playbackRate
713
- .cancelScheduledValues(now)
714
- .setValueAtTime(baseRate, now);
726
+ .cancelScheduledValues(scheduleTime)
727
+ .setValueAtTime(baseRate, scheduleTime);
715
728
  const modEnvToPitch = voiceParams.modEnvToPitch;
716
729
  if (modEnvToPitch === 0)
717
730
  return;
718
731
  const basePitch = this.rateToCent(baseRate);
719
732
  const peekPitch = basePitch + modEnvToPitch;
720
733
  const peekRate = this.centToRate(peekPitch);
721
- const modDelay = startTime + voiceParams.modDelay;
734
+ const modDelay = note.startTime + voiceParams.modDelay;
722
735
  const modAttack = modDelay + voiceParams.modAttack;
723
736
  const modHold = modAttack + voiceParams.modHold;
724
737
  const modDecay = modHold + voiceParams.modDecay;
@@ -733,8 +746,7 @@ class MidyGMLite {
733
746
  const maxFrequency = 20000; // max Hz of initialFilterFc
734
747
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
735
748
  }
736
- setFilterEnvelope(note) {
737
- const now = this.audioContext.currentTime;
749
+ setFilterEnvelope(note, scheduleTime) {
738
750
  const { voiceParams, startTime } = note;
739
751
  const baseFreq = this.centToHz(voiceParams.initialFilterFc);
740
752
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
@@ -748,14 +760,14 @@ class MidyGMLite {
748
760
  const modHold = modAttack + voiceParams.modHold;
749
761
  const modDecay = modHold + voiceParams.modDecay;
750
762
  note.filterNode.frequency
751
- .cancelScheduledValues(now)
763
+ .cancelScheduledValues(scheduleTime)
752
764
  .setValueAtTime(adjustedBaseFreq, startTime)
753
765
  .setValueAtTime(adjustedBaseFreq, modDelay)
754
766
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
755
767
  .setValueAtTime(adjustedPeekFreq, modHold)
756
768
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
757
769
  }
758
- startModulation(channel, note, startTime) {
770
+ startModulation(channel, note, scheduleTime) {
759
771
  const { voiceParams } = note;
760
772
  note.modulationLFO = new OscillatorNode(this.audioContext, {
761
773
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -764,10 +776,10 @@ class MidyGMLite {
764
776
  gain: voiceParams.modLfoToFilterFc,
765
777
  });
766
778
  note.modulationDepth = new GainNode(this.audioContext);
767
- this.setModLfoToPitch(channel, note);
779
+ this.setModLfoToPitch(channel, note, scheduleTime);
768
780
  note.volumeDepth = new GainNode(this.audioContext);
769
- this.setModLfoToVolume(note);
770
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
781
+ this.setModLfoToVolume(note, scheduleTime);
782
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
771
783
  note.modulationLFO.connect(note.filterDepth);
772
784
  note.filterDepth.connect(note.filterNode.frequency);
773
785
  note.modulationLFO.connect(note.modulationDepth);
@@ -794,6 +806,7 @@ class MidyGMLite {
794
806
  }
795
807
  }
796
808
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
809
+ const now = this.audioContext.currentTime;
797
810
  const state = channel.state;
798
811
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
799
812
  const voiceParams = voice.getAllParams(controllerState);
@@ -805,11 +818,11 @@ class MidyGMLite {
805
818
  type: "lowpass",
806
819
  Q: voiceParams.initialFilterQ / 10, // dB
807
820
  });
808
- this.setVolumeEnvelope(note);
809
- this.setFilterEnvelope(note);
810
- this.setPitchEnvelope(note);
821
+ this.setVolumeEnvelope(note, now);
822
+ this.setFilterEnvelope(note, now);
823
+ this.setPitchEnvelope(note, now);
811
824
  if (0 < state.modulationDepth) {
812
- this.startModulation(channel, note, startTime);
825
+ this.startModulation(channel, note, now);
813
826
  }
814
827
  note.bufferSource.connect(note.filterNode);
815
828
  note.filterNode.connect(note.volumeEnvelopeNode);
@@ -836,7 +849,7 @@ class MidyGMLite {
836
849
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
837
850
  const [prevNote, prevChannelNumber] = prevEntry;
838
851
  if (!prevNote.ending) {
839
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
852
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
840
853
  startTime, undefined, // portamentoNoteNumber
841
854
  true);
842
855
  }
@@ -851,9 +864,9 @@ class MidyGMLite {
851
864
  scheduledNotes.set(noteNumber, [note]);
852
865
  }
853
866
  }
854
- noteOn(channelNumber, noteNumber, velocity) {
855
- const now = this.audioContext.currentTime;
856
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
867
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
868
+ scheduleTime ??= this.audioContext.currentTime;
869
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
857
870
  }
858
871
  stopNote(endTime, stopTime, scheduledNotes, index) {
859
872
  const note = scheduledNotes[index];
@@ -880,7 +893,7 @@ class MidyGMLite {
880
893
  note.bufferSource.stop(stopTime);
881
894
  });
882
895
  }
883
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
896
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
884
897
  const channel = this.channels[channelNumber];
885
898
  if (!force && 0.5 < channel.state.sustainPedal)
886
899
  return;
@@ -902,127 +915,119 @@ class MidyGMLite {
902
915
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
903
916
  }
904
917
  }
905
- releaseNote(channelNumber, noteNumber, velocity) {
906
- const now = this.audioContext.currentTime;
907
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
918
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
919
+ scheduleTime ??= this.audioContext.currentTime;
920
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
908
921
  }
909
- releaseSustainPedal(channelNumber, halfVelocity) {
922
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
910
923
  const velocity = halfVelocity * 2;
911
924
  const channel = this.channels[channelNumber];
912
925
  const promises = [];
913
- channel.state.sustainPedal = halfVelocity;
914
- channel.scheduledNotes.forEach((noteList) => {
915
- for (let i = 0; i < noteList.length; i++) {
916
- const note = noteList[i];
917
- if (!note)
918
- continue;
919
- const { noteNumber } = note;
920
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
921
- promises.push(promise);
922
- }
926
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
927
+ const { noteNumber } = note;
928
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
929
+ promises.push(promise);
923
930
  });
924
931
  return promises;
925
932
  }
926
- handleMIDIMessage(statusByte, data1, data2) {
933
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
927
934
  const channelNumber = statusByte & 0x0F;
928
935
  const messageType = statusByte & 0xF0;
929
936
  switch (messageType) {
930
937
  case 0x80:
931
- return this.releaseNote(channelNumber, data1, data2);
938
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
932
939
  case 0x90:
933
- return this.noteOn(channelNumber, data1, data2);
940
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
934
941
  case 0xB0:
935
- return this.handleControlChange(channelNumber, data1, data2);
942
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
936
943
  case 0xC0:
937
- return this.handleProgramChange(channelNumber, data1);
944
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
938
945
  case 0xE0:
939
- return this.handlePitchBendMessage(channelNumber, data1, data2);
946
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
940
947
  default:
941
948
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
942
949
  }
943
950
  }
944
- handleProgramChange(channelNumber, program) {
951
+ handleProgramChange(channelNumber, program, _scheduleTime) {
945
952
  const channel = this.channels[channelNumber];
946
953
  channel.program = program;
947
954
  }
948
- handlePitchBendMessage(channelNumber, lsb, msb) {
955
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
949
956
  const pitchBend = msb * 128 + lsb;
950
- this.setPitchBend(channelNumber, pitchBend);
957
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
951
958
  }
952
- setPitchBend(channelNumber, value) {
959
+ setPitchBend(channelNumber, value, scheduleTime) {
960
+ scheduleTime ??= this.audioContext.currentTime;
953
961
  const channel = this.channels[channelNumber];
954
962
  const state = channel.state;
955
963
  const prev = state.pitchWheel * 2 - 1;
956
964
  const next = (value - 8192) / 8192;
957
965
  state.pitchWheel = value / 16383;
958
966
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
959
- this.updateChannelDetune(channel);
960
- this.applyVoiceParams(channel, 14);
967
+ this.updateChannelDetune(channel, scheduleTime);
968
+ this.applyVoiceParams(channel, 14, scheduleTime);
961
969
  }
962
- setModLfoToPitch(channel, note) {
963
- const now = this.audioContext.currentTime;
970
+ setModLfoToPitch(channel, note, scheduleTime) {
964
971
  const modLfoToPitch = note.voiceParams.modLfoToPitch;
965
972
  const baseDepth = Math.abs(modLfoToPitch) +
966
973
  channel.state.modulationDepth;
967
974
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
968
975
  note.modulationDepth.gain
969
- .cancelScheduledValues(now)
970
- .setValueAtTime(modulationDepth, now);
976
+ .cancelScheduledValues(scheduleTime)
977
+ .setValueAtTime(modulationDepth, scheduleTime);
971
978
  }
972
- setModLfoToFilterFc(note) {
973
- const now = this.audioContext.currentTime;
979
+ setModLfoToFilterFc(note, scheduleTime) {
974
980
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
975
981
  note.filterDepth.gain
976
- .cancelScheduledValues(now)
977
- .setValueAtTime(modLfoToFilterFc, now);
982
+ .cancelScheduledValues(scheduleTime)
983
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
978
984
  }
979
- setModLfoToVolume(note) {
980
- const now = this.audioContext.currentTime;
985
+ setModLfoToVolume(note, scheduleTime) {
981
986
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
982
987
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
983
988
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
984
989
  note.volumeDepth.gain
985
- .cancelScheduledValues(now)
986
- .setValueAtTime(volumeDepth, now);
990
+ .cancelScheduledValues(scheduleTime)
991
+ .setValueAtTime(volumeDepth, scheduleTime);
987
992
  }
988
- setDelayModLFO(note) {
989
- const now = this.audioContext.currentTime;
993
+ setDelayModLFO(note, scheduleTime) {
990
994
  const startTime = note.startTime;
991
- if (startTime < now)
995
+ if (startTime < scheduleTime)
992
996
  return;
993
- note.modulationLFO.stop(now);
997
+ note.modulationLFO.stop(scheduleTime);
994
998
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
995
999
  note.modulationLFO.connect(note.filterDepth);
996
1000
  }
997
- setFreqModLFO(note) {
998
- const now = this.audioContext.currentTime;
1001
+ setFreqModLFO(note, scheduleTime) {
999
1002
  const freqModLFO = note.voiceParams.freqModLFO;
1000
1003
  note.modulationLFO.frequency
1001
- .cancelScheduledValues(now)
1002
- .setValueAtTime(freqModLFO, now);
1004
+ .cancelScheduledValues(scheduleTime)
1005
+ .setValueAtTime(freqModLFO, scheduleTime);
1003
1006
  }
1004
1007
  createVoiceParamsHandlers() {
1005
1008
  return {
1006
- modLfoToPitch: (channel, note, _prevValue) => {
1009
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1007
1010
  if (0 < channel.state.modulationDepth) {
1008
- this.setModLfoToPitch(channel, note);
1011
+ this.setModLfoToPitch(channel, note, scheduleTime);
1009
1012
  }
1010
1013
  },
1011
- vibLfoToPitch: (_channel, _note, _prevValue) => { },
1012
- modLfoToFilterFc: (channel, note, _prevValue) => {
1013
- if (0 < channel.state.modulationDepth)
1014
- this.setModLfoToFilterFc(note);
1014
+ vibLfoToPitch: (_channel, _note, _prevValue, _scheduleTime) => { },
1015
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1016
+ if (0 < channel.state.modulationDepth) {
1017
+ this.setModLfoToFilterFc(note, scheduleTime);
1018
+ }
1015
1019
  },
1016
- modLfoToVolume: (channel, note, _prevValue) => {
1017
- if (0 < channel.state.modulationDepth)
1018
- this.setModLfoToVolume(note);
1020
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1021
+ if (0 < channel.state.modulationDepth) {
1022
+ this.setModLfoToVolume(note, scheduleTime);
1023
+ }
1019
1024
  },
1020
- chorusEffectsSend: (_channel, _note, _prevValue) => { },
1021
- reverbEffectsSend: (_channel, _note, _prevValue) => { },
1022
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1023
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1024
- delayVibLFO: (_channel, _note, _prevValue) => { },
1025
- freqVibLFO: (_channel, _note, _prevValue) => { },
1025
+ chorusEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1026
+ reverbEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1027
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1028
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1029
+ delayVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1030
+ freqVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1026
1031
  };
1027
1032
  }
1028
1033
  getControllerState(channel, noteNumber, velocity) {
@@ -1032,7 +1037,7 @@ class MidyGMLite {
1032
1037
  state[3] = noteNumber / 127;
1033
1038
  return state;
1034
1039
  }
1035
- applyVoiceParams(channel, controllerType) {
1040
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1036
1041
  channel.scheduledNotes.forEach((noteList) => {
1037
1042
  for (let i = 0; i < noteList.length; i++) {
1038
1043
  const note = noteList[i];
@@ -1048,7 +1053,7 @@ class MidyGMLite {
1048
1053
  continue;
1049
1054
  note.voiceParams[key] = value;
1050
1055
  if (key in this.voiceParamsHandlers) {
1051
- this.voiceParamsHandlers[key](channel, note, prevValue);
1056
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1052
1057
  }
1053
1058
  else if (filterEnvelopeKeySet.has(key)) {
1054
1059
  if (appliedFilterEnvelope)
@@ -1060,8 +1065,8 @@ class MidyGMLite {
1060
1065
  if (key in voiceParams)
1061
1066
  noteVoiceParams[key] = voiceParams[key];
1062
1067
  }
1063
- this.setFilterEnvelope(note);
1064
- this.setPitchEnvelope(note);
1068
+ this.setFilterEnvelope(note, scheduleTime);
1069
+ this.setPitchEnvelope(note, scheduleTime);
1065
1070
  }
1066
1071
  else if (volumeEnvelopeKeySet.has(key)) {
1067
1072
  if (appliedVolumeEnvelope)
@@ -1073,7 +1078,7 @@ class MidyGMLite {
1073
1078
  if (key in voiceParams)
1074
1079
  noteVoiceParams[key] = voiceParams[key];
1075
1080
  }
1076
- this.setVolumeEnvelope(channel, note);
1081
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1077
1082
  }
1078
1083
  }
1079
1084
  }
@@ -1095,44 +1100,39 @@ class MidyGMLite {
1095
1100
  123: this.allNotesOff,
1096
1101
  };
1097
1102
  }
1098
- handleControlChange(channelNumber, controllerType, value) {
1103
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1099
1104
  const handler = this.controlChangeHandlers[controllerType];
1100
1105
  if (handler) {
1101
- handler.call(this, channelNumber, value);
1106
+ handler.call(this, channelNumber, value, scheduleTime);
1102
1107
  const channel = this.channels[channelNumber];
1103
- this.applyVoiceParams(channel, controllerType + 128);
1108
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1104
1109
  }
1105
1110
  else {
1106
1111
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
1107
1112
  }
1108
1113
  }
1109
- updateModulation(channel) {
1110
- const now = this.audioContext.currentTime;
1114
+ updateModulation(channel, scheduleTime) {
1115
+ scheduleTime ??= this.audioContext.currentTime;
1111
1116
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1112
- channel.scheduledNotes.forEach((noteList) => {
1113
- for (let i = 0; i < noteList.length; i++) {
1114
- const note = noteList[i];
1115
- if (!note)
1116
- continue;
1117
- if (note.modulationDepth) {
1118
- note.modulationDepth.gain.setValueAtTime(depth, now);
1119
- }
1120
- else {
1121
- this.setPitchEnvelope(note);
1122
- this.startModulation(channel, note, now);
1123
- }
1117
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1118
+ if (note.modulationDepth) {
1119
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1120
+ }
1121
+ else {
1122
+ this.setPitchEnvelope(note, scheduleTime);
1123
+ this.startModulation(channel, note, scheduleTime);
1124
1124
  }
1125
1125
  });
1126
1126
  }
1127
- setModulationDepth(channelNumber, modulation) {
1127
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1128
1128
  const channel = this.channels[channelNumber];
1129
1129
  channel.state.modulationDepth = modulation / 127;
1130
- this.updateModulation(channel);
1130
+ this.updateModulation(channel, scheduleTime);
1131
1131
  }
1132
- setVolume(channelNumber, volume) {
1132
+ setVolume(channelNumber, volume, scheduleTime) {
1133
1133
  const channel = this.channels[channelNumber];
1134
1134
  channel.state.volume = volume / 127;
1135
- this.updateChannelVolume(channel);
1135
+ this.updateChannelVolume(channel, scheduleTime);
1136
1136
  }
1137
1137
  panToGain(pan) {
1138
1138
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1141,36 +1141,36 @@ class MidyGMLite {
1141
1141
  gainRight: Math.sin(theta),
1142
1142
  };
1143
1143
  }
1144
- setPan(channelNumber, pan) {
1144
+ setPan(channelNumber, pan, scheduleTime) {
1145
1145
  const channel = this.channels[channelNumber];
1146
1146
  channel.state.pan = pan / 127;
1147
- this.updateChannelVolume(channel);
1147
+ this.updateChannelVolume(channel, scheduleTime);
1148
1148
  }
1149
- setExpression(channelNumber, expression) {
1149
+ setExpression(channelNumber, expression, scheduleTime) {
1150
1150
  const channel = this.channels[channelNumber];
1151
1151
  channel.state.expression = expression / 127;
1152
- this.updateChannelVolume(channel);
1152
+ this.updateChannelVolume(channel, scheduleTime);
1153
1153
  }
1154
- dataEntryLSB(channelNumber, value) {
1154
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1155
1155
  this.channels[channelNumber].dataLSB = value;
1156
- this.handleRPN(channelNumber);
1156
+ this.handleRPN(channelNumber, scheduleTime);
1157
1157
  }
1158
- updateChannelVolume(channel) {
1159
- const now = this.audioContext.currentTime;
1158
+ updateChannelVolume(channel, scheduleTime) {
1160
1159
  const state = channel.state;
1161
1160
  const volume = state.volume * state.expression;
1162
1161
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1163
1162
  channel.gainL.gain
1164
- .cancelScheduledValues(now)
1165
- .setValueAtTime(volume * gainLeft, now);
1163
+ .cancelScheduledValues(scheduleTime)
1164
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1166
1165
  channel.gainR.gain
1167
- .cancelScheduledValues(now)
1168
- .setValueAtTime(volume * gainRight, now);
1166
+ .cancelScheduledValues(scheduleTime)
1167
+ .setValueAtTime(volume * gainRight, scheduleTime);
1169
1168
  }
1170
- setSustainPedal(channelNumber, value) {
1169
+ setSustainPedal(channelNumber, value, scheduleTime) {
1170
+ scheduleTime ??= this.audioContext.currentTime;
1171
1171
  this.channels[channelNumber].state.sustainPedal = value / 127;
1172
1172
  if (value < 64) {
1173
- this.releaseSustainPedal(channelNumber, value);
1173
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1174
1174
  }
1175
1175
  }
1176
1176
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
@@ -1191,12 +1191,12 @@ class MidyGMLite {
1191
1191
  channel.dataLSB = minLSB;
1192
1192
  }
1193
1193
  }
1194
- handleRPN(channelNumber) {
1194
+ handleRPN(channelNumber, scheduleTime) {
1195
1195
  const channel = this.channels[channelNumber];
1196
1196
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1197
1197
  switch (rpn) {
1198
1198
  case 0:
1199
- this.handlePitchBendRangeRPN(channelNumber);
1199
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1200
1200
  break;
1201
1201
  default:
1202
1202
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
@@ -1208,28 +1208,30 @@ class MidyGMLite {
1208
1208
  setRPNLSB(channelNumber, value) {
1209
1209
  this.channels[channelNumber].rpnLSB = value;
1210
1210
  }
1211
- dataEntryMSB(channelNumber, value) {
1211
+ dataEntryMSB(channelNumber, value, scheduleTime) {
1212
1212
  this.channels[channelNumber].dataMSB = value;
1213
- this.handleRPN(channelNumber);
1213
+ this.handleRPN(channelNumber, scheduleTime);
1214
1214
  }
1215
- handlePitchBendRangeRPN(channelNumber) {
1215
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
1216
1216
  const channel = this.channels[channelNumber];
1217
1217
  this.limitData(channel, 0, 127, 0, 99);
1218
1218
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1219
- this.setPitchBendRange(channelNumber, pitchBendRange);
1219
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1220
1220
  }
1221
- setPitchBendRange(channelNumber, value) {
1221
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1222
+ scheduleTime ??= this.audioContext.currentTime;
1222
1223
  const channel = this.channels[channelNumber];
1223
1224
  const state = channel.state;
1224
1225
  const prev = state.pitchWheelSensitivity;
1225
1226
  const next = value / 128;
1226
1227
  state.pitchWheelSensitivity = next;
1227
1228
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1228
- this.updateChannelDetune(channel);
1229
- this.applyVoiceParams(channel, 16);
1229
+ this.updateChannelDetune(channel, scheduleTime);
1230
+ this.applyVoiceParams(channel, 16, scheduleTime);
1230
1231
  }
1231
- allSoundOff(channelNumber) {
1232
- return this.stopChannelNotes(channelNumber, 0, true);
1232
+ allSoundOff(channelNumber, _value, scheduleTime) {
1233
+ scheduleTime ??= this.audioContext.currentTime;
1234
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1233
1235
  }
1234
1236
  resetAllControllers(channelNumber) {
1235
1237
  const stateTypes = [
@@ -1253,10 +1255,11 @@ class MidyGMLite {
1253
1255
  channel[type] = this.constructor.channelSettings[type];
1254
1256
  }
1255
1257
  }
1256
- allNotesOff(channelNumber) {
1257
- return this.stopChannelNotes(channelNumber, 0, false);
1258
+ allNotesOff(channelNumber, _value, scheduleTime) {
1259
+ scheduleTime ??= this.audioContext.currentTime;
1260
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1258
1261
  }
1259
- handleUniversalNonRealTimeExclusiveMessage(data) {
1262
+ handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
1260
1263
  switch (data[2]) {
1261
1264
  case 9:
1262
1265
  switch (data[3]) {
@@ -1280,12 +1283,12 @@ class MidyGMLite {
1280
1283
  }
1281
1284
  this.channels[9].bank = 128;
1282
1285
  }
1283
- handleUniversalRealTimeExclusiveMessage(data) {
1286
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1284
1287
  switch (data[2]) {
1285
1288
  case 4:
1286
1289
  switch (data[3]) {
1287
1290
  case 1:
1288
- return this.handleMasterVolumeSysEx(data);
1291
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
1289
1292
  default:
1290
1293
  console.warn(`Unsupported Exclusive Message: ${data}`);
1291
1294
  }
@@ -1294,42 +1297,40 @@ class MidyGMLite {
1294
1297
  console.warn(`Unsupported Exclusive Message: ${data}`);
1295
1298
  }
1296
1299
  }
1297
- handleMasterVolumeSysEx(data) {
1300
+ handleMasterVolumeSysEx(data, scheduleTime) {
1298
1301
  const volume = (data[5] * 128 + data[4]) / 16383;
1299
- this.setMasterVolume(volume);
1302
+ this.setMasterVolume(volume, scheduleTime);
1300
1303
  }
1301
- setMasterVolume(volume) {
1304
+ setMasterVolume(volume, scheduleTime) {
1305
+ scheduleTime ??= this.audioContext.currentTime;
1302
1306
  if (volume < 0 && 1 < volume) {
1303
1307
  console.error("Master Volume is out of range");
1304
1308
  }
1305
1309
  else {
1306
- const now = this.audioContext.currentTime;
1307
- this.masterVolume.gain.cancelScheduledValues(now);
1308
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
1310
+ this.masterVolume.gain
1311
+ .cancelScheduledValues(scheduleTime)
1312
+ .setValueAtTime(volume * volume, scheduleTime);
1309
1313
  }
1310
1314
  }
1311
- handleExclusiveMessage(data) {
1312
- console.warn(`Unsupported Exclusive Message: ${data}`);
1313
- }
1314
- handleSysEx(data) {
1315
+ handleSysEx(data, scheduleTime) {
1315
1316
  switch (data[0]) {
1316
1317
  case 126:
1317
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
1318
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
1318
1319
  case 127:
1319
- return this.handleUniversalRealTimeExclusiveMessage(data);
1320
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
1320
1321
  default:
1321
- return this.handleExclusiveMessage(data);
1322
+ console.warn(`Unsupported Exclusive Message: ${data}`);
1322
1323
  }
1323
1324
  }
1324
- scheduleTask(callback, startTime) {
1325
+ scheduleTask(callback, scheduleTime) {
1325
1326
  return new Promise((resolve) => {
1326
1327
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
1327
1328
  bufferSource.onended = () => {
1328
1329
  callback();
1329
1330
  resolve();
1330
1331
  };
1331
- bufferSource.start(startTime);
1332
- bufferSource.stop(startTime);
1332
+ bufferSource.start(scheduleTime);
1333
+ bufferSource.stop(scheduleTime);
1333
1334
  });
1334
1335
  }
1335
1336
  }