@marmooo/midy 0.3.3 → 0.3.5

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.
@@ -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
  }
@@ -326,13 +317,13 @@ class MidyGM2 {
326
317
  writable: true,
327
318
  value: this.initSoundFontTable()
328
319
  });
329
- Object.defineProperty(this, "audioBufferCounter", {
320
+ Object.defineProperty(this, "voiceCounter", {
330
321
  enumerable: true,
331
322
  configurable: true,
332
323
  writable: true,
333
324
  value: new Map()
334
325
  });
335
- Object.defineProperty(this, "audioBufferCache", {
326
+ Object.defineProperty(this, "voiceCache", {
336
327
  enumerable: true,
337
328
  configurable: true,
338
329
  writable: true,
@@ -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);
@@ -452,13 +420,11 @@ class MidyGM2 {
452
420
  const presetHeaders = soundFont.parsed.presetHeaders;
453
421
  for (let i = 0; i < presetHeaders.length; i++) {
454
422
  const presetHeader = presetHeaders[i];
455
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
456
- const banks = this.soundFontTable[presetHeader.preset];
457
- banks.set(presetHeader.bank, index);
458
- }
423
+ const banks = this.soundFontTable[presetHeader.preset];
424
+ banks.set(presetHeader.bank, index);
459
425
  }
460
426
  }
461
- async loadSoundFont(input) {
427
+ async toUint8Array(input) {
462
428
  let uint8Array;
463
429
  if (typeof input === "string") {
464
430
  const response = await fetch(input);
@@ -471,23 +437,32 @@ class MidyGM2 {
471
437
  else {
472
438
  throw new TypeError("input must be a URL string or Uint8Array");
473
439
  }
474
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
475
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
476
- this.addSoundFont(soundFont);
440
+ return uint8Array;
477
441
  }
478
- async loadMIDI(input) {
479
- let uint8Array;
480
- if (typeof input === "string") {
481
- const response = await fetch(input);
482
- const arrayBuffer = await response.arrayBuffer();
483
- uint8Array = new Uint8Array(arrayBuffer);
484
- }
485
- else if (input instanceof Uint8Array) {
486
- uint8Array = input;
442
+ async loadSoundFont(input) {
443
+ this.voiceCounter.clear();
444
+ if (Array.isArray(input)) {
445
+ const promises = new Array(input.length);
446
+ for (let i = 0; i < input.length; i++) {
447
+ promises[i] = this.toUint8Array(input[i]);
448
+ }
449
+ const uint8Arrays = await Promise.all(promises);
450
+ for (let i = 0; i < uint8Arrays.length; i++) {
451
+ const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
452
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
453
+ this.addSoundFont(soundFont);
454
+ }
487
455
  }
488
456
  else {
489
- throw new TypeError("input must be a URL string or Uint8Array");
457
+ const uint8Array = await this.toUint8Array(input);
458
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
459
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
460
+ this.addSoundFont(soundFont);
490
461
  }
462
+ }
463
+ async loadMIDI(input) {
464
+ this.voiceCounter.clear();
465
+ const uint8Array = await this.toUint8Array(input);
491
466
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
492
467
  this.ticksPerBeat = midi.header.ticksPerBeat;
493
468
  const midiData = this.extractMidiData(midi);
@@ -495,7 +470,46 @@ class MidyGM2 {
495
470
  this.timeline = midiData.timeline;
496
471
  this.totalTime = this.calcTotalTime();
497
472
  }
498
- setChannelAudioNodes(audioContext) {
473
+ cacheVoiceIds() {
474
+ const timeline = this.timeline;
475
+ for (let i = 0; i < timeline.length; i++) {
476
+ const event = timeline[i];
477
+ switch (event.type) {
478
+ case "noteOn": {
479
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
480
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
481
+ break;
482
+ }
483
+ case "controller":
484
+ if (event.controllerType === 0) {
485
+ this.setBankMSB(event.channel, event.value);
486
+ }
487
+ else if (event.controllerType === 32) {
488
+ this.setBankLSB(event.channel, event.value);
489
+ }
490
+ break;
491
+ case "programChange":
492
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
493
+ }
494
+ }
495
+ for (const [audioBufferId, count] of this.voiceCounter) {
496
+ if (count === 1)
497
+ this.voiceCounter.delete(audioBufferId);
498
+ }
499
+ this.GM2SystemOn();
500
+ }
501
+ getVoiceId(channel, noteNumber, velocity) {
502
+ const bankNumber = this.calcBank(channel);
503
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
504
+ .get(bankNumber);
505
+ if (soundFontIndex === undefined)
506
+ return;
507
+ const soundFont = this.soundFonts[soundFontIndex];
508
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
509
+ const { instrument, sampleID } = voice.generators;
510
+ return `${soundFontIndex}:${instrument}:${sampleID}`;
511
+ }
512
+ createChannelAudioNodes(audioContext) {
499
513
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
500
514
  const gainL = new GainNode(audioContext, { gain: gainLeft });
501
515
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -510,10 +524,9 @@ class MidyGM2 {
510
524
  };
511
525
  }
512
526
  resetChannelTable(channel) {
513
- this.resetControlTable(channel.controlTable);
527
+ channel.controlTable.fill(-1);
514
528
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
515
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
516
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
529
+ channel.channelPressureTable.fill(-1);
517
530
  channel.keyBasedInstrumentControlTable.fill(-1);
518
531
  }
519
532
  createChannels(audioContext) {
@@ -523,46 +536,26 @@ class MidyGM2 {
523
536
  isDrum: false,
524
537
  state: new ControllerState(),
525
538
  ...this.constructor.channelSettings,
526
- ...this.setChannelAudioNodes(audioContext),
539
+ ...this.createChannelAudioNodes(audioContext),
527
540
  scheduledNotes: [],
528
541
  sustainNotes: [],
529
542
  sostenutoNotes: [],
530
543
  controlTable: this.initControlTable(),
531
544
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
532
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
545
+ channelPressureTable: new Int8Array(6).fill(-1),
533
546
  keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
547
+ keyBasedGainLs: new Array(128),
548
+ keyBasedGainRs: new Array(128),
534
549
  };
535
550
  });
536
551
  return channels;
537
552
  }
538
- async createNoteBuffer(voiceParams, isSF3) {
553
+ async createAudioBuffer(voiceParams) {
554
+ const sample = voiceParams.sample;
539
555
  const sampleStart = voiceParams.start;
540
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
541
- if (isSF3) {
542
- const sample = voiceParams.sample;
543
- const start = sample.byteOffset + sampleStart;
544
- const end = sample.byteOffset + sampleEnd;
545
- const buffer = sample.buffer.slice(start, end);
546
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
547
- return audioBuffer;
548
- }
549
- else {
550
- const sample = voiceParams.sample;
551
- const start = sample.byteOffset + sampleStart;
552
- const end = sample.byteOffset + sampleEnd;
553
- const buffer = sample.buffer.slice(start, end);
554
- const audioBuffer = new AudioBuffer({
555
- numberOfChannels: 1,
556
- length: sample.length,
557
- sampleRate: voiceParams.sampleRate,
558
- });
559
- const channelData = audioBuffer.getChannelData(0);
560
- const int16Array = new Int16Array(buffer);
561
- for (let i = 0; i < int16Array.length; i++) {
562
- channelData[i] = int16Array[i] / 32768;
563
- }
564
- return audioBuffer;
565
- }
556
+ const sampleEnd = sample.data.length + voiceParams.end;
557
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
558
+ return audioBuffer;
566
559
  }
567
560
  isLoopDrum(channel, noteNumber) {
568
561
  const programNumber = channel.programNumber;
@@ -572,10 +565,9 @@ class MidyGM2 {
572
565
  createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
573
566
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
574
567
  bufferSource.buffer = audioBuffer;
575
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
576
- if (channel.isDrum) {
577
- bufferSource.loop = this.isLoopDrum(channel, noteNumber);
578
- }
568
+ bufferSource.loop = channel.isDrum
569
+ ? this.isLoopDrum(channel, noteNumber)
570
+ : (voiceParams.sampleModes % 2 !== 0);
579
571
  if (bufferSource.loop) {
580
572
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
581
573
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -600,13 +592,13 @@ class MidyGM2 {
600
592
  break;
601
593
  }
602
594
  case "controller":
603
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
595
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
604
596
  break;
605
597
  case "programChange":
606
- this.handleProgramChange(event.channel, event.programNumber, startTime);
598
+ this.setProgramChange(event.channel, event.programNumber, startTime);
607
599
  break;
608
600
  case "channelAftertouch":
609
- this.handleChannelPressure(event.channel, event.amount, startTime);
601
+ this.setChannelPressure(event.channel, event.amount, startTime);
610
602
  break;
611
603
  case "pitchBend":
612
604
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -640,8 +632,9 @@ class MidyGM2 {
640
632
  this.notePromises = [];
641
633
  this.exclusiveClassNotes.fill(undefined);
642
634
  this.drumExclusiveClassNotes.fill(undefined);
643
- this.audioBufferCache.clear();
635
+ this.voiceCache.clear();
644
636
  for (let i = 0; i < this.channels.length; i++) {
637
+ this.channels[i].scheduledNotes = [];
645
638
  this.resetAllStates(i);
646
639
  }
647
640
  resolve();
@@ -663,8 +656,9 @@ class MidyGM2 {
663
656
  this.notePromises = [];
664
657
  this.exclusiveClassNotes.fill(undefined);
665
658
  this.drumExclusiveClassNotes.fill(undefined);
666
- this.audioBufferCache.clear();
659
+ this.voiceCache.clear();
667
660
  for (let i = 0; i < this.channels.length; i++) {
661
+ this.channels[i].scheduledNotes = [];
668
662
  this.resetAllStates(i);
669
663
  }
670
664
  this.isStopping = false;
@@ -697,11 +691,7 @@ class MidyGM2 {
697
691
  secondToTicks(second, secondsPerBeat) {
698
692
  return second * this.ticksPerBeat / secondsPerBeat;
699
693
  }
700
- getAudioBufferId(programNumber, noteNumber, velocity) {
701
- return `${programNumber}:${noteNumber}:${velocity}`;
702
- }
703
694
  extractMidiData(midi) {
704
- this.audioBufferCounter.clear();
705
695
  const instruments = new Set();
706
696
  const timeline = [];
707
697
  const tmpChannels = new Array(this.channels.length);
@@ -722,8 +712,6 @@ class MidyGM2 {
722
712
  switch (event.type) {
723
713
  case "noteOn": {
724
714
  const channel = tmpChannels[event.channel];
725
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
726
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
727
715
  if (channel.programNumber < 0) {
728
716
  channel.programNumber = event.programNumber;
729
717
  switch (channel.bankMSB) {
@@ -773,10 +761,6 @@ class MidyGM2 {
773
761
  timeline.push(event);
774
762
  }
775
763
  }
776
- for (const [audioBufferId, count] of this.audioBufferCounter) {
777
- if (count === 1)
778
- this.audioBufferCounter.delete(audioBufferId);
779
- }
780
764
  const priority = {
781
765
  controller: 0,
782
766
  sysEx: 1,
@@ -821,7 +805,6 @@ class MidyGM2 {
821
805
  this.notePromises.push(promise);
822
806
  promises.push(promise);
823
807
  });
824
- channel.scheduledNotes = [];
825
808
  return Promise.all(promises);
826
809
  }
827
810
  stopNotes(velocity, force, scheduleTime) {
@@ -835,6 +818,8 @@ class MidyGM2 {
835
818
  if (this.isPlaying || this.isPaused)
836
819
  return;
837
820
  this.resumeTime = 0;
821
+ if (this.voiceCounter.size === 0)
822
+ this.cacheVoiceIds();
838
823
  await this.playNotes();
839
824
  this.isPlaying = false;
840
825
  }
@@ -877,7 +862,7 @@ class MidyGM2 {
877
862
  }
878
863
  processScheduledNotes(channel, callback) {
879
864
  const scheduledNotes = channel.scheduledNotes;
880
- for (let i = 0; i < scheduledNotes.length; i++) {
865
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
881
866
  const note = scheduledNotes[i];
882
867
  if (!note)
883
868
  continue;
@@ -888,14 +873,14 @@ class MidyGM2 {
888
873
  }
889
874
  processActiveNotes(channel, scheduleTime, callback) {
890
875
  const scheduledNotes = channel.scheduledNotes;
891
- for (let i = 0; i < scheduledNotes.length; i++) {
876
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
892
877
  const note = scheduledNotes[i];
893
878
  if (!note)
894
879
  continue;
895
880
  if (note.ending)
896
881
  continue;
897
882
  if (scheduleTime < note.startTime)
898
- continue;
883
+ break;
899
884
  callback(note);
900
885
  }
901
886
  }
@@ -984,6 +969,22 @@ class MidyGM2 {
984
969
  const output = allpasses.at(-1);
985
970
  return { input, output };
986
971
  }
972
+ createReverbEffect(audioContext) {
973
+ const { algorithm, time: rt60, feedback } = this.reverb;
974
+ switch (algorithm) {
975
+ case "ConvolutionReverb": {
976
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
977
+ return this.createConvolutionReverb(audioContext, impulse);
978
+ }
979
+ case "SchroederReverb": {
980
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
981
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
982
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
983
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
984
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
985
+ }
986
+ }
987
+ }
987
988
  createChorusEffect(audioContext) {
988
989
  const input = new GainNode(audioContext);
989
990
  const output = new GainNode(audioContext);
@@ -1048,9 +1049,16 @@ class MidyGM2 {
1048
1049
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1049
1050
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1050
1051
  const pitch = pitchWheel * pitchWheelSensitivity;
1051
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1052
- const pressure = pressureDepth * channel.state.channelPressure;
1053
- return tuning + pitch + pressure;
1052
+ const channelPressureRaw = channel.channelPressureTable[0];
1053
+ if (0 <= channelPressureRaw) {
1054
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1055
+ const channelPressure = channelPressureDepth *
1056
+ channel.state.channelPressure;
1057
+ return tuning + pitch + channelPressure;
1058
+ }
1059
+ else {
1060
+ return tuning + pitch;
1061
+ }
1054
1062
  }
1055
1063
  calcNoteDetune(channel, note) {
1056
1064
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -1277,35 +1285,32 @@ class MidyGM2 {
1277
1285
  note.vibratoLFO.connect(note.vibratoDepth);
1278
1286
  note.vibratoDepth.connect(note.bufferSource.detune);
1279
1287
  }
1280
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1281
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1282
- const cache = this.audioBufferCache.get(audioBufferId);
1288
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1289
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1290
+ const cache = this.voiceCache.get(audioBufferId);
1283
1291
  if (cache) {
1284
1292
  cache.counter += 1;
1285
1293
  if (cache.maxCount <= cache.counter) {
1286
- this.audioBufferCache.delete(audioBufferId);
1294
+ this.voiceCache.delete(audioBufferId);
1287
1295
  }
1288
1296
  return cache.audioBuffer;
1289
1297
  }
1290
1298
  else {
1291
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1292
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1299
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1300
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1293
1301
  const cache = { audioBuffer, maxCount, counter: 1 };
1294
- this.audioBufferCache.set(audioBufferId, cache);
1302
+ this.voiceCache.set(audioBufferId, cache);
1295
1303
  return audioBuffer;
1296
1304
  }
1297
1305
  }
1298
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1306
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1299
1307
  const now = this.audioContext.currentTime;
1300
1308
  const state = channel.state;
1301
1309
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1302
1310
  const voiceParams = voice.getAllParams(controllerState);
1303
1311
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1304
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1312
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1305
1313
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1306
- note.volumeNode = new GainNode(this.audioContext);
1307
- note.gainL = new GainNode(this.audioContext);
1308
- note.gainR = new GainNode(this.audioContext);
1309
1314
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1310
1315
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1311
1316
  type: "lowpass",
@@ -1338,9 +1343,6 @@ class MidyGM2 {
1338
1343
  }
1339
1344
  note.bufferSource.connect(note.filterNode);
1340
1345
  note.filterNode.connect(note.volumeEnvelopeNode);
1341
- note.volumeEnvelopeNode.connect(note.volumeNode);
1342
- note.volumeNode.connect(note.gainL);
1343
- note.volumeNode.connect(note.gainR);
1344
1346
  if (0 < state.chorusSendLevel) {
1345
1347
  this.setChorusEffectsSend(channel, note, 0, now);
1346
1348
  }
@@ -1399,28 +1401,30 @@ class MidyGM2 {
1399
1401
  }
1400
1402
  this.drumExclusiveClassNotes[index] = note;
1401
1403
  }
1402
- isDrumNoteOffException(channel, noteNumber) {
1403
- if (!channel.isDrum)
1404
- return false;
1405
- const programNumber = channel.programNumber;
1406
- return !((programNumber === 48 && noteNumber === 88) ||
1407
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1408
- }
1409
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1404
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1410
1405
  const channel = this.channels[channelNumber];
1411
1406
  const bankNumber = this.calcBank(channel, channelNumber);
1412
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1407
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1408
+ .get(bankNumber);
1413
1409
  if (soundFontIndex === undefined)
1414
1410
  return;
1415
1411
  const soundFont = this.soundFonts[soundFontIndex];
1416
1412
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1417
1413
  if (!voice)
1418
1414
  return;
1419
- const isSF3 = soundFont.parsed.info.version.major === 3;
1420
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1421
- note.noteOffEvent = noteOffEvent;
1422
- note.gainL.connect(channel.gainL);
1423
- note.gainR.connect(channel.gainR);
1415
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1416
+ if (channel.isDrum) {
1417
+ const audioContext = this.audioContext;
1418
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1419
+ channel.keyBasedGainLs[noteNumber] = gainL;
1420
+ channel.keyBasedGainRs[noteNumber] = gainR;
1421
+ note.volumeEnvelopeNode.connect(gainL);
1422
+ note.volumeEnvelopeNode.connect(gainR);
1423
+ }
1424
+ else {
1425
+ note.volumeEnvelopeNode.connect(channel.gainL);
1426
+ note.volumeEnvelopeNode.connect(channel.gainR);
1427
+ }
1424
1428
  if (0.5 <= channel.state.sustainPedal) {
1425
1429
  channel.sustainNotes.push(note);
1426
1430
  }
@@ -1438,9 +1442,6 @@ class MidyGM2 {
1438
1442
  note.bufferSource.disconnect();
1439
1443
  note.filterNode.disconnect();
1440
1444
  note.volumeEnvelopeNode.disconnect();
1441
- note.volumeNode.disconnect();
1442
- note.gainL.disconnect();
1443
- note.gainR.disconnect();
1444
1445
  if (note.modulationDepth) {
1445
1446
  note.volumeDepth.disconnect();
1446
1447
  note.modulationDepth.disconnect();
@@ -1493,15 +1494,29 @@ class MidyGM2 {
1493
1494
  return;
1494
1495
  }
1495
1496
  }
1496
- const note = this.findNoteOffTarget(channel, noteNumber);
1497
- if (!note)
1497
+ const index = this.findNoteOffIndex(channel, noteNumber);
1498
+ if (index < 0)
1498
1499
  return;
1500
+ const note = channel.scheduledNotes[index];
1499
1501
  note.ending = true;
1502
+ this.setNoteIndex(channel, index);
1500
1503
  this.releaseNote(channel, note, endTime);
1501
1504
  }
1502
- findNoteOffTarget(channel, noteNumber) {
1505
+ setNoteIndex(channel, index) {
1506
+ let allEnds = true;
1507
+ for (let i = channel.scheduleIndex; i < index; i++) {
1508
+ const note = channel.scheduledNotes[i];
1509
+ if (note && !note.ending) {
1510
+ allEnds = false;
1511
+ break;
1512
+ }
1513
+ }
1514
+ if (allEnds)
1515
+ channel.scheduleIndex = index + 1;
1516
+ }
1517
+ findNoteOffIndex(channel, noteNumber) {
1503
1518
  const scheduledNotes = channel.scheduledNotes;
1504
- for (let i = 0; i < scheduledNotes.length; i++) {
1519
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1505
1520
  const note = scheduledNotes[i];
1506
1521
  if (!note)
1507
1522
  continue;
@@ -1509,8 +1524,9 @@ class MidyGM2 {
1509
1524
  continue;
1510
1525
  if (note.noteNumber !== noteNumber)
1511
1526
  continue;
1512
- return note;
1527
+ return i;
1513
1528
  }
1529
+ return -1;
1514
1530
  }
1515
1531
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1516
1532
  scheduleTime ??= this.audioContext.currentTime;
@@ -1550,18 +1566,18 @@ class MidyGM2 {
1550
1566
  case 0x90:
1551
1567
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1552
1568
  case 0xB0:
1553
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1569
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1554
1570
  case 0xC0:
1555
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1571
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1556
1572
  case 0xD0:
1557
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1573
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1558
1574
  case 0xE0:
1559
1575
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1560
1576
  default:
1561
1577
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1562
1578
  }
1563
1579
  }
1564
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1580
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1565
1581
  const channel = this.channels[channelNumber];
1566
1582
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1567
1583
  channel.programNumber = programNumber;
@@ -1576,16 +1592,17 @@ class MidyGM2 {
1576
1592
  }
1577
1593
  }
1578
1594
  }
1579
- handleChannelPressure(channelNumber, value, scheduleTime) {
1595
+ setChannelPressure(channelNumber, value, scheduleTime) {
1580
1596
  const channel = this.channels[channelNumber];
1581
1597
  if (channel.isDrum)
1582
1598
  return;
1583
1599
  const prev = channel.state.channelPressure;
1584
1600
  const next = value / 127;
1585
1601
  channel.state.channelPressure = next;
1586
- if (channel.channelPressureTable[0] !== 64) {
1587
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1588
- channel.detune += pressureDepth * (next - prev);
1602
+ const channelPressureRaw = channel.channelPressureTable[0];
1603
+ if (0 <= channelPressureRaw) {
1604
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1605
+ channel.detune += channelPressureDepth * (next - prev);
1589
1606
  }
1590
1607
  const table = channel.channelPressureTable;
1591
1608
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1645,10 +1662,12 @@ class MidyGM2 {
1645
1662
  .setValueAtTime(volumeDepth, scheduleTime);
1646
1663
  }
1647
1664
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1648
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1649
1665
  let value = note.voiceParams.reverbEffectsSend;
1650
- if (0 <= keyBasedValue) {
1651
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1666
+ if (channel.isDrum) {
1667
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1668
+ if (0 <= keyBasedValue) {
1669
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1670
+ }
1652
1671
  }
1653
1672
  if (0 < prevValue) {
1654
1673
  if (0 < value) {
@@ -1673,13 +1692,15 @@ class MidyGM2 {
1673
1692
  }
1674
1693
  }
1675
1694
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1676
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1677
1695
  let value = note.voiceParams.chorusEffectsSend;
1678
- if (0 <= keyBasedValue) {
1679
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1696
+ if (channel.isDrum) {
1697
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1698
+ if (0 <= keyBasedValue) {
1699
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1700
+ }
1680
1701
  }
1681
1702
  if (0 < prevValue) {
1682
- if (0 < vaule) {
1703
+ if (0 < value) {
1683
1704
  note.chorusEffectsSend.gain
1684
1705
  .cancelScheduledValues(scheduleTime)
1685
1706
  .setValueAtTime(value, scheduleTime);
@@ -1694,7 +1715,7 @@ class MidyGM2 {
1694
1715
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1695
1716
  gain: value,
1696
1717
  });
1697
- note.volumeNode.connect(note.chorusEffectsSend);
1718
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1698
1719
  }
1699
1720
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1700
1721
  }
@@ -1781,8 +1802,9 @@ class MidyGM2 {
1781
1802
  this.processScheduledNotes(channel, (note) => {
1782
1803
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1783
1804
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1784
- let appliedFilterEnvelope = false;
1785
- let appliedVolumeEnvelope = false;
1805
+ let applyVolumeEnvelope = false;
1806
+ let applyFilterEnvelope = false;
1807
+ let applyPitchEnvelope = false;
1786
1808
  for (const [key, value] of Object.entries(voiceParams)) {
1787
1809
  const prevValue = note.voiceParams[key];
1788
1810
  if (value === prevValue)
@@ -1791,37 +1813,23 @@ class MidyGM2 {
1791
1813
  if (key in this.voiceParamsHandlers) {
1792
1814
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1793
1815
  }
1794
- else if (filterEnvelopeKeySet.has(key)) {
1795
- if (appliedFilterEnvelope)
1796
- continue;
1797
- appliedFilterEnvelope = true;
1798
- const noteVoiceParams = note.voiceParams;
1799
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1800
- const key = filterEnvelopeKeys[i];
1801
- if (key in voiceParams)
1802
- noteVoiceParams[key] = voiceParams[key];
1803
- }
1804
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1805
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1806
- }
1807
- else {
1808
- this.setFilterEnvelope(channel, note, scheduleTime);
1809
- }
1810
- this.setPitchEnvelope(note, scheduleTime);
1811
- }
1812
- else if (volumeEnvelopeKeySet.has(key)) {
1813
- if (appliedVolumeEnvelope)
1814
- continue;
1815
- appliedVolumeEnvelope = true;
1816
- const noteVoiceParams = note.voiceParams;
1817
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1818
- const key = volumeEnvelopeKeys[i];
1819
- if (key in voiceParams)
1820
- noteVoiceParams[key] = voiceParams[key];
1821
- }
1822
- this.setVolumeEnvelope(channel, note, scheduleTime);
1816
+ else {
1817
+ if (volumeEnvelopeKeySet.has(key))
1818
+ applyVolumeEnvelope = true;
1819
+ if (filterEnvelopeKeySet.has(key))
1820
+ applyFilterEnvelope = true;
1821
+ if (pitchEnvelopeKeySet.has(key))
1822
+ applyPitchEnvelope = true;
1823
1823
  }
1824
1824
  }
1825
+ if (applyVolumeEnvelope) {
1826
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1827
+ }
1828
+ if (applyFilterEnvelope) {
1829
+ this.setFilterEnvelope(channel, note, scheduleTime);
1830
+ }
1831
+ if (applyPitchEnvelope)
1832
+ this.setPitchEnvelope(note, scheduleTime);
1825
1833
  });
1826
1834
  }
1827
1835
  createControlChangeHandlers() {
@@ -1852,13 +1860,13 @@ class MidyGM2 {
1852
1860
  handlers[127] = this.polyOn;
1853
1861
  return handlers;
1854
1862
  }
1855
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1863
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1856
1864
  const handler = this.controlChangeHandlers[controllerType];
1857
1865
  if (handler) {
1858
1866
  handler.call(this, channelNumber, value, scheduleTime);
1859
1867
  const channel = this.channels[channelNumber];
1860
1868
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1861
- this.applyControlTable(channel, controllerType);
1869
+ this.applyControlTable(channel, controllerType, scheduleTime);
1862
1870
  }
1863
1871
  else {
1864
1872
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1915,22 +1923,12 @@ class MidyGM2 {
1915
1923
  return;
1916
1924
  this.updatePortamento(channel, scheduleTime);
1917
1925
  }
1918
- setKeyBasedVolume(channel, scheduleTime) {
1919
- this.processScheduledNotes(channel, (note) => {
1920
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1921
- if (0 <= keyBasedValue) {
1922
- note.volumeNode.gain
1923
- .cancelScheduledValues(scheduleTime)
1924
- .setValueAtTime(keyBasedValue / 127, scheduleTime);
1925
- }
1926
- });
1927
- }
1928
1926
  setVolume(channelNumber, volume, scheduleTime) {
1929
1927
  scheduleTime ??= this.audioContext.currentTime;
1930
1928
  const channel = this.channels[channelNumber];
1931
1929
  channel.state.volume = volume / 127;
1932
1930
  this.updateChannelVolume(channel, scheduleTime);
1933
- this.setKeyBasedVolume(channel, scheduleTime);
1931
+ this.updateKeyBasedVolume(channel, scheduleTime);
1934
1932
  }
1935
1933
  panToGain(pan) {
1936
1934
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1939,26 +1937,12 @@ class MidyGM2 {
1939
1937
  gainRight: Math.sin(theta),
1940
1938
  };
1941
1939
  }
1942
- setKeyBasedPan(channel, scheduleTime) {
1943
- this.processScheduledNotes(channel, (note) => {
1944
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1945
- if (0 <= keyBasedValue) {
1946
- const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1947
- note.gainL.gain
1948
- .cancelScheduledValues(scheduleTime)
1949
- .setValueAtTime(gainLeft, scheduleTime);
1950
- note.gainR.gain
1951
- .cancelScheduledValues(scheduleTime)
1952
- .setValueAtTime(gainRight, scheduleTime);
1953
- }
1954
- });
1955
- }
1956
1940
  setPan(channelNumber, pan, scheduleTime) {
1957
1941
  scheduleTime ??= this.audioContext.currentTime;
1958
1942
  const channel = this.channels[channelNumber];
1959
1943
  channel.state.pan = pan / 127;
1960
1944
  this.updateChannelVolume(channel, scheduleTime);
1961
- this.setKeyBasedPan(channel, scheduleTime);
1945
+ this.updateKeyBasedVolume(channel, scheduleTime);
1962
1946
  }
1963
1947
  setExpression(channelNumber, expression, scheduleTime) {
1964
1948
  scheduleTime ??= this.audioContext.currentTime;
@@ -1984,6 +1968,34 @@ class MidyGM2 {
1984
1968
  .cancelScheduledValues(scheduleTime)
1985
1969
  .setValueAtTime(volume * gainRight, scheduleTime);
1986
1970
  }
1971
+ updateKeyBasedVolume(channel, scheduleTime) {
1972
+ if (!channel.isDrum)
1973
+ return;
1974
+ const state = channel.state;
1975
+ const defaultVolume = state.volume * state.expression;
1976
+ const defaultPan = state.pan;
1977
+ for (let i = 0; i < 128; i++) {
1978
+ const gainL = channel.keyBasedGainLs[i];
1979
+ const gainR = channel.keyBasedGainLs[i];
1980
+ if (!gainL)
1981
+ continue;
1982
+ if (!gainR)
1983
+ continue;
1984
+ const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
1985
+ const volume = (0 <= keyBasedVolume)
1986
+ ? defaultVolume * keyBasedVolume / 64
1987
+ : defaultVolume;
1988
+ const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
1989
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
1990
+ const { gainLeft, gainRight } = this.panToGain(pan);
1991
+ gainL.gain
1992
+ .cancelScheduledValues(scheduleTime)
1993
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1994
+ gainR.gain
1995
+ .cancelScheduledValues(scheduleTime)
1996
+ .setValueAtTime(volume * gainRight, scheduleTime);
1997
+ }
1998
+ }
1987
1999
  setSustainPedal(channelNumber, value, scheduleTime) {
1988
2000
  const channel = this.channels[channelNumber];
1989
2001
  if (channel.isDrum)
@@ -2245,7 +2257,7 @@ class MidyGM2 {
2245
2257
  const entries = Object.entries(defaultControllerState);
2246
2258
  for (const [key, { type, defaultValue }] of entries) {
2247
2259
  if (128 <= type) {
2248
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2260
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2249
2261
  }
2250
2262
  else {
2251
2263
  state[key] = defaultValue;
@@ -2277,7 +2289,7 @@ class MidyGM2 {
2277
2289
  const key = keys[i];
2278
2290
  const { type, defaultValue } = defaultControllerState[key];
2279
2291
  if (128 <= type) {
2280
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2292
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2281
2293
  }
2282
2294
  else {
2283
2295
  state[key] = defaultValue;
@@ -2485,8 +2497,7 @@ class MidyGM2 {
2485
2497
  setReverbType(type) {
2486
2498
  this.reverb.time = this.getReverbTimeFromType(type);
2487
2499
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2488
- const { audioContext, options } = this;
2489
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2500
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2490
2501
  }
2491
2502
  getReverbTimeFromType(type) {
2492
2503
  switch (type) {
@@ -2508,8 +2519,7 @@ class MidyGM2 {
2508
2519
  }
2509
2520
  setReverbTime(value) {
2510
2521
  this.reverb.time = this.getReverbTime(value);
2511
- const { audioContext, options } = this;
2512
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2522
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2513
2523
  }
2514
2524
  getReverbTime(value) {
2515
2525
  return Math.exp((value - 40) * 0.025);
@@ -2680,51 +2690,63 @@ class MidyGM2 {
2680
2690
  }
2681
2691
  }
2682
2692
  getFilterCutoffControl(channel) {
2683
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2684
- channel.state.channelPressure;
2693
+ const channelPressureRaw = channel.channelPressureTable[1];
2694
+ const channelPressure = (0 <= channelPressureRaw)
2695
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2696
+ : 0;
2685
2697
  return channelPressure * 15;
2686
2698
  }
2687
2699
  getAmplitudeControl(channel) {
2688
- const channelPressure = channel.channelPressureTable[2] *
2689
- channel.state.channelPressure;
2700
+ const channelPressureRaw = channel.channelPressureTable[2];
2701
+ const channelPressure = (0 <= channelPressureRaw)
2702
+ ? channelPressureRaw * channel.state.channelPressure
2703
+ : 0;
2690
2704
  return channelPressure / 64;
2691
2705
  }
2692
2706
  getLFOPitchDepth(channel) {
2693
- const channelPressure = channel.channelPressureTable[3] *
2694
- channel.state.channelPressure;
2707
+ const channelPressureRaw = channel.channelPressureTable[3];
2708
+ const channelPressure = (0 <= channelPressureRaw)
2709
+ ? channelPressureRaw * channel.state.channelPressure
2710
+ : 0;
2695
2711
  return channelPressure / 127 * 600;
2696
2712
  }
2697
2713
  getLFOFilterDepth(channel) {
2698
- const channelPressure = channel.channelPressureTable[4] *
2699
- channel.state.channelPressure;
2714
+ const channelPressureRaw = channel.channelPressureTable[4];
2715
+ const channelPressure = (0 <= channelPressureRaw)
2716
+ ? channelPressureRaw * channel.state.channelPressure
2717
+ : 0;
2700
2718
  return channelPressure / 127 * 2400;
2701
2719
  }
2702
2720
  getLFOAmplitudeDepth(channel) {
2703
- const channelPressure = channel.channelPressureTable[5] *
2704
- channel.state.channelPressure;
2721
+ const channelPressureRaw = channel.channelPressureTable[5];
2722
+ const channelPressure = (0 <= channelPressureRaw)
2723
+ ? channelPressureRaw * channel.state.channelPressure
2724
+ : 0;
2705
2725
  return channelPressure / 127;
2706
2726
  }
2707
- setControllerParameters(channel, note, table) {
2708
- if (table[0] !== 64)
2709
- this.updateDetune(channel, note);
2727
+ setControllerParameters(channel, note, table, scheduleTime) {
2728
+ if (0 <= table[0])
2729
+ this.updateDetune(channel, note, scueduleTime);
2710
2730
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2711
- if (table[1] !== 64)
2712
- this.setPortamentoFilterEnvelope(channel, note);
2713
- if (table[2] !== 64)
2714
- this.setPortamentoVolumeEnvelope(channel, note);
2731
+ if (0 <= table[1]) {
2732
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2733
+ }
2734
+ if (0 <= table[2]) {
2735
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2736
+ }
2715
2737
  }
2716
2738
  else {
2717
- if (table[1] !== 64)
2718
- this.setFilterEnvelope(channel, note);
2719
- if (table[2] !== 64)
2720
- this.setVolumeEnvelope(channel, note);
2721
- }
2722
- if (table[3] !== 0)
2723
- this.setModLfoToPitch(channel, note);
2724
- if (table[4] !== 0)
2725
- this.setModLfoToFilterFc(channel, note);
2726
- if (table[5] !== 0)
2727
- this.setModLfoToVolume(channel, note);
2739
+ if (0 <= table[1])
2740
+ this.setFilterEnvelope(channel, note, scheduleTime);
2741
+ if (0 <= table[2])
2742
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2743
+ }
2744
+ if (0 <= table[3])
2745
+ this.setModLfoToPitch(channel, note, scheduleTime);
2746
+ if (0 <= table[4])
2747
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2748
+ if (0 <= table[5])
2749
+ this.setModLfoToVolume(channel, note, scheduleTime);
2728
2750
  }
2729
2751
  handlePressureSysEx(data, tableName) {
2730
2752
  const channelNumber = data[4];
@@ -2739,27 +2761,16 @@ class MidyGM2 {
2739
2761
  }
2740
2762
  }
2741
2763
  initControlTable() {
2742
- const channelCount = 128;
2764
+ const ccCount = 128;
2743
2765
  const slotSize = 6;
2744
- const table = new Uint8Array(channelCount * slotSize);
2745
- return this.resetControlTable(table);
2766
+ return new Int8Array(ccCount * slotSize).fill(-1);
2746
2767
  }
2747
- resetControlTable(table) {
2748
- const channelCount = 128;
2749
- const slotSize = 6;
2750
- const defaultValues = [64, 64, 64, 0, 0, 0];
2751
- for (let ch = 0; ch < channelCount; ch++) {
2752
- const offset = ch * slotSize;
2753
- table.set(defaultValues, offset);
2754
- }
2755
- return table;
2756
- }
2757
- applyControlTable(channel, controllerType) {
2768
+ applyControlTable(channel, controllerType, scheduleTime) {
2758
2769
  const slotSize = 6;
2759
2770
  const offset = controllerType * slotSize;
2760
2771
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2761
2772
  this.processScheduledNotes(channel, (note) => {
2762
- this.setControllerParameters(channel, note, table);
2773
+ this.setControllerParameters(channel, note, table, scheduleTime);
2763
2774
  });
2764
2775
  }
2765
2776
  handleControlChangeSysEx(data) {
@@ -2775,7 +2786,7 @@ class MidyGM2 {
2775
2786
  table[pp] = rr;
2776
2787
  }
2777
2788
  }
2778
- getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2789
+ getKeyBasedValue(channel, keyNumber, controllerType) {
2779
2790
  const index = keyNumber * 128 + controllerType;
2780
2791
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2781
2792
  return controlValue;
@@ -2783,7 +2794,7 @@ class MidyGM2 {
2783
2794
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2784
2795
  const channelNumber = data[4];
2785
2796
  const channel = this.channels[channelNumber];
2786
- if (channel.isDrum)
2797
+ if (!channel.isDrum)
2787
2798
  return;
2788
2799
  const keyNumber = data[5];
2789
2800
  const table = channel.keyBasedInstrumentControlTable;
@@ -2793,7 +2804,7 @@ class MidyGM2 {
2793
2804
  const index = keyNumber * 128 + controllerType;
2794
2805
  table[index] = value;
2795
2806
  }
2796
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2807
+ this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2797
2808
  }
2798
2809
  handleSysEx(data, scheduleTime) {
2799
2810
  switch (data[0]) {
@@ -2831,6 +2842,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2831
2842
  configurable: true,
2832
2843
  writable: true,
2833
2844
  value: {
2845
+ scheduleIndex: 0,
2834
2846
  detune: 0,
2835
2847
  programNumber: 0,
2836
2848
  bank: 121 * 128,