@marmooo/midy 0.4.1 → 0.4.3

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.
@@ -14,23 +14,23 @@ class Note {
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
- Object.defineProperty(this, "index", {
17
+ Object.defineProperty(this, "adjustedBaseFreq", {
18
18
  enumerable: true,
19
19
  configurable: true,
20
20
  writable: true,
21
- value: -1
21
+ value: 20000
22
22
  });
23
- Object.defineProperty(this, "ending", {
23
+ Object.defineProperty(this, "index", {
24
24
  enumerable: true,
25
25
  configurable: true,
26
26
  writable: true,
27
- value: false
27
+ value: -1
28
28
  });
29
- Object.defineProperty(this, "pending", {
29
+ Object.defineProperty(this, "ending", {
30
30
  enumerable: true,
31
31
  configurable: true,
32
32
  writable: true,
33
- value: true
33
+ value: false
34
34
  });
35
35
  Object.defineProperty(this, "bufferSource", {
36
36
  enumerable: true,
@@ -77,6 +77,9 @@ class Note {
77
77
  this.noteNumber = noteNumber;
78
78
  this.velocity = velocity;
79
79
  this.startTime = startTime;
80
+ this.ready = new Promise((resolve) => {
81
+ this.resolveReady = resolve;
82
+ });
80
83
  }
81
84
  }
82
85
  const drumExclusiveClasses = new Uint8Array(128);
@@ -163,8 +166,14 @@ const pitchEnvelopeKeys = [
163
166
  "playbackRate",
164
167
  ];
165
168
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
166
- export class MidyGMLite {
169
+ function cbToRatio(cb) {
170
+ return Math.pow(10, cb / 200);
171
+ }
172
+ const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
173
+ const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
174
+ export class MidyGMLite extends EventTarget {
167
175
  constructor(audioContext) {
176
+ super();
168
177
  Object.defineProperty(this, "mode", {
169
178
  enumerable: true,
170
179
  configurable: true,
@@ -279,6 +288,26 @@ export class MidyGMLite {
279
288
  writable: true,
280
289
  value: false
281
290
  });
291
+ Object.defineProperty(this, "totalTimeEventTypes", {
292
+ enumerable: true,
293
+ configurable: true,
294
+ writable: true,
295
+ value: new Set([
296
+ "noteOff",
297
+ ])
298
+ });
299
+ Object.defineProperty(this, "tempo", {
300
+ enumerable: true,
301
+ configurable: true,
302
+ writable: true,
303
+ value: 1
304
+ });
305
+ Object.defineProperty(this, "loop", {
306
+ enumerable: true,
307
+ configurable: true,
308
+ writable: true,
309
+ value: false
310
+ });
282
311
  Object.defineProperty(this, "playPromise", {
283
312
  enumerable: true,
284
313
  configurable: true,
@@ -387,13 +416,13 @@ export class MidyGMLite {
387
416
  this.totalTime = this.calcTotalTime();
388
417
  }
389
418
  cacheVoiceIds() {
390
- const timeline = this.timeline;
419
+ const { channels, timeline, voiceCounter } = this;
391
420
  for (let i = 0; i < timeline.length; i++) {
392
421
  const event = timeline[i];
393
422
  switch (event.type) {
394
423
  case "noteOn": {
395
- const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
396
- this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
424
+ const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
425
+ voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
397
426
  break;
398
427
  }
399
428
  case "controller":
@@ -408,9 +437,9 @@ export class MidyGMLite {
408
437
  this.setProgramChange(event.channel, event.programNumber, event.startTime);
409
438
  }
410
439
  }
411
- for (const [audioBufferId, count] of this.voiceCounter) {
440
+ for (const [audioBufferId, count] of voiceCounter) {
412
441
  if (count === 1)
413
- this.voiceCounter.delete(audioBufferId);
442
+ voiceCounter.delete(audioBufferId);
414
443
  }
415
444
  this.GM1SystemOn();
416
445
  }
@@ -419,7 +448,12 @@ export class MidyGMLite {
419
448
  const bankTable = this.soundFontTable[programNumber];
420
449
  if (!bankTable)
421
450
  return;
422
- const bank = channel.isDrum ? 128 : 0;
451
+ let bank = channel.isDrum ? 128 : 0;
452
+ if (bankTable[bank] === undefined) {
453
+ if (channel.isDrum)
454
+ return;
455
+ bank = 0;
456
+ }
423
457
  const soundFontIndex = bankTable[bank];
424
458
  if (soundFontIndex === undefined)
425
459
  return;
@@ -474,19 +508,21 @@ export class MidyGMLite {
474
508
  }
475
509
  return bufferSource;
476
510
  }
477
- async scheduleTimelineEvents(scheduleTime, queueIndex) {
511
+ scheduleTimelineEvents(scheduleTime, queueIndex) {
478
512
  const timeOffset = this.resumeTime - this.startTime;
479
513
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
480
514
  const schedulingOffset = this.startDelay - timeOffset;
481
515
  const timeline = this.timeline;
516
+ const inverseTempo = 1 / this.tempo;
482
517
  while (queueIndex < timeline.length) {
483
518
  const event = timeline[queueIndex];
484
- if (lookAheadCheckTime < event.startTime)
519
+ const t = event.startTime * inverseTempo;
520
+ if (lookAheadCheckTime < t)
485
521
  break;
486
- const startTime = event.startTime + schedulingOffset;
522
+ const startTime = t + schedulingOffset;
487
523
  switch (event.type) {
488
524
  case "noteOn":
489
- await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
525
+ this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
490
526
  break;
491
527
  case "noteOff": {
492
528
  this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
@@ -509,8 +545,10 @@ export class MidyGMLite {
509
545
  return queueIndex;
510
546
  }
511
547
  getQueueIndex(second) {
512
- for (let i = 0; i < this.timeline.length; i++) {
513
- if (second <= this.timeline[i].startTime) {
548
+ const timeline = this.timeline;
549
+ const inverseTempo = 1 / this.tempo;
550
+ for (let i = 0; i < timeline.length; i++) {
551
+ if (second <= timeline[i].startTime * inverseTempo) {
514
552
  return i;
515
553
  }
516
554
  }
@@ -521,79 +559,112 @@ export class MidyGMLite {
521
559
  this.drumExclusiveClassNotes.fill(undefined);
522
560
  this.voiceCache.clear();
523
561
  this.realtimeVoiceCache.clear();
524
- for (let i = 0; i < this.channels.length; i++) {
525
- this.channels[i].scheduledNotes = [];
562
+ const channels = this.channels;
563
+ for (let i = 0; i < channels.length; i++) {
564
+ channels[i].scheduledNotes = [];
526
565
  this.resetChannelStates(i);
527
566
  }
528
567
  }
529
568
  updateStates(queueIndex, nextQueueIndex) {
569
+ const { timeline, resumeTime } = this;
570
+ const inverseTempo = 1 / this.tempo;
571
+ const now = this.audioContext.currentTime;
530
572
  if (nextQueueIndex < queueIndex)
531
573
  queueIndex = 0;
532
574
  for (let i = queueIndex; i < nextQueueIndex; i++) {
533
- const event = this.timeline[i];
575
+ const event = timeline[i];
534
576
  switch (event.type) {
535
577
  case "controller":
536
- this.setControlChange(event.channel, event.controllerType, event.value, 0);
578
+ this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
537
579
  break;
538
580
  case "programChange":
539
- this.setProgramChange(event.channel, event.programNumber, 0);
581
+ this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
540
582
  break;
541
583
  case "pitchBend":
542
- this.setPitchBend(event.channel, event.value + 8192, 0);
584
+ this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
543
585
  break;
544
586
  case "sysEx":
545
- this.handleSysEx(event.data, 0);
587
+ this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
546
588
  }
547
589
  }
548
590
  }
549
591
  async playNotes() {
550
- if (this.audioContext.state === "suspended") {
551
- await this.audioContext.resume();
592
+ const audioContext = this.audioContext;
593
+ if (audioContext.state === "suspended") {
594
+ await audioContext.resume();
552
595
  }
596
+ const paused = this.isPaused;
553
597
  this.isPlaying = true;
554
598
  this.isPaused = false;
555
- this.startTime = this.audioContext.currentTime;
599
+ this.startTime = audioContext.currentTime;
600
+ if (paused) {
601
+ this.dispatchEvent(new Event("resumed"));
602
+ }
603
+ else {
604
+ this.dispatchEvent(new Event("started"));
605
+ }
556
606
  let queueIndex = this.getQueueIndex(this.resumeTime);
557
- let finished = false;
607
+ let exitReason;
558
608
  this.notePromises = [];
559
- while (queueIndex < this.timeline.length) {
560
- const now = this.audioContext.currentTime;
609
+ while (true) {
610
+ const now = audioContext.currentTime;
611
+ if (this.totalTime < this.currentTime() ||
612
+ this.timeline.length <= queueIndex) {
613
+ await this.stopNotes(0, true, now);
614
+ if (this.loop) {
615
+ this.resetAllStates();
616
+ this.startTime = audioContext.currentTime;
617
+ this.resumeTime = 0;
618
+ queueIndex = 0;
619
+ this.dispatchEvent(new Event("looped"));
620
+ continue;
621
+ }
622
+ else {
623
+ await audioContext.suspend();
624
+ exitReason = "ended";
625
+ break;
626
+ }
627
+ }
561
628
  if (this.isPausing) {
562
629
  await this.stopNotes(0, true, now);
563
- await this.audioContext.suspend();
564
- this.notePromises = [];
630
+ await audioContext.suspend();
631
+ this.isPausing = false;
632
+ exitReason = "paused";
565
633
  break;
566
634
  }
567
635
  else if (this.isStopping) {
568
636
  await this.stopNotes(0, true, now);
569
- await this.audioContext.suspend();
570
- finished = true;
637
+ await audioContext.suspend();
638
+ this.isStopping = false;
639
+ exitReason = "stopped";
571
640
  break;
572
641
  }
573
642
  else if (this.isSeeking) {
574
- await this.stopNotes(0, true, now);
575
- this.startTime = this.audioContext.currentTime;
643
+ this.stopNotes(0, true, now);
644
+ this.startTime = audioContext.currentTime;
576
645
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
577
646
  this.updateStates(queueIndex, nextQueueIndex);
578
647
  queueIndex = nextQueueIndex;
579
648
  this.isSeeking = false;
649
+ this.dispatchEvent(new Event("seeked"));
580
650
  continue;
581
651
  }
582
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
652
+ queueIndex = this.scheduleTimelineEvents(now, queueIndex);
583
653
  const waitTime = now + this.noteCheckInterval;
584
654
  await this.scheduleTask(() => { }, waitTime);
585
655
  }
586
- if (this.timeline.length <= queueIndex) {
587
- const now = this.audioContext.currentTime;
588
- await this.stopNotes(0, true, now);
589
- await this.audioContext.suspend();
590
- finished = true;
591
- }
592
- if (finished) {
593
- this.notePromises = [];
656
+ if (exitReason !== "paused") {
594
657
  this.resetAllStates();
595
658
  }
596
659
  this.isPlaying = false;
660
+ if (exitReason === "paused") {
661
+ this.isPaused = true;
662
+ this.dispatchEvent(new Event("paused"));
663
+ }
664
+ else {
665
+ this.isPaused = false;
666
+ this.dispatchEvent(new Event(exitReason));
667
+ }
597
668
  }
598
669
  ticksToSecond(ticks, secondsPerBeat) {
599
670
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -680,11 +751,13 @@ export class MidyGMLite {
680
751
  return Promise.all(promises);
681
752
  }
682
753
  stopNotes(velocity, force, scheduleTime) {
683
- const promises = [];
684
- for (let i = 0; i < this.channels.length; i++) {
685
- promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
754
+ const channels = this.channels;
755
+ for (let i = 0; i < channels.length; i++) {
756
+ this.stopChannelNotes(i, velocity, force, scheduleTime);
686
757
  }
687
- return Promise.all(this.notePromises);
758
+ const stopPromise = Promise.all(this.notePromises);
759
+ this.notePromises = [];
760
+ return stopPromise;
688
761
  }
689
762
  async start() {
690
763
  if (this.isPlaying || this.isPaused)
@@ -700,24 +773,20 @@ export class MidyGMLite {
700
773
  return;
701
774
  this.isStopping = true;
702
775
  await this.playPromise;
703
- this.isStopping = false;
704
776
  }
705
777
  async pause() {
706
778
  if (!this.isPlaying || this.isPaused)
707
779
  return;
708
780
  const now = this.audioContext.currentTime;
709
- this.resumeTime = now - this.startTime - this.startDelay;
781
+ this.resumeTime = now + this.resumeTime - this.startTime;
710
782
  this.isPausing = true;
711
783
  await this.playPromise;
712
- this.isPausing = false;
713
- this.isPaused = true;
714
784
  }
715
785
  async resume() {
716
786
  if (!this.isPaused)
717
787
  return;
718
788
  this.playPromise = this.playNotes();
719
789
  await this.playPromise;
720
- this.isPaused = false;
721
790
  }
722
791
  seekTo(second) {
723
792
  this.resumeTime = second;
@@ -726,11 +795,17 @@ export class MidyGMLite {
726
795
  }
727
796
  }
728
797
  calcTotalTime() {
798
+ const totalTimeEventTypes = this.totalTimeEventTypes;
799
+ const timeline = this.timeline;
800
+ const inverseTempo = 1 / this.tempo;
729
801
  let totalTime = 0;
730
- for (let i = 0; i < this.timeline.length; i++) {
731
- const event = this.timeline[i];
732
- if (totalTime < event.startTime)
733
- totalTime = event.startTime;
802
+ for (let i = 0; i < timeline.length; i++) {
803
+ const event = timeline[i];
804
+ if (!totalTimeEventTypes.has(event.type))
805
+ continue;
806
+ const t = event.startTime * inverseTempo;
807
+ if (totalTime < t)
808
+ totalTime = t;
734
809
  }
735
810
  return totalTime + this.startDelay;
736
811
  }
@@ -740,19 +815,23 @@ export class MidyGMLite {
740
815
  const now = this.audioContext.currentTime;
741
816
  return now + this.resumeTime - this.startTime;
742
817
  }
743
- processScheduledNotes(channel, callback) {
818
+ async processScheduledNotes(channel, callback) {
744
819
  const scheduledNotes = channel.scheduledNotes;
820
+ const tasks = [];
745
821
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
746
822
  const note = scheduledNotes[i];
747
823
  if (!note)
748
824
  continue;
749
825
  if (note.ending)
750
826
  continue;
751
- callback(note);
827
+ const task = note.ready.then(() => callback(note));
828
+ tasks.push(task);
752
829
  }
830
+ await Promise.all(tasks);
753
831
  }
754
- processActiveNotes(channel, scheduleTime, callback) {
832
+ async processActiveNotes(channel, scheduleTime, callback) {
755
833
  const scheduledNotes = channel.scheduledNotes;
834
+ const tasks = [];
756
835
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
757
836
  const note = scheduledNotes[i];
758
837
  if (!note)
@@ -761,11 +840,10 @@ export class MidyGMLite {
761
840
  continue;
762
841
  if (scheduleTime < note.startTime)
763
842
  break;
764
- callback(note);
843
+ const task = note.ready.then(() => callback(note));
844
+ tasks.push(task);
765
845
  }
766
- }
767
- cbToRatio(cb) {
768
- return Math.pow(10, cb / 200);
846
+ await Promise.all(tasks);
769
847
  }
770
848
  rateToCent(rate) {
771
849
  return 1200 * Math.log2(rate);
@@ -793,26 +871,26 @@ export class MidyGMLite {
793
871
  }
794
872
  setVolumeEnvelope(note, scheduleTime) {
795
873
  const { voiceParams, startTime } = note;
796
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
874
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
797
875
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
798
876
  const volDelay = startTime + voiceParams.volDelay;
799
877
  const volAttack = volDelay + voiceParams.volAttack;
800
878
  const volHold = volAttack + voiceParams.volHold;
801
- const volDecay = volHold + voiceParams.volDecay;
879
+ const decayDuration = voiceParams.volDecay;
802
880
  note.volumeEnvelopeNode.gain
803
881
  .cancelScheduledValues(scheduleTime)
804
882
  .setValueAtTime(0, startTime)
805
- .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
806
- .exponentialRampToValueAtTime(attackVolume, volAttack)
883
+ .setValueAtTime(0, volDelay)
884
+ .linearRampToValueAtTime(attackVolume, volAttack)
807
885
  .setValueAtTime(attackVolume, volHold)
808
- .linearRampToValueAtTime(sustainVolume, volDecay);
886
+ .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
809
887
  }
810
888
  setPitchEnvelope(note, scheduleTime) {
811
889
  const { voiceParams } = note;
812
890
  const baseRate = voiceParams.playbackRate;
813
891
  note.bufferSource.playbackRate
814
892
  .cancelScheduledValues(scheduleTime)
815
- .setValueAtTime(baseRate, scheduleTime);
893
+ .setValueAtTime(baseRate, note.startTime);
816
894
  const modEnvToPitch = voiceParams.modEnvToPitch;
817
895
  if (modEnvToPitch === 0)
818
896
  return;
@@ -822,12 +900,12 @@ export class MidyGMLite {
822
900
  const modDelay = note.startTime + voiceParams.modDelay;
823
901
  const modAttack = modDelay + voiceParams.modAttack;
824
902
  const modHold = modAttack + voiceParams.modHold;
825
- const modDecay = modHold + voiceParams.modDecay;
903
+ const decayDuration = voiceParams.modDecay;
826
904
  note.bufferSource.playbackRate
827
905
  .setValueAtTime(baseRate, modDelay)
828
- .exponentialRampToValueAtTime(peekRate, modAttack)
906
+ .linearRampToValueAtTime(peekRate, modAttack)
829
907
  .setValueAtTime(peekRate, modHold)
830
- .linearRampToValueAtTime(baseRate, modDecay);
908
+ .setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
831
909
  }
832
910
  clampCutoffFrequency(frequency) {
833
911
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -836,36 +914,42 @@ export class MidyGMLite {
836
914
  }
837
915
  setFilterEnvelope(note, scheduleTime) {
838
916
  const { voiceParams, startTime } = note;
839
- const baseFreq = this.centToHz(voiceParams.initialFilterFc);
840
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
841
- const sustainFreq = baseFreq +
842
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
917
+ const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
918
+ const baseCent = voiceParams.initialFilterFc;
919
+ const peekCent = baseCent + modEnvToFilterFc;
920
+ const sustainCent = baseCent +
921
+ modEnvToFilterFc * (1 - voiceParams.modSustain);
922
+ const baseFreq = this.centToHz(baseCent);
923
+ const peekFreq = this.centToHz(peekCent);
924
+ const sustainFreq = this.centToHz(sustainCent);
843
925
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
844
926
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
845
927
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
846
928
  const modDelay = startTime + voiceParams.modDelay;
847
929
  const modAttack = modDelay + voiceParams.modAttack;
848
930
  const modHold = modAttack + voiceParams.modHold;
849
- const modDecay = modHold + voiceParams.modDecay;
931
+ const decayDuration = voiceParams.modDecay;
932
+ note.adjustedBaseFreq = adjustedBaseFreq;
850
933
  note.filterNode.frequency
851
934
  .cancelScheduledValues(scheduleTime)
852
935
  .setValueAtTime(adjustedBaseFreq, startTime)
853
936
  .setValueAtTime(adjustedBaseFreq, modDelay)
854
- .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
937
+ .linearRampToValueAtTime(adjustedPeekFreq, modAttack)
855
938
  .setValueAtTime(adjustedPeekFreq, modHold)
856
- .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
939
+ .setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
857
940
  }
858
941
  startModulation(channel, note, scheduleTime) {
942
+ const audioContext = this.audioContext;
859
943
  const { voiceParams } = note;
860
- note.modulationLFO = new OscillatorNode(this.audioContext, {
944
+ note.modulationLFO = new OscillatorNode(audioContext, {
861
945
  frequency: this.centToHz(voiceParams.freqModLFO),
862
946
  });
863
- note.filterDepth = new GainNode(this.audioContext, {
947
+ note.filterDepth = new GainNode(audioContext, {
864
948
  gain: voiceParams.modLfoToFilterFc,
865
949
  });
866
- note.modulationDepth = new GainNode(this.audioContext);
950
+ note.modulationDepth = new GainNode(audioContext);
867
951
  this.setModLfoToPitch(channel, note, scheduleTime);
868
- note.volumeDepth = new GainNode(this.audioContext);
952
+ note.volumeDepth = new GainNode(audioContext);
869
953
  this.setModLfoToVolume(note, scheduleTime);
870
954
  note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
871
955
  note.modulationLFO.connect(note.filterDepth);
@@ -904,7 +988,8 @@ export class MidyGMLite {
904
988
  }
905
989
  }
906
990
  async setNoteAudioNode(channel, note, realtime) {
907
- const now = this.audioContext.currentTime;
991
+ const audioContext = this.audioContext;
992
+ const now = audioContext.currentTime;
908
993
  const { noteNumber, velocity, startTime } = note;
909
994
  const state = channel.state;
910
995
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -912,8 +997,8 @@ export class MidyGMLite {
912
997
  note.voiceParams = voiceParams;
913
998
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
914
999
  note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
915
- note.volumeEnvelopeNode = new GainNode(this.audioContext);
916
- note.filterNode = new BiquadFilterNode(this.audioContext, {
1000
+ note.volumeEnvelopeNode = new GainNode(audioContext);
1001
+ note.filterNode = new BiquadFilterNode(audioContext, {
917
1002
  type: "lowpass",
918
1003
  Q: voiceParams.initialFilterQ / 10, // dB
919
1004
  });
@@ -988,7 +1073,12 @@ export class MidyGMLite {
988
1073
  const bankTable = this.soundFontTable[programNumber];
989
1074
  if (!bankTable)
990
1075
  return;
991
- const bank = channel.isDrum ? 128 : 0;
1076
+ let bank = channel.isDrum ? 128 : 0;
1077
+ if (bankTable[bank] === undefined) {
1078
+ if (channel.isDrum)
1079
+ return;
1080
+ bank = 0;
1081
+ }
992
1082
  const soundFontIndex = bankTable[bank];
993
1083
  if (soundFontIndex === undefined)
994
1084
  return;
@@ -998,11 +1088,7 @@ export class MidyGMLite {
998
1088
  return;
999
1089
  await this.setNoteAudioNode(channel, note, realtime);
1000
1090
  this.setNoteRouting(channelNumber, note, startTime);
1001
- note.pending = false;
1002
- const off = note.offEvent;
1003
- if (off) {
1004
- this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1005
- }
1091
+ note.resolveReady();
1006
1092
  }
1007
1093
  disconnectNote(note) {
1008
1094
  note.bufferSource.disconnect();
@@ -1016,27 +1102,27 @@ export class MidyGMLite {
1016
1102
  }
1017
1103
  releaseNote(channel, note, endTime) {
1018
1104
  endTime ??= this.audioContext.currentTime;
1019
- const volRelease = endTime + note.voiceParams.volRelease;
1105
+ const duration = note.voiceParams.volRelease;
1106
+ const volRelease = endTime + duration;
1020
1107
  const modRelease = endTime + note.voiceParams.modRelease;
1021
- const stopTime = Math.min(volRelease, modRelease);
1022
1108
  note.filterNode.frequency
1023
1109
  .cancelScheduledValues(endTime)
1024
- .linearRampToValueAtTime(0, modRelease);
1110
+ .linearRampToValueAtTime(note.adjustedBaseFreq, modRelease);
1025
1111
  note.volumeEnvelopeNode.gain
1026
1112
  .cancelScheduledValues(endTime)
1027
- .linearRampToValueAtTime(0, volRelease);
1113
+ .setTargetAtTime(0, endTime, duration * releaseCurve);
1028
1114
  return new Promise((resolve) => {
1029
1115
  this.scheduleTask(() => {
1030
1116
  const bufferSource = note.bufferSource;
1031
1117
  bufferSource.loop = false;
1032
- bufferSource.stop(stopTime);
1118
+ bufferSource.stop(volRelease);
1033
1119
  this.disconnectNote(note);
1034
1120
  channel.scheduledNotes[note.index] = undefined;
1035
1121
  resolve();
1036
- }, stopTime);
1122
+ }, volRelease);
1037
1123
  });
1038
1124
  }
1039
- noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1125
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1040
1126
  const channel = this.channels[channelNumber];
1041
1127
  if (!force) {
1042
1128
  if (channel.isDrum)
@@ -1048,13 +1134,11 @@ export class MidyGMLite {
1048
1134
  if (index < 0)
1049
1135
  return;
1050
1136
  const note = channel.scheduledNotes[index];
1051
- if (note.pending) {
1052
- note.offEvent = { velocity, startTime: endTime };
1053
- return;
1054
- }
1055
1137
  note.ending = true;
1056
1138
  this.setNoteIndex(channel, index);
1057
- const promise = this.releaseNote(channel, note, endTime);
1139
+ const promise = note.ready.then(() => {
1140
+ return this.releaseNote(channel, note, endTime);
1141
+ });
1058
1142
  this.notePromises.push(promise);
1059
1143
  return promise;
1060
1144
  }
@@ -1142,7 +1226,8 @@ export class MidyGMLite {
1142
1226
  }
1143
1227
  setPitchBend(channelNumber, value, scheduleTime) {
1144
1228
  const channel = this.channels[channelNumber];
1145
- scheduleTime ??= this.audioContext.currentTime;
1229
+ if (!(0 <= scheduleTime))
1230
+ scheduleTime = this.audioContext.currentTime;
1146
1231
  const state = channel.state;
1147
1232
  const prev = state.pitchWheel * 2 - 1;
1148
1233
  const next = (value - 8192) / 8192;
@@ -1173,7 +1258,7 @@ export class MidyGMLite {
1173
1258
  }
1174
1259
  setModLfoToVolume(note, scheduleTime) {
1175
1260
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1176
- const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1261
+ const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
1177
1262
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1178
1263
  note.volumeDepth.gain
1179
1264
  .cancelScheduledValues(scheduleTime)
@@ -1306,12 +1391,14 @@ export class MidyGMLite {
1306
1391
  }
1307
1392
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1308
1393
  const channel = this.channels[channelNumber];
1309
- scheduleTime ??= this.audioContext.currentTime;
1394
+ if (!(0 <= scheduleTime))
1395
+ scheduleTime = this.audioContext.currentTime;
1310
1396
  channel.state.modulationDepth = modulation / 127;
1311
1397
  this.updateModulation(channel, scheduleTime);
1312
1398
  }
1313
1399
  setVolume(channelNumber, volume, scheduleTime) {
1314
- scheduleTime ??= this.audioContext.currentTime;
1400
+ if (!(0 <= scheduleTime))
1401
+ scheduleTime = this.audioContext.currentTime;
1315
1402
  const channel = this.channels[channelNumber];
1316
1403
  channel.state.volume = volume / 127;
1317
1404
  this.updateChannelVolume(channel, scheduleTime);
@@ -1324,13 +1411,15 @@ export class MidyGMLite {
1324
1411
  };
1325
1412
  }
1326
1413
  setPan(channelNumber, pan, scheduleTime) {
1327
- scheduleTime ??= this.audioContext.currentTime;
1414
+ if (!(0 <= scheduleTime))
1415
+ scheduleTime = this.audioContext.currentTime;
1328
1416
  const channel = this.channels[channelNumber];
1329
1417
  channel.state.pan = pan / 127;
1330
1418
  this.updateChannelVolume(channel, scheduleTime);
1331
1419
  }
1332
1420
  setExpression(channelNumber, expression, scheduleTime) {
1333
- scheduleTime ??= this.audioContext.currentTime;
1421
+ if (!(0 <= scheduleTime))
1422
+ scheduleTime = this.audioContext.currentTime;
1334
1423
  const channel = this.channels[channelNumber];
1335
1424
  channel.state.expression = expression / 127;
1336
1425
  this.updateChannelVolume(channel, scheduleTime);
@@ -1352,7 +1441,8 @@ export class MidyGMLite {
1352
1441
  }
1353
1442
  setSustainPedal(channelNumber, value, scheduleTime) {
1354
1443
  const channel = this.channels[channelNumber];
1355
- scheduleTime ??= this.audioContext.currentTime;
1444
+ if (!(0 <= scheduleTime))
1445
+ scheduleTime = this.audioContext.currentTime;
1356
1446
  channel.state.sustainPedal = value / 127;
1357
1447
  if (64 <= value) {
1358
1448
  this.processScheduledNotes(channel, (note) => {
@@ -1410,7 +1500,8 @@ export class MidyGMLite {
1410
1500
  }
1411
1501
  setPitchBendRange(channelNumber, value, scheduleTime) {
1412
1502
  const channel = this.channels[channelNumber];
1413
- scheduleTime ??= this.audioContext.currentTime;
1503
+ if (!(0 <= scheduleTime))
1504
+ scheduleTime = this.audioContext.currentTime;
1414
1505
  const state = channel.state;
1415
1506
  const prev = state.pitchWheelSensitivity;
1416
1507
  const next = value / 12800;
@@ -1420,7 +1511,8 @@ export class MidyGMLite {
1420
1511
  this.applyVoiceParams(channel, 16, scheduleTime);
1421
1512
  }
1422
1513
  allSoundOff(channelNumber, _value, scheduleTime) {
1423
- scheduleTime ??= this.audioContext.currentTime;
1514
+ if (!(0 <= scheduleTime))
1515
+ scheduleTime = this.audioContext.currentTime;
1424
1516
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
1425
1517
  }
1426
1518
  resetChannelStates(channelNumber) {
@@ -1472,7 +1564,8 @@ export class MidyGMLite {
1472
1564
  }
1473
1565
  }
1474
1566
  allNotesOff(channelNumber, _value, scheduleTime) {
1475
- scheduleTime ??= this.audioContext.currentTime;
1567
+ if (!(0 <= scheduleTime))
1568
+ scheduleTime = this.audioContext.currentTime;
1476
1569
  return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
1477
1570
  }
1478
1571
  handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
@@ -1493,14 +1586,16 @@ export class MidyGMLite {
1493
1586
  }
1494
1587
  }
1495
1588
  GM1SystemOn(scheduleTime) {
1496
- scheduleTime ??= this.audioContext.currentTime;
1589
+ const channels = this.channels;
1590
+ if (!(0 <= scheduleTime))
1591
+ scheduleTime = this.audioContext.currentTime;
1497
1592
  this.mode = "GM1";
1498
- for (let i = 0; i < this.channels.length; i++) {
1593
+ for (let i = 0; i < channels.length; i++) {
1499
1594
  this.allSoundOff(i, 0, scheduleTime);
1500
- const channel = this.channels[i];
1595
+ const channel = channels[i];
1501
1596
  channel.isDrum = false;
1502
1597
  }
1503
- this.channels[9].isDrum = true;
1598
+ channels[9].isDrum = true;
1504
1599
  }
1505
1600
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1506
1601
  switch (data[2]) {
@@ -1521,7 +1616,8 @@ export class MidyGMLite {
1521
1616
  this.setMasterVolume(volume, scheduleTime);
1522
1617
  }
1523
1618
  setMasterVolume(value, scheduleTime) {
1524
- scheduleTime ??= this.audioContext.currentTime;
1619
+ if (!(0 <= scheduleTime))
1620
+ scheduleTime = this.audioContext.currentTime;
1525
1621
  this.masterVolume.gain
1526
1622
  .cancelScheduledValues(scheduleTime)
1527
1623
  .setValueAtTime(value * value, scheduleTime);