@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/script/midy.js CHANGED
@@ -17,12 +17,30 @@ class Note {
17
17
  writable: true,
18
18
  value: void 0
19
19
  });
20
+ Object.defineProperty(this, "volumeEnvelopeNode", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
20
26
  Object.defineProperty(this, "volumeNode", {
21
27
  enumerable: true,
22
28
  configurable: true,
23
29
  writable: true,
24
30
  value: void 0
25
31
  });
32
+ Object.defineProperty(this, "gainL", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ Object.defineProperty(this, "gainR", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: void 0
43
+ });
26
44
  Object.defineProperty(this, "volumeDepth", {
27
45
  enumerable: true,
28
46
  configurable: true,
@@ -344,15 +362,15 @@ class Midy {
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 Midy {
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 Midy {
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(),
@@ -923,7 +942,9 @@ class Midy {
923
942
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
924
943
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
925
944
  const pitch = pitchWheel * pitchWheelSensitivity;
926
- return tuning + pitch;
945
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
946
+ const pressure = pressureDepth * channel.state.channelPressure;
947
+ return tuning + pitch + pressure;
927
948
  }
928
949
  calcNoteDetune(channel, note) {
929
950
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -943,14 +964,19 @@ class Midy {
943
964
  }
944
965
  });
945
966
  }
967
+ getPortamentoTime(channel) {
968
+ const factor = 5 * Math.log(10) / 127;
969
+ const time = channel.state.portamentoTime;
970
+ return Math.log(time) / factor;
971
+ }
946
972
  setPortamentoStartVolumeEnvelope(channel, note) {
947
973
  const now = this.audioContext.currentTime;
948
974
  const { voiceParams, startTime } = note;
949
975
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
950
976
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
951
977
  const volDelay = startTime + voiceParams.volDelay;
952
- const portamentoTime = volDelay + channel.state.portamentoTime;
953
- note.volumeNode.gain
978
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
979
+ note.volumeEnvelopeNode.gain
954
980
  .cancelScheduledValues(now)
955
981
  .setValueAtTime(0, volDelay)
956
982
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
@@ -959,13 +985,16 @@ class Midy {
959
985
  const now = this.audioContext.currentTime;
960
986
  const state = channel.state;
961
987
  const { voiceParams, startTime } = note;
962
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
988
+ const pressureDepth = channel.pressureTable[2] / 64;
989
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
990
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
991
+ pressure;
963
992
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
964
993
  const volDelay = startTime + voiceParams.volDelay;
965
994
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
966
995
  const volHold = volAttack + voiceParams.volHold;
967
996
  const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
968
- note.volumeNode.gain
997
+ note.volumeEnvelopeNode.gain
969
998
  .cancelScheduledValues(now)
970
999
  .setValueAtTime(0, startTime)
971
1000
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -1007,14 +1036,17 @@ class Midy {
1007
1036
  const { voiceParams, noteNumber, startTime } = note;
1008
1037
  const softPedalFactor = 1 -
1009
1038
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1010
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1011
- softPedalFactor * state.brightness * 2;
1039
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1040
+ const pressure = pressureDepth * channel.state.channelPressure;
1041
+ const baseCent = voiceParams.initialFilterFc + pressure;
1042
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1043
+ state.brightness * 2;
1012
1044
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1013
1045
  const sustainFreq = baseFreq +
1014
1046
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1015
1047
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1016
1048
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1017
- const portamentoTime = startTime + channel.state.portamentoTime;
1049
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
1018
1050
  const modDelay = startTime + voiceParams.modDelay;
1019
1051
  note.filterNode.frequency
1020
1052
  .cancelScheduledValues(now)
@@ -1059,14 +1091,14 @@ class Midy {
1059
1091
  note.modulationDepth = new GainNode(this.audioContext);
1060
1092
  this.setModLfoToPitch(channel, note);
1061
1093
  note.volumeDepth = new GainNode(this.audioContext);
1062
- this.setModLfoToVolume(note);
1094
+ this.setModLfoToVolume(channel, note);
1063
1095
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1064
1096
  note.modulationLFO.connect(note.filterDepth);
1065
1097
  note.filterDepth.connect(note.filterNode.frequency);
1066
1098
  note.modulationLFO.connect(note.modulationDepth);
1067
1099
  note.modulationDepth.connect(note.bufferSource.detune);
1068
1100
  note.modulationLFO.connect(note.volumeDepth);
1069
- note.volumeDepth.connect(note.volumeNode.gain);
1101
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1070
1102
  }
1071
1103
  startVibrato(channel, note, startTime) {
1072
1104
  const { voiceParams } = note;
@@ -1088,6 +1120,9 @@ class Midy {
1088
1120
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1089
1121
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1090
1122
  note.volumeNode = new GainNode(this.audioContext);
1123
+ note.gainL = new GainNode(this.audioContext);
1124
+ note.gainR = new GainNode(this.audioContext);
1125
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1091
1126
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1092
1127
  type: "lowpass",
1093
1128
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
@@ -1114,7 +1149,10 @@ class Midy {
1114
1149
  channel.currentBufferSource = note.bufferSource;
1115
1150
  }
1116
1151
  note.bufferSource.connect(note.filterNode);
1117
- note.filterNode.connect(note.volumeNode);
1152
+ note.filterNode.connect(note.volumeEnvelopeNode);
1153
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1154
+ note.volumeNode.connect(note.gainL);
1155
+ note.volumeNode.connect(note.gainR);
1118
1156
  if (0 < channel.chorusSendLevel) {
1119
1157
  this.setChorusEffectsSend(channel, note, 0);
1120
1158
  }
@@ -1145,8 +1183,8 @@ class Midy {
1145
1183
  if (!voice)
1146
1184
  return;
1147
1185
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1148
- note.volumeNode.connect(channel.gainL);
1149
- note.volumeNode.connect(channel.gainR);
1186
+ note.gainL.connect(channel.gainL);
1187
+ note.gainR.connect(channel.gainR);
1150
1188
  if (channel.state.sostenutoPedal) {
1151
1189
  channel.sostenutoNotes.set(noteNumber, note);
1152
1190
  }
@@ -1177,7 +1215,7 @@ class Midy {
1177
1215
  }
1178
1216
  stopNote(endTime, stopTime, scheduledNotes, index) {
1179
1217
  const note = scheduledNotes[index];
1180
- note.volumeNode.gain
1218
+ note.volumeEnvelopeNode.gain
1181
1219
  .cancelScheduledValues(endTime)
1182
1220
  .linearRampToValueAtTime(0, stopTime);
1183
1221
  note.ending = true;
@@ -1188,8 +1226,11 @@ class Midy {
1188
1226
  note.bufferSource.onended = () => {
1189
1227
  scheduledNotes[index] = null;
1190
1228
  note.bufferSource.disconnect();
1191
- note.volumeNode.disconnect();
1192
1229
  note.filterNode.disconnect();
1230
+ note.volumeEnvelopeNode.disconnect();
1231
+ note.volumeNode.disconnect();
1232
+ note.gainL.disconnect();
1233
+ note.gainR.disconnect();
1193
1234
  if (note.modulationDepth) {
1194
1235
  note.volumeDepth.disconnect();
1195
1236
  note.modulationDepth.disconnect();
@@ -1239,7 +1280,7 @@ class Midy {
1239
1280
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1240
1281
  }
1241
1282
  else {
1242
- const portamentoTime = endTime + state.portamentoTime;
1283
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1243
1284
  const deltaNote = portamentoNoteNumber - noteNumber;
1244
1285
  const baseRate = note.voiceParams.playbackRate;
1245
1286
  const targetRate = baseRate * Math.pow(2, deltaNote / 12);
@@ -1314,7 +1355,7 @@ class Midy {
1314
1355
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1315
1356
  if (activeNotes.has(noteNumber)) {
1316
1357
  const activeNote = activeNotes.get(noteNumber);
1317
- const gain = activeNote.volumeNode.gain.value;
1358
+ const gain = activeNote.gainL.gain.value;
1318
1359
  activeNote.volumeNode.gain
1319
1360
  .cancelScheduledValues(now)
1320
1361
  .setValueAtTime(gain * pressure, now);
@@ -1327,20 +1368,24 @@ class Midy {
1327
1368
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1328
1369
  channel.program = program;
1329
1370
  }
1330
- handleChannelPressure(channelNumber, pressure) {
1331
- const now = this.audioContext.currentTime;
1371
+ handleChannelPressure(channelNumber, value) {
1332
1372
  const channel = this.channels[channelNumber];
1333
- pressure /= 64;
1334
- channel.channelPressure = pressure;
1335
- const activeNotes = this.getActiveNotes(channel, now);
1336
- if (channel.channelPressure.amplitudeControl !== 1) {
1337
- activeNotes.forEach((activeNote) => {
1338
- const gain = activeNote.volumeNode.gain.value;
1339
- activeNote.volumeNode.gain
1340
- .cancelScheduledValues(now)
1341
- .setValueAtTime(gain * pressure, now);
1342
- });
1343
- }
1373
+ const prev = channel.state.channelPressure;
1374
+ const next = value / 127;
1375
+ channel.state.channelPressure = next;
1376
+ if (channel.pressureTable[0] !== 64) {
1377
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1378
+ channel.detune += pressureDepth * (next - prev);
1379
+ }
1380
+ const table = channel.pressureTable;
1381
+ channel.scheduledNotes.forEach((noteList) => {
1382
+ for (let i = 0; i < noteList.length; i++) {
1383
+ const note = noteList[i];
1384
+ if (!note)
1385
+ continue;
1386
+ this.applyDestinationSettings(channel, note, table);
1387
+ }
1388
+ });
1344
1389
  // this.applyVoiceParams(channel, 13);
1345
1390
  }
1346
1391
  handlePitchBendMessage(channelNumber, lsb, msb) {
@@ -1359,13 +1404,15 @@ class Midy {
1359
1404
  }
1360
1405
  setModLfoToPitch(channel, note) {
1361
1406
  const now = this.audioContext.currentTime;
1362
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1363
- const modulationDepth = Math.abs(modLfoToPitch) +
1407
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1408
+ const pressure = pressureDepth * channel.state.channelPressure;
1409
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1410
+ const baseDepth = Math.abs(modLfoToPitch) +
1364
1411
  channel.state.modulationDepth;
1365
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1412
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1366
1413
  note.modulationDepth.gain
1367
1414
  .cancelScheduledValues(now)
1368
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1415
+ .setValueAtTime(modulationDepth, now);
1369
1416
  }
1370
1417
  setVibLfoToPitch(channel, note) {
1371
1418
  const now = this.audioContext.currentTime;
@@ -1377,69 +1424,75 @@ class Midy {
1377
1424
  .cancelScheduledValues(now)
1378
1425
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1379
1426
  }
1380
- setModLfoToFilterFc(note) {
1427
+ setModLfoToFilterFc(channel, note) {
1381
1428
  const now = this.audioContext.currentTime;
1382
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1429
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1430
+ const pressure = pressureDepth * channel.state.channelPressure;
1431
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1383
1432
  note.filterDepth.gain
1384
1433
  .cancelScheduledValues(now)
1385
1434
  .setValueAtTime(modLfoToFilterFc, now);
1386
1435
  }
1387
- setModLfoToVolume(note) {
1436
+ setModLfoToVolume(channel, note) {
1388
1437
  const now = this.audioContext.currentTime;
1389
1438
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1390
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1391
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1439
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1440
+ const pressureDepth = channel.pressureTable[5] / 127;
1441
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1442
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1392
1443
  note.volumeDepth.gain
1393
1444
  .cancelScheduledValues(now)
1394
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1445
+ .setValueAtTime(volumeDepth, now);
1395
1446
  }
1396
- setChorusEffectsSend(note, prevValue) {
1447
+ setReverbEffectsSend(channel, note, prevValue) {
1397
1448
  if (0 < prevValue) {
1398
- if (0 < note.voiceParams.chorusEffectsSend) {
1449
+ if (0 < note.voiceParams.reverbEffectsSend) {
1399
1450
  const now = this.audioContext.currentTime;
1400
- const value = note.voiceParams.chorusEffectsSend;
1401
- note.chorusEffectsSend.gain
1451
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1452
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1453
+ note.reverbEffectsSend.gain
1402
1454
  .cancelScheduledValues(now)
1403
1455
  .setValueAtTime(value, now);
1404
1456
  }
1405
1457
  else {
1406
- note.chorusEffectsSend.disconnect();
1458
+ note.reverbEffectsSend.disconnect();
1407
1459
  }
1408
1460
  }
1409
1461
  else {
1410
- if (0 < note.voiceParams.chorusEffectsSend) {
1411
- if (!note.chorusEffectsSend) {
1412
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1413
- gain: note.voiceParams.chorusEffectsSend,
1462
+ if (0 < note.voiceParams.reverbEffectsSend) {
1463
+ if (!note.reverbEffectsSend) {
1464
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1465
+ gain: note.voiceParams.reverbEffectsSend,
1414
1466
  });
1415
- note.volumeNode.connect(note.chorusEffectsSend);
1467
+ note.volumeNode.connect(note.reverbEffectsSend);
1416
1468
  }
1417
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1469
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1418
1470
  }
1419
1471
  }
1420
1472
  }
1421
- setReverbEffectsSend(note, prevValue) {
1473
+ setChorusEffectsSend(channel, note, prevValue) {
1422
1474
  if (0 < prevValue) {
1423
- if (0 < note.voiceParams.reverbEffectsSend) {
1475
+ if (0 < note.voiceParams.chorusEffectsSend) {
1424
1476
  const now = this.audioContext.currentTime;
1425
- const value = note.voiceParams.reverbEffectsSend;
1426
- note.reverbEffectsSend.gain
1477
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1478
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1479
+ note.chorusEffectsSend.gain
1427
1480
  .cancelScheduledValues(now)
1428
1481
  .setValueAtTime(value, now);
1429
1482
  }
1430
1483
  else {
1431
- note.reverbEffectsSend.disconnect();
1484
+ note.chorusEffectsSend.disconnect();
1432
1485
  }
1433
1486
  }
1434
1487
  else {
1435
- if (0 < note.voiceParams.reverbEffectsSend) {
1436
- if (!note.reverbEffectsSend) {
1437
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1438
- gain: note.voiceParams.reverbEffectsSend,
1488
+ if (0 < note.voiceParams.chorusEffectsSend) {
1489
+ if (!note.chorusEffectsSend) {
1490
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1491
+ gain: note.voiceParams.chorusEffectsSend,
1439
1492
  });
1440
- note.volumeNode.connect(note.reverbEffectsSend);
1493
+ note.volumeNode.connect(note.chorusEffectsSend);
1441
1494
  }
1442
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1495
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1443
1496
  }
1444
1497
  }
1445
1498
  }
@@ -1472,30 +1525,32 @@ class Midy {
1472
1525
  }
1473
1526
  },
1474
1527
  modLfoToFilterFc: (channel, note, _prevValue) => {
1475
- if (0 < channel.state.modulationDepth)
1476
- this.setModLfoToFilterFc(note);
1528
+ if (0 < channel.state.modulationDepth) {
1529
+ this.setModLfoToFilterFc(channel, note);
1530
+ }
1477
1531
  },
1478
- modLfoToVolume: (channel, note) => {
1479
- if (0 < channel.state.modulationDepth)
1480
- this.setModLfoToVolume(note);
1532
+ modLfoToVolume: (channel, note, _prevValue) => {
1533
+ if (0 < channel.state.modulationDepth) {
1534
+ this.setModLfoToVolume(channel, note);
1535
+ }
1481
1536
  },
1482
- chorusEffectsSend: (_channel, note, prevValue) => {
1483
- this.setChorusEffectsSend(note, prevValue);
1537
+ chorusEffectsSend: (channel, note, prevValue) => {
1538
+ this.setChorusEffectsSend(channel, note, prevValue);
1484
1539
  },
1485
- reverbEffectsSend: (_channel, note, prevValue) => {
1486
- this.setReverbEffectsSend(note, prevValue);
1540
+ reverbEffectsSend: (channel, note, prevValue) => {
1541
+ this.setReverbEffectsSend(channel, note, prevValue);
1487
1542
  },
1488
1543
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1489
1544
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1490
1545
  delayVibLFO: (channel, note, prevValue) => {
1491
1546
  if (0 < channel.state.vibratoDepth) {
1492
1547
  const now = this.audioContext.currentTime;
1493
- const prevStartTime = note.startTime +
1494
- prevValue * channel.state.vibratoDelay * 2;
1548
+ const vibratoDelay = channel.state.vibratoDelay * 2;
1549
+ const prevStartTime = note.startTime + prevValue * vibratoDelay;
1495
1550
  if (now < prevStartTime)
1496
1551
  return;
1497
- const startTime = note.startTime +
1498
- value * channel.state.vibratoDelay * 2;
1552
+ const value = note.voiceParams.delayVibLFO;
1553
+ const startTime = note.startTime + value * vibratoDelay;
1499
1554
  note.vibratoLFO.stop(now);
1500
1555
  note.vibratoLFO.start(startTime);
1501
1556
  }
@@ -1503,9 +1558,10 @@ class Midy {
1503
1558
  freqVibLFO: (channel, note, _prevValue) => {
1504
1559
  if (0 < channel.state.vibratoDepth) {
1505
1560
  const now = this.audioContext.currentTime;
1561
+ const freqVibLFO = note.voiceParams.freqVibLFO;
1506
1562
  note.vibratoLFO.frequency
1507
1563
  .cancelScheduledValues(now)
1508
- .setValueAtTime(value * sate.vibratoRate, now);
1564
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate, now);
1509
1565
  }
1510
1566
  },
1511
1567
  };
@@ -1612,8 +1668,8 @@ class Midy {
1612
1668
  if (handler) {
1613
1669
  handler.call(this, channelNumber, value);
1614
1670
  const channel = this.channels[channelNumber];
1615
- const controller = 128 + controllerType;
1616
- this.applyVoiceParams(channel, controller);
1671
+ this.applyVoiceParams(channel, controllerType + 128);
1672
+ this.applyControlTable(channel, controllerType);
1617
1673
  }
1618
1674
  else {
1619
1675
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1624,13 +1680,14 @@ class Midy {
1624
1680
  }
1625
1681
  updateModulation(channel) {
1626
1682
  const now = this.audioContext.currentTime;
1683
+ const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1627
1684
  channel.scheduledNotes.forEach((noteList) => {
1628
1685
  for (let i = 0; i < noteList.length; i++) {
1629
1686
  const note = noteList[i];
1630
1687
  if (!note)
1631
1688
  continue;
1632
1689
  if (note.modulationDepth) {
1633
- note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
1690
+ note.modulationDepth.gain.setValueAtTime(depth, now);
1634
1691
  }
1635
1692
  else {
1636
1693
  this.setPitchEnvelope(note);
@@ -1641,8 +1698,7 @@ class Midy {
1641
1698
  }
1642
1699
  setModulationDepth(channelNumber, modulation) {
1643
1700
  const channel = this.channels[channelNumber];
1644
- channel.state.modulationDepth = (modulation / 127) *
1645
- channel.modulationDepthRange;
1701
+ channel.state.modulationDepth = modulation / 127;
1646
1702
  this.updateModulation(channel);
1647
1703
  }
1648
1704
  setPortamentoTime(channelNumber, portamentoTime) {
@@ -1650,10 +1706,27 @@ class Midy {
1650
1706
  const factor = 5 * Math.log(10) / 127;
1651
1707
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1652
1708
  }
1709
+ setKeyBasedVolume(channel) {
1710
+ const now = this.audioContext.currentTime;
1711
+ channel.scheduledNotes.forEach((noteList) => {
1712
+ for (let i = 0; i < noteList.length; i++) {
1713
+ const note = noteList[i];
1714
+ if (!note)
1715
+ continue;
1716
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1717
+ if (keyBasedValue === 0)
1718
+ continue;
1719
+ note.volumeNode.gain
1720
+ .cancelScheduledValues(now)
1721
+ .setValueAtTime(1 + keyBasedValue, now);
1722
+ }
1723
+ });
1724
+ }
1653
1725
  setVolume(channelNumber, volume) {
1654
1726
  const channel = this.channels[channelNumber];
1655
1727
  channel.state.volume = volume / 127;
1656
1728
  this.updateChannelVolume(channel);
1729
+ this.setKeyBasedVolume(channel);
1657
1730
  }
1658
1731
  panToGain(pan) {
1659
1732
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1662,10 +1735,31 @@ class Midy {
1662
1735
  gainRight: Math.sin(theta),
1663
1736
  };
1664
1737
  }
1738
+ setKeyBasedPan(channel) {
1739
+ const now = this.audioContext.currentTime;
1740
+ channel.scheduledNotes.forEach((noteList) => {
1741
+ for (let i = 0; i < noteList.length; i++) {
1742
+ const note = noteList[i];
1743
+ if (!note)
1744
+ continue;
1745
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1746
+ if (keyBasedValue === 0)
1747
+ continue;
1748
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1749
+ note.gainL.gain
1750
+ .cancelScheduledValues(now)
1751
+ .setValueAtTime(gainLeft, now);
1752
+ note.gainR.gain
1753
+ .cancelScheduledValues(now)
1754
+ .setValueAtTime(gainRight, now);
1755
+ }
1756
+ });
1757
+ }
1665
1758
  setPan(channelNumber, pan) {
1666
1759
  const channel = this.channels[channelNumber];
1667
1760
  channel.state.pan = pan / 127;
1668
1761
  this.updateChannelVolume(channel);
1762
+ this.setKeyBasedPan(channel);
1669
1763
  }
1670
1764
  setExpression(channelNumber, expression) {
1671
1765
  const channel = this.channels[channelNumber];
@@ -1827,7 +1921,7 @@ class Midy {
1827
1921
  const note = noteList[i];
1828
1922
  if (!note)
1829
1923
  continue;
1830
- this.setReverbEffectsSend(note, 0);
1924
+ this.setReverbEffectsSend(channel, note, 0);
1831
1925
  }
1832
1926
  });
1833
1927
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -1868,7 +1962,7 @@ class Midy {
1868
1962
  const note = noteList[i];
1869
1963
  if (!note)
1870
1964
  continue;
1871
- this.setChorusEffectsSend(note, 0);
1965
+ this.setChorusEffectsSend(channel, note, 0);
1872
1966
  }
1873
1967
  });
1874
1968
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -1998,7 +2092,6 @@ class Midy {
1998
2092
  setModulationDepthRange(channelNumber, modulationDepthRange) {
1999
2093
  const channel = this.channels[channelNumber];
2000
2094
  channel.modulationDepthRange = modulationDepthRange;
2001
- channel.modulationDepth = (modulation / 127) * modulationDepthRange;
2002
2095
  this.updateModulation(channel);
2003
2096
  }
2004
2097
  allSoundOff(channelNumber) {
@@ -2051,7 +2144,7 @@ class Midy {
2051
2144
  switch (data[3]) {
2052
2145
  case 8:
2053
2146
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2054
- return this.handleScaleOctaveTuning1ByteFormat(data);
2147
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2055
2148
  default:
2056
2149
  console.warn(`Unsupported Exclusive Message: ${data}`);
2057
2150
  }
@@ -2104,7 +2197,7 @@ class Midy {
2104
2197
  return this.handleMasterFineTuningSysEx(data);
2105
2198
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2106
2199
  return this.handleMasterCoarseTuningSysEx(data);
2107
- case 5:
2200
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2108
2201
  return this.handleGlobalParameterControlSysEx(data);
2109
2202
  default:
2110
2203
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2112,31 +2205,27 @@ class Midy {
2112
2205
  break;
2113
2206
  case 8:
2114
2207
  switch (data[3]) {
2115
- case 8:
2116
- // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2208
+ case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2117
2209
  // TODO: realtime
2118
- return this.handleScaleOctaveTuning1ByteFormat(data);
2210
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2119
2211
  default:
2120
2212
  console.warn(`Unsupported Exclusive Message: ${data}`);
2121
2213
  }
2122
2214
  break;
2123
2215
  case 9:
2124
2216
  switch (data[3]) {
2125
- // case 1:
2126
- // // TODO
2127
- // return this.setChannelPressure();
2128
- // case 3:
2129
- // // TODO
2130
- // return this.setControlChange();
2217
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2218
+ return this.handleChannelPressureSysEx(data);
2219
+ case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2220
+ return this.handleControlChangeSysEx(data);
2131
2221
  default:
2132
2222
  console.warn(`Unsupported Exclusive Message: ${data}`);
2133
2223
  }
2134
2224
  break;
2135
2225
  case 10:
2136
2226
  switch (data[3]) {
2137
- // case 1:
2138
- // // TODO
2139
- // return this.handleKeyBasedInstrumentControl();
2227
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2228
+ return this.handleKeyBasedInstrumentControlSysEx(data);
2140
2229
  default:
2141
2230
  console.warn(`Unsupported Exclusive Message: ${data}`);
2142
2231
  }
@@ -2155,8 +2244,8 @@ class Midy {
2155
2244
  }
2156
2245
  else {
2157
2246
  const now = this.audioContext.currentTime;
2158
- this.masterGain.gain.cancelScheduledValues(now);
2159
- this.masterGain.gain.setValueAtTime(volume * volume, now);
2247
+ this.masterVolume.gain.cancelScheduledValues(now);
2248
+ this.masterVolume.gain.setValueAtTime(volume * volume, now);
2160
2249
  }
2161
2250
  }
2162
2251
  handleMasterFineTuningSysEx(data) {
@@ -2181,40 +2270,6 @@ class Midy {
2181
2270
  channel.detune += next - prev;
2182
2271
  this.updateDetune(channel);
2183
2272
  }
2184
- getChannelBitmap(data) {
2185
- const bitmap = new Array(16).fill(false);
2186
- const ff = data[4] & 0b11;
2187
- const gg = data[5] & 0x7F;
2188
- const hh = data[6] & 0x7F;
2189
- for (let bit = 0; bit < 7; bit++) {
2190
- if (hh & (1 << bit))
2191
- bitmap[bit] = true;
2192
- }
2193
- for (let bit = 0; bit < 7; bit++) {
2194
- if (gg & (1 << bit))
2195
- bitmap[bit + 7] = true;
2196
- }
2197
- for (let bit = 0; bit < 2; bit++) {
2198
- if (ff & (1 << bit))
2199
- bitmap[bit + 14] = true;
2200
- }
2201
- return bitmap;
2202
- }
2203
- handleScaleOctaveTuning1ByteFormat(data) {
2204
- if (data.length < 18) {
2205
- console.error("Data length is too short");
2206
- return;
2207
- }
2208
- const channelBitmap = this.getChannelBitmap(data);
2209
- for (let i = 0; i < channelBitmap.length; i++) {
2210
- if (!channelBitmap[i])
2211
- continue;
2212
- for (let j = 0; j < 12; j++) {
2213
- const value = data[j + 7] - 64; // cent
2214
- this.channels[i].scaleOctaveTuningTable[j] = value;
2215
- }
2216
- }
2217
- }
2218
2273
  handleGlobalParameterControlSysEx(data) {
2219
2274
  if (data[7] === 1) {
2220
2275
  switch (data[8]) {
@@ -2401,6 +2456,122 @@ class Midy {
2401
2456
  getChorusSendToReverb(value) {
2402
2457
  return value * 0.00787;
2403
2458
  }
2459
+ getChannelBitmap(data) {
2460
+ const bitmap = new Array(16).fill(false);
2461
+ const ff = data[4] & 0b11;
2462
+ const gg = data[5] & 0x7F;
2463
+ const hh = data[6] & 0x7F;
2464
+ for (let bit = 0; bit < 7; bit++) {
2465
+ if (hh & (1 << bit))
2466
+ bitmap[bit] = true;
2467
+ }
2468
+ for (let bit = 0; bit < 7; bit++) {
2469
+ if (gg & (1 << bit))
2470
+ bitmap[bit + 7] = true;
2471
+ }
2472
+ for (let bit = 0; bit < 2; bit++) {
2473
+ if (ff & (1 << bit))
2474
+ bitmap[bit + 14] = true;
2475
+ }
2476
+ return bitmap;
2477
+ }
2478
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2479
+ if (data.length < 18) {
2480
+ console.error("Data length is too short");
2481
+ return;
2482
+ }
2483
+ const channelBitmap = this.getChannelBitmap(data);
2484
+ for (let i = 0; i < channelBitmap.length; i++) {
2485
+ if (!channelBitmap[i])
2486
+ continue;
2487
+ for (let j = 0; j < 12; j++) {
2488
+ const value = data[j + 7] - 64; // cent
2489
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2490
+ }
2491
+ }
2492
+ }
2493
+ applyDestinationSettings(channel, note, table) {
2494
+ if (table[0] !== 64) {
2495
+ this.updateDetune(channel);
2496
+ }
2497
+ if (!note.portamento) {
2498
+ if (table[1] !== 64) {
2499
+ this.setFilterEnvelope(channel, note);
2500
+ }
2501
+ if (table[2] !== 64) {
2502
+ this.setVolumeEnvelope(channel, note);
2503
+ }
2504
+ }
2505
+ if (table[3] !== 0) {
2506
+ this.setModLfoToPitch(channel, note);
2507
+ }
2508
+ if (table[4] !== 0) {
2509
+ this.setModLfoToFilterFc(channel, note);
2510
+ }
2511
+ if (table[5] !== 0) {
2512
+ this.setModLfoToVolume(channel, note);
2513
+ }
2514
+ }
2515
+ handleChannelPressureSysEx(data) {
2516
+ const channelNumber = data[4];
2517
+ const table = this.channels[channelNumber].pressureTable;
2518
+ for (let i = 5; i < data.length - 1; i += 2) {
2519
+ const pp = data[i];
2520
+ const rr = data[i + 1];
2521
+ table[pp] = rr;
2522
+ }
2523
+ }
2524
+ initControlTable() {
2525
+ const channelCount = 128;
2526
+ const slotSize = 6;
2527
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2528
+ const table = new Uint8Array(channelCount * slotSize);
2529
+ for (let ch = 0; ch < channelCount; ch++) {
2530
+ const offset = ch * slotSize;
2531
+ table.set(defaultValues, offset);
2532
+ }
2533
+ return table;
2534
+ }
2535
+ applyControlTable(channel, controllerType) {
2536
+ const slotSize = 6;
2537
+ const offset = controllerType * slotSize;
2538
+ const table = channel.controlTable.subarray(offset, offset + slotSize);
2539
+ channel.scheduledNotes.forEach((noteList) => {
2540
+ for (let i = 0; i < noteList.length; i++) {
2541
+ const note = noteList[i];
2542
+ if (!note)
2543
+ continue;
2544
+ this.applyDestinationSettings(channel, note, table);
2545
+ }
2546
+ });
2547
+ }
2548
+ handleControlChangeSysEx(data) {
2549
+ const channelNumber = data[4];
2550
+ const controllerType = data[5];
2551
+ const table = this.channels[channelNumber].controlTable[controllerType];
2552
+ for (let i = 6; i < data.length - 1; i += 2) {
2553
+ const pp = data[i];
2554
+ const rr = data[i + 1];
2555
+ table[pp] = rr;
2556
+ }
2557
+ }
2558
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2559
+ const index = keyNumber * 128 + controllerType;
2560
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2561
+ return (controlValue + 64) / 64;
2562
+ }
2563
+ handleKeyBasedInstrumentControlSysEx(data) {
2564
+ const channelNumber = data[4];
2565
+ const keyNumber = data[5];
2566
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2567
+ for (let i = 6; i < data.length - 1; i += 2) {
2568
+ const controllerType = data[i];
2569
+ const value = data[i + 1];
2570
+ const index = keyNumber * 128 + controllerType;
2571
+ table[index] = value - 64;
2572
+ }
2573
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2574
+ }
2404
2575
  handleExclusiveMessage(data) {
2405
2576
  console.warn(`Unsupported Exclusive Message: ${data}`);
2406
2577
  }
@@ -2435,6 +2606,8 @@ Object.defineProperty(Midy, "channelSettings", {
2435
2606
  currentBufferSource: null,
2436
2607
  detune: 0,
2437
2608
  scaleOctaveTuningTable: new Array(12).fill(0), // cent
2609
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2610
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2438
2611
  program: 0,
2439
2612
  bank: 121 * 128,
2440
2613
  bankMSB: 121,