@marmooo/midy 0.0.8 → 0.0.9

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-GM2.js CHANGED
@@ -51,7 +51,7 @@ class Note {
51
51
  }
52
52
  }
53
53
  export class MidyGM2 {
54
- constructor(audioContext) {
54
+ constructor(audioContext, options = this.defaultOptions) {
55
55
  Object.defineProperty(this, "ticksPerBeat", {
56
56
  enumerable: true,
57
57
  configurable: true,
@@ -184,7 +184,19 @@ export class MidyGM2 {
184
184
  writable: true,
185
185
  value: []
186
186
  });
187
+ Object.defineProperty(this, "defaultOptions", {
188
+ enumerable: true,
189
+ configurable: true,
190
+ writable: true,
191
+ value: {
192
+ reverbAlgorithm: (audioContext) => {
193
+ // return this.createConvolutionReverb(audioContext);
194
+ return this.createSchroederReverb(audioContext);
195
+ },
196
+ }
197
+ });
187
198
  this.audioContext = audioContext;
199
+ this.options = { ...this.defaultOptions, ...options };
188
200
  this.masterGain = new GainNode(audioContext);
189
201
  this.masterGain.connect(audioContext.destination);
190
202
  this.channels = this.createChannels(audioContext);
@@ -225,23 +237,18 @@ export class MidyGM2 {
225
237
  this.totalTime = this.calcTotalTime();
226
238
  }
227
239
  setChannelAudioNodes(audioContext) {
228
- const { gainLeft, gainRight } = this.panToGain(MidyGM2.channelSettings.pan);
240
+ const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
229
241
  const gainL = new GainNode(audioContext, { gain: gainLeft });
230
242
  const gainR = new GainNode(audioContext, { gain: gainRight });
231
243
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
232
244
  gainL.connect(merger, 0, 0);
233
245
  gainR.connect(merger, 0, 1);
234
- merger.connect(this.masterGain);
235
- const reverbEffect = this.createReverbEffect(audioContext);
246
+ const reverbEffect = this.options.reverbAlgorithm(audioContext);
236
247
  const chorusEffect = this.createChorusEffect(audioContext);
237
- chorusEffect.lfo.start();
238
- reverbEffect.dryGain.connect(gainL);
239
- reverbEffect.dryGain.connect(gainR);
240
- reverbEffect.wetGain.connect(gainL);
241
- reverbEffect.wetGain.connect(gainR);
242
248
  return {
243
249
  gainL,
244
250
  gainR,
251
+ merger,
245
252
  reverbEffect,
246
253
  chorusEffect,
247
254
  };
@@ -249,13 +256,13 @@ export class MidyGM2 {
249
256
  createChannels(audioContext) {
250
257
  const channels = Array.from({ length: 16 }, () => {
251
258
  return {
252
- ...MidyGM2.channelSettings,
253
- ...MidyGM2.effectSettings,
259
+ ...this.constructor.channelSettings,
260
+ ...this.constructor.effectSettings,
254
261
  ...this.setChannelAudioNodes(audioContext),
255
262
  scheduledNotes: new Map(),
256
263
  sostenutoNotes: new Map(),
257
264
  channelPressure: {
258
- ...MidyGM2.controllerDestinationSettings,
265
+ ...this.constructor.controllerDestinationSettings,
259
266
  },
260
267
  };
261
268
  });
@@ -583,8 +590,12 @@ export class MidyGM2 {
583
590
  }
584
591
  return noteList[0];
585
592
  }
586
- createReverbEffect(audioContext, options = {}) {
593
+ createConvolutionReverb(audioContext, options = {}) {
587
594
  const { decay = 0.8, preDecay = 0, } = options;
595
+ const input = new GainNode(audioContext);
596
+ const output = new GainNode(audioContext);
597
+ const dryGain = new GainNode(audioContext);
598
+ const wetGain = new GainNode(audioContext);
588
599
  const sampleRate = audioContext.sampleRate;
589
600
  const length = sampleRate * decay;
590
601
  const impulse = new AudioBuffer({
@@ -598,27 +609,82 @@ export class MidyGM2 {
598
609
  for (let i = 0; i < preDecayLength; i++) {
599
610
  channelData[i] = Math.random() * 2 - 1;
600
611
  }
612
+ const attenuationFactor = 1 / (sampleRate * decay);
601
613
  for (let i = preDecayLength; i < length; i++) {
602
- const attenuation = Math.exp(-(i - preDecayLength) / sampleRate / decay);
614
+ const attenuation = Math.exp(-(i - preDecayLength) * attenuationFactor);
603
615
  channelData[i] = (Math.random() * 2 - 1) * attenuation;
604
616
  }
605
617
  }
606
618
  const convolverNode = new ConvolverNode(audioContext, {
607
619
  buffer: impulse,
608
620
  });
609
- const dryGain = new GainNode(audioContext);
610
- const wetGain = new GainNode(audioContext);
621
+ input.connect(convolverNode);
611
622
  convolverNode.connect(wetGain);
623
+ wetGain.connect(output);
624
+ dryGain.connect(output);
612
625
  return {
613
- convolverNode,
626
+ input,
627
+ output,
614
628
  dryGain,
615
629
  wetGain,
630
+ convolverNode,
616
631
  };
617
632
  }
633
+ createCombFilter(audioContext, input, delay, feedback) {
634
+ const delayNode = new DelayNode(audioContext, {
635
+ maxDelayTime: delay,
636
+ delayTime: delay,
637
+ });
638
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
639
+ input.connect(delayNode);
640
+ delayNode.connect(feedbackGain);
641
+ feedbackGain.connect(delayNode);
642
+ return delayNode;
643
+ }
644
+ createAllpassFilter(audioContext, input, delay, feedback) {
645
+ const delayNode = new DelayNode(audioContext, {
646
+ maxDelayTime: delay,
647
+ delayTime: delay,
648
+ });
649
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
650
+ const passGain = new GainNode(audioContext, { gain: 1 - feedback });
651
+ input.connect(delayNode);
652
+ delayNode.connect(feedbackGain);
653
+ feedbackGain.connect(delayNode);
654
+ delayNode.connect(passGain);
655
+ return passGain;
656
+ }
657
+ // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
658
+ // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
659
+ createSchroederReverb(audioContext, options = {}) {
660
+ const { combDelays = [0.31, 0.34, 0.37, 0.40], combFeedbacks = [0.86, 0.87, 0.88, 0.89], allpassDelays = [0.02, 0.05], allpassFeedbacks = [0.7, 0.7], mix = 0.5, } = options;
661
+ const input = new GainNode(audioContext);
662
+ const output = new GainNode(audioContext);
663
+ const mergerGain = new GainNode(audioContext, {
664
+ gain: 1 / (combDelays.length * 2),
665
+ });
666
+ const dryGain = new GainNode(audioContext, { gain: 1 - mix });
667
+ const wetGain = new GainNode(audioContext, { gain: mix });
668
+ for (let i = 0; i < combDelays.length; i++) {
669
+ const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
670
+ comb.connect(mergerGain);
671
+ }
672
+ const allpasses = [];
673
+ for (let i = 0; i < allpassDelays.length; i++) {
674
+ const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
675
+ allpasses.push(allpass);
676
+ }
677
+ allpasses.at(-1).connect(wetGain);
678
+ input.connect(dryGain);
679
+ dryGain.connect(output);
680
+ wetGain.connect(output);
681
+ return { input, output, dryGain, wetGain };
682
+ }
618
683
  createChorusEffect(audioContext, options = {}) {
619
684
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
620
685
  const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
621
686
  const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
687
+ const output = new GainNode(audioContext);
622
688
  const chorusGains = [];
623
689
  const delayNodes = [];
624
690
  const baseGain = 1 / chorusCount;
@@ -628,50 +694,47 @@ export class MidyGM2 {
628
694
  const delayNode = new DelayNode(audioContext, {
629
695
  maxDelayTime: delayTime,
630
696
  });
631
- delayNodes.push(delayNode);
632
697
  const chorusGain = new GainNode(audioContext, { gain: baseGain });
698
+ delayNodes.push(delayNode);
633
699
  chorusGains.push(chorusGain);
634
- lfo.connect(lfoGain);
635
700
  lfoGain.connect(delayNode.delayTime);
636
701
  delayNode.connect(chorusGain);
702
+ chorusGain.connect(output);
637
703
  }
704
+ lfo.connect(lfoGain);
705
+ lfo.start();
638
706
  return {
639
707
  lfo,
640
708
  lfoGain,
641
709
  delayNodes,
642
710
  chorusGains,
711
+ output,
643
712
  };
644
713
  }
645
- connectNoteEffects(channel, gainNode) {
714
+ connectEffects(channel, gainNode) {
715
+ gainNode.connect(channel.merger);
646
716
  if (channel.reverb === 0) {
647
717
  if (channel.chorus === 0) { // no effect
648
- gainNode.connect(channel.gainL);
649
- gainNode.connect(channel.gainR);
718
+ channel.merger.connect(this.masterGain);
650
719
  }
651
720
  else { // chorus
652
721
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
653
- gainNode.connect(delayNode);
654
- });
655
- channel.chorusEffect.chorusGains.forEach((chorusGain) => {
656
- chorusGain.connect(channel.gainL);
657
- chorusGain.connect(channel.gainR);
722
+ channel.merger.connect(delayNode);
658
723
  });
724
+ channel.chorusEffect.output.connect(this.masterGain);
659
725
  }
660
726
  }
661
727
  else {
662
728
  if (channel.chorus === 0) { // reverb
663
- gainNode.connect(channel.reverbEffect.convolverNode);
664
- gainNode.connect(channel.reverbEffect.dryGain);
729
+ channel.merger.connect(channel.reverbEffect.input);
730
+ channel.reverbEffect.output.connect(this.masterGain);
665
731
  }
666
732
  else { // reverb + chorus
667
- gainNode.connect(channel.reverbEffect.convolverNode);
668
- gainNode.connect(channel.reverbEffect.dryGain);
669
733
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
670
- gainNode.connect(delayNode);
671
- });
672
- channel.chorusEffect.chorusGains.forEach((chorusGain) => {
673
- chorusGain.connect(channel.reverbEffect.convolverNode);
734
+ channel.merger.connect(delayNode);
674
735
  });
736
+ channel.merger.connect(channel.reverbEffect.input);
737
+ channel.reverbEffect.output.connect(this.masterGain);
675
738
  }
676
739
  }
677
740
  }
@@ -742,7 +805,7 @@ export class MidyGM2 {
742
805
  startModulation(channel, note, time) {
743
806
  const { instrumentKey } = note;
744
807
  note.modLFOGain = new GainNode(this.audioContext, {
745
- gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
808
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume + channel.modulation),
746
809
  });
747
810
  note.modLFO = new OscillatorNode(this.audioContext, {
748
811
  frequency: this.centToHz(instrumentKey.freqModLFO),
@@ -794,7 +857,7 @@ export class MidyGM2 {
794
857
  if (!instrumentKey)
795
858
  return;
796
859
  const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
797
- this.connectNoteEffects(channel, note.gainNode);
860
+ this.connectEffects(channel, note.gainNode);
798
861
  if (channel.sostenutoPedal) {
799
862
  channel.sostenutoNotes.set(noteNumber, note);
800
863
  }
@@ -818,42 +881,48 @@ export class MidyGM2 {
818
881
  return;
819
882
  if (!channel.scheduledNotes.has(noteNumber))
820
883
  return;
821
- const targetNotes = channel.scheduledNotes.get(noteNumber);
822
- for (let i = 0; i < targetNotes.length; i++) {
823
- const targetNote = targetNotes[i];
824
- if (!targetNote)
884
+ const scheduledNotes = channel.scheduledNotes.get(noteNumber);
885
+ for (let i = 0; i < scheduledNotes.length; i++) {
886
+ const note = scheduledNotes[i];
887
+ if (!note)
825
888
  continue;
826
- if (targetNote.ending)
889
+ if (note.ending)
827
890
  continue;
828
- const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
829
891
  const velocityRate = (velocity + 127) / 127;
830
- const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
831
- gainNode.gain.cancelScheduledValues(stopTime);
832
- gainNode.gain.linearRampToValueAtTime(0, volEndTime);
892
+ const volEndTime = stopTime +
893
+ note.instrumentKey.volRelease * velocityRate;
894
+ note.gainNode.gain
895
+ .cancelScheduledValues(stopTime)
896
+ .linearRampToValueAtTime(0, volEndTime);
833
897
  const maxFreq = this.audioContext.sampleRate / 2;
834
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
898
+ const baseFreq = this.centToHz(note.instrumentKey.initialFilterFc);
835
899
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
836
- const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
837
- filterNode.frequency
900
+ const modEndTime = stopTime +
901
+ note.instrumentKey.modRelease * velocityRate;
902
+ note.filterNode.frequency
838
903
  .cancelScheduledValues(stopTime)
839
904
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
840
- targetNote.ending = true;
905
+ note.ending = true;
841
906
  this.scheduleTask(() => {
842
- bufferSource.loop = false;
907
+ note.bufferSource.loop = false;
843
908
  }, stopTime);
844
909
  return new Promise((resolve) => {
845
- bufferSource.onended = () => {
846
- targetNotes[i] = null;
847
- bufferSource.disconnect(0);
848
- filterNode.disconnect(0);
849
- gainNode.disconnect(0);
850
- if (modLFOGain)
851
- modLFOGain.disconnect(0);
852
- if (modLFO)
853
- modLFO.stop();
910
+ note.bufferSource.onended = () => {
911
+ scheduledNotes[i] = null;
912
+ note.bufferSource.disconnect();
913
+ note.filterNode.disconnect();
914
+ note.gainNode.disconnect();
915
+ if (note.modLFOGain)
916
+ note.modLFOGain.disconnect();
917
+ if (note.vibLFOGain)
918
+ note.vibLFOGain.disconnect();
919
+ if (note.modLFO)
920
+ note.modLFO.stop();
921
+ if (note.vibLFO)
922
+ note.vibLFO.stop();
854
923
  resolve();
855
924
  };
856
- bufferSource.stop(volEndTime);
925
+ note.bufferSource.stop(volEndTime);
857
926
  });
858
927
  }
859
928
  }
@@ -37,25 +37,7 @@ export class MidyGMLite {
37
37
  notePromises: any[];
38
38
  audioContext: any;
39
39
  masterGain: any;
40
- channels: {
41
- scheduledNotes: Map<any, any>;
42
- gainL: any;
43
- gainR: any;
44
- expression: number;
45
- modulation: number;
46
- sustainPedal: boolean;
47
- rpnMSB: number;
48
- rpnLSB: number;
49
- pitchBendRange: number;
50
- volume: number;
51
- pan: number;
52
- bank: number;
53
- dataMSB: number;
54
- dataLSB: number;
55
- program: number;
56
- pitchBend: number;
57
- modulationDepthRange: number;
58
- }[];
40
+ channels: any[];
59
41
  initSoundFontTable(): any[];
60
42
  addSoundFont(soundFont: any): void;
61
43
  loadSoundFont(soundFontUrl: any): Promise<void>;
@@ -63,26 +45,9 @@ export class MidyGMLite {
63
45
  setChannelAudioNodes(audioContext: any): {
64
46
  gainL: any;
65
47
  gainR: any;
48
+ merger: any;
66
49
  };
67
- createChannels(audioContext: any): {
68
- scheduledNotes: Map<any, any>;
69
- gainL: any;
70
- gainR: any;
71
- expression: number;
72
- modulation: number;
73
- sustainPedal: boolean;
74
- rpnMSB: number;
75
- rpnLSB: number;
76
- pitchBendRange: number;
77
- volume: number;
78
- pan: number;
79
- bank: number;
80
- dataMSB: number;
81
- dataLSB: number;
82
- program: number;
83
- pitchBend: number;
84
- modulationDepthRange: number;
85
- }[];
50
+ createChannels(audioContext: any): any[];
86
51
  createNoteBuffer(instrumentKey: any, isSF3: any): Promise<any>;
87
52
  createNoteBufferNode(instrumentKey: any, isSF3: any): Promise<any>;
88
53
  convertToFloat32Array(uint8Array: any): Float32Array;
@@ -105,7 +70,7 @@ export class MidyGMLite {
105
70
  currentTime(): number;
106
71
  getActiveNotes(channel: any, time: any): Map<any, any>;
107
72
  getActiveNote(noteList: any, time: any): any;
108
- connectNoteEffects(channel: any, gainNode: any): void;
73
+ connectEffects(channel: any, gainNode: any): void;
109
74
  cbToRatio(cb: any): number;
110
75
  centToHz(cent: any): number;
111
76
  calcSemitoneOffset(channel: any): number;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AAqBA;IAmBE;;;;;;;;;MASE;IAEF;;;;;;;MAOE;IAEF,+BAMC;IA5CD,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAuBhB,kBAAgC;IAChC,gBAA4C;IAE5C;;;;;;;;;;;;;;;;;;QAAiD;IAInD,4BAMC;IAED,mCASC;IAED,gDAMC;IAED,sCASC;IAED;;;MAcC;IAED;;;;;;;;;;;;;;;;;;QAUC;IAED,+DAyBC;IAED,mEAWC;IAED,qDAOC;IAED,2EA+CC;IAED,mCAOC;IAED,0BA+CC;IAED,uDAEC;IAED,wDAEC;IAED;;;MA8DC;IAED,4BAsBC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,uDASC;IAED,6CAQC;IAED,sDAGC;IAED,2BAEC;IAED,4BAEC;IAED,yCAEC;IAED,mFAGC;IAED,iDAiBC;IAED,iDAiCC;IAED,0DAmBC;IAED,wHA6BC;IAED,kGA6BC;IAED,0EAGC;IAED,sIAmDC;IAED,0FAGC;IAED,kEAeC;IAED,wFAiBC;IAED,4DAGC;IAED,qEAGC;IAED,uDAOC;IAED,mFA+BC;IAED,qCAcC;IAED,yDAIC;IACD,iDAIC;IAED;;;MAMC;IAED,2CAIC;IAED,yDAIC;IAED,mDAGC;IAED,sCAUC;IAED,sDAMC;IAED,oCAYC;IAED,gDAEC;IAED,gDAEC;IAED,mDAGC;IAED,oDAUC;IAED,kDAKC;IAED,iEAOC;IAED,uCAoBC;IAED,8CAEC;IAED,uCAoBC;IAED,4DAgBC;IAED,oBAQC;IAED,yDAaC;IAED,yCAGC;IAED,mCAQC;IAED,wCAEC;IAED,6BASC;IAED,0DAUC;CACF;AAx/BD;IAOE,gFAKC;IAXD,kBAAa;IACb,cAAS;IACT,gBAAW;IACX,YAAO;IACP,gBAAW;IAGT,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,mBAAkC;CAErC"}
1
+ {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AAqBA;IAmBE;;;;;;;;;MASE;IAEF;;;;;;;MAOE;IAEF,+BAMC;IA5CD,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAuBhB,kBAAgC;IAChC,gBAA4C;IAE5C,gBAAiD;IAInD,4BAMC;IAED,mCASC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAcC;IAED,yCAUC;IAED,+DAyBC;IAED,mEAWC;IAED,qDAOC;IAED,2EA+CC;IAED,mCAOC;IAED,0BA+CC;IAED,uDAEC;IAED,wDAEC;IAED;;;MA8DC;IAED,4BAsBC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,uDASC;IAED,6CAQC;IAED,kDAGC;IAED,2BAEC;IAED,4BAEC;IAED,yCAEC;IAED,mFAGC;IAED,iDAiBC;IAED,iDAiCC;IAED,0DAmBC;IAED,wHA6BC;IAED,kGA6BC;IAED,0EAGC;IAED,sIA8CC;IAED,0FAGC;IAED,kEAeC;IAED,wFAiBC;IAED,4DAGC;IAED,qEAGC;IAED,uDAOC;IAED,mFA+BC;IAED,qCAcC;IAED,yDAIC;IACD,iDAIC;IAED;;;MAMC;IAED,2CAIC;IAED,yDAIC;IAED,mDAGC;IAED,sCAUC;IAED,sDAMC;IAED,oCAYC;IAED,gDAEC;IAED,gDAEC;IAED,mDAGC;IAED,oDAUC;IAED,kDAKC;IAED,iEAOC;IAED,uCAoBC;IAED,8CAEC;IAED,uCAoBC;IAED,4DAgBC;IAED,oBAQC;IAED,yDAaC;IAED,yCAGC;IAED,mCAQC;IAED,wCAEC;IAED,6BASC;IAED,0DAUC;CACF;AAn/BD;IAOE,gFAKC;IAXD,kBAAa;IACb,cAAS;IACT,gBAAW;IACX,YAAO;IACP,gBAAW;IAGT,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,mBAAkC;CAErC"}
@@ -183,23 +183,23 @@ export class MidyGMLite {
183
183
  this.totalTime = this.calcTotalTime();
184
184
  }
185
185
  setChannelAudioNodes(audioContext) {
186
- const { gainLeft, gainRight } = this.panToGain(MidyGMLite.channelSettings.pan);
186
+ const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
187
187
  const gainL = new GainNode(audioContext, { gain: gainLeft });
188
188
  const gainR = new GainNode(audioContext, { gain: gainRight });
189
189
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
190
190
  gainL.connect(merger, 0, 0);
191
191
  gainR.connect(merger, 0, 1);
192
- merger.connect(this.masterGain);
193
192
  return {
194
193
  gainL,
195
194
  gainR,
195
+ merger,
196
196
  };
197
197
  }
198
198
  createChannels(audioContext) {
199
199
  const channels = Array.from({ length: 16 }, () => {
200
200
  return {
201
- ...MidyGMLite.channelSettings,
202
- ...MidyGMLite.effectSettings,
201
+ ...this.constructor.channelSettings,
202
+ ...this.constructor.effectSettings,
203
203
  ...this.setChannelAudioNodes(audioContext),
204
204
  scheduledNotes: new Map(),
205
205
  };
@@ -491,9 +491,9 @@ export class MidyGMLite {
491
491
  }
492
492
  return noteList[0];
493
493
  }
494
- connectNoteEffects(channel, gainNode) {
495
- gainNode.connect(channel.gainL);
496
- gainNode.connect(channel.gainR);
494
+ connectEffects(channel, gainNode) {
495
+ gainNode.connect(channel.merger);
496
+ merger.connect(this.masterGain);
497
497
  }
498
498
  cbToRatio(cb) {
499
499
  return Math.pow(10, cb / 200);
@@ -559,7 +559,7 @@ export class MidyGMLite {
559
559
  startModulation(channel, note, time) {
560
560
  const { instrumentKey } = note;
561
561
  note.modLFOGain = new GainNode(this.audioContext, {
562
- gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
562
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume + channel.modulation),
563
563
  });
564
564
  note.modLFO = new OscillatorNode(this.audioContext, {
565
565
  frequency: this.centToHz(instrumentKey.freqModLFO),
@@ -598,7 +598,7 @@ export class MidyGMLite {
598
598
  if (!instrumentKey)
599
599
  return;
600
600
  const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
601
- this.connectNoteEffects(channel, note.gainNode);
601
+ this.connectEffects(channel, note.gainNode);
602
602
  const scheduledNotes = channel.scheduledNotes;
603
603
  if (scheduledNotes.has(noteNumber)) {
604
604
  scheduledNotes.get(noteNumber).push(note);
@@ -617,39 +617,41 @@ export class MidyGMLite {
617
617
  return;
618
618
  if (!channel.scheduledNotes.has(noteNumber))
619
619
  return;
620
- const targetNotes = channel.scheduledNotes.get(noteNumber);
621
- for (let i = 0; i < targetNotes.length; i++) {
622
- const targetNote = targetNotes[i];
623
- if (!targetNote)
620
+ const scheduledNotes = channel.scheduledNotes.get(noteNumber);
621
+ for (let i = 0; i < scheduledNotes.length; i++) {
622
+ const note = scheduledNotes[i];
623
+ if (!note)
624
624
  continue;
625
- if (targetNote.ending)
625
+ if (note.ending)
626
626
  continue;
627
- const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
628
627
  const velocityRate = (velocity + 127) / 127;
629
- const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
630
- gainNode.gain.cancelScheduledValues(stopTime);
631
- gainNode.gain.linearRampToValueAtTime(0, volEndTime);
628
+ const volEndTime = stopTime +
629
+ note.instrumentKey.volRelease * velocityRate;
630
+ note.gainNode.gain
631
+ .cancelScheduledValues(stopTime)
632
+ .linearRampToValueAtTime(0, volEndTime);
632
633
  const maxFreq = this.audioContext.sampleRate / 2;
633
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
634
+ const baseFreq = this.centToHz(note.instrumentKey.initialFilterFc);
634
635
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
635
- const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
636
- filterNode.frequency
636
+ const modEndTime = stopTime +
637
+ note.instrumentKey.modRelease * velocityRate;
638
+ note.filterNode.frequency
637
639
  .cancelScheduledValues(stopTime)
638
640
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
639
- targetNote.ending = true;
641
+ note.ending = true;
640
642
  this.scheduleTask(() => {
641
- bufferSource.loop = false;
643
+ note.bufferSource.loop = false;
642
644
  }, stopTime);
643
645
  return new Promise((resolve) => {
644
- bufferSource.onended = () => {
645
- targetNotes[i] = null;
646
- bufferSource.disconnect(0);
647
- filterNode.disconnect(0);
648
- gainNode.disconnect(0);
649
- if (modLFOGain)
650
- modLFOGain.disconnect(0);
651
- if (modLFO)
652
- modLFO.stop();
646
+ note.bufferSource.onended = () => {
647
+ scheduledNotes[i] = null;
648
+ note.bufferSource.disconnect();
649
+ note.filterNode.disconnect();
650
+ note.gainNode.disconnect();
651
+ if (note.modLFOGain)
652
+ note.modLFOGain.disconnect();
653
+ if (note.modLFO)
654
+ note.modLFO.stop();
653
655
  resolve();
654
656
  };
655
657
  bufferSource.stop(volEndTime);