@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-GM2.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,49 @@ 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, "modLfo", {
54
64
  enumerable: true,
55
65
  configurable: true,
56
66
  writable: true,
57
67
  value: void 0
58
- });
59
- Object.defineProperty(this, "volumeDepth", {
68
+ }); // CC#1 modulation LFO
69
+ Object.defineProperty(this, "modLfoToPitch", {
60
70
  enumerable: true,
61
71
  configurable: true,
62
72
  writable: true,
63
73
  value: void 0
64
74
  });
65
- Object.defineProperty(this, "modulationLFO", {
75
+ Object.defineProperty(this, "modLfoToFilterFc", {
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, "modLfoToVolume", {
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, "vibLfo", {
78
88
  enumerable: true,
79
89
  configurable: true,
80
90
  writable: true,
81
91
  value: void 0
82
- });
83
- Object.defineProperty(this, "vibratoDepth", {
92
+ }); // vibrato LFO
93
+ Object.defineProperty(this, "vibLfoToPitch", {
84
94
  enumerable: true,
85
95
  configurable: true,
86
96
  writable: true,
@@ -231,7 +241,20 @@ const pitchEnvelopeKeys = [
231
241
  "playbackRate",
232
242
  ];
233
243
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
244
+ const effectParameters = [
245
+ 2400 / 64, // cent
246
+ 9600 / 64, // cent
247
+ 1 / 64,
248
+ 600 / 127, // cent
249
+ 2400 / 127, // cent
250
+ 1 / 127,
251
+ ];
252
+ const pressureBaselines = new Int8Array([64, 64, 0, 0, 0, 0]);
234
253
  const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
254
+ const defaultControlValues = new Int8Array([
255
+ ...[-1, -1, -1, -1, -1, -1],
256
+ ...defaultPressureValues,
257
+ ]);
235
258
  function cbToRatio(cb) {
236
259
  return Math.pow(10, cb / 200);
237
260
  }
@@ -380,6 +403,12 @@ export class MidyGM2 extends EventTarget {
380
403
  writable: true,
381
404
  value: new Map()
382
405
  });
406
+ Object.defineProperty(this, "decodeMethod", {
407
+ enumerable: true,
408
+ configurable: true,
409
+ writable: true,
410
+ value: "wasm-audio-decoders"
411
+ });
383
412
  Object.defineProperty(this, "isPlaying", {
384
413
  enumerable: true,
385
414
  configurable: true,
@@ -477,6 +506,7 @@ export class MidyGM2 extends EventTarget {
477
506
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
478
507
  this.controlChangeHandlers = this.createControlChangeHandlers();
479
508
  this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
509
+ this.effectHandlers = this.createEffectHandlers();
480
510
  this.channels = this.createChannels(audioContext);
481
511
  this.reverbEffect = this.createReverbEffect(audioContext);
482
512
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -604,7 +634,7 @@ export class MidyGM2 extends EventTarget {
604
634
  };
605
635
  }
606
636
  resetChannelTable(channel) {
607
- channel.controlTable.fill(-1);
637
+ channel.controlTable.set(defaultControlValues);
608
638
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
609
639
  channel.channelPressureTable.set(defaultPressureValues);
610
640
  channel.keyBasedTable.fill(-1);
@@ -620,7 +650,7 @@ export class MidyGM2 extends EventTarget {
620
650
  scheduledNotes: [],
621
651
  sustainNotes: [],
622
652
  sostenutoNotes: [],
623
- controlTable: this.initControlTable(),
653
+ controlTable: new Int8Array(defaultControlValues),
624
654
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
625
655
  channelPressureTable: new Int8Array(defaultPressureValues),
626
656
  keyBasedTable: new Int8Array(128 * 128).fill(-1),
@@ -630,11 +660,57 @@ export class MidyGM2 extends EventTarget {
630
660
  });
631
661
  return channels;
632
662
  }
663
+ decodeOggVorbis(sample) {
664
+ const task = decoderQueue.then(async () => {
665
+ const decoder = await initDecoder();
666
+ const slice = sample.data.slice();
667
+ const { channelData, sampleRate, errors } = await decoder.decodeFile(slice);
668
+ if (0 < errors.length) {
669
+ throw new Error(errors.join(", "));
670
+ }
671
+ const audioBuffer = new AudioBuffer({
672
+ numberOfChannels: channelData.length,
673
+ length: channelData[0].length,
674
+ sampleRate,
675
+ });
676
+ for (let ch = 0; ch < channelData.length; ch++) {
677
+ audioBuffer.getChannelData(ch).set(channelData[ch]);
678
+ }
679
+ return audioBuffer;
680
+ });
681
+ decoderQueue = task.catch(() => { });
682
+ return task;
683
+ }
633
684
  async createAudioBuffer(voiceParams) {
634
- const { sample, start, end } = voiceParams;
635
- const sampleEnd = sample.data.length + end;
636
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
637
- return audioBuffer;
685
+ const sample = voiceParams.sample;
686
+ if (sample.type === "compressed") {
687
+ switch (this.decodeMethod) {
688
+ case "decodeAudioData": {
689
+ // https://jakearchibald.com/2016/sounds-fun/
690
+ // https://github.com/WebAudio/web-audio-api/issues/1091
691
+ // decodeAudioData() has priming issues on Safari
692
+ const arrayBuffer = sample.data.slice().buffer;
693
+ return await this.audioContext.decodeAudioData(arrayBuffer);
694
+ }
695
+ case "wasm-audio-decoders":
696
+ return await this.decodeOggVorbis(sample);
697
+ default:
698
+ throw new Error(`Unknown decodeMethod: ${this.decodeMethod}`);
699
+ }
700
+ }
701
+ else {
702
+ const data = sample.data;
703
+ const end = data.length + voiceParams.end;
704
+ const subarray = data.subarray(voiceParams.start, end);
705
+ const pcm = sample.decodePCM(subarray);
706
+ const audioBuffer = new AudioBuffer({
707
+ numberOfChannels: 1,
708
+ length: pcm.length,
709
+ sampleRate: sample.sampleHeader.sampleRate,
710
+ });
711
+ audioBuffer.getChannelData(0).set(pcm);
712
+ return audioBuffer;
713
+ }
638
714
  }
639
715
  isLoopDrum(channel, noteNumber) {
640
716
  const programNumber = channel.programNumber;
@@ -708,7 +784,7 @@ export class MidyGM2 extends EventTarget {
708
784
  this.voiceCache.clear();
709
785
  this.realtimeVoiceCache.clear();
710
786
  const channels = this.channels;
711
- for (let ch = 0; i < channels.length; ch++) {
787
+ for (let ch = 0; ch < channels.length; ch++) {
712
788
  channels[ch].scheduledNotes = [];
713
789
  this.resetChannelStates(ch);
714
790
  }
@@ -1198,16 +1274,8 @@ export class MidyGM2 extends EventTarget {
1198
1274
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1199
1275
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1200
1276
  const pitch = pitchWheel * pitchWheelSensitivity;
1201
- const channelPressureRaw = channel.channelPressureTable[0];
1202
- if (0 <= channelPressureRaw) {
1203
- const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1204
- const channelPressure = channelPressureDepth *
1205
- channel.state.channelPressure;
1206
- return tuning + pitch + channelPressure;
1207
- }
1208
- else {
1209
- return tuning + pitch;
1210
- }
1277
+ const effect = this.getChannelPitchControl(channel);
1278
+ return tuning + pitch + effect;
1211
1279
  }
1212
1280
  updateChannelDetune(channel, scheduleTime) {
1213
1281
  this.processScheduledNotes(channel, (note) => {
@@ -1225,8 +1293,7 @@ export class MidyGM2 extends EventTarget {
1225
1293
  calcNoteDetune(channel, note) {
1226
1294
  const noteDetune = note.voiceParams.detune +
1227
1295
  this.calcScaleOctaveTuning(channel, note);
1228
- const pitchControl = this.getPitchControl(channel, note);
1229
- return channel.detune + noteDetune + pitchControl;
1296
+ return channel.detune + noteDetune;
1230
1297
  }
1231
1298
  getPortamentoTime(channel, note) {
1232
1299
  const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
@@ -1383,7 +1450,7 @@ export class MidyGM2 extends EventTarget {
1383
1450
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1384
1451
  const modDelay = startTime + voiceParams.modDelay;
1385
1452
  note.adjustedBaseFreq = adjustedSustainFreq;
1386
- note.filterNode.frequency
1453
+ note.filterEnvelopeNode.frequency
1387
1454
  .cancelScheduledValues(scheduleTime)
1388
1455
  .setValueAtTime(adjustedBaseFreq, startTime)
1389
1456
  .setValueAtTime(adjustedBaseFreq, modDelay)
@@ -1409,7 +1476,7 @@ export class MidyGM2 extends EventTarget {
1409
1476
  const modHold = modAttack + voiceParams.modHold;
1410
1477
  const decayDuration = voiceParams.modDecay;
1411
1478
  note.adjustedBaseFreq = adjustedBaseFreq;
1412
- note.filterNode.frequency
1479
+ note.filterEnvelopeNode.frequency
1413
1480
  .cancelScheduledValues(scheduleTime)
1414
1481
  .setValueAtTime(adjustedBaseFreq, startTime)
1415
1482
  .setValueAtTime(adjustedBaseFreq, modDelay)
@@ -1420,37 +1487,37 @@ export class MidyGM2 extends EventTarget {
1420
1487
  startModulation(channel, note, scheduleTime) {
1421
1488
  const audioContext = this.audioContext;
1422
1489
  const { voiceParams } = note;
1423
- note.modulationLFO = new OscillatorNode(audioContext, {
1490
+ note.modLfo = new OscillatorNode(audioContext, {
1424
1491
  frequency: this.centToHz(voiceParams.freqModLFO),
1425
1492
  });
1426
- note.filterDepth = new GainNode(audioContext, {
1493
+ note.modLfoToFilterFc = new GainNode(audioContext, {
1427
1494
  gain: voiceParams.modLfoToFilterFc,
1428
1495
  });
1429
- note.modulationDepth = new GainNode(audioContext);
1496
+ note.modLfoToPitch = new GainNode(audioContext);
1430
1497
  this.setModLfoToPitch(channel, note, scheduleTime);
1431
- note.volumeDepth = new GainNode(audioContext);
1498
+ note.modLfoToVolume = new GainNode(audioContext);
1432
1499
  this.setModLfoToVolume(note, scheduleTime);
1433
- note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1434
- note.modulationLFO.connect(note.filterDepth);
1435
- note.filterDepth.connect(note.filterNode.frequency);
1436
- note.modulationLFO.connect(note.modulationDepth);
1437
- note.modulationDepth.connect(note.bufferSource.detune);
1438
- note.modulationLFO.connect(note.volumeDepth);
1439
- note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1500
+ note.modLfo.start(note.startTime + voiceParams.delayModLFO);
1501
+ note.modLfo.connect(note.modLfoToFilterFc);
1502
+ note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
1503
+ note.modLfo.connect(note.modLfoToPitch);
1504
+ note.modLfoToPitch.connect(note.bufferSource.detune);
1505
+ note.modLfo.connect(note.modLfoToVolume);
1506
+ note.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
1440
1507
  }
1441
1508
  startVibrato(channel, note, scheduleTime) {
1442
1509
  const { voiceParams } = note;
1443
1510
  const state = channel.state;
1444
1511
  const vibratoRate = state.vibratoRate * 2;
1445
1512
  const vibratoDelay = state.vibratoDelay * 2;
1446
- note.vibratoLFO = new OscillatorNode(this.audioContext, {
1513
+ note.vibLfo = new OscillatorNode(this.audioContext, {
1447
1514
  frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
1448
1515
  });
1449
- note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1450
- note.vibratoDepth = new GainNode(this.audioContext);
1516
+ note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1517
+ note.vibLfoToPitch = new GainNode(this.audioContext);
1451
1518
  this.setVibLfoToPitch(channel, note, scheduleTime);
1452
- note.vibratoLFO.connect(note.vibratoDepth);
1453
- note.vibratoDepth.connect(note.bufferSource.detune);
1519
+ note.vibLfo.connect(note.vibLfoToPitch);
1520
+ note.vibLfoToPitch.connect(note.bufferSource.detune);
1454
1521
  }
1455
1522
  async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1456
1523
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
@@ -1491,7 +1558,7 @@ export class MidyGM2 extends EventTarget {
1491
1558
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1492
1559
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1493
1560
  note.volumeEnvelopeNode = new GainNode(audioContext);
1494
- note.filterNode = new BiquadFilterNode(audioContext, {
1561
+ note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
1495
1562
  type: "lowpass",
1496
1563
  Q: voiceParams.initialFilterQ / 10, // dB
1497
1564
  });
@@ -1521,8 +1588,8 @@ export class MidyGM2 extends EventTarget {
1521
1588
  channel.currentBufferSource.stop(startTime);
1522
1589
  channel.currentBufferSource = note.bufferSource;
1523
1590
  }
1524
- note.bufferSource.connect(note.filterNode);
1525
- note.filterNode.connect(note.volumeEnvelopeNode);
1591
+ note.bufferSource.connect(note.filterEnvelopeNode);
1592
+ note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
1526
1593
  this.setChorusSend(channel, note, now);
1527
1594
  this.setReverbSend(channel, note, now);
1528
1595
  if (voiceParams.sample.type === "compressed") {
@@ -1603,8 +1670,6 @@ export class MidyGM2 extends EventTarget {
1603
1670
  scheduledNotes.push(note);
1604
1671
  const programNumber = channel.programNumber;
1605
1672
  const bankTable = this.soundFontTable[programNumber];
1606
- if (!bankTable)
1607
- return;
1608
1673
  let bank = channel.isDrum ? 128 : channel.bankLSB;
1609
1674
  if (bankTable[bank] === undefined) {
1610
1675
  if (channel.isDrum)
@@ -1624,16 +1689,16 @@ export class MidyGM2 extends EventTarget {
1624
1689
  }
1625
1690
  disconnectNote(note) {
1626
1691
  note.bufferSource.disconnect();
1627
- note.filterNode.disconnect();
1692
+ note.filterEnvelopeNode.disconnect();
1628
1693
  note.volumeEnvelopeNode.disconnect();
1629
- if (note.modulationDepth) {
1630
- note.volumeDepth.disconnect();
1631
- note.modulationDepth.disconnect();
1632
- note.modulationLFO.stop();
1694
+ if (note.modLfoToPitch) {
1695
+ note.modLfoToVolume.disconnect();
1696
+ note.modLfoToPitch.disconnect();
1697
+ note.modLfo.stop();
1633
1698
  }
1634
- if (note.vibratoDepth) {
1635
- note.vibratoDepth.disconnect();
1636
- note.vibratoLFO.stop();
1699
+ if (note.vibLfoToPitch) {
1700
+ note.vibLfoToPitch.disconnect();
1701
+ note.vibLfo.stop();
1637
1702
  }
1638
1703
  if (note.reverbSend) {
1639
1704
  note.reverbSend.disconnect();
@@ -1646,7 +1711,7 @@ export class MidyGM2 extends EventTarget {
1646
1711
  endTime ??= this.audioContext.currentTime;
1647
1712
  const volDuration = note.voiceParams.volRelease;
1648
1713
  const volRelease = endTime + volDuration;
1649
- note.filterNode.frequency
1714
+ note.filterEnvelopeNode.frequency
1650
1715
  .cancelScheduledValues(endTime)
1651
1716
  .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
1652
1717
  note.volumeEnvelopeNode.gain
@@ -1787,17 +1852,12 @@ export class MidyGM2 extends EventTarget {
1787
1852
  const channel = this.channels[channelNumber];
1788
1853
  if (channel.isDrum)
1789
1854
  return;
1790
- const prev = channel.state.channelPressure;
1791
- const next = value / 127;
1792
- channel.state.channelPressure = next;
1793
- const channelPressureRaw = channel.channelPressureTable[0];
1794
- if (0 <= channelPressureRaw) {
1795
- const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1796
- channel.detune += channelPressureDepth * (next - prev);
1797
- }
1798
- const table = channel.channelPressureTable;
1855
+ const prev = this.calcChannelPressureEffectValue(channel, 0);
1856
+ channel.state.channelPressure = value / 127;
1857
+ const next = this.calcChannelPressureEffectValue(channel, 0);
1858
+ channel.detune += next - prev;
1799
1859
  this.processActiveNotes(channel, scheduleTime, (note) => {
1800
- this.setEffects(channel, note, table, scheduleTime);
1860
+ this.setChannelPressureEffects(channel, note, scheduleTime);
1801
1861
  });
1802
1862
  this.applyVoiceParams(channel, 13, scheduleTime);
1803
1863
  }
@@ -1820,13 +1880,13 @@ export class MidyGM2 extends EventTarget {
1820
1880
  this.applyVoiceParams(channel, 14, scheduleTime);
1821
1881
  }
1822
1882
  setModLfoToPitch(channel, note, scheduleTime) {
1823
- if (note.modulationDepth) {
1883
+ if (note.modLfoToPitch) {
1824
1884
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1825
1885
  this.getLFOPitchDepth(channel, note);
1826
1886
  const baseDepth = Math.abs(modLfoToPitch) +
1827
1887
  channel.state.modulationDepthMSB;
1828
1888
  const depth = baseDepth * Math.sign(modLfoToPitch);
1829
- note.modulationDepth.gain
1889
+ note.modLfoToPitch.gain
1830
1890
  .cancelScheduledValues(scheduleTime)
1831
1891
  .setValueAtTime(depth, scheduleTime);
1832
1892
  }
@@ -1835,12 +1895,12 @@ export class MidyGM2 extends EventTarget {
1835
1895
  }
1836
1896
  }
1837
1897
  setVibLfoToPitch(channel, note, scheduleTime) {
1838
- if (note.vibratoDepth) {
1898
+ if (note.vibLfoToPitch) {
1839
1899
  const vibratoDepth = channel.state.vibratoDepth * 2;
1840
1900
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1841
1901
  const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
1842
1902
  const depth = baseDepth * Math.sign(vibLfoToPitch);
1843
- note.vibratoDepth.gain
1903
+ note.vibLfoToPitch.gain
1844
1904
  .cancelScheduledValues(scheduleTime)
1845
1905
  .setValueAtTime(depth, scheduleTime);
1846
1906
  }
@@ -1851,18 +1911,18 @@ export class MidyGM2 extends EventTarget {
1851
1911
  setModLfoToFilterFc(channel, note, scheduleTime) {
1852
1912
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1853
1913
  this.getLFOFilterDepth(channel);
1854
- note.filterDepth.gain
1914
+ note.modLfoToFilterFc.gain
1855
1915
  .cancelScheduledValues(scheduleTime)
1856
1916
  .setValueAtTime(modLfoToFilterFc, scheduleTime);
1857
1917
  }
1858
1918
  setModLfoToVolume(channel, note, scheduleTime) {
1859
1919
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1860
1920
  const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
1861
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1921
+ const depth = baseDepth * Math.sign(modLfoToVolume) *
1862
1922
  (1 + this.getLFOAmplitudeDepth(channel));
1863
- note.volumeDepth.gain
1923
+ note.modLfoToVolume.gain
1864
1924
  .cancelScheduledValues(scheduleTime)
1865
- .setValueAtTime(volumeDepth, scheduleTime);
1925
+ .setValueAtTime(depth, scheduleTime);
1866
1926
  }
1867
1927
  setReverbSend(channel, note, scheduleTime) {
1868
1928
  let value = note.voiceParams.reverbEffectsSend *
@@ -1927,13 +1987,13 @@ export class MidyGM2 extends EventTarget {
1927
1987
  setDelayModLFO(note) {
1928
1988
  const startTime = note.startTime + note.voiceParams.delayModLFO;
1929
1989
  try {
1930
- note.modulationLFO.start(startTime);
1990
+ note.modLfo.start(startTime);
1931
1991
  }
1932
1992
  catch { /* empty */ }
1933
1993
  }
1934
1994
  setFreqModLFO(note, scheduleTime) {
1935
1995
  const freqModLFO = note.voiceParams.freqModLFO;
1936
- note.modulationLFO.frequency
1996
+ note.modLfo.frequency
1937
1997
  .cancelScheduledValues(scheduleTime)
1938
1998
  .setValueAtTime(freqModLFO, scheduleTime);
1939
1999
  }
@@ -1942,14 +2002,14 @@ export class MidyGM2 extends EventTarget {
1942
2002
  const value = note.voiceParams.delayVibLFO;
1943
2003
  const startTime = note.startTime + value * vibratoDelay;
1944
2004
  try {
1945
- note.vibratoLFO.start(startTime);
2005
+ note.vibLfo.start(startTime);
1946
2006
  }
1947
2007
  catch { /* empty */ }
1948
2008
  }
1949
2009
  setFreqVibLFO(channel, note, scheduleTime) {
1950
2010
  const vibratoRate = channel.state.vibratoRate * 2;
1951
2011
  const freqVibLFO = note.voiceParams.freqVibLFO;
1952
- note.vibratoLFO.frequency
2012
+ note.vibLfo.frequency
1953
2013
  .cancelScheduledValues(scheduleTime)
1954
2014
  .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
1955
2015
  }
@@ -1993,7 +2053,7 @@ export class MidyGM2 extends EventTarget {
1993
2053
  },
1994
2054
  delayVibLFO: (channel, note, _scheduleTime) => {
1995
2055
  if (0 < channel.state.vibratoDepth) {
1996
- setDelayVibLFO(channel, note);
2056
+ this.setDelayVibLFO(channel, note);
1997
2057
  }
1998
2058
  },
1999
2059
  freqVibLFO: (channel, note, scheduleTime) => {
@@ -2082,12 +2142,16 @@ export class MidyGM2 extends EventTarget {
2082
2142
  return handlers;
2083
2143
  }
2084
2144
  setControlChange(channelNumber, controllerType, value, scheduleTime) {
2145
+ if (!(0 <= scheduleTime))
2146
+ scheduleTime = this.audioContext.currentTime;
2085
2147
  const handler = this.controlChangeHandlers[controllerType];
2086
2148
  if (handler) {
2087
2149
  handler.call(this, channelNumber, value, scheduleTime);
2088
2150
  const channel = this.channels[channelNumber];
2089
2151
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
2090
- this.setControlChangeEffects(channel, controllerType, scheduleTime);
2152
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2153
+ this.setControlChangeEffects(channel, note, scheduleTime);
2154
+ });
2091
2155
  }
2092
2156
  else {
2093
2157
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -2100,8 +2164,8 @@ export class MidyGM2 extends EventTarget {
2100
2164
  const depth = channel.state.modulationDepthMSB *
2101
2165
  channel.modulationDepthRange;
2102
2166
  this.processScheduledNotes(channel, (note) => {
2103
- if (note.modulationDepth) {
2104
- note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
2167
+ if (note.modLfoToPitch) {
2168
+ note.modLfoToPitch.gain.setValueAtTime(depth, scheduleTime);
2105
2169
  }
2106
2170
  else {
2107
2171
  this.startModulation(channel, note, scheduleTime);
@@ -2149,6 +2213,9 @@ export class MidyGM2 extends EventTarget {
2149
2213
  scheduleTime = this.audioContext.currentTime;
2150
2214
  const channel = this.channels[channelNumber];
2151
2215
  channel.state.volumeMSB = value / 127;
2216
+ this.applyVolume(channel, scheduleTime);
2217
+ }
2218
+ applyVolume(channel, scheduleTime) {
2152
2219
  if (channel.isDrum) {
2153
2220
  for (let i = 0; i < 128; i++) {
2154
2221
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2195,7 +2262,8 @@ export class MidyGM2 extends EventTarget {
2195
2262
  }
2196
2263
  updateChannelVolume(channel, scheduleTime) {
2197
2264
  const state = channel.state;
2198
- const gain = state.volumeMSB * state.expressionMSB;
2265
+ const effect = this.getChannelAmplitudeControl(channel);
2266
+ const gain = state.volumeMSB * state.expressionMSB * (1 + effect);
2199
2267
  const { gainLeft, gainRight } = this.panToGain(state.panMSB);
2200
2268
  channel.gainL.gain
2201
2269
  .cancelScheduledValues(scheduleTime)
@@ -2603,7 +2671,7 @@ export class MidyGM2 extends EventTarget {
2603
2671
  case 9:
2604
2672
  switch (data[3]) {
2605
2673
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2606
- return this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
2674
+ return this.handleChannelPressureSysEx(data, scheduelTime);
2607
2675
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2608
2676
  return this.handleControlChangeSysEx(data, scheduleTime);
2609
2677
  default:
@@ -2644,8 +2712,8 @@ export class MidyGM2 extends EventTarget {
2644
2712
  this.masterFineTuning = next;
2645
2713
  const detuneChange = next - prev;
2646
2714
  const channels = this.channels;
2647
- for (let ch = 0; i < channels.length; ch++) {
2648
- const channel = this.channels[ch];
2715
+ for (let ch = 0; ch < channels.length; ch++) {
2716
+ const channel = channels[ch];
2649
2717
  if (channel.isDrum)
2650
2718
  continue;
2651
2719
  channel.detune += detuneChange;
@@ -2663,7 +2731,7 @@ export class MidyGM2 extends EventTarget {
2663
2731
  const detuneChange = next - prev;
2664
2732
  const channels = this.channels;
2665
2733
  for (let ch = 0; ch < channels.length; ch++) {
2666
- const channel = this.channels[ch];
2734
+ const channel = channels[ch];
2667
2735
  if (channel.isDrum)
2668
2736
  continue;
2669
2737
  channel.detune += detuneChange;
@@ -2888,70 +2956,105 @@ export class MidyGM2 extends EventTarget {
2888
2956
  this.updateChannelDetune(channel, scheduleTime);
2889
2957
  }
2890
2958
  }
2959
+ calcEffectValue(channel, destination) {
2960
+ return this.calcChannelEffectValue(channel, destination);
2961
+ }
2962
+ calcChannelEffectValue(channel, destination) {
2963
+ return this.calcControlChangeEffectValue(channel, destination) +
2964
+ this.calcChannelPressureEffectValue(channel, destination);
2965
+ }
2966
+ calcControlChangeEffectValue(channel, destination) {
2967
+ const controlType = channel.controlTable[destination];
2968
+ if (controlType < 0)
2969
+ return 0;
2970
+ const pressure = channel.state.array[controlType];
2971
+ if (pressure <= 0)
2972
+ return 0;
2973
+ const baseline = pressureBaselines[destination];
2974
+ const tableValue = channel.controlTable[destination + 6];
2975
+ const value = (tableValue - baseline) * pressure;
2976
+ return value * effectParameters[destination];
2977
+ }
2978
+ calcChannelPressureEffectValue(channel, destination) {
2979
+ const pressure = channel.state.channelPressure;
2980
+ if (pressure <= 0)
2981
+ return 0;
2982
+ const baseline = pressureBaselines[destination];
2983
+ const tableValue = channel.channelPressureTable[destination];
2984
+ const value = (tableValue - baseline) * pressure;
2985
+ return value * effectParameters[destination];
2986
+ }
2987
+ getChannelPitchControl(channel) {
2988
+ return this.calcChannelEffectValue(channel, 0);
2989
+ }
2990
+ getPitchControl(channel, note) {
2991
+ return this.calcEffectValue(channel, note, 0);
2992
+ }
2891
2993
  getFilterCutoffControl(channel) {
2892
- const channelPressureRaw = channel.channelPressureTable[1];
2893
- const channelPressure = (0 <= channelPressureRaw)
2894
- ? (channelPressureRaw - 64) * channel.state.channelPressure
2895
- : 0;
2896
- return channelPressure * 15;
2897
- }
2898
- getAmplitudeControl(channel) {
2899
- const channelPressureRaw = channel.channelPressureTable[2];
2900
- const channelPressure = (0 <= channelPressureRaw)
2901
- ? channel.state.channelPressure * 127 / channelPressureRaw
2902
- : 0;
2903
- return channelPressure;
2994
+ return this.calcEffectValue(channel, 1);
2995
+ }
2996
+ getChannelAmplitudeControl(channel) {
2997
+ return this.calcChannelEffectValue(channel, 2);
2904
2998
  }
2905
2999
  getLFOPitchDepth(channel) {
2906
- const channelPressureRaw = channel.channelPressureTable[3];
2907
- const channelPressure = (0 <= channelPressureRaw)
2908
- ? channelPressureRaw * channel.state.channelPressure
2909
- : 0;
2910
- return channelPressure / 127 * 600;
3000
+ return this.calcEffectValue(channel, 3);
2911
3001
  }
2912
3002
  getLFOFilterDepth(channel) {
2913
- const channelPressureRaw = channel.channelPressureTable[4];
2914
- const channelPressure = (0 <= channelPressureRaw)
2915
- ? channelPressureRaw * channel.state.channelPressure
2916
- : 0;
2917
- return channelPressure / 127 * 2400;
3003
+ return this.calcEffectValue(channel, 4);
2918
3004
  }
2919
3005
  getLFOAmplitudeDepth(channel) {
2920
- const channelPressureRaw = channel.channelPressureTable[5];
2921
- const channelPressure = (0 <= channelPressureRaw)
2922
- ? channelPressureRaw * channel.state.channelPressure
2923
- : 0;
2924
- return channelPressure / 127;
2925
- }
2926
- setEffects(channel, note, table, scheduleTime) {
2927
- if (0 < table[0]) {
3006
+ return this.calcEffectValue(channel, 5);
3007
+ }
3008
+ createEffectHandlers() {
3009
+ const handlers = new Array(6);
3010
+ handlers[0] = (channel, note, scheduleTime) => {
2928
3011
  if (this.isPortamento(channel, note)) {
2929
3012
  this.setPortamentoDetune(channel, note, scheduleTime);
2930
3013
  }
2931
3014
  else {
2932
3015
  this.setDetune(channel, note, scheduleTime);
2933
3016
  }
2934
- }
2935
- if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2936
- if (0 < table[1]) {
3017
+ };
3018
+ handlers[1] = (channel, note, scheduleTime) => {
3019
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
2937
3020
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2938
3021
  }
2939
- if (0 < table[2]) {
2940
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
3022
+ else {
3023
+ this.setFilterEnvelope(channel, note, scheduleTime);
2941
3024
  }
3025
+ };
3026
+ handlers[2] = (channel, note, scheduleTime) => this.applyVolume(channel, note, scheduleTime);
3027
+ handlers[3] = (channel, note, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
3028
+ handlers[4] = (channel, note, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
3029
+ handlers[5] = (channel, note, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
3030
+ return handlers;
3031
+ }
3032
+ setControlChangeEffects(channel, note, scheduleTime) {
3033
+ const handlers = this.effectHandlers;
3034
+ for (let i = 0; i < handlers.length; i++) {
3035
+ const baseline = pressureBaselines[i];
3036
+ const tableValue = channel.controlTable[i + 6];
3037
+ if (baseline === tableValue)
3038
+ continue;
3039
+ handlers[i](channel, note, scheduleTime);
2942
3040
  }
2943
- else {
2944
- if (0 < table[1])
2945
- this.setFilterEnvelope(channel, note, scheduleTime);
2946
- if (0 < table[2])
2947
- this.setVolumeEnvelope(channel, note, scheduleTime);
3041
+ }
3042
+ setChannelPressureEffects(channel, note, scheduleTime) {
3043
+ this.setPressureEffects(channel, note, "channelPressureTable", scheduleTime);
3044
+ }
3045
+ setPressureEffects(channel, note, tableName, scheduleTime) {
3046
+ const handlers = this.effectHandlers;
3047
+ const table = channel[tableName];
3048
+ for (let i = 0; i < handlers.length; i++) {
3049
+ const baseline = pressureBaselines[i];
3050
+ const tableValue = table[i];
3051
+ if (baseline === tableValue)
3052
+ continue;
3053
+ handlers[i](channel, note, scheduleTime);
2948
3054
  }
2949
- if (0 < table[3])
2950
- this.setModLfoToPitch(channel, note, scheduleTime);
2951
- if (0 < table[4])
2952
- this.setModLfoToFilterFc(channel, note, scheduleTime);
2953
- if (0 < table[5])
2954
- this.setModLfoToVolume(channel, note, scheduleTime);
3055
+ }
3056
+ handleChannelPressureSysEx(data, scheduleTime) {
3057
+ this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
2955
3058
  }
2956
3059
  handlePressureSysEx(data, tableName, scheduleTime) {
2957
3060
  const channelNumber = data[4];
@@ -2963,39 +3066,32 @@ export class MidyGM2 extends EventTarget {
2963
3066
  const pp = data[i];
2964
3067
  const rr = data[i + 1];
2965
3068
  table[pp] = rr;
3069
+ const handler = this.effectHandlers[pp];
3070
+ this.processActiveNotes(channel, scheduleTime, (note) => {
3071
+ if (handler)
3072
+ handler(channel, note, scheduleTime);
3073
+ });
2966
3074
  }
2967
- this.processActiveNotes(channel, scheduleTime, (note) => {
2968
- this.setEffects(channel, note, table, scheduleTime);
2969
- });
2970
- }
2971
- initControlTable() {
2972
- const ccCount = 128;
2973
- const slotSize = 6;
2974
- return new Int8Array(ccCount * slotSize).fill(-1);
2975
- }
2976
- setControlChangeEffects(channel, controllerType, scheduleTime) {
2977
- const slotSize = 6;
2978
- const offset = controllerType * slotSize;
2979
- const table = channel.controlTable.subarray(offset, offset + slotSize);
2980
- this.processScheduledNotes(channel, (note) => {
2981
- this.setEffects(channel, note, table, scheduleTime);
2982
- });
2983
3075
  }
2984
3076
  handleControlChangeSysEx(data, scheduleTime) {
2985
3077
  const channelNumber = data[4];
2986
3078
  const channel = this.channels[channelNumber];
2987
3079
  if (channel.isDrum)
2988
3080
  return;
2989
- const slotSize = 6;
2990
- const controllerType = data[5];
2991
- const offset = controllerType * slotSize;
2992
3081
  const table = channel.controlTable;
3082
+ table.set(defaultControlValues);
3083
+ const controllerType = data[5];
2993
3084
  for (let i = 6; i < data.length; i += 2) {
2994
3085
  const pp = data[i];
2995
3086
  const rr = data[i + 1];
2996
- table[offset + pp] = rr;
3087
+ table[pp] = controllerType;
3088
+ table[pp + 6] = rr;
3089
+ const handler = this.effectHandlers[pp];
3090
+ this.processActiveNotes(channel, scheduleTime, (note) => {
3091
+ if (handler)
3092
+ handler(channel, note, scheduleTime);
3093
+ });
2997
3094
  }
2998
- this.setControlChangeEffects(channel, controllerType, scheduleTime);
2999
3095
  }
3000
3096
  getKeyBasedValue(channel, keyNumber, controllerType) {
3001
3097
  const index = keyNumber * 128 + controllerType;