@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,37 +69,31 @@ class Note {
69
69
  writable: true,
70
70
  value: void 0
71
71
  });
72
- Object.defineProperty(this, "volumeEnvelopeNode", {
73
- enumerable: true,
74
- configurable: true,
75
- writable: true,
76
- value: void 0
77
- });
78
- Object.defineProperty(this, "volumeDepth", {
72
+ Object.defineProperty(this, "filterDepth", {
79
73
  enumerable: true,
80
74
  configurable: true,
81
75
  writable: true,
82
76
  value: void 0
83
77
  });
84
- Object.defineProperty(this, "modulationLFO", {
78
+ Object.defineProperty(this, "volumeEnvelopeNode", {
85
79
  enumerable: true,
86
80
  configurable: true,
87
81
  writable: true,
88
82
  value: void 0
89
83
  });
90
- Object.defineProperty(this, "modulationDepth", {
84
+ Object.defineProperty(this, "volumeDepth", {
91
85
  enumerable: true,
92
86
  configurable: true,
93
87
  writable: true,
94
88
  value: void 0
95
89
  });
96
- Object.defineProperty(this, "vibratoLFO", {
90
+ Object.defineProperty(this, "modulationLFO", {
97
91
  enumerable: true,
98
92
  configurable: true,
99
93
  writable: true,
100
94
  value: void 0
101
95
  });
102
- Object.defineProperty(this, "vibratoDepth", {
96
+ Object.defineProperty(this, "modulationDepth", {
103
97
  enumerable: true,
104
98
  configurable: true,
105
99
  writable: true,
@@ -412,31 +406,32 @@ class MidyGM1 {
412
406
  const event = this.timeline[queueIndex];
413
407
  if (event.startTime > t + this.lookAhead)
414
408
  break;
409
+ const startTime = event.startTime + this.startDelay - offset;
415
410
  switch (event.type) {
416
411
  case "noteOn":
417
412
  if (event.velocity !== 0) {
418
- 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);
419
414
  break;
420
415
  }
421
416
  /* falls through */
422
417
  case "noteOff": {
423
- 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);
424
419
  if (notePromise) {
425
420
  this.notePromises.push(notePromise);
426
421
  }
427
422
  break;
428
423
  }
429
424
  case "controller":
430
- this.handleControlChange(event.channel, event.controllerType, event.value);
425
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
431
426
  break;
432
427
  case "programChange":
433
- this.handleProgramChange(event.channel, event.programNumber);
428
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
434
429
  break;
435
430
  case "pitchBend":
436
- this.setPitchBend(event.channel, event.value + 8192);
431
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
437
432
  break;
438
433
  case "sysEx":
439
- this.handleSysEx(event.data);
434
+ this.handleSysEx(event.data, startTime);
440
435
  }
441
436
  queueIndex++;
442
437
  }
@@ -467,10 +462,11 @@ class MidyGM1 {
467
462
  resolve();
468
463
  return;
469
464
  }
470
- const t = this.audioContext.currentTime + offset;
465
+ const now = this.audioContext.currentTime;
466
+ const t = now + offset;
471
467
  queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
472
468
  if (this.isPausing) {
473
- await this.stopNotes(0, true);
469
+ await this.stopNotes(0, true, now);
474
470
  this.notePromises = [];
475
471
  resolve();
476
472
  this.isPausing = false;
@@ -478,7 +474,7 @@ class MidyGM1 {
478
474
  return;
479
475
  }
480
476
  else if (this.isStopping) {
481
- await this.stopNotes(0, true);
477
+ await this.stopNotes(0, true, now);
482
478
  this.notePromises = [];
483
479
  this.exclusiveClassMap.clear();
484
480
  this.audioBufferCache.clear();
@@ -488,7 +484,7 @@ class MidyGM1 {
488
484
  return;
489
485
  }
490
486
  else if (this.isSeeking) {
491
- this.stopNotes(0, true);
487
+ this.stopNotes(0, true, now);
492
488
  this.exclusiveClassMap.clear();
493
489
  this.startTime = this.audioContext.currentTime;
494
490
  queueIndex = this.getQueueIndex(this.resumeTime);
@@ -497,7 +493,6 @@ class MidyGM1 {
497
493
  await schedulePlayback();
498
494
  }
499
495
  else {
500
- const now = this.audioContext.currentTime;
501
496
  const waitTime = now + this.noteCheckInterval;
502
497
  await this.scheduleTask(() => { }, waitTime);
503
498
  await schedulePlayback();
@@ -581,24 +576,26 @@ class MidyGM1 {
581
576
  }
582
577
  return { instruments, timeline };
583
578
  }
584
- async stopChannelNotes(channelNumber, velocity, force) {
585
- const now = this.audioContext.currentTime;
579
+ stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
586
580
  const channel = this.channels[channelNumber];
581
+ const promises = [];
587
582
  channel.scheduledNotes.forEach((noteList) => {
588
583
  for (let i = 0; i < noteList.length; i++) {
589
584
  const note = noteList[i];
590
585
  if (!note)
591
586
  continue;
592
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, force);
587
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
593
588
  this.notePromises.push(promise);
589
+ promises.push(promise);
594
590
  }
595
591
  });
596
592
  channel.scheduledNotes.clear();
597
- await Promise.all(this.notePromises);
593
+ return Promise.all(promises);
598
594
  }
599
- stopNotes(velocity, force) {
595
+ stopNotes(velocity, force, scheduleTime) {
596
+ const promises = [];
600
597
  for (let i = 0; i < this.channels.length; i++) {
601
- this.stopChannelNotes(i, velocity, force);
598
+ promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
602
599
  }
603
600
  return Promise.all(this.notePromises);
604
601
  }
@@ -646,22 +643,34 @@ class MidyGM1 {
646
643
  const now = this.audioContext.currentTime;
647
644
  return this.resumeTime + now - this.startTime - this.startDelay;
648
645
  }
649
- 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) {
650
659
  const activeNotes = new SparseMap(128);
651
660
  channel.scheduledNotes.forEach((noteList) => {
652
- const activeNote = this.getActiveNote(noteList, time);
661
+ const activeNote = this.getActiveNote(noteList, scheduleTime);
653
662
  if (activeNote) {
654
663
  activeNotes.set(activeNote.noteNumber, activeNote);
655
664
  }
656
665
  });
657
666
  return activeNotes;
658
667
  }
659
- getActiveNote(noteList, time) {
668
+ getActiveNote(noteList, scheduleTime) {
660
669
  for (let i = noteList.length - 1; i >= 0; i--) {
661
670
  const note = noteList[i];
662
671
  if (!note)
663
672
  return;
664
- if (time < note.startTime)
673
+ if (scheduleTime < note.startTime)
665
674
  continue;
666
675
  return (note.ending) ? null : note;
667
676
  }
@@ -686,24 +695,17 @@ class MidyGM1 {
686
695
  const pitch = pitchWheel * pitchWheelSensitivity;
687
696
  return tuning + pitch;
688
697
  }
689
- updateChannelDetune(channel) {
690
- channel.scheduledNotes.forEach((noteList) => {
691
- for (let i = 0; i < noteList.length; i++) {
692
- const note = noteList[i];
693
- if (!note)
694
- continue;
695
- this.updateDetune(channel, note);
696
- }
698
+ updateChannelDetune(channel, scheduleTime) {
699
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
700
+ this.updateDetune(channel, note, scheduleTime);
697
701
  });
698
702
  }
699
- updateDetune(channel, note) {
700
- const now = this.audioContext.currentTime;
703
+ updateDetune(channel, note, scheduleTime) {
701
704
  note.bufferSource.detune
702
- .cancelScheduledValues(now)
703
- .setValueAtTime(channel.detune, now);
705
+ .cancelScheduledValues(scheduleTime)
706
+ .setValueAtTime(channel.detune, scheduleTime);
704
707
  }
705
- setVolumeEnvelope(note) {
706
- const now = this.audioContext.currentTime;
708
+ setVolumeEnvelope(note, scheduleTime) {
707
709
  const { voiceParams, startTime } = note;
708
710
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
709
711
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
@@ -712,27 +714,26 @@ class MidyGM1 {
712
714
  const volHold = volAttack + voiceParams.volHold;
713
715
  const volDecay = volHold + voiceParams.volDecay;
714
716
  note.volumeEnvelopeNode.gain
715
- .cancelScheduledValues(now)
717
+ .cancelScheduledValues(scheduleTime)
716
718
  .setValueAtTime(0, startTime)
717
719
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
718
720
  .exponentialRampToValueAtTime(attackVolume, volAttack)
719
721
  .setValueAtTime(attackVolume, volHold)
720
722
  .linearRampToValueAtTime(sustainVolume, volDecay);
721
723
  }
722
- setPitchEnvelope(note) {
723
- const now = this.audioContext.currentTime;
724
+ setPitchEnvelope(note, scheduleTime) {
724
725
  const { voiceParams } = note;
725
726
  const baseRate = voiceParams.playbackRate;
726
727
  note.bufferSource.playbackRate
727
- .cancelScheduledValues(now)
728
- .setValueAtTime(baseRate, now);
728
+ .cancelScheduledValues(scheduleTime)
729
+ .setValueAtTime(baseRate, scheduleTime);
729
730
  const modEnvToPitch = voiceParams.modEnvToPitch;
730
731
  if (modEnvToPitch === 0)
731
732
  return;
732
733
  const basePitch = this.rateToCent(baseRate);
733
734
  const peekPitch = basePitch + modEnvToPitch;
734
735
  const peekRate = this.centToRate(peekPitch);
735
- const modDelay = startTime + voiceParams.modDelay;
736
+ const modDelay = note.startTime + voiceParams.modDelay;
736
737
  const modAttack = modDelay + voiceParams.modAttack;
737
738
  const modHold = modAttack + voiceParams.modHold;
738
739
  const modDecay = modHold + voiceParams.modDecay;
@@ -747,8 +748,7 @@ class MidyGM1 {
747
748
  const maxFrequency = 20000; // max Hz of initialFilterFc
748
749
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
749
750
  }
750
- setFilterEnvelope(note) {
751
- const now = this.audioContext.currentTime;
751
+ setFilterEnvelope(note, scheduleTime) {
752
752
  const { voiceParams, startTime } = note;
753
753
  const baseFreq = this.centToHz(voiceParams.initialFilterFc);
754
754
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
@@ -762,14 +762,14 @@ class MidyGM1 {
762
762
  const modHold = modAttack + voiceParams.modHold;
763
763
  const modDecay = modHold + voiceParams.modDecay;
764
764
  note.filterNode.frequency
765
- .cancelScheduledValues(now)
765
+ .cancelScheduledValues(scheduleTime)
766
766
  .setValueAtTime(adjustedBaseFreq, startTime)
767
767
  .setValueAtTime(adjustedBaseFreq, modDelay)
768
768
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
769
769
  .setValueAtTime(adjustedPeekFreq, modHold)
770
770
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
771
771
  }
772
- startModulation(channel, note, startTime) {
772
+ startModulation(channel, note, scheduleTime) {
773
773
  const { voiceParams } = note;
774
774
  note.modulationLFO = new OscillatorNode(this.audioContext, {
775
775
  frequency: this.centToHz(voiceParams.freqModLFO),
@@ -778,10 +778,10 @@ class MidyGM1 {
778
778
  gain: voiceParams.modLfoToFilterFc,
779
779
  });
780
780
  note.modulationDepth = new GainNode(this.audioContext);
781
- this.setModLfoToPitch(channel, note);
781
+ this.setModLfoToPitch(channel, note, scheduleTime);
782
782
  note.volumeDepth = new GainNode(this.audioContext);
783
- this.setModLfoToVolume(note);
784
- note.modulationLFO.start(startTime + voiceParams.delayModLFO);
783
+ this.setModLfoToVolume(note, scheduleTime);
784
+ note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
785
785
  note.modulationLFO.connect(note.filterDepth);
786
786
  note.filterDepth.connect(note.filterNode.frequency);
787
787
  note.modulationLFO.connect(note.modulationDepth);
@@ -808,6 +808,7 @@ class MidyGM1 {
808
808
  }
809
809
  }
810
810
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
811
+ const now = this.audioContext.currentTime;
811
812
  const state = channel.state;
812
813
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
813
814
  const voiceParams = voice.getAllParams(controllerState);
@@ -819,11 +820,11 @@ class MidyGM1 {
819
820
  type: "lowpass",
820
821
  Q: voiceParams.initialFilterQ / 10, // dB
821
822
  });
822
- this.setVolumeEnvelope(note);
823
- this.setFilterEnvelope(note);
824
- this.setPitchEnvelope(note);
823
+ this.setVolumeEnvelope(note, now);
824
+ this.setFilterEnvelope(note, now);
825
+ this.setPitchEnvelope(note, now);
825
826
  if (0 < state.modulationDepth) {
826
- this.startModulation(channel, note, startTime);
827
+ this.startModulation(channel, note, now);
827
828
  }
828
829
  note.bufferSource.connect(note.filterNode);
829
830
  note.filterNode.connect(note.volumeEnvelopeNode);
@@ -850,7 +851,7 @@ class MidyGM1 {
850
851
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
851
852
  const [prevNote, prevChannelNumber] = prevEntry;
852
853
  if (!prevNote.ending) {
853
- this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
854
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
854
855
  startTime, undefined, // portamentoNoteNumber
855
856
  true);
856
857
  }
@@ -865,9 +866,9 @@ class MidyGM1 {
865
866
  scheduledNotes.set(noteNumber, [note]);
866
867
  }
867
868
  }
868
- noteOn(channelNumber, noteNumber, velocity) {
869
- const now = this.audioContext.currentTime;
870
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
869
+ noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
870
+ scheduleTime ??= this.audioContext.currentTime;
871
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
871
872
  }
872
873
  stopNote(endTime, stopTime, scheduledNotes, index) {
873
874
  const note = scheduledNotes[index];
@@ -889,16 +890,12 @@ class MidyGM1 {
889
890
  note.modulationDepth.disconnect();
890
891
  note.modulationLFO.stop();
891
892
  }
892
- if (note.vibratoDepth) {
893
- note.vibratoDepth.disconnect();
894
- note.vibratoLFO.stop();
895
- }
896
893
  resolve();
897
894
  };
898
895
  note.bufferSource.stop(stopTime);
899
896
  });
900
897
  }
901
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
898
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
902
899
  const channel = this.channels[channelNumber];
903
900
  if (!force && 0.5 < channel.state.sustainPedal)
904
901
  return;
@@ -920,161 +917,119 @@ class MidyGM1 {
920
917
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
921
918
  }
922
919
  }
923
- releaseNote(channelNumber, noteNumber, velocity) {
924
- const now = this.audioContext.currentTime;
925
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
920
+ noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
921
+ scheduleTime ??= this.audioContext.currentTime;
922
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
926
923
  }
927
- releaseSustainPedal(channelNumber, halfVelocity) {
924
+ releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
928
925
  const velocity = halfVelocity * 2;
929
926
  const channel = this.channels[channelNumber];
930
927
  const promises = [];
931
- channel.state.sustainPedal = halfVelocity;
932
- channel.scheduledNotes.forEach((noteList) => {
933
- for (let i = 0; i < noteList.length; i++) {
934
- const note = noteList[i];
935
- if (!note)
936
- continue;
937
- const { noteNumber } = note;
938
- const promise = this.releaseNote(channelNumber, noteNumber, velocity);
939
- promises.push(promise);
940
- }
928
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
929
+ const { noteNumber } = note;
930
+ const promise = this.noteOff(channelNumber, noteNumber, velocity);
931
+ promises.push(promise);
941
932
  });
942
933
  return promises;
943
934
  }
944
- handleMIDIMessage(statusByte, data1, data2) {
935
+ handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
945
936
  const channelNumber = statusByte & 0x0F;
946
937
  const messageType = statusByte & 0xF0;
947
938
  switch (messageType) {
948
939
  case 0x80:
949
- return this.releaseNote(channelNumber, data1, data2);
940
+ return this.noteOff(channelNumber, data1, data2, scheduleTime);
950
941
  case 0x90:
951
- return this.noteOn(channelNumber, data1, data2);
942
+ return this.noteOn(channelNumber, data1, data2, scheduleTime);
952
943
  case 0xB0:
953
- return this.handleControlChange(channelNumber, data1, data2);
944
+ return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
954
945
  case 0xC0:
955
- return this.handleProgramChange(channelNumber, data1);
946
+ return this.handleProgramChange(channelNumber, data1, scheduleTime);
956
947
  case 0xE0:
957
- return this.handlePitchBendMessage(channelNumber, data1, data2);
948
+ return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
958
949
  default:
959
950
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
960
951
  }
961
952
  }
962
- handleProgramChange(channelNumber, program) {
953
+ handleProgramChange(channelNumber, program, _scheduleTime) {
963
954
  const channel = this.channels[channelNumber];
964
955
  channel.program = program;
965
956
  }
966
- handlePitchBendMessage(channelNumber, lsb, msb) {
957
+ handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
967
958
  const pitchBend = msb * 128 + lsb;
968
- this.setPitchBend(channelNumber, pitchBend);
959
+ this.setPitchBend(channelNumber, pitchBend, scheduleTime);
969
960
  }
970
- setPitchBend(channelNumber, value) {
961
+ setPitchBend(channelNumber, value, scheduleTime) {
962
+ scheduleTime ??= this.audioContext.currentTime;
971
963
  const channel = this.channels[channelNumber];
972
964
  const state = channel.state;
973
965
  const prev = state.pitchWheel * 2 - 1;
974
966
  const next = (value - 8192) / 8192;
975
967
  state.pitchWheel = value / 16383;
976
968
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
977
- this.updateChannelDetune(channel);
978
- this.applyVoiceParams(channel, 14);
969
+ this.updateChannelDetune(channel, scheduleTime);
970
+ this.applyVoiceParams(channel, 14, scheduleTime);
979
971
  }
980
- setModLfoToPitch(channel, note) {
981
- const now = this.audioContext.currentTime;
972
+ setModLfoToPitch(channel, note, scheduleTime) {
982
973
  const modLfoToPitch = note.voiceParams.modLfoToPitch;
983
974
  const baseDepth = Math.abs(modLfoToPitch) +
984
975
  channel.state.modulationDepth;
985
976
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
986
977
  note.modulationDepth.gain
987
- .cancelScheduledValues(now)
988
- .setValueAtTime(modulationDepth, now);
978
+ .cancelScheduledValues(scheduleTime)
979
+ .setValueAtTime(modulationDepth, scheduleTime);
989
980
  }
990
- setVibLfoToPitch(channel, note) {
991
- const now = this.audioContext.currentTime;
992
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
993
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
994
- 2;
995
- const vibratoDepthSign = 0 < vibLfoToPitch;
996
- note.vibratoDepth.gain
997
- .cancelScheduledValues(now)
998
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
999
- }
1000
- setModLfoToFilterFc(note) {
1001
- const now = this.audioContext.currentTime;
981
+ setModLfoToFilterFc(note, scheduleTime) {
1002
982
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1003
983
  note.filterDepth.gain
1004
- .cancelScheduledValues(now)
1005
- .setValueAtTime(modLfoToFilterFc, now);
984
+ .cancelScheduledValues(scheduleTime)
985
+ .setValueAtTime(modLfoToFilterFc, scheduleTime);
1006
986
  }
1007
- setModLfoToVolume(note) {
1008
- const now = this.audioContext.currentTime;
987
+ setModLfoToVolume(note, scheduleTime) {
1009
988
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1010
989
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1011
990
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1012
991
  note.volumeDepth.gain
1013
- .cancelScheduledValues(now)
1014
- .setValueAtTime(volumeDepth, now);
992
+ .cancelScheduledValues(scheduleTime)
993
+ .setValueAtTime(volumeDepth, scheduleTime);
1015
994
  }
1016
- setDelayModLFO(note) {
1017
- const now = this.audioContext.currentTime;
995
+ setDelayModLFO(note, scheduleTime) {
1018
996
  const startTime = note.startTime;
1019
- if (startTime < now)
997
+ if (startTime < scheduleTime)
1020
998
  return;
1021
- note.modulationLFO.stop(now);
999
+ note.modulationLFO.stop(scheduleTime);
1022
1000
  note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1023
1001
  note.modulationLFO.connect(note.filterDepth);
1024
1002
  }
1025
- setFreqModLFO(note) {
1026
- const now = this.audioContext.currentTime;
1003
+ setFreqModLFO(note, scheduleTime) {
1027
1004
  const freqModLFO = note.voiceParams.freqModLFO;
1028
1005
  note.modulationLFO.frequency
1029
- .cancelScheduledValues(now)
1030
- .setValueAtTime(freqModLFO, now);
1006
+ .cancelScheduledValues(scheduleTime)
1007
+ .setValueAtTime(freqModLFO, scheduleTime);
1031
1008
  }
1032
1009
  createVoiceParamsHandlers() {
1033
1010
  return {
1034
- modLfoToPitch: (channel, note, _prevValue) => {
1011
+ modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1035
1012
  if (0 < channel.state.modulationDepth) {
1036
- this.setModLfoToPitch(channel, note);
1037
- }
1038
- },
1039
- vibLfoToPitch: (channel, note, _prevValue) => {
1040
- if (0 < channel.state.vibratoDepth) {
1041
- this.setVibLfoToPitch(channel, note);
1013
+ this.setModLfoToPitch(channel, note, scheduleTime);
1042
1014
  }
1043
1015
  },
1044
- modLfoToFilterFc: (channel, note, _prevValue) => {
1045
- if (0 < channel.state.modulationDepth)
1046
- this.setModLfoToFilterFc(note);
1047
- },
1048
- modLfoToVolume: (channel, note, _prevValue) => {
1049
- if (0 < channel.state.modulationDepth)
1050
- this.setModLfoToVolume(note);
1051
- },
1052
- chorusEffectsSend: (_channel, _note, _prevValue) => { },
1053
- reverbEffectsSend: (_channel, _note, _prevValue) => { },
1054
- delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1055
- freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1056
- delayVibLFO: (channel, note, prevValue) => {
1057
- if (0 < channel.state.vibratoDepth) {
1058
- const now = this.audioContext.currentTime;
1059
- const vibratoDelay = channel.state.vibratoDelay * 2;
1060
- const prevStartTime = note.startTime + prevValue * vibratoDelay;
1061
- if (now < prevStartTime)
1062
- return;
1063
- const value = note.voiceParams.delayVibLFO;
1064
- const startTime = note.startTime + value * vibratoDelay;
1065
- note.vibratoLFO.stop(now);
1066
- note.vibratoLFO.start(startTime);
1016
+ vibLfoToPitch: (_channel, _note, _prevValue, _scheduleTime) => { },
1017
+ modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1018
+ if (0 < channel.state.modulationDepth) {
1019
+ this.setModLfoToFilterFc(note, scheduleTime);
1067
1020
  }
1068
1021
  },
1069
- freqVibLFO: (channel, note, _prevValue) => {
1070
- if (0 < channel.state.vibratoDepth) {
1071
- const now = this.audioContext.currentTime;
1072
- const freqVibLFO = note.voiceParams.freqVibLFO;
1073
- note.vibratoLFO.frequency
1074
- .cancelScheduledValues(now)
1075
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1022
+ modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1023
+ if (0 < channel.state.modulationDepth) {
1024
+ this.setModLfoToVolume(note, scheduleTime);
1076
1025
  }
1077
1026
  },
1027
+ chorusEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1028
+ reverbEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
1029
+ delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1030
+ freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1031
+ delayVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1032
+ freqVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
1078
1033
  };
1079
1034
  }
1080
1035
  getControllerState(channel, noteNumber, velocity) {
@@ -1084,7 +1039,7 @@ class MidyGM1 {
1084
1039
  state[3] = noteNumber / 127;
1085
1040
  return state;
1086
1041
  }
1087
- applyVoiceParams(channel, controllerType) {
1042
+ applyVoiceParams(channel, controllerType, scheduleTime) {
1088
1043
  channel.scheduledNotes.forEach((noteList) => {
1089
1044
  for (let i = 0; i < noteList.length; i++) {
1090
1045
  const note = noteList[i];
@@ -1100,7 +1055,7 @@ class MidyGM1 {
1100
1055
  continue;
1101
1056
  note.voiceParams[key] = value;
1102
1057
  if (key in this.voiceParamsHandlers) {
1103
- this.voiceParamsHandlers[key](channel, note, prevValue);
1058
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1104
1059
  }
1105
1060
  else if (filterEnvelopeKeySet.has(key)) {
1106
1061
  if (appliedFilterEnvelope)
@@ -1112,8 +1067,8 @@ class MidyGM1 {
1112
1067
  if (key in voiceParams)
1113
1068
  noteVoiceParams[key] = voiceParams[key];
1114
1069
  }
1115
- this.setFilterEnvelope(note);
1116
- this.setPitchEnvelope(note);
1070
+ this.setFilterEnvelope(note, scheduleTime);
1071
+ this.setPitchEnvelope(note, scheduleTime);
1117
1072
  }
1118
1073
  else if (volumeEnvelopeKeySet.has(key)) {
1119
1074
  if (appliedVolumeEnvelope)
@@ -1125,7 +1080,7 @@ class MidyGM1 {
1125
1080
  if (key in voiceParams)
1126
1081
  noteVoiceParams[key] = voiceParams[key];
1127
1082
  }
1128
- this.setVolumeEnvelope(channel, note);
1083
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1129
1084
  }
1130
1085
  }
1131
1086
  }
@@ -1147,44 +1102,39 @@ class MidyGM1 {
1147
1102
  123: this.allNotesOff,
1148
1103
  };
1149
1104
  }
1150
- handleControlChange(channelNumber, controllerType, value) {
1105
+ handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1151
1106
  const handler = this.controlChangeHandlers[controllerType];
1152
1107
  if (handler) {
1153
- handler.call(this, channelNumber, value);
1108
+ handler.call(this, channelNumber, value, scheduleTime);
1154
1109
  const channel = this.channels[channelNumber];
1155
- this.applyVoiceParams(channel, controllerType + 128);
1110
+ this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1156
1111
  }
1157
1112
  else {
1158
1113
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
1159
1114
  }
1160
1115
  }
1161
- updateModulation(channel) {
1162
- const now = this.audioContext.currentTime;
1116
+ updateModulation(channel, scheduleTime) {
1117
+ scheduleTime ??= this.audioContext.currentTime;
1163
1118
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1164
- channel.scheduledNotes.forEach((noteList) => {
1165
- for (let i = 0; i < noteList.length; i++) {
1166
- const note = noteList[i];
1167
- if (!note)
1168
- continue;
1169
- if (note.modulationDepth) {
1170
- note.modulationDepth.gain.setValueAtTime(depth, now);
1171
- }
1172
- else {
1173
- this.setPitchEnvelope(note);
1174
- this.startModulation(channel, note, now);
1175
- }
1119
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1120
+ if (note.modulationDepth) {
1121
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1122
+ }
1123
+ else {
1124
+ this.setPitchEnvelope(note, scheduleTime);
1125
+ this.startModulation(channel, note, scheduleTime);
1176
1126
  }
1177
1127
  });
1178
1128
  }
1179
- setModulationDepth(channelNumber, modulation) {
1129
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1180
1130
  const channel = this.channels[channelNumber];
1181
1131
  channel.state.modulationDepth = modulation / 127;
1182
- this.updateModulation(channel);
1132
+ this.updateModulation(channel, scheduleTime);
1183
1133
  }
1184
- setVolume(channelNumber, volume) {
1134
+ setVolume(channelNumber, volume, scheduleTime) {
1185
1135
  const channel = this.channels[channelNumber];
1186
1136
  channel.state.volume = volume / 127;
1187
- this.updateChannelVolume(channel);
1137
+ this.updateChannelVolume(channel, scheduleTime);
1188
1138
  }
1189
1139
  panToGain(pan) {
1190
1140
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1193,36 +1143,36 @@ class MidyGM1 {
1193
1143
  gainRight: Math.sin(theta),
1194
1144
  };
1195
1145
  }
1196
- setPan(channelNumber, pan) {
1146
+ setPan(channelNumber, pan, scheduleTime) {
1197
1147
  const channel = this.channels[channelNumber];
1198
1148
  channel.state.pan = pan / 127;
1199
- this.updateChannelVolume(channel);
1149
+ this.updateChannelVolume(channel, scheduleTime);
1200
1150
  }
1201
- setExpression(channelNumber, expression) {
1151
+ setExpression(channelNumber, expression, scheduleTime) {
1202
1152
  const channel = this.channels[channelNumber];
1203
1153
  channel.state.expression = expression / 127;
1204
- this.updateChannelVolume(channel);
1154
+ this.updateChannelVolume(channel, scheduleTime);
1205
1155
  }
1206
- dataEntryLSB(channelNumber, value) {
1156
+ dataEntryLSB(channelNumber, value, scheduleTime) {
1207
1157
  this.channels[channelNumber].dataLSB = value;
1208
- this.handleRPN(channelNumber, 0);
1158
+ this.handleRPN(channelNumber, scheduleTime);
1209
1159
  }
1210
- updateChannelVolume(channel) {
1211
- const now = this.audioContext.currentTime;
1160
+ updateChannelVolume(channel, scheduleTime) {
1212
1161
  const state = channel.state;
1213
1162
  const volume = state.volume * state.expression;
1214
1163
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1215
1164
  channel.gainL.gain
1216
- .cancelScheduledValues(now)
1217
- .setValueAtTime(volume * gainLeft, now);
1165
+ .cancelScheduledValues(scheduleTime)
1166
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1218
1167
  channel.gainR.gain
1219
- .cancelScheduledValues(now)
1220
- .setValueAtTime(volume * gainRight, now);
1168
+ .cancelScheduledValues(scheduleTime)
1169
+ .setValueAtTime(volume * gainRight, scheduleTime);
1221
1170
  }
1222
- setSustainPedal(channelNumber, value) {
1171
+ setSustainPedal(channelNumber, value, scheduleTime) {
1172
+ scheduleTime ??= this.audioContext.currentTime;
1223
1173
  this.channels[channelNumber].state.sustainPedal = value / 127;
1224
1174
  if (value < 64) {
1225
- this.releaseSustainPedal(channelNumber, value);
1175
+ this.releaseSustainPedal(channelNumber, value, scheduleTime);
1226
1176
  }
1227
1177
  }
1228
1178
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
@@ -1251,12 +1201,12 @@ class MidyGM1 {
1251
1201
  channel.dataMSB = minMSB;
1252
1202
  }
1253
1203
  }
1254
- handleRPN(channelNumber) {
1204
+ handleRPN(channelNumber, scheduleTime) {
1255
1205
  const channel = this.channels[channelNumber];
1256
1206
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1257
1207
  switch (rpn) {
1258
1208
  case 0:
1259
- this.handlePitchBendRangeRPN(channelNumber);
1209
+ this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1260
1210
  break;
1261
1211
  case 1:
1262
1212
  this.handleFineTuningRPN(channelNumber);
@@ -1274,25 +1224,26 @@ class MidyGM1 {
1274
1224
  setRPNLSB(channelNumber, value) {
1275
1225
  this.channels[channelNumber].rpnLSB = value;
1276
1226
  }
1277
- dataEntryMSB(channelNumber, value) {
1227
+ dataEntryMSB(channelNumber, value, scheduleTime) {
1278
1228
  this.channels[channelNumber].dataMSB = value;
1279
- this.handleRPN(channelNumber);
1229
+ this.handleRPN(channelNumber, scheduleTime);
1280
1230
  }
1281
- handlePitchBendRangeRPN(channelNumber) {
1231
+ handlePitchBendRangeRPN(channelNumber, scheduleTime) {
1282
1232
  const channel = this.channels[channelNumber];
1283
1233
  this.limitData(channel, 0, 127, 0, 99);
1284
1234
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1285
- this.setPitchBendRange(channelNumber, pitchBendRange);
1235
+ this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1286
1236
  }
1287
- setPitchBendRange(channelNumber, value) {
1237
+ setPitchBendRange(channelNumber, value, scheduleTime) {
1238
+ scheduleTime ??= this.audioContext.currentTime;
1288
1239
  const channel = this.channels[channelNumber];
1289
1240
  const state = channel.state;
1290
1241
  const prev = state.pitchWheelSensitivity;
1291
1242
  const next = value / 128;
1292
1243
  state.pitchWheelSensitivity = next;
1293
1244
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1294
- this.updateChannelDetune(channel);
1295
- this.applyVoiceParams(channel, 16);
1245
+ this.updateChannelDetune(channel, scheduleTime);
1246
+ this.applyVoiceParams(channel, 16, scheduleTime);
1296
1247
  }
1297
1248
  handleFineTuningRPN(channelNumber) {
1298
1249
  const channel = this.channels[channelNumber];
@@ -1322,8 +1273,9 @@ class MidyGM1 {
1322
1273
  channel.detune += next - prev;
1323
1274
  this.updateChannelDetune(channel);
1324
1275
  }
1325
- allSoundOff(channelNumber) {
1326
- return this.stopChannelNotes(channelNumber, 0, true);
1276
+ allSoundOff(channelNumber, _value, scheduleTime) {
1277
+ scheduleTime ??= this.audioContext.currentTime;
1278
+ return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1327
1279
  }
1328
1280
  resetAllControllers(channelNumber) {
1329
1281
  const stateTypes = [
@@ -1347,10 +1299,11 @@ class MidyGM1 {
1347
1299
  channel[type] = this.constructor.channelSettings[type];
1348
1300
  }
1349
1301
  }
1350
- allNotesOff(channelNumber) {
1351
- return this.stopChannelNotes(channelNumber, 0, false);
1302
+ allNotesOff(channelNumber, _value, scheduleTime) {
1303
+ scheduleTime ??= this.audioContext.currentTime;
1304
+ return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1352
1305
  }
1353
- handleUniversalNonRealTimeExclusiveMessage(data) {
1306
+ handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
1354
1307
  switch (data[2]) {
1355
1308
  case 9:
1356
1309
  switch (data[3]) {
@@ -1374,12 +1327,12 @@ class MidyGM1 {
1374
1327
  }
1375
1328
  this.channels[9].bank = 128;
1376
1329
  }
1377
- handleUniversalRealTimeExclusiveMessage(data) {
1330
+ handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1378
1331
  switch (data[2]) {
1379
1332
  case 4:
1380
1333
  switch (data[3]) {
1381
1334
  case 1:
1382
- return this.handleMasterVolumeSysEx(data);
1335
+ return this.handleMasterVolumeSysEx(data, scheduleTime);
1383
1336
  default:
1384
1337
  console.warn(`Unsupported Exclusive Message: ${data}`);
1385
1338
  }
@@ -1388,42 +1341,40 @@ class MidyGM1 {
1388
1341
  console.warn(`Unsupported Exclusive Message: ${data}`);
1389
1342
  }
1390
1343
  }
1391
- handleMasterVolumeSysEx(data) {
1344
+ handleMasterVolumeSysEx(data, scheduleTime) {
1392
1345
  const volume = (data[5] * 128 + data[4]) / 16383;
1393
- this.setMasterVolume(volume);
1346
+ this.setMasterVolume(volume, scheduleTime);
1394
1347
  }
1395
- setMasterVolume(volume) {
1348
+ setMasterVolume(volume, scheduleTime) {
1349
+ scheduleTime ??= this.audioContext.currentTime;
1396
1350
  if (volume < 0 && 1 < volume) {
1397
1351
  console.error("Master Volume is out of range");
1398
1352
  }
1399
1353
  else {
1400
- const now = this.audioContext.currentTime;
1401
- this.masterVolume.gain.cancelScheduledValues(now);
1402
- this.masterVolume.gain.setValueAtTime(volume * volume, now);
1354
+ this.masterVolume.gain
1355
+ .cancelScheduledValues(scheduleTime)
1356
+ .setValueAtTime(volume * volume, scheduleTime);
1403
1357
  }
1404
1358
  }
1405
- handleExclusiveMessage(data) {
1406
- console.warn(`Unsupported Exclusive Message: ${data}`);
1407
- }
1408
- handleSysEx(data) {
1359
+ handleSysEx(data, scheduleTime) {
1409
1360
  switch (data[0]) {
1410
1361
  case 126:
1411
- return this.handleUniversalNonRealTimeExclusiveMessage(data);
1362
+ return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
1412
1363
  case 127:
1413
- return this.handleUniversalRealTimeExclusiveMessage(data);
1364
+ return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
1414
1365
  default:
1415
- return this.handleExclusiveMessage(data);
1366
+ console.warn(`Unsupported Exclusive Message: ${data}`);
1416
1367
  }
1417
1368
  }
1418
- scheduleTask(callback, startTime) {
1369
+ scheduleTask(callback, scheduleTime) {
1419
1370
  return new Promise((resolve) => {
1420
1371
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
1421
1372
  bufferSource.onended = () => {
1422
1373
  callback();
1423
1374
  resolve();
1424
1375
  };
1425
- bufferSource.start(startTime);
1426
- bufferSource.stop(startTime);
1376
+ bufferSource.start(scheduleTime);
1377
+ bufferSource.stop(scheduleTime);
1427
1378
  });
1428
1379
  }
1429
1380
  }