@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/esm/midy.js CHANGED
@@ -68,13 +68,13 @@ class Note {
68
68
  writable: true,
69
69
  value: void 0
70
70
  });
71
- Object.defineProperty(this, "reverbEffectsSend", {
71
+ Object.defineProperty(this, "reverbSend", {
72
72
  enumerable: true,
73
73
  configurable: true,
74
74
  writable: true,
75
75
  value: void 0
76
76
  });
77
- Object.defineProperty(this, "chorusEffectsSend", {
77
+ Object.defineProperty(this, "chorusSend", {
78
78
  enumerable: true,
79
79
  configurable: true,
80
80
  writable: true,
@@ -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,13 +371,13 @@ export class Midy {
371
371
  writable: true,
372
372
  value: false
373
373
  });
374
- Object.defineProperty(this, "timeline", {
374
+ Object.defineProperty(this, "playPromise", {
375
375
  enumerable: true,
376
376
  configurable: true,
377
377
  writable: true,
378
- value: []
378
+ value: void 0
379
379
  });
380
- Object.defineProperty(this, "instruments", {
380
+ Object.defineProperty(this, "timeline", {
381
381
  enumerable: true,
382
382
  configurable: true,
383
383
  writable: true,
@@ -389,6 +389,12 @@ export class Midy {
389
389
  writable: true,
390
390
  value: []
391
391
  });
392
+ Object.defineProperty(this, "instruments", {
393
+ enumerable: true,
394
+ configurable: true,
395
+ writable: true,
396
+ value: new Set()
397
+ });
392
398
  Object.defineProperty(this, "exclusiveClassNotes", {
393
399
  enumerable: true,
394
400
  configurable: true,
@@ -519,7 +525,7 @@ export class Midy {
519
525
  const soundFont = this.soundFonts[soundFontIndex];
520
526
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
521
527
  const { instrument, sampleID } = voice.generators;
522
- return `${soundFontIndex}:${instrument}:${sampleID}`;
528
+ return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
523
529
  }
524
530
  createChannelAudioNodes(audioContext) {
525
531
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
@@ -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;
@@ -924,13 +944,11 @@ export class Midy {
924
944
  return impulse;
925
945
  }
926
946
  createConvolutionReverb(audioContext, impulse) {
927
- const input = new GainNode(audioContext);
928
947
  const convolverNode = new ConvolverNode(audioContext, {
929
948
  buffer: impulse,
930
949
  });
931
- input.connect(convolverNode);
932
950
  return {
933
- input,
951
+ input: convolverNode,
934
952
  output: convolverNode,
935
953
  convolverNode,
936
954
  };
@@ -1166,28 +1184,29 @@ export class Midy {
1166
1184
  return Math.exp(y);
1167
1185
  }
1168
1186
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1169
- const state = channel.state;
1170
1187
  const { voiceParams, startTime } = note;
1171
1188
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1172
1189
  (1 + this.getAmplitudeControl(channel, note));
1173
1190
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1174
1191
  const volDelay = startTime + voiceParams.volDelay;
1175
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1192
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1193
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1176
1194
  const volHold = volAttack + voiceParams.volHold;
1177
1195
  note.volumeEnvelopeNode.gain
1178
1196
  .cancelScheduledValues(scheduleTime)
1179
1197
  .setValueAtTime(sustainVolume, volHold);
1180
1198
  }
1181
1199
  setVolumeEnvelope(channel, note, scheduleTime) {
1182
- const state = channel.state;
1183
1200
  const { voiceParams, startTime } = note;
1184
1201
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1185
1202
  (1 + this.getAmplitudeControl(channel, note));
1186
1203
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1187
1204
  const volDelay = startTime + voiceParams.volDelay;
1188
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1205
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1206
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1189
1207
  const volHold = volAttack + voiceParams.volHold;
1190
- const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
1208
+ const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
1209
+ const volDecay = volHold + voiceParams.volDecay * decayTime;
1191
1210
  note.volumeEnvelopeNode.gain
1192
1211
  .cancelScheduledValues(scheduleTime)
1193
1212
  .setValueAtTime(0, startTime)
@@ -1230,14 +1249,13 @@ export class Midy {
1230
1249
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1231
1250
  }
1232
1251
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1233
- const state = channel.state;
1234
1252
  const { voiceParams, startTime } = note;
1235
1253
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1236
1254
  const baseCent = voiceParams.initialFilterFc +
1237
1255
  this.getFilterCutoffControl(channel, note);
1238
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1239
- state.brightness * 2;
1240
- 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;
1241
1259
  const sustainFreq = baseFreq +
1242
1260
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1243
1261
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1251,15 +1269,14 @@ export class Midy {
1251
1269
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1252
1270
  }
1253
1271
  setFilterEnvelope(channel, note, scheduleTime) {
1254
- const state = channel.state;
1255
1272
  const { voiceParams, startTime } = note;
1256
1273
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1257
1274
  const baseCent = voiceParams.initialFilterFc +
1258
1275
  this.getFilterCutoffControl(channel, note);
1259
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1260
- state.brightness * 2;
1276
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1277
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1261
1278
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1262
- softPedalFactor * state.brightness * 2;
1279
+ softPedalFactor * brightness;
1263
1280
  const sustainFreq = baseFreq +
1264
1281
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1265
1282
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1299,11 +1316,12 @@ export class Midy {
1299
1316
  }
1300
1317
  startVibrato(channel, note, scheduleTime) {
1301
1318
  const { voiceParams } = note;
1302
- const state = channel.state;
1319
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1320
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1303
1321
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1304
- frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1322
+ frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
1305
1323
  });
1306
- note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1324
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1307
1325
  note.vibratoDepth = new GainNode(this.audioContext);
1308
1326
  this.setVibLfoToPitch(channel, note, scheduleTime);
1309
1327
  note.vibratoLFO.connect(note.vibratoDepth);
@@ -1336,9 +1354,10 @@ export class Midy {
1336
1354
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1337
1355
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1338
1356
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1357
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
1339
1358
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1340
1359
  type: "lowpass",
1341
- Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1360
+ Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1342
1361
  });
1343
1362
  const prevNote = channel.scheduledNotes.at(-1);
1344
1363
  if (prevNote && prevNote.noteNumber !== noteNumber) {
@@ -1367,12 +1386,8 @@ export class Midy {
1367
1386
  }
1368
1387
  note.bufferSource.connect(note.filterNode);
1369
1388
  note.filterNode.connect(note.volumeEnvelopeNode);
1370
- if (0 < state.chorusSendLevel) {
1371
- this.setChorusEffectsSend(channel, note, 0, now);
1372
- }
1373
- if (0 < state.reverbSendLevel) {
1374
- this.setReverbEffectsSend(channel, note, 0, now);
1375
- }
1389
+ this.setChorusSend(channel, note, now);
1390
+ this.setReverbSend(channel, note, now);
1376
1391
  note.bufferSource.start(startTime);
1377
1392
  return note;
1378
1393
  }
@@ -1438,10 +1453,14 @@ export class Midy {
1438
1453
  return;
1439
1454
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1440
1455
  if (channel.isDrum) {
1441
- const audioContext = this.audioContext;
1442
- const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1443
- channel.keyBasedGainLs[noteNumber] = gainL;
1444
- channel.keyBasedGainRs[noteNumber] = gainR;
1456
+ const { keyBasedGainLs, keyBasedGainRs } = channel;
1457
+ let gainL = keyBasedGainLs[noteNumber];
1458
+ let gainR = keyBasedGainRs[noteNumber];
1459
+ if (!gainL) {
1460
+ const audioNodes = this.createChannelAudioNodes(this.audioContext);
1461
+ gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1462
+ gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1463
+ }
1445
1464
  note.volumeEnvelopeNode.connect(gainL);
1446
1465
  note.volumeEnvelopeNode.connect(gainR);
1447
1466
  }
@@ -1475,16 +1494,16 @@ export class Midy {
1475
1494
  note.vibratoDepth.disconnect();
1476
1495
  note.vibratoLFO.stop();
1477
1496
  }
1478
- if (note.reverbEffectsSend) {
1479
- note.reverbEffectsSend.disconnect();
1497
+ if (note.reverbSend) {
1498
+ note.reverbSend.disconnect();
1480
1499
  }
1481
- if (note.chorusEffectsSend) {
1482
- note.chorusEffectsSend.disconnect();
1500
+ if (note.chorusSend) {
1501
+ note.chorusSend.disconnect();
1483
1502
  }
1484
1503
  }
1485
1504
  releaseNote(channel, note, endTime) {
1486
- const volRelease = endTime +
1487
- note.voiceParams.volRelease * channel.state.releaseTime * 2;
1505
+ const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1506
+ const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1488
1507
  const modRelease = endTime + note.voiceParams.modRelease;
1489
1508
  const stopTime = Math.min(volRelease, modRelease);
1490
1509
  note.filterNode.frequency
@@ -1610,7 +1629,7 @@ export class Midy {
1610
1629
  this.processActiveNotes(channel, scheduleTime, (note) => {
1611
1630
  if (note.noteNumber === noteNumber) {
1612
1631
  note.pressure = pressure;
1613
- this.setControllerParameters(channel, note, table, scheduleTime);
1632
+ this.setEffects(channel, note, table, scheduleTime);
1614
1633
  }
1615
1634
  });
1616
1635
  this.applyVoiceParams(channel, 10);
@@ -1629,6 +1648,7 @@ export class Midy {
1629
1648
  break;
1630
1649
  }
1631
1650
  }
1651
+ channel.keyBasedTable.fill(-1);
1632
1652
  }
1633
1653
  setChannelPressure(channelNumber, value, scheduleTime) {
1634
1654
  const channel = this.channels[channelNumber];
@@ -1644,7 +1664,7 @@ export class Midy {
1644
1664
  }
1645
1665
  const table = channel.channelPressureTable;
1646
1666
  this.processActiveNotes(channel, scheduleTime, (note) => {
1647
- this.setControllerParameters(channel, note, table, scheduleTime);
1667
+ this.setEffects(channel, note, table, scheduleTime);
1648
1668
  });
1649
1669
  this.applyVoiceParams(channel, 13);
1650
1670
  }
@@ -1666,22 +1686,33 @@ export class Midy {
1666
1686
  this.applyVoiceParams(channel, 14, scheduleTime);
1667
1687
  }
1668
1688
  setModLfoToPitch(channel, note, scheduleTime) {
1669
- const modLfoToPitch = note.voiceParams.modLfoToPitch +
1670
- this.getLFOPitchDepth(channel, note);
1671
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1672
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1673
- note.modulationDepth.gain
1674
- .cancelScheduledValues(scheduleTime)
1675
- .setValueAtTime(modulationDepth, scheduleTime);
1689
+ if (note.modulationDepth) {
1690
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1691
+ this.getLFOPitchDepth(channel, note);
1692
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1693
+ const depth = baseDepth * Math.sign(modLfoToPitch);
1694
+ note.modulationDepth.gain
1695
+ .cancelScheduledValues(scheduleTime)
1696
+ .setValueAtTime(depth, scheduleTime);
1697
+ }
1698
+ else {
1699
+ this.startModulation(channel, note, scheduleTime);
1700
+ }
1676
1701
  }
1677
1702
  setVibLfoToPitch(channel, note, scheduleTime) {
1678
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1679
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1680
- 2;
1681
- const vibratoDepthSign = 0 < vibLfoToPitch;
1682
- note.vibratoDepth.gain
1683
- .cancelScheduledValues(scheduleTime)
1684
- .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
+ }
1685
1716
  }
1686
1717
  setModLfoToFilterFc(channel, note, scheduleTime) {
1687
1718
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
@@ -1699,73 +1730,72 @@ export class Midy {
1699
1730
  .cancelScheduledValues(scheduleTime)
1700
1731
  .setValueAtTime(volumeDepth, scheduleTime);
1701
1732
  }
1702
- setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1703
- let value = note.voiceParams.reverbEffectsSend;
1733
+ setReverbSend(channel, note, scheduleTime) {
1734
+ let value = note.voiceParams.reverbEffectsSend *
1735
+ channel.state.reverbSendLevel;
1704
1736
  if (channel.isDrum) {
1705
1737
  const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1706
- if (0 <= keyBasedValue) {
1707
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1708
- }
1738
+ if (0 <= keyBasedValue)
1739
+ value = keyBasedValue / 127;
1709
1740
  }
1710
- if (0 < prevValue) {
1741
+ if (!note.reverbSend) {
1711
1742
  if (0 < value) {
1712
- note.reverbEffectsSend.gain
1713
- .cancelScheduledValues(scheduleTime)
1714
- .setValueAtTime(value, scheduleTime);
1715
- }
1716
- else {
1717
- note.reverbEffectsSend.disconnect();
1743
+ note.reverbSend = new GainNode(this.audioContext, { gain: value });
1744
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1745
+ note.reverbSend.connect(this.reverbEffect.input);
1718
1746
  }
1719
1747
  }
1720
1748
  else {
1749
+ note.reverbSend.gain
1750
+ .cancelScheduledValues(scheduleTime)
1751
+ .setValueAtTime(value, scheduleTime);
1721
1752
  if (0 < value) {
1722
- if (!note.reverbEffectsSend) {
1723
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1724
- gain: value,
1725
- });
1726
- note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1753
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1754
+ }
1755
+ else {
1756
+ try {
1757
+ note.volumeEnvelopeNode.disconnect(note.reverbSend);
1727
1758
  }
1728
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1759
+ catch { /* empty */ }
1729
1760
  }
1730
1761
  }
1731
1762
  }
1732
- setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1733
- let value = note.voiceParams.chorusEffectsSend;
1763
+ setChorusSend(channel, note, scheduleTime) {
1764
+ let value = note.voiceParams.chorusEffectsSend *
1765
+ channel.state.chorusSendLevel;
1734
1766
  if (channel.isDrum) {
1735
1767
  const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1736
- if (0 <= keyBasedValue) {
1737
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1738
- }
1768
+ if (0 <= keyBasedValue)
1769
+ value = keyBasedValue / 127;
1739
1770
  }
1740
- if (0 < prevValue) {
1771
+ if (!note.chorusSend) {
1741
1772
  if (0 < value) {
1742
- note.chorusEffectsSend.gain
1743
- .cancelScheduledValues(scheduleTime)
1744
- .setValueAtTime(value, scheduleTime);
1745
- }
1746
- else {
1747
- note.chorusEffectsSend.disconnect();
1773
+ note.chorusSend = new GainNode(this.audioContext, { gain: value });
1774
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1775
+ note.chorusSend.connect(this.chorusEffect.input);
1748
1776
  }
1749
1777
  }
1750
1778
  else {
1779
+ note.chorusSend.gain
1780
+ .cancelScheduledValues(scheduleTime)
1781
+ .setValueAtTime(value, scheduleTime);
1751
1782
  if (0 < value) {
1752
- if (!note.chorusEffectsSend) {
1753
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1754
- gain: value,
1755
- });
1756
- note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1783
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1784
+ }
1785
+ else {
1786
+ try {
1787
+ note.volumeEnvelopeNode.disconnect(note.chorusSend);
1757
1788
  }
1758
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1789
+ catch { /* empty */ }
1759
1790
  }
1760
1791
  }
1761
1792
  }
1762
- setDelayModLFO(note, scheduleTime) {
1763
- const startTime = note.startTime;
1764
- if (startTime < scheduleTime)
1765
- return;
1766
- note.modulationLFO.stop(scheduleTime);
1767
- note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1768
- 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 */ }
1769
1799
  }
1770
1800
  setFreqModLFO(note, scheduleTime) {
1771
1801
  const freqModLFO = note.voiceParams.freqModLFO;
@@ -1774,54 +1804,65 @@ export class Midy {
1774
1804
  .setValueAtTime(freqModLFO, scheduleTime);
1775
1805
  }
1776
1806
  setFreqVibLFO(channel, note, scheduleTime) {
1807
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1777
1808
  const freqVibLFO = note.voiceParams.freqVibLFO;
1778
1809
  note.vibratoLFO.frequency
1779
1810
  .cancelScheduledValues(scheduleTime)
1780
- .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 */ }
1781
1821
  }
1782
1822
  createVoiceParamsHandlers() {
1783
1823
  return {
1784
- modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1824
+ modLfoToPitch: (channel, note, scheduleTime) => {
1785
1825
  if (0 < channel.state.modulationDepth) {
1786
1826
  this.setModLfoToPitch(channel, note, scheduleTime);
1787
1827
  }
1788
1828
  },
1789
- vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1829
+ vibLfoToPitch: (channel, note, scheduleTime) => {
1790
1830
  if (0 < channel.state.vibratoDepth) {
1791
1831
  this.setVibLfoToPitch(channel, note, scheduleTime);
1792
1832
  }
1793
1833
  },
1794
- modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1834
+ modLfoToFilterFc: (channel, note, scheduleTime) => {
1795
1835
  if (0 < channel.state.modulationDepth) {
1796
1836
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1797
1837
  }
1798
1838
  },
1799
- modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1839
+ modLfoToVolume: (channel, note, scheduleTime) => {
1800
1840
  if (0 < channel.state.modulationDepth) {
1801
1841
  this.setModLfoToVolume(channel, note, scheduleTime);
1802
1842
  }
1803
1843
  },
1804
- chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1805
- this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1844
+ chorusEffectsSend: (channel, note, scheduleTime) => {
1845
+ this.setChorusSend(channel, note, scheduleTime);
1806
1846
  },
1807
- reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1808
- this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1847
+ reverbEffectsSend: (channel, note, scheduleTime) => {
1848
+ this.setReverbSend(channel, note, scheduleTime);
1809
1849
  },
1810
- delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1811
- freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1812
- 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) => {
1813
1861
  if (0 < channel.state.vibratoDepth) {
1814
- const vibratoDelay = channel.state.vibratoDelay * 2;
1815
- const prevStartTime = note.startTime + prevValue * vibratoDelay;
1816
- if (scheduleTime < prevStartTime)
1817
- return;
1818
- const value = note.voiceParams.delayVibLFO;
1819
- const startTime = note.startTime + value * vibratoDelay;
1820
- note.vibratoLFO.stop(scheduleTime);
1821
- note.vibratoLFO.start(startTime);
1862
+ setDelayVibLFO(channel, note);
1822
1863
  }
1823
1864
  },
1824
- freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1865
+ freqVibLFO: (channel, note, scheduleTime) => {
1825
1866
  if (0 < channel.state.vibratoDepth) {
1826
1867
  this.setFreqVibLFO(channel, note, scheduleTime);
1827
1868
  }
@@ -1850,7 +1891,7 @@ export class Midy {
1850
1891
  continue;
1851
1892
  note.voiceParams[key] = value;
1852
1893
  if (key in this.voiceParamsHandlers) {
1853
- this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1894
+ this.voiceParamsHandlers[key](channel, note, scheduleTime);
1854
1895
  }
1855
1896
  else {
1856
1897
  if (volumeEnvelopeKeySet.has(key))
@@ -1915,7 +1956,7 @@ export class Midy {
1915
1956
  handler.call(this, channelNumber, value, scheduleTime);
1916
1957
  const channel = this.channels[channelNumber];
1917
1958
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1918
- this.applyControlTable(channel, controllerType, scheduleTime);
1959
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
1919
1960
  }
1920
1961
  else {
1921
1962
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1931,7 +1972,6 @@ export class Midy {
1931
1972
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1932
1973
  }
1933
1974
  else {
1934
- this.setPitchEnvelope(note, scheduleTime);
1935
1975
  this.startModulation(channel, note, scheduleTime);
1936
1976
  }
1937
1977
  });
@@ -1976,8 +2016,14 @@ export class Midy {
1976
2016
  scheduleTime ??= this.audioContext.currentTime;
1977
2017
  const channel = this.channels[channelNumber];
1978
2018
  channel.state.volume = volume / 127;
1979
- this.updateChannelVolume(channel, scheduleTime);
1980
- this.updateKeyBasedVolume(channel, scheduleTime);
2019
+ if (channel.isDrum) {
2020
+ for (let i = 0; i < 128; i++) {
2021
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
2022
+ }
2023
+ }
2024
+ else {
2025
+ this.updateChannelVolume(channel, scheduleTime);
2026
+ }
1981
2027
  }
1982
2028
  panToGain(pan) {
1983
2029
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1990,8 +2036,14 @@ export class Midy {
1990
2036
  scheduleTime ??= this.audioContext.currentTime;
1991
2037
  const channel = this.channels[channelNumber];
1992
2038
  channel.state.pan = pan / 127;
1993
- this.updateChannelVolume(channel, scheduleTime);
1994
- this.updateKeyBasedVolume(channel, scheduleTime);
2039
+ if (channel.isDrum) {
2040
+ for (let i = 0; i < 128; i++) {
2041
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
2042
+ }
2043
+ }
2044
+ else {
2045
+ this.updateChannelVolume(channel, scheduleTime);
2046
+ }
1995
2047
  }
1996
2048
  setExpression(channelNumber, expression, scheduleTime) {
1997
2049
  scheduleTime ??= this.audioContext.currentTime;
@@ -2017,33 +2069,27 @@ export class Midy {
2017
2069
  .cancelScheduledValues(scheduleTime)
2018
2070
  .setValueAtTime(volume * gainRight, scheduleTime);
2019
2071
  }
2020
- updateKeyBasedVolume(channel, scheduleTime) {
2021
- if (!channel.isDrum)
2072
+ updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2073
+ const gainL = channel.keyBasedGainLs[keyNumber];
2074
+ if (!gainL)
2022
2075
  return;
2076
+ const gainR = channel.keyBasedGainRs[keyNumber];
2023
2077
  const state = channel.state;
2024
2078
  const defaultVolume = state.volume * state.expression;
2025
2079
  const defaultPan = state.pan;
2026
- for (let i = 0; i < 128; i++) {
2027
- const gainL = channel.keyBasedGainLs[i];
2028
- const gainR = channel.keyBasedGainLs[i];
2029
- if (!gainL)
2030
- continue;
2031
- if (!gainR)
2032
- continue;
2033
- const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
2034
- const volume = (0 <= keyBasedVolume)
2035
- ? defaultVolume * keyBasedVolume / 64
2036
- : defaultVolume;
2037
- const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
2038
- const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2039
- const { gainLeft, gainRight } = this.panToGain(pan);
2040
- gainL.gain
2041
- .cancelScheduledValues(scheduleTime)
2042
- .setValueAtTime(volume * gainLeft, scheduleTime);
2043
- gainR.gain
2044
- .cancelScheduledValues(scheduleTime)
2045
- .setValueAtTime(volume * gainRight, scheduleTime);
2046
- }
2080
+ const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2081
+ const volume = (0 <= keyBasedVolume)
2082
+ ? defaultVolume * keyBasedVolume / 64
2083
+ : defaultVolume;
2084
+ const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2085
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2086
+ const { gainLeft, gainRight } = this.panToGain(pan);
2087
+ gainL.gain
2088
+ .cancelScheduledValues(scheduleTime)
2089
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2090
+ gainR.gain
2091
+ .cancelScheduledValues(scheduleTime)
2092
+ .setValueAtTime(volume * gainRight, scheduleTime);
2047
2093
  }
2048
2094
  setSustainPedal(channelNumber, value, scheduleTime) {
2049
2095
  const channel = this.channels[channelNumber];
@@ -2106,18 +2152,27 @@ export class Midy {
2106
2152
  }
2107
2153
  });
2108
2154
  }
2109
- setFilterResonance(channelNumber, filterResonance, scheduleTime) {
2155
+ setFilterResonance(channelNumber, ccValue, scheduleTime) {
2110
2156
  const channel = this.channels[channelNumber];
2111
2157
  if (channel.isDrum)
2112
2158
  return;
2113
2159
  scheduleTime ??= this.audioContext.currentTime;
2114
2160
  const state = channel.state;
2115
- state.filterResonance = filterResonance / 127;
2161
+ state.filterResonance = ccValue / 127;
2162
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2116
2163
  this.processScheduledNotes(channel, (note) => {
2117
- const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2164
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2118
2165
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2119
2166
  });
2120
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
+ }
2121
2176
  setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2122
2177
  const channel = this.channels[channelNumber];
2123
2178
  if (channel.isDrum)
@@ -2132,9 +2187,9 @@ export class Midy {
2132
2187
  scheduleTime ??= this.audioContext.currentTime;
2133
2188
  channel.state.attackTime = attackTime / 127;
2134
2189
  this.processScheduledNotes(channel, (note) => {
2135
- if (note.startTime < scheduleTime)
2136
- return false;
2137
- this.setVolumeEnvelope(channel, note, scheduleTime);
2190
+ if (scheduleTime < note.startTime) {
2191
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2192
+ }
2138
2193
  });
2139
2194
  }
2140
2195
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2209,67 +2264,19 @@ export class Midy {
2209
2264
  scheduleTime ??= this.audioContext.currentTime;
2210
2265
  const channel = this.channels[channelNumber];
2211
2266
  const state = channel.state;
2212
- const reverbEffect = this.reverbEffect;
2213
- if (0 < state.reverbSendLevel) {
2214
- if (0 < reverbSendLevel) {
2215
- state.reverbSendLevel = reverbSendLevel / 127;
2216
- reverbEffect.input.gain
2217
- .cancelScheduledValues(scheduleTime)
2218
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2219
- }
2220
- else {
2221
- this.processScheduledNotes(channel, (note) => {
2222
- if (note.voiceParams.reverbEffectsSend <= 0)
2223
- return false;
2224
- if (note.reverbEffectsSend)
2225
- note.reverbEffectsSend.disconnect();
2226
- });
2227
- }
2228
- }
2229
- else {
2230
- if (0 < reverbSendLevel) {
2231
- this.processScheduledNotes(channel, (note) => {
2232
- this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2233
- });
2234
- state.reverbSendLevel = reverbSendLevel / 127;
2235
- reverbEffect.input.gain
2236
- .cancelScheduledValues(scheduleTime)
2237
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2238
- }
2239
- }
2267
+ state.reverbSendLevel = reverbSendLevel / 127;
2268
+ this.processScheduledNotes(channel, (note) => {
2269
+ this.setReverbSend(channel, note, scheduleTime);
2270
+ });
2240
2271
  }
2241
2272
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2242
2273
  scheduleTime ??= this.audioContext.currentTime;
2243
2274
  const channel = this.channels[channelNumber];
2244
2275
  const state = channel.state;
2245
- const chorusEffect = this.chorusEffect;
2246
- if (0 < state.chorusSendLevel) {
2247
- if (0 < chorusSendLevel) {
2248
- state.chorusSendLevel = chorusSendLevel / 127;
2249
- chorusEffect.input.gain
2250
- .cancelScheduledValues(scheduleTime)
2251
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2252
- }
2253
- else {
2254
- this.processScheduledNotes(channel, (note) => {
2255
- if (note.voiceParams.chorusEffectsSend <= 0)
2256
- return false;
2257
- if (note.chorusEffectsSend)
2258
- note.chorusEffectsSend.disconnect();
2259
- });
2260
- }
2261
- }
2262
- else {
2263
- if (0 < chorusSendLevel) {
2264
- this.processScheduledNotes(channel, (note) => {
2265
- this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2266
- });
2267
- state.chorusSendLevel = chorusSendLevel / 127;
2268
- chorusEffect.input.gain
2269
- .cancelScheduledValues(scheduleTime)
2270
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2271
- }
2272
- }
2276
+ state.chorusSendLevel = chorusSendLevel / 127;
2277
+ this.processScheduledNotes(channel, (note) => {
2278
+ this.setChorusSend(channel, note, scheduleTime);
2279
+ });
2273
2280
  }
2274
2281
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
2275
2282
  if (maxLSB < channel.dataLSB) {
@@ -2343,8 +2350,8 @@ export class Midy {
2343
2350
  }
2344
2351
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2345
2352
  const channel = this.channels[channelNumber];
2346
- this.limitData(channel, 0, 127, 0, 99);
2347
- const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2353
+ this.limitData(channel, 0, 127, 0, 127);
2354
+ const pitchBendRange = (channel.dataMSB + channel.dataLSB / 128) * 100;
2348
2355
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2349
2356
  }
2350
2357
  setPitchBendRange(channelNumber, value, scheduleTime) {
@@ -2354,7 +2361,7 @@ export class Midy {
2354
2361
  scheduleTime ??= this.audioContext.currentTime;
2355
2362
  const state = channel.state;
2356
2363
  const prev = state.pitchWheelSensitivity;
2357
- const next = value / 128;
2364
+ const next = value / 12800;
2358
2365
  state.pitchWheelSensitivity = next;
2359
2366
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2360
2367
  this.updateChannelDetune(channel, scheduleTime);
@@ -2363,7 +2370,8 @@ export class Midy {
2363
2370
  handleFineTuningRPN(channelNumber, scheduleTime) {
2364
2371
  const channel = this.channels[channelNumber];
2365
2372
  this.limitData(channel, 0, 127, 0, 127);
2366
- const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
2373
+ const value = channel.dataMSB * 128 + channel.dataLSB;
2374
+ const fineTuning = (value - 8192) / 8192 * 100;
2367
2375
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2368
2376
  }
2369
2377
  setFineTuning(channelNumber, value, scheduleTime) {
@@ -2372,7 +2380,7 @@ export class Midy {
2372
2380
  return;
2373
2381
  scheduleTime ??= this.audioContext.currentTime;
2374
2382
  const prev = channel.fineTuning;
2375
- const next = (value - 8192) / 8.192; // cent
2383
+ const next = value;
2376
2384
  channel.fineTuning = next;
2377
2385
  channel.detune += next - prev;
2378
2386
  this.updateChannelDetune(channel, scheduleTime);
@@ -2380,7 +2388,7 @@ export class Midy {
2380
2388
  handleCoarseTuningRPN(channelNumber, scheduleTime) {
2381
2389
  const channel = this.channels[channelNumber];
2382
2390
  this.limitDataMSB(channel, 0, 127);
2383
- const coarseTuning = channel.dataMSB;
2391
+ const coarseTuning = (channel.dataMSB - 64) * 100;
2384
2392
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2385
2393
  }
2386
2394
  setCoarseTuning(channelNumber, value, scheduleTime) {
@@ -2389,7 +2397,7 @@ export class Midy {
2389
2397
  return;
2390
2398
  scheduleTime ??= this.audioContext.currentTime;
2391
2399
  const prev = channel.coarseTuning;
2392
- const next = (value - 64) * 100; // cent
2400
+ const next = value;
2393
2401
  channel.coarseTuning = next;
2394
2402
  channel.detune += next - prev;
2395
2403
  this.updateChannelDetune(channel, scheduleTime);
@@ -2397,22 +2405,22 @@ export class Midy {
2397
2405
  handleModulationDepthRangeRPN(channelNumber, scheduleTime) {
2398
2406
  const channel = this.channels[channelNumber];
2399
2407
  this.limitData(channel, 0, 127, 0, 127);
2400
- const modulationDepthRange = (dataMSB + dataLSB / 128) * 100;
2401
- this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2408
+ const value = (channel.dataMSB + channel.dataLSB / 128) * 100;
2409
+ this.setModulationDepthRange(channelNumber, value, scheduleTime);
2402
2410
  }
2403
- setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2411
+ setModulationDepthRange(channelNumber, value, scheduleTime) {
2404
2412
  const channel = this.channels[channelNumber];
2405
2413
  if (channel.isDrum)
2406
2414
  return;
2407
2415
  scheduleTime ??= this.audioContext.currentTime;
2408
- channel.modulationDepthRange = modulationDepthRange;
2416
+ channel.modulationDepthRange = value;
2409
2417
  this.updateModulation(channel, scheduleTime);
2410
2418
  }
2411
2419
  allSoundOff(channelNumber, _value, scheduleTime) {
2412
2420
  scheduleTime ??= this.audioContext.currentTime;
2413
2421
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2414
2422
  }
2415
- resetAllStates(channelNumber) {
2423
+ resetChannelStates(channelNumber) {
2416
2424
  const scheduleTime = this.audioContext.currentTime;
2417
2425
  const channel = this.channels[channelNumber];
2418
2426
  const state = channel.state;
@@ -2430,8 +2438,8 @@ export class Midy {
2430
2438
  }
2431
2439
  this.resetChannelTable(channel);
2432
2440
  this.mode = "GM2";
2433
- this.masterFineTuning = 0; // cb
2434
- this.masterCoarseTuning = 0; // cb
2441
+ this.masterFineTuning = 0; // cent
2442
+ this.masterCoarseTuning = 0; // cent
2435
2443
  }
2436
2444
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2437
2445
  resetAllControllers(channelNumber, _value, scheduleTime) {
@@ -2580,11 +2588,11 @@ export class Midy {
2580
2588
  case 9:
2581
2589
  switch (data[3]) {
2582
2590
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2583
- return this.handlePressureSysEx(data, "channelPressureTable");
2591
+ return this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
2584
2592
  case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2585
- return this.handlePressureSysEx(data, "polyphonicKeyPressureTable");
2593
+ return this.handlePressureSysEx(data, "polyphonicKeyPressureTable", scheduleTime);
2586
2594
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2587
- return this.handleControlChangeSysEx(data);
2595
+ return this.handleControlChangeSysEx(data, scheduleTime);
2588
2596
  default:
2589
2597
  console.warn(`Unsupported Exclusive Message: ${data}`);
2590
2598
  }
@@ -2617,12 +2625,13 @@ export class Midy {
2617
2625
  }
2618
2626
  }
2619
2627
  handleMasterFineTuningSysEx(data, scheduleTime) {
2620
- const fineTuning = data[5] * 128 + data[4];
2628
+ const value = (data[5] * 128 + data[4]) / 16383;
2629
+ const fineTuning = (value - 8192) / 8192 * 100;
2621
2630
  this.setMasterFineTuning(fineTuning, scheduleTime);
2622
2631
  }
2623
2632
  setMasterFineTuning(value, scheduleTime) {
2624
2633
  const prev = this.masterFineTuning;
2625
- const next = (value - 8192) / 8.192; // cent
2634
+ const next = value;
2626
2635
  this.masterFineTuning = next;
2627
2636
  const detuneChange = next - prev;
2628
2637
  for (let i = 0; i < this.channels.length; i++) {
@@ -2634,12 +2643,12 @@ export class Midy {
2634
2643
  }
2635
2644
  }
2636
2645
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2637
- const coarseTuning = data[4];
2646
+ const coarseTuning = (data[4] - 64) * 100;
2638
2647
  this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2639
2648
  }
2640
2649
  setMasterCoarseTuning(value, scheduleTime) {
2641
2650
  const prev = this.masterCoarseTuning;
2642
- const next = (value - 64) * 100; // cent
2651
+ const next = value;
2643
2652
  this.masterCoarseTuning = next;
2644
2653
  const detuneChange = next - prev;
2645
2654
  for (let i = 0; i < this.channels.length; i++) {
@@ -2955,9 +2964,9 @@ export class Midy {
2955
2964
  : 0;
2956
2965
  return (channelPressure + polyphonicKeyPressure) / 254;
2957
2966
  }
2958
- setControllerParameters(channel, note, table, scheduleTime) {
2967
+ setEffects(channel, note, table, scheduleTime) {
2959
2968
  if (0 <= table[0])
2960
- this.updateDetune(channel, note, scueduleTime);
2969
+ this.updateDetune(channel, note, scheduleTime);
2961
2970
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2962
2971
  if (0 <= table[1]) {
2963
2972
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2979,7 +2988,7 @@ export class Midy {
2979
2988
  if (0 <= table[5])
2980
2989
  this.setModLfoToVolume(channel, note, scheduleTime);
2981
2990
  }
2982
- handlePressureSysEx(data, tableName) {
2991
+ handlePressureSysEx(data, tableName, scheduleTime) {
2983
2992
  const channelNumber = data[4];
2984
2993
  const channel = this.channels[channelNumber];
2985
2994
  if (channel.isDrum)
@@ -2990,36 +2999,42 @@ export class Midy {
2990
2999
  const rr = data[i + 1];
2991
3000
  table[pp] = rr;
2992
3001
  }
3002
+ this.processActiveNotes(channel, scheduleTime, (note) => {
3003
+ this.setEffects(channel, note, table, scheduleTime);
3004
+ });
2993
3005
  }
2994
3006
  initControlTable() {
2995
3007
  const ccCount = 128;
2996
3008
  const slotSize = 6;
2997
3009
  return new Int8Array(ccCount * slotSize).fill(-1);
2998
3010
  }
2999
- applyControlTable(channel, controllerType, scheduleTime) {
3011
+ setControlChangeEffects(channel, controllerType, scheduleTime) {
3000
3012
  const slotSize = 6;
3001
3013
  const offset = controllerType * slotSize;
3002
3014
  const table = channel.controlTable.subarray(offset, offset + slotSize);
3003
3015
  this.processScheduledNotes(channel, (note) => {
3004
- this.setControllerParameters(channel, note, table, scheduleTime);
3016
+ this.setEffects(channel, note, table, scheduleTime);
3005
3017
  });
3006
3018
  }
3007
- handleControlChangeSysEx(data) {
3019
+ handleControlChangeSysEx(data, scheduleTime) {
3008
3020
  const channelNumber = data[4];
3009
3021
  const channel = this.channels[channelNumber];
3010
3022
  if (channel.isDrum)
3011
3023
  return;
3024
+ const slotSize = 6;
3012
3025
  const controllerType = data[5];
3013
- const table = channel.controlTable[controllerType];
3014
- for (let i = 6; i < data.length - 1; i += 2) {
3026
+ const offset = controllerType * slotSize;
3027
+ const table = channel.controlTable;
3028
+ for (let i = 6; i < data.length; i += 2) {
3015
3029
  const pp = data[i];
3016
3030
  const rr = data[i + 1];
3017
- table[pp] = rr;
3031
+ table[offset + pp] = rr;
3018
3032
  }
3033
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
3019
3034
  }
3020
3035
  getKeyBasedValue(channel, keyNumber, controllerType) {
3021
3036
  const index = keyNumber * 128 + controllerType;
3022
- const controlValue = channel.keyBasedInstrumentControlTable[index];
3037
+ const controlValue = channel.keyBasedTable[index];
3023
3038
  return controlValue;
3024
3039
  }
3025
3040
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
@@ -3028,14 +3043,102 @@ export class Midy {
3028
3043
  if (!channel.isDrum)
3029
3044
  return;
3030
3045
  const keyNumber = data[5];
3031
- const table = channel.keyBasedInstrumentControlTable;
3032
- for (let i = 6; i < data.length - 1; i += 2) {
3046
+ const table = channel.keyBasedTable;
3047
+ for (let i = 6; i < data.length; i += 2) {
3033
3048
  const controllerType = data[i];
3034
3049
  const value = data[i + 1];
3035
3050
  const index = keyNumber * 128 + controllerType;
3036
3051
  table[index] = value;
3052
+ switch (controllerType) {
3053
+ case 7:
3054
+ case 10:
3055
+ this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3056
+ break;
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;
3140
+ }
3037
3141
  }
3038
- this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3039
3142
  }
3040
3143
  handleSysEx(data, scheduleTime) {
3041
3144
  switch (data[0]) {
@@ -3084,7 +3187,7 @@ Object.defineProperty(Midy, "channelSettings", {
3084
3187
  rpnLSB: 127,
3085
3188
  mono: false, // CC#124, CC#125
3086
3189
  modulationDepthRange: 50, // cent
3087
- fineTuning: 0, // cb
3088
- coarseTuning: 0, // cb
3190
+ fineTuning: 0, // cent
3191
+ coarseTuning: 0, // cent
3089
3192
  }
3090
3193
  });