@marmooo/midy 0.0.1 → 0.0.3

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
@@ -11,12 +11,6 @@ class Midy {
11
11
  writable: true,
12
12
  value: 120
13
13
  });
14
- Object.defineProperty(this, "secondsPerBeat", {
15
- enumerable: true,
16
- configurable: true,
17
- writable: true,
18
- value: 0.5
19
- });
20
14
  Object.defineProperty(this, "totalTime", {
21
15
  enumerable: true,
22
16
  configurable: true,
@@ -177,10 +171,10 @@ class Midy {
177
171
  const response = await fetch(midiUrl);
178
172
  const arrayBuffer = await response.arrayBuffer();
179
173
  const midi = (0, _esm_js_1.parseMidi)(new Uint8Array(arrayBuffer));
174
+ this.ticksPerBeat = midi.header.ticksPerBeat;
180
175
  const midiData = this.extractMidiData(midi);
181
176
  this.instruments = midiData.instruments;
182
177
  this.timeline = midiData.timeline;
183
- this.ticksPerBeat = midi.header.ticksPerBeat;
184
178
  this.totalTime = this.calcTotalTime();
185
179
  }
186
180
  setChannelAudioNodes(audioContext) {
@@ -266,18 +260,20 @@ class Midy {
266
260
  async scheduleTimelineEvents(t, offset, queueIndex) {
267
261
  while (queueIndex < this.timeline.length) {
268
262
  const event = this.timeline[queueIndex];
269
- const time = this.ticksToSecond(event.ticks, this.secondsPerBeat);
270
- if (time > t + this.lookAhead)
263
+ if (event.startTime > t + this.lookAhead)
271
264
  break;
272
265
  switch (event.type) {
273
266
  case "controller":
274
267
  this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
275
268
  break;
276
269
  case "noteOn":
277
- await this.scheduleNoteOn(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, time + this.startDelay - offset);
278
- break;
270
+ if (event.velocity !== 0) {
271
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
272
+ break;
273
+ }
274
+ /* falls through */
279
275
  case "noteOff": {
280
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, time + this.startDelay - offset);
276
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
281
277
  if (notePromise) {
282
278
  this.notePromises.push(notePromise);
283
279
  }
@@ -286,9 +282,6 @@ class Midy {
286
282
  case "programChange":
287
283
  this.handleProgramChange(event.channel, event.programNumber);
288
284
  break;
289
- case "setTempo":
290
- this.secondsPerBeat = event.microsecondsPerBeat / 1000000;
291
- break;
292
285
  case "sysEx":
293
286
  this.handleSysEx(event.data);
294
287
  }
@@ -297,9 +290,8 @@ class Midy {
297
290
  return queueIndex;
298
291
  }
299
292
  getQueueIndex(second) {
300
- const ticks = this.secondToTicks(second, this.secondsPerBeat);
301
293
  for (let i = 0; i < this.timeline.length; i++) {
302
- if (ticks <= this.timeline[i].ticks) {
294
+ if (second <= this.timeline[i].startTime) {
303
295
  return i;
304
296
  }
305
297
  }
@@ -441,18 +433,28 @@ class Midy {
441
433
  timeline.push(event);
442
434
  });
443
435
  });
436
+ const priority = {
437
+ setTempo: 0,
438
+ controller: 1,
439
+ };
444
440
  timeline.sort((a, b) => {
445
- if (a.ticks !== b.ticks) {
441
+ if (a.ticks !== b.ticks)
446
442
  return a.ticks - b.ticks;
447
- }
448
- if (a.type !== "controller" && b.type === "controller") {
449
- return -1;
450
- }
451
- if (a.type === "controller" && b.type !== "controller") {
452
- return 1;
453
- }
454
- return 0;
443
+ return (priority[a.type] || 2) - (priority[b.type] || 2);
455
444
  });
445
+ let prevTempoTime = 0;
446
+ let prevTempoTicks = 0;
447
+ let secondsPerBeat = 0.5;
448
+ for (let i = 0; i < timeline.length; i++) {
449
+ const event = timeline[i];
450
+ const timeFromPrevTempo = this.ticksToSecond(event.ticks - prevTempoTicks, secondsPerBeat);
451
+ event.startTime = prevTempoTime + timeFromPrevTempo;
452
+ if (event.type === "setTempo") {
453
+ prevTempoTime += this.ticksToSecond(event.ticks - prevTempoTicks, secondsPerBeat);
454
+ secondsPerBeat = event.microsecondsPerBeat / 1000000;
455
+ prevTempoTicks = event.ticks;
456
+ }
457
+ }
456
458
  return { instruments, timeline };
457
459
  }
458
460
  stopNotes() {
@@ -504,32 +506,12 @@ class Midy {
504
506
  }
505
507
  }
506
508
  calcTotalTime() {
507
- const endOfTracks = [];
508
- let prevTicks = 0;
509
509
  let totalTime = 0;
510
- let secondsPerBeat = 0.5;
511
510
  for (let i = 0; i < this.timeline.length; i++) {
512
511
  const event = this.timeline[i];
513
- switch (event.type) {
514
- case "setTempo": {
515
- const durationTicks = event.ticks - prevTicks;
516
- totalTime += this.ticksToSecond(durationTicks, secondsPerBeat);
517
- secondsPerBeat = event.microsecondsPerBeat / 1000000;
518
- prevTicks = event.ticks;
519
- break;
520
- }
521
- case "endOfTrack":
522
- endOfTracks.push(event);
523
- }
524
- }
525
- let maxTicks = 0;
526
- for (let i = 0; i < endOfTracks.length; i++) {
527
- const event = endOfTracks[i];
528
- if (maxTicks < event.ticks)
529
- maxTicks = event.ticks;
512
+ if (totalTime < event.startTime)
513
+ totalTime = event.startTime;
530
514
  }
531
- const durationTicks = maxTicks - prevTicks;
532
- totalTime += this.ticksToSecond(durationTicks, secondsPerBeat);
533
515
  return totalTime;
534
516
  }
535
517
  currentTime() {
@@ -557,11 +539,8 @@ class Midy {
557
539
  const lfo = new OscillatorNode(audioContext, {
558
540
  frequency: 5,
559
541
  });
560
- const lfoGain = new GainNode(audioContext);
561
- lfo.connect(lfoGain);
562
542
  return {
563
543
  lfo,
564
- lfoGain,
565
544
  };
566
545
  }
567
546
  createReverbEffect(audioContext, options = {}) {
@@ -693,33 +672,44 @@ class Midy {
693
672
  .exponentialRampToValueAtTime(attackVolume, volAttack)
694
673
  .setValueAtTime(attackVolume, volHold)
695
674
  .linearRampToValueAtTime(sustainVolume, volDecay);
696
- if (channel.modulation > 0) {
697
- const lfoGain = channel.modulationEffect.lfoGain;
698
- lfoGain.connect(bufferSource.detune);
699
- lfoGain.gain.cancelScheduledValues(startTime + channel.vibratoDelay);
700
- lfoGain.gain.setValueAtTime(channel.modulation, startTime + channel.vibratoDelay);
701
- }
702
675
  // filter envelope
703
676
  const softPedalFactor = 1 -
704
677
  (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
678
+ const maxFreq = this.audioContext.sampleRate / 2;
705
679
  const baseFreq = this.centToHz(noteInfo.initialFilterFc) * softPedalFactor;
706
680
  const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc) * softPedalFactor;
707
681
  const sustainFreq = (baseFreq +
708
682
  (peekFreq - baseFreq) * (1 - noteInfo.modSustain)) * softPedalFactor;
683
+ const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
684
+ const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
685
+ const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
709
686
  const filterNode = new BiquadFilterNode(this.audioContext, {
710
687
  type: "lowpass",
711
- Q: this.cbToRatio(noteInfo.initialFilterQ),
712
- frequency: baseFreq,
688
+ Q: noteInfo.initialFilterQ / 10, // dB
689
+ frequency: adjustedBaseFreq,
713
690
  });
714
691
  const modDelay = startTime + noteInfo.modDelay;
715
692
  const modAttack = modDelay + noteInfo.modAttack;
716
693
  const modHold = modAttack + noteInfo.modHold;
717
694
  const modDecay = modHold + noteInfo.modDecay;
718
695
  filterNode.frequency
719
- .setValueAtTime(baseFreq, modDelay)
720
- .exponentialRampToValueAtTime(peekFreq, modAttack)
721
- .setValueAtTime(peekFreq, modHold)
722
- .linearRampToValueAtTime(sustainFreq, modDecay);
696
+ .setValueAtTime(adjustedBaseFreq, modDelay)
697
+ .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
698
+ .setValueAtTime(adjustedPeekFreq, modHold)
699
+ .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
700
+ let lfoGain;
701
+ if (channel.modulation > 0) {
702
+ const vibratoDelay = startTime + channel.vibratoDelay;
703
+ const vibratoAttack = vibratoDelay + 0.1;
704
+ lfoGain = new GainNode(this.audioContext, {
705
+ gain: 0,
706
+ });
707
+ lfoGain.gain
708
+ .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
709
+ .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
710
+ channel.modulationEffect.lfo.connect(lfoGain);
711
+ lfoGain.connect(bufferSource.detune);
712
+ }
723
713
  bufferSource.connect(filterNode);
724
714
  filterNode.connect(gainNode);
725
715
  if (this.mono && channel.currentBufferSource) {
@@ -763,11 +753,12 @@ class Midy {
763
753
  }
764
754
  const scheduledNotes = channel.scheduledNotes;
765
755
  const scheduledNote = {
766
- gainNode,
767
- filterNode,
768
756
  bufferSource,
769
- noteNumber,
757
+ filterNode,
758
+ gainNode,
759
+ lfoGain,
770
760
  noteInfo,
761
+ noteNumber,
771
762
  startTime,
772
763
  };
773
764
  if (scheduledNotes.has(noteNumber)) {
@@ -796,15 +787,18 @@ class Midy {
796
787
  continue;
797
788
  if (targetNote.ending)
798
789
  continue;
799
- const { bufferSource, filterNode, gainNode, noteInfo } = targetNote;
790
+ const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
800
791
  const velocityRate = (velocity + 127) / 127;
801
792
  const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
802
793
  gainNode.gain.cancelScheduledValues(stopTime);
803
794
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
795
+ const maxFreq = this.audioContext.sampleRate / 2;
804
796
  const baseFreq = this.centToHz(noteInfo.initialFilterFc);
797
+ const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
805
798
  const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
806
- filterNode.frequency.cancelScheduledValues(stopTime);
807
- filterNode.frequency.linearRampToValueAtTime(baseFreq, modEndTime);
799
+ filterNode.frequency
800
+ .cancelScheduledValues(stopTime)
801
+ .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
808
802
  targetNote.ending = true;
809
803
  this.scheduleTask(() => {
810
804
  bufferSource.loop = false;
@@ -815,6 +809,8 @@ class Midy {
815
809
  bufferSource.disconnect(0);
816
810
  filterNode.disconnect(0);
817
811
  gainNode.disconnect(0);
812
+ if (lfoGain)
813
+ lfoGain.disconnect(0);
818
814
  resolve();
819
815
  };
820
816
  bufferSource.stop(volEndTime);
@@ -825,38 +821,34 @@ class Midy {
825
821
  const now = this.audioContext.currentTime;
826
822
  return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
827
823
  }
828
- releaseSustainPedal(channelNumber) {
829
- const now = this.audioContext.currentTime;
824
+ releaseSustainPedal(channelNumber, halfVelocity) {
825
+ const velocity = halfVelocity * 2;
830
826
  const channel = this.channels[channelNumber];
827
+ const promises = [];
831
828
  channel.sustainPedal = false;
832
829
  channel.scheduledNotes.forEach((scheduledNotes) => {
833
830
  scheduledNotes.forEach((scheduledNote) => {
834
831
  if (scheduledNote) {
835
- const { gainNode, filterNode, bufferSource, noteInfo } = scheduledNote;
836
- const volEndTime = now + noteInfo.volRelease;
837
- gainNode.gain.cancelScheduledValues(now);
838
- gainNode.gain.linearRampToValueAtTime(0, volEndTime);
839
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
840
- const modEndTime = now + noteInfo.modRelease;
841
- filterNode.frequency.cancelScheduledValues(now);
842
- filterNode.frequency.linearRampToValueAtTime(baseFreq, modEndTime);
843
- bufferSource.stop(volEndTime);
832
+ const { noteNumber } = scheduledNote;
833
+ const promise = this.releaseNote(channelNumber, noteNumber, velocity);
834
+ promises.push(promise);
844
835
  }
845
836
  });
846
837
  });
838
+ return promises;
847
839
  }
848
- releaseSostenuto(channelNumber) {
849
- const now = this.audioContext.currentTime;
840
+ releaseSostenutoPedal(channelNumber, halfVelocity) {
841
+ const velocity = halfVelocity * 2;
850
842
  const channel = this.channels[channelNumber];
843
+ const promises = [];
851
844
  channel.sostenutoPedal = false;
852
845
  channel.sostenutoNotes.forEach((activeNote) => {
853
- const { gainNode, bufferSource, noteInfo } = activeNote;
854
- const fadeTime = noteInfo.volRelease;
855
- gainNode.gain.cancelScheduledValues(now);
856
- gainNode.gain.linearRampToValueAtTime(0, now + fadeTime);
857
- bufferSource.stop(now + fadeTime);
846
+ const { noteNumber } = activeNote;
847
+ const promise = this.releaseNote(channelNumber, noteNumber, velocity);
848
+ promises.push(promise);
858
849
  });
859
850
  channel.sostenutoNotes.clear();
851
+ return promises;
860
852
  }
861
853
  handleMIDIMessage(statusByte, data1, data2) {
862
854
  const channelNumber = omni ? 0 : statusByte & 0x0F;
@@ -889,7 +881,7 @@ class Midy {
889
881
  scheduledNotes.forEach((scheduledNote) => {
890
882
  if (scheduledNote) {
891
883
  const { initialAttenuation } = scheduledNote.noteInfo;
892
- const gain = this.cbToRatio(initialAttenuation) * pressure;
884
+ const gain = this.cbToRatio(-initialAttenuation) * pressure;
893
885
  scheduledNote.gainNode.gain.cancelScheduledValues(now);
894
886
  scheduledNote.gainNode.gain.setValueAtTime(gain, now);
895
887
  }
@@ -977,13 +969,9 @@ class Midy {
977
969
  this.channels[channelNumber].bankMSB = msb;
978
970
  }
979
971
  setModulation(channelNumber, modulation) {
980
- const now = this.audioContext.currentTime;
981
972
  const channel = this.channels[channelNumber];
982
- channel.modulation = (modulation * 100 / 127) *
983
- channel.modulationDepthRange;
984
- const lfoGain = channel.modulationEffect.lfoGain;
985
- lfoGain.gain.cancelScheduledValues(now);
986
- lfoGain.gain.setValueAtTime(channel.modulation, now);
973
+ channel.modulation = (modulation / 127) *
974
+ (channel.modulationDepthRange * 100);
987
975
  }
988
976
  setPortamentoTime(channelNumber, portamentoTime) {
989
977
  this.channels[channelNumber].portamentoTime = portamentoTime / 127;
@@ -1018,7 +1006,7 @@ class Midy {
1018
1006
  const isOn = value >= 64;
1019
1007
  this.channels[channelNumber].sustainPedal = isOn;
1020
1008
  if (!isOn) {
1021
- this.releaseSustainPedal(channelNumber);
1009
+ this.releaseSustainPedal(channelNumber, value);
1022
1010
  }
1023
1011
  }
1024
1012
  setPortamento(channelNumber, value) {
@@ -1047,25 +1035,29 @@ class Midy {
1047
1035
  const activeNotes = this.getActiveNotes(channel);
1048
1036
  channel.sostenutoNotes = new Map(activeNotes);
1049
1037
  }
1038
+ else {
1039
+ this.releaseSostenutoPedal(channelNumber, value);
1040
+ }
1050
1041
  }
1051
1042
  setSoftPedal(channelNumber, softPedal) {
1052
1043
  const channel = this.channels[channelNumber];
1053
1044
  channel.softPedal = softPedal / 127;
1054
- this.updateChannelGain(channel);
1055
1045
  }
1056
1046
  setVibratoRate(channelNumber, vibratoRate) {
1057
1047
  const now = this.audioContext.currentTime;
1058
1048
  const channel = this.channels[channelNumber];
1059
1049
  channel.vibratoRate = vibratoRate / 127 * 4 + 3; // 3-7Hz
1060
- channel.modulationEffect.lfo.frequency.cancelScheduledValues(now);
1061
- channel.modulationEffect.lfo.frequency.setValueAtTime(depth, now);
1050
+ channel.modulationEffect.lfo.frequency
1051
+ .cancelScheduledValues(now)
1052
+ .setValueAtTime(channel.vibratoRate, now);
1062
1053
  }
1063
1054
  setVibratoDepth(channelNumber, vibratoDepth) {
1064
1055
  const now = this.audioContext.currentTime;
1065
1056
  const channel = this.channels[channelNumber];
1066
1057
  channel.vibratoDepth = vibratoDepth / 127;
1067
- channel.modulationEffect.lfoGain.gain.cancelScheduledValues(now);
1068
- channel.modulationEffect.lfoGain.gain.setValueAtTime(depth, now);
1058
+ channel.modulationEffect.lfoGain.gain
1059
+ .cancelScheduledValues(now)
1060
+ .setValueAtTime(channel.vibratoDepth, now);
1069
1061
  }
1070
1062
  setVibratoDelay(channelNumber, vibratoDelay) {
1071
1063
  // Access Virus: 0-10sec
@@ -1337,7 +1329,7 @@ Object.defineProperty(Midy, "channelSettings", {
1337
1329
  writable: true,
1338
1330
  value: {
1339
1331
  currentBufferSource: null,
1340
- volume: 1,
1332
+ volume: 100 / 127,
1341
1333
  pan: 0,
1342
1334
  portamentoTime: 0,
1343
1335
  reverb: 0,
@@ -1354,7 +1346,7 @@ Object.defineProperty(Midy, "channelSettings", {
1354
1346
  pitchBend: 0,
1355
1347
  fineTuning: 0,
1356
1348
  coarseTuning: 0,
1357
- modulationDepthRange: 2,
1349
+ modulationDepthRange: 0.5,
1358
1350
  }
1359
1351
  });
1360
1352
  Object.defineProperty(Midy, "effectSettings", {