@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/esm/midy.js CHANGED
@@ -51,7 +51,7 @@ class Note {
51
51
  }
52
52
  }
53
53
  export class Midy {
54
- constructor(audioContext) {
54
+ constructor(audioContext, options = this.defaultOptions) {
55
55
  Object.defineProperty(this, "ticksPerBeat", {
56
56
  enumerable: true,
57
57
  configurable: true,
@@ -75,13 +75,13 @@ export class Midy {
75
75
  configurable: true,
76
76
  writable: true,
77
77
  value: 0
78
- });
78
+ }); // cb
79
79
  Object.defineProperty(this, "masterCoarseTuning", {
80
80
  enumerable: true,
81
81
  configurable: true,
82
82
  writable: true,
83
83
  value: 0
84
- });
84
+ }); // cb
85
85
  Object.defineProperty(this, "mono", {
86
86
  enumerable: true,
87
87
  configurable: true,
@@ -184,7 +184,19 @@ export class Midy {
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 Midy {
225
237
  this.totalTime = this.calcTotalTime();
226
238
  }
227
239
  setChannelAudioNodes(audioContext) {
228
- const { gainLeft, gainRight } = this.panToGain(Midy.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,16 +256,16 @@ export class Midy {
249
256
  createChannels(audioContext) {
250
257
  const channels = Array.from({ length: 16 }, () => {
251
258
  return {
252
- ...Midy.channelSettings,
253
- ...Midy.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
  polyphonicKeyPressure: {
258
- ...Midy.controllerDestinationSettings,
265
+ ...this.constructor.controllerDestinationSettings,
259
266
  },
260
267
  channelPressure: {
261
- ...Midy.controllerDestinationSettings,
268
+ ...this.constructor.controllerDestinationSettings,
262
269
  },
263
270
  };
264
271
  });
@@ -589,8 +596,12 @@ export class Midy {
589
596
  }
590
597
  return noteList[0];
591
598
  }
592
- createReverbEffect(audioContext, options = {}) {
599
+ createConvolutionReverb(audioContext, options = {}) {
593
600
  const { decay = 0.8, preDecay = 0, } = options;
601
+ const input = new GainNode(audioContext);
602
+ const output = new GainNode(audioContext);
603
+ const dryGain = new GainNode(audioContext);
604
+ const wetGain = new GainNode(audioContext);
594
605
  const sampleRate = audioContext.sampleRate;
595
606
  const length = sampleRate * decay;
596
607
  const impulse = new AudioBuffer({
@@ -604,27 +615,82 @@ export class Midy {
604
615
  for (let i = 0; i < preDecayLength; i++) {
605
616
  channelData[i] = Math.random() * 2 - 1;
606
617
  }
618
+ const attenuationFactor = 1 / (sampleRate * decay);
607
619
  for (let i = preDecayLength; i < length; i++) {
608
- const attenuation = Math.exp(-(i - preDecayLength) / sampleRate / decay);
620
+ const attenuation = Math.exp(-(i - preDecayLength) * attenuationFactor);
609
621
  channelData[i] = (Math.random() * 2 - 1) * attenuation;
610
622
  }
611
623
  }
612
624
  const convolverNode = new ConvolverNode(audioContext, {
613
625
  buffer: impulse,
614
626
  });
615
- const dryGain = new GainNode(audioContext);
616
- const wetGain = new GainNode(audioContext);
627
+ input.connect(convolverNode);
617
628
  convolverNode.connect(wetGain);
629
+ wetGain.connect(output);
630
+ dryGain.connect(output);
618
631
  return {
619
- convolverNode,
632
+ input,
633
+ output,
620
634
  dryGain,
621
635
  wetGain,
636
+ convolverNode,
622
637
  };
623
638
  }
639
+ createCombFilter(audioContext, input, delay, feedback) {
640
+ const delayNode = new DelayNode(audioContext, {
641
+ maxDelayTime: delay,
642
+ delayTime: delay,
643
+ });
644
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
645
+ input.connect(delayNode);
646
+ delayNode.connect(feedbackGain);
647
+ feedbackGain.connect(delayNode);
648
+ return delayNode;
649
+ }
650
+ createAllpassFilter(audioContext, input, delay, feedback) {
651
+ const delayNode = new DelayNode(audioContext, {
652
+ maxDelayTime: delay,
653
+ delayTime: delay,
654
+ });
655
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
656
+ const passGain = new GainNode(audioContext, { gain: 1 - feedback });
657
+ input.connect(delayNode);
658
+ delayNode.connect(feedbackGain);
659
+ feedbackGain.connect(delayNode);
660
+ delayNode.connect(passGain);
661
+ return passGain;
662
+ }
663
+ // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
664
+ // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
665
+ createSchroederReverb(audioContext, options = {}) {
666
+ 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;
667
+ const input = new GainNode(audioContext);
668
+ const output = new GainNode(audioContext);
669
+ const mergerGain = new GainNode(audioContext, {
670
+ gain: 1 / (combDelays.length * 2),
671
+ });
672
+ const dryGain = new GainNode(audioContext, { gain: 1 - mix });
673
+ const wetGain = new GainNode(audioContext, { gain: mix });
674
+ for (let i = 0; i < combDelays.length; i++) {
675
+ const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
676
+ comb.connect(mergerGain);
677
+ }
678
+ const allpasses = [];
679
+ for (let i = 0; i < allpassDelays.length; i++) {
680
+ const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
681
+ allpasses.push(allpass);
682
+ }
683
+ allpasses.at(-1).connect(wetGain);
684
+ input.connect(dryGain);
685
+ dryGain.connect(output);
686
+ wetGain.connect(output);
687
+ return { input, output, dryGain, wetGain };
688
+ }
624
689
  createChorusEffect(audioContext, options = {}) {
625
690
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
626
691
  const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
627
692
  const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
693
+ const output = new GainNode(audioContext);
628
694
  const chorusGains = [];
629
695
  const delayNodes = [];
630
696
  const baseGain = 1 / chorusCount;
@@ -634,50 +700,47 @@ export class Midy {
634
700
  const delayNode = new DelayNode(audioContext, {
635
701
  maxDelayTime: delayTime,
636
702
  });
637
- delayNodes.push(delayNode);
638
703
  const chorusGain = new GainNode(audioContext, { gain: baseGain });
704
+ delayNodes.push(delayNode);
639
705
  chorusGains.push(chorusGain);
640
- lfo.connect(lfoGain);
641
706
  lfoGain.connect(delayNode.delayTime);
642
707
  delayNode.connect(chorusGain);
708
+ chorusGain.connect(output);
643
709
  }
710
+ lfo.connect(lfoGain);
711
+ lfo.start();
644
712
  return {
645
713
  lfo,
646
714
  lfoGain,
647
715
  delayNodes,
648
716
  chorusGains,
717
+ output,
649
718
  };
650
719
  }
651
- connectNoteEffects(channel, gainNode) {
720
+ connectEffects(channel, gainNode) {
721
+ gainNode.connect(channel.merger);
652
722
  if (channel.reverb === 0) {
653
723
  if (channel.chorus === 0) { // no effect
654
- gainNode.connect(channel.gainL);
655
- gainNode.connect(channel.gainR);
724
+ channel.merger.connect(this.masterGain);
656
725
  }
657
726
  else { // chorus
658
727
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
659
- gainNode.connect(delayNode);
660
- });
661
- channel.chorusEffect.chorusGains.forEach((chorusGain) => {
662
- chorusGain.connect(channel.gainL);
663
- chorusGain.connect(channel.gainR);
728
+ channel.merger.connect(delayNode);
664
729
  });
730
+ channel.chorusEffect.output.connect(this.masterGain);
665
731
  }
666
732
  }
667
733
  else {
668
734
  if (channel.chorus === 0) { // reverb
669
- gainNode.connect(channel.reverbEffect.convolverNode);
670
- gainNode.connect(channel.reverbEffect.dryGain);
735
+ channel.merger.connect(channel.reverbEffect.input);
736
+ channel.reverbEffect.output.connect(this.masterGain);
671
737
  }
672
738
  else { // reverb + chorus
673
- gainNode.connect(channel.reverbEffect.convolverNode);
674
- gainNode.connect(channel.reverbEffect.dryGain);
675
739
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
676
- gainNode.connect(delayNode);
677
- });
678
- channel.chorusEffect.chorusGains.forEach((chorusGain) => {
679
- chorusGain.connect(channel.reverbEffect.convolverNode);
740
+ channel.merger.connect(delayNode);
680
741
  });
742
+ channel.merger.connect(channel.reverbEffect.input);
743
+ channel.reverbEffect.output.connect(this.masterGain);
681
744
  }
682
745
  }
683
746
  }
@@ -748,7 +811,7 @@ export class Midy {
748
811
  startModulation(channel, note, time) {
749
812
  const { instrumentKey } = note;
750
813
  note.modLFOGain = new GainNode(this.audioContext, {
751
- gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
814
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume + channel.modulation),
752
815
  });
753
816
  note.modLFO = new OscillatorNode(this.audioContext, {
754
817
  frequency: this.centToHz(instrumentKey.freqModLFO),
@@ -817,7 +880,7 @@ export class Midy {
817
880
  if (!instrumentKey)
818
881
  return;
819
882
  const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
820
- this.connectNoteEffects(channel, note.gainNode);
883
+ this.connectEffects(channel, note.gainNode);
821
884
  if (channel.sostenutoPedal) {
822
885
  channel.sostenutoNotes.set(noteNumber, note);
823
886
  }
@@ -841,46 +904,48 @@ export class Midy {
841
904
  return;
842
905
  if (!channel.scheduledNotes.has(noteNumber))
843
906
  return;
844
- const targetNotes = channel.scheduledNotes.get(noteNumber);
845
- for (let i = 0; i < targetNotes.length; i++) {
846
- const targetNote = targetNotes[i];
847
- if (!targetNote)
907
+ const scheduledNotes = channel.scheduledNotes.get(noteNumber);
908
+ for (let i = 0; i < scheduledNotes.length; i++) {
909
+ const note = scheduledNotes[i];
910
+ if (!note)
848
911
  continue;
849
- if (targetNote.ending)
912
+ if (note.ending)
850
913
  continue;
851
- const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, vibLFO, vibLFOGain, instrumentKey, } = targetNote;
852
914
  const velocityRate = (velocity + 127) / 127;
853
- const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
854
- gainNode.gain.cancelScheduledValues(stopTime);
855
- gainNode.gain.linearRampToValueAtTime(0, volEndTime);
915
+ const volEndTime = stopTime +
916
+ note.instrumentKey.volRelease * velocityRate;
917
+ note.gainNode.gain
918
+ .cancelScheduledValues(stopTime)
919
+ .linearRampToValueAtTime(0, volEndTime);
856
920
  const maxFreq = this.audioContext.sampleRate / 2;
857
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
921
+ const baseFreq = this.centToHz(note.instrumentKey.initialFilterFc);
858
922
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
859
- const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
860
- filterNode.frequency
923
+ const modEndTime = stopTime +
924
+ note.instrumentKey.modRelease * velocityRate;
925
+ note.filterNode.frequency
861
926
  .cancelScheduledValues(stopTime)
862
927
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
863
- targetNote.ending = true;
928
+ note.ending = true;
864
929
  this.scheduleTask(() => {
865
- bufferSource.loop = false;
930
+ note.bufferSource.loop = false;
866
931
  }, stopTime);
867
932
  return new Promise((resolve) => {
868
- bufferSource.onended = () => {
869
- targetNotes[i] = null;
870
- bufferSource.disconnect(0);
871
- filterNode.disconnect(0);
872
- gainNode.disconnect(0);
873
- if (modLFOGain)
874
- modLFOGain.disconnect(0);
875
- if (vibLFOGain)
876
- vibLFOGain.disconnect(0);
877
- if (modLFO)
878
- modLFO.stop();
879
- if (vibLFO)
880
- vibLFO.stop();
933
+ note.bufferSource.onended = () => {
934
+ scheduledNotes[i] = null;
935
+ note.bufferSource.disconnect();
936
+ note.filterNode.disconnect();
937
+ note.gainNode.disconnect();
938
+ if (note.modLFOGain)
939
+ note.modLFOGain.disconnect();
940
+ if (note.vibLFOGain)
941
+ note.vibLFOGain.disconnect();
942
+ if (note.modLFO)
943
+ note.modLFO.stop();
944
+ if (note.vibLFO)
945
+ note.vibLFO.stop();
881
946
  resolve();
882
947
  };
883
- bufferSource.stop(volEndTime);
948
+ note.bufferSource.stop(volEndTime);
884
949
  });
885
950
  }
886
951
  }
@@ -995,7 +1060,7 @@ export class Midy {
995
1060
  case 5:
996
1061
  return this.setPortamentoTime(channelNumber, value);
997
1062
  case 6:
998
- return this.setDataEntry(channelNumber, value, true);
1063
+ return this.dataEntryMSB(channelNumber, value);
999
1064
  case 7:
1000
1065
  return this.setVolume(channelNumber, value);
1001
1066
  case 10:
@@ -1005,7 +1070,7 @@ export class Midy {
1005
1070
  case 32:
1006
1071
  return this.setBankLSB(channelNumber, value);
1007
1072
  case 38:
1008
- return this.setDataEntry(channelNumber, value, false);
1073
+ return this.dataEntryLSB(channelNumber, value);
1009
1074
  case 64:
1010
1075
  return this.setSustainPedal(channelNumber, value);
1011
1076
  case 65:
@@ -1022,13 +1087,13 @@ export class Midy {
1022
1087
  case 78:
1023
1088
  return this.setVibratoDelay(channelNumber, value);
1024
1089
  case 91:
1025
- return this.setReverb(channelNumber, value);
1090
+ return this.setReverbSendLevel(channelNumber, value);
1026
1091
  case 93:
1027
- return this.setChorus(channelNumber, value);
1092
+ return this.setChorusSendLevel(channelNumber, value);
1028
1093
  case 96: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1029
- return incrementRPNValue(channelNumber);
1094
+ return this.dataIncrement(channelNumber);
1030
1095
  case 97: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1031
- return decrementRPNValue(channelNumber);
1096
+ return this.dataDecrement(channelNumber);
1032
1097
  case 100:
1033
1098
  return this.setRPNLSB(channelNumber, value);
1034
1099
  case 101:
@@ -1054,22 +1119,24 @@ export class Midy {
1054
1119
  setBankMSB(channelNumber, msb) {
1055
1120
  this.channels[channelNumber].bankMSB = msb;
1056
1121
  }
1057
- setModulation(channelNumber, modulation) {
1122
+ updateModulation(channel) {
1058
1123
  const now = this.audioContext.currentTime;
1059
- const channel = this.channels[channelNumber];
1060
- channel.modulation = (modulation / 127) *
1061
- (channel.modulationDepthRange * 100);
1062
1124
  const activeNotes = this.getActiveNotes(channel, now);
1063
1125
  activeNotes.forEach((activeNote) => {
1064
1126
  if (activeNote.modLFO) {
1065
- activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
1066
- channel.modulation, now);
1127
+ const { gainNode, instrumentKey } = activeNote;
1128
+ gainNode.gain.setValueAtTime(this.cbToRatio(instrumentKey.modLfoToVolume + channel.modulation), now);
1067
1129
  }
1068
1130
  else {
1069
1131
  this.startModulation(channel, activeNote, now);
1070
1132
  }
1071
1133
  });
1072
1134
  }
1135
+ setModulation(channelNumber, modulation) {
1136
+ const channel = this.channels[channelNumber];
1137
+ channel.modulation = (modulation / 127) * channel.modulationDepthRange;
1138
+ this.updateModulation(channel);
1139
+ }
1073
1140
  setPortamentoTime(channelNumber, portamentoTime) {
1074
1141
  this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1075
1142
  }
@@ -1098,6 +1165,10 @@ export class Midy {
1098
1165
  setBankLSB(channelNumber, lsb) {
1099
1166
  this.channels[channelNumber].bankLSB = lsb;
1100
1167
  }
1168
+ dataEntryLSB(channelNumber, value) {
1169
+ this.channels[channelNumber].dataLSB = value;
1170
+ this.handleRPN(channelNumber, 0);
1171
+ }
1101
1172
  updateChannelGain(channel) {
1102
1173
  const now = this.audioContext.currentTime;
1103
1174
  const volume = channel.volume * channel.expression;
@@ -1119,7 +1190,7 @@ export class Midy {
1119
1190
  setPortamento(channelNumber, value) {
1120
1191
  this.channels[channelNumber].portamento = value >= 64;
1121
1192
  }
1122
- setReverb(channelNumber, reverb) {
1193
+ setReverbSendLevel(channelNumber, reverb) {
1123
1194
  const now = this.audioContext.currentTime;
1124
1195
  const channel = this.channels[channelNumber];
1125
1196
  const reverbEffect = channel.reverbEffect;
@@ -1129,7 +1200,7 @@ export class Midy {
1129
1200
  reverbEffect.wetGain.gain.cancelScheduledValues(now);
1130
1201
  reverbEffect.wetGain.gain.setValueAtTime(channel.reverb, now);
1131
1202
  }
1132
- setChorus(channelNumber, chorus) {
1203
+ setChorusSendLevel(channelNumber, chorus) {
1133
1204
  const channel = this.channels[channelNumber];
1134
1205
  channel.chorus = chorus / 127;
1135
1206
  channel.chorusEffect.lfoGain = channel.chorus;
@@ -1195,31 +1266,34 @@ export class Midy {
1195
1266
  channel.dataMSB = minMSB;
1196
1267
  }
1197
1268
  }
1198
- // TODO: support 3-4?
1199
1269
  handleRPN(channelNumber, value) {
1200
1270
  const channel = this.channels[channelNumber];
1201
1271
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1202
1272
  switch (rpn) {
1203
1273
  case 0:
1204
1274
  channel.dataLSB += value;
1205
- this.handlePitchBendRangeMessage(channelNumber);
1275
+ this.handlePitchBendRangeRPN(channelNumber);
1206
1276
  break;
1207
1277
  case 1:
1208
1278
  channel.dataLSB += value;
1209
- this.handleFineTuningMessage(channelNumber);
1279
+ this.handleFineTuningRPN(channelNumber);
1210
1280
  break;
1211
1281
  case 2:
1212
1282
  channel.dataMSB += value;
1213
- this.handleCoarseTuningMessage(channelNumber);
1283
+ this.handleCoarseTuningRPN(channelNumber);
1284
+ break;
1285
+ case 5:
1286
+ channel.dataLSB += value;
1287
+ this.handleModulationDepthRangeRPN(channelNumber);
1214
1288
  break;
1215
1289
  default:
1216
1290
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1217
1291
  }
1218
1292
  }
1219
- incrementRPNValue(channelNumber) {
1293
+ dataIncrement(channelNumber) {
1220
1294
  this.handleRPN(channelNumber, 1);
1221
1295
  }
1222
- decrementRPNValue(channelNumber) {
1296
+ dataDecrement(channelNumber) {
1223
1297
  this.handleRPN(channelNumber, -1);
1224
1298
  }
1225
1299
  setRPNMSB(channelNumber, value) {
@@ -1228,9 +1302,8 @@ export class Midy {
1228
1302
  setRPNLSB(channelNumber, value) {
1229
1303
  this.channels[channelNumber].rpnLSB = value;
1230
1304
  }
1231
- setDataEntry(channelNumber, value, isMSB) {
1232
- const channel = this.channels[channelNumber];
1233
- isMSB ? channel.dataMSB = value : channel.dataLSB = value;
1305
+ dataEntryMSB(channelNumber, value) {
1306
+ this.channels[channelNumber].dataMSB = value;
1234
1307
  this.handleRPN(channelNumber, 0);
1235
1308
  }
1236
1309
  updateDetune(channel, detuneChange) {
@@ -1244,7 +1317,7 @@ export class Midy {
1244
1317
  .setValueAtTime(detune, now);
1245
1318
  });
1246
1319
  }
1247
- handlePitchBendRangeMessage(channelNumber) {
1320
+ handlePitchBendRangeRPN(channelNumber) {
1248
1321
  const channel = this.channels[channelNumber];
1249
1322
  this.limitData(channel, 0, 127, 0, 99);
1250
1323
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
@@ -1258,7 +1331,7 @@ export class Midy {
1258
1331
  channel.pitchBend * 100;
1259
1332
  this.updateDetune(channel, detuneChange);
1260
1333
  }
1261
- handleFineTuningMessage(channelNumber) {
1334
+ handleFineTuningRPN(channelNumber) {
1262
1335
  const channel = this.channels[channelNumber];
1263
1336
  this.limitData(channel, 0, 127, 0, 127);
1264
1337
  const fineTuning = (channel.dataMSB * 128 + channel.dataLSB - 8192) / 8192;
@@ -1266,9 +1339,12 @@ export class Midy {
1266
1339
  }
1267
1340
  setFineTuning(channelNumber, fineTuning) {
1268
1341
  const channel = this.channels[channelNumber];
1342
+ const prevFineTuning = channel.fineTuning;
1269
1343
  channel.fineTuning = fineTuning;
1344
+ const detuneChange = channel.fineTuning - prevFineTuning;
1345
+ this.updateDetune(channel, detuneChange);
1270
1346
  }
1271
- handleCoarseTuningMessage(channelNumber) {
1347
+ handleCoarseTuningRPN(channelNumber) {
1272
1348
  const channel = this.channels[channelNumber];
1273
1349
  this.limitDataMSB(channel, 0, 127);
1274
1350
  const coarseTuning = channel.dataMSB - 64;
@@ -1276,7 +1352,22 @@ export class Midy {
1276
1352
  }
1277
1353
  setCoarseTuning(channelNumber, coarseTuning) {
1278
1354
  const channel = this.channels[channelNumber];
1279
- channel.fineTuning = coarseTuning;
1355
+ const prevCoarseTuning = channel.coarseTuning;
1356
+ channel.coarseTuning = coarseTuning;
1357
+ const detuneChange = channel.coarseTuning - prevCoarseTuning;
1358
+ this.updateDetune(channel, detuneChange);
1359
+ }
1360
+ handleModulationDepthRangeRPN(channelNumber) {
1361
+ const channel = this.channels[channelNumber];
1362
+ this.limitData(channel, 0, 127, 0, 127);
1363
+ const modulationDepthRange = dataMSB + dataLSB / 128;
1364
+ this.setModulationDepthRange(channelNumber, modulationDepthRange);
1365
+ }
1366
+ setModulationDepthRange(channelNumber, modulationDepthRange) {
1367
+ const channel = this.channels[channelNumber];
1368
+ channel.modulationDepthRange = modulationDepthRange;
1369
+ channel.modulation = (modulation / 127) * channel.modulationDepthRange;
1370
+ this.updateModulation(channel);
1280
1371
  }
1281
1372
  allSoundOff(channelNumber) {
1282
1373
  const now = this.audioContext.currentTime;
@@ -1367,9 +1458,9 @@ export class Midy {
1367
1458
  switch (data[3]) {
1368
1459
  case 1:
1369
1460
  return this.handleMasterVolumeSysEx(data);
1370
- case 3:
1461
+ case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
1371
1462
  return this.handleMasterFineTuningSysEx(data);
1372
- case 4:
1463
+ case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
1373
1464
  return this.handleMasterCoarseTuningSysEx(data);
1374
1465
  // case 5: // TODO: Global Parameter Control
1375
1466
  default:
@@ -1494,9 +1585,9 @@ Object.defineProperty(Midy, "channelSettings", {
1494
1585
  dataLSB: 0,
1495
1586
  program: 0,
1496
1587
  pitchBend: 0,
1497
- fineTuning: 0,
1498
- coarseTuning: 0,
1499
- modulationDepthRange: 0.5,
1588
+ fineTuning: 0, // cb
1589
+ coarseTuning: 0, // cb
1590
+ modulationDepthRange: 0.5, // cb
1500
1591
  }
1501
1592
  });
1502
1593
  Object.defineProperty(Midy, "effectSettings", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marmooo/midy",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
5
5
  "repository": {
6
6
  "type": "git",