@marmooo/midy 0.2.1 → 0.2.2

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
@@ -17,12 +17,30 @@ class Note {
17
17
  writable: true,
18
18
  value: void 0
19
19
  });
20
+ Object.defineProperty(this, "volumeEnvelopeNode", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
20
26
  Object.defineProperty(this, "volumeNode", {
21
27
  enumerable: true,
22
28
  configurable: true,
23
29
  writable: true,
24
30
  value: void 0
25
31
  });
32
+ Object.defineProperty(this, "gainL", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ Object.defineProperty(this, "gainR", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: void 0
43
+ });
26
44
  Object.defineProperty(this, "volumeDepth", {
27
45
  enumerable: true,
28
46
  configurable: true,
@@ -923,7 +941,9 @@ class Midy {
923
941
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
924
942
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
925
943
  const pitch = pitchWheel * pitchWheelSensitivity;
926
- return tuning + pitch;
944
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
945
+ const pressure = pressureDepth * channel.state.channelPressure;
946
+ return tuning + pitch + pressure;
927
947
  }
928
948
  calcNoteDetune(channel, note) {
929
949
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -943,14 +963,19 @@ class Midy {
943
963
  }
944
964
  });
945
965
  }
966
+ getPortamentoTime(channel) {
967
+ const factor = 5 * Math.log(10) / 127;
968
+ const time = channel.state.portamentoTime;
969
+ return Math.log(time) / factor;
970
+ }
946
971
  setPortamentoStartVolumeEnvelope(channel, note) {
947
972
  const now = this.audioContext.currentTime;
948
973
  const { voiceParams, startTime } = note;
949
974
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
950
975
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
951
976
  const volDelay = startTime + voiceParams.volDelay;
952
- const portamentoTime = volDelay + channel.state.portamentoTime;
953
- note.volumeNode.gain
977
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
978
+ note.volumeEnvelopeNode.gain
954
979
  .cancelScheduledValues(now)
955
980
  .setValueAtTime(0, volDelay)
956
981
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
@@ -959,13 +984,16 @@ class Midy {
959
984
  const now = this.audioContext.currentTime;
960
985
  const state = channel.state;
961
986
  const { voiceParams, startTime } = note;
962
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
987
+ const pressureDepth = channel.pressureTable[2] / 64;
988
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
989
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
990
+ pressure;
963
991
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
964
992
  const volDelay = startTime + voiceParams.volDelay;
965
993
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
966
994
  const volHold = volAttack + voiceParams.volHold;
967
995
  const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
968
- note.volumeNode.gain
996
+ note.volumeEnvelopeNode.gain
969
997
  .cancelScheduledValues(now)
970
998
  .setValueAtTime(0, startTime)
971
999
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -1007,14 +1035,17 @@ class Midy {
1007
1035
  const { voiceParams, noteNumber, startTime } = note;
1008
1036
  const softPedalFactor = 1 -
1009
1037
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1010
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1011
- softPedalFactor * state.brightness * 2;
1038
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1039
+ const pressure = pressureDepth * channel.state.channelPressure;
1040
+ const baseCent = voiceParams.initialFilterFc + pressure;
1041
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1042
+ state.brightness * 2;
1012
1043
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1013
1044
  const sustainFreq = baseFreq +
1014
1045
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1015
1046
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1016
1047
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1017
- const portamentoTime = startTime + channel.state.portamentoTime;
1048
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
1018
1049
  const modDelay = startTime + voiceParams.modDelay;
1019
1050
  note.filterNode.frequency
1020
1051
  .cancelScheduledValues(now)
@@ -1059,14 +1090,14 @@ class Midy {
1059
1090
  note.modulationDepth = new GainNode(this.audioContext);
1060
1091
  this.setModLfoToPitch(channel, note);
1061
1092
  note.volumeDepth = new GainNode(this.audioContext);
1062
- this.setModLfoToVolume(note);
1093
+ this.setModLfoToVolume(channel, note);
1063
1094
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1064
1095
  note.modulationLFO.connect(note.filterDepth);
1065
1096
  note.filterDepth.connect(note.filterNode.frequency);
1066
1097
  note.modulationLFO.connect(note.modulationDepth);
1067
1098
  note.modulationDepth.connect(note.bufferSource.detune);
1068
1099
  note.modulationLFO.connect(note.volumeDepth);
1069
- note.volumeDepth.connect(note.volumeNode.gain);
1100
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1070
1101
  }
1071
1102
  startVibrato(channel, note, startTime) {
1072
1103
  const { voiceParams } = note;
@@ -1088,6 +1119,9 @@ class Midy {
1088
1119
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1089
1120
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1090
1121
  note.volumeNode = new GainNode(this.audioContext);
1122
+ note.gainL = new GainNode(this.audioContext);
1123
+ note.gainR = new GainNode(this.audioContext);
1124
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1091
1125
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1092
1126
  type: "lowpass",
1093
1127
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
@@ -1114,7 +1148,10 @@ class Midy {
1114
1148
  channel.currentBufferSource = note.bufferSource;
1115
1149
  }
1116
1150
  note.bufferSource.connect(note.filterNode);
1117
- note.filterNode.connect(note.volumeNode);
1151
+ note.filterNode.connect(note.volumeEnvelopeNode);
1152
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1153
+ note.volumeNode.connect(note.gainL);
1154
+ note.volumeNode.connect(note.gainR);
1118
1155
  if (0 < channel.chorusSendLevel) {
1119
1156
  this.setChorusEffectsSend(channel, note, 0);
1120
1157
  }
@@ -1145,8 +1182,8 @@ class Midy {
1145
1182
  if (!voice)
1146
1183
  return;
1147
1184
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1148
- note.volumeNode.connect(channel.gainL);
1149
- note.volumeNode.connect(channel.gainR);
1185
+ note.gainL.connect(channel.gainL);
1186
+ note.gainR.connect(channel.gainR);
1150
1187
  if (channel.state.sostenutoPedal) {
1151
1188
  channel.sostenutoNotes.set(noteNumber, note);
1152
1189
  }
@@ -1177,7 +1214,7 @@ class Midy {
1177
1214
  }
1178
1215
  stopNote(endTime, stopTime, scheduledNotes, index) {
1179
1216
  const note = scheduledNotes[index];
1180
- note.volumeNode.gain
1217
+ note.volumeEnvelopeNode.gain
1181
1218
  .cancelScheduledValues(endTime)
1182
1219
  .linearRampToValueAtTime(0, stopTime);
1183
1220
  note.ending = true;
@@ -1188,8 +1225,11 @@ class Midy {
1188
1225
  note.bufferSource.onended = () => {
1189
1226
  scheduledNotes[index] = null;
1190
1227
  note.bufferSource.disconnect();
1191
- note.volumeNode.disconnect();
1192
1228
  note.filterNode.disconnect();
1229
+ note.volumeEnvelopeNode.disconnect();
1230
+ note.volumeNode.disconnect();
1231
+ note.gainL.disconnect();
1232
+ note.gainR.disconnect();
1193
1233
  if (note.modulationDepth) {
1194
1234
  note.volumeDepth.disconnect();
1195
1235
  note.modulationDepth.disconnect();
@@ -1239,7 +1279,7 @@ class Midy {
1239
1279
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1240
1280
  }
1241
1281
  else {
1242
- const portamentoTime = endTime + state.portamentoTime;
1282
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1243
1283
  const deltaNote = portamentoNoteNumber - noteNumber;
1244
1284
  const baseRate = note.voiceParams.playbackRate;
1245
1285
  const targetRate = baseRate * Math.pow(2, deltaNote / 12);
@@ -1314,7 +1354,7 @@ class Midy {
1314
1354
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1315
1355
  if (activeNotes.has(noteNumber)) {
1316
1356
  const activeNote = activeNotes.get(noteNumber);
1317
- const gain = activeNote.volumeNode.gain.value;
1357
+ const gain = activeNote.gainL.gain.value;
1318
1358
  activeNote.volumeNode.gain
1319
1359
  .cancelScheduledValues(now)
1320
1360
  .setValueAtTime(gain * pressure, now);
@@ -1327,22 +1367,45 @@ class Midy {
1327
1367
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1328
1368
  channel.program = program;
1329
1369
  }
1330
- handleChannelPressure(channelNumber, pressure) {
1331
- const now = this.audioContext.currentTime;
1370
+ handleChannelPressure(channelNumber, value) {
1332
1371
  const channel = this.channels[channelNumber];
1333
- pressure /= 64;
1334
- channel.channelPressure = pressure;
1335
- const activeNotes = this.getActiveNotes(channel, now);
1336
- if (channel.channelPressure.amplitudeControl !== 1) {
1337
- activeNotes.forEach((activeNote) => {
1338
- const gain = activeNote.volumeNode.gain.value;
1339
- activeNote.volumeNode.gain
1340
- .cancelScheduledValues(now)
1341
- .setValueAtTime(gain * pressure, now);
1342
- });
1372
+ const prev = channel.state.channelPressure;
1373
+ const next = value / 127;
1374
+ channel.state.channelPressure = next;
1375
+ if (channel.pressureTable[0] !== 64) {
1376
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1377
+ channel.detune += pressureDepth * (next - prev);
1343
1378
  }
1379
+ channel.scheduledNotes.forEach((noteList) => {
1380
+ for (let i = 0; i < noteList.length; i++) {
1381
+ const note = noteList[i];
1382
+ if (!note)
1383
+ continue;
1384
+ this.setChannelPressure(channel, note);
1385
+ }
1386
+ });
1344
1387
  // this.applyVoiceParams(channel, 13);
1345
1388
  }
1389
+ setChannelPressure(channel, note) {
1390
+ if (channel.pressureTable[0] !== 64) {
1391
+ this.updateDetune(channel);
1392
+ }
1393
+ if (channel.pressureTable[1] !== 64 && !note.portamento) {
1394
+ this.setFilterEnvelope(channel, note);
1395
+ }
1396
+ if (channel.pressureTable[2] !== 64 && !note.portamento) {
1397
+ this.setVolumeEnvelope(channel, note);
1398
+ }
1399
+ if (channel.pressureTable[3] !== 0) {
1400
+ this.setModLfoToPitch(channel, note);
1401
+ }
1402
+ if (channel.pressureTable[4] !== 0) {
1403
+ this.setModLfoToFilterFc(channel, note);
1404
+ }
1405
+ if (channel.pressureTable[5] !== 0) {
1406
+ this.setModLfoToVolume(channel, note);
1407
+ }
1408
+ }
1346
1409
  handlePitchBendMessage(channelNumber, lsb, msb) {
1347
1410
  const pitchBend = msb * 128 + lsb;
1348
1411
  this.setPitchBend(channelNumber, pitchBend);
@@ -1359,13 +1422,15 @@ class Midy {
1359
1422
  }
1360
1423
  setModLfoToPitch(channel, note) {
1361
1424
  const now = this.audioContext.currentTime;
1362
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1363
- const modulationDepth = Math.abs(modLfoToPitch) +
1425
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1426
+ const pressure = pressureDepth * channel.state.channelPressure;
1427
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1428
+ const baseDepth = Math.abs(modLfoToPitch) +
1364
1429
  channel.state.modulationDepth;
1365
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1430
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1366
1431
  note.modulationDepth.gain
1367
1432
  .cancelScheduledValues(now)
1368
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1433
+ .setValueAtTime(modulationDepth, now);
1369
1434
  }
1370
1435
  setVibLfoToPitch(channel, note) {
1371
1436
  const now = this.audioContext.currentTime;
@@ -1377,69 +1442,75 @@ class Midy {
1377
1442
  .cancelScheduledValues(now)
1378
1443
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1379
1444
  }
1380
- setModLfoToFilterFc(note) {
1445
+ setModLfoToFilterFc(channel, note) {
1381
1446
  const now = this.audioContext.currentTime;
1382
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1447
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1448
+ const pressure = pressureDepth * channel.state.channelPressure;
1449
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1383
1450
  note.filterDepth.gain
1384
1451
  .cancelScheduledValues(now)
1385
1452
  .setValueAtTime(modLfoToFilterFc, now);
1386
1453
  }
1387
- setModLfoToVolume(note) {
1454
+ setModLfoToVolume(channel, note) {
1388
1455
  const now = this.audioContext.currentTime;
1389
1456
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1390
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1391
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1457
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1458
+ const pressureDepth = channel.pressureTable[5] / 127;
1459
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1460
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1392
1461
  note.volumeDepth.gain
1393
1462
  .cancelScheduledValues(now)
1394
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1463
+ .setValueAtTime(volumeDepth, now);
1395
1464
  }
1396
- setChorusEffectsSend(note, prevValue) {
1465
+ setReverbEffectsSend(channel, note, prevValue) {
1397
1466
  if (0 < prevValue) {
1398
- if (0 < note.voiceParams.chorusEffectsSend) {
1467
+ if (0 < note.voiceParams.reverbEffectsSend) {
1399
1468
  const now = this.audioContext.currentTime;
1400
- const value = note.voiceParams.chorusEffectsSend;
1401
- note.chorusEffectsSend.gain
1469
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1470
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1471
+ note.reverbEffectsSend.gain
1402
1472
  .cancelScheduledValues(now)
1403
1473
  .setValueAtTime(value, now);
1404
1474
  }
1405
1475
  else {
1406
- note.chorusEffectsSend.disconnect();
1476
+ note.reverbEffectsSend.disconnect();
1407
1477
  }
1408
1478
  }
1409
1479
  else {
1410
- if (0 < note.voiceParams.chorusEffectsSend) {
1411
- if (!note.chorusEffectsSend) {
1412
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1413
- gain: note.voiceParams.chorusEffectsSend,
1480
+ if (0 < note.voiceParams.reverbEffectsSend) {
1481
+ if (!note.reverbEffectsSend) {
1482
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1483
+ gain: note.voiceParams.reverbEffectsSend,
1414
1484
  });
1415
- note.volumeNode.connect(note.chorusEffectsSend);
1485
+ note.volumeNode.connect(note.reverbEffectsSend);
1416
1486
  }
1417
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1487
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1418
1488
  }
1419
1489
  }
1420
1490
  }
1421
- setReverbEffectsSend(note, prevValue) {
1491
+ setChorusEffectsSend(channel, note, prevValue) {
1422
1492
  if (0 < prevValue) {
1423
- if (0 < note.voiceParams.reverbEffectsSend) {
1493
+ if (0 < note.voiceParams.chorusEffectsSend) {
1424
1494
  const now = this.audioContext.currentTime;
1425
- const value = note.voiceParams.reverbEffectsSend;
1426
- note.reverbEffectsSend.gain
1495
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1496
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1497
+ note.chorusEffectsSend.gain
1427
1498
  .cancelScheduledValues(now)
1428
1499
  .setValueAtTime(value, now);
1429
1500
  }
1430
1501
  else {
1431
- note.reverbEffectsSend.disconnect();
1502
+ note.chorusEffectsSend.disconnect();
1432
1503
  }
1433
1504
  }
1434
1505
  else {
1435
- if (0 < note.voiceParams.reverbEffectsSend) {
1436
- if (!note.reverbEffectsSend) {
1437
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1438
- gain: note.voiceParams.reverbEffectsSend,
1506
+ if (0 < note.voiceParams.chorusEffectsSend) {
1507
+ if (!note.chorusEffectsSend) {
1508
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1509
+ gain: note.voiceParams.chorusEffectsSend,
1439
1510
  });
1440
- note.volumeNode.connect(note.reverbEffectsSend);
1511
+ note.volumeNode.connect(note.chorusEffectsSend);
1441
1512
  }
1442
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1513
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1443
1514
  }
1444
1515
  }
1445
1516
  }
@@ -1472,18 +1543,20 @@ class Midy {
1472
1543
  }
1473
1544
  },
1474
1545
  modLfoToFilterFc: (channel, note, _prevValue) => {
1475
- if (0 < channel.state.modulationDepth)
1476
- this.setModLfoToFilterFc(note);
1546
+ if (0 < channel.state.modulationDepth) {
1547
+ this.setModLfoToFilterFc(channel, note);
1548
+ }
1477
1549
  },
1478
1550
  modLfoToVolume: (channel, note) => {
1479
- if (0 < channel.state.modulationDepth)
1480
- this.setModLfoToVolume(note);
1551
+ if (0 < channel.state.modulationDepth) {
1552
+ this.setModLfoToVolume(channel, note);
1553
+ }
1481
1554
  },
1482
- chorusEffectsSend: (_channel, note, prevValue) => {
1483
- this.setChorusEffectsSend(note, prevValue);
1555
+ chorusEffectsSend: (channel, note, prevValue) => {
1556
+ this.setChorusEffectsSend(channel, note, prevValue);
1484
1557
  },
1485
- reverbEffectsSend: (_channel, note, prevValue) => {
1486
- this.setReverbEffectsSend(note, prevValue);
1558
+ reverbEffectsSend: (channel, note, prevValue) => {
1559
+ this.setReverbEffectsSend(channel, note, prevValue);
1487
1560
  },
1488
1561
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1489
1562
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
@@ -1650,10 +1723,27 @@ class Midy {
1650
1723
  const factor = 5 * Math.log(10) / 127;
1651
1724
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1652
1725
  }
1726
+ setKeyBasedVolume(channel) {
1727
+ const now = this.audioContext.currentTime;
1728
+ channel.scheduledNotes.forEach((noteList) => {
1729
+ for (let i = 0; i < noteList.length; i++) {
1730
+ const note = noteList[i];
1731
+ if (!note)
1732
+ continue;
1733
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1734
+ if (keyBasedValue === 0)
1735
+ continue;
1736
+ note.volumeNode.gain
1737
+ .cancelScheduledValues(now)
1738
+ .setValueAtTime(1 + keyBasedValue, now);
1739
+ }
1740
+ });
1741
+ }
1653
1742
  setVolume(channelNumber, volume) {
1654
1743
  const channel = this.channels[channelNumber];
1655
1744
  channel.state.volume = volume / 127;
1656
1745
  this.updateChannelVolume(channel);
1746
+ this.setKeyBasedVolume(channel);
1657
1747
  }
1658
1748
  panToGain(pan) {
1659
1749
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1662,10 +1752,31 @@ class Midy {
1662
1752
  gainRight: Math.sin(theta),
1663
1753
  };
1664
1754
  }
1755
+ setKeyBasedPan(channel) {
1756
+ const now = this.audioContext.currentTime;
1757
+ channel.scheduledNotes.forEach((noteList) => {
1758
+ for (let i = 0; i < noteList.length; i++) {
1759
+ const note = noteList[i];
1760
+ if (!note)
1761
+ continue;
1762
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1763
+ if (keyBasedValue === 0)
1764
+ continue;
1765
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1766
+ note.gainL.gain
1767
+ .cancelScheduledValues(now)
1768
+ .setValueAtTime(gainLeft, now);
1769
+ note.gainR.gain
1770
+ .cancelScheduledValues(now)
1771
+ .setValueAtTime(gainRight, now);
1772
+ }
1773
+ });
1774
+ }
1665
1775
  setPan(channelNumber, pan) {
1666
1776
  const channel = this.channels[channelNumber];
1667
1777
  channel.state.pan = pan / 127;
1668
1778
  this.updateChannelVolume(channel);
1779
+ this.setKeyBasedPan(channel);
1669
1780
  }
1670
1781
  setExpression(channelNumber, expression) {
1671
1782
  const channel = this.channels[channelNumber];
@@ -1827,7 +1938,7 @@ class Midy {
1827
1938
  const note = noteList[i];
1828
1939
  if (!note)
1829
1940
  continue;
1830
- this.setReverbEffectsSend(note, 0);
1941
+ this.setReverbEffectsSend(channel, note, 0);
1831
1942
  }
1832
1943
  });
1833
1944
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -1868,7 +1979,7 @@ class Midy {
1868
1979
  const note = noteList[i];
1869
1980
  if (!note)
1870
1981
  continue;
1871
- this.setChorusEffectsSend(note, 0);
1982
+ this.setChorusEffectsSend(channel, note, 0);
1872
1983
  }
1873
1984
  });
1874
1985
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2051,7 +2162,7 @@ class Midy {
2051
2162
  switch (data[3]) {
2052
2163
  case 8:
2053
2164
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2054
- return this.handleScaleOctaveTuning1ByteFormat(data);
2165
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2055
2166
  default:
2056
2167
  console.warn(`Unsupported Exclusive Message: ${data}`);
2057
2168
  }
@@ -2104,7 +2215,7 @@ class Midy {
2104
2215
  return this.handleMasterFineTuningSysEx(data);
2105
2216
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2106
2217
  return this.handleMasterCoarseTuningSysEx(data);
2107
- case 5:
2218
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2108
2219
  return this.handleGlobalParameterControlSysEx(data);
2109
2220
  default:
2110
2221
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2112,19 +2223,17 @@ class Midy {
2112
2223
  break;
2113
2224
  case 8:
2114
2225
  switch (data[3]) {
2115
- case 8:
2116
- // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2226
+ case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2117
2227
  // TODO: realtime
2118
- return this.handleScaleOctaveTuning1ByteFormat(data);
2228
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2119
2229
  default:
2120
2230
  console.warn(`Unsupported Exclusive Message: ${data}`);
2121
2231
  }
2122
2232
  break;
2123
2233
  case 9:
2124
2234
  switch (data[3]) {
2125
- // case 1:
2126
- // // TODO
2127
- // return this.setChannelPressure();
2235
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2236
+ return this.handleChannelPressureSysEx(data);
2128
2237
  // case 3:
2129
2238
  // // TODO
2130
2239
  // return this.setControlChange();
@@ -2134,9 +2243,8 @@ class Midy {
2134
2243
  break;
2135
2244
  case 10:
2136
2245
  switch (data[3]) {
2137
- // case 1:
2138
- // // TODO
2139
- // return this.handleKeyBasedInstrumentControl();
2246
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2247
+ return this.handleKeyBasedInstrumentControlSysEx(data);
2140
2248
  default:
2141
2249
  console.warn(`Unsupported Exclusive Message: ${data}`);
2142
2250
  }
@@ -2181,40 +2289,6 @@ class Midy {
2181
2289
  channel.detune += next - prev;
2182
2290
  this.updateDetune(channel);
2183
2291
  }
2184
- getChannelBitmap(data) {
2185
- const bitmap = new Array(16).fill(false);
2186
- const ff = data[4] & 0b11;
2187
- const gg = data[5] & 0x7F;
2188
- const hh = data[6] & 0x7F;
2189
- for (let bit = 0; bit < 7; bit++) {
2190
- if (hh & (1 << bit))
2191
- bitmap[bit] = true;
2192
- }
2193
- for (let bit = 0; bit < 7; bit++) {
2194
- if (gg & (1 << bit))
2195
- bitmap[bit + 7] = true;
2196
- }
2197
- for (let bit = 0; bit < 2; bit++) {
2198
- if (ff & (1 << bit))
2199
- bitmap[bit + 14] = true;
2200
- }
2201
- return bitmap;
2202
- }
2203
- handleScaleOctaveTuning1ByteFormat(data) {
2204
- if (data.length < 18) {
2205
- console.error("Data length is too short");
2206
- return;
2207
- }
2208
- const channelBitmap = this.getChannelBitmap(data);
2209
- for (let i = 0; i < channelBitmap.length; i++) {
2210
- if (!channelBitmap[i])
2211
- continue;
2212
- for (let j = 0; j < 12; j++) {
2213
- const value = data[j + 7] - 64; // cent
2214
- this.channels[i].scaleOctaveTuningTable[j] = value;
2215
- }
2216
- }
2217
- }
2218
2292
  handleGlobalParameterControlSysEx(data) {
2219
2293
  if (data[7] === 1) {
2220
2294
  switch (data[8]) {
@@ -2401,6 +2475,66 @@ class Midy {
2401
2475
  getChorusSendToReverb(value) {
2402
2476
  return value * 0.00787;
2403
2477
  }
2478
+ getChannelBitmap(data) {
2479
+ const bitmap = new Array(16).fill(false);
2480
+ const ff = data[4] & 0b11;
2481
+ const gg = data[5] & 0x7F;
2482
+ const hh = data[6] & 0x7F;
2483
+ for (let bit = 0; bit < 7; bit++) {
2484
+ if (hh & (1 << bit))
2485
+ bitmap[bit] = true;
2486
+ }
2487
+ for (let bit = 0; bit < 7; bit++) {
2488
+ if (gg & (1 << bit))
2489
+ bitmap[bit + 7] = true;
2490
+ }
2491
+ for (let bit = 0; bit < 2; bit++) {
2492
+ if (ff & (1 << bit))
2493
+ bitmap[bit + 14] = true;
2494
+ }
2495
+ return bitmap;
2496
+ }
2497
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2498
+ if (data.length < 18) {
2499
+ console.error("Data length is too short");
2500
+ return;
2501
+ }
2502
+ const channelBitmap = this.getChannelBitmap(data);
2503
+ for (let i = 0; i < channelBitmap.length; i++) {
2504
+ if (!channelBitmap[i])
2505
+ continue;
2506
+ for (let j = 0; j < 12; j++) {
2507
+ const value = data[j + 7] - 64; // cent
2508
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2509
+ }
2510
+ }
2511
+ }
2512
+ handleChannelPressureSysEx(data) {
2513
+ const channelNumber = data[4];
2514
+ const table = this.channels[channelNumber].pressureTable;
2515
+ for (let i = 5; i < data.length - 1; i += 2) {
2516
+ const pp = data[i];
2517
+ const rr = data[i + 1];
2518
+ table[pp] = rr;
2519
+ }
2520
+ }
2521
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2522
+ const index = keyNumber * 128 + controllerType;
2523
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2524
+ return (controlValue + 64) / 64;
2525
+ }
2526
+ handleKeyBasedInstrumentControlSysEx(data) {
2527
+ const channelNumber = data[4];
2528
+ const keyNumber = data[5];
2529
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2530
+ for (let i = 6; i < data.length - 1; i += 2) {
2531
+ const controllerType = data[i];
2532
+ const value = data[i + 1];
2533
+ const index = keyNumber * 128 + controllerType;
2534
+ table[index] = value - 64;
2535
+ }
2536
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2537
+ }
2404
2538
  handleExclusiveMessage(data) {
2405
2539
  console.warn(`Unsupported Exclusive Message: ${data}`);
2406
2540
  }
@@ -2435,6 +2569,8 @@ Object.defineProperty(Midy, "channelSettings", {
2435
2569
  currentBufferSource: null,
2436
2570
  detune: 0,
2437
2571
  scaleOctaveTuningTable: new Array(12).fill(0), // cent
2572
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2573
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2438
2574
  program: 0,
2439
2575
  bank: 121 * 128,
2440
2576
  bankMSB: 121,