@marmooo/midy 0.1.5 → 0.1.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.
Files changed (37) hide show
  1. package/esm/midy-GM1.d.ts +6 -5
  2. package/esm/midy-GM1.d.ts.map +1 -1
  3. package/esm/midy-GM1.js +97 -65
  4. package/esm/midy-GM2.d.ts +15 -9
  5. package/esm/midy-GM2.d.ts.map +1 -1
  6. package/esm/midy-GM2.js +316 -129
  7. package/esm/midy-GMLite.d.ts +6 -5
  8. package/esm/midy-GMLite.d.ts.map +1 -1
  9. package/esm/midy-GMLite.js +97 -65
  10. package/esm/midy.d.ts +15 -9
  11. package/esm/midy.d.ts.map +1 -1
  12. package/esm/midy.js +317 -134
  13. package/package.json +5 -1
  14. package/script/midy-GM1.d.ts +6 -5
  15. package/script/midy-GM1.d.ts.map +1 -1
  16. package/script/midy-GM1.js +100 -68
  17. package/script/midy-GM2.d.ts +15 -9
  18. package/script/midy-GM2.d.ts.map +1 -1
  19. package/script/midy-GM2.js +319 -132
  20. package/script/midy-GMLite.d.ts +6 -5
  21. package/script/midy-GMLite.d.ts.map +1 -1
  22. package/script/midy-GMLite.js +100 -68
  23. package/script/midy.d.ts +15 -9
  24. package/script/midy.d.ts.map +1 -1
  25. package/script/midy.js +320 -137
  26. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts +0 -149
  27. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts.map +0 -1
  28. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js +0 -180
  29. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +0 -84
  30. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +0 -1
  31. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +0 -216
  32. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts +0 -149
  33. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts.map +0 -1
  34. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js +0 -190
  35. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +0 -84
  36. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +0 -1
  37. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +0 -221
package/esm/midy.js CHANGED
@@ -1,5 +1,5 @@
1
- import { parseMidi } from "./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js";
2
- import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js";
1
+ import { parseMidi } from "midi-file";
2
+ import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
3
  class Note {
4
4
  constructor(noteNumber, velocity, startTime, instrumentKey) {
5
5
  Object.defineProperty(this, "bufferSource", {
@@ -50,6 +50,18 @@ class Note {
50
50
  writable: true,
51
51
  value: void 0
52
52
  });
53
+ Object.defineProperty(this, "reverbEffectsSend", {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: void 0
58
+ });
59
+ Object.defineProperty(this, "chorusEffectsSend", {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value: void 0
64
+ });
53
65
  this.noteNumber = noteNumber;
54
66
  this.velocity = velocity;
55
67
  this.startTime = startTime;
@@ -205,6 +217,12 @@ export class Midy {
205
217
  writable: true,
206
218
  value: []
207
219
  });
220
+ Object.defineProperty(this, "exclusiveClassMap", {
221
+ enumerable: true,
222
+ configurable: true,
223
+ writable: true,
224
+ value: new Map()
225
+ });
208
226
  Object.defineProperty(this, "defaultOptions", {
209
227
  enumerable: true,
210
228
  configurable: true,
@@ -249,12 +267,14 @@ export class Midy {
249
267
  addSoundFont(soundFont) {
250
268
  const index = this.soundFonts.length;
251
269
  this.soundFonts.push(soundFont);
252
- soundFont.parsed.presetHeaders.forEach((presetHeader) => {
270
+ const presetHeaders = soundFont.parsed.presetHeaders;
271
+ for (let i = 0; i < presetHeaders.length; i++) {
272
+ const presetHeader = presetHeaders[i];
253
273
  if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
254
274
  const banks = this.soundFontTable[presetHeader.preset];
255
275
  banks.set(presetHeader.bank, index);
256
276
  }
257
- });
277
+ }
258
278
  }
259
279
  async loadSoundFont(soundFontUrl) {
260
280
  const response = await fetch(soundFontUrl);
@@ -306,27 +326,31 @@ export class Midy {
306
326
  return channels;
307
327
  }
308
328
  async createNoteBuffer(instrumentKey, isSF3) {
329
+ const sampleStart = instrumentKey.start;
309
330
  const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
310
331
  if (isSF3) {
311
- const sample = new Uint8Array(instrumentKey.sample.length);
312
- sample.set(instrumentKey.sample);
313
- const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
314
- for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
315
- const channelData = audioBuffer.getChannelData(channel);
316
- channelData.set(channelData.subarray(0, sampleEnd));
317
- }
332
+ const sample = instrumentKey.sample;
333
+ const start = sample.byteOffset + sampleStart;
334
+ const end = sample.byteOffset + sampleEnd;
335
+ const buffer = sample.buffer.slice(start, end);
336
+ const audioBuffer = await this.audioContext.decodeAudioData(buffer);
318
337
  return audioBuffer;
319
338
  }
320
339
  else {
321
- const sample = instrumentKey.sample.subarray(0, sampleEnd);
322
- const floatSample = this.convertToFloat32Array(sample);
340
+ const sample = instrumentKey.sample;
341
+ const start = sample.byteOffset + sampleStart;
342
+ const end = sample.byteOffset + sampleEnd;
343
+ const buffer = sample.buffer.slice(start, end);
323
344
  const audioBuffer = new AudioBuffer({
324
345
  numberOfChannels: 1,
325
346
  length: sample.length,
326
347
  sampleRate: instrumentKey.sampleRate,
327
348
  });
328
349
  const channelData = audioBuffer.getChannelData(0);
329
- channelData.set(floatSample);
350
+ const int16Array = new Int16Array(buffer);
351
+ for (let i = 0; i < int16Array.length; i++) {
352
+ channelData[i] = int16Array[i] / 32768;
353
+ }
330
354
  return audioBuffer;
331
355
  }
332
356
  }
@@ -342,13 +366,23 @@ export class Midy {
342
366
  }
343
367
  return bufferSource;
344
368
  }
345
- convertToFloat32Array(uint8Array) {
346
- const int16Array = new Int16Array(uint8Array.buffer);
347
- const float32Array = new Float32Array(int16Array.length);
348
- for (let i = 0; i < int16Array.length; i++) {
349
- float32Array[i] = int16Array[i] / 32768;
369
+ findPortamentoTarget(queueIndex) {
370
+ const endEvent = this.timeline[queueIndex];
371
+ if (!this.channels[endEvent.channel].portamento)
372
+ return;
373
+ const endTime = endEvent.startTime;
374
+ let target;
375
+ while (++queueIndex < this.timeline.length) {
376
+ const event = this.timeline[queueIndex];
377
+ if (endTime !== event.startTime)
378
+ break;
379
+ if (event.type !== "noteOn")
380
+ continue;
381
+ if (!target || event.noteNumber < target.noteNumber) {
382
+ target = event;
383
+ }
350
384
  }
351
- return float32Array;
385
+ return target;
352
386
  }
353
387
  async scheduleTimelineEvents(t, offset, queueIndex) {
354
388
  while (queueIndex < this.timeline.length) {
@@ -358,12 +392,15 @@ export class Midy {
358
392
  switch (event.type) {
359
393
  case "noteOn":
360
394
  if (event.velocity !== 0) {
361
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
395
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
362
396
  break;
363
397
  }
364
398
  /* falls through */
365
399
  case "noteOff": {
366
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
400
+ const portamentoTarget = this.findPortamentoTarget(queueIndex);
401
+ if (portamentoTarget)
402
+ portamentoTarget.portamento = true;
403
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
367
404
  if (notePromise) {
368
405
  this.notePromises.push(notePromise);
369
406
  }
@@ -411,6 +448,7 @@ export class Midy {
411
448
  if (queueIndex >= this.timeline.length) {
412
449
  await Promise.all(this.notePromises);
413
450
  this.notePromises = [];
451
+ this.exclusiveClassMap.clear();
414
452
  resolve();
415
453
  return;
416
454
  }
@@ -426,6 +464,7 @@ export class Midy {
426
464
  }
427
465
  else if (this.isStopping) {
428
466
  await this.stopNotes(0, true);
467
+ this.exclusiveClassMap.clear();
429
468
  this.notePromises = [];
430
469
  resolve();
431
470
  this.isStopping = false;
@@ -434,6 +473,7 @@ export class Midy {
434
473
  }
435
474
  else if (this.isSeeking) {
436
475
  this.stopNotes(0, true);
476
+ this.exclusiveClassMap.clear();
437
477
  this.startTime = this.audioContext.currentTime;
438
478
  queueIndex = this.getQueueIndex(this.resumeTime);
439
479
  offset = this.resumeTime - this.startTime;
@@ -467,9 +507,11 @@ export class Midy {
467
507
  bankLSB: this.channels[i].bankLSB,
468
508
  };
469
509
  }
470
- midi.tracks.forEach((track) => {
510
+ for (let i = 0; i < midi.tracks.length; i++) {
511
+ const track = midi.tracks[i];
471
512
  let currentTicks = 0;
472
- track.forEach((event) => {
513
+ for (let j = 0; j < track.length; j++) {
514
+ const event = track[j];
473
515
  currentTicks += event.deltaTime;
474
516
  event.ticks = currentTicks;
475
517
  switch (event.type) {
@@ -522,16 +564,18 @@ export class Midy {
522
564
  }
523
565
  delete event.deltaTime;
524
566
  timeline.push(event);
525
- });
526
- });
567
+ }
568
+ }
527
569
  const priority = {
528
570
  controller: 0,
529
571
  sysEx: 1,
572
+ noteOff: 2, // for portamento
573
+ noteOn: 3,
530
574
  };
531
575
  timeline.sort((a, b) => {
532
576
  if (a.ticks !== b.ticks)
533
577
  return a.ticks - b.ticks;
534
- return (priority[a.type] || 2) - (priority[b.type] || 2);
578
+ return (priority[a.type] || 4) - (priority[b.type] || 4);
535
579
  });
536
580
  let prevTempoTime = 0;
537
581
  let prevTempoTicks = 0;
@@ -548,7 +592,7 @@ export class Midy {
548
592
  }
549
593
  return { instruments, timeline };
550
594
  }
551
- async stopChannelNotes(channelNumber, velocity, stopPedal) {
595
+ async stopChannelNotes(channelNumber, velocity, force) {
552
596
  const now = this.audioContext.currentTime;
553
597
  const channel = this.channels[channelNumber];
554
598
  channel.scheduledNotes.forEach((noteList) => {
@@ -556,16 +600,17 @@ export class Midy {
556
600
  const note = noteList[i];
557
601
  if (!note)
558
602
  continue;
559
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, stopPedal);
603
+ const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
604
+ force);
560
605
  this.notePromises.push(promise);
561
606
  }
562
607
  });
563
608
  channel.scheduledNotes.clear();
564
609
  await Promise.all(this.notePromises);
565
610
  }
566
- stopNotes(velocity, stopPedal) {
611
+ stopNotes(velocity, force) {
567
612
  for (let i = 0; i < this.channels.length; i++) {
568
- this.stopChannelNotes(i, velocity, stopPedal);
613
+ this.stopChannelNotes(i, velocity, force);
569
614
  }
570
615
  return Promise.all(this.notePromises);
571
616
  }
@@ -657,14 +702,14 @@ export class Midy {
657
702
  return impulse;
658
703
  }
659
704
  createConvolutionReverb(audioContext, impulse) {
660
- const output = new GainNode(audioContext);
705
+ const input = new GainNode(audioContext);
661
706
  const convolverNode = new ConvolverNode(audioContext, {
662
707
  buffer: impulse,
663
708
  });
664
- convolverNode.connect(output);
709
+ input.connect(convolverNode);
665
710
  return {
666
- input: convolverNode,
667
- output,
711
+ input,
712
+ output: convolverNode,
668
713
  convolverNode,
669
714
  };
670
715
  }
@@ -706,7 +751,6 @@ export class Midy {
706
751
  // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
707
752
  createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
708
753
  const input = new GainNode(audioContext);
709
- const output = new GainNode(audioContext);
710
754
  const mergerGain = new GainNode(audioContext);
711
755
  for (let i = 0; i < combDelays.length; i++) {
712
756
  const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
@@ -717,7 +761,7 @@ export class Midy {
717
761
  const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
718
762
  allpasses.push(allpass);
719
763
  }
720
- allpasses.at(-1).connect(output);
764
+ const output = allpasses.at(-1);
721
765
  return { input, output };
722
766
  }
723
767
  createChorusEffect(audioContext) {
@@ -779,6 +823,17 @@ export class Midy {
779
823
  return instrumentKey.playbackRate(noteNumber) *
780
824
  Math.pow(2, semitoneOffset / 12);
781
825
  }
826
+ setPortamentoStartVolumeEnvelope(channel, note) {
827
+ const { instrumentKey, startTime } = note;
828
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
829
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
830
+ const volDelay = startTime + instrumentKey.volDelay;
831
+ const portamentoTime = volDelay + channel.portamentoTime;
832
+ note.volumeNode.gain
833
+ .cancelScheduledValues(startTime)
834
+ .setValueAtTime(0, volDelay)
835
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
836
+ }
782
837
  setVolumeEnvelope(channel, note) {
783
838
  const { instrumentKey, startTime } = note;
784
839
  const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
@@ -818,6 +873,25 @@ export class Midy {
818
873
  const maxFrequency = 20000; // max Hz of initialFilterFc
819
874
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
820
875
  }
876
+ setPortamentoStartFilterEnvelope(channel, note) {
877
+ const { instrumentKey, noteNumber, startTime } = note;
878
+ const softPedalFactor = 1 -
879
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
880
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
881
+ softPedalFactor * channel.brightness;
882
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor * channel.brightness;
883
+ const sustainFreq = baseFreq +
884
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
885
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
886
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
887
+ const portamentoTime = startTime + channel.portamentoTime;
888
+ const modDelay = startTime + instrumentKey.modDelay;
889
+ note.filterNode.frequency
890
+ .cancelScheduledValues(startTime)
891
+ .setValueAtTime(adjustedBaseFreq, startTime)
892
+ .setValueAtTime(adjustedBaseFreq, modDelay)
893
+ .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
894
+ }
821
895
  setFilterEnvelope(channel, note) {
822
896
  const { instrumentKey, noteNumber, startTime } = note;
823
897
  const softPedalFactor = 1 -
@@ -885,7 +959,7 @@ export class Midy {
885
959
  note.vibratoLFO.connect(note.vibratoDepth);
886
960
  note.vibratoDepth.connect(note.bufferSource.detune);
887
961
  }
888
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
962
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3) {
889
963
  const semitoneOffset = this.calcSemitoneOffset(channel);
890
964
  const note = new Note(noteNumber, velocity, startTime, instrumentKey);
891
965
  note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
@@ -894,8 +968,14 @@ export class Midy {
894
968
  type: "lowpass",
895
969
  Q: instrumentKey.initialFilterQ / 10 * channel.filterResonance, // dB
896
970
  });
897
- this.setVolumeEnvelope(channel, note);
898
- this.setFilterEnvelope(channel, note);
971
+ if (portamento) {
972
+ this.setPortamentoStartVolumeEnvelope(channel, note);
973
+ this.setPortamentoStartFilterEnvelope(channel, note);
974
+ }
975
+ else {
976
+ this.setVolumeEnvelope(channel, note);
977
+ this.setFilterEnvelope(channel, note);
978
+ }
899
979
  if (0 < channel.vibratoDepth) {
900
980
  this.startVibrato(channel, note, startTime);
901
981
  }
@@ -912,7 +992,21 @@ export class Midy {
912
992
  }
913
993
  note.bufferSource.connect(note.filterNode);
914
994
  note.filterNode.connect(note.volumeNode);
915
- note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
995
+ if (0 < channel.reverbSendLevel && 0 < instrumentKey.reverbEffectsSend) {
996
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
997
+ gain: instrumentKey.reverbEffectsSend,
998
+ });
999
+ note.volumeNode.connect(note.reverbEffectsSend);
1000
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1001
+ }
1002
+ if (0 < channel.chorusSendLevel && 0 < instrumentKey.chorusEffectsSend) {
1003
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1004
+ gain: instrumentKey.chorusEffectsSend,
1005
+ });
1006
+ note.volumeNode.connect(note.chorusEffectsSend);
1007
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1008
+ }
1009
+ note.bufferSource.start(startTime);
916
1010
  return note;
917
1011
  }
918
1012
  calcBank(channel, channelNumber) {
@@ -924,7 +1018,7 @@ export class Midy {
924
1018
  }
925
1019
  return channel.bank;
926
1020
  }
927
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1021
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
928
1022
  const channel = this.channels[channelNumber];
929
1023
  const bankNumber = this.calcBank(channel, channelNumber);
930
1024
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
@@ -935,12 +1029,25 @@ export class Midy {
935
1029
  const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
936
1030
  if (!instrumentKey)
937
1031
  return;
938
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
1032
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3);
939
1033
  note.volumeNode.connect(channel.gainL);
940
1034
  note.volumeNode.connect(channel.gainR);
941
1035
  if (channel.sostenutoPedal) {
942
1036
  channel.sostenutoNotes.set(noteNumber, note);
943
1037
  }
1038
+ const exclusiveClass = instrumentKey.exclusiveClass;
1039
+ if (exclusiveClass !== 0) {
1040
+ if (this.exclusiveClassMap.has(exclusiveClass)) {
1041
+ const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1042
+ const [prevNote, prevChannelNumber] = prevEntry;
1043
+ if (!prevNote.ending) {
1044
+ this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1045
+ startTime, undefined, // portamentoNoteNumber
1046
+ true);
1047
+ }
1048
+ }
1049
+ this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1050
+ }
944
1051
  const scheduledNotes = channel.scheduledNotes;
945
1052
  if (scheduledNotes.has(noteNumber)) {
946
1053
  scheduledNotes.get(noteNumber).push(note);
@@ -949,13 +1056,48 @@ export class Midy {
949
1056
  scheduledNotes.set(noteNumber, [note]);
950
1057
  }
951
1058
  }
952
- noteOn(channelNumber, noteNumber, velocity) {
1059
+ noteOn(channelNumber, noteNumber, velocity, portamento) {
953
1060
  const now = this.audioContext.currentTime;
954
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
1061
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1062
+ }
1063
+ stopNote(endTime, stopTime, scheduledNotes, index) {
1064
+ const note = scheduledNotes[index];
1065
+ note.volumeNode.gain
1066
+ .cancelScheduledValues(endTime)
1067
+ .linearRampToValueAtTime(0, stopTime);
1068
+ note.ending = true;
1069
+ this.scheduleTask(() => {
1070
+ note.bufferSource.loop = false;
1071
+ }, stopTime);
1072
+ return new Promise((resolve) => {
1073
+ note.bufferSource.onended = () => {
1074
+ scheduledNotes[index] = null;
1075
+ note.bufferSource.disconnect();
1076
+ note.volumeNode.disconnect();
1077
+ note.filterNode.disconnect();
1078
+ if (note.modulationDepth) {
1079
+ note.volumeDepth.disconnect();
1080
+ note.modulationDepth.disconnect();
1081
+ note.modulationLFO.stop();
1082
+ }
1083
+ if (note.vibratoDepth) {
1084
+ note.vibratoDepth.disconnect();
1085
+ note.vibratoLFO.stop();
1086
+ }
1087
+ if (note.reverbEffectsSend) {
1088
+ note.reverbEffectsSend.disconnect();
1089
+ }
1090
+ if (note.chorusEffectsSend) {
1091
+ note.chorusEffectsSend.disconnect();
1092
+ }
1093
+ resolve();
1094
+ };
1095
+ note.bufferSource.stop(stopTime);
1096
+ });
955
1097
  }
956
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, stopPedal = false) {
1098
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
957
1099
  const channel = this.channels[channelNumber];
958
- if (stopPedal) {
1100
+ if (!force) {
959
1101
  if (channel.sustainPedal)
960
1102
  return;
961
1103
  if (channel.sostenutoNotes.has(noteNumber))
@@ -970,43 +1112,30 @@ export class Midy {
970
1112
  continue;
971
1113
  if (note.ending)
972
1114
  continue;
973
- const volEndTime = stopTime +
974
- note.instrumentKey.volRelease * channel.releaseTime;
975
- note.volumeNode.gain
976
- .cancelScheduledValues(stopTime)
977
- .linearRampToValueAtTime(0, volEndTime);
978
- const modRelease = stopTime + note.instrumentKey.modRelease;
979
- note.filterNode.frequency
980
- .cancelScheduledValues(stopTime)
981
- .linearRampToValueAtTime(0, modRelease);
982
- note.ending = true;
983
- this.scheduleTask(() => {
984
- note.bufferSource.loop = false;
985
- }, stopTime);
986
- return new Promise((resolve) => {
987
- note.bufferSource.onended = () => {
988
- scheduledNotes[i] = null;
989
- note.bufferSource.disconnect();
990
- note.volumeNode.disconnect();
991
- note.filterNode.disconnect();
992
- if (note.modulationDepth) {
993
- note.volumeDepth.disconnect();
994
- note.modulationDepth.disconnect();
995
- note.modulationLFO.stop();
996
- }
997
- if (note.vibratoDepth) {
998
- note.vibratoDepth.disconnect();
999
- note.vibratoLFO.stop();
1000
- }
1001
- resolve();
1002
- };
1003
- note.bufferSource.stop(volEndTime);
1004
- });
1115
+ if (portamentoNoteNumber === undefined) {
1116
+ const volRelease = endTime +
1117
+ note.instrumentKey.volRelease * channel.releaseTime;
1118
+ const modRelease = endTime + note.instrumentKey.modRelease;
1119
+ note.filterNode.frequency
1120
+ .cancelScheduledValues(endTime)
1121
+ .linearRampToValueAtTime(0, modRelease);
1122
+ const stopTime = Math.min(volRelease, modRelease);
1123
+ return this.stopNote(endTime, stopTime, scheduledNotes, i);
1124
+ }
1125
+ else {
1126
+ const portamentoTime = endTime + channel.portamentoTime;
1127
+ const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1128
+ const detune = note.bufferSource.detune.value + detuneChange;
1129
+ note.bufferSource.detune
1130
+ .cancelScheduledValues(endTime)
1131
+ .linearRampToValueAtTime(detune, portamentoTime);
1132
+ return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1133
+ }
1005
1134
  }
1006
1135
  }
1007
- releaseNote(channelNumber, noteNumber, velocity) {
1136
+ releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1008
1137
  const now = this.audioContext.currentTime;
1009
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
1138
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1010
1139
  }
1011
1140
  releaseSustainPedal(channelNumber, halfVelocity) {
1012
1141
  const velocity = halfVelocity * 2;
@@ -1047,7 +1176,7 @@ export class Midy {
1047
1176
  case 0x90:
1048
1177
  return this.noteOn(channelNumber, data1, data2);
1049
1178
  case 0xA0:
1050
- return; // this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
1179
+ return this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
1051
1180
  case 0xB0:
1052
1181
  return this.handleControlChange(channelNumber, data1, data2);
1053
1182
  case 0xC0:
@@ -1181,12 +1310,14 @@ export class Midy {
1181
1310
  this.updateModulation(channel);
1182
1311
  }
1183
1312
  setPortamentoTime(channelNumber, portamentoTime) {
1184
- this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1313
+ const channel = this.channels[channelNumber];
1314
+ const factor = 5 * Math.log(10) / 127;
1315
+ channel.portamentoTime = Math.exp(factor * portamentoTime);
1185
1316
  }
1186
1317
  setVolume(channelNumber, volume) {
1187
1318
  const channel = this.channels[channelNumber];
1188
1319
  channel.volume = volume / 127;
1189
- this.updateChannelGain(channel);
1320
+ this.updateChannelVolume(channel);
1190
1321
  }
1191
1322
  panToGain(pan) {
1192
1323
  const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
@@ -1198,12 +1329,12 @@ export class Midy {
1198
1329
  setPan(channelNumber, pan) {
1199
1330
  const channel = this.channels[channelNumber];
1200
1331
  channel.pan = pan;
1201
- this.updateChannelGain(channel);
1332
+ this.updateChannelVolume(channel);
1202
1333
  }
1203
1334
  setExpression(channelNumber, expression) {
1204
1335
  const channel = this.channels[channelNumber];
1205
1336
  channel.expression = expression / 127;
1206
- this.updateChannelGain(channel);
1337
+ this.updateChannelVolume(channel);
1207
1338
  }
1208
1339
  setBankLSB(channelNumber, lsb) {
1209
1340
  this.channels[channelNumber].bankLSB = lsb;
@@ -1212,7 +1343,7 @@ export class Midy {
1212
1343
  this.channels[channelNumber].dataLSB = value;
1213
1344
  this.handleRPN(channelNumber, 0);
1214
1345
  }
1215
- updateChannelGain(channel) {
1346
+ updateChannelVolume(channel) {
1216
1347
  const now = this.audioContext.currentTime;
1217
1348
  const volume = channel.volume * channel.expression;
1218
1349
  const { gainLeft, gainRight } = this.panToGain(channel.pan);
@@ -1230,30 +1361,54 @@ export class Midy {
1230
1361
  this.releaseSustainPedal(channelNumber, value);
1231
1362
  }
1232
1363
  }
1233
- // TODO
1234
1364
  setPortamento(channelNumber, value) {
1235
1365
  this.channels[channelNumber].portamento = value >= 64;
1236
1366
  }
1237
1367
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1238
1368
  const channel = this.channels[channelNumber];
1369
+ const reverbEffect = this.reverbEffect;
1239
1370
  if (0 < channel.reverbSendLevel) {
1240
1371
  if (0 < reverbSendLevel) {
1241
1372
  const now = this.audioContext.currentTime;
1242
1373
  channel.reverbSendLevel = reverbSendLevel / 127;
1243
- reverbEffect.output.gain.cancelScheduledValues(now);
1244
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1374
+ reverbEffect.input.gain.cancelScheduledValues(now);
1375
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1245
1376
  }
1246
1377
  else {
1247
- channel.merger.disconnect(reverbEffect.input);
1378
+ channel.scheduledNotes.forEach((noteList) => {
1379
+ for (let i = 0; i < noteList.length; i++) {
1380
+ const note = noteList[i];
1381
+ if (!note)
1382
+ continue;
1383
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1384
+ continue;
1385
+ note.reverbEffectsSend.disconnect();
1386
+ }
1387
+ });
1248
1388
  }
1249
1389
  }
1250
1390
  else {
1251
1391
  if (0 < reverbSendLevel) {
1252
- channel.merger.connect(reverbEffect.input);
1253
1392
  const now = this.audioContext.currentTime;
1393
+ channel.scheduledNotes.forEach((noteList) => {
1394
+ for (let i = 0; i < noteList.length; i++) {
1395
+ const note = noteList[i];
1396
+ if (!note)
1397
+ continue;
1398
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1399
+ continue;
1400
+ if (!note.reverbEffectsSend) {
1401
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1402
+ gain: note.instrumentKey.reverbEffectsSend,
1403
+ });
1404
+ note.volumeNode.connect(note.reverbEffectsSend);
1405
+ }
1406
+ note.reverbEffectsSend.connect(reverbEffect.input);
1407
+ }
1408
+ });
1254
1409
  channel.reverbSendLevel = reverbSendLevel / 127;
1255
- reverbEffect.output.gain.cancelScheduledValues(now);
1256
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1410
+ reverbEffect.input.gain.cancelScheduledValues(now);
1411
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1257
1412
  }
1258
1413
  }
1259
1414
  }
@@ -1264,20 +1419,44 @@ export class Midy {
1264
1419
  if (0 < chorusSendLevel) {
1265
1420
  const now = this.audioContext.currentTime;
1266
1421
  channel.chorusSendLevel = chorusSendLevel / 127;
1267
- chorusEffect.output.gain.cancelScheduledValues(now);
1268
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1422
+ chorusEffect.input.gain.cancelScheduledValues(now);
1423
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1269
1424
  }
1270
1425
  else {
1271
- channel.merger.disconnect(chorusEffect.input);
1426
+ channel.scheduledNotes.forEach((noteList) => {
1427
+ for (let i = 0; i < noteList.length; i++) {
1428
+ const note = noteList[i];
1429
+ if (!note)
1430
+ continue;
1431
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1432
+ continue;
1433
+ note.chorusEffectsSend.disconnect();
1434
+ }
1435
+ });
1272
1436
  }
1273
1437
  }
1274
1438
  else {
1275
1439
  if (0 < chorusSendLevel) {
1276
- channel.merger.connect(chorusEffect.input);
1277
1440
  const now = this.audioContext.currentTime;
1441
+ channel.scheduledNotes.forEach((noteList) => {
1442
+ for (let i = 0; i < noteList.length; i++) {
1443
+ const note = noteList[i];
1444
+ if (!note)
1445
+ continue;
1446
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1447
+ continue;
1448
+ if (!note.chorusEffectsSend) {
1449
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1450
+ gain: note.instrumentKey.chorusEffectsSend,
1451
+ });
1452
+ note.volumeNode.connect(note.chorusEffectsSend);
1453
+ }
1454
+ note.chorusEffectsSend.connect(chorusEffect.input);
1455
+ }
1456
+ });
1278
1457
  channel.chorusSendLevel = chorusSendLevel / 127;
1279
- chorusEffect.output.gain.cancelScheduledValues(now);
1280
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1458
+ chorusEffect.input.gain.cancelScheduledValues(now);
1459
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1281
1460
  }
1282
1461
  }
1283
1462
  }
@@ -1554,20 +1733,22 @@ export class Midy {
1554
1733
  }
1555
1734
  }
1556
1735
  GM1SystemOn() {
1557
- this.channels.forEach((channel) => {
1736
+ for (let i = 0; i < this.channels.length; i++) {
1737
+ const channel = this.channels[i];
1558
1738
  channel.bankMSB = 0;
1559
1739
  channel.bankLSB = 0;
1560
1740
  channel.bank = 0;
1561
- });
1741
+ }
1562
1742
  this.channels[9].bankMSB = 1;
1563
1743
  this.channels[9].bank = 128;
1564
1744
  }
1565
1745
  GM2SystemOn() {
1566
- this.channels.forEach((channel) => {
1746
+ for (let i = 0; i < this.channels.length; i++) {
1747
+ const channel = this.channels[i];
1567
1748
  channel.bankMSB = 121;
1568
1749
  channel.bankLSB = 0;
1569
1750
  channel.bank = 121 * 128;
1570
- });
1751
+ }
1571
1752
  this.channels[9].bankMSB = 120;
1572
1753
  this.channels[9].bank = 120 * 128;
1573
1754
  }
@@ -1708,10 +1889,8 @@ export class Midy {
1708
1889
  }
1709
1890
  setReverbTime(value) {
1710
1891
  this.reverb.time = this.getReverbTime(value);
1711
- const { audioContext, channels, options } = this;
1712
- for (let i = 0; i < channels.length; i++) {
1713
- channels[i].reverbEffect = options.reverbAlgorithm(audioContext);
1714
- }
1892
+ const { audioContext, options } = this;
1893
+ this.reverbEffect = options.reverbAlgorithm(audioContext);
1715
1894
  }
1716
1895
  getReverbTime(value) {
1717
1896
  return Math.pow(Math.E, (value - 40) * 0.025);
@@ -1788,10 +1967,7 @@ export class Midy {
1788
1967
  const now = this.audioContext.currentTime;
1789
1968
  const modRate = this.getChorusModRate(value);
1790
1969
  this.chorus.modRate = modRate;
1791
- for (let i = 0; i < this.channels.length; i++) {
1792
- const lfo = this.channels[i].chorusEffect.lfo;
1793
- lfo.frequency.setValueAtTime(modRate, now);
1794
- }
1970
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
1795
1971
  }
1796
1972
  getChorusModRate(value) {
1797
1973
  return value * 0.122; // Hz
@@ -1800,12 +1976,9 @@ export class Midy {
1800
1976
  const now = this.audioContext.currentTime;
1801
1977
  const modDepth = this.getChorusModDepth(value);
1802
1978
  this.chorus.modDepth = modDepth;
1803
- for (let i = 0; i < this.channels.length; i++) {
1804
- const chorusEffect = this.channels[i].chorusEffect;
1805
- chorusEffect.lfoGain.gain
1806
- .cancelScheduledValues(now)
1807
- .setValueAtTime(modDepth / 2, now);
1808
- }
1979
+ this.chorusEffect.lfoGain.gain
1980
+ .cancelScheduledValues(now)
1981
+ .setValueAtTime(modDepth / 2, now);
1809
1982
  }
1810
1983
  getChorusModDepth(value) {
1811
1984
  return (value + 1) / 3200; // second
@@ -1814,14 +1987,11 @@ export class Midy {
1814
1987
  const now = this.audioContext.currentTime;
1815
1988
  const feedback = this.getChorusFeedback(value);
1816
1989
  this.chorus.feedback = feedback;
1817
- for (let i = 0; i < this.channels.length; i++) {
1818
- const chorusEffect = this.channels[i].chorusEffect;
1819
- for (let j = 0; j < chorusEffect.feedbackGains.length; j++) {
1820
- const feedbackGain = chorusEffect.feedbackGains[j];
1821
- feedbackGain.gain
1822
- .cancelScheduledValues(now)
1823
- .setValueAtTime(feedback, now);
1824
- }
1990
+ const chorusEffect = this.chorusEffect;
1991
+ for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
1992
+ chorusEffect.feedbackGains[i].gain
1993
+ .cancelScheduledValues(now)
1994
+ .setValueAtTime(feedback, now);
1825
1995
  }
1826
1996
  }
1827
1997
  getChorusFeedback(value) {
@@ -1829,15 +1999,28 @@ export class Midy {
1829
1999
  }
1830
2000
  setChorusSendToReverb(value) {
1831
2001
  const sendToReverb = this.getChorusSendToReverb(value);
1832
- if (0 < sendToReverb) {
1833
- const now = this.audioContext.currentTime;
2002
+ const sendGain = this.chorusEffect.sendGain;
2003
+ if (0 < this.chorus.sendToReverb) {
1834
2004
  this.chorus.sendToReverb = sendToReverb;
1835
- this.chorusEffect.sendGain.gain
1836
- .cancelScheduledValues(now)
1837
- .setValueAtTime(sendToReverb, now);
2005
+ if (0 < sendToReverb) {
2006
+ const now = this.audioContext.currentTime;
2007
+ sendGain.gain
2008
+ .cancelScheduledValues(now)
2009
+ .setValueAtTime(sendToReverb, now);
2010
+ }
2011
+ else {
2012
+ sendGain.disconnect();
2013
+ }
1838
2014
  }
1839
- else if (this.chorus.sendToReverb !== 0) {
1840
- this.chorusEffect.sendGain.disconnect(this.reverbEffect.input);
2015
+ else {
2016
+ this.chorus.sendToReverb = sendToReverb;
2017
+ if (0 < sendToReverb) {
2018
+ const now = this.audioContext.currentTime;
2019
+ sendGain.connect(this.reverbEffect.input);
2020
+ sendGain.gain
2021
+ .cancelScheduledValues(now)
2022
+ .setValueAtTime(sendToReverb, now);
2023
+ }
1841
2024
  }
1842
2025
  }
1843
2026
  getChorusSendToReverb(value) {
@@ -1876,7 +2059,7 @@ Object.defineProperty(Midy, "channelSettings", {
1876
2059
  currentBufferSource: null,
1877
2060
  volume: 100 / 127,
1878
2061
  pan: 64,
1879
- portamentoTime: 0,
2062
+ portamentoTime: 1, // sec
1880
2063
  filterResonance: 1,
1881
2064
  releaseTime: 1,
1882
2065
  attackTime: 1,