@marmooo/midy 0.0.7 → 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,
@@ -78,13 +78,13 @@ class Midy {
78
78
  configurable: true,
79
79
  writable: true,
80
80
  value: 0
81
- });
81
+ }); // cb
82
82
  Object.defineProperty(this, "masterCoarseTuning", {
83
83
  enumerable: true,
84
84
  configurable: true,
85
85
  writable: true,
86
86
  value: 0
87
- });
87
+ }); // cb
88
88
  Object.defineProperty(this, "mono", {
89
89
  enumerable: true,
90
90
  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
  }
@@ -751,7 +814,7 @@ class Midy {
751
814
  startModulation(channel, note, time) {
752
815
  const { instrumentKey } = note;
753
816
  note.modLFOGain = new GainNode(this.audioContext, {
754
- gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
817
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume + channel.modulation),
755
818
  });
756
819
  note.modLFO = new OscillatorNode(this.audioContext, {
757
820
  frequency: this.centToHz(instrumentKey.freqModLFO),
@@ -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
  }
@@ -998,7 +1063,7 @@ class Midy {
998
1063
  case 5:
999
1064
  return this.setPortamentoTime(channelNumber, value);
1000
1065
  case 6:
1001
- return this.setDataEntry(channelNumber, value, true);
1066
+ return this.dataEntryMSB(channelNumber, value);
1002
1067
  case 7:
1003
1068
  return this.setVolume(channelNumber, value);
1004
1069
  case 10:
@@ -1008,7 +1073,7 @@ class Midy {
1008
1073
  case 32:
1009
1074
  return this.setBankLSB(channelNumber, value);
1010
1075
  case 38:
1011
- return this.setDataEntry(channelNumber, value, false);
1076
+ return this.dataEntryLSB(channelNumber, value);
1012
1077
  case 64:
1013
1078
  return this.setSustainPedal(channelNumber, value);
1014
1079
  case 65:
@@ -1025,13 +1090,13 @@ class Midy {
1025
1090
  case 78:
1026
1091
  return this.setVibratoDelay(channelNumber, value);
1027
1092
  case 91:
1028
- return this.setReverb(channelNumber, value);
1093
+ return this.setReverbSendLevel(channelNumber, value);
1029
1094
  case 93:
1030
- return this.setChorus(channelNumber, value);
1095
+ return this.setChorusSendLevel(channelNumber, value);
1031
1096
  case 96: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1032
- return incrementRPNValue(channelNumber);
1097
+ return this.dataIncrement(channelNumber);
1033
1098
  case 97: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1034
- return decrementRPNValue(channelNumber);
1099
+ return this.dataDecrement(channelNumber);
1035
1100
  case 100:
1036
1101
  return this.setRPNLSB(channelNumber, value);
1037
1102
  case 101:
@@ -1057,22 +1122,24 @@ class Midy {
1057
1122
  setBankMSB(channelNumber, msb) {
1058
1123
  this.channels[channelNumber].bankMSB = msb;
1059
1124
  }
1060
- setModulation(channelNumber, modulation) {
1125
+ updateModulation(channel) {
1061
1126
  const now = this.audioContext.currentTime;
1062
- const channel = this.channels[channelNumber];
1063
- channel.modulation = (modulation / 127) *
1064
- (channel.modulationDepthRange * 100);
1065
1127
  const activeNotes = this.getActiveNotes(channel, now);
1066
1128
  activeNotes.forEach((activeNote) => {
1067
1129
  if (activeNote.modLFO) {
1068
- activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
1069
- channel.modulation, now);
1130
+ const { gainNode, instrumentKey } = activeNote;
1131
+ gainNode.gain.setValueAtTime(this.cbToRatio(instrumentKey.modLfoToVolume + channel.modulation), now);
1070
1132
  }
1071
1133
  else {
1072
1134
  this.startModulation(channel, activeNote, now);
1073
1135
  }
1074
1136
  });
1075
1137
  }
1138
+ setModulation(channelNumber, modulation) {
1139
+ const channel = this.channels[channelNumber];
1140
+ channel.modulation = (modulation / 127) * channel.modulationDepthRange;
1141
+ this.updateModulation(channel);
1142
+ }
1076
1143
  setPortamentoTime(channelNumber, portamentoTime) {
1077
1144
  this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1078
1145
  }
@@ -1101,6 +1168,10 @@ class Midy {
1101
1168
  setBankLSB(channelNumber, lsb) {
1102
1169
  this.channels[channelNumber].bankLSB = lsb;
1103
1170
  }
1171
+ dataEntryLSB(channelNumber, value) {
1172
+ this.channels[channelNumber].dataLSB = value;
1173
+ this.handleRPN(channelNumber, 0);
1174
+ }
1104
1175
  updateChannelGain(channel) {
1105
1176
  const now = this.audioContext.currentTime;
1106
1177
  const volume = channel.volume * channel.expression;
@@ -1122,7 +1193,7 @@ class Midy {
1122
1193
  setPortamento(channelNumber, value) {
1123
1194
  this.channels[channelNumber].portamento = value >= 64;
1124
1195
  }
1125
- setReverb(channelNumber, reverb) {
1196
+ setReverbSendLevel(channelNumber, reverb) {
1126
1197
  const now = this.audioContext.currentTime;
1127
1198
  const channel = this.channels[channelNumber];
1128
1199
  const reverbEffect = channel.reverbEffect;
@@ -1132,7 +1203,7 @@ class Midy {
1132
1203
  reverbEffect.wetGain.gain.cancelScheduledValues(now);
1133
1204
  reverbEffect.wetGain.gain.setValueAtTime(channel.reverb, now);
1134
1205
  }
1135
- setChorus(channelNumber, chorus) {
1206
+ setChorusSendLevel(channelNumber, chorus) {
1136
1207
  const channel = this.channels[channelNumber];
1137
1208
  channel.chorus = chorus / 127;
1138
1209
  channel.chorusEffect.lfoGain = channel.chorus;
@@ -1198,31 +1269,34 @@ class Midy {
1198
1269
  channel.dataMSB = minMSB;
1199
1270
  }
1200
1271
  }
1201
- // TODO: support 3-4?
1202
1272
  handleRPN(channelNumber, value) {
1203
1273
  const channel = this.channels[channelNumber];
1204
1274
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1205
1275
  switch (rpn) {
1206
1276
  case 0:
1207
1277
  channel.dataLSB += value;
1208
- this.handlePitchBendRangeMessage(channelNumber);
1278
+ this.handlePitchBendRangeRPN(channelNumber);
1209
1279
  break;
1210
1280
  case 1:
1211
1281
  channel.dataLSB += value;
1212
- this.handleFineTuningMessage(channelNumber);
1282
+ this.handleFineTuningRPN(channelNumber);
1213
1283
  break;
1214
1284
  case 2:
1215
1285
  channel.dataMSB += value;
1216
- this.handleCoarseTuningMessage(channelNumber);
1286
+ this.handleCoarseTuningRPN(channelNumber);
1287
+ break;
1288
+ case 5:
1289
+ channel.dataLSB += value;
1290
+ this.handleModulationDepthRangeRPN(channelNumber);
1217
1291
  break;
1218
1292
  default:
1219
1293
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1220
1294
  }
1221
1295
  }
1222
- incrementRPNValue(channelNumber) {
1296
+ dataIncrement(channelNumber) {
1223
1297
  this.handleRPN(channelNumber, 1);
1224
1298
  }
1225
- decrementRPNValue(channelNumber) {
1299
+ dataDecrement(channelNumber) {
1226
1300
  this.handleRPN(channelNumber, -1);
1227
1301
  }
1228
1302
  setRPNMSB(channelNumber, value) {
@@ -1231,9 +1305,8 @@ class Midy {
1231
1305
  setRPNLSB(channelNumber, value) {
1232
1306
  this.channels[channelNumber].rpnLSB = value;
1233
1307
  }
1234
- setDataEntry(channelNumber, value, isMSB) {
1235
- const channel = this.channels[channelNumber];
1236
- isMSB ? channel.dataMSB = value : channel.dataLSB = value;
1308
+ dataEntryMSB(channelNumber, value) {
1309
+ this.channels[channelNumber].dataMSB = value;
1237
1310
  this.handleRPN(channelNumber, 0);
1238
1311
  }
1239
1312
  updateDetune(channel, detuneChange) {
@@ -1247,7 +1320,7 @@ class Midy {
1247
1320
  .setValueAtTime(detune, now);
1248
1321
  });
1249
1322
  }
1250
- handlePitchBendRangeMessage(channelNumber) {
1323
+ handlePitchBendRangeRPN(channelNumber) {
1251
1324
  const channel = this.channels[channelNumber];
1252
1325
  this.limitData(channel, 0, 127, 0, 99);
1253
1326
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
@@ -1261,7 +1334,7 @@ class Midy {
1261
1334
  channel.pitchBend * 100;
1262
1335
  this.updateDetune(channel, detuneChange);
1263
1336
  }
1264
- handleFineTuningMessage(channelNumber) {
1337
+ handleFineTuningRPN(channelNumber) {
1265
1338
  const channel = this.channels[channelNumber];
1266
1339
  this.limitData(channel, 0, 127, 0, 127);
1267
1340
  const fineTuning = (channel.dataMSB * 128 + channel.dataLSB - 8192) / 8192;
@@ -1269,9 +1342,12 @@ class Midy {
1269
1342
  }
1270
1343
  setFineTuning(channelNumber, fineTuning) {
1271
1344
  const channel = this.channels[channelNumber];
1345
+ const prevFineTuning = channel.fineTuning;
1272
1346
  channel.fineTuning = fineTuning;
1347
+ const detuneChange = channel.fineTuning - prevFineTuning;
1348
+ this.updateDetune(channel, detuneChange);
1273
1349
  }
1274
- handleCoarseTuningMessage(channelNumber) {
1350
+ handleCoarseTuningRPN(channelNumber) {
1275
1351
  const channel = this.channels[channelNumber];
1276
1352
  this.limitDataMSB(channel, 0, 127);
1277
1353
  const coarseTuning = channel.dataMSB - 64;
@@ -1279,7 +1355,22 @@ class Midy {
1279
1355
  }
1280
1356
  setCoarseTuning(channelNumber, coarseTuning) {
1281
1357
  const channel = this.channels[channelNumber];
1282
- channel.fineTuning = coarseTuning;
1358
+ const prevCoarseTuning = channel.coarseTuning;
1359
+ channel.coarseTuning = coarseTuning;
1360
+ const detuneChange = channel.coarseTuning - prevCoarseTuning;
1361
+ this.updateDetune(channel, detuneChange);
1362
+ }
1363
+ handleModulationDepthRangeRPN(channelNumber) {
1364
+ const channel = this.channels[channelNumber];
1365
+ this.limitData(channel, 0, 127, 0, 127);
1366
+ const modulationDepthRange = dataMSB + dataLSB / 128;
1367
+ this.setModulationDepthRange(channelNumber, modulationDepthRange);
1368
+ }
1369
+ setModulationDepthRange(channelNumber, modulationDepthRange) {
1370
+ const channel = this.channels[channelNumber];
1371
+ channel.modulationDepthRange = modulationDepthRange;
1372
+ channel.modulation = (modulation / 127) * channel.modulationDepthRange;
1373
+ this.updateModulation(channel);
1283
1374
  }
1284
1375
  allSoundOff(channelNumber) {
1285
1376
  const now = this.audioContext.currentTime;
@@ -1370,9 +1461,9 @@ class Midy {
1370
1461
  switch (data[3]) {
1371
1462
  case 1:
1372
1463
  return this.handleMasterVolumeSysEx(data);
1373
- case 3:
1464
+ case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
1374
1465
  return this.handleMasterFineTuningSysEx(data);
1375
- case 4:
1466
+ case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
1376
1467
  return this.handleMasterCoarseTuningSysEx(data);
1377
1468
  // case 5: // TODO: Global Parameter Control
1378
1469
  default:
@@ -1498,9 +1589,9 @@ Object.defineProperty(Midy, "channelSettings", {
1498
1589
  dataLSB: 0,
1499
1590
  program: 0,
1500
1591
  pitchBend: 0,
1501
- fineTuning: 0,
1502
- coarseTuning: 0,
1503
- modulationDepthRange: 0.5,
1592
+ fineTuning: 0, // cb
1593
+ coarseTuning: 0, // cb
1594
+ modulationDepthRange: 0.5, // cb
1504
1595
  }
1505
1596
  });
1506
1597
  Object.defineProperty(Midy, "effectSettings", {