@marmooo/midy 0.4.2 → 0.4.4

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
@@ -14,6 +14,12 @@ class Note {
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
+ Object.defineProperty(this, "adjustedBaseFreq", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: 20000
22
+ });
17
23
  Object.defineProperty(this, "index", {
18
24
  enumerable: true,
19
25
  configurable: true,
@@ -247,9 +253,25 @@ const pitchEnvelopeKeys = [
247
253
  "playbackRate",
248
254
  ];
249
255
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
256
+ const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
257
+ function cbToRatio(cb) {
258
+ return Math.pow(10, cb / 200);
259
+ }
260
+ const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
261
+ const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
250
262
  export class Midy extends EventTarget {
251
263
  constructor(audioContext) {
252
264
  super();
265
+ // https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
266
+ // https://pubmed.ncbi.nlm.nih.gov/12488797/
267
+ // Gap detection studies indicate humans detect temporal discontinuities
268
+ // around 2–3 ms. Smoothing over ~4 ms is perceived as continuous.
269
+ Object.defineProperty(this, "perceptualSmoothingTime", {
270
+ enumerable: true,
271
+ configurable: true,
272
+ writable: true,
273
+ value: 0.004
274
+ });
253
275
  Object.defineProperty(this, "mode", {
254
276
  enumerable: true,
255
277
  configurable: true,
@@ -410,11 +432,25 @@ export class Midy extends EventTarget {
410
432
  writable: true,
411
433
  value: false
412
434
  });
435
+ Object.defineProperty(this, "totalTimeEventTypes", {
436
+ enumerable: true,
437
+ configurable: true,
438
+ writable: true,
439
+ value: new Set([
440
+ "noteOff",
441
+ ])
442
+ });
443
+ Object.defineProperty(this, "tempo", {
444
+ enumerable: true,
445
+ configurable: true,
446
+ writable: true,
447
+ value: 1
448
+ });
413
449
  Object.defineProperty(this, "loop", {
414
450
  enumerable: true,
415
451
  configurable: true,
416
452
  writable: true,
417
- value: true
453
+ value: false
418
454
  });
419
455
  Object.defineProperty(this, "loopStart", {
420
456
  enumerable: true,
@@ -458,6 +494,33 @@ export class Midy extends EventTarget {
458
494
  writable: true,
459
495
  value: new Array(this.numChannels * drumExclusiveClassCount)
460
496
  });
497
+ Object.defineProperty(this, "mpeEnabled", {
498
+ enumerable: true,
499
+ configurable: true,
500
+ writable: true,
501
+ value: false
502
+ });
503
+ Object.defineProperty(this, "lowerMPEMembers", {
504
+ enumerable: true,
505
+ configurable: true,
506
+ writable: true,
507
+ value: 0
508
+ });
509
+ Object.defineProperty(this, "upperMPEMembers", {
510
+ enumerable: true,
511
+ configurable: true,
512
+ writable: true,
513
+ value: 0
514
+ });
515
+ Object.defineProperty(this, "mpeState", {
516
+ enumerable: true,
517
+ configurable: true,
518
+ writable: true,
519
+ value: {
520
+ channelToNote: new Map(),
521
+ noteToChannel: new Map(),
522
+ }
523
+ });
461
524
  this.audioContext = audioContext;
462
525
  this.masterVolume = new GainNode(audioContext);
463
526
  this.scheduler = new GainNode(audioContext, { gain: 0 });
@@ -535,13 +598,13 @@ export class Midy extends EventTarget {
535
598
  this.totalTime = this.calcTotalTime();
536
599
  }
537
600
  cacheVoiceIds() {
538
- const timeline = this.timeline;
601
+ const { channels, timeline, voiceCounter } = this;
539
602
  for (let i = 0; i < timeline.length; i++) {
540
603
  const event = timeline[i];
541
604
  switch (event.type) {
542
605
  case "noteOn": {
543
- const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
544
- this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
606
+ const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
607
+ voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
545
608
  break;
546
609
  }
547
610
  case "controller":
@@ -556,9 +619,9 @@ export class Midy extends EventTarget {
556
619
  this.setProgramChange(event.channel, event.programNumber, event.startTime);
557
620
  }
558
621
  }
559
- for (const [audioBufferId, count] of this.voiceCounter) {
622
+ for (const [audioBufferId, count] of voiceCounter) {
560
623
  if (count === 1)
561
- this.voiceCounter.delete(audioBufferId);
624
+ voiceCounter.delete(audioBufferId);
562
625
  }
563
626
  this.GM2SystemOn();
564
627
  }
@@ -567,8 +630,12 @@ export class Midy extends EventTarget {
567
630
  const bankTable = this.soundFontTable[programNumber];
568
631
  if (!bankTable)
569
632
  return;
570
- const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
571
- const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
633
+ let bank = channel.isDrum ? 128 : channel.bankLSB;
634
+ if (bankTable[bank] === undefined) {
635
+ if (channel.isDrum)
636
+ return;
637
+ bank = 0;
638
+ }
572
639
  const soundFontIndex = bankTable[bank];
573
640
  if (soundFontIndex === undefined)
574
641
  return;
@@ -594,8 +661,8 @@ export class Midy extends EventTarget {
594
661
  resetChannelTable(channel) {
595
662
  channel.controlTable.fill(-1);
596
663
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
597
- channel.channelPressureTable.fill(-1);
598
- channel.polyphonicKeyPressureTable.fill(-1);
664
+ channel.channelPressureTable.set(defaultPressureValues);
665
+ channel.polyphonicKeyPressureTable.set(defaultPressureValues);
599
666
  channel.keyBasedTable.fill(-1);
600
667
  }
601
668
  createChannels(audioContext) {
@@ -611,8 +678,8 @@ export class Midy extends EventTarget {
611
678
  sostenutoNotes: [],
612
679
  controlTable: this.initControlTable(),
613
680
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
614
- channelPressureTable: new Int8Array(6).fill(-1),
615
- polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
681
+ channelPressureTable: new Int8Array(defaultPressureValues),
682
+ polyphonicKeyPressureTable: new Int8Array(defaultPressureValues),
616
683
  keyBasedTable: new Int8Array(128 * 128).fill(-1),
617
684
  keyBasedGainLs: new Array(128),
618
685
  keyBasedGainRs: new Array(128),
@@ -648,11 +715,13 @@ export class Midy extends EventTarget {
648
715
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
649
716
  const schedulingOffset = this.startDelay - timeOffset;
650
717
  const timeline = this.timeline;
718
+ const inverseTempo = 1 / this.tempo;
651
719
  while (queueIndex < timeline.length) {
652
720
  const event = timeline[queueIndex];
653
- if (lookAheadCheckTime < event.startTime)
721
+ const t = event.startTime * inverseTempo;
722
+ if (lookAheadCheckTime < t)
654
723
  break;
655
- const startTime = event.startTime + schedulingOffset;
724
+ const startTime = t + schedulingOffset;
656
725
  switch (event.type) {
657
726
  case "noteOn":
658
727
  this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
@@ -684,8 +753,10 @@ export class Midy extends EventTarget {
684
753
  return queueIndex;
685
754
  }
686
755
  getQueueIndex(second) {
687
- for (let i = 0; i < this.timeline.length; i++) {
688
- if (second <= this.timeline[i].startTime) {
756
+ const timeline = this.timeline;
757
+ const inverseTempo = 1 / this.tempo;
758
+ for (let i = 0; i < timeline.length; i++) {
759
+ if (second <= timeline[i].startTime * inverseTempo) {
689
760
  return i;
690
761
  }
691
762
  }
@@ -696,40 +767,44 @@ export class Midy extends EventTarget {
696
767
  this.drumExclusiveClassNotes.fill(undefined);
697
768
  this.voiceCache.clear();
698
769
  this.realtimeVoiceCache.clear();
699
- for (let i = 0; i < this.channels.length; i++) {
700
- this.channels[i].scheduledNotes = [];
770
+ const channels = this.channels;
771
+ for (let i = 0; i < channels.length; i++) {
772
+ channels[i].scheduledNotes = [];
701
773
  this.resetChannelStates(i);
702
774
  }
703
775
  }
704
776
  updateStates(queueIndex, nextQueueIndex) {
777
+ const { timeline, resumeTime } = this;
778
+ const inverseTempo = 1 / this.tempo;
705
779
  const now = this.audioContext.currentTime;
706
780
  if (nextQueueIndex < queueIndex)
707
781
  queueIndex = 0;
708
782
  for (let i = queueIndex; i < nextQueueIndex; i++) {
709
- const event = this.timeline[i];
783
+ const event = timeline[i];
710
784
  switch (event.type) {
711
785
  case "controller":
712
- this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
786
+ this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
713
787
  break;
714
788
  case "programChange":
715
- this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
789
+ this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
716
790
  break;
717
791
  case "pitchBend":
718
- this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
792
+ this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
719
793
  break;
720
794
  case "sysEx":
721
- this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
795
+ this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
722
796
  }
723
797
  }
724
798
  }
725
799
  async playNotes() {
726
- if (this.audioContext.state === "suspended") {
727
- await this.audioContext.resume();
800
+ const audioContext = this.audioContext;
801
+ if (audioContext.state === "suspended") {
802
+ await audioContext.resume();
728
803
  }
729
804
  const paused = this.isPaused;
730
805
  this.isPlaying = true;
731
806
  this.isPaused = false;
732
- this.startTime = this.audioContext.currentTime;
807
+ this.startTime = audioContext.currentTime;
733
808
  if (paused) {
734
809
  this.dispatchEvent(new Event("resumed"));
735
810
  }
@@ -740,20 +815,20 @@ export class Midy extends EventTarget {
740
815
  let exitReason;
741
816
  this.notePromises = [];
742
817
  while (true) {
743
- const now = this.audioContext.currentTime;
818
+ const now = audioContext.currentTime;
744
819
  if (0 < this.lastActiveSensing &&
745
820
  this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
746
821
  await this.stopNotes(0, true, now);
747
- await this.audioContext.suspend();
822
+ await audioContext.suspend();
748
823
  exitReason = "aborted";
749
824
  break;
750
825
  }
751
- if (this.timeline.length <= queueIndex) {
826
+ if (this.totalTime < this.currentTime() ||
827
+ this.timeline.length <= queueIndex) {
752
828
  await this.stopNotes(0, true, now);
753
829
  if (this.loop) {
754
- this.notePromises = [];
755
830
  this.resetAllStates();
756
- this.startTime = this.audioContext.currentTime;
831
+ this.startTime = audioContext.currentTime;
757
832
  this.resumeTime = this.loopStart;
758
833
  if (0 < this.loopStart) {
759
834
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
@@ -767,29 +842,28 @@ export class Midy extends EventTarget {
767
842
  continue;
768
843
  }
769
844
  else {
770
- await this.audioContext.suspend();
845
+ await audioContext.suspend();
771
846
  exitReason = "ended";
772
847
  break;
773
848
  }
774
849
  }
775
850
  if (this.isPausing) {
776
851
  await this.stopNotes(0, true, now);
777
- await this.audioContext.suspend();
778
- this.notePromises = [];
852
+ await audioContext.suspend();
779
853
  this.isPausing = false;
780
854
  exitReason = "paused";
781
855
  break;
782
856
  }
783
857
  else if (this.isStopping) {
784
858
  await this.stopNotes(0, true, now);
785
- await this.audioContext.suspend();
859
+ await audioContext.suspend();
786
860
  this.isStopping = false;
787
861
  exitReason = "stopped";
788
862
  break;
789
863
  }
790
864
  else if (this.isSeeking) {
791
865
  this.stopNotes(0, true, now);
792
- this.startTime = this.audioContext.currentTime;
866
+ this.startTime = audioContext.currentTime;
793
867
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
794
868
  this.updateStates(queueIndex, nextQueueIndex);
795
869
  queueIndex = nextQueueIndex;
@@ -802,7 +876,6 @@ export class Midy extends EventTarget {
802
876
  await this.scheduleTask(() => { }, waitTime);
803
877
  }
804
878
  if (exitReason !== "paused") {
805
- this.notePromises = [];
806
879
  this.resetAllStates();
807
880
  this.lastActiveSensing = 0;
808
881
  }
@@ -931,11 +1004,13 @@ export class Midy extends EventTarget {
931
1004
  return Promise.all(promises);
932
1005
  }
933
1006
  stopNotes(velocity, force, scheduleTime) {
934
- const promises = [];
935
- for (let i = 0; i < this.channels.length; i++) {
936
- promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
1007
+ const channels = this.channels;
1008
+ for (let i = 0; i < channels.length; i++) {
1009
+ this.stopChannelNotes(i, velocity, force, scheduleTime);
937
1010
  }
938
- return Promise.all(this.notePromises);
1011
+ const stopPromise = Promise.all(this.notePromises);
1012
+ this.notePromises = [];
1013
+ return stopPromise;
939
1014
  }
940
1015
  async start() {
941
1016
  if (this.isPlaying || this.isPaused)
@@ -972,12 +1047,25 @@ export class Midy extends EventTarget {
972
1047
  this.isSeeking = true;
973
1048
  }
974
1049
  }
1050
+ tempoChange(tempo) {
1051
+ const timeScale = this.tempo / tempo;
1052
+ this.resumeTime = this.resumeTime * timeScale;
1053
+ this.tempo = tempo;
1054
+ this.totalTime = this.calcTotalTime();
1055
+ this.seekTo(this.currentTime() * timeScale);
1056
+ }
975
1057
  calcTotalTime() {
1058
+ const totalTimeEventTypes = this.totalTimeEventTypes;
1059
+ const timeline = this.timeline;
1060
+ const inverseTempo = 1 / this.tempo;
976
1061
  let totalTime = 0;
977
- for (let i = 0; i < this.timeline.length; i++) {
978
- const event = this.timeline[i];
979
- if (totalTime < event.startTime)
980
- totalTime = event.startTime;
1062
+ for (let i = 0; i < timeline.length; i++) {
1063
+ const event = timeline[i];
1064
+ if (!totalTimeEventTypes.has(event.type))
1065
+ continue;
1066
+ const t = event.startTime * inverseTempo;
1067
+ if (totalTime < t)
1068
+ totalTime = t;
981
1069
  }
982
1070
  return totalTime + this.startDelay;
983
1071
  }
@@ -1159,9 +1247,6 @@ export class Midy extends EventTarget {
1159
1247
  feedbackGains,
1160
1248
  };
1161
1249
  }
1162
- cbToRatio(cb) {
1163
- return Math.pow(10, cb / 200);
1164
- }
1165
1250
  rateToCent(rate) {
1166
1251
  return 1200 * Math.log2(rate);
1167
1252
  }
@@ -1191,39 +1276,24 @@ export class Midy extends EventTarget {
1191
1276
  return tuning + pitch;
1192
1277
  }
1193
1278
  }
1194
- calcNoteDetune(channel, note) {
1195
- return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1196
- }
1197
1279
  updateChannelDetune(channel, scheduleTime) {
1198
1280
  this.processScheduledNotes(channel, (note) => {
1199
- this.updateDetune(channel, note, scheduleTime);
1281
+ if (this.isPortamento(channel, note)) {
1282
+ this.setPortamentoDetune(channel, note, scheduleTime);
1283
+ }
1284
+ else {
1285
+ this.setDetune(channel, note, scheduleTime);
1286
+ }
1200
1287
  });
1201
1288
  }
1202
- updateDetune(channel, note, scheduleTime) {
1203
- const noteDetune = this.calcNoteDetune(channel, note);
1289
+ calcScaleOctaveTuning(channel, note) {
1290
+ return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1291
+ }
1292
+ calcNoteDetune(channel, note) {
1293
+ const noteDetune = note.voiceParams.detune +
1294
+ this.calcScaleOctaveTuning(channel, note);
1204
1295
  const pitchControl = this.getPitchControl(channel, note);
1205
- const detune = channel.detune + noteDetune + pitchControl;
1206
- if (channel.portamentoControl) {
1207
- const state = channel.state;
1208
- const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1209
- note.portamentoNoteNumber = portamentoNoteNumber;
1210
- channel.portamentoControl = false;
1211
- state.portamentoNoteNumber = 0;
1212
- }
1213
- if (this.isPortamento(channel, note)) {
1214
- const startTime = note.startTime;
1215
- const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1216
- const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1217
- note.bufferSource.detune
1218
- .cancelScheduledValues(scheduleTime)
1219
- .setValueAtTime(detune - deltaCent, scheduleTime)
1220
- .linearRampToValueAtTime(detune, portamentoTime);
1221
- }
1222
- else {
1223
- note.bufferSource.detune
1224
- .cancelScheduledValues(scheduleTime)
1225
- .setValueAtTime(detune, scheduleTime);
1226
- }
1296
+ return channel.detune + noteDetune + pitchControl;
1227
1297
  }
1228
1298
  getPortamentoTime(channel, note) {
1229
1299
  const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
@@ -1290,20 +1360,17 @@ export class Midy extends EventTarget {
1290
1360
  }
1291
1361
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1292
1362
  const { voiceParams, startTime } = note;
1293
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1363
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
1294
1364
  (1 + this.getAmplitudeControl(channel, note));
1295
1365
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1296
- const volDelay = startTime + voiceParams.volDelay;
1297
- const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1298
- const volAttack = volDelay + voiceParams.volAttack * attackTime;
1299
- const volHold = volAttack + voiceParams.volHold;
1366
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1300
1367
  note.volumeEnvelopeNode.gain
1301
1368
  .cancelScheduledValues(scheduleTime)
1302
- .setValueAtTime(sustainVolume, volHold);
1369
+ .exponentialRampToValueAtTime(sustainVolume, portamentoTime);
1303
1370
  }
1304
1371
  setVolumeEnvelope(channel, note, scheduleTime) {
1305
1372
  const { voiceParams, startTime } = note;
1306
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1373
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
1307
1374
  (1 + this.getAmplitudeControl(channel, note));
1308
1375
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1309
1376
  const volDelay = startTime + voiceParams.volDelay;
@@ -1311,42 +1378,69 @@ export class Midy extends EventTarget {
1311
1378
  const volAttack = volDelay + voiceParams.volAttack * attackTime;
1312
1379
  const volHold = volAttack + voiceParams.volHold;
1313
1380
  const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
1314
- const volDecay = volHold + voiceParams.volDecay * decayTime;
1381
+ const decayDuration = voiceParams.volDecay * decayTime;
1315
1382
  note.volumeEnvelopeNode.gain
1316
1383
  .cancelScheduledValues(scheduleTime)
1317
1384
  .setValueAtTime(0, startTime)
1318
- .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
1385
+ .setValueAtTime(1e-6, volDelay)
1319
1386
  .exponentialRampToValueAtTime(attackVolume, volAttack)
1320
1387
  .setValueAtTime(attackVolume, volHold)
1321
- .linearRampToValueAtTime(sustainVolume, volDecay);
1388
+ .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
1389
+ }
1390
+ setPortamentoDetune(channel, note, scheduleTime) {
1391
+ if (channel.portamentoControl) {
1392
+ const state = channel.state;
1393
+ const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1394
+ note.portamentoNoteNumber = portamentoNoteNumber;
1395
+ channel.portamentoControl = false;
1396
+ state.portamentoNoteNumber = 0;
1397
+ }
1398
+ const detune = this.calcNoteDetune(channel, note);
1399
+ const startTime = note.startTime;
1400
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1401
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1402
+ note.bufferSource.detune
1403
+ .cancelScheduledValues(scheduleTime)
1404
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1405
+ .linearRampToValueAtTime(detune, portamentoTime);
1406
+ }
1407
+ setDetune(channel, note, scheduleTime) {
1408
+ const detune = this.calcNoteDetune(channel, note);
1409
+ note.bufferSource.detune
1410
+ .cancelScheduledValues(scheduleTime)
1411
+ .setValueAtTime(detune, scheduleTime);
1412
+ const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
1413
+ note.bufferSource.detune
1414
+ .cancelAndHoldAtTime(scheduleTime)
1415
+ .setTargetAtTime(detune, scheduleTime, timeConstant);
1322
1416
  }
1323
- setPortamentoPitchEnvelope(note, scheduleTime) {
1417
+ setPortamentoPitchEnvelope(channel, note, scheduleTime) {
1324
1418
  const baseRate = note.voiceParams.playbackRate;
1419
+ const portamentoTime = note.startTime +
1420
+ this.getPortamentoTime(channel, note);
1325
1421
  note.bufferSource.playbackRate
1326
1422
  .cancelScheduledValues(scheduleTime)
1327
- .setValueAtTime(baseRate, scheduleTime);
1423
+ .exponentialRampToValueAtTime(baseRate, portamentoTime);
1328
1424
  }
1329
1425
  setPitchEnvelope(note, scheduleTime) {
1330
- const { voiceParams } = note;
1426
+ const { bufferSource, voiceParams } = note;
1331
1427
  const baseRate = voiceParams.playbackRate;
1332
- note.bufferSource.playbackRate
1428
+ bufferSource.playbackRate
1333
1429
  .cancelScheduledValues(scheduleTime)
1334
1430
  .setValueAtTime(baseRate, scheduleTime);
1335
1431
  const modEnvToPitch = voiceParams.modEnvToPitch;
1336
1432
  if (modEnvToPitch === 0)
1337
1433
  return;
1338
- const basePitch = this.rateToCent(baseRate);
1339
- const peekPitch = basePitch + modEnvToPitch;
1340
- const peekRate = this.centToRate(peekPitch);
1434
+ const peekRate = baseRate * this.centToRate(modEnvToPitch);
1341
1435
  const modDelay = note.startTime + voiceParams.modDelay;
1342
1436
  const modAttack = modDelay + voiceParams.modAttack;
1343
1437
  const modHold = modAttack + voiceParams.modHold;
1344
- const modDecay = modHold + voiceParams.modDecay;
1345
- note.bufferSource.playbackRate
1438
+ const decayDuration = voiceParams.modDecay;
1439
+ bufferSource.playbackRate
1346
1440
  .setValueAtTime(baseRate, modDelay)
1347
1441
  .exponentialRampToValueAtTime(peekRate, modAttack)
1348
1442
  .setValueAtTime(peekRate, modHold)
1349
- .linearRampToValueAtTime(baseRate, modDecay);
1443
+ .setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
1350
1444
  }
1351
1445
  clampCutoffFrequency(frequency) {
1352
1446
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -1356,60 +1450,67 @@ export class Midy extends EventTarget {
1356
1450
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1357
1451
  const { voiceParams, startTime } = note;
1358
1452
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1453
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1454
+ const scale = softPedalFactor * brightness;
1359
1455
  const baseCent = voiceParams.initialFilterFc +
1360
1456
  this.getFilterCutoffControl(channel, note);
1361
- const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1362
- const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1363
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * brightness;
1364
- const sustainFreq = baseFreq +
1365
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1457
+ const sustainCent = baseCent +
1458
+ voiceParams.modEnvToFilterFc * (1 - voiceParams.modSustain);
1459
+ const baseFreq = this.centToHz(baseCent) * scale;
1460
+ const sustainFreq = this.centToHz(sustainCent) * scale;
1366
1461
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1367
1462
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1368
1463
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1369
1464
  const modDelay = startTime + voiceParams.modDelay;
1465
+ note.adjustedBaseFreq = adjustedSustainFreq;
1370
1466
  note.filterNode.frequency
1371
1467
  .cancelScheduledValues(scheduleTime)
1372
1468
  .setValueAtTime(adjustedBaseFreq, startTime)
1373
1469
  .setValueAtTime(adjustedBaseFreq, modDelay)
1374
- .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1470
+ .exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1375
1471
  }
1376
1472
  setFilterEnvelope(channel, note, scheduleTime) {
1377
1473
  const { voiceParams, startTime } = note;
1378
- const softPedalFactor = this.getSoftPedalFactor(channel, note);
1474
+ const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
1379
1475
  const baseCent = voiceParams.initialFilterFc +
1380
1476
  this.getFilterCutoffControl(channel, note);
1477
+ const peekCent = baseCent + modEnvToFilterFc;
1478
+ const sustainCent = baseCent +
1479
+ modEnvToFilterFc * (1 - voiceParams.modSustain);
1480
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1381
1481
  const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1382
- const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1383
- const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1384
- softPedalFactor * brightness;
1385
- const sustainFreq = baseFreq +
1386
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1482
+ const scale = softPedalFactor * brightness;
1483
+ const baseFreq = this.centToHz(baseCent) * scale;
1484
+ const peekFreq = this.centToHz(peekCent) * scale;
1485
+ const sustainFreq = this.centToHz(sustainCent) * scale;
1387
1486
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1388
1487
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
1389
1488
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1390
1489
  const modDelay = startTime + voiceParams.modDelay;
1391
1490
  const modAttack = modDelay + voiceParams.modAttack;
1392
1491
  const modHold = modAttack + voiceParams.modHold;
1393
- const modDecay = modHold + voiceParams.modDecay;
1492
+ const decayDuration = modHold + voiceParams.modDecay;
1493
+ note.adjustedBaseFreq = adjustedBaseFreq;
1394
1494
  note.filterNode.frequency
1395
1495
  .cancelScheduledValues(scheduleTime)
1396
1496
  .setValueAtTime(adjustedBaseFreq, startTime)
1397
1497
  .setValueAtTime(adjustedBaseFreq, modDelay)
1398
1498
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1399
1499
  .setValueAtTime(adjustedPeekFreq, modHold)
1400
- .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
1500
+ .setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
1401
1501
  }
1402
1502
  startModulation(channel, note, scheduleTime) {
1503
+ const audioContext = this.audioContext;
1403
1504
  const { voiceParams } = note;
1404
- note.modulationLFO = new OscillatorNode(this.audioContext, {
1505
+ note.modulationLFO = new OscillatorNode(audioContext, {
1405
1506
  frequency: this.centToHz(voiceParams.freqModLFO),
1406
1507
  });
1407
- note.filterDepth = new GainNode(this.audioContext, {
1508
+ note.filterDepth = new GainNode(audioContext, {
1408
1509
  gain: voiceParams.modLfoToFilterFc,
1409
1510
  });
1410
- note.modulationDepth = new GainNode(this.audioContext);
1511
+ note.modulationDepth = new GainNode(audioContext);
1411
1512
  this.setModLfoToPitch(channel, note, scheduleTime);
1412
- note.volumeDepth = new GainNode(this.audioContext);
1513
+ note.volumeDepth = new GainNode(audioContext);
1413
1514
  this.setModLfoToVolume(channel, note, scheduleTime);
1414
1515
  note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1415
1516
  note.modulationLFO.connect(note.filterDepth);
@@ -1461,7 +1562,8 @@ export class Midy extends EventTarget {
1461
1562
  }
1462
1563
  }
1463
1564
  async setNoteAudioNode(channel, note, realtime) {
1464
- const now = this.audioContext.currentTime;
1565
+ const audioContext = this.audioContext;
1566
+ const now = audioContext.currentTime;
1465
1567
  const { noteNumber, velocity, startTime } = note;
1466
1568
  const state = channel.state;
1467
1569
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
@@ -1469,9 +1571,9 @@ export class Midy extends EventTarget {
1469
1571
  note.voiceParams = voiceParams;
1470
1572
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1471
1573
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1472
- note.volumeEnvelopeNode = new GainNode(this.audioContext);
1574
+ note.volumeEnvelopeNode = new GainNode(audioContext);
1473
1575
  const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
1474
- note.filterNode = new BiquadFilterNode(this.audioContext, {
1576
+ note.filterNode = new BiquadFilterNode(audioContext, {
1475
1577
  type: "lowpass",
1476
1578
  Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1477
1579
  });
@@ -1482,14 +1584,15 @@ export class Midy extends EventTarget {
1482
1584
  if (!channel.isDrum && this.isPortamento(channel, note)) {
1483
1585
  this.setPortamentoVolumeEnvelope(channel, note, now);
1484
1586
  this.setPortamentoFilterEnvelope(channel, note, now);
1485
- this.setPortamentoPitchEnvelope(note, now);
1587
+ this.setPortamentoPitchEnvelope(channel, note, now);
1588
+ this.setPortamentoDetune(channel, note, now);
1486
1589
  }
1487
1590
  else {
1488
1591
  this.setVolumeEnvelope(channel, note, now);
1489
1592
  this.setFilterEnvelope(channel, note, now);
1490
1593
  this.setPitchEnvelope(note, now);
1594
+ this.setDetune(channel, note, now);
1491
1595
  }
1492
- this.updateDetune(channel, note, now);
1493
1596
  if (0 < state.vibratoDepth) {
1494
1597
  this.startVibrato(channel, note, now);
1495
1598
  }
@@ -1572,6 +1675,16 @@ export class Midy extends EventTarget {
1572
1675
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1573
1676
  }
1574
1677
  async noteOn(channelNumber, noteNumber, velocity, startTime) {
1678
+ if (this.mpeEnabled) {
1679
+ const note = await this.startNote(channelNumber, noteNumber, velocity, startTime);
1680
+ this.mpeState.channelToNote.set(channelNumber, note.index);
1681
+ this.mpeState.noteToChannel.set(note.index, channelNumber);
1682
+ }
1683
+ else {
1684
+ await this.startNote(channelNumber, noteNumber, velocity, startTime);
1685
+ }
1686
+ }
1687
+ async startNote(channelNumber, noteNumber, velocity, startTime) {
1575
1688
  const channel = this.channels[channelNumber];
1576
1689
  const realtime = startTime === undefined;
1577
1690
  if (realtime)
@@ -1582,10 +1695,12 @@ export class Midy extends EventTarget {
1582
1695
  scheduledNotes.push(note);
1583
1696
  const programNumber = channel.programNumber;
1584
1697
  const bankTable = this.soundFontTable[programNumber];
1585
- if (!bankTable)
1586
- return;
1587
- const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1588
- const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1698
+ let bank = channel.isDrum ? 128 : channel.bankLSB;
1699
+ if (bankTable[bank] === undefined) {
1700
+ if (channel.isDrum)
1701
+ return;
1702
+ bank = 0;
1703
+ }
1589
1704
  const soundFontIndex = bankTable[bank];
1590
1705
  if (soundFontIndex === undefined)
1591
1706
  return;
@@ -1596,6 +1711,7 @@ export class Midy extends EventTarget {
1596
1711
  await this.setNoteAudioNode(channel, note, realtime);
1597
1712
  this.setNoteRouting(channelNumber, note, startTime);
1598
1713
  note.resolveReady();
1714
+ return note;
1599
1715
  }
1600
1716
  disconnectNote(note) {
1601
1717
  note.bufferSource.disconnect();
@@ -1620,27 +1736,45 @@ export class Midy extends EventTarget {
1620
1736
  releaseNote(channel, note, endTime) {
1621
1737
  endTime ??= this.audioContext.currentTime;
1622
1738
  const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1623
- const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1624
- const modRelease = endTime + note.voiceParams.modRelease;
1625
- const stopTime = Math.min(volRelease, modRelease);
1739
+ const volDuration = note.voiceParams.volRelease * releaseTime;
1740
+ const volRelease = endTime + volDuration;
1626
1741
  note.filterNode.frequency
1627
1742
  .cancelScheduledValues(endTime)
1628
- .linearRampToValueAtTime(0, modRelease);
1743
+ .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
1629
1744
  note.volumeEnvelopeNode.gain
1630
1745
  .cancelScheduledValues(endTime)
1631
- .linearRampToValueAtTime(0, volRelease);
1746
+ .setTargetAtTime(0, endTime, volDuration * releaseCurve);
1632
1747
  return new Promise((resolve) => {
1633
1748
  this.scheduleTask(() => {
1634
1749
  const bufferSource = note.bufferSource;
1635
1750
  bufferSource.loop = false;
1636
- bufferSource.stop(stopTime);
1751
+ bufferSource.stop(volRelease);
1637
1752
  this.disconnectNote(note);
1638
1753
  channel.scheduledNotes[note.index] = undefined;
1639
1754
  resolve();
1640
- }, stopTime);
1755
+ }, volRelease);
1641
1756
  });
1642
1757
  }
1643
- noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1758
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1759
+ if (this.mpeEnabled) {
1760
+ const noteIndex = this.mpeState.channelToNote.get(channelNumber);
1761
+ if (noteIndex === undefined)
1762
+ return;
1763
+ const channel = this.channels[channelNumber];
1764
+ const note = channel.scheduledNotes[noteIndex];
1765
+ note.ending = true;
1766
+ const promise = note.ready.then(() => {
1767
+ return this.releaseNote(channel, note, endTime);
1768
+ });
1769
+ this.mpeState.channelToNote.delete(channelNumber);
1770
+ this.mpeState.noteToChannel.delete(noteIndex);
1771
+ return promise;
1772
+ }
1773
+ else {
1774
+ return this.stopNote(channelNumber, noteNumber, velocity, endTime, force);
1775
+ }
1776
+ }
1777
+ stopNote(channelNumber, noteNumber, _velocity, endTime, force) {
1644
1778
  const channel = this.channels[channelNumber];
1645
1779
  const state = channel.state;
1646
1780
  if (!force) {
@@ -1757,6 +1891,10 @@ export class Midy extends EventTarget {
1757
1891
  }
1758
1892
  setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1759
1893
  const channel = this.channels[channelNumber];
1894
+ if (channel.isMPEMember)
1895
+ return;
1896
+ if (!(0 <= scheduleTime))
1897
+ scheduleTime = this.audioContext.currentTime;
1760
1898
  const table = channel.polyphonicKeyPressureTable;
1761
1899
  this.processActiveNotes(channel, scheduleTime, (note) => {
1762
1900
  if (note.noteNumber === noteNumber) {
@@ -1782,6 +1920,8 @@ export class Midy extends EventTarget {
1782
1920
  }
1783
1921
  }
1784
1922
  setChannelPressure(channelNumber, value, scheduleTime) {
1923
+ if (!(0 <= scheduleTime))
1924
+ scheduleTime = this.audioContext.currentTime;
1785
1925
  const channel = this.channels[channelNumber];
1786
1926
  if (channel.isDrum)
1787
1927
  return;
@@ -1857,7 +1997,7 @@ export class Midy extends EventTarget {
1857
1997
  }
1858
1998
  setModLfoToVolume(channel, note, scheduleTime) {
1859
1999
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1860
- const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
2000
+ const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
1861
2001
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1862
2002
  (1 + this.getLFOAmplitudeDepth(channel, note));
1863
2003
  note.volumeDepth.gain
@@ -1937,13 +2077,6 @@ export class Midy extends EventTarget {
1937
2077
  .cancelScheduledValues(scheduleTime)
1938
2078
  .setValueAtTime(freqModLFO, scheduleTime);
1939
2079
  }
1940
- setFreqVibLFO(channel, note, scheduleTime) {
1941
- const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1942
- const freqVibLFO = note.voiceParams.freqVibLFO;
1943
- note.vibratoLFO.frequency
1944
- .cancelScheduledValues(scheduleTime)
1945
- .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
1946
- }
1947
2080
  setDelayVibLFO(channel, note) {
1948
2081
  const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1949
2082
  const value = note.voiceParams.delayVibLFO;
@@ -1953,6 +2086,13 @@ export class Midy extends EventTarget {
1953
2086
  }
1954
2087
  catch { /* empty */ }
1955
2088
  }
2089
+ setFreqVibLFO(channel, note, scheduleTime) {
2090
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
2091
+ const freqVibLFO = note.voiceParams.freqVibLFO;
2092
+ note.vibratoLFO.frequency
2093
+ .cancelScheduledValues(scheduleTime)
2094
+ .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
2095
+ }
1956
2096
  createVoiceParamsHandlers() {
1957
2097
  return {
1958
2098
  modLfoToPitch: (channel, note, scheduleTime) => {
@@ -2006,6 +2146,14 @@ export class Midy extends EventTarget {
2006
2146
  this.setFreqVibLFO(channel, note, scheduleTime);
2007
2147
  }
2008
2148
  },
2149
+ detune: (channel, note, scheduleTime) => {
2150
+ if (this.isPortamento(channel, note)) {
2151
+ this.setPortamentoDetune(channel, note, scheduleTime);
2152
+ }
2153
+ else {
2154
+ this.setDetune(channel, note, scheduleTime);
2155
+ }
2156
+ },
2009
2157
  };
2010
2158
  }
2011
2159
  getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
@@ -2097,6 +2245,21 @@ export class Midy extends EventTarget {
2097
2245
  return handlers;
2098
2246
  }
2099
2247
  setControlChange(channelNumber, controllerType, value, scheduleTime) {
2248
+ const channel = this.channels[channelNumber];
2249
+ if (channel.isMPEMember) {
2250
+ this.applyControlChange(channelNumber, controllerType, value, scheduleTime);
2251
+ }
2252
+ else if (channel.isMPEManager) {
2253
+ channel.state[controllerType] = value / 127;
2254
+ for (const memberChannel of this.mpeState.channelToNote.keys()) {
2255
+ this.applyControlChange(memberChannel, controllerType, value, scheduleTime);
2256
+ }
2257
+ }
2258
+ else {
2259
+ this.applyControlChange(channelNumber, controllerType, value, scheduleTime);
2260
+ }
2261
+ }
2262
+ applyControlChange(channelNumber, controllerType, value, scheduleTime) {
2100
2263
  const handler = this.controlChangeHandlers[controllerType];
2101
2264
  if (handler) {
2102
2265
  handler.call(this, channelNumber, value, scheduleTime);
@@ -2143,14 +2306,14 @@ export class Midy extends EventTarget {
2143
2306
  if (this.isPortamento(channel, note)) {
2144
2307
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2145
2308
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2146
- this.setPortamentoPitchEnvelope(note, scheduleTime);
2147
- this.updateDetune(channel, note, scheduleTime);
2309
+ this.setPortamentoPitchEnvelope(channel, note, scheduleTime);
2310
+ this.setPortamentoDetune(channel, note, scheduleTime);
2148
2311
  }
2149
2312
  else {
2150
2313
  this.setVolumeEnvelope(channel, note, scheduleTime);
2151
2314
  this.setFilterEnvelope(channel, note, scheduleTime);
2152
2315
  this.setPitchEnvelope(note, scheduleTime);
2153
- this.updateDetune(channel, note, scheduleTime);
2316
+ this.setDetune(channel, note, scheduleTime);
2154
2317
  }
2155
2318
  });
2156
2319
  }
@@ -2519,6 +2682,12 @@ export class Midy extends EventTarget {
2519
2682
  channel.dataLSB += value;
2520
2683
  this.handleModulationDepthRangeRPN(channelNumber, scheduleTime);
2521
2684
  break;
2685
+ case 6: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp053.pdf
2686
+ channel.dataLSB += value;
2687
+ this.handleMIDIPolyphonicExpressionRPN(channelNumber, scheduleTime);
2688
+ break;
2689
+ case 16383: // NULL
2690
+ break;
2522
2691
  default:
2523
2692
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
2524
2693
  }
@@ -2617,11 +2786,43 @@ export class Midy extends EventTarget {
2617
2786
  channel.modulationDepthRange = value;
2618
2787
  this.updateModulation(channel, scheduleTime);
2619
2788
  }
2789
+ handleMIDIPolyphonicExpressionRPN(channelNumber, _scheduleTime) {
2790
+ this.setMIDIPolyphonicExpression(channelNumber, channel.dataMSB);
2791
+ }
2792
+ setMIDIPolyphonicExpression(channelNumber, value) {
2793
+ if (channelNumber !== 0 && channelNumber !== 15)
2794
+ return;
2795
+ const members = value & 15;
2796
+ if (channelNumber === 0) {
2797
+ this.lowerMPEMembers = members;
2798
+ }
2799
+ else {
2800
+ this.upperMPEMembers = members;
2801
+ }
2802
+ this.mpeEnabled = this.lowerMPEMembers > 0 || this.upperMPEMembers > 0;
2803
+ const lowerStart = 1;
2804
+ const lowerEnd = this.lowerMPEMembers;
2805
+ const upperStart = 16 - this.upperMPEMembers;
2806
+ const upperEnd = 14;
2807
+ for (let i = 0; i < 16; i++) {
2808
+ const isLower = this.lowerMPEMembers && lowerStart <= i && i <= lowerEnd;
2809
+ const isUpper = this.upperMPEMembers && upperStart <= i && i <= upperEnd;
2810
+ this.channels[i].isMPEMember = this.mpeEnabled && (isLower || isUpper);
2811
+ this.channels[i].isMPEManager = this.mpeEnabled && (i === 0 || i === 15);
2812
+ }
2813
+ }
2620
2814
  setRPGMakerLoop(_channelNumber, _value, scheduleTime) {
2621
2815
  scheduleTime ??= this.audioContext.currentTime;
2622
2816
  this.loopStart = scheduleTime + this.resumeTime - this.startTime;
2623
2817
  }
2624
- allSoundOff(channelNumber, _value, scheduleTime) {
2818
+ allSoundOff(channelNumber, value, scheduleTime) {
2819
+ if (this.channels[channelNumber].isMPEManager)
2820
+ return;
2821
+ this.applyAllSoundOff(channelNumber, value, scheduleTime);
2822
+ }
2823
+ applyAllSoundOff(channelNumber, _value, scheduleTime) {
2824
+ if (this.channels[channelNumber].isMPEManager)
2825
+ return;
2625
2826
  if (!(0 <= scheduleTime))
2626
2827
  scheduleTime = this.audioContext.currentTime;
2627
2828
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
@@ -2693,15 +2894,21 @@ export class Midy extends EventTarget {
2693
2894
  this.allNotesOff(channelNumber, value, scheduleTime);
2694
2895
  }
2695
2896
  omniOn(channelNumber, value, scheduleTime) {
2897
+ if (this.mpeEnabled)
2898
+ return;
2696
2899
  this.allNotesOff(channelNumber, value, scheduleTime);
2697
2900
  }
2698
2901
  monoOn(channelNumber, value, scheduleTime) {
2699
2902
  const channel = this.channels[channelNumber];
2903
+ if (channel.isMPEManager)
2904
+ return;
2700
2905
  this.allNotesOff(channelNumber, value, scheduleTime);
2701
2906
  channel.mono = true;
2702
2907
  }
2703
2908
  polyOn(channelNumber, value, scheduleTime) {
2704
2909
  const channel = this.channels[channelNumber];
2910
+ if (channel.isMPEManager)
2911
+ return;
2705
2912
  this.allNotesOff(channelNumber, value, scheduleTime);
2706
2913
  channel.mono = false;
2707
2914
  }
@@ -2738,32 +2945,34 @@ export class Midy extends EventTarget {
2738
2945
  }
2739
2946
  }
2740
2947
  GM1SystemOn(scheduleTime) {
2948
+ const channels = this.channels;
2741
2949
  if (!(0 <= scheduleTime))
2742
2950
  scheduleTime = this.audioContext.currentTime;
2743
2951
  this.mode = "GM1";
2744
- for (let i = 0; i < this.channels.length; i++) {
2745
- this.allSoundOff(i, 0, scheduleTime);
2746
- const channel = this.channels[i];
2952
+ for (let i = 0; i < channels.length; i++) {
2953
+ this.applyAllSoundOff(i, 0, scheduleTime);
2954
+ const channel = channels[i];
2747
2955
  channel.bankMSB = 0;
2748
2956
  channel.bankLSB = 0;
2749
2957
  channel.isDrum = false;
2750
2958
  }
2751
- this.channels[9].bankMSB = 1;
2752
- this.channels[9].isDrum = true;
2959
+ channels[9].bankMSB = 1;
2960
+ channels[9].isDrum = true;
2753
2961
  }
2754
2962
  GM2SystemOn(scheduleTime) {
2963
+ const channels = this.channels;
2755
2964
  if (!(0 <= scheduleTime))
2756
2965
  scheduleTime = this.audioContext.currentTime;
2757
2966
  this.mode = "GM2";
2758
- for (let i = 0; i < this.channels.length; i++) {
2759
- this.allSoundOff(i, 0, scheduleTime);
2760
- const channel = this.channels[i];
2967
+ for (let i = 0; i < channels.length; i++) {
2968
+ this.applyAllSoundOff(i, 0, scheduleTime);
2969
+ const channel = channels[i];
2761
2970
  channel.bankMSB = 121;
2762
2971
  channel.bankLSB = 0;
2763
2972
  channel.isDrum = false;
2764
2973
  }
2765
- this.channels[9].bankMSB = 120;
2766
- this.channels[9].isDrum = true;
2974
+ channels[9].bankMSB = 120;
2975
+ channels[9].isDrum = true;
2767
2976
  }
2768
2977
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2769
2978
  switch (data[2]) {
@@ -3106,7 +3315,7 @@ export class Midy extends EventTarget {
3106
3315
  }
3107
3316
  getPitchControl(channel, note) {
3108
3317
  const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
3109
- if (polyphonicKeyPressureRaw < 0)
3318
+ if (polyphonicKeyPressureRaw <= 0)
3110
3319
  return 0;
3111
3320
  const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
3112
3321
  note.pressure;
@@ -3126,13 +3335,13 @@ export class Midy extends EventTarget {
3126
3335
  getAmplitudeControl(channel, note) {
3127
3336
  const channelPressureRaw = channel.channelPressureTable[2];
3128
3337
  const channelPressure = (0 <= channelPressureRaw)
3129
- ? channelPressureRaw * channel.state.channelPressure
3338
+ ? channel.state.channelPressure * 127 / channelPressureRaw
3130
3339
  : 0;
3131
3340
  const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
3132
3341
  const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
3133
- ? polyphonicKeyPressureRaw * note.pressure
3342
+ ? note.pressure / polyphonicKeyPressureRaw
3134
3343
  : 0;
3135
- return (channelPressure + polyphonicKeyPressure) / 128;
3344
+ return channelPressure + polyphonicKeyPressure;
3136
3345
  }
3137
3346
  getLFOPitchDepth(channel, note) {
3138
3347
  const channelPressureRaw = channel.channelPressureTable[3];
@@ -3168,27 +3377,33 @@ export class Midy extends EventTarget {
3168
3377
  return (channelPressure + polyphonicKeyPressure) / 254;
3169
3378
  }
3170
3379
  setEffects(channel, note, table, scheduleTime) {
3171
- if (0 <= table[0])
3172
- this.updateDetune(channel, note, scheduleTime);
3380
+ if (0 < table[0]) {
3381
+ if (this.isPortamento(channel, note)) {
3382
+ this.setPortamentoDetune(channel, note, scheduleTime);
3383
+ }
3384
+ else {
3385
+ this.setDetune(channel, note, scheduleTime);
3386
+ }
3387
+ }
3173
3388
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
3174
- if (0 <= table[1]) {
3389
+ if (0 < table[1]) {
3175
3390
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
3176
3391
  }
3177
- if (0 <= table[2]) {
3392
+ if (0 < table[2]) {
3178
3393
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
3179
3394
  }
3180
3395
  }
3181
3396
  else {
3182
- if (0 <= table[1])
3397
+ if (0 < table[1])
3183
3398
  this.setFilterEnvelope(channel, note, scheduleTime);
3184
- if (0 <= table[2])
3399
+ if (0 < table[2])
3185
3400
  this.setVolumeEnvelope(channel, note, scheduleTime);
3186
3401
  }
3187
- if (0 <= table[3])
3402
+ if (0 < table[3])
3188
3403
  this.setModLfoToPitch(channel, note, scheduleTime);
3189
- if (0 <= table[4])
3404
+ if (0 < table[4])
3190
3405
  this.setModLfoToFilterFc(channel, note, scheduleTime);
3191
- if (0 <= table[5])
3406
+ if (0 < table[5])
3192
3407
  this.setModLfoToVolume(channel, note, scheduleTime);
3193
3408
  }
3194
3409
  handlePressureSysEx(data, tableName, scheduleTime) {
@@ -3370,5 +3585,7 @@ Object.defineProperty(Midy, "channelSettings", {
3370
3585
  fineTuning: 0, // cent
3371
3586
  coarseTuning: 0, // cent
3372
3587
  portamentoControl: false,
3588
+ isMPEMember: false,
3589
+ isMPEManager: false,
3373
3590
  }
3374
3591
  });