@marmooo/midy 0.4.6 → 0.4.8

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
@@ -3,6 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
+ const ogg_vorbis_1 = require("@wasm-audio-decoders/ogg-vorbis");
7
+ let decoderPromise = null;
8
+ let decoderQueue = Promise.resolve();
9
+ function initDecoder() {
10
+ if (!decoderPromise) {
11
+ const instance = new ogg_vorbis_1.OggVorbisDecoderWebWorker();
12
+ decoderPromise = instance.ready.then(() => instance);
13
+ }
14
+ return decoderPromise;
15
+ }
6
16
  class Note {
7
17
  constructor(noteNumber, velocity, startTime) {
8
18
  Object.defineProperty(this, "voice", {
@@ -41,49 +51,55 @@ class Note {
41
51
  writable: true,
42
52
  value: void 0
43
53
  });
44
- Object.defineProperty(this, "filterNode", {
54
+ Object.defineProperty(this, "filterEnvelopeNode", {
45
55
  enumerable: true,
46
56
  configurable: true,
47
57
  writable: true,
48
58
  value: void 0
49
59
  });
50
- Object.defineProperty(this, "filterDepth", {
60
+ Object.defineProperty(this, "volumeEnvelopeNode", {
51
61
  enumerable: true,
52
62
  configurable: true,
53
63
  writable: true,
54
64
  value: void 0
55
65
  });
56
- Object.defineProperty(this, "volumeEnvelopeNode", {
66
+ Object.defineProperty(this, "volumeNode", {
57
67
  enumerable: true,
58
68
  configurable: true,
59
69
  writable: true,
60
70
  value: void 0
61
- });
62
- Object.defineProperty(this, "volumeDepth", {
71
+ }); // polyphonic key pressure
72
+ Object.defineProperty(this, "modLfo", {
63
73
  enumerable: true,
64
74
  configurable: true,
65
75
  writable: true,
66
76
  value: void 0
67
- });
68
- Object.defineProperty(this, "modulationLFO", {
77
+ }); // CC#1 modulation LFO
78
+ Object.defineProperty(this, "modLfoToPitch", {
69
79
  enumerable: true,
70
80
  configurable: true,
71
81
  writable: true,
72
82
  value: void 0
73
83
  });
74
- Object.defineProperty(this, "modulationDepth", {
84
+ Object.defineProperty(this, "modLfoToFilterFc", {
75
85
  enumerable: true,
76
86
  configurable: true,
77
87
  writable: true,
78
88
  value: void 0
79
89
  });
80
- Object.defineProperty(this, "vibratoLFO", {
90
+ Object.defineProperty(this, "modLfoToVolume", {
81
91
  enumerable: true,
82
92
  configurable: true,
83
93
  writable: true,
84
94
  value: void 0
85
95
  });
86
- Object.defineProperty(this, "vibratoDepth", {
96
+ Object.defineProperty(this, "vibLfo", {
97
+ enumerable: true,
98
+ configurable: true,
99
+ writable: true,
100
+ value: void 0
101
+ }); // vibrato LFO
102
+ Object.defineProperty(this, "vibLfoToPitch", {
87
103
  enumerable: true,
88
104
  configurable: true,
89
105
  writable: true,
@@ -256,7 +272,20 @@ const pitchEnvelopeKeys = [
256
272
  "playbackRate",
257
273
  ];
258
274
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
275
+ const effectParameters = [
276
+ 2400 / 64, // cent
277
+ 9600 / 64, // cent
278
+ 1 / 64,
279
+ 600 / 127, // cent
280
+ 2400 / 127, // cent
281
+ 1 / 127,
282
+ ];
283
+ const pressureBaselines = new Int8Array([64, 64, 0, 0, 0, 0]);
259
284
  const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
285
+ const defaultControlValues = new Int8Array([
286
+ ...[-1, -1, -1, -1, -1, -1],
287
+ ...defaultPressureValues,
288
+ ]);
260
289
  function cbToRatio(cb) {
261
290
  return Math.pow(10, cb / 200);
262
291
  }
@@ -405,6 +434,12 @@ class Midy extends EventTarget {
405
434
  writable: true,
406
435
  value: new Map()
407
436
  });
437
+ Object.defineProperty(this, "decodeMethod", {
438
+ enumerable: true,
439
+ configurable: true,
440
+ writable: true,
441
+ value: "wasm-audio-decoders"
442
+ });
408
443
  Object.defineProperty(this, "isPlaying", {
409
444
  enumerable: true,
410
445
  configurable: true,
@@ -524,6 +559,9 @@ class Midy extends EventTarget {
524
559
  noteToChannel: new Map(),
525
560
  }
526
561
  });
562
+ this.decoder = new ogg_vorbis_1.OggVorbisDecoderWebWorker();
563
+ this.decoderReady = this.decoder.ready;
564
+ this.decoderQueue = Promise.resolve();
527
565
  this.audioContext = audioContext;
528
566
  this.masterVolume = new GainNode(audioContext);
529
567
  this.scheduler = new GainNode(audioContext, { gain: 0 });
@@ -535,6 +573,7 @@ class Midy extends EventTarget {
535
573
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
536
574
  this.controlChangeHandlers = this.createControlChangeHandlers();
537
575
  this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
576
+ this.effectHandlers = this.createEffectHandlers();
538
577
  this.channels = this.createChannels(audioContext);
539
578
  this.reverbEffect = this.createReverbEffect(audioContext);
540
579
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -662,7 +701,7 @@ class Midy extends EventTarget {
662
701
  };
663
702
  }
664
703
  resetChannelTable(channel) {
665
- channel.controlTable.fill(-1);
704
+ channel.controlTable.set(defaultControlValues);
666
705
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
667
706
  channel.channelPressureTable.set(defaultPressureValues);
668
707
  channel.polyphonicKeyPressureTable.set(defaultPressureValues);
@@ -679,7 +718,7 @@ class Midy extends EventTarget {
679
718
  scheduledNotes: [],
680
719
  sustainNotes: [],
681
720
  sostenutoNotes: [],
682
- controlTable: this.initControlTable(),
721
+ controlTable: new Int8Array(defaultControlValues),
683
722
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
684
723
  channelPressureTable: new Int8Array(defaultPressureValues),
685
724
  polyphonicKeyPressureTable: new Int8Array(defaultPressureValues),
@@ -690,11 +729,57 @@ class Midy extends EventTarget {
690
729
  });
691
730
  return channels;
692
731
  }
732
+ decodeOggVorbis(sample) {
733
+ const task = decoderQueue.then(async () => {
734
+ const decoder = await initDecoder();
735
+ const slice = sample.data.slice();
736
+ const { channelData, sampleRate, errors } = await decoder.decodeFile(slice);
737
+ if (0 < errors.length) {
738
+ throw new Error(errors.join(", "));
739
+ }
740
+ const audioBuffer = new AudioBuffer({
741
+ numberOfChannels: channelData.length,
742
+ length: channelData[0].length,
743
+ sampleRate,
744
+ });
745
+ for (let ch = 0; ch < channelData.length; ch++) {
746
+ audioBuffer.getChannelData(ch).set(channelData[ch]);
747
+ }
748
+ return audioBuffer;
749
+ });
750
+ decoderQueue = task.catch(() => { });
751
+ return task;
752
+ }
693
753
  async createAudioBuffer(voiceParams) {
694
- const { sample, start, end } = voiceParams;
695
- const sampleEnd = sample.data.length + end;
696
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
697
- return audioBuffer;
754
+ const sample = voiceParams.sample;
755
+ if (sample.type === "compressed") {
756
+ switch (this.decodeMethod) {
757
+ case "decodeAudioData": {
758
+ // https://jakearchibald.com/2016/sounds-fun/
759
+ // https://github.com/WebAudio/web-audio-api/issues/1091
760
+ // decodeAudioData() has priming issues on Safari
761
+ const arrayBuffer = sample.data.slice().buffer;
762
+ return await this.audioContext.decodeAudioData(arrayBuffer);
763
+ }
764
+ case "wasm-audio-decoders":
765
+ return await this.decodeOggVorbis(sample);
766
+ default:
767
+ throw new Error(`Unknown decodeMethod: ${this.decodeMethod}`);
768
+ }
769
+ }
770
+ else {
771
+ const data = sample.data;
772
+ const end = data.length + voiceParams.end;
773
+ const subarray = data.subarray(voiceParams.start, end);
774
+ const pcm = sample.decodePCM(subarray);
775
+ const audioBuffer = new AudioBuffer({
776
+ numberOfChannels: 1,
777
+ length: pcm.length,
778
+ sampleRate: sample.sampleHeader.sampleRate,
779
+ });
780
+ audioBuffer.getChannelData(0).set(pcm);
781
+ return audioBuffer;
782
+ }
698
783
  }
699
784
  isLoopDrum(channel, noteNumber) {
700
785
  const programNumber = channel.programNumber;
@@ -1008,8 +1093,8 @@ class Midy extends EventTarget {
1008
1093
  }
1009
1094
  stopNotes(velocity, force, scheduleTime) {
1010
1095
  const channels = this.channels;
1011
- for (let i = 0; i < channels.length; i++) {
1012
- this.stopChannelNotes(i, velocity, force, scheduleTime);
1096
+ for (let ch = 0; ch < channels.length; ch++) {
1097
+ this.stopChannelNotes(ch, velocity, force, scheduleTime);
1013
1098
  }
1014
1099
  const stopPromise = Promise.all(this.notePromises);
1015
1100
  this.notePromises = [];
@@ -1284,16 +1369,8 @@ class Midy extends EventTarget {
1284
1369
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1285
1370
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1286
1371
  const pitch = pitchWheel * pitchWheelSensitivity;
1287
- const channelPressureRaw = channel.channelPressureTable[0];
1288
- if (0 <= channelPressureRaw) {
1289
- const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1290
- const channelPressure = channelPressureDepth *
1291
- channel.state.channelPressure;
1292
- return tuning + pitch + channelPressure;
1293
- }
1294
- else {
1295
- return tuning + pitch;
1296
- }
1372
+ const effect = this.getChannelPitchControl(channel);
1373
+ return tuning + pitch + effect;
1297
1374
  }
1298
1375
  updateChannelDetune(channel, scheduleTime) {
1299
1376
  this.processScheduledNotes(channel, (note) => {
@@ -1311,7 +1388,7 @@ class Midy extends EventTarget {
1311
1388
  calcNoteDetune(channel, note) {
1312
1389
  const noteDetune = note.voiceParams.detune +
1313
1390
  this.calcScaleOctaveTuning(channel, note);
1314
- const pitchControl = this.getPitchControl(channel, note);
1391
+ const pitchControl = this.getNotePitchControl(channel, note);
1315
1392
  return channel.detune + noteDetune + pitchControl;
1316
1393
  }
1317
1394
  getPortamentoTime(channel, note) {
@@ -1380,7 +1457,7 @@ class Midy extends EventTarget {
1380
1457
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1381
1458
  const { voiceParams, startTime } = note;
1382
1459
  const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
1383
- (1 + this.getAmplitudeControl(channel, note));
1460
+ (1 + this.getChannelAmplitudeControl(channel));
1384
1461
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1385
1462
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1386
1463
  note.volumeEnvelopeNode.gain
@@ -1388,15 +1465,17 @@ class Midy extends EventTarget {
1388
1465
  .exponentialRampToValueAtTime(sustainVolume, portamentoTime);
1389
1466
  }
1390
1467
  setVolumeEnvelope(channel, note, scheduleTime) {
1391
- const { voiceParams, startTime } = note;
1468
+ const { voiceParams, startTime, noteNumber } = note;
1392
1469
  const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
1393
- (1 + this.getAmplitudeControl(channel, note));
1470
+ (1 + this.getChannelAmplitudeControl(channel));
1394
1471
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1395
1472
  const volDelay = startTime + voiceParams.volDelay;
1396
- const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1473
+ const attackTime = this.getRelativeKeyBasedValue(channel, noteNumber, 73) *
1474
+ 2;
1397
1475
  const volAttack = volDelay + voiceParams.volAttack * attackTime;
1398
1476
  const volHold = volAttack + voiceParams.volHold;
1399
- const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
1477
+ const decayTime = this.getRelativeKeyBasedValue(channel, noteNumber, 75) *
1478
+ 2;
1400
1479
  const decayDuration = voiceParams.volDecay * decayTime;
1401
1480
  note.volumeEnvelopeNode.gain
1402
1481
  .cancelScheduledValues(scheduleTime)
@@ -1406,6 +1485,12 @@ class Midy extends EventTarget {
1406
1485
  .setValueAtTime(attackVolume, volHold)
1407
1486
  .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
1408
1487
  }
1488
+ setVolumeNode(channel, note, scheduleTime) {
1489
+ const depth = 1 + this.getNoteAmplitudeControl(channel, note);
1490
+ note.volumeNode.gain
1491
+ .cancelScheduledValues(scheduleTime)
1492
+ .setValueAtTime(depth, scheduleTime);
1493
+ }
1409
1494
  setPortamentoDetune(channel, note, scheduleTime) {
1410
1495
  if (channel.portamentoControl) {
1411
1496
  const state = channel.state;
@@ -1467,9 +1552,10 @@ class Midy extends EventTarget {
1467
1552
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1468
1553
  }
1469
1554
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1470
- const { voiceParams, startTime } = note;
1555
+ const { voiceParams, startTime, noteNumber } = note;
1471
1556
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1472
- const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1557
+ const brightness = this.getRelativeKeyBasedValue(channel, noteNumber, 74) *
1558
+ 2;
1473
1559
  const scale = softPedalFactor * brightness;
1474
1560
  const baseCent = voiceParams.initialFilterFc +
1475
1561
  this.getFilterCutoffControl(channel, note);
@@ -1482,14 +1568,14 @@ class Midy extends EventTarget {
1482
1568
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1483
1569
  const modDelay = startTime + voiceParams.modDelay;
1484
1570
  note.adjustedBaseFreq = adjustedSustainFreq;
1485
- note.filterNode.frequency
1571
+ note.filterEnvelopeNode.frequency
1486
1572
  .cancelScheduledValues(scheduleTime)
1487
1573
  .setValueAtTime(adjustedBaseFreq, startTime)
1488
1574
  .setValueAtTime(adjustedBaseFreq, modDelay)
1489
1575
  .exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1490
1576
  }
1491
1577
  setFilterEnvelope(channel, note, scheduleTime) {
1492
- const { voiceParams, startTime } = note;
1578
+ const { voiceParams, startTime, noteNumber } = note;
1493
1579
  const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
1494
1580
  const baseCent = voiceParams.initialFilterFc +
1495
1581
  this.getFilterCutoffControl(channel, note);
@@ -1497,7 +1583,8 @@ class Midy extends EventTarget {
1497
1583
  const sustainCent = baseCent +
1498
1584
  modEnvToFilterFc * (1 - voiceParams.modSustain);
1499
1585
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1500
- const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1586
+ const brightness = this.getRelativeKeyBasedValue(channel, noteNumber, 74) *
1587
+ 2;
1501
1588
  const scale = softPedalFactor * brightness;
1502
1589
  const baseFreq = this.centToHz(baseCent) * scale;
1503
1590
  const peekFreq = this.centToHz(peekCent) * scale;
@@ -1508,9 +1595,9 @@ class Midy extends EventTarget {
1508
1595
  const modDelay = startTime + voiceParams.modDelay;
1509
1596
  const modAttack = modDelay + voiceParams.modAttack;
1510
1597
  const modHold = modAttack + voiceParams.modHold;
1511
- const decayDuration = modHold + voiceParams.modDecay;
1598
+ const decayDuration = voiceParams.modDecay;
1512
1599
  note.adjustedBaseFreq = adjustedBaseFreq;
1513
- note.filterNode.frequency
1600
+ note.filterEnvelopeNode.frequency
1514
1601
  .cancelScheduledValues(scheduleTime)
1515
1602
  .setValueAtTime(adjustedBaseFreq, startTime)
1516
1603
  .setValueAtTime(adjustedBaseFreq, modDelay)
@@ -1521,36 +1608,37 @@ class Midy extends EventTarget {
1521
1608
  startModulation(channel, note, scheduleTime) {
1522
1609
  const audioContext = this.audioContext;
1523
1610
  const { voiceParams } = note;
1524
- note.modulationLFO = new OscillatorNode(audioContext, {
1611
+ note.modLfo = new OscillatorNode(audioContext, {
1525
1612
  frequency: this.centToHz(voiceParams.freqModLFO),
1526
1613
  });
1527
- note.filterDepth = new GainNode(audioContext, {
1614
+ note.modLfoToFilterFc = new GainNode(audioContext, {
1528
1615
  gain: voiceParams.modLfoToFilterFc,
1529
1616
  });
1530
- note.modulationDepth = new GainNode(audioContext);
1617
+ note.modLfoToPitch = new GainNode(audioContext);
1531
1618
  this.setModLfoToPitch(channel, note, scheduleTime);
1532
- note.volumeDepth = new GainNode(audioContext);
1619
+ note.modLfoToVolume = new GainNode(audioContext);
1533
1620
  this.setModLfoToVolume(channel, note, scheduleTime);
1534
- note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1535
- note.modulationLFO.connect(note.filterDepth);
1536
- note.filterDepth.connect(note.filterNode.frequency);
1537
- note.modulationLFO.connect(note.modulationDepth);
1538
- note.modulationDepth.connect(note.bufferSource.detune);
1539
- note.modulationLFO.connect(note.volumeDepth);
1540
- note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1621
+ note.modLfo.start(note.startTime + voiceParams.delayModLFO);
1622
+ note.modLfo.connect(note.modLfoToFilterFc);
1623
+ note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
1624
+ note.modLfo.connect(note.modLfoToPitch);
1625
+ note.modLfoToPitch.connect(note.bufferSource.detune);
1626
+ note.modLfo.connect(note.modLfoToVolume);
1627
+ note.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
1541
1628
  }
1542
1629
  startVibrato(channel, note, scheduleTime) {
1543
- const { voiceParams } = note;
1544
- const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1545
- const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1546
- note.vibratoLFO = new OscillatorNode(this.audioContext, {
1630
+ const { voiceParams, noteNumber } = note;
1631
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, noteNumber, 76) *
1632
+ 2;
1633
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, noteNumber, 78) * 2;
1634
+ note.vibLfo = new OscillatorNode(this.audioContext, {
1547
1635
  frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
1548
1636
  });
1549
- note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1550
- note.vibratoDepth = new GainNode(this.audioContext);
1637
+ note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1638
+ note.vibLfoToPitch = new GainNode(this.audioContext);
1551
1639
  this.setVibLfoToPitch(channel, note, scheduleTime);
1552
- note.vibratoLFO.connect(note.vibratoDepth);
1553
- note.vibratoDepth.connect(note.bufferSource.detune);
1640
+ note.vibLfo.connect(note.vibLfoToPitch);
1641
+ note.vibLfoToPitch.connect(note.bufferSource.detune);
1554
1642
  }
1555
1643
  async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1556
1644
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
@@ -1591,8 +1679,9 @@ class Midy extends EventTarget {
1591
1679
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1592
1680
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1593
1681
  note.volumeEnvelopeNode = new GainNode(audioContext);
1594
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
1595
- note.filterNode = new BiquadFilterNode(audioContext, {
1682
+ note.volumeNode = new GainNode(audioContext);
1683
+ const filterResonance = this.getRelativeKeyBasedValue(channel, noteNumber, 71);
1684
+ note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
1596
1685
  type: "lowpass",
1597
1686
  Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1598
1687
  });
@@ -1600,6 +1689,7 @@ class Midy extends EventTarget {
1600
1689
  if (prevNote && prevNote.noteNumber !== noteNumber) {
1601
1690
  note.portamentoNoteNumber = prevNote.noteNumber;
1602
1691
  }
1692
+ this.setVolumeNode(channel, note, now);
1603
1693
  if (!channel.isDrum && this.isPortamento(channel, note)) {
1604
1694
  this.setPortamentoVolumeEnvelope(channel, note, now);
1605
1695
  this.setPortamentoFilterEnvelope(channel, note, now);
@@ -1622,8 +1712,9 @@ class Midy extends EventTarget {
1622
1712
  channel.currentBufferSource.stop(startTime);
1623
1713
  channel.currentBufferSource = note.bufferSource;
1624
1714
  }
1625
- note.bufferSource.connect(note.filterNode);
1626
- note.filterNode.connect(note.volumeEnvelopeNode);
1715
+ note.bufferSource.connect(note.filterEnvelopeNode);
1716
+ note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
1717
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1627
1718
  this.setChorusSend(channel, note, now);
1628
1719
  this.setReverbSend(channel, note, now);
1629
1720
  if (voiceParams.sample.type === "compressed") {
@@ -1670,7 +1761,7 @@ class Midy extends EventTarget {
1670
1761
  }
1671
1762
  setNoteRouting(channelNumber, note, startTime) {
1672
1763
  const channel = this.channels[channelNumber];
1673
- const { noteNumber, volumeEnvelopeNode } = note;
1764
+ const { noteNumber, volumeNode } = note;
1674
1765
  if (channel.isDrum) {
1675
1766
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1676
1767
  let gainL = keyBasedGainLs[noteNumber];
@@ -1680,12 +1771,12 @@ class Midy extends EventTarget {
1680
1771
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1681
1772
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1682
1773
  }
1683
- volumeEnvelopeNode.connect(gainL);
1684
- volumeEnvelopeNode.connect(gainR);
1774
+ volumeNode.connect(gainL);
1775
+ volumeNode.connect(gainR);
1685
1776
  }
1686
1777
  else {
1687
- volumeEnvelopeNode.connect(channel.gainL);
1688
- volumeEnvelopeNode.connect(channel.gainR);
1778
+ volumeNode.connect(channel.gainL);
1779
+ volumeNode.connect(channel.gainR);
1689
1780
  }
1690
1781
  if (0.5 <= channel.state.sustainPedal) {
1691
1782
  channel.sustainNotes.push(note);
@@ -1717,6 +1808,8 @@ class Midy extends EventTarget {
1717
1808
  scheduledNotes.push(note);
1718
1809
  const programNumber = channel.programNumber;
1719
1810
  const bankTable = this.soundFontTable[programNumber];
1811
+ if (!bankTable)
1812
+ return;
1720
1813
  let bank = channel.isDrum ? 128 : channel.bankLSB;
1721
1814
  if (bankTable[bank] === undefined) {
1722
1815
  if (channel.isDrum)
@@ -1737,16 +1830,17 @@ class Midy extends EventTarget {
1737
1830
  }
1738
1831
  disconnectNote(note) {
1739
1832
  note.bufferSource.disconnect();
1740
- note.filterNode.disconnect();
1833
+ note.filterEnvelopeNode.disconnect();
1741
1834
  note.volumeEnvelopeNode.disconnect();
1742
- if (note.modulationDepth) {
1743
- note.volumeDepth.disconnect();
1744
- note.modulationDepth.disconnect();
1745
- note.modulationLFO.stop();
1835
+ note.volumeNode.disconnect();
1836
+ if (note.modLfoToPitch) {
1837
+ note.modLfoToVolume.disconnect();
1838
+ note.modLfoToPitch.disconnect();
1839
+ note.modLfo.stop();
1746
1840
  }
1747
- if (note.vibratoDepth) {
1748
- note.vibratoDepth.disconnect();
1749
- note.vibratoLFO.stop();
1841
+ if (note.vibLfoToPitch) {
1842
+ note.vibLfoToPitch.disconnect();
1843
+ note.vibLfo.stop();
1750
1844
  }
1751
1845
  if (note.reverbSend) {
1752
1846
  note.reverbSend.disconnect();
@@ -1757,10 +1851,10 @@ class Midy extends EventTarget {
1757
1851
  }
1758
1852
  releaseNote(channel, note, endTime) {
1759
1853
  endTime ??= this.audioContext.currentTime;
1760
- const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1854
+ const releaseTime = this.getRelativeKeyBasedValue(channel, note.noteNumber, 72) * 2;
1761
1855
  const volDuration = note.voiceParams.volRelease * releaseTime;
1762
1856
  const volRelease = endTime + volDuration;
1763
- note.filterNode.frequency
1857
+ note.filterEnvelopeNode.frequency
1764
1858
  .cancelScheduledValues(endTime)
1765
1859
  .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
1766
1860
  note.volumeEnvelopeNode.gain
@@ -1930,11 +2024,10 @@ class Midy extends EventTarget {
1930
2024
  return;
1931
2025
  if (!(0 <= scheduleTime))
1932
2026
  scheduleTime = this.audioContext.currentTime;
1933
- const table = channel.polyphonicKeyPressureTable;
1934
2027
  this.processActiveNotes(channel, scheduleTime, (note) => {
1935
2028
  if (note.noteNumber === noteNumber) {
1936
2029
  note.pressure = pressure;
1937
- this.setEffects(channel, note, table, scheduleTime);
2030
+ this.setPolyphonicKeyPressureEffects(channel, note, scheduleTime);
1938
2031
  }
1939
2032
  });
1940
2033
  this.applyVoiceParams(channel, 10, scheduleTime);
@@ -1960,27 +2053,22 @@ class Midy extends EventTarget {
1960
2053
  }
1961
2054
  }
1962
2055
  setChannelPressure(channelNumber, value, scheduleTime) {
2056
+ if (!(0 <= scheduleTime))
2057
+ scheduleTime = this.audioContext.currentTime;
1963
2058
  this.applyToMPEChannels(channelNumber, (ch) => {
1964
2059
  this.applyChannelPressure(ch, value, scheduleTime);
1965
2060
  });
1966
2061
  }
1967
2062
  applyChannelPressure(channelNumber, value, scheduleTime) {
1968
- if (!(0 <= scheduleTime))
1969
- scheduleTime = this.audioContext.currentTime;
1970
2063
  const channel = this.channels[channelNumber];
1971
2064
  if (channel.isDrum)
1972
2065
  return;
1973
- const prev = channel.state.channelPressure;
1974
- const next = value / 127;
1975
- channel.state.channelPressure = next;
1976
- const channelPressureRaw = channel.channelPressureTable[0];
1977
- if (0 <= channelPressureRaw) {
1978
- const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1979
- channel.detune += channelPressureDepth * (next - prev);
1980
- }
1981
- const table = channel.channelPressureTable;
2066
+ const prev = this.calcChannelPressureEffectValue(channel, 0);
2067
+ channel.state.channelPressure = value / 127;
2068
+ const next = this.calcChannelPressureEffectValue(channel, 0);
2069
+ channel.detune += next - prev;
1982
2070
  this.processActiveNotes(channel, scheduleTime, (note) => {
1983
- this.setEffects(channel, note, table, scheduleTime);
2071
+ this.setChannelPressureEffects(channel, note, scheduleTime);
1984
2072
  });
1985
2073
  this.applyVoiceParams(channel, 13, scheduleTime);
1986
2074
  }
@@ -2015,7 +2103,7 @@ class Midy extends EventTarget {
2015
2103
  this.getLFOPitchDepth(channel, note);
2016
2104
  const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
2017
2105
  const depth = baseDepth * Math.sign(modLfoToPitch);
2018
- note.modulationDepth.gain
2106
+ note.modLfoToPitch.gain
2019
2107
  .cancelScheduledValues(scheduleTime)
2020
2108
  .setValueAtTime(depth, scheduleTime);
2021
2109
  }
@@ -2024,13 +2112,12 @@ class Midy extends EventTarget {
2024
2112
  }
2025
2113
  }
2026
2114
  setVibLfoToPitch(channel, note, scheduleTime) {
2027
- if (note.vibratoDepth) {
2028
- const vibratoDepth = this.getKeyBasedValue(channel, note.noteNumber, 77) *
2029
- 2;
2115
+ if (note.vibLfoToPitch) {
2116
+ const vibratoDepth = this.getRelativeKeyBasedValue(channel, note.noteNumber, 77) * 2;
2030
2117
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
2031
2118
  const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
2032
2119
  const depth = baseDepth * Math.sign(vibLfoToPitch);
2033
- note.vibratoDepth.gain
2120
+ note.vibLfoToPitch.gain
2034
2121
  .cancelScheduledValues(scheduleTime)
2035
2122
  .setValueAtTime(depth, scheduleTime);
2036
2123
  }
@@ -2041,18 +2128,18 @@ class Midy extends EventTarget {
2041
2128
  setModLfoToFilterFc(channel, note, scheduleTime) {
2042
2129
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
2043
2130
  this.getLFOFilterDepth(channel, note);
2044
- note.filterDepth.gain
2131
+ note.modLfoToFilterFc.gain
2045
2132
  .cancelScheduledValues(scheduleTime)
2046
2133
  .setValueAtTime(modLfoToFilterFc, scheduleTime);
2047
2134
  }
2048
2135
  setModLfoToVolume(channel, note, scheduleTime) {
2049
2136
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
2050
2137
  const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
2051
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
2138
+ const depth = baseDepth * Math.sign(modLfoToVolume) *
2052
2139
  (1 + this.getLFOAmplitudeDepth(channel, note));
2053
- note.volumeDepth.gain
2140
+ note.modLfoToVolume.gain
2054
2141
  .cancelScheduledValues(scheduleTime)
2055
- .setValueAtTime(volumeDepth, scheduleTime);
2142
+ .setValueAtTime(depth, scheduleTime);
2056
2143
  }
2057
2144
  setReverbSend(channel, note, scheduleTime) {
2058
2145
  let value = note.voiceParams.reverbEffectsSend *
@@ -2065,7 +2152,7 @@ class Midy extends EventTarget {
2065
2152
  if (!note.reverbSend) {
2066
2153
  if (0 < value) {
2067
2154
  note.reverbSend = new GainNode(this.audioContext, { gain: value });
2068
- note.volumeEnvelopeNode.connect(note.reverbSend);
2155
+ note.volumeNode.connect(note.reverbSend);
2069
2156
  note.reverbSend.connect(this.reverbEffect.input);
2070
2157
  }
2071
2158
  }
@@ -2074,11 +2161,11 @@ class Midy extends EventTarget {
2074
2161
  .cancelScheduledValues(scheduleTime)
2075
2162
  .setValueAtTime(value, scheduleTime);
2076
2163
  if (0 < value) {
2077
- note.volumeEnvelopeNode.connect(note.reverbSend);
2164
+ note.volumeNode.connect(note.reverbSend);
2078
2165
  }
2079
2166
  else {
2080
2167
  try {
2081
- note.volumeEnvelopeNode.disconnect(note.reverbSend);
2168
+ note.volumeNode.disconnect(note.reverbSend);
2082
2169
  }
2083
2170
  catch { /* empty */ }
2084
2171
  }
@@ -2095,7 +2182,7 @@ class Midy extends EventTarget {
2095
2182
  if (!note.chorusSend) {
2096
2183
  if (0 < value) {
2097
2184
  note.chorusSend = new GainNode(this.audioContext, { gain: value });
2098
- note.volumeEnvelopeNode.connect(note.chorusSend);
2185
+ note.volumeNode.connect(note.chorusSend);
2099
2186
  note.chorusSend.connect(this.chorusEffect.input);
2100
2187
  }
2101
2188
  }
@@ -2104,11 +2191,11 @@ class Midy extends EventTarget {
2104
2191
  .cancelScheduledValues(scheduleTime)
2105
2192
  .setValueAtTime(value, scheduleTime);
2106
2193
  if (0 < value) {
2107
- note.volumeEnvelopeNode.connect(note.chorusSend);
2194
+ note.volumeNode.connect(note.chorusSend);
2108
2195
  }
2109
2196
  else {
2110
2197
  try {
2111
- note.volumeEnvelopeNode.disconnect(note.chorusSend);
2198
+ note.volumeNode.disconnect(note.chorusSend);
2112
2199
  }
2113
2200
  catch { /* empty */ }
2114
2201
  }
@@ -2117,29 +2204,29 @@ class Midy extends EventTarget {
2117
2204
  setDelayModLFO(note) {
2118
2205
  const startTime = note.startTime + note.voiceParams.delayModLFO;
2119
2206
  try {
2120
- note.modulationLFO.start(startTime);
2207
+ note.modLfo.start(startTime);
2121
2208
  }
2122
2209
  catch { /* empty */ }
2123
2210
  }
2124
2211
  setFreqModLFO(note, scheduleTime) {
2125
2212
  const freqModLFO = note.voiceParams.freqModLFO;
2126
- note.modulationLFO.frequency
2213
+ note.modLfo.frequency
2127
2214
  .cancelScheduledValues(scheduleTime)
2128
2215
  .setValueAtTime(freqModLFO, scheduleTime);
2129
2216
  }
2130
2217
  setDelayVibLFO(channel, note) {
2131
- const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
2218
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note.noteNumber, 78) * 2;
2132
2219
  const value = note.voiceParams.delayVibLFO;
2133
2220
  const startTime = note.startTime + value * vibratoDelay;
2134
2221
  try {
2135
- note.vibratoLFO.start(startTime);
2222
+ note.vibLfo.start(startTime);
2136
2223
  }
2137
2224
  catch { /* empty */ }
2138
2225
  }
2139
2226
  setFreqVibLFO(channel, note, scheduleTime) {
2140
- const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
2227
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note.noteNumber, 76) * 2;
2141
2228
  const freqVibLFO = note.voiceParams.freqVibLFO;
2142
- note.vibratoLFO.frequency
2229
+ note.vibLfo.frequency
2143
2230
  .cancelScheduledValues(scheduleTime)
2144
2231
  .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
2145
2232
  }
@@ -2188,7 +2275,7 @@ class Midy extends EventTarget {
2188
2275
  },
2189
2276
  delayVibLFO: (channel, note, _scheduleTime) => {
2190
2277
  if (0 < channel.state.vibratoDepth) {
2191
- setDelayVibLFO(channel, note);
2278
+ this.setDelayVibLFO(channel, note);
2192
2279
  }
2193
2280
  },
2194
2281
  freqVibLFO: (channel, note, scheduleTime) => {
@@ -2295,6 +2382,8 @@ class Midy extends EventTarget {
2295
2382
  return handlers;
2296
2383
  }
2297
2384
  setControlChange(channelNumber, controllerType, value, scheduleTime) {
2385
+ if (!(0 <= scheduleTime))
2386
+ scheduleTime = this.audioContext.currentTime;
2298
2387
  this.applyToMPEChannels(channelNumber, (ch) => {
2299
2388
  this.applyControlChange(ch, controllerType, value, scheduleTime);
2300
2389
  });
@@ -2305,7 +2394,9 @@ class Midy extends EventTarget {
2305
2394
  handler.call(this, channelNumber, value, scheduleTime);
2306
2395
  const channel = this.channels[channelNumber];
2307
2396
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
2308
- this.setControlChangeEffects(channel, controllerType, scheduleTime);
2397
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2398
+ this.setControlChangeEffects(channel, note, scheduleTime);
2399
+ });
2309
2400
  }
2310
2401
  else {
2311
2402
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -2377,6 +2468,9 @@ class Midy extends EventTarget {
2377
2468
  const intPart = Math.trunc(value);
2378
2469
  state.volumeMSB = intPart / 127;
2379
2470
  state.volumeLSB = value - intPart;
2471
+ this.applyVolume(channel, scheduleTime);
2472
+ }
2473
+ applyVolume(channel, scheduleTime) {
2380
2474
  if (channel.isDrum) {
2381
2475
  for (let i = 0; i < 128; i++) {
2382
2476
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2387,7 +2481,7 @@ class Midy extends EventTarget {
2387
2481
  }
2388
2482
  }
2389
2483
  panToGain(pan) {
2390
- const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
2484
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
2391
2485
  return {
2392
2486
  gainLeft: Math.cos(theta),
2393
2487
  gainRight: Math.sin(theta),
@@ -2432,7 +2526,8 @@ class Midy extends EventTarget {
2432
2526
  const volume = volumeMSB + volumeLSB / 128;
2433
2527
  const expression = expressionMSB + expressionLSB / 128;
2434
2528
  const pan = panMSB + panLSB / 128;
2435
- const gain = volume * expression;
2529
+ const effect = this.getChannelAmplitudeControl(channel);
2530
+ const gain = volume * expression * (1 + effect);
2436
2531
  const { gainLeft, gainRight } = this.panToGain(pan);
2437
2532
  channel.gainL.gain
2438
2533
  .cancelScheduledValues(scheduleTime)
@@ -2542,19 +2637,11 @@ class Midy extends EventTarget {
2542
2637
  const state = channel.state;
2543
2638
  state.filterResonance = ccValue / 127;
2544
2639
  this.processScheduledNotes(channel, (note) => {
2545
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2640
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note.noteNumber, 71);
2546
2641
  const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2547
- note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2642
+ note.filterEnvelopeNode.Q.setValueAtTime(Q, scheduleTime);
2548
2643
  });
2549
2644
  }
2550
- getRelativeKeyBasedValue(channel, note, controllerType) {
2551
- const ccState = channel.state.array[128 + controllerType];
2552
- const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, controllerType);
2553
- if (keyBasedValue < 0)
2554
- return ccState;
2555
- const keyValue = ccState + keyBasedValue / 127 - 0.5;
2556
- return keyValue < 0 ? keyValue : 0;
2557
- }
2558
2645
  setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2559
2646
  const channel = this.channels[channelNumber];
2560
2647
  if (channel.isDrum)
@@ -2827,6 +2914,7 @@ class Midy extends EventTarget {
2827
2914
  this.updateModulation(channel, scheduleTime);
2828
2915
  }
2829
2916
  handleMIDIPolyphonicExpressionRPN(channelNumber, _scheduleTime) {
2917
+ const channel = this.channels[channelNumber];
2830
2918
  this.setMIDIPolyphonicExpression(channelNumber, channel.dataMSB);
2831
2919
  }
2832
2920
  setMIDIPolyphonicExpression(channelNumber, value) {
@@ -3047,9 +3135,9 @@ class Midy extends EventTarget {
3047
3135
  case 9:
3048
3136
  switch (data[3]) {
3049
3137
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
3050
- return this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
3138
+ return this.handleChannelPressureSysEx(data, scheduelTime);
3051
3139
  case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
3052
- return this.handlePressureSysEx(data, "polyphonicKeyPressureTable", scheduleTime);
3140
+ return this.handlePolyphonicKeyPressureSysEx(data, scheduleTime);
3053
3141
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
3054
3142
  return this.handleControlChangeSysEx(data, scheduleTime);
3055
3143
  default:
@@ -3358,98 +3446,137 @@ class Midy extends EventTarget {
3358
3446
  this.updateChannelDetune(channel, scheduleTime);
3359
3447
  }
3360
3448
  }
3361
- getPitchControl(channel, note) {
3362
- const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
3363
- if (polyphonicKeyPressureRaw <= 0)
3449
+ calcEffectValue(channel, note, destination) {
3450
+ return this.calcChannelEffectValue(channel, destination) +
3451
+ this.calcNoteEffectValue(channel, note, destination);
3452
+ }
3453
+ calcChannelEffectValue(channel, destination) {
3454
+ return this.calcControlChangeEffectValue(channel, destination) +
3455
+ this.calcChannelPressureEffectValue(channel, destination);
3456
+ }
3457
+ calcControlChangeEffectValue(channel, destination) {
3458
+ const controlType = channel.controlTable[destination];
3459
+ if (controlType < 0)
3460
+ return 0;
3461
+ const pressure = channel.state.array[controlType];
3462
+ if (pressure <= 0)
3463
+ return 0;
3464
+ const baseline = pressureBaselines[destination];
3465
+ const tableValue = channel.controlTable[destination + 6];
3466
+ const value = (tableValue - baseline) * pressure;
3467
+ return value * effectParameters[destination];
3468
+ }
3469
+ calcChannelPressureEffectValue(channel, destination) {
3470
+ const pressure = channel.state.channelPressure;
3471
+ if (pressure <= 0)
3472
+ return 0;
3473
+ const baseline = pressureBaselines[destination];
3474
+ const tableValue = channel.channelPressureTable[destination];
3475
+ const value = (tableValue - baseline) * pressure;
3476
+ return value * effectParameters[destination];
3477
+ }
3478
+ calcNoteEffectValue(channel, note, destination) {
3479
+ const pressure = note.pressure;
3480
+ if (pressure <= 0)
3364
3481
  return 0;
3365
- const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
3366
- note.pressure;
3367
- return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
3482
+ const baseline = pressureBaselines[destination];
3483
+ const tableValue = channel.polyphonicKeyPressureTable[destination];
3484
+ const value = (tableValue - baseline) * pressure / 127;
3485
+ return value * effectParameters[destination];
3486
+ }
3487
+ getChannelPitchControl(channel) {
3488
+ return this.calcChannelEffectValue(channel, 0);
3489
+ }
3490
+ getNotePitchControl(channel, note) {
3491
+ return this.calcNoteEffectValue(channel, note, 0);
3492
+ }
3493
+ getPitchControl(channel, note) {
3494
+ return this.calcEffectValue(channel, note, 0);
3368
3495
  }
3369
3496
  getFilterCutoffControl(channel, note) {
3370
- const channelPressureRaw = channel.channelPressureTable[1];
3371
- const channelPressure = (0 <= channelPressureRaw)
3372
- ? (channelPressureRaw - 64) * channel.state.channelPressure
3373
- : 0;
3374
- const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[1];
3375
- const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
3376
- ? (polyphonicKeyPressureRaw - 64) * note.pressure
3377
- : 0;
3378
- return (channelPressure + polyphonicKeyPressure) * 15;
3497
+ return this.calcEffectValue(channel, note, 1);
3498
+ }
3499
+ getChannelAmplitudeControl(channel) {
3500
+ return this.calcChannelEffectValue(channel, 2);
3501
+ }
3502
+ getNoteAmplitudeControl(channel, note) {
3503
+ return this.calcNoteEffectValue(channel, note, 2);
3379
3504
  }
3380
3505
  getAmplitudeControl(channel, note) {
3381
- const channelPressureRaw = channel.channelPressureTable[2];
3382
- const channelPressure = (0 <= channelPressureRaw)
3383
- ? channel.state.channelPressure * 127 / channelPressureRaw
3384
- : 0;
3385
- const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
3386
- const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
3387
- ? note.pressure / polyphonicKeyPressureRaw
3388
- : 0;
3389
- return channelPressure + polyphonicKeyPressure;
3506
+ return this.calcEffectValue(channel, note, 2);
3390
3507
  }
3391
3508
  getLFOPitchDepth(channel, note) {
3392
- const channelPressureRaw = channel.channelPressureTable[3];
3393
- const channelPressure = (0 <= channelPressureRaw)
3394
- ? channelPressureRaw * channel.state.channelPressure
3395
- : 0;
3396
- const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
3397
- const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
3398
- ? polyphonicKeyPressureRaw * note.pressure
3399
- : 0;
3400
- return (channelPressure + polyphonicKeyPressure) / 254 * 600;
3509
+ return this.calcEffectValue(channel, note, 3);
3401
3510
  }
3402
3511
  getLFOFilterDepth(channel, note) {
3403
- const channelPressureRaw = channel.channelPressureTable[4];
3404
- const channelPressure = (0 <= channelPressureRaw)
3405
- ? channelPressureRaw * channel.state.channelPressure
3406
- : 0;
3407
- const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
3408
- const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
3409
- ? polyphonicKeyPressureRaw * note.pressure
3410
- : 0;
3411
- return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
3512
+ return this.calcEffectValue(channel, note, 4);
3412
3513
  }
3413
3514
  getLFOAmplitudeDepth(channel, note) {
3414
- const channelPressureRaw = channel.channelPressureTable[5];
3415
- const channelPressure = (0 <= channelPressureRaw)
3416
- ? channelPressureRaw * channel.state.channelPressure
3417
- : 0;
3418
- const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[5];
3419
- const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
3420
- ? polyphonicKeyPressureRaw * note.pressure
3421
- : 0;
3422
- return (channelPressure + polyphonicKeyPressure) / 254;
3423
- }
3424
- setEffects(channel, note, table, scheduleTime) {
3425
- if (0 < table[0]) {
3515
+ return this.calcEffectValue(channel, note, 5);
3516
+ }
3517
+ createEffectHandlers() {
3518
+ const handlers = new Array(6);
3519
+ handlers[0] = (channel, note, _tableName, scheduleTime) => {
3426
3520
  if (this.isPortamento(channel, note)) {
3427
3521
  this.setPortamentoDetune(channel, note, scheduleTime);
3428
3522
  }
3429
3523
  else {
3430
3524
  this.setDetune(channel, note, scheduleTime);
3431
3525
  }
3432
- }
3433
- if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
3434
- if (0 < table[1]) {
3526
+ };
3527
+ handlers[1] = (channel, note, _tableName, scheduleTime) => {
3528
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
3435
3529
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
3436
3530
  }
3437
- if (0 < table[2]) {
3438
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
3531
+ else {
3532
+ this.setFilterEnvelope(channel, note, scheduleTime);
3533
+ }
3534
+ };
3535
+ handlers[2] = (channel, note, tableName, scheduleTime) => {
3536
+ if (tableName === "polyphonicKeyPressureTable") {
3537
+ this.setVolumeNode(channel, note, scheduleTime);
3439
3538
  }
3539
+ else {
3540
+ this.applyVolume(channel, scheduleTime);
3541
+ }
3542
+ };
3543
+ handlers[3] = (channel, note, _tableName, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
3544
+ handlers[4] = (channel, note, _tableName, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
3545
+ handlers[5] = (channel, note, _tableName, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
3546
+ return handlers;
3547
+ }
3548
+ setControlChangeEffects(channel, note, scheduleTime) {
3549
+ const handlers = this.effectHandlers;
3550
+ for (let i = 0; i < handlers.length; i++) {
3551
+ const baseline = pressureBaselines[i];
3552
+ const tableValue = channel.controlTable[i + 6];
3553
+ if (baseline === tableValue)
3554
+ continue;
3555
+ handlers[i](channel, note, "controlTable", scheduleTime);
3440
3556
  }
3441
- else {
3442
- if (0 < table[1])
3443
- this.setFilterEnvelope(channel, note, scheduleTime);
3444
- if (0 < table[2])
3445
- this.setVolumeEnvelope(channel, note, scheduleTime);
3557
+ }
3558
+ setChannelPressureEffects(channel, note, scheduleTime) {
3559
+ this.setPressureEffects(channel, note, "channelPressureTable", scheduleTime);
3560
+ }
3561
+ setPolyphonicKeyPressureEffects(channel, note, scheduleTime) {
3562
+ this.setPressureEffects(channel, note, "polyphonicKeyPressureTable", scheduleTime);
3563
+ }
3564
+ setPressureEffects(channel, note, tableName, scheduleTime) {
3565
+ const handlers = this.effectHandlers;
3566
+ const table = channel[tableName];
3567
+ for (let i = 0; i < handlers.length; i++) {
3568
+ const baseline = pressureBaselines[i];
3569
+ const tableValue = table[i];
3570
+ if (baseline === tableValue)
3571
+ continue;
3572
+ handlers[i](channel, note, tableName, scheduleTime);
3446
3573
  }
3447
- if (0 < table[3])
3448
- this.setModLfoToPitch(channel, note, scheduleTime);
3449
- if (0 < table[4])
3450
- this.setModLfoToFilterFc(channel, note, scheduleTime);
3451
- if (0 < table[5])
3452
- this.setModLfoToVolume(channel, note, scheduleTime);
3574
+ }
3575
+ handleChannelPressureSysEx(data, scheduleTime) {
3576
+ this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
3577
+ }
3578
+ handlePolyphonicKeyPressureSysEx(data, scheduleTime) {
3579
+ this.handlePressureSysEx(data, "polyphonicKeyPressureTable", scheduleTime);
3453
3580
  }
3454
3581
  handlePressureSysEx(data, tableName, scheduleTime) {
3455
3582
  const channelNumber = data[4];
@@ -3461,39 +3588,41 @@ class Midy extends EventTarget {
3461
3588
  const pp = data[i];
3462
3589
  const rr = data[i + 1];
3463
3590
  table[pp] = rr;
3591
+ const handler = this.effectHandlers[pp];
3592
+ this.processActiveNotes(channel, scheduleTime, (note) => {
3593
+ if (handler)
3594
+ handler(channel, note, tableName, scheduleTime);
3595
+ });
3464
3596
  }
3465
- this.processActiveNotes(channel, scheduleTime, (note) => {
3466
- this.setEffects(channel, note, table, scheduleTime);
3467
- });
3468
- }
3469
- initControlTable() {
3470
- const ccCount = 128;
3471
- const slotSize = 6;
3472
- return new Int8Array(ccCount * slotSize).fill(-1);
3473
- }
3474
- setControlChangeEffects(channel, controllerType, scheduleTime) {
3475
- const slotSize = 6;
3476
- const offset = controllerType * slotSize;
3477
- const table = channel.controlTable.subarray(offset, offset + slotSize);
3478
- this.processScheduledNotes(channel, (note) => {
3479
- this.setEffects(channel, note, table, scheduleTime);
3480
- });
3481
3597
  }
3482
3598
  handleControlChangeSysEx(data, scheduleTime) {
3483
3599
  const channelNumber = data[4];
3484
3600
  const channel = this.channels[channelNumber];
3485
3601
  if (channel.isDrum)
3486
3602
  return;
3487
- const slotSize = 6;
3488
- const controllerType = data[5];
3489
- const offset = controllerType * slotSize;
3490
3603
  const table = channel.controlTable;
3604
+ table.set(defaultControlValues);
3605
+ const controllerType = data[5];
3491
3606
  for (let i = 6; i < data.length; i += 2) {
3492
3607
  const pp = data[i];
3493
3608
  const rr = data[i + 1];
3494
- table[offset + pp] = rr;
3609
+ table[pp] = controllerType;
3610
+ table[pp + 6] = rr;
3611
+ const handler = this.effectHandlers[pp];
3612
+ this.processActiveNotes(channel, scheduleTime, (note) => {
3613
+ if (handler)
3614
+ handler(channel, note, "controlTable", scheduleTime);
3615
+ });
3495
3616
  }
3496
- this.setControlChangeEffects(channel, controllerType, scheduleTime);
3617
+ }
3618
+ getRelativeKeyBasedValue(channel, keyNumber, controllerType) {
3619
+ const ccState = channel.state.array[128 + controllerType];
3620
+ if (!channel.isDrum)
3621
+ return ccState;
3622
+ const keyBasedValue = this.getKeyBasedValue(channel, keyNumber, controllerType);
3623
+ if (keyBasedValue < 0)
3624
+ return ccState;
3625
+ return ccState * keyBasedValue / 64;
3497
3626
  }
3498
3627
  getKeyBasedValue(channel, keyNumber, controllerType) {
3499
3628
  const index = keyNumber * 128 + controllerType;
@@ -3506,9 +3635,9 @@ class Midy extends EventTarget {
3506
3635
  handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3507
3636
  handlers[71] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3508
3637
  if (note.noteNumber === keyNumber) {
3509
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3638
+ const filterResonance = this.getRelativeKeyBasedValue(channel, keyNumber, 71);
3510
3639
  const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3511
- note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3640
+ note.filterEnvelopeNode.Q.setValueAtTime(Q, scheduleTime);
3512
3641
  }
3513
3642
  });
3514
3643
  handlers[73] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {