@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.
@@ -11,11 +11,11 @@ class Note {
11
11
  writable: true,
12
12
  value: -1
13
13
  });
14
- Object.defineProperty(this, "noteOffEvent", {
14
+ Object.defineProperty(this, "ending", {
15
15
  enumerable: true,
16
16
  configurable: true,
17
17
  writable: true,
18
- value: void 0
18
+ value: false
19
19
  });
20
20
  Object.defineProperty(this, "bufferSource", {
21
21
  enumerable: true,
@@ -47,24 +47,6 @@ class Note {
47
47
  writable: true,
48
48
  value: void 0
49
49
  });
50
- Object.defineProperty(this, "volumeNode", {
51
- enumerable: true,
52
- configurable: true,
53
- writable: true,
54
- value: void 0
55
- });
56
- Object.defineProperty(this, "gainL", {
57
- enumerable: true,
58
- configurable: true,
59
- writable: true,
60
- value: void 0
61
- });
62
- Object.defineProperty(this, "gainR", {
63
- enumerable: true,
64
- configurable: true,
65
- writable: true,
66
- value: void 0
67
- });
68
50
  Object.defineProperty(this, "modulationLFO", {
69
51
  enumerable: true,
70
52
  configurable: true,
@@ -202,6 +184,16 @@ class ControllerState {
202
184
  }
203
185
  }
204
186
  }
187
+ const volumeEnvelopeKeys = [
188
+ "volDelay",
189
+ "volAttack",
190
+ "volHold",
191
+ "volDecay",
192
+ "volSustain",
193
+ "volRelease",
194
+ "initialAttenuation",
195
+ ];
196
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
205
197
  const filterEnvelopeKeys = [
206
198
  "modEnvToPitch",
207
199
  "initialFilterFc",
@@ -211,22 +203,20 @@ const filterEnvelopeKeys = [
211
203
  "modHold",
212
204
  "modDecay",
213
205
  "modSustain",
214
- "modRelease",
215
- "playbackRate",
216
206
  ];
217
207
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
218
- const volumeEnvelopeKeys = [
219
- "volDelay",
220
- "volAttack",
221
- "volHold",
222
- "volDecay",
223
- "volSustain",
224
- "volRelease",
225
- "initialAttenuation",
208
+ const pitchEnvelopeKeys = [
209
+ "modEnvToPitch",
210
+ "modDelay",
211
+ "modAttack",
212
+ "modHold",
213
+ "modDecay",
214
+ "modSustain",
215
+ "playbackRate",
226
216
  ];
227
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
217
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
228
218
  class MidyGM2 {
229
- constructor(audioContext, options = this.defaultOptions) {
219
+ constructor(audioContext) {
230
220
  Object.defineProperty(this, "mode", {
231
221
  enumerable: true,
232
222
  configurable: true,
@@ -250,6 +240,7 @@ class MidyGM2 {
250
240
  configurable: true,
251
241
  writable: true,
252
242
  value: {
243
+ algorithm: "SchroederReverb",
253
244
  time: this.getReverbTime(64),
254
245
  feedback: 0.8,
255
246
  }
@@ -398,30 +389,7 @@ class MidyGM2 {
398
389
  writable: true,
399
390
  value: new Array(this.numChannels * drumExclusiveClassCount)
400
391
  });
401
- Object.defineProperty(this, "defaultOptions", {
402
- enumerable: true,
403
- configurable: true,
404
- writable: true,
405
- value: {
406
- reverbAlgorithm: (audioContext) => {
407
- const { time: rt60, feedback } = this.reverb;
408
- // const delay = this.calcDelay(rt60, feedback);
409
- // const impulse = this.createConvolutionReverbImpulse(
410
- // audioContext,
411
- // rt60,
412
- // delay,
413
- // );
414
- // return this.createConvolutionReverb(audioContext, impulse);
415
- const combFeedbacks = this.generateDistributedArray(feedback, 4);
416
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
417
- const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
418
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
419
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
420
- },
421
- }
422
- });
423
392
  this.audioContext = audioContext;
424
- this.options = { ...this.defaultOptions, ...options };
425
393
  this.masterVolume = new GainNode(audioContext);
426
394
  this.scheduler = new GainNode(audioContext, { gain: 0 });
427
395
  this.schedulerBuffer = new AudioBuffer({
@@ -431,7 +399,7 @@ class MidyGM2 {
431
399
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
432
400
  this.controlChangeHandlers = this.createControlChangeHandlers();
433
401
  this.channels = this.createChannels(audioContext);
434
- this.reverbEffect = this.options.reverbAlgorithm(audioContext);
402
+ this.reverbEffect = this.createReverbEffect(audioContext);
435
403
  this.chorusEffect = this.createChorusEffect(audioContext);
436
404
  this.chorusEffect.output.connect(this.masterVolume);
437
405
  this.reverbEffect.output.connect(this.masterVolume);
@@ -458,24 +426,44 @@ class MidyGM2 {
458
426
  }
459
427
  }
460
428
  }
461
- async loadSoundFont(soundFontUrl) {
462
- const response = await fetch(soundFontUrl);
463
- const arrayBuffer = await response.arrayBuffer();
464
- const parsed = (0, soundfont_parser_1.parse)(new Uint8Array(arrayBuffer));
429
+ async loadSoundFont(input) {
430
+ let uint8Array;
431
+ if (typeof input === "string") {
432
+ const response = await fetch(input);
433
+ const arrayBuffer = await response.arrayBuffer();
434
+ uint8Array = new Uint8Array(arrayBuffer);
435
+ }
436
+ else if (input instanceof Uint8Array) {
437
+ uint8Array = input;
438
+ }
439
+ else {
440
+ throw new TypeError("input must be a URL string or Uint8Array");
441
+ }
442
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
465
443
  const soundFont = new soundfont_parser_1.SoundFont(parsed);
466
444
  this.addSoundFont(soundFont);
467
445
  }
468
- async loadMIDI(midiUrl) {
469
- const response = await fetch(midiUrl);
470
- const arrayBuffer = await response.arrayBuffer();
471
- const midi = (0, midi_file_1.parseMidi)(new Uint8Array(arrayBuffer));
446
+ async loadMIDI(input) {
447
+ let uint8Array;
448
+ if (typeof input === "string") {
449
+ const response = await fetch(input);
450
+ const arrayBuffer = await response.arrayBuffer();
451
+ uint8Array = new Uint8Array(arrayBuffer);
452
+ }
453
+ else if (input instanceof Uint8Array) {
454
+ uint8Array = input;
455
+ }
456
+ else {
457
+ throw new TypeError("input must be a URL string or Uint8Array");
458
+ }
459
+ const midi = (0, midi_file_1.parseMidi)(uint8Array);
472
460
  this.ticksPerBeat = midi.header.ticksPerBeat;
473
461
  const midiData = this.extractMidiData(midi);
474
462
  this.instruments = midiData.instruments;
475
463
  this.timeline = midiData.timeline;
476
464
  this.totalTime = this.calcTotalTime();
477
465
  }
478
- setChannelAudioNodes(audioContext) {
466
+ createChannelAudioNodes(audioContext) {
479
467
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
480
468
  const gainL = new GainNode(audioContext, { gain: gainLeft });
481
469
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -490,10 +478,10 @@ class MidyGM2 {
490
478
  };
491
479
  }
492
480
  resetChannelTable(channel) {
493
- this.resetControlTable(channel.controlTable);
481
+ channel.controlTable.fill(-1);
494
482
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
495
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
496
- channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
483
+ channel.channelPressureTable.fill(-1);
484
+ channel.keyBasedInstrumentControlTable.fill(-1);
497
485
  }
498
486
  createChannels(audioContext) {
499
487
  const channels = Array.from({ length: this.numChannels }, () => {
@@ -502,14 +490,16 @@ class MidyGM2 {
502
490
  isDrum: false,
503
491
  state: new ControllerState(),
504
492
  ...this.constructor.channelSettings,
505
- ...this.setChannelAudioNodes(audioContext),
493
+ ...this.createChannelAudioNodes(audioContext),
506
494
  scheduledNotes: [],
507
495
  sustainNotes: [],
508
496
  sostenutoNotes: [],
509
497
  controlTable: this.initControlTable(),
510
498
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
511
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
512
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
499
+ channelPressureTable: new Int8Array(6).fill(-1),
500
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
501
+ keyBasedGainLs: new Array(128),
502
+ keyBasedGainRs: new Array(128),
513
503
  };
514
504
  });
515
505
  return channels;
@@ -543,10 +533,17 @@ class MidyGM2 {
543
533
  return audioBuffer;
544
534
  }
545
535
  }
546
- createBufferSource(voiceParams, audioBuffer) {
536
+ isLoopDrum(channel, noteNumber) {
537
+ const programNumber = channel.programNumber;
538
+ return ((programNumber === 48 && noteNumber === 88) ||
539
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
540
+ }
541
+ createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
547
542
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
548
543
  bufferSource.buffer = audioBuffer;
549
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
544
+ bufferSource.loop = channel.isDrum
545
+ ? this.isLoopDrum(channel, noteNumber)
546
+ : (voiceParams.sampleModes % 2 !== 0);
550
547
  if (bufferSource.loop) {
551
548
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
552
549
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -561,12 +558,13 @@ class MidyGM2 {
561
558
  const delay = this.startDelay - resumeTime;
562
559
  const startTime = event.startTime + delay;
563
560
  switch (event.type) {
564
- case "noteOn": {
565
- const noteOffEvent = {
566
- ...event.noteOffEvent,
567
- startTime: event.noteOffEvent.startTime + delay,
568
- };
569
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
561
+ case "noteOn":
562
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
563
+ break;
564
+ case "noteOff": {
565
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
566
+ if (notePromise)
567
+ this.notePromises.push(notePromise);
570
568
  break;
571
569
  }
572
570
  case "controller":
@@ -671,6 +669,7 @@ class MidyGM2 {
671
669
  return `${programNumber}:${noteNumber}:${velocity}`;
672
670
  }
673
671
  extractMidiData(midi) {
672
+ this.audioBufferCounter.clear();
674
673
  const instruments = new Set();
675
674
  const timeline = [];
676
675
  const tmpChannels = new Array(this.channels.length);
@@ -770,38 +769,13 @@ class MidyGM2 {
770
769
  prevTempoTicks = event.ticks;
771
770
  }
772
771
  }
773
- const activeNotes = new Array(this.channels.length * 128);
774
- for (let i = 0; i < activeNotes.length; i++) {
775
- activeNotes[i] = [];
776
- }
777
- for (let i = 0; i < timeline.length; i++) {
778
- const event = timeline[i];
779
- switch (event.type) {
780
- case "noteOn": {
781
- const index = event.channel * 128 + event.noteNumber;
782
- activeNotes[index].push(event);
783
- break;
784
- }
785
- case "noteOff": {
786
- const index = event.channel * 128 + event.noteNumber;
787
- const noteOn = activeNotes[index].pop();
788
- if (noteOn) {
789
- noteOn.noteOffEvent = event;
790
- }
791
- else {
792
- const eventString = JSON.stringify(event, null, 2);
793
- console.warn(`noteOff without matching noteOn: ${eventString}`);
794
- }
795
- }
796
- }
797
- }
798
772
  return { instruments, timeline };
799
773
  }
800
774
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
801
775
  const channel = this.channels[channelNumber];
802
776
  const promises = [];
803
777
  this.processActiveNotes(channel, scheduleTime, (note) => {
804
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
778
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
805
779
  this.notePromises.push(promise);
806
780
  promises.push(promise);
807
781
  });
@@ -810,8 +784,8 @@ class MidyGM2 {
810
784
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
811
785
  const channel = this.channels[channelNumber];
812
786
  const promises = [];
813
- this.processScheduledNotes(channel, (note) => {
814
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
787
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
788
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
815
789
  this.notePromises.push(promise);
816
790
  promises.push(promise);
817
791
  });
@@ -869,7 +843,7 @@ class MidyGM2 {
869
843
  const now = this.audioContext.currentTime;
870
844
  return this.resumeTime + now - this.startTime - this.startDelay;
871
845
  }
872
- processScheduledNotes(channel, callback) {
846
+ processScheduledNotes(channel, scheduleTime, callback) {
873
847
  const scheduledNotes = channel.scheduledNotes;
874
848
  for (let i = 0; i < scheduledNotes.length; i++) {
875
849
  const note = scheduledNotes[i];
@@ -877,6 +851,8 @@ class MidyGM2 {
877
851
  continue;
878
852
  if (note.ending)
879
853
  continue;
854
+ if (note.startTime < scheduleTime)
855
+ continue;
880
856
  callback(note);
881
857
  }
882
858
  }
@@ -888,11 +864,8 @@ class MidyGM2 {
888
864
  continue;
889
865
  if (note.ending)
890
866
  continue;
891
- const noteOffEvent = note.noteOffEvent;
892
- if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
893
- continue;
894
867
  if (scheduleTime < note.startTime)
895
- continue;
868
+ break;
896
869
  callback(note);
897
870
  }
898
871
  }
@@ -981,6 +954,22 @@ class MidyGM2 {
981
954
  const output = allpasses.at(-1);
982
955
  return { input, output };
983
956
  }
957
+ createReverbEffect(audioContext) {
958
+ const { algorithm, time: rt60, feedback } = this.reverb;
959
+ switch (algorithm) {
960
+ case "ConvolutionReverb": {
961
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
962
+ return this.createConvolutionReverb(audioContext, impulse);
963
+ }
964
+ case "SchroederReverb": {
965
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
966
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
967
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
968
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
969
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
970
+ }
971
+ }
972
+ }
984
973
  createChorusEffect(audioContext) {
985
974
  const input = new GainNode(audioContext);
986
975
  const output = new GainNode(audioContext);
@@ -1045,22 +1034,28 @@ class MidyGM2 {
1045
1034
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1046
1035
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1047
1036
  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;
1037
+ const channelPressureRaw = channel.channelPressureTable[0];
1038
+ if (0 <= channelPressureRaw) {
1039
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1040
+ const channelPressure = channelPressureDepth *
1041
+ channel.state.channelPressure;
1042
+ return tuning + pitch + channelPressure;
1043
+ }
1044
+ else {
1045
+ return tuning + pitch;
1046
+ }
1051
1047
  }
1052
1048
  calcNoteDetune(channel, note) {
1053
1049
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1054
1050
  }
1055
1051
  updateChannelDetune(channel, scheduleTime) {
1056
- this.processScheduledNotes(channel, (note) => {
1052
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1057
1053
  this.updateDetune(channel, note, scheduleTime);
1058
1054
  });
1059
1055
  }
1060
1056
  updateDetune(channel, note, scheduleTime) {
1061
1057
  const noteDetune = this.calcNoteDetune(channel, note);
1062
- const pitchControl = this.getPitchControl(channel, note);
1063
- const detune = channel.detune + noteDetune + pitchControl;
1058
+ const detune = channel.detune + noteDetune;
1064
1059
  if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1065
1060
  const startTime = note.startTime;
1066
1061
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
@@ -1200,10 +1195,8 @@ class MidyGM2 {
1200
1195
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1201
1196
  }
1202
1197
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1203
- const state = channel.state;
1204
- const { voiceParams, noteNumber, startTime } = note;
1205
- const softPedalFactor = 1 -
1206
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1198
+ const { voiceParams, startTime } = note;
1199
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1207
1200
  const baseCent = voiceParams.initialFilterFc +
1208
1201
  this.getFilterCutoffControl(channel);
1209
1202
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
@@ -1221,10 +1214,8 @@ class MidyGM2 {
1221
1214
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1222
1215
  }
1223
1216
  setFilterEnvelope(channel, note, scheduleTime) {
1224
- const state = channel.state;
1225
- const { voiceParams, noteNumber, startTime } = note;
1226
- const softPedalFactor = 1 -
1227
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1217
+ const { voiceParams, startTime } = note;
1218
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1228
1219
  const baseCent = voiceParams.initialFilterFc +
1229
1220
  this.getFilterCutoffControl(channel);
1230
1221
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
@@ -1304,10 +1295,7 @@ class MidyGM2 {
1304
1295
  const voiceParams = voice.getAllParams(controllerState);
1305
1296
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1306
1297
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1307
- note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1308
- note.volumeNode = new GainNode(this.audioContext);
1309
- note.gainL = new GainNode(this.audioContext);
1310
- note.gainR = new GainNode(this.audioContext);
1298
+ note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1311
1299
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1312
1300
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1313
1301
  type: "lowpass",
@@ -1340,13 +1328,10 @@ class MidyGM2 {
1340
1328
  }
1341
1329
  note.bufferSource.connect(note.filterNode);
1342
1330
  note.filterNode.connect(note.volumeEnvelopeNode);
1343
- note.volumeEnvelopeNode.connect(note.volumeNode);
1344
- note.volumeNode.connect(note.gainL);
1345
- note.volumeNode.connect(note.gainR);
1346
- if (0 < channel.chorusSendLevel) {
1331
+ if (0 < state.chorusSendLevel) {
1347
1332
  this.setChorusEffectsSend(channel, note, 0, now);
1348
1333
  }
1349
- if (0 < channel.reverbSendLevel) {
1334
+ if (0 < state.reverbSendLevel) {
1350
1335
  this.setReverbEffectsSend(channel, note, 0, now);
1351
1336
  }
1352
1337
  note.bufferSource.start(startTime);
@@ -1376,7 +1361,7 @@ class MidyGM2 {
1376
1361
  if (prev) {
1377
1362
  const [prevNote, prevChannelNumber] = prev;
1378
1363
  if (prevNote && !prevNote.ending) {
1379
- this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1364
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1380
1365
  startTime, true);
1381
1366
  }
1382
1367
  }
@@ -1396,19 +1381,12 @@ class MidyGM2 {
1396
1381
  channelNumber;
1397
1382
  const prevNote = this.drumExclusiveClassNotes[index];
1398
1383
  if (prevNote && !prevNote.ending) {
1399
- this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1384
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1400
1385
  startTime, true);
1401
1386
  }
1402
1387
  this.drumExclusiveClassNotes[index] = note;
1403
1388
  }
1404
- isDrumNoteOffException(channel, noteNumber) {
1405
- if (!channel.isDrum)
1406
- return false;
1407
- const programNumber = channel.programNumber;
1408
- return !((programNumber === 48 && noteNumber === 88) ||
1409
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1410
- }
1411
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1389
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1412
1390
  const channel = this.channels[channelNumber];
1413
1391
  const bankNumber = this.calcBank(channel, channelNumber);
1414
1392
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1420,9 +1398,18 @@ class MidyGM2 {
1420
1398
  return;
1421
1399
  const isSF3 = soundFont.parsed.info.version.major === 3;
1422
1400
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1423
- note.noteOffEvent = noteOffEvent;
1424
- note.gainL.connect(channel.gainL);
1425
- note.gainR.connect(channel.gainR);
1401
+ if (channel.isDrum) {
1402
+ const audioContext = this.audioContext;
1403
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1404
+ channel.keyBasedGainLs[noteNumber] = gainL;
1405
+ channel.keyBasedGainRs[noteNumber] = gainR;
1406
+ note.volumeEnvelopeNode.connect(gainL);
1407
+ note.volumeEnvelopeNode.connect(gainR);
1408
+ }
1409
+ else {
1410
+ note.volumeEnvelopeNode.connect(channel.gainL);
1411
+ note.volumeEnvelopeNode.connect(channel.gainR);
1412
+ }
1426
1413
  if (0.5 <= channel.state.sustainPedal) {
1427
1414
  channel.sustainNotes.push(note);
1428
1415
  }
@@ -1431,31 +1418,6 @@ class MidyGM2 {
1431
1418
  const scheduledNotes = channel.scheduledNotes;
1432
1419
  note.index = scheduledNotes.length;
1433
1420
  scheduledNotes.push(note);
1434
- if (this.isDrumNoteOffException(channel, noteNumber)) {
1435
- const stopTime = startTime + note.bufferSource.buffer.duration;
1436
- const promise = new Promise((resolve) => {
1437
- note.bufferSource.onended = () => {
1438
- scheduledNotes[note.index] = undefined;
1439
- this.disconnectNote(note);
1440
- resolve();
1441
- };
1442
- note.bufferSource.stop(stopTime);
1443
- });
1444
- this.notePromises.push(promise);
1445
- }
1446
- else if (noteOffEvent) {
1447
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1448
- const portamentoTime = this.getPortamentoTime(channel, note);
1449
- const portamentoEndTime = startTime + portamentoTime;
1450
- const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
1451
- Math.max(noteOffEvent.startTime, portamentoEndTime), false);
1452
- this.notePromises.push(notePromise);
1453
- }
1454
- else {
1455
- const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
1456
- this.notePromises.push(notePromise);
1457
- }
1458
- }
1459
1421
  }
1460
1422
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1461
1423
  scheduleTime ??= this.audioContext.currentTime;
@@ -1465,9 +1427,6 @@ class MidyGM2 {
1465
1427
  note.bufferSource.disconnect();
1466
1428
  note.filterNode.disconnect();
1467
1429
  note.volumeEnvelopeNode.disconnect();
1468
- note.volumeNode.disconnect();
1469
- note.gainL.disconnect();
1470
- note.gainR.disconnect();
1471
1430
  if (note.modulationDepth) {
1472
1431
  note.volumeDepth.disconnect();
1473
1432
  note.modulationDepth.disconnect();
@@ -1484,41 +1443,47 @@ class MidyGM2 {
1484
1443
  note.chorusEffectsSend.disconnect();
1485
1444
  }
1486
1445
  }
1487
- stopNote(channel, note, endTime, stopTime) {
1446
+ releaseNote(channel, note, endTime) {
1447
+ const volRelease = endTime + note.voiceParams.volRelease;
1448
+ const modRelease = endTime + note.voiceParams.modRelease;
1449
+ const stopTime = Math.min(volRelease, modRelease);
1450
+ note.filterNode.frequency
1451
+ .cancelScheduledValues(endTime)
1452
+ .linearRampToValueAtTime(0, modRelease);
1488
1453
  note.volumeEnvelopeNode.gain
1489
1454
  .cancelScheduledValues(endTime)
1490
- .linearRampToValueAtTime(0, stopTime);
1491
- note.ending = true;
1492
- this.scheduleTask(() => {
1493
- note.bufferSource.loop = false;
1494
- }, stopTime);
1455
+ .linearRampToValueAtTime(0, volRelease);
1495
1456
  return new Promise((resolve) => {
1496
- note.bufferSource.onended = () => {
1497
- channel.scheduledNotes[note.index] = undefined;
1457
+ this.scheduleTask(() => {
1458
+ const bufferSource = note.bufferSource;
1459
+ bufferSource.loop = false;
1460
+ bufferSource.stop(stopTime);
1498
1461
  this.disconnectNote(note);
1462
+ channel.scheduledNotes[note.index] = undefined;
1499
1463
  resolve();
1500
- };
1501
- note.bufferSource.stop(stopTime);
1464
+ }, stopTime);
1502
1465
  });
1503
1466
  }
1504
- scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1467
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1505
1468
  const channel = this.channels[channelNumber];
1506
- if (this.isDrumNoteOffException(channel, note.noteNumber))
1507
- return;
1508
1469
  const state = channel.state;
1509
1470
  if (!force) {
1510
- if (0.5 <= state.sustainPedal)
1511
- return;
1512
- if (0.5 <= channel.state.sostenutoPedal)
1513
- return;
1471
+ if (channel.isDrum) {
1472
+ if (!this.isLoopDrum(channel, noteNumber))
1473
+ return;
1474
+ }
1475
+ else {
1476
+ if (0.5 <= state.sustainPedal)
1477
+ return;
1478
+ if (0.5 <= state.sostenutoPedal)
1479
+ return;
1480
+ }
1514
1481
  }
1515
- const volRelease = endTime + note.voiceParams.volRelease;
1516
- const modRelease = endTime + note.voiceParams.modRelease;
1517
- note.filterNode.frequency
1518
- .cancelScheduledValues(endTime)
1519
- .linearRampToValueAtTime(0, modRelease);
1520
- const stopTime = Math.min(volRelease, modRelease);
1521
- return this.stopNote(channel, note, endTime, stopTime);
1482
+ const note = this.findNoteOffTarget(channel, noteNumber);
1483
+ if (!note)
1484
+ return;
1485
+ note.ending = true;
1486
+ this.releaseNote(channel, note, endTime);
1522
1487
  }
1523
1488
  findNoteOffTarget(channel, noteNumber) {
1524
1489
  const scheduledNotes = channel.scheduledNotes;
@@ -1535,16 +1500,14 @@ class MidyGM2 {
1535
1500
  }
1536
1501
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1537
1502
  scheduleTime ??= this.audioContext.currentTime;
1538
- const channel = this.channels[channelNumber];
1539
- const note = this.findNoteOffTarget(channel, noteNumber);
1540
- return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1503
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1541
1504
  }
1542
1505
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1543
1506
  const velocity = halfVelocity * 2;
1544
1507
  const channel = this.channels[channelNumber];
1545
1508
  const promises = [];
1546
1509
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1547
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1510
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1548
1511
  promises.push(promise);
1549
1512
  }
1550
1513
  channel.sustainNotes = [];
@@ -1558,7 +1521,7 @@ class MidyGM2 {
1558
1521
  channel.state.sostenutoPedal = 0;
1559
1522
  for (let i = 0; i < sostenutoNotes.length; i++) {
1560
1523
  const note = sostenutoNotes[i];
1561
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
1524
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1562
1525
  promises.push(promise);
1563
1526
  }
1564
1527
  channel.sostenutoNotes = [];
@@ -1606,9 +1569,10 @@ class MidyGM2 {
1606
1569
  const prev = channel.state.channelPressure;
1607
1570
  const next = value / 127;
1608
1571
  channel.state.channelPressure = next;
1609
- if (channel.channelPressureTable[0] !== 64) {
1610
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1611
- channel.detune += pressureDepth * (next - prev);
1572
+ const channelPressureRaw = channel.channelPressureTable[0];
1573
+ if (0 <= channelPressureRaw) {
1574
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1575
+ channel.detune += channelPressureDepth * (next - prev);
1612
1576
  }
1613
1577
  const table = channel.channelPressureTable;
1614
1578
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1668,10 +1632,15 @@ class MidyGM2 {
1668
1632
  .setValueAtTime(volumeDepth, scheduleTime);
1669
1633
  }
1670
1634
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1635
+ let value = note.voiceParams.reverbEffectsSend;
1636
+ if (channel.isDrum) {
1637
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1638
+ if (0 <= keyBasedValue) {
1639
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1640
+ }
1641
+ }
1671
1642
  if (0 < prevValue) {
1672
- if (0 < note.voiceParams.reverbEffectsSend) {
1673
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1674
- const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1643
+ if (0 < value) {
1675
1644
  note.reverbEffectsSend.gain
1676
1645
  .cancelScheduledValues(scheduleTime)
1677
1646
  .setValueAtTime(value, scheduleTime);
@@ -1681,10 +1650,10 @@ class MidyGM2 {
1681
1650
  }
1682
1651
  }
1683
1652
  else {
1684
- if (0 < note.voiceParams.reverbEffectsSend) {
1653
+ if (0 < value) {
1685
1654
  if (!note.reverbEffectsSend) {
1686
1655
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1687
- gain: note.voiceParams.reverbEffectsSend,
1656
+ gain: value,
1688
1657
  });
1689
1658
  note.volumeNode.connect(note.reverbEffectsSend);
1690
1659
  }
@@ -1693,10 +1662,15 @@ class MidyGM2 {
1693
1662
  }
1694
1663
  }
1695
1664
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1665
+ let value = note.voiceParams.chorusEffectsSend;
1666
+ if (channel.isDrum) {
1667
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1668
+ if (0 <= keyBasedValue) {
1669
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1670
+ }
1671
+ }
1696
1672
  if (0 < prevValue) {
1697
- if (0 < note.voiceParams.chorusEffectsSend) {
1698
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1699
- const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1673
+ if (0 < vaule) {
1700
1674
  note.chorusEffectsSend.gain
1701
1675
  .cancelScheduledValues(scheduleTime)
1702
1676
  .setValueAtTime(value, scheduleTime);
@@ -1706,12 +1680,12 @@ class MidyGM2 {
1706
1680
  }
1707
1681
  }
1708
1682
  else {
1709
- if (0 < note.voiceParams.chorusEffectsSend) {
1683
+ if (0 < value) {
1710
1684
  if (!note.chorusEffectsSend) {
1711
1685
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1712
- gain: note.voiceParams.chorusEffectsSend,
1686
+ gain: value,
1713
1687
  });
1714
- note.volumeNode.connect(note.chorusEffectsSend);
1688
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1715
1689
  }
1716
1690
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1717
1691
  }
@@ -1795,11 +1769,12 @@ class MidyGM2 {
1795
1769
  return state;
1796
1770
  }
1797
1771
  applyVoiceParams(channel, controllerType, scheduleTime) {
1798
- this.processScheduledNotes(channel, (note) => {
1772
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1799
1773
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1800
1774
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1801
- let appliedFilterEnvelope = false;
1802
- let appliedVolumeEnvelope = false;
1775
+ let applyVolumeEnvelope = false;
1776
+ let applyFilterEnvelope = false;
1777
+ let applyPitchEnvelope = false;
1803
1778
  for (const [key, value] of Object.entries(voiceParams)) {
1804
1779
  const prevValue = note.voiceParams[key];
1805
1780
  if (value === prevValue)
@@ -1808,37 +1783,23 @@ class MidyGM2 {
1808
1783
  if (key in this.voiceParamsHandlers) {
1809
1784
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1810
1785
  }
1811
- else if (filterEnvelopeKeySet.has(key)) {
1812
- if (appliedFilterEnvelope)
1813
- continue;
1814
- appliedFilterEnvelope = true;
1815
- const noteVoiceParams = note.voiceParams;
1816
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1817
- const key = filterEnvelopeKeys[i];
1818
- if (key in voiceParams)
1819
- noteVoiceParams[key] = voiceParams[key];
1820
- }
1821
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1822
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1823
- }
1824
- else {
1825
- this.setFilterEnvelope(channel, note, scheduleTime);
1826
- }
1827
- this.setPitchEnvelope(note, scheduleTime);
1828
- }
1829
- else if (volumeEnvelopeKeySet.has(key)) {
1830
- if (appliedVolumeEnvelope)
1831
- continue;
1832
- appliedVolumeEnvelope = true;
1833
- const noteVoiceParams = note.voiceParams;
1834
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1835
- const key = volumeEnvelopeKeys[i];
1836
- if (key in voiceParams)
1837
- noteVoiceParams[key] = voiceParams[key];
1838
- }
1839
- this.setVolumeEnvelope(channel, note, scheduleTime);
1786
+ else {
1787
+ if (volumeEnvelopeKeySet.has(key))
1788
+ applyVolumeEnvelope = true;
1789
+ if (filterEnvelopeKeySet.has(key))
1790
+ applyFilterEnvelope = true;
1791
+ if (pitchEnvelopeKeySet.has(key))
1792
+ applyPitchEnvelope = true;
1840
1793
  }
1841
1794
  }
1795
+ if (applyVolumeEnvelope) {
1796
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1797
+ }
1798
+ if (applyFilterEnvelope) {
1799
+ this.setFilterEnvelope(channel, note, scheduleTime);
1800
+ }
1801
+ if (applyPitchEnvelope)
1802
+ this.setPitchEnvelope(note, scheduleTime);
1842
1803
  });
1843
1804
  }
1844
1805
  createControlChangeHandlers() {
@@ -1875,7 +1836,7 @@ class MidyGM2 {
1875
1836
  handler.call(this, channelNumber, value, scheduleTime);
1876
1837
  const channel = this.channels[channelNumber];
1877
1838
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1878
- this.applyControlTable(channel, controllerType);
1839
+ this.applyControlTable(channel, controllerType, scheduleTime);
1879
1840
  }
1880
1841
  else {
1881
1842
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1886,7 +1847,7 @@ class MidyGM2 {
1886
1847
  }
1887
1848
  updateModulation(channel, scheduleTime) {
1888
1849
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1889
- this.processScheduledNotes(channel, (note) => {
1850
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1890
1851
  if (note.modulationDepth) {
1891
1852
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1892
1853
  }
@@ -1905,7 +1866,7 @@ class MidyGM2 {
1905
1866
  this.updateModulation(channel, scheduleTime);
1906
1867
  }
1907
1868
  updatePortamento(channel, scheduleTime) {
1908
- this.processScheduledNotes(channel, (note) => {
1869
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1909
1870
  if (0.5 <= channel.state.portamento) {
1910
1871
  if (0 <= note.portamentoNoteNumber) {
1911
1872
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1932,22 +1893,12 @@ class MidyGM2 {
1932
1893
  return;
1933
1894
  this.updatePortamento(channel, scheduleTime);
1934
1895
  }
1935
- setKeyBasedVolume(channel, scheduleTime) {
1936
- this.processScheduledNotes(channel, (note) => {
1937
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1938
- if (keyBasedValue !== 0) {
1939
- note.volumeNode.gain
1940
- .cancelScheduledValues(scheduleTime)
1941
- .setValueAtTime(1 + keyBasedValue, scheduleTime);
1942
- }
1943
- });
1944
- }
1945
1896
  setVolume(channelNumber, volume, scheduleTime) {
1946
1897
  scheduleTime ??= this.audioContext.currentTime;
1947
1898
  const channel = this.channels[channelNumber];
1948
1899
  channel.state.volume = volume / 127;
1949
1900
  this.updateChannelVolume(channel, scheduleTime);
1950
- this.setKeyBasedVolume(channel, scheduleTime);
1901
+ this.updateKeyBasedVolume(channel, scheduleTime);
1951
1902
  }
1952
1903
  panToGain(pan) {
1953
1904
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1956,26 +1907,12 @@ class MidyGM2 {
1956
1907
  gainRight: Math.sin(theta),
1957
1908
  };
1958
1909
  }
1959
- setKeyBasedPan(channel, scheduleTime) {
1960
- this.processScheduledNotes(channel, (note) => {
1961
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1962
- if (keyBasedValue !== 0) {
1963
- const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1964
- note.gainL.gain
1965
- .cancelScheduledValues(scheduleTime)
1966
- .setValueAtTime(gainLeft, scheduleTime);
1967
- note.gainR.gain
1968
- .cancelScheduledValues(scheduleTime)
1969
- .setValueAtTime(gainRight, scheduleTime);
1970
- }
1971
- });
1972
- }
1973
1910
  setPan(channelNumber, pan, scheduleTime) {
1974
1911
  scheduleTime ??= this.audioContext.currentTime;
1975
1912
  const channel = this.channels[channelNumber];
1976
1913
  channel.state.pan = pan / 127;
1977
1914
  this.updateChannelVolume(channel, scheduleTime);
1978
- this.setKeyBasedPan(channel, scheduleTime);
1915
+ this.updateKeyBasedVolume(channel, scheduleTime);
1979
1916
  }
1980
1917
  setExpression(channelNumber, expression, scheduleTime) {
1981
1918
  scheduleTime ??= this.audioContext.currentTime;
@@ -2001,6 +1938,34 @@ class MidyGM2 {
2001
1938
  .cancelScheduledValues(scheduleTime)
2002
1939
  .setValueAtTime(volume * gainRight, scheduleTime);
2003
1940
  }
1941
+ updateKeyBasedVolume(channel, scheduleTime) {
1942
+ if (!channel.isDrum)
1943
+ return;
1944
+ const state = channel.state;
1945
+ const defaultVolume = state.volume * state.expression;
1946
+ const defaultPan = state.pan;
1947
+ for (let i = 0; i < 128; i++) {
1948
+ const gainL = channel.keyBasedGainLs[i];
1949
+ const gainR = channel.keyBasedGainLs[i];
1950
+ if (!gainL)
1951
+ continue;
1952
+ if (!gainR)
1953
+ continue;
1954
+ const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
1955
+ const volume = (0 <= keyBasedVolume)
1956
+ ? defaultVolume * keyBasedVolume / 64
1957
+ : defaultVolume;
1958
+ const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
1959
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
1960
+ const { gainLeft, gainRight } = this.panToGain(pan);
1961
+ gainL.gain
1962
+ .cancelScheduledValues(scheduleTime)
1963
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1964
+ gainR.gain
1965
+ .cancelScheduledValues(scheduleTime)
1966
+ .setValueAtTime(volume * gainRight, scheduleTime);
1967
+ }
1968
+ }
2004
1969
  setSustainPedal(channelNumber, value, scheduleTime) {
2005
1970
  const channel = this.channels[channelNumber];
2006
1971
  if (channel.isDrum)
@@ -2008,7 +1973,7 @@ class MidyGM2 {
2008
1973
  scheduleTime ??= this.audioContext.currentTime;
2009
1974
  channel.state.sustainPedal = value / 127;
2010
1975
  if (64 <= value) {
2011
- this.processScheduledNotes(channel, (note) => {
1976
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2012
1977
  channel.sustainNotes.push(note);
2013
1978
  });
2014
1979
  }
@@ -2041,6 +2006,9 @@ class MidyGM2 {
2041
2006
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
2042
2007
  }
2043
2008
  }
2009
+ getSoftPedalFactor(channel, note) {
2010
+ return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
2011
+ }
2044
2012
  setSoftPedal(channelNumber, softPedal, scheduleTime) {
2045
2013
  const channel = this.channels[channelNumber];
2046
2014
  if (channel.isDrum)
@@ -2048,7 +2016,7 @@ class MidyGM2 {
2048
2016
  const state = channel.state;
2049
2017
  scheduleTime ??= this.audioContext.currentTime;
2050
2018
  state.softPedal = softPedal / 127;
2051
- this.processScheduledNotes(channel, (note) => {
2019
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2052
2020
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2053
2021
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2054
2022
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2072,16 +2040,17 @@ class MidyGM2 {
2072
2040
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2073
2041
  }
2074
2042
  else {
2075
- this.processScheduledNotes(channel, (note) => {
2043
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2076
2044
  if (note.voiceParams.reverbEffectsSend <= 0)
2077
2045
  return false;
2078
- note.reverbEffectsSend.disconnect();
2046
+ if (note.reverbEffectsSend)
2047
+ note.reverbEffectsSend.disconnect();
2079
2048
  });
2080
2049
  }
2081
2050
  }
2082
2051
  else {
2083
2052
  if (0 < reverbSendLevel) {
2084
- this.processScheduledNotes(channel, (note) => {
2053
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2085
2054
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2086
2055
  });
2087
2056
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2104,16 +2073,17 @@ class MidyGM2 {
2104
2073
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2105
2074
  }
2106
2075
  else {
2107
- this.processScheduledNotes(channel, (note) => {
2076
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2108
2077
  if (note.voiceParams.chorusEffectsSend <= 0)
2109
2078
  return false;
2110
- note.chorusEffectsSend.disconnect();
2079
+ if (note.chorusEffectsSend)
2080
+ note.chorusEffectsSend.disconnect();
2111
2081
  });
2112
2082
  }
2113
2083
  }
2114
2084
  else {
2115
2085
  if (0 < chorusSendLevel) {
2116
- this.processScheduledNotes(channel, (note) => {
2086
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2117
2087
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2118
2088
  });
2119
2089
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2497,8 +2467,7 @@ class MidyGM2 {
2497
2467
  setReverbType(type) {
2498
2468
  this.reverb.time = this.getReverbTimeFromType(type);
2499
2469
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2500
- const { audioContext, options } = this;
2501
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2470
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2502
2471
  }
2503
2472
  getReverbTimeFromType(type) {
2504
2473
  switch (type) {
@@ -2520,8 +2489,7 @@ class MidyGM2 {
2520
2489
  }
2521
2490
  setReverbTime(value) {
2522
2491
  this.reverb.time = this.getReverbTime(value);
2523
- const { audioContext, options } = this;
2524
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2492
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2525
2493
  }
2526
2494
  getReverbTime(value) {
2527
2495
  return Math.exp((value - 40) * 0.025);
@@ -2692,50 +2660,60 @@ class MidyGM2 {
2692
2660
  }
2693
2661
  }
2694
2662
  getFilterCutoffControl(channel) {
2695
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2696
- channel.state.channelPressure;
2663
+ const channelPressureRaw = channel.channelPressureTable[1];
2664
+ const channelPressure = (0 <= channelPressureRaw)
2665
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2666
+ : 0;
2697
2667
  return channelPressure * 15;
2698
2668
  }
2699
2669
  getAmplitudeControl(channel) {
2700
- const channelPressure = channel.channelPressureTable[2] *
2701
- channel.state.channelPressure;
2670
+ const channelPressureRaw = channel.channelPressureTable[2];
2671
+ const channelPressure = (0 <= channelPressureRaw)
2672
+ ? channelPressureRaw * channel.state.channelPressure
2673
+ : 0;
2702
2674
  return channelPressure / 64;
2703
2675
  }
2704
2676
  getLFOPitchDepth(channel) {
2705
- const channelPressure = channel.channelPressureTable[3] *
2706
- channel.state.channelPressure;
2677
+ const channelPressureRaw = channel.channelPressureTable[3];
2678
+ const channelPressure = (0 <= channelPressureRaw)
2679
+ ? channelPressureRaw * channel.state.channelPressure
2680
+ : 0;
2707
2681
  return channelPressure / 127 * 600;
2708
2682
  }
2709
2683
  getLFOFilterDepth(channel) {
2710
- const channelPressure = channel.channelPressureTable[4] *
2711
- channel.state.channelPressure;
2684
+ const channelPressureRaw = channel.channelPressureTable[4];
2685
+ const channelPressure = (0 <= channelPressureRaw)
2686
+ ? channelPressureRaw * channel.state.channelPressure
2687
+ : 0;
2712
2688
  return channelPressure / 127 * 2400;
2713
2689
  }
2714
2690
  getLFOAmplitudeDepth(channel) {
2715
- const channelPressure = channel.channelPressureTable[5] *
2716
- channel.state.channelPressure;
2691
+ const channelPressureRaw = channel.channelPressureTable[5];
2692
+ const channelPressure = (0 <= channelPressureRaw)
2693
+ ? channelPressureRaw * channel.state.channelPressure
2694
+ : 0;
2717
2695
  return channelPressure / 127;
2718
2696
  }
2719
2697
  setControllerParameters(channel, note, table) {
2720
- if (table[0] !== 64)
2698
+ if (0 <= table[0])
2721
2699
  this.updateDetune(channel, note);
2722
2700
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2723
- if (table[1] !== 64)
2701
+ if (0 <= table[1])
2724
2702
  this.setPortamentoFilterEnvelope(channel, note);
2725
- if (table[2] !== 64)
2703
+ if (0 <= table[2])
2726
2704
  this.setPortamentoVolumeEnvelope(channel, note);
2727
2705
  }
2728
2706
  else {
2729
- if (table[1] !== 64)
2707
+ if (0 <= table[1])
2730
2708
  this.setFilterEnvelope(channel, note);
2731
- if (table[2] !== 64)
2709
+ if (0 <= table[2])
2732
2710
  this.setVolumeEnvelope(channel, note);
2733
2711
  }
2734
- if (table[3] !== 0)
2712
+ if (0 <= table[3])
2735
2713
  this.setModLfoToPitch(channel, note);
2736
- if (table[4] !== 0)
2714
+ if (0 <= table[4])
2737
2715
  this.setModLfoToFilterFc(channel, note);
2738
- if (table[5] !== 0)
2716
+ if (0 <= table[5])
2739
2717
  this.setModLfoToVolume(channel, note);
2740
2718
  }
2741
2719
  handlePressureSysEx(data, tableName) {
@@ -2751,26 +2729,15 @@ class MidyGM2 {
2751
2729
  }
2752
2730
  }
2753
2731
  initControlTable() {
2754
- const channelCount = 128;
2732
+ const ccCount = 128;
2755
2733
  const slotSize = 6;
2756
- const table = new Uint8Array(channelCount * slotSize);
2757
- return this.resetControlTable(table);
2734
+ return new Int8Array(ccCount * slotSize).fill(-1);
2758
2735
  }
2759
- resetControlTable(table) {
2760
- const channelCount = 128;
2761
- const slotSize = 6;
2762
- const defaultValues = [64, 64, 64, 0, 0, 0];
2763
- for (let ch = 0; ch < channelCount; ch++) {
2764
- const offset = ch * slotSize;
2765
- table.set(defaultValues, offset);
2766
- }
2767
- return table;
2768
- }
2769
- applyControlTable(channel, controllerType) {
2736
+ applyControlTable(channel, controllerType, scheduleTime) {
2770
2737
  const slotSize = 6;
2771
2738
  const offset = controllerType * slotSize;
2772
2739
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2773
- this.processScheduledNotes(channel, (note) => {
2740
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2774
2741
  this.setControllerParameters(channel, note, table);
2775
2742
  });
2776
2743
  }
@@ -2790,12 +2757,12 @@ class MidyGM2 {
2790
2757
  getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2791
2758
  const index = keyNumber * 128 + controllerType;
2792
2759
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2793
- return (controlValue + 64) / 64;
2760
+ return controlValue;
2794
2761
  }
2795
2762
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2796
2763
  const channelNumber = data[4];
2797
2764
  const channel = this.channels[channelNumber];
2798
- if (channel.isDrum)
2765
+ if (!channel.isDrum)
2799
2766
  return;
2800
2767
  const keyNumber = data[5];
2801
2768
  const table = channel.keyBasedInstrumentControlTable;
@@ -2803,7 +2770,7 @@ class MidyGM2 {
2803
2770
  const controllerType = data[i];
2804
2771
  const value = data[i + 1];
2805
2772
  const index = keyNumber * 128 + controllerType;
2806
- table[index] = value - 64;
2773
+ table[index] = value;
2807
2774
  }
2808
2775
  this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2809
2776
  }