@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/esm/midy.js CHANGED
@@ -14,12 +14,30 @@ class Note {
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
+ Object.defineProperty(this, "volumeEnvelopeNode", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: void 0
22
+ });
17
23
  Object.defineProperty(this, "volumeNode", {
18
24
  enumerable: true,
19
25
  configurable: true,
20
26
  writable: true,
21
27
  value: void 0
22
28
  });
29
+ Object.defineProperty(this, "gainL", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: void 0
34
+ });
35
+ Object.defineProperty(this, "gainR", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: void 0
40
+ });
23
41
  Object.defineProperty(this, "volumeDepth", {
24
42
  enumerable: true,
25
43
  configurable: true,
@@ -920,7 +938,9 @@ export class Midy {
920
938
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
921
939
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
922
940
  const pitch = pitchWheel * pitchWheelSensitivity;
923
- return tuning + pitch;
941
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
942
+ const pressure = pressureDepth * channel.state.channelPressure;
943
+ return tuning + pitch + pressure;
924
944
  }
925
945
  calcNoteDetune(channel, note) {
926
946
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -940,14 +960,19 @@ export class Midy {
940
960
  }
941
961
  });
942
962
  }
963
+ getPortamentoTime(channel) {
964
+ const factor = 5 * Math.log(10) / 127;
965
+ const time = channel.state.portamentoTime;
966
+ return Math.log(time) / factor;
967
+ }
943
968
  setPortamentoStartVolumeEnvelope(channel, note) {
944
969
  const now = this.audioContext.currentTime;
945
970
  const { voiceParams, startTime } = note;
946
971
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
947
972
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
948
973
  const volDelay = startTime + voiceParams.volDelay;
949
- const portamentoTime = volDelay + channel.state.portamentoTime;
950
- note.volumeNode.gain
974
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
975
+ note.volumeEnvelopeNode.gain
951
976
  .cancelScheduledValues(now)
952
977
  .setValueAtTime(0, volDelay)
953
978
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
@@ -956,13 +981,16 @@ export class Midy {
956
981
  const now = this.audioContext.currentTime;
957
982
  const state = channel.state;
958
983
  const { voiceParams, startTime } = note;
959
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
984
+ const pressureDepth = channel.pressureTable[2] / 64;
985
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
986
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
987
+ pressure;
960
988
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
961
989
  const volDelay = startTime + voiceParams.volDelay;
962
990
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
963
991
  const volHold = volAttack + voiceParams.volHold;
964
992
  const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
965
- note.volumeNode.gain
993
+ note.volumeEnvelopeNode.gain
966
994
  .cancelScheduledValues(now)
967
995
  .setValueAtTime(0, startTime)
968
996
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -1004,14 +1032,17 @@ export class Midy {
1004
1032
  const { voiceParams, noteNumber, startTime } = note;
1005
1033
  const softPedalFactor = 1 -
1006
1034
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1007
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1008
- softPedalFactor * state.brightness * 2;
1035
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1036
+ const pressure = pressureDepth * channel.state.channelPressure;
1037
+ const baseCent = voiceParams.initialFilterFc + pressure;
1038
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1039
+ state.brightness * 2;
1009
1040
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1010
1041
  const sustainFreq = baseFreq +
1011
1042
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1012
1043
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1013
1044
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1014
- const portamentoTime = startTime + channel.state.portamentoTime;
1045
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
1015
1046
  const modDelay = startTime + voiceParams.modDelay;
1016
1047
  note.filterNode.frequency
1017
1048
  .cancelScheduledValues(now)
@@ -1056,14 +1087,14 @@ export class Midy {
1056
1087
  note.modulationDepth = new GainNode(this.audioContext);
1057
1088
  this.setModLfoToPitch(channel, note);
1058
1089
  note.volumeDepth = new GainNode(this.audioContext);
1059
- this.setModLfoToVolume(note);
1090
+ this.setModLfoToVolume(channel, note);
1060
1091
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1061
1092
  note.modulationLFO.connect(note.filterDepth);
1062
1093
  note.filterDepth.connect(note.filterNode.frequency);
1063
1094
  note.modulationLFO.connect(note.modulationDepth);
1064
1095
  note.modulationDepth.connect(note.bufferSource.detune);
1065
1096
  note.modulationLFO.connect(note.volumeDepth);
1066
- note.volumeDepth.connect(note.volumeNode.gain);
1097
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1067
1098
  }
1068
1099
  startVibrato(channel, note, startTime) {
1069
1100
  const { voiceParams } = note;
@@ -1085,6 +1116,9 @@ export class Midy {
1085
1116
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1086
1117
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1087
1118
  note.volumeNode = new GainNode(this.audioContext);
1119
+ note.gainL = new GainNode(this.audioContext);
1120
+ note.gainR = new GainNode(this.audioContext);
1121
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1088
1122
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1089
1123
  type: "lowpass",
1090
1124
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
@@ -1111,7 +1145,10 @@ export class Midy {
1111
1145
  channel.currentBufferSource = note.bufferSource;
1112
1146
  }
1113
1147
  note.bufferSource.connect(note.filterNode);
1114
- note.filterNode.connect(note.volumeNode);
1148
+ note.filterNode.connect(note.volumeEnvelopeNode);
1149
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1150
+ note.volumeNode.connect(note.gainL);
1151
+ note.volumeNode.connect(note.gainR);
1115
1152
  if (0 < channel.chorusSendLevel) {
1116
1153
  this.setChorusEffectsSend(channel, note, 0);
1117
1154
  }
@@ -1142,8 +1179,8 @@ export class Midy {
1142
1179
  if (!voice)
1143
1180
  return;
1144
1181
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1145
- note.volumeNode.connect(channel.gainL);
1146
- note.volumeNode.connect(channel.gainR);
1182
+ note.gainL.connect(channel.gainL);
1183
+ note.gainR.connect(channel.gainR);
1147
1184
  if (channel.state.sostenutoPedal) {
1148
1185
  channel.sostenutoNotes.set(noteNumber, note);
1149
1186
  }
@@ -1174,7 +1211,7 @@ export class Midy {
1174
1211
  }
1175
1212
  stopNote(endTime, stopTime, scheduledNotes, index) {
1176
1213
  const note = scheduledNotes[index];
1177
- note.volumeNode.gain
1214
+ note.volumeEnvelopeNode.gain
1178
1215
  .cancelScheduledValues(endTime)
1179
1216
  .linearRampToValueAtTime(0, stopTime);
1180
1217
  note.ending = true;
@@ -1185,8 +1222,11 @@ export class Midy {
1185
1222
  note.bufferSource.onended = () => {
1186
1223
  scheduledNotes[index] = null;
1187
1224
  note.bufferSource.disconnect();
1188
- note.volumeNode.disconnect();
1189
1225
  note.filterNode.disconnect();
1226
+ note.volumeEnvelopeNode.disconnect();
1227
+ note.volumeNode.disconnect();
1228
+ note.gainL.disconnect();
1229
+ note.gainR.disconnect();
1190
1230
  if (note.modulationDepth) {
1191
1231
  note.volumeDepth.disconnect();
1192
1232
  note.modulationDepth.disconnect();
@@ -1236,7 +1276,7 @@ export class Midy {
1236
1276
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1237
1277
  }
1238
1278
  else {
1239
- const portamentoTime = endTime + state.portamentoTime;
1279
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1240
1280
  const deltaNote = portamentoNoteNumber - noteNumber;
1241
1281
  const baseRate = note.voiceParams.playbackRate;
1242
1282
  const targetRate = baseRate * Math.pow(2, deltaNote / 12);
@@ -1311,7 +1351,7 @@ export class Midy {
1311
1351
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1312
1352
  if (activeNotes.has(noteNumber)) {
1313
1353
  const activeNote = activeNotes.get(noteNumber);
1314
- const gain = activeNote.volumeNode.gain.value;
1354
+ const gain = activeNote.gainL.gain.value;
1315
1355
  activeNote.volumeNode.gain
1316
1356
  .cancelScheduledValues(now)
1317
1357
  .setValueAtTime(gain * pressure, now);
@@ -1324,22 +1364,45 @@ export class Midy {
1324
1364
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1325
1365
  channel.program = program;
1326
1366
  }
1327
- handleChannelPressure(channelNumber, pressure) {
1328
- const now = this.audioContext.currentTime;
1367
+ handleChannelPressure(channelNumber, value) {
1329
1368
  const channel = this.channels[channelNumber];
1330
- pressure /= 64;
1331
- channel.channelPressure = pressure;
1332
- const activeNotes = this.getActiveNotes(channel, now);
1333
- if (channel.channelPressure.amplitudeControl !== 1) {
1334
- activeNotes.forEach((activeNote) => {
1335
- const gain = activeNote.volumeNode.gain.value;
1336
- activeNote.volumeNode.gain
1337
- .cancelScheduledValues(now)
1338
- .setValueAtTime(gain * pressure, now);
1339
- });
1369
+ const prev = channel.state.channelPressure;
1370
+ const next = value / 127;
1371
+ channel.state.channelPressure = next;
1372
+ if (channel.pressureTable[0] !== 64) {
1373
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1374
+ channel.detune += pressureDepth * (next - prev);
1340
1375
  }
1376
+ channel.scheduledNotes.forEach((noteList) => {
1377
+ for (let i = 0; i < noteList.length; i++) {
1378
+ const note = noteList[i];
1379
+ if (!note)
1380
+ continue;
1381
+ this.setChannelPressure(channel, note);
1382
+ }
1383
+ });
1341
1384
  // this.applyVoiceParams(channel, 13);
1342
1385
  }
1386
+ setChannelPressure(channel, note) {
1387
+ if (channel.pressureTable[0] !== 64) {
1388
+ this.updateDetune(channel);
1389
+ }
1390
+ if (channel.pressureTable[1] !== 64 && !note.portamento) {
1391
+ this.setFilterEnvelope(channel, note);
1392
+ }
1393
+ if (channel.pressureTable[2] !== 64 && !note.portamento) {
1394
+ this.setVolumeEnvelope(channel, note);
1395
+ }
1396
+ if (channel.pressureTable[3] !== 0) {
1397
+ this.setModLfoToPitch(channel, note);
1398
+ }
1399
+ if (channel.pressureTable[4] !== 0) {
1400
+ this.setModLfoToFilterFc(channel, note);
1401
+ }
1402
+ if (channel.pressureTable[5] !== 0) {
1403
+ this.setModLfoToVolume(channel, note);
1404
+ }
1405
+ }
1343
1406
  handlePitchBendMessage(channelNumber, lsb, msb) {
1344
1407
  const pitchBend = msb * 128 + lsb;
1345
1408
  this.setPitchBend(channelNumber, pitchBend);
@@ -1356,13 +1419,15 @@ export class Midy {
1356
1419
  }
1357
1420
  setModLfoToPitch(channel, note) {
1358
1421
  const now = this.audioContext.currentTime;
1359
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1360
- const modulationDepth = Math.abs(modLfoToPitch) +
1422
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1423
+ const pressure = pressureDepth * channel.state.channelPressure;
1424
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1425
+ const baseDepth = Math.abs(modLfoToPitch) +
1361
1426
  channel.state.modulationDepth;
1362
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1427
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1363
1428
  note.modulationDepth.gain
1364
1429
  .cancelScheduledValues(now)
1365
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1430
+ .setValueAtTime(modulationDepth, now);
1366
1431
  }
1367
1432
  setVibLfoToPitch(channel, note) {
1368
1433
  const now = this.audioContext.currentTime;
@@ -1374,69 +1439,75 @@ export class Midy {
1374
1439
  .cancelScheduledValues(now)
1375
1440
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1376
1441
  }
1377
- setModLfoToFilterFc(note) {
1442
+ setModLfoToFilterFc(channel, note) {
1378
1443
  const now = this.audioContext.currentTime;
1379
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1444
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1445
+ const pressure = pressureDepth * channel.state.channelPressure;
1446
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1380
1447
  note.filterDepth.gain
1381
1448
  .cancelScheduledValues(now)
1382
1449
  .setValueAtTime(modLfoToFilterFc, now);
1383
1450
  }
1384
- setModLfoToVolume(note) {
1451
+ setModLfoToVolume(channel, note) {
1385
1452
  const now = this.audioContext.currentTime;
1386
1453
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1387
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1388
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1454
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1455
+ const pressureDepth = channel.pressureTable[5] / 127;
1456
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1457
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1389
1458
  note.volumeDepth.gain
1390
1459
  .cancelScheduledValues(now)
1391
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1460
+ .setValueAtTime(volumeDepth, now);
1392
1461
  }
1393
- setChorusEffectsSend(note, prevValue) {
1462
+ setReverbEffectsSend(channel, note, prevValue) {
1394
1463
  if (0 < prevValue) {
1395
- if (0 < note.voiceParams.chorusEffectsSend) {
1464
+ if (0 < note.voiceParams.reverbEffectsSend) {
1396
1465
  const now = this.audioContext.currentTime;
1397
- const value = note.voiceParams.chorusEffectsSend;
1398
- note.chorusEffectsSend.gain
1466
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1467
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1468
+ note.reverbEffectsSend.gain
1399
1469
  .cancelScheduledValues(now)
1400
1470
  .setValueAtTime(value, now);
1401
1471
  }
1402
1472
  else {
1403
- note.chorusEffectsSend.disconnect();
1473
+ note.reverbEffectsSend.disconnect();
1404
1474
  }
1405
1475
  }
1406
1476
  else {
1407
- if (0 < note.voiceParams.chorusEffectsSend) {
1408
- if (!note.chorusEffectsSend) {
1409
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1410
- gain: note.voiceParams.chorusEffectsSend,
1477
+ if (0 < note.voiceParams.reverbEffectsSend) {
1478
+ if (!note.reverbEffectsSend) {
1479
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1480
+ gain: note.voiceParams.reverbEffectsSend,
1411
1481
  });
1412
- note.volumeNode.connect(note.chorusEffectsSend);
1482
+ note.volumeNode.connect(note.reverbEffectsSend);
1413
1483
  }
1414
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1484
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1415
1485
  }
1416
1486
  }
1417
1487
  }
1418
- setReverbEffectsSend(note, prevValue) {
1488
+ setChorusEffectsSend(channel, note, prevValue) {
1419
1489
  if (0 < prevValue) {
1420
- if (0 < note.voiceParams.reverbEffectsSend) {
1490
+ if (0 < note.voiceParams.chorusEffectsSend) {
1421
1491
  const now = this.audioContext.currentTime;
1422
- const value = note.voiceParams.reverbEffectsSend;
1423
- note.reverbEffectsSend.gain
1492
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1493
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1494
+ note.chorusEffectsSend.gain
1424
1495
  .cancelScheduledValues(now)
1425
1496
  .setValueAtTime(value, now);
1426
1497
  }
1427
1498
  else {
1428
- note.reverbEffectsSend.disconnect();
1499
+ note.chorusEffectsSend.disconnect();
1429
1500
  }
1430
1501
  }
1431
1502
  else {
1432
- if (0 < note.voiceParams.reverbEffectsSend) {
1433
- if (!note.reverbEffectsSend) {
1434
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1435
- gain: note.voiceParams.reverbEffectsSend,
1503
+ if (0 < note.voiceParams.chorusEffectsSend) {
1504
+ if (!note.chorusEffectsSend) {
1505
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1506
+ gain: note.voiceParams.chorusEffectsSend,
1436
1507
  });
1437
- note.volumeNode.connect(note.reverbEffectsSend);
1508
+ note.volumeNode.connect(note.chorusEffectsSend);
1438
1509
  }
1439
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1510
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1440
1511
  }
1441
1512
  }
1442
1513
  }
@@ -1469,18 +1540,20 @@ export class Midy {
1469
1540
  }
1470
1541
  },
1471
1542
  modLfoToFilterFc: (channel, note, _prevValue) => {
1472
- if (0 < channel.state.modulationDepth)
1473
- this.setModLfoToFilterFc(note);
1543
+ if (0 < channel.state.modulationDepth) {
1544
+ this.setModLfoToFilterFc(channel, note);
1545
+ }
1474
1546
  },
1475
1547
  modLfoToVolume: (channel, note) => {
1476
- if (0 < channel.state.modulationDepth)
1477
- this.setModLfoToVolume(note);
1548
+ if (0 < channel.state.modulationDepth) {
1549
+ this.setModLfoToVolume(channel, note);
1550
+ }
1478
1551
  },
1479
- chorusEffectsSend: (_channel, note, prevValue) => {
1480
- this.setChorusEffectsSend(note, prevValue);
1552
+ chorusEffectsSend: (channel, note, prevValue) => {
1553
+ this.setChorusEffectsSend(channel, note, prevValue);
1481
1554
  },
1482
- reverbEffectsSend: (_channel, note, prevValue) => {
1483
- this.setReverbEffectsSend(note, prevValue);
1555
+ reverbEffectsSend: (channel, note, prevValue) => {
1556
+ this.setReverbEffectsSend(channel, note, prevValue);
1484
1557
  },
1485
1558
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1486
1559
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
@@ -1647,10 +1720,27 @@ export class Midy {
1647
1720
  const factor = 5 * Math.log(10) / 127;
1648
1721
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1649
1722
  }
1723
+ setKeyBasedVolume(channel) {
1724
+ const now = this.audioContext.currentTime;
1725
+ channel.scheduledNotes.forEach((noteList) => {
1726
+ for (let i = 0; i < noteList.length; i++) {
1727
+ const note = noteList[i];
1728
+ if (!note)
1729
+ continue;
1730
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1731
+ if (keyBasedValue === 0)
1732
+ continue;
1733
+ note.volumeNode.gain
1734
+ .cancelScheduledValues(now)
1735
+ .setValueAtTime(1 + keyBasedValue, now);
1736
+ }
1737
+ });
1738
+ }
1650
1739
  setVolume(channelNumber, volume) {
1651
1740
  const channel = this.channels[channelNumber];
1652
1741
  channel.state.volume = volume / 127;
1653
1742
  this.updateChannelVolume(channel);
1743
+ this.setKeyBasedVolume(channel);
1654
1744
  }
1655
1745
  panToGain(pan) {
1656
1746
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1659,10 +1749,31 @@ export class Midy {
1659
1749
  gainRight: Math.sin(theta),
1660
1750
  };
1661
1751
  }
1752
+ setKeyBasedPan(channel) {
1753
+ const now = this.audioContext.currentTime;
1754
+ channel.scheduledNotes.forEach((noteList) => {
1755
+ for (let i = 0; i < noteList.length; i++) {
1756
+ const note = noteList[i];
1757
+ if (!note)
1758
+ continue;
1759
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1760
+ if (keyBasedValue === 0)
1761
+ continue;
1762
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1763
+ note.gainL.gain
1764
+ .cancelScheduledValues(now)
1765
+ .setValueAtTime(gainLeft, now);
1766
+ note.gainR.gain
1767
+ .cancelScheduledValues(now)
1768
+ .setValueAtTime(gainRight, now);
1769
+ }
1770
+ });
1771
+ }
1662
1772
  setPan(channelNumber, pan) {
1663
1773
  const channel = this.channels[channelNumber];
1664
1774
  channel.state.pan = pan / 127;
1665
1775
  this.updateChannelVolume(channel);
1776
+ this.setKeyBasedPan(channel);
1666
1777
  }
1667
1778
  setExpression(channelNumber, expression) {
1668
1779
  const channel = this.channels[channelNumber];
@@ -1824,7 +1935,7 @@ export class Midy {
1824
1935
  const note = noteList[i];
1825
1936
  if (!note)
1826
1937
  continue;
1827
- this.setReverbEffectsSend(note, 0);
1938
+ this.setReverbEffectsSend(channel, note, 0);
1828
1939
  }
1829
1940
  });
1830
1941
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -1865,7 +1976,7 @@ export class Midy {
1865
1976
  const note = noteList[i];
1866
1977
  if (!note)
1867
1978
  continue;
1868
- this.setChorusEffectsSend(note, 0);
1979
+ this.setChorusEffectsSend(channel, note, 0);
1869
1980
  }
1870
1981
  });
1871
1982
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2048,7 +2159,7 @@ export class Midy {
2048
2159
  switch (data[3]) {
2049
2160
  case 8:
2050
2161
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2051
- return this.handleScaleOctaveTuning1ByteFormat(data);
2162
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2052
2163
  default:
2053
2164
  console.warn(`Unsupported Exclusive Message: ${data}`);
2054
2165
  }
@@ -2101,7 +2212,7 @@ export class Midy {
2101
2212
  return this.handleMasterFineTuningSysEx(data);
2102
2213
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2103
2214
  return this.handleMasterCoarseTuningSysEx(data);
2104
- case 5:
2215
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2105
2216
  return this.handleGlobalParameterControlSysEx(data);
2106
2217
  default:
2107
2218
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2109,19 +2220,17 @@ export class Midy {
2109
2220
  break;
2110
2221
  case 8:
2111
2222
  switch (data[3]) {
2112
- case 8:
2113
- // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2223
+ case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2114
2224
  // TODO: realtime
2115
- return this.handleScaleOctaveTuning1ByteFormat(data);
2225
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2116
2226
  default:
2117
2227
  console.warn(`Unsupported Exclusive Message: ${data}`);
2118
2228
  }
2119
2229
  break;
2120
2230
  case 9:
2121
2231
  switch (data[3]) {
2122
- // case 1:
2123
- // // TODO
2124
- // return this.setChannelPressure();
2232
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2233
+ return this.handleChannelPressureSysEx(data);
2125
2234
  // case 3:
2126
2235
  // // TODO
2127
2236
  // return this.setControlChange();
@@ -2131,9 +2240,8 @@ export class Midy {
2131
2240
  break;
2132
2241
  case 10:
2133
2242
  switch (data[3]) {
2134
- // case 1:
2135
- // // TODO
2136
- // return this.handleKeyBasedInstrumentControl();
2243
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2244
+ return this.handleKeyBasedInstrumentControlSysEx(data);
2137
2245
  default:
2138
2246
  console.warn(`Unsupported Exclusive Message: ${data}`);
2139
2247
  }
@@ -2178,40 +2286,6 @@ export class Midy {
2178
2286
  channel.detune += next - prev;
2179
2287
  this.updateDetune(channel);
2180
2288
  }
2181
- getChannelBitmap(data) {
2182
- const bitmap = new Array(16).fill(false);
2183
- const ff = data[4] & 0b11;
2184
- const gg = data[5] & 0x7F;
2185
- const hh = data[6] & 0x7F;
2186
- for (let bit = 0; bit < 7; bit++) {
2187
- if (hh & (1 << bit))
2188
- bitmap[bit] = true;
2189
- }
2190
- for (let bit = 0; bit < 7; bit++) {
2191
- if (gg & (1 << bit))
2192
- bitmap[bit + 7] = true;
2193
- }
2194
- for (let bit = 0; bit < 2; bit++) {
2195
- if (ff & (1 << bit))
2196
- bitmap[bit + 14] = true;
2197
- }
2198
- return bitmap;
2199
- }
2200
- handleScaleOctaveTuning1ByteFormat(data) {
2201
- if (data.length < 18) {
2202
- console.error("Data length is too short");
2203
- return;
2204
- }
2205
- const channelBitmap = this.getChannelBitmap(data);
2206
- for (let i = 0; i < channelBitmap.length; i++) {
2207
- if (!channelBitmap[i])
2208
- continue;
2209
- for (let j = 0; j < 12; j++) {
2210
- const value = data[j + 7] - 64; // cent
2211
- this.channels[i].scaleOctaveTuningTable[j] = value;
2212
- }
2213
- }
2214
- }
2215
2289
  handleGlobalParameterControlSysEx(data) {
2216
2290
  if (data[7] === 1) {
2217
2291
  switch (data[8]) {
@@ -2398,6 +2472,66 @@ export class Midy {
2398
2472
  getChorusSendToReverb(value) {
2399
2473
  return value * 0.00787;
2400
2474
  }
2475
+ getChannelBitmap(data) {
2476
+ const bitmap = new Array(16).fill(false);
2477
+ const ff = data[4] & 0b11;
2478
+ const gg = data[5] & 0x7F;
2479
+ const hh = data[6] & 0x7F;
2480
+ for (let bit = 0; bit < 7; bit++) {
2481
+ if (hh & (1 << bit))
2482
+ bitmap[bit] = true;
2483
+ }
2484
+ for (let bit = 0; bit < 7; bit++) {
2485
+ if (gg & (1 << bit))
2486
+ bitmap[bit + 7] = true;
2487
+ }
2488
+ for (let bit = 0; bit < 2; bit++) {
2489
+ if (ff & (1 << bit))
2490
+ bitmap[bit + 14] = true;
2491
+ }
2492
+ return bitmap;
2493
+ }
2494
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2495
+ if (data.length < 18) {
2496
+ console.error("Data length is too short");
2497
+ return;
2498
+ }
2499
+ const channelBitmap = this.getChannelBitmap(data);
2500
+ for (let i = 0; i < channelBitmap.length; i++) {
2501
+ if (!channelBitmap[i])
2502
+ continue;
2503
+ for (let j = 0; j < 12; j++) {
2504
+ const value = data[j + 7] - 64; // cent
2505
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2506
+ }
2507
+ }
2508
+ }
2509
+ handleChannelPressureSysEx(data) {
2510
+ const channelNumber = data[4];
2511
+ const table = this.channels[channelNumber].pressureTable;
2512
+ for (let i = 5; i < data.length - 1; i += 2) {
2513
+ const pp = data[i];
2514
+ const rr = data[i + 1];
2515
+ table[pp] = rr;
2516
+ }
2517
+ }
2518
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2519
+ const index = keyNumber * 128 + controllerType;
2520
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2521
+ return (controlValue + 64) / 64;
2522
+ }
2523
+ handleKeyBasedInstrumentControlSysEx(data) {
2524
+ const channelNumber = data[4];
2525
+ const keyNumber = data[5];
2526
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2527
+ for (let i = 6; i < data.length - 1; i += 2) {
2528
+ const controllerType = data[i];
2529
+ const value = data[i + 1];
2530
+ const index = keyNumber * 128 + controllerType;
2531
+ table[index] = value - 64;
2532
+ }
2533
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2534
+ }
2401
2535
  handleExclusiveMessage(data) {
2402
2536
  console.warn(`Unsupported Exclusive Message: ${data}`);
2403
2537
  }
@@ -2431,6 +2565,8 @@ Object.defineProperty(Midy, "channelSettings", {
2431
2565
  currentBufferSource: null,
2432
2566
  detune: 0,
2433
2567
  scaleOctaveTuningTable: new Array(12).fill(0), // cent
2568
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2569
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2434
2570
  program: 0,
2435
2571
  bank: 121 * 128,
2436
2572
  bankMSB: 121,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marmooo/midy",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -132,7 +132,7 @@ export class MidyGM1 {
132
132
  delayVibLFO: (channel: any, note: any, prevValue: any) => void;
133
133
  freqVibLFO: (channel: any, note: any, _prevValue: any) => void;
134
134
  };
135
- getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array;
135
+ getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array<any>;
136
136
  applyVoiceParams(channel: any, controllerType: any): void;
137
137
  createControlChangeHandlers(): {
138
138
  1: (channelNumber: any, modulation: any) => void;
@@ -189,7 +189,7 @@ declare class Note {
189
189
  constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
190
190
  bufferSource: any;
191
191
  filterNode: any;
192
- volumeNode: any;
192
+ volumeEnvelopeNode: any;
193
193
  volumeDepth: any;
194
194
  modulationLFO: any;
195
195
  modulationDepth: any;