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