@marmooo/midy 0.2.1 → 0.2.3

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-GM2.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,
@@ -341,15 +359,15 @@ export class MidyGM2 {
341
359
  });
342
360
  this.audioContext = audioContext;
343
361
  this.options = { ...this.defaultOptions, ...options };
344
- this.masterGain = new GainNode(audioContext);
362
+ this.masterVolume = new GainNode(audioContext);
345
363
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
346
364
  this.controlChangeHandlers = this.createControlChangeHandlers();
347
365
  this.channels = this.createChannels(audioContext);
348
366
  this.reverbEffect = this.options.reverbAlgorithm(audioContext);
349
367
  this.chorusEffect = this.createChorusEffect(audioContext);
350
- this.chorusEffect.output.connect(this.masterGain);
351
- this.reverbEffect.output.connect(this.masterGain);
352
- this.masterGain.connect(audioContext.destination);
368
+ this.chorusEffect.output.connect(this.masterVolume);
369
+ this.reverbEffect.output.connect(this.masterVolume);
370
+ this.masterVolume.connect(audioContext.destination);
353
371
  this.GM2SystemOn();
354
372
  }
355
373
  initSoundFontTable() {
@@ -395,7 +413,7 @@ export class MidyGM2 {
395
413
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
396
414
  gainL.connect(merger, 0, 0);
397
415
  gainR.connect(merger, 0, 1);
398
- merger.connect(this.masterGain);
416
+ merger.connect(this.masterVolume);
399
417
  return {
400
418
  gainL,
401
419
  gainR,
@@ -407,6 +425,7 @@ export class MidyGM2 {
407
425
  return {
408
426
  ...this.constructor.channelSettings,
409
427
  state: new ControllerState(),
428
+ controlTable: this.initControlTable(),
410
429
  ...this.setChannelAudioNodes(audioContext),
411
430
  scheduledNotes: new Map(),
412
431
  sostenutoNotes: new Map(),
@@ -907,15 +926,16 @@ export class MidyGM2 {
907
926
  centToHz(cent) {
908
927
  return 8.176 * this.centToRate(cent);
909
928
  }
910
- calcDetune(channel, note) {
929
+ calcChannelDetune(channel) {
911
930
  const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
912
931
  const channelTuning = channel.coarseTuning + channel.fineTuning;
913
- const scaleOctaveTuning = channel.scaleOctaveTuningTable[note.noteNumber % 12];
914
- const tuning = masterTuning + channelTuning + scaleOctaveTuning;
932
+ const tuning = masterTuning + channelTuning;
915
933
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
916
934
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
917
935
  const pitch = pitchWheel * pitchWheelSensitivity;
918
- return tuning + pitch;
936
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
937
+ const pressure = pressureDepth * channel.state.channelPressure;
938
+ return tuning + pitch + pressure;
919
939
  }
920
940
  calcNoteDetune(channel, note) {
921
941
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -935,28 +955,37 @@ export class MidyGM2 {
935
955
  }
936
956
  });
937
957
  }
958
+ getPortamentoTime(channel) {
959
+ const factor = 5 * Math.log(10) / 127;
960
+ const time = channel.state.portamentoTime;
961
+ return Math.log(time) / factor;
962
+ }
938
963
  setPortamentoStartVolumeEnvelope(channel, note) {
939
964
  const now = this.audioContext.currentTime;
940
965
  const { voiceParams, startTime } = note;
941
966
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
942
967
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
943
968
  const volDelay = startTime + voiceParams.volDelay;
944
- const portamentoTime = volDelay + channel.state.portamentoTime;
945
- note.volumeNode.gain
969
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
970
+ note.volumeEnvelopeNode.gain
946
971
  .cancelScheduledValues(now)
947
972
  .setValueAtTime(0, volDelay)
948
973
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
949
974
  }
950
- setVolumeEnvelope(note) {
975
+ setVolumeEnvelope(channel, note) {
951
976
  const now = this.audioContext.currentTime;
977
+ const state = channel.state;
952
978
  const { voiceParams, startTime } = note;
953
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
979
+ const pressureDepth = channel.pressureTable[2] / 64;
980
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
981
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
982
+ pressure;
954
983
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
955
984
  const volDelay = startTime + voiceParams.volDelay;
956
985
  const volAttack = volDelay + voiceParams.volAttack;
957
986
  const volHold = volAttack + voiceParams.volHold;
958
987
  const volDecay = volHold + voiceParams.volDecay;
959
- note.volumeNode.gain
988
+ note.volumeEnvelopeNode.gain
960
989
  .cancelScheduledValues(now)
961
990
  .setValueAtTime(0, startTime)
962
991
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -998,14 +1027,16 @@ export class MidyGM2 {
998
1027
  const { voiceParams, noteNumber, startTime } = note;
999
1028
  const softPedalFactor = 1 -
1000
1029
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1001
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1002
- softPedalFactor;
1030
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1031
+ const pressure = pressureDepth * channel.state.channelPressure;
1032
+ const baseCent = voiceParams.initialFilterFc + pressure;
1033
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1003
1034
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
1004
1035
  const sustainFreq = baseFreq +
1005
1036
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1006
1037
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1007
1038
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1008
- const portamentoTime = startTime + channel.state.portamentoTime;
1039
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
1009
1040
  const modDelay = startTime + voiceParams.modDelay;
1010
1041
  note.filterNode.frequency
1011
1042
  .cancelScheduledValues(now)
@@ -1050,14 +1081,14 @@ export class MidyGM2 {
1050
1081
  note.modulationDepth = new GainNode(this.audioContext);
1051
1082
  this.setModLfoToPitch(channel, note);
1052
1083
  note.volumeDepth = new GainNode(this.audioContext);
1053
- this.setModLfoToVolume(note);
1084
+ this.setModLfoToVolume(channel, note);
1054
1085
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1055
1086
  note.modulationLFO.connect(note.filterDepth);
1056
1087
  note.filterDepth.connect(note.filterNode.frequency);
1057
1088
  note.modulationLFO.connect(note.modulationDepth);
1058
1089
  note.modulationDepth.connect(note.bufferSource.detune);
1059
1090
  note.modulationLFO.connect(note.volumeDepth);
1060
- note.volumeDepth.connect(note.volumeNode.gain);
1091
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1061
1092
  }
1062
1093
  startVibrato(channel, note, startTime) {
1063
1094
  const { voiceParams } = note;
@@ -1079,6 +1110,9 @@ export class MidyGM2 {
1079
1110
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1080
1111
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1081
1112
  note.volumeNode = new GainNode(this.audioContext);
1113
+ note.gainL = new GainNode(this.audioContext);
1114
+ note.gainR = new GainNode(this.audioContext);
1115
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1082
1116
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1083
1117
  type: "lowpass",
1084
1118
  Q: voiceParams.initialFilterQ / 10, // dB
@@ -1105,7 +1139,10 @@ export class MidyGM2 {
1105
1139
  channel.currentBufferSource = note.bufferSource;
1106
1140
  }
1107
1141
  note.bufferSource.connect(note.filterNode);
1108
- note.filterNode.connect(note.volumeNode);
1142
+ note.filterNode.connect(note.volumeEnvelopeNode);
1143
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1144
+ note.volumeNode.connect(note.gainL);
1145
+ note.volumeNode.connect(note.gainR);
1109
1146
  if (0 < channel.chorusSendLevel) {
1110
1147
  this.setChorusEffectsSend(channel, note, 0);
1111
1148
  }
@@ -1136,8 +1173,8 @@ export class MidyGM2 {
1136
1173
  if (!voice)
1137
1174
  return;
1138
1175
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1139
- note.volumeNode.connect(channel.gainL);
1140
- note.volumeNode.connect(channel.gainR);
1176
+ note.gainL.connect(channel.gainL);
1177
+ note.gainR.connect(channel.gainR);
1141
1178
  if (channel.state.sostenutoPedal) {
1142
1179
  channel.sostenutoNotes.set(noteNumber, note);
1143
1180
  }
@@ -1168,7 +1205,7 @@ export class MidyGM2 {
1168
1205
  }
1169
1206
  stopNote(endTime, stopTime, scheduledNotes, index) {
1170
1207
  const note = scheduledNotes[index];
1171
- note.volumeNode.gain
1208
+ note.volumeEnvelopeNode.gain
1172
1209
  .cancelScheduledValues(endTime)
1173
1210
  .linearRampToValueAtTime(0, stopTime);
1174
1211
  note.ending = true;
@@ -1179,8 +1216,11 @@ export class MidyGM2 {
1179
1216
  note.bufferSource.onended = () => {
1180
1217
  scheduledNotes[index] = null;
1181
1218
  note.bufferSource.disconnect();
1182
- note.volumeNode.disconnect();
1183
1219
  note.filterNode.disconnect();
1220
+ note.volumeEnvelopeNode.disconnect();
1221
+ note.volumeNode.disconnect();
1222
+ note.gainL.disconnect();
1223
+ note.gainR.disconnect();
1184
1224
  if (note.modulationDepth) {
1185
1225
  note.volumeDepth.disconnect();
1186
1226
  note.modulationDepth.disconnect();
@@ -1229,7 +1269,7 @@ export class MidyGM2 {
1229
1269
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1230
1270
  }
1231
1271
  else {
1232
- const portamentoTime = endTime + state.portamentoTime;
1272
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1233
1273
  const deltaNote = portamentoNoteNumber - noteNumber;
1234
1274
  const baseRate = note.voiceParams.playbackRate;
1235
1275
  const targetRate = baseRate * Math.pow(2, deltaNote / 12);
@@ -1299,22 +1339,48 @@ export class MidyGM2 {
1299
1339
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1300
1340
  channel.program = program;
1301
1341
  }
1302
- handleChannelPressure(channelNumber, pressure) {
1303
- const now = this.audioContext.currentTime;
1342
+ handleChannelPressure(channelNumber, value) {
1304
1343
  const channel = this.channels[channelNumber];
1305
- pressure /= 64;
1306
- channel.channelPressure = pressure;
1307
- const activeNotes = this.getActiveNotes(channel, now);
1308
- if (channel.channelPressure.amplitudeControl !== 1) {
1309
- activeNotes.forEach((activeNote) => {
1310
- const gain = activeNote.volumeNode.gain.value;
1311
- activeNote.volumeNode.gain
1312
- .cancelScheduledValues(now)
1313
- .setValueAtTime(gain * pressure, now);
1314
- });
1315
- }
1344
+ const prev = channel.state.channelPressure;
1345
+ const next = value / 127;
1346
+ channel.state.channelPressure = next;
1347
+ if (channel.pressureTable[0] !== 64) {
1348
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1349
+ channel.detune += pressureDepth * (next - prev);
1350
+ }
1351
+ const table = channel.pressureTable;
1352
+ channel.scheduledNotes.forEach((noteList) => {
1353
+ for (let i = 0; i < noteList.length; i++) {
1354
+ const note = noteList[i];
1355
+ if (!note)
1356
+ continue;
1357
+ this.applyDestinationSettings(channel, note, table);
1358
+ }
1359
+ });
1316
1360
  // this.applyVoiceParams(channel, 13);
1317
1361
  }
1362
+ setChannelPressure(channel, note) {
1363
+ if (channel.pressureTable[0] !== 64) {
1364
+ this.updateDetune(channel);
1365
+ }
1366
+ if (!note.portamento) {
1367
+ if (channel.pressureTable[1] !== 64) {
1368
+ this.setFilterEnvelope(channel, note);
1369
+ }
1370
+ if (channel.pressureTable[2] !== 64) {
1371
+ this.setVolumeEnvelope(channel, note);
1372
+ }
1373
+ }
1374
+ if (channel.pressureTable[3] !== 0) {
1375
+ this.setModLfoToPitch(channel, note);
1376
+ }
1377
+ if (channel.pressureTable[4] !== 0) {
1378
+ this.setModLfoToFilterFc(channel, note);
1379
+ }
1380
+ if (channel.pressureTable[5] !== 0) {
1381
+ this.setModLfoToVolume(channel, note);
1382
+ }
1383
+ }
1318
1384
  handlePitchBendMessage(channelNumber, lsb, msb) {
1319
1385
  const pitchBend = msb * 128 + lsb;
1320
1386
  this.setPitchBend(channelNumber, pitchBend);
@@ -1331,13 +1397,15 @@ export class MidyGM2 {
1331
1397
  }
1332
1398
  setModLfoToPitch(channel, note) {
1333
1399
  const now = this.audioContext.currentTime;
1334
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1335
- const modulationDepth = Math.abs(modLfoToPitch) +
1400
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1401
+ const pressure = pressureDepth * channel.state.channelPressure;
1402
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1403
+ const baseDepth = Math.abs(modLfoToPitch) +
1336
1404
  channel.state.modulationDepth;
1337
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1405
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1338
1406
  note.modulationDepth.gain
1339
1407
  .cancelScheduledValues(now)
1340
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1408
+ .setValueAtTime(modulationDepth, now);
1341
1409
  }
1342
1410
  setVibLfoToPitch(channel, note) {
1343
1411
  const now = this.audioContext.currentTime;
@@ -1349,69 +1417,75 @@ export class MidyGM2 {
1349
1417
  .cancelScheduledValues(now)
1350
1418
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1351
1419
  }
1352
- setModLfoToFilterFc(note) {
1420
+ setModLfoToFilterFc(channel, note) {
1353
1421
  const now = this.audioContext.currentTime;
1354
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1422
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1423
+ const pressure = pressureDepth * channel.state.channelPressure;
1424
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1355
1425
  note.filterDepth.gain
1356
1426
  .cancelScheduledValues(now)
1357
1427
  .setValueAtTime(modLfoToFilterFc, now);
1358
1428
  }
1359
- setModLfoToVolume(note) {
1429
+ setModLfoToVolume(channel, note) {
1360
1430
  const now = this.audioContext.currentTime;
1361
1431
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1362
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1363
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1432
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1433
+ const pressureDepth = channel.pressureTable[5] / 127;
1434
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1435
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1364
1436
  note.volumeDepth.gain
1365
1437
  .cancelScheduledValues(now)
1366
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1438
+ .setValueAtTime(volumeDepth, now);
1367
1439
  }
1368
- setChorusEffectsSend(note, prevValue) {
1440
+ setReverbEffectsSend(channel, note, prevValue) {
1369
1441
  if (0 < prevValue) {
1370
- if (0 < note.voiceParams.chorusEffectsSend) {
1442
+ if (0 < note.voiceParams.reverbEffectsSend) {
1371
1443
  const now = this.audioContext.currentTime;
1372
- const value = note.voiceParams.chorusEffectsSend;
1373
- note.chorusEffectsSend.gain
1444
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1445
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1446
+ note.reverbEffectsSend.gain
1374
1447
  .cancelScheduledValues(now)
1375
1448
  .setValueAtTime(value, now);
1376
1449
  }
1377
1450
  else {
1378
- note.chorusEffectsSend.disconnect();
1451
+ note.reverbEffectsSend.disconnect();
1379
1452
  }
1380
1453
  }
1381
1454
  else {
1382
- if (0 < note.voiceParams.chorusEffectsSend) {
1383
- if (!note.chorusEffectsSend) {
1384
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1385
- gain: note.voiceParams.chorusEffectsSend,
1455
+ if (0 < note.voiceParams.reverbEffectsSend) {
1456
+ if (!note.reverbEffectsSend) {
1457
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1458
+ gain: note.voiceParams.reverbEffectsSend,
1386
1459
  });
1387
- note.volumeNode.connect(note.chorusEffectsSend);
1460
+ note.volumeNode.connect(note.reverbEffectsSend);
1388
1461
  }
1389
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1462
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1390
1463
  }
1391
1464
  }
1392
1465
  }
1393
- setReverbEffectsSend(note, prevValue) {
1466
+ setChorusEffectsSend(channel, note, prevValue) {
1394
1467
  if (0 < prevValue) {
1395
- if (0 < note.voiceParams.reverbEffectsSend) {
1468
+ if (0 < note.voiceParams.chorusEffectsSend) {
1396
1469
  const now = this.audioContext.currentTime;
1397
- const value = note.voiceParams.reverbEffectsSend;
1398
- note.reverbEffectsSend.gain
1470
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1471
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1472
+ note.chorusEffectsSend.gain
1399
1473
  .cancelScheduledValues(now)
1400
1474
  .setValueAtTime(value, now);
1401
1475
  }
1402
1476
  else {
1403
- note.reverbEffectsSend.disconnect();
1477
+ note.chorusEffectsSend.disconnect();
1404
1478
  }
1405
1479
  }
1406
1480
  else {
1407
- if (0 < note.voiceParams.reverbEffectsSend) {
1408
- if (!note.reverbEffectsSend) {
1409
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1410
- gain: note.voiceParams.reverbEffectsSend,
1481
+ if (0 < note.voiceParams.chorusEffectsSend) {
1482
+ if (!note.chorusEffectsSend) {
1483
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1484
+ gain: note.voiceParams.chorusEffectsSend,
1411
1485
  });
1412
- note.volumeNode.connect(note.reverbEffectsSend);
1486
+ note.volumeNode.connect(note.chorusEffectsSend);
1413
1487
  }
1414
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1488
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1415
1489
  }
1416
1490
  }
1417
1491
  }
@@ -1444,30 +1518,32 @@ export class MidyGM2 {
1444
1518
  }
1445
1519
  },
1446
1520
  modLfoToFilterFc: (channel, note, _prevValue) => {
1447
- if (0 < channel.state.modulationDepth)
1448
- this.setModLfoToFilterFc(note);
1521
+ if (0 < channel.state.modulationDepth) {
1522
+ this.setModLfoToFilterFc(channel, note);
1523
+ }
1449
1524
  },
1450
- modLfoToVolume: (channel, note) => {
1451
- if (0 < channel.state.modulationDepth)
1452
- this.setModLfoToVolume(note);
1525
+ modLfoToVolume: (channel, note, _prevValue) => {
1526
+ if (0 < channel.state.modulationDepth) {
1527
+ this.setModLfoToVolume(channel, note);
1528
+ }
1453
1529
  },
1454
- chorusEffectsSend: (_channel, note, prevValue) => {
1455
- this.setChorusEffectsSend(note, prevValue);
1530
+ chorusEffectsSend: (channel, note, prevValue) => {
1531
+ this.setChorusEffectsSend(channel, note, prevValue);
1456
1532
  },
1457
- reverbEffectsSend: (_channel, note, prevValue) => {
1458
- this.setReverbEffectsSend(note, prevValue);
1533
+ reverbEffectsSend: (channel, note, prevValue) => {
1534
+ this.setReverbEffectsSend(channel, note, prevValue);
1459
1535
  },
1460
1536
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1461
1537
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1462
1538
  delayVibLFO: (channel, note, prevValue) => {
1463
1539
  if (0 < channel.state.vibratoDepth) {
1464
1540
  const now = this.audioContext.currentTime;
1465
- const prevStartTime = note.startTime +
1466
- prevValue * channel.state.vibratoDelay * 2;
1541
+ const vibratoDelay = channel.state.vibratoDelay * 2;
1542
+ const prevStartTime = note.startTime + prevValue * vibratoDelay;
1467
1543
  if (now < prevStartTime)
1468
1544
  return;
1469
- const startTime = note.startTime +
1470
- value * channel.state.vibratoDelay * 2;
1545
+ const value = note.voiceParams.delayVibLFO;
1546
+ const startTime = note.startTime + value * vibratoDelay;
1471
1547
  note.vibratoLFO.stop(now);
1472
1548
  note.vibratoLFO.start(startTime);
1473
1549
  }
@@ -1475,9 +1551,10 @@ export class MidyGM2 {
1475
1551
  freqVibLFO: (channel, note, _prevValue) => {
1476
1552
  if (0 < channel.state.vibratoDepth) {
1477
1553
  const now = this.audioContext.currentTime;
1554
+ const freqVibLFO = note.voiceParams.freqVibLFO;
1478
1555
  note.vibratoLFO.frequency
1479
1556
  .cancelScheduledValues(now)
1480
- .setValueAtTime(value * sate.vibratoRate, now);
1557
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate, now);
1481
1558
  }
1482
1559
  },
1483
1560
  };
@@ -1574,8 +1651,8 @@ export class MidyGM2 {
1574
1651
  if (handler) {
1575
1652
  handler.call(this, channelNumber, value);
1576
1653
  const channel = this.channels[channelNumber];
1577
- const controller = 128 + controllerType;
1578
- this.applyVoiceParams(channel, controller);
1654
+ this.applyVoiceParams(channel, controller + 128);
1655
+ this.applyControlTable(channel, controllerType);
1579
1656
  }
1580
1657
  else {
1581
1658
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1586,13 +1663,14 @@ export class MidyGM2 {
1586
1663
  }
1587
1664
  updateModulation(channel) {
1588
1665
  const now = this.audioContext.currentTime;
1666
+ const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1589
1667
  channel.scheduledNotes.forEach((noteList) => {
1590
1668
  for (let i = 0; i < noteList.length; i++) {
1591
1669
  const note = noteList[i];
1592
1670
  if (!note)
1593
1671
  continue;
1594
1672
  if (note.modulationDepth) {
1595
- note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
1673
+ note.modulationDepth.gain.setValueAtTime(depth, now);
1596
1674
  }
1597
1675
  else {
1598
1676
  this.setPitchEnvelope(note);
@@ -1603,8 +1681,7 @@ export class MidyGM2 {
1603
1681
  }
1604
1682
  setModulationDepth(channelNumber, modulation) {
1605
1683
  const channel = this.channels[channelNumber];
1606
- channel.state.modulationDepth = (modulation / 127) *
1607
- channel.modulationDepthRange;
1684
+ channel.state.modulationDepth = modulation / 127;
1608
1685
  this.updateModulation(channel);
1609
1686
  }
1610
1687
  setPortamentoTime(channelNumber, portamentoTime) {
@@ -1612,10 +1689,27 @@ export class MidyGM2 {
1612
1689
  const factor = 5 * Math.log(10) / 127;
1613
1690
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1614
1691
  }
1692
+ setKeyBasedVolume(channel) {
1693
+ const now = this.audioContext.currentTime;
1694
+ channel.scheduledNotes.forEach((noteList) => {
1695
+ for (let i = 0; i < noteList.length; i++) {
1696
+ const note = noteList[i];
1697
+ if (!note)
1698
+ continue;
1699
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1700
+ if (keyBasedValue === 0)
1701
+ continue;
1702
+ note.volumeNode.gain
1703
+ .cancelScheduledValues(now)
1704
+ .setValueAtTime(1 + keyBasedValue, now);
1705
+ }
1706
+ });
1707
+ }
1615
1708
  setVolume(channelNumber, volume) {
1616
1709
  const channel = this.channels[channelNumber];
1617
1710
  channel.state.volume = volume / 127;
1618
1711
  this.updateChannelVolume(channel);
1712
+ this.setKeyBasedVolume(channel);
1619
1713
  }
1620
1714
  panToGain(pan) {
1621
1715
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1624,10 +1718,31 @@ export class MidyGM2 {
1624
1718
  gainRight: Math.sin(theta),
1625
1719
  };
1626
1720
  }
1721
+ setKeyBasedPan(channel) {
1722
+ const now = this.audioContext.currentTime;
1723
+ channel.scheduledNotes.forEach((noteList) => {
1724
+ for (let i = 0; i < noteList.length; i++) {
1725
+ const note = noteList[i];
1726
+ if (!note)
1727
+ continue;
1728
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1729
+ if (keyBasedValue === 0)
1730
+ continue;
1731
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1732
+ note.gainL.gain
1733
+ .cancelScheduledValues(now)
1734
+ .setValueAtTime(gainLeft, now);
1735
+ note.gainR.gain
1736
+ .cancelScheduledValues(now)
1737
+ .setValueAtTime(gainRight, now);
1738
+ }
1739
+ });
1740
+ }
1627
1741
  setPan(channelNumber, pan) {
1628
1742
  const channel = this.channels[channelNumber];
1629
1743
  channel.state.pan = pan / 127;
1630
1744
  this.updateChannelVolume(channel);
1745
+ this.setKeyBasedPan(channel);
1631
1746
  }
1632
1747
  setExpression(channelNumber, expression) {
1633
1748
  const channel = this.channels[channelNumber];
@@ -1662,6 +1777,22 @@ export class MidyGM2 {
1662
1777
  setPortamento(channelNumber, value) {
1663
1778
  this.channels[channelNumber].state.portamento = value / 127;
1664
1779
  }
1780
+ setSostenutoPedal(channelNumber, value) {
1781
+ const channel = this.channels[channelNumber];
1782
+ channel.state.sostenutoPedal = value / 127;
1783
+ if (64 <= value) {
1784
+ const now = this.audioContext.currentTime;
1785
+ const activeNotes = this.getActiveNotes(channel, now);
1786
+ channel.sostenutoNotes = new Map(activeNotes);
1787
+ }
1788
+ else {
1789
+ this.releaseSostenutoPedal(channelNumber, value);
1790
+ }
1791
+ }
1792
+ setSoftPedal(channelNumber, softPedal) {
1793
+ const channel = this.channels[channelNumber];
1794
+ channel.state.softPedal = softPedal / 127;
1795
+ }
1665
1796
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1666
1797
  const channel = this.channels[channelNumber];
1667
1798
  const state = channel.state;
@@ -1694,7 +1825,7 @@ export class MidyGM2 {
1694
1825
  const note = noteList[i];
1695
1826
  if (!note)
1696
1827
  continue;
1697
- this.setReverbEffectsSend(note, 0);
1828
+ this.setReverbEffectsSend(channel, note, 0);
1698
1829
  }
1699
1830
  });
1700
1831
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -1735,7 +1866,7 @@ export class MidyGM2 {
1735
1866
  const note = noteList[i];
1736
1867
  if (!note)
1737
1868
  continue;
1738
- this.setChorusEffectsSend(note, 0);
1869
+ this.setChorusEffectsSend(channel, note, 0);
1739
1870
  }
1740
1871
  });
1741
1872
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -1744,22 +1875,6 @@ export class MidyGM2 {
1744
1875
  }
1745
1876
  }
1746
1877
  }
1747
- setSostenutoPedal(channelNumber, value) {
1748
- const channel = this.channels[channelNumber];
1749
- channel.state.sostenutoPedal = value / 127;
1750
- if (64 <= value) {
1751
- const now = this.audioContext.currentTime;
1752
- const activeNotes = this.getActiveNotes(channel, now);
1753
- channel.sostenutoNotes = new Map(activeNotes);
1754
- }
1755
- else {
1756
- this.releaseSostenutoPedal(channelNumber, value);
1757
- }
1758
- }
1759
- setSoftPedal(channelNumber, softPedal) {
1760
- const channel = this.channels[channelNumber];
1761
- channel.state.softPedal = softPedal / 127;
1762
- }
1763
1878
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1764
1879
  if (maxLSB < channel.dataLSB) {
1765
1880
  channel.dataMSB++;
@@ -1869,7 +1984,6 @@ export class MidyGM2 {
1869
1984
  setModulationDepthRange(channelNumber, modulationDepthRange) {
1870
1985
  const channel = this.channels[channelNumber];
1871
1986
  channel.modulationDepthRange = modulationDepthRange;
1872
- channel.modulationDepth = (modulation / 127) * modulationDepthRange;
1873
1987
  this.updateModulation(channel);
1874
1988
  }
1875
1989
  allSoundOff(channelNumber) {
@@ -1922,7 +2036,7 @@ export class MidyGM2 {
1922
2036
  switch (data[3]) {
1923
2037
  case 8:
1924
2038
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
1925
- return this.handleScaleOctaveTuning1ByteFormat(data);
2039
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
1926
2040
  default:
1927
2041
  console.warn(`Unsupported Exclusive Message: ${data}`);
1928
2042
  }
@@ -1975,7 +2089,7 @@ export class MidyGM2 {
1975
2089
  return this.handleMasterFineTuningSysEx(data);
1976
2090
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
1977
2091
  return this.handleMasterCoarseTuningSysEx(data);
1978
- case 5:
2092
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
1979
2093
  return this.handleGlobalParameterControlSysEx(data);
1980
2094
  default:
1981
2095
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -1983,21 +2097,18 @@ export class MidyGM2 {
1983
2097
  break;
1984
2098
  case 9:
1985
2099
  switch (data[3]) {
1986
- // case 1:
1987
- // // TODO
1988
- // return this.setChannelPressure();
1989
- // case 3:
1990
- // // TODO
1991
- // return this.setControlChange();
2100
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2101
+ return this.handleChannelPressureSysEx(data);
2102
+ case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2103
+ return this.handleControlChangeSysEx(data);
1992
2104
  default:
1993
2105
  console.warn(`Unsupported Exclusive Message: ${data}`);
1994
2106
  }
1995
2107
  break;
1996
2108
  case 10:
1997
2109
  switch (data[3]) {
1998
- // case 1:
1999
- // // TODO
2000
- // return this.handleKeyBasedInstrumentControl();
2110
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2111
+ return this.handleKeyBasedInstrumentControlSysEx(data);
2001
2112
  default:
2002
2113
  console.warn(`Unsupported Exclusive Message: ${data}`);
2003
2114
  }
@@ -2016,8 +2127,8 @@ export class MidyGM2 {
2016
2127
  }
2017
2128
  else {
2018
2129
  const now = this.audioContext.currentTime;
2019
- this.masterGain.gain.cancelScheduledValues(now);
2020
- this.masterGain.gain.setValueAtTime(volume * volume, now);
2130
+ this.masterVolume.gain.cancelScheduledValues(now);
2131
+ this.masterVolume.gain.setValueAtTime(volume * volume, now);
2021
2132
  }
2022
2133
  }
2023
2134
  handleMasterFineTuningSysEx(data) {
@@ -2042,40 +2153,6 @@ export class MidyGM2 {
2042
2153
  channel.detune += next - prev;
2043
2154
  this.updateDetune(channel);
2044
2155
  }
2045
- getChannelBitmap(data) {
2046
- const bitmap = new Array(16).fill(false);
2047
- const ff = data[4] & 0b11;
2048
- const gg = data[5] & 0x7F;
2049
- const hh = data[6] & 0x7F;
2050
- for (let bit = 0; bit < 7; bit++) {
2051
- if (hh & (1 << bit))
2052
- bitmap[bit] = true;
2053
- }
2054
- for (let bit = 0; bit < 7; bit++) {
2055
- if (gg & (1 << bit))
2056
- bitmap[bit + 7] = true;
2057
- }
2058
- for (let bit = 0; bit < 2; bit++) {
2059
- if (ff & (1 << bit))
2060
- bitmap[bit + 14] = true;
2061
- }
2062
- return bitmap;
2063
- }
2064
- handleScaleOctaveTuning1ByteFormat(data) {
2065
- if (data.length < 18) {
2066
- console.error("Data length is too short");
2067
- return;
2068
- }
2069
- const channelBitmap = this.getChannelBitmap(data);
2070
- for (let i = 0; i < channelBitmap.length; i++) {
2071
- if (!channelBitmap[i])
2072
- continue;
2073
- for (let j = 0; j < 12; j++) {
2074
- const value = data[j + 7] - 64; // cent
2075
- this.channels[i].scaleOctaveTuningTable[j] = value;
2076
- }
2077
- }
2078
- }
2079
2156
  handleGlobalParameterControlSysEx(data) {
2080
2157
  if (data[7] === 1) {
2081
2158
  switch (data[8]) {
@@ -2262,6 +2339,122 @@ export class MidyGM2 {
2262
2339
  getChorusSendToReverb(value) {
2263
2340
  return value * 0.00787;
2264
2341
  }
2342
+ getChannelBitmap(data) {
2343
+ const bitmap = new Array(16).fill(false);
2344
+ const ff = data[4] & 0b11;
2345
+ const gg = data[5] & 0x7F;
2346
+ const hh = data[6] & 0x7F;
2347
+ for (let bit = 0; bit < 7; bit++) {
2348
+ if (hh & (1 << bit))
2349
+ bitmap[bit] = true;
2350
+ }
2351
+ for (let bit = 0; bit < 7; bit++) {
2352
+ if (gg & (1 << bit))
2353
+ bitmap[bit + 7] = true;
2354
+ }
2355
+ for (let bit = 0; bit < 2; bit++) {
2356
+ if (ff & (1 << bit))
2357
+ bitmap[bit + 14] = true;
2358
+ }
2359
+ return bitmap;
2360
+ }
2361
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2362
+ if (data.length < 18) {
2363
+ console.error("Data length is too short");
2364
+ return;
2365
+ }
2366
+ const channelBitmap = this.getChannelBitmap(data);
2367
+ for (let i = 0; i < channelBitmap.length; i++) {
2368
+ if (!channelBitmap[i])
2369
+ continue;
2370
+ for (let j = 0; j < 12; j++) {
2371
+ const value = data[j + 7] - 64; // cent
2372
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2373
+ }
2374
+ }
2375
+ }
2376
+ applyDestinationSettings(channel, note, table) {
2377
+ if (table[0] !== 64) {
2378
+ this.updateDetune(channel);
2379
+ }
2380
+ if (!note.portamento) {
2381
+ if (table[1] !== 64) {
2382
+ this.setFilterEnvelope(channel, note);
2383
+ }
2384
+ if (table[2] !== 64) {
2385
+ this.setVolumeEnvelope(channel, note);
2386
+ }
2387
+ }
2388
+ if (table[3] !== 0) {
2389
+ this.setModLfoToPitch(channel, note);
2390
+ }
2391
+ if (table[4] !== 0) {
2392
+ this.setModLfoToFilterFc(channel, note);
2393
+ }
2394
+ if (table[5] !== 0) {
2395
+ this.setModLfoToVolume(channel, note);
2396
+ }
2397
+ }
2398
+ handleChannelPressureSysEx(data) {
2399
+ const channelNumber = data[4];
2400
+ const table = this.channels[channelNumber].pressureTable;
2401
+ for (let i = 5; i < data.length - 1; i += 2) {
2402
+ const pp = data[i];
2403
+ const rr = data[i + 1];
2404
+ table[pp] = rr;
2405
+ }
2406
+ }
2407
+ initControlTable() {
2408
+ const channelCount = 128;
2409
+ const slotSize = 6;
2410
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2411
+ const table = new Uint8Array(channelCount * slotSize);
2412
+ for (let ch = 0; ch < channelCount; ch++) {
2413
+ const offset = ch * slotSize;
2414
+ table.set(defaultValues, offset);
2415
+ }
2416
+ return table;
2417
+ }
2418
+ applyControlTable(channel, controllerType) {
2419
+ const slotSize = 6;
2420
+ const offset = controllerType * slotSize;
2421
+ const table = channel.controlTable.subarray(offset, offset + slotSize);
2422
+ channel.scheduledNotes.forEach((noteList) => {
2423
+ for (let i = 0; i < noteList.length; i++) {
2424
+ const note = noteList[i];
2425
+ if (!note)
2426
+ continue;
2427
+ this.applyDestinationSettings(channel, note, table);
2428
+ }
2429
+ });
2430
+ }
2431
+ handleControlChangeSysEx(data) {
2432
+ const channelNumber = data[4];
2433
+ const controllerType = data[5];
2434
+ const table = this.channels[channelNumber].controlTable[controllerType];
2435
+ for (let i = 6; i < data.length - 1; i += 2) {
2436
+ const pp = data[i];
2437
+ const rr = data[i + 1];
2438
+ table[pp] = rr;
2439
+ }
2440
+ }
2441
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2442
+ const index = keyNumber * 128 + controllerType;
2443
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2444
+ return (controlValue + 64) / 64;
2445
+ }
2446
+ handleKeyBasedInstrumentControlSysEx(data) {
2447
+ const channelNumber = data[4];
2448
+ const keyNumber = data[5];
2449
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2450
+ for (let i = 6; i < data.length - 1; i += 2) {
2451
+ const controllerType = data[i];
2452
+ const value = data[i + 1];
2453
+ const index = keyNumber * 128 + controllerType;
2454
+ table[index] = value - 64;
2455
+ }
2456
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2457
+ }
2265
2458
  handleExclusiveMessage(data) {
2266
2459
  console.warn(`Unsupported Exclusive Message: ${data}`);
2267
2460
  }
@@ -2295,6 +2488,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2295
2488
  currentBufferSource: null,
2296
2489
  detune: 0,
2297
2490
  scaleOctaveTuningTable: new Array(12).fill(0), // cent
2491
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2492
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2298
2493
  program: 0,
2299
2494
  bank: 121 * 128,
2300
2495
  bankMSB: 121,