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