@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.
package/esm/midy-GM1.js CHANGED
@@ -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
  // normalized to 0-1 for use with the SF2 modulator model
@@ -150,8 +153,14 @@ const pitchEnvelopeKeys = [
150
153
  "playbackRate",
151
154
  ];
152
155
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
153
- export class MidyGM1 {
156
+ function cbToRatio(cb) {
157
+ return Math.pow(10, cb / 200);
158
+ }
159
+ const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
160
+ const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
161
+ export class MidyGM1 extends EventTarget {
154
162
  constructor(audioContext) {
163
+ super();
155
164
  Object.defineProperty(this, "mode", {
156
165
  enumerable: true,
157
166
  configurable: true,
@@ -266,6 +275,26 @@ export class MidyGM1 {
266
275
  writable: true,
267
276
  value: false
268
277
  });
278
+ Object.defineProperty(this, "totalTimeEventTypes", {
279
+ enumerable: true,
280
+ configurable: true,
281
+ writable: true,
282
+ value: new Set([
283
+ "noteOff",
284
+ ])
285
+ });
286
+ Object.defineProperty(this, "tempo", {
287
+ enumerable: true,
288
+ configurable: true,
289
+ writable: true,
290
+ value: 1
291
+ });
292
+ Object.defineProperty(this, "loop", {
293
+ enumerable: true,
294
+ configurable: true,
295
+ writable: true,
296
+ value: false
297
+ });
269
298
  Object.defineProperty(this, "playPromise", {
270
299
  enumerable: true,
271
300
  configurable: true,
@@ -368,13 +397,13 @@ export class MidyGM1 {
368
397
  this.totalTime = this.calcTotalTime();
369
398
  }
370
399
  cacheVoiceIds() {
371
- const timeline = this.timeline;
400
+ const { channels, timeline, voiceCounter } = this;
372
401
  for (let i = 0; i < timeline.length; i++) {
373
402
  const event = timeline[i];
374
403
  switch (event.type) {
375
404
  case "noteOn": {
376
- const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
377
- this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
405
+ const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
406
+ voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
378
407
  break;
379
408
  }
380
409
  case "controller":
@@ -389,9 +418,9 @@ export class MidyGM1 {
389
418
  this.setProgramChange(event.channel, event.programNumber, event.startTime);
390
419
  }
391
420
  }
392
- for (const [audioBufferId, count] of this.voiceCounter) {
421
+ for (const [audioBufferId, count] of voiceCounter) {
393
422
  if (count === 1)
394
- this.voiceCounter.delete(audioBufferId);
423
+ voiceCounter.delete(audioBufferId);
395
424
  }
396
425
  this.GM1SystemOn();
397
426
  }
@@ -400,7 +429,12 @@ export class MidyGM1 {
400
429
  const bankTable = this.soundFontTable[programNumber];
401
430
  if (!bankTable)
402
431
  return;
403
- const bank = channel.isDrum ? 128 : 0;
432
+ let bank = channel.isDrum ? 128 : 0;
433
+ if (bankTable[bank] === undefined) {
434
+ if (channel.isDrum)
435
+ return;
436
+ bank = 0;
437
+ }
404
438
  const soundFontIndex = bankTable[bank];
405
439
  if (soundFontIndex === undefined)
406
440
  return;
@@ -453,19 +487,21 @@ export class MidyGM1 {
453
487
  }
454
488
  return bufferSource;
455
489
  }
456
- async scheduleTimelineEvents(scheduleTime, queueIndex) {
490
+ scheduleTimelineEvents(scheduleTime, queueIndex) {
457
491
  const timeOffset = this.resumeTime - this.startTime;
458
492
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
459
493
  const schedulingOffset = this.startDelay - timeOffset;
460
494
  const timeline = this.timeline;
495
+ const inverseTempo = 1 / this.tempo;
461
496
  while (queueIndex < timeline.length) {
462
497
  const event = timeline[queueIndex];
463
- if (lookAheadCheckTime < event.startTime)
498
+ const t = event.startTime * inverseTempo;
499
+ if (lookAheadCheckTime < t)
464
500
  break;
465
- const startTime = event.startTime + schedulingOffset;
501
+ const startTime = t + schedulingOffset;
466
502
  switch (event.type) {
467
503
  case "noteOn":
468
- await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
504
+ this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
469
505
  break;
470
506
  case "noteOff": {
471
507
  this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
@@ -488,8 +524,10 @@ export class MidyGM1 {
488
524
  return queueIndex;
489
525
  }
490
526
  getQueueIndex(second) {
491
- for (let i = 0; i < this.timeline.length; i++) {
492
- if (second <= this.timeline[i].startTime) {
527
+ const timeline = this.timeline;
528
+ const inverseTempo = 1 / this.tempo;
529
+ for (let i = 0; i < timeline.length; i++) {
530
+ if (second <= timeline[i].startTime * inverseTempo) {
493
531
  return i;
494
532
  }
495
533
  }
@@ -500,79 +538,112 @@ export class MidyGM1 {
500
538
  this.drumExclusiveClassNotes.fill(undefined);
501
539
  this.voiceCache.clear();
502
540
  this.realtimeVoiceCache.clear();
503
- for (let i = 0; i < this.channels.length; i++) {
504
- this.channels[i].scheduledNotes = [];
541
+ const channels = this.channels;
542
+ for (let i = 0; i < channels.length; i++) {
543
+ channels[i].scheduledNotes = [];
505
544
  this.resetChannelStates(i);
506
545
  }
507
546
  }
508
547
  updateStates(queueIndex, nextQueueIndex) {
548
+ const { timeline, resumeTime } = this;
549
+ const inverseTempo = 1 / this.tempo;
550
+ const now = this.audioContext.currentTime;
509
551
  if (nextQueueIndex < queueIndex)
510
552
  queueIndex = 0;
511
553
  for (let i = queueIndex; i < nextQueueIndex; i++) {
512
- const event = this.timeline[i];
554
+ const event = timeline[i];
513
555
  switch (event.type) {
514
556
  case "controller":
515
- this.setControlChange(event.channel, event.controllerType, event.value, 0);
557
+ this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
516
558
  break;
517
559
  case "programChange":
518
- this.setProgramChange(event.channel, event.programNumber, 0);
560
+ this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
519
561
  break;
520
562
  case "pitchBend":
521
- this.setPitchBend(event.channel, event.value + 8192, 0);
563
+ this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
522
564
  break;
523
565
  case "sysEx":
524
- this.handleSysEx(event.data, 0);
566
+ this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
525
567
  }
526
568
  }
527
569
  }
528
570
  async playNotes() {
529
- if (this.audioContext.state === "suspended") {
530
- await this.audioContext.resume();
571
+ const audioContext = this.audioContext;
572
+ if (audioContext.state === "suspended") {
573
+ await audioContext.resume();
531
574
  }
575
+ const paused = this.isPaused;
532
576
  this.isPlaying = true;
533
577
  this.isPaused = false;
534
- this.startTime = this.audioContext.currentTime;
578
+ this.startTime = audioContext.currentTime;
579
+ if (paused) {
580
+ this.dispatchEvent(new Event("resumed"));
581
+ }
582
+ else {
583
+ this.dispatchEvent(new Event("started"));
584
+ }
535
585
  let queueIndex = this.getQueueIndex(this.resumeTime);
536
- let finished = false;
586
+ let exitReason;
537
587
  this.notePromises = [];
538
- while (queueIndex < this.timeline.length) {
539
- const now = this.audioContext.currentTime;
588
+ while (true) {
589
+ const now = audioContext.currentTime;
590
+ if (this.totalTime < this.currentTime() ||
591
+ this.timeline.length <= queueIndex) {
592
+ await this.stopNotes(0, true, now);
593
+ if (this.loop) {
594
+ this.resetAllStates();
595
+ this.startTime = audioContext.currentTime;
596
+ this.resumeTime = 0;
597
+ queueIndex = 0;
598
+ this.dispatchEvent(new Event("looped"));
599
+ continue;
600
+ }
601
+ else {
602
+ await audioContext.suspend();
603
+ exitReason = "ended";
604
+ break;
605
+ }
606
+ }
540
607
  if (this.isPausing) {
541
608
  await this.stopNotes(0, true, now);
542
- await this.audioContext.suspend();
543
- this.notePromises = [];
609
+ await audioContext.suspend();
610
+ this.isPausing = false;
611
+ exitReason = "paused";
544
612
  break;
545
613
  }
546
614
  else if (this.isStopping) {
547
615
  await this.stopNotes(0, true, now);
548
- await this.audioContext.suspend();
549
- finished = true;
616
+ await audioContext.suspend();
617
+ this.isStopping = false;
618
+ exitReason = "stopped";
550
619
  break;
551
620
  }
552
621
  else if (this.isSeeking) {
553
- await this.stopNotes(0, true, now);
554
- this.startTime = this.audioContext.currentTime;
622
+ this.stopNotes(0, true, now);
623
+ this.startTime = audioContext.currentTime;
555
624
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
556
625
  this.updateStates(queueIndex, nextQueueIndex);
557
626
  queueIndex = nextQueueIndex;
558
627
  this.isSeeking = false;
628
+ this.dispatchEvent(new Event("seeked"));
559
629
  continue;
560
630
  }
561
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
631
+ queueIndex = this.scheduleTimelineEvents(now, queueIndex);
562
632
  const waitTime = now + this.noteCheckInterval;
563
633
  await this.scheduleTask(() => { }, waitTime);
564
634
  }
565
- if (this.timeline.length <= queueIndex) {
566
- const now = this.audioContext.currentTime;
567
- await this.stopNotes(0, true, now);
568
- await this.audioContext.suspend();
569
- finished = true;
570
- }
571
- if (finished) {
572
- this.notePromises = [];
635
+ if (exitReason !== "paused") {
573
636
  this.resetAllStates();
574
637
  }
575
638
  this.isPlaying = false;
639
+ if (exitReason === "paused") {
640
+ this.isPaused = true;
641
+ this.dispatchEvent(new Event("paused"));
642
+ }
643
+ else {
644
+ this.isPaused = false;
645
+ this.dispatchEvent(new Event(exitReason));
646
+ }
576
647
  }
577
648
  ticksToSecond(ticks, secondsPerBeat) {
578
649
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -659,11 +730,13 @@ export class MidyGM1 {
659
730
  return Promise.all(promises);
660
731
  }
661
732
  stopNotes(velocity, force, scheduleTime) {
662
- const promises = [];
663
- for (let i = 0; i < this.channels.length; i++) {
664
- promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
733
+ const channels = this.channels;
734
+ for (let i = 0; i < channels.length; i++) {
735
+ this.stopChannelNotes(i, velocity, force, scheduleTime);
665
736
  }
666
- return Promise.all(this.notePromises);
737
+ const stopPromise = Promise.all(this.notePromises);
738
+ this.notePromises = [];
739
+ return stopPromise;
667
740
  }
668
741
  async start() {
669
742
  if (this.isPlaying || this.isPaused)
@@ -679,24 +752,20 @@ export class MidyGM1 {
679
752
  return;
680
753
  this.isStopping = true;
681
754
  await this.playPromise;
682
- this.isStopping = false;
683
755
  }
684
756
  async pause() {
685
757
  if (!this.isPlaying || this.isPaused)
686
758
  return;
687
759
  const now = this.audioContext.currentTime;
688
- this.resumeTime = now - this.startTime - this.startDelay;
760
+ this.resumeTime = now + this.resumeTime - this.startTime;
689
761
  this.isPausing = true;
690
762
  await this.playPromise;
691
- this.isPausing = false;
692
- this.isPaused = true;
693
763
  }
694
764
  async resume() {
695
765
  if (!this.isPaused)
696
766
  return;
697
767
  this.playPromise = this.playNotes();
698
768
  await this.playPromise;
699
- this.isPaused = false;
700
769
  }
701
770
  seekTo(second) {
702
771
  this.resumeTime = second;
@@ -705,11 +774,17 @@ export class MidyGM1 {
705
774
  }
706
775
  }
707
776
  calcTotalTime() {
777
+ const totalTimeEventTypes = this.totalTimeEventTypes;
778
+ const timeline = this.timeline;
779
+ const inverseTempo = 1 / this.tempo;
708
780
  let totalTime = 0;
709
- for (let i = 0; i < this.timeline.length; i++) {
710
- const event = this.timeline[i];
711
- if (totalTime < event.startTime)
712
- totalTime = event.startTime;
781
+ for (let i = 0; i < timeline.length; i++) {
782
+ const event = timeline[i];
783
+ if (!totalTimeEventTypes.has(event.type))
784
+ continue;
785
+ const t = event.startTime * inverseTempo;
786
+ if (totalTime < t)
787
+ totalTime = t;
713
788
  }
714
789
  return totalTime + this.startDelay;
715
790
  }
@@ -719,19 +794,23 @@ export class MidyGM1 {
719
794
  const now = this.audioContext.currentTime;
720
795
  return now + this.resumeTime - this.startTime;
721
796
  }
722
- processScheduledNotes(channel, callback) {
797
+ async processScheduledNotes(channel, callback) {
723
798
  const scheduledNotes = channel.scheduledNotes;
799
+ const tasks = [];
724
800
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
725
801
  const note = scheduledNotes[i];
726
802
  if (!note)
727
803
  continue;
728
804
  if (note.ending)
729
805
  continue;
730
- callback(note);
806
+ const task = note.ready.then(() => callback(note));
807
+ tasks.push(task);
731
808
  }
809
+ await Promise.all(tasks);
732
810
  }
733
- processActiveNotes(channel, scheduleTime, callback) {
811
+ async processActiveNotes(channel, scheduleTime, callback) {
734
812
  const scheduledNotes = channel.scheduledNotes;
813
+ const tasks = [];
735
814
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
736
815
  const note = scheduledNotes[i];
737
816
  if (!note)
@@ -740,11 +819,10 @@ export class MidyGM1 {
740
819
  continue;
741
820
  if (scheduleTime < note.startTime)
742
821
  break;
743
- callback(note);
822
+ const task = note.ready.then(() => callback(note));
823
+ tasks.push(task);
744
824
  }
745
- }
746
- cbToRatio(cb) {
747
- return Math.pow(10, cb / 200);
825
+ await Promise.all(tasks);
748
826
  }
749
827
  rateToCent(rate) {
750
828
  return 1200 * Math.log2(rate);
@@ -774,26 +852,26 @@ export class MidyGM1 {
774
852
  }
775
853
  setVolumeEnvelope(note, scheduleTime) {
776
854
  const { voiceParams, startTime } = note;
777
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
855
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
778
856
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
779
857
  const volDelay = startTime + voiceParams.volDelay;
780
858
  const volAttack = volDelay + voiceParams.volAttack;
781
859
  const volHold = volAttack + voiceParams.volHold;
782
- const volDecay = volHold + voiceParams.volDecay;
860
+ const decayDuration = voiceParams.volDecay;
783
861
  note.volumeEnvelopeNode.gain
784
862
  .cancelScheduledValues(scheduleTime)
785
863
  .setValueAtTime(0, startTime)
786
- .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
787
- .exponentialRampToValueAtTime(attackVolume, volAttack)
864
+ .setValueAtTime(0, volDelay)
865
+ .linearRampToValueAtTime(attackVolume, volAttack)
788
866
  .setValueAtTime(attackVolume, volHold)
789
- .linearRampToValueAtTime(sustainVolume, volDecay);
867
+ .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
790
868
  }
791
869
  setPitchEnvelope(note, scheduleTime) {
792
870
  const { voiceParams } = note;
793
871
  const baseRate = voiceParams.playbackRate;
794
872
  note.bufferSource.playbackRate
795
873
  .cancelScheduledValues(scheduleTime)
796
- .setValueAtTime(baseRate, scheduleTime);
874
+ .setValueAtTime(baseRate, note.startTime);
797
875
  const modEnvToPitch = voiceParams.modEnvToPitch;
798
876
  if (modEnvToPitch === 0)
799
877
  return;
@@ -803,12 +881,12 @@ export class MidyGM1 {
803
881
  const modDelay = note.startTime + voiceParams.modDelay;
804
882
  const modAttack = modDelay + voiceParams.modAttack;
805
883
  const modHold = modAttack + voiceParams.modHold;
806
- const modDecay = modHold + voiceParams.modDecay;
884
+ const decayDuration = voiceParams.modDecay;
807
885
  note.bufferSource.playbackRate
808
886
  .setValueAtTime(baseRate, modDelay)
809
- .exponentialRampToValueAtTime(peekRate, modAttack)
887
+ .linearRampToValueAtTime(peekRate, modAttack)
810
888
  .setValueAtTime(peekRate, modHold)
811
- .linearRampToValueAtTime(baseRate, modDecay);
889
+ .setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
812
890
  }
813
891
  clampCutoffFrequency(frequency) {
814
892
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -817,36 +895,42 @@ export class MidyGM1 {
817
895
  }
818
896
  setFilterEnvelope(note, scheduleTime) {
819
897
  const { voiceParams, startTime } = note;
820
- const baseFreq = this.centToHz(voiceParams.initialFilterFc);
821
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
822
- const sustainFreq = baseFreq +
823
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
898
+ const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
899
+ const baseCent = voiceParams.initialFilterFc;
900
+ const peekCent = baseCent + modEnvToFilterFc;
901
+ const sustainCent = baseCent +
902
+ modEnvToFilterFc * (1 - voiceParams.modSustain);
903
+ const baseFreq = this.centToHz(baseCent);
904
+ const peekFreq = this.centToHz(peekCent);
905
+ const sustainFreq = this.centToHz(sustainCent);
824
906
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
825
907
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
826
908
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
827
909
  const modDelay = startTime + voiceParams.modDelay;
828
910
  const modAttack = modDelay + voiceParams.modAttack;
829
911
  const modHold = modAttack + voiceParams.modHold;
830
- const modDecay = modHold + voiceParams.modDecay;
912
+ const decayDuration = voiceParams.modDecay;
913
+ note.adjustedBaseFreq = adjustedBaseFreq;
831
914
  note.filterNode.frequency
832
915
  .cancelScheduledValues(scheduleTime)
833
916
  .setValueAtTime(adjustedBaseFreq, startTime)
834
917
  .setValueAtTime(adjustedBaseFreq, modDelay)
835
- .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
918
+ .linearRampToValueAtTime(adjustedPeekFreq, modAttack)
836
919
  .setValueAtTime(adjustedPeekFreq, modHold)
837
- .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
920
+ .setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
838
921
  }
839
922
  startModulation(channel, note, scheduleTime) {
923
+ const audioContext = this.audioContext;
840
924
  const { voiceParams } = note;
841
- note.modulationLFO = new OscillatorNode(this.audioContext, {
925
+ note.modulationLFO = new OscillatorNode(audioContext, {
842
926
  frequency: this.centToHz(voiceParams.freqModLFO),
843
927
  });
844
- note.filterDepth = new GainNode(this.audioContext, {
928
+ note.filterDepth = new GainNode(audioContext, {
845
929
  gain: voiceParams.modLfoToFilterFc,
846
930
  });
847
- note.modulationDepth = new GainNode(this.audioContext);
931
+ note.modulationDepth = new GainNode(audioContext);
848
932
  this.setModLfoToPitch(channel, note, scheduleTime);
849
- note.volumeDepth = new GainNode(this.audioContext);
933
+ note.volumeDepth = new GainNode(audioContext);
850
934
  this.setModLfoToVolume(note, scheduleTime);
851
935
  note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
852
936
  note.modulationLFO.connect(note.filterDepth);
@@ -885,7 +969,8 @@ export class MidyGM1 {
885
969
  }
886
970
  }
887
971
  async setNoteAudioNode(channel, note, realtime) {
888
- const now = this.audioContext.currentTime;
972
+ const audioContext = this.audioContext;
973
+ const now = audioContext.currentTime;
889
974
  const { noteNumber, velocity, startTime } = note;
890
975
  const state = channel.state;
891
976
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -893,8 +978,8 @@ export class MidyGM1 {
893
978
  note.voiceParams = voiceParams;
894
979
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
895
980
  note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
896
- note.volumeEnvelopeNode = new GainNode(this.audioContext);
897
- note.filterNode = new BiquadFilterNode(this.audioContext, {
981
+ note.volumeEnvelopeNode = new GainNode(audioContext);
982
+ note.filterNode = new BiquadFilterNode(audioContext, {
898
983
  type: "lowpass",
899
984
  Q: voiceParams.initialFilterQ / 10, // dB
900
985
  });
@@ -953,7 +1038,12 @@ export class MidyGM1 {
953
1038
  const bankTable = this.soundFontTable[programNumber];
954
1039
  if (!bankTable)
955
1040
  return;
956
- const bank = channel.isDrum ? 128 : 0;
1041
+ let bank = channel.isDrum ? 128 : 0;
1042
+ if (bankTable[bank] === undefined) {
1043
+ if (channel.isDrum)
1044
+ return;
1045
+ bank = 0;
1046
+ }
957
1047
  const soundFontIndex = bankTable[bank];
958
1048
  if (soundFontIndex === undefined)
959
1049
  return;
@@ -963,11 +1053,7 @@ export class MidyGM1 {
963
1053
  return;
964
1054
  await this.setNoteAudioNode(channel, note, realtime);
965
1055
  this.setNoteRouting(channelNumber, note, startTime);
966
- note.pending = false;
967
- const off = note.offEvent;
968
- if (off) {
969
- this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
970
- }
1056
+ note.resolveReady();
971
1057
  }
972
1058
  disconnectNote(note) {
973
1059
  note.bufferSource.disconnect();
@@ -981,27 +1067,27 @@ export class MidyGM1 {
981
1067
  }
982
1068
  releaseNote(channel, note, endTime) {
983
1069
  endTime ??= this.audioContext.currentTime;
984
- const volRelease = endTime + note.voiceParams.volRelease;
1070
+ const duration = note.voiceParams.volRelease * releaseTime;
1071
+ const volRelease = endTime + duration;
985
1072
  const modRelease = endTime + note.voiceParams.modRelease;
986
- const stopTime = Math.min(volRelease, modRelease);
987
1073
  note.filterNode.frequency
988
1074
  .cancelScheduledValues(endTime)
989
- .linearRampToValueAtTime(0, modRelease);
1075
+ .linearRampToValueAtTime(note.adjustedBaseFreq, modRelease);
990
1076
  note.volumeEnvelopeNode.gain
991
1077
  .cancelScheduledValues(endTime)
992
- .linearRampToValueAtTime(0, volRelease);
1078
+ .setTargetAtTime(0, endTime, duration * releaseCurve);
993
1079
  return new Promise((resolve) => {
994
1080
  this.scheduleTask(() => {
995
1081
  const bufferSource = note.bufferSource;
996
1082
  bufferSource.loop = false;
997
- bufferSource.stop(stopTime);
1083
+ bufferSource.stop(volRelease);
998
1084
  this.disconnectNote(note);
999
1085
  channel.scheduledNotes[note.index] = undefined;
1000
1086
  resolve();
1001
- }, stopTime);
1087
+ }, volRelease);
1002
1088
  });
1003
1089
  }
1004
- noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1090
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1005
1091
  const channel = this.channels[channelNumber];
1006
1092
  if (!force && 0.5 <= channel.state.sustainPedal)
1007
1093
  return;
@@ -1009,13 +1095,11 @@ export class MidyGM1 {
1009
1095
  if (index < 0)
1010
1096
  return;
1011
1097
  const note = channel.scheduledNotes[index];
1012
- if (note.pending) {
1013
- note.offEvent = { velocity, startTime: endTime };
1014
- return;
1015
- }
1016
1098
  note.ending = true;
1017
1099
  this.setNoteIndex(channel, index);
1018
- const promise = this.releaseNote(channel, note, endTime);
1100
+ const promise = note.ready.then(() => {
1101
+ return this.releaseNote(channel, note, endTime);
1102
+ });
1019
1103
  this.notePromises.push(promise);
1020
1104
  return promise;
1021
1105
  }
@@ -1103,7 +1187,8 @@ export class MidyGM1 {
1103
1187
  }
1104
1188
  setPitchBend(channelNumber, value, scheduleTime) {
1105
1189
  const channel = this.channels[channelNumber];
1106
- scheduleTime ??= this.audioContext.currentTime;
1190
+ if (!(0 <= scheduleTime))
1191
+ scheduleTime = this.audioContext.currentTime;
1107
1192
  const state = channel.state;
1108
1193
  const prev = state.pitchWheel * 2 - 1;
1109
1194
  const next = (value - 8192) / 8192;
@@ -1134,7 +1219,7 @@ export class MidyGM1 {
1134
1219
  }
1135
1220
  setModLfoToVolume(note, scheduleTime) {
1136
1221
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1137
- const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1222
+ const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
1138
1223
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
1139
1224
  note.volumeDepth.gain
1140
1225
  .cancelScheduledValues(scheduleTime)
@@ -1268,12 +1353,14 @@ export class MidyGM1 {
1268
1353
  }
1269
1354
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1270
1355
  const channel = this.channels[channelNumber];
1271
- scheduleTime ??= this.audioContext.currentTime;
1356
+ if (!(0 <= scheduleTime))
1357
+ scheduleTime = this.audioContext.currentTime;
1272
1358
  channel.state.modulationDepthMSB = modulation / 127;
1273
1359
  this.updateModulation(channel, scheduleTime);
1274
1360
  }
1275
1361
  setVolume(channelNumber, volume, scheduleTime) {
1276
- scheduleTime ??= this.audioContext.currentTime;
1362
+ if (!(0 <= scheduleTime))
1363
+ scheduleTime = this.audioContext.currentTime;
1277
1364
  const channel = this.channels[channelNumber];
1278
1365
  channel.state.volumeMSB = volume / 127;
1279
1366
  this.updateChannelVolume(channel, scheduleTime);
@@ -1286,13 +1373,15 @@ export class MidyGM1 {
1286
1373
  };
1287
1374
  }
1288
1375
  setPan(channelNumber, pan, scheduleTime) {
1289
- scheduleTime ??= this.audioContext.currentTime;
1376
+ if (!(0 <= scheduleTime))
1377
+ scheduleTime = this.audioContext.currentTime;
1290
1378
  const channel = this.channels[channelNumber];
1291
1379
  channel.state.panMSB = pan / 127;
1292
1380
  this.updateChannelVolume(channel, scheduleTime);
1293
1381
  }
1294
1382
  setExpression(channelNumber, expression, scheduleTime) {
1295
- scheduleTime ??= this.audioContext.currentTime;
1383
+ if (!(0 <= scheduleTime))
1384
+ scheduleTime = this.audioContext.currentTime;
1296
1385
  const channel = this.channels[channelNumber];
1297
1386
  channel.state.expressionMSB = expression / 127;
1298
1387
  this.updateChannelVolume(channel, scheduleTime);
@@ -1314,7 +1403,8 @@ export class MidyGM1 {
1314
1403
  }
1315
1404
  setSustainPedal(channelNumber, value, scheduleTime) {
1316
1405
  const channel = this.channels[channelNumber];
1317
- scheduleTime ??= this.audioContext.currentTime;
1406
+ if (!(0 <= scheduleTime))
1407
+ scheduleTime = this.audioContext.currentTime;
1318
1408
  channel.state.sustainPedal = value / 127;
1319
1409
  if (64 <= value) {
1320
1410
  this.processScheduledNotes(channel, (note) => {
@@ -1386,7 +1476,8 @@ export class MidyGM1 {
1386
1476
  }
1387
1477
  setPitchBendRange(channelNumber, value, scheduleTime) {
1388
1478
  const channel = this.channels[channelNumber];
1389
- scheduleTime ??= this.audioContext.currentTime;
1479
+ if (!(0 <= scheduleTime))
1480
+ scheduleTime = this.audioContext.currentTime;
1390
1481
  const state = channel.state;
1391
1482
  const prev = state.pitchWheelSensitivity;
1392
1483
  const next = value / 12800;
@@ -1404,7 +1495,8 @@ export class MidyGM1 {
1404
1495
  }
1405
1496
  setFineTuning(channelNumber, value, scheduleTime) {
1406
1497
  const channel = this.channels[channelNumber];
1407
- scheduleTime ??= this.audioContext.currentTime;
1498
+ if (!(0 <= scheduleTime))
1499
+ scheduleTime = this.audioContext.currentTime;
1408
1500
  const prev = channel.fineTuning;
1409
1501
  const next = value;
1410
1502
  channel.fineTuning = next;
@@ -1419,7 +1511,8 @@ export class MidyGM1 {
1419
1511
  }
1420
1512
  setCoarseTuning(channelNumber, value, scheduleTime) {
1421
1513
  const channel = this.channels[channelNumber];
1422
- scheduleTime ??= this.audioContext.currentTime;
1514
+ if (!(0 <= scheduleTime))
1515
+ scheduleTime = this.audioContext.currentTime;
1423
1516
  const prev = channel.coarseTuning;
1424
1517
  const next = value;
1425
1518
  channel.coarseTuning = next;
@@ -1427,7 +1520,8 @@ export class MidyGM1 {
1427
1520
  this.updateChannelDetune(channel, scheduleTime);
1428
1521
  }
1429
1522
  allSoundOff(channelNumber, _value, scheduleTime) {
1430
- scheduleTime ??= this.audioContext.currentTime;
1523
+ if (!(0 <= scheduleTime))
1524
+ scheduleTime = this.audioContext.currentTime;
1431
1525
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
1432
1526
  }
1433
1527
  resetChannelStates(channelNumber) {
@@ -1479,7 +1573,8 @@ export class MidyGM1 {
1479
1573
  }
1480
1574
  }
1481
1575
  allNotesOff(channelNumber, _value, scheduleTime) {
1482
- scheduleTime ??= this.audioContext.currentTime;
1576
+ if (!(0 <= scheduleTime))
1577
+ scheduleTime = this.audioContext.currentTime;
1483
1578
  return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
1484
1579
  }
1485
1580
  handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
@@ -1500,14 +1595,16 @@ export class MidyGM1 {
1500
1595
  }
1501
1596
  }
1502
1597
  GM1SystemOn(scheduleTime) {
1503
- scheduleTime ??= this.audioContext.currentTime;
1598
+ const channels = this.channels;
1599
+ if (!(0 <= scheduleTime))
1600
+ scheduleTime = this.audioContext.currentTime;
1504
1601
  this.mode = "GM1";
1505
- for (let i = 0; i < this.channels.length; i++) {
1602
+ for (let i = 0; i < channels.length; i++) {
1506
1603
  this.allSoundOff(i, 0, scheduleTime);
1507
- const channel = this.channels[i];
1604
+ const channel = channels[i];
1508
1605
  channel.isDrum = false;
1509
1606
  }
1510
- this.channels[9].isDrum = true;
1607
+ channels[9].isDrum = true;
1511
1608
  }
1512
1609
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1513
1610
  switch (data[2]) {
@@ -1528,7 +1625,8 @@ export class MidyGM1 {
1528
1625
  this.setMasterVolume(volume, scheduleTime);
1529
1626
  }
1530
1627
  setMasterVolume(value, scheduleTime) {
1531
- scheduleTime ??= this.audioContext.currentTime;
1628
+ if (!(0 <= scheduleTime))
1629
+ scheduleTime = this.audioContext.currentTime;
1532
1630
  this.masterVolume.gain
1533
1631
  .cancelScheduledValues(scheduleTime)
1534
1632
  .setValueAtTime(value * value, scheduleTime);