@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/script/midy.js CHANGED
@@ -54,7 +54,7 @@ class Note {
54
54
  }
55
55
  }
56
56
  class Midy {
57
- constructor(audioContext) {
57
+ constructor(audioContext, options = this.defaultOptions) {
58
58
  Object.defineProperty(this, "ticksPerBeat", {
59
59
  enumerable: true,
60
60
  configurable: true,
@@ -187,7 +187,19 @@ class Midy {
187
187
  writable: true,
188
188
  value: []
189
189
  });
190
+ Object.defineProperty(this, "defaultOptions", {
191
+ enumerable: true,
192
+ configurable: true,
193
+ writable: true,
194
+ value: {
195
+ reverbAlgorithm: (audioContext) => {
196
+ // return this.createConvolutionReverb(audioContext);
197
+ return this.createSchroederReverb(audioContext);
198
+ },
199
+ }
200
+ });
190
201
  this.audioContext = audioContext;
202
+ this.options = { ...this.defaultOptions, ...options };
191
203
  this.masterGain = new GainNode(audioContext);
192
204
  this.masterGain.connect(audioContext.destination);
193
205
  this.channels = this.createChannels(audioContext);
@@ -228,23 +240,18 @@ class Midy {
228
240
  this.totalTime = this.calcTotalTime();
229
241
  }
230
242
  setChannelAudioNodes(audioContext) {
231
- const { gainLeft, gainRight } = this.panToGain(Midy.channelSettings.pan);
243
+ const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
232
244
  const gainL = new GainNode(audioContext, { gain: gainLeft });
233
245
  const gainR = new GainNode(audioContext, { gain: gainRight });
234
246
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
235
247
  gainL.connect(merger, 0, 0);
236
248
  gainR.connect(merger, 0, 1);
237
- merger.connect(this.masterGain);
238
- const reverbEffect = this.createReverbEffect(audioContext);
249
+ const reverbEffect = this.options.reverbAlgorithm(audioContext);
239
250
  const chorusEffect = this.createChorusEffect(audioContext);
240
- chorusEffect.lfo.start();
241
- reverbEffect.dryGain.connect(gainL);
242
- reverbEffect.dryGain.connect(gainR);
243
- reverbEffect.wetGain.connect(gainL);
244
- reverbEffect.wetGain.connect(gainR);
245
251
  return {
246
252
  gainL,
247
253
  gainR,
254
+ merger,
248
255
  reverbEffect,
249
256
  chorusEffect,
250
257
  };
@@ -252,16 +259,16 @@ class Midy {
252
259
  createChannels(audioContext) {
253
260
  const channels = Array.from({ length: 16 }, () => {
254
261
  return {
255
- ...Midy.channelSettings,
256
- ...Midy.effectSettings,
262
+ ...this.constructor.channelSettings,
263
+ ...this.constructor.effectSettings,
257
264
  ...this.setChannelAudioNodes(audioContext),
258
265
  scheduledNotes: new Map(),
259
266
  sostenutoNotes: new Map(),
260
267
  polyphonicKeyPressure: {
261
- ...Midy.controllerDestinationSettings,
268
+ ...this.constructor.controllerDestinationSettings,
262
269
  },
263
270
  channelPressure: {
264
- ...Midy.controllerDestinationSettings,
271
+ ...this.constructor.controllerDestinationSettings,
265
272
  },
266
273
  };
267
274
  });
@@ -592,8 +599,12 @@ class Midy {
592
599
  }
593
600
  return noteList[0];
594
601
  }
595
- createReverbEffect(audioContext, options = {}) {
602
+ createConvolutionReverb(audioContext, options = {}) {
596
603
  const { decay = 0.8, preDecay = 0, } = options;
604
+ const input = new GainNode(audioContext);
605
+ const output = new GainNode(audioContext);
606
+ const dryGain = new GainNode(audioContext);
607
+ const wetGain = new GainNode(audioContext);
597
608
  const sampleRate = audioContext.sampleRate;
598
609
  const length = sampleRate * decay;
599
610
  const impulse = new AudioBuffer({
@@ -607,27 +618,82 @@ class Midy {
607
618
  for (let i = 0; i < preDecayLength; i++) {
608
619
  channelData[i] = Math.random() * 2 - 1;
609
620
  }
621
+ const attenuationFactor = 1 / (sampleRate * decay);
610
622
  for (let i = preDecayLength; i < length; i++) {
611
- const attenuation = Math.exp(-(i - preDecayLength) / sampleRate / decay);
623
+ const attenuation = Math.exp(-(i - preDecayLength) * attenuationFactor);
612
624
  channelData[i] = (Math.random() * 2 - 1) * attenuation;
613
625
  }
614
626
  }
615
627
  const convolverNode = new ConvolverNode(audioContext, {
616
628
  buffer: impulse,
617
629
  });
618
- const dryGain = new GainNode(audioContext);
619
- const wetGain = new GainNode(audioContext);
630
+ input.connect(convolverNode);
620
631
  convolverNode.connect(wetGain);
632
+ wetGain.connect(output);
633
+ dryGain.connect(output);
621
634
  return {
622
- convolverNode,
635
+ input,
636
+ output,
623
637
  dryGain,
624
638
  wetGain,
639
+ convolverNode,
625
640
  };
626
641
  }
642
+ createCombFilter(audioContext, input, delay, feedback) {
643
+ const delayNode = new DelayNode(audioContext, {
644
+ maxDelayTime: delay,
645
+ delayTime: delay,
646
+ });
647
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
648
+ input.connect(delayNode);
649
+ delayNode.connect(feedbackGain);
650
+ feedbackGain.connect(delayNode);
651
+ return delayNode;
652
+ }
653
+ createAllpassFilter(audioContext, input, delay, feedback) {
654
+ const delayNode = new DelayNode(audioContext, {
655
+ maxDelayTime: delay,
656
+ delayTime: delay,
657
+ });
658
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
659
+ const passGain = new GainNode(audioContext, { gain: 1 - feedback });
660
+ input.connect(delayNode);
661
+ delayNode.connect(feedbackGain);
662
+ feedbackGain.connect(delayNode);
663
+ delayNode.connect(passGain);
664
+ return passGain;
665
+ }
666
+ // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
667
+ // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
668
+ createSchroederReverb(audioContext, options = {}) {
669
+ 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;
670
+ const input = new GainNode(audioContext);
671
+ const output = new GainNode(audioContext);
672
+ const mergerGain = new GainNode(audioContext, {
673
+ gain: 1 / (combDelays.length * 2),
674
+ });
675
+ const dryGain = new GainNode(audioContext, { gain: 1 - mix });
676
+ const wetGain = new GainNode(audioContext, { gain: mix });
677
+ for (let i = 0; i < combDelays.length; i++) {
678
+ const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
679
+ comb.connect(mergerGain);
680
+ }
681
+ const allpasses = [];
682
+ for (let i = 0; i < allpassDelays.length; i++) {
683
+ const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
684
+ allpasses.push(allpass);
685
+ }
686
+ allpasses.at(-1).connect(wetGain);
687
+ input.connect(dryGain);
688
+ dryGain.connect(output);
689
+ wetGain.connect(output);
690
+ return { input, output, dryGain, wetGain };
691
+ }
627
692
  createChorusEffect(audioContext, options = {}) {
628
693
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
629
694
  const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
630
695
  const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
696
+ const output = new GainNode(audioContext);
631
697
  const chorusGains = [];
632
698
  const delayNodes = [];
633
699
  const baseGain = 1 / chorusCount;
@@ -637,50 +703,47 @@ class Midy {
637
703
  const delayNode = new DelayNode(audioContext, {
638
704
  maxDelayTime: delayTime,
639
705
  });
640
- delayNodes.push(delayNode);
641
706
  const chorusGain = new GainNode(audioContext, { gain: baseGain });
707
+ delayNodes.push(delayNode);
642
708
  chorusGains.push(chorusGain);
643
- lfo.connect(lfoGain);
644
709
  lfoGain.connect(delayNode.delayTime);
645
710
  delayNode.connect(chorusGain);
711
+ chorusGain.connect(output);
646
712
  }
713
+ lfo.connect(lfoGain);
714
+ lfo.start();
647
715
  return {
648
716
  lfo,
649
717
  lfoGain,
650
718
  delayNodes,
651
719
  chorusGains,
720
+ output,
652
721
  };
653
722
  }
654
- connectNoteEffects(channel, gainNode) {
723
+ connectEffects(channel, gainNode) {
724
+ gainNode.connect(channel.merger);
655
725
  if (channel.reverb === 0) {
656
726
  if (channel.chorus === 0) { // no effect
657
- gainNode.connect(channel.gainL);
658
- gainNode.connect(channel.gainR);
727
+ channel.merger.connect(this.masterGain);
659
728
  }
660
729
  else { // chorus
661
730
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
662
- gainNode.connect(delayNode);
663
- });
664
- channel.chorusEffect.chorusGains.forEach((chorusGain) => {
665
- chorusGain.connect(channel.gainL);
666
- chorusGain.connect(channel.gainR);
731
+ channel.merger.connect(delayNode);
667
732
  });
733
+ channel.chorusEffect.output.connect(this.masterGain);
668
734
  }
669
735
  }
670
736
  else {
671
737
  if (channel.chorus === 0) { // reverb
672
- gainNode.connect(channel.reverbEffect.convolverNode);
673
- gainNode.connect(channel.reverbEffect.dryGain);
738
+ channel.merger.connect(channel.reverbEffect.input);
739
+ channel.reverbEffect.output.connect(this.masterGain);
674
740
  }
675
741
  else { // reverb + chorus
676
- gainNode.connect(channel.reverbEffect.convolverNode);
677
- gainNode.connect(channel.reverbEffect.dryGain);
678
742
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
679
- gainNode.connect(delayNode);
680
- });
681
- channel.chorusEffect.chorusGains.forEach((chorusGain) => {
682
- chorusGain.connect(channel.reverbEffect.convolverNode);
743
+ channel.merger.connect(delayNode);
683
744
  });
745
+ channel.merger.connect(channel.reverbEffect.input);
746
+ channel.reverbEffect.output.connect(this.masterGain);
684
747
  }
685
748
  }
686
749
  }
@@ -820,7 +883,7 @@ class Midy {
820
883
  if (!instrumentKey)
821
884
  return;
822
885
  const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
823
- this.connectNoteEffects(channel, note.gainNode);
886
+ this.connectEffects(channel, note.gainNode);
824
887
  if (channel.sostenutoPedal) {
825
888
  channel.sostenutoNotes.set(noteNumber, note);
826
889
  }
@@ -844,46 +907,48 @@ class Midy {
844
907
  return;
845
908
  if (!channel.scheduledNotes.has(noteNumber))
846
909
  return;
847
- const targetNotes = channel.scheduledNotes.get(noteNumber);
848
- for (let i = 0; i < targetNotes.length; i++) {
849
- const targetNote = targetNotes[i];
850
- if (!targetNote)
910
+ const scheduledNotes = channel.scheduledNotes.get(noteNumber);
911
+ for (let i = 0; i < scheduledNotes.length; i++) {
912
+ const note = scheduledNotes[i];
913
+ if (!note)
851
914
  continue;
852
- if (targetNote.ending)
915
+ if (note.ending)
853
916
  continue;
854
- const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, vibLFO, vibLFOGain, instrumentKey, } = targetNote;
855
917
  const velocityRate = (velocity + 127) / 127;
856
- const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
857
- gainNode.gain.cancelScheduledValues(stopTime);
858
- gainNode.gain.linearRampToValueAtTime(0, volEndTime);
918
+ const volEndTime = stopTime +
919
+ note.instrumentKey.volRelease * velocityRate;
920
+ note.gainNode.gain
921
+ .cancelScheduledValues(stopTime)
922
+ .linearRampToValueAtTime(0, volEndTime);
859
923
  const maxFreq = this.audioContext.sampleRate / 2;
860
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
924
+ const baseFreq = this.centToHz(note.instrumentKey.initialFilterFc);
861
925
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
862
- const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
863
- filterNode.frequency
926
+ const modEndTime = stopTime +
927
+ note.instrumentKey.modRelease * velocityRate;
928
+ note.filterNode.frequency
864
929
  .cancelScheduledValues(stopTime)
865
930
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
866
- targetNote.ending = true;
931
+ note.ending = true;
867
932
  this.scheduleTask(() => {
868
- bufferSource.loop = false;
933
+ note.bufferSource.loop = false;
869
934
  }, stopTime);
870
935
  return new Promise((resolve) => {
871
- bufferSource.onended = () => {
872
- targetNotes[i] = null;
873
- bufferSource.disconnect(0);
874
- filterNode.disconnect(0);
875
- gainNode.disconnect(0);
876
- if (modLFOGain)
877
- modLFOGain.disconnect(0);
878
- if (vibLFOGain)
879
- vibLFOGain.disconnect(0);
880
- if (modLFO)
881
- modLFO.stop();
882
- if (vibLFO)
883
- vibLFO.stop();
936
+ note.bufferSource.onended = () => {
937
+ scheduledNotes[i] = null;
938
+ note.bufferSource.disconnect();
939
+ note.filterNode.disconnect();
940
+ note.gainNode.disconnect();
941
+ if (note.modLFOGain)
942
+ note.modLFOGain.disconnect();
943
+ if (note.vibLFOGain)
944
+ note.vibLFOGain.disconnect();
945
+ if (note.modLFO)
946
+ note.modLFO.stop();
947
+ if (note.vibLFO)
948
+ note.vibLFO.stop();
884
949
  resolve();
885
950
  };
886
- bufferSource.stop(volEndTime);
951
+ note.bufferSource.stop(volEndTime);
887
952
  });
888
953
  }
889
954
  }