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