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