@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/README.md +1 -0
- package/esm/midy-GM1.d.ts +10 -5
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +61 -34
- package/esm/midy-GM2.d.ts +15 -9
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +122 -78
- package/esm/midy-GMLite.d.ts +10 -5
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +64 -36
- package/esm/midy.d.ts +54 -6
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +232 -71
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +10 -5
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +61 -34
- package/script/midy-GM2.d.ts +15 -9
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +122 -78
- package/script/midy-GMLite.d.ts +10 -5
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +64 -36
- package/script/midy.d.ts +54 -6
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +232 -71
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.
|
|
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
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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(
|
|
1360
|
-
.
|
|
1388
|
+
.setValueAtTime(1e-6, volDelay)
|
|
1389
|
+
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
1361
1390
|
.setValueAtTime(attackVolume, volHold)
|
|
1362
1391
|
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
1363
1392
|
}
|
|
1364
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
1431
|
+
bufferSource.playbackRate
|
|
1376
1432
|
.cancelScheduledValues(scheduleTime)
|
|
1377
|
-
.setValueAtTime(baseRate,
|
|
1433
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1378
1434
|
const modEnvToPitch = voiceParams.modEnvToPitch;
|
|
1379
1435
|
if (modEnvToPitch === 0)
|
|
1380
1436
|
return;
|
|
1381
|
-
const
|
|
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
|
-
|
|
1442
|
+
bufferSource.playbackRate
|
|
1389
1443
|
.setValueAtTime(baseRate, modDelay)
|
|
1390
|
-
.
|
|
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
|
|
1411
|
-
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
1677
|
-
const volRelease = endTime +
|
|
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
|
-
.
|
|
1746
|
+
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1682
1747
|
note.volumeEnvelopeNode.gain
|
|
1683
1748
|
.cancelScheduledValues(endTime)
|
|
1684
|
-
.setTargetAtTime(0, endTime,
|
|
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,
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
});
|