@marmooo/midy 0.3.6 → 0.3.7

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
@@ -243,13 +243,13 @@ class Midy {
243
243
  configurable: true,
244
244
  writable: true,
245
245
  value: 0
246
- }); // cb
246
+ }); // cent
247
247
  Object.defineProperty(this, "masterCoarseTuning", {
248
248
  enumerable: true,
249
249
  configurable: true,
250
250
  writable: true,
251
251
  value: 0
252
- }); // cb
252
+ }); // cent
253
253
  Object.defineProperty(this, "reverb", {
254
254
  enumerable: true,
255
255
  configurable: true,
@@ -374,6 +374,12 @@ class Midy {
374
374
  writable: true,
375
375
  value: false
376
376
  });
377
+ Object.defineProperty(this, "playPromise", {
378
+ enumerable: true,
379
+ configurable: true,
380
+ writable: true,
381
+ value: void 0
382
+ });
377
383
  Object.defineProperty(this, "timeline", {
378
384
  enumerable: true,
379
385
  configurable: true,
@@ -543,7 +549,7 @@ class Midy {
543
549
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
544
550
  channel.channelPressureTable.fill(-1);
545
551
  channel.polyphonicKeyPressureTable.fill(-1);
546
- channel.keyBasedInstrumentControlTable.fill(-1);
552
+ channel.keyBasedTable.fill(-1);
547
553
  }
548
554
  createChannels(audioContext) {
549
555
  const channels = Array.from({ length: this.numChannels }, () => {
@@ -560,7 +566,7 @@ class Midy {
560
566
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
561
567
  channelPressureTable: new Int8Array(6).fill(-1),
562
568
  polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
563
- keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
569
+ keyBasedTable: new Int8Array(128 * 128).fill(-1),
564
570
  keyBasedGainLs: new Array(128),
565
571
  keyBasedGainRs: new Array(128),
566
572
  };
@@ -638,72 +644,80 @@ class Midy {
638
644
  }
639
645
  return 0;
640
646
  }
641
- playNotes() {
642
- return new Promise((resolve) => {
643
- this.isPlaying = true;
644
- this.isPaused = false;
645
- this.startTime = this.audioContext.currentTime;
646
- let queueIndex = this.getQueueIndex(this.resumeTime);
647
- let resumeTime = this.resumeTime - this.startTime;
647
+ resetAllStates() {
648
+ this.exclusiveClassNotes.fill(undefined);
649
+ this.drumExclusiveClassNotes.fill(undefined);
650
+ this.voiceCache.clear();
651
+ for (let i = 0; i < this.channels.length; i++) {
652
+ this.channels[i].scheduledNotes = [];
653
+ this.resetChannelStates(i);
654
+ }
655
+ }
656
+ updateStates(queueIndex, nextQueueIndex) {
657
+ if (nextQueueIndex < queueIndex)
658
+ queueIndex = 0;
659
+ for (let i = queueIndex; i < nextQueueIndex; i++) {
660
+ const event = this.timeline[i];
661
+ switch (event.type) {
662
+ case "controller":
663
+ this.setControlChange(event.channel, event.controllerType, event.value, 0);
664
+ break;
665
+ case "programChange":
666
+ this.setProgramChange(event.channel, event.programNumber, 0);
667
+ break;
668
+ case "pitchBend":
669
+ this.setPitchBend(event.channel, event.value + 8192, 0);
670
+ break;
671
+ case "sysEx":
672
+ this.handleSysEx(event.data, 0);
673
+ }
674
+ }
675
+ }
676
+ async playNotes() {
677
+ if (this.audioContext.state === "suspended") {
678
+ await this.audioContext.resume();
679
+ }
680
+ this.isPlaying = true;
681
+ this.isPaused = false;
682
+ this.startTime = this.audioContext.currentTime;
683
+ let queueIndex = this.getQueueIndex(this.resumeTime);
684
+ let resumeTime = this.resumeTime - this.startTime;
685
+ let finished = false;
686
+ this.notePromises = [];
687
+ while (queueIndex < this.timeline.length) {
688
+ const now = this.audioContext.currentTime;
689
+ const t = now + resumeTime;
690
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
691
+ if (this.isPausing) {
692
+ await this.stopNotes(0, true, now);
693
+ await this.audioContext.suspend();
694
+ this.notePromises = [];
695
+ break;
696
+ }
697
+ else if (this.isStopping) {
698
+ await this.stopNotes(0, true, now);
699
+ await this.audioContext.suspend();
700
+ finished = true;
701
+ break;
702
+ }
703
+ else if (this.isSeeking) {
704
+ await this.stopNotes(0, true, now);
705
+ this.startTime = this.audioContext.currentTime;
706
+ const nextQueueIndex = this.getQueueIndex(this.resumeTime);
707
+ this.updateStates(queueIndex, nextQueueIndex);
708
+ queueIndex = nextQueueIndex;
709
+ resumeTime = this.resumeTime - this.startTime;
710
+ this.isSeeking = false;
711
+ continue;
712
+ }
713
+ const waitTime = now + this.noteCheckInterval;
714
+ await this.scheduleTask(() => { }, waitTime);
715
+ }
716
+ if (finished) {
648
717
  this.notePromises = [];
649
- const schedulePlayback = async () => {
650
- if (queueIndex >= this.timeline.length) {
651
- await Promise.all(this.notePromises);
652
- this.notePromises = [];
653
- this.exclusiveClassNotes.fill(undefined);
654
- this.drumExclusiveClassNotes.fill(undefined);
655
- this.voiceCache.clear();
656
- for (let i = 0; i < this.channels.length; i++) {
657
- this.channels[i].scheduledNotes = [];
658
- this.resetAllStates(i);
659
- }
660
- resolve();
661
- return;
662
- }
663
- const now = this.audioContext.currentTime;
664
- const t = now + resumeTime;
665
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
666
- if (this.isPausing) {
667
- await this.stopNotes(0, true, now);
668
- this.notePromises = [];
669
- this.isPausing = false;
670
- this.isPaused = true;
671
- resolve();
672
- return;
673
- }
674
- else if (this.isStopping) {
675
- await this.stopNotes(0, true, now);
676
- this.notePromises = [];
677
- this.exclusiveClassNotes.fill(undefined);
678
- this.drumExclusiveClassNotes.fill(undefined);
679
- this.voiceCache.clear();
680
- for (let i = 0; i < this.channels.length; i++) {
681
- this.channels[i].scheduledNotes = [];
682
- this.resetAllStates(i);
683
- }
684
- this.isStopping = false;
685
- this.isPaused = false;
686
- resolve();
687
- return;
688
- }
689
- else if (this.isSeeking) {
690
- this.stopNotes(0, true, now);
691
- this.exclusiveClassNotes.fill(undefined);
692
- this.drumExclusiveClassNotes.fill(undefined);
693
- this.startTime = this.audioContext.currentTime;
694
- queueIndex = this.getQueueIndex(this.resumeTime);
695
- resumeTime = this.resumeTime - this.startTime;
696
- this.isSeeking = false;
697
- await schedulePlayback();
698
- }
699
- else {
700
- const waitTime = now + this.noteCheckInterval;
701
- await this.scheduleTask(() => { }, waitTime);
702
- await schedulePlayback();
703
- }
704
- };
705
- schedulePlayback();
706
- });
718
+ this.resetAllStates();
719
+ }
720
+ this.isPlaying = false;
707
721
  }
708
722
  ticksToSecond(ticks, secondsPerBeat) {
709
723
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -840,26 +854,32 @@ class Midy {
840
854
  this.resumeTime = 0;
841
855
  if (this.voiceCounter.size === 0)
842
856
  this.cacheVoiceIds();
843
- await this.playNotes();
844
- this.isPlaying = false;
857
+ this.playPromise = this.playNotes();
858
+ await this.playPromise;
845
859
  }
846
- stop() {
860
+ async stop() {
847
861
  if (!this.isPlaying)
848
862
  return;
849
863
  this.isStopping = true;
864
+ await this.playPromise;
865
+ this.isStopping = false;
850
866
  }
851
- pause() {
867
+ async pause() {
852
868
  if (!this.isPlaying || this.isPaused)
853
869
  return;
854
870
  const now = this.audioContext.currentTime;
855
871
  this.resumeTime += now - this.startTime - this.startDelay;
856
872
  this.isPausing = true;
873
+ await this.playPromise;
874
+ this.isPausing = false;
875
+ this.isPaused = true;
857
876
  }
858
877
  async resume() {
859
878
  if (!this.isPaused)
860
879
  return;
861
- await this.playNotes();
862
- this.isPlaying = false;
880
+ this.playPromise = this.playNotes();
881
+ await this.playPromise;
882
+ this.isPaused = false;
863
883
  }
864
884
  seekTo(second) {
865
885
  this.resumeTime = second;
@@ -1167,28 +1187,29 @@ class Midy {
1167
1187
  return Math.exp(y);
1168
1188
  }
1169
1189
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1170
- const state = channel.state;
1171
1190
  const { voiceParams, startTime } = note;
1172
1191
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1173
1192
  (1 + this.getAmplitudeControl(channel, note));
1174
1193
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1175
1194
  const volDelay = startTime + voiceParams.volDelay;
1176
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1195
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1196
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1177
1197
  const volHold = volAttack + voiceParams.volHold;
1178
1198
  note.volumeEnvelopeNode.gain
1179
1199
  .cancelScheduledValues(scheduleTime)
1180
1200
  .setValueAtTime(sustainVolume, volHold);
1181
1201
  }
1182
1202
  setVolumeEnvelope(channel, note, scheduleTime) {
1183
- const state = channel.state;
1184
1203
  const { voiceParams, startTime } = note;
1185
1204
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1186
1205
  (1 + this.getAmplitudeControl(channel, note));
1187
1206
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1188
1207
  const volDelay = startTime + voiceParams.volDelay;
1189
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1208
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1209
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1190
1210
  const volHold = volAttack + voiceParams.volHold;
1191
- const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
1211
+ const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
1212
+ const volDecay = volHold + voiceParams.volDecay * decayTime;
1192
1213
  note.volumeEnvelopeNode.gain
1193
1214
  .cancelScheduledValues(scheduleTime)
1194
1215
  .setValueAtTime(0, startTime)
@@ -1231,14 +1252,13 @@ class Midy {
1231
1252
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1232
1253
  }
1233
1254
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1234
- const state = channel.state;
1235
1255
  const { voiceParams, startTime } = note;
1236
1256
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1237
1257
  const baseCent = voiceParams.initialFilterFc +
1238
1258
  this.getFilterCutoffControl(channel, note);
1239
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1240
- state.brightness * 2;
1241
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1259
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1260
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1261
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * brightness;
1242
1262
  const sustainFreq = baseFreq +
1243
1263
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1244
1264
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1252,15 +1272,14 @@ class Midy {
1252
1272
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1253
1273
  }
1254
1274
  setFilterEnvelope(channel, note, scheduleTime) {
1255
- const state = channel.state;
1256
1275
  const { voiceParams, startTime } = note;
1257
1276
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1258
1277
  const baseCent = voiceParams.initialFilterFc +
1259
1278
  this.getFilterCutoffControl(channel, note);
1260
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1261
- state.brightness * 2;
1279
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1280
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1262
1281
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1263
- softPedalFactor * state.brightness * 2;
1282
+ softPedalFactor * brightness;
1264
1283
  const sustainFreq = baseFreq +
1265
1284
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1266
1285
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1300,11 +1319,12 @@ class Midy {
1300
1319
  }
1301
1320
  startVibrato(channel, note, scheduleTime) {
1302
1321
  const { voiceParams } = note;
1303
- const state = channel.state;
1322
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1323
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1304
1324
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1305
- frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1325
+ frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
1306
1326
  });
1307
- note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1327
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1308
1328
  note.vibratoDepth = new GainNode(this.audioContext);
1309
1329
  this.setVibLfoToPitch(channel, note, scheduleTime);
1310
1330
  note.vibratoLFO.connect(note.vibratoDepth);
@@ -1337,9 +1357,10 @@ class Midy {
1337
1357
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1338
1358
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1339
1359
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1360
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
1340
1361
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1341
1362
  type: "lowpass",
1342
- Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1363
+ Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1343
1364
  });
1344
1365
  const prevNote = channel.scheduledNotes.at(-1);
1345
1366
  if (prevNote && prevNote.noteNumber !== noteNumber) {
@@ -1484,8 +1505,8 @@ class Midy {
1484
1505
  }
1485
1506
  }
1486
1507
  releaseNote(channel, note, endTime) {
1487
- const volRelease = endTime +
1488
- note.voiceParams.volRelease * channel.state.releaseTime * 2;
1508
+ const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1509
+ const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1489
1510
  const modRelease = endTime + note.voiceParams.modRelease;
1490
1511
  const stopTime = Math.min(volRelease, modRelease);
1491
1512
  note.filterNode.frequency
@@ -1630,6 +1651,7 @@ class Midy {
1630
1651
  break;
1631
1652
  }
1632
1653
  }
1654
+ channel.keyBasedTable.fill(-1);
1633
1655
  }
1634
1656
  setChannelPressure(channelNumber, value, scheduleTime) {
1635
1657
  const channel = this.channels[channelNumber];
@@ -1671,23 +1693,29 @@ class Midy {
1671
1693
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1672
1694
  this.getLFOPitchDepth(channel, note);
1673
1695
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1674
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1696
+ const depth = baseDepth * Math.sign(modLfoToPitch);
1675
1697
  note.modulationDepth.gain
1676
1698
  .cancelScheduledValues(scheduleTime)
1677
- .setValueAtTime(modulationDepth, scheduleTime);
1699
+ .setValueAtTime(depth, scheduleTime);
1678
1700
  }
1679
1701
  else {
1680
1702
  this.startModulation(channel, note, scheduleTime);
1681
1703
  }
1682
1704
  }
1683
1705
  setVibLfoToPitch(channel, note, scheduleTime) {
1684
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1685
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1686
- 2;
1687
- const vibratoDepthSign = 0 < vibLfoToPitch;
1688
- note.vibratoDepth.gain
1689
- .cancelScheduledValues(scheduleTime)
1690
- .setValueAtTime(vibratoDepth * vibratoDepthSign, scheduleTime);
1706
+ if (note.vibratoDepth) {
1707
+ const vibratoDepth = this.getKeyBasedValue(channel, note.noteNumber, 77) *
1708
+ 2;
1709
+ const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1710
+ const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
1711
+ const depth = baseDepth * Math.sign(vibLfoToPitch);
1712
+ note.vibratoDepth.gain
1713
+ .cancelScheduledValues(scheduleTime)
1714
+ .setValueAtTime(depth, scheduleTime);
1715
+ }
1716
+ else {
1717
+ this.startVibrato(channel, note, scheduleTime);
1718
+ }
1691
1719
  }
1692
1720
  setModLfoToFilterFc(channel, note, scheduleTime) {
1693
1721
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
@@ -1765,13 +1793,12 @@ class Midy {
1765
1793
  }
1766
1794
  }
1767
1795
  }
1768
- setDelayModLFO(note, scheduleTime) {
1769
- const startTime = note.startTime;
1770
- if (startTime < scheduleTime)
1771
- return;
1772
- note.modulationLFO.stop(scheduleTime);
1773
- note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1774
- note.modulationLFO.connect(note.filterDepth);
1796
+ setDelayModLFO(note) {
1797
+ const startTime = note.startTime + note.voiceParams.delayModLFO;
1798
+ try {
1799
+ note.modulationLFO.start(startTime);
1800
+ }
1801
+ catch { /* empty */ }
1775
1802
  }
1776
1803
  setFreqModLFO(note, scheduleTime) {
1777
1804
  const freqModLFO = note.voiceParams.freqModLFO;
@@ -1780,54 +1807,65 @@ class Midy {
1780
1807
  .setValueAtTime(freqModLFO, scheduleTime);
1781
1808
  }
1782
1809
  setFreqVibLFO(channel, note, scheduleTime) {
1810
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1783
1811
  const freqVibLFO = note.voiceParams.freqVibLFO;
1784
1812
  note.vibratoLFO.frequency
1785
1813
  .cancelScheduledValues(scheduleTime)
1786
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, scheduleTime);
1814
+ .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
1815
+ }
1816
+ setDelayVibLFO(channel, note) {
1817
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1818
+ const value = note.voiceParams.delayVibLFO;
1819
+ const startTime = note.startTime + value * vibratoDelay;
1820
+ try {
1821
+ note.vibratoLFO.start(startTime);
1822
+ }
1823
+ catch { /* empty */ }
1787
1824
  }
1788
1825
  createVoiceParamsHandlers() {
1789
1826
  return {
1790
- modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1827
+ modLfoToPitch: (channel, note, scheduleTime) => {
1791
1828
  if (0 < channel.state.modulationDepth) {
1792
1829
  this.setModLfoToPitch(channel, note, scheduleTime);
1793
1830
  }
1794
1831
  },
1795
- vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1832
+ vibLfoToPitch: (channel, note, scheduleTime) => {
1796
1833
  if (0 < channel.state.vibratoDepth) {
1797
1834
  this.setVibLfoToPitch(channel, note, scheduleTime);
1798
1835
  }
1799
1836
  },
1800
- modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1837
+ modLfoToFilterFc: (channel, note, scheduleTime) => {
1801
1838
  if (0 < channel.state.modulationDepth) {
1802
1839
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1803
1840
  }
1804
1841
  },
1805
- modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1842
+ modLfoToVolume: (channel, note, scheduleTime) => {
1806
1843
  if (0 < channel.state.modulationDepth) {
1807
1844
  this.setModLfoToVolume(channel, note, scheduleTime);
1808
1845
  }
1809
1846
  },
1810
- chorusEffectsSend: (channel, note, _prevValue, scheduleTime) => {
1847
+ chorusEffectsSend: (channel, note, scheduleTime) => {
1811
1848
  this.setChorusSend(channel, note, scheduleTime);
1812
1849
  },
1813
- reverbEffectsSend: (channel, note, _prevValue, scheduleTime) => {
1850
+ reverbEffectsSend: (channel, note, scheduleTime) => {
1814
1851
  this.setReverbSend(channel, note, scheduleTime);
1815
1852
  },
1816
- delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1817
- freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1818
- delayVibLFO: (channel, note, prevValue, scheduleTime) => {
1853
+ delayModLFO: (_channel, note, _scheduleTime) => {
1854
+ if (0 < channel.state.modulationDepth) {
1855
+ this.setDelayModLFO(note);
1856
+ }
1857
+ },
1858
+ freqModLFO: (_channel, note, scheduleTime) => {
1859
+ if (0 < channel.state.modulationDepth) {
1860
+ this.setFreqModLFO(note, scheduleTime);
1861
+ }
1862
+ },
1863
+ delayVibLFO: (channel, note, _scheduleTime) => {
1819
1864
  if (0 < channel.state.vibratoDepth) {
1820
- const vibratoDelay = channel.state.vibratoDelay * 2;
1821
- const prevStartTime = note.startTime + prevValue * vibratoDelay;
1822
- if (scheduleTime < prevStartTime)
1823
- return;
1824
- const value = note.voiceParams.delayVibLFO;
1825
- const startTime = note.startTime + value * vibratoDelay;
1826
- note.vibratoLFO.stop(scheduleTime);
1827
- note.vibratoLFO.start(startTime);
1865
+ setDelayVibLFO(channel, note);
1828
1866
  }
1829
1867
  },
1830
- freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1868
+ freqVibLFO: (channel, note, scheduleTime) => {
1831
1869
  if (0 < channel.state.vibratoDepth) {
1832
1870
  this.setFreqVibLFO(channel, note, scheduleTime);
1833
1871
  }
@@ -1856,7 +1894,7 @@ class Midy {
1856
1894
  continue;
1857
1895
  note.voiceParams[key] = value;
1858
1896
  if (key in this.voiceParamsHandlers) {
1859
- this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1897
+ this.voiceParamsHandlers[key](channel, note, scheduleTime);
1860
1898
  }
1861
1899
  else {
1862
1900
  if (volumeEnvelopeKeySet.has(key))
@@ -2035,13 +2073,13 @@ class Midy {
2035
2073
  .setValueAtTime(volume * gainRight, scheduleTime);
2036
2074
  }
2037
2075
  updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2038
- const state = channel.state;
2039
- const defaultVolume = state.volume * state.expression;
2040
- const defaultPan = state.pan;
2041
2076
  const gainL = channel.keyBasedGainLs[keyNumber];
2042
- const gainR = channel.keyBasedGainRs[keyNumber];
2043
2077
  if (!gainL)
2044
2078
  return;
2079
+ const gainR = channel.keyBasedGainRs[keyNumber];
2080
+ const state = channel.state;
2081
+ const defaultVolume = state.volume * state.expression;
2082
+ const defaultPan = state.pan;
2045
2083
  const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2046
2084
  const volume = (0 <= keyBasedVolume)
2047
2085
  ? defaultVolume * keyBasedVolume / 64
@@ -2117,18 +2155,27 @@ class Midy {
2117
2155
  }
2118
2156
  });
2119
2157
  }
2120
- setFilterResonance(channelNumber, filterResonance, scheduleTime) {
2158
+ setFilterResonance(channelNumber, ccValue, scheduleTime) {
2121
2159
  const channel = this.channels[channelNumber];
2122
2160
  if (channel.isDrum)
2123
2161
  return;
2124
2162
  scheduleTime ??= this.audioContext.currentTime;
2125
2163
  const state = channel.state;
2126
- state.filterResonance = filterResonance / 127;
2164
+ state.filterResonance = ccValue / 127;
2165
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2127
2166
  this.processScheduledNotes(channel, (note) => {
2128
- const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2167
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2129
2168
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2130
2169
  });
2131
2170
  }
2171
+ getRelativeKeyBasedValue(channel, note, controllerType) {
2172
+ const ccState = channel.state.array[128 + controllerType];
2173
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, controllerType);
2174
+ if (keyBasedValue < 0)
2175
+ return ccState;
2176
+ const keyValue = ccState + keyBasedValue / 127 - 0.5;
2177
+ return keyValue < 0 ? keyValue : 0;
2178
+ }
2132
2179
  setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2133
2180
  const channel = this.channels[channelNumber];
2134
2181
  if (channel.isDrum)
@@ -2143,9 +2190,9 @@ class Midy {
2143
2190
  scheduleTime ??= this.audioContext.currentTime;
2144
2191
  channel.state.attackTime = attackTime / 127;
2145
2192
  this.processScheduledNotes(channel, (note) => {
2146
- if (note.startTime < scheduleTime)
2147
- return false;
2148
- this.setVolumeEnvelope(channel, note, scheduleTime);
2193
+ if (scheduleTime < note.startTime) {
2194
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2195
+ }
2149
2196
  });
2150
2197
  }
2151
2198
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2306,8 +2353,8 @@ class Midy {
2306
2353
  }
2307
2354
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2308
2355
  const channel = this.channels[channelNumber];
2309
- this.limitData(channel, 0, 127, 0, 99);
2310
- const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2356
+ this.limitData(channel, 0, 127, 0, 127);
2357
+ const pitchBendRange = (channel.dataMSB + channel.dataLSB / 128) * 100;
2311
2358
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2312
2359
  }
2313
2360
  setPitchBendRange(channelNumber, value, scheduleTime) {
@@ -2317,7 +2364,7 @@ class Midy {
2317
2364
  scheduleTime ??= this.audioContext.currentTime;
2318
2365
  const state = channel.state;
2319
2366
  const prev = state.pitchWheelSensitivity;
2320
- const next = value / 128;
2367
+ const next = value / 12800;
2321
2368
  state.pitchWheelSensitivity = next;
2322
2369
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2323
2370
  this.updateChannelDetune(channel, scheduleTime);
@@ -2326,7 +2373,8 @@ class Midy {
2326
2373
  handleFineTuningRPN(channelNumber, scheduleTime) {
2327
2374
  const channel = this.channels[channelNumber];
2328
2375
  this.limitData(channel, 0, 127, 0, 127);
2329
- const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
2376
+ const value = channel.dataMSB * 128 + channel.dataLSB;
2377
+ const fineTuning = (value - 8192) / 8192 * 100;
2330
2378
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2331
2379
  }
2332
2380
  setFineTuning(channelNumber, value, scheduleTime) {
@@ -2335,7 +2383,7 @@ class Midy {
2335
2383
  return;
2336
2384
  scheduleTime ??= this.audioContext.currentTime;
2337
2385
  const prev = channel.fineTuning;
2338
- const next = (value - 8192) / 8.192; // cent
2386
+ const next = value;
2339
2387
  channel.fineTuning = next;
2340
2388
  channel.detune += next - prev;
2341
2389
  this.updateChannelDetune(channel, scheduleTime);
@@ -2343,7 +2391,7 @@ class Midy {
2343
2391
  handleCoarseTuningRPN(channelNumber, scheduleTime) {
2344
2392
  const channel = this.channels[channelNumber];
2345
2393
  this.limitDataMSB(channel, 0, 127);
2346
- const coarseTuning = channel.dataMSB;
2394
+ const coarseTuning = (channel.dataMSB - 64) * 100;
2347
2395
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2348
2396
  }
2349
2397
  setCoarseTuning(channelNumber, value, scheduleTime) {
@@ -2352,7 +2400,7 @@ class Midy {
2352
2400
  return;
2353
2401
  scheduleTime ??= this.audioContext.currentTime;
2354
2402
  const prev = channel.coarseTuning;
2355
- const next = (value - 64) * 100; // cent
2403
+ const next = value;
2356
2404
  channel.coarseTuning = next;
2357
2405
  channel.detune += next - prev;
2358
2406
  this.updateChannelDetune(channel, scheduleTime);
@@ -2360,22 +2408,22 @@ class Midy {
2360
2408
  handleModulationDepthRangeRPN(channelNumber, scheduleTime) {
2361
2409
  const channel = this.channels[channelNumber];
2362
2410
  this.limitData(channel, 0, 127, 0, 127);
2363
- const modulationDepthRange = (dataMSB + dataLSB / 128) * 100;
2364
- this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2411
+ const value = (channel.dataMSB + channel.dataLSB / 128) * 100;
2412
+ this.setModulationDepthRange(channelNumber, value, scheduleTime);
2365
2413
  }
2366
- setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2414
+ setModulationDepthRange(channelNumber, value, scheduleTime) {
2367
2415
  const channel = this.channels[channelNumber];
2368
2416
  if (channel.isDrum)
2369
2417
  return;
2370
2418
  scheduleTime ??= this.audioContext.currentTime;
2371
- channel.modulationDepthRange = modulationDepthRange;
2419
+ channel.modulationDepthRange = value;
2372
2420
  this.updateModulation(channel, scheduleTime);
2373
2421
  }
2374
2422
  allSoundOff(channelNumber, _value, scheduleTime) {
2375
2423
  scheduleTime ??= this.audioContext.currentTime;
2376
2424
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2377
2425
  }
2378
- resetAllStates(channelNumber) {
2426
+ resetChannelStates(channelNumber) {
2379
2427
  const scheduleTime = this.audioContext.currentTime;
2380
2428
  const channel = this.channels[channelNumber];
2381
2429
  const state = channel.state;
@@ -2393,8 +2441,8 @@ class Midy {
2393
2441
  }
2394
2442
  this.resetChannelTable(channel);
2395
2443
  this.mode = "GM2";
2396
- this.masterFineTuning = 0; // cb
2397
- this.masterCoarseTuning = 0; // cb
2444
+ this.masterFineTuning = 0; // cent
2445
+ this.masterCoarseTuning = 0; // cent
2398
2446
  }
2399
2447
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2400
2448
  resetAllControllers(channelNumber, _value, scheduleTime) {
@@ -2580,12 +2628,13 @@ class Midy {
2580
2628
  }
2581
2629
  }
2582
2630
  handleMasterFineTuningSysEx(data, scheduleTime) {
2583
- const fineTuning = data[5] * 128 + data[4];
2631
+ const value = (data[5] * 128 + data[4]) / 16383;
2632
+ const fineTuning = (value - 8192) / 8192 * 100;
2584
2633
  this.setMasterFineTuning(fineTuning, scheduleTime);
2585
2634
  }
2586
2635
  setMasterFineTuning(value, scheduleTime) {
2587
2636
  const prev = this.masterFineTuning;
2588
- const next = (value - 8192) / 8.192; // cent
2637
+ const next = value;
2589
2638
  this.masterFineTuning = next;
2590
2639
  const detuneChange = next - prev;
2591
2640
  for (let i = 0; i < this.channels.length; i++) {
@@ -2597,12 +2646,12 @@ class Midy {
2597
2646
  }
2598
2647
  }
2599
2648
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2600
- const coarseTuning = data[4];
2649
+ const coarseTuning = (data[4] - 64) * 100;
2601
2650
  this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2602
2651
  }
2603
2652
  setMasterCoarseTuning(value, scheduleTime) {
2604
2653
  const prev = this.masterCoarseTuning;
2605
- const next = (value - 64) * 100; // cent
2654
+ const next = value;
2606
2655
  this.masterCoarseTuning = next;
2607
2656
  const detuneChange = next - prev;
2608
2657
  for (let i = 0; i < this.channels.length; i++) {
@@ -2988,7 +3037,7 @@ class Midy {
2988
3037
  }
2989
3038
  getKeyBasedValue(channel, keyNumber, controllerType) {
2990
3039
  const index = keyNumber * 128 + controllerType;
2991
- const controlValue = channel.keyBasedInstrumentControlTable[index];
3040
+ const controlValue = channel.keyBasedTable[index];
2992
3041
  return controlValue;
2993
3042
  }
2994
3043
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
@@ -2997,7 +3046,7 @@ class Midy {
2997
3046
  if (!channel.isDrum)
2998
3047
  return;
2999
3048
  const keyNumber = data[5];
3000
- const table = channel.keyBasedInstrumentControlTable;
3049
+ const table = channel.keyBasedTable;
3001
3050
  for (let i = 6; i < data.length; i += 2) {
3002
3051
  const controllerType = data[i];
3003
3052
  const value = data[i + 1];
@@ -3008,8 +3057,89 @@ class Midy {
3008
3057
  case 10:
3009
3058
  this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3010
3059
  break;
3011
- default: // TODO
3012
- this.setControlChange(channelNumber, controllerType, value, scheduleTime);
3060
+ case 71:
3061
+ this.processScheduledNotes(channel, (note) => {
3062
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3063
+ if (note.noteNumber === keyNumber) {
3064
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3065
+ note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3066
+ }
3067
+ });
3068
+ break;
3069
+ case 73:
3070
+ this.processScheduledNotes(channel, (note) => {
3071
+ if (note.noteNumber === keyNumber) {
3072
+ if (0.5 <= channel.state.portamento &&
3073
+ 0 <= note.portamentoNoteNumber) {
3074
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
3075
+ }
3076
+ else {
3077
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3078
+ }
3079
+ }
3080
+ });
3081
+ break;
3082
+ case 74:
3083
+ this.processScheduledNotes(channel, (note) => {
3084
+ if (note.noteNumber === keyNumber) {
3085
+ if (0.5 <= channel.state.portamento &&
3086
+ 0 <= note.portamentoNoteNumber) {
3087
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
3088
+ }
3089
+ else {
3090
+ this.setFilterEnvelope(channel, note, scheduleTime);
3091
+ }
3092
+ }
3093
+ });
3094
+ break;
3095
+ case 75:
3096
+ this.processScheduledNotes(channel, (note) => {
3097
+ if (note.noteNumber === keyNumber) {
3098
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3099
+ }
3100
+ });
3101
+ break;
3102
+ case 76:
3103
+ if (channel.state.vibratoDepth <= 0)
3104
+ break;
3105
+ this.processScheduledNotes(channel, (note) => {
3106
+ if (note.noteNumber === keyNumber) {
3107
+ this.setFreqVibLFO(channel, note, scheduleTime);
3108
+ }
3109
+ });
3110
+ break;
3111
+ case 77:
3112
+ if (channel.state.vibratoDepth <= 0)
3113
+ break;
3114
+ this.processScheduledNotes(channel, (note) => {
3115
+ if (note.noteNumber === keyNumber) {
3116
+ this.setVibLfoToPitch(channel, note, scheduleTime);
3117
+ }
3118
+ });
3119
+ break;
3120
+ case 78:
3121
+ if (channel.state.vibratoDepth <= 0)
3122
+ break;
3123
+ this.processScheduledNotes(channel, (note) => {
3124
+ if (note.noteNumber === keyNumber) {
3125
+ this.setDelayVibLFO(channel, note);
3126
+ }
3127
+ });
3128
+ break;
3129
+ case 91:
3130
+ this.processScheduledNotes(channel, (note) => {
3131
+ if (note.noteNumber === keyNumber) {
3132
+ this.setReverbSend(channel, note, scheduleTime);
3133
+ }
3134
+ });
3135
+ break;
3136
+ case 93:
3137
+ this.processScheduledNotes(channel, (note) => {
3138
+ if (note.noteNumber === keyNumber) {
3139
+ this.setChorusSend(channel, note, scheduleTime);
3140
+ }
3141
+ });
3142
+ break;
3013
3143
  }
3014
3144
  }
3015
3145
  }
@@ -3061,7 +3191,7 @@ Object.defineProperty(Midy, "channelSettings", {
3061
3191
  rpnLSB: 127,
3062
3192
  mono: false, // CC#124, CC#125
3063
3193
  modulationDepthRange: 50, // cent
3064
- fineTuning: 0, // cb
3065
- coarseTuning: 0, // cb
3194
+ fineTuning: 0, // cent
3195
+ coarseTuning: 0, // cent
3066
3196
  }
3067
3197
  });