@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/script/midy.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = void 0;
4
- const _esm_js_1 = require("./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js");
5
- const _esm_js_2 = require("./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js");
4
+ const midi_file_1 = require("midi-file");
5
+ const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
6
  class Note {
7
7
  constructor(noteNumber, velocity, startTime, instrumentKey) {
8
8
  Object.defineProperty(this, "bufferSource", {
@@ -53,6 +53,18 @@ class Note {
53
53
  writable: true,
54
54
  value: void 0
55
55
  });
56
+ Object.defineProperty(this, "reverbEffectsSend", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: void 0
61
+ });
62
+ Object.defineProperty(this, "chorusEffectsSend", {
63
+ enumerable: true,
64
+ configurable: true,
65
+ writable: true,
66
+ value: void 0
67
+ });
56
68
  this.noteNumber = noteNumber;
57
69
  this.velocity = velocity;
58
70
  this.startTime = startTime;
@@ -208,6 +220,12 @@ class Midy {
208
220
  writable: true,
209
221
  value: []
210
222
  });
223
+ Object.defineProperty(this, "exclusiveClassMap", {
224
+ enumerable: true,
225
+ configurable: true,
226
+ writable: true,
227
+ value: new Map()
228
+ });
211
229
  Object.defineProperty(this, "defaultOptions", {
212
230
  enumerable: true,
213
231
  configurable: true,
@@ -252,24 +270,26 @@ class Midy {
252
270
  addSoundFont(soundFont) {
253
271
  const index = this.soundFonts.length;
254
272
  this.soundFonts.push(soundFont);
255
- soundFont.parsed.presetHeaders.forEach((presetHeader) => {
273
+ const presetHeaders = soundFont.parsed.presetHeaders;
274
+ for (let i = 0; i < presetHeaders.length; i++) {
275
+ const presetHeader = presetHeaders[i];
256
276
  if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
257
277
  const banks = this.soundFontTable[presetHeader.preset];
258
278
  banks.set(presetHeader.bank, index);
259
279
  }
260
- });
280
+ }
261
281
  }
262
282
  async loadSoundFont(soundFontUrl) {
263
283
  const response = await fetch(soundFontUrl);
264
284
  const arrayBuffer = await response.arrayBuffer();
265
- const parsed = (0, _esm_js_2.parse)(new Uint8Array(arrayBuffer));
266
- const soundFont = new _esm_js_2.SoundFont(parsed);
285
+ const parsed = (0, soundfont_parser_1.parse)(new Uint8Array(arrayBuffer));
286
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
267
287
  this.addSoundFont(soundFont);
268
288
  }
269
289
  async loadMIDI(midiUrl) {
270
290
  const response = await fetch(midiUrl);
271
291
  const arrayBuffer = await response.arrayBuffer();
272
- const midi = (0, _esm_js_1.parseMidi)(new Uint8Array(arrayBuffer));
292
+ const midi = (0, midi_file_1.parseMidi)(new Uint8Array(arrayBuffer));
273
293
  this.ticksPerBeat = midi.header.ticksPerBeat;
274
294
  const midiData = this.extractMidiData(midi);
275
295
  this.instruments = midiData.instruments;
@@ -309,27 +329,31 @@ class Midy {
309
329
  return channels;
310
330
  }
311
331
  async createNoteBuffer(instrumentKey, isSF3) {
332
+ const sampleStart = instrumentKey.start;
312
333
  const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
313
334
  if (isSF3) {
314
- const sample = new Uint8Array(instrumentKey.sample.length);
315
- sample.set(instrumentKey.sample);
316
- 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
- }
335
+ const sample = instrumentKey.sample;
336
+ const start = sample.byteOffset + sampleStart;
337
+ const end = sample.byteOffset + sampleEnd;
338
+ const buffer = sample.buffer.slice(start, end);
339
+ const audioBuffer = await this.audioContext.decodeAudioData(buffer);
321
340
  return audioBuffer;
322
341
  }
323
342
  else {
324
- const sample = instrumentKey.sample.subarray(0, sampleEnd);
325
- const floatSample = this.convertToFloat32Array(sample);
343
+ const sample = instrumentKey.sample;
344
+ const start = sample.byteOffset + sampleStart;
345
+ const end = sample.byteOffset + sampleEnd;
346
+ const buffer = sample.buffer.slice(start, end);
326
347
  const audioBuffer = new AudioBuffer({
327
348
  numberOfChannels: 1,
328
349
  length: sample.length,
329
350
  sampleRate: instrumentKey.sampleRate,
330
351
  });
331
352
  const channelData = audioBuffer.getChannelData(0);
332
- channelData.set(floatSample);
353
+ const int16Array = new Int16Array(buffer);
354
+ for (let i = 0; i < int16Array.length; i++) {
355
+ channelData[i] = int16Array[i] / 32768;
356
+ }
333
357
  return audioBuffer;
334
358
  }
335
359
  }
@@ -345,13 +369,23 @@ class Midy {
345
369
  }
346
370
  return bufferSource;
347
371
  }
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;
372
+ findPortamentoTarget(queueIndex) {
373
+ const endEvent = this.timeline[queueIndex];
374
+ if (!this.channels[endEvent.channel].portamento)
375
+ return;
376
+ const endTime = endEvent.startTime;
377
+ let target;
378
+ while (++queueIndex < this.timeline.length) {
379
+ const event = this.timeline[queueIndex];
380
+ if (endTime !== event.startTime)
381
+ break;
382
+ if (event.type !== "noteOn")
383
+ continue;
384
+ if (!target || event.noteNumber < target.noteNumber) {
385
+ target = event;
386
+ }
353
387
  }
354
- return float32Array;
388
+ return target;
355
389
  }
356
390
  async scheduleTimelineEvents(t, offset, queueIndex) {
357
391
  while (queueIndex < this.timeline.length) {
@@ -361,12 +395,15 @@ class Midy {
361
395
  switch (event.type) {
362
396
  case "noteOn":
363
397
  if (event.velocity !== 0) {
364
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
398
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
365
399
  break;
366
400
  }
367
401
  /* falls through */
368
402
  case "noteOff": {
369
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
403
+ const portamentoTarget = this.findPortamentoTarget(queueIndex);
404
+ if (portamentoTarget)
405
+ portamentoTarget.portamento = true;
406
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
370
407
  if (notePromise) {
371
408
  this.notePromises.push(notePromise);
372
409
  }
@@ -414,6 +451,7 @@ class Midy {
414
451
  if (queueIndex >= this.timeline.length) {
415
452
  await Promise.all(this.notePromises);
416
453
  this.notePromises = [];
454
+ this.exclusiveClassMap.clear();
417
455
  resolve();
418
456
  return;
419
457
  }
@@ -429,6 +467,7 @@ class Midy {
429
467
  }
430
468
  else if (this.isStopping) {
431
469
  await this.stopNotes(0, true);
470
+ this.exclusiveClassMap.clear();
432
471
  this.notePromises = [];
433
472
  resolve();
434
473
  this.isStopping = false;
@@ -437,6 +476,7 @@ class Midy {
437
476
  }
438
477
  else if (this.isSeeking) {
439
478
  this.stopNotes(0, true);
479
+ this.exclusiveClassMap.clear();
440
480
  this.startTime = this.audioContext.currentTime;
441
481
  queueIndex = this.getQueueIndex(this.resumeTime);
442
482
  offset = this.resumeTime - this.startTime;
@@ -470,9 +510,11 @@ class Midy {
470
510
  bankLSB: this.channels[i].bankLSB,
471
511
  };
472
512
  }
473
- midi.tracks.forEach((track) => {
513
+ for (let i = 0; i < midi.tracks.length; i++) {
514
+ const track = midi.tracks[i];
474
515
  let currentTicks = 0;
475
- track.forEach((event) => {
516
+ for (let j = 0; j < track.length; j++) {
517
+ const event = track[j];
476
518
  currentTicks += event.deltaTime;
477
519
  event.ticks = currentTicks;
478
520
  switch (event.type) {
@@ -525,16 +567,18 @@ class Midy {
525
567
  }
526
568
  delete event.deltaTime;
527
569
  timeline.push(event);
528
- });
529
- });
570
+ }
571
+ }
530
572
  const priority = {
531
573
  controller: 0,
532
574
  sysEx: 1,
575
+ noteOff: 2, // for portamento
576
+ noteOn: 3,
533
577
  };
534
578
  timeline.sort((a, b) => {
535
579
  if (a.ticks !== b.ticks)
536
580
  return a.ticks - b.ticks;
537
- return (priority[a.type] || 2) - (priority[b.type] || 2);
581
+ return (priority[a.type] || 4) - (priority[b.type] || 4);
538
582
  });
539
583
  let prevTempoTime = 0;
540
584
  let prevTempoTicks = 0;
@@ -551,7 +595,7 @@ class Midy {
551
595
  }
552
596
  return { instruments, timeline };
553
597
  }
554
- async stopChannelNotes(channelNumber, velocity, stopPedal) {
598
+ async stopChannelNotes(channelNumber, velocity, force) {
555
599
  const now = this.audioContext.currentTime;
556
600
  const channel = this.channels[channelNumber];
557
601
  channel.scheduledNotes.forEach((noteList) => {
@@ -559,16 +603,17 @@ class Midy {
559
603
  const note = noteList[i];
560
604
  if (!note)
561
605
  continue;
562
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, stopPedal);
606
+ const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
607
+ force);
563
608
  this.notePromises.push(promise);
564
609
  }
565
610
  });
566
611
  channel.scheduledNotes.clear();
567
612
  await Promise.all(this.notePromises);
568
613
  }
569
- stopNotes(velocity, stopPedal) {
614
+ stopNotes(velocity, force) {
570
615
  for (let i = 0; i < this.channels.length; i++) {
571
- this.stopChannelNotes(i, velocity, stopPedal);
616
+ this.stopChannelNotes(i, velocity, force);
572
617
  }
573
618
  return Promise.all(this.notePromises);
574
619
  }
@@ -660,14 +705,14 @@ class Midy {
660
705
  return impulse;
661
706
  }
662
707
  createConvolutionReverb(audioContext, impulse) {
663
- const output = new GainNode(audioContext);
708
+ const input = new GainNode(audioContext);
664
709
  const convolverNode = new ConvolverNode(audioContext, {
665
710
  buffer: impulse,
666
711
  });
667
- convolverNode.connect(output);
712
+ input.connect(convolverNode);
668
713
  return {
669
- input: convolverNode,
670
- output,
714
+ input,
715
+ output: convolverNode,
671
716
  convolverNode,
672
717
  };
673
718
  }
@@ -709,7 +754,6 @@ class Midy {
709
754
  // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
710
755
  createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
711
756
  const input = new GainNode(audioContext);
712
- const output = new GainNode(audioContext);
713
757
  const mergerGain = new GainNode(audioContext);
714
758
  for (let i = 0; i < combDelays.length; i++) {
715
759
  const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
@@ -720,7 +764,7 @@ class Midy {
720
764
  const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
721
765
  allpasses.push(allpass);
722
766
  }
723
- allpasses.at(-1).connect(output);
767
+ const output = allpasses.at(-1);
724
768
  return { input, output };
725
769
  }
726
770
  createChorusEffect(audioContext) {
@@ -782,6 +826,17 @@ class Midy {
782
826
  return instrumentKey.playbackRate(noteNumber) *
783
827
  Math.pow(2, semitoneOffset / 12);
784
828
  }
829
+ setPortamentoStartVolumeEnvelope(channel, note) {
830
+ const { instrumentKey, startTime } = note;
831
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
832
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
833
+ const volDelay = startTime + instrumentKey.volDelay;
834
+ const portamentoTime = volDelay + channel.portamentoTime;
835
+ note.volumeNode.gain
836
+ .cancelScheduledValues(startTime)
837
+ .setValueAtTime(0, volDelay)
838
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
839
+ }
785
840
  setVolumeEnvelope(channel, note) {
786
841
  const { instrumentKey, startTime } = note;
787
842
  const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
@@ -821,6 +876,25 @@ class Midy {
821
876
  const maxFrequency = 20000; // max Hz of initialFilterFc
822
877
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
823
878
  }
879
+ setPortamentoStartFilterEnvelope(channel, note) {
880
+ const { instrumentKey, noteNumber, startTime } = note;
881
+ const softPedalFactor = 1 -
882
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
883
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
884
+ softPedalFactor * channel.brightness;
885
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor * channel.brightness;
886
+ const sustainFreq = baseFreq +
887
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
888
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
889
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
890
+ const portamentoTime = startTime + channel.portamentoTime;
891
+ const modDelay = startTime + instrumentKey.modDelay;
892
+ note.filterNode.frequency
893
+ .cancelScheduledValues(startTime)
894
+ .setValueAtTime(adjustedBaseFreq, startTime)
895
+ .setValueAtTime(adjustedBaseFreq, modDelay)
896
+ .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
897
+ }
824
898
  setFilterEnvelope(channel, note) {
825
899
  const { instrumentKey, noteNumber, startTime } = note;
826
900
  const softPedalFactor = 1 -
@@ -888,7 +962,7 @@ class Midy {
888
962
  note.vibratoLFO.connect(note.vibratoDepth);
889
963
  note.vibratoDepth.connect(note.bufferSource.detune);
890
964
  }
891
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
965
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3) {
892
966
  const semitoneOffset = this.calcSemitoneOffset(channel);
893
967
  const note = new Note(noteNumber, velocity, startTime, instrumentKey);
894
968
  note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
@@ -897,8 +971,14 @@ class Midy {
897
971
  type: "lowpass",
898
972
  Q: instrumentKey.initialFilterQ / 10 * channel.filterResonance, // dB
899
973
  });
900
- this.setVolumeEnvelope(channel, note);
901
- this.setFilterEnvelope(channel, note);
974
+ if (portamento) {
975
+ this.setPortamentoStartVolumeEnvelope(channel, note);
976
+ this.setPortamentoStartFilterEnvelope(channel, note);
977
+ }
978
+ else {
979
+ this.setVolumeEnvelope(channel, note);
980
+ this.setFilterEnvelope(channel, note);
981
+ }
902
982
  if (0 < channel.vibratoDepth) {
903
983
  this.startVibrato(channel, note, startTime);
904
984
  }
@@ -915,7 +995,21 @@ class Midy {
915
995
  }
916
996
  note.bufferSource.connect(note.filterNode);
917
997
  note.filterNode.connect(note.volumeNode);
918
- note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
998
+ if (0 < channel.reverbSendLevel && 0 < instrumentKey.reverbEffectsSend) {
999
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1000
+ gain: instrumentKey.reverbEffectsSend,
1001
+ });
1002
+ note.volumeNode.connect(note.reverbEffectsSend);
1003
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
1004
+ }
1005
+ if (0 < channel.chorusSendLevel && 0 < instrumentKey.chorusEffectsSend) {
1006
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1007
+ gain: instrumentKey.chorusEffectsSend,
1008
+ });
1009
+ note.volumeNode.connect(note.chorusEffectsSend);
1010
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1011
+ }
1012
+ note.bufferSource.start(startTime);
919
1013
  return note;
920
1014
  }
921
1015
  calcBank(channel, channelNumber) {
@@ -927,7 +1021,7 @@ class Midy {
927
1021
  }
928
1022
  return channel.bank;
929
1023
  }
930
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1024
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
931
1025
  const channel = this.channels[channelNumber];
932
1026
  const bankNumber = this.calcBank(channel, channelNumber);
933
1027
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
@@ -938,12 +1032,25 @@ class Midy {
938
1032
  const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
939
1033
  if (!instrumentKey)
940
1034
  return;
941
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
1035
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3);
942
1036
  note.volumeNode.connect(channel.gainL);
943
1037
  note.volumeNode.connect(channel.gainR);
944
1038
  if (channel.sostenutoPedal) {
945
1039
  channel.sostenutoNotes.set(noteNumber, note);
946
1040
  }
1041
+ const exclusiveClass = instrumentKey.exclusiveClass;
1042
+ if (exclusiveClass !== 0) {
1043
+ if (this.exclusiveClassMap.has(exclusiveClass)) {
1044
+ const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1045
+ const [prevNote, prevChannelNumber] = prevEntry;
1046
+ if (!prevNote.ending) {
1047
+ this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1048
+ startTime, undefined, // portamentoNoteNumber
1049
+ true);
1050
+ }
1051
+ }
1052
+ this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1053
+ }
947
1054
  const scheduledNotes = channel.scheduledNotes;
948
1055
  if (scheduledNotes.has(noteNumber)) {
949
1056
  scheduledNotes.get(noteNumber).push(note);
@@ -952,13 +1059,48 @@ class Midy {
952
1059
  scheduledNotes.set(noteNumber, [note]);
953
1060
  }
954
1061
  }
955
- noteOn(channelNumber, noteNumber, velocity) {
1062
+ noteOn(channelNumber, noteNumber, velocity, portamento) {
956
1063
  const now = this.audioContext.currentTime;
957
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
1064
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1065
+ }
1066
+ stopNote(endTime, stopTime, scheduledNotes, index) {
1067
+ const note = scheduledNotes[index];
1068
+ note.volumeNode.gain
1069
+ .cancelScheduledValues(endTime)
1070
+ .linearRampToValueAtTime(0, stopTime);
1071
+ note.ending = true;
1072
+ this.scheduleTask(() => {
1073
+ note.bufferSource.loop = false;
1074
+ }, stopTime);
1075
+ return new Promise((resolve) => {
1076
+ note.bufferSource.onended = () => {
1077
+ scheduledNotes[index] = null;
1078
+ note.bufferSource.disconnect();
1079
+ note.volumeNode.disconnect();
1080
+ note.filterNode.disconnect();
1081
+ if (note.modulationDepth) {
1082
+ note.volumeDepth.disconnect();
1083
+ note.modulationDepth.disconnect();
1084
+ note.modulationLFO.stop();
1085
+ }
1086
+ if (note.vibratoDepth) {
1087
+ note.vibratoDepth.disconnect();
1088
+ note.vibratoLFO.stop();
1089
+ }
1090
+ if (note.reverbEffectsSend) {
1091
+ note.reverbEffectsSend.disconnect();
1092
+ }
1093
+ if (note.chorusEffectsSend) {
1094
+ note.chorusEffectsSend.disconnect();
1095
+ }
1096
+ resolve();
1097
+ };
1098
+ note.bufferSource.stop(stopTime);
1099
+ });
958
1100
  }
959
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, stopPedal = false) {
1101
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
960
1102
  const channel = this.channels[channelNumber];
961
- if (stopPedal) {
1103
+ if (!force) {
962
1104
  if (channel.sustainPedal)
963
1105
  return;
964
1106
  if (channel.sostenutoNotes.has(noteNumber))
@@ -973,43 +1115,30 @@ class Midy {
973
1115
  continue;
974
1116
  if (note.ending)
975
1117
  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
- });
1118
+ if (portamentoNoteNumber === undefined) {
1119
+ const volRelease = endTime +
1120
+ note.instrumentKey.volRelease * channel.releaseTime;
1121
+ const modRelease = endTime + note.instrumentKey.modRelease;
1122
+ note.filterNode.frequency
1123
+ .cancelScheduledValues(endTime)
1124
+ .linearRampToValueAtTime(0, modRelease);
1125
+ const stopTime = Math.min(volRelease, modRelease);
1126
+ return this.stopNote(endTime, stopTime, scheduledNotes, i);
1127
+ }
1128
+ else {
1129
+ const portamentoTime = endTime + channel.portamentoTime;
1130
+ const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1131
+ const detune = note.bufferSource.detune.value + detuneChange;
1132
+ note.bufferSource.detune
1133
+ .cancelScheduledValues(endTime)
1134
+ .linearRampToValueAtTime(detune, portamentoTime);
1135
+ return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1136
+ }
1008
1137
  }
1009
1138
  }
1010
- releaseNote(channelNumber, noteNumber, velocity) {
1139
+ releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1011
1140
  const now = this.audioContext.currentTime;
1012
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
1141
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1013
1142
  }
1014
1143
  releaseSustainPedal(channelNumber, halfVelocity) {
1015
1144
  const velocity = halfVelocity * 2;
@@ -1050,7 +1179,7 @@ class Midy {
1050
1179
  case 0x90:
1051
1180
  return this.noteOn(channelNumber, data1, data2);
1052
1181
  case 0xA0:
1053
- return; // this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
1182
+ return this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
1054
1183
  case 0xB0:
1055
1184
  return this.handleControlChange(channelNumber, data1, data2);
1056
1185
  case 0xC0:
@@ -1184,12 +1313,14 @@ class Midy {
1184
1313
  this.updateModulation(channel);
1185
1314
  }
1186
1315
  setPortamentoTime(channelNumber, portamentoTime) {
1187
- this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1316
+ const channel = this.channels[channelNumber];
1317
+ const factor = 5 * Math.log(10) / 127;
1318
+ channel.portamentoTime = Math.exp(factor * portamentoTime);
1188
1319
  }
1189
1320
  setVolume(channelNumber, volume) {
1190
1321
  const channel = this.channels[channelNumber];
1191
1322
  channel.volume = volume / 127;
1192
- this.updateChannelGain(channel);
1323
+ this.updateChannelVolume(channel);
1193
1324
  }
1194
1325
  panToGain(pan) {
1195
1326
  const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
@@ -1201,12 +1332,12 @@ class Midy {
1201
1332
  setPan(channelNumber, pan) {
1202
1333
  const channel = this.channels[channelNumber];
1203
1334
  channel.pan = pan;
1204
- this.updateChannelGain(channel);
1335
+ this.updateChannelVolume(channel);
1205
1336
  }
1206
1337
  setExpression(channelNumber, expression) {
1207
1338
  const channel = this.channels[channelNumber];
1208
1339
  channel.expression = expression / 127;
1209
- this.updateChannelGain(channel);
1340
+ this.updateChannelVolume(channel);
1210
1341
  }
1211
1342
  setBankLSB(channelNumber, lsb) {
1212
1343
  this.channels[channelNumber].bankLSB = lsb;
@@ -1215,7 +1346,7 @@ class Midy {
1215
1346
  this.channels[channelNumber].dataLSB = value;
1216
1347
  this.handleRPN(channelNumber, 0);
1217
1348
  }
1218
- updateChannelGain(channel) {
1349
+ updateChannelVolume(channel) {
1219
1350
  const now = this.audioContext.currentTime;
1220
1351
  const volume = channel.volume * channel.expression;
1221
1352
  const { gainLeft, gainRight } = this.panToGain(channel.pan);
@@ -1233,30 +1364,54 @@ class Midy {
1233
1364
  this.releaseSustainPedal(channelNumber, value);
1234
1365
  }
1235
1366
  }
1236
- // TODO
1237
1367
  setPortamento(channelNumber, value) {
1238
1368
  this.channels[channelNumber].portamento = value >= 64;
1239
1369
  }
1240
1370
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1241
1371
  const channel = this.channels[channelNumber];
1372
+ const reverbEffect = this.reverbEffect;
1242
1373
  if (0 < channel.reverbSendLevel) {
1243
1374
  if (0 < reverbSendLevel) {
1244
1375
  const now = this.audioContext.currentTime;
1245
1376
  channel.reverbSendLevel = reverbSendLevel / 127;
1246
- reverbEffect.output.gain.cancelScheduledValues(now);
1247
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1377
+ reverbEffect.input.gain.cancelScheduledValues(now);
1378
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1248
1379
  }
1249
1380
  else {
1250
- channel.merger.disconnect(reverbEffect.input);
1381
+ channel.scheduledNotes.forEach((noteList) => {
1382
+ for (let i = 0; i < noteList.length; i++) {
1383
+ const note = noteList[i];
1384
+ if (!note)
1385
+ continue;
1386
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1387
+ continue;
1388
+ note.reverbEffectsSend.disconnect();
1389
+ }
1390
+ });
1251
1391
  }
1252
1392
  }
1253
1393
  else {
1254
1394
  if (0 < reverbSendLevel) {
1255
- channel.merger.connect(reverbEffect.input);
1256
1395
  const now = this.audioContext.currentTime;
1396
+ channel.scheduledNotes.forEach((noteList) => {
1397
+ for (let i = 0; i < noteList.length; i++) {
1398
+ const note = noteList[i];
1399
+ if (!note)
1400
+ continue;
1401
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1402
+ continue;
1403
+ if (!note.reverbEffectsSend) {
1404
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1405
+ gain: note.instrumentKey.reverbEffectsSend,
1406
+ });
1407
+ note.volumeNode.connect(note.reverbEffectsSend);
1408
+ }
1409
+ note.reverbEffectsSend.connect(reverbEffect.input);
1410
+ }
1411
+ });
1257
1412
  channel.reverbSendLevel = reverbSendLevel / 127;
1258
- reverbEffect.output.gain.cancelScheduledValues(now);
1259
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1413
+ reverbEffect.input.gain.cancelScheduledValues(now);
1414
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1260
1415
  }
1261
1416
  }
1262
1417
  }
@@ -1267,20 +1422,44 @@ class Midy {
1267
1422
  if (0 < chorusSendLevel) {
1268
1423
  const now = this.audioContext.currentTime;
1269
1424
  channel.chorusSendLevel = chorusSendLevel / 127;
1270
- chorusEffect.output.gain.cancelScheduledValues(now);
1271
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1425
+ chorusEffect.input.gain.cancelScheduledValues(now);
1426
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1272
1427
  }
1273
1428
  else {
1274
- channel.merger.disconnect(chorusEffect.input);
1429
+ channel.scheduledNotes.forEach((noteList) => {
1430
+ for (let i = 0; i < noteList.length; i++) {
1431
+ const note = noteList[i];
1432
+ if (!note)
1433
+ continue;
1434
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1435
+ continue;
1436
+ note.chorusEffectsSend.disconnect();
1437
+ }
1438
+ });
1275
1439
  }
1276
1440
  }
1277
1441
  else {
1278
1442
  if (0 < chorusSendLevel) {
1279
- channel.merger.connect(chorusEffect.input);
1280
1443
  const now = this.audioContext.currentTime;
1444
+ channel.scheduledNotes.forEach((noteList) => {
1445
+ for (let i = 0; i < noteList.length; i++) {
1446
+ const note = noteList[i];
1447
+ if (!note)
1448
+ continue;
1449
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1450
+ continue;
1451
+ if (!note.chorusEffectsSend) {
1452
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1453
+ gain: note.instrumentKey.chorusEffectsSend,
1454
+ });
1455
+ note.volumeNode.connect(note.chorusEffectsSend);
1456
+ }
1457
+ note.chorusEffectsSend.connect(chorusEffect.input);
1458
+ }
1459
+ });
1281
1460
  channel.chorusSendLevel = chorusSendLevel / 127;
1282
- chorusEffect.output.gain.cancelScheduledValues(now);
1283
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1461
+ chorusEffect.input.gain.cancelScheduledValues(now);
1462
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1284
1463
  }
1285
1464
  }
1286
1465
  }
@@ -1557,20 +1736,22 @@ class Midy {
1557
1736
  }
1558
1737
  }
1559
1738
  GM1SystemOn() {
1560
- this.channels.forEach((channel) => {
1739
+ for (let i = 0; i < this.channels.length; i++) {
1740
+ const channel = this.channels[i];
1561
1741
  channel.bankMSB = 0;
1562
1742
  channel.bankLSB = 0;
1563
1743
  channel.bank = 0;
1564
- });
1744
+ }
1565
1745
  this.channels[9].bankMSB = 1;
1566
1746
  this.channels[9].bank = 128;
1567
1747
  }
1568
1748
  GM2SystemOn() {
1569
- this.channels.forEach((channel) => {
1749
+ for (let i = 0; i < this.channels.length; i++) {
1750
+ const channel = this.channels[i];
1570
1751
  channel.bankMSB = 121;
1571
1752
  channel.bankLSB = 0;
1572
1753
  channel.bank = 121 * 128;
1573
- });
1754
+ }
1574
1755
  this.channels[9].bankMSB = 120;
1575
1756
  this.channels[9].bank = 120 * 128;
1576
1757
  }
@@ -1711,10 +1892,8 @@ class Midy {
1711
1892
  }
1712
1893
  setReverbTime(value) {
1713
1894
  this.reverb.time = this.getReverbTime(value);
1714
- const { audioContext, channels, options } = this;
1715
- for (let i = 0; i < channels.length; i++) {
1716
- channels[i].reverbEffect = options.reverbAlgorithm(audioContext);
1717
- }
1895
+ const { audioContext, options } = this;
1896
+ this.reverbEffect = options.reverbAlgorithm(audioContext);
1718
1897
  }
1719
1898
  getReverbTime(value) {
1720
1899
  return Math.pow(Math.E, (value - 40) * 0.025);
@@ -1791,10 +1970,7 @@ class Midy {
1791
1970
  const now = this.audioContext.currentTime;
1792
1971
  const modRate = this.getChorusModRate(value);
1793
1972
  this.chorus.modRate = modRate;
1794
- for (let i = 0; i < this.channels.length; i++) {
1795
- const lfo = this.channels[i].chorusEffect.lfo;
1796
- lfo.frequency.setValueAtTime(modRate, now);
1797
- }
1973
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
1798
1974
  }
1799
1975
  getChorusModRate(value) {
1800
1976
  return value * 0.122; // Hz
@@ -1803,12 +1979,9 @@ class Midy {
1803
1979
  const now = this.audioContext.currentTime;
1804
1980
  const modDepth = this.getChorusModDepth(value);
1805
1981
  this.chorus.modDepth = modDepth;
1806
- for (let i = 0; i < this.channels.length; i++) {
1807
- const chorusEffect = this.channels[i].chorusEffect;
1808
- chorusEffect.lfoGain.gain
1809
- .cancelScheduledValues(now)
1810
- .setValueAtTime(modDepth / 2, now);
1811
- }
1982
+ this.chorusEffect.lfoGain.gain
1983
+ .cancelScheduledValues(now)
1984
+ .setValueAtTime(modDepth / 2, now);
1812
1985
  }
1813
1986
  getChorusModDepth(value) {
1814
1987
  return (value + 1) / 3200; // second
@@ -1817,14 +1990,11 @@ class Midy {
1817
1990
  const now = this.audioContext.currentTime;
1818
1991
  const feedback = this.getChorusFeedback(value);
1819
1992
  this.chorus.feedback = feedback;
1820
- for (let i = 0; i < this.channels.length; i++) {
1821
- const chorusEffect = this.channels[i].chorusEffect;
1822
- for (let j = 0; j < chorusEffect.feedbackGains.length; j++) {
1823
- const feedbackGain = chorusEffect.feedbackGains[j];
1824
- feedbackGain.gain
1825
- .cancelScheduledValues(now)
1826
- .setValueAtTime(feedback, now);
1827
- }
1993
+ const chorusEffect = this.chorusEffect;
1994
+ for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
1995
+ chorusEffect.feedbackGains[i].gain
1996
+ .cancelScheduledValues(now)
1997
+ .setValueAtTime(feedback, now);
1828
1998
  }
1829
1999
  }
1830
2000
  getChorusFeedback(value) {
@@ -1832,15 +2002,28 @@ class Midy {
1832
2002
  }
1833
2003
  setChorusSendToReverb(value) {
1834
2004
  const sendToReverb = this.getChorusSendToReverb(value);
1835
- if (0 < sendToReverb) {
1836
- const now = this.audioContext.currentTime;
2005
+ const sendGain = this.chorusEffect.sendGain;
2006
+ if (0 < this.chorus.sendToReverb) {
1837
2007
  this.chorus.sendToReverb = sendToReverb;
1838
- this.chorusEffect.sendGain.gain
1839
- .cancelScheduledValues(now)
1840
- .setValueAtTime(sendToReverb, now);
2008
+ if (0 < sendToReverb) {
2009
+ const now = this.audioContext.currentTime;
2010
+ sendGain.gain
2011
+ .cancelScheduledValues(now)
2012
+ .setValueAtTime(sendToReverb, now);
2013
+ }
2014
+ else {
2015
+ sendGain.disconnect();
2016
+ }
1841
2017
  }
1842
- else if (this.chorus.sendToReverb !== 0) {
1843
- this.chorusEffect.sendGain.disconnect(this.reverbEffect.input);
2018
+ else {
2019
+ this.chorus.sendToReverb = sendToReverb;
2020
+ if (0 < sendToReverb) {
2021
+ const now = this.audioContext.currentTime;
2022
+ sendGain.connect(this.reverbEffect.input);
2023
+ sendGain.gain
2024
+ .cancelScheduledValues(now)
2025
+ .setValueAtTime(sendToReverb, now);
2026
+ }
1844
2027
  }
1845
2028
  }
1846
2029
  getChorusSendToReverb(value) {
@@ -1880,7 +2063,7 @@ Object.defineProperty(Midy, "channelSettings", {
1880
2063
  currentBufferSource: null,
1881
2064
  volume: 100 / 127,
1882
2065
  pan: 64,
1883
- portamentoTime: 0,
2066
+ portamentoTime: 1, // sec
1884
2067
  filterResonance: 1,
1885
2068
  releaseTime: 1,
1886
2069
  attackTime: 1,