@marmooo/midy 0.3.4 → 0.3.6

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
@@ -68,13 +68,13 @@ class Note {
68
68
  writable: true,
69
69
  value: void 0
70
70
  });
71
- Object.defineProperty(this, "reverbEffectsSend", {
71
+ Object.defineProperty(this, "reverbSend", {
72
72
  enumerable: true,
73
73
  configurable: true,
74
74
  writable: true,
75
75
  value: void 0
76
76
  });
77
- Object.defineProperty(this, "chorusEffectsSend", {
77
+ Object.defineProperty(this, "chorusSend", {
78
78
  enumerable: true,
79
79
  configurable: true,
80
80
  writable: true,
@@ -314,13 +314,13 @@ export class MidyGM2 {
314
314
  writable: true,
315
315
  value: this.initSoundFontTable()
316
316
  });
317
- Object.defineProperty(this, "audioBufferCounter", {
317
+ Object.defineProperty(this, "voiceCounter", {
318
318
  enumerable: true,
319
319
  configurable: true,
320
320
  writable: true,
321
321
  value: new Map()
322
322
  });
323
- Object.defineProperty(this, "audioBufferCache", {
323
+ Object.defineProperty(this, "voiceCache", {
324
324
  enumerable: true,
325
325
  configurable: true,
326
326
  writable: true,
@@ -362,17 +362,17 @@ export class MidyGM2 {
362
362
  writable: true,
363
363
  value: []
364
364
  });
365
- Object.defineProperty(this, "instruments", {
365
+ Object.defineProperty(this, "notePromises", {
366
366
  enumerable: true,
367
367
  configurable: true,
368
368
  writable: true,
369
369
  value: []
370
370
  });
371
- Object.defineProperty(this, "notePromises", {
371
+ Object.defineProperty(this, "instruments", {
372
372
  enumerable: true,
373
373
  configurable: true,
374
374
  writable: true,
375
- value: []
375
+ value: new Set()
376
376
  });
377
377
  Object.defineProperty(this, "exclusiveClassNotes", {
378
378
  enumerable: true,
@@ -417,13 +417,11 @@ export class MidyGM2 {
417
417
  const presetHeaders = soundFont.parsed.presetHeaders;
418
418
  for (let i = 0; i < presetHeaders.length; i++) {
419
419
  const presetHeader = presetHeaders[i];
420
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
421
- const banks = this.soundFontTable[presetHeader.preset];
422
- banks.set(presetHeader.bank, index);
423
- }
420
+ const banks = this.soundFontTable[presetHeader.preset];
421
+ banks.set(presetHeader.bank, index);
424
422
  }
425
423
  }
426
- async loadSoundFont(input) {
424
+ async toUint8Array(input) {
427
425
  let uint8Array;
428
426
  if (typeof input === "string") {
429
427
  const response = await fetch(input);
@@ -436,23 +434,32 @@ export class MidyGM2 {
436
434
  else {
437
435
  throw new TypeError("input must be a URL string or Uint8Array");
438
436
  }
439
- const parsed = parse(uint8Array);
440
- const soundFont = new SoundFont(parsed);
441
- this.addSoundFont(soundFont);
437
+ return uint8Array;
442
438
  }
443
- async loadMIDI(input) {
444
- let uint8Array;
445
- if (typeof input === "string") {
446
- const response = await fetch(input);
447
- const arrayBuffer = await response.arrayBuffer();
448
- uint8Array = new Uint8Array(arrayBuffer);
449
- }
450
- else if (input instanceof Uint8Array) {
451
- uint8Array = input;
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
+ }
452
452
  }
453
453
  else {
454
- 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);
455
458
  }
459
+ }
460
+ async loadMIDI(input) {
461
+ this.voiceCounter.clear();
462
+ const uint8Array = await this.toUint8Array(input);
456
463
  const midi = parseMidi(uint8Array);
457
464
  this.ticksPerBeat = midi.header.ticksPerBeat;
458
465
  const midiData = this.extractMidiData(midi);
@@ -460,6 +467,45 @@ export class MidyGM2 {
460
467
  this.timeline = midiData.timeline;
461
468
  this.totalTime = this.calcTotalTime();
462
469
  }
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 * (2 ** 32) + (instrument << 16) + sampleID;
508
+ }
463
509
  createChannelAudioNodes(audioContext) {
464
510
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
465
511
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -501,34 +547,12 @@ export class MidyGM2 {
501
547
  });
502
548
  return channels;
503
549
  }
504
- async createNoteBuffer(voiceParams, isSF3) {
550
+ async createAudioBuffer(voiceParams) {
551
+ const sample = voiceParams.sample;
505
552
  const sampleStart = voiceParams.start;
506
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
507
- if (isSF3) {
508
- const sample = voiceParams.sample;
509
- const start = sample.byteOffset + sampleStart;
510
- const end = sample.byteOffset + sampleEnd;
511
- const buffer = sample.buffer.slice(start, end);
512
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
513
- return audioBuffer;
514
- }
515
- else {
516
- const sample = voiceParams.sample;
517
- const start = sample.byteOffset + sampleStart;
518
- const end = sample.byteOffset + sampleEnd;
519
- const buffer = sample.buffer.slice(start, end);
520
- const audioBuffer = new AudioBuffer({
521
- numberOfChannels: 1,
522
- length: sample.length,
523
- sampleRate: voiceParams.sampleRate,
524
- });
525
- const channelData = audioBuffer.getChannelData(0);
526
- const int16Array = new Int16Array(buffer);
527
- for (let i = 0; i < int16Array.length; i++) {
528
- channelData[i] = int16Array[i] / 32768;
529
- }
530
- return audioBuffer;
531
- }
553
+ const sampleEnd = sample.data.length + voiceParams.end;
554
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
555
+ return audioBuffer;
532
556
  }
533
557
  isLoopDrum(channel, noteNumber) {
534
558
  const programNumber = channel.programNumber;
@@ -565,13 +589,13 @@ export class MidyGM2 {
565
589
  break;
566
590
  }
567
591
  case "controller":
568
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
592
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
569
593
  break;
570
594
  case "programChange":
571
- this.handleProgramChange(event.channel, event.programNumber, startTime);
595
+ this.setProgramChange(event.channel, event.programNumber, startTime);
572
596
  break;
573
597
  case "channelAftertouch":
574
- this.handleChannelPressure(event.channel, event.amount, startTime);
598
+ this.setChannelPressure(event.channel, event.amount, startTime);
575
599
  break;
576
600
  case "pitchBend":
577
601
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -605,8 +629,9 @@ export class MidyGM2 {
605
629
  this.notePromises = [];
606
630
  this.exclusiveClassNotes.fill(undefined);
607
631
  this.drumExclusiveClassNotes.fill(undefined);
608
- this.audioBufferCache.clear();
632
+ this.voiceCache.clear();
609
633
  for (let i = 0; i < this.channels.length; i++) {
634
+ this.channels[i].scheduledNotes = [];
610
635
  this.resetAllStates(i);
611
636
  }
612
637
  resolve();
@@ -628,8 +653,9 @@ export class MidyGM2 {
628
653
  this.notePromises = [];
629
654
  this.exclusiveClassNotes.fill(undefined);
630
655
  this.drumExclusiveClassNotes.fill(undefined);
631
- this.audioBufferCache.clear();
656
+ this.voiceCache.clear();
632
657
  for (let i = 0; i < this.channels.length; i++) {
658
+ this.channels[i].scheduledNotes = [];
633
659
  this.resetAllStates(i);
634
660
  }
635
661
  this.isStopping = false;
@@ -662,11 +688,7 @@ export class MidyGM2 {
662
688
  secondToTicks(second, secondsPerBeat) {
663
689
  return second * this.ticksPerBeat / secondsPerBeat;
664
690
  }
665
- getAudioBufferId(programNumber, noteNumber, velocity) {
666
- return `${programNumber}:${noteNumber}:${velocity}`;
667
- }
668
691
  extractMidiData(midi) {
669
- this.audioBufferCounter.clear();
670
692
  const instruments = new Set();
671
693
  const timeline = [];
672
694
  const tmpChannels = new Array(this.channels.length);
@@ -687,8 +709,6 @@ export class MidyGM2 {
687
709
  switch (event.type) {
688
710
  case "noteOn": {
689
711
  const channel = tmpChannels[event.channel];
690
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
691
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
692
712
  if (channel.programNumber < 0) {
693
713
  channel.programNumber = event.programNumber;
694
714
  switch (channel.bankMSB) {
@@ -738,10 +758,6 @@ export class MidyGM2 {
738
758
  timeline.push(event);
739
759
  }
740
760
  }
741
- for (const [audioBufferId, count] of this.audioBufferCounter) {
742
- if (count === 1)
743
- this.audioBufferCounter.delete(audioBufferId);
744
- }
745
761
  const priority = {
746
762
  controller: 0,
747
763
  sysEx: 1,
@@ -781,12 +797,11 @@ export class MidyGM2 {
781
797
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
782
798
  const channel = this.channels[channelNumber];
783
799
  const promises = [];
784
- this.processScheduledNotes(channel, scheduleTime, (note) => {
800
+ this.processScheduledNotes(channel, (note) => {
785
801
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
786
802
  this.notePromises.push(promise);
787
803
  promises.push(promise);
788
804
  });
789
- channel.scheduledNotes = [];
790
805
  return Promise.all(promises);
791
806
  }
792
807
  stopNotes(velocity, force, scheduleTime) {
@@ -800,6 +815,8 @@ export class MidyGM2 {
800
815
  if (this.isPlaying || this.isPaused)
801
816
  return;
802
817
  this.resumeTime = 0;
818
+ if (this.voiceCounter.size === 0)
819
+ this.cacheVoiceIds();
803
820
  await this.playNotes();
804
821
  this.isPlaying = false;
805
822
  }
@@ -840,22 +857,20 @@ export class MidyGM2 {
840
857
  const now = this.audioContext.currentTime;
841
858
  return this.resumeTime + now - this.startTime - this.startDelay;
842
859
  }
843
- processScheduledNotes(channel, scheduleTime, callback) {
860
+ processScheduledNotes(channel, callback) {
844
861
  const scheduledNotes = channel.scheduledNotes;
845
- for (let i = 0; i < scheduledNotes.length; i++) {
862
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
846
863
  const note = scheduledNotes[i];
847
864
  if (!note)
848
865
  continue;
849
866
  if (note.ending)
850
867
  continue;
851
- if (note.startTime < scheduleTime)
852
- continue;
853
868
  callback(note);
854
869
  }
855
870
  }
856
871
  processActiveNotes(channel, scheduleTime, callback) {
857
872
  const scheduledNotes = channel.scheduledNotes;
858
- for (let i = 0; i < scheduledNotes.length; i++) {
873
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
859
874
  const note = scheduledNotes[i];
860
875
  if (!note)
861
876
  continue;
@@ -889,13 +904,11 @@ export class MidyGM2 {
889
904
  return impulse;
890
905
  }
891
906
  createConvolutionReverb(audioContext, impulse) {
892
- const input = new GainNode(audioContext);
893
907
  const convolverNode = new ConvolverNode(audioContext, {
894
908
  buffer: impulse,
895
909
  });
896
- input.connect(convolverNode);
897
910
  return {
898
- input,
911
+ input: convolverNode,
899
912
  output: convolverNode,
900
913
  convolverNode,
901
914
  };
@@ -1046,7 +1059,7 @@ export class MidyGM2 {
1046
1059
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1047
1060
  }
1048
1061
  updateChannelDetune(channel, scheduleTime) {
1049
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1062
+ this.processScheduledNotes(channel, (note) => {
1050
1063
  this.updateDetune(channel, note, scheduleTime);
1051
1064
  });
1052
1065
  }
@@ -1267,31 +1280,31 @@ export class MidyGM2 {
1267
1280
  note.vibratoLFO.connect(note.vibratoDepth);
1268
1281
  note.vibratoDepth.connect(note.bufferSource.detune);
1269
1282
  }
1270
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1271
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1272
- const cache = this.audioBufferCache.get(audioBufferId);
1283
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1284
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1285
+ const cache = this.voiceCache.get(audioBufferId);
1273
1286
  if (cache) {
1274
1287
  cache.counter += 1;
1275
1288
  if (cache.maxCount <= cache.counter) {
1276
- this.audioBufferCache.delete(audioBufferId);
1289
+ this.voiceCache.delete(audioBufferId);
1277
1290
  }
1278
1291
  return cache.audioBuffer;
1279
1292
  }
1280
1293
  else {
1281
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1282
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1294
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1295
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1283
1296
  const cache = { audioBuffer, maxCount, counter: 1 };
1284
- this.audioBufferCache.set(audioBufferId, cache);
1297
+ this.voiceCache.set(audioBufferId, cache);
1285
1298
  return audioBuffer;
1286
1299
  }
1287
1300
  }
1288
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1301
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1289
1302
  const now = this.audioContext.currentTime;
1290
1303
  const state = channel.state;
1291
1304
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1292
1305
  const voiceParams = voice.getAllParams(controllerState);
1293
1306
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1294
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1307
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1295
1308
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1296
1309
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1297
1310
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -1325,12 +1338,8 @@ export class MidyGM2 {
1325
1338
  }
1326
1339
  note.bufferSource.connect(note.filterNode);
1327
1340
  note.filterNode.connect(note.volumeEnvelopeNode);
1328
- if (0 < state.chorusSendLevel) {
1329
- this.setChorusEffectsSend(channel, note, 0, now);
1330
- }
1331
- if (0 < state.reverbSendLevel) {
1332
- this.setReverbEffectsSend(channel, note, 0, now);
1333
- }
1341
+ this.setChorusSend(channel, note, now);
1342
+ this.setReverbSend(channel, note, now);
1334
1343
  note.bufferSource.start(startTime);
1335
1344
  return note;
1336
1345
  }
@@ -1386,20 +1395,24 @@ export class MidyGM2 {
1386
1395
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1387
1396
  const channel = this.channels[channelNumber];
1388
1397
  const bankNumber = this.calcBank(channel, channelNumber);
1389
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1398
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1399
+ .get(bankNumber);
1390
1400
  if (soundFontIndex === undefined)
1391
1401
  return;
1392
1402
  const soundFont = this.soundFonts[soundFontIndex];
1393
1403
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1394
1404
  if (!voice)
1395
1405
  return;
1396
- const isSF3 = soundFont.parsed.info.version.major === 3;
1397
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1406
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1398
1407
  if (channel.isDrum) {
1399
- const audioContext = this.audioContext;
1400
- const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1401
- channel.keyBasedGainLs[noteNumber] = gainL;
1402
- channel.keyBasedGainRs[noteNumber] = gainR;
1408
+ const { keyBasedGainLs, keyBasedGainRs } = channel;
1409
+ let gainL = keyBasedGainLs[noteNumber];
1410
+ let gainR = keyBasedGainRs[noteNumber];
1411
+ if (!gainL) {
1412
+ const audioNodes = this.createChannelAudioNodes(this.audioContext);
1413
+ gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1414
+ gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1415
+ }
1403
1416
  note.volumeEnvelopeNode.connect(gainL);
1404
1417
  note.volumeEnvelopeNode.connect(gainR);
1405
1418
  }
@@ -1433,11 +1446,11 @@ export class MidyGM2 {
1433
1446
  note.vibratoDepth.disconnect();
1434
1447
  note.vibratoLFO.stop();
1435
1448
  }
1436
- if (note.reverbEffectsSend) {
1437
- note.reverbEffectsSend.disconnect();
1449
+ if (note.reverbSend) {
1450
+ note.reverbSend.disconnect();
1438
1451
  }
1439
- if (note.chorusEffectsSend) {
1440
- note.chorusEffectsSend.disconnect();
1452
+ if (note.chorusSend) {
1453
+ note.chorusSend.disconnect();
1441
1454
  }
1442
1455
  }
1443
1456
  releaseNote(channel, note, endTime) {
@@ -1476,15 +1489,29 @@ export class MidyGM2 {
1476
1489
  return;
1477
1490
  }
1478
1491
  }
1479
- const note = this.findNoteOffTarget(channel, noteNumber);
1480
- if (!note)
1492
+ const index = this.findNoteOffIndex(channel, noteNumber);
1493
+ if (index < 0)
1481
1494
  return;
1495
+ const note = channel.scheduledNotes[index];
1482
1496
  note.ending = true;
1497
+ this.setNoteIndex(channel, index);
1483
1498
  this.releaseNote(channel, note, endTime);
1484
1499
  }
1485
- findNoteOffTarget(channel, noteNumber) {
1500
+ setNoteIndex(channel, index) {
1501
+ let allEnds = true;
1502
+ for (let i = channel.scheduleIndex; i < index; i++) {
1503
+ const note = channel.scheduledNotes[i];
1504
+ if (note && !note.ending) {
1505
+ allEnds = false;
1506
+ break;
1507
+ }
1508
+ }
1509
+ if (allEnds)
1510
+ channel.scheduleIndex = index + 1;
1511
+ }
1512
+ findNoteOffIndex(channel, noteNumber) {
1486
1513
  const scheduledNotes = channel.scheduledNotes;
1487
- for (let i = 0; i < scheduledNotes.length; i++) {
1514
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1488
1515
  const note = scheduledNotes[i];
1489
1516
  if (!note)
1490
1517
  continue;
@@ -1492,8 +1519,9 @@ export class MidyGM2 {
1492
1519
  continue;
1493
1520
  if (note.noteNumber !== noteNumber)
1494
1521
  continue;
1495
- return note;
1522
+ return i;
1496
1523
  }
1524
+ return -1;
1497
1525
  }
1498
1526
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1499
1527
  scheduleTime ??= this.audioContext.currentTime;
@@ -1533,18 +1561,18 @@ export class MidyGM2 {
1533
1561
  case 0x90:
1534
1562
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1535
1563
  case 0xB0:
1536
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1564
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1537
1565
  case 0xC0:
1538
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1566
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1539
1567
  case 0xD0:
1540
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1568
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1541
1569
  case 0xE0:
1542
1570
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1543
1571
  default:
1544
1572
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1545
1573
  }
1546
1574
  }
1547
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1575
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1548
1576
  const channel = this.channels[channelNumber];
1549
1577
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1550
1578
  channel.programNumber = programNumber;
@@ -1559,7 +1587,7 @@ export class MidyGM2 {
1559
1587
  }
1560
1588
  }
1561
1589
  }
1562
- handleChannelPressure(channelNumber, value, scheduleTime) {
1590
+ setChannelPressure(channelNumber, value, scheduleTime) {
1563
1591
  const channel = this.channels[channelNumber];
1564
1592
  if (channel.isDrum)
1565
1593
  return;
@@ -1573,7 +1601,7 @@ export class MidyGM2 {
1573
1601
  }
1574
1602
  const table = channel.channelPressureTable;
1575
1603
  this.processActiveNotes(channel, scheduleTime, (note) => {
1576
- this.setControllerParameters(channel, note, table);
1604
+ this.setEffects(channel, note, table);
1577
1605
  });
1578
1606
  this.applyVoiceParams(channel, 13);
1579
1607
  }
@@ -1595,13 +1623,18 @@ export class MidyGM2 {
1595
1623
  this.applyVoiceParams(channel, 14, scheduleTime);
1596
1624
  }
1597
1625
  setModLfoToPitch(channel, note, scheduleTime) {
1598
- const modLfoToPitch = note.voiceParams.modLfoToPitch +
1599
- this.getLFOPitchDepth(channel);
1600
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1601
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1602
- note.modulationDepth.gain
1603
- .cancelScheduledValues(scheduleTime)
1604
- .setValueAtTime(modulationDepth, scheduleTime);
1626
+ if (note.modulationDepth) {
1627
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1628
+ this.getLFOPitchDepth(channel, note);
1629
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1630
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1631
+ note.modulationDepth.gain
1632
+ .cancelScheduledValues(scheduleTime)
1633
+ .setValueAtTime(modulationDepth, scheduleTime);
1634
+ }
1635
+ else {
1636
+ this.startModulation(channel, note, scheduleTime);
1637
+ }
1605
1638
  }
1606
1639
  setVibLfoToPitch(channel, note, scheduleTime) {
1607
1640
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
@@ -1628,63 +1661,63 @@ export class MidyGM2 {
1628
1661
  .cancelScheduledValues(scheduleTime)
1629
1662
  .setValueAtTime(volumeDepth, scheduleTime);
1630
1663
  }
1631
- setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1632
- let value = note.voiceParams.reverbEffectsSend;
1664
+ setReverbSend(channel, note, scheduleTime) {
1665
+ let value = note.voiceParams.reverbEffectsSend *
1666
+ channel.state.reverbSendLevel;
1633
1667
  if (channel.isDrum) {
1634
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1635
- if (0 <= keyBasedValue) {
1636
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1637
- }
1668
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1669
+ if (0 <= keyBasedValue)
1670
+ value = keyBasedValue / 127;
1638
1671
  }
1639
- if (0 < prevValue) {
1672
+ if (!note.reverbSend) {
1640
1673
  if (0 < value) {
1641
- note.reverbEffectsSend.gain
1642
- .cancelScheduledValues(scheduleTime)
1643
- .setValueAtTime(value, scheduleTime);
1644
- }
1645
- else {
1646
- note.reverbEffectsSend.disconnect();
1674
+ note.reverbSend = new GainNode(this.audioContext, { gain: value });
1675
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1676
+ note.reverbSend.connect(this.reverbEffect.input);
1647
1677
  }
1648
1678
  }
1649
1679
  else {
1680
+ note.reverbSend.gain
1681
+ .cancelScheduledValues(scheduleTime)
1682
+ .setValueAtTime(value, scheduleTime);
1650
1683
  if (0 < value) {
1651
- if (!note.reverbEffectsSend) {
1652
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1653
- gain: value,
1654
- });
1655
- note.volumeNode.connect(note.reverbEffectsSend);
1684
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1685
+ }
1686
+ else {
1687
+ try {
1688
+ note.volumeEnvelopeNode.disconnect(note.reverbSend);
1656
1689
  }
1657
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1690
+ catch { /* empty */ }
1658
1691
  }
1659
1692
  }
1660
1693
  }
1661
- setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1662
- let value = note.voiceParams.chorusEffectsSend;
1694
+ setChorusSend(channel, note, scheduleTime) {
1695
+ let value = note.voiceParams.chorusEffectsSend *
1696
+ channel.state.chorusSendLevel;
1663
1697
  if (channel.isDrum) {
1664
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1665
- if (0 <= keyBasedValue) {
1666
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1667
- }
1698
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1699
+ if (0 <= keyBasedValue)
1700
+ value = keyBasedValue / 127;
1668
1701
  }
1669
- if (0 < prevValue) {
1670
- if (0 < vaule) {
1671
- note.chorusEffectsSend.gain
1672
- .cancelScheduledValues(scheduleTime)
1673
- .setValueAtTime(value, scheduleTime);
1674
- }
1675
- else {
1676
- note.chorusEffectsSend.disconnect();
1702
+ if (!note.chorusSend) {
1703
+ if (0 < value) {
1704
+ note.chorusSend = new GainNode(this.audioContext, { gain: value });
1705
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1706
+ note.chorusSend.connect(this.chorusEffect.input);
1677
1707
  }
1678
1708
  }
1679
1709
  else {
1710
+ note.chorusSend.gain
1711
+ .cancelScheduledValues(scheduleTime)
1712
+ .setValueAtTime(value, scheduleTime);
1680
1713
  if (0 < value) {
1681
- if (!note.chorusEffectsSend) {
1682
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1683
- gain: value,
1684
- });
1685
- note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1714
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1715
+ }
1716
+ else {
1717
+ try {
1718
+ note.volumeEnvelopeNode.disconnect(note.chorusSend);
1686
1719
  }
1687
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1720
+ catch { /* empty */ }
1688
1721
  }
1689
1722
  }
1690
1723
  }
@@ -1766,7 +1799,7 @@ export class MidyGM2 {
1766
1799
  return state;
1767
1800
  }
1768
1801
  applyVoiceParams(channel, controllerType, scheduleTime) {
1769
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1802
+ this.processScheduledNotes(channel, (note) => {
1770
1803
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1771
1804
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1772
1805
  let applyVolumeEnvelope = false;
@@ -1827,13 +1860,13 @@ export class MidyGM2 {
1827
1860
  handlers[127] = this.polyOn;
1828
1861
  return handlers;
1829
1862
  }
1830
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1863
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1831
1864
  const handler = this.controlChangeHandlers[controllerType];
1832
1865
  if (handler) {
1833
1866
  handler.call(this, channelNumber, value, scheduleTime);
1834
1867
  const channel = this.channels[channelNumber];
1835
1868
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1836
- this.applyControlTable(channel, controllerType, scheduleTime);
1869
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
1837
1870
  }
1838
1871
  else {
1839
1872
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1844,12 +1877,11 @@ export class MidyGM2 {
1844
1877
  }
1845
1878
  updateModulation(channel, scheduleTime) {
1846
1879
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1847
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1880
+ this.processScheduledNotes(channel, (note) => {
1848
1881
  if (note.modulationDepth) {
1849
1882
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1850
1883
  }
1851
1884
  else {
1852
- this.setPitchEnvelope(note, scheduleTime);
1853
1885
  this.startModulation(channel, note, scheduleTime);
1854
1886
  }
1855
1887
  });
@@ -1863,7 +1895,7 @@ export class MidyGM2 {
1863
1895
  this.updateModulation(channel, scheduleTime);
1864
1896
  }
1865
1897
  updatePortamento(channel, scheduleTime) {
1866
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1898
+ this.processScheduledNotes(channel, (note) => {
1867
1899
  if (0.5 <= channel.state.portamento) {
1868
1900
  if (0 <= note.portamentoNoteNumber) {
1869
1901
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1894,8 +1926,14 @@ export class MidyGM2 {
1894
1926
  scheduleTime ??= this.audioContext.currentTime;
1895
1927
  const channel = this.channels[channelNumber];
1896
1928
  channel.state.volume = volume / 127;
1897
- this.updateChannelVolume(channel, scheduleTime);
1898
- this.updateKeyBasedVolume(channel, scheduleTime);
1929
+ if (channel.isDrum) {
1930
+ for (let i = 0; i < 128; i++) {
1931
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
1932
+ }
1933
+ }
1934
+ else {
1935
+ this.updateChannelVolume(channel, scheduleTime);
1936
+ }
1899
1937
  }
1900
1938
  panToGain(pan) {
1901
1939
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1908,8 +1946,14 @@ export class MidyGM2 {
1908
1946
  scheduleTime ??= this.audioContext.currentTime;
1909
1947
  const channel = this.channels[channelNumber];
1910
1948
  channel.state.pan = pan / 127;
1911
- this.updateChannelVolume(channel, scheduleTime);
1912
- this.updateKeyBasedVolume(channel, scheduleTime);
1949
+ if (channel.isDrum) {
1950
+ for (let i = 0; i < 128; i++) {
1951
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
1952
+ }
1953
+ }
1954
+ else {
1955
+ this.updateChannelVolume(channel, scheduleTime);
1956
+ }
1913
1957
  }
1914
1958
  setExpression(channelNumber, expression, scheduleTime) {
1915
1959
  scheduleTime ??= this.audioContext.currentTime;
@@ -1935,33 +1979,27 @@ export class MidyGM2 {
1935
1979
  .cancelScheduledValues(scheduleTime)
1936
1980
  .setValueAtTime(volume * gainRight, scheduleTime);
1937
1981
  }
1938
- updateKeyBasedVolume(channel, scheduleTime) {
1939
- if (!channel.isDrum)
1940
- return;
1982
+ updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
1941
1983
  const state = channel.state;
1942
1984
  const defaultVolume = state.volume * state.expression;
1943
1985
  const defaultPan = state.pan;
1944
- for (let i = 0; i < 128; i++) {
1945
- const gainL = channel.keyBasedGainLs[i];
1946
- const gainR = channel.keyBasedGainLs[i];
1947
- if (!gainL)
1948
- continue;
1949
- if (!gainR)
1950
- continue;
1951
- const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
1952
- const volume = (0 <= keyBasedVolume)
1953
- ? defaultVolume * keyBasedVolume / 64
1954
- : defaultVolume;
1955
- const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
1956
- const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
1957
- const { gainLeft, gainRight } = this.panToGain(pan);
1958
- gainL.gain
1959
- .cancelScheduledValues(scheduleTime)
1960
- .setValueAtTime(volume * gainLeft, scheduleTime);
1961
- gainR.gain
1962
- .cancelScheduledValues(scheduleTime)
1963
- .setValueAtTime(volume * gainRight, scheduleTime);
1964
- }
1986
+ const gainL = channel.keyBasedGainLs[keyNumber];
1987
+ const gainR = channel.keyBasedGainRs[keyNumber];
1988
+ if (!gainL)
1989
+ return;
1990
+ const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
1991
+ const volume = (0 <= keyBasedVolume)
1992
+ ? defaultVolume * keyBasedVolume / 64
1993
+ : defaultVolume;
1994
+ const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
1995
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
1996
+ const { gainLeft, gainRight } = this.panToGain(pan);
1997
+ gainL.gain
1998
+ .cancelScheduledValues(scheduleTime)
1999
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2000
+ gainR.gain
2001
+ .cancelScheduledValues(scheduleTime)
2002
+ .setValueAtTime(volume * gainRight, scheduleTime);
1965
2003
  }
1966
2004
  setSustainPedal(channelNumber, value, scheduleTime) {
1967
2005
  const channel = this.channels[channelNumber];
@@ -1970,7 +2008,7 @@ export class MidyGM2 {
1970
2008
  scheduleTime ??= this.audioContext.currentTime;
1971
2009
  channel.state.sustainPedal = value / 127;
1972
2010
  if (64 <= value) {
1973
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2011
+ this.processScheduledNotes(channel, (note) => {
1974
2012
  channel.sustainNotes.push(note);
1975
2013
  });
1976
2014
  }
@@ -2013,7 +2051,7 @@ export class MidyGM2 {
2013
2051
  const state = channel.state;
2014
2052
  scheduleTime ??= this.audioContext.currentTime;
2015
2053
  state.softPedal = softPedal / 127;
2016
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2054
+ this.processScheduledNotes(channel, (note) => {
2017
2055
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2018
2056
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2019
2057
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2028,67 +2066,19 @@ export class MidyGM2 {
2028
2066
  scheduleTime ??= this.audioContext.currentTime;
2029
2067
  const channel = this.channels[channelNumber];
2030
2068
  const state = channel.state;
2031
- const reverbEffect = this.reverbEffect;
2032
- if (0 < state.reverbSendLevel) {
2033
- if (0 < reverbSendLevel) {
2034
- state.reverbSendLevel = reverbSendLevel / 127;
2035
- reverbEffect.input.gain
2036
- .cancelScheduledValues(scheduleTime)
2037
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2038
- }
2039
- else {
2040
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2041
- if (note.voiceParams.reverbEffectsSend <= 0)
2042
- return false;
2043
- if (note.reverbEffectsSend)
2044
- note.reverbEffectsSend.disconnect();
2045
- });
2046
- }
2047
- }
2048
- else {
2049
- if (0 < reverbSendLevel) {
2050
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2051
- this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2052
- });
2053
- state.reverbSendLevel = reverbSendLevel / 127;
2054
- reverbEffect.input.gain
2055
- .cancelScheduledValues(scheduleTime)
2056
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2057
- }
2058
- }
2069
+ state.reverbSendLevel = reverbSendLevel / 127;
2070
+ this.processScheduledNotes(channel, (note) => {
2071
+ this.setReverbSend(channel, note, scheduleTime);
2072
+ });
2059
2073
  }
2060
2074
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2061
2075
  scheduleTime ??= this.audioContext.currentTime;
2062
2076
  const channel = this.channels[channelNumber];
2063
2077
  const state = channel.state;
2064
- const chorusEffect = this.chorusEffect;
2065
- if (0 < state.chorusSendLevel) {
2066
- if (0 < chorusSendLevel) {
2067
- state.chorusSendLevel = chorusSendLevel / 127;
2068
- chorusEffect.input.gain
2069
- .cancelScheduledValues(scheduleTime)
2070
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2071
- }
2072
- else {
2073
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2074
- if (note.voiceParams.chorusEffectsSend <= 0)
2075
- return false;
2076
- if (note.chorusEffectsSend)
2077
- note.chorusEffectsSend.disconnect();
2078
- });
2079
- }
2080
- }
2081
- else {
2082
- if (0 < chorusSendLevel) {
2083
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2084
- this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2085
- });
2086
- state.chorusSendLevel = chorusSendLevel / 127;
2087
- chorusEffect.input.gain
2088
- .cancelScheduledValues(scheduleTime)
2089
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2090
- }
2091
- }
2078
+ state.chorusSendLevel = chorusSendLevel / 127;
2079
+ this.processScheduledNotes(channel, (note) => {
2080
+ this.setChorusSend(channel, note, scheduleTime);
2081
+ });
2092
2082
  }
2093
2083
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
2094
2084
  if (maxLSB < channel.dataLSB) {
@@ -2224,7 +2214,7 @@ export class MidyGM2 {
2224
2214
  const entries = Object.entries(defaultControllerState);
2225
2215
  for (const [key, { type, defaultValue }] of entries) {
2226
2216
  if (128 <= type) {
2227
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2217
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2228
2218
  }
2229
2219
  else {
2230
2220
  state[key] = defaultValue;
@@ -2256,7 +2246,7 @@ export class MidyGM2 {
2256
2246
  const key = keys[i];
2257
2247
  const { type, defaultValue } = defaultControllerState[key];
2258
2248
  if (128 <= type) {
2259
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2249
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2260
2250
  }
2261
2251
  else {
2262
2252
  state[key] = defaultValue;
@@ -2370,9 +2360,9 @@ export class MidyGM2 {
2370
2360
  case 9:
2371
2361
  switch (data[3]) {
2372
2362
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2373
- return this.handlePressureSysEx(data, "channelPressureTable");
2363
+ return this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
2374
2364
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2375
- return this.handleControlChangeSysEx(data);
2365
+ return this.handleControlChangeSysEx(data, scheduleTime);
2376
2366
  default:
2377
2367
  console.warn(`Unsupported Exclusive Message: ${data}`);
2378
2368
  }
@@ -2691,29 +2681,31 @@ export class MidyGM2 {
2691
2681
  : 0;
2692
2682
  return channelPressure / 127;
2693
2683
  }
2694
- setControllerParameters(channel, note, table) {
2684
+ setEffects(channel, note, table, scheduleTime) {
2695
2685
  if (0 <= table[0])
2696
- this.updateDetune(channel, note);
2686
+ this.updateDetune(channel, note, scheduleTime);
2697
2687
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2698
- if (0 <= table[1])
2699
- this.setPortamentoFilterEnvelope(channel, note);
2700
- if (0 <= table[2])
2701
- this.setPortamentoVolumeEnvelope(channel, note);
2688
+ if (0 <= table[1]) {
2689
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2690
+ }
2691
+ if (0 <= table[2]) {
2692
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2693
+ }
2702
2694
  }
2703
2695
  else {
2704
2696
  if (0 <= table[1])
2705
- this.setFilterEnvelope(channel, note);
2697
+ this.setFilterEnvelope(channel, note, scheduleTime);
2706
2698
  if (0 <= table[2])
2707
- this.setVolumeEnvelope(channel, note);
2699
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2708
2700
  }
2709
2701
  if (0 <= table[3])
2710
- this.setModLfoToPitch(channel, note);
2702
+ this.setModLfoToPitch(channel, note, scheduleTime);
2711
2703
  if (0 <= table[4])
2712
- this.setModLfoToFilterFc(channel, note);
2704
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2713
2705
  if (0 <= table[5])
2714
- this.setModLfoToVolume(channel, note);
2706
+ this.setModLfoToVolume(channel, note, scheduleTime);
2715
2707
  }
2716
- handlePressureSysEx(data, tableName) {
2708
+ handlePressureSysEx(data, tableName, scheduleTime) {
2717
2709
  const channelNumber = data[4];
2718
2710
  const channel = this.channels[channelNumber];
2719
2711
  if (channel.isDrum)
@@ -2724,34 +2716,40 @@ export class MidyGM2 {
2724
2716
  const rr = data[i + 1];
2725
2717
  table[pp] = rr;
2726
2718
  }
2719
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2720
+ this.setEffects(channel, note, table, scheduleTime);
2721
+ });
2727
2722
  }
2728
2723
  initControlTable() {
2729
2724
  const ccCount = 128;
2730
2725
  const slotSize = 6;
2731
2726
  return new Int8Array(ccCount * slotSize).fill(-1);
2732
2727
  }
2733
- applyControlTable(channel, controllerType, scheduleTime) {
2728
+ setControlChangeEffects(channel, controllerType, scheduleTime) {
2734
2729
  const slotSize = 6;
2735
2730
  const offset = controllerType * slotSize;
2736
2731
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2737
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2738
- this.setControllerParameters(channel, note, table);
2732
+ this.processScheduledNotes(channel, (note) => {
2733
+ this.setEffects(channel, note, table, scheduleTime);
2739
2734
  });
2740
2735
  }
2741
- handleControlChangeSysEx(data) {
2736
+ handleControlChangeSysEx(data, scheduleTime) {
2742
2737
  const channelNumber = data[4];
2743
2738
  const channel = this.channels[channelNumber];
2744
2739
  if (channel.isDrum)
2745
2740
  return;
2741
+ const slotSize = 6;
2746
2742
  const controllerType = data[5];
2747
- const table = channel.controlTable[controllerType];
2748
- for (let i = 6; i < data.length - 1; i += 2) {
2743
+ const offset = controllerType * slotSize;
2744
+ const table = channel.controlTable;
2745
+ for (let i = 6; i < data.length; i += 2) {
2749
2746
  const pp = data[i];
2750
2747
  const rr = data[i + 1];
2751
- table[pp] = rr;
2748
+ table[offset + pp] = rr;
2752
2749
  }
2750
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
2753
2751
  }
2754
- getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2752
+ getKeyBasedValue(channel, keyNumber, controllerType) {
2755
2753
  const index = keyNumber * 128 + controllerType;
2756
2754
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2757
2755
  return controlValue;
@@ -2763,13 +2761,20 @@ export class MidyGM2 {
2763
2761
  return;
2764
2762
  const keyNumber = data[5];
2765
2763
  const table = channel.keyBasedInstrumentControlTable;
2766
- for (let i = 6; i < data.length - 1; i += 2) {
2764
+ for (let i = 6; i < data.length; i += 2) {
2767
2765
  const controllerType = data[i];
2768
2766
  const value = data[i + 1];
2769
2767
  const index = keyNumber * 128 + controllerType;
2770
2768
  table[index] = value;
2769
+ switch (controllerType) {
2770
+ case 7:
2771
+ case 10:
2772
+ this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2773
+ break;
2774
+ default: // TODO
2775
+ this.setControlChange(channelNumber, controllerType, value, scheduleTime);
2776
+ }
2771
2777
  }
2772
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2773
2778
  }
2774
2779
  handleSysEx(data, scheduleTime) {
2775
2780
  switch (data[0]) {
@@ -2806,6 +2811,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2806
2811
  configurable: true,
2807
2812
  writable: true,
2808
2813
  value: {
2814
+ scheduleIndex: 0,
2809
2815
  detune: 0,
2810
2816
  programNumber: 0,
2811
2817
  bank: 121 * 128,