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