@marmooo/midy 0.3.5 → 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
@@ -71,13 +71,13 @@ class Note {
71
71
  writable: true,
72
72
  value: void 0
73
73
  });
74
- Object.defineProperty(this, "reverbEffectsSend", {
74
+ Object.defineProperty(this, "reverbSend", {
75
75
  enumerable: true,
76
76
  configurable: true,
77
77
  writable: true,
78
78
  value: void 0
79
79
  });
80
- Object.defineProperty(this, "chorusEffectsSend", {
80
+ Object.defineProperty(this, "chorusSend", {
81
81
  enumerable: true,
82
82
  configurable: true,
83
83
  writable: true,
@@ -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,13 +374,13 @@ class Midy {
374
374
  writable: true,
375
375
  value: false
376
376
  });
377
- Object.defineProperty(this, "timeline", {
377
+ Object.defineProperty(this, "playPromise", {
378
378
  enumerable: true,
379
379
  configurable: true,
380
380
  writable: true,
381
- value: []
381
+ value: void 0
382
382
  });
383
- Object.defineProperty(this, "instruments", {
383
+ Object.defineProperty(this, "timeline", {
384
384
  enumerable: true,
385
385
  configurable: true,
386
386
  writable: true,
@@ -392,6 +392,12 @@ class Midy {
392
392
  writable: true,
393
393
  value: []
394
394
  });
395
+ Object.defineProperty(this, "instruments", {
396
+ enumerable: true,
397
+ configurable: true,
398
+ writable: true,
399
+ value: new Set()
400
+ });
395
401
  Object.defineProperty(this, "exclusiveClassNotes", {
396
402
  enumerable: true,
397
403
  configurable: true,
@@ -522,7 +528,7 @@ class Midy {
522
528
  const soundFont = this.soundFonts[soundFontIndex];
523
529
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
524
530
  const { instrument, sampleID } = voice.generators;
525
- return `${soundFontIndex}:${instrument}:${sampleID}`;
531
+ return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
526
532
  }
527
533
  createChannelAudioNodes(audioContext) {
528
534
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
@@ -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;
@@ -927,13 +947,11 @@ class Midy {
927
947
  return impulse;
928
948
  }
929
949
  createConvolutionReverb(audioContext, impulse) {
930
- const input = new GainNode(audioContext);
931
950
  const convolverNode = new ConvolverNode(audioContext, {
932
951
  buffer: impulse,
933
952
  });
934
- input.connect(convolverNode);
935
953
  return {
936
- input,
954
+ input: convolverNode,
937
955
  output: convolverNode,
938
956
  convolverNode,
939
957
  };
@@ -1169,28 +1187,29 @@ class Midy {
1169
1187
  return Math.exp(y);
1170
1188
  }
1171
1189
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1172
- const state = channel.state;
1173
1190
  const { voiceParams, startTime } = note;
1174
1191
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1175
1192
  (1 + this.getAmplitudeControl(channel, note));
1176
1193
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1177
1194
  const volDelay = startTime + voiceParams.volDelay;
1178
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1195
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1196
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1179
1197
  const volHold = volAttack + voiceParams.volHold;
1180
1198
  note.volumeEnvelopeNode.gain
1181
1199
  .cancelScheduledValues(scheduleTime)
1182
1200
  .setValueAtTime(sustainVolume, volHold);
1183
1201
  }
1184
1202
  setVolumeEnvelope(channel, note, scheduleTime) {
1185
- const state = channel.state;
1186
1203
  const { voiceParams, startTime } = note;
1187
1204
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1188
1205
  (1 + this.getAmplitudeControl(channel, note));
1189
1206
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1190
1207
  const volDelay = startTime + voiceParams.volDelay;
1191
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1208
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1209
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1192
1210
  const volHold = volAttack + voiceParams.volHold;
1193
- const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
1211
+ const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
1212
+ const volDecay = volHold + voiceParams.volDecay * decayTime;
1194
1213
  note.volumeEnvelopeNode.gain
1195
1214
  .cancelScheduledValues(scheduleTime)
1196
1215
  .setValueAtTime(0, startTime)
@@ -1233,14 +1252,13 @@ class Midy {
1233
1252
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1234
1253
  }
1235
1254
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1236
- const state = channel.state;
1237
1255
  const { voiceParams, startTime } = note;
1238
1256
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1239
1257
  const baseCent = voiceParams.initialFilterFc +
1240
1258
  this.getFilterCutoffControl(channel, note);
1241
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1242
- state.brightness * 2;
1243
- 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;
1244
1262
  const sustainFreq = baseFreq +
1245
1263
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1246
1264
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1254,15 +1272,14 @@ class Midy {
1254
1272
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1255
1273
  }
1256
1274
  setFilterEnvelope(channel, note, scheduleTime) {
1257
- const state = channel.state;
1258
1275
  const { voiceParams, startTime } = note;
1259
1276
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1260
1277
  const baseCent = voiceParams.initialFilterFc +
1261
1278
  this.getFilterCutoffControl(channel, note);
1262
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1263
- state.brightness * 2;
1279
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1280
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1264
1281
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1265
- softPedalFactor * state.brightness * 2;
1282
+ softPedalFactor * brightness;
1266
1283
  const sustainFreq = baseFreq +
1267
1284
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1268
1285
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1302,11 +1319,12 @@ class Midy {
1302
1319
  }
1303
1320
  startVibrato(channel, note, scheduleTime) {
1304
1321
  const { voiceParams } = note;
1305
- const state = channel.state;
1322
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1323
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1306
1324
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1307
- frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1325
+ frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
1308
1326
  });
1309
- note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1327
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1310
1328
  note.vibratoDepth = new GainNode(this.audioContext);
1311
1329
  this.setVibLfoToPitch(channel, note, scheduleTime);
1312
1330
  note.vibratoLFO.connect(note.vibratoDepth);
@@ -1339,9 +1357,10 @@ class Midy {
1339
1357
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1340
1358
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1341
1359
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1360
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
1342
1361
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1343
1362
  type: "lowpass",
1344
- Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1363
+ Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1345
1364
  });
1346
1365
  const prevNote = channel.scheduledNotes.at(-1);
1347
1366
  if (prevNote && prevNote.noteNumber !== noteNumber) {
@@ -1370,12 +1389,8 @@ class Midy {
1370
1389
  }
1371
1390
  note.bufferSource.connect(note.filterNode);
1372
1391
  note.filterNode.connect(note.volumeEnvelopeNode);
1373
- if (0 < state.chorusSendLevel) {
1374
- this.setChorusEffectsSend(channel, note, 0, now);
1375
- }
1376
- if (0 < state.reverbSendLevel) {
1377
- this.setReverbEffectsSend(channel, note, 0, now);
1378
- }
1392
+ this.setChorusSend(channel, note, now);
1393
+ this.setReverbSend(channel, note, now);
1379
1394
  note.bufferSource.start(startTime);
1380
1395
  return note;
1381
1396
  }
@@ -1441,10 +1456,14 @@ class Midy {
1441
1456
  return;
1442
1457
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1443
1458
  if (channel.isDrum) {
1444
- const audioContext = this.audioContext;
1445
- const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1446
- channel.keyBasedGainLs[noteNumber] = gainL;
1447
- channel.keyBasedGainRs[noteNumber] = gainR;
1459
+ const { keyBasedGainLs, keyBasedGainRs } = channel;
1460
+ let gainL = keyBasedGainLs[noteNumber];
1461
+ let gainR = keyBasedGainRs[noteNumber];
1462
+ if (!gainL) {
1463
+ const audioNodes = this.createChannelAudioNodes(this.audioContext);
1464
+ gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1465
+ gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1466
+ }
1448
1467
  note.volumeEnvelopeNode.connect(gainL);
1449
1468
  note.volumeEnvelopeNode.connect(gainR);
1450
1469
  }
@@ -1478,16 +1497,16 @@ class Midy {
1478
1497
  note.vibratoDepth.disconnect();
1479
1498
  note.vibratoLFO.stop();
1480
1499
  }
1481
- if (note.reverbEffectsSend) {
1482
- note.reverbEffectsSend.disconnect();
1500
+ if (note.reverbSend) {
1501
+ note.reverbSend.disconnect();
1483
1502
  }
1484
- if (note.chorusEffectsSend) {
1485
- note.chorusEffectsSend.disconnect();
1503
+ if (note.chorusSend) {
1504
+ note.chorusSend.disconnect();
1486
1505
  }
1487
1506
  }
1488
1507
  releaseNote(channel, note, endTime) {
1489
- const volRelease = endTime +
1490
- note.voiceParams.volRelease * channel.state.releaseTime * 2;
1508
+ const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1509
+ const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1491
1510
  const modRelease = endTime + note.voiceParams.modRelease;
1492
1511
  const stopTime = Math.min(volRelease, modRelease);
1493
1512
  note.filterNode.frequency
@@ -1613,7 +1632,7 @@ class Midy {
1613
1632
  this.processActiveNotes(channel, scheduleTime, (note) => {
1614
1633
  if (note.noteNumber === noteNumber) {
1615
1634
  note.pressure = pressure;
1616
- this.setControllerParameters(channel, note, table, scheduleTime);
1635
+ this.setEffects(channel, note, table, scheduleTime);
1617
1636
  }
1618
1637
  });
1619
1638
  this.applyVoiceParams(channel, 10);
@@ -1632,6 +1651,7 @@ class Midy {
1632
1651
  break;
1633
1652
  }
1634
1653
  }
1654
+ channel.keyBasedTable.fill(-1);
1635
1655
  }
1636
1656
  setChannelPressure(channelNumber, value, scheduleTime) {
1637
1657
  const channel = this.channels[channelNumber];
@@ -1647,7 +1667,7 @@ class Midy {
1647
1667
  }
1648
1668
  const table = channel.channelPressureTable;
1649
1669
  this.processActiveNotes(channel, scheduleTime, (note) => {
1650
- this.setControllerParameters(channel, note, table, scheduleTime);
1670
+ this.setEffects(channel, note, table, scheduleTime);
1651
1671
  });
1652
1672
  this.applyVoiceParams(channel, 13);
1653
1673
  }
@@ -1669,22 +1689,33 @@ class Midy {
1669
1689
  this.applyVoiceParams(channel, 14, scheduleTime);
1670
1690
  }
1671
1691
  setModLfoToPitch(channel, note, scheduleTime) {
1672
- const modLfoToPitch = note.voiceParams.modLfoToPitch +
1673
- this.getLFOPitchDepth(channel, note);
1674
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1675
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1676
- note.modulationDepth.gain
1677
- .cancelScheduledValues(scheduleTime)
1678
- .setValueAtTime(modulationDepth, scheduleTime);
1692
+ if (note.modulationDepth) {
1693
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1694
+ this.getLFOPitchDepth(channel, note);
1695
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1696
+ const depth = baseDepth * Math.sign(modLfoToPitch);
1697
+ note.modulationDepth.gain
1698
+ .cancelScheduledValues(scheduleTime)
1699
+ .setValueAtTime(depth, scheduleTime);
1700
+ }
1701
+ else {
1702
+ this.startModulation(channel, note, scheduleTime);
1703
+ }
1679
1704
  }
1680
1705
  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);
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
+ }
1688
1719
  }
1689
1720
  setModLfoToFilterFc(channel, note, scheduleTime) {
1690
1721
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
@@ -1702,73 +1733,72 @@ class Midy {
1702
1733
  .cancelScheduledValues(scheduleTime)
1703
1734
  .setValueAtTime(volumeDepth, scheduleTime);
1704
1735
  }
1705
- setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1706
- let value = note.voiceParams.reverbEffectsSend;
1736
+ setReverbSend(channel, note, scheduleTime) {
1737
+ let value = note.voiceParams.reverbEffectsSend *
1738
+ channel.state.reverbSendLevel;
1707
1739
  if (channel.isDrum) {
1708
1740
  const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1709
- if (0 <= keyBasedValue) {
1710
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1711
- }
1741
+ if (0 <= keyBasedValue)
1742
+ value = keyBasedValue / 127;
1712
1743
  }
1713
- if (0 < prevValue) {
1744
+ if (!note.reverbSend) {
1714
1745
  if (0 < value) {
1715
- note.reverbEffectsSend.gain
1716
- .cancelScheduledValues(scheduleTime)
1717
- .setValueAtTime(value, scheduleTime);
1718
- }
1719
- else {
1720
- note.reverbEffectsSend.disconnect();
1746
+ note.reverbSend = new GainNode(this.audioContext, { gain: value });
1747
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1748
+ note.reverbSend.connect(this.reverbEffect.input);
1721
1749
  }
1722
1750
  }
1723
1751
  else {
1752
+ note.reverbSend.gain
1753
+ .cancelScheduledValues(scheduleTime)
1754
+ .setValueAtTime(value, scheduleTime);
1724
1755
  if (0 < value) {
1725
- if (!note.reverbEffectsSend) {
1726
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1727
- gain: value,
1728
- });
1729
- note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1756
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1757
+ }
1758
+ else {
1759
+ try {
1760
+ note.volumeEnvelopeNode.disconnect(note.reverbSend);
1730
1761
  }
1731
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1762
+ catch { /* empty */ }
1732
1763
  }
1733
1764
  }
1734
1765
  }
1735
- setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1736
- let value = note.voiceParams.chorusEffectsSend;
1766
+ setChorusSend(channel, note, scheduleTime) {
1767
+ let value = note.voiceParams.chorusEffectsSend *
1768
+ channel.state.chorusSendLevel;
1737
1769
  if (channel.isDrum) {
1738
1770
  const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1739
- if (0 <= keyBasedValue) {
1740
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1741
- }
1771
+ if (0 <= keyBasedValue)
1772
+ value = keyBasedValue / 127;
1742
1773
  }
1743
- if (0 < prevValue) {
1774
+ if (!note.chorusSend) {
1744
1775
  if (0 < value) {
1745
- note.chorusEffectsSend.gain
1746
- .cancelScheduledValues(scheduleTime)
1747
- .setValueAtTime(value, scheduleTime);
1748
- }
1749
- else {
1750
- note.chorusEffectsSend.disconnect();
1776
+ note.chorusSend = new GainNode(this.audioContext, { gain: value });
1777
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1778
+ note.chorusSend.connect(this.chorusEffect.input);
1751
1779
  }
1752
1780
  }
1753
1781
  else {
1782
+ note.chorusSend.gain
1783
+ .cancelScheduledValues(scheduleTime)
1784
+ .setValueAtTime(value, scheduleTime);
1754
1785
  if (0 < value) {
1755
- if (!note.chorusEffectsSend) {
1756
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1757
- gain: value,
1758
- });
1759
- note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1786
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1787
+ }
1788
+ else {
1789
+ try {
1790
+ note.volumeEnvelopeNode.disconnect(note.chorusSend);
1760
1791
  }
1761
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1792
+ catch { /* empty */ }
1762
1793
  }
1763
1794
  }
1764
1795
  }
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);
1796
+ setDelayModLFO(note) {
1797
+ const startTime = note.startTime + note.voiceParams.delayModLFO;
1798
+ try {
1799
+ note.modulationLFO.start(startTime);
1800
+ }
1801
+ catch { /* empty */ }
1772
1802
  }
1773
1803
  setFreqModLFO(note, scheduleTime) {
1774
1804
  const freqModLFO = note.voiceParams.freqModLFO;
@@ -1777,54 +1807,65 @@ class Midy {
1777
1807
  .setValueAtTime(freqModLFO, scheduleTime);
1778
1808
  }
1779
1809
  setFreqVibLFO(channel, note, scheduleTime) {
1810
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1780
1811
  const freqVibLFO = note.voiceParams.freqVibLFO;
1781
1812
  note.vibratoLFO.frequency
1782
1813
  .cancelScheduledValues(scheduleTime)
1783
- .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 */ }
1784
1824
  }
1785
1825
  createVoiceParamsHandlers() {
1786
1826
  return {
1787
- modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1827
+ modLfoToPitch: (channel, note, scheduleTime) => {
1788
1828
  if (0 < channel.state.modulationDepth) {
1789
1829
  this.setModLfoToPitch(channel, note, scheduleTime);
1790
1830
  }
1791
1831
  },
1792
- vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1832
+ vibLfoToPitch: (channel, note, scheduleTime) => {
1793
1833
  if (0 < channel.state.vibratoDepth) {
1794
1834
  this.setVibLfoToPitch(channel, note, scheduleTime);
1795
1835
  }
1796
1836
  },
1797
- modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1837
+ modLfoToFilterFc: (channel, note, scheduleTime) => {
1798
1838
  if (0 < channel.state.modulationDepth) {
1799
1839
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1800
1840
  }
1801
1841
  },
1802
- modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1842
+ modLfoToVolume: (channel, note, scheduleTime) => {
1803
1843
  if (0 < channel.state.modulationDepth) {
1804
1844
  this.setModLfoToVolume(channel, note, scheduleTime);
1805
1845
  }
1806
1846
  },
1807
- chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1808
- this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1847
+ chorusEffectsSend: (channel, note, scheduleTime) => {
1848
+ this.setChorusSend(channel, note, scheduleTime);
1809
1849
  },
1810
- reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1811
- this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1850
+ reverbEffectsSend: (channel, note, scheduleTime) => {
1851
+ this.setReverbSend(channel, note, scheduleTime);
1812
1852
  },
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) => {
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) => {
1816
1864
  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);
1865
+ setDelayVibLFO(channel, note);
1825
1866
  }
1826
1867
  },
1827
- freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1868
+ freqVibLFO: (channel, note, scheduleTime) => {
1828
1869
  if (0 < channel.state.vibratoDepth) {
1829
1870
  this.setFreqVibLFO(channel, note, scheduleTime);
1830
1871
  }
@@ -1853,7 +1894,7 @@ class Midy {
1853
1894
  continue;
1854
1895
  note.voiceParams[key] = value;
1855
1896
  if (key in this.voiceParamsHandlers) {
1856
- this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1897
+ this.voiceParamsHandlers[key](channel, note, scheduleTime);
1857
1898
  }
1858
1899
  else {
1859
1900
  if (volumeEnvelopeKeySet.has(key))
@@ -1918,7 +1959,7 @@ class Midy {
1918
1959
  handler.call(this, channelNumber, value, scheduleTime);
1919
1960
  const channel = this.channels[channelNumber];
1920
1961
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1921
- this.applyControlTable(channel, controllerType, scheduleTime);
1962
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
1922
1963
  }
1923
1964
  else {
1924
1965
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1934,7 +1975,6 @@ class Midy {
1934
1975
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1935
1976
  }
1936
1977
  else {
1937
- this.setPitchEnvelope(note, scheduleTime);
1938
1978
  this.startModulation(channel, note, scheduleTime);
1939
1979
  }
1940
1980
  });
@@ -1979,8 +2019,14 @@ class Midy {
1979
2019
  scheduleTime ??= this.audioContext.currentTime;
1980
2020
  const channel = this.channels[channelNumber];
1981
2021
  channel.state.volume = volume / 127;
1982
- this.updateChannelVolume(channel, scheduleTime);
1983
- this.updateKeyBasedVolume(channel, scheduleTime);
2022
+ if (channel.isDrum) {
2023
+ for (let i = 0; i < 128; i++) {
2024
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
2025
+ }
2026
+ }
2027
+ else {
2028
+ this.updateChannelVolume(channel, scheduleTime);
2029
+ }
1984
2030
  }
1985
2031
  panToGain(pan) {
1986
2032
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1993,8 +2039,14 @@ class Midy {
1993
2039
  scheduleTime ??= this.audioContext.currentTime;
1994
2040
  const channel = this.channels[channelNumber];
1995
2041
  channel.state.pan = pan / 127;
1996
- this.updateChannelVolume(channel, scheduleTime);
1997
- this.updateKeyBasedVolume(channel, scheduleTime);
2042
+ if (channel.isDrum) {
2043
+ for (let i = 0; i < 128; i++) {
2044
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
2045
+ }
2046
+ }
2047
+ else {
2048
+ this.updateChannelVolume(channel, scheduleTime);
2049
+ }
1998
2050
  }
1999
2051
  setExpression(channelNumber, expression, scheduleTime) {
2000
2052
  scheduleTime ??= this.audioContext.currentTime;
@@ -2020,33 +2072,27 @@ class Midy {
2020
2072
  .cancelScheduledValues(scheduleTime)
2021
2073
  .setValueAtTime(volume * gainRight, scheduleTime);
2022
2074
  }
2023
- updateKeyBasedVolume(channel, scheduleTime) {
2024
- if (!channel.isDrum)
2075
+ updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2076
+ const gainL = channel.keyBasedGainLs[keyNumber];
2077
+ if (!gainL)
2025
2078
  return;
2079
+ const gainR = channel.keyBasedGainRs[keyNumber];
2026
2080
  const state = channel.state;
2027
2081
  const defaultVolume = state.volume * state.expression;
2028
2082
  const defaultPan = state.pan;
2029
- for (let i = 0; i < 128; i++) {
2030
- const gainL = channel.keyBasedGainLs[i];
2031
- const gainR = channel.keyBasedGainLs[i];
2032
- if (!gainL)
2033
- continue;
2034
- if (!gainR)
2035
- continue;
2036
- const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
2037
- const volume = (0 <= keyBasedVolume)
2038
- ? defaultVolume * keyBasedVolume / 64
2039
- : defaultVolume;
2040
- const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
2041
- const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2042
- const { gainLeft, gainRight } = this.panToGain(pan);
2043
- gainL.gain
2044
- .cancelScheduledValues(scheduleTime)
2045
- .setValueAtTime(volume * gainLeft, scheduleTime);
2046
- gainR.gain
2047
- .cancelScheduledValues(scheduleTime)
2048
- .setValueAtTime(volume * gainRight, scheduleTime);
2049
- }
2083
+ const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2084
+ const volume = (0 <= keyBasedVolume)
2085
+ ? defaultVolume * keyBasedVolume / 64
2086
+ : defaultVolume;
2087
+ const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2088
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2089
+ const { gainLeft, gainRight } = this.panToGain(pan);
2090
+ gainL.gain
2091
+ .cancelScheduledValues(scheduleTime)
2092
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2093
+ gainR.gain
2094
+ .cancelScheduledValues(scheduleTime)
2095
+ .setValueAtTime(volume * gainRight, scheduleTime);
2050
2096
  }
2051
2097
  setSustainPedal(channelNumber, value, scheduleTime) {
2052
2098
  const channel = this.channels[channelNumber];
@@ -2109,18 +2155,27 @@ class Midy {
2109
2155
  }
2110
2156
  });
2111
2157
  }
2112
- setFilterResonance(channelNumber, filterResonance, scheduleTime) {
2158
+ setFilterResonance(channelNumber, ccValue, scheduleTime) {
2113
2159
  const channel = this.channels[channelNumber];
2114
2160
  if (channel.isDrum)
2115
2161
  return;
2116
2162
  scheduleTime ??= this.audioContext.currentTime;
2117
2163
  const state = channel.state;
2118
- state.filterResonance = filterResonance / 127;
2164
+ state.filterResonance = ccValue / 127;
2165
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2119
2166
  this.processScheduledNotes(channel, (note) => {
2120
- const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2167
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2121
2168
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2122
2169
  });
2123
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
+ }
2124
2179
  setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2125
2180
  const channel = this.channels[channelNumber];
2126
2181
  if (channel.isDrum)
@@ -2135,9 +2190,9 @@ class Midy {
2135
2190
  scheduleTime ??= this.audioContext.currentTime;
2136
2191
  channel.state.attackTime = attackTime / 127;
2137
2192
  this.processScheduledNotes(channel, (note) => {
2138
- if (note.startTime < scheduleTime)
2139
- return false;
2140
- this.setVolumeEnvelope(channel, note, scheduleTime);
2193
+ if (scheduleTime < note.startTime) {
2194
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2195
+ }
2141
2196
  });
2142
2197
  }
2143
2198
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2212,67 +2267,19 @@ class Midy {
2212
2267
  scheduleTime ??= this.audioContext.currentTime;
2213
2268
  const channel = this.channels[channelNumber];
2214
2269
  const state = channel.state;
2215
- const reverbEffect = this.reverbEffect;
2216
- if (0 < state.reverbSendLevel) {
2217
- if (0 < reverbSendLevel) {
2218
- state.reverbSendLevel = reverbSendLevel / 127;
2219
- reverbEffect.input.gain
2220
- .cancelScheduledValues(scheduleTime)
2221
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2222
- }
2223
- else {
2224
- this.processScheduledNotes(channel, (note) => {
2225
- if (note.voiceParams.reverbEffectsSend <= 0)
2226
- return false;
2227
- if (note.reverbEffectsSend)
2228
- note.reverbEffectsSend.disconnect();
2229
- });
2230
- }
2231
- }
2232
- else {
2233
- if (0 < reverbSendLevel) {
2234
- this.processScheduledNotes(channel, (note) => {
2235
- this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2236
- });
2237
- state.reverbSendLevel = reverbSendLevel / 127;
2238
- reverbEffect.input.gain
2239
- .cancelScheduledValues(scheduleTime)
2240
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2241
- }
2242
- }
2270
+ state.reverbSendLevel = reverbSendLevel / 127;
2271
+ this.processScheduledNotes(channel, (note) => {
2272
+ this.setReverbSend(channel, note, scheduleTime);
2273
+ });
2243
2274
  }
2244
2275
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2245
2276
  scheduleTime ??= this.audioContext.currentTime;
2246
2277
  const channel = this.channels[channelNumber];
2247
2278
  const state = channel.state;
2248
- const chorusEffect = this.chorusEffect;
2249
- if (0 < state.chorusSendLevel) {
2250
- if (0 < chorusSendLevel) {
2251
- state.chorusSendLevel = chorusSendLevel / 127;
2252
- chorusEffect.input.gain
2253
- .cancelScheduledValues(scheduleTime)
2254
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2255
- }
2256
- else {
2257
- this.processScheduledNotes(channel, (note) => {
2258
- if (note.voiceParams.chorusEffectsSend <= 0)
2259
- return false;
2260
- if (note.chorusEffectsSend)
2261
- note.chorusEffectsSend.disconnect();
2262
- });
2263
- }
2264
- }
2265
- else {
2266
- if (0 < chorusSendLevel) {
2267
- this.processScheduledNotes(channel, (note) => {
2268
- this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2269
- });
2270
- state.chorusSendLevel = chorusSendLevel / 127;
2271
- chorusEffect.input.gain
2272
- .cancelScheduledValues(scheduleTime)
2273
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2274
- }
2275
- }
2279
+ state.chorusSendLevel = chorusSendLevel / 127;
2280
+ this.processScheduledNotes(channel, (note) => {
2281
+ this.setChorusSend(channel, note, scheduleTime);
2282
+ });
2276
2283
  }
2277
2284
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
2278
2285
  if (maxLSB < channel.dataLSB) {
@@ -2346,8 +2353,8 @@ class Midy {
2346
2353
  }
2347
2354
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2348
2355
  const channel = this.channels[channelNumber];
2349
- this.limitData(channel, 0, 127, 0, 99);
2350
- const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2356
+ this.limitData(channel, 0, 127, 0, 127);
2357
+ const pitchBendRange = (channel.dataMSB + channel.dataLSB / 128) * 100;
2351
2358
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2352
2359
  }
2353
2360
  setPitchBendRange(channelNumber, value, scheduleTime) {
@@ -2357,7 +2364,7 @@ class Midy {
2357
2364
  scheduleTime ??= this.audioContext.currentTime;
2358
2365
  const state = channel.state;
2359
2366
  const prev = state.pitchWheelSensitivity;
2360
- const next = value / 128;
2367
+ const next = value / 12800;
2361
2368
  state.pitchWheelSensitivity = next;
2362
2369
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2363
2370
  this.updateChannelDetune(channel, scheduleTime);
@@ -2366,7 +2373,8 @@ class Midy {
2366
2373
  handleFineTuningRPN(channelNumber, scheduleTime) {
2367
2374
  const channel = this.channels[channelNumber];
2368
2375
  this.limitData(channel, 0, 127, 0, 127);
2369
- const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
2376
+ const value = channel.dataMSB * 128 + channel.dataLSB;
2377
+ const fineTuning = (value - 8192) / 8192 * 100;
2370
2378
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2371
2379
  }
2372
2380
  setFineTuning(channelNumber, value, scheduleTime) {
@@ -2375,7 +2383,7 @@ class Midy {
2375
2383
  return;
2376
2384
  scheduleTime ??= this.audioContext.currentTime;
2377
2385
  const prev = channel.fineTuning;
2378
- const next = (value - 8192) / 8.192; // cent
2386
+ const next = value;
2379
2387
  channel.fineTuning = next;
2380
2388
  channel.detune += next - prev;
2381
2389
  this.updateChannelDetune(channel, scheduleTime);
@@ -2383,7 +2391,7 @@ class Midy {
2383
2391
  handleCoarseTuningRPN(channelNumber, scheduleTime) {
2384
2392
  const channel = this.channels[channelNumber];
2385
2393
  this.limitDataMSB(channel, 0, 127);
2386
- const coarseTuning = channel.dataMSB;
2394
+ const coarseTuning = (channel.dataMSB - 64) * 100;
2387
2395
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2388
2396
  }
2389
2397
  setCoarseTuning(channelNumber, value, scheduleTime) {
@@ -2392,7 +2400,7 @@ class Midy {
2392
2400
  return;
2393
2401
  scheduleTime ??= this.audioContext.currentTime;
2394
2402
  const prev = channel.coarseTuning;
2395
- const next = (value - 64) * 100; // cent
2403
+ const next = value;
2396
2404
  channel.coarseTuning = next;
2397
2405
  channel.detune += next - prev;
2398
2406
  this.updateChannelDetune(channel, scheduleTime);
@@ -2400,22 +2408,22 @@ class Midy {
2400
2408
  handleModulationDepthRangeRPN(channelNumber, scheduleTime) {
2401
2409
  const channel = this.channels[channelNumber];
2402
2410
  this.limitData(channel, 0, 127, 0, 127);
2403
- const modulationDepthRange = (dataMSB + dataLSB / 128) * 100;
2404
- this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2411
+ const value = (channel.dataMSB + channel.dataLSB / 128) * 100;
2412
+ this.setModulationDepthRange(channelNumber, value, scheduleTime);
2405
2413
  }
2406
- setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2414
+ setModulationDepthRange(channelNumber, value, scheduleTime) {
2407
2415
  const channel = this.channels[channelNumber];
2408
2416
  if (channel.isDrum)
2409
2417
  return;
2410
2418
  scheduleTime ??= this.audioContext.currentTime;
2411
- channel.modulationDepthRange = modulationDepthRange;
2419
+ channel.modulationDepthRange = value;
2412
2420
  this.updateModulation(channel, scheduleTime);
2413
2421
  }
2414
2422
  allSoundOff(channelNumber, _value, scheduleTime) {
2415
2423
  scheduleTime ??= this.audioContext.currentTime;
2416
2424
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2417
2425
  }
2418
- resetAllStates(channelNumber) {
2426
+ resetChannelStates(channelNumber) {
2419
2427
  const scheduleTime = this.audioContext.currentTime;
2420
2428
  const channel = this.channels[channelNumber];
2421
2429
  const state = channel.state;
@@ -2433,8 +2441,8 @@ class Midy {
2433
2441
  }
2434
2442
  this.resetChannelTable(channel);
2435
2443
  this.mode = "GM2";
2436
- this.masterFineTuning = 0; // cb
2437
- this.masterCoarseTuning = 0; // cb
2444
+ this.masterFineTuning = 0; // cent
2445
+ this.masterCoarseTuning = 0; // cent
2438
2446
  }
2439
2447
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2440
2448
  resetAllControllers(channelNumber, _value, scheduleTime) {
@@ -2583,11 +2591,11 @@ class Midy {
2583
2591
  case 9:
2584
2592
  switch (data[3]) {
2585
2593
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2586
- return this.handlePressureSysEx(data, "channelPressureTable");
2594
+ return this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
2587
2595
  case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2588
- return this.handlePressureSysEx(data, "polyphonicKeyPressureTable");
2596
+ return this.handlePressureSysEx(data, "polyphonicKeyPressureTable", scheduleTime);
2589
2597
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2590
- return this.handleControlChangeSysEx(data);
2598
+ return this.handleControlChangeSysEx(data, scheduleTime);
2591
2599
  default:
2592
2600
  console.warn(`Unsupported Exclusive Message: ${data}`);
2593
2601
  }
@@ -2620,12 +2628,13 @@ class Midy {
2620
2628
  }
2621
2629
  }
2622
2630
  handleMasterFineTuningSysEx(data, scheduleTime) {
2623
- const fineTuning = data[5] * 128 + data[4];
2631
+ const value = (data[5] * 128 + data[4]) / 16383;
2632
+ const fineTuning = (value - 8192) / 8192 * 100;
2624
2633
  this.setMasterFineTuning(fineTuning, scheduleTime);
2625
2634
  }
2626
2635
  setMasterFineTuning(value, scheduleTime) {
2627
2636
  const prev = this.masterFineTuning;
2628
- const next = (value - 8192) / 8.192; // cent
2637
+ const next = value;
2629
2638
  this.masterFineTuning = next;
2630
2639
  const detuneChange = next - prev;
2631
2640
  for (let i = 0; i < this.channels.length; i++) {
@@ -2637,12 +2646,12 @@ class Midy {
2637
2646
  }
2638
2647
  }
2639
2648
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2640
- const coarseTuning = data[4];
2649
+ const coarseTuning = (data[4] - 64) * 100;
2641
2650
  this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2642
2651
  }
2643
2652
  setMasterCoarseTuning(value, scheduleTime) {
2644
2653
  const prev = this.masterCoarseTuning;
2645
- const next = (value - 64) * 100; // cent
2654
+ const next = value;
2646
2655
  this.masterCoarseTuning = next;
2647
2656
  const detuneChange = next - prev;
2648
2657
  for (let i = 0; i < this.channels.length; i++) {
@@ -2958,9 +2967,9 @@ class Midy {
2958
2967
  : 0;
2959
2968
  return (channelPressure + polyphonicKeyPressure) / 254;
2960
2969
  }
2961
- setControllerParameters(channel, note, table, scheduleTime) {
2970
+ setEffects(channel, note, table, scheduleTime) {
2962
2971
  if (0 <= table[0])
2963
- this.updateDetune(channel, note, scueduleTime);
2972
+ this.updateDetune(channel, note, scheduleTime);
2964
2973
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2965
2974
  if (0 <= table[1]) {
2966
2975
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2982,7 +2991,7 @@ class Midy {
2982
2991
  if (0 <= table[5])
2983
2992
  this.setModLfoToVolume(channel, note, scheduleTime);
2984
2993
  }
2985
- handlePressureSysEx(data, tableName) {
2994
+ handlePressureSysEx(data, tableName, scheduleTime) {
2986
2995
  const channelNumber = data[4];
2987
2996
  const channel = this.channels[channelNumber];
2988
2997
  if (channel.isDrum)
@@ -2993,36 +3002,42 @@ class Midy {
2993
3002
  const rr = data[i + 1];
2994
3003
  table[pp] = rr;
2995
3004
  }
3005
+ this.processActiveNotes(channel, scheduleTime, (note) => {
3006
+ this.setEffects(channel, note, table, scheduleTime);
3007
+ });
2996
3008
  }
2997
3009
  initControlTable() {
2998
3010
  const ccCount = 128;
2999
3011
  const slotSize = 6;
3000
3012
  return new Int8Array(ccCount * slotSize).fill(-1);
3001
3013
  }
3002
- applyControlTable(channel, controllerType, scheduleTime) {
3014
+ setControlChangeEffects(channel, controllerType, scheduleTime) {
3003
3015
  const slotSize = 6;
3004
3016
  const offset = controllerType * slotSize;
3005
3017
  const table = channel.controlTable.subarray(offset, offset + slotSize);
3006
3018
  this.processScheduledNotes(channel, (note) => {
3007
- this.setControllerParameters(channel, note, table, scheduleTime);
3019
+ this.setEffects(channel, note, table, scheduleTime);
3008
3020
  });
3009
3021
  }
3010
- handleControlChangeSysEx(data) {
3022
+ handleControlChangeSysEx(data, scheduleTime) {
3011
3023
  const channelNumber = data[4];
3012
3024
  const channel = this.channels[channelNumber];
3013
3025
  if (channel.isDrum)
3014
3026
  return;
3027
+ const slotSize = 6;
3015
3028
  const controllerType = data[5];
3016
- const table = channel.controlTable[controllerType];
3017
- for (let i = 6; i < data.length - 1; i += 2) {
3029
+ const offset = controllerType * slotSize;
3030
+ const table = channel.controlTable;
3031
+ for (let i = 6; i < data.length; i += 2) {
3018
3032
  const pp = data[i];
3019
3033
  const rr = data[i + 1];
3020
- table[pp] = rr;
3034
+ table[offset + pp] = rr;
3021
3035
  }
3036
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
3022
3037
  }
3023
3038
  getKeyBasedValue(channel, keyNumber, controllerType) {
3024
3039
  const index = keyNumber * 128 + controllerType;
3025
- const controlValue = channel.keyBasedInstrumentControlTable[index];
3040
+ const controlValue = channel.keyBasedTable[index];
3026
3041
  return controlValue;
3027
3042
  }
3028
3043
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
@@ -3031,14 +3046,102 @@ class Midy {
3031
3046
  if (!channel.isDrum)
3032
3047
  return;
3033
3048
  const keyNumber = data[5];
3034
- const table = channel.keyBasedInstrumentControlTable;
3035
- for (let i = 6; i < data.length - 1; i += 2) {
3049
+ const table = channel.keyBasedTable;
3050
+ for (let i = 6; i < data.length; i += 2) {
3036
3051
  const controllerType = data[i];
3037
3052
  const value = data[i + 1];
3038
3053
  const index = keyNumber * 128 + controllerType;
3039
3054
  table[index] = value;
3055
+ switch (controllerType) {
3056
+ case 7:
3057
+ case 10:
3058
+ this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3059
+ break;
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;
3143
+ }
3040
3144
  }
3041
- this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3042
3145
  }
3043
3146
  handleSysEx(data, scheduleTime) {
3044
3147
  switch (data[0]) {
@@ -3088,7 +3191,7 @@ Object.defineProperty(Midy, "channelSettings", {
3088
3191
  rpnLSB: 127,
3089
3192
  mono: false, // CC#124, CC#125
3090
3193
  modulationDepthRange: 50, // cent
3091
- fineTuning: 0, // cb
3092
- coarseTuning: 0, // cb
3194
+ fineTuning: 0, // cent
3195
+ coarseTuning: 0, // cent
3093
3196
  }
3094
3197
  });