@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/esm/midy.js CHANGED
@@ -44,24 +44,6 @@ class Note {
44
44
  writable: true,
45
45
  value: void 0
46
46
  });
47
- Object.defineProperty(this, "volumeNode", {
48
- enumerable: true,
49
- configurable: true,
50
- writable: true,
51
- value: void 0
52
- });
53
- Object.defineProperty(this, "gainL", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: void 0
58
- });
59
- Object.defineProperty(this, "gainR", {
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- value: void 0
64
- });
65
47
  Object.defineProperty(this, "modulationLFO", {
66
48
  enumerable: true,
67
49
  configurable: true,
@@ -214,6 +196,16 @@ class ControllerState {
214
196
  }
215
197
  }
216
198
  }
199
+ const volumeEnvelopeKeys = [
200
+ "volDelay",
201
+ "volAttack",
202
+ "volHold",
203
+ "volDecay",
204
+ "volSustain",
205
+ "volRelease",
206
+ "initialAttenuation",
207
+ ];
208
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
217
209
  const filterEnvelopeKeys = [
218
210
  "modEnvToPitch",
219
211
  "initialFilterFc",
@@ -223,22 +215,20 @@ const filterEnvelopeKeys = [
223
215
  "modHold",
224
216
  "modDecay",
225
217
  "modSustain",
226
- "modRelease",
227
- "playbackRate",
228
218
  ];
229
219
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
230
- const volumeEnvelopeKeys = [
231
- "volDelay",
232
- "volAttack",
233
- "volHold",
234
- "volDecay",
235
- "volSustain",
236
- "volRelease",
237
- "initialAttenuation",
220
+ const pitchEnvelopeKeys = [
221
+ "modEnvToPitch",
222
+ "modDelay",
223
+ "modAttack",
224
+ "modHold",
225
+ "modDecay",
226
+ "modSustain",
227
+ "playbackRate",
238
228
  ];
239
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
229
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
240
230
  export class Midy {
241
- constructor(audioContext, options = this.defaultOptions) {
231
+ constructor(audioContext) {
242
232
  Object.defineProperty(this, "mode", {
243
233
  enumerable: true,
244
234
  configurable: true,
@@ -262,6 +252,7 @@ export class Midy {
262
252
  configurable: true,
263
253
  writable: true,
264
254
  value: {
255
+ algorithm: "SchroederReverb",
265
256
  time: this.getReverbTime(64),
266
257
  feedback: 0.8,
267
258
  }
@@ -410,30 +401,7 @@ export class Midy {
410
401
  writable: true,
411
402
  value: new Array(this.numChannels * drumExclusiveClassCount)
412
403
  });
413
- Object.defineProperty(this, "defaultOptions", {
414
- enumerable: true,
415
- configurable: true,
416
- writable: true,
417
- value: {
418
- reverbAlgorithm: (audioContext) => {
419
- const { time: rt60, feedback } = this.reverb;
420
- // const delay = this.calcDelay(rt60, feedback);
421
- // const impulse = this.createConvolutionReverbImpulse(
422
- // audioContext,
423
- // rt60,
424
- // delay,
425
- // );
426
- // return this.createConvolutionReverb(audioContext, impulse);
427
- const combFeedbacks = this.generateDistributedArray(feedback, 4);
428
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
429
- const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
430
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
431
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
432
- },
433
- }
434
- });
435
404
  this.audioContext = audioContext;
436
- this.options = { ...this.defaultOptions, ...options };
437
405
  this.masterVolume = new GainNode(audioContext);
438
406
  this.scheduler = new GainNode(audioContext, { gain: 0 });
439
407
  this.schedulerBuffer = new AudioBuffer({
@@ -443,7 +411,7 @@ export class Midy {
443
411
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
444
412
  this.controlChangeHandlers = this.createControlChangeHandlers();
445
413
  this.channels = this.createChannels(audioContext);
446
- this.reverbEffect = this.options.reverbAlgorithm(audioContext);
414
+ this.reverbEffect = this.createReverbEffect(audioContext);
447
415
  this.chorusEffect = this.createChorusEffect(audioContext);
448
416
  this.chorusEffect.output.connect(this.masterVolume);
449
417
  this.reverbEffect.output.connect(this.masterVolume);
@@ -507,7 +475,7 @@ export class Midy {
507
475
  this.timeline = midiData.timeline;
508
476
  this.totalTime = this.calcTotalTime();
509
477
  }
510
- setChannelAudioNodes(audioContext) {
478
+ createChannelAudioNodes(audioContext) {
511
479
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
512
480
  const gainL = new GainNode(audioContext, { gain: gainLeft });
513
481
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -522,10 +490,10 @@ export class Midy {
522
490
  };
523
491
  }
524
492
  resetChannelTable(channel) {
525
- this.resetControlTable(channel.controlTable);
493
+ channel.controlTable.fill(-1);
526
494
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
527
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
528
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
495
+ channel.channelPressureTable.fill(-1);
496
+ channel.polyphonicKeyPressureTable.fill(-1);
529
497
  channel.keyBasedInstrumentControlTable.fill(-1);
530
498
  }
531
499
  createChannels(audioContext) {
@@ -535,15 +503,17 @@ export class Midy {
535
503
  isDrum: false,
536
504
  state: new ControllerState(),
537
505
  ...this.constructor.channelSettings,
538
- ...this.setChannelAudioNodes(audioContext),
506
+ ...this.createChannelAudioNodes(audioContext),
539
507
  scheduledNotes: [],
540
508
  sustainNotes: [],
541
509
  sostenutoNotes: [],
542
510
  controlTable: this.initControlTable(),
543
511
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
544
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
545
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
512
+ channelPressureTable: new Int8Array(6).fill(-1),
513
+ polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
546
514
  keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
515
+ keyBasedGainLs: new Array(128),
516
+ keyBasedGainRs: new Array(128),
547
517
  };
548
518
  });
549
519
  return channels;
@@ -585,10 +555,9 @@ export class Midy {
585
555
  createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
586
556
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
587
557
  bufferSource.buffer = audioBuffer;
588
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
589
- if (channel.isDrum) {
590
- bufferSource.loop = this.isLoopDrum(channel, noteNumber);
591
- }
558
+ bufferSource.loop = channel.isDrum
559
+ ? this.isLoopDrum(channel, noteNumber)
560
+ : (voiceParams.sampleModes % 2 !== 0);
592
561
  if (bufferSource.loop) {
593
562
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
594
563
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -832,7 +801,7 @@ export class Midy {
832
801
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
833
802
  const channel = this.channels[channelNumber];
834
803
  const promises = [];
835
- this.processScheduledNotes(channel, (note) => {
804
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
836
805
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
837
806
  this.notePromises.push(promise);
838
807
  promises.push(promise);
@@ -891,7 +860,7 @@ export class Midy {
891
860
  const now = this.audioContext.currentTime;
892
861
  return this.resumeTime + now - this.startTime - this.startDelay;
893
862
  }
894
- processScheduledNotes(channel, callback) {
863
+ processScheduledNotes(channel, scheduleTime, callback) {
895
864
  const scheduledNotes = channel.scheduledNotes;
896
865
  for (let i = 0; i < scheduledNotes.length; i++) {
897
866
  const note = scheduledNotes[i];
@@ -899,6 +868,8 @@ export class Midy {
899
868
  continue;
900
869
  if (note.ending)
901
870
  continue;
871
+ if (note.startTime < scheduleTime)
872
+ continue;
902
873
  callback(note);
903
874
  }
904
875
  }
@@ -911,7 +882,7 @@ export class Midy {
911
882
  if (note.ending)
912
883
  continue;
913
884
  if (scheduleTime < note.startTime)
914
- continue;
885
+ break;
915
886
  callback(note);
916
887
  }
917
888
  }
@@ -1000,6 +971,22 @@ export class Midy {
1000
971
  const output = allpasses.at(-1);
1001
972
  return { input, output };
1002
973
  }
974
+ createReverbEffect(audioContext) {
975
+ const { algorithm, time: rt60, feedback } = this.reverb;
976
+ switch (algorithm) {
977
+ case "ConvolutionReverb": {
978
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
979
+ return this.createConvolutionReverb(audioContext, impulse);
980
+ }
981
+ case "SchroederReverb": {
982
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
983
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
984
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
985
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
986
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
987
+ }
988
+ }
989
+ }
1003
990
  createChorusEffect(audioContext) {
1004
991
  const input = new GainNode(audioContext);
1005
992
  const output = new GainNode(audioContext);
@@ -1064,15 +1051,22 @@ export class Midy {
1064
1051
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1065
1052
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1066
1053
  const pitch = pitchWheel * pitchWheelSensitivity;
1067
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1068
- const pressure = pressureDepth * channel.state.channelPressure;
1069
- return tuning + pitch + pressure;
1054
+ const channelPressureRaw = channel.channelPressureTable[0];
1055
+ if (0 <= channelPressureRaw) {
1056
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1057
+ const channelPressure = channelPressureDepth *
1058
+ channel.state.channelPressure;
1059
+ return tuning + pitch + channelPressure;
1060
+ }
1061
+ else {
1062
+ return tuning + pitch;
1063
+ }
1070
1064
  }
1071
1065
  calcNoteDetune(channel, note) {
1072
1066
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1073
1067
  }
1074
1068
  updateChannelDetune(channel, scheduleTime) {
1075
- this.processScheduledNotes(channel, (note) => {
1069
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1076
1070
  this.updateDetune(channel, note, scheduleTime);
1077
1071
  });
1078
1072
  }
@@ -1321,14 +1315,11 @@ export class Midy {
1321
1315
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1322
1316
  const now = this.audioContext.currentTime;
1323
1317
  const state = channel.state;
1324
- const controllerState = this.getControllerState(channel, noteNumber, velocity);
1318
+ const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1325
1319
  const voiceParams = voice.getAllParams(controllerState);
1326
1320
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1327
1321
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1328
1322
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1329
- note.volumeNode = new GainNode(this.audioContext);
1330
- note.gainL = new GainNode(this.audioContext);
1331
- note.gainR = new GainNode(this.audioContext);
1332
1323
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1333
1324
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1334
1325
  type: "lowpass",
@@ -1361,9 +1352,6 @@ export class Midy {
1361
1352
  }
1362
1353
  note.bufferSource.connect(note.filterNode);
1363
1354
  note.filterNode.connect(note.volumeEnvelopeNode);
1364
- note.volumeEnvelopeNode.connect(note.volumeNode);
1365
- note.volumeNode.connect(note.gainL);
1366
- note.volumeNode.connect(note.gainR);
1367
1355
  if (0 < state.chorusSendLevel) {
1368
1356
  this.setChorusEffectsSend(channel, note, 0, now);
1369
1357
  }
@@ -1422,7 +1410,7 @@ export class Midy {
1422
1410
  }
1423
1411
  this.drumExclusiveClassNotes[index] = note;
1424
1412
  }
1425
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1413
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1426
1414
  const channel = this.channels[channelNumber];
1427
1415
  const bankNumber = this.calcBank(channel, channelNumber);
1428
1416
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1434,9 +1422,18 @@ export class Midy {
1434
1422
  return;
1435
1423
  const isSF3 = soundFont.parsed.info.version.major === 3;
1436
1424
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1437
- note.noteOffEvent = noteOffEvent;
1438
- note.gainL.connect(channel.gainL);
1439
- note.gainR.connect(channel.gainR);
1425
+ if (channel.isDrum) {
1426
+ const audioContext = this.audioContext;
1427
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1428
+ channel.keyBasedGainLs[noteNumber] = gainL;
1429
+ channel.keyBasedGainRs[noteNumber] = gainR;
1430
+ note.volumeEnvelopeNode.connect(gainL);
1431
+ note.volumeEnvelopeNode.connect(gainR);
1432
+ }
1433
+ else {
1434
+ note.volumeEnvelopeNode.connect(channel.gainL);
1435
+ note.volumeEnvelopeNode.connect(channel.gainR);
1436
+ }
1440
1437
  if (0.5 <= channel.state.sustainPedal) {
1441
1438
  channel.sustainNotes.push(note);
1442
1439
  }
@@ -1454,9 +1451,6 @@ export class Midy {
1454
1451
  note.bufferSource.disconnect();
1455
1452
  note.filterNode.disconnect();
1456
1453
  note.volumeEnvelopeNode.disconnect();
1457
- note.volumeNode.disconnect();
1458
- note.gainL.disconnect();
1459
- note.gainR.disconnect();
1460
1454
  if (note.modulationDepth) {
1461
1455
  note.volumeDepth.disconnect();
1462
1456
  note.modulationDepth.disconnect();
@@ -1582,10 +1576,10 @@ export class Midy {
1582
1576
  }
1583
1577
  handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1584
1578
  const channel = this.channels[channelNumber];
1585
- channel.state.polyphonicKeyPressure = pressure / 127;
1586
1579
  const table = channel.polyphonicKeyPressureTable;
1587
1580
  this.processActiveNotes(channel, scheduleTime, (note) => {
1588
1581
  if (note.noteNumber === noteNumber) {
1582
+ note.pressure = pressure;
1589
1583
  this.setControllerParameters(channel, note, table);
1590
1584
  }
1591
1585
  });
@@ -1613,9 +1607,10 @@ export class Midy {
1613
1607
  const prev = channel.state.channelPressure;
1614
1608
  const next = value / 127;
1615
1609
  channel.state.channelPressure = next;
1616
- if (channel.channelPressureTable[0] !== 64) {
1617
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1618
- channel.detune += pressureDepth * (next - prev);
1610
+ const channelPressureRaw = channel.channelPressureTable[0];
1611
+ if (0 <= channelPressureRaw) {
1612
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1613
+ channel.detune += channelPressureDepth * (next - prev);
1619
1614
  }
1620
1615
  const table = channel.channelPressureTable;
1621
1616
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1675,10 +1670,12 @@ export class Midy {
1675
1670
  .setValueAtTime(volumeDepth, scheduleTime);
1676
1671
  }
1677
1672
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1678
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1679
1673
  let value = note.voiceParams.reverbEffectsSend;
1680
- if (0 <= keyBasedValue) {
1681
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1674
+ if (channel.isDrum) {
1675
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1676
+ if (0 <= keyBasedValue) {
1677
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1678
+ }
1682
1679
  }
1683
1680
  if (0 < prevValue) {
1684
1681
  if (0 < value) {
@@ -1696,17 +1693,19 @@ export class Midy {
1696
1693
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1697
1694
  gain: value,
1698
1695
  });
1699
- note.volumeNode.connect(note.reverbEffectsSend);
1696
+ note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1700
1697
  }
1701
1698
  note.reverbEffectsSend.connect(this.reverbEffect.input);
1702
1699
  }
1703
1700
  }
1704
1701
  }
1705
1702
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1706
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1707
1703
  let value = note.voiceParams.chorusEffectsSend;
1708
- if (0 <= keyBasedValue) {
1709
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1704
+ if (channel.isDrum) {
1705
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1706
+ if (0 <= keyBasedValue) {
1707
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1708
+ }
1710
1709
  }
1711
1710
  if (0 < prevValue) {
1712
1711
  if (0 < vaule) {
@@ -1724,7 +1723,7 @@ export class Midy {
1724
1723
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1725
1724
  gain: value,
1726
1725
  });
1727
- note.volumeNode.connect(note.chorusEffectsSend);
1726
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1728
1727
  }
1729
1728
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1730
1729
  }
@@ -1799,21 +1798,22 @@ export class Midy {
1799
1798
  },
1800
1799
  };
1801
1800
  }
1802
- getControllerState(channel, noteNumber, velocity) {
1801
+ getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
1803
1802
  const state = new Float32Array(channel.state.array.length);
1804
1803
  state.set(channel.state.array);
1805
1804
  state[2] = velocity / 127;
1806
1805
  state[3] = noteNumber / 127;
1807
- state[10] = state.polyphonicKeyPressure / 127;
1806
+ state[10] = polyphonicKeyPressure / 127;
1808
1807
  state[13] = state.channelPressure / 127;
1809
1808
  return state;
1810
1809
  }
1811
1810
  applyVoiceParams(channel, controllerType, scheduleTime) {
1812
- this.processScheduledNotes(channel, (note) => {
1813
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1811
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1812
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1814
1813
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1815
- let appliedFilterEnvelope = false;
1816
- let appliedVolumeEnvelope = false;
1814
+ let applyVolumeEnvelope = false;
1815
+ let applyFilterEnvelope = false;
1816
+ let applyPitchEnvelope = false;
1817
1817
  for (const [key, value] of Object.entries(voiceParams)) {
1818
1818
  const prevValue = note.voiceParams[key];
1819
1819
  if (value === prevValue)
@@ -1822,37 +1822,23 @@ export class Midy {
1822
1822
  if (key in this.voiceParamsHandlers) {
1823
1823
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1824
1824
  }
1825
- else if (filterEnvelopeKeySet.has(key)) {
1826
- if (appliedFilterEnvelope)
1827
- continue;
1828
- appliedFilterEnvelope = true;
1829
- const noteVoiceParams = note.voiceParams;
1830
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1831
- const key = filterEnvelopeKeys[i];
1832
- if (key in voiceParams)
1833
- noteVoiceParams[key] = voiceParams[key];
1834
- }
1835
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1836
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1837
- }
1838
- else {
1839
- this.setFilterEnvelope(channel, note, scheduleTime);
1840
- }
1841
- this.setPitchEnvelope(note, scheduleTime);
1842
- }
1843
- else if (volumeEnvelopeKeySet.has(key)) {
1844
- if (appliedVolumeEnvelope)
1845
- continue;
1846
- appliedVolumeEnvelope = true;
1847
- const noteVoiceParams = note.voiceParams;
1848
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1849
- const key = volumeEnvelopeKeys[i];
1850
- if (key in voiceParams)
1851
- noteVoiceParams[key] = voiceParams[key];
1852
- }
1853
- this.setVolumeEnvelope(channel, note, scheduleTime);
1825
+ else {
1826
+ if (volumeEnvelopeKeySet.has(key))
1827
+ applyVolumeEnvelope = true;
1828
+ if (filterEnvelopeKeySet.has(key))
1829
+ applyFilterEnvelope = true;
1830
+ if (pitchEnvelopeKeySet.has(key))
1831
+ applyPitchEnvelope = true;
1854
1832
  }
1855
1833
  }
1834
+ if (applyVolumeEnvelope) {
1835
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1836
+ }
1837
+ if (applyFilterEnvelope) {
1838
+ this.setFilterEnvelope(channel, note, scheduleTime);
1839
+ }
1840
+ if (applyPitchEnvelope)
1841
+ this.setPitchEnvelope(note, scheduleTime);
1856
1842
  });
1857
1843
  }
1858
1844
  createControlChangeHandlers() {
@@ -1899,7 +1885,7 @@ export class Midy {
1899
1885
  handler.call(this, channelNumber, value, scheduleTime);
1900
1886
  const channel = this.channels[channelNumber];
1901
1887
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1902
- this.applyControlTable(channel, controllerType);
1888
+ this.applyControlTable(channel, controllerType, scheduleTime);
1903
1889
  }
1904
1890
  else {
1905
1891
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1910,7 +1896,7 @@ export class Midy {
1910
1896
  }
1911
1897
  updateModulation(channel, scheduleTime) {
1912
1898
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1913
- this.processScheduledNotes(channel, (note) => {
1899
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1914
1900
  if (note.modulationDepth) {
1915
1901
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1916
1902
  }
@@ -1929,7 +1915,7 @@ export class Midy {
1929
1915
  this.updateModulation(channel, scheduleTime);
1930
1916
  }
1931
1917
  updatePortamento(channel, scheduleTime) {
1932
- this.processScheduledNotes(channel, (note) => {
1918
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1933
1919
  if (0.5 <= channel.state.portamento) {
1934
1920
  if (0 <= note.portamentoNoteNumber) {
1935
1921
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1956,22 +1942,12 @@ export class Midy {
1956
1942
  return;
1957
1943
  this.updatePortamento(channel, scheduleTime);
1958
1944
  }
1959
- setKeyBasedVolume(channel, scheduleTime) {
1960
- this.processScheduledNotes(channel, (note) => {
1961
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1962
- if (0 <= keyBasedValue) {
1963
- note.volumeNode.gain
1964
- .cancelScheduledValues(scheduleTime)
1965
- .setValueAtTime(keyBasedValue / 127, scheduleTime);
1966
- }
1967
- });
1968
- }
1969
1945
  setVolume(channelNumber, volume, scheduleTime) {
1970
1946
  scheduleTime ??= this.audioContext.currentTime;
1971
1947
  const channel = this.channels[channelNumber];
1972
1948
  channel.state.volume = volume / 127;
1973
1949
  this.updateChannelVolume(channel, scheduleTime);
1974
- this.setKeyBasedVolume(channel, scheduleTime);
1950
+ this.updateKeyBasedVolume(channel, scheduleTime);
1975
1951
  }
1976
1952
  panToGain(pan) {
1977
1953
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1980,26 +1956,12 @@ export class Midy {
1980
1956
  gainRight: Math.sin(theta),
1981
1957
  };
1982
1958
  }
1983
- setKeyBasedPan(channel, scheduleTime) {
1984
- this.processScheduledNotes(channel, (note) => {
1985
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1986
- if (0 <= keyBasedValue) {
1987
- const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1988
- note.gainL.gain
1989
- .cancelScheduledValues(scheduleTime)
1990
- .setValueAtTime(gainLeft, scheduleTime);
1991
- note.gainR.gain
1992
- .cancelScheduledValues(scheduleTime)
1993
- .setValueAtTime(gainRight, scheduleTime);
1994
- }
1995
- });
1996
- }
1997
1959
  setPan(channelNumber, pan, scheduleTime) {
1998
1960
  scheduleTime ??= this.audioContext.currentTime;
1999
1961
  const channel = this.channels[channelNumber];
2000
1962
  channel.state.pan = pan / 127;
2001
1963
  this.updateChannelVolume(channel, scheduleTime);
2002
- this.setKeyBasedPan(channel, scheduleTime);
1964
+ this.updateKeyBasedVolume(channel, scheduleTime);
2003
1965
  }
2004
1966
  setExpression(channelNumber, expression, scheduleTime) {
2005
1967
  scheduleTime ??= this.audioContext.currentTime;
@@ -2025,6 +1987,34 @@ export class Midy {
2025
1987
  .cancelScheduledValues(scheduleTime)
2026
1988
  .setValueAtTime(volume * gainRight, scheduleTime);
2027
1989
  }
1990
+ updateKeyBasedVolume(channel, scheduleTime) {
1991
+ if (!channel.isDrum)
1992
+ return;
1993
+ const state = channel.state;
1994
+ const defaultVolume = state.volume * state.expression;
1995
+ const defaultPan = state.pan;
1996
+ for (let i = 0; i < 128; i++) {
1997
+ const gainL = channel.keyBasedGainLs[i];
1998
+ const gainR = channel.keyBasedGainLs[i];
1999
+ if (!gainL)
2000
+ continue;
2001
+ if (!gainR)
2002
+ continue;
2003
+ const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
2004
+ const volume = (0 <= keyBasedVolume)
2005
+ ? defaultVolume * keyBasedVolume / 64
2006
+ : defaultVolume;
2007
+ const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
2008
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2009
+ const { gainLeft, gainRight } = this.panToGain(pan);
2010
+ gainL.gain
2011
+ .cancelScheduledValues(scheduleTime)
2012
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2013
+ gainR.gain
2014
+ .cancelScheduledValues(scheduleTime)
2015
+ .setValueAtTime(volume * gainRight, scheduleTime);
2016
+ }
2017
+ }
2028
2018
  setSustainPedal(channelNumber, value, scheduleTime) {
2029
2019
  const channel = this.channels[channelNumber];
2030
2020
  if (channel.isDrum)
@@ -2032,7 +2022,7 @@ export class Midy {
2032
2022
  scheduleTime ??= this.audioContext.currentTime;
2033
2023
  channel.state.sustainPedal = value / 127;
2034
2024
  if (64 <= value) {
2035
- this.processScheduledNotes(channel, (note) => {
2025
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2036
2026
  channel.sustainNotes.push(note);
2037
2027
  });
2038
2028
  }
@@ -2075,7 +2065,7 @@ export class Midy {
2075
2065
  const state = channel.state;
2076
2066
  scheduleTime ??= this.audioContext.currentTime;
2077
2067
  state.softPedal = softPedal / 127;
2078
- this.processScheduledNotes(channel, (note) => {
2068
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2079
2069
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2080
2070
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2081
2071
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2093,7 +2083,7 @@ export class Midy {
2093
2083
  scheduleTime ??= this.audioContext.currentTime;
2094
2084
  const state = channel.state;
2095
2085
  state.filterResonance = filterResonance / 127;
2096
- this.processScheduledNotes(channel, (note) => {
2086
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2097
2087
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2098
2088
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2099
2089
  });
@@ -2111,7 +2101,7 @@ export class Midy {
2111
2101
  return;
2112
2102
  scheduleTime ??= this.audioContext.currentTime;
2113
2103
  channel.state.attackTime = attackTime / 127;
2114
- this.processScheduledNotes(channel, (note) => {
2104
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2115
2105
  if (note.startTime < scheduleTime)
2116
2106
  return false;
2117
2107
  this.setVolumeEnvelope(channel, note);
@@ -2124,7 +2114,7 @@ export class Midy {
2124
2114
  const state = channel.state;
2125
2115
  scheduleTime ??= this.audioContext.currentTime;
2126
2116
  state.brightness = brightness / 127;
2127
- this.processScheduledNotes(channel, (note) => {
2117
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2128
2118
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2129
2119
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2130
2120
  }
@@ -2139,7 +2129,7 @@ export class Midy {
2139
2129
  return;
2140
2130
  scheduleTime ??= this.audioContext.currentTime;
2141
2131
  channel.state.decayTime = dacayTime / 127;
2142
- this.processScheduledNotes(channel, (note) => {
2132
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2143
2133
  this.setVolumeEnvelope(channel, note, scheduleTime);
2144
2134
  });
2145
2135
  }
@@ -2151,7 +2141,7 @@ export class Midy {
2151
2141
  channel.state.vibratoRate = vibratoRate / 127;
2152
2142
  if (channel.vibratoDepth <= 0)
2153
2143
  return;
2154
- this.processScheduledNotes(channel, (note) => {
2144
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2155
2145
  this.setVibLfoToPitch(channel, note, scheduleTime);
2156
2146
  });
2157
2147
  }
@@ -2163,12 +2153,12 @@ export class Midy {
2163
2153
  const prev = channel.state.vibratoDepth;
2164
2154
  channel.state.vibratoDepth = vibratoDepth / 127;
2165
2155
  if (0 < prev) {
2166
- this.processScheduledNotes(channel, (note) => {
2156
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2167
2157
  this.setFreqVibLFO(channel, note, scheduleTime);
2168
2158
  });
2169
2159
  }
2170
2160
  else {
2171
- this.processScheduledNotes(channel, (note) => {
2161
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2172
2162
  this.startVibrato(channel, note, scheduleTime);
2173
2163
  });
2174
2164
  }
@@ -2180,7 +2170,7 @@ export class Midy {
2180
2170
  scheduleTime ??= this.audioContext.currentTime;
2181
2171
  channel.state.vibratoDelay = vibratoDelay / 127;
2182
2172
  if (0 < channel.state.vibratoDepth) {
2183
- this.processScheduledNotes(channel, (note) => {
2173
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2184
2174
  this.startVibrato(channel, note, scheduleTime);
2185
2175
  });
2186
2176
  }
@@ -2198,7 +2188,7 @@ export class Midy {
2198
2188
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2199
2189
  }
2200
2190
  else {
2201
- this.processScheduledNotes(channel, (note) => {
2191
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2202
2192
  if (note.voiceParams.reverbEffectsSend <= 0)
2203
2193
  return false;
2204
2194
  if (note.reverbEffectsSend)
@@ -2208,7 +2198,7 @@ export class Midy {
2208
2198
  }
2209
2199
  else {
2210
2200
  if (0 < reverbSendLevel) {
2211
- this.processScheduledNotes(channel, (note) => {
2201
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2212
2202
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2213
2203
  });
2214
2204
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2231,7 +2221,7 @@ export class Midy {
2231
2221
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2232
2222
  }
2233
2223
  else {
2234
- this.processScheduledNotes(channel, (note) => {
2224
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2235
2225
  if (note.voiceParams.chorusEffectsSend <= 0)
2236
2226
  return false;
2237
2227
  if (note.chorusEffectsSend)
@@ -2241,7 +2231,7 @@ export class Midy {
2241
2231
  }
2242
2232
  else {
2243
2233
  if (0 < chorusSendLevel) {
2244
- this.processScheduledNotes(channel, (note) => {
2234
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2245
2235
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2246
2236
  });
2247
2237
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2656,8 +2646,7 @@ export class Midy {
2656
2646
  setReverbType(type) {
2657
2647
  this.reverb.time = this.getReverbTimeFromType(type);
2658
2648
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2659
- const { audioContext, options } = this;
2660
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2649
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2661
2650
  }
2662
2651
  getReverbTimeFromType(type) {
2663
2652
  switch (type) {
@@ -2679,8 +2668,7 @@ export class Midy {
2679
2668
  }
2680
2669
  setReverbTime(value) {
2681
2670
  this.reverb.time = this.getReverbTime(value);
2682
- const { audioContext, options } = this;
2683
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2671
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2684
2672
  }
2685
2673
  getReverbTime(value) {
2686
2674
  return Math.exp((value - 40) * 0.025);
@@ -2875,65 +2863,88 @@ export class Midy {
2875
2863
  }
2876
2864
  }
2877
2865
  getPitchControl(channel, note) {
2878
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2866
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
2867
+ if (polyphonicKeyPressureRaw < 0)
2868
+ return 0;
2869
+ const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
2879
2870
  note.pressure;
2880
2871
  return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2881
2872
  }
2882
2873
  getFilterCutoffControl(channel, note) {
2883
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2884
- channel.state.channelPressure;
2885
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2886
- note.pressure;
2874
+ const channelPressureRaw = channel.channelPressureTable[1];
2875
+ const channelPressure = (0 <= channelPressureRaw)
2876
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2877
+ : 0;
2878
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[1];
2879
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2880
+ ? (polyphonicKeyPressureRaw - 64) * note.pressure
2881
+ : 0;
2887
2882
  return (channelPressure + polyphonicKeyPressure) * 15;
2888
2883
  }
2889
2884
  getAmplitudeControl(channel, note) {
2890
- const channelPressure = channel.channelPressureTable[2] *
2891
- channel.state.channelPressure;
2892
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2893
- note.pressure;
2885
+ const channelPressureRaw = channel.channelPressureTable[2];
2886
+ const channelPressure = (0 <= channelPressureRaw)
2887
+ ? channelPressureRaw * channel.state.channelPressure
2888
+ : 0;
2889
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
2890
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2891
+ ? polyphonicKeyPressureRaw * note.pressure
2892
+ : 0;
2894
2893
  return (channelPressure + polyphonicKeyPressure) / 128;
2895
2894
  }
2896
2895
  getLFOPitchDepth(channel, note) {
2897
- const channelPressure = channel.channelPressureTable[3] *
2898
- channel.state.channelPressure;
2899
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2900
- note.pressure;
2896
+ const channelPressureRaw = channel.channelPressureTable[3];
2897
+ const channelPressure = (0 <= channelPressureRaw)
2898
+ ? channelPressureRaw * channel.state.channelPressure
2899
+ : 0;
2900
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
2901
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2902
+ ? polyphonicKeyPressureRaw * note.pressure
2903
+ : 0;
2901
2904
  return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2902
2905
  }
2903
2906
  getLFOFilterDepth(channel, note) {
2904
- const channelPressure = channel.channelPressureTable[4] *
2905
- channel.state.channelPressure;
2906
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2907
- note.pressure;
2907
+ const channelPressureRaw = channel.channelPressureTable[4];
2908
+ const channelPressure = (0 <= channelPressureRaw)
2909
+ ? channelPressureRaw * channel.state.channelPressure
2910
+ : 0;
2911
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
2912
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2913
+ ? polyphonicKeyPressureRaw * note.pressure
2914
+ : 0;
2908
2915
  return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2909
2916
  }
2910
2917
  getLFOAmplitudeDepth(channel, note) {
2911
- const channelPressure = channel.channelPressureTable[5] *
2912
- channel.state.channelPressure;
2913
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2914
- note.pressure;
2918
+ const channelPressureRaw = channel.channelPressureTable[5];
2919
+ const channelPressure = (0 <= channelPressureRaw)
2920
+ ? channelPressureRaw * channel.state.channelPressure
2921
+ : 0;
2922
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[5];
2923
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2924
+ ? polyphonicKeyPressureRaw * note.pressure
2925
+ : 0;
2915
2926
  return (channelPressure + polyphonicKeyPressure) / 254;
2916
2927
  }
2917
2928
  setControllerParameters(channel, note, table) {
2918
- if (table[0] !== 64)
2929
+ if (0 <= table[0])
2919
2930
  this.updateDetune(channel, note);
2920
2931
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2921
- if (table[1] !== 64)
2932
+ if (0 <= table[1])
2922
2933
  this.setPortamentoFilterEnvelope(channel, note);
2923
- if (table[2] !== 64)
2934
+ if (0 <= table[2])
2924
2935
  this.setPortamentoVolumeEnvelope(channel, note);
2925
2936
  }
2926
2937
  else {
2927
- if (table[1] !== 64)
2938
+ if (0 <= table[1])
2928
2939
  this.setFilterEnvelope(channel, note);
2929
- if (table[2] !== 64)
2940
+ if (0 <= table[2])
2930
2941
  this.setVolumeEnvelope(channel, note);
2931
2942
  }
2932
- if (table[3] !== 0)
2943
+ if (0 <= table[3])
2933
2944
  this.setModLfoToPitch(channel, note);
2934
- if (table[4] !== 0)
2945
+ if (0 <= table[4])
2935
2946
  this.setModLfoToFilterFc(channel, note);
2936
- if (table[5] !== 0)
2947
+ if (0 <= table[5])
2937
2948
  this.setModLfoToVolume(channel, note);
2938
2949
  }
2939
2950
  handlePressureSysEx(data, tableName) {
@@ -2949,26 +2960,15 @@ export class Midy {
2949
2960
  }
2950
2961
  }
2951
2962
  initControlTable() {
2952
- const channelCount = 128;
2963
+ const ccCount = 128;
2953
2964
  const slotSize = 6;
2954
- const table = new Uint8Array(channelCount * slotSize);
2955
- return this.resetControlTable(table);
2965
+ return new Int8Array(ccCount * slotSize).fill(-1);
2956
2966
  }
2957
- resetControlTable(table) {
2958
- const channelCount = 128;
2959
- const slotSize = 6;
2960
- const defaultValues = [64, 64, 64, 0, 0, 0];
2961
- for (let ch = 0; ch < channelCount; ch++) {
2962
- const offset = ch * slotSize;
2963
- table.set(defaultValues, offset);
2964
- }
2965
- return table;
2966
- }
2967
- applyControlTable(channel, controllerType) {
2967
+ applyControlTable(channel, controllerType, scheduleTime) {
2968
2968
  const slotSize = 6;
2969
2969
  const offset = controllerType * slotSize;
2970
2970
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2971
- this.processScheduledNotes(channel, (note) => {
2971
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2972
2972
  this.setControllerParameters(channel, note, table);
2973
2973
  });
2974
2974
  }
@@ -2993,7 +2993,7 @@ export class Midy {
2993
2993
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2994
2994
  const channelNumber = data[4];
2995
2995
  const channel = this.channels[channelNumber];
2996
- if (channel.isDrum)
2996
+ if (!channel.isDrum)
2997
2997
  return;
2998
2998
  const keyNumber = data[5];
2999
2999
  const table = channel.keyBasedInstrumentControlTable;