@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-GM2.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 MidyGM2 {
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 MidyGM2 {
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);
@@ -303,27 +323,31 @@ export class MidyGM2 {
303
323
  return channels;
304
324
  }
305
325
  async createNoteBuffer(instrumentKey, isSF3) {
326
+ const sampleStart = instrumentKey.start;
306
327
  const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
307
328
  if (isSF3) {
308
- const sample = new Uint8Array(instrumentKey.sample.length);
309
- sample.set(instrumentKey.sample);
310
- const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
311
- for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
312
- const channelData = audioBuffer.getChannelData(channel);
313
- channelData.set(channelData.subarray(0, sampleEnd));
314
- }
329
+ const sample = instrumentKey.sample;
330
+ const start = sample.byteOffset + sampleStart;
331
+ const end = sample.byteOffset + sampleEnd;
332
+ const buffer = sample.buffer.slice(start, end);
333
+ const audioBuffer = await this.audioContext.decodeAudioData(buffer);
315
334
  return audioBuffer;
316
335
  }
317
336
  else {
318
- const sample = instrumentKey.sample.subarray(0, sampleEnd);
319
- const floatSample = this.convertToFloat32Array(sample);
337
+ const sample = instrumentKey.sample;
338
+ const start = sample.byteOffset + sampleStart;
339
+ const end = sample.byteOffset + sampleEnd;
340
+ const buffer = sample.buffer.slice(start, end);
320
341
  const audioBuffer = new AudioBuffer({
321
342
  numberOfChannels: 1,
322
343
  length: sample.length,
323
344
  sampleRate: instrumentKey.sampleRate,
324
345
  });
325
346
  const channelData = audioBuffer.getChannelData(0);
326
- channelData.set(floatSample);
347
+ const int16Array = new Int16Array(buffer);
348
+ for (let i = 0; i < int16Array.length; i++) {
349
+ channelData[i] = int16Array[i] / 32768;
350
+ }
327
351
  return audioBuffer;
328
352
  }
329
353
  }
@@ -339,13 +363,23 @@ export class MidyGM2 {
339
363
  }
340
364
  return bufferSource;
341
365
  }
342
- convertToFloat32Array(uint8Array) {
343
- const int16Array = new Int16Array(uint8Array.buffer);
344
- const float32Array = new Float32Array(int16Array.length);
345
- for (let i = 0; i < int16Array.length; i++) {
346
- float32Array[i] = int16Array[i] / 32768;
366
+ findPortamentoTarget(queueIndex) {
367
+ const endEvent = this.timeline[queueIndex];
368
+ if (!this.channels[endEvent.channel].portamento)
369
+ return;
370
+ const endTime = endEvent.startTime;
371
+ let target;
372
+ while (++queueIndex < this.timeline.length) {
373
+ const event = this.timeline[queueIndex];
374
+ if (endTime !== event.startTime)
375
+ break;
376
+ if (event.type !== "noteOn")
377
+ continue;
378
+ if (!target || event.noteNumber < target.noteNumber) {
379
+ target = event;
380
+ }
347
381
  }
348
- return float32Array;
382
+ return target;
349
383
  }
350
384
  async scheduleTimelineEvents(t, offset, queueIndex) {
351
385
  while (queueIndex < this.timeline.length) {
@@ -355,12 +389,15 @@ export class MidyGM2 {
355
389
  switch (event.type) {
356
390
  case "noteOn":
357
391
  if (event.velocity !== 0) {
358
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
392
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
359
393
  break;
360
394
  }
361
395
  /* falls through */
362
396
  case "noteOff": {
363
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
397
+ const portamentoTarget = this.findPortamentoTarget(queueIndex);
398
+ if (portamentoTarget)
399
+ portamentoTarget.portamento = true;
400
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
364
401
  if (notePromise) {
365
402
  this.notePromises.push(notePromise);
366
403
  }
@@ -405,6 +442,7 @@ export class MidyGM2 {
405
442
  if (queueIndex >= this.timeline.length) {
406
443
  await Promise.all(this.notePromises);
407
444
  this.notePromises = [];
445
+ this.exclusiveClassMap.clear();
408
446
  resolve();
409
447
  return;
410
448
  }
@@ -420,6 +458,7 @@ export class MidyGM2 {
420
458
  }
421
459
  else if (this.isStopping) {
422
460
  await this.stopNotes(0, true);
461
+ this.exclusiveClassMap.clear();
423
462
  this.notePromises = [];
424
463
  resolve();
425
464
  this.isStopping = false;
@@ -428,6 +467,7 @@ export class MidyGM2 {
428
467
  }
429
468
  else if (this.isSeeking) {
430
469
  this.stopNotes(0, true);
470
+ this.exclusiveClassMap.clear();
431
471
  this.startTime = this.audioContext.currentTime;
432
472
  queueIndex = this.getQueueIndex(this.resumeTime);
433
473
  offset = this.resumeTime - this.startTime;
@@ -461,9 +501,11 @@ export class MidyGM2 {
461
501
  bankLSB: this.channels[i].bankLSB,
462
502
  };
463
503
  }
464
- midi.tracks.forEach((track) => {
504
+ for (let i = 0; i < midi.tracks.length; i++) {
505
+ const track = midi.tracks[i];
465
506
  let currentTicks = 0;
466
- track.forEach((event) => {
507
+ for (let j = 0; j < track.length; j++) {
508
+ const event = track[j];
467
509
  currentTicks += event.deltaTime;
468
510
  event.ticks = currentTicks;
469
511
  switch (event.type) {
@@ -516,16 +558,18 @@ export class MidyGM2 {
516
558
  }
517
559
  delete event.deltaTime;
518
560
  timeline.push(event);
519
- });
520
- });
561
+ }
562
+ }
521
563
  const priority = {
522
564
  controller: 0,
523
565
  sysEx: 1,
566
+ noteOff: 2, // for portamento
567
+ noteOn: 3,
524
568
  };
525
569
  timeline.sort((a, b) => {
526
570
  if (a.ticks !== b.ticks)
527
571
  return a.ticks - b.ticks;
528
- return (priority[a.type] || 2) - (priority[b.type] || 2);
572
+ return (priority[a.type] || 4) - (priority[b.type] || 4);
529
573
  });
530
574
  let prevTempoTime = 0;
531
575
  let prevTempoTicks = 0;
@@ -542,7 +586,7 @@ export class MidyGM2 {
542
586
  }
543
587
  return { instruments, timeline };
544
588
  }
545
- async stopChannelNotes(channelNumber, velocity, stopPedal) {
589
+ async stopChannelNotes(channelNumber, velocity, force) {
546
590
  const now = this.audioContext.currentTime;
547
591
  const channel = this.channels[channelNumber];
548
592
  channel.scheduledNotes.forEach((noteList) => {
@@ -550,16 +594,17 @@ export class MidyGM2 {
550
594
  const note = noteList[i];
551
595
  if (!note)
552
596
  continue;
553
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, stopPedal);
597
+ const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
598
+ force);
554
599
  this.notePromises.push(promise);
555
600
  }
556
601
  });
557
602
  channel.scheduledNotes.clear();
558
603
  await Promise.all(this.notePromises);
559
604
  }
560
- stopNotes(velocity, stopPedal) {
605
+ stopNotes(velocity, force) {
561
606
  for (let i = 0; i < this.channels.length; i++) {
562
- this.stopChannelNotes(i, velocity, stopPedal);
607
+ this.stopChannelNotes(i, velocity, force);
563
608
  }
564
609
  return Promise.all(this.notePromises);
565
610
  }
@@ -651,14 +696,14 @@ export class MidyGM2 {
651
696
  return impulse;
652
697
  }
653
698
  createConvolutionReverb(audioContext, impulse) {
654
- const output = new GainNode(audioContext);
699
+ const input = new GainNode(audioContext);
655
700
  const convolverNode = new ConvolverNode(audioContext, {
656
701
  buffer: impulse,
657
702
  });
658
- convolverNode.connect(output);
703
+ input.connect(convolverNode);
659
704
  return {
660
- input: convolverNode,
661
- output,
705
+ input,
706
+ output: convolverNode,
662
707
  convolverNode,
663
708
  };
664
709
  }
@@ -700,7 +745,6 @@ export class MidyGM2 {
700
745
  // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
701
746
  createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
702
747
  const input = new GainNode(audioContext);
703
- const output = new GainNode(audioContext);
704
748
  const mergerGain = new GainNode(audioContext);
705
749
  for (let i = 0; i < combDelays.length; i++) {
706
750
  const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
@@ -711,7 +755,7 @@ export class MidyGM2 {
711
755
  const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
712
756
  allpasses.push(allpass);
713
757
  }
714
- allpasses.at(-1).connect(output);
758
+ const output = allpasses.at(-1);
715
759
  return { input, output };
716
760
  }
717
761
  createChorusEffect(audioContext) {
@@ -773,6 +817,17 @@ export class MidyGM2 {
773
817
  return instrumentKey.playbackRate(noteNumber) *
774
818
  Math.pow(2, semitoneOffset / 12);
775
819
  }
820
+ setPortamentoStartVolumeEnvelope(channel, note) {
821
+ const { instrumentKey, startTime } = note;
822
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
823
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
824
+ const volDelay = startTime + instrumentKey.volDelay;
825
+ const portamentoTime = volDelay + channel.portamentoTime;
826
+ note.volumeNode.gain
827
+ .cancelScheduledValues(startTime)
828
+ .setValueAtTime(0, volDelay)
829
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
830
+ }
776
831
  setVolumeEnvelope(note) {
777
832
  const { instrumentKey, startTime } = note;
778
833
  const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
@@ -812,6 +867,25 @@ export class MidyGM2 {
812
867
  const maxFrequency = 20000; // max Hz of initialFilterFc
813
868
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
814
869
  }
870
+ setPortamentoStartFilterEnvelope(channel, note) {
871
+ const { instrumentKey, noteNumber, startTime } = note;
872
+ const softPedalFactor = 1 -
873
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
874
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
875
+ softPedalFactor;
876
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
877
+ const sustainFreq = baseFreq +
878
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
879
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
880
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
881
+ const portamentoTime = startTime + channel.portamentoTime;
882
+ const modDelay = startTime + instrumentKey.modDelay;
883
+ note.filterNode.frequency
884
+ .cancelScheduledValues(startTime)
885
+ .setValueAtTime(adjustedBaseFreq, startTime)
886
+ .setValueAtTime(adjustedBaseFreq, modDelay)
887
+ .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
888
+ }
815
889
  setFilterEnvelope(channel, note) {
816
890
  const { instrumentKey, noteNumber, startTime } = note;
817
891
  const softPedalFactor = 1 -
@@ -879,17 +953,23 @@ export class MidyGM2 {
879
953
  note.vibratoLFO.connect(note.vibratoDepth);
880
954
  note.vibratoDepth.connect(note.bufferSource.detune);
881
955
  }
882
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
956
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3) {
883
957
  const semitoneOffset = this.calcSemitoneOffset(channel);
884
958
  const note = new Note(noteNumber, velocity, startTime, instrumentKey);
885
959
  note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
886
960
  note.volumeNode = new GainNode(this.audioContext);
887
961
  note.filterNode = new BiquadFilterNode(this.audioContext, {
888
962
  type: "lowpass",
889
- Q: instrumentKey.initialFilterQ / 10 * channel.filterResonance, // dB
963
+ Q: instrumentKey.initialFilterQ / 10, // dB
890
964
  });
891
- this.setVolumeEnvelope(note);
892
- this.setFilterEnvelope(channel, note);
965
+ if (portamento) {
966
+ this.setPortamentoStartVolumeEnvelope(channel, note);
967
+ this.setPortamentoStartFilterEnvelope(channel, note);
968
+ }
969
+ else {
970
+ this.setVolumeEnvelope(note);
971
+ this.setFilterEnvelope(channel, note);
972
+ }
893
973
  if (0 < channel.vibratoDepth) {
894
974
  this.startVibrato(channel, note, startTime);
895
975
  }
@@ -906,7 +986,21 @@ export class MidyGM2 {
906
986
  }
907
987
  note.bufferSource.connect(note.filterNode);
908
988
  note.filterNode.connect(note.volumeNode);
909
- note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
989
+ if (0 < channel.reverbSendLevel && 0 < instrumentKey.reverbEffectsSend) {
990
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
991
+ gain: instrumentKey.reverbEffectsSend,
992
+ });
993
+ note.volumeNode.connect(note.reverbEffectsSend);
994
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
995
+ }
996
+ if (0 < channel.chorusSendLevel && 0 < instrumentKey.chorusEffectsSend) {
997
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
998
+ gain: instrumentKey.chorusEffectsSend,
999
+ });
1000
+ note.volumeNode.connect(note.chorusEffectsSend);
1001
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1002
+ }
1003
+ note.bufferSource.start(startTime);
910
1004
  return note;
911
1005
  }
912
1006
  calcBank(channel, channelNumber) {
@@ -918,7 +1012,7 @@ export class MidyGM2 {
918
1012
  }
919
1013
  return channel.bank;
920
1014
  }
921
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1015
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
922
1016
  const channel = this.channels[channelNumber];
923
1017
  const bankNumber = this.calcBank(channel, channelNumber);
924
1018
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
@@ -929,12 +1023,25 @@ export class MidyGM2 {
929
1023
  const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
930
1024
  if (!instrumentKey)
931
1025
  return;
932
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
1026
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3);
933
1027
  note.volumeNode.connect(channel.gainL);
934
1028
  note.volumeNode.connect(channel.gainR);
935
1029
  if (channel.sostenutoPedal) {
936
1030
  channel.sostenutoNotes.set(noteNumber, note);
937
1031
  }
1032
+ const exclusiveClass = instrumentKey.exclusiveClass;
1033
+ if (exclusiveClass !== 0) {
1034
+ if (this.exclusiveClassMap.has(exclusiveClass)) {
1035
+ const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1036
+ const [prevNote, prevChannelNumber] = prevEntry;
1037
+ if (!prevNote.ending) {
1038
+ this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1039
+ startTime, undefined, // portamentoNoteNumber
1040
+ true);
1041
+ }
1042
+ }
1043
+ this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1044
+ }
938
1045
  const scheduledNotes = channel.scheduledNotes;
939
1046
  if (scheduledNotes.has(noteNumber)) {
940
1047
  scheduledNotes.get(noteNumber).push(note);
@@ -943,13 +1050,48 @@ export class MidyGM2 {
943
1050
  scheduledNotes.set(noteNumber, [note]);
944
1051
  }
945
1052
  }
946
- noteOn(channelNumber, noteNumber, velocity) {
1053
+ noteOn(channelNumber, noteNumber, velocity, portamento) {
947
1054
  const now = this.audioContext.currentTime;
948
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
1055
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1056
+ }
1057
+ stopNote(endTime, stopTime, scheduledNotes, index) {
1058
+ const note = scheduledNotes[index];
1059
+ note.volumeNode.gain
1060
+ .cancelScheduledValues(endTime)
1061
+ .linearRampToValueAtTime(0, stopTime);
1062
+ note.ending = true;
1063
+ this.scheduleTask(() => {
1064
+ note.bufferSource.loop = false;
1065
+ }, stopTime);
1066
+ return new Promise((resolve) => {
1067
+ note.bufferSource.onended = () => {
1068
+ scheduledNotes[index] = null;
1069
+ note.bufferSource.disconnect();
1070
+ note.volumeNode.disconnect();
1071
+ note.filterNode.disconnect();
1072
+ if (note.modulationDepth) {
1073
+ note.volumeDepth.disconnect();
1074
+ note.modulationDepth.disconnect();
1075
+ note.modulationLFO.stop();
1076
+ }
1077
+ if (note.vibratoDepth) {
1078
+ note.vibratoDepth.disconnect();
1079
+ note.vibratoLFO.stop();
1080
+ }
1081
+ if (note.reverbEffectsSend) {
1082
+ note.reverbEffectsSend.disconnect();
1083
+ }
1084
+ if (note.chorusEffectsSend) {
1085
+ note.chorusEffectsSend.disconnect();
1086
+ }
1087
+ resolve();
1088
+ };
1089
+ note.bufferSource.stop(stopTime);
1090
+ });
949
1091
  }
950
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, stopPedal = false) {
1092
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
951
1093
  const channel = this.channels[channelNumber];
952
- if (stopPedal) {
1094
+ if (!force) {
953
1095
  if (channel.sustainPedal)
954
1096
  return;
955
1097
  if (channel.sostenutoNotes.has(noteNumber))
@@ -964,38 +1106,29 @@ export class MidyGM2 {
964
1106
  continue;
965
1107
  if (note.ending)
966
1108
  continue;
967
- const volEndTime = stopTime + note.instrumentKey.volRelease;
968
- note.volumeNode.gain
969
- .cancelScheduledValues(stopTime)
970
- .linearRampToValueAtTime(0, volEndTime);
971
- const modRelease = stopTime + note.instrumentKey.modRelease;
972
- note.filterNode.frequency
973
- .cancelScheduledValues(stopTime)
974
- .linearRampToValueAtTime(0, modRelease);
975
- note.ending = true;
976
- this.scheduleTask(() => {
977
- note.bufferSource.loop = false;
978
- }, stopTime);
979
- return new Promise((resolve) => {
980
- note.bufferSource.onended = () => {
981
- scheduledNotes[i] = null;
982
- note.bufferSource.disconnect();
983
- note.volumeNode.disconnect();
984
- note.filterNode.disconnect();
985
- if (note.modulationDepth) {
986
- note.volumeDepth.disconnect();
987
- note.modulationDepth.disconnect();
988
- note.modulationLFO.stop();
989
- }
990
- resolve();
991
- };
992
- note.bufferSource.stop(volEndTime);
993
- });
1109
+ if (portamentoNoteNumber === undefined) {
1110
+ const volRelease = endTime + note.instrumentKey.volRelease;
1111
+ const modRelease = endTime + note.instrumentKey.modRelease;
1112
+ note.filterNode.frequency
1113
+ .cancelScheduledValues(endTime)
1114
+ .linearRampToValueAtTime(0, modRelease);
1115
+ const stopTime = Math.min(volRelease, modRelease);
1116
+ return this.stopNote(endTime, stopTime, scheduledNotes, i);
1117
+ }
1118
+ else {
1119
+ const portamentoTime = endTime + channel.portamentoTime;
1120
+ const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1121
+ const detune = note.bufferSource.detune.value + detuneChange;
1122
+ note.bufferSource.detune
1123
+ .cancelScheduledValues(endTime)
1124
+ .linearRampToValueAtTime(detune, portamentoTime);
1125
+ return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1126
+ }
994
1127
  }
995
1128
  }
996
- releaseNote(channelNumber, noteNumber, velocity) {
1129
+ releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
997
1130
  const now = this.audioContext.currentTime;
998
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
1131
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
999
1132
  }
1000
1133
  releaseSustainPedal(channelNumber, halfVelocity) {
1001
1134
  const velocity = halfVelocity * 2;
@@ -1143,12 +1276,14 @@ export class MidyGM2 {
1143
1276
  this.updateModulation(channel);
1144
1277
  }
1145
1278
  setPortamentoTime(channelNumber, portamentoTime) {
1146
- this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1279
+ const channel = this.channels[channelNumber];
1280
+ const factor = 5 * Math.log(10) / 127;
1281
+ channel.portamentoTime = Math.exp(factor * portamentoTime);
1147
1282
  }
1148
1283
  setVolume(channelNumber, volume) {
1149
1284
  const channel = this.channels[channelNumber];
1150
1285
  channel.volume = volume / 127;
1151
- this.updateChannelGain(channel);
1286
+ this.updateChannelVolume(channel);
1152
1287
  }
1153
1288
  panToGain(pan) {
1154
1289
  const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
@@ -1160,12 +1295,12 @@ export class MidyGM2 {
1160
1295
  setPan(channelNumber, pan) {
1161
1296
  const channel = this.channels[channelNumber];
1162
1297
  channel.pan = pan;
1163
- this.updateChannelGain(channel);
1298
+ this.updateChannelVolume(channel);
1164
1299
  }
1165
1300
  setExpression(channelNumber, expression) {
1166
1301
  const channel = this.channels[channelNumber];
1167
1302
  channel.expression = expression / 127;
1168
- this.updateChannelGain(channel);
1303
+ this.updateChannelVolume(channel);
1169
1304
  }
1170
1305
  setBankLSB(channelNumber, lsb) {
1171
1306
  this.channels[channelNumber].bankLSB = lsb;
@@ -1174,7 +1309,7 @@ export class MidyGM2 {
1174
1309
  this.channels[channelNumber].dataLSB = value;
1175
1310
  this.handleRPN(channelNumber);
1176
1311
  }
1177
- updateChannelGain(channel) {
1312
+ updateChannelVolume(channel) {
1178
1313
  const now = this.audioContext.currentTime;
1179
1314
  const volume = channel.volume * channel.expression;
1180
1315
  const { gainLeft, gainRight } = this.panToGain(channel.pan);
@@ -1192,30 +1327,54 @@ export class MidyGM2 {
1192
1327
  this.releaseSustainPedal(channelNumber, value);
1193
1328
  }
1194
1329
  }
1195
- // TODO
1196
1330
  setPortamento(channelNumber, value) {
1197
1331
  this.channels[channelNumber].portamento = value >= 64;
1198
1332
  }
1199
1333
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1200
1334
  const channel = this.channels[channelNumber];
1335
+ const reverbEffect = this.reverbEffect;
1201
1336
  if (0 < channel.reverbSendLevel) {
1202
1337
  if (0 < reverbSendLevel) {
1203
1338
  const now = this.audioContext.currentTime;
1204
1339
  channel.reverbSendLevel = reverbSendLevel / 127;
1205
- reverbEffect.output.gain.cancelScheduledValues(now);
1206
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1340
+ reverbEffect.input.gain.cancelScheduledValues(now);
1341
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1207
1342
  }
1208
1343
  else {
1209
- channel.merger.disconnect(reverbEffect.input);
1344
+ channel.scheduledNotes.forEach((noteList) => {
1345
+ for (let i = 0; i < noteList.length; i++) {
1346
+ const note = noteList[i];
1347
+ if (!note)
1348
+ continue;
1349
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1350
+ continue;
1351
+ note.reverbEffectsSend.disconnect();
1352
+ }
1353
+ });
1210
1354
  }
1211
1355
  }
1212
1356
  else {
1213
1357
  if (0 < reverbSendLevel) {
1214
- channel.merger.connect(reverbEffect.input);
1215
1358
  const now = this.audioContext.currentTime;
1359
+ channel.scheduledNotes.forEach((noteList) => {
1360
+ for (let i = 0; i < noteList.length; i++) {
1361
+ const note = noteList[i];
1362
+ if (!note)
1363
+ continue;
1364
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1365
+ continue;
1366
+ if (!note.reverbEffectsSend) {
1367
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1368
+ gain: note.instrumentKey.reverbEffectsSend,
1369
+ });
1370
+ note.volumeNode.connect(note.reverbEffectsSend);
1371
+ }
1372
+ note.reverbEffectsSend.connect(reverbEffect.input);
1373
+ }
1374
+ });
1216
1375
  channel.reverbSendLevel = reverbSendLevel / 127;
1217
- reverbEffect.output.gain.cancelScheduledValues(now);
1218
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1376
+ reverbEffect.input.gain.cancelScheduledValues(now);
1377
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1219
1378
  }
1220
1379
  }
1221
1380
  }
@@ -1226,20 +1385,44 @@ export class MidyGM2 {
1226
1385
  if (0 < chorusSendLevel) {
1227
1386
  const now = this.audioContext.currentTime;
1228
1387
  channel.chorusSendLevel = chorusSendLevel / 127;
1229
- chorusEffect.output.gain.cancelScheduledValues(now);
1230
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1388
+ chorusEffect.input.gain.cancelScheduledValues(now);
1389
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1231
1390
  }
1232
1391
  else {
1233
- channel.merger.disconnect(chorusEffect.input);
1392
+ channel.scheduledNotes.forEach((noteList) => {
1393
+ for (let i = 0; i < noteList.length; i++) {
1394
+ const note = noteList[i];
1395
+ if (!note)
1396
+ continue;
1397
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1398
+ continue;
1399
+ note.chorusEffectsSend.disconnect();
1400
+ }
1401
+ });
1234
1402
  }
1235
1403
  }
1236
1404
  else {
1237
1405
  if (0 < chorusSendLevel) {
1238
- channel.merger.connect(chorusEffect.input);
1239
1406
  const now = this.audioContext.currentTime;
1407
+ channel.scheduledNotes.forEach((noteList) => {
1408
+ for (let i = 0; i < noteList.length; i++) {
1409
+ const note = noteList[i];
1410
+ if (!note)
1411
+ continue;
1412
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1413
+ continue;
1414
+ if (!note.chorusEffectsSend) {
1415
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1416
+ gain: note.instrumentKey.chorusEffectsSend,
1417
+ });
1418
+ note.volumeNode.connect(note.chorusEffectsSend);
1419
+ }
1420
+ note.chorusEffectsSend.connect(chorusEffect.input);
1421
+ }
1422
+ });
1240
1423
  channel.chorusSendLevel = chorusSendLevel / 127;
1241
- chorusEffect.output.gain.cancelScheduledValues(now);
1242
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1424
+ chorusEffect.input.gain.cancelScheduledValues(now);
1425
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1243
1426
  }
1244
1427
  }
1245
1428
  }
@@ -1425,20 +1608,22 @@ export class MidyGM2 {
1425
1608
  }
1426
1609
  }
1427
1610
  GM1SystemOn() {
1428
- this.channels.forEach((channel) => {
1611
+ for (let i = 0; i < this.channels.length; i++) {
1612
+ const channel = this.channels[i];
1429
1613
  channel.bankMSB = 0;
1430
1614
  channel.bankLSB = 0;
1431
1615
  channel.bank = 0;
1432
- });
1616
+ }
1433
1617
  this.channels[9].bankMSB = 1;
1434
1618
  this.channels[9].bank = 128;
1435
1619
  }
1436
1620
  GM2SystemOn() {
1437
- this.channels.forEach((channel) => {
1621
+ for (let i = 0; i < this.channels.length; i++) {
1622
+ const channel = this.channels[i];
1438
1623
  channel.bankMSB = 121;
1439
1624
  channel.bankLSB = 0;
1440
1625
  channel.bank = 121 * 128;
1441
- });
1626
+ }
1442
1627
  this.channels[9].bankMSB = 120;
1443
1628
  this.channels[9].bank = 120 * 128;
1444
1629
  }
@@ -1579,10 +1764,8 @@ export class MidyGM2 {
1579
1764
  }
1580
1765
  setReverbTime(value) {
1581
1766
  this.reverb.time = this.getReverbTime(value);
1582
- const { audioContext, channels, options } = this;
1583
- for (let i = 0; i < channels.length; i++) {
1584
- channels[i].reverbEffect = options.reverbAlgorithm(audioContext);
1585
- }
1767
+ const { audioContext, options } = this;
1768
+ this.reverbEffect = options.reverbAlgorithm(audioContext);
1586
1769
  }
1587
1770
  getReverbTime(value) {
1588
1771
  return Math.pow(Math.E, (value - 40) * 0.025);
@@ -1659,10 +1842,7 @@ export class MidyGM2 {
1659
1842
  const now = this.audioContext.currentTime;
1660
1843
  const modRate = this.getChorusModRate(value);
1661
1844
  this.chorus.modRate = modRate;
1662
- for (let i = 0; i < this.channels.length; i++) {
1663
- const lfo = this.channels[i].chorusEffect.lfo;
1664
- lfo.frequency.setValueAtTime(modRate, now);
1665
- }
1845
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
1666
1846
  }
1667
1847
  getChorusModRate(value) {
1668
1848
  return value * 0.122; // Hz
@@ -1671,12 +1851,9 @@ export class MidyGM2 {
1671
1851
  const now = this.audioContext.currentTime;
1672
1852
  const modDepth = this.getChorusModDepth(value);
1673
1853
  this.chorus.modDepth = modDepth;
1674
- for (let i = 0; i < this.channels.length; i++) {
1675
- const chorusEffect = this.channels[i].chorusEffect;
1676
- chorusEffect.lfoGain.gain
1677
- .cancelScheduledValues(now)
1678
- .setValueAtTime(modDepth / 2, now);
1679
- }
1854
+ this.chorusEffect.lfoGain.gain
1855
+ .cancelScheduledValues(now)
1856
+ .setValueAtTime(modDepth / 2, now);
1680
1857
  }
1681
1858
  getChorusModDepth(value) {
1682
1859
  return (value + 1) / 3200; // second
@@ -1685,14 +1862,11 @@ export class MidyGM2 {
1685
1862
  const now = this.audioContext.currentTime;
1686
1863
  const feedback = this.getChorusFeedback(value);
1687
1864
  this.chorus.feedback = feedback;
1688
- for (let i = 0; i < this.channels.length; i++) {
1689
- const chorusEffect = this.channels[i].chorusEffect;
1690
- for (let j = 0; j < chorusEffect.feedbackGains.length; j++) {
1691
- const feedbackGain = chorusEffect.feedbackGains[j];
1692
- feedbackGain.gain
1693
- .cancelScheduledValues(now)
1694
- .setValueAtTime(feedback, now);
1695
- }
1865
+ const chorusEffect = this.chorusEffect;
1866
+ for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
1867
+ chorusEffect.feedbackGains[i].gain
1868
+ .cancelScheduledValues(now)
1869
+ .setValueAtTime(feedback, now);
1696
1870
  }
1697
1871
  }
1698
1872
  getChorusFeedback(value) {
@@ -1700,15 +1874,28 @@ export class MidyGM2 {
1700
1874
  }
1701
1875
  setChorusSendToReverb(value) {
1702
1876
  const sendToReverb = this.getChorusSendToReverb(value);
1703
- if (0 < sendToReverb) {
1704
- const now = this.audioContext.currentTime;
1877
+ const sendGain = this.chorusEffect.sendGain;
1878
+ if (0 < this.chorus.sendToReverb) {
1705
1879
  this.chorus.sendToReverb = sendToReverb;
1706
- this.chorusEffect.sendGain.gain
1707
- .cancelScheduledValues(now)
1708
- .setValueAtTime(sendToReverb, now);
1880
+ if (0 < sendToReverb) {
1881
+ const now = this.audioContext.currentTime;
1882
+ sendGain.gain
1883
+ .cancelScheduledValues(now)
1884
+ .setValueAtTime(sendToReverb, now);
1885
+ }
1886
+ else {
1887
+ sendGain.disconnect();
1888
+ }
1709
1889
  }
1710
- else if (this.chorus.sendToReverb !== 0) {
1711
- this.chorusEffect.sendGain.disconnect(this.reverbEffect.input);
1890
+ else {
1891
+ this.chorus.sendToReverb = sendToReverb;
1892
+ if (0 < sendToReverb) {
1893
+ const now = this.audioContext.currentTime;
1894
+ sendGain.connect(this.reverbEffect.input);
1895
+ sendGain.gain
1896
+ .cancelScheduledValues(now)
1897
+ .setValueAtTime(sendToReverb, now);
1898
+ }
1712
1899
  }
1713
1900
  }
1714
1901
  getChorusSendToReverb(value) {
@@ -1747,7 +1934,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
1747
1934
  currentBufferSource: null,
1748
1935
  volume: 100 / 127,
1749
1936
  pan: 64,
1750
- portamentoTime: 0,
1937
+ portamentoTime: 1, // sec
1751
1938
  reverbSendLevel: 0,
1752
1939
  chorusSendLevel: 0,
1753
1940
  vibratoRate: 1,