@marmooo/midy 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/script/midy.js CHANGED
@@ -228,22 +228,23 @@ class Midy {
228
228
  this.totalTime = this.calcTotalTime();
229
229
  }
230
230
  setChannelAudioNodes(audioContext) {
231
- const gainNode = new GainNode(audioContext, {
232
- gain: Midy.channelSettings.volume,
233
- });
234
- const pannerNode = new StereoPannerNode(audioContext, {
235
- pan: Midy.channelSettings.pan,
236
- });
231
+ const { gainLeft, gainRight } = this.panToGain(Midy.channelSettings.pan);
232
+ const gainL = new GainNode(audioContext, { gain: gainLeft });
233
+ const gainR = new GainNode(audioContext, { gain: gainRight });
234
+ const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
235
+ gainL.connect(merger, 0, 0);
236
+ gainR.connect(merger, 0, 1);
237
+ merger.connect(this.masterGain);
237
238
  const reverbEffect = this.createReverbEffect(audioContext);
238
239
  const chorusEffect = this.createChorusEffect(audioContext);
239
240
  chorusEffect.lfo.start();
240
- reverbEffect.dryGain.connect(pannerNode);
241
- reverbEffect.wetGain.connect(pannerNode);
242
- pannerNode.connect(gainNode);
243
- gainNode.connect(this.masterGain);
241
+ reverbEffect.dryGain.connect(gainL);
242
+ reverbEffect.dryGain.connect(gainR);
243
+ reverbEffect.wetGain.connect(gainL);
244
+ reverbEffect.wetGain.connect(gainR);
244
245
  return {
245
- gainNode,
246
- pannerNode,
246
+ gainL,
247
+ gainR,
247
248
  reverbEffect,
248
249
  chorusEffect,
249
250
  };
@@ -343,7 +344,7 @@ class Midy {
343
344
  this.handleChannelPressure(event.channel, event.amount);
344
345
  break;
345
346
  case "pitchBend":
346
- this.handlePitchBend(event.channel, event.value);
347
+ this.setPitchBend(event.channel, event.value);
347
348
  break;
348
349
  case "sysEx":
349
350
  this.handleSysEx(event.data);
@@ -423,7 +424,6 @@ class Midy {
423
424
  const tmpChannels = new Array(16);
424
425
  for (let i = 0; i < tmpChannels.length; i++) {
425
426
  tmpChannels[i] = {
426
- durationTicks: new Map(),
427
427
  programNumber: -1,
428
428
  bankMSB: this.channels[i].bankMSB,
429
429
  bankLSB: this.channels[i].bankLSB,
@@ -453,16 +453,6 @@ class Midy {
453
453
  }
454
454
  channel.programNumber = 0;
455
455
  }
456
- channel.durationTicks.set(event.noteNumber, {
457
- ticks: event.ticks,
458
- noteOn: event,
459
- });
460
- break;
461
- }
462
- case "noteOff": {
463
- const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
464
- .get(event.noteNumber);
465
- noteOn.durationTicks = event.ticks - ticks;
466
456
  break;
467
457
  }
468
458
  case "controller":
@@ -497,8 +487,8 @@ class Midy {
497
487
  });
498
488
  });
499
489
  const priority = {
500
- setTempo: 0,
501
- controller: 1,
490
+ controller: 0,
491
+ sysEx: 1,
502
492
  };
503
493
  timeline.sort((a, b) => {
504
494
  if (a.ticks !== b.ticks)
@@ -636,12 +626,8 @@ class Midy {
636
626
  }
637
627
  createChorusEffect(audioContext, options = {}) {
638
628
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
639
- const lfo = new OscillatorNode(audioContext, {
640
- frequency: chorusRate,
641
- });
642
- const lfoGain = new GainNode(audioContext, {
643
- gain: chorusDepth,
644
- });
629
+ const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
630
+ const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
645
631
  const chorusGains = [];
646
632
  const delayNodes = [];
647
633
  const baseGain = 1 / chorusCount;
@@ -652,9 +638,7 @@ class Midy {
652
638
  maxDelayTime: delayTime,
653
639
  });
654
640
  delayNodes.push(delayNode);
655
- const chorusGain = new GainNode(audioContext, {
656
- gain: baseGain,
657
- });
641
+ const chorusGain = new GainNode(audioContext, { gain: baseGain });
658
642
  chorusGains.push(chorusGain);
659
643
  lfo.connect(lfoGain);
660
644
  lfoGain.connect(delayNode.delayTime);
@@ -670,14 +654,16 @@ class Midy {
670
654
  connectNoteEffects(channel, gainNode) {
671
655
  if (channel.reverb === 0) {
672
656
  if (channel.chorus === 0) { // no effect
673
- gainNode.connect(channel.pannerNode);
657
+ gainNode.connect(channel.gainL);
658
+ gainNode.connect(channel.gainR);
674
659
  }
675
660
  else { // chorus
676
661
  channel.chorusEffect.delayNodes.forEach((delayNode) => {
677
662
  gainNode.connect(delayNode);
678
663
  });
679
664
  channel.chorusEffect.chorusGains.forEach((chorusGain) => {
680
- chorusGain.connect(channel.pannerNode);
665
+ chorusGain.connect(channel.gainL);
666
+ chorusGain.connect(channel.gainR);
681
667
  });
682
668
  }
683
669
  }
@@ -716,9 +702,7 @@ class Midy {
716
702
  }
717
703
  setVolumeEnvelope(channel, note) {
718
704
  const { instrumentKey, startTime, velocity } = note;
719
- note.gainNode = new GainNode(this.audioContext, {
720
- gain: 0,
721
- });
705
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
722
706
  let volume = (velocity / 127) * channel.volume * channel.expression;
723
707
  if (volume === 0)
724
708
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
@@ -995,21 +979,15 @@ class Midy {
995
979
  }
996
980
  handlePitchBendMessage(channelNumber, lsb, msb) {
997
981
  const pitchBend = msb * 128 + lsb;
998
- this.handlePitchBend(channelNumber, pitchBend);
982
+ this.setPitchBend(channelNumber, pitchBend);
999
983
  }
1000
- handlePitchBend(channelNumber, pitchBend) {
1001
- const now = this.audioContext.currentTime;
984
+ setPitchBend(channelNumber, pitchBend) {
1002
985
  const channel = this.channels[channelNumber];
986
+ const prevPitchBend = channel.pitchBend;
1003
987
  channel.pitchBend = (pitchBend - 8192) / 8192;
1004
- const semitoneOffset = this.calcSemitoneOffset(channel);
1005
- const activeNotes = this.getActiveNotes(channel, now);
1006
- activeNotes.forEach((activeNote) => {
1007
- const { bufferSource, instrumentKey, noteNumber } = activeNote;
1008
- const playbackRate = calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
1009
- bufferSource.playbackRate
1010
- .cancelScheduledValues(now)
1011
- .setValueAtTime(playbackRate * pressure, now);
1012
- });
988
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
989
+ channel.pitchBendRange * 100;
990
+ this.updateDetune(channel, detuneChange);
1013
991
  }
1014
992
  handleControlChange(channelNumber, controller, value) {
1015
993
  switch (controller) {
@@ -1050,14 +1028,14 @@ class Midy {
1050
1028
  return this.setReverb(channelNumber, value);
1051
1029
  case 93:
1052
1030
  return this.setChorus(channelNumber, value);
1053
- case 96:
1031
+ case 96: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1054
1032
  return incrementRPNValue(channelNumber);
1055
- case 97:
1033
+ case 97: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1056
1034
  return decrementRPNValue(channelNumber);
1057
1035
  case 100:
1058
- return this.setRPNMSB(channelNumber, value);
1059
- case 101:
1060
1036
  return this.setRPNLSB(channelNumber, value);
1037
+ case 101:
1038
+ return this.setRPNMSB(channelNumber, value);
1061
1039
  case 120:
1062
1040
  return this.allSoundOff(channelNumber);
1063
1041
  case 121:
@@ -1103,12 +1081,17 @@ class Midy {
1103
1081
  channel.volume = volume / 127;
1104
1082
  this.updateChannelGain(channel);
1105
1083
  }
1084
+ panToGain(pan) {
1085
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1086
+ return {
1087
+ gainLeft: Math.cos(theta),
1088
+ gainRight: Math.sin(theta),
1089
+ };
1090
+ }
1106
1091
  setPan(channelNumber, pan) {
1107
- const now = this.audioContext.currentTime;
1108
1092
  const channel = this.channels[channelNumber];
1109
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
1110
- channel.pannerNode.pan.cancelScheduledValues(now);
1111
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
1093
+ channel.pan = pan;
1094
+ this.updateChannelGain(channel);
1112
1095
  }
1113
1096
  setExpression(channelNumber, expression) {
1114
1097
  const channel = this.channels[channelNumber];
@@ -1121,8 +1104,13 @@ class Midy {
1121
1104
  updateChannelGain(channel) {
1122
1105
  const now = this.audioContext.currentTime;
1123
1106
  const volume = channel.volume * channel.expression;
1124
- channel.gainNode.gain.cancelScheduledValues(now);
1125
- channel.gainNode.gain.setValueAtTime(volume, now);
1107
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
1108
+ channel.gainL.gain
1109
+ .cancelScheduledValues(now)
1110
+ .setValueAtTime(volume * gainLeft, now);
1111
+ channel.gainR.gain
1112
+ .cancelScheduledValues(now)
1113
+ .setValueAtTime(volume * gainRight, now);
1126
1114
  }
1127
1115
  setSustainPedal(channelNumber, value) {
1128
1116
  const isOn = value >= 64;
@@ -1184,39 +1172,58 @@ class Midy {
1184
1172
  const channel = this.channels[channelNumber];
1185
1173
  channel.vibratoDelay = vibratoDelay / 127 * 5; // 0-5sec
1186
1174
  }
1187
- incrementRPNValue(channelNumber) {
1175
+ limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1176
+ if (maxLSB < channel.dataLSB) {
1177
+ channel.dataMSB++;
1178
+ channel.dataLSB = minLSB;
1179
+ }
1180
+ else if (channel.dataLSB < 0) {
1181
+ channel.dataMSB--;
1182
+ channel.dataLSB = maxLSB;
1183
+ }
1184
+ if (maxMSB < channel.dataMSB) {
1185
+ channel.dataMSB = maxMSB;
1186
+ channel.dataLSB = maxLSB;
1187
+ }
1188
+ else if (channel.dataMSB < 0) {
1189
+ channel.dataMSB = minMSB;
1190
+ channel.dataLSB = minLSB;
1191
+ }
1192
+ }
1193
+ limitDataMSB(channel, minMSB, maxMSB) {
1194
+ if (maxMSB < channel.dataMSB) {
1195
+ channel.dataMSB = maxMSB;
1196
+ }
1197
+ else if (channel.dataMSB < 0) {
1198
+ channel.dataMSB = minMSB;
1199
+ }
1200
+ }
1201
+ // TODO: support 3-4?
1202
+ handleRPN(channelNumber, value) {
1188
1203
  const channel = this.channels[channelNumber];
1189
1204
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1190
1205
  switch (rpn) {
1191
1206
  case 0:
1192
- channel.pitchBendRange = Math.min(1, channel.pitchBendRange + 1);
1207
+ channel.dataLSB += value;
1208
+ this.handlePitchBendRangeMessage(channelNumber);
1193
1209
  break;
1194
1210
  case 1:
1195
- channel.fineTuning = Math.min(1, channel.fineTuning + 1);
1211
+ channel.dataLSB += value;
1212
+ this.handleFineTuningMessage(channelNumber);
1196
1213
  break;
1197
1214
  case 2:
1198
- channel.coarseTuning = Math.min(88, channel.coarseTuning + 1);
1215
+ channel.dataMSB += value;
1216
+ this.handleCoarseTuningMessage(channelNumber);
1199
1217
  break;
1200
1218
  default:
1201
1219
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1202
1220
  }
1203
1221
  }
1222
+ incrementRPNValue(channelNumber) {
1223
+ this.handleRPN(channelNumber, 1);
1224
+ }
1204
1225
  decrementRPNValue(channelNumber) {
1205
- const channel = this.channels[channelNumber];
1206
- const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1207
- switch (rpn) {
1208
- case 0:
1209
- channel.pitchBendRange = Math.max(-1, channel.pitchBendRange - 1);
1210
- break;
1211
- case 1:
1212
- channel.fineTuning = Math.max(-1, channel.fineTuning - 1);
1213
- break;
1214
- case 2:
1215
- channel.coarseTuning = Math.max(40, channel.coarseTuning - 1);
1216
- break;
1217
- default:
1218
- console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB}, LSB=${channel.rpnLSB}.`);
1219
- }
1226
+ this.handleRPN(channelNumber, -1);
1220
1227
  }
1221
1228
  setRPNMSB(channelNumber, value) {
1222
1229
  this.channels[channelNumber].rpnMSB = value;
@@ -1224,28 +1231,55 @@ class Midy {
1224
1231
  setRPNLSB(channelNumber, value) {
1225
1232
  this.channels[channelNumber].rpnLSB = value;
1226
1233
  }
1227
- // TODO: support 3-4?
1228
1234
  setDataEntry(channelNumber, value, isMSB) {
1229
1235
  const channel = this.channels[channelNumber];
1230
- const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1231
1236
  isMSB ? channel.dataMSB = value : channel.dataLSB = value;
1232
- const { dataMSB, dataLSB } = channel;
1233
- switch (rpn) {
1234
- case 0:
1235
- channel.pitchBendRange = dataMSB + dataLSB / 100;
1236
- break;
1237
- case 1:
1238
- channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
1239
- break;
1240
- case 2:
1241
- channel.coarseTuning = dataMSB - 64;
1242
- break;
1243
- case 5:
1244
- channel.modulationDepthRange = dataMSB + dataLSB / 128;
1245
- break;
1246
- default:
1247
- console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1248
- }
1237
+ this.handleRPN(channelNumber, 0);
1238
+ }
1239
+ updateDetune(channel, detuneChange) {
1240
+ const now = this.audioContext.currentTime;
1241
+ const activeNotes = this.getActiveNotes(channel, now);
1242
+ activeNotes.forEach((activeNote) => {
1243
+ const { bufferSource } = activeNote;
1244
+ const detune = bufferSource.detune.value + detuneChange;
1245
+ bufferSource.detune
1246
+ .cancelScheduledValues(now)
1247
+ .setValueAtTime(detune, now);
1248
+ });
1249
+ }
1250
+ handlePitchBendRangeMessage(channelNumber) {
1251
+ const channel = this.channels[channelNumber];
1252
+ this.limitData(channel, 0, 127, 0, 99);
1253
+ const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
1254
+ this.setPitchBendRange(channelNumber, pitchBendRange);
1255
+ }
1256
+ setPitchBendRange(channelNumber, pitchBendRange) {
1257
+ const channel = this.channels[channelNumber];
1258
+ const prevPitchBendRange = channel.pitchBendRange;
1259
+ channel.pitchBendRange = pitchBendRange;
1260
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
1261
+ channel.pitchBend * 100;
1262
+ this.updateDetune(channel, detuneChange);
1263
+ }
1264
+ handleFineTuningMessage(channelNumber) {
1265
+ const channel = this.channels[channelNumber];
1266
+ this.limitData(channel, 0, 127, 0, 127);
1267
+ const fineTuning = (channel.dataMSB * 128 + channel.dataLSB - 8192) / 8192;
1268
+ this.setFineTuning(channelNumber, fineTuning);
1269
+ }
1270
+ setFineTuning(channelNumber, fineTuning) {
1271
+ const channel = this.channels[channelNumber];
1272
+ channel.fineTuning = fineTuning;
1273
+ }
1274
+ handleCoarseTuningMessage(channelNumber) {
1275
+ const channel = this.channels[channelNumber];
1276
+ this.limitDataMSB(channel, 0, 127);
1277
+ const coarseTuning = channel.dataMSB - 64;
1278
+ this.setFineTuning(channelNumber, coarseTuning);
1279
+ }
1280
+ setCoarseTuning(channelNumber, coarseTuning) {
1281
+ const channel = this.channels[channelNumber];
1282
+ channel.fineTuning = coarseTuning;
1249
1283
  }
1250
1284
  allSoundOff(channelNumber) {
1251
1285
  const now = this.audioContext.currentTime;
@@ -1318,8 +1352,8 @@ class Midy {
1318
1352
  channel.bankLSB = 0;
1319
1353
  channel.bank = 0;
1320
1354
  });
1321
- this.channels[9].bankMSB = 120;
1322
- this.channels[9].bank = 120 * 128;
1355
+ this.channels[9].bankMSB = 1;
1356
+ this.channels[9].bank = 128;
1323
1357
  }
1324
1358
  GM2SystemOn() {
1325
1359
  this.channels.forEach((channel) => {
@@ -1337,9 +1371,9 @@ class Midy {
1337
1371
  case 1:
1338
1372
  return this.handleMasterVolumeSysEx(data);
1339
1373
  case 3:
1340
- return this.handleMasterFineTuning(data);
1374
+ return this.handleMasterFineTuningSysEx(data);
1341
1375
  case 4:
1342
- return this.handleMasterCoarseTuning(data);
1376
+ return this.handleMasterCoarseTuningSysEx(data);
1343
1377
  // case 5: // TODO: Global Parameter Control
1344
1378
  default:
1345
1379
  console.warn(`Unsupported Exclusive Message ${data}`);
@@ -1381,9 +1415,9 @@ class Midy {
1381
1415
  }
1382
1416
  handleMasterVolumeSysEx(data) {
1383
1417
  const volume = (data[5] * 128 + data[4]) / 16383;
1384
- this.handleMasterVolume(volume);
1418
+ this.setMasterVolume(volume);
1385
1419
  }
1386
- handleMasterVolume(volume) {
1420
+ setMasterVolume(volume) {
1387
1421
  if (volume < 0 && 1 < volume) {
1388
1422
  console.error("Master Volume is out of range");
1389
1423
  }
@@ -1395,9 +1429,9 @@ class Midy {
1395
1429
  }
1396
1430
  handleMasterFineTuningSysEx(data) {
1397
1431
  const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
1398
- this.handleMasterFineTuning(fineTuning);
1432
+ this.setMasterFineTuning(fineTuning);
1399
1433
  }
1400
- handleMasterFineTuning(fineTuning) {
1434
+ setMasterFineTuning(fineTuning) {
1401
1435
  if (fineTuning < -1 && 1 < fineTuning) {
1402
1436
  console.error("Master Fine Tuning value is out of range");
1403
1437
  }
@@ -1407,9 +1441,9 @@ class Midy {
1407
1441
  }
1408
1442
  handleMasterCoarseTuningSysEx(data) {
1409
1443
  const coarseTuning = data[4];
1410
- this.handleMasterCoarseTuning(coarseTuning);
1444
+ this.setMasterCoarseTuning(coarseTuning);
1411
1445
  }
1412
- handleMasterCoarseTuning(coarseTuning) {
1446
+ setMasterCoarseTuning(coarseTuning) {
1413
1447
  if (coarseTuning < 0 && 127 < coarseTuning) {
1414
1448
  console.error("Master Coarse Tuning value is out of range");
1415
1449
  }
@@ -1450,7 +1484,7 @@ Object.defineProperty(Midy, "channelSettings", {
1450
1484
  value: {
1451
1485
  currentBufferSource: null,
1452
1486
  volume: 100 / 127,
1453
- pan: 0,
1487
+ pan: 64,
1454
1488
  portamentoTime: 0,
1455
1489
  reverb: 0,
1456
1490
  chorus: 0,