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