@marmooo/midy 0.3.3 → 0.3.4

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
@@ -47,24 +47,6 @@ class Note {
47
47
  writable: true,
48
48
  value: void 0
49
49
  });
50
- Object.defineProperty(this, "volumeNode", {
51
- enumerable: true,
52
- configurable: true,
53
- writable: true,
54
- value: void 0
55
- });
56
- Object.defineProperty(this, "gainL", {
57
- enumerable: true,
58
- configurable: true,
59
- writable: true,
60
- value: void 0
61
- });
62
- Object.defineProperty(this, "gainR", {
63
- enumerable: true,
64
- configurable: true,
65
- writable: true,
66
- value: void 0
67
- });
68
50
  Object.defineProperty(this, "modulationLFO", {
69
51
  enumerable: true,
70
52
  configurable: true,
@@ -217,6 +199,16 @@ class ControllerState {
217
199
  }
218
200
  }
219
201
  }
202
+ const volumeEnvelopeKeys = [
203
+ "volDelay",
204
+ "volAttack",
205
+ "volHold",
206
+ "volDecay",
207
+ "volSustain",
208
+ "volRelease",
209
+ "initialAttenuation",
210
+ ];
211
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
220
212
  const filterEnvelopeKeys = [
221
213
  "modEnvToPitch",
222
214
  "initialFilterFc",
@@ -226,22 +218,20 @@ const filterEnvelopeKeys = [
226
218
  "modHold",
227
219
  "modDecay",
228
220
  "modSustain",
229
- "modRelease",
230
- "playbackRate",
231
221
  ];
232
222
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
233
- const volumeEnvelopeKeys = [
234
- "volDelay",
235
- "volAttack",
236
- "volHold",
237
- "volDecay",
238
- "volSustain",
239
- "volRelease",
240
- "initialAttenuation",
223
+ const pitchEnvelopeKeys = [
224
+ "modEnvToPitch",
225
+ "modDelay",
226
+ "modAttack",
227
+ "modHold",
228
+ "modDecay",
229
+ "modSustain",
230
+ "playbackRate",
241
231
  ];
242
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
232
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
243
233
  class Midy {
244
- constructor(audioContext, options = this.defaultOptions) {
234
+ constructor(audioContext) {
245
235
  Object.defineProperty(this, "mode", {
246
236
  enumerable: true,
247
237
  configurable: true,
@@ -265,6 +255,7 @@ class Midy {
265
255
  configurable: true,
266
256
  writable: true,
267
257
  value: {
258
+ algorithm: "SchroederReverb",
268
259
  time: this.getReverbTime(64),
269
260
  feedback: 0.8,
270
261
  }
@@ -413,30 +404,7 @@ class Midy {
413
404
  writable: true,
414
405
  value: new Array(this.numChannels * drumExclusiveClassCount)
415
406
  });
416
- Object.defineProperty(this, "defaultOptions", {
417
- enumerable: true,
418
- configurable: true,
419
- writable: true,
420
- value: {
421
- reverbAlgorithm: (audioContext) => {
422
- const { time: rt60, feedback } = this.reverb;
423
- // const delay = this.calcDelay(rt60, feedback);
424
- // const impulse = this.createConvolutionReverbImpulse(
425
- // audioContext,
426
- // rt60,
427
- // delay,
428
- // );
429
- // return this.createConvolutionReverb(audioContext, impulse);
430
- const combFeedbacks = this.generateDistributedArray(feedback, 4);
431
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
432
- const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
433
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
434
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
435
- },
436
- }
437
- });
438
407
  this.audioContext = audioContext;
439
- this.options = { ...this.defaultOptions, ...options };
440
408
  this.masterVolume = new GainNode(audioContext);
441
409
  this.scheduler = new GainNode(audioContext, { gain: 0 });
442
410
  this.schedulerBuffer = new AudioBuffer({
@@ -446,7 +414,7 @@ class Midy {
446
414
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
447
415
  this.controlChangeHandlers = this.createControlChangeHandlers();
448
416
  this.channels = this.createChannels(audioContext);
449
- this.reverbEffect = this.options.reverbAlgorithm(audioContext);
417
+ this.reverbEffect = this.createReverbEffect(audioContext);
450
418
  this.chorusEffect = this.createChorusEffect(audioContext);
451
419
  this.chorusEffect.output.connect(this.masterVolume);
452
420
  this.reverbEffect.output.connect(this.masterVolume);
@@ -510,7 +478,7 @@ class Midy {
510
478
  this.timeline = midiData.timeline;
511
479
  this.totalTime = this.calcTotalTime();
512
480
  }
513
- setChannelAudioNodes(audioContext) {
481
+ createChannelAudioNodes(audioContext) {
514
482
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
515
483
  const gainL = new GainNode(audioContext, { gain: gainLeft });
516
484
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -525,10 +493,10 @@ class Midy {
525
493
  };
526
494
  }
527
495
  resetChannelTable(channel) {
528
- this.resetControlTable(channel.controlTable);
496
+ channel.controlTable.fill(-1);
529
497
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
530
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
531
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
498
+ channel.channelPressureTable.fill(-1);
499
+ channel.polyphonicKeyPressureTable.fill(-1);
532
500
  channel.keyBasedInstrumentControlTable.fill(-1);
533
501
  }
534
502
  createChannels(audioContext) {
@@ -538,15 +506,17 @@ class Midy {
538
506
  isDrum: false,
539
507
  state: new ControllerState(),
540
508
  ...this.constructor.channelSettings,
541
- ...this.setChannelAudioNodes(audioContext),
509
+ ...this.createChannelAudioNodes(audioContext),
542
510
  scheduledNotes: [],
543
511
  sustainNotes: [],
544
512
  sostenutoNotes: [],
545
513
  controlTable: this.initControlTable(),
546
514
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
547
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
548
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
515
+ channelPressureTable: new Int8Array(6).fill(-1),
516
+ polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
549
517
  keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
518
+ keyBasedGainLs: new Array(128),
519
+ keyBasedGainRs: new Array(128),
550
520
  };
551
521
  });
552
522
  return channels;
@@ -588,10 +558,9 @@ class Midy {
588
558
  createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
589
559
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
590
560
  bufferSource.buffer = audioBuffer;
591
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
592
- if (channel.isDrum) {
593
- bufferSource.loop = this.isLoopDrum(channel, noteNumber);
594
- }
561
+ bufferSource.loop = channel.isDrum
562
+ ? this.isLoopDrum(channel, noteNumber)
563
+ : (voiceParams.sampleModes % 2 !== 0);
595
564
  if (bufferSource.loop) {
596
565
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
597
566
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -835,7 +804,7 @@ class Midy {
835
804
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
836
805
  const channel = this.channels[channelNumber];
837
806
  const promises = [];
838
- this.processScheduledNotes(channel, (note) => {
807
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
839
808
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
840
809
  this.notePromises.push(promise);
841
810
  promises.push(promise);
@@ -894,7 +863,7 @@ class Midy {
894
863
  const now = this.audioContext.currentTime;
895
864
  return this.resumeTime + now - this.startTime - this.startDelay;
896
865
  }
897
- processScheduledNotes(channel, callback) {
866
+ processScheduledNotes(channel, scheduleTime, callback) {
898
867
  const scheduledNotes = channel.scheduledNotes;
899
868
  for (let i = 0; i < scheduledNotes.length; i++) {
900
869
  const note = scheduledNotes[i];
@@ -902,6 +871,8 @@ class Midy {
902
871
  continue;
903
872
  if (note.ending)
904
873
  continue;
874
+ if (note.startTime < scheduleTime)
875
+ continue;
905
876
  callback(note);
906
877
  }
907
878
  }
@@ -914,7 +885,7 @@ class Midy {
914
885
  if (note.ending)
915
886
  continue;
916
887
  if (scheduleTime < note.startTime)
917
- continue;
888
+ break;
918
889
  callback(note);
919
890
  }
920
891
  }
@@ -1003,6 +974,22 @@ class Midy {
1003
974
  const output = allpasses.at(-1);
1004
975
  return { input, output };
1005
976
  }
977
+ createReverbEffect(audioContext) {
978
+ const { algorithm, time: rt60, feedback } = this.reverb;
979
+ switch (algorithm) {
980
+ case "ConvolutionReverb": {
981
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
982
+ return this.createConvolutionReverb(audioContext, impulse);
983
+ }
984
+ case "SchroederReverb": {
985
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
986
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
987
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
988
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
989
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
990
+ }
991
+ }
992
+ }
1006
993
  createChorusEffect(audioContext) {
1007
994
  const input = new GainNode(audioContext);
1008
995
  const output = new GainNode(audioContext);
@@ -1067,15 +1054,22 @@ class Midy {
1067
1054
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1068
1055
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1069
1056
  const pitch = pitchWheel * pitchWheelSensitivity;
1070
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1071
- const pressure = pressureDepth * channel.state.channelPressure;
1072
- return tuning + pitch + pressure;
1057
+ const channelPressureRaw = channel.channelPressureTable[0];
1058
+ if (0 <= channelPressureRaw) {
1059
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1060
+ const channelPressure = channelPressureDepth *
1061
+ channel.state.channelPressure;
1062
+ return tuning + pitch + channelPressure;
1063
+ }
1064
+ else {
1065
+ return tuning + pitch;
1066
+ }
1073
1067
  }
1074
1068
  calcNoteDetune(channel, note) {
1075
1069
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1076
1070
  }
1077
1071
  updateChannelDetune(channel, scheduleTime) {
1078
- this.processScheduledNotes(channel, (note) => {
1072
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1079
1073
  this.updateDetune(channel, note, scheduleTime);
1080
1074
  });
1081
1075
  }
@@ -1324,14 +1318,11 @@ class Midy {
1324
1318
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1325
1319
  const now = this.audioContext.currentTime;
1326
1320
  const state = channel.state;
1327
- const controllerState = this.getControllerState(channel, noteNumber, velocity);
1321
+ const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1328
1322
  const voiceParams = voice.getAllParams(controllerState);
1329
1323
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1330
1324
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1331
1325
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1332
- note.volumeNode = new GainNode(this.audioContext);
1333
- note.gainL = new GainNode(this.audioContext);
1334
- note.gainR = new GainNode(this.audioContext);
1335
1326
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1336
1327
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1337
1328
  type: "lowpass",
@@ -1364,9 +1355,6 @@ class Midy {
1364
1355
  }
1365
1356
  note.bufferSource.connect(note.filterNode);
1366
1357
  note.filterNode.connect(note.volumeEnvelopeNode);
1367
- note.volumeEnvelopeNode.connect(note.volumeNode);
1368
- note.volumeNode.connect(note.gainL);
1369
- note.volumeNode.connect(note.gainR);
1370
1358
  if (0 < state.chorusSendLevel) {
1371
1359
  this.setChorusEffectsSend(channel, note, 0, now);
1372
1360
  }
@@ -1425,7 +1413,7 @@ class Midy {
1425
1413
  }
1426
1414
  this.drumExclusiveClassNotes[index] = note;
1427
1415
  }
1428
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1416
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1429
1417
  const channel = this.channels[channelNumber];
1430
1418
  const bankNumber = this.calcBank(channel, channelNumber);
1431
1419
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1437,9 +1425,18 @@ class Midy {
1437
1425
  return;
1438
1426
  const isSF3 = soundFont.parsed.info.version.major === 3;
1439
1427
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1440
- note.noteOffEvent = noteOffEvent;
1441
- note.gainL.connect(channel.gainL);
1442
- note.gainR.connect(channel.gainR);
1428
+ if (channel.isDrum) {
1429
+ const audioContext = this.audioContext;
1430
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1431
+ channel.keyBasedGainLs[noteNumber] = gainL;
1432
+ channel.keyBasedGainRs[noteNumber] = gainR;
1433
+ note.volumeEnvelopeNode.connect(gainL);
1434
+ note.volumeEnvelopeNode.connect(gainR);
1435
+ }
1436
+ else {
1437
+ note.volumeEnvelopeNode.connect(channel.gainL);
1438
+ note.volumeEnvelopeNode.connect(channel.gainR);
1439
+ }
1443
1440
  if (0.5 <= channel.state.sustainPedal) {
1444
1441
  channel.sustainNotes.push(note);
1445
1442
  }
@@ -1457,9 +1454,6 @@ class Midy {
1457
1454
  note.bufferSource.disconnect();
1458
1455
  note.filterNode.disconnect();
1459
1456
  note.volumeEnvelopeNode.disconnect();
1460
- note.volumeNode.disconnect();
1461
- note.gainL.disconnect();
1462
- note.gainR.disconnect();
1463
1457
  if (note.modulationDepth) {
1464
1458
  note.volumeDepth.disconnect();
1465
1459
  note.modulationDepth.disconnect();
@@ -1585,10 +1579,10 @@ class Midy {
1585
1579
  }
1586
1580
  handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1587
1581
  const channel = this.channels[channelNumber];
1588
- channel.state.polyphonicKeyPressure = pressure / 127;
1589
1582
  const table = channel.polyphonicKeyPressureTable;
1590
1583
  this.processActiveNotes(channel, scheduleTime, (note) => {
1591
1584
  if (note.noteNumber === noteNumber) {
1585
+ note.pressure = pressure;
1592
1586
  this.setControllerParameters(channel, note, table);
1593
1587
  }
1594
1588
  });
@@ -1616,9 +1610,10 @@ class Midy {
1616
1610
  const prev = channel.state.channelPressure;
1617
1611
  const next = value / 127;
1618
1612
  channel.state.channelPressure = next;
1619
- if (channel.channelPressureTable[0] !== 64) {
1620
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1621
- channel.detune += pressureDepth * (next - prev);
1613
+ const channelPressureRaw = channel.channelPressureTable[0];
1614
+ if (0 <= channelPressureRaw) {
1615
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1616
+ channel.detune += channelPressureDepth * (next - prev);
1622
1617
  }
1623
1618
  const table = channel.channelPressureTable;
1624
1619
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1678,10 +1673,12 @@ class Midy {
1678
1673
  .setValueAtTime(volumeDepth, scheduleTime);
1679
1674
  }
1680
1675
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1681
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1682
1676
  let value = note.voiceParams.reverbEffectsSend;
1683
- if (0 <= keyBasedValue) {
1684
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1677
+ if (channel.isDrum) {
1678
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1679
+ if (0 <= keyBasedValue) {
1680
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1681
+ }
1685
1682
  }
1686
1683
  if (0 < prevValue) {
1687
1684
  if (0 < value) {
@@ -1699,17 +1696,19 @@ class Midy {
1699
1696
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1700
1697
  gain: value,
1701
1698
  });
1702
- note.volumeNode.connect(note.reverbEffectsSend);
1699
+ note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1703
1700
  }
1704
1701
  note.reverbEffectsSend.connect(this.reverbEffect.input);
1705
1702
  }
1706
1703
  }
1707
1704
  }
1708
1705
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1709
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1710
1706
  let value = note.voiceParams.chorusEffectsSend;
1711
- if (0 <= keyBasedValue) {
1712
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1707
+ if (channel.isDrum) {
1708
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1709
+ if (0 <= keyBasedValue) {
1710
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1711
+ }
1713
1712
  }
1714
1713
  if (0 < prevValue) {
1715
1714
  if (0 < vaule) {
@@ -1727,7 +1726,7 @@ class Midy {
1727
1726
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1728
1727
  gain: value,
1729
1728
  });
1730
- note.volumeNode.connect(note.chorusEffectsSend);
1729
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1731
1730
  }
1732
1731
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1733
1732
  }
@@ -1802,21 +1801,22 @@ class Midy {
1802
1801
  },
1803
1802
  };
1804
1803
  }
1805
- getControllerState(channel, noteNumber, velocity) {
1804
+ getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
1806
1805
  const state = new Float32Array(channel.state.array.length);
1807
1806
  state.set(channel.state.array);
1808
1807
  state[2] = velocity / 127;
1809
1808
  state[3] = noteNumber / 127;
1810
- state[10] = state.polyphonicKeyPressure / 127;
1809
+ state[10] = polyphonicKeyPressure / 127;
1811
1810
  state[13] = state.channelPressure / 127;
1812
1811
  return state;
1813
1812
  }
1814
1813
  applyVoiceParams(channel, controllerType, scheduleTime) {
1815
- this.processScheduledNotes(channel, (note) => {
1816
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1814
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1815
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1817
1816
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1818
- let appliedFilterEnvelope = false;
1819
- let appliedVolumeEnvelope = false;
1817
+ let applyVolumeEnvelope = false;
1818
+ let applyFilterEnvelope = false;
1819
+ let applyPitchEnvelope = false;
1820
1820
  for (const [key, value] of Object.entries(voiceParams)) {
1821
1821
  const prevValue = note.voiceParams[key];
1822
1822
  if (value === prevValue)
@@ -1825,37 +1825,23 @@ class Midy {
1825
1825
  if (key in this.voiceParamsHandlers) {
1826
1826
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1827
1827
  }
1828
- else if (filterEnvelopeKeySet.has(key)) {
1829
- if (appliedFilterEnvelope)
1830
- continue;
1831
- appliedFilterEnvelope = true;
1832
- const noteVoiceParams = note.voiceParams;
1833
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1834
- const key = filterEnvelopeKeys[i];
1835
- if (key in voiceParams)
1836
- noteVoiceParams[key] = voiceParams[key];
1837
- }
1838
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1839
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1840
- }
1841
- else {
1842
- this.setFilterEnvelope(channel, note, scheduleTime);
1843
- }
1844
- this.setPitchEnvelope(note, scheduleTime);
1845
- }
1846
- else if (volumeEnvelopeKeySet.has(key)) {
1847
- if (appliedVolumeEnvelope)
1848
- continue;
1849
- appliedVolumeEnvelope = true;
1850
- const noteVoiceParams = note.voiceParams;
1851
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1852
- const key = volumeEnvelopeKeys[i];
1853
- if (key in voiceParams)
1854
- noteVoiceParams[key] = voiceParams[key];
1855
- }
1856
- this.setVolumeEnvelope(channel, note, scheduleTime);
1828
+ else {
1829
+ if (volumeEnvelopeKeySet.has(key))
1830
+ applyVolumeEnvelope = true;
1831
+ if (filterEnvelopeKeySet.has(key))
1832
+ applyFilterEnvelope = true;
1833
+ if (pitchEnvelopeKeySet.has(key))
1834
+ applyPitchEnvelope = true;
1857
1835
  }
1858
1836
  }
1837
+ if (applyVolumeEnvelope) {
1838
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1839
+ }
1840
+ if (applyFilterEnvelope) {
1841
+ this.setFilterEnvelope(channel, note, scheduleTime);
1842
+ }
1843
+ if (applyPitchEnvelope)
1844
+ this.setPitchEnvelope(note, scheduleTime);
1859
1845
  });
1860
1846
  }
1861
1847
  createControlChangeHandlers() {
@@ -1902,7 +1888,7 @@ class Midy {
1902
1888
  handler.call(this, channelNumber, value, scheduleTime);
1903
1889
  const channel = this.channels[channelNumber];
1904
1890
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1905
- this.applyControlTable(channel, controllerType);
1891
+ this.applyControlTable(channel, controllerType, scheduleTime);
1906
1892
  }
1907
1893
  else {
1908
1894
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1913,7 +1899,7 @@ class Midy {
1913
1899
  }
1914
1900
  updateModulation(channel, scheduleTime) {
1915
1901
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1916
- this.processScheduledNotes(channel, (note) => {
1902
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1917
1903
  if (note.modulationDepth) {
1918
1904
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1919
1905
  }
@@ -1932,7 +1918,7 @@ class Midy {
1932
1918
  this.updateModulation(channel, scheduleTime);
1933
1919
  }
1934
1920
  updatePortamento(channel, scheduleTime) {
1935
- this.processScheduledNotes(channel, (note) => {
1921
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1936
1922
  if (0.5 <= channel.state.portamento) {
1937
1923
  if (0 <= note.portamentoNoteNumber) {
1938
1924
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1959,22 +1945,12 @@ class Midy {
1959
1945
  return;
1960
1946
  this.updatePortamento(channel, scheduleTime);
1961
1947
  }
1962
- setKeyBasedVolume(channel, scheduleTime) {
1963
- this.processScheduledNotes(channel, (note) => {
1964
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1965
- if (0 <= keyBasedValue) {
1966
- note.volumeNode.gain
1967
- .cancelScheduledValues(scheduleTime)
1968
- .setValueAtTime(keyBasedValue / 127, scheduleTime);
1969
- }
1970
- });
1971
- }
1972
1948
  setVolume(channelNumber, volume, scheduleTime) {
1973
1949
  scheduleTime ??= this.audioContext.currentTime;
1974
1950
  const channel = this.channels[channelNumber];
1975
1951
  channel.state.volume = volume / 127;
1976
1952
  this.updateChannelVolume(channel, scheduleTime);
1977
- this.setKeyBasedVolume(channel, scheduleTime);
1953
+ this.updateKeyBasedVolume(channel, scheduleTime);
1978
1954
  }
1979
1955
  panToGain(pan) {
1980
1956
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1983,26 +1959,12 @@ class Midy {
1983
1959
  gainRight: Math.sin(theta),
1984
1960
  };
1985
1961
  }
1986
- setKeyBasedPan(channel, scheduleTime) {
1987
- this.processScheduledNotes(channel, (note) => {
1988
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1989
- if (0 <= keyBasedValue) {
1990
- const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1991
- note.gainL.gain
1992
- .cancelScheduledValues(scheduleTime)
1993
- .setValueAtTime(gainLeft, scheduleTime);
1994
- note.gainR.gain
1995
- .cancelScheduledValues(scheduleTime)
1996
- .setValueAtTime(gainRight, scheduleTime);
1997
- }
1998
- });
1999
- }
2000
1962
  setPan(channelNumber, pan, scheduleTime) {
2001
1963
  scheduleTime ??= this.audioContext.currentTime;
2002
1964
  const channel = this.channels[channelNumber];
2003
1965
  channel.state.pan = pan / 127;
2004
1966
  this.updateChannelVolume(channel, scheduleTime);
2005
- this.setKeyBasedPan(channel, scheduleTime);
1967
+ this.updateKeyBasedVolume(channel, scheduleTime);
2006
1968
  }
2007
1969
  setExpression(channelNumber, expression, scheduleTime) {
2008
1970
  scheduleTime ??= this.audioContext.currentTime;
@@ -2028,6 +1990,34 @@ class Midy {
2028
1990
  .cancelScheduledValues(scheduleTime)
2029
1991
  .setValueAtTime(volume * gainRight, scheduleTime);
2030
1992
  }
1993
+ updateKeyBasedVolume(channel, scheduleTime) {
1994
+ if (!channel.isDrum)
1995
+ return;
1996
+ const state = channel.state;
1997
+ const defaultVolume = state.volume * state.expression;
1998
+ const defaultPan = state.pan;
1999
+ for (let i = 0; i < 128; i++) {
2000
+ const gainL = channel.keyBasedGainLs[i];
2001
+ const gainR = channel.keyBasedGainLs[i];
2002
+ if (!gainL)
2003
+ continue;
2004
+ if (!gainR)
2005
+ continue;
2006
+ const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
2007
+ const volume = (0 <= keyBasedVolume)
2008
+ ? defaultVolume * keyBasedVolume / 64
2009
+ : defaultVolume;
2010
+ const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
2011
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2012
+ const { gainLeft, gainRight } = this.panToGain(pan);
2013
+ gainL.gain
2014
+ .cancelScheduledValues(scheduleTime)
2015
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2016
+ gainR.gain
2017
+ .cancelScheduledValues(scheduleTime)
2018
+ .setValueAtTime(volume * gainRight, scheduleTime);
2019
+ }
2020
+ }
2031
2021
  setSustainPedal(channelNumber, value, scheduleTime) {
2032
2022
  const channel = this.channels[channelNumber];
2033
2023
  if (channel.isDrum)
@@ -2035,7 +2025,7 @@ class Midy {
2035
2025
  scheduleTime ??= this.audioContext.currentTime;
2036
2026
  channel.state.sustainPedal = value / 127;
2037
2027
  if (64 <= value) {
2038
- this.processScheduledNotes(channel, (note) => {
2028
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2039
2029
  channel.sustainNotes.push(note);
2040
2030
  });
2041
2031
  }
@@ -2078,7 +2068,7 @@ class Midy {
2078
2068
  const state = channel.state;
2079
2069
  scheduleTime ??= this.audioContext.currentTime;
2080
2070
  state.softPedal = softPedal / 127;
2081
- this.processScheduledNotes(channel, (note) => {
2071
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2082
2072
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2083
2073
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2084
2074
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2096,7 +2086,7 @@ class Midy {
2096
2086
  scheduleTime ??= this.audioContext.currentTime;
2097
2087
  const state = channel.state;
2098
2088
  state.filterResonance = filterResonance / 127;
2099
- this.processScheduledNotes(channel, (note) => {
2089
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2100
2090
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2101
2091
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2102
2092
  });
@@ -2114,7 +2104,7 @@ class Midy {
2114
2104
  return;
2115
2105
  scheduleTime ??= this.audioContext.currentTime;
2116
2106
  channel.state.attackTime = attackTime / 127;
2117
- this.processScheduledNotes(channel, (note) => {
2107
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2118
2108
  if (note.startTime < scheduleTime)
2119
2109
  return false;
2120
2110
  this.setVolumeEnvelope(channel, note);
@@ -2127,7 +2117,7 @@ class Midy {
2127
2117
  const state = channel.state;
2128
2118
  scheduleTime ??= this.audioContext.currentTime;
2129
2119
  state.brightness = brightness / 127;
2130
- this.processScheduledNotes(channel, (note) => {
2120
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2131
2121
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2132
2122
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2133
2123
  }
@@ -2142,7 +2132,7 @@ class Midy {
2142
2132
  return;
2143
2133
  scheduleTime ??= this.audioContext.currentTime;
2144
2134
  channel.state.decayTime = dacayTime / 127;
2145
- this.processScheduledNotes(channel, (note) => {
2135
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2146
2136
  this.setVolumeEnvelope(channel, note, scheduleTime);
2147
2137
  });
2148
2138
  }
@@ -2154,7 +2144,7 @@ class Midy {
2154
2144
  channel.state.vibratoRate = vibratoRate / 127;
2155
2145
  if (channel.vibratoDepth <= 0)
2156
2146
  return;
2157
- this.processScheduledNotes(channel, (note) => {
2147
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2158
2148
  this.setVibLfoToPitch(channel, note, scheduleTime);
2159
2149
  });
2160
2150
  }
@@ -2166,12 +2156,12 @@ class Midy {
2166
2156
  const prev = channel.state.vibratoDepth;
2167
2157
  channel.state.vibratoDepth = vibratoDepth / 127;
2168
2158
  if (0 < prev) {
2169
- this.processScheduledNotes(channel, (note) => {
2159
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2170
2160
  this.setFreqVibLFO(channel, note, scheduleTime);
2171
2161
  });
2172
2162
  }
2173
2163
  else {
2174
- this.processScheduledNotes(channel, (note) => {
2164
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2175
2165
  this.startVibrato(channel, note, scheduleTime);
2176
2166
  });
2177
2167
  }
@@ -2183,7 +2173,7 @@ class Midy {
2183
2173
  scheduleTime ??= this.audioContext.currentTime;
2184
2174
  channel.state.vibratoDelay = vibratoDelay / 127;
2185
2175
  if (0 < channel.state.vibratoDepth) {
2186
- this.processScheduledNotes(channel, (note) => {
2176
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2187
2177
  this.startVibrato(channel, note, scheduleTime);
2188
2178
  });
2189
2179
  }
@@ -2201,7 +2191,7 @@ class Midy {
2201
2191
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2202
2192
  }
2203
2193
  else {
2204
- this.processScheduledNotes(channel, (note) => {
2194
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2205
2195
  if (note.voiceParams.reverbEffectsSend <= 0)
2206
2196
  return false;
2207
2197
  if (note.reverbEffectsSend)
@@ -2211,7 +2201,7 @@ class Midy {
2211
2201
  }
2212
2202
  else {
2213
2203
  if (0 < reverbSendLevel) {
2214
- this.processScheduledNotes(channel, (note) => {
2204
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2215
2205
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2216
2206
  });
2217
2207
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2234,7 +2224,7 @@ class Midy {
2234
2224
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2235
2225
  }
2236
2226
  else {
2237
- this.processScheduledNotes(channel, (note) => {
2227
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2238
2228
  if (note.voiceParams.chorusEffectsSend <= 0)
2239
2229
  return false;
2240
2230
  if (note.chorusEffectsSend)
@@ -2244,7 +2234,7 @@ class Midy {
2244
2234
  }
2245
2235
  else {
2246
2236
  if (0 < chorusSendLevel) {
2247
- this.processScheduledNotes(channel, (note) => {
2237
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2248
2238
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2249
2239
  });
2250
2240
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2659,8 +2649,7 @@ class Midy {
2659
2649
  setReverbType(type) {
2660
2650
  this.reverb.time = this.getReverbTimeFromType(type);
2661
2651
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2662
- const { audioContext, options } = this;
2663
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2652
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2664
2653
  }
2665
2654
  getReverbTimeFromType(type) {
2666
2655
  switch (type) {
@@ -2682,8 +2671,7 @@ class Midy {
2682
2671
  }
2683
2672
  setReverbTime(value) {
2684
2673
  this.reverb.time = this.getReverbTime(value);
2685
- const { audioContext, options } = this;
2686
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2674
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2687
2675
  }
2688
2676
  getReverbTime(value) {
2689
2677
  return Math.exp((value - 40) * 0.025);
@@ -2878,65 +2866,88 @@ class Midy {
2878
2866
  }
2879
2867
  }
2880
2868
  getPitchControl(channel, note) {
2881
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2869
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
2870
+ if (polyphonicKeyPressureRaw < 0)
2871
+ return 0;
2872
+ const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
2882
2873
  note.pressure;
2883
2874
  return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2884
2875
  }
2885
2876
  getFilterCutoffControl(channel, note) {
2886
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2887
- channel.state.channelPressure;
2888
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2889
- note.pressure;
2877
+ const channelPressureRaw = channel.channelPressureTable[1];
2878
+ const channelPressure = (0 <= channelPressureRaw)
2879
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2880
+ : 0;
2881
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[1];
2882
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2883
+ ? (polyphonicKeyPressureRaw - 64) * note.pressure
2884
+ : 0;
2890
2885
  return (channelPressure + polyphonicKeyPressure) * 15;
2891
2886
  }
2892
2887
  getAmplitudeControl(channel, note) {
2893
- const channelPressure = channel.channelPressureTable[2] *
2894
- channel.state.channelPressure;
2895
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2896
- note.pressure;
2888
+ const channelPressureRaw = channel.channelPressureTable[2];
2889
+ const channelPressure = (0 <= channelPressureRaw)
2890
+ ? channelPressureRaw * channel.state.channelPressure
2891
+ : 0;
2892
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
2893
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2894
+ ? polyphonicKeyPressureRaw * note.pressure
2895
+ : 0;
2897
2896
  return (channelPressure + polyphonicKeyPressure) / 128;
2898
2897
  }
2899
2898
  getLFOPitchDepth(channel, note) {
2900
- const channelPressure = channel.channelPressureTable[3] *
2901
- channel.state.channelPressure;
2902
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2903
- note.pressure;
2899
+ const channelPressureRaw = channel.channelPressureTable[3];
2900
+ const channelPressure = (0 <= channelPressureRaw)
2901
+ ? channelPressureRaw * channel.state.channelPressure
2902
+ : 0;
2903
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
2904
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2905
+ ? polyphonicKeyPressureRaw * note.pressure
2906
+ : 0;
2904
2907
  return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2905
2908
  }
2906
2909
  getLFOFilterDepth(channel, note) {
2907
- const channelPressure = channel.channelPressureTable[4] *
2908
- channel.state.channelPressure;
2909
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2910
- note.pressure;
2910
+ const channelPressureRaw = channel.channelPressureTable[4];
2911
+ const channelPressure = (0 <= channelPressureRaw)
2912
+ ? channelPressureRaw * channel.state.channelPressure
2913
+ : 0;
2914
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
2915
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2916
+ ? polyphonicKeyPressureRaw * note.pressure
2917
+ : 0;
2911
2918
  return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2912
2919
  }
2913
2920
  getLFOAmplitudeDepth(channel, note) {
2914
- const channelPressure = channel.channelPressureTable[5] *
2915
- channel.state.channelPressure;
2916
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2917
- note.pressure;
2921
+ const channelPressureRaw = channel.channelPressureTable[5];
2922
+ const channelPressure = (0 <= channelPressureRaw)
2923
+ ? channelPressureRaw * channel.state.channelPressure
2924
+ : 0;
2925
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[5];
2926
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2927
+ ? polyphonicKeyPressureRaw * note.pressure
2928
+ : 0;
2918
2929
  return (channelPressure + polyphonicKeyPressure) / 254;
2919
2930
  }
2920
2931
  setControllerParameters(channel, note, table) {
2921
- if (table[0] !== 64)
2932
+ if (0 <= table[0])
2922
2933
  this.updateDetune(channel, note);
2923
2934
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2924
- if (table[1] !== 64)
2935
+ if (0 <= table[1])
2925
2936
  this.setPortamentoFilterEnvelope(channel, note);
2926
- if (table[2] !== 64)
2937
+ if (0 <= table[2])
2927
2938
  this.setPortamentoVolumeEnvelope(channel, note);
2928
2939
  }
2929
2940
  else {
2930
- if (table[1] !== 64)
2941
+ if (0 <= table[1])
2931
2942
  this.setFilterEnvelope(channel, note);
2932
- if (table[2] !== 64)
2943
+ if (0 <= table[2])
2933
2944
  this.setVolumeEnvelope(channel, note);
2934
2945
  }
2935
- if (table[3] !== 0)
2946
+ if (0 <= table[3])
2936
2947
  this.setModLfoToPitch(channel, note);
2937
- if (table[4] !== 0)
2948
+ if (0 <= table[4])
2938
2949
  this.setModLfoToFilterFc(channel, note);
2939
- if (table[5] !== 0)
2950
+ if (0 <= table[5])
2940
2951
  this.setModLfoToVolume(channel, note);
2941
2952
  }
2942
2953
  handlePressureSysEx(data, tableName) {
@@ -2952,26 +2963,15 @@ class Midy {
2952
2963
  }
2953
2964
  }
2954
2965
  initControlTable() {
2955
- const channelCount = 128;
2966
+ const ccCount = 128;
2956
2967
  const slotSize = 6;
2957
- const table = new Uint8Array(channelCount * slotSize);
2958
- return this.resetControlTable(table);
2968
+ return new Int8Array(ccCount * slotSize).fill(-1);
2959
2969
  }
2960
- resetControlTable(table) {
2961
- const channelCount = 128;
2962
- const slotSize = 6;
2963
- const defaultValues = [64, 64, 64, 0, 0, 0];
2964
- for (let ch = 0; ch < channelCount; ch++) {
2965
- const offset = ch * slotSize;
2966
- table.set(defaultValues, offset);
2967
- }
2968
- return table;
2969
- }
2970
- applyControlTable(channel, controllerType) {
2970
+ applyControlTable(channel, controllerType, scheduleTime) {
2971
2971
  const slotSize = 6;
2972
2972
  const offset = controllerType * slotSize;
2973
2973
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2974
- this.processScheduledNotes(channel, (note) => {
2974
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2975
2975
  this.setControllerParameters(channel, note, table);
2976
2976
  });
2977
2977
  }
@@ -2996,7 +2996,7 @@ class Midy {
2996
2996
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2997
2997
  const channelNumber = data[4];
2998
2998
  const channel = this.channels[channelNumber];
2999
- if (channel.isDrum)
2999
+ if (!channel.isDrum)
3000
3000
  return;
3001
3001
  const keyNumber = data[5];
3002
3002
  const table = channel.keyBasedInstrumentControlTable;