@marmooo/midy 0.4.2 → 0.4.4

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,6 +14,12 @@ class Note {
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
+ Object.defineProperty(this, "adjustedBaseFreq", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: 20000
22
+ });
17
23
  Object.defineProperty(this, "index", {
18
24
  enumerable: true,
19
25
  configurable: true,
@@ -160,9 +166,24 @@ const pitchEnvelopeKeys = [
160
166
  "playbackRate",
161
167
  ];
162
168
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
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)));
163
174
  export class MidyGMLite extends EventTarget {
164
175
  constructor(audioContext) {
165
176
  super();
177
+ // https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
178
+ // https://pubmed.ncbi.nlm.nih.gov/12488797/
179
+ // Gap detection studies indicate humans detect temporal discontinuities
180
+ // around 2–3 ms. Smoothing over ~4 ms is perceived as continuous.
181
+ Object.defineProperty(this, "perceptualSmoothingTime", {
182
+ enumerable: true,
183
+ configurable: true,
184
+ writable: true,
185
+ value: 0.004
186
+ });
166
187
  Object.defineProperty(this, "mode", {
167
188
  enumerable: true,
168
189
  configurable: true,
@@ -277,6 +298,20 @@ export class MidyGMLite extends EventTarget {
277
298
  writable: true,
278
299
  value: false
279
300
  });
301
+ Object.defineProperty(this, "totalTimeEventTypes", {
302
+ enumerable: true,
303
+ configurable: true,
304
+ writable: true,
305
+ value: new Set([
306
+ "noteOff",
307
+ ])
308
+ });
309
+ Object.defineProperty(this, "tempo", {
310
+ enumerable: true,
311
+ configurable: true,
312
+ writable: true,
313
+ value: 1
314
+ });
280
315
  Object.defineProperty(this, "loop", {
281
316
  enumerable: true,
282
317
  configurable: true,
@@ -391,13 +426,13 @@ export class MidyGMLite extends EventTarget {
391
426
  this.totalTime = this.calcTotalTime();
392
427
  }
393
428
  cacheVoiceIds() {
394
- const timeline = this.timeline;
429
+ const { channels, timeline, voiceCounter } = this;
395
430
  for (let i = 0; i < timeline.length; i++) {
396
431
  const event = timeline[i];
397
432
  switch (event.type) {
398
433
  case "noteOn": {
399
- const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
400
- this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
434
+ const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
435
+ voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
401
436
  break;
402
437
  }
403
438
  case "controller":
@@ -412,9 +447,9 @@ export class MidyGMLite extends EventTarget {
412
447
  this.setProgramChange(event.channel, event.programNumber, event.startTime);
413
448
  }
414
449
  }
415
- for (const [audioBufferId, count] of this.voiceCounter) {
450
+ for (const [audioBufferId, count] of voiceCounter) {
416
451
  if (count === 1)
417
- this.voiceCounter.delete(audioBufferId);
452
+ voiceCounter.delete(audioBufferId);
418
453
  }
419
454
  this.GM1SystemOn();
420
455
  }
@@ -423,7 +458,12 @@ export class MidyGMLite extends EventTarget {
423
458
  const bankTable = this.soundFontTable[programNumber];
424
459
  if (!bankTable)
425
460
  return;
426
- const bank = channel.isDrum ? 128 : 0;
461
+ let bank = channel.isDrum ? 128 : 0;
462
+ if (bankTable[bank] === undefined) {
463
+ if (channel.isDrum)
464
+ return;
465
+ bank = 0;
466
+ }
427
467
  const soundFontIndex = bankTable[bank];
428
468
  if (soundFontIndex === undefined)
429
469
  return;
@@ -483,11 +523,13 @@ export class MidyGMLite extends EventTarget {
483
523
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
484
524
  const schedulingOffset = this.startDelay - timeOffset;
485
525
  const timeline = this.timeline;
526
+ const inverseTempo = 1 / this.tempo;
486
527
  while (queueIndex < timeline.length) {
487
528
  const event = timeline[queueIndex];
488
- if (lookAheadCheckTime < event.startTime)
529
+ const t = event.startTime * inverseTempo;
530
+ if (lookAheadCheckTime < t)
489
531
  break;
490
- const startTime = event.startTime + schedulingOffset;
532
+ const startTime = t + schedulingOffset;
491
533
  switch (event.type) {
492
534
  case "noteOn":
493
535
  this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
@@ -513,8 +555,10 @@ export class MidyGMLite extends EventTarget {
513
555
  return queueIndex;
514
556
  }
515
557
  getQueueIndex(second) {
516
- for (let i = 0; i < this.timeline.length; i++) {
517
- if (second <= this.timeline[i].startTime) {
558
+ const timeline = this.timeline;
559
+ const inverseTempo = 1 / this.tempo;
560
+ for (let i = 0; i < timeline.length; i++) {
561
+ if (second <= timeline[i].startTime * inverseTempo) {
518
562
  return i;
519
563
  }
520
564
  }
@@ -525,40 +569,44 @@ export class MidyGMLite extends EventTarget {
525
569
  this.drumExclusiveClassNotes.fill(undefined);
526
570
  this.voiceCache.clear();
527
571
  this.realtimeVoiceCache.clear();
528
- for (let i = 0; i < this.channels.length; i++) {
529
- this.channels[i].scheduledNotes = [];
572
+ const channels = this.channels;
573
+ for (let i = 0; i < channels.length; i++) {
574
+ channels[i].scheduledNotes = [];
530
575
  this.resetChannelStates(i);
531
576
  }
532
577
  }
533
578
  updateStates(queueIndex, nextQueueIndex) {
579
+ const { timeline, resumeTime } = this;
580
+ const inverseTempo = 1 / this.tempo;
534
581
  const now = this.audioContext.currentTime;
535
582
  if (nextQueueIndex < queueIndex)
536
583
  queueIndex = 0;
537
584
  for (let i = queueIndex; i < nextQueueIndex; i++) {
538
- const event = this.timeline[i];
585
+ const event = timeline[i];
539
586
  switch (event.type) {
540
587
  case "controller":
541
- this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
588
+ this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
542
589
  break;
543
590
  case "programChange":
544
- this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
591
+ this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
545
592
  break;
546
593
  case "pitchBend":
547
- this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
594
+ this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
548
595
  break;
549
596
  case "sysEx":
550
- this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
597
+ this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
551
598
  }
552
599
  }
553
600
  }
554
601
  async playNotes() {
555
- if (this.audioContext.state === "suspended") {
556
- await this.audioContext.resume();
602
+ const audioContext = this.audioContext;
603
+ if (audioContext.state === "suspended") {
604
+ await audioContext.resume();
557
605
  }
558
606
  const paused = this.isPaused;
559
607
  this.isPlaying = true;
560
608
  this.isPaused = false;
561
- this.startTime = this.audioContext.currentTime;
609
+ this.startTime = audioContext.currentTime;
562
610
  if (paused) {
563
611
  this.dispatchEvent(new Event("resumed"));
564
612
  }
@@ -569,42 +617,41 @@ export class MidyGMLite extends EventTarget {
569
617
  let exitReason;
570
618
  this.notePromises = [];
571
619
  while (true) {
572
- const now = this.audioContext.currentTime;
573
- if (this.timeline.length <= queueIndex) {
620
+ const now = audioContext.currentTime;
621
+ if (this.totalTime < this.currentTime() ||
622
+ this.timeline.length <= queueIndex) {
574
623
  await this.stopNotes(0, true, now);
575
624
  if (this.loop) {
576
- this.notePromises = [];
577
625
  this.resetAllStates();
578
- this.startTime = this.audioContext.currentTime;
626
+ this.startTime = audioContext.currentTime;
579
627
  this.resumeTime = 0;
580
628
  queueIndex = 0;
581
629
  this.dispatchEvent(new Event("looped"));
582
630
  continue;
583
631
  }
584
632
  else {
585
- await this.audioContext.suspend();
633
+ await audioContext.suspend();
586
634
  exitReason = "ended";
587
635
  break;
588
636
  }
589
637
  }
590
638
  if (this.isPausing) {
591
639
  await this.stopNotes(0, true, now);
592
- await this.audioContext.suspend();
593
- this.notePromises = [];
640
+ await audioContext.suspend();
594
641
  this.isPausing = false;
595
642
  exitReason = "paused";
596
643
  break;
597
644
  }
598
645
  else if (this.isStopping) {
599
646
  await this.stopNotes(0, true, now);
600
- await this.audioContext.suspend();
647
+ await audioContext.suspend();
601
648
  this.isStopping = false;
602
649
  exitReason = "stopped";
603
650
  break;
604
651
  }
605
652
  else if (this.isSeeking) {
606
653
  this.stopNotes(0, true, now);
607
- this.startTime = this.audioContext.currentTime;
654
+ this.startTime = audioContext.currentTime;
608
655
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
609
656
  this.updateStates(queueIndex, nextQueueIndex);
610
657
  queueIndex = nextQueueIndex;
@@ -617,7 +664,6 @@ export class MidyGMLite extends EventTarget {
617
664
  await this.scheduleTask(() => { }, waitTime);
618
665
  }
619
666
  if (exitReason !== "paused") {
620
- this.notePromises = [];
621
667
  this.resetAllStates();
622
668
  }
623
669
  this.isPlaying = false;
@@ -715,11 +761,13 @@ export class MidyGMLite extends EventTarget {
715
761
  return Promise.all(promises);
716
762
  }
717
763
  stopNotes(velocity, force, scheduleTime) {
718
- const promises = [];
719
- for (let i = 0; i < this.channels.length; i++) {
720
- promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
764
+ const channels = this.channels;
765
+ for (let i = 0; i < channels.length; i++) {
766
+ this.stopChannelNotes(i, velocity, force, scheduleTime);
721
767
  }
722
- return Promise.all(this.notePromises);
768
+ const stopPromise = Promise.all(this.notePromises);
769
+ this.notePromises = [];
770
+ return stopPromise;
723
771
  }
724
772
  async start() {
725
773
  if (this.isPlaying || this.isPaused)
@@ -756,12 +804,25 @@ export class MidyGMLite extends EventTarget {
756
804
  this.isSeeking = true;
757
805
  }
758
806
  }
807
+ tempoChange(tempo) {
808
+ const timeScale = this.tempo / tempo;
809
+ this.resumeTime = this.resumeTime * timeScale;
810
+ this.tempo = tempo;
811
+ this.totalTime = this.calcTotalTime();
812
+ this.seekTo(this.currentTime() * timeScale);
813
+ }
759
814
  calcTotalTime() {
815
+ const totalTimeEventTypes = this.totalTimeEventTypes;
816
+ const timeline = this.timeline;
817
+ const inverseTempo = 1 / this.tempo;
760
818
  let totalTime = 0;
761
- for (let i = 0; i < this.timeline.length; i++) {
762
- const event = this.timeline[i];
763
- if (totalTime < event.startTime)
764
- totalTime = event.startTime;
819
+ for (let i = 0; i < timeline.length; i++) {
820
+ const event = timeline[i];
821
+ if (!totalTimeEventTypes.has(event.type))
822
+ continue;
823
+ const t = event.startTime * inverseTempo;
824
+ if (totalTime < t)
825
+ totalTime = t;
765
826
  }
766
827
  return totalTime + this.startDelay;
767
828
  }
@@ -801,9 +862,6 @@ export class MidyGMLite extends EventTarget {
801
862
  }
802
863
  await Promise.all(tasks);
803
864
  }
804
- cbToRatio(cb) {
805
- return Math.pow(10, cb / 200);
806
- }
807
865
  rateToCent(rate) {
808
866
  return 1200 * Math.log2(rate);
809
867
  }
@@ -820,51 +878,57 @@ export class MidyGMLite extends EventTarget {
820
878
  }
821
879
  updateChannelDetune(channel, scheduleTime) {
822
880
  this.processScheduledNotes(channel, (note) => {
823
- this.updateDetune(channel, note, scheduleTime);
881
+ this.setDetune(channel, note, scheduleTime);
824
882
  });
825
883
  }
826
- updateDetune(channel, note, scheduleTime) {
827
- note.bufferSource.detune
828
- .cancelScheduledValues(scheduleTime)
829
- .setValueAtTime(channel.detune, scheduleTime);
884
+ calcNoteDetune(channel, note) {
885
+ return channel.detune + note.voiceParams.detune;
830
886
  }
831
887
  setVolumeEnvelope(note, scheduleTime) {
832
888
  const { voiceParams, startTime } = note;
833
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
889
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
834
890
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
835
891
  const volDelay = startTime + voiceParams.volDelay;
836
892
  const volAttack = volDelay + voiceParams.volAttack;
837
893
  const volHold = volAttack + voiceParams.volHold;
838
- const volDecay = volHold + voiceParams.volDecay;
894
+ const decayDuration = voiceParams.volDecay;
839
895
  note.volumeEnvelopeNode.gain
840
896
  .cancelScheduledValues(scheduleTime)
841
897
  .setValueAtTime(0, startTime)
842
- .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
898
+ .setValueAtTime(1e-6, volDelay)
843
899
  .exponentialRampToValueAtTime(attackVolume, volAttack)
844
900
  .setValueAtTime(attackVolume, volHold)
845
- .linearRampToValueAtTime(sustainVolume, volDecay);
901
+ .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
902
+ }
903
+ setDetune(channel, note, scheduleTime) {
904
+ const detune = this.calcNoteDetune(channel, note);
905
+ note.bufferSource.detune
906
+ .cancelScheduledValues(scheduleTime)
907
+ .setValueAtTime(detune, scheduleTime);
908
+ const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
909
+ note.bufferSource.detune
910
+ .cancelAndHoldAtTime(scheduleTime)
911
+ .setTargetAtTime(detune, scheduleTime, timeConstant);
846
912
  }
847
913
  setPitchEnvelope(note, scheduleTime) {
848
- const { voiceParams } = note;
914
+ const { bufferSource, voiceParams } = note;
849
915
  const baseRate = voiceParams.playbackRate;
850
- note.bufferSource.playbackRate
916
+ bufferSource.playbackRate
851
917
  .cancelScheduledValues(scheduleTime)
852
918
  .setValueAtTime(baseRate, scheduleTime);
853
919
  const modEnvToPitch = voiceParams.modEnvToPitch;
854
920
  if (modEnvToPitch === 0)
855
921
  return;
856
- const basePitch = this.rateToCent(baseRate);
857
- const peekPitch = basePitch + modEnvToPitch;
858
- const peekRate = this.centToRate(peekPitch);
922
+ const peekRate = baseRate * this.centToRate(modEnvToPitch);
859
923
  const modDelay = note.startTime + voiceParams.modDelay;
860
924
  const modAttack = modDelay + voiceParams.modAttack;
861
925
  const modHold = modAttack + voiceParams.modHold;
862
- const modDecay = modHold + voiceParams.modDecay;
863
- note.bufferSource.playbackRate
926
+ const decayDuration = voiceParams.modDecay;
927
+ bufferSource.playbackRate
864
928
  .setValueAtTime(baseRate, modDelay)
865
929
  .exponentialRampToValueAtTime(peekRate, modAttack)
866
930
  .setValueAtTime(peekRate, modHold)
867
- .linearRampToValueAtTime(baseRate, modDecay);
931
+ .setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
868
932
  }
869
933
  clampCutoffFrequency(frequency) {
870
934
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -873,36 +937,42 @@ export class MidyGMLite extends EventTarget {
873
937
  }
874
938
  setFilterEnvelope(note, scheduleTime) {
875
939
  const { voiceParams, startTime } = note;
876
- const baseFreq = this.centToHz(voiceParams.initialFilterFc);
877
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
878
- const sustainFreq = baseFreq +
879
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
940
+ const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
941
+ const baseCent = voiceParams.initialFilterFc;
942
+ const peekCent = baseCent + modEnvToFilterFc;
943
+ const sustainCent = baseCent +
944
+ modEnvToFilterFc * (1 - voiceParams.modSustain);
945
+ const baseFreq = this.centToHz(baseCent);
946
+ const peekFreq = this.centToHz(peekCent);
947
+ const sustainFreq = this.centToHz(sustainCent);
880
948
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
881
949
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
882
950
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
883
951
  const modDelay = startTime + voiceParams.modDelay;
884
952
  const modAttack = modDelay + voiceParams.modAttack;
885
953
  const modHold = modAttack + voiceParams.modHold;
886
- const modDecay = modHold + voiceParams.modDecay;
954
+ const decayDuration = voiceParams.modDecay;
955
+ note.adjustedBaseFreq = adjustedBaseFreq;
887
956
  note.filterNode.frequency
888
957
  .cancelScheduledValues(scheduleTime)
889
958
  .setValueAtTime(adjustedBaseFreq, startTime)
890
959
  .setValueAtTime(adjustedBaseFreq, modDelay)
891
960
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
892
961
  .setValueAtTime(adjustedPeekFreq, modHold)
893
- .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
962
+ .setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
894
963
  }
895
964
  startModulation(channel, note, scheduleTime) {
965
+ const audioContext = this.audioContext;
896
966
  const { voiceParams } = note;
897
- note.modulationLFO = new OscillatorNode(this.audioContext, {
967
+ note.modulationLFO = new OscillatorNode(audioContext, {
898
968
  frequency: this.centToHz(voiceParams.freqModLFO),
899
969
  });
900
- note.filterDepth = new GainNode(this.audioContext, {
970
+ note.filterDepth = new GainNode(audioContext, {
901
971
  gain: voiceParams.modLfoToFilterFc,
902
972
  });
903
- note.modulationDepth = new GainNode(this.audioContext);
973
+ note.modulationDepth = new GainNode(audioContext);
904
974
  this.setModLfoToPitch(channel, note, scheduleTime);
905
- note.volumeDepth = new GainNode(this.audioContext);
975
+ note.volumeDepth = new GainNode(audioContext);
906
976
  this.setModLfoToVolume(note, scheduleTime);
907
977
  note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
908
978
  note.modulationLFO.connect(note.filterDepth);
@@ -941,7 +1011,8 @@ export class MidyGMLite extends EventTarget {
941
1011
  }
942
1012
  }
943
1013
  async setNoteAudioNode(channel, note, realtime) {
944
- const now = this.audioContext.currentTime;
1014
+ const audioContext = this.audioContext;
1015
+ const now = audioContext.currentTime;
945
1016
  const { noteNumber, velocity, startTime } = note;
946
1017
  const state = channel.state;
947
1018
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -949,15 +1020,15 @@ export class MidyGMLite extends EventTarget {
949
1020
  note.voiceParams = voiceParams;
950
1021
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
951
1022
  note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
952
- note.volumeEnvelopeNode = new GainNode(this.audioContext);
953
- note.filterNode = new BiquadFilterNode(this.audioContext, {
1023
+ note.volumeEnvelopeNode = new GainNode(audioContext);
1024
+ note.filterNode = new BiquadFilterNode(audioContext, {
954
1025
  type: "lowpass",
955
1026
  Q: voiceParams.initialFilterQ / 10, // dB
956
1027
  });
957
1028
  this.setVolumeEnvelope(note, now);
958
1029
  this.setFilterEnvelope(note, now);
959
1030
  this.setPitchEnvelope(note, now);
960
- this.updateDetune(channel, note, now);
1031
+ this.setDetune(channel, note, now);
961
1032
  if (0 < state.modulationDepthMSB) {
962
1033
  this.startModulation(channel, note, now);
963
1034
  }
@@ -1025,7 +1096,12 @@ export class MidyGMLite extends EventTarget {
1025
1096
  const bankTable = this.soundFontTable[programNumber];
1026
1097
  if (!bankTable)
1027
1098
  return;
1028
- const bank = channel.isDrum ? 128 : 0;
1099
+ let bank = channel.isDrum ? 128 : 0;
1100
+ if (bankTable[bank] === undefined) {
1101
+ if (channel.isDrum)
1102
+ return;
1103
+ bank = 0;
1104
+ }
1029
1105
  const soundFontIndex = bankTable[bank];
1030
1106
  if (soundFontIndex === undefined)
1031
1107
  return;
@@ -1049,27 +1125,26 @@ export class MidyGMLite extends EventTarget {
1049
1125
  }
1050
1126
  releaseNote(channel, note, endTime) {
1051
1127
  endTime ??= this.audioContext.currentTime;
1052
- const volRelease = endTime + note.voiceParams.volRelease;
1053
- const modRelease = endTime + note.voiceParams.modRelease;
1054
- const stopTime = Math.min(volRelease, modRelease);
1128
+ const volDuration = note.voiceParams.volRelease;
1129
+ const volRelease = endTime + volDuration;
1055
1130
  note.filterNode.frequency
1056
1131
  .cancelScheduledValues(endTime)
1057
- .linearRampToValueAtTime(0, modRelease);
1132
+ .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
1058
1133
  note.volumeEnvelopeNode.gain
1059
1134
  .cancelScheduledValues(endTime)
1060
- .linearRampToValueAtTime(0, volRelease);
1135
+ .setTargetAtTime(0, endTime, volDuration * releaseCurve);
1061
1136
  return new Promise((resolve) => {
1062
1137
  this.scheduleTask(() => {
1063
1138
  const bufferSource = note.bufferSource;
1064
1139
  bufferSource.loop = false;
1065
- bufferSource.stop(stopTime);
1140
+ bufferSource.stop(volRelease);
1066
1141
  this.disconnectNote(note);
1067
1142
  channel.scheduledNotes[note.index] = undefined;
1068
1143
  resolve();
1069
- }, stopTime);
1144
+ }, volRelease);
1070
1145
  });
1071
1146
  }
1072
- async noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1147
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1073
1148
  const channel = this.channels[channelNumber];
1074
1149
  if (!force) {
1075
1150
  if (channel.isDrum)
@@ -1205,7 +1280,7 @@ export class MidyGMLite extends EventTarget {
1205
1280
  }
1206
1281
  setModLfoToVolume(note, scheduleTime) {
1207
1282
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1208
- const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1283
+ const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
1209
1284
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1210
1285
  note.volumeDepth.gain
1211
1286
  .cancelScheduledValues(scheduleTime)
@@ -1251,12 +1326,15 @@ export class MidyGMLite extends EventTarget {
1251
1326
  }
1252
1327
  },
1253
1328
  freqModLFO: (_channel, note, scheduleTime) => {
1254
- if (0 < channel.state.modulationDepth) {
1329
+ if (0 < channel.state.modulationDepthMSB) {
1255
1330
  this.setFreqModLFO(note, scheduleTime);
1256
1331
  }
1257
1332
  },
1258
1333
  delayVibLFO: (_channel, _note, _scheduleTime) => { },
1259
1334
  freqVibLFO: (_channel, _note, _scheduleTime) => { },
1335
+ detune: (channel, note, scheduleTime) => {
1336
+ this.setDetune(channel, note, scheduleTime);
1337
+ },
1260
1338
  };
1261
1339
  }
1262
1340
  getControllerState(channel, noteNumber, velocity) {
@@ -1326,7 +1404,8 @@ export class MidyGMLite extends EventTarget {
1326
1404
  }
1327
1405
  }
1328
1406
  updateModulation(channel, scheduleTime) {
1329
- const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1407
+ const depth = channel.state.modulationDepthMSB *
1408
+ channel.modulationDepthRange;
1330
1409
  this.processScheduledNotes(channel, (note) => {
1331
1410
  if (note.modulationDepth) {
1332
1411
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
@@ -1336,18 +1415,18 @@ export class MidyGMLite extends EventTarget {
1336
1415
  }
1337
1416
  });
1338
1417
  }
1339
- setModulationDepth(channelNumber, modulation, scheduleTime) {
1418
+ setModulationDepth(channelNumber, value, scheduleTime) {
1340
1419
  const channel = this.channels[channelNumber];
1341
1420
  if (!(0 <= scheduleTime))
1342
1421
  scheduleTime = this.audioContext.currentTime;
1343
- channel.state.modulationDepth = modulation / 127;
1422
+ channel.state.modulationDepthMSB = value / 127;
1344
1423
  this.updateModulation(channel, scheduleTime);
1345
1424
  }
1346
- setVolume(channelNumber, volume, scheduleTime) {
1425
+ setVolume(channelNumber, value, scheduleTime) {
1347
1426
  if (!(0 <= scheduleTime))
1348
1427
  scheduleTime = this.audioContext.currentTime;
1349
1428
  const channel = this.channels[channelNumber];
1350
- channel.state.volume = volume / 127;
1429
+ channel.state.volumeMSB = value / 127;
1351
1430
  this.updateChannelVolume(channel, scheduleTime);
1352
1431
  }
1353
1432
  panToGain(pan) {
@@ -1357,18 +1436,18 @@ export class MidyGMLite extends EventTarget {
1357
1436
  gainRight: Math.sin(theta),
1358
1437
  };
1359
1438
  }
1360
- setPan(channelNumber, pan, scheduleTime) {
1439
+ setPan(channelNumber, value, scheduleTime) {
1361
1440
  if (!(0 <= scheduleTime))
1362
1441
  scheduleTime = this.audioContext.currentTime;
1363
1442
  const channel = this.channels[channelNumber];
1364
- channel.state.pan = pan / 127;
1443
+ channel.state.panMSB = value / 127;
1365
1444
  this.updateChannelVolume(channel, scheduleTime);
1366
1445
  }
1367
- setExpression(channelNumber, expression, scheduleTime) {
1446
+ setExpression(channelNumber, value, scheduleTime) {
1368
1447
  if (!(0 <= scheduleTime))
1369
1448
  scheduleTime = this.audioContext.currentTime;
1370
1449
  const channel = this.channels[channelNumber];
1371
- channel.state.expression = expression / 127;
1450
+ channel.state.expressionMSB = value / 127;
1372
1451
  this.updateChannelVolume(channel, scheduleTime);
1373
1452
  }
1374
1453
  dataEntryLSB(channelNumber, value, scheduleTime) {
@@ -1377,14 +1456,14 @@ export class MidyGMLite extends EventTarget {
1377
1456
  }
1378
1457
  updateChannelVolume(channel, scheduleTime) {
1379
1458
  const state = channel.state;
1380
- const volume = state.volume * state.expression;
1381
- const { gainLeft, gainRight } = this.panToGain(state.pan);
1459
+ const gain = state.volumeMSB * state.expressionMSB;
1460
+ const { gainLeft, gainRight } = this.panToGain(state.panMSB);
1382
1461
  channel.gainL.gain
1383
1462
  .cancelScheduledValues(scheduleTime)
1384
- .setValueAtTime(volume * gainLeft, scheduleTime);
1463
+ .setValueAtTime(gain * gainLeft, scheduleTime);
1385
1464
  channel.gainR.gain
1386
1465
  .cancelScheduledValues(scheduleTime)
1387
- .setValueAtTime(volume * gainRight, scheduleTime);
1466
+ .setValueAtTime(gain * gainRight, scheduleTime);
1388
1467
  }
1389
1468
  setSustainPedal(channelNumber, value, scheduleTime) {
1390
1469
  const channel = this.channels[channelNumber];
@@ -1425,6 +1504,8 @@ export class MidyGMLite extends EventTarget {
1425
1504
  case 0:
1426
1505
  this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1427
1506
  break;
1507
+ case 16383: // NULL
1508
+ break;
1428
1509
  default:
1429
1510
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1430
1511
  }
@@ -1533,15 +1614,16 @@ export class MidyGMLite extends EventTarget {
1533
1614
  }
1534
1615
  }
1535
1616
  GM1SystemOn(scheduleTime) {
1617
+ const channels = this.channels;
1536
1618
  if (!(0 <= scheduleTime))
1537
1619
  scheduleTime = this.audioContext.currentTime;
1538
1620
  this.mode = "GM1";
1539
- for (let i = 0; i < this.channels.length; i++) {
1621
+ for (let i = 0; i < channels.length; i++) {
1540
1622
  this.allSoundOff(i, 0, scheduleTime);
1541
- const channel = this.channels[i];
1623
+ const channel = channels[i];
1542
1624
  channel.isDrum = false;
1543
1625
  }
1544
- this.channels[9].isDrum = true;
1626
+ channels[9].isDrum = true;
1545
1627
  }
1546
1628
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1547
1629
  switch (data[2]) {