@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.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 Midy {
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 Midy {
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 Midy {
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(),
@@ -920,7 +939,9 @@ export class Midy {
920
939
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
921
940
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
922
941
  const pitch = pitchWheel * pitchWheelSensitivity;
923
- return tuning + pitch;
942
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
943
+ const pressure = pressureDepth * channel.state.channelPressure;
944
+ return tuning + pitch + pressure;
924
945
  }
925
946
  calcNoteDetune(channel, note) {
926
947
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -940,14 +961,19 @@ export class Midy {
940
961
  }
941
962
  });
942
963
  }
964
+ getPortamentoTime(channel) {
965
+ const factor = 5 * Math.log(10) / 127;
966
+ const time = channel.state.portamentoTime;
967
+ return Math.log(time) / factor;
968
+ }
943
969
  setPortamentoStartVolumeEnvelope(channel, note) {
944
970
  const now = this.audioContext.currentTime;
945
971
  const { voiceParams, startTime } = note;
946
972
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
947
973
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
948
974
  const volDelay = startTime + voiceParams.volDelay;
949
- const portamentoTime = volDelay + channel.state.portamentoTime;
950
- note.volumeNode.gain
975
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
976
+ note.volumeEnvelopeNode.gain
951
977
  .cancelScheduledValues(now)
952
978
  .setValueAtTime(0, volDelay)
953
979
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
@@ -956,13 +982,16 @@ export class Midy {
956
982
  const now = this.audioContext.currentTime;
957
983
  const state = channel.state;
958
984
  const { voiceParams, startTime } = note;
959
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
985
+ const pressureDepth = channel.pressureTable[2] / 64;
986
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
987
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
988
+ pressure;
960
989
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
961
990
  const volDelay = startTime + voiceParams.volDelay;
962
991
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
963
992
  const volHold = volAttack + voiceParams.volHold;
964
993
  const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
965
- note.volumeNode.gain
994
+ note.volumeEnvelopeNode.gain
966
995
  .cancelScheduledValues(now)
967
996
  .setValueAtTime(0, startTime)
968
997
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -1004,14 +1033,17 @@ export class Midy {
1004
1033
  const { voiceParams, noteNumber, startTime } = note;
1005
1034
  const softPedalFactor = 1 -
1006
1035
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1007
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1008
- softPedalFactor * state.brightness * 2;
1036
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1037
+ const pressure = pressureDepth * channel.state.channelPressure;
1038
+ const baseCent = voiceParams.initialFilterFc + pressure;
1039
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1040
+ state.brightness * 2;
1009
1041
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1010
1042
  const sustainFreq = baseFreq +
1011
1043
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1012
1044
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1013
1045
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1014
- const portamentoTime = startTime + channel.state.portamentoTime;
1046
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
1015
1047
  const modDelay = startTime + voiceParams.modDelay;
1016
1048
  note.filterNode.frequency
1017
1049
  .cancelScheduledValues(now)
@@ -1056,14 +1088,14 @@ export class Midy {
1056
1088
  note.modulationDepth = new GainNode(this.audioContext);
1057
1089
  this.setModLfoToPitch(channel, note);
1058
1090
  note.volumeDepth = new GainNode(this.audioContext);
1059
- this.setModLfoToVolume(note);
1091
+ this.setModLfoToVolume(channel, note);
1060
1092
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1061
1093
  note.modulationLFO.connect(note.filterDepth);
1062
1094
  note.filterDepth.connect(note.filterNode.frequency);
1063
1095
  note.modulationLFO.connect(note.modulationDepth);
1064
1096
  note.modulationDepth.connect(note.bufferSource.detune);
1065
1097
  note.modulationLFO.connect(note.volumeDepth);
1066
- note.volumeDepth.connect(note.volumeNode.gain);
1098
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1067
1099
  }
1068
1100
  startVibrato(channel, note, startTime) {
1069
1101
  const { voiceParams } = note;
@@ -1085,6 +1117,9 @@ export class Midy {
1085
1117
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1086
1118
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1087
1119
  note.volumeNode = new GainNode(this.audioContext);
1120
+ note.gainL = new GainNode(this.audioContext);
1121
+ note.gainR = new GainNode(this.audioContext);
1122
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1088
1123
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1089
1124
  type: "lowpass",
1090
1125
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
@@ -1111,7 +1146,10 @@ export class Midy {
1111
1146
  channel.currentBufferSource = note.bufferSource;
1112
1147
  }
1113
1148
  note.bufferSource.connect(note.filterNode);
1114
- note.filterNode.connect(note.volumeNode);
1149
+ note.filterNode.connect(note.volumeEnvelopeNode);
1150
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1151
+ note.volumeNode.connect(note.gainL);
1152
+ note.volumeNode.connect(note.gainR);
1115
1153
  if (0 < channel.chorusSendLevel) {
1116
1154
  this.setChorusEffectsSend(channel, note, 0);
1117
1155
  }
@@ -1142,8 +1180,8 @@ export class Midy {
1142
1180
  if (!voice)
1143
1181
  return;
1144
1182
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1145
- note.volumeNode.connect(channel.gainL);
1146
- note.volumeNode.connect(channel.gainR);
1183
+ note.gainL.connect(channel.gainL);
1184
+ note.gainR.connect(channel.gainR);
1147
1185
  if (channel.state.sostenutoPedal) {
1148
1186
  channel.sostenutoNotes.set(noteNumber, note);
1149
1187
  }
@@ -1174,7 +1212,7 @@ export class Midy {
1174
1212
  }
1175
1213
  stopNote(endTime, stopTime, scheduledNotes, index) {
1176
1214
  const note = scheduledNotes[index];
1177
- note.volumeNode.gain
1215
+ note.volumeEnvelopeNode.gain
1178
1216
  .cancelScheduledValues(endTime)
1179
1217
  .linearRampToValueAtTime(0, stopTime);
1180
1218
  note.ending = true;
@@ -1185,8 +1223,11 @@ export class Midy {
1185
1223
  note.bufferSource.onended = () => {
1186
1224
  scheduledNotes[index] = null;
1187
1225
  note.bufferSource.disconnect();
1188
- note.volumeNode.disconnect();
1189
1226
  note.filterNode.disconnect();
1227
+ note.volumeEnvelopeNode.disconnect();
1228
+ note.volumeNode.disconnect();
1229
+ note.gainL.disconnect();
1230
+ note.gainR.disconnect();
1190
1231
  if (note.modulationDepth) {
1191
1232
  note.volumeDepth.disconnect();
1192
1233
  note.modulationDepth.disconnect();
@@ -1236,7 +1277,7 @@ export class Midy {
1236
1277
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1237
1278
  }
1238
1279
  else {
1239
- const portamentoTime = endTime + state.portamentoTime;
1280
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1240
1281
  const deltaNote = portamentoNoteNumber - noteNumber;
1241
1282
  const baseRate = note.voiceParams.playbackRate;
1242
1283
  const targetRate = baseRate * Math.pow(2, deltaNote / 12);
@@ -1311,7 +1352,7 @@ export class Midy {
1311
1352
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1312
1353
  if (activeNotes.has(noteNumber)) {
1313
1354
  const activeNote = activeNotes.get(noteNumber);
1314
- const gain = activeNote.volumeNode.gain.value;
1355
+ const gain = activeNote.gainL.gain.value;
1315
1356
  activeNote.volumeNode.gain
1316
1357
  .cancelScheduledValues(now)
1317
1358
  .setValueAtTime(gain * pressure, now);
@@ -1324,20 +1365,24 @@ export class Midy {
1324
1365
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1325
1366
  channel.program = program;
1326
1367
  }
1327
- handleChannelPressure(channelNumber, pressure) {
1328
- const now = this.audioContext.currentTime;
1368
+ handleChannelPressure(channelNumber, value) {
1329
1369
  const channel = this.channels[channelNumber];
1330
- pressure /= 64;
1331
- channel.channelPressure = pressure;
1332
- const activeNotes = this.getActiveNotes(channel, now);
1333
- if (channel.channelPressure.amplitudeControl !== 1) {
1334
- activeNotes.forEach((activeNote) => {
1335
- const gain = activeNote.volumeNode.gain.value;
1336
- activeNote.volumeNode.gain
1337
- .cancelScheduledValues(now)
1338
- .setValueAtTime(gain * pressure, now);
1339
- });
1340
- }
1370
+ const prev = channel.state.channelPressure;
1371
+ const next = value / 127;
1372
+ channel.state.channelPressure = next;
1373
+ if (channel.pressureTable[0] !== 64) {
1374
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1375
+ channel.detune += pressureDepth * (next - prev);
1376
+ }
1377
+ const table = channel.pressureTable;
1378
+ channel.scheduledNotes.forEach((noteList) => {
1379
+ for (let i = 0; i < noteList.length; i++) {
1380
+ const note = noteList[i];
1381
+ if (!note)
1382
+ continue;
1383
+ this.applyDestinationSettings(channel, note, table);
1384
+ }
1385
+ });
1341
1386
  // this.applyVoiceParams(channel, 13);
1342
1387
  }
1343
1388
  handlePitchBendMessage(channelNumber, lsb, msb) {
@@ -1356,13 +1401,15 @@ export class Midy {
1356
1401
  }
1357
1402
  setModLfoToPitch(channel, note) {
1358
1403
  const now = this.audioContext.currentTime;
1359
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1360
- const modulationDepth = Math.abs(modLfoToPitch) +
1404
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1405
+ const pressure = pressureDepth * channel.state.channelPressure;
1406
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1407
+ const baseDepth = Math.abs(modLfoToPitch) +
1361
1408
  channel.state.modulationDepth;
1362
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1409
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1363
1410
  note.modulationDepth.gain
1364
1411
  .cancelScheduledValues(now)
1365
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1412
+ .setValueAtTime(modulationDepth, now);
1366
1413
  }
1367
1414
  setVibLfoToPitch(channel, note) {
1368
1415
  const now = this.audioContext.currentTime;
@@ -1374,69 +1421,75 @@ export class Midy {
1374
1421
  .cancelScheduledValues(now)
1375
1422
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1376
1423
  }
1377
- setModLfoToFilterFc(note) {
1424
+ setModLfoToFilterFc(channel, note) {
1378
1425
  const now = this.audioContext.currentTime;
1379
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1426
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1427
+ const pressure = pressureDepth * channel.state.channelPressure;
1428
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1380
1429
  note.filterDepth.gain
1381
1430
  .cancelScheduledValues(now)
1382
1431
  .setValueAtTime(modLfoToFilterFc, now);
1383
1432
  }
1384
- setModLfoToVolume(note) {
1433
+ setModLfoToVolume(channel, note) {
1385
1434
  const now = this.audioContext.currentTime;
1386
1435
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1387
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1388
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1436
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1437
+ const pressureDepth = channel.pressureTable[5] / 127;
1438
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1439
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1389
1440
  note.volumeDepth.gain
1390
1441
  .cancelScheduledValues(now)
1391
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1442
+ .setValueAtTime(volumeDepth, now);
1392
1443
  }
1393
- setChorusEffectsSend(note, prevValue) {
1444
+ setReverbEffectsSend(channel, note, prevValue) {
1394
1445
  if (0 < prevValue) {
1395
- if (0 < note.voiceParams.chorusEffectsSend) {
1446
+ if (0 < note.voiceParams.reverbEffectsSend) {
1396
1447
  const now = this.audioContext.currentTime;
1397
- const value = note.voiceParams.chorusEffectsSend;
1398
- note.chorusEffectsSend.gain
1448
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1449
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1450
+ note.reverbEffectsSend.gain
1399
1451
  .cancelScheduledValues(now)
1400
1452
  .setValueAtTime(value, now);
1401
1453
  }
1402
1454
  else {
1403
- note.chorusEffectsSend.disconnect();
1455
+ note.reverbEffectsSend.disconnect();
1404
1456
  }
1405
1457
  }
1406
1458
  else {
1407
- if (0 < note.voiceParams.chorusEffectsSend) {
1408
- if (!note.chorusEffectsSend) {
1409
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1410
- gain: note.voiceParams.chorusEffectsSend,
1459
+ if (0 < note.voiceParams.reverbEffectsSend) {
1460
+ if (!note.reverbEffectsSend) {
1461
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1462
+ gain: note.voiceParams.reverbEffectsSend,
1411
1463
  });
1412
- note.volumeNode.connect(note.chorusEffectsSend);
1464
+ note.volumeNode.connect(note.reverbEffectsSend);
1413
1465
  }
1414
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1466
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1415
1467
  }
1416
1468
  }
1417
1469
  }
1418
- setReverbEffectsSend(note, prevValue) {
1470
+ setChorusEffectsSend(channel, note, prevValue) {
1419
1471
  if (0 < prevValue) {
1420
- if (0 < note.voiceParams.reverbEffectsSend) {
1472
+ if (0 < note.voiceParams.chorusEffectsSend) {
1421
1473
  const now = this.audioContext.currentTime;
1422
- const value = note.voiceParams.reverbEffectsSend;
1423
- note.reverbEffectsSend.gain
1474
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1475
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1476
+ note.chorusEffectsSend.gain
1424
1477
  .cancelScheduledValues(now)
1425
1478
  .setValueAtTime(value, now);
1426
1479
  }
1427
1480
  else {
1428
- note.reverbEffectsSend.disconnect();
1481
+ note.chorusEffectsSend.disconnect();
1429
1482
  }
1430
1483
  }
1431
1484
  else {
1432
- if (0 < note.voiceParams.reverbEffectsSend) {
1433
- if (!note.reverbEffectsSend) {
1434
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1435
- gain: note.voiceParams.reverbEffectsSend,
1485
+ if (0 < note.voiceParams.chorusEffectsSend) {
1486
+ if (!note.chorusEffectsSend) {
1487
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1488
+ gain: note.voiceParams.chorusEffectsSend,
1436
1489
  });
1437
- note.volumeNode.connect(note.reverbEffectsSend);
1490
+ note.volumeNode.connect(note.chorusEffectsSend);
1438
1491
  }
1439
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1492
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1440
1493
  }
1441
1494
  }
1442
1495
  }
@@ -1469,30 +1522,32 @@ export class Midy {
1469
1522
  }
1470
1523
  },
1471
1524
  modLfoToFilterFc: (channel, note, _prevValue) => {
1472
- if (0 < channel.state.modulationDepth)
1473
- this.setModLfoToFilterFc(note);
1525
+ if (0 < channel.state.modulationDepth) {
1526
+ this.setModLfoToFilterFc(channel, note);
1527
+ }
1474
1528
  },
1475
- modLfoToVolume: (channel, note) => {
1476
- if (0 < channel.state.modulationDepth)
1477
- this.setModLfoToVolume(note);
1529
+ modLfoToVolume: (channel, note, _prevValue) => {
1530
+ if (0 < channel.state.modulationDepth) {
1531
+ this.setModLfoToVolume(channel, note);
1532
+ }
1478
1533
  },
1479
- chorusEffectsSend: (_channel, note, prevValue) => {
1480
- this.setChorusEffectsSend(note, prevValue);
1534
+ chorusEffectsSend: (channel, note, prevValue) => {
1535
+ this.setChorusEffectsSend(channel, note, prevValue);
1481
1536
  },
1482
- reverbEffectsSend: (_channel, note, prevValue) => {
1483
- this.setReverbEffectsSend(note, prevValue);
1537
+ reverbEffectsSend: (channel, note, prevValue) => {
1538
+ this.setReverbEffectsSend(channel, note, prevValue);
1484
1539
  },
1485
1540
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1486
1541
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
1487
1542
  delayVibLFO: (channel, note, prevValue) => {
1488
1543
  if (0 < channel.state.vibratoDepth) {
1489
1544
  const now = this.audioContext.currentTime;
1490
- const prevStartTime = note.startTime +
1491
- prevValue * channel.state.vibratoDelay * 2;
1545
+ const vibratoDelay = channel.state.vibratoDelay * 2;
1546
+ const prevStartTime = note.startTime + prevValue * vibratoDelay;
1492
1547
  if (now < prevStartTime)
1493
1548
  return;
1494
- const startTime = note.startTime +
1495
- value * channel.state.vibratoDelay * 2;
1549
+ const value = note.voiceParams.delayVibLFO;
1550
+ const startTime = note.startTime + value * vibratoDelay;
1496
1551
  note.vibratoLFO.stop(now);
1497
1552
  note.vibratoLFO.start(startTime);
1498
1553
  }
@@ -1500,9 +1555,10 @@ export class Midy {
1500
1555
  freqVibLFO: (channel, note, _prevValue) => {
1501
1556
  if (0 < channel.state.vibratoDepth) {
1502
1557
  const now = this.audioContext.currentTime;
1558
+ const freqVibLFO = note.voiceParams.freqVibLFO;
1503
1559
  note.vibratoLFO.frequency
1504
1560
  .cancelScheduledValues(now)
1505
- .setValueAtTime(value * sate.vibratoRate, now);
1561
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate, now);
1506
1562
  }
1507
1563
  },
1508
1564
  };
@@ -1609,8 +1665,8 @@ export class Midy {
1609
1665
  if (handler) {
1610
1666
  handler.call(this, channelNumber, value);
1611
1667
  const channel = this.channels[channelNumber];
1612
- const controller = 128 + controllerType;
1613
- this.applyVoiceParams(channel, controller);
1668
+ this.applyVoiceParams(channel, controllerType + 128);
1669
+ this.applyControlTable(channel, controllerType);
1614
1670
  }
1615
1671
  else {
1616
1672
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1621,13 +1677,14 @@ export class Midy {
1621
1677
  }
1622
1678
  updateModulation(channel) {
1623
1679
  const now = this.audioContext.currentTime;
1680
+ const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1624
1681
  channel.scheduledNotes.forEach((noteList) => {
1625
1682
  for (let i = 0; i < noteList.length; i++) {
1626
1683
  const note = noteList[i];
1627
1684
  if (!note)
1628
1685
  continue;
1629
1686
  if (note.modulationDepth) {
1630
- note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
1687
+ note.modulationDepth.gain.setValueAtTime(depth, now);
1631
1688
  }
1632
1689
  else {
1633
1690
  this.setPitchEnvelope(note);
@@ -1638,8 +1695,7 @@ export class Midy {
1638
1695
  }
1639
1696
  setModulationDepth(channelNumber, modulation) {
1640
1697
  const channel = this.channels[channelNumber];
1641
- channel.state.modulationDepth = (modulation / 127) *
1642
- channel.modulationDepthRange;
1698
+ channel.state.modulationDepth = modulation / 127;
1643
1699
  this.updateModulation(channel);
1644
1700
  }
1645
1701
  setPortamentoTime(channelNumber, portamentoTime) {
@@ -1647,10 +1703,27 @@ export class Midy {
1647
1703
  const factor = 5 * Math.log(10) / 127;
1648
1704
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1649
1705
  }
1706
+ setKeyBasedVolume(channel) {
1707
+ const now = this.audioContext.currentTime;
1708
+ channel.scheduledNotes.forEach((noteList) => {
1709
+ for (let i = 0; i < noteList.length; i++) {
1710
+ const note = noteList[i];
1711
+ if (!note)
1712
+ continue;
1713
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1714
+ if (keyBasedValue === 0)
1715
+ continue;
1716
+ note.volumeNode.gain
1717
+ .cancelScheduledValues(now)
1718
+ .setValueAtTime(1 + keyBasedValue, now);
1719
+ }
1720
+ });
1721
+ }
1650
1722
  setVolume(channelNumber, volume) {
1651
1723
  const channel = this.channels[channelNumber];
1652
1724
  channel.state.volume = volume / 127;
1653
1725
  this.updateChannelVolume(channel);
1726
+ this.setKeyBasedVolume(channel);
1654
1727
  }
1655
1728
  panToGain(pan) {
1656
1729
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1659,10 +1732,31 @@ export class Midy {
1659
1732
  gainRight: Math.sin(theta),
1660
1733
  };
1661
1734
  }
1735
+ setKeyBasedPan(channel) {
1736
+ const now = this.audioContext.currentTime;
1737
+ channel.scheduledNotes.forEach((noteList) => {
1738
+ for (let i = 0; i < noteList.length; i++) {
1739
+ const note = noteList[i];
1740
+ if (!note)
1741
+ continue;
1742
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1743
+ if (keyBasedValue === 0)
1744
+ continue;
1745
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1746
+ note.gainL.gain
1747
+ .cancelScheduledValues(now)
1748
+ .setValueAtTime(gainLeft, now);
1749
+ note.gainR.gain
1750
+ .cancelScheduledValues(now)
1751
+ .setValueAtTime(gainRight, now);
1752
+ }
1753
+ });
1754
+ }
1662
1755
  setPan(channelNumber, pan) {
1663
1756
  const channel = this.channels[channelNumber];
1664
1757
  channel.state.pan = pan / 127;
1665
1758
  this.updateChannelVolume(channel);
1759
+ this.setKeyBasedPan(channel);
1666
1760
  }
1667
1761
  setExpression(channelNumber, expression) {
1668
1762
  const channel = this.channels[channelNumber];
@@ -1824,7 +1918,7 @@ export class Midy {
1824
1918
  const note = noteList[i];
1825
1919
  if (!note)
1826
1920
  continue;
1827
- this.setReverbEffectsSend(note, 0);
1921
+ this.setReverbEffectsSend(channel, note, 0);
1828
1922
  }
1829
1923
  });
1830
1924
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -1865,7 +1959,7 @@ export class Midy {
1865
1959
  const note = noteList[i];
1866
1960
  if (!note)
1867
1961
  continue;
1868
- this.setChorusEffectsSend(note, 0);
1962
+ this.setChorusEffectsSend(channel, note, 0);
1869
1963
  }
1870
1964
  });
1871
1965
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -1995,7 +2089,6 @@ export class Midy {
1995
2089
  setModulationDepthRange(channelNumber, modulationDepthRange) {
1996
2090
  const channel = this.channels[channelNumber];
1997
2091
  channel.modulationDepthRange = modulationDepthRange;
1998
- channel.modulationDepth = (modulation / 127) * modulationDepthRange;
1999
2092
  this.updateModulation(channel);
2000
2093
  }
2001
2094
  allSoundOff(channelNumber) {
@@ -2048,7 +2141,7 @@ export class Midy {
2048
2141
  switch (data[3]) {
2049
2142
  case 8:
2050
2143
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2051
- return this.handleScaleOctaveTuning1ByteFormat(data);
2144
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2052
2145
  default:
2053
2146
  console.warn(`Unsupported Exclusive Message: ${data}`);
2054
2147
  }
@@ -2101,7 +2194,7 @@ export class Midy {
2101
2194
  return this.handleMasterFineTuningSysEx(data);
2102
2195
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2103
2196
  return this.handleMasterCoarseTuningSysEx(data);
2104
- case 5:
2197
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2105
2198
  return this.handleGlobalParameterControlSysEx(data);
2106
2199
  default:
2107
2200
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2109,31 +2202,27 @@ export class Midy {
2109
2202
  break;
2110
2203
  case 8:
2111
2204
  switch (data[3]) {
2112
- case 8:
2113
- // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2205
+ case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2114
2206
  // TODO: realtime
2115
- return this.handleScaleOctaveTuning1ByteFormat(data);
2207
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2116
2208
  default:
2117
2209
  console.warn(`Unsupported Exclusive Message: ${data}`);
2118
2210
  }
2119
2211
  break;
2120
2212
  case 9:
2121
2213
  switch (data[3]) {
2122
- // case 1:
2123
- // // TODO
2124
- // return this.setChannelPressure();
2125
- // case 3:
2126
- // // TODO
2127
- // return this.setControlChange();
2214
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2215
+ return this.handleChannelPressureSysEx(data);
2216
+ case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2217
+ return this.handleControlChangeSysEx(data);
2128
2218
  default:
2129
2219
  console.warn(`Unsupported Exclusive Message: ${data}`);
2130
2220
  }
2131
2221
  break;
2132
2222
  case 10:
2133
2223
  switch (data[3]) {
2134
- // case 1:
2135
- // // TODO
2136
- // return this.handleKeyBasedInstrumentControl();
2224
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2225
+ return this.handleKeyBasedInstrumentControlSysEx(data);
2137
2226
  default:
2138
2227
  console.warn(`Unsupported Exclusive Message: ${data}`);
2139
2228
  }
@@ -2152,8 +2241,8 @@ export class Midy {
2152
2241
  }
2153
2242
  else {
2154
2243
  const now = this.audioContext.currentTime;
2155
- this.masterGain.gain.cancelScheduledValues(now);
2156
- this.masterGain.gain.setValueAtTime(volume * volume, now);
2244
+ this.masterVolume.gain.cancelScheduledValues(now);
2245
+ this.masterVolume.gain.setValueAtTime(volume * volume, now);
2157
2246
  }
2158
2247
  }
2159
2248
  handleMasterFineTuningSysEx(data) {
@@ -2178,40 +2267,6 @@ export class Midy {
2178
2267
  channel.detune += next - prev;
2179
2268
  this.updateDetune(channel);
2180
2269
  }
2181
- getChannelBitmap(data) {
2182
- const bitmap = new Array(16).fill(false);
2183
- const ff = data[4] & 0b11;
2184
- const gg = data[5] & 0x7F;
2185
- const hh = data[6] & 0x7F;
2186
- for (let bit = 0; bit < 7; bit++) {
2187
- if (hh & (1 << bit))
2188
- bitmap[bit] = true;
2189
- }
2190
- for (let bit = 0; bit < 7; bit++) {
2191
- if (gg & (1 << bit))
2192
- bitmap[bit + 7] = true;
2193
- }
2194
- for (let bit = 0; bit < 2; bit++) {
2195
- if (ff & (1 << bit))
2196
- bitmap[bit + 14] = true;
2197
- }
2198
- return bitmap;
2199
- }
2200
- handleScaleOctaveTuning1ByteFormat(data) {
2201
- if (data.length < 18) {
2202
- console.error("Data length is too short");
2203
- return;
2204
- }
2205
- const channelBitmap = this.getChannelBitmap(data);
2206
- for (let i = 0; i < channelBitmap.length; i++) {
2207
- if (!channelBitmap[i])
2208
- continue;
2209
- for (let j = 0; j < 12; j++) {
2210
- const value = data[j + 7] - 64; // cent
2211
- this.channels[i].scaleOctaveTuningTable[j] = value;
2212
- }
2213
- }
2214
- }
2215
2270
  handleGlobalParameterControlSysEx(data) {
2216
2271
  if (data[7] === 1) {
2217
2272
  switch (data[8]) {
@@ -2398,6 +2453,122 @@ export class Midy {
2398
2453
  getChorusSendToReverb(value) {
2399
2454
  return value * 0.00787;
2400
2455
  }
2456
+ getChannelBitmap(data) {
2457
+ const bitmap = new Array(16).fill(false);
2458
+ const ff = data[4] & 0b11;
2459
+ const gg = data[5] & 0x7F;
2460
+ const hh = data[6] & 0x7F;
2461
+ for (let bit = 0; bit < 7; bit++) {
2462
+ if (hh & (1 << bit))
2463
+ bitmap[bit] = true;
2464
+ }
2465
+ for (let bit = 0; bit < 7; bit++) {
2466
+ if (gg & (1 << bit))
2467
+ bitmap[bit + 7] = true;
2468
+ }
2469
+ for (let bit = 0; bit < 2; bit++) {
2470
+ if (ff & (1 << bit))
2471
+ bitmap[bit + 14] = true;
2472
+ }
2473
+ return bitmap;
2474
+ }
2475
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2476
+ if (data.length < 18) {
2477
+ console.error("Data length is too short");
2478
+ return;
2479
+ }
2480
+ const channelBitmap = this.getChannelBitmap(data);
2481
+ for (let i = 0; i < channelBitmap.length; i++) {
2482
+ if (!channelBitmap[i])
2483
+ continue;
2484
+ for (let j = 0; j < 12; j++) {
2485
+ const value = data[j + 7] - 64; // cent
2486
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2487
+ }
2488
+ }
2489
+ }
2490
+ applyDestinationSettings(channel, note, table) {
2491
+ if (table[0] !== 64) {
2492
+ this.updateDetune(channel);
2493
+ }
2494
+ if (!note.portamento) {
2495
+ if (table[1] !== 64) {
2496
+ this.setFilterEnvelope(channel, note);
2497
+ }
2498
+ if (table[2] !== 64) {
2499
+ this.setVolumeEnvelope(channel, note);
2500
+ }
2501
+ }
2502
+ if (table[3] !== 0) {
2503
+ this.setModLfoToPitch(channel, note);
2504
+ }
2505
+ if (table[4] !== 0) {
2506
+ this.setModLfoToFilterFc(channel, note);
2507
+ }
2508
+ if (table[5] !== 0) {
2509
+ this.setModLfoToVolume(channel, note);
2510
+ }
2511
+ }
2512
+ handleChannelPressureSysEx(data) {
2513
+ const channelNumber = data[4];
2514
+ const table = this.channels[channelNumber].pressureTable;
2515
+ for (let i = 5; i < data.length - 1; i += 2) {
2516
+ const pp = data[i];
2517
+ const rr = data[i + 1];
2518
+ table[pp] = rr;
2519
+ }
2520
+ }
2521
+ initControlTable() {
2522
+ const channelCount = 128;
2523
+ const slotSize = 6;
2524
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2525
+ const table = new Uint8Array(channelCount * slotSize);
2526
+ for (let ch = 0; ch < channelCount; ch++) {
2527
+ const offset = ch * slotSize;
2528
+ table.set(defaultValues, offset);
2529
+ }
2530
+ return table;
2531
+ }
2532
+ applyControlTable(channel, controllerType) {
2533
+ const slotSize = 6;
2534
+ const offset = controllerType * slotSize;
2535
+ const table = channel.controlTable.subarray(offset, offset + slotSize);
2536
+ channel.scheduledNotes.forEach((noteList) => {
2537
+ for (let i = 0; i < noteList.length; i++) {
2538
+ const note = noteList[i];
2539
+ if (!note)
2540
+ continue;
2541
+ this.applyDestinationSettings(channel, note, table);
2542
+ }
2543
+ });
2544
+ }
2545
+ handleControlChangeSysEx(data) {
2546
+ const channelNumber = data[4];
2547
+ const controllerType = data[5];
2548
+ const table = this.channels[channelNumber].controlTable[controllerType];
2549
+ for (let i = 6; i < data.length - 1; i += 2) {
2550
+ const pp = data[i];
2551
+ const rr = data[i + 1];
2552
+ table[pp] = rr;
2553
+ }
2554
+ }
2555
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2556
+ const index = keyNumber * 128 + controllerType;
2557
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2558
+ return (controlValue + 64) / 64;
2559
+ }
2560
+ handleKeyBasedInstrumentControlSysEx(data) {
2561
+ const channelNumber = data[4];
2562
+ const keyNumber = data[5];
2563
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2564
+ for (let i = 6; i < data.length - 1; i += 2) {
2565
+ const controllerType = data[i];
2566
+ const value = data[i + 1];
2567
+ const index = keyNumber * 128 + controllerType;
2568
+ table[index] = value - 64;
2569
+ }
2570
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2571
+ }
2401
2572
  handleExclusiveMessage(data) {
2402
2573
  console.warn(`Unsupported Exclusive Message: ${data}`);
2403
2574
  }
@@ -2431,6 +2602,8 @@ Object.defineProperty(Midy, "channelSettings", {
2431
2602
  currentBufferSource: null,
2432
2603
  detune: 0,
2433
2604
  scaleOctaveTuningTable: new Array(12).fill(0), // cent
2605
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2606
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2434
2607
  program: 0,
2435
2608
  bank: 121 * 128,
2436
2609
  bankMSB: 121,