@marmooo/midy 0.4.3 → 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/script/midy.js CHANGED
@@ -265,6 +265,16 @@ const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
265
265
  class Midy extends EventTarget {
266
266
  constructor(audioContext) {
267
267
  super();
268
+ // https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
269
+ // https://pubmed.ncbi.nlm.nih.gov/12488797/
270
+ // Gap detection studies indicate humans detect temporal discontinuities
271
+ // around 2–3 ms. Smoothing over ~4 ms is perceived as continuous.
272
+ Object.defineProperty(this, "perceptualSmoothingTime", {
273
+ enumerable: true,
274
+ configurable: true,
275
+ writable: true,
276
+ value: 0.004
277
+ });
268
278
  Object.defineProperty(this, "mode", {
269
279
  enumerable: true,
270
280
  configurable: true,
@@ -487,6 +497,33 @@ class Midy extends EventTarget {
487
497
  writable: true,
488
498
  value: new Array(this.numChannels * drumExclusiveClassCount)
489
499
  });
500
+ Object.defineProperty(this, "mpeEnabled", {
501
+ enumerable: true,
502
+ configurable: true,
503
+ writable: true,
504
+ value: false
505
+ });
506
+ Object.defineProperty(this, "lowerMPEMembers", {
507
+ enumerable: true,
508
+ configurable: true,
509
+ writable: true,
510
+ value: 0
511
+ });
512
+ Object.defineProperty(this, "upperMPEMembers", {
513
+ enumerable: true,
514
+ configurable: true,
515
+ writable: true,
516
+ value: 0
517
+ });
518
+ Object.defineProperty(this, "mpeState", {
519
+ enumerable: true,
520
+ configurable: true,
521
+ writable: true,
522
+ value: {
523
+ channelToNote: new Map(),
524
+ noteToChannel: new Map(),
525
+ }
526
+ });
490
527
  this.audioContext = audioContext;
491
528
  this.masterVolume = new GainNode(audioContext);
492
529
  this.scheduler = new GainNode(audioContext, { gain: 0 });
@@ -1013,6 +1050,13 @@ class Midy extends EventTarget {
1013
1050
  this.isSeeking = true;
1014
1051
  }
1015
1052
  }
1053
+ tempoChange(tempo) {
1054
+ const timeScale = this.tempo / tempo;
1055
+ this.resumeTime = this.resumeTime * timeScale;
1056
+ this.tempo = tempo;
1057
+ this.totalTime = this.calcTotalTime();
1058
+ this.seekTo(this.currentTime() * timeScale);
1059
+ }
1016
1060
  calcTotalTime() {
1017
1061
  const totalTimeEventTypes = this.totalTimeEventTypes;
1018
1062
  const timeline = this.timeline;
@@ -1235,39 +1279,24 @@ class Midy extends EventTarget {
1235
1279
  return tuning + pitch;
1236
1280
  }
1237
1281
  }
1238
- calcNoteDetune(channel, note) {
1239
- return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1240
- }
1241
1282
  updateChannelDetune(channel, scheduleTime) {
1242
1283
  this.processScheduledNotes(channel, (note) => {
1243
- this.updateDetune(channel, note, scheduleTime);
1284
+ if (this.isPortamento(channel, note)) {
1285
+ this.setPortamentoDetune(channel, note, scheduleTime);
1286
+ }
1287
+ else {
1288
+ this.setDetune(channel, note, scheduleTime);
1289
+ }
1244
1290
  });
1245
1291
  }
1246
- updateDetune(channel, note, scheduleTime) {
1247
- const noteDetune = this.calcNoteDetune(channel, note);
1292
+ calcScaleOctaveTuning(channel, note) {
1293
+ return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1294
+ }
1295
+ calcNoteDetune(channel, note) {
1296
+ const noteDetune = note.voiceParams.detune +
1297
+ this.calcScaleOctaveTuning(channel, note);
1248
1298
  const pitchControl = this.getPitchControl(channel, note);
1249
- const detune = channel.detune + noteDetune + pitchControl;
1250
- if (channel.portamentoControl) {
1251
- const state = channel.state;
1252
- const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1253
- note.portamentoNoteNumber = portamentoNoteNumber;
1254
- channel.portamentoControl = false;
1255
- state.portamentoNoteNumber = 0;
1256
- }
1257
- if (this.isPortamento(channel, note)) {
1258
- const startTime = note.startTime;
1259
- const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1260
- const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1261
- note.bufferSource.detune
1262
- .cancelScheduledValues(scheduleTime)
1263
- .setValueAtTime(detune - deltaCent, scheduleTime)
1264
- .linearRampToValueAtTime(detune, portamentoTime);
1265
- }
1266
- else {
1267
- note.bufferSource.detune
1268
- .cancelScheduledValues(scheduleTime)
1269
- .setValueAtTime(detune, scheduleTime);
1270
- }
1299
+ return channel.detune + noteDetune + pitchControl;
1271
1300
  }
1272
1301
  getPortamentoTime(channel, note) {
1273
1302
  const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
@@ -1340,7 +1369,7 @@ class Midy extends EventTarget {
1340
1369
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1341
1370
  note.volumeEnvelopeNode.gain
1342
1371
  .cancelScheduledValues(scheduleTime)
1343
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1372
+ .exponentialRampToValueAtTime(sustainVolume, portamentoTime);
1344
1373
  }
1345
1374
  setVolumeEnvelope(channel, note, scheduleTime) {
1346
1375
  const { voiceParams, startTime } = note;
@@ -1356,38 +1385,63 @@ class Midy extends EventTarget {
1356
1385
  note.volumeEnvelopeNode.gain
1357
1386
  .cancelScheduledValues(scheduleTime)
1358
1387
  .setValueAtTime(0, startTime)
1359
- .setValueAtTime(0, volDelay)
1360
- .linearRampToValueAtTime(attackVolume, volAttack)
1388
+ .setValueAtTime(1e-6, volDelay)
1389
+ .exponentialRampToValueAtTime(attackVolume, volAttack)
1361
1390
  .setValueAtTime(attackVolume, volHold)
1362
1391
  .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
1363
1392
  }
1364
- setPortamentoPitchEnvelope(note, scheduleTime) {
1393
+ setPortamentoDetune(channel, note, scheduleTime) {
1394
+ if (channel.portamentoControl) {
1395
+ const state = channel.state;
1396
+ const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1397
+ note.portamentoNoteNumber = portamentoNoteNumber;
1398
+ channel.portamentoControl = false;
1399
+ state.portamentoNoteNumber = 0;
1400
+ }
1401
+ const detune = this.calcNoteDetune(channel, note);
1402
+ const startTime = note.startTime;
1403
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1404
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1405
+ note.bufferSource.detune
1406
+ .cancelScheduledValues(scheduleTime)
1407
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1408
+ .linearRampToValueAtTime(detune, portamentoTime);
1409
+ }
1410
+ setDetune(channel, note, scheduleTime) {
1411
+ const detune = this.calcNoteDetune(channel, note);
1412
+ note.bufferSource.detune
1413
+ .cancelScheduledValues(scheduleTime)
1414
+ .setValueAtTime(detune, scheduleTime);
1415
+ const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
1416
+ note.bufferSource.detune
1417
+ .cancelAndHoldAtTime(scheduleTime)
1418
+ .setTargetAtTime(detune, scheduleTime, timeConstant);
1419
+ }
1420
+ setPortamentoPitchEnvelope(channel, note, scheduleTime) {
1365
1421
  const baseRate = note.voiceParams.playbackRate;
1366
1422
  const portamentoTime = note.startTime +
1367
1423
  this.getPortamentoTime(channel, note);
1368
1424
  note.bufferSource.playbackRate
1369
1425
  .cancelScheduledValues(scheduleTime)
1370
- .linearRampToValueAtTime(baseRate, portamentoTime);
1426
+ .exponentialRampToValueAtTime(baseRate, portamentoTime);
1371
1427
  }
1372
1428
  setPitchEnvelope(note, scheduleTime) {
1373
- const { voiceParams } = note;
1429
+ const { bufferSource, voiceParams } = note;
1374
1430
  const baseRate = voiceParams.playbackRate;
1375
- note.bufferSource.playbackRate
1431
+ bufferSource.playbackRate
1376
1432
  .cancelScheduledValues(scheduleTime)
1377
- .setValueAtTime(baseRate, note.startTime);
1433
+ .setValueAtTime(baseRate, scheduleTime);
1378
1434
  const modEnvToPitch = voiceParams.modEnvToPitch;
1379
1435
  if (modEnvToPitch === 0)
1380
1436
  return;
1381
- const basePitch = this.rateToCent(baseRate);
1382
- const peekPitch = basePitch + modEnvToPitch;
1383
- const peekRate = this.centToRate(peekPitch);
1437
+ const peekRate = baseRate * this.centToRate(modEnvToPitch);
1384
1438
  const modDelay = note.startTime + voiceParams.modDelay;
1385
1439
  const modAttack = modDelay + voiceParams.modAttack;
1386
1440
  const modHold = modAttack + voiceParams.modHold;
1387
1441
  const decayDuration = voiceParams.modDecay;
1388
- note.bufferSource.playbackRate
1442
+ bufferSource.playbackRate
1389
1443
  .setValueAtTime(baseRate, modDelay)
1390
- .linearRampToValueAtTime(peekRate, modAttack)
1444
+ .exponentialRampToValueAtTime(peekRate, modAttack)
1391
1445
  .setValueAtTime(peekRate, modHold)
1392
1446
  .setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
1393
1447
  }
@@ -1405,10 +1459,10 @@ class Midy extends EventTarget {
1405
1459
  this.getFilterCutoffControl(channel, note);
1406
1460
  const sustainCent = baseCent +
1407
1461
  voiceParams.modEnvToFilterFc * (1 - voiceParams.modSustain);
1408
- const baseFreq = this.centToHz(baseCent);
1409
- const sustainFreq = this.centToHz(sustainCent);
1410
- const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq * scale);
1411
- const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq * scale);
1462
+ const baseFreq = this.centToHz(baseCent) * scale;
1463
+ const sustainFreq = this.centToHz(sustainCent) * scale;
1464
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1465
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1412
1466
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1413
1467
  const modDelay = startTime + voiceParams.modDelay;
1414
1468
  note.adjustedBaseFreq = adjustedSustainFreq;
@@ -1416,7 +1470,7 @@ class Midy extends EventTarget {
1416
1470
  .cancelScheduledValues(scheduleTime)
1417
1471
  .setValueAtTime(adjustedBaseFreq, startTime)
1418
1472
  .setValueAtTime(adjustedBaseFreq, modDelay)
1419
- .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1473
+ .exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1420
1474
  }
1421
1475
  setFilterEnvelope(channel, note, scheduleTime) {
1422
1476
  const { voiceParams, startTime } = note;
@@ -1444,7 +1498,7 @@ class Midy extends EventTarget {
1444
1498
  .cancelScheduledValues(scheduleTime)
1445
1499
  .setValueAtTime(adjustedBaseFreq, startTime)
1446
1500
  .setValueAtTime(adjustedBaseFreq, modDelay)
1447
- .linearRampToValueAtTime(adjustedPeekFreq, modAttack)
1501
+ .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1448
1502
  .setValueAtTime(adjustedPeekFreq, modHold)
1449
1503
  .setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
1450
1504
  }
@@ -1533,14 +1587,15 @@ class Midy extends EventTarget {
1533
1587
  if (!channel.isDrum && this.isPortamento(channel, note)) {
1534
1588
  this.setPortamentoVolumeEnvelope(channel, note, now);
1535
1589
  this.setPortamentoFilterEnvelope(channel, note, now);
1536
- this.setPortamentoPitchEnvelope(note, now);
1590
+ this.setPortamentoPitchEnvelope(channel, note, now);
1591
+ this.setPortamentoDetune(channel, note, now);
1537
1592
  }
1538
1593
  else {
1539
1594
  this.setVolumeEnvelope(channel, note, now);
1540
1595
  this.setFilterEnvelope(channel, note, now);
1541
1596
  this.setPitchEnvelope(note, now);
1597
+ this.setDetune(channel, note, now);
1542
1598
  }
1543
- this.updateDetune(channel, note, now);
1544
1599
  if (0 < state.vibratoDepth) {
1545
1600
  this.startVibrato(channel, note, now);
1546
1601
  }
@@ -1623,6 +1678,16 @@ class Midy extends EventTarget {
1623
1678
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1624
1679
  }
1625
1680
  async noteOn(channelNumber, noteNumber, velocity, startTime) {
1681
+ if (this.mpeEnabled) {
1682
+ const note = await this.startNote(channelNumber, noteNumber, velocity, startTime);
1683
+ this.mpeState.channelToNote.set(channelNumber, note.index);
1684
+ this.mpeState.noteToChannel.set(note.index, channelNumber);
1685
+ }
1686
+ else {
1687
+ await this.startNote(channelNumber, noteNumber, velocity, startTime);
1688
+ }
1689
+ }
1690
+ async startNote(channelNumber, noteNumber, velocity, startTime) {
1626
1691
  const channel = this.channels[channelNumber];
1627
1692
  const realtime = startTime === undefined;
1628
1693
  if (realtime)
@@ -1649,6 +1714,7 @@ class Midy extends EventTarget {
1649
1714
  await this.setNoteAudioNode(channel, note, realtime);
1650
1715
  this.setNoteRouting(channelNumber, note, startTime);
1651
1716
  note.resolveReady();
1717
+ return note;
1652
1718
  }
1653
1719
  disconnectNote(note) {
1654
1720
  note.bufferSource.disconnect();
@@ -1673,15 +1739,14 @@ class Midy extends EventTarget {
1673
1739
  releaseNote(channel, note, endTime) {
1674
1740
  endTime ??= this.audioContext.currentTime;
1675
1741
  const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1676
- const duration = note.voiceParams.volRelease * releaseTime;
1677
- const volRelease = endTime + duration;
1678
- const modRelease = endTime + note.voiceParams.modRelease;
1742
+ const volDuration = note.voiceParams.volRelease * releaseTime;
1743
+ const volRelease = endTime + volDuration;
1679
1744
  note.filterNode.frequency
1680
1745
  .cancelScheduledValues(endTime)
1681
- .linearRampToValueAtTime(note.adjustedBaseFreq, modRelease);
1746
+ .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
1682
1747
  note.volumeEnvelopeNode.gain
1683
1748
  .cancelScheduledValues(endTime)
1684
- .setTargetAtTime(0, endTime, duration * releaseCurve);
1749
+ .setTargetAtTime(0, endTime, volDuration * releaseCurve);
1685
1750
  return new Promise((resolve) => {
1686
1751
  this.scheduleTask(() => {
1687
1752
  const bufferSource = note.bufferSource;
@@ -1693,7 +1758,26 @@ class Midy extends EventTarget {
1693
1758
  }, volRelease);
1694
1759
  });
1695
1760
  }
1696
- noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1761
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1762
+ if (this.mpeEnabled) {
1763
+ const noteIndex = this.mpeState.channelToNote.get(channelNumber);
1764
+ if (noteIndex === undefined)
1765
+ return;
1766
+ const channel = this.channels[channelNumber];
1767
+ const note = channel.scheduledNotes[noteIndex];
1768
+ note.ending = true;
1769
+ const promise = note.ready.then(() => {
1770
+ return this.releaseNote(channel, note, endTime);
1771
+ });
1772
+ this.mpeState.channelToNote.delete(channelNumber);
1773
+ this.mpeState.noteToChannel.delete(noteIndex);
1774
+ return promise;
1775
+ }
1776
+ else {
1777
+ return this.stopNote(channelNumber, noteNumber, velocity, endTime, force);
1778
+ }
1779
+ }
1780
+ stopNote(channelNumber, noteNumber, _velocity, endTime, force) {
1697
1781
  const channel = this.channels[channelNumber];
1698
1782
  const state = channel.state;
1699
1783
  if (!force) {
@@ -1809,9 +1893,11 @@ class Midy extends EventTarget {
1809
1893
  this.lastActiveSensing = performance.now();
1810
1894
  }
1811
1895
  setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1896
+ const channel = this.channels[channelNumber];
1897
+ if (channel.isMPEMember)
1898
+ return;
1812
1899
  if (!(0 <= scheduleTime))
1813
1900
  scheduleTime = this.audioContext.currentTime;
1814
- const channel = this.channels[channelNumber];
1815
1901
  const table = channel.polyphonicKeyPressureTable;
1816
1902
  this.processActiveNotes(channel, scheduleTime, (note) => {
1817
1903
  if (note.noteNumber === noteNumber) {
@@ -1994,13 +2080,6 @@ class Midy extends EventTarget {
1994
2080
  .cancelScheduledValues(scheduleTime)
1995
2081
  .setValueAtTime(freqModLFO, scheduleTime);
1996
2082
  }
1997
- setFreqVibLFO(channel, note, scheduleTime) {
1998
- const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1999
- const freqVibLFO = note.voiceParams.freqVibLFO;
2000
- note.vibratoLFO.frequency
2001
- .cancelScheduledValues(scheduleTime)
2002
- .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
2003
- }
2004
2083
  setDelayVibLFO(channel, note) {
2005
2084
  const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
2006
2085
  const value = note.voiceParams.delayVibLFO;
@@ -2010,6 +2089,13 @@ class Midy extends EventTarget {
2010
2089
  }
2011
2090
  catch { /* empty */ }
2012
2091
  }
2092
+ setFreqVibLFO(channel, note, scheduleTime) {
2093
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
2094
+ const freqVibLFO = note.voiceParams.freqVibLFO;
2095
+ note.vibratoLFO.frequency
2096
+ .cancelScheduledValues(scheduleTime)
2097
+ .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
2098
+ }
2013
2099
  createVoiceParamsHandlers() {
2014
2100
  return {
2015
2101
  modLfoToPitch: (channel, note, scheduleTime) => {
@@ -2063,6 +2149,14 @@ class Midy extends EventTarget {
2063
2149
  this.setFreqVibLFO(channel, note, scheduleTime);
2064
2150
  }
2065
2151
  },
2152
+ detune: (channel, note, scheduleTime) => {
2153
+ if (this.isPortamento(channel, note)) {
2154
+ this.setPortamentoDetune(channel, note, scheduleTime);
2155
+ }
2156
+ else {
2157
+ this.setDetune(channel, note, scheduleTime);
2158
+ }
2159
+ },
2066
2160
  };
2067
2161
  }
2068
2162
  getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
@@ -2154,6 +2248,21 @@ class Midy extends EventTarget {
2154
2248
  return handlers;
2155
2249
  }
2156
2250
  setControlChange(channelNumber, controllerType, value, scheduleTime) {
2251
+ const channel = this.channels[channelNumber];
2252
+ if (channel.isMPEMember) {
2253
+ this.applyControlChange(channelNumber, controllerType, value, scheduleTime);
2254
+ }
2255
+ else if (channel.isMPEManager) {
2256
+ channel.state[controllerType] = value / 127;
2257
+ for (const memberChannel of this.mpeState.channelToNote.keys()) {
2258
+ this.applyControlChange(memberChannel, controllerType, value, scheduleTime);
2259
+ }
2260
+ }
2261
+ else {
2262
+ this.applyControlChange(channelNumber, controllerType, value, scheduleTime);
2263
+ }
2264
+ }
2265
+ applyControlChange(channelNumber, controllerType, value, scheduleTime) {
2157
2266
  const handler = this.controlChangeHandlers[controllerType];
2158
2267
  if (handler) {
2159
2268
  handler.call(this, channelNumber, value, scheduleTime);
@@ -2200,14 +2309,14 @@ class Midy extends EventTarget {
2200
2309
  if (this.isPortamento(channel, note)) {
2201
2310
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2202
2311
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2203
- this.setPortamentoPitchEnvelope(note, scheduleTime);
2204
- this.updateDetune(channel, note, scheduleTime);
2312
+ this.setPortamentoPitchEnvelope(channel, note, scheduleTime);
2313
+ this.setPortamentoDetune(channel, note, scheduleTime);
2205
2314
  }
2206
2315
  else {
2207
2316
  this.setVolumeEnvelope(channel, note, scheduleTime);
2208
2317
  this.setFilterEnvelope(channel, note, scheduleTime);
2209
2318
  this.setPitchEnvelope(note, scheduleTime);
2210
- this.updateDetune(channel, note, scheduleTime);
2319
+ this.setDetune(channel, note, scheduleTime);
2211
2320
  }
2212
2321
  });
2213
2322
  }
@@ -2576,6 +2685,12 @@ class Midy extends EventTarget {
2576
2685
  channel.dataLSB += value;
2577
2686
  this.handleModulationDepthRangeRPN(channelNumber, scheduleTime);
2578
2687
  break;
2688
+ case 6: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp053.pdf
2689
+ channel.dataLSB += value;
2690
+ this.handleMIDIPolyphonicExpressionRPN(channelNumber, scheduleTime);
2691
+ break;
2692
+ case 16383: // NULL
2693
+ break;
2579
2694
  default:
2580
2695
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
2581
2696
  }
@@ -2674,11 +2789,43 @@ class Midy extends EventTarget {
2674
2789
  channel.modulationDepthRange = value;
2675
2790
  this.updateModulation(channel, scheduleTime);
2676
2791
  }
2792
+ handleMIDIPolyphonicExpressionRPN(channelNumber, _scheduleTime) {
2793
+ this.setMIDIPolyphonicExpression(channelNumber, channel.dataMSB);
2794
+ }
2795
+ setMIDIPolyphonicExpression(channelNumber, value) {
2796
+ if (channelNumber !== 0 && channelNumber !== 15)
2797
+ return;
2798
+ const members = value & 15;
2799
+ if (channelNumber === 0) {
2800
+ this.lowerMPEMembers = members;
2801
+ }
2802
+ else {
2803
+ this.upperMPEMembers = members;
2804
+ }
2805
+ this.mpeEnabled = this.lowerMPEMembers > 0 || this.upperMPEMembers > 0;
2806
+ const lowerStart = 1;
2807
+ const lowerEnd = this.lowerMPEMembers;
2808
+ const upperStart = 16 - this.upperMPEMembers;
2809
+ const upperEnd = 14;
2810
+ for (let i = 0; i < 16; i++) {
2811
+ const isLower = this.lowerMPEMembers && lowerStart <= i && i <= lowerEnd;
2812
+ const isUpper = this.upperMPEMembers && upperStart <= i && i <= upperEnd;
2813
+ this.channels[i].isMPEMember = this.mpeEnabled && (isLower || isUpper);
2814
+ this.channels[i].isMPEManager = this.mpeEnabled && (i === 0 || i === 15);
2815
+ }
2816
+ }
2677
2817
  setRPGMakerLoop(_channelNumber, _value, scheduleTime) {
2678
2818
  scheduleTime ??= this.audioContext.currentTime;
2679
2819
  this.loopStart = scheduleTime + this.resumeTime - this.startTime;
2680
2820
  }
2681
- allSoundOff(channelNumber, _value, scheduleTime) {
2821
+ allSoundOff(channelNumber, value, scheduleTime) {
2822
+ if (this.channels[channelNumber].isMPEManager)
2823
+ return;
2824
+ this.applyAllSoundOff(channelNumber, value, scheduleTime);
2825
+ }
2826
+ applyAllSoundOff(channelNumber, _value, scheduleTime) {
2827
+ if (this.channels[channelNumber].isMPEManager)
2828
+ return;
2682
2829
  if (!(0 <= scheduleTime))
2683
2830
  scheduleTime = this.audioContext.currentTime;
2684
2831
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
@@ -2750,15 +2897,21 @@ class Midy extends EventTarget {
2750
2897
  this.allNotesOff(channelNumber, value, scheduleTime);
2751
2898
  }
2752
2899
  omniOn(channelNumber, value, scheduleTime) {
2900
+ if (this.mpeEnabled)
2901
+ return;
2753
2902
  this.allNotesOff(channelNumber, value, scheduleTime);
2754
2903
  }
2755
2904
  monoOn(channelNumber, value, scheduleTime) {
2756
2905
  const channel = this.channels[channelNumber];
2906
+ if (channel.isMPEManager)
2907
+ return;
2757
2908
  this.allNotesOff(channelNumber, value, scheduleTime);
2758
2909
  channel.mono = true;
2759
2910
  }
2760
2911
  polyOn(channelNumber, value, scheduleTime) {
2761
2912
  const channel = this.channels[channelNumber];
2913
+ if (channel.isMPEManager)
2914
+ return;
2762
2915
  this.allNotesOff(channelNumber, value, scheduleTime);
2763
2916
  channel.mono = false;
2764
2917
  }
@@ -2800,7 +2953,7 @@ class Midy extends EventTarget {
2800
2953
  scheduleTime = this.audioContext.currentTime;
2801
2954
  this.mode = "GM1";
2802
2955
  for (let i = 0; i < channels.length; i++) {
2803
- this.allSoundOff(i, 0, scheduleTime);
2956
+ this.applyAllSoundOff(i, 0, scheduleTime);
2804
2957
  const channel = channels[i];
2805
2958
  channel.bankMSB = 0;
2806
2959
  channel.bankLSB = 0;
@@ -2815,7 +2968,7 @@ class Midy extends EventTarget {
2815
2968
  scheduleTime = this.audioContext.currentTime;
2816
2969
  this.mode = "GM2";
2817
2970
  for (let i = 0; i < channels.length; i++) {
2818
- this.allSoundOff(i, 0, scheduleTime);
2971
+ this.applyAllSoundOff(i, 0, scheduleTime);
2819
2972
  const channel = channels[i];
2820
2973
  channel.bankMSB = 121;
2821
2974
  channel.bankLSB = 0;
@@ -3227,8 +3380,14 @@ class Midy extends EventTarget {
3227
3380
  return (channelPressure + polyphonicKeyPressure) / 254;
3228
3381
  }
3229
3382
  setEffects(channel, note, table, scheduleTime) {
3230
- if (0 < table[0])
3231
- this.updateDetune(channel, note, scheduleTime);
3383
+ if (0 < table[0]) {
3384
+ if (this.isPortamento(channel, note)) {
3385
+ this.setPortamentoDetune(channel, note, scheduleTime);
3386
+ }
3387
+ else {
3388
+ this.setDetune(channel, note, scheduleTime);
3389
+ }
3390
+ }
3232
3391
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
3233
3392
  if (0 < table[1]) {
3234
3393
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -3430,5 +3589,7 @@ Object.defineProperty(Midy, "channelSettings", {
3430
3589
  fineTuning: 0, // cent
3431
3590
  coarseTuning: 0, // cent
3432
3591
  portamentoControl: false,
3592
+ isMPEMember: false,
3593
+ isMPEManager: false,
3433
3594
  }
3434
3595
  });