@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.
package/esm/midy-GM2.js CHANGED
@@ -44,24 +44,6 @@ class Note {
44
44
  writable: true,
45
45
  value: void 0
46
46
  });
47
- Object.defineProperty(this, "volumeNode", {
48
- enumerable: true,
49
- configurable: true,
50
- writable: true,
51
- value: void 0
52
- });
53
- Object.defineProperty(this, "gainL", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: void 0
58
- });
59
- Object.defineProperty(this, "gainR", {
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- value: void 0
64
- });
65
47
  Object.defineProperty(this, "modulationLFO", {
66
48
  enumerable: true,
67
49
  configurable: true,
@@ -199,6 +181,16 @@ class ControllerState {
199
181
  }
200
182
  }
201
183
  }
184
+ const volumeEnvelopeKeys = [
185
+ "volDelay",
186
+ "volAttack",
187
+ "volHold",
188
+ "volDecay",
189
+ "volSustain",
190
+ "volRelease",
191
+ "initialAttenuation",
192
+ ];
193
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
202
194
  const filterEnvelopeKeys = [
203
195
  "modEnvToPitch",
204
196
  "initialFilterFc",
@@ -208,22 +200,20 @@ const filterEnvelopeKeys = [
208
200
  "modHold",
209
201
  "modDecay",
210
202
  "modSustain",
211
- "modRelease",
212
- "playbackRate",
213
203
  ];
214
204
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
215
- const volumeEnvelopeKeys = [
216
- "volDelay",
217
- "volAttack",
218
- "volHold",
219
- "volDecay",
220
- "volSustain",
221
- "volRelease",
222
- "initialAttenuation",
205
+ const pitchEnvelopeKeys = [
206
+ "modEnvToPitch",
207
+ "modDelay",
208
+ "modAttack",
209
+ "modHold",
210
+ "modDecay",
211
+ "modSustain",
212
+ "playbackRate",
223
213
  ];
224
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
214
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
225
215
  export class MidyGM2 {
226
- constructor(audioContext, options = this.defaultOptions) {
216
+ constructor(audioContext) {
227
217
  Object.defineProperty(this, "mode", {
228
218
  enumerable: true,
229
219
  configurable: true,
@@ -247,6 +237,7 @@ export class MidyGM2 {
247
237
  configurable: true,
248
238
  writable: true,
249
239
  value: {
240
+ algorithm: "SchroederReverb",
250
241
  time: this.getReverbTime(64),
251
242
  feedback: 0.8,
252
243
  }
@@ -323,13 +314,13 @@ export class MidyGM2 {
323
314
  writable: true,
324
315
  value: this.initSoundFontTable()
325
316
  });
326
- Object.defineProperty(this, "audioBufferCounter", {
317
+ Object.defineProperty(this, "voiceCounter", {
327
318
  enumerable: true,
328
319
  configurable: true,
329
320
  writable: true,
330
321
  value: new Map()
331
322
  });
332
- Object.defineProperty(this, "audioBufferCache", {
323
+ Object.defineProperty(this, "voiceCache", {
333
324
  enumerable: true,
334
325
  configurable: true,
335
326
  writable: true,
@@ -395,30 +386,7 @@ export class MidyGM2 {
395
386
  writable: true,
396
387
  value: new Array(this.numChannels * drumExclusiveClassCount)
397
388
  });
398
- Object.defineProperty(this, "defaultOptions", {
399
- enumerable: true,
400
- configurable: true,
401
- writable: true,
402
- value: {
403
- reverbAlgorithm: (audioContext) => {
404
- const { time: rt60, feedback } = this.reverb;
405
- // const delay = this.calcDelay(rt60, feedback);
406
- // const impulse = this.createConvolutionReverbImpulse(
407
- // audioContext,
408
- // rt60,
409
- // delay,
410
- // );
411
- // return this.createConvolutionReverb(audioContext, impulse);
412
- const combFeedbacks = this.generateDistributedArray(feedback, 4);
413
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
414
- const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
415
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
416
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
417
- },
418
- }
419
- });
420
389
  this.audioContext = audioContext;
421
- this.options = { ...this.defaultOptions, ...options };
422
390
  this.masterVolume = new GainNode(audioContext);
423
391
  this.scheduler = new GainNode(audioContext, { gain: 0 });
424
392
  this.schedulerBuffer = new AudioBuffer({
@@ -428,7 +396,7 @@ export class MidyGM2 {
428
396
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
429
397
  this.controlChangeHandlers = this.createControlChangeHandlers();
430
398
  this.channels = this.createChannels(audioContext);
431
- this.reverbEffect = this.options.reverbAlgorithm(audioContext);
399
+ this.reverbEffect = this.createReverbEffect(audioContext);
432
400
  this.chorusEffect = this.createChorusEffect(audioContext);
433
401
  this.chorusEffect.output.connect(this.masterVolume);
434
402
  this.reverbEffect.output.connect(this.masterVolume);
@@ -449,13 +417,11 @@ export class MidyGM2 {
449
417
  const presetHeaders = soundFont.parsed.presetHeaders;
450
418
  for (let i = 0; i < presetHeaders.length; i++) {
451
419
  const presetHeader = presetHeaders[i];
452
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
453
- const banks = this.soundFontTable[presetHeader.preset];
454
- banks.set(presetHeader.bank, index);
455
- }
420
+ const banks = this.soundFontTable[presetHeader.preset];
421
+ banks.set(presetHeader.bank, index);
456
422
  }
457
423
  }
458
- async loadSoundFont(input) {
424
+ async toUint8Array(input) {
459
425
  let uint8Array;
460
426
  if (typeof input === "string") {
461
427
  const response = await fetch(input);
@@ -468,23 +434,32 @@ export class MidyGM2 {
468
434
  else {
469
435
  throw new TypeError("input must be a URL string or Uint8Array");
470
436
  }
471
- const parsed = parse(uint8Array);
472
- const soundFont = new SoundFont(parsed);
473
- this.addSoundFont(soundFont);
437
+ return uint8Array;
474
438
  }
475
- async loadMIDI(input) {
476
- let uint8Array;
477
- if (typeof input === "string") {
478
- const response = await fetch(input);
479
- const arrayBuffer = await response.arrayBuffer();
480
- uint8Array = new Uint8Array(arrayBuffer);
481
- }
482
- else if (input instanceof Uint8Array) {
483
- uint8Array = input;
439
+ async loadSoundFont(input) {
440
+ this.voiceCounter.clear();
441
+ if (Array.isArray(input)) {
442
+ const promises = new Array(input.length);
443
+ for (let i = 0; i < input.length; i++) {
444
+ promises[i] = this.toUint8Array(input[i]);
445
+ }
446
+ const uint8Arrays = await Promise.all(promises);
447
+ for (let i = 0; i < uint8Arrays.length; i++) {
448
+ const parsed = parse(uint8Arrays[i]);
449
+ const soundFont = new SoundFont(parsed);
450
+ this.addSoundFont(soundFont);
451
+ }
484
452
  }
485
453
  else {
486
- throw new TypeError("input must be a URL string or Uint8Array");
454
+ const uint8Array = await this.toUint8Array(input);
455
+ const parsed = parse(uint8Array);
456
+ const soundFont = new SoundFont(parsed);
457
+ this.addSoundFont(soundFont);
487
458
  }
459
+ }
460
+ async loadMIDI(input) {
461
+ this.voiceCounter.clear();
462
+ const uint8Array = await this.toUint8Array(input);
488
463
  const midi = parseMidi(uint8Array);
489
464
  this.ticksPerBeat = midi.header.ticksPerBeat;
490
465
  const midiData = this.extractMidiData(midi);
@@ -492,7 +467,46 @@ export class MidyGM2 {
492
467
  this.timeline = midiData.timeline;
493
468
  this.totalTime = this.calcTotalTime();
494
469
  }
495
- setChannelAudioNodes(audioContext) {
470
+ cacheVoiceIds() {
471
+ const timeline = this.timeline;
472
+ for (let i = 0; i < timeline.length; i++) {
473
+ const event = timeline[i];
474
+ switch (event.type) {
475
+ case "noteOn": {
476
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
477
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
478
+ break;
479
+ }
480
+ case "controller":
481
+ if (event.controllerType === 0) {
482
+ this.setBankMSB(event.channel, event.value);
483
+ }
484
+ else if (event.controllerType === 32) {
485
+ this.setBankLSB(event.channel, event.value);
486
+ }
487
+ break;
488
+ case "programChange":
489
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
490
+ }
491
+ }
492
+ for (const [audioBufferId, count] of this.voiceCounter) {
493
+ if (count === 1)
494
+ this.voiceCounter.delete(audioBufferId);
495
+ }
496
+ this.GM2SystemOn();
497
+ }
498
+ getVoiceId(channel, noteNumber, velocity) {
499
+ const bankNumber = this.calcBank(channel);
500
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
501
+ .get(bankNumber);
502
+ if (soundFontIndex === undefined)
503
+ return;
504
+ const soundFont = this.soundFonts[soundFontIndex];
505
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
506
+ const { instrument, sampleID } = voice.generators;
507
+ return `${soundFontIndex}:${instrument}:${sampleID}`;
508
+ }
509
+ createChannelAudioNodes(audioContext) {
496
510
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
497
511
  const gainL = new GainNode(audioContext, { gain: gainLeft });
498
512
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -507,10 +521,9 @@ export class MidyGM2 {
507
521
  };
508
522
  }
509
523
  resetChannelTable(channel) {
510
- this.resetControlTable(channel.controlTable);
524
+ channel.controlTable.fill(-1);
511
525
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
512
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
513
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
526
+ channel.channelPressureTable.fill(-1);
514
527
  channel.keyBasedInstrumentControlTable.fill(-1);
515
528
  }
516
529
  createChannels(audioContext) {
@@ -520,46 +533,26 @@ export class MidyGM2 {
520
533
  isDrum: false,
521
534
  state: new ControllerState(),
522
535
  ...this.constructor.channelSettings,
523
- ...this.setChannelAudioNodes(audioContext),
536
+ ...this.createChannelAudioNodes(audioContext),
524
537
  scheduledNotes: [],
525
538
  sustainNotes: [],
526
539
  sostenutoNotes: [],
527
540
  controlTable: this.initControlTable(),
528
541
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
529
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
542
+ channelPressureTable: new Int8Array(6).fill(-1),
530
543
  keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
544
+ keyBasedGainLs: new Array(128),
545
+ keyBasedGainRs: new Array(128),
531
546
  };
532
547
  });
533
548
  return channels;
534
549
  }
535
- async createNoteBuffer(voiceParams, isSF3) {
550
+ async createAudioBuffer(voiceParams) {
551
+ const sample = voiceParams.sample;
536
552
  const sampleStart = voiceParams.start;
537
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
538
- if (isSF3) {
539
- const sample = voiceParams.sample;
540
- const start = sample.byteOffset + sampleStart;
541
- const end = sample.byteOffset + sampleEnd;
542
- const buffer = sample.buffer.slice(start, end);
543
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
544
- return audioBuffer;
545
- }
546
- else {
547
- const sample = voiceParams.sample;
548
- const start = sample.byteOffset + sampleStart;
549
- const end = sample.byteOffset + sampleEnd;
550
- const buffer = sample.buffer.slice(start, end);
551
- const audioBuffer = new AudioBuffer({
552
- numberOfChannels: 1,
553
- length: sample.length,
554
- sampleRate: voiceParams.sampleRate,
555
- });
556
- const channelData = audioBuffer.getChannelData(0);
557
- const int16Array = new Int16Array(buffer);
558
- for (let i = 0; i < int16Array.length; i++) {
559
- channelData[i] = int16Array[i] / 32768;
560
- }
561
- return audioBuffer;
562
- }
553
+ const sampleEnd = sample.data.length + voiceParams.end;
554
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
555
+ return audioBuffer;
563
556
  }
564
557
  isLoopDrum(channel, noteNumber) {
565
558
  const programNumber = channel.programNumber;
@@ -569,10 +562,9 @@ export class MidyGM2 {
569
562
  createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
570
563
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
571
564
  bufferSource.buffer = audioBuffer;
572
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
573
- if (channel.isDrum) {
574
- bufferSource.loop = this.isLoopDrum(channel, noteNumber);
575
- }
565
+ bufferSource.loop = channel.isDrum
566
+ ? this.isLoopDrum(channel, noteNumber)
567
+ : (voiceParams.sampleModes % 2 !== 0);
576
568
  if (bufferSource.loop) {
577
569
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
578
570
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -597,13 +589,13 @@ export class MidyGM2 {
597
589
  break;
598
590
  }
599
591
  case "controller":
600
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
592
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
601
593
  break;
602
594
  case "programChange":
603
- this.handleProgramChange(event.channel, event.programNumber, startTime);
595
+ this.setProgramChange(event.channel, event.programNumber, startTime);
604
596
  break;
605
597
  case "channelAftertouch":
606
- this.handleChannelPressure(event.channel, event.amount, startTime);
598
+ this.setChannelPressure(event.channel, event.amount, startTime);
607
599
  break;
608
600
  case "pitchBend":
609
601
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -637,8 +629,9 @@ export class MidyGM2 {
637
629
  this.notePromises = [];
638
630
  this.exclusiveClassNotes.fill(undefined);
639
631
  this.drumExclusiveClassNotes.fill(undefined);
640
- this.audioBufferCache.clear();
632
+ this.voiceCache.clear();
641
633
  for (let i = 0; i < this.channels.length; i++) {
634
+ this.channels[i].scheduledNotes = [];
642
635
  this.resetAllStates(i);
643
636
  }
644
637
  resolve();
@@ -660,8 +653,9 @@ export class MidyGM2 {
660
653
  this.notePromises = [];
661
654
  this.exclusiveClassNotes.fill(undefined);
662
655
  this.drumExclusiveClassNotes.fill(undefined);
663
- this.audioBufferCache.clear();
656
+ this.voiceCache.clear();
664
657
  for (let i = 0; i < this.channels.length; i++) {
658
+ this.channels[i].scheduledNotes = [];
665
659
  this.resetAllStates(i);
666
660
  }
667
661
  this.isStopping = false;
@@ -694,11 +688,7 @@ export class MidyGM2 {
694
688
  secondToTicks(second, secondsPerBeat) {
695
689
  return second * this.ticksPerBeat / secondsPerBeat;
696
690
  }
697
- getAudioBufferId(programNumber, noteNumber, velocity) {
698
- return `${programNumber}:${noteNumber}:${velocity}`;
699
- }
700
691
  extractMidiData(midi) {
701
- this.audioBufferCounter.clear();
702
692
  const instruments = new Set();
703
693
  const timeline = [];
704
694
  const tmpChannels = new Array(this.channels.length);
@@ -719,8 +709,6 @@ export class MidyGM2 {
719
709
  switch (event.type) {
720
710
  case "noteOn": {
721
711
  const channel = tmpChannels[event.channel];
722
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
723
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
724
712
  if (channel.programNumber < 0) {
725
713
  channel.programNumber = event.programNumber;
726
714
  switch (channel.bankMSB) {
@@ -770,10 +758,6 @@ export class MidyGM2 {
770
758
  timeline.push(event);
771
759
  }
772
760
  }
773
- for (const [audioBufferId, count] of this.audioBufferCounter) {
774
- if (count === 1)
775
- this.audioBufferCounter.delete(audioBufferId);
776
- }
777
761
  const priority = {
778
762
  controller: 0,
779
763
  sysEx: 1,
@@ -818,7 +802,6 @@ export class MidyGM2 {
818
802
  this.notePromises.push(promise);
819
803
  promises.push(promise);
820
804
  });
821
- channel.scheduledNotes = [];
822
805
  return Promise.all(promises);
823
806
  }
824
807
  stopNotes(velocity, force, scheduleTime) {
@@ -832,6 +815,8 @@ export class MidyGM2 {
832
815
  if (this.isPlaying || this.isPaused)
833
816
  return;
834
817
  this.resumeTime = 0;
818
+ if (this.voiceCounter.size === 0)
819
+ this.cacheVoiceIds();
835
820
  await this.playNotes();
836
821
  this.isPlaying = false;
837
822
  }
@@ -874,7 +859,7 @@ export class MidyGM2 {
874
859
  }
875
860
  processScheduledNotes(channel, callback) {
876
861
  const scheduledNotes = channel.scheduledNotes;
877
- for (let i = 0; i < scheduledNotes.length; i++) {
862
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
878
863
  const note = scheduledNotes[i];
879
864
  if (!note)
880
865
  continue;
@@ -885,14 +870,14 @@ export class MidyGM2 {
885
870
  }
886
871
  processActiveNotes(channel, scheduleTime, callback) {
887
872
  const scheduledNotes = channel.scheduledNotes;
888
- for (let i = 0; i < scheduledNotes.length; i++) {
873
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
889
874
  const note = scheduledNotes[i];
890
875
  if (!note)
891
876
  continue;
892
877
  if (note.ending)
893
878
  continue;
894
879
  if (scheduleTime < note.startTime)
895
- continue;
880
+ break;
896
881
  callback(note);
897
882
  }
898
883
  }
@@ -981,6 +966,22 @@ export class MidyGM2 {
981
966
  const output = allpasses.at(-1);
982
967
  return { input, output };
983
968
  }
969
+ createReverbEffect(audioContext) {
970
+ const { algorithm, time: rt60, feedback } = this.reverb;
971
+ switch (algorithm) {
972
+ case "ConvolutionReverb": {
973
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
974
+ return this.createConvolutionReverb(audioContext, impulse);
975
+ }
976
+ case "SchroederReverb": {
977
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
978
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
979
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
980
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
981
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
982
+ }
983
+ }
984
+ }
984
985
  createChorusEffect(audioContext) {
985
986
  const input = new GainNode(audioContext);
986
987
  const output = new GainNode(audioContext);
@@ -1045,9 +1046,16 @@ export class MidyGM2 {
1045
1046
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1046
1047
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1047
1048
  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;
1049
+ const channelPressureRaw = channel.channelPressureTable[0];
1050
+ if (0 <= channelPressureRaw) {
1051
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1052
+ const channelPressure = channelPressureDepth *
1053
+ channel.state.channelPressure;
1054
+ return tuning + pitch + channelPressure;
1055
+ }
1056
+ else {
1057
+ return tuning + pitch;
1058
+ }
1051
1059
  }
1052
1060
  calcNoteDetune(channel, note) {
1053
1061
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -1274,35 +1282,32 @@ export class MidyGM2 {
1274
1282
  note.vibratoLFO.connect(note.vibratoDepth);
1275
1283
  note.vibratoDepth.connect(note.bufferSource.detune);
1276
1284
  }
1277
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1278
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1279
- const cache = this.audioBufferCache.get(audioBufferId);
1285
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1286
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1287
+ const cache = this.voiceCache.get(audioBufferId);
1280
1288
  if (cache) {
1281
1289
  cache.counter += 1;
1282
1290
  if (cache.maxCount <= cache.counter) {
1283
- this.audioBufferCache.delete(audioBufferId);
1291
+ this.voiceCache.delete(audioBufferId);
1284
1292
  }
1285
1293
  return cache.audioBuffer;
1286
1294
  }
1287
1295
  else {
1288
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1289
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1296
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1297
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1290
1298
  const cache = { audioBuffer, maxCount, counter: 1 };
1291
- this.audioBufferCache.set(audioBufferId, cache);
1299
+ this.voiceCache.set(audioBufferId, cache);
1292
1300
  return audioBuffer;
1293
1301
  }
1294
1302
  }
1295
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1303
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1296
1304
  const now = this.audioContext.currentTime;
1297
1305
  const state = channel.state;
1298
1306
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1299
1307
  const voiceParams = voice.getAllParams(controllerState);
1300
1308
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1301
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1309
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1302
1310
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1303
- note.volumeNode = new GainNode(this.audioContext);
1304
- note.gainL = new GainNode(this.audioContext);
1305
- note.gainR = new GainNode(this.audioContext);
1306
1311
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1307
1312
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1308
1313
  type: "lowpass",
@@ -1335,9 +1340,6 @@ export class MidyGM2 {
1335
1340
  }
1336
1341
  note.bufferSource.connect(note.filterNode);
1337
1342
  note.filterNode.connect(note.volumeEnvelopeNode);
1338
- note.volumeEnvelopeNode.connect(note.volumeNode);
1339
- note.volumeNode.connect(note.gainL);
1340
- note.volumeNode.connect(note.gainR);
1341
1343
  if (0 < state.chorusSendLevel) {
1342
1344
  this.setChorusEffectsSend(channel, note, 0, now);
1343
1345
  }
@@ -1396,28 +1398,30 @@ export class MidyGM2 {
1396
1398
  }
1397
1399
  this.drumExclusiveClassNotes[index] = note;
1398
1400
  }
1399
- isDrumNoteOffException(channel, noteNumber) {
1400
- if (!channel.isDrum)
1401
- return false;
1402
- const programNumber = channel.programNumber;
1403
- return !((programNumber === 48 && noteNumber === 88) ||
1404
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1405
- }
1406
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1401
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1407
1402
  const channel = this.channels[channelNumber];
1408
1403
  const bankNumber = this.calcBank(channel, channelNumber);
1409
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1404
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1405
+ .get(bankNumber);
1410
1406
  if (soundFontIndex === undefined)
1411
1407
  return;
1412
1408
  const soundFont = this.soundFonts[soundFontIndex];
1413
1409
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1414
1410
  if (!voice)
1415
1411
  return;
1416
- const isSF3 = soundFont.parsed.info.version.major === 3;
1417
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1418
- note.noteOffEvent = noteOffEvent;
1419
- note.gainL.connect(channel.gainL);
1420
- note.gainR.connect(channel.gainR);
1412
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1413
+ if (channel.isDrum) {
1414
+ const audioContext = this.audioContext;
1415
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1416
+ channel.keyBasedGainLs[noteNumber] = gainL;
1417
+ channel.keyBasedGainRs[noteNumber] = gainR;
1418
+ note.volumeEnvelopeNode.connect(gainL);
1419
+ note.volumeEnvelopeNode.connect(gainR);
1420
+ }
1421
+ else {
1422
+ note.volumeEnvelopeNode.connect(channel.gainL);
1423
+ note.volumeEnvelopeNode.connect(channel.gainR);
1424
+ }
1421
1425
  if (0.5 <= channel.state.sustainPedal) {
1422
1426
  channel.sustainNotes.push(note);
1423
1427
  }
@@ -1435,9 +1439,6 @@ export class MidyGM2 {
1435
1439
  note.bufferSource.disconnect();
1436
1440
  note.filterNode.disconnect();
1437
1441
  note.volumeEnvelopeNode.disconnect();
1438
- note.volumeNode.disconnect();
1439
- note.gainL.disconnect();
1440
- note.gainR.disconnect();
1441
1442
  if (note.modulationDepth) {
1442
1443
  note.volumeDepth.disconnect();
1443
1444
  note.modulationDepth.disconnect();
@@ -1490,15 +1491,29 @@ export class MidyGM2 {
1490
1491
  return;
1491
1492
  }
1492
1493
  }
1493
- const note = this.findNoteOffTarget(channel, noteNumber);
1494
- if (!note)
1494
+ const index = this.findNoteOffIndex(channel, noteNumber);
1495
+ if (index < 0)
1495
1496
  return;
1497
+ const note = channel.scheduledNotes[index];
1496
1498
  note.ending = true;
1499
+ this.setNoteIndex(channel, index);
1497
1500
  this.releaseNote(channel, note, endTime);
1498
1501
  }
1499
- findNoteOffTarget(channel, noteNumber) {
1502
+ setNoteIndex(channel, index) {
1503
+ let allEnds = true;
1504
+ for (let i = channel.scheduleIndex; i < index; i++) {
1505
+ const note = channel.scheduledNotes[i];
1506
+ if (note && !note.ending) {
1507
+ allEnds = false;
1508
+ break;
1509
+ }
1510
+ }
1511
+ if (allEnds)
1512
+ channel.scheduleIndex = index + 1;
1513
+ }
1514
+ findNoteOffIndex(channel, noteNumber) {
1500
1515
  const scheduledNotes = channel.scheduledNotes;
1501
- for (let i = 0; i < scheduledNotes.length; i++) {
1516
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1502
1517
  const note = scheduledNotes[i];
1503
1518
  if (!note)
1504
1519
  continue;
@@ -1506,8 +1521,9 @@ export class MidyGM2 {
1506
1521
  continue;
1507
1522
  if (note.noteNumber !== noteNumber)
1508
1523
  continue;
1509
- return note;
1524
+ return i;
1510
1525
  }
1526
+ return -1;
1511
1527
  }
1512
1528
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1513
1529
  scheduleTime ??= this.audioContext.currentTime;
@@ -1547,18 +1563,18 @@ export class MidyGM2 {
1547
1563
  case 0x90:
1548
1564
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1549
1565
  case 0xB0:
1550
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1566
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1551
1567
  case 0xC0:
1552
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1568
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1553
1569
  case 0xD0:
1554
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1570
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1555
1571
  case 0xE0:
1556
1572
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1557
1573
  default:
1558
1574
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1559
1575
  }
1560
1576
  }
1561
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1577
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1562
1578
  const channel = this.channels[channelNumber];
1563
1579
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1564
1580
  channel.programNumber = programNumber;
@@ -1573,16 +1589,17 @@ export class MidyGM2 {
1573
1589
  }
1574
1590
  }
1575
1591
  }
1576
- handleChannelPressure(channelNumber, value, scheduleTime) {
1592
+ setChannelPressure(channelNumber, value, scheduleTime) {
1577
1593
  const channel = this.channels[channelNumber];
1578
1594
  if (channel.isDrum)
1579
1595
  return;
1580
1596
  const prev = channel.state.channelPressure;
1581
1597
  const next = value / 127;
1582
1598
  channel.state.channelPressure = next;
1583
- if (channel.channelPressureTable[0] !== 64) {
1584
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1585
- channel.detune += pressureDepth * (next - prev);
1599
+ const channelPressureRaw = channel.channelPressureTable[0];
1600
+ if (0 <= channelPressureRaw) {
1601
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1602
+ channel.detune += channelPressureDepth * (next - prev);
1586
1603
  }
1587
1604
  const table = channel.channelPressureTable;
1588
1605
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1642,10 +1659,12 @@ export class MidyGM2 {
1642
1659
  .setValueAtTime(volumeDepth, scheduleTime);
1643
1660
  }
1644
1661
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1645
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1646
1662
  let value = note.voiceParams.reverbEffectsSend;
1647
- if (0 <= keyBasedValue) {
1648
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1663
+ if (channel.isDrum) {
1664
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1665
+ if (0 <= keyBasedValue) {
1666
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1667
+ }
1649
1668
  }
1650
1669
  if (0 < prevValue) {
1651
1670
  if (0 < value) {
@@ -1670,13 +1689,15 @@ export class MidyGM2 {
1670
1689
  }
1671
1690
  }
1672
1691
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1673
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1674
1692
  let value = note.voiceParams.chorusEffectsSend;
1675
- if (0 <= keyBasedValue) {
1676
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1693
+ if (channel.isDrum) {
1694
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1695
+ if (0 <= keyBasedValue) {
1696
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1697
+ }
1677
1698
  }
1678
1699
  if (0 < prevValue) {
1679
- if (0 < vaule) {
1700
+ if (0 < value) {
1680
1701
  note.chorusEffectsSend.gain
1681
1702
  .cancelScheduledValues(scheduleTime)
1682
1703
  .setValueAtTime(value, scheduleTime);
@@ -1691,7 +1712,7 @@ export class MidyGM2 {
1691
1712
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1692
1713
  gain: value,
1693
1714
  });
1694
- note.volumeNode.connect(note.chorusEffectsSend);
1715
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1695
1716
  }
1696
1717
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1697
1718
  }
@@ -1778,8 +1799,9 @@ export class MidyGM2 {
1778
1799
  this.processScheduledNotes(channel, (note) => {
1779
1800
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1780
1801
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1781
- let appliedFilterEnvelope = false;
1782
- let appliedVolumeEnvelope = false;
1802
+ let applyVolumeEnvelope = false;
1803
+ let applyFilterEnvelope = false;
1804
+ let applyPitchEnvelope = false;
1783
1805
  for (const [key, value] of Object.entries(voiceParams)) {
1784
1806
  const prevValue = note.voiceParams[key];
1785
1807
  if (value === prevValue)
@@ -1788,37 +1810,23 @@ export class MidyGM2 {
1788
1810
  if (key in this.voiceParamsHandlers) {
1789
1811
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1790
1812
  }
1791
- else if (filterEnvelopeKeySet.has(key)) {
1792
- if (appliedFilterEnvelope)
1793
- continue;
1794
- appliedFilterEnvelope = true;
1795
- const noteVoiceParams = note.voiceParams;
1796
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1797
- const key = filterEnvelopeKeys[i];
1798
- if (key in voiceParams)
1799
- noteVoiceParams[key] = voiceParams[key];
1800
- }
1801
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1802
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1803
- }
1804
- else {
1805
- this.setFilterEnvelope(channel, note, scheduleTime);
1806
- }
1807
- this.setPitchEnvelope(note, scheduleTime);
1808
- }
1809
- else if (volumeEnvelopeKeySet.has(key)) {
1810
- if (appliedVolumeEnvelope)
1811
- continue;
1812
- appliedVolumeEnvelope = true;
1813
- const noteVoiceParams = note.voiceParams;
1814
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1815
- const key = volumeEnvelopeKeys[i];
1816
- if (key in voiceParams)
1817
- noteVoiceParams[key] = voiceParams[key];
1818
- }
1819
- this.setVolumeEnvelope(channel, note, scheduleTime);
1813
+ else {
1814
+ if (volumeEnvelopeKeySet.has(key))
1815
+ applyVolumeEnvelope = true;
1816
+ if (filterEnvelopeKeySet.has(key))
1817
+ applyFilterEnvelope = true;
1818
+ if (pitchEnvelopeKeySet.has(key))
1819
+ applyPitchEnvelope = true;
1820
1820
  }
1821
1821
  }
1822
+ if (applyVolumeEnvelope) {
1823
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1824
+ }
1825
+ if (applyFilterEnvelope) {
1826
+ this.setFilterEnvelope(channel, note, scheduleTime);
1827
+ }
1828
+ if (applyPitchEnvelope)
1829
+ this.setPitchEnvelope(note, scheduleTime);
1822
1830
  });
1823
1831
  }
1824
1832
  createControlChangeHandlers() {
@@ -1849,13 +1857,13 @@ export class MidyGM2 {
1849
1857
  handlers[127] = this.polyOn;
1850
1858
  return handlers;
1851
1859
  }
1852
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1860
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1853
1861
  const handler = this.controlChangeHandlers[controllerType];
1854
1862
  if (handler) {
1855
1863
  handler.call(this, channelNumber, value, scheduleTime);
1856
1864
  const channel = this.channels[channelNumber];
1857
1865
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1858
- this.applyControlTable(channel, controllerType);
1866
+ this.applyControlTable(channel, controllerType, scheduleTime);
1859
1867
  }
1860
1868
  else {
1861
1869
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1912,22 +1920,12 @@ export class MidyGM2 {
1912
1920
  return;
1913
1921
  this.updatePortamento(channel, scheduleTime);
1914
1922
  }
1915
- setKeyBasedVolume(channel, scheduleTime) {
1916
- this.processScheduledNotes(channel, (note) => {
1917
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1918
- if (0 <= keyBasedValue) {
1919
- note.volumeNode.gain
1920
- .cancelScheduledValues(scheduleTime)
1921
- .setValueAtTime(keyBasedValue / 127, scheduleTime);
1922
- }
1923
- });
1924
- }
1925
1923
  setVolume(channelNumber, volume, scheduleTime) {
1926
1924
  scheduleTime ??= this.audioContext.currentTime;
1927
1925
  const channel = this.channels[channelNumber];
1928
1926
  channel.state.volume = volume / 127;
1929
1927
  this.updateChannelVolume(channel, scheduleTime);
1930
- this.setKeyBasedVolume(channel, scheduleTime);
1928
+ this.updateKeyBasedVolume(channel, scheduleTime);
1931
1929
  }
1932
1930
  panToGain(pan) {
1933
1931
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1936,26 +1934,12 @@ export class MidyGM2 {
1936
1934
  gainRight: Math.sin(theta),
1937
1935
  };
1938
1936
  }
1939
- setKeyBasedPan(channel, scheduleTime) {
1940
- this.processScheduledNotes(channel, (note) => {
1941
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1942
- if (0 <= keyBasedValue) {
1943
- const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1944
- note.gainL.gain
1945
- .cancelScheduledValues(scheduleTime)
1946
- .setValueAtTime(gainLeft, scheduleTime);
1947
- note.gainR.gain
1948
- .cancelScheduledValues(scheduleTime)
1949
- .setValueAtTime(gainRight, scheduleTime);
1950
- }
1951
- });
1952
- }
1953
1937
  setPan(channelNumber, pan, scheduleTime) {
1954
1938
  scheduleTime ??= this.audioContext.currentTime;
1955
1939
  const channel = this.channels[channelNumber];
1956
1940
  channel.state.pan = pan / 127;
1957
1941
  this.updateChannelVolume(channel, scheduleTime);
1958
- this.setKeyBasedPan(channel, scheduleTime);
1942
+ this.updateKeyBasedVolume(channel, scheduleTime);
1959
1943
  }
1960
1944
  setExpression(channelNumber, expression, scheduleTime) {
1961
1945
  scheduleTime ??= this.audioContext.currentTime;
@@ -1981,6 +1965,34 @@ export class MidyGM2 {
1981
1965
  .cancelScheduledValues(scheduleTime)
1982
1966
  .setValueAtTime(volume * gainRight, scheduleTime);
1983
1967
  }
1968
+ updateKeyBasedVolume(channel, scheduleTime) {
1969
+ if (!channel.isDrum)
1970
+ return;
1971
+ const state = channel.state;
1972
+ const defaultVolume = state.volume * state.expression;
1973
+ const defaultPan = state.pan;
1974
+ for (let i = 0; i < 128; i++) {
1975
+ const gainL = channel.keyBasedGainLs[i];
1976
+ const gainR = channel.keyBasedGainLs[i];
1977
+ if (!gainL)
1978
+ continue;
1979
+ if (!gainR)
1980
+ continue;
1981
+ const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
1982
+ const volume = (0 <= keyBasedVolume)
1983
+ ? defaultVolume * keyBasedVolume / 64
1984
+ : defaultVolume;
1985
+ const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
1986
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
1987
+ const { gainLeft, gainRight } = this.panToGain(pan);
1988
+ gainL.gain
1989
+ .cancelScheduledValues(scheduleTime)
1990
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1991
+ gainR.gain
1992
+ .cancelScheduledValues(scheduleTime)
1993
+ .setValueAtTime(volume * gainRight, scheduleTime);
1994
+ }
1995
+ }
1984
1996
  setSustainPedal(channelNumber, value, scheduleTime) {
1985
1997
  const channel = this.channels[channelNumber];
1986
1998
  if (channel.isDrum)
@@ -2242,7 +2254,7 @@ export class MidyGM2 {
2242
2254
  const entries = Object.entries(defaultControllerState);
2243
2255
  for (const [key, { type, defaultValue }] of entries) {
2244
2256
  if (128 <= type) {
2245
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2257
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2246
2258
  }
2247
2259
  else {
2248
2260
  state[key] = defaultValue;
@@ -2274,7 +2286,7 @@ export class MidyGM2 {
2274
2286
  const key = keys[i];
2275
2287
  const { type, defaultValue } = defaultControllerState[key];
2276
2288
  if (128 <= type) {
2277
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2289
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2278
2290
  }
2279
2291
  else {
2280
2292
  state[key] = defaultValue;
@@ -2482,8 +2494,7 @@ export class MidyGM2 {
2482
2494
  setReverbType(type) {
2483
2495
  this.reverb.time = this.getReverbTimeFromType(type);
2484
2496
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2485
- const { audioContext, options } = this;
2486
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2497
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2487
2498
  }
2488
2499
  getReverbTimeFromType(type) {
2489
2500
  switch (type) {
@@ -2505,8 +2516,7 @@ export class MidyGM2 {
2505
2516
  }
2506
2517
  setReverbTime(value) {
2507
2518
  this.reverb.time = this.getReverbTime(value);
2508
- const { audioContext, options } = this;
2509
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2519
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2510
2520
  }
2511
2521
  getReverbTime(value) {
2512
2522
  return Math.exp((value - 40) * 0.025);
@@ -2677,51 +2687,63 @@ export class MidyGM2 {
2677
2687
  }
2678
2688
  }
2679
2689
  getFilterCutoffControl(channel) {
2680
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2681
- channel.state.channelPressure;
2690
+ const channelPressureRaw = channel.channelPressureTable[1];
2691
+ const channelPressure = (0 <= channelPressureRaw)
2692
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2693
+ : 0;
2682
2694
  return channelPressure * 15;
2683
2695
  }
2684
2696
  getAmplitudeControl(channel) {
2685
- const channelPressure = channel.channelPressureTable[2] *
2686
- channel.state.channelPressure;
2697
+ const channelPressureRaw = channel.channelPressureTable[2];
2698
+ const channelPressure = (0 <= channelPressureRaw)
2699
+ ? channelPressureRaw * channel.state.channelPressure
2700
+ : 0;
2687
2701
  return channelPressure / 64;
2688
2702
  }
2689
2703
  getLFOPitchDepth(channel) {
2690
- const channelPressure = channel.channelPressureTable[3] *
2691
- channel.state.channelPressure;
2704
+ const channelPressureRaw = channel.channelPressureTable[3];
2705
+ const channelPressure = (0 <= channelPressureRaw)
2706
+ ? channelPressureRaw * channel.state.channelPressure
2707
+ : 0;
2692
2708
  return channelPressure / 127 * 600;
2693
2709
  }
2694
2710
  getLFOFilterDepth(channel) {
2695
- const channelPressure = channel.channelPressureTable[4] *
2696
- channel.state.channelPressure;
2711
+ const channelPressureRaw = channel.channelPressureTable[4];
2712
+ const channelPressure = (0 <= channelPressureRaw)
2713
+ ? channelPressureRaw * channel.state.channelPressure
2714
+ : 0;
2697
2715
  return channelPressure / 127 * 2400;
2698
2716
  }
2699
2717
  getLFOAmplitudeDepth(channel) {
2700
- const channelPressure = channel.channelPressureTable[5] *
2701
- channel.state.channelPressure;
2718
+ const channelPressureRaw = channel.channelPressureTable[5];
2719
+ const channelPressure = (0 <= channelPressureRaw)
2720
+ ? channelPressureRaw * channel.state.channelPressure
2721
+ : 0;
2702
2722
  return channelPressure / 127;
2703
2723
  }
2704
- setControllerParameters(channel, note, table) {
2705
- if (table[0] !== 64)
2706
- this.updateDetune(channel, note);
2724
+ setControllerParameters(channel, note, table, scheduleTime) {
2725
+ if (0 <= table[0])
2726
+ this.updateDetune(channel, note, scueduleTime);
2707
2727
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2708
- if (table[1] !== 64)
2709
- this.setPortamentoFilterEnvelope(channel, note);
2710
- if (table[2] !== 64)
2711
- this.setPortamentoVolumeEnvelope(channel, note);
2728
+ if (0 <= table[1]) {
2729
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2730
+ }
2731
+ if (0 <= table[2]) {
2732
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2733
+ }
2712
2734
  }
2713
2735
  else {
2714
- if (table[1] !== 64)
2715
- this.setFilterEnvelope(channel, note);
2716
- if (table[2] !== 64)
2717
- this.setVolumeEnvelope(channel, note);
2718
- }
2719
- if (table[3] !== 0)
2720
- this.setModLfoToPitch(channel, note);
2721
- if (table[4] !== 0)
2722
- this.setModLfoToFilterFc(channel, note);
2723
- if (table[5] !== 0)
2724
- this.setModLfoToVolume(channel, note);
2736
+ if (0 <= table[1])
2737
+ this.setFilterEnvelope(channel, note, scheduleTime);
2738
+ if (0 <= table[2])
2739
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2740
+ }
2741
+ if (0 <= table[3])
2742
+ this.setModLfoToPitch(channel, note, scheduleTime);
2743
+ if (0 <= table[4])
2744
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2745
+ if (0 <= table[5])
2746
+ this.setModLfoToVolume(channel, note, scheduleTime);
2725
2747
  }
2726
2748
  handlePressureSysEx(data, tableName) {
2727
2749
  const channelNumber = data[4];
@@ -2736,27 +2758,16 @@ export class MidyGM2 {
2736
2758
  }
2737
2759
  }
2738
2760
  initControlTable() {
2739
- const channelCount = 128;
2761
+ const ccCount = 128;
2740
2762
  const slotSize = 6;
2741
- const table = new Uint8Array(channelCount * slotSize);
2742
- return this.resetControlTable(table);
2763
+ return new Int8Array(ccCount * slotSize).fill(-1);
2743
2764
  }
2744
- resetControlTable(table) {
2745
- const channelCount = 128;
2746
- const slotSize = 6;
2747
- const defaultValues = [64, 64, 64, 0, 0, 0];
2748
- for (let ch = 0; ch < channelCount; ch++) {
2749
- const offset = ch * slotSize;
2750
- table.set(defaultValues, offset);
2751
- }
2752
- return table;
2753
- }
2754
- applyControlTable(channel, controllerType) {
2765
+ applyControlTable(channel, controllerType, scheduleTime) {
2755
2766
  const slotSize = 6;
2756
2767
  const offset = controllerType * slotSize;
2757
2768
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2758
2769
  this.processScheduledNotes(channel, (note) => {
2759
- this.setControllerParameters(channel, note, table);
2770
+ this.setControllerParameters(channel, note, table, scheduleTime);
2760
2771
  });
2761
2772
  }
2762
2773
  handleControlChangeSysEx(data) {
@@ -2772,7 +2783,7 @@ export class MidyGM2 {
2772
2783
  table[pp] = rr;
2773
2784
  }
2774
2785
  }
2775
- getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2786
+ getKeyBasedValue(channel, keyNumber, controllerType) {
2776
2787
  const index = keyNumber * 128 + controllerType;
2777
2788
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2778
2789
  return controlValue;
@@ -2780,7 +2791,7 @@ export class MidyGM2 {
2780
2791
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2781
2792
  const channelNumber = data[4];
2782
2793
  const channel = this.channels[channelNumber];
2783
- if (channel.isDrum)
2794
+ if (!channel.isDrum)
2784
2795
  return;
2785
2796
  const keyNumber = data[5];
2786
2797
  const table = channel.keyBasedInstrumentControlTable;
@@ -2790,7 +2801,7 @@ export class MidyGM2 {
2790
2801
  const index = keyNumber * 128 + controllerType;
2791
2802
  table[index] = value;
2792
2803
  }
2793
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2804
+ this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2794
2805
  }
2795
2806
  handleSysEx(data, scheduleTime) {
2796
2807
  switch (data[0]) {
@@ -2827,6 +2838,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2827
2838
  configurable: true,
2828
2839
  writable: true,
2829
2840
  value: {
2841
+ scheduleIndex: 0,
2830
2842
  detune: 0,
2831
2843
  programNumber: 0,
2832
2844
  bank: 121 * 128,