@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-GM2.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,
@@ -199,6 +181,16 @@ class ControllerState {
199
181
  }
200
182
  }
201
183
  }
184
+ const volumeEnvelopeKeys = [
185
+ "volDelay",
186
+ "volAttack",
187
+ "volHold",
188
+ "volDecay",
189
+ "volSustain",
190
+ "volRelease",
191
+ "initialAttenuation",
192
+ ];
193
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
202
194
  const filterEnvelopeKeys = [
203
195
  "modEnvToPitch",
204
196
  "initialFilterFc",
@@ -208,22 +200,20 @@ const filterEnvelopeKeys = [
208
200
  "modHold",
209
201
  "modDecay",
210
202
  "modSustain",
211
- "modRelease",
212
- "playbackRate",
213
203
  ];
214
204
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
215
- const volumeEnvelopeKeys = [
216
- "volDelay",
217
- "volAttack",
218
- "volHold",
219
- "volDecay",
220
- "volSustain",
221
- "volRelease",
222
- "initialAttenuation",
205
+ const pitchEnvelopeKeys = [
206
+ "modEnvToPitch",
207
+ "modDelay",
208
+ "modAttack",
209
+ "modHold",
210
+ "modDecay",
211
+ "modSustain",
212
+ "playbackRate",
223
213
  ];
224
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
214
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
225
215
  export class MidyGM2 {
226
- constructor(audioContext, options = this.defaultOptions) {
216
+ constructor(audioContext) {
227
217
  Object.defineProperty(this, "mode", {
228
218
  enumerable: true,
229
219
  configurable: true,
@@ -247,6 +237,7 @@ export class MidyGM2 {
247
237
  configurable: true,
248
238
  writable: true,
249
239
  value: {
240
+ algorithm: "SchroederReverb",
250
241
  time: this.getReverbTime(64),
251
242
  feedback: 0.8,
252
243
  }
@@ -395,30 +386,7 @@ export class MidyGM2 {
395
386
  writable: true,
396
387
  value: new Array(this.numChannels * drumExclusiveClassCount)
397
388
  });
398
- Object.defineProperty(this, "defaultOptions", {
399
- enumerable: true,
400
- configurable: true,
401
- writable: true,
402
- value: {
403
- reverbAlgorithm: (audioContext) => {
404
- const { time: rt60, feedback } = this.reverb;
405
- // const delay = this.calcDelay(rt60, feedback);
406
- // const impulse = this.createConvolutionReverbImpulse(
407
- // audioContext,
408
- // rt60,
409
- // delay,
410
- // );
411
- // return this.createConvolutionReverb(audioContext, impulse);
412
- const combFeedbacks = this.generateDistributedArray(feedback, 4);
413
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
414
- const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
415
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
416
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
417
- },
418
- }
419
- });
420
389
  this.audioContext = audioContext;
421
- this.options = { ...this.defaultOptions, ...options };
422
390
  this.masterVolume = new GainNode(audioContext);
423
391
  this.scheduler = new GainNode(audioContext, { gain: 0 });
424
392
  this.schedulerBuffer = new AudioBuffer({
@@ -428,7 +396,7 @@ export class MidyGM2 {
428
396
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
429
397
  this.controlChangeHandlers = this.createControlChangeHandlers();
430
398
  this.channels = this.createChannels(audioContext);
431
- this.reverbEffect = this.options.reverbAlgorithm(audioContext);
399
+ this.reverbEffect = this.createReverbEffect(audioContext);
432
400
  this.chorusEffect = this.createChorusEffect(audioContext);
433
401
  this.chorusEffect.output.connect(this.masterVolume);
434
402
  this.reverbEffect.output.connect(this.masterVolume);
@@ -492,7 +460,7 @@ export class MidyGM2 {
492
460
  this.timeline = midiData.timeline;
493
461
  this.totalTime = this.calcTotalTime();
494
462
  }
495
- setChannelAudioNodes(audioContext) {
463
+ createChannelAudioNodes(audioContext) {
496
464
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
497
465
  const gainL = new GainNode(audioContext, { gain: gainLeft });
498
466
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -507,10 +475,9 @@ export class MidyGM2 {
507
475
  };
508
476
  }
509
477
  resetChannelTable(channel) {
510
- this.resetControlTable(channel.controlTable);
478
+ channel.controlTable.fill(-1);
511
479
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
512
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
513
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
480
+ channel.channelPressureTable.fill(-1);
514
481
  channel.keyBasedInstrumentControlTable.fill(-1);
515
482
  }
516
483
  createChannels(audioContext) {
@@ -520,14 +487,16 @@ export class MidyGM2 {
520
487
  isDrum: false,
521
488
  state: new ControllerState(),
522
489
  ...this.constructor.channelSettings,
523
- ...this.setChannelAudioNodes(audioContext),
490
+ ...this.createChannelAudioNodes(audioContext),
524
491
  scheduledNotes: [],
525
492
  sustainNotes: [],
526
493
  sostenutoNotes: [],
527
494
  controlTable: this.initControlTable(),
528
495
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
529
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
496
+ channelPressureTable: new Int8Array(6).fill(-1),
530
497
  keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
498
+ keyBasedGainLs: new Array(128),
499
+ keyBasedGainRs: new Array(128),
531
500
  };
532
501
  });
533
502
  return channels;
@@ -569,10 +538,9 @@ export class MidyGM2 {
569
538
  createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
570
539
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
571
540
  bufferSource.buffer = audioBuffer;
572
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
573
- if (channel.isDrum) {
574
- bufferSource.loop = this.isLoopDrum(channel, noteNumber);
575
- }
541
+ bufferSource.loop = channel.isDrum
542
+ ? this.isLoopDrum(channel, noteNumber)
543
+ : (voiceParams.sampleModes % 2 !== 0);
576
544
  if (bufferSource.loop) {
577
545
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
578
546
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -813,7 +781,7 @@ export class MidyGM2 {
813
781
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
814
782
  const channel = this.channels[channelNumber];
815
783
  const promises = [];
816
- this.processScheduledNotes(channel, (note) => {
784
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
817
785
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
818
786
  this.notePromises.push(promise);
819
787
  promises.push(promise);
@@ -872,7 +840,7 @@ export class MidyGM2 {
872
840
  const now = this.audioContext.currentTime;
873
841
  return this.resumeTime + now - this.startTime - this.startDelay;
874
842
  }
875
- processScheduledNotes(channel, callback) {
843
+ processScheduledNotes(channel, scheduleTime, callback) {
876
844
  const scheduledNotes = channel.scheduledNotes;
877
845
  for (let i = 0; i < scheduledNotes.length; i++) {
878
846
  const note = scheduledNotes[i];
@@ -880,6 +848,8 @@ export class MidyGM2 {
880
848
  continue;
881
849
  if (note.ending)
882
850
  continue;
851
+ if (note.startTime < scheduleTime)
852
+ continue;
883
853
  callback(note);
884
854
  }
885
855
  }
@@ -892,7 +862,7 @@ export class MidyGM2 {
892
862
  if (note.ending)
893
863
  continue;
894
864
  if (scheduleTime < note.startTime)
895
- continue;
865
+ break;
896
866
  callback(note);
897
867
  }
898
868
  }
@@ -981,6 +951,22 @@ export class MidyGM2 {
981
951
  const output = allpasses.at(-1);
982
952
  return { input, output };
983
953
  }
954
+ createReverbEffect(audioContext) {
955
+ const { algorithm, time: rt60, feedback } = this.reverb;
956
+ switch (algorithm) {
957
+ case "ConvolutionReverb": {
958
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
959
+ return this.createConvolutionReverb(audioContext, impulse);
960
+ }
961
+ case "SchroederReverb": {
962
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
963
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
964
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
965
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
966
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
967
+ }
968
+ }
969
+ }
984
970
  createChorusEffect(audioContext) {
985
971
  const input = new GainNode(audioContext);
986
972
  const output = new GainNode(audioContext);
@@ -1045,15 +1031,22 @@ export class MidyGM2 {
1045
1031
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1046
1032
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1047
1033
  const pitch = pitchWheel * pitchWheelSensitivity;
1048
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1049
- const pressure = pressureDepth * channel.state.channelPressure;
1050
- return tuning + pitch + pressure;
1034
+ const channelPressureRaw = channel.channelPressureTable[0];
1035
+ if (0 <= channelPressureRaw) {
1036
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1037
+ const channelPressure = channelPressureDepth *
1038
+ channel.state.channelPressure;
1039
+ return tuning + pitch + channelPressure;
1040
+ }
1041
+ else {
1042
+ return tuning + pitch;
1043
+ }
1051
1044
  }
1052
1045
  calcNoteDetune(channel, note) {
1053
1046
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1054
1047
  }
1055
1048
  updateChannelDetune(channel, scheduleTime) {
1056
- this.processScheduledNotes(channel, (note) => {
1049
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1057
1050
  this.updateDetune(channel, note, scheduleTime);
1058
1051
  });
1059
1052
  }
@@ -1300,9 +1293,6 @@ export class MidyGM2 {
1300
1293
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1301
1294
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1302
1295
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1303
- note.volumeNode = new GainNode(this.audioContext);
1304
- note.gainL = new GainNode(this.audioContext);
1305
- note.gainR = new GainNode(this.audioContext);
1306
1296
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1307
1297
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1308
1298
  type: "lowpass",
@@ -1335,9 +1325,6 @@ export class MidyGM2 {
1335
1325
  }
1336
1326
  note.bufferSource.connect(note.filterNode);
1337
1327
  note.filterNode.connect(note.volumeEnvelopeNode);
1338
- note.volumeEnvelopeNode.connect(note.volumeNode);
1339
- note.volumeNode.connect(note.gainL);
1340
- note.volumeNode.connect(note.gainR);
1341
1328
  if (0 < state.chorusSendLevel) {
1342
1329
  this.setChorusEffectsSend(channel, note, 0, now);
1343
1330
  }
@@ -1396,14 +1383,7 @@ export class MidyGM2 {
1396
1383
  }
1397
1384
  this.drumExclusiveClassNotes[index] = note;
1398
1385
  }
1399
- isDrumNoteOffException(channel, noteNumber) {
1400
- if (!channel.isDrum)
1401
- return false;
1402
- const programNumber = channel.programNumber;
1403
- return !((programNumber === 48 && noteNumber === 88) ||
1404
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1405
- }
1406
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1386
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1407
1387
  const channel = this.channels[channelNumber];
1408
1388
  const bankNumber = this.calcBank(channel, channelNumber);
1409
1389
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1415,9 +1395,18 @@ export class MidyGM2 {
1415
1395
  return;
1416
1396
  const isSF3 = soundFont.parsed.info.version.major === 3;
1417
1397
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1418
- note.noteOffEvent = noteOffEvent;
1419
- note.gainL.connect(channel.gainL);
1420
- note.gainR.connect(channel.gainR);
1398
+ if (channel.isDrum) {
1399
+ const audioContext = this.audioContext;
1400
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1401
+ channel.keyBasedGainLs[noteNumber] = gainL;
1402
+ channel.keyBasedGainRs[noteNumber] = gainR;
1403
+ note.volumeEnvelopeNode.connect(gainL);
1404
+ note.volumeEnvelopeNode.connect(gainR);
1405
+ }
1406
+ else {
1407
+ note.volumeEnvelopeNode.connect(channel.gainL);
1408
+ note.volumeEnvelopeNode.connect(channel.gainR);
1409
+ }
1421
1410
  if (0.5 <= channel.state.sustainPedal) {
1422
1411
  channel.sustainNotes.push(note);
1423
1412
  }
@@ -1435,9 +1424,6 @@ export class MidyGM2 {
1435
1424
  note.bufferSource.disconnect();
1436
1425
  note.filterNode.disconnect();
1437
1426
  note.volumeEnvelopeNode.disconnect();
1438
- note.volumeNode.disconnect();
1439
- note.gainL.disconnect();
1440
- note.gainR.disconnect();
1441
1427
  if (note.modulationDepth) {
1442
1428
  note.volumeDepth.disconnect();
1443
1429
  note.modulationDepth.disconnect();
@@ -1580,9 +1566,10 @@ export class MidyGM2 {
1580
1566
  const prev = channel.state.channelPressure;
1581
1567
  const next = value / 127;
1582
1568
  channel.state.channelPressure = next;
1583
- if (channel.channelPressureTable[0] !== 64) {
1584
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1585
- channel.detune += pressureDepth * (next - prev);
1569
+ const channelPressureRaw = channel.channelPressureTable[0];
1570
+ if (0 <= channelPressureRaw) {
1571
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1572
+ channel.detune += channelPressureDepth * (next - prev);
1586
1573
  }
1587
1574
  const table = channel.channelPressureTable;
1588
1575
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1642,10 +1629,12 @@ export class MidyGM2 {
1642
1629
  .setValueAtTime(volumeDepth, scheduleTime);
1643
1630
  }
1644
1631
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1645
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1646
1632
  let value = note.voiceParams.reverbEffectsSend;
1647
- if (0 <= keyBasedValue) {
1648
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1633
+ if (channel.isDrum) {
1634
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1635
+ if (0 <= keyBasedValue) {
1636
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1637
+ }
1649
1638
  }
1650
1639
  if (0 < prevValue) {
1651
1640
  if (0 < value) {
@@ -1670,10 +1659,12 @@ export class MidyGM2 {
1670
1659
  }
1671
1660
  }
1672
1661
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1673
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1674
1662
  let value = note.voiceParams.chorusEffectsSend;
1675
- if (0 <= keyBasedValue) {
1676
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1663
+ if (channel.isDrum) {
1664
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1665
+ if (0 <= keyBasedValue) {
1666
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1667
+ }
1677
1668
  }
1678
1669
  if (0 < prevValue) {
1679
1670
  if (0 < vaule) {
@@ -1691,7 +1682,7 @@ export class MidyGM2 {
1691
1682
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1692
1683
  gain: value,
1693
1684
  });
1694
- note.volumeNode.connect(note.chorusEffectsSend);
1685
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1695
1686
  }
1696
1687
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1697
1688
  }
@@ -1775,11 +1766,12 @@ export class MidyGM2 {
1775
1766
  return state;
1776
1767
  }
1777
1768
  applyVoiceParams(channel, controllerType, scheduleTime) {
1778
- this.processScheduledNotes(channel, (note) => {
1769
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1779
1770
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1780
1771
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1781
- let appliedFilterEnvelope = false;
1782
- let appliedVolumeEnvelope = false;
1772
+ let applyVolumeEnvelope = false;
1773
+ let applyFilterEnvelope = false;
1774
+ let applyPitchEnvelope = false;
1783
1775
  for (const [key, value] of Object.entries(voiceParams)) {
1784
1776
  const prevValue = note.voiceParams[key];
1785
1777
  if (value === prevValue)
@@ -1788,37 +1780,23 @@ export class MidyGM2 {
1788
1780
  if (key in this.voiceParamsHandlers) {
1789
1781
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1790
1782
  }
1791
- else if (filterEnvelopeKeySet.has(key)) {
1792
- if (appliedFilterEnvelope)
1793
- continue;
1794
- appliedFilterEnvelope = true;
1795
- const noteVoiceParams = note.voiceParams;
1796
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1797
- const key = filterEnvelopeKeys[i];
1798
- if (key in voiceParams)
1799
- noteVoiceParams[key] = voiceParams[key];
1800
- }
1801
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1802
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1803
- }
1804
- else {
1805
- this.setFilterEnvelope(channel, note, scheduleTime);
1806
- }
1807
- this.setPitchEnvelope(note, scheduleTime);
1808
- }
1809
- else if (volumeEnvelopeKeySet.has(key)) {
1810
- if (appliedVolumeEnvelope)
1811
- continue;
1812
- appliedVolumeEnvelope = true;
1813
- const noteVoiceParams = note.voiceParams;
1814
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1815
- const key = volumeEnvelopeKeys[i];
1816
- if (key in voiceParams)
1817
- noteVoiceParams[key] = voiceParams[key];
1818
- }
1819
- this.setVolumeEnvelope(channel, note, scheduleTime);
1783
+ else {
1784
+ if (volumeEnvelopeKeySet.has(key))
1785
+ applyVolumeEnvelope = true;
1786
+ if (filterEnvelopeKeySet.has(key))
1787
+ applyFilterEnvelope = true;
1788
+ if (pitchEnvelopeKeySet.has(key))
1789
+ applyPitchEnvelope = true;
1820
1790
  }
1821
1791
  }
1792
+ if (applyVolumeEnvelope) {
1793
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1794
+ }
1795
+ if (applyFilterEnvelope) {
1796
+ this.setFilterEnvelope(channel, note, scheduleTime);
1797
+ }
1798
+ if (applyPitchEnvelope)
1799
+ this.setPitchEnvelope(note, scheduleTime);
1822
1800
  });
1823
1801
  }
1824
1802
  createControlChangeHandlers() {
@@ -1855,7 +1833,7 @@ export class MidyGM2 {
1855
1833
  handler.call(this, channelNumber, value, scheduleTime);
1856
1834
  const channel = this.channels[channelNumber];
1857
1835
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1858
- this.applyControlTable(channel, controllerType);
1836
+ this.applyControlTable(channel, controllerType, scheduleTime);
1859
1837
  }
1860
1838
  else {
1861
1839
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1866,7 +1844,7 @@ export class MidyGM2 {
1866
1844
  }
1867
1845
  updateModulation(channel, scheduleTime) {
1868
1846
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1869
- this.processScheduledNotes(channel, (note) => {
1847
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1870
1848
  if (note.modulationDepth) {
1871
1849
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1872
1850
  }
@@ -1885,7 +1863,7 @@ export class MidyGM2 {
1885
1863
  this.updateModulation(channel, scheduleTime);
1886
1864
  }
1887
1865
  updatePortamento(channel, scheduleTime) {
1888
- this.processScheduledNotes(channel, (note) => {
1866
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1889
1867
  if (0.5 <= channel.state.portamento) {
1890
1868
  if (0 <= note.portamentoNoteNumber) {
1891
1869
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1912,22 +1890,12 @@ export class MidyGM2 {
1912
1890
  return;
1913
1891
  this.updatePortamento(channel, scheduleTime);
1914
1892
  }
1915
- setKeyBasedVolume(channel, scheduleTime) {
1916
- this.processScheduledNotes(channel, (note) => {
1917
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1918
- if (0 <= keyBasedValue) {
1919
- note.volumeNode.gain
1920
- .cancelScheduledValues(scheduleTime)
1921
- .setValueAtTime(keyBasedValue / 127, scheduleTime);
1922
- }
1923
- });
1924
- }
1925
1893
  setVolume(channelNumber, volume, scheduleTime) {
1926
1894
  scheduleTime ??= this.audioContext.currentTime;
1927
1895
  const channel = this.channels[channelNumber];
1928
1896
  channel.state.volume = volume / 127;
1929
1897
  this.updateChannelVolume(channel, scheduleTime);
1930
- this.setKeyBasedVolume(channel, scheduleTime);
1898
+ this.updateKeyBasedVolume(channel, scheduleTime);
1931
1899
  }
1932
1900
  panToGain(pan) {
1933
1901
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1936,26 +1904,12 @@ export class MidyGM2 {
1936
1904
  gainRight: Math.sin(theta),
1937
1905
  };
1938
1906
  }
1939
- setKeyBasedPan(channel, scheduleTime) {
1940
- this.processScheduledNotes(channel, (note) => {
1941
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1942
- if (0 <= keyBasedValue) {
1943
- const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1944
- note.gainL.gain
1945
- .cancelScheduledValues(scheduleTime)
1946
- .setValueAtTime(gainLeft, scheduleTime);
1947
- note.gainR.gain
1948
- .cancelScheduledValues(scheduleTime)
1949
- .setValueAtTime(gainRight, scheduleTime);
1950
- }
1951
- });
1952
- }
1953
1907
  setPan(channelNumber, pan, scheduleTime) {
1954
1908
  scheduleTime ??= this.audioContext.currentTime;
1955
1909
  const channel = this.channels[channelNumber];
1956
1910
  channel.state.pan = pan / 127;
1957
1911
  this.updateChannelVolume(channel, scheduleTime);
1958
- this.setKeyBasedPan(channel, scheduleTime);
1912
+ this.updateKeyBasedVolume(channel, scheduleTime);
1959
1913
  }
1960
1914
  setExpression(channelNumber, expression, scheduleTime) {
1961
1915
  scheduleTime ??= this.audioContext.currentTime;
@@ -1981,6 +1935,34 @@ export class MidyGM2 {
1981
1935
  .cancelScheduledValues(scheduleTime)
1982
1936
  .setValueAtTime(volume * gainRight, scheduleTime);
1983
1937
  }
1938
+ updateKeyBasedVolume(channel, scheduleTime) {
1939
+ if (!channel.isDrum)
1940
+ return;
1941
+ const state = channel.state;
1942
+ const defaultVolume = state.volume * state.expression;
1943
+ const defaultPan = state.pan;
1944
+ for (let i = 0; i < 128; i++) {
1945
+ const gainL = channel.keyBasedGainLs[i];
1946
+ const gainR = channel.keyBasedGainLs[i];
1947
+ if (!gainL)
1948
+ continue;
1949
+ if (!gainR)
1950
+ continue;
1951
+ const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
1952
+ const volume = (0 <= keyBasedVolume)
1953
+ ? defaultVolume * keyBasedVolume / 64
1954
+ : defaultVolume;
1955
+ const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
1956
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
1957
+ const { gainLeft, gainRight } = this.panToGain(pan);
1958
+ gainL.gain
1959
+ .cancelScheduledValues(scheduleTime)
1960
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1961
+ gainR.gain
1962
+ .cancelScheduledValues(scheduleTime)
1963
+ .setValueAtTime(volume * gainRight, scheduleTime);
1964
+ }
1965
+ }
1984
1966
  setSustainPedal(channelNumber, value, scheduleTime) {
1985
1967
  const channel = this.channels[channelNumber];
1986
1968
  if (channel.isDrum)
@@ -1988,7 +1970,7 @@ export class MidyGM2 {
1988
1970
  scheduleTime ??= this.audioContext.currentTime;
1989
1971
  channel.state.sustainPedal = value / 127;
1990
1972
  if (64 <= value) {
1991
- this.processScheduledNotes(channel, (note) => {
1973
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1992
1974
  channel.sustainNotes.push(note);
1993
1975
  });
1994
1976
  }
@@ -2031,7 +2013,7 @@ export class MidyGM2 {
2031
2013
  const state = channel.state;
2032
2014
  scheduleTime ??= this.audioContext.currentTime;
2033
2015
  state.softPedal = softPedal / 127;
2034
- this.processScheduledNotes(channel, (note) => {
2016
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2035
2017
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2036
2018
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2037
2019
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2055,7 +2037,7 @@ export class MidyGM2 {
2055
2037
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2056
2038
  }
2057
2039
  else {
2058
- this.processScheduledNotes(channel, (note) => {
2040
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2059
2041
  if (note.voiceParams.reverbEffectsSend <= 0)
2060
2042
  return false;
2061
2043
  if (note.reverbEffectsSend)
@@ -2065,7 +2047,7 @@ export class MidyGM2 {
2065
2047
  }
2066
2048
  else {
2067
2049
  if (0 < reverbSendLevel) {
2068
- this.processScheduledNotes(channel, (note) => {
2050
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2069
2051
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2070
2052
  });
2071
2053
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2088,7 +2070,7 @@ export class MidyGM2 {
2088
2070
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2089
2071
  }
2090
2072
  else {
2091
- this.processScheduledNotes(channel, (note) => {
2073
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2092
2074
  if (note.voiceParams.chorusEffectsSend <= 0)
2093
2075
  return false;
2094
2076
  if (note.chorusEffectsSend)
@@ -2098,7 +2080,7 @@ export class MidyGM2 {
2098
2080
  }
2099
2081
  else {
2100
2082
  if (0 < chorusSendLevel) {
2101
- this.processScheduledNotes(channel, (note) => {
2083
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2102
2084
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2103
2085
  });
2104
2086
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2482,8 +2464,7 @@ export class MidyGM2 {
2482
2464
  setReverbType(type) {
2483
2465
  this.reverb.time = this.getReverbTimeFromType(type);
2484
2466
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2485
- const { audioContext, options } = this;
2486
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2467
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2487
2468
  }
2488
2469
  getReverbTimeFromType(type) {
2489
2470
  switch (type) {
@@ -2505,8 +2486,7 @@ export class MidyGM2 {
2505
2486
  }
2506
2487
  setReverbTime(value) {
2507
2488
  this.reverb.time = this.getReverbTime(value);
2508
- const { audioContext, options } = this;
2509
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2489
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2510
2490
  }
2511
2491
  getReverbTime(value) {
2512
2492
  return Math.exp((value - 40) * 0.025);
@@ -2677,50 +2657,60 @@ export class MidyGM2 {
2677
2657
  }
2678
2658
  }
2679
2659
  getFilterCutoffControl(channel) {
2680
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2681
- channel.state.channelPressure;
2660
+ const channelPressureRaw = channel.channelPressureTable[1];
2661
+ const channelPressure = (0 <= channelPressureRaw)
2662
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2663
+ : 0;
2682
2664
  return channelPressure * 15;
2683
2665
  }
2684
2666
  getAmplitudeControl(channel) {
2685
- const channelPressure = channel.channelPressureTable[2] *
2686
- channel.state.channelPressure;
2667
+ const channelPressureRaw = channel.channelPressureTable[2];
2668
+ const channelPressure = (0 <= channelPressureRaw)
2669
+ ? channelPressureRaw * channel.state.channelPressure
2670
+ : 0;
2687
2671
  return channelPressure / 64;
2688
2672
  }
2689
2673
  getLFOPitchDepth(channel) {
2690
- const channelPressure = channel.channelPressureTable[3] *
2691
- channel.state.channelPressure;
2674
+ const channelPressureRaw = channel.channelPressureTable[3];
2675
+ const channelPressure = (0 <= channelPressureRaw)
2676
+ ? channelPressureRaw * channel.state.channelPressure
2677
+ : 0;
2692
2678
  return channelPressure / 127 * 600;
2693
2679
  }
2694
2680
  getLFOFilterDepth(channel) {
2695
- const channelPressure = channel.channelPressureTable[4] *
2696
- channel.state.channelPressure;
2681
+ const channelPressureRaw = channel.channelPressureTable[4];
2682
+ const channelPressure = (0 <= channelPressureRaw)
2683
+ ? channelPressureRaw * channel.state.channelPressure
2684
+ : 0;
2697
2685
  return channelPressure / 127 * 2400;
2698
2686
  }
2699
2687
  getLFOAmplitudeDepth(channel) {
2700
- const channelPressure = channel.channelPressureTable[5] *
2701
- channel.state.channelPressure;
2688
+ const channelPressureRaw = channel.channelPressureTable[5];
2689
+ const channelPressure = (0 <= channelPressureRaw)
2690
+ ? channelPressureRaw * channel.state.channelPressure
2691
+ : 0;
2702
2692
  return channelPressure / 127;
2703
2693
  }
2704
2694
  setControllerParameters(channel, note, table) {
2705
- if (table[0] !== 64)
2695
+ if (0 <= table[0])
2706
2696
  this.updateDetune(channel, note);
2707
2697
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2708
- if (table[1] !== 64)
2698
+ if (0 <= table[1])
2709
2699
  this.setPortamentoFilterEnvelope(channel, note);
2710
- if (table[2] !== 64)
2700
+ if (0 <= table[2])
2711
2701
  this.setPortamentoVolumeEnvelope(channel, note);
2712
2702
  }
2713
2703
  else {
2714
- if (table[1] !== 64)
2704
+ if (0 <= table[1])
2715
2705
  this.setFilterEnvelope(channel, note);
2716
- if (table[2] !== 64)
2706
+ if (0 <= table[2])
2717
2707
  this.setVolumeEnvelope(channel, note);
2718
2708
  }
2719
- if (table[3] !== 0)
2709
+ if (0 <= table[3])
2720
2710
  this.setModLfoToPitch(channel, note);
2721
- if (table[4] !== 0)
2711
+ if (0 <= table[4])
2722
2712
  this.setModLfoToFilterFc(channel, note);
2723
- if (table[5] !== 0)
2713
+ if (0 <= table[5])
2724
2714
  this.setModLfoToVolume(channel, note);
2725
2715
  }
2726
2716
  handlePressureSysEx(data, tableName) {
@@ -2736,26 +2726,15 @@ export class MidyGM2 {
2736
2726
  }
2737
2727
  }
2738
2728
  initControlTable() {
2739
- const channelCount = 128;
2729
+ const ccCount = 128;
2740
2730
  const slotSize = 6;
2741
- const table = new Uint8Array(channelCount * slotSize);
2742
- return this.resetControlTable(table);
2731
+ return new Int8Array(ccCount * slotSize).fill(-1);
2743
2732
  }
2744
- resetControlTable(table) {
2745
- const channelCount = 128;
2746
- const slotSize = 6;
2747
- const defaultValues = [64, 64, 64, 0, 0, 0];
2748
- for (let ch = 0; ch < channelCount; ch++) {
2749
- const offset = ch * slotSize;
2750
- table.set(defaultValues, offset);
2751
- }
2752
- return table;
2753
- }
2754
- applyControlTable(channel, controllerType) {
2733
+ applyControlTable(channel, controllerType, scheduleTime) {
2755
2734
  const slotSize = 6;
2756
2735
  const offset = controllerType * slotSize;
2757
2736
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2758
- this.processScheduledNotes(channel, (note) => {
2737
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2759
2738
  this.setControllerParameters(channel, note, table);
2760
2739
  });
2761
2740
  }
@@ -2780,7 +2759,7 @@ export class MidyGM2 {
2780
2759
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2781
2760
  const channelNumber = data[4];
2782
2761
  const channel = this.channels[channelNumber];
2783
- if (channel.isDrum)
2762
+ if (!channel.isDrum)
2784
2763
  return;
2785
2764
  const keyNumber = data[5];
2786
2765
  const table = channel.keyBasedInstrumentControlTable;