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