@marmooo/midy 0.2.6 → 0.2.7

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