@marmooo/midy 0.1.5 → 0.1.6

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
@@ -252,12 +252,14 @@ class Midy {
252
252
  addSoundFont(soundFont) {
253
253
  const index = this.soundFonts.length;
254
254
  this.soundFonts.push(soundFont);
255
- soundFont.parsed.presetHeaders.forEach((presetHeader) => {
255
+ const presetHeaders = soundFont.parsed.presetHeaders;
256
+ for (let i = 0; i < presetHeaders.length; i++) {
257
+ const presetHeader = presetHeaders[i];
256
258
  if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
257
259
  const banks = this.soundFontTable[presetHeader.preset];
258
260
  banks.set(presetHeader.bank, index);
259
261
  }
260
- });
262
+ }
261
263
  }
262
264
  async loadSoundFont(soundFontUrl) {
263
265
  const response = await fetch(soundFontUrl);
@@ -309,27 +311,25 @@ class Midy {
309
311
  return channels;
310
312
  }
311
313
  async createNoteBuffer(instrumentKey, isSF3) {
314
+ const sampleStart = instrumentKey.start;
312
315
  const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
313
316
  if (isSF3) {
314
- const sample = new Uint8Array(instrumentKey.sample.length);
315
- sample.set(instrumentKey.sample);
317
+ const sample = instrumentKey.sample.slice(sampleStart, sampleEnd);
316
318
  const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
317
- for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
318
- const channelData = audioBuffer.getChannelData(channel);
319
- channelData.set(channelData.subarray(0, sampleEnd));
320
- }
321
319
  return audioBuffer;
322
320
  }
323
321
  else {
324
- const sample = instrumentKey.sample.subarray(0, sampleEnd);
325
- const floatSample = this.convertToFloat32Array(sample);
322
+ const sample = instrumentKey.sample.subarray(sampleStart, sampleEnd);
326
323
  const audioBuffer = new AudioBuffer({
327
324
  numberOfChannels: 1,
328
325
  length: sample.length,
329
326
  sampleRate: instrumentKey.sampleRate,
330
327
  });
331
328
  const channelData = audioBuffer.getChannelData(0);
332
- channelData.set(floatSample);
329
+ const int16Array = new Int16Array(sample.buffer);
330
+ for (let i = 0; i < int16Array.length; i++) {
331
+ channelData[i] = int16Array[i] / 32768;
332
+ }
333
333
  return audioBuffer;
334
334
  }
335
335
  }
@@ -345,13 +345,23 @@ class Midy {
345
345
  }
346
346
  return bufferSource;
347
347
  }
348
- convertToFloat32Array(uint8Array) {
349
- const int16Array = new Int16Array(uint8Array.buffer);
350
- const float32Array = new Float32Array(int16Array.length);
351
- for (let i = 0; i < int16Array.length; i++) {
352
- float32Array[i] = int16Array[i] / 32768;
348
+ findPortamentoTarget(queueIndex) {
349
+ const endEvent = this.timeline[queueIndex];
350
+ if (!this.channels[endEvent.channel].portamento)
351
+ return;
352
+ const endTime = endEvent.startTime;
353
+ let target;
354
+ while (++queueIndex < this.timeline.length) {
355
+ const event = this.timeline[queueIndex];
356
+ if (endTime !== event.startTime)
357
+ break;
358
+ if (event.type !== "noteOn")
359
+ continue;
360
+ if (!target || event.noteNumber < target.noteNumber) {
361
+ target = event;
362
+ }
353
363
  }
354
- return float32Array;
364
+ return target;
355
365
  }
356
366
  async scheduleTimelineEvents(t, offset, queueIndex) {
357
367
  while (queueIndex < this.timeline.length) {
@@ -361,12 +371,15 @@ class Midy {
361
371
  switch (event.type) {
362
372
  case "noteOn":
363
373
  if (event.velocity !== 0) {
364
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
374
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
365
375
  break;
366
376
  }
367
377
  /* falls through */
368
378
  case "noteOff": {
369
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
379
+ const portamentoTarget = this.findPortamentoTarget(queueIndex);
380
+ if (portamentoTarget)
381
+ portamentoTarget.portamento = true;
382
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
370
383
  if (notePromise) {
371
384
  this.notePromises.push(notePromise);
372
385
  }
@@ -470,9 +483,11 @@ class Midy {
470
483
  bankLSB: this.channels[i].bankLSB,
471
484
  };
472
485
  }
473
- midi.tracks.forEach((track) => {
486
+ for (let i = 0; i < midi.tracks.length; i++) {
487
+ const track = midi.tracks[i];
474
488
  let currentTicks = 0;
475
- track.forEach((event) => {
489
+ for (let j = 0; j < track.length; j++) {
490
+ const event = track[j];
476
491
  currentTicks += event.deltaTime;
477
492
  event.ticks = currentTicks;
478
493
  switch (event.type) {
@@ -525,16 +540,18 @@ class Midy {
525
540
  }
526
541
  delete event.deltaTime;
527
542
  timeline.push(event);
528
- });
529
- });
543
+ }
544
+ }
530
545
  const priority = {
531
546
  controller: 0,
532
547
  sysEx: 1,
548
+ noteOff: 2, // for portamento
549
+ noteOn: 3,
533
550
  };
534
551
  timeline.sort((a, b) => {
535
552
  if (a.ticks !== b.ticks)
536
553
  return a.ticks - b.ticks;
537
- return (priority[a.type] || 2) - (priority[b.type] || 2);
554
+ return (priority[a.type] || 4) - (priority[b.type] || 4);
538
555
  });
539
556
  let prevTempoTime = 0;
540
557
  let prevTempoTicks = 0;
@@ -551,7 +568,7 @@ class Midy {
551
568
  }
552
569
  return { instruments, timeline };
553
570
  }
554
- async stopChannelNotes(channelNumber, velocity, stopPedal) {
571
+ async stopChannelNotes(channelNumber, velocity, force) {
555
572
  const now = this.audioContext.currentTime;
556
573
  const channel = this.channels[channelNumber];
557
574
  channel.scheduledNotes.forEach((noteList) => {
@@ -559,16 +576,17 @@ class Midy {
559
576
  const note = noteList[i];
560
577
  if (!note)
561
578
  continue;
562
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, stopPedal);
579
+ const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
580
+ force);
563
581
  this.notePromises.push(promise);
564
582
  }
565
583
  });
566
584
  channel.scheduledNotes.clear();
567
585
  await Promise.all(this.notePromises);
568
586
  }
569
- stopNotes(velocity, stopPedal) {
587
+ stopNotes(velocity, force) {
570
588
  for (let i = 0; i < this.channels.length; i++) {
571
- this.stopChannelNotes(i, velocity, stopPedal);
589
+ this.stopChannelNotes(i, velocity, force);
572
590
  }
573
591
  return Promise.all(this.notePromises);
574
592
  }
@@ -782,6 +800,17 @@ class Midy {
782
800
  return instrumentKey.playbackRate(noteNumber) *
783
801
  Math.pow(2, semitoneOffset / 12);
784
802
  }
803
+ setPortamentoStartVolumeEnvelope(channel, note) {
804
+ const { instrumentKey, startTime } = note;
805
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
806
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
807
+ const volDelay = startTime + instrumentKey.volDelay;
808
+ const portamentoTime = volDelay + channel.portamentoTime;
809
+ note.volumeNode.gain
810
+ .cancelScheduledValues(startTime)
811
+ .setValueAtTime(0, volDelay)
812
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
813
+ }
785
814
  setVolumeEnvelope(channel, note) {
786
815
  const { instrumentKey, startTime } = note;
787
816
  const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
@@ -821,6 +850,25 @@ class Midy {
821
850
  const maxFrequency = 20000; // max Hz of initialFilterFc
822
851
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
823
852
  }
853
+ setPortamentoStartFilterEnvelope(channel, note) {
854
+ const { instrumentKey, noteNumber, startTime } = note;
855
+ const softPedalFactor = 1 -
856
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
857
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
858
+ softPedalFactor * channel.brightness;
859
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor * channel.brightness;
860
+ const sustainFreq = baseFreq +
861
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
862
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
863
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
864
+ const portamentoTime = startTime + channel.portamentoTime;
865
+ const modDelay = startTime + instrumentKey.modDelay;
866
+ note.filterNode.frequency
867
+ .cancelScheduledValues(startTime)
868
+ .setValueAtTime(adjustedBaseFreq, startTime)
869
+ .setValueAtTime(adjustedBaseFreq, modDelay)
870
+ .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
871
+ }
824
872
  setFilterEnvelope(channel, note) {
825
873
  const { instrumentKey, noteNumber, startTime } = note;
826
874
  const softPedalFactor = 1 -
@@ -888,7 +936,7 @@ class Midy {
888
936
  note.vibratoLFO.connect(note.vibratoDepth);
889
937
  note.vibratoDepth.connect(note.bufferSource.detune);
890
938
  }
891
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
939
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3) {
892
940
  const semitoneOffset = this.calcSemitoneOffset(channel);
893
941
  const note = new Note(noteNumber, velocity, startTime, instrumentKey);
894
942
  note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
@@ -897,8 +945,14 @@ class Midy {
897
945
  type: "lowpass",
898
946
  Q: instrumentKey.initialFilterQ / 10 * channel.filterResonance, // dB
899
947
  });
900
- this.setVolumeEnvelope(channel, note);
901
- this.setFilterEnvelope(channel, note);
948
+ if (portamento) {
949
+ this.setPortamentoStartVolumeEnvelope(channel, note);
950
+ this.setPortamentoStartFilterEnvelope(channel, note);
951
+ }
952
+ else {
953
+ this.setVolumeEnvelope(channel, note);
954
+ this.setFilterEnvelope(channel, note);
955
+ }
902
956
  if (0 < channel.vibratoDepth) {
903
957
  this.startVibrato(channel, note, startTime);
904
958
  }
@@ -915,7 +969,7 @@ class Midy {
915
969
  }
916
970
  note.bufferSource.connect(note.filterNode);
917
971
  note.filterNode.connect(note.volumeNode);
918
- note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
972
+ note.bufferSource.start(startTime);
919
973
  return note;
920
974
  }
921
975
  calcBank(channel, channelNumber) {
@@ -927,7 +981,7 @@ class Midy {
927
981
  }
928
982
  return channel.bank;
929
983
  }
930
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
984
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
931
985
  const channel = this.channels[channelNumber];
932
986
  const bankNumber = this.calcBank(channel, channelNumber);
933
987
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
@@ -938,7 +992,7 @@ class Midy {
938
992
  const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
939
993
  if (!instrumentKey)
940
994
  return;
941
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
995
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3);
942
996
  note.volumeNode.connect(channel.gainL);
943
997
  note.volumeNode.connect(channel.gainR);
944
998
  if (channel.sostenutoPedal) {
@@ -952,13 +1006,42 @@ class Midy {
952
1006
  scheduledNotes.set(noteNumber, [note]);
953
1007
  }
954
1008
  }
955
- noteOn(channelNumber, noteNumber, velocity) {
1009
+ noteOn(channelNumber, noteNumber, velocity, portamento) {
956
1010
  const now = this.audioContext.currentTime;
957
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
1011
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
958
1012
  }
959
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, stopPedal = false) {
1013
+ stopNote(stopTime, endTime, scheduledNotes, index) {
1014
+ const note = scheduledNotes[index];
1015
+ note.volumeNode.gain
1016
+ .cancelScheduledValues(stopTime)
1017
+ .linearRampToValueAtTime(0, endTime);
1018
+ note.ending = true;
1019
+ this.scheduleTask(() => {
1020
+ note.bufferSource.loop = false;
1021
+ }, endTime);
1022
+ return new Promise((resolve) => {
1023
+ note.bufferSource.onended = () => {
1024
+ scheduledNotes[index] = null;
1025
+ note.bufferSource.disconnect();
1026
+ note.volumeNode.disconnect();
1027
+ note.filterNode.disconnect();
1028
+ if (note.modulationDepth) {
1029
+ note.volumeDepth.disconnect();
1030
+ note.modulationDepth.disconnect();
1031
+ note.modulationLFO.stop();
1032
+ }
1033
+ if (note.vibratoDepth) {
1034
+ note.vibratoDepth.disconnect();
1035
+ note.vibratoLFO.stop();
1036
+ }
1037
+ resolve();
1038
+ };
1039
+ note.bufferSource.stop(endTime);
1040
+ });
1041
+ }
1042
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, portamentoNoteNumber, force) {
960
1043
  const channel = this.channels[channelNumber];
961
- if (stopPedal) {
1044
+ if (!force) {
962
1045
  if (channel.sustainPedal)
963
1046
  return;
964
1047
  if (channel.sostenutoNotes.has(noteNumber))
@@ -973,43 +1056,29 @@ class Midy {
973
1056
  continue;
974
1057
  if (note.ending)
975
1058
  continue;
976
- const volEndTime = stopTime +
977
- note.instrumentKey.volRelease * channel.releaseTime;
978
- note.volumeNode.gain
979
- .cancelScheduledValues(stopTime)
980
- .linearRampToValueAtTime(0, volEndTime);
981
- const modRelease = stopTime + note.instrumentKey.modRelease;
982
- note.filterNode.frequency
983
- .cancelScheduledValues(stopTime)
984
- .linearRampToValueAtTime(0, modRelease);
985
- note.ending = true;
986
- this.scheduleTask(() => {
987
- note.bufferSource.loop = false;
988
- }, stopTime);
989
- return new Promise((resolve) => {
990
- note.bufferSource.onended = () => {
991
- scheduledNotes[i] = null;
992
- note.bufferSource.disconnect();
993
- note.volumeNode.disconnect();
994
- note.filterNode.disconnect();
995
- if (note.modulationDepth) {
996
- note.volumeDepth.disconnect();
997
- note.modulationDepth.disconnect();
998
- note.modulationLFO.stop();
999
- }
1000
- if (note.vibratoDepth) {
1001
- note.vibratoDepth.disconnect();
1002
- note.vibratoLFO.stop();
1003
- }
1004
- resolve();
1005
- };
1006
- note.bufferSource.stop(volEndTime);
1007
- });
1059
+ if (portamentoNoteNumber === undefined) {
1060
+ const volEndTime = stopTime +
1061
+ note.instrumentKey.volRelease * channel.releaseTime;
1062
+ const modRelease = stopTime + note.instrumentKey.modRelease;
1063
+ note.filterNode.frequency
1064
+ .cancelScheduledValues(stopTime)
1065
+ .linearRampToValueAtTime(0, modRelease);
1066
+ return this.stopNote(stopTime, volEndTime, scheduledNotes, i);
1067
+ }
1068
+ else {
1069
+ const portamentoTime = stopTime + channel.portamentoTime;
1070
+ const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1071
+ const detune = note.bufferSource.detune.value + detuneChange;
1072
+ note.bufferSource.detune
1073
+ .cancelScheduledValues(stopTime)
1074
+ .linearRampToValueAtTime(detune, portamentoTime);
1075
+ return this.stopNote(stopTime, portamentoTime, scheduledNotes, i);
1076
+ }
1008
1077
  }
1009
1078
  }
1010
- releaseNote(channelNumber, noteNumber, velocity) {
1079
+ releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1011
1080
  const now = this.audioContext.currentTime;
1012
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
1081
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1013
1082
  }
1014
1083
  releaseSustainPedal(channelNumber, halfVelocity) {
1015
1084
  const velocity = halfVelocity * 2;
@@ -1184,12 +1253,14 @@ class Midy {
1184
1253
  this.updateModulation(channel);
1185
1254
  }
1186
1255
  setPortamentoTime(channelNumber, portamentoTime) {
1187
- this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1256
+ const channel = this.channels[channelNumber];
1257
+ const factor = 5 * Math.log(10) / 127;
1258
+ channel.portamentoTime = Math.exp(factor * portamentoTime);
1188
1259
  }
1189
1260
  setVolume(channelNumber, volume) {
1190
1261
  const channel = this.channels[channelNumber];
1191
1262
  channel.volume = volume / 127;
1192
- this.updateChannelGain(channel);
1263
+ this.updateChannelVolume(channel);
1193
1264
  }
1194
1265
  panToGain(pan) {
1195
1266
  const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
@@ -1201,12 +1272,12 @@ class Midy {
1201
1272
  setPan(channelNumber, pan) {
1202
1273
  const channel = this.channels[channelNumber];
1203
1274
  channel.pan = pan;
1204
- this.updateChannelGain(channel);
1275
+ this.updateChannelVolume(channel);
1205
1276
  }
1206
1277
  setExpression(channelNumber, expression) {
1207
1278
  const channel = this.channels[channelNumber];
1208
1279
  channel.expression = expression / 127;
1209
- this.updateChannelGain(channel);
1280
+ this.updateChannelVolume(channel);
1210
1281
  }
1211
1282
  setBankLSB(channelNumber, lsb) {
1212
1283
  this.channels[channelNumber].bankLSB = lsb;
@@ -1215,7 +1286,7 @@ class Midy {
1215
1286
  this.channels[channelNumber].dataLSB = value;
1216
1287
  this.handleRPN(channelNumber, 0);
1217
1288
  }
1218
- updateChannelGain(channel) {
1289
+ updateChannelVolume(channel) {
1219
1290
  const now = this.audioContext.currentTime;
1220
1291
  const volume = channel.volume * channel.expression;
1221
1292
  const { gainLeft, gainRight } = this.panToGain(channel.pan);
@@ -1233,12 +1304,12 @@ class Midy {
1233
1304
  this.releaseSustainPedal(channelNumber, value);
1234
1305
  }
1235
1306
  }
1236
- // TODO
1237
1307
  setPortamento(channelNumber, value) {
1238
1308
  this.channels[channelNumber].portamento = value >= 64;
1239
1309
  }
1240
1310
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1241
1311
  const channel = this.channels[channelNumber];
1312
+ const reverbEffect = this.reverbEffect;
1242
1313
  if (0 < channel.reverbSendLevel) {
1243
1314
  if (0 < reverbSendLevel) {
1244
1315
  const now = this.audioContext.currentTime;
@@ -1557,20 +1628,22 @@ class Midy {
1557
1628
  }
1558
1629
  }
1559
1630
  GM1SystemOn() {
1560
- this.channels.forEach((channel) => {
1631
+ for (let i = 0; i < this.channels.length; i++) {
1632
+ const channel = this.channels[i];
1561
1633
  channel.bankMSB = 0;
1562
1634
  channel.bankLSB = 0;
1563
1635
  channel.bank = 0;
1564
- });
1636
+ }
1565
1637
  this.channels[9].bankMSB = 1;
1566
1638
  this.channels[9].bank = 128;
1567
1639
  }
1568
1640
  GM2SystemOn() {
1569
- this.channels.forEach((channel) => {
1641
+ for (let i = 0; i < this.channels.length; i++) {
1642
+ const channel = this.channels[i];
1570
1643
  channel.bankMSB = 121;
1571
1644
  channel.bankLSB = 0;
1572
1645
  channel.bank = 121 * 128;
1573
- });
1646
+ }
1574
1647
  this.channels[9].bankMSB = 120;
1575
1648
  this.channels[9].bank = 120 * 128;
1576
1649
  }
@@ -1880,7 +1953,7 @@ Object.defineProperty(Midy, "channelSettings", {
1880
1953
  currentBufferSource: null,
1881
1954
  volume: 100 / 127,
1882
1955
  pan: 64,
1883
- portamentoTime: 0,
1956
+ portamentoTime: 1, // sec
1884
1957
  filterResonance: 1,
1885
1958
  releaseTime: 1,
1886
1959
  attackTime: 1,