@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/script/midy.js CHANGED
@@ -71,13 +71,13 @@ class Note {
71
71
  writable: true,
72
72
  value: void 0
73
73
  });
74
- Object.defineProperty(this, "reverbEffectsSend", {
74
+ Object.defineProperty(this, "reverbSend", {
75
75
  enumerable: true,
76
76
  configurable: true,
77
77
  writable: true,
78
78
  value: void 0
79
79
  });
80
- Object.defineProperty(this, "chorusEffectsSend", {
80
+ Object.defineProperty(this, "chorusSend", {
81
81
  enumerable: true,
82
82
  configurable: true,
83
83
  writable: true,
@@ -332,13 +332,13 @@ class Midy {
332
332
  writable: true,
333
333
  value: this.initSoundFontTable()
334
334
  });
335
- Object.defineProperty(this, "audioBufferCounter", {
335
+ Object.defineProperty(this, "voiceCounter", {
336
336
  enumerable: true,
337
337
  configurable: true,
338
338
  writable: true,
339
339
  value: new Map()
340
340
  });
341
- Object.defineProperty(this, "audioBufferCache", {
341
+ Object.defineProperty(this, "voiceCache", {
342
342
  enumerable: true,
343
343
  configurable: true,
344
344
  writable: true,
@@ -380,17 +380,17 @@ class Midy {
380
380
  writable: true,
381
381
  value: []
382
382
  });
383
- Object.defineProperty(this, "instruments", {
383
+ Object.defineProperty(this, "notePromises", {
384
384
  enumerable: true,
385
385
  configurable: true,
386
386
  writable: true,
387
387
  value: []
388
388
  });
389
- Object.defineProperty(this, "notePromises", {
389
+ Object.defineProperty(this, "instruments", {
390
390
  enumerable: true,
391
391
  configurable: true,
392
392
  writable: true,
393
- value: []
393
+ value: new Set()
394
394
  });
395
395
  Object.defineProperty(this, "exclusiveClassNotes", {
396
396
  enumerable: true,
@@ -435,13 +435,11 @@ class Midy {
435
435
  const presetHeaders = soundFont.parsed.presetHeaders;
436
436
  for (let i = 0; i < presetHeaders.length; i++) {
437
437
  const presetHeader = presetHeaders[i];
438
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
439
- const banks = this.soundFontTable[presetHeader.preset];
440
- banks.set(presetHeader.bank, index);
441
- }
438
+ const banks = this.soundFontTable[presetHeader.preset];
439
+ banks.set(presetHeader.bank, index);
442
440
  }
443
441
  }
444
- async loadSoundFont(input) {
442
+ async toUint8Array(input) {
445
443
  let uint8Array;
446
444
  if (typeof input === "string") {
447
445
  const response = await fetch(input);
@@ -454,23 +452,32 @@ class Midy {
454
452
  else {
455
453
  throw new TypeError("input must be a URL string or Uint8Array");
456
454
  }
457
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
458
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
459
- this.addSoundFont(soundFont);
455
+ return uint8Array;
460
456
  }
461
- async loadMIDI(input) {
462
- let uint8Array;
463
- if (typeof input === "string") {
464
- const response = await fetch(input);
465
- const arrayBuffer = await response.arrayBuffer();
466
- uint8Array = new Uint8Array(arrayBuffer);
467
- }
468
- else if (input instanceof Uint8Array) {
469
- uint8Array = input;
457
+ async loadSoundFont(input) {
458
+ this.voiceCounter.clear();
459
+ if (Array.isArray(input)) {
460
+ const promises = new Array(input.length);
461
+ for (let i = 0; i < input.length; i++) {
462
+ promises[i] = this.toUint8Array(input[i]);
463
+ }
464
+ const uint8Arrays = await Promise.all(promises);
465
+ for (let i = 0; i < uint8Arrays.length; i++) {
466
+ const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
467
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
468
+ this.addSoundFont(soundFont);
469
+ }
470
470
  }
471
471
  else {
472
- throw new TypeError("input must be a URL string or Uint8Array");
472
+ const uint8Array = await this.toUint8Array(input);
473
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
474
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
475
+ this.addSoundFont(soundFont);
473
476
  }
477
+ }
478
+ async loadMIDI(input) {
479
+ this.voiceCounter.clear();
480
+ const uint8Array = await this.toUint8Array(input);
474
481
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
475
482
  this.ticksPerBeat = midi.header.ticksPerBeat;
476
483
  const midiData = this.extractMidiData(midi);
@@ -478,6 +485,45 @@ class Midy {
478
485
  this.timeline = midiData.timeline;
479
486
  this.totalTime = this.calcTotalTime();
480
487
  }
488
+ cacheVoiceIds() {
489
+ const timeline = this.timeline;
490
+ for (let i = 0; i < timeline.length; i++) {
491
+ const event = timeline[i];
492
+ switch (event.type) {
493
+ case "noteOn": {
494
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
495
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
496
+ break;
497
+ }
498
+ case "controller":
499
+ if (event.controllerType === 0) {
500
+ this.setBankMSB(event.channel, event.value);
501
+ }
502
+ else if (event.controllerType === 32) {
503
+ this.setBankLSB(event.channel, event.value);
504
+ }
505
+ break;
506
+ case "programChange":
507
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
508
+ }
509
+ }
510
+ for (const [audioBufferId, count] of this.voiceCounter) {
511
+ if (count === 1)
512
+ this.voiceCounter.delete(audioBufferId);
513
+ }
514
+ this.GM2SystemOn();
515
+ }
516
+ getVoiceId(channel, noteNumber, velocity) {
517
+ const bankNumber = this.calcBank(channel);
518
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
519
+ .get(bankNumber);
520
+ if (soundFontIndex === undefined)
521
+ return;
522
+ const soundFont = this.soundFonts[soundFontIndex];
523
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
524
+ const { instrument, sampleID } = voice.generators;
525
+ return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
526
+ }
481
527
  createChannelAudioNodes(audioContext) {
482
528
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
483
529
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -521,34 +567,12 @@ class Midy {
521
567
  });
522
568
  return channels;
523
569
  }
524
- async createNoteBuffer(voiceParams, isSF3) {
570
+ async createAudioBuffer(voiceParams) {
571
+ const sample = voiceParams.sample;
525
572
  const sampleStart = voiceParams.start;
526
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
527
- if (isSF3) {
528
- const sample = voiceParams.sample;
529
- const start = sample.byteOffset + sampleStart;
530
- const end = sample.byteOffset + sampleEnd;
531
- const buffer = sample.buffer.slice(start, end);
532
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
533
- return audioBuffer;
534
- }
535
- else {
536
- const sample = voiceParams.sample;
537
- const start = sample.byteOffset + sampleStart;
538
- const end = sample.byteOffset + sampleEnd;
539
- const buffer = sample.buffer.slice(start, end);
540
- const audioBuffer = new AudioBuffer({
541
- numberOfChannels: 1,
542
- length: sample.length,
543
- sampleRate: voiceParams.sampleRate,
544
- });
545
- const channelData = audioBuffer.getChannelData(0);
546
- const int16Array = new Int16Array(buffer);
547
- for (let i = 0; i < int16Array.length; i++) {
548
- channelData[i] = int16Array[i] / 32768;
549
- }
550
- return audioBuffer;
551
- }
573
+ const sampleEnd = sample.data.length + voiceParams.end;
574
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
575
+ return audioBuffer;
552
576
  }
553
577
  isLoopDrum(channel, noteNumber) {
554
578
  const programNumber = channel.programNumber;
@@ -585,16 +609,16 @@ class Midy {
585
609
  break;
586
610
  }
587
611
  case "noteAftertouch":
588
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
612
+ this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
589
613
  break;
590
614
  case "controller":
591
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
615
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
592
616
  break;
593
617
  case "programChange":
594
- this.handleProgramChange(event.channel, event.programNumber, startTime);
618
+ this.setProgramChange(event.channel, event.programNumber, startTime);
595
619
  break;
596
620
  case "channelAftertouch":
597
- this.handleChannelPressure(event.channel, event.amount, startTime);
621
+ this.setChannelPressure(event.channel, event.amount, startTime);
598
622
  break;
599
623
  case "pitchBend":
600
624
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -628,8 +652,9 @@ class Midy {
628
652
  this.notePromises = [];
629
653
  this.exclusiveClassNotes.fill(undefined);
630
654
  this.drumExclusiveClassNotes.fill(undefined);
631
- this.audioBufferCache.clear();
655
+ this.voiceCache.clear();
632
656
  for (let i = 0; i < this.channels.length; i++) {
657
+ this.channels[i].scheduledNotes = [];
633
658
  this.resetAllStates(i);
634
659
  }
635
660
  resolve();
@@ -651,8 +676,9 @@ class Midy {
651
676
  this.notePromises = [];
652
677
  this.exclusiveClassNotes.fill(undefined);
653
678
  this.drumExclusiveClassNotes.fill(undefined);
654
- this.audioBufferCache.clear();
679
+ this.voiceCache.clear();
655
680
  for (let i = 0; i < this.channels.length; i++) {
681
+ this.channels[i].scheduledNotes = [];
656
682
  this.resetAllStates(i);
657
683
  }
658
684
  this.isStopping = false;
@@ -685,11 +711,7 @@ class Midy {
685
711
  secondToTicks(second, secondsPerBeat) {
686
712
  return second * this.ticksPerBeat / secondsPerBeat;
687
713
  }
688
- getAudioBufferId(programNumber, noteNumber, velocity) {
689
- return `${programNumber}:${noteNumber}:${velocity}`;
690
- }
691
714
  extractMidiData(midi) {
692
- this.audioBufferCounter.clear();
693
715
  const instruments = new Set();
694
716
  const timeline = [];
695
717
  const tmpChannels = new Array(this.channels.length);
@@ -710,8 +732,6 @@ class Midy {
710
732
  switch (event.type) {
711
733
  case "noteOn": {
712
734
  const channel = tmpChannels[event.channel];
713
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
714
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
715
735
  if (channel.programNumber < 0) {
716
736
  channel.programNumber = event.programNumber;
717
737
  switch (channel.bankMSB) {
@@ -761,10 +781,6 @@ class Midy {
761
781
  timeline.push(event);
762
782
  }
763
783
  }
764
- for (const [audioBufferId, count] of this.audioBufferCounter) {
765
- if (count === 1)
766
- this.audioBufferCounter.delete(audioBufferId);
767
- }
768
784
  const priority = {
769
785
  controller: 0,
770
786
  sysEx: 1,
@@ -804,12 +820,11 @@ class Midy {
804
820
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
805
821
  const channel = this.channels[channelNumber];
806
822
  const promises = [];
807
- this.processScheduledNotes(channel, scheduleTime, (note) => {
823
+ this.processScheduledNotes(channel, (note) => {
808
824
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
809
825
  this.notePromises.push(promise);
810
826
  promises.push(promise);
811
827
  });
812
- channel.scheduledNotes = [];
813
828
  return Promise.all(promises);
814
829
  }
815
830
  stopNotes(velocity, force, scheduleTime) {
@@ -823,6 +838,8 @@ class Midy {
823
838
  if (this.isPlaying || this.isPaused)
824
839
  return;
825
840
  this.resumeTime = 0;
841
+ if (this.voiceCounter.size === 0)
842
+ this.cacheVoiceIds();
826
843
  await this.playNotes();
827
844
  this.isPlaying = false;
828
845
  }
@@ -863,22 +880,20 @@ class Midy {
863
880
  const now = this.audioContext.currentTime;
864
881
  return this.resumeTime + now - this.startTime - this.startDelay;
865
882
  }
866
- processScheduledNotes(channel, scheduleTime, callback) {
883
+ processScheduledNotes(channel, callback) {
867
884
  const scheduledNotes = channel.scheduledNotes;
868
- for (let i = 0; i < scheduledNotes.length; i++) {
885
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
869
886
  const note = scheduledNotes[i];
870
887
  if (!note)
871
888
  continue;
872
889
  if (note.ending)
873
890
  continue;
874
- if (note.startTime < scheduleTime)
875
- continue;
876
891
  callback(note);
877
892
  }
878
893
  }
879
894
  processActiveNotes(channel, scheduleTime, callback) {
880
895
  const scheduledNotes = channel.scheduledNotes;
881
- for (let i = 0; i < scheduledNotes.length; i++) {
896
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
882
897
  const note = scheduledNotes[i];
883
898
  if (!note)
884
899
  continue;
@@ -912,13 +927,11 @@ class Midy {
912
927
  return impulse;
913
928
  }
914
929
  createConvolutionReverb(audioContext, impulse) {
915
- const input = new GainNode(audioContext);
916
930
  const convolverNode = new ConvolverNode(audioContext, {
917
931
  buffer: impulse,
918
932
  });
919
- input.connect(convolverNode);
920
933
  return {
921
- input,
934
+ input: convolverNode,
922
935
  output: convolverNode,
923
936
  convolverNode,
924
937
  };
@@ -1069,7 +1082,7 @@ class Midy {
1069
1082
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1070
1083
  }
1071
1084
  updateChannelDetune(channel, scheduleTime) {
1072
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1085
+ this.processScheduledNotes(channel, (note) => {
1073
1086
  this.updateDetune(channel, note, scheduleTime);
1074
1087
  });
1075
1088
  }
@@ -1297,31 +1310,31 @@ class Midy {
1297
1310
  note.vibratoLFO.connect(note.vibratoDepth);
1298
1311
  note.vibratoDepth.connect(note.bufferSource.detune);
1299
1312
  }
1300
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1301
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1302
- const cache = this.audioBufferCache.get(audioBufferId);
1313
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1314
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1315
+ const cache = this.voiceCache.get(audioBufferId);
1303
1316
  if (cache) {
1304
1317
  cache.counter += 1;
1305
1318
  if (cache.maxCount <= cache.counter) {
1306
- this.audioBufferCache.delete(audioBufferId);
1319
+ this.voiceCache.delete(audioBufferId);
1307
1320
  }
1308
1321
  return cache.audioBuffer;
1309
1322
  }
1310
1323
  else {
1311
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1312
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1324
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1325
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1313
1326
  const cache = { audioBuffer, maxCount, counter: 1 };
1314
- this.audioBufferCache.set(audioBufferId, cache);
1327
+ this.voiceCache.set(audioBufferId, cache);
1315
1328
  return audioBuffer;
1316
1329
  }
1317
1330
  }
1318
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1331
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1319
1332
  const now = this.audioContext.currentTime;
1320
1333
  const state = channel.state;
1321
1334
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1322
1335
  const voiceParams = voice.getAllParams(controllerState);
1323
1336
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1324
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1337
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1325
1338
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1326
1339
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1327
1340
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -1355,12 +1368,8 @@ class Midy {
1355
1368
  }
1356
1369
  note.bufferSource.connect(note.filterNode);
1357
1370
  note.filterNode.connect(note.volumeEnvelopeNode);
1358
- if (0 < state.chorusSendLevel) {
1359
- this.setChorusEffectsSend(channel, note, 0, now);
1360
- }
1361
- if (0 < state.reverbSendLevel) {
1362
- this.setReverbEffectsSend(channel, note, 0, now);
1363
- }
1371
+ this.setChorusSend(channel, note, now);
1372
+ this.setReverbSend(channel, note, now);
1364
1373
  note.bufferSource.start(startTime);
1365
1374
  return note;
1366
1375
  }
@@ -1416,20 +1425,24 @@ class Midy {
1416
1425
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1417
1426
  const channel = this.channels[channelNumber];
1418
1427
  const bankNumber = this.calcBank(channel, channelNumber);
1419
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1428
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1429
+ .get(bankNumber);
1420
1430
  if (soundFontIndex === undefined)
1421
1431
  return;
1422
1432
  const soundFont = this.soundFonts[soundFontIndex];
1423
1433
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1424
1434
  if (!voice)
1425
1435
  return;
1426
- const isSF3 = soundFont.parsed.info.version.major === 3;
1427
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1436
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1428
1437
  if (channel.isDrum) {
1429
- const audioContext = this.audioContext;
1430
- const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1431
- channel.keyBasedGainLs[noteNumber] = gainL;
1432
- channel.keyBasedGainRs[noteNumber] = gainR;
1438
+ const { keyBasedGainLs, keyBasedGainRs } = channel;
1439
+ let gainL = keyBasedGainLs[noteNumber];
1440
+ let gainR = keyBasedGainRs[noteNumber];
1441
+ if (!gainL) {
1442
+ const audioNodes = this.createChannelAudioNodes(this.audioContext);
1443
+ gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1444
+ gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1445
+ }
1433
1446
  note.volumeEnvelopeNode.connect(gainL);
1434
1447
  note.volumeEnvelopeNode.connect(gainR);
1435
1448
  }
@@ -1463,11 +1476,11 @@ class Midy {
1463
1476
  note.vibratoDepth.disconnect();
1464
1477
  note.vibratoLFO.stop();
1465
1478
  }
1466
- if (note.reverbEffectsSend) {
1467
- note.reverbEffectsSend.disconnect();
1479
+ if (note.reverbSend) {
1480
+ note.reverbSend.disconnect();
1468
1481
  }
1469
- if (note.chorusEffectsSend) {
1470
- note.chorusEffectsSend.disconnect();
1482
+ if (note.chorusSend) {
1483
+ note.chorusSend.disconnect();
1471
1484
  }
1472
1485
  }
1473
1486
  releaseNote(channel, note, endTime) {
@@ -1507,15 +1520,29 @@ class Midy {
1507
1520
  return;
1508
1521
  }
1509
1522
  }
1510
- const note = this.findNoteOffTarget(channel, noteNumber);
1511
- if (!note)
1523
+ const index = this.findNoteOffIndex(channel, noteNumber);
1524
+ if (index < 0)
1512
1525
  return;
1526
+ const note = channel.scheduledNotes[index];
1513
1527
  note.ending = true;
1528
+ this.setNoteIndex(channel, index);
1514
1529
  this.releaseNote(channel, note, endTime);
1515
1530
  }
1516
- findNoteOffTarget(channel, noteNumber) {
1531
+ setNoteIndex(channel, index) {
1532
+ let allEnds = true;
1533
+ for (let i = channel.scheduleIndex; i < index; i++) {
1534
+ const note = channel.scheduledNotes[i];
1535
+ if (note && !note.ending) {
1536
+ allEnds = false;
1537
+ break;
1538
+ }
1539
+ }
1540
+ if (allEnds)
1541
+ channel.scheduleIndex = index + 1;
1542
+ }
1543
+ findNoteOffIndex(channel, noteNumber) {
1517
1544
  const scheduledNotes = channel.scheduledNotes;
1518
- for (let i = 0; i < scheduledNotes.length; i++) {
1545
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1519
1546
  const note = scheduledNotes[i];
1520
1547
  if (!note)
1521
1548
  continue;
@@ -1523,8 +1550,9 @@ class Midy {
1523
1550
  continue;
1524
1551
  if (note.noteNumber !== noteNumber)
1525
1552
  continue;
1526
- return note;
1553
+ return i;
1527
1554
  }
1555
+ return -1;
1528
1556
  }
1529
1557
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1530
1558
  scheduleTime ??= this.audioContext.currentTime;
@@ -1564,31 +1592,31 @@ class Midy {
1564
1592
  case 0x90:
1565
1593
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1566
1594
  case 0xA0:
1567
- return this.handlePolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1595
+ return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1568
1596
  case 0xB0:
1569
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1597
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1570
1598
  case 0xC0:
1571
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1599
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1572
1600
  case 0xD0:
1573
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1601
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1574
1602
  case 0xE0:
1575
1603
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1576
1604
  default:
1577
1605
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1578
1606
  }
1579
1607
  }
1580
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1608
+ setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1581
1609
  const channel = this.channels[channelNumber];
1582
1610
  const table = channel.polyphonicKeyPressureTable;
1583
1611
  this.processActiveNotes(channel, scheduleTime, (note) => {
1584
1612
  if (note.noteNumber === noteNumber) {
1585
1613
  note.pressure = pressure;
1586
- this.setControllerParameters(channel, note, table);
1614
+ this.setEffects(channel, note, table, scheduleTime);
1587
1615
  }
1588
1616
  });
1589
1617
  this.applyVoiceParams(channel, 10);
1590
1618
  }
1591
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1619
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1592
1620
  const channel = this.channels[channelNumber];
1593
1621
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1594
1622
  channel.programNumber = programNumber;
@@ -1603,7 +1631,7 @@ class Midy {
1603
1631
  }
1604
1632
  }
1605
1633
  }
1606
- handleChannelPressure(channelNumber, value, scheduleTime) {
1634
+ setChannelPressure(channelNumber, value, scheduleTime) {
1607
1635
  const channel = this.channels[channelNumber];
1608
1636
  if (channel.isDrum)
1609
1637
  return;
@@ -1617,7 +1645,7 @@ class Midy {
1617
1645
  }
1618
1646
  const table = channel.channelPressureTable;
1619
1647
  this.processActiveNotes(channel, scheduleTime, (note) => {
1620
- this.setControllerParameters(channel, note, table);
1648
+ this.setEffects(channel, note, table, scheduleTime);
1621
1649
  });
1622
1650
  this.applyVoiceParams(channel, 13);
1623
1651
  }
@@ -1639,13 +1667,18 @@ class Midy {
1639
1667
  this.applyVoiceParams(channel, 14, scheduleTime);
1640
1668
  }
1641
1669
  setModLfoToPitch(channel, note, scheduleTime) {
1642
- const modLfoToPitch = note.voiceParams.modLfoToPitch +
1643
- this.getLFOPitchDepth(channel, note);
1644
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1645
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1646
- note.modulationDepth.gain
1647
- .cancelScheduledValues(scheduleTime)
1648
- .setValueAtTime(modulationDepth, scheduleTime);
1670
+ if (note.modulationDepth) {
1671
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1672
+ this.getLFOPitchDepth(channel, note);
1673
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1674
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1675
+ note.modulationDepth.gain
1676
+ .cancelScheduledValues(scheduleTime)
1677
+ .setValueAtTime(modulationDepth, scheduleTime);
1678
+ }
1679
+ else {
1680
+ this.startModulation(channel, note, scheduleTime);
1681
+ }
1649
1682
  }
1650
1683
  setVibLfoToPitch(channel, note, scheduleTime) {
1651
1684
  const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
@@ -1672,63 +1705,63 @@ class Midy {
1672
1705
  .cancelScheduledValues(scheduleTime)
1673
1706
  .setValueAtTime(volumeDepth, scheduleTime);
1674
1707
  }
1675
- setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1676
- let value = note.voiceParams.reverbEffectsSend;
1708
+ setReverbSend(channel, note, scheduleTime) {
1709
+ let value = note.voiceParams.reverbEffectsSend *
1710
+ channel.state.reverbSendLevel;
1677
1711
  if (channel.isDrum) {
1678
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1679
- if (0 <= keyBasedValue) {
1680
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1681
- }
1712
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1713
+ if (0 <= keyBasedValue)
1714
+ value = keyBasedValue / 127;
1682
1715
  }
1683
- if (0 < prevValue) {
1716
+ if (!note.reverbSend) {
1684
1717
  if (0 < value) {
1685
- note.reverbEffectsSend.gain
1686
- .cancelScheduledValues(scheduleTime)
1687
- .setValueAtTime(value, scheduleTime);
1688
- }
1689
- else {
1690
- note.reverbEffectsSend.disconnect();
1718
+ note.reverbSend = new GainNode(this.audioContext, { gain: value });
1719
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1720
+ note.reverbSend.connect(this.reverbEffect.input);
1691
1721
  }
1692
1722
  }
1693
1723
  else {
1724
+ note.reverbSend.gain
1725
+ .cancelScheduledValues(scheduleTime)
1726
+ .setValueAtTime(value, scheduleTime);
1694
1727
  if (0 < value) {
1695
- if (!note.reverbEffectsSend) {
1696
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1697
- gain: value,
1698
- });
1699
- note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1728
+ note.volumeEnvelopeNode.connect(note.reverbSend);
1729
+ }
1730
+ else {
1731
+ try {
1732
+ note.volumeEnvelopeNode.disconnect(note.reverbSend);
1700
1733
  }
1701
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1734
+ catch { /* empty */ }
1702
1735
  }
1703
1736
  }
1704
1737
  }
1705
- setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1706
- let value = note.voiceParams.chorusEffectsSend;
1738
+ setChorusSend(channel, note, scheduleTime) {
1739
+ let value = note.voiceParams.chorusEffectsSend *
1740
+ channel.state.chorusSendLevel;
1707
1741
  if (channel.isDrum) {
1708
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1709
- if (0 <= keyBasedValue) {
1710
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1711
- }
1742
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1743
+ if (0 <= keyBasedValue)
1744
+ value = keyBasedValue / 127;
1712
1745
  }
1713
- if (0 < prevValue) {
1714
- if (0 < vaule) {
1715
- note.chorusEffectsSend.gain
1716
- .cancelScheduledValues(scheduleTime)
1717
- .setValueAtTime(value, scheduleTime);
1718
- }
1719
- else {
1720
- note.chorusEffectsSend.disconnect();
1746
+ if (!note.chorusSend) {
1747
+ if (0 < value) {
1748
+ note.chorusSend = new GainNode(this.audioContext, { gain: value });
1749
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1750
+ note.chorusSend.connect(this.chorusEffect.input);
1721
1751
  }
1722
1752
  }
1723
1753
  else {
1754
+ note.chorusSend.gain
1755
+ .cancelScheduledValues(scheduleTime)
1756
+ .setValueAtTime(value, scheduleTime);
1724
1757
  if (0 < value) {
1725
- if (!note.chorusEffectsSend) {
1726
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1727
- gain: value,
1728
- });
1729
- note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1758
+ note.volumeEnvelopeNode.connect(note.chorusSend);
1759
+ }
1760
+ else {
1761
+ try {
1762
+ note.volumeEnvelopeNode.disconnect(note.chorusSend);
1730
1763
  }
1731
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1764
+ catch { /* empty */ }
1732
1765
  }
1733
1766
  }
1734
1767
  }
@@ -1774,11 +1807,11 @@ class Midy {
1774
1807
  this.setModLfoToVolume(channel, note, scheduleTime);
1775
1808
  }
1776
1809
  },
1777
- chorusEffectsSend: (channel, note, prevValue, scheduleTime) => {
1778
- this.setChorusEffectsSend(channel, note, prevValue, scheduleTime);
1810
+ chorusEffectsSend: (channel, note, _prevValue, scheduleTime) => {
1811
+ this.setChorusSend(channel, note, scheduleTime);
1779
1812
  },
1780
- reverbEffectsSend: (channel, note, prevValue, scheduleTime) => {
1781
- this.setReverbEffectsSend(channel, note, prevValue, scheduleTime);
1813
+ reverbEffectsSend: (channel, note, _prevValue, scheduleTime) => {
1814
+ this.setReverbSend(channel, note, scheduleTime);
1782
1815
  },
1783
1816
  delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1784
1817
  freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
@@ -1811,7 +1844,7 @@ class Midy {
1811
1844
  return state;
1812
1845
  }
1813
1846
  applyVoiceParams(channel, controllerType, scheduleTime) {
1814
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1847
+ this.processScheduledNotes(channel, (note) => {
1815
1848
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1816
1849
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1817
1850
  let applyVolumeEnvelope = false;
@@ -1882,13 +1915,13 @@ class Midy {
1882
1915
  handlers[127] = this.polyOn;
1883
1916
  return handlers;
1884
1917
  }
1885
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1918
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1886
1919
  const handler = this.controlChangeHandlers[controllerType];
1887
1920
  if (handler) {
1888
1921
  handler.call(this, channelNumber, value, scheduleTime);
1889
1922
  const channel = this.channels[channelNumber];
1890
1923
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1891
- this.applyControlTable(channel, controllerType, scheduleTime);
1924
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
1892
1925
  }
1893
1926
  else {
1894
1927
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1899,12 +1932,11 @@ class Midy {
1899
1932
  }
1900
1933
  updateModulation(channel, scheduleTime) {
1901
1934
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1902
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1935
+ this.processScheduledNotes(channel, (note) => {
1903
1936
  if (note.modulationDepth) {
1904
1937
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1905
1938
  }
1906
1939
  else {
1907
- this.setPitchEnvelope(note, scheduleTime);
1908
1940
  this.startModulation(channel, note, scheduleTime);
1909
1941
  }
1910
1942
  });
@@ -1918,7 +1950,7 @@ class Midy {
1918
1950
  this.updateModulation(channel, scheduleTime);
1919
1951
  }
1920
1952
  updatePortamento(channel, scheduleTime) {
1921
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1953
+ this.processScheduledNotes(channel, (note) => {
1922
1954
  if (0.5 <= channel.state.portamento) {
1923
1955
  if (0 <= note.portamentoNoteNumber) {
1924
1956
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1949,8 +1981,14 @@ class Midy {
1949
1981
  scheduleTime ??= this.audioContext.currentTime;
1950
1982
  const channel = this.channels[channelNumber];
1951
1983
  channel.state.volume = volume / 127;
1952
- this.updateChannelVolume(channel, scheduleTime);
1953
- this.updateKeyBasedVolume(channel, scheduleTime);
1984
+ if (channel.isDrum) {
1985
+ for (let i = 0; i < 128; i++) {
1986
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
1987
+ }
1988
+ }
1989
+ else {
1990
+ this.updateChannelVolume(channel, scheduleTime);
1991
+ }
1954
1992
  }
1955
1993
  panToGain(pan) {
1956
1994
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1963,8 +2001,14 @@ class Midy {
1963
2001
  scheduleTime ??= this.audioContext.currentTime;
1964
2002
  const channel = this.channels[channelNumber];
1965
2003
  channel.state.pan = pan / 127;
1966
- this.updateChannelVolume(channel, scheduleTime);
1967
- this.updateKeyBasedVolume(channel, scheduleTime);
2004
+ if (channel.isDrum) {
2005
+ for (let i = 0; i < 128; i++) {
2006
+ this.updateKeyBasedVolume(channel, i, scheduleTime);
2007
+ }
2008
+ }
2009
+ else {
2010
+ this.updateChannelVolume(channel, scheduleTime);
2011
+ }
1968
2012
  }
1969
2013
  setExpression(channelNumber, expression, scheduleTime) {
1970
2014
  scheduleTime ??= this.audioContext.currentTime;
@@ -1990,33 +2034,27 @@ class Midy {
1990
2034
  .cancelScheduledValues(scheduleTime)
1991
2035
  .setValueAtTime(volume * gainRight, scheduleTime);
1992
2036
  }
1993
- updateKeyBasedVolume(channel, scheduleTime) {
1994
- if (!channel.isDrum)
1995
- return;
2037
+ updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
1996
2038
  const state = channel.state;
1997
2039
  const defaultVolume = state.volume * state.expression;
1998
2040
  const defaultPan = state.pan;
1999
- for (let i = 0; i < 128; i++) {
2000
- const gainL = channel.keyBasedGainLs[i];
2001
- const gainR = channel.keyBasedGainLs[i];
2002
- if (!gainL)
2003
- continue;
2004
- if (!gainR)
2005
- continue;
2006
- const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
2007
- const volume = (0 <= keyBasedVolume)
2008
- ? defaultVolume * keyBasedVolume / 64
2009
- : defaultVolume;
2010
- const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
2011
- const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2012
- const { gainLeft, gainRight } = this.panToGain(pan);
2013
- gainL.gain
2014
- .cancelScheduledValues(scheduleTime)
2015
- .setValueAtTime(volume * gainLeft, scheduleTime);
2016
- gainR.gain
2017
- .cancelScheduledValues(scheduleTime)
2018
- .setValueAtTime(volume * gainRight, scheduleTime);
2019
- }
2041
+ const gainL = channel.keyBasedGainLs[keyNumber];
2042
+ const gainR = channel.keyBasedGainRs[keyNumber];
2043
+ if (!gainL)
2044
+ return;
2045
+ const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2046
+ const volume = (0 <= keyBasedVolume)
2047
+ ? defaultVolume * keyBasedVolume / 64
2048
+ : defaultVolume;
2049
+ const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2050
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2051
+ const { gainLeft, gainRight } = this.panToGain(pan);
2052
+ gainL.gain
2053
+ .cancelScheduledValues(scheduleTime)
2054
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2055
+ gainR.gain
2056
+ .cancelScheduledValues(scheduleTime)
2057
+ .setValueAtTime(volume * gainRight, scheduleTime);
2020
2058
  }
2021
2059
  setSustainPedal(channelNumber, value, scheduleTime) {
2022
2060
  const channel = this.channels[channelNumber];
@@ -2025,7 +2063,7 @@ class Midy {
2025
2063
  scheduleTime ??= this.audioContext.currentTime;
2026
2064
  channel.state.sustainPedal = value / 127;
2027
2065
  if (64 <= value) {
2028
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2066
+ this.processScheduledNotes(channel, (note) => {
2029
2067
  channel.sustainNotes.push(note);
2030
2068
  });
2031
2069
  }
@@ -2068,7 +2106,7 @@ class Midy {
2068
2106
  const state = channel.state;
2069
2107
  scheduleTime ??= this.audioContext.currentTime;
2070
2108
  state.softPedal = softPedal / 127;
2071
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2109
+ this.processScheduledNotes(channel, (note) => {
2072
2110
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2073
2111
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2074
2112
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2086,7 +2124,7 @@ class Midy {
2086
2124
  scheduleTime ??= this.audioContext.currentTime;
2087
2125
  const state = channel.state;
2088
2126
  state.filterResonance = filterResonance / 127;
2089
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2127
+ this.processScheduledNotes(channel, (note) => {
2090
2128
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2091
2129
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2092
2130
  });
@@ -2104,10 +2142,10 @@ class Midy {
2104
2142
  return;
2105
2143
  scheduleTime ??= this.audioContext.currentTime;
2106
2144
  channel.state.attackTime = attackTime / 127;
2107
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2145
+ this.processScheduledNotes(channel, (note) => {
2108
2146
  if (note.startTime < scheduleTime)
2109
2147
  return false;
2110
- this.setVolumeEnvelope(channel, note);
2148
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2111
2149
  });
2112
2150
  }
2113
2151
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2117,12 +2155,12 @@ class Midy {
2117
2155
  const state = channel.state;
2118
2156
  scheduleTime ??= this.audioContext.currentTime;
2119
2157
  state.brightness = brightness / 127;
2120
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2158
+ this.processScheduledNotes(channel, (note) => {
2121
2159
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2122
2160
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2123
2161
  }
2124
2162
  else {
2125
- this.setFilterEnvelope(channel, note);
2163
+ this.setFilterEnvelope(channel, note, scheduleTime);
2126
2164
  }
2127
2165
  });
2128
2166
  }
@@ -2132,7 +2170,7 @@ class Midy {
2132
2170
  return;
2133
2171
  scheduleTime ??= this.audioContext.currentTime;
2134
2172
  channel.state.decayTime = dacayTime / 127;
2135
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2173
+ this.processScheduledNotes(channel, (note) => {
2136
2174
  this.setVolumeEnvelope(channel, note, scheduleTime);
2137
2175
  });
2138
2176
  }
@@ -2144,7 +2182,7 @@ class Midy {
2144
2182
  channel.state.vibratoRate = vibratoRate / 127;
2145
2183
  if (channel.vibratoDepth <= 0)
2146
2184
  return;
2147
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2185
+ this.processScheduledNotes(channel, (note) => {
2148
2186
  this.setVibLfoToPitch(channel, note, scheduleTime);
2149
2187
  });
2150
2188
  }
@@ -2156,12 +2194,12 @@ class Midy {
2156
2194
  const prev = channel.state.vibratoDepth;
2157
2195
  channel.state.vibratoDepth = vibratoDepth / 127;
2158
2196
  if (0 < prev) {
2159
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2197
+ this.processScheduledNotes(channel, (note) => {
2160
2198
  this.setFreqVibLFO(channel, note, scheduleTime);
2161
2199
  });
2162
2200
  }
2163
2201
  else {
2164
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2202
+ this.processScheduledNotes(channel, (note) => {
2165
2203
  this.startVibrato(channel, note, scheduleTime);
2166
2204
  });
2167
2205
  }
@@ -2173,7 +2211,7 @@ class Midy {
2173
2211
  scheduleTime ??= this.audioContext.currentTime;
2174
2212
  channel.state.vibratoDelay = vibratoDelay / 127;
2175
2213
  if (0 < channel.state.vibratoDepth) {
2176
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2214
+ this.processScheduledNotes(channel, (note) => {
2177
2215
  this.startVibrato(channel, note, scheduleTime);
2178
2216
  });
2179
2217
  }
@@ -2182,67 +2220,19 @@ class Midy {
2182
2220
  scheduleTime ??= this.audioContext.currentTime;
2183
2221
  const channel = this.channels[channelNumber];
2184
2222
  const state = channel.state;
2185
- const reverbEffect = this.reverbEffect;
2186
- if (0 < state.reverbSendLevel) {
2187
- if (0 < reverbSendLevel) {
2188
- state.reverbSendLevel = reverbSendLevel / 127;
2189
- reverbEffect.input.gain
2190
- .cancelScheduledValues(scheduleTime)
2191
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2192
- }
2193
- else {
2194
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2195
- if (note.voiceParams.reverbEffectsSend <= 0)
2196
- return false;
2197
- if (note.reverbEffectsSend)
2198
- note.reverbEffectsSend.disconnect();
2199
- });
2200
- }
2201
- }
2202
- else {
2203
- if (0 < reverbSendLevel) {
2204
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2205
- this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2206
- });
2207
- state.reverbSendLevel = reverbSendLevel / 127;
2208
- reverbEffect.input.gain
2209
- .cancelScheduledValues(scheduleTime)
2210
- .setValueAtTime(state.reverbSendLevel, scheduleTime);
2211
- }
2212
- }
2223
+ state.reverbSendLevel = reverbSendLevel / 127;
2224
+ this.processScheduledNotes(channel, (note) => {
2225
+ this.setReverbSend(channel, note, scheduleTime);
2226
+ });
2213
2227
  }
2214
2228
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
2215
2229
  scheduleTime ??= this.audioContext.currentTime;
2216
2230
  const channel = this.channels[channelNumber];
2217
2231
  const state = channel.state;
2218
- const chorusEffect = this.chorusEffect;
2219
- if (0 < state.chorusSendLevel) {
2220
- if (0 < chorusSendLevel) {
2221
- state.chorusSendLevel = chorusSendLevel / 127;
2222
- chorusEffect.input.gain
2223
- .cancelScheduledValues(scheduleTime)
2224
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2225
- }
2226
- else {
2227
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2228
- if (note.voiceParams.chorusEffectsSend <= 0)
2229
- return false;
2230
- if (note.chorusEffectsSend)
2231
- note.chorusEffectsSend.disconnect();
2232
- });
2233
- }
2234
- }
2235
- else {
2236
- if (0 < chorusSendLevel) {
2237
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2238
- this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2239
- });
2240
- state.chorusSendLevel = chorusSendLevel / 127;
2241
- chorusEffect.input.gain
2242
- .cancelScheduledValues(scheduleTime)
2243
- .setValueAtTime(state.chorusSendLevel, scheduleTime);
2244
- }
2245
- }
2232
+ state.chorusSendLevel = chorusSendLevel / 127;
2233
+ this.processScheduledNotes(channel, (note) => {
2234
+ this.setChorusSend(channel, note, scheduleTime);
2235
+ });
2246
2236
  }
2247
2237
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
2248
2238
  if (maxLSB < channel.dataLSB) {
@@ -2392,7 +2382,7 @@ class Midy {
2392
2382
  const entries = Object.entries(defaultControllerState);
2393
2383
  for (const [key, { type, defaultValue }] of entries) {
2394
2384
  if (128 <= type) {
2395
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2385
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2396
2386
  }
2397
2387
  else {
2398
2388
  state[key] = defaultValue;
@@ -2425,7 +2415,7 @@ class Midy {
2425
2415
  const key = keys[i];
2426
2416
  const { type, defaultValue } = defaultControllerState[key];
2427
2417
  if (128 <= type) {
2428
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2418
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2429
2419
  }
2430
2420
  else {
2431
2421
  state[key] = defaultValue;
@@ -2553,11 +2543,11 @@ class Midy {
2553
2543
  case 9:
2554
2544
  switch (data[3]) {
2555
2545
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2556
- return this.handlePressureSysEx(data, "channelPressureTable");
2546
+ return this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
2557
2547
  case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2558
- return this.handlePressureSysEx(data, "polyphonicKeyPressureTable");
2548
+ return this.handlePressureSysEx(data, "polyphonicKeyPressureTable", scheduleTime);
2559
2549
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2560
- return this.handleControlChangeSysEx(data);
2550
+ return this.handleControlChangeSysEx(data, scheduleTime);
2561
2551
  default:
2562
2552
  console.warn(`Unsupported Exclusive Message: ${data}`);
2563
2553
  }
@@ -2928,29 +2918,31 @@ class Midy {
2928
2918
  : 0;
2929
2919
  return (channelPressure + polyphonicKeyPressure) / 254;
2930
2920
  }
2931
- setControllerParameters(channel, note, table) {
2921
+ setEffects(channel, note, table, scheduleTime) {
2932
2922
  if (0 <= table[0])
2933
- this.updateDetune(channel, note);
2923
+ this.updateDetune(channel, note, scheduleTime);
2934
2924
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2935
- if (0 <= table[1])
2936
- this.setPortamentoFilterEnvelope(channel, note);
2937
- if (0 <= table[2])
2938
- this.setPortamentoVolumeEnvelope(channel, note);
2925
+ if (0 <= table[1]) {
2926
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2927
+ }
2928
+ if (0 <= table[2]) {
2929
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2930
+ }
2939
2931
  }
2940
2932
  else {
2941
2933
  if (0 <= table[1])
2942
- this.setFilterEnvelope(channel, note);
2934
+ this.setFilterEnvelope(channel, note, scheduleTime);
2943
2935
  if (0 <= table[2])
2944
- this.setVolumeEnvelope(channel, note);
2936
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2945
2937
  }
2946
2938
  if (0 <= table[3])
2947
- this.setModLfoToPitch(channel, note);
2939
+ this.setModLfoToPitch(channel, note, scheduleTime);
2948
2940
  if (0 <= table[4])
2949
- this.setModLfoToFilterFc(channel, note);
2941
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2950
2942
  if (0 <= table[5])
2951
- this.setModLfoToVolume(channel, note);
2943
+ this.setModLfoToVolume(channel, note, scheduleTime);
2952
2944
  }
2953
- handlePressureSysEx(data, tableName) {
2945
+ handlePressureSysEx(data, tableName, scheduleTime) {
2954
2946
  const channelNumber = data[4];
2955
2947
  const channel = this.channels[channelNumber];
2956
2948
  if (channel.isDrum)
@@ -2961,34 +2953,40 @@ class Midy {
2961
2953
  const rr = data[i + 1];
2962
2954
  table[pp] = rr;
2963
2955
  }
2956
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2957
+ this.setEffects(channel, note, table, scheduleTime);
2958
+ });
2964
2959
  }
2965
2960
  initControlTable() {
2966
2961
  const ccCount = 128;
2967
2962
  const slotSize = 6;
2968
2963
  return new Int8Array(ccCount * slotSize).fill(-1);
2969
2964
  }
2970
- applyControlTable(channel, controllerType, scheduleTime) {
2965
+ setControlChangeEffects(channel, controllerType, scheduleTime) {
2971
2966
  const slotSize = 6;
2972
2967
  const offset = controllerType * slotSize;
2973
2968
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2974
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2975
- this.setControllerParameters(channel, note, table);
2969
+ this.processScheduledNotes(channel, (note) => {
2970
+ this.setEffects(channel, note, table, scheduleTime);
2976
2971
  });
2977
2972
  }
2978
- handleControlChangeSysEx(data) {
2973
+ handleControlChangeSysEx(data, scheduleTime) {
2979
2974
  const channelNumber = data[4];
2980
2975
  const channel = this.channels[channelNumber];
2981
2976
  if (channel.isDrum)
2982
2977
  return;
2978
+ const slotSize = 6;
2983
2979
  const controllerType = data[5];
2984
- const table = channel.controlTable[controllerType];
2985
- for (let i = 6; i < data.length - 1; i += 2) {
2980
+ const offset = controllerType * slotSize;
2981
+ const table = channel.controlTable;
2982
+ for (let i = 6; i < data.length; i += 2) {
2986
2983
  const pp = data[i];
2987
2984
  const rr = data[i + 1];
2988
- table[pp] = rr;
2985
+ table[offset + pp] = rr;
2989
2986
  }
2987
+ this.setControlChangeEffects(channel, controllerType, scheduleTime);
2990
2988
  }
2991
- getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2989
+ getKeyBasedValue(channel, keyNumber, controllerType) {
2992
2990
  const index = keyNumber * 128 + controllerType;
2993
2991
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2994
2992
  return controlValue;
@@ -3000,13 +2998,20 @@ class Midy {
3000
2998
  return;
3001
2999
  const keyNumber = data[5];
3002
3000
  const table = channel.keyBasedInstrumentControlTable;
3003
- for (let i = 6; i < data.length - 1; i += 2) {
3001
+ for (let i = 6; i < data.length; i += 2) {
3004
3002
  const controllerType = data[i];
3005
3003
  const value = data[i + 1];
3006
3004
  const index = keyNumber * 128 + controllerType;
3007
3005
  table[index] = value;
3006
+ switch (controllerType) {
3007
+ case 7:
3008
+ case 10:
3009
+ this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3010
+ break;
3011
+ default: // TODO
3012
+ this.setControlChange(channelNumber, controllerType, value, scheduleTime);
3013
+ }
3008
3014
  }
3009
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3010
3015
  }
3011
3016
  handleSysEx(data, scheduleTime) {
3012
3017
  switch (data[0]) {
@@ -3044,6 +3049,7 @@ Object.defineProperty(Midy, "channelSettings", {
3044
3049
  configurable: true,
3045
3050
  writable: true,
3046
3051
  value: {
3052
+ scheduleIndex: 0,
3047
3053
  detune: 0,
3048
3054
  programNumber: 0,
3049
3055
  bank: 121 * 128,