@marmooo/midy 0.3.2 → 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
@@ -8,11 +8,11 @@ class Note {
8
8
  writable: true,
9
9
  value: -1
10
10
  });
11
- Object.defineProperty(this, "noteOffEvent", {
11
+ Object.defineProperty(this, "ending", {
12
12
  enumerable: true,
13
13
  configurable: true,
14
14
  writable: true,
15
- value: void 0
15
+ value: false
16
16
  });
17
17
  Object.defineProperty(this, "bufferSource", {
18
18
  enumerable: true,
@@ -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);
@@ -455,24 +423,44 @@ export class MidyGM2 {
455
423
  }
456
424
  }
457
425
  }
458
- async loadSoundFont(soundFontUrl) {
459
- const response = await fetch(soundFontUrl);
460
- const arrayBuffer = await response.arrayBuffer();
461
- const parsed = parse(new Uint8Array(arrayBuffer));
426
+ async loadSoundFont(input) {
427
+ let uint8Array;
428
+ if (typeof input === "string") {
429
+ const response = await fetch(input);
430
+ const arrayBuffer = await response.arrayBuffer();
431
+ uint8Array = new Uint8Array(arrayBuffer);
432
+ }
433
+ else if (input instanceof Uint8Array) {
434
+ uint8Array = input;
435
+ }
436
+ else {
437
+ throw new TypeError("input must be a URL string or Uint8Array");
438
+ }
439
+ const parsed = parse(uint8Array);
462
440
  const soundFont = new SoundFont(parsed);
463
441
  this.addSoundFont(soundFont);
464
442
  }
465
- async loadMIDI(midiUrl) {
466
- const response = await fetch(midiUrl);
467
- const arrayBuffer = await response.arrayBuffer();
468
- const midi = parseMidi(new Uint8Array(arrayBuffer));
443
+ async loadMIDI(input) {
444
+ let uint8Array;
445
+ if (typeof input === "string") {
446
+ const response = await fetch(input);
447
+ const arrayBuffer = await response.arrayBuffer();
448
+ uint8Array = new Uint8Array(arrayBuffer);
449
+ }
450
+ else if (input instanceof Uint8Array) {
451
+ uint8Array = input;
452
+ }
453
+ else {
454
+ throw new TypeError("input must be a URL string or Uint8Array");
455
+ }
456
+ const midi = parseMidi(uint8Array);
469
457
  this.ticksPerBeat = midi.header.ticksPerBeat;
470
458
  const midiData = this.extractMidiData(midi);
471
459
  this.instruments = midiData.instruments;
472
460
  this.timeline = midiData.timeline;
473
461
  this.totalTime = this.calcTotalTime();
474
462
  }
475
- setChannelAudioNodes(audioContext) {
463
+ createChannelAudioNodes(audioContext) {
476
464
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
477
465
  const gainL = new GainNode(audioContext, { gain: gainLeft });
478
466
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -487,10 +475,10 @@ export class MidyGM2 {
487
475
  };
488
476
  }
489
477
  resetChannelTable(channel) {
490
- this.resetControlTable(channel.controlTable);
478
+ channel.controlTable.fill(-1);
491
479
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
492
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
493
- channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
480
+ channel.channelPressureTable.fill(-1);
481
+ channel.keyBasedInstrumentControlTable.fill(-1);
494
482
  }
495
483
  createChannels(audioContext) {
496
484
  const channels = Array.from({ length: this.numChannels }, () => {
@@ -499,14 +487,16 @@ export class MidyGM2 {
499
487
  isDrum: false,
500
488
  state: new ControllerState(),
501
489
  ...this.constructor.channelSettings,
502
- ...this.setChannelAudioNodes(audioContext),
490
+ ...this.createChannelAudioNodes(audioContext),
503
491
  scheduledNotes: [],
504
492
  sustainNotes: [],
505
493
  sostenutoNotes: [],
506
494
  controlTable: this.initControlTable(),
507
495
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
508
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
509
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
496
+ channelPressureTable: new Int8Array(6).fill(-1),
497
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
498
+ keyBasedGainLs: new Array(128),
499
+ keyBasedGainRs: new Array(128),
510
500
  };
511
501
  });
512
502
  return channels;
@@ -540,10 +530,17 @@ export class MidyGM2 {
540
530
  return audioBuffer;
541
531
  }
542
532
  }
543
- createBufferSource(voiceParams, audioBuffer) {
533
+ isLoopDrum(channel, noteNumber) {
534
+ const programNumber = channel.programNumber;
535
+ return ((programNumber === 48 && noteNumber === 88) ||
536
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
537
+ }
538
+ createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
544
539
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
545
540
  bufferSource.buffer = audioBuffer;
546
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
541
+ bufferSource.loop = channel.isDrum
542
+ ? this.isLoopDrum(channel, noteNumber)
543
+ : (voiceParams.sampleModes % 2 !== 0);
547
544
  if (bufferSource.loop) {
548
545
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
549
546
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -558,12 +555,13 @@ export class MidyGM2 {
558
555
  const delay = this.startDelay - resumeTime;
559
556
  const startTime = event.startTime + delay;
560
557
  switch (event.type) {
561
- case "noteOn": {
562
- const noteOffEvent = {
563
- ...event.noteOffEvent,
564
- startTime: event.noteOffEvent.startTime + delay,
565
- };
566
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
558
+ case "noteOn":
559
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
560
+ break;
561
+ case "noteOff": {
562
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
563
+ if (notePromise)
564
+ this.notePromises.push(notePromise);
567
565
  break;
568
566
  }
569
567
  case "controller":
@@ -668,6 +666,7 @@ export class MidyGM2 {
668
666
  return `${programNumber}:${noteNumber}:${velocity}`;
669
667
  }
670
668
  extractMidiData(midi) {
669
+ this.audioBufferCounter.clear();
671
670
  const instruments = new Set();
672
671
  const timeline = [];
673
672
  const tmpChannels = new Array(this.channels.length);
@@ -767,38 +766,13 @@ export class MidyGM2 {
767
766
  prevTempoTicks = event.ticks;
768
767
  }
769
768
  }
770
- const activeNotes = new Array(this.channels.length * 128);
771
- for (let i = 0; i < activeNotes.length; i++) {
772
- activeNotes[i] = [];
773
- }
774
- for (let i = 0; i < timeline.length; i++) {
775
- const event = timeline[i];
776
- switch (event.type) {
777
- case "noteOn": {
778
- const index = event.channel * 128 + event.noteNumber;
779
- activeNotes[index].push(event);
780
- break;
781
- }
782
- case "noteOff": {
783
- const index = event.channel * 128 + event.noteNumber;
784
- const noteOn = activeNotes[index].pop();
785
- if (noteOn) {
786
- noteOn.noteOffEvent = event;
787
- }
788
- else {
789
- const eventString = JSON.stringify(event, null, 2);
790
- console.warn(`noteOff without matching noteOn: ${eventString}`);
791
- }
792
- }
793
- }
794
- }
795
769
  return { instruments, timeline };
796
770
  }
797
771
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
798
772
  const channel = this.channels[channelNumber];
799
773
  const promises = [];
800
774
  this.processActiveNotes(channel, scheduleTime, (note) => {
801
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
775
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
802
776
  this.notePromises.push(promise);
803
777
  promises.push(promise);
804
778
  });
@@ -807,8 +781,8 @@ export class MidyGM2 {
807
781
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
808
782
  const channel = this.channels[channelNumber];
809
783
  const promises = [];
810
- this.processScheduledNotes(channel, (note) => {
811
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
784
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
785
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
812
786
  this.notePromises.push(promise);
813
787
  promises.push(promise);
814
788
  });
@@ -866,7 +840,7 @@ export class MidyGM2 {
866
840
  const now = this.audioContext.currentTime;
867
841
  return this.resumeTime + now - this.startTime - this.startDelay;
868
842
  }
869
- processScheduledNotes(channel, callback) {
843
+ processScheduledNotes(channel, scheduleTime, callback) {
870
844
  const scheduledNotes = channel.scheduledNotes;
871
845
  for (let i = 0; i < scheduledNotes.length; i++) {
872
846
  const note = scheduledNotes[i];
@@ -874,6 +848,8 @@ export class MidyGM2 {
874
848
  continue;
875
849
  if (note.ending)
876
850
  continue;
851
+ if (note.startTime < scheduleTime)
852
+ continue;
877
853
  callback(note);
878
854
  }
879
855
  }
@@ -885,11 +861,8 @@ export class MidyGM2 {
885
861
  continue;
886
862
  if (note.ending)
887
863
  continue;
888
- const noteOffEvent = note.noteOffEvent;
889
- if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
890
- continue;
891
864
  if (scheduleTime < note.startTime)
892
- continue;
865
+ break;
893
866
  callback(note);
894
867
  }
895
868
  }
@@ -978,6 +951,22 @@ export class MidyGM2 {
978
951
  const output = allpasses.at(-1);
979
952
  return { input, output };
980
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
+ }
981
970
  createChorusEffect(audioContext) {
982
971
  const input = new GainNode(audioContext);
983
972
  const output = new GainNode(audioContext);
@@ -1042,22 +1031,28 @@ export class MidyGM2 {
1042
1031
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1043
1032
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1044
1033
  const pitch = pitchWheel * pitchWheelSensitivity;
1045
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1046
- const pressure = pressureDepth * channel.state.channelPressure;
1047
- 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
+ }
1048
1044
  }
1049
1045
  calcNoteDetune(channel, note) {
1050
1046
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1051
1047
  }
1052
1048
  updateChannelDetune(channel, scheduleTime) {
1053
- this.processScheduledNotes(channel, (note) => {
1049
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1054
1050
  this.updateDetune(channel, note, scheduleTime);
1055
1051
  });
1056
1052
  }
1057
1053
  updateDetune(channel, note, scheduleTime) {
1058
1054
  const noteDetune = this.calcNoteDetune(channel, note);
1059
- const pitchControl = this.getPitchControl(channel, note);
1060
- const detune = channel.detune + noteDetune + pitchControl;
1055
+ const detune = channel.detune + noteDetune;
1061
1056
  if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1062
1057
  const startTime = note.startTime;
1063
1058
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
@@ -1197,10 +1192,8 @@ export class MidyGM2 {
1197
1192
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1198
1193
  }
1199
1194
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1200
- const state = channel.state;
1201
- const { voiceParams, noteNumber, startTime } = note;
1202
- const softPedalFactor = 1 -
1203
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1195
+ const { voiceParams, startTime } = note;
1196
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1204
1197
  const baseCent = voiceParams.initialFilterFc +
1205
1198
  this.getFilterCutoffControl(channel);
1206
1199
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
@@ -1218,10 +1211,8 @@ export class MidyGM2 {
1218
1211
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1219
1212
  }
1220
1213
  setFilterEnvelope(channel, note, scheduleTime) {
1221
- const state = channel.state;
1222
- const { voiceParams, noteNumber, startTime } = note;
1223
- const softPedalFactor = 1 -
1224
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1214
+ const { voiceParams, startTime } = note;
1215
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1225
1216
  const baseCent = voiceParams.initialFilterFc +
1226
1217
  this.getFilterCutoffControl(channel);
1227
1218
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
@@ -1301,10 +1292,7 @@ export class MidyGM2 {
1301
1292
  const voiceParams = voice.getAllParams(controllerState);
1302
1293
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1303
1294
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1304
- note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1305
- note.volumeNode = new GainNode(this.audioContext);
1306
- note.gainL = new GainNode(this.audioContext);
1307
- note.gainR = new GainNode(this.audioContext);
1295
+ note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1308
1296
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1309
1297
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1310
1298
  type: "lowpass",
@@ -1337,13 +1325,10 @@ export class MidyGM2 {
1337
1325
  }
1338
1326
  note.bufferSource.connect(note.filterNode);
1339
1327
  note.filterNode.connect(note.volumeEnvelopeNode);
1340
- note.volumeEnvelopeNode.connect(note.volumeNode);
1341
- note.volumeNode.connect(note.gainL);
1342
- note.volumeNode.connect(note.gainR);
1343
- if (0 < channel.chorusSendLevel) {
1328
+ if (0 < state.chorusSendLevel) {
1344
1329
  this.setChorusEffectsSend(channel, note, 0, now);
1345
1330
  }
1346
- if (0 < channel.reverbSendLevel) {
1331
+ if (0 < state.reverbSendLevel) {
1347
1332
  this.setReverbEffectsSend(channel, note, 0, now);
1348
1333
  }
1349
1334
  note.bufferSource.start(startTime);
@@ -1373,7 +1358,7 @@ export class MidyGM2 {
1373
1358
  if (prev) {
1374
1359
  const [prevNote, prevChannelNumber] = prev;
1375
1360
  if (prevNote && !prevNote.ending) {
1376
- this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1361
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1377
1362
  startTime, true);
1378
1363
  }
1379
1364
  }
@@ -1393,19 +1378,12 @@ export class MidyGM2 {
1393
1378
  channelNumber;
1394
1379
  const prevNote = this.drumExclusiveClassNotes[index];
1395
1380
  if (prevNote && !prevNote.ending) {
1396
- this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1381
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1397
1382
  startTime, true);
1398
1383
  }
1399
1384
  this.drumExclusiveClassNotes[index] = note;
1400
1385
  }
1401
- isDrumNoteOffException(channel, noteNumber) {
1402
- if (!channel.isDrum)
1403
- return false;
1404
- const programNumber = channel.programNumber;
1405
- return !((programNumber === 48 && noteNumber === 88) ||
1406
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1407
- }
1408
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1386
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1409
1387
  const channel = this.channels[channelNumber];
1410
1388
  const bankNumber = this.calcBank(channel, channelNumber);
1411
1389
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1417,9 +1395,18 @@ export class MidyGM2 {
1417
1395
  return;
1418
1396
  const isSF3 = soundFont.parsed.info.version.major === 3;
1419
1397
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1420
- note.noteOffEvent = noteOffEvent;
1421
- note.gainL.connect(channel.gainL);
1422
- 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
+ }
1423
1410
  if (0.5 <= channel.state.sustainPedal) {
1424
1411
  channel.sustainNotes.push(note);
1425
1412
  }
@@ -1428,31 +1415,6 @@ export class MidyGM2 {
1428
1415
  const scheduledNotes = channel.scheduledNotes;
1429
1416
  note.index = scheduledNotes.length;
1430
1417
  scheduledNotes.push(note);
1431
- if (this.isDrumNoteOffException(channel, noteNumber)) {
1432
- const stopTime = startTime + note.bufferSource.buffer.duration;
1433
- const promise = new Promise((resolve) => {
1434
- note.bufferSource.onended = () => {
1435
- scheduledNotes[note.index] = undefined;
1436
- this.disconnectNote(note);
1437
- resolve();
1438
- };
1439
- note.bufferSource.stop(stopTime);
1440
- });
1441
- this.notePromises.push(promise);
1442
- }
1443
- else if (noteOffEvent) {
1444
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1445
- const portamentoTime = this.getPortamentoTime(channel, note);
1446
- const portamentoEndTime = startTime + portamentoTime;
1447
- const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
1448
- Math.max(noteOffEvent.startTime, portamentoEndTime), false);
1449
- this.notePromises.push(notePromise);
1450
- }
1451
- else {
1452
- const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
1453
- this.notePromises.push(notePromise);
1454
- }
1455
- }
1456
1418
  }
1457
1419
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1458
1420
  scheduleTime ??= this.audioContext.currentTime;
@@ -1462,9 +1424,6 @@ export class MidyGM2 {
1462
1424
  note.bufferSource.disconnect();
1463
1425
  note.filterNode.disconnect();
1464
1426
  note.volumeEnvelopeNode.disconnect();
1465
- note.volumeNode.disconnect();
1466
- note.gainL.disconnect();
1467
- note.gainR.disconnect();
1468
1427
  if (note.modulationDepth) {
1469
1428
  note.volumeDepth.disconnect();
1470
1429
  note.modulationDepth.disconnect();
@@ -1481,41 +1440,47 @@ export class MidyGM2 {
1481
1440
  note.chorusEffectsSend.disconnect();
1482
1441
  }
1483
1442
  }
1484
- stopNote(channel, note, endTime, stopTime) {
1443
+ releaseNote(channel, note, endTime) {
1444
+ const volRelease = endTime + note.voiceParams.volRelease;
1445
+ const modRelease = endTime + note.voiceParams.modRelease;
1446
+ const stopTime = Math.min(volRelease, modRelease);
1447
+ note.filterNode.frequency
1448
+ .cancelScheduledValues(endTime)
1449
+ .linearRampToValueAtTime(0, modRelease);
1485
1450
  note.volumeEnvelopeNode.gain
1486
1451
  .cancelScheduledValues(endTime)
1487
- .linearRampToValueAtTime(0, stopTime);
1488
- note.ending = true;
1489
- this.scheduleTask(() => {
1490
- note.bufferSource.loop = false;
1491
- }, stopTime);
1452
+ .linearRampToValueAtTime(0, volRelease);
1492
1453
  return new Promise((resolve) => {
1493
- note.bufferSource.onended = () => {
1494
- channel.scheduledNotes[note.index] = undefined;
1454
+ this.scheduleTask(() => {
1455
+ const bufferSource = note.bufferSource;
1456
+ bufferSource.loop = false;
1457
+ bufferSource.stop(stopTime);
1495
1458
  this.disconnectNote(note);
1459
+ channel.scheduledNotes[note.index] = undefined;
1496
1460
  resolve();
1497
- };
1498
- note.bufferSource.stop(stopTime);
1461
+ }, stopTime);
1499
1462
  });
1500
1463
  }
1501
- scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1464
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1502
1465
  const channel = this.channels[channelNumber];
1503
- if (this.isDrumNoteOffException(channel, note.noteNumber))
1504
- return;
1505
1466
  const state = channel.state;
1506
1467
  if (!force) {
1507
- if (0.5 <= state.sustainPedal)
1508
- return;
1509
- if (0.5 <= channel.state.sostenutoPedal)
1510
- return;
1468
+ if (channel.isDrum) {
1469
+ if (!this.isLoopDrum(channel, noteNumber))
1470
+ return;
1471
+ }
1472
+ else {
1473
+ if (0.5 <= state.sustainPedal)
1474
+ return;
1475
+ if (0.5 <= state.sostenutoPedal)
1476
+ return;
1477
+ }
1511
1478
  }
1512
- const volRelease = endTime + note.voiceParams.volRelease;
1513
- const modRelease = endTime + note.voiceParams.modRelease;
1514
- note.filterNode.frequency
1515
- .cancelScheduledValues(endTime)
1516
- .linearRampToValueAtTime(0, modRelease);
1517
- const stopTime = Math.min(volRelease, modRelease);
1518
- return this.stopNote(channel, note, endTime, stopTime);
1479
+ const note = this.findNoteOffTarget(channel, noteNumber);
1480
+ if (!note)
1481
+ return;
1482
+ note.ending = true;
1483
+ this.releaseNote(channel, note, endTime);
1519
1484
  }
1520
1485
  findNoteOffTarget(channel, noteNumber) {
1521
1486
  const scheduledNotes = channel.scheduledNotes;
@@ -1532,16 +1497,14 @@ export class MidyGM2 {
1532
1497
  }
1533
1498
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1534
1499
  scheduleTime ??= this.audioContext.currentTime;
1535
- const channel = this.channels[channelNumber];
1536
- const note = this.findNoteOffTarget(channel, noteNumber);
1537
- return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1500
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1538
1501
  }
1539
1502
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1540
1503
  const velocity = halfVelocity * 2;
1541
1504
  const channel = this.channels[channelNumber];
1542
1505
  const promises = [];
1543
1506
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1544
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1507
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1545
1508
  promises.push(promise);
1546
1509
  }
1547
1510
  channel.sustainNotes = [];
@@ -1555,7 +1518,7 @@ export class MidyGM2 {
1555
1518
  channel.state.sostenutoPedal = 0;
1556
1519
  for (let i = 0; i < sostenutoNotes.length; i++) {
1557
1520
  const note = sostenutoNotes[i];
1558
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
1521
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1559
1522
  promises.push(promise);
1560
1523
  }
1561
1524
  channel.sostenutoNotes = [];
@@ -1603,9 +1566,10 @@ export class MidyGM2 {
1603
1566
  const prev = channel.state.channelPressure;
1604
1567
  const next = value / 127;
1605
1568
  channel.state.channelPressure = next;
1606
- if (channel.channelPressureTable[0] !== 64) {
1607
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1608
- 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);
1609
1573
  }
1610
1574
  const table = channel.channelPressureTable;
1611
1575
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1665,10 +1629,15 @@ export class MidyGM2 {
1665
1629
  .setValueAtTime(volumeDepth, scheduleTime);
1666
1630
  }
1667
1631
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1632
+ let value = note.voiceParams.reverbEffectsSend;
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
+ }
1638
+ }
1668
1639
  if (0 < prevValue) {
1669
- if (0 < note.voiceParams.reverbEffectsSend) {
1670
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1671
- const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1640
+ if (0 < value) {
1672
1641
  note.reverbEffectsSend.gain
1673
1642
  .cancelScheduledValues(scheduleTime)
1674
1643
  .setValueAtTime(value, scheduleTime);
@@ -1678,10 +1647,10 @@ export class MidyGM2 {
1678
1647
  }
1679
1648
  }
1680
1649
  else {
1681
- if (0 < note.voiceParams.reverbEffectsSend) {
1650
+ if (0 < value) {
1682
1651
  if (!note.reverbEffectsSend) {
1683
1652
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1684
- gain: note.voiceParams.reverbEffectsSend,
1653
+ gain: value,
1685
1654
  });
1686
1655
  note.volumeNode.connect(note.reverbEffectsSend);
1687
1656
  }
@@ -1690,10 +1659,15 @@ export class MidyGM2 {
1690
1659
  }
1691
1660
  }
1692
1661
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1662
+ let value = note.voiceParams.chorusEffectsSend;
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
+ }
1668
+ }
1693
1669
  if (0 < prevValue) {
1694
- if (0 < note.voiceParams.chorusEffectsSend) {
1695
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1696
- const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1670
+ if (0 < vaule) {
1697
1671
  note.chorusEffectsSend.gain
1698
1672
  .cancelScheduledValues(scheduleTime)
1699
1673
  .setValueAtTime(value, scheduleTime);
@@ -1703,12 +1677,12 @@ export class MidyGM2 {
1703
1677
  }
1704
1678
  }
1705
1679
  else {
1706
- if (0 < note.voiceParams.chorusEffectsSend) {
1680
+ if (0 < value) {
1707
1681
  if (!note.chorusEffectsSend) {
1708
1682
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1709
- gain: note.voiceParams.chorusEffectsSend,
1683
+ gain: value,
1710
1684
  });
1711
- note.volumeNode.connect(note.chorusEffectsSend);
1685
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1712
1686
  }
1713
1687
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1714
1688
  }
@@ -1792,11 +1766,12 @@ export class MidyGM2 {
1792
1766
  return state;
1793
1767
  }
1794
1768
  applyVoiceParams(channel, controllerType, scheduleTime) {
1795
- this.processScheduledNotes(channel, (note) => {
1769
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1796
1770
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1797
1771
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1798
- let appliedFilterEnvelope = false;
1799
- let appliedVolumeEnvelope = false;
1772
+ let applyVolumeEnvelope = false;
1773
+ let applyFilterEnvelope = false;
1774
+ let applyPitchEnvelope = false;
1800
1775
  for (const [key, value] of Object.entries(voiceParams)) {
1801
1776
  const prevValue = note.voiceParams[key];
1802
1777
  if (value === prevValue)
@@ -1805,37 +1780,23 @@ export class MidyGM2 {
1805
1780
  if (key in this.voiceParamsHandlers) {
1806
1781
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1807
1782
  }
1808
- else if (filterEnvelopeKeySet.has(key)) {
1809
- if (appliedFilterEnvelope)
1810
- continue;
1811
- appliedFilterEnvelope = true;
1812
- const noteVoiceParams = note.voiceParams;
1813
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1814
- const key = filterEnvelopeKeys[i];
1815
- if (key in voiceParams)
1816
- noteVoiceParams[key] = voiceParams[key];
1817
- }
1818
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1819
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1820
- }
1821
- else {
1822
- this.setFilterEnvelope(channel, note, scheduleTime);
1823
- }
1824
- this.setPitchEnvelope(note, scheduleTime);
1825
- }
1826
- else if (volumeEnvelopeKeySet.has(key)) {
1827
- if (appliedVolumeEnvelope)
1828
- continue;
1829
- appliedVolumeEnvelope = true;
1830
- const noteVoiceParams = note.voiceParams;
1831
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1832
- const key = volumeEnvelopeKeys[i];
1833
- if (key in voiceParams)
1834
- noteVoiceParams[key] = voiceParams[key];
1835
- }
1836
- 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;
1837
1790
  }
1838
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);
1839
1800
  });
1840
1801
  }
1841
1802
  createControlChangeHandlers() {
@@ -1872,7 +1833,7 @@ export class MidyGM2 {
1872
1833
  handler.call(this, channelNumber, value, scheduleTime);
1873
1834
  const channel = this.channels[channelNumber];
1874
1835
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1875
- this.applyControlTable(channel, controllerType);
1836
+ this.applyControlTable(channel, controllerType, scheduleTime);
1876
1837
  }
1877
1838
  else {
1878
1839
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1883,7 +1844,7 @@ export class MidyGM2 {
1883
1844
  }
1884
1845
  updateModulation(channel, scheduleTime) {
1885
1846
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1886
- this.processScheduledNotes(channel, (note) => {
1847
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1887
1848
  if (note.modulationDepth) {
1888
1849
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1889
1850
  }
@@ -1902,7 +1863,7 @@ export class MidyGM2 {
1902
1863
  this.updateModulation(channel, scheduleTime);
1903
1864
  }
1904
1865
  updatePortamento(channel, scheduleTime) {
1905
- this.processScheduledNotes(channel, (note) => {
1866
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1906
1867
  if (0.5 <= channel.state.portamento) {
1907
1868
  if (0 <= note.portamentoNoteNumber) {
1908
1869
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1929,22 +1890,12 @@ export class MidyGM2 {
1929
1890
  return;
1930
1891
  this.updatePortamento(channel, scheduleTime);
1931
1892
  }
1932
- setKeyBasedVolume(channel, scheduleTime) {
1933
- this.processScheduledNotes(channel, (note) => {
1934
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1935
- if (keyBasedValue !== 0) {
1936
- note.volumeNode.gain
1937
- .cancelScheduledValues(scheduleTime)
1938
- .setValueAtTime(1 + keyBasedValue, scheduleTime);
1939
- }
1940
- });
1941
- }
1942
1893
  setVolume(channelNumber, volume, scheduleTime) {
1943
1894
  scheduleTime ??= this.audioContext.currentTime;
1944
1895
  const channel = this.channels[channelNumber];
1945
1896
  channel.state.volume = volume / 127;
1946
1897
  this.updateChannelVolume(channel, scheduleTime);
1947
- this.setKeyBasedVolume(channel, scheduleTime);
1898
+ this.updateKeyBasedVolume(channel, scheduleTime);
1948
1899
  }
1949
1900
  panToGain(pan) {
1950
1901
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1953,26 +1904,12 @@ export class MidyGM2 {
1953
1904
  gainRight: Math.sin(theta),
1954
1905
  };
1955
1906
  }
1956
- setKeyBasedPan(channel, scheduleTime) {
1957
- this.processScheduledNotes(channel, (note) => {
1958
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1959
- if (keyBasedValue !== 0) {
1960
- const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1961
- note.gainL.gain
1962
- .cancelScheduledValues(scheduleTime)
1963
- .setValueAtTime(gainLeft, scheduleTime);
1964
- note.gainR.gain
1965
- .cancelScheduledValues(scheduleTime)
1966
- .setValueAtTime(gainRight, scheduleTime);
1967
- }
1968
- });
1969
- }
1970
1907
  setPan(channelNumber, pan, scheduleTime) {
1971
1908
  scheduleTime ??= this.audioContext.currentTime;
1972
1909
  const channel = this.channels[channelNumber];
1973
1910
  channel.state.pan = pan / 127;
1974
1911
  this.updateChannelVolume(channel, scheduleTime);
1975
- this.setKeyBasedPan(channel, scheduleTime);
1912
+ this.updateKeyBasedVolume(channel, scheduleTime);
1976
1913
  }
1977
1914
  setExpression(channelNumber, expression, scheduleTime) {
1978
1915
  scheduleTime ??= this.audioContext.currentTime;
@@ -1998,6 +1935,34 @@ export class MidyGM2 {
1998
1935
  .cancelScheduledValues(scheduleTime)
1999
1936
  .setValueAtTime(volume * gainRight, scheduleTime);
2000
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
+ }
2001
1966
  setSustainPedal(channelNumber, value, scheduleTime) {
2002
1967
  const channel = this.channels[channelNumber];
2003
1968
  if (channel.isDrum)
@@ -2005,7 +1970,7 @@ export class MidyGM2 {
2005
1970
  scheduleTime ??= this.audioContext.currentTime;
2006
1971
  channel.state.sustainPedal = value / 127;
2007
1972
  if (64 <= value) {
2008
- this.processScheduledNotes(channel, (note) => {
1973
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2009
1974
  channel.sustainNotes.push(note);
2010
1975
  });
2011
1976
  }
@@ -2038,6 +2003,9 @@ export class MidyGM2 {
2038
2003
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
2039
2004
  }
2040
2005
  }
2006
+ getSoftPedalFactor(channel, note) {
2007
+ return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
2008
+ }
2041
2009
  setSoftPedal(channelNumber, softPedal, scheduleTime) {
2042
2010
  const channel = this.channels[channelNumber];
2043
2011
  if (channel.isDrum)
@@ -2045,7 +2013,7 @@ export class MidyGM2 {
2045
2013
  const state = channel.state;
2046
2014
  scheduleTime ??= this.audioContext.currentTime;
2047
2015
  state.softPedal = softPedal / 127;
2048
- this.processScheduledNotes(channel, (note) => {
2016
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2049
2017
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2050
2018
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2051
2019
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2069,16 +2037,17 @@ export class MidyGM2 {
2069
2037
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2070
2038
  }
2071
2039
  else {
2072
- this.processScheduledNotes(channel, (note) => {
2040
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2073
2041
  if (note.voiceParams.reverbEffectsSend <= 0)
2074
2042
  return false;
2075
- note.reverbEffectsSend.disconnect();
2043
+ if (note.reverbEffectsSend)
2044
+ note.reverbEffectsSend.disconnect();
2076
2045
  });
2077
2046
  }
2078
2047
  }
2079
2048
  else {
2080
2049
  if (0 < reverbSendLevel) {
2081
- this.processScheduledNotes(channel, (note) => {
2050
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2082
2051
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2083
2052
  });
2084
2053
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2101,16 +2070,17 @@ export class MidyGM2 {
2101
2070
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2102
2071
  }
2103
2072
  else {
2104
- this.processScheduledNotes(channel, (note) => {
2073
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2105
2074
  if (note.voiceParams.chorusEffectsSend <= 0)
2106
2075
  return false;
2107
- note.chorusEffectsSend.disconnect();
2076
+ if (note.chorusEffectsSend)
2077
+ note.chorusEffectsSend.disconnect();
2108
2078
  });
2109
2079
  }
2110
2080
  }
2111
2081
  else {
2112
2082
  if (0 < chorusSendLevel) {
2113
- this.processScheduledNotes(channel, (note) => {
2083
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2114
2084
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2115
2085
  });
2116
2086
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2494,8 +2464,7 @@ export class MidyGM2 {
2494
2464
  setReverbType(type) {
2495
2465
  this.reverb.time = this.getReverbTimeFromType(type);
2496
2466
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2497
- const { audioContext, options } = this;
2498
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2467
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2499
2468
  }
2500
2469
  getReverbTimeFromType(type) {
2501
2470
  switch (type) {
@@ -2517,8 +2486,7 @@ export class MidyGM2 {
2517
2486
  }
2518
2487
  setReverbTime(value) {
2519
2488
  this.reverb.time = this.getReverbTime(value);
2520
- const { audioContext, options } = this;
2521
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2489
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2522
2490
  }
2523
2491
  getReverbTime(value) {
2524
2492
  return Math.exp((value - 40) * 0.025);
@@ -2689,50 +2657,60 @@ export class MidyGM2 {
2689
2657
  }
2690
2658
  }
2691
2659
  getFilterCutoffControl(channel) {
2692
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2693
- channel.state.channelPressure;
2660
+ const channelPressureRaw = channel.channelPressureTable[1];
2661
+ const channelPressure = (0 <= channelPressureRaw)
2662
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2663
+ : 0;
2694
2664
  return channelPressure * 15;
2695
2665
  }
2696
2666
  getAmplitudeControl(channel) {
2697
- const channelPressure = channel.channelPressureTable[2] *
2698
- channel.state.channelPressure;
2667
+ const channelPressureRaw = channel.channelPressureTable[2];
2668
+ const channelPressure = (0 <= channelPressureRaw)
2669
+ ? channelPressureRaw * channel.state.channelPressure
2670
+ : 0;
2699
2671
  return channelPressure / 64;
2700
2672
  }
2701
2673
  getLFOPitchDepth(channel) {
2702
- const channelPressure = channel.channelPressureTable[3] *
2703
- channel.state.channelPressure;
2674
+ const channelPressureRaw = channel.channelPressureTable[3];
2675
+ const channelPressure = (0 <= channelPressureRaw)
2676
+ ? channelPressureRaw * channel.state.channelPressure
2677
+ : 0;
2704
2678
  return channelPressure / 127 * 600;
2705
2679
  }
2706
2680
  getLFOFilterDepth(channel) {
2707
- const channelPressure = channel.channelPressureTable[4] *
2708
- channel.state.channelPressure;
2681
+ const channelPressureRaw = channel.channelPressureTable[4];
2682
+ const channelPressure = (0 <= channelPressureRaw)
2683
+ ? channelPressureRaw * channel.state.channelPressure
2684
+ : 0;
2709
2685
  return channelPressure / 127 * 2400;
2710
2686
  }
2711
2687
  getLFOAmplitudeDepth(channel) {
2712
- const channelPressure = channel.channelPressureTable[5] *
2713
- channel.state.channelPressure;
2688
+ const channelPressureRaw = channel.channelPressureTable[5];
2689
+ const channelPressure = (0 <= channelPressureRaw)
2690
+ ? channelPressureRaw * channel.state.channelPressure
2691
+ : 0;
2714
2692
  return channelPressure / 127;
2715
2693
  }
2716
2694
  setControllerParameters(channel, note, table) {
2717
- if (table[0] !== 64)
2695
+ if (0 <= table[0])
2718
2696
  this.updateDetune(channel, note);
2719
2697
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2720
- if (table[1] !== 64)
2698
+ if (0 <= table[1])
2721
2699
  this.setPortamentoFilterEnvelope(channel, note);
2722
- if (table[2] !== 64)
2700
+ if (0 <= table[2])
2723
2701
  this.setPortamentoVolumeEnvelope(channel, note);
2724
2702
  }
2725
2703
  else {
2726
- if (table[1] !== 64)
2704
+ if (0 <= table[1])
2727
2705
  this.setFilterEnvelope(channel, note);
2728
- if (table[2] !== 64)
2706
+ if (0 <= table[2])
2729
2707
  this.setVolumeEnvelope(channel, note);
2730
2708
  }
2731
- if (table[3] !== 0)
2709
+ if (0 <= table[3])
2732
2710
  this.setModLfoToPitch(channel, note);
2733
- if (table[4] !== 0)
2711
+ if (0 <= table[4])
2734
2712
  this.setModLfoToFilterFc(channel, note);
2735
- if (table[5] !== 0)
2713
+ if (0 <= table[5])
2736
2714
  this.setModLfoToVolume(channel, note);
2737
2715
  }
2738
2716
  handlePressureSysEx(data, tableName) {
@@ -2748,26 +2726,15 @@ export class MidyGM2 {
2748
2726
  }
2749
2727
  }
2750
2728
  initControlTable() {
2751
- const channelCount = 128;
2729
+ const ccCount = 128;
2752
2730
  const slotSize = 6;
2753
- const table = new Uint8Array(channelCount * slotSize);
2754
- return this.resetControlTable(table);
2731
+ return new Int8Array(ccCount * slotSize).fill(-1);
2755
2732
  }
2756
- resetControlTable(table) {
2757
- const channelCount = 128;
2758
- const slotSize = 6;
2759
- const defaultValues = [64, 64, 64, 0, 0, 0];
2760
- for (let ch = 0; ch < channelCount; ch++) {
2761
- const offset = ch * slotSize;
2762
- table.set(defaultValues, offset);
2763
- }
2764
- return table;
2765
- }
2766
- applyControlTable(channel, controllerType) {
2733
+ applyControlTable(channel, controllerType, scheduleTime) {
2767
2734
  const slotSize = 6;
2768
2735
  const offset = controllerType * slotSize;
2769
2736
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2770
- this.processScheduledNotes(channel, (note) => {
2737
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2771
2738
  this.setControllerParameters(channel, note, table);
2772
2739
  });
2773
2740
  }
@@ -2787,12 +2754,12 @@ export class MidyGM2 {
2787
2754
  getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2788
2755
  const index = keyNumber * 128 + controllerType;
2789
2756
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2790
- return (controlValue + 64) / 64;
2757
+ return controlValue;
2791
2758
  }
2792
2759
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2793
2760
  const channelNumber = data[4];
2794
2761
  const channel = this.channels[channelNumber];
2795
- if (channel.isDrum)
2762
+ if (!channel.isDrum)
2796
2763
  return;
2797
2764
  const keyNumber = data[5];
2798
2765
  const table = channel.keyBasedInstrumentControlTable;
@@ -2800,7 +2767,7 @@ export class MidyGM2 {
2800
2767
  const controllerType = data[i];
2801
2768
  const value = data[i + 1];
2802
2769
  const index = keyNumber * 128 + controllerType;
2803
- table[index] = value - 64;
2770
+ table[index] = value;
2804
2771
  }
2805
2772
  this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2806
2773
  }