@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.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,
@@ -904,16 +922,48 @@ export class Midy {
904
922
  cbToRatio(cb) {
905
923
  return Math.pow(10, cb / 200);
906
924
  }
925
+ rateToCent(rate) {
926
+ return 1200 * Math.log2(rate);
927
+ }
928
+ centToRate(cent) {
929
+ return Math.pow(2, cent / 1200);
930
+ }
907
931
  centToHz(cent) {
908
- return 8.176 * Math.pow(2, cent / 1200);
932
+ return 8.176 * this.centToRate(cent);
909
933
  }
910
- calcSemitoneOffset(channel) {
934
+ calcChannelDetune(channel) {
911
935
  const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
912
936
  const channelTuning = channel.coarseTuning + channel.fineTuning;
937
+ const tuning = masterTuning + channelTuning;
913
938
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
914
- const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
939
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
915
940
  const pitch = pitchWheel * pitchWheelSensitivity;
916
- return masterTuning + channelTuning + pitch;
941
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
942
+ const pressure = pressureDepth * channel.state.channelPressure;
943
+ return tuning + pitch + pressure;
944
+ }
945
+ calcNoteDetune(channel, note) {
946
+ return channel.scaleOctaveTuningTable[note.noteNumber % 12];
947
+ }
948
+ updateDetune(channel) {
949
+ const now = this.audioContext.currentTime;
950
+ channel.scheduledNotes.forEach((noteList) => {
951
+ for (let i = 0; i < noteList.length; i++) {
952
+ const note = noteList[i];
953
+ if (!note)
954
+ continue;
955
+ const noteDetune = this.calcNoteDetune(channel, note);
956
+ const detune = channel.detune + noteDetune;
957
+ note.bufferSource.detune
958
+ .cancelScheduledValues(now)
959
+ .setValueAtTime(detune, now);
960
+ }
961
+ });
962
+ }
963
+ getPortamentoTime(channel) {
964
+ const factor = 5 * Math.log(10) / 127;
965
+ const time = channel.state.portamentoTime;
966
+ return Math.log(time) / factor;
917
967
  }
918
968
  setPortamentoStartVolumeEnvelope(channel, note) {
919
969
  const now = this.audioContext.currentTime;
@@ -921,8 +971,8 @@ export class Midy {
921
971
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
922
972
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
923
973
  const volDelay = startTime + voiceParams.volDelay;
924
- const portamentoTime = volDelay + channel.state.portamentoTime;
925
- note.volumeNode.gain
974
+ const portamentoTime = volDelay + this.getPortamentoTime(channel);
975
+ note.volumeEnvelopeNode.gain
926
976
  .cancelScheduledValues(now)
927
977
  .setValueAtTime(0, volDelay)
928
978
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
@@ -931,13 +981,16 @@ export class Midy {
931
981
  const now = this.audioContext.currentTime;
932
982
  const state = channel.state;
933
983
  const { voiceParams, startTime } = note;
934
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
984
+ const pressureDepth = channel.pressureTable[2] / 64;
985
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
986
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
987
+ pressure;
935
988
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
936
989
  const volDelay = startTime + voiceParams.volDelay;
937
990
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
938
991
  const volHold = volAttack + voiceParams.volHold;
939
992
  const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
940
- note.volumeNode.gain
993
+ note.volumeEnvelopeNode.gain
941
994
  .cancelScheduledValues(now)
942
995
  .setValueAtTime(0, startTime)
943
996
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
@@ -945,32 +998,28 @@ export class Midy {
945
998
  .setValueAtTime(attackVolume, volHold)
946
999
  .linearRampToValueAtTime(sustainVolume, volDecay);
947
1000
  }
948
- setPlaybackRate(note) {
1001
+ setPitchEnvelope(note) {
949
1002
  const now = this.audioContext.currentTime;
1003
+ const { voiceParams } = note;
1004
+ const baseRate = voiceParams.playbackRate;
950
1005
  note.bufferSource.playbackRate
951
1006
  .cancelScheduledValues(now)
952
- .setValueAtTime(note.voiceParams.playbackRate, now);
953
- }
954
- setPitch(channel, note) {
955
- const now = this.audioContext.currentTime;
956
- const { startTime } = note;
957
- const basePitch = this.calcSemitoneOffset(channel) * 100;
958
- note.bufferSource.detune
959
- .cancelScheduledValues(now)
960
- .setValueAtTime(basePitch, startTime);
961
- const modEnvToPitch = note.voiceParams.modEnvToPitch;
1007
+ .setValueAtTime(baseRate, now);
1008
+ const modEnvToPitch = voiceParams.modEnvToPitch;
962
1009
  if (modEnvToPitch === 0)
963
1010
  return;
1011
+ const basePitch = this.rateToCent(baseRate);
964
1012
  const peekPitch = basePitch + modEnvToPitch;
1013
+ const peekRate = this.centToRate(peekPitch);
965
1014
  const modDelay = startTime + voiceParams.modDelay;
966
1015
  const modAttack = modDelay + voiceParams.modAttack;
967
1016
  const modHold = modAttack + voiceParams.modHold;
968
1017
  const modDecay = modHold + voiceParams.modDecay;
969
- note.bufferSource.detune
970
- .setValueAtTime(basePitch, modDelay)
971
- .exponentialRampToValueAtTime(peekPitch, modAttack)
972
- .setValueAtTime(peekPitch, modHold)
973
- .linearRampToValueAtTime(basePitch, modDecay);
1018
+ note.bufferSource.playbackRate
1019
+ .setValueAtTime(baseRate, modDelay)
1020
+ .exponentialRampToValueAtTime(peekRate, modAttack)
1021
+ .setValueAtTime(peekRate, modHold)
1022
+ .linearRampToValueAtTime(baseRate, modDecay);
974
1023
  }
975
1024
  clampCutoffFrequency(frequency) {
976
1025
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -983,14 +1032,17 @@ export class Midy {
983
1032
  const { voiceParams, noteNumber, startTime } = note;
984
1033
  const softPedalFactor = 1 -
985
1034
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
986
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
987
- softPedalFactor * state.brightness * 2;
1035
+ const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1036
+ const pressure = pressureDepth * channel.state.channelPressure;
1037
+ const baseCent = voiceParams.initialFilterFc + pressure;
1038
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1039
+ state.brightness * 2;
988
1040
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
989
1041
  const sustainFreq = baseFreq +
990
1042
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
991
1043
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
992
1044
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
993
- const portamentoTime = startTime + channel.state.portamentoTime;
1045
+ const portamentoTime = startTime + this.getPortamentoTime(channel);
994
1046
  const modDelay = startTime + voiceParams.modDelay;
995
1047
  note.filterNode.frequency
996
1048
  .cancelScheduledValues(now)
@@ -1035,14 +1087,14 @@ export class Midy {
1035
1087
  note.modulationDepth = new GainNode(this.audioContext);
1036
1088
  this.setModLfoToPitch(channel, note);
1037
1089
  note.volumeDepth = new GainNode(this.audioContext);
1038
- this.setModLfoToVolume(note);
1090
+ this.setModLfoToVolume(channel, note);
1039
1091
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1040
1092
  note.modulationLFO.connect(note.filterDepth);
1041
1093
  note.filterDepth.connect(note.filterNode.frequency);
1042
1094
  note.modulationLFO.connect(note.modulationDepth);
1043
1095
  note.modulationDepth.connect(note.bufferSource.detune);
1044
1096
  note.modulationLFO.connect(note.volumeDepth);
1045
- note.volumeDepth.connect(note.volumeNode.gain);
1097
+ note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
1046
1098
  }
1047
1099
  startVibrato(channel, note, startTime) {
1048
1100
  const { voiceParams } = note;
@@ -1064,6 +1116,9 @@ export class Midy {
1064
1116
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1065
1117
  note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1066
1118
  note.volumeNode = new GainNode(this.audioContext);
1119
+ note.gainL = new GainNode(this.audioContext);
1120
+ note.gainR = new GainNode(this.audioContext);
1121
+ note.volumeEnvelopeNode = new GainNode(this.audioContext);
1067
1122
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1068
1123
  type: "lowpass",
1069
1124
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
@@ -1081,9 +1136,8 @@ export class Midy {
1081
1136
  if (0 < state.vibratoDepth) {
1082
1137
  this.startVibrato(channel, note, startTime);
1083
1138
  }
1084
- this.setPlaybackRate(note);
1139
+ this.setPitchEnvelope(note);
1085
1140
  if (0 < state.modulationDepth) {
1086
- this.setPitch(channel, note);
1087
1141
  this.startModulation(channel, note, startTime);
1088
1142
  }
1089
1143
  if (this.mono && channel.currentBufferSource) {
@@ -1091,7 +1145,10 @@ export class Midy {
1091
1145
  channel.currentBufferSource = note.bufferSource;
1092
1146
  }
1093
1147
  note.bufferSource.connect(note.filterNode);
1094
- note.filterNode.connect(note.volumeNode);
1148
+ note.filterNode.connect(note.volumeEnvelopeNode);
1149
+ note.volumeEnvelopeNode.connect(note.volumeNode);
1150
+ note.volumeNode.connect(note.gainL);
1151
+ note.volumeNode.connect(note.gainR);
1095
1152
  if (0 < channel.chorusSendLevel) {
1096
1153
  this.setChorusEffectsSend(channel, note, 0);
1097
1154
  }
@@ -1122,8 +1179,8 @@ export class Midy {
1122
1179
  if (!voice)
1123
1180
  return;
1124
1181
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1125
- note.volumeNode.connect(channel.gainL);
1126
- note.volumeNode.connect(channel.gainR);
1182
+ note.gainL.connect(channel.gainL);
1183
+ note.gainR.connect(channel.gainR);
1127
1184
  if (channel.state.sostenutoPedal) {
1128
1185
  channel.sostenutoNotes.set(noteNumber, note);
1129
1186
  }
@@ -1154,7 +1211,7 @@ export class Midy {
1154
1211
  }
1155
1212
  stopNote(endTime, stopTime, scheduledNotes, index) {
1156
1213
  const note = scheduledNotes[index];
1157
- note.volumeNode.gain
1214
+ note.volumeEnvelopeNode.gain
1158
1215
  .cancelScheduledValues(endTime)
1159
1216
  .linearRampToValueAtTime(0, stopTime);
1160
1217
  note.ending = true;
@@ -1165,8 +1222,11 @@ export class Midy {
1165
1222
  note.bufferSource.onended = () => {
1166
1223
  scheduledNotes[index] = null;
1167
1224
  note.bufferSource.disconnect();
1168
- note.volumeNode.disconnect();
1169
1225
  note.filterNode.disconnect();
1226
+ note.volumeEnvelopeNode.disconnect();
1227
+ note.volumeNode.disconnect();
1228
+ note.gainL.disconnect();
1229
+ note.gainR.disconnect();
1170
1230
  if (note.modulationDepth) {
1171
1231
  note.volumeDepth.disconnect();
1172
1232
  note.modulationDepth.disconnect();
@@ -1216,12 +1276,13 @@ export class Midy {
1216
1276
  return this.stopNote(endTime, stopTime, scheduledNotes, i);
1217
1277
  }
1218
1278
  else {
1219
- const portamentoTime = endTime + state.portamentoTime;
1220
- const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1221
- const detune = note.bufferSource.detune.value + detuneChange;
1222
- note.bufferSource.detune
1279
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1280
+ const deltaNote = portamentoNoteNumber - noteNumber;
1281
+ const baseRate = note.voiceParams.playbackRate;
1282
+ const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1283
+ note.bufferSource.playbackRate
1223
1284
  .cancelScheduledValues(endTime)
1224
- .linearRampToValueAtTime(detune, portamentoTime);
1285
+ .linearRampToValueAtTime(targetRate, portamentoTime);
1225
1286
  return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1226
1287
  }
1227
1288
  }
@@ -1290,7 +1351,7 @@ export class Midy {
1290
1351
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1291
1352
  if (activeNotes.has(noteNumber)) {
1292
1353
  const activeNote = activeNotes.get(noteNumber);
1293
- const gain = activeNote.volumeNode.gain.value;
1354
+ const gain = activeNote.gainL.gain.value;
1294
1355
  activeNote.volumeNode.gain
1295
1356
  .cancelScheduledValues(now)
1296
1357
  .setValueAtTime(gain * pressure, now);
@@ -1303,22 +1364,45 @@ export class Midy {
1303
1364
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1304
1365
  channel.program = program;
1305
1366
  }
1306
- handleChannelPressure(channelNumber, pressure) {
1307
- const now = this.audioContext.currentTime;
1367
+ handleChannelPressure(channelNumber, value) {
1308
1368
  const channel = this.channels[channelNumber];
1309
- pressure /= 64;
1310
- channel.channelPressure = pressure;
1311
- const activeNotes = this.getActiveNotes(channel, now);
1312
- if (channel.channelPressure.amplitudeControl !== 1) {
1313
- activeNotes.forEach((activeNote) => {
1314
- const gain = activeNote.volumeNode.gain.value;
1315
- activeNote.volumeNode.gain
1316
- .cancelScheduledValues(now)
1317
- .setValueAtTime(gain * pressure, now);
1318
- });
1369
+ const prev = channel.state.channelPressure;
1370
+ const next = value / 127;
1371
+ channel.state.channelPressure = next;
1372
+ if (channel.pressureTable[0] !== 64) {
1373
+ const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1374
+ channel.detune += pressureDepth * (next - prev);
1319
1375
  }
1376
+ channel.scheduledNotes.forEach((noteList) => {
1377
+ for (let i = 0; i < noteList.length; i++) {
1378
+ const note = noteList[i];
1379
+ if (!note)
1380
+ continue;
1381
+ this.setChannelPressure(channel, note);
1382
+ }
1383
+ });
1320
1384
  // this.applyVoiceParams(channel, 13);
1321
1385
  }
1386
+ setChannelPressure(channel, note) {
1387
+ if (channel.pressureTable[0] !== 64) {
1388
+ this.updateDetune(channel);
1389
+ }
1390
+ if (channel.pressureTable[1] !== 64 && !note.portamento) {
1391
+ this.setFilterEnvelope(channel, note);
1392
+ }
1393
+ if (channel.pressureTable[2] !== 64 && !note.portamento) {
1394
+ this.setVolumeEnvelope(channel, note);
1395
+ }
1396
+ if (channel.pressureTable[3] !== 0) {
1397
+ this.setModLfoToPitch(channel, note);
1398
+ }
1399
+ if (channel.pressureTable[4] !== 0) {
1400
+ this.setModLfoToFilterFc(channel, note);
1401
+ }
1402
+ if (channel.pressureTable[5] !== 0) {
1403
+ this.setModLfoToVolume(channel, note);
1404
+ }
1405
+ }
1322
1406
  handlePitchBendMessage(channelNumber, lsb, msb) {
1323
1407
  const pitchBend = msb * 128 + lsb;
1324
1408
  this.setPitchBend(channelNumber, pitchBend);
@@ -1326,98 +1410,107 @@ export class Midy {
1326
1410
  setPitchBend(channelNumber, value) {
1327
1411
  const channel = this.channels[channelNumber];
1328
1412
  const state = channel.state;
1413
+ const prev = state.pitchWheel * 2 - 1;
1414
+ const next = (value - 8192) / 8192;
1329
1415
  state.pitchWheel = value / 16383;
1330
- const pitchWheel = (value - 8192) / 8192;
1331
- const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
1332
- this.updateDetune(channel, detuneChange);
1416
+ channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1417
+ this.updateDetune(channel);
1333
1418
  this.applyVoiceParams(channel, 14);
1334
1419
  }
1335
1420
  setModLfoToPitch(channel, note) {
1336
1421
  const now = this.audioContext.currentTime;
1337
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1338
- const modulationDepth = Math.abs(modLfoToPitch) +
1422
+ const pressureDepth = channel.pressureTable[3] / 127 * 600;
1423
+ const pressure = pressureDepth * channel.state.channelPressure;
1424
+ const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1425
+ const baseDepth = Math.abs(modLfoToPitch) +
1339
1426
  channel.state.modulationDepth;
1340
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
1427
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1341
1428
  note.modulationDepth.gain
1342
1429
  .cancelScheduledValues(now)
1343
- .setValueAtTime(modulationDepth * modulationDepthSign, now);
1430
+ .setValueAtTime(modulationDepth, now);
1344
1431
  }
1345
- setModLfoToVolume(note) {
1432
+ setVibLfoToPitch(channel, note) {
1433
+ const now = this.audioContext.currentTime;
1434
+ const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1435
+ const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1436
+ 2;
1437
+ const vibratoDepthSign = 0 < vibLfoToPitch;
1438
+ note.vibratoDepth.gain
1439
+ .cancelScheduledValues(now)
1440
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1441
+ }
1442
+ setModLfoToFilterFc(channel, note) {
1443
+ const now = this.audioContext.currentTime;
1444
+ const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1445
+ const pressure = pressureDepth * channel.state.channelPressure;
1446
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1447
+ note.filterDepth.gain
1448
+ .cancelScheduledValues(now)
1449
+ .setValueAtTime(modLfoToFilterFc, now);
1450
+ }
1451
+ setModLfoToVolume(channel, note) {
1346
1452
  const now = this.audioContext.currentTime;
1347
1453
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1348
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1349
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
1454
+ const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1455
+ const pressureDepth = channel.pressureTable[5] / 127;
1456
+ const pressure = 1 + pressureDepth * channel.state.channelPressure;
1457
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1350
1458
  note.volumeDepth.gain
1351
1459
  .cancelScheduledValues(now)
1352
- .setValueAtTime(volumeDepth * volumeDepthSign, now);
1460
+ .setValueAtTime(volumeDepth, now);
1353
1461
  }
1354
- setChorusEffectsSend(note, prevValue) {
1462
+ setReverbEffectsSend(channel, note, prevValue) {
1355
1463
  if (0 < prevValue) {
1356
- if (0 < note.voiceParams.chorusEffectsSend) {
1464
+ if (0 < note.voiceParams.reverbEffectsSend) {
1357
1465
  const now = this.audioContext.currentTime;
1358
- const value = note.voiceParams.chorusEffectsSend;
1359
- note.chorusEffectsSend.gain
1466
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1467
+ const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1468
+ note.reverbEffectsSend.gain
1360
1469
  .cancelScheduledValues(now)
1361
1470
  .setValueAtTime(value, now);
1362
1471
  }
1363
1472
  else {
1364
- note.chorusEffectsSend.disconnect();
1473
+ note.reverbEffectsSend.disconnect();
1365
1474
  }
1366
1475
  }
1367
1476
  else {
1368
- if (0 < note.voiceParams.chorusEffectsSend) {
1369
- if (!note.chorusEffectsSend) {
1370
- note.chorusEffectsSend = new GainNode(this.audioContext, {
1371
- gain: note.voiceParams.chorusEffectsSend,
1477
+ if (0 < note.voiceParams.reverbEffectsSend) {
1478
+ if (!note.reverbEffectsSend) {
1479
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1480
+ gain: note.voiceParams.reverbEffectsSend,
1372
1481
  });
1373
- note.volumeNode.connect(note.chorusEffectsSend);
1482
+ note.volumeNode.connect(note.reverbEffectsSend);
1374
1483
  }
1375
- note.chorusEffectsSend.connect(this.chorusEffect.input);
1484
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1376
1485
  }
1377
1486
  }
1378
1487
  }
1379
- setReverbEffectsSend(note, prevValue) {
1488
+ setChorusEffectsSend(channel, note, prevValue) {
1380
1489
  if (0 < prevValue) {
1381
- if (0 < note.voiceParams.reverbEffectsSend) {
1490
+ if (0 < note.voiceParams.chorusEffectsSend) {
1382
1491
  const now = this.audioContext.currentTime;
1383
- const value = note.voiceParams.reverbEffectsSend;
1384
- note.reverbEffectsSend.gain
1492
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1493
+ const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1494
+ note.chorusEffectsSend.gain
1385
1495
  .cancelScheduledValues(now)
1386
1496
  .setValueAtTime(value, now);
1387
1497
  }
1388
1498
  else {
1389
- note.reverbEffectsSend.disconnect();
1499
+ note.chorusEffectsSend.disconnect();
1390
1500
  }
1391
1501
  }
1392
1502
  else {
1393
- if (0 < note.voiceParams.reverbEffectsSend) {
1394
- if (!note.reverbEffectsSend) {
1395
- note.reverbEffectsSend = new GainNode(this.audioContext, {
1396
- gain: note.voiceParams.reverbEffectsSend,
1503
+ if (0 < note.voiceParams.chorusEffectsSend) {
1504
+ if (!note.chorusEffectsSend) {
1505
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1506
+ gain: note.voiceParams.chorusEffectsSend,
1397
1507
  });
1398
- note.volumeNode.connect(note.reverbEffectsSend);
1508
+ note.volumeNode.connect(note.chorusEffectsSend);
1399
1509
  }
1400
- note.reverbEffectsSend.connect(this.reverbEffect.input);
1510
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1401
1511
  }
1402
1512
  }
1403
1513
  }
1404
- setVibLfoToPitch(channel, note) {
1405
- const now = this.audioContext.currentTime;
1406
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1407
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1408
- 2;
1409
- const vibratoDepthSign = 0 < vibLfoToPitch;
1410
- note.vibratoDepth.gain
1411
- .cancelScheduledValues(now)
1412
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1413
- }
1414
- setModLfoToFilterFc(note) {
1415
- const now = this.audioContext.currentTime;
1416
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
1417
- note.filterDepth.gain
1418
- .cancelScheduledValues(now)
1419
- .setValueAtTime(modLfoToFilterFc, now);
1420
- }
1421
1514
  setDelayModLFO(note) {
1422
1515
  const now = this.audioContext.currentTime;
1423
1516
  const startTime = note.startTime;
@@ -1447,18 +1540,20 @@ export class Midy {
1447
1540
  }
1448
1541
  },
1449
1542
  modLfoToFilterFc: (channel, note, _prevValue) => {
1450
- if (0 < channel.state.modulationDepth)
1451
- this.setModLfoToFilterFc(note);
1543
+ if (0 < channel.state.modulationDepth) {
1544
+ this.setModLfoToFilterFc(channel, note);
1545
+ }
1452
1546
  },
1453
1547
  modLfoToVolume: (channel, note) => {
1454
- if (0 < channel.state.modulationDepth)
1455
- this.setModLfoToVolume(note);
1548
+ if (0 < channel.state.modulationDepth) {
1549
+ this.setModLfoToVolume(channel, note);
1550
+ }
1456
1551
  },
1457
- chorusEffectsSend: (_channel, note, prevValue) => {
1458
- this.setChorusEffectsSend(note, prevValue);
1552
+ chorusEffectsSend: (channel, note, prevValue) => {
1553
+ this.setChorusEffectsSend(channel, note, prevValue);
1459
1554
  },
1460
- reverbEffectsSend: (_channel, note, prevValue) => {
1461
- this.setReverbEffectsSend(note, prevValue);
1555
+ reverbEffectsSend: (channel, note, prevValue) => {
1556
+ this.setReverbEffectsSend(channel, note, prevValue);
1462
1557
  },
1463
1558
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
1464
1559
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
@@ -1526,7 +1621,7 @@ export class Midy {
1526
1621
  else {
1527
1622
  this.setFilterEnvelope(channel, note);
1528
1623
  }
1529
- this.setPitch(channel, note);
1624
+ this.setPitchEnvelope(note);
1530
1625
  }
1531
1626
  else if (volumeEnvelopeKeySet.has(key)) {
1532
1627
  if (appliedVolumeEnvelope)
@@ -1608,7 +1703,7 @@ export class Midy {
1608
1703
  note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
1609
1704
  }
1610
1705
  else {
1611
- this.setPitch(channel, note);
1706
+ this.setPitchEnvelope(note);
1612
1707
  this.startModulation(channel, note, now);
1613
1708
  }
1614
1709
  }
@@ -1625,10 +1720,27 @@ export class Midy {
1625
1720
  const factor = 5 * Math.log(10) / 127;
1626
1721
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1627
1722
  }
1723
+ setKeyBasedVolume(channel) {
1724
+ const now = this.audioContext.currentTime;
1725
+ channel.scheduledNotes.forEach((noteList) => {
1726
+ for (let i = 0; i < noteList.length; i++) {
1727
+ const note = noteList[i];
1728
+ if (!note)
1729
+ continue;
1730
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1731
+ if (keyBasedValue === 0)
1732
+ continue;
1733
+ note.volumeNode.gain
1734
+ .cancelScheduledValues(now)
1735
+ .setValueAtTime(1 + keyBasedValue, now);
1736
+ }
1737
+ });
1738
+ }
1628
1739
  setVolume(channelNumber, volume) {
1629
1740
  const channel = this.channels[channelNumber];
1630
1741
  channel.state.volume = volume / 127;
1631
1742
  this.updateChannelVolume(channel);
1743
+ this.setKeyBasedVolume(channel);
1632
1744
  }
1633
1745
  panToGain(pan) {
1634
1746
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1637,10 +1749,31 @@ export class Midy {
1637
1749
  gainRight: Math.sin(theta),
1638
1750
  };
1639
1751
  }
1752
+ setKeyBasedPan(channel) {
1753
+ const now = this.audioContext.currentTime;
1754
+ channel.scheduledNotes.forEach((noteList) => {
1755
+ for (let i = 0; i < noteList.length; i++) {
1756
+ const note = noteList[i];
1757
+ if (!note)
1758
+ continue;
1759
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1760
+ if (keyBasedValue === 0)
1761
+ continue;
1762
+ const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1763
+ note.gainL.gain
1764
+ .cancelScheduledValues(now)
1765
+ .setValueAtTime(gainLeft, now);
1766
+ note.gainR.gain
1767
+ .cancelScheduledValues(now)
1768
+ .setValueAtTime(gainRight, now);
1769
+ }
1770
+ });
1771
+ }
1640
1772
  setPan(channelNumber, pan) {
1641
1773
  const channel = this.channels[channelNumber];
1642
1774
  channel.state.pan = pan / 127;
1643
1775
  this.updateChannelVolume(channel);
1776
+ this.setKeyBasedPan(channel);
1644
1777
  }
1645
1778
  setExpression(channelNumber, expression) {
1646
1779
  const channel = this.channels[channelNumber];
@@ -1675,88 +1808,6 @@ export class Midy {
1675
1808
  setPortamento(channelNumber, value) {
1676
1809
  this.channels[channelNumber].state.portamento = value / 127;
1677
1810
  }
1678
- setReverbSendLevel(channelNumber, reverbSendLevel) {
1679
- const channel = this.channels[channelNumber];
1680
- const state = channel.state;
1681
- const reverbEffect = this.reverbEffect;
1682
- if (0 < state.reverbSendLevel) {
1683
- if (0 < reverbSendLevel) {
1684
- const now = this.audioContext.currentTime;
1685
- state.reverbSendLevel = reverbSendLevel / 127;
1686
- reverbEffect.input.gain.cancelScheduledValues(now);
1687
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1688
- }
1689
- else {
1690
- channel.scheduledNotes.forEach((noteList) => {
1691
- for (let i = 0; i < noteList.length; i++) {
1692
- const note = noteList[i];
1693
- if (!note)
1694
- continue;
1695
- if (note.voiceParams.reverbEffectsSend <= 0)
1696
- continue;
1697
- note.reverbEffectsSend.disconnect();
1698
- }
1699
- });
1700
- }
1701
- }
1702
- else {
1703
- if (0 < reverbSendLevel) {
1704
- const now = this.audioContext.currentTime;
1705
- channel.scheduledNotes.forEach((noteList) => {
1706
- for (let i = 0; i < noteList.length; i++) {
1707
- const note = noteList[i];
1708
- if (!note)
1709
- continue;
1710
- this.setReverbEffectsSend(note, 0);
1711
- }
1712
- });
1713
- state.reverbSendLevel = reverbSendLevel / 127;
1714
- reverbEffect.input.gain.cancelScheduledValues(now);
1715
- reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1716
- }
1717
- }
1718
- }
1719
- setChorusSendLevel(channelNumber, chorusSendLevel) {
1720
- const channel = this.channels[channelNumber];
1721
- const state = channel.state;
1722
- const chorusEffect = this.chorusEffect;
1723
- if (0 < state.chorusSendLevel) {
1724
- if (0 < chorusSendLevel) {
1725
- const now = this.audioContext.currentTime;
1726
- state.chorusSendLevel = chorusSendLevel / 127;
1727
- chorusEffect.input.gain.cancelScheduledValues(now);
1728
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1729
- }
1730
- else {
1731
- channel.scheduledNotes.forEach((noteList) => {
1732
- for (let i = 0; i < noteList.length; i++) {
1733
- const note = noteList[i];
1734
- if (!note)
1735
- continue;
1736
- if (note.voiceParams.chorusEffectsSend <= 0)
1737
- continue;
1738
- note.chorusEffectsSend.disconnect();
1739
- }
1740
- });
1741
- }
1742
- }
1743
- else {
1744
- if (0 < chorusSendLevel) {
1745
- const now = this.audioContext.currentTime;
1746
- channel.scheduledNotes.forEach((noteList) => {
1747
- for (let i = 0; i < noteList.length; i++) {
1748
- const note = noteList[i];
1749
- if (!note)
1750
- continue;
1751
- this.setChorusEffectsSend(note, 0);
1752
- }
1753
- });
1754
- state.chorusSendLevel = chorusSendLevel / 127;
1755
- chorusEffect.input.gain.cancelScheduledValues(now);
1756
- chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1757
- }
1758
- }
1759
- }
1760
1811
  setSostenutoPedal(channelNumber, value) {
1761
1812
  const channel = this.channels[channelNumber];
1762
1813
  channel.state.sostenutoPedal = value / 127;
@@ -1852,6 +1903,88 @@ export class Midy {
1852
1903
  const channel = this.channels[channelNumber];
1853
1904
  channel.state.vibratoDelay = vibratoDelay / 64;
1854
1905
  }
1906
+ setReverbSendLevel(channelNumber, reverbSendLevel) {
1907
+ const channel = this.channels[channelNumber];
1908
+ const state = channel.state;
1909
+ const reverbEffect = this.reverbEffect;
1910
+ if (0 < state.reverbSendLevel) {
1911
+ if (0 < reverbSendLevel) {
1912
+ const now = this.audioContext.currentTime;
1913
+ state.reverbSendLevel = reverbSendLevel / 127;
1914
+ reverbEffect.input.gain.cancelScheduledValues(now);
1915
+ reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1916
+ }
1917
+ else {
1918
+ channel.scheduledNotes.forEach((noteList) => {
1919
+ for (let i = 0; i < noteList.length; i++) {
1920
+ const note = noteList[i];
1921
+ if (!note)
1922
+ continue;
1923
+ if (note.voiceParams.reverbEffectsSend <= 0)
1924
+ continue;
1925
+ note.reverbEffectsSend.disconnect();
1926
+ }
1927
+ });
1928
+ }
1929
+ }
1930
+ else {
1931
+ if (0 < reverbSendLevel) {
1932
+ const now = this.audioContext.currentTime;
1933
+ channel.scheduledNotes.forEach((noteList) => {
1934
+ for (let i = 0; i < noteList.length; i++) {
1935
+ const note = noteList[i];
1936
+ if (!note)
1937
+ continue;
1938
+ this.setReverbEffectsSend(channel, note, 0);
1939
+ }
1940
+ });
1941
+ state.reverbSendLevel = reverbSendLevel / 127;
1942
+ reverbEffect.input.gain.cancelScheduledValues(now);
1943
+ reverbEffect.input.gain.setValueAtTime(state.reverbSendLevel, now);
1944
+ }
1945
+ }
1946
+ }
1947
+ setChorusSendLevel(channelNumber, chorusSendLevel) {
1948
+ const channel = this.channels[channelNumber];
1949
+ const state = channel.state;
1950
+ const chorusEffect = this.chorusEffect;
1951
+ if (0 < state.chorusSendLevel) {
1952
+ if (0 < chorusSendLevel) {
1953
+ const now = this.audioContext.currentTime;
1954
+ state.chorusSendLevel = chorusSendLevel / 127;
1955
+ chorusEffect.input.gain.cancelScheduledValues(now);
1956
+ chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1957
+ }
1958
+ else {
1959
+ channel.scheduledNotes.forEach((noteList) => {
1960
+ for (let i = 0; i < noteList.length; i++) {
1961
+ const note = noteList[i];
1962
+ if (!note)
1963
+ continue;
1964
+ if (note.voiceParams.chorusEffectsSend <= 0)
1965
+ continue;
1966
+ note.chorusEffectsSend.disconnect();
1967
+ }
1968
+ });
1969
+ }
1970
+ }
1971
+ else {
1972
+ if (0 < chorusSendLevel) {
1973
+ const now = this.audioContext.currentTime;
1974
+ channel.scheduledNotes.forEach((noteList) => {
1975
+ for (let i = 0; i < noteList.length; i++) {
1976
+ const note = noteList[i];
1977
+ if (!note)
1978
+ continue;
1979
+ this.setChorusEffectsSend(channel, note, 0);
1980
+ }
1981
+ });
1982
+ state.chorusSendLevel = chorusSendLevel / 127;
1983
+ chorusEffect.input.gain.cancelScheduledValues(now);
1984
+ chorusEffect.input.gain.setValueAtTime(state.chorusSendLevel, now);
1985
+ }
1986
+ }
1987
+ }
1855
1988
  limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1856
1989
  if (maxLSB < channel.dataLSB) {
1857
1990
  channel.dataMSB++;
@@ -1920,59 +2053,49 @@ export class Midy {
1920
2053
  this.channels[channelNumber].dataMSB = value;
1921
2054
  this.handleRPN(channelNumber, 0);
1922
2055
  }
1923
- updateDetune(channel, detune) {
1924
- const now = this.audioContext.currentTime;
1925
- channel.scheduledNotes.forEach((noteList) => {
1926
- for (let i = 0; i < noteList.length; i++) {
1927
- const note = noteList[i];
1928
- if (!note)
1929
- continue;
1930
- const { bufferSource } = note;
1931
- bufferSource.detune
1932
- .cancelScheduledValues(now)
1933
- .setValueAtTime(detune, now);
1934
- }
1935
- });
1936
- }
1937
2056
  handlePitchBendRangeRPN(channelNumber) {
1938
2057
  const channel = this.channels[channelNumber];
1939
2058
  this.limitData(channel, 0, 127, 0, 99);
1940
2059
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1941
2060
  this.setPitchBendRange(channelNumber, pitchBendRange);
1942
2061
  }
1943
- setPitchBendRange(channelNumber, pitchWheelSensitivity) {
2062
+ setPitchBendRange(channelNumber, value) {
1944
2063
  const channel = this.channels[channelNumber];
1945
2064
  const state = channel.state;
1946
- state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
1947
- const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
1948
- this.updateDetune(channel, detune);
2065
+ const prev = state.pitchWheelSensitivity;
2066
+ const next = value / 128;
2067
+ state.pitchWheelSensitivity = next;
2068
+ channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2069
+ this.updateDetune(channel);
1949
2070
  this.applyVoiceParams(channel, 16);
1950
2071
  }
1951
2072
  handleFineTuningRPN(channelNumber) {
1952
2073
  const channel = this.channels[channelNumber];
1953
2074
  this.limitData(channel, 0, 127, 0, 127);
1954
- const fineTuning = (channel.dataMSB * 128 + channel.dataLSB - 8192) / 8192;
2075
+ const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
1955
2076
  this.setFineTuning(channelNumber, fineTuning);
1956
2077
  }
1957
- setFineTuning(channelNumber, fineTuning) {
2078
+ setFineTuning(channelNumber, value) {
1958
2079
  const channel = this.channels[channelNumber];
1959
- const prevFineTuning = channel.fineTuning;
1960
- channel.fineTuning = fineTuning;
1961
- const detuneChange = channel.fineTuning - prevFineTuning;
1962
- this.updateDetune(channel, detuneChange);
2080
+ const prev = channel.fineTuning;
2081
+ const next = (value - 8192) / 8.192; // cent
2082
+ channel.fineTuning = next;
2083
+ channel.detune += next - prev;
2084
+ this.updateDetune(channel);
1963
2085
  }
1964
2086
  handleCoarseTuningRPN(channelNumber) {
1965
2087
  const channel = this.channels[channelNumber];
1966
2088
  this.limitDataMSB(channel, 0, 127);
1967
- const coarseTuning = channel.dataMSB - 64;
1968
- this.setFineTuning(channelNumber, coarseTuning);
2089
+ const coarseTuning = channel.dataMSB;
2090
+ this.setCoarseTuning(channelNumber, coarseTuning);
1969
2091
  }
1970
- setCoarseTuning(channelNumber, coarseTuning) {
2092
+ setCoarseTuning(channelNumber, value) {
1971
2093
  const channel = this.channels[channelNumber];
1972
- const prevCoarseTuning = channel.coarseTuning;
1973
- channel.coarseTuning = coarseTuning;
1974
- const detuneChange = channel.coarseTuning - prevCoarseTuning;
1975
- this.updateDetune(channel, detuneChange);
2094
+ const prev = channel.coarseTuning;
2095
+ const next = (value - 64) * 100; // cent
2096
+ channel.coarseTuning = next;
2097
+ channel.detune += next - prev;
2098
+ this.updateDetune(channel);
1976
2099
  }
1977
2100
  handleModulationDepthRangeRPN(channelNumber) {
1978
2101
  const channel = this.channels[channelNumber];
@@ -2032,6 +2155,15 @@ export class Midy {
2032
2155
  }
2033
2156
  handleUniversalNonRealTimeExclusiveMessage(data) {
2034
2157
  switch (data[2]) {
2158
+ case 8:
2159
+ switch (data[3]) {
2160
+ case 8:
2161
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2162
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2163
+ default:
2164
+ console.warn(`Unsupported Exclusive Message: ${data}`);
2165
+ }
2166
+ break;
2035
2167
  case 9:
2036
2168
  switch (data[3]) {
2037
2169
  case 1:
@@ -2080,7 +2212,7 @@ export class Midy {
2080
2212
  return this.handleMasterFineTuningSysEx(data);
2081
2213
  case 4: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca25.pdf
2082
2214
  return this.handleMasterCoarseTuningSysEx(data);
2083
- case 5:
2215
+ case 5: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca24.pdf
2084
2216
  return this.handleGlobalParameterControlSysEx(data);
2085
2217
  default:
2086
2218
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2088,18 +2220,17 @@ export class Midy {
2088
2220
  break;
2089
2221
  case 8:
2090
2222
  switch (data[3]) {
2091
- // case 8:
2092
- // // TODO
2093
- // return this.handleScaleOctaveTuning1ByteFormat();
2223
+ case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2224
+ // TODO: realtime
2225
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2094
2226
  default:
2095
2227
  console.warn(`Unsupported Exclusive Message: ${data}`);
2096
2228
  }
2097
2229
  break;
2098
2230
  case 9:
2099
2231
  switch (data[3]) {
2100
- // case 1:
2101
- // // TODO
2102
- // return this.setChannelPressure();
2232
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2233
+ return this.handleChannelPressureSysEx(data);
2103
2234
  // case 3:
2104
2235
  // // TODO
2105
2236
  // return this.setControlChange();
@@ -2109,9 +2240,8 @@ export class Midy {
2109
2240
  break;
2110
2241
  case 10:
2111
2242
  switch (data[3]) {
2112
- // case 1:
2113
- // // TODO
2114
- // return this.handleKeyBasedInstrumentControl();
2243
+ case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca23.pdf
2244
+ return this.handleKeyBasedInstrumentControlSysEx(data);
2115
2245
  default:
2116
2246
  console.warn(`Unsupported Exclusive Message: ${data}`);
2117
2247
  }
@@ -2135,28 +2265,26 @@ export class Midy {
2135
2265
  }
2136
2266
  }
2137
2267
  handleMasterFineTuningSysEx(data) {
2138
- const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
2268
+ const fineTuning = data[5] * 128 + data[4];
2139
2269
  this.setMasterFineTuning(fineTuning);
2140
2270
  }
2141
- setMasterFineTuning(fineTuning) {
2142
- if (fineTuning < -1 && 1 < fineTuning) {
2143
- console.error("Master Fine Tuning value is out of range");
2144
- }
2145
- else {
2146
- this.masterFineTuning = fineTuning;
2147
- }
2271
+ setMasterFineTuning(value) {
2272
+ const prev = this.masterFineTuning;
2273
+ const next = (value - 8192) / 8.192; // cent
2274
+ this.masterFineTuning = next;
2275
+ channel.detune += next - prev;
2276
+ this.updateDetune(channel);
2148
2277
  }
2149
2278
  handleMasterCoarseTuningSysEx(data) {
2150
2279
  const coarseTuning = data[4];
2151
2280
  this.setMasterCoarseTuning(coarseTuning);
2152
2281
  }
2153
- setMasterCoarseTuning(coarseTuning) {
2154
- if (coarseTuning < 0 && 127 < coarseTuning) {
2155
- console.error("Master Coarse Tuning value is out of range");
2156
- }
2157
- else {
2158
- this.masterCoarseTuning = coarseTuning - 64;
2159
- }
2282
+ setMasterCoarseTuning(value) {
2283
+ const prev = this.masterCoarseTuning;
2284
+ const next = (value - 64) * 100; // cent
2285
+ this.masterCoarseTuning = next;
2286
+ channel.detune += next - prev;
2287
+ this.updateDetune(channel);
2160
2288
  }
2161
2289
  handleGlobalParameterControlSysEx(data) {
2162
2290
  if (data[7] === 1) {
@@ -2344,6 +2472,66 @@ export class Midy {
2344
2472
  getChorusSendToReverb(value) {
2345
2473
  return value * 0.00787;
2346
2474
  }
2475
+ getChannelBitmap(data) {
2476
+ const bitmap = new Array(16).fill(false);
2477
+ const ff = data[4] & 0b11;
2478
+ const gg = data[5] & 0x7F;
2479
+ const hh = data[6] & 0x7F;
2480
+ for (let bit = 0; bit < 7; bit++) {
2481
+ if (hh & (1 << bit))
2482
+ bitmap[bit] = true;
2483
+ }
2484
+ for (let bit = 0; bit < 7; bit++) {
2485
+ if (gg & (1 << bit))
2486
+ bitmap[bit + 7] = true;
2487
+ }
2488
+ for (let bit = 0; bit < 2; bit++) {
2489
+ if (ff & (1 << bit))
2490
+ bitmap[bit + 14] = true;
2491
+ }
2492
+ return bitmap;
2493
+ }
2494
+ handleScaleOctaveTuning1ByteFormatSysEx(data) {
2495
+ if (data.length < 18) {
2496
+ console.error("Data length is too short");
2497
+ return;
2498
+ }
2499
+ const channelBitmap = this.getChannelBitmap(data);
2500
+ for (let i = 0; i < channelBitmap.length; i++) {
2501
+ if (!channelBitmap[i])
2502
+ continue;
2503
+ for (let j = 0; j < 12; j++) {
2504
+ const value = data[j + 7] - 64; // cent
2505
+ this.channels[i].scaleOctaveTuningTable[j] = value;
2506
+ }
2507
+ }
2508
+ }
2509
+ handleChannelPressureSysEx(data) {
2510
+ const channelNumber = data[4];
2511
+ const table = this.channels[channelNumber].pressureTable;
2512
+ for (let i = 5; i < data.length - 1; i += 2) {
2513
+ const pp = data[i];
2514
+ const rr = data[i + 1];
2515
+ table[pp] = rr;
2516
+ }
2517
+ }
2518
+ getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2519
+ const index = keyNumber * 128 + controllerType;
2520
+ const controlValue = channel.keyBasedInstrumentControlTable[index];
2521
+ return (controlValue + 64) / 64;
2522
+ }
2523
+ handleKeyBasedInstrumentControlSysEx(data) {
2524
+ const channelNumber = data[4];
2525
+ const keyNumber = data[5];
2526
+ const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2527
+ for (let i = 6; i < data.length - 1; i += 2) {
2528
+ const controllerType = data[i];
2529
+ const value = data[i + 1];
2530
+ const index = keyNumber * 128 + controllerType;
2531
+ table[index] = value - 64;
2532
+ }
2533
+ this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127);
2534
+ }
2347
2535
  handleExclusiveMessage(data) {
2348
2536
  console.warn(`Unsupported Exclusive Message: ${data}`);
2349
2537
  }
@@ -2375,6 +2563,10 @@ Object.defineProperty(Midy, "channelSettings", {
2375
2563
  writable: true,
2376
2564
  value: {
2377
2565
  currentBufferSource: null,
2566
+ detune: 0,
2567
+ scaleOctaveTuningTable: new Array(12).fill(0), // cent
2568
+ pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2569
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2378
2570
  program: 0,
2379
2571
  bank: 121 * 128,
2380
2572
  bankMSB: 121,