@marmooo/midy 0.2.6 → 0.2.8

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