@marmooo/midy 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,
@@ -901,16 +919,48 @@ class MidyGM2 {
901
919
  cbToRatio(cb) {
902
920
  return Math.pow(10, cb / 200);
903
921
  }
922
+ rateToCent(rate) {
923
+ return 1200 * Math.log2(rate);
924
+ }
925
+ centToRate(cent) {
926
+ return Math.pow(2, cent / 1200);
927
+ }
904
928
  centToHz(cent) {
905
- return 8.176 * Math.pow(2, cent / 1200);
929
+ return 8.176 * this.centToRate(cent);
906
930
  }
907
- calcSemitoneOffset(channel) {
931
+ calcChannelDetune(channel) {
908
932
  const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
909
933
  const channelTuning = channel.coarseTuning + channel.fineTuning;
934
+ const tuning = masterTuning + channelTuning;
910
935
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
911
- const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
936
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
912
937
  const pitch = pitchWheel * pitchWheelSensitivity;
913
- return masterTuning + channelTuning + pitch;
938
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
939
+ const pressure = pressureDepth * channel.state.channelPressure;
940
+ return tuning + pitch + pressure;
941
+ }
942
+ calcNoteDetune(channel, note) {
943
+ return channel.scaleOctaveTuningTable[note.noteNumber % 12];
944
+ }
945
+ updateDetune(channel) {
946
+ const now = this.audioContext.currentTime;
947
+ channel.scheduledNotes.forEach((noteList) => {
948
+ for (let i = 0; i < noteList.length; i++) {
949
+ const note = noteList[i];
950
+ if (!note)
951
+ continue;
952
+ const noteDetune = this.calcNoteDetune(channel, note);
953
+ const detune = channel.detune + noteDetune;
954
+ note.bufferSource.detune
955
+ .cancelScheduledValues(now)
956
+ .setValueAtTime(detune, now);
957
+ }
958
+ });
959
+ }
960
+ getPortamentoTime(channel) {
961
+ const factor = 5 * Math.log(10) / 127;
962
+ const time = channel.state.portamentoTime;
963
+ return Math.log(time) / factor;
914
964
  }
915
965
  setPortamentoStartVolumeEnvelope(channel, note) {
916
966
  const now = this.audioContext.currentTime;
@@ -918,22 +968,26 @@ class MidyGM2 {
918
968
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
919
969
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
920
970
  const volDelay = startTime + voiceParams.volDelay;
921
- const portamentoTime = volDelay + channel.state.portamentoTime;
922
- note.volumeNode.gain
971
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
972
+ note.volumeEnvelopeNode.gain
923
973
  .cancelScheduledValues(now)
924
974
  .setValueAtTime(0, volDelay)
925
975
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
926
976
  }
927
- setVolumeEnvelope(note) {
977
+ setVolumeEnvelope(channel, note) {
928
978
  const now = this.audioContext.currentTime;
979
+ const state = channel.state;
929
980
  const { voiceParams, startTime } = note;
930
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
981
+ const pressureDepth = channel.pressureTable[2] / 64;
982
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
983
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
984
+ pressure;
931
985
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
932
986
  const volDelay = startTime + voiceParams.volDelay;
933
987
  const volAttack = volDelay + voiceParams.volAttack;
934
988
  const volHold = volAttack + voiceParams.volHold;
935
989
  const volDecay = volHold + voiceParams.volDecay;
936
- note.volumeNode.gain
990
+ note.volumeEnvelopeNode.gain
937
991
  .cancelScheduledValues(now)
938
992
  .setValueAtTime(0, startTime)
939
993
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -941,32 +995,28 @@ class MidyGM2 {
941
995
  .setValueAtTime(attackVolume, volHold)
942
996
  .linearRampToValueAtTime(sustainVolume, volDecay);
943
997
  }
944
- setPlaybackRate(note) {
998
+ setPitchEnvelope(note) {
945
999
  const now = this.audioContext.currentTime;
1000
+ const { voiceParams } = note;
1001
+ const baseRate = voiceParams.playbackRate;
946
1002
  note.bufferSource.playbackRate
947
1003
  .cancelScheduledValues(now)
948
- .setValueAtTime(note.voiceParams.playbackRate, now);
949
- }
950
- setPitch(channel, note) {
951
- const now = this.audioContext.currentTime;
952
- const { startTime } = note;
953
- const basePitch = this.calcSemitoneOffset(channel) * 100;
954
- note.bufferSource.detune
955
- .cancelScheduledValues(now)
956
- .setValueAtTime(basePitch, startTime);
957
- const modEnvToPitch = note.voiceParams.modEnvToPitch;
1004
+ .setValueAtTime(baseRate, now);
1005
+ const modEnvToPitch = voiceParams.modEnvToPitch;
958
1006
  if (modEnvToPitch === 0)
959
1007
  return;
1008
+ const basePitch = this.rateToCent(baseRate);
960
1009
  const peekPitch = basePitch + modEnvToPitch;
1010
+ const peekRate = this.centToRate(peekPitch);
961
1011
  const modDelay = startTime + voiceParams.modDelay;
962
1012
  const modAttack = modDelay + voiceParams.modAttack;
963
1013
  const modHold = modAttack + voiceParams.modHold;
964
1014
  const modDecay = modHold + voiceParams.modDecay;
965
- note.bufferSource.detune
966
- .setValueAtTime(basePitch, modDelay)
967
- .exponentialRampToValueAtTime(peekPitch, modAttack)
968
- .setValueAtTime(peekPitch, modHold)
969
- .linearRampToValueAtTime(basePitch, modDecay);
1015
+ note.bufferSource.playbackRate
1016
+ .setValueAtTime(baseRate, modDelay)
1017
+ .exponentialRampToValueAtTime(peekRate, modAttack)
1018
+ .setValueAtTime(peekRate, modHold)
1019
+ .linearRampToValueAtTime(baseRate, modDecay);
970
1020
  }
971
1021
  clampCutoffFrequency(frequency) {
972
1022
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -979,14 +1029,16 @@ class MidyGM2 {
979
1029
  const { voiceParams, noteNumber, startTime } = note;
980
1030
  const softPedalFactor = 1 -
981
1031
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
982
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
983
- softPedalFactor;
1032
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1033
+ const pressure = pressureDepth * channel.state.channelPressure;
1034
+ const baseCent = voiceParams.initialFilterFc + pressure;
1035
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor;
984
1036
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
985
1037
  const sustainFreq = baseFreq +
986
1038
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
987
1039
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
988
1040
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
989
- const portamentoTime = startTime + channel.state.portamentoTime;
1041
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
990
1042
  const modDelay = startTime + voiceParams.modDelay;
991
1043
  note.filterNode.frequency
992
1044
  .cancelScheduledValues(now)
@@ -1031,14 +1083,14 @@ class MidyGM2 {
1031
1083
  note.modulationDepth = new GainNode(this.audioContext);
1032
1084
  this.setModLfoToPitch(channel, note);
1033
1085
  note.volumeDepth = new GainNode(this.audioContext);
1034
- this.setModLfoToVolume(note);
1086
+ this.setModLfoToVolume(channel, note);
1035
1087
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1036
1088
  note.modulationLFO.connect(note.filterDepth);
1037
1089
  note.filterDepth.connect(note.filterNode.frequency);
1038
1090
  note.modulationLFO.connect(note.modulationDepth);
1039
1091
  note.modulationDepth.connect(note.bufferSource.detune);
1040
1092
  note.modulationLFO.connect(note.volumeDepth);
1041
- note.volumeDepth.connect(note.volumeNode.gain);
1093
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1042
1094
  }
1043
1095
  startVibrato(channel, note, startTime) {
1044
1096
  const { voiceParams } = note;
@@ -1060,6 +1112,9 @@ class MidyGM2 {
1060
1112
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1061
1113
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1062
1114
  note.volumeNode = new GainNode(this.audioContext);
1115
+ note.gainL = new GainNode(this.audioContext);
1116
+ note.gainR = new GainNode(this.audioContext);
1117
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1063
1118
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1064
1119
  type: "lowpass",
1065
1120
  Q: voiceParams.initialFilterQ / 10, // dB
@@ -1077,9 +1132,8 @@ class MidyGM2 {
1077
1132
  if (0 < state.vibratoDepth) {
1078
1133
  this.startVibrato(channel, note, startTime);
1079
1134
  }
1080
- this.setPlaybackRate(note);
1135
+ this.setPitchEnvelope(note);
1081
1136
  if (0 < state.modulationDepth) {
1082
- this.setPitch(channel, note);
1083
1137
  this.startModulation(channel, note, startTime);
1084
1138
  }
1085
1139
  if (this.mono && channel.currentBufferSource) {
@@ -1087,7 +1141,10 @@ class MidyGM2 {
1087
1141
  channel.currentBufferSource = note.bufferSource;
1088
1142
  }
1089
1143
  note.bufferSource.connect(note.filterNode);
1090
- note.filterNode.connect(note.volumeNode);
1144
+ note.filterNode.connect(note.volumeEnvelopeNode);
1145
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1146
+ note.volumeNode.connect(note.gainL);
1147
+ note.volumeNode.connect(note.gainR);
1091
1148
  if (0 < channel.chorusSendLevel) {
1092
1149
  this.setChorusEffectsSend(channel, note, 0);
1093
1150
  }
@@ -1118,8 +1175,8 @@ class MidyGM2 {
1118
1175
  if (!voice)
1119
1176
  return;
1120
1177
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1121
- note.volumeNode.connect(channel.gainL);
1122
- note.volumeNode.connect(channel.gainR);
1178
+ note.gainL.connect(channel.gainL);
1179
+ note.gainR.connect(channel.gainR);
1123
1180
  if (channel.state.sostenutoPedal) {
1124
1181
  channel.sostenutoNotes.set(noteNumber, note);
1125
1182
  }
@@ -1150,7 +1207,7 @@ class MidyGM2 {
1150
1207
  }
1151
1208
  stopNote(endTime, stopTime, scheduledNotes, index) {
1152
1209
  const note = scheduledNotes[index];
1153
- note.volumeNode.gain
1210
+ note.volumeEnvelopeNode.gain
1154
1211
  .cancelScheduledValues(endTime)
1155
1212
  .linearRampToValueAtTime(0, stopTime);
1156
1213
  note.ending = true;
@@ -1161,8 +1218,11 @@ class MidyGM2 {
1161
1218
  note.bufferSource.onended = () => {
1162
1219
  scheduledNotes[index] = null;
1163
1220
  note.bufferSource.disconnect();
1164
- note.volumeNode.disconnect();
1165
1221
  note.filterNode.disconnect();
1222
+ note.volumeEnvelopeNode.disconnect();
1223
+ note.volumeNode.disconnect();
1224
+ note.gainL.disconnect();
1225
+ note.gainR.disconnect();
1166
1226
  if (note.modulationDepth) {
1167
1227
  note.volumeDepth.disconnect();
1168
1228
  note.modulationDepth.disconnect();
@@ -1211,12 +1271,13 @@ class MidyGM2 {
1211
1271
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1212
1272
  }
1213
1273
  else {
1214
- const portamentoTime = endTime + state.portamentoTime;
1215
- const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1216
- const detune = note.bufferSource.detune.value + detuneChange;
1217
- note.bufferSource.detune
1274
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1275
+ const deltaNote = portamentoNoteNumber - noteNumber;
1276
+ const baseRate = note.voiceParams.playbackRate;
1277
+ const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1278
+ note.bufferSource.playbackRate
1218
1279
  .cancelScheduledValues(endTime)
1219
- .linearRampToValueAtTime(detune, portamentoTime);
1280
+ .linearRampToValueAtTime(targetRate, portamentoTime);
1220
1281
  return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1221
1282
  }
1222
1283
  }
@@ -1280,22 +1341,45 @@ class MidyGM2 {
1280
1341
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1281
1342
  channel.program = program;
1282
1343
  }
1283
- handleChannelPressure(channelNumber, pressure) {
1284
- const now = this.audioContext.currentTime;
1344
+ handleChannelPressure(channelNumber, value) {
1285
1345
  const channel = this.channels[channelNumber];
1286
- pressure /= 64;
1287
- channel.channelPressure = pressure;
1288
- const activeNotes = this.getActiveNotes(channel, now);
1289
- if (channel.channelPressure.amplitudeControl !== 1) {
1290
- activeNotes.forEach((activeNote) => {
1291
- const gain = activeNote.volumeNode.gain.value;
1292
- activeNote.volumeNode.gain
1293
- .cancelScheduledValues(now)
1294
- .setValueAtTime(gain * pressure, now);
1295
- });
1346
+ const prev = channel.state.channelPressure;
1347
+ const next = value / 127;
1348
+ channel.state.channelPressure = next;
1349
+ if (channel.pressureTable[0] !== 64) {
1350
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1351
+ channel.detune += pressureDepth * (next - prev);
1296
1352
  }
1353
+ channel.scheduledNotes.forEach((noteList) => {
1354
+ for (let i = 0; i < noteList.length; i++) {
1355
+ const note = noteList[i];
1356
+ if (!note)
1357
+ continue;
1358
+ this.setChannelPressure(channel, note);
1359
+ }
1360
+ });
1297
1361
  // this.applyVoiceParams(channel, 13);
1298
1362
  }
1363
+ setChannelPressure(channel, note) {
1364
+ if (channel.pressureTable[0] !== 64) {
1365
+ this.updateDetune(channel);
1366
+ }
1367
+ if (channel.pressureTable[1] !== 64 && !note.portamento) {
1368
+ this.setFilterEnvelope(channel, note);
1369
+ }
1370
+ if (channel.pressureTable[2] !== 64 && !note.portamento) {
1371
+ this.setVolumeEnvelope(channel, note);
1372
+ }
1373
+ if (channel.pressureTable[3] !== 0) {
1374
+ this.setModLfoToPitch(channel, note);
1375
+ }
1376
+ if (channel.pressureTable[4] !== 0) {
1377
+ this.setModLfoToFilterFc(channel, note);
1378
+ }
1379
+ if (channel.pressureTable[5] !== 0) {
1380
+ this.setModLfoToVolume(channel, note);
1381
+ }
1382
+ }
1299
1383
  handlePitchBendMessage(channelNumber, lsb, msb) {
1300
1384
  const pitchBend = msb * 128 + lsb;
1301
1385
  this.setPitchBend(channelNumber, pitchBend);
@@ -1303,98 +1387,107 @@ class MidyGM2 {
1303
1387
  setPitchBend(channelNumber, value) {
1304
1388
  const channel = this.channels[channelNumber];
1305
1389
  const state = channel.state;
1390
+ const prev = state.pitchWheel * 2 - 1;
1391
+ const next = (value - 8192) / 8192;
1306
1392
  state.pitchWheel = value / 16383;
1307
- const pitchWheel = (value - 8192) / 8192;
1308
- const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
1309
- this.updateDetune(channel, detuneChange);
1393
+ channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1394
+ this.updateDetune(channel);
1310
1395
  this.applyVoiceParams(channel, 14);
1311
1396
  }
1312
1397
  setModLfoToPitch(channel, note) {
1313
1398
  const now = this.audioContext.currentTime;
1314
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1315
- const modulationDepth = Math.abs(modLfoToPitch) +
1399
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1400
+ const pressure = pressureDepth * channel.state.channelPressure;
1401
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1402
+ const baseDepth = Math.abs(modLfoToPitch) +
1316
1403
  channel.state.modulationDepth;
1317
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1404
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1318
1405
  note.modulationDepth.gain
1319
1406
  .cancelScheduledValues(now)
1320
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1407
+ .setValueAtTime(modulationDepth, now);
1408
+ }
1409
+ setVibLfoToPitch(channel, note) {
1410
+ const now = this.audioContext.currentTime;
1411
+ const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1412
+ const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1413
+ 2;
1414
+ const vibratoDepthSign = 0 < vibLfoToPitch;
1415
+ note.vibratoDepth.gain
1416
+ .cancelScheduledValues(now)
1417
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1418
+ }
1419
+ setModLfoToFilterFc(channel, note) {
1420
+ const now = this.audioContext.currentTime;
1421
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1422
+ const pressure = pressureDepth * channel.state.channelPressure;
1423
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1424
+ note.filterDepth.gain
1425
+ .cancelScheduledValues(now)
1426
+ .setValueAtTime(modLfoToFilterFc, now);
1321
1427
  }
1322
- setModLfoToVolume(note) {
1428
+ setModLfoToVolume(channel, note) {
1323
1429
  const now = this.audioContext.currentTime;
1324
1430
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1325
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1326
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1431
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1432
+ const pressureDepth = channel.pressureTable[5] / 127;
1433
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1434
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1327
1435
  note.volumeDepth.gain
1328
1436
  .cancelScheduledValues(now)
1329
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1437
+ .setValueAtTime(volumeDepth, now);
1330
1438
  }
1331
- setChorusEffectsSend(note, prevValue) {
1439
+ setReverbEffectsSend(channel, note, prevValue) {
1332
1440
  if (0 < prevValue) {
1333
- if (0 < note.voiceParams.chorusEffectsSend) {
1441
+ if (0 < note.voiceParams.reverbEffectsSend) {
1334
1442
  const now = this.audioContext.currentTime;
1335
- const value = note.voiceParams.chorusEffectsSend;
1336
- note.chorusEffectsSend.gain
1443
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1444
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1445
+ note.reverbEffectsSend.gain
1337
1446
  .cancelScheduledValues(now)
1338
1447
  .setValueAtTime(value, now);
1339
1448
  }
1340
1449
  else {
1341
- note.chorusEffectsSend.disconnect();
1450
+ note.reverbEffectsSend.disconnect();
1342
1451
  }
1343
1452
  }
1344
1453
  else {
1345
- if (0 < note.voiceParams.chorusEffectsSend) {
1346
- if (!note.chorusEffectsSend) {
1347
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1348
- gain: note.voiceParams.chorusEffectsSend,
1454
+ if (0 < note.voiceParams.reverbEffectsSend) {
1455
+ if (!note.reverbEffectsSend) {
1456
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1457
+ gain: note.voiceParams.reverbEffectsSend,
1349
1458
  });
1350
- note.volumeNode.connect(note.chorusEffectsSend);
1459
+ note.volumeNode.connect(note.reverbEffectsSend);
1351
1460
  }
1352
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1461
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1353
1462
  }
1354
1463
  }
1355
1464
  }
1356
- setReverbEffectsSend(note, prevValue) {
1465
+ setChorusEffectsSend(channel, note, prevValue) {
1357
1466
  if (0 < prevValue) {
1358
- if (0 < note.voiceParams.reverbEffectsSend) {
1467
+ if (0 < note.voiceParams.chorusEffectsSend) {
1359
1468
  const now = this.audioContext.currentTime;
1360
- const value = note.voiceParams.reverbEffectsSend;
1361
- note.reverbEffectsSend.gain
1469
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1470
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1471
+ note.chorusEffectsSend.gain
1362
1472
  .cancelScheduledValues(now)
1363
1473
  .setValueAtTime(value, now);
1364
1474
  }
1365
1475
  else {
1366
- note.reverbEffectsSend.disconnect();
1476
+ note.chorusEffectsSend.disconnect();
1367
1477
  }
1368
1478
  }
1369
1479
  else {
1370
- if (0 < note.voiceParams.reverbEffectsSend) {
1371
- if (!note.reverbEffectsSend) {
1372
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1373
- gain: note.voiceParams.reverbEffectsSend,
1480
+ if (0 < note.voiceParams.chorusEffectsSend) {
1481
+ if (!note.chorusEffectsSend) {
1482
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1483
+ gain: note.voiceParams.chorusEffectsSend,
1374
1484
  });
1375
- note.volumeNode.connect(note.reverbEffectsSend);
1485
+ note.volumeNode.connect(note.chorusEffectsSend);
1376
1486
  }
1377
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1487
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1378
1488
  }
1379
1489
  }
1380
1490
  }
1381
- setVibLfoToPitch(channel, note) {
1382
- const now = this.audioContext.currentTime;
1383
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1384
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1385
- 2;
1386
- const vibratoDepthSign = 0 < vibLfoToPitch;
1387
- note.vibratoDepth.gain
1388
- .cancelScheduledValues(now)
1389
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1390
- }
1391
- setModLfoToFilterFc(note) {
1392
- const now = this.audioContext.currentTime;
1393
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1394
- note.filterDepth.gain
1395
- .cancelScheduledValues(now)
1396
- .setValueAtTime(modLfoToFilterFc, now);
1397
- }
1398
1491
  setDelayModLFO(note) {
1399
1492
  const now = this.audioContext.currentTime;
1400
1493
  const startTime = note.startTime;
@@ -1424,18 +1517,20 @@ class MidyGM2 {
1424
1517
  }
1425
1518
  },
1426
1519
  modLfoToFilterFc: (channel, note, _prevValue) => {
1427
- if (0 < channel.state.modulationDepth)
1428
- this.setModLfoToFilterFc(note);
1520
+ if (0 < channel.state.modulationDepth) {
1521
+ this.setModLfoToFilterFc(channel, note);
1522
+ }
1429
1523
  },
1430
1524
  modLfoToVolume: (channel, note) => {
1431
- if (0 < channel.state.modulationDepth)
1432
- this.setModLfoToVolume(note);
1525
+ if (0 < channel.state.modulationDepth) {
1526
+ this.setModLfoToVolume(channel, note);
1527
+ }
1433
1528
  },
1434
- chorusEffectsSend: (_channel, note, prevValue) => {
1435
- this.setChorusEffectsSend(note, prevValue);
1529
+ chorusEffectsSend: (channel, note, prevValue) => {
1530
+ this.setChorusEffectsSend(channel, note, prevValue);
1436
1531
  },
1437
- reverbEffectsSend: (_channel, note, prevValue) => {
1438
- this.setReverbEffectsSend(note, prevValue);
1532
+ reverbEffectsSend: (channel, note, prevValue) => {
1533
+ this.setReverbEffectsSend(channel, note, prevValue);
1439
1534
  },
1440
1535
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1441
1536
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
@@ -1503,7 +1598,7 @@ class MidyGM2 {
1503
1598
  else {
1504
1599
  this.setFilterEnvelope(channel, note);
1505
1600
  }
1506
- this.setPitch(channel, note);
1601
+ this.setPitchEnvelope(note);
1507
1602
  }
1508
1603
  else if (volumeEnvelopeKeySet.has(key)) {
1509
1604
  if (appliedVolumeEnvelope)
@@ -1575,7 +1670,7 @@ class MidyGM2 {
1575
1670
  note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
1576
1671
  }
1577
1672
  else {
1578
- this.setPitch(channel, note);
1673
+ this.setPitchEnvelope(note);
1579
1674
  this.startModulation(channel, note, now);
1580
1675
  }
1581
1676
  }
@@ -1592,10 +1687,27 @@ class MidyGM2 {
1592
1687
  const factor = 5 * Math.log(10) / 127;
1593
1688
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1594
1689
  }
1690
+ setKeyBasedVolume(channel) {
1691
+ const now = this.audioContext.currentTime;
1692
+ channel.scheduledNotes.forEach((noteList) => {
1693
+ for (let i = 0; i < noteList.length; i++) {
1694
+ const note = noteList[i];
1695
+ if (!note)
1696
+ continue;
1697
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1698
+ if (keyBasedValue === 0)
1699
+ continue;
1700
+ note.volumeNode.gain
1701
+ .cancelScheduledValues(now)
1702
+ .setValueAtTime(1 + keyBasedValue, now);
1703
+ }
1704
+ });
1705
+ }
1595
1706
  setVolume(channelNumber, volume) {
1596
1707
  const channel = this.channels[channelNumber];
1597
1708
  channel.state.volume = volume / 127;
1598
1709
  this.updateChannelVolume(channel);
1710
+ this.setKeyBasedVolume(channel);
1599
1711
  }
1600
1712
  panToGain(pan) {
1601
1713
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1604,10 +1716,31 @@ class MidyGM2 {
1604
1716
  gainRight: Math.sin(theta),
1605
1717
  };
1606
1718
  }
1719
+ setKeyBasedPan(channel) {
1720
+ const now = this.audioContext.currentTime;
1721
+ channel.scheduledNotes.forEach((noteList) => {
1722
+ for (let i = 0; i < noteList.length; i++) {
1723
+ const note = noteList[i];
1724
+ if (!note)
1725
+ continue;
1726
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1727
+ if (keyBasedValue === 0)
1728
+ continue;
1729
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1730
+ note.gainL.gain
1731
+ .cancelScheduledValues(now)
1732
+ .setValueAtTime(gainLeft, now);
1733
+ note.gainR.gain
1734
+ .cancelScheduledValues(now)
1735
+ .setValueAtTime(gainRight, now);
1736
+ }
1737
+ });
1738
+ }
1607
1739
  setPan(channelNumber, pan) {
1608
1740
  const channel = this.channels[channelNumber];
1609
1741
  channel.state.pan = pan / 127;
1610
1742
  this.updateChannelVolume(channel);
1743
+ this.setKeyBasedPan(channel);
1611
1744
  }
1612
1745
  setExpression(channelNumber, expression) {
1613
1746
  const channel = this.channels[channelNumber];
@@ -1642,6 +1775,22 @@ class MidyGM2 {
1642
1775
  setPortamento(channelNumber, value) {
1643
1776
  this.channels[channelNumber].state.portamento = value / 127;
1644
1777
  }
1778
+ setSostenutoPedal(channelNumber, value) {
1779
+ const channel = this.channels[channelNumber];
1780
+ channel.state.sostenutoPedal = value / 127;
1781
+ if (64 <= value) {
1782
+ const now = this.audioContext.currentTime;
1783
+ const activeNotes = this.getActiveNotes(channel, now);
1784
+ channel.sostenutoNotes = new Map(activeNotes);
1785
+ }
1786
+ else {
1787
+ this.releaseSostenutoPedal(channelNumber, value);
1788
+ }
1789
+ }
1790
+ setSoftPedal(channelNumber, softPedal) {
1791
+ const channel = this.channels[channelNumber];
1792
+ channel.state.softPedal = softPedal / 127;
1793
+ }
1645
1794
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1646
1795
  const channel = this.channels[channelNumber];
1647
1796
  const state = channel.state;
@@ -1674,7 +1823,7 @@ class MidyGM2 {
1674
1823
  const note = noteList[i];
1675
1824
  if (!note)
1676
1825
  continue;
1677
- this.setReverbEffectsSend(note, 0);
1826
+ this.setReverbEffectsSend(channel, note, 0);
1678
1827
  }
1679
1828
  });
1680
1829
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -1715,7 +1864,7 @@ class MidyGM2 {
1715
1864
  const note = noteList[i];
1716
1865
  if (!note)
1717
1866
  continue;
1718
- this.setChorusEffectsSend(note, 0);
1867
+ this.setChorusEffectsSend(channel, note, 0);
1719
1868
  }
1720
1869
  });
1721
1870
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -1724,22 +1873,6 @@ class MidyGM2 {
1724
1873
  }
1725
1874
  }
1726
1875
  }
1727
- setSostenutoPedal(channelNumber, value) {
1728
- const channel = this.channels[channelNumber];
1729
- channel.state.sostenutoPedal = value / 127;
1730
- if (64 <= value) {
1731
- const now = this.audioContext.currentTime;
1732
- const activeNotes = this.getActiveNotes(channel, now);
1733
- channel.sostenutoNotes = new Map(activeNotes);
1734
- }
1735
- else {
1736
- this.releaseSostenutoPedal(channelNumber, value);
1737
- }
1738
- }
1739
- setSoftPedal(channelNumber, softPedal) {
1740
- const channel = this.channels[channelNumber];
1741
- channel.state.softPedal = softPedal / 127;
1742
- }
1743
1876
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1744
1877
  if (maxLSB < channel.dataLSB) {
1745
1878
  channel.dataMSB++;
@@ -1796,59 +1929,49 @@ class MidyGM2 {
1796
1929
  this.channels[channelNumber].dataMSB = value;
1797
1930
  this.handleRPN(channelNumber);
1798
1931
  }
1799
- updateDetune(channel, detune) {
1800
- const now = this.audioContext.currentTime;
1801
- channel.scheduledNotes.forEach((noteList) => {
1802
- for (let i = 0; i < noteList.length; i++) {
1803
- const note = noteList[i];
1804
- if (!note)
1805
- continue;
1806
- const { bufferSource } = note;
1807
- bufferSource.detune
1808
- .cancelScheduledValues(now)
1809
- .setValueAtTime(detune, now);
1810
- }
1811
- });
1812
- }
1813
1932
  handlePitchBendRangeRPN(channelNumber) {
1814
1933
  const channel = this.channels[channelNumber];
1815
1934
  this.limitData(channel, 0, 127, 0, 99);
1816
1935
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1817
1936
  this.setPitchBendRange(channelNumber, pitchBendRange);
1818
1937
  }
1819
- setPitchBendRange(channelNumber, pitchWheelSensitivity) {
1938
+ setPitchBendRange(channelNumber, value) {
1820
1939
  const channel = this.channels[channelNumber];
1821
1940
  const state = channel.state;
1822
- state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
1823
- const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
1824
- this.updateDetune(channel, detune);
1941
+ const prev = state.pitchWheelSensitivity;
1942
+ const next = value / 128;
1943
+ state.pitchWheelSensitivity = next;
1944
+ channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1945
+ this.updateDetune(channel);
1825
1946
  this.applyVoiceParams(channel, 16);
1826
1947
  }
1827
1948
  handleFineTuningRPN(channelNumber) {
1828
1949
  const channel = this.channels[channelNumber];
1829
1950
  this.limitData(channel, 0, 127, 0, 127);
1830
- const fineTuning = (channel.dataMSB * 128 + channel.dataLSB - 8192) / 8192;
1951
+ const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
1831
1952
  this.setFineTuning(channelNumber, fineTuning);
1832
1953
  }
1833
- setFineTuning(channelNumber, fineTuning) {
1954
+ setFineTuning(channelNumber, value) {
1834
1955
  const channel = this.channels[channelNumber];
1835
- const prevFineTuning = channel.fineTuning;
1836
- channel.fineTuning = fineTuning;
1837
- const detuneChange = channel.fineTuning - prevFineTuning;
1838
- this.updateDetune(channel, detuneChange);
1956
+ const prev = channel.fineTuning;
1957
+ const next = (value - 8192) / 8.192; // cent
1958
+ channel.fineTuning = next;
1959
+ channel.detune += next - prev;
1960
+ this.updateDetune(channel);
1839
1961
  }
1840
1962
  handleCoarseTuningRPN(channelNumber) {
1841
1963
  const channel = this.channels[channelNumber];
1842
1964
  this.limitDataMSB(channel, 0, 127);
1843
- const coarseTuning = channel.dataMSB - 64;
1844
- this.setFineTuning(channelNumber, coarseTuning);
1965
+ const coarseTuning = channel.dataMSB;
1966
+ this.setCoarseTuning(channelNumber, coarseTuning);
1845
1967
  }
1846
- setCoarseTuning(channelNumber, coarseTuning) {
1968
+ setCoarseTuning(channelNumber, value) {
1847
1969
  const channel = this.channels[channelNumber];
1848
- const prevCoarseTuning = channel.coarseTuning;
1849
- channel.coarseTuning = coarseTuning;
1850
- const detuneChange = channel.coarseTuning - prevCoarseTuning;
1851
- this.updateDetune(channel, detuneChange);
1970
+ const prev = channel.coarseTuning;
1971
+ const next = (value - 64) * 100; // cent
1972
+ channel.coarseTuning = next;
1973
+ channel.detune += next - prev;
1974
+ this.updateDetune(channel);
1852
1975
  }
1853
1976
  handleModulationDepthRangeRPN(channelNumber) {
1854
1977
  const channel = this.channels[channelNumber];
@@ -1908,6 +2031,15 @@ class MidyGM2 {
1908
2031
  }
1909
2032
  handleUniversalNonRealTimeExclusiveMessage(data) {
1910
2033
  switch (data[2]) {
2034
+ case 8:
2035
+ switch (data[3]) {
2036
+ case 8:
2037
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2038
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2039
+ default:
2040
+ console.warn(`Unsupported Exclusive Message: ${data}`);
2041
+ }
2042
+ break;
1911
2043
  case 9:
1912
2044
  switch (data[3]) {
1913
2045
  case 1:
@@ -1956,26 +2088,16 @@ class MidyGM2 {
1956
2088
  return this.handleMasterFineTuningSysEx(data);
1957
2089
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
1958
2090
  return this.handleMasterCoarseTuningSysEx(data);
1959
- case 5:
2091
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
1960
2092
  return this.handleGlobalParameterControlSysEx(data);
1961
2093
  default:
1962
2094
  console.warn(`Unsupported Exclusive Message: ${data}`);
1963
2095
  }
1964
2096
  break;
1965
- case 8:
1966
- switch (data[3]) {
1967
- // case 8:
1968
- // // TODO
1969
- // return this.handleScaleOctaveTuning1ByteFormat();
1970
- default:
1971
- console.warn(`Unsupported Exclusive Message: ${data}`);
1972
- }
1973
- break;
1974
2097
  case 9:
1975
2098
  switch (data[3]) {
1976
- // case 1:
1977
- // // TODO
1978
- // return this.setChannelPressure();
2099
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2100
+ return this.handleChannelPressureSysEx(data);
1979
2101
  // case 3:
1980
2102
  // // TODO
1981
2103
  // return this.setControlChange();
@@ -1985,9 +2107,8 @@ class MidyGM2 {
1985
2107
  break;
1986
2108
  case 10:
1987
2109
  switch (data[3]) {
1988
- // case 1:
1989
- // // TODO
1990
- // return this.handleKeyBasedInstrumentControl();
2110
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2111
+ return this.handleKeyBasedInstrumentControlSysEx(data);
1991
2112
  default:
1992
2113
  console.warn(`Unsupported Exclusive Message: ${data}`);
1993
2114
  }
@@ -2011,28 +2132,26 @@ class MidyGM2 {
2011
2132
  }
2012
2133
  }
2013
2134
  handleMasterFineTuningSysEx(data) {
2014
- const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
2135
+ const fineTuning = data[5] * 128 + data[4];
2015
2136
  this.setMasterFineTuning(fineTuning);
2016
2137
  }
2017
- setMasterFineTuning(fineTuning) {
2018
- if (fineTuning < -1 && 1 < fineTuning) {
2019
- console.error("Master Fine Tuning value is out of range");
2020
- }
2021
- else {
2022
- this.masterFineTuning = fineTuning;
2023
- }
2138
+ setMasterFineTuning(value) {
2139
+ const prev = this.masterFineTuning;
2140
+ const next = (value - 8192) / 8.192; // cent
2141
+ this.masterFineTuning = next;
2142
+ channel.detune += next - prev;
2143
+ this.updateDetune(channel);
2024
2144
  }
2025
2145
  handleMasterCoarseTuningSysEx(data) {
2026
2146
  const coarseTuning = data[4];
2027
2147
  this.setMasterCoarseTuning(coarseTuning);
2028
2148
  }
2029
- setMasterCoarseTuning(coarseTuning) {
2030
- if (coarseTuning < 0 && 127 < coarseTuning) {
2031
- console.error("Master Coarse Tuning value is out of range");
2032
- }
2033
- else {
2034
- this.masterCoarseTuning = coarseTuning - 64;
2035
- }
2149
+ setMasterCoarseTuning(value) {
2150
+ const prev = this.masterCoarseTuning;
2151
+ const next = (value - 64) * 100; // cent
2152
+ this.masterCoarseTuning = next;
2153
+ channel.detune += next - prev;
2154
+ this.updateDetune(channel);
2036
2155
  }
2037
2156
  handleGlobalParameterControlSysEx(data) {
2038
2157
  if (data[7] === 1) {
@@ -2220,6 +2339,66 @@ class MidyGM2 {
2220
2339
  getChorusSendToReverb(value) {
2221
2340
  return value * 0.00787;
2222
2341
  }
2342
+ getChannelBitmap(data) {
2343
+ const bitmap = new Array(16).fill(false);
2344
+ const ff = data[4] & 0b11;
2345
+ const gg = data[5] & 0x7F;
2346
+ const hh = data[6] & 0x7F;
2347
+ for (let bit = 0; bit < 7; bit++) {
2348
+ if (hh & (1 << bit))
2349
+ bitmap[bit] = true;
2350
+ }
2351
+ for (let bit = 0; bit < 7; bit++) {
2352
+ if (gg & (1 << bit))
2353
+ bitmap[bit + 7] = true;
2354
+ }
2355
+ for (let bit = 0; bit < 2; bit++) {
2356
+ if (ff & (1 << bit))
2357
+ bitmap[bit + 14] = true;
2358
+ }
2359
+ return bitmap;
2360
+ }
2361
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2362
+ if (data.length < 18) {
2363
+ console.error("Data length is too short");
2364
+ return;
2365
+ }
2366
+ const channelBitmap = this.getChannelBitmap(data);
2367
+ for (let i = 0; i < channelBitmap.length; i++) {
2368
+ if (!channelBitmap[i])
2369
+ continue;
2370
+ for (let j = 0; j < 12; j++) {
2371
+ const value = data[j + 7] - 64; // cent
2372
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2373
+ }
2374
+ }
2375
+ }
2376
+ handleChannelPressureSysEx(data) {
2377
+ const channelNumber = data[4];
2378
+ const table = this.channels[channelNumber].pressureTable;
2379
+ for (let i = 5; i < data.length - 1; i += 2) {
2380
+ const pp = data[i];
2381
+ const rr = data[i + 1];
2382
+ table[pp] = rr;
2383
+ }
2384
+ }
2385
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2386
+ const index = keyNumber * 128 + controllerType;
2387
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2388
+ return (controlValue + 64) / 64;
2389
+ }
2390
+ handleKeyBasedInstrumentControlSysEx(data) {
2391
+ const channelNumber = data[4];
2392
+ const keyNumber = data[5];
2393
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2394
+ for (let i = 6; i < data.length - 1; i += 2) {
2395
+ const controllerType = data[i];
2396
+ const value = data[i + 1];
2397
+ const index = keyNumber * 128 + controllerType;
2398
+ table[index] = value - 64;
2399
+ }
2400
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2401
+ }
2223
2402
  handleExclusiveMessage(data) {
2224
2403
  console.warn(`Unsupported Exclusive Message: ${data}`);
2225
2404
  }
@@ -2252,6 +2431,10 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2252
2431
  writable: true,
2253
2432
  value: {
2254
2433
  currentBufferSource: null,
2434
+ detune: 0,
2435
+ scaleOctaveTuningTable: new Array(12).fill(0), // cent
2436
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2437
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2255
2438
  program: 0,
2256
2439
  bank: 121 * 128,
2257
2440
  bankMSB: 121,