@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
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGM2 = 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 MidyGM2 {
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 MidyGM2 {
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;
@@ -306,27 +326,31 @@ class MidyGM2 {
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 @@ class MidyGM2 {
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 @@ class MidyGM2 {
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
  }
@@ -408,6 +445,7 @@ class MidyGM2 {
408
445
  if (queueIndex >= this.timeline.length) {
409
446
  await Promise.all(this.notePromises);
410
447
  this.notePromises = [];
448
+ this.exclusiveClassMap.clear();
411
449
  resolve();
412
450
  return;
413
451
  }
@@ -423,6 +461,7 @@ class MidyGM2 {
423
461
  }
424
462
  else if (this.isStopping) {
425
463
  await this.stopNotes(0, true);
464
+ this.exclusiveClassMap.clear();
426
465
  this.notePromises = [];
427
466
  resolve();
428
467
  this.isStopping = false;
@@ -431,6 +470,7 @@ class MidyGM2 {
431
470
  }
432
471
  else if (this.isSeeking) {
433
472
  this.stopNotes(0, true);
473
+ this.exclusiveClassMap.clear();
434
474
  this.startTime = this.audioContext.currentTime;
435
475
  queueIndex = this.getQueueIndex(this.resumeTime);
436
476
  offset = this.resumeTime - this.startTime;
@@ -464,9 +504,11 @@ class MidyGM2 {
464
504
  bankLSB: this.channels[i].bankLSB,
465
505
  };
466
506
  }
467
- midi.tracks.forEach((track) => {
507
+ for (let i = 0; i < midi.tracks.length; i++) {
508
+ const track = midi.tracks[i];
468
509
  let currentTicks = 0;
469
- track.forEach((event) => {
510
+ for (let j = 0; j < track.length; j++) {
511
+ const event = track[j];
470
512
  currentTicks += event.deltaTime;
471
513
  event.ticks = currentTicks;
472
514
  switch (event.type) {
@@ -519,16 +561,18 @@ class MidyGM2 {
519
561
  }
520
562
  delete event.deltaTime;
521
563
  timeline.push(event);
522
- });
523
- });
564
+ }
565
+ }
524
566
  const priority = {
525
567
  controller: 0,
526
568
  sysEx: 1,
569
+ noteOff: 2, // for portamento
570
+ noteOn: 3,
527
571
  };
528
572
  timeline.sort((a, b) => {
529
573
  if (a.ticks !== b.ticks)
530
574
  return a.ticks - b.ticks;
531
- return (priority[a.type] || 2) - (priority[b.type] || 2);
575
+ return (priority[a.type] || 4) - (priority[b.type] || 4);
532
576
  });
533
577
  let prevTempoTime = 0;
534
578
  let prevTempoTicks = 0;
@@ -545,7 +589,7 @@ class MidyGM2 {
545
589
  }
546
590
  return { instruments, timeline };
547
591
  }
548
- async stopChannelNotes(channelNumber, velocity, stopPedal) {
592
+ async stopChannelNotes(channelNumber, velocity, force) {
549
593
  const now = this.audioContext.currentTime;
550
594
  const channel = this.channels[channelNumber];
551
595
  channel.scheduledNotes.forEach((noteList) => {
@@ -553,16 +597,17 @@ class MidyGM2 {
553
597
  const note = noteList[i];
554
598
  if (!note)
555
599
  continue;
556
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, stopPedal);
600
+ const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
601
+ force);
557
602
  this.notePromises.push(promise);
558
603
  }
559
604
  });
560
605
  channel.scheduledNotes.clear();
561
606
  await Promise.all(this.notePromises);
562
607
  }
563
- stopNotes(velocity, stopPedal) {
608
+ stopNotes(velocity, force) {
564
609
  for (let i = 0; i < this.channels.length; i++) {
565
- this.stopChannelNotes(i, velocity, stopPedal);
610
+ this.stopChannelNotes(i, velocity, force);
566
611
  }
567
612
  return Promise.all(this.notePromises);
568
613
  }
@@ -654,14 +699,14 @@ class MidyGM2 {
654
699
  return impulse;
655
700
  }
656
701
  createConvolutionReverb(audioContext, impulse) {
657
- const output = new GainNode(audioContext);
702
+ const input = new GainNode(audioContext);
658
703
  const convolverNode = new ConvolverNode(audioContext, {
659
704
  buffer: impulse,
660
705
  });
661
- convolverNode.connect(output);
706
+ input.connect(convolverNode);
662
707
  return {
663
- input: convolverNode,
664
- output,
708
+ input,
709
+ output: convolverNode,
665
710
  convolverNode,
666
711
  };
667
712
  }
@@ -703,7 +748,6 @@ class MidyGM2 {
703
748
  // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
704
749
  createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
705
750
  const input = new GainNode(audioContext);
706
- const output = new GainNode(audioContext);
707
751
  const mergerGain = new GainNode(audioContext);
708
752
  for (let i = 0; i < combDelays.length; i++) {
709
753
  const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
@@ -714,7 +758,7 @@ class MidyGM2 {
714
758
  const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
715
759
  allpasses.push(allpass);
716
760
  }
717
- allpasses.at(-1).connect(output);
761
+ const output = allpasses.at(-1);
718
762
  return { input, output };
719
763
  }
720
764
  createChorusEffect(audioContext) {
@@ -776,6 +820,17 @@ class MidyGM2 {
776
820
  return instrumentKey.playbackRate(noteNumber) *
777
821
  Math.pow(2, semitoneOffset / 12);
778
822
  }
823
+ setPortamentoStartVolumeEnvelope(channel, note) {
824
+ const { instrumentKey, startTime } = note;
825
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
826
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
827
+ const volDelay = startTime + instrumentKey.volDelay;
828
+ const portamentoTime = volDelay + channel.portamentoTime;
829
+ note.volumeNode.gain
830
+ .cancelScheduledValues(startTime)
831
+ .setValueAtTime(0, volDelay)
832
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
833
+ }
779
834
  setVolumeEnvelope(note) {
780
835
  const { instrumentKey, startTime } = note;
781
836
  const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
@@ -815,6 +870,25 @@ class MidyGM2 {
815
870
  const maxFrequency = 20000; // max Hz of initialFilterFc
816
871
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
817
872
  }
873
+ setPortamentoStartFilterEnvelope(channel, note) {
874
+ const { instrumentKey, noteNumber, startTime } = note;
875
+ const softPedalFactor = 1 -
876
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
877
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
878
+ softPedalFactor;
879
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
880
+ const sustainFreq = baseFreq +
881
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
882
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
883
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
884
+ const portamentoTime = startTime + channel.portamentoTime;
885
+ const modDelay = startTime + instrumentKey.modDelay;
886
+ note.filterNode.frequency
887
+ .cancelScheduledValues(startTime)
888
+ .setValueAtTime(adjustedBaseFreq, startTime)
889
+ .setValueAtTime(adjustedBaseFreq, modDelay)
890
+ .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
891
+ }
818
892
  setFilterEnvelope(channel, note) {
819
893
  const { instrumentKey, noteNumber, startTime } = note;
820
894
  const softPedalFactor = 1 -
@@ -882,17 +956,23 @@ class MidyGM2 {
882
956
  note.vibratoLFO.connect(note.vibratoDepth);
883
957
  note.vibratoDepth.connect(note.bufferSource.detune);
884
958
  }
885
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
959
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3) {
886
960
  const semitoneOffset = this.calcSemitoneOffset(channel);
887
961
  const note = new Note(noteNumber, velocity, startTime, instrumentKey);
888
962
  note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
889
963
  note.volumeNode = new GainNode(this.audioContext);
890
964
  note.filterNode = new BiquadFilterNode(this.audioContext, {
891
965
  type: "lowpass",
892
- Q: instrumentKey.initialFilterQ / 10 * channel.filterResonance, // dB
966
+ Q: instrumentKey.initialFilterQ / 10, // dB
893
967
  });
894
- this.setVolumeEnvelope(note);
895
- this.setFilterEnvelope(channel, note);
968
+ if (portamento) {
969
+ this.setPortamentoStartVolumeEnvelope(channel, note);
970
+ this.setPortamentoStartFilterEnvelope(channel, note);
971
+ }
972
+ else {
973
+ this.setVolumeEnvelope(note);
974
+ this.setFilterEnvelope(channel, note);
975
+ }
896
976
  if (0 < channel.vibratoDepth) {
897
977
  this.startVibrato(channel, note, startTime);
898
978
  }
@@ -909,7 +989,21 @@ class MidyGM2 {
909
989
  }
910
990
  note.bufferSource.connect(note.filterNode);
911
991
  note.filterNode.connect(note.volumeNode);
912
- note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
992
+ if (0 < channel.reverbSendLevel && 0 < instrumentKey.reverbEffectsSend) {
993
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
994
+ gain: instrumentKey.reverbEffectsSend,
995
+ });
996
+ note.volumeNode.connect(note.reverbEffectsSend);
997
+ note.reverbEffectsSend.connect(this.reverbEffect.input);
998
+ }
999
+ if (0 < channel.chorusSendLevel && 0 < instrumentKey.chorusEffectsSend) {
1000
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1001
+ gain: instrumentKey.chorusEffectsSend,
1002
+ });
1003
+ note.volumeNode.connect(note.chorusEffectsSend);
1004
+ note.chorusEffectsSend.connect(this.chorusEffect.input);
1005
+ }
1006
+ note.bufferSource.start(startTime);
913
1007
  return note;
914
1008
  }
915
1009
  calcBank(channel, channelNumber) {
@@ -921,7 +1015,7 @@ class MidyGM2 {
921
1015
  }
922
1016
  return channel.bank;
923
1017
  }
924
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1018
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
925
1019
  const channel = this.channels[channelNumber];
926
1020
  const bankNumber = this.calcBank(channel, channelNumber);
927
1021
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
@@ -932,12 +1026,25 @@ class MidyGM2 {
932
1026
  const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
933
1027
  if (!instrumentKey)
934
1028
  return;
935
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
1029
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3);
936
1030
  note.volumeNode.connect(channel.gainL);
937
1031
  note.volumeNode.connect(channel.gainR);
938
1032
  if (channel.sostenutoPedal) {
939
1033
  channel.sostenutoNotes.set(noteNumber, note);
940
1034
  }
1035
+ const exclusiveClass = instrumentKey.exclusiveClass;
1036
+ if (exclusiveClass !== 0) {
1037
+ if (this.exclusiveClassMap.has(exclusiveClass)) {
1038
+ const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1039
+ const [prevNote, prevChannelNumber] = prevEntry;
1040
+ if (!prevNote.ending) {
1041
+ this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1042
+ startTime, undefined, // portamentoNoteNumber
1043
+ true);
1044
+ }
1045
+ }
1046
+ this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1047
+ }
941
1048
  const scheduledNotes = channel.scheduledNotes;
942
1049
  if (scheduledNotes.has(noteNumber)) {
943
1050
  scheduledNotes.get(noteNumber).push(note);
@@ -946,13 +1053,48 @@ class MidyGM2 {
946
1053
  scheduledNotes.set(noteNumber, [note]);
947
1054
  }
948
1055
  }
949
- noteOn(channelNumber, noteNumber, velocity) {
1056
+ noteOn(channelNumber, noteNumber, velocity, portamento) {
950
1057
  const now = this.audioContext.currentTime;
951
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
1058
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
1059
+ }
1060
+ stopNote(endTime, stopTime, scheduledNotes, index) {
1061
+ const note = scheduledNotes[index];
1062
+ note.volumeNode.gain
1063
+ .cancelScheduledValues(endTime)
1064
+ .linearRampToValueAtTime(0, stopTime);
1065
+ note.ending = true;
1066
+ this.scheduleTask(() => {
1067
+ note.bufferSource.loop = false;
1068
+ }, stopTime);
1069
+ return new Promise((resolve) => {
1070
+ note.bufferSource.onended = () => {
1071
+ scheduledNotes[index] = null;
1072
+ note.bufferSource.disconnect();
1073
+ note.volumeNode.disconnect();
1074
+ note.filterNode.disconnect();
1075
+ if (note.modulationDepth) {
1076
+ note.volumeDepth.disconnect();
1077
+ note.modulationDepth.disconnect();
1078
+ note.modulationLFO.stop();
1079
+ }
1080
+ if (note.vibratoDepth) {
1081
+ note.vibratoDepth.disconnect();
1082
+ note.vibratoLFO.stop();
1083
+ }
1084
+ if (note.reverbEffectsSend) {
1085
+ note.reverbEffectsSend.disconnect();
1086
+ }
1087
+ if (note.chorusEffectsSend) {
1088
+ note.chorusEffectsSend.disconnect();
1089
+ }
1090
+ resolve();
1091
+ };
1092
+ note.bufferSource.stop(stopTime);
1093
+ });
952
1094
  }
953
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, stopPedal = false) {
1095
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, portamentoNoteNumber, force) {
954
1096
  const channel = this.channels[channelNumber];
955
- if (stopPedal) {
1097
+ if (!force) {
956
1098
  if (channel.sustainPedal)
957
1099
  return;
958
1100
  if (channel.sostenutoNotes.has(noteNumber))
@@ -967,38 +1109,29 @@ class MidyGM2 {
967
1109
  continue;
968
1110
  if (note.ending)
969
1111
  continue;
970
- const volEndTime = stopTime + note.instrumentKey.volRelease;
971
- note.volumeNode.gain
972
- .cancelScheduledValues(stopTime)
973
- .linearRampToValueAtTime(0, volEndTime);
974
- const modRelease = stopTime + note.instrumentKey.modRelease;
975
- note.filterNode.frequency
976
- .cancelScheduledValues(stopTime)
977
- .linearRampToValueAtTime(0, modRelease);
978
- note.ending = true;
979
- this.scheduleTask(() => {
980
- note.bufferSource.loop = false;
981
- }, stopTime);
982
- return new Promise((resolve) => {
983
- note.bufferSource.onended = () => {
984
- scheduledNotes[i] = null;
985
- note.bufferSource.disconnect();
986
- note.volumeNode.disconnect();
987
- note.filterNode.disconnect();
988
- if (note.modulationDepth) {
989
- note.volumeDepth.disconnect();
990
- note.modulationDepth.disconnect();
991
- note.modulationLFO.stop();
992
- }
993
- resolve();
994
- };
995
- note.bufferSource.stop(volEndTime);
996
- });
1112
+ if (portamentoNoteNumber === undefined) {
1113
+ const volRelease = endTime + note.instrumentKey.volRelease;
1114
+ const modRelease = endTime + note.instrumentKey.modRelease;
1115
+ note.filterNode.frequency
1116
+ .cancelScheduledValues(endTime)
1117
+ .linearRampToValueAtTime(0, modRelease);
1118
+ const stopTime = Math.min(volRelease, modRelease);
1119
+ return this.stopNote(endTime, stopTime, scheduledNotes, i);
1120
+ }
1121
+ else {
1122
+ const portamentoTime = endTime + channel.portamentoTime;
1123
+ const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1124
+ const detune = note.bufferSource.detune.value + detuneChange;
1125
+ note.bufferSource.detune
1126
+ .cancelScheduledValues(endTime)
1127
+ .linearRampToValueAtTime(detune, portamentoTime);
1128
+ return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1129
+ }
997
1130
  }
998
1131
  }
999
- releaseNote(channelNumber, noteNumber, velocity) {
1132
+ releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1000
1133
  const now = this.audioContext.currentTime;
1001
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
1134
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1002
1135
  }
1003
1136
  releaseSustainPedal(channelNumber, halfVelocity) {
1004
1137
  const velocity = halfVelocity * 2;
@@ -1146,12 +1279,14 @@ class MidyGM2 {
1146
1279
  this.updateModulation(channel);
1147
1280
  }
1148
1281
  setPortamentoTime(channelNumber, portamentoTime) {
1149
- this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1282
+ const channel = this.channels[channelNumber];
1283
+ const factor = 5 * Math.log(10) / 127;
1284
+ channel.portamentoTime = Math.exp(factor * portamentoTime);
1150
1285
  }
1151
1286
  setVolume(channelNumber, volume) {
1152
1287
  const channel = this.channels[channelNumber];
1153
1288
  channel.volume = volume / 127;
1154
- this.updateChannelGain(channel);
1289
+ this.updateChannelVolume(channel);
1155
1290
  }
1156
1291
  panToGain(pan) {
1157
1292
  const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
@@ -1163,12 +1298,12 @@ class MidyGM2 {
1163
1298
  setPan(channelNumber, pan) {
1164
1299
  const channel = this.channels[channelNumber];
1165
1300
  channel.pan = pan;
1166
- this.updateChannelGain(channel);
1301
+ this.updateChannelVolume(channel);
1167
1302
  }
1168
1303
  setExpression(channelNumber, expression) {
1169
1304
  const channel = this.channels[channelNumber];
1170
1305
  channel.expression = expression / 127;
1171
- this.updateChannelGain(channel);
1306
+ this.updateChannelVolume(channel);
1172
1307
  }
1173
1308
  setBankLSB(channelNumber, lsb) {
1174
1309
  this.channels[channelNumber].bankLSB = lsb;
@@ -1177,7 +1312,7 @@ class MidyGM2 {
1177
1312
  this.channels[channelNumber].dataLSB = value;
1178
1313
  this.handleRPN(channelNumber);
1179
1314
  }
1180
- updateChannelGain(channel) {
1315
+ updateChannelVolume(channel) {
1181
1316
  const now = this.audioContext.currentTime;
1182
1317
  const volume = channel.volume * channel.expression;
1183
1318
  const { gainLeft, gainRight } = this.panToGain(channel.pan);
@@ -1195,30 +1330,54 @@ class MidyGM2 {
1195
1330
  this.releaseSustainPedal(channelNumber, value);
1196
1331
  }
1197
1332
  }
1198
- // TODO
1199
1333
  setPortamento(channelNumber, value) {
1200
1334
  this.channels[channelNumber].portamento = value >= 64;
1201
1335
  }
1202
1336
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1203
1337
  const channel = this.channels[channelNumber];
1338
+ const reverbEffect = this.reverbEffect;
1204
1339
  if (0 < channel.reverbSendLevel) {
1205
1340
  if (0 < reverbSendLevel) {
1206
1341
  const now = this.audioContext.currentTime;
1207
1342
  channel.reverbSendLevel = reverbSendLevel / 127;
1208
- reverbEffect.output.gain.cancelScheduledValues(now);
1209
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1343
+ reverbEffect.input.gain.cancelScheduledValues(now);
1344
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1210
1345
  }
1211
1346
  else {
1212
- channel.merger.disconnect(reverbEffect.input);
1347
+ channel.scheduledNotes.forEach((noteList) => {
1348
+ for (let i = 0; i < noteList.length; i++) {
1349
+ const note = noteList[i];
1350
+ if (!note)
1351
+ continue;
1352
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1353
+ continue;
1354
+ note.reverbEffectsSend.disconnect();
1355
+ }
1356
+ });
1213
1357
  }
1214
1358
  }
1215
1359
  else {
1216
1360
  if (0 < reverbSendLevel) {
1217
- channel.merger.connect(reverbEffect.input);
1218
1361
  const now = this.audioContext.currentTime;
1362
+ channel.scheduledNotes.forEach((noteList) => {
1363
+ for (let i = 0; i < noteList.length; i++) {
1364
+ const note = noteList[i];
1365
+ if (!note)
1366
+ continue;
1367
+ if (note.instrumentKey.reverbEffectsSend <= 0)
1368
+ continue;
1369
+ if (!note.reverbEffectsSend) {
1370
+ note.reverbEffectsSend = new GainNode(this.audioContext, {
1371
+ gain: note.instrumentKey.reverbEffectsSend,
1372
+ });
1373
+ note.volumeNode.connect(note.reverbEffectsSend);
1374
+ }
1375
+ note.reverbEffectsSend.connect(reverbEffect.input);
1376
+ }
1377
+ });
1219
1378
  channel.reverbSendLevel = reverbSendLevel / 127;
1220
- reverbEffect.output.gain.cancelScheduledValues(now);
1221
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1379
+ reverbEffect.input.gain.cancelScheduledValues(now);
1380
+ reverbEffect.input.gain.setValueAtTime(channel.reverbSendLevel, now);
1222
1381
  }
1223
1382
  }
1224
1383
  }
@@ -1229,20 +1388,44 @@ class MidyGM2 {
1229
1388
  if (0 < chorusSendLevel) {
1230
1389
  const now = this.audioContext.currentTime;
1231
1390
  channel.chorusSendLevel = chorusSendLevel / 127;
1232
- chorusEffect.output.gain.cancelScheduledValues(now);
1233
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1391
+ chorusEffect.input.gain.cancelScheduledValues(now);
1392
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1234
1393
  }
1235
1394
  else {
1236
- channel.merger.disconnect(chorusEffect.input);
1395
+ channel.scheduledNotes.forEach((noteList) => {
1396
+ for (let i = 0; i < noteList.length; i++) {
1397
+ const note = noteList[i];
1398
+ if (!note)
1399
+ continue;
1400
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1401
+ continue;
1402
+ note.chorusEffectsSend.disconnect();
1403
+ }
1404
+ });
1237
1405
  }
1238
1406
  }
1239
1407
  else {
1240
1408
  if (0 < chorusSendLevel) {
1241
- channel.merger.connect(chorusEffect.input);
1242
1409
  const now = this.audioContext.currentTime;
1410
+ channel.scheduledNotes.forEach((noteList) => {
1411
+ for (let i = 0; i < noteList.length; i++) {
1412
+ const note = noteList[i];
1413
+ if (!note)
1414
+ continue;
1415
+ if (note.instrumentKey.chorusEffectsSend <= 0)
1416
+ continue;
1417
+ if (!note.chorusEffectsSend) {
1418
+ note.chorusEffectsSend = new GainNode(this.audioContext, {
1419
+ gain: note.instrumentKey.chorusEffectsSend,
1420
+ });
1421
+ note.volumeNode.connect(note.chorusEffectsSend);
1422
+ }
1423
+ note.chorusEffectsSend.connect(chorusEffect.input);
1424
+ }
1425
+ });
1243
1426
  channel.chorusSendLevel = chorusSendLevel / 127;
1244
- chorusEffect.output.gain.cancelScheduledValues(now);
1245
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1427
+ chorusEffect.input.gain.cancelScheduledValues(now);
1428
+ chorusEffect.input.gain.setValueAtTime(channel.chorusSendLevel, now);
1246
1429
  }
1247
1430
  }
1248
1431
  }
@@ -1428,20 +1611,22 @@ class MidyGM2 {
1428
1611
  }
1429
1612
  }
1430
1613
  GM1SystemOn() {
1431
- this.channels.forEach((channel) => {
1614
+ for (let i = 0; i < this.channels.length; i++) {
1615
+ const channel = this.channels[i];
1432
1616
  channel.bankMSB = 0;
1433
1617
  channel.bankLSB = 0;
1434
1618
  channel.bank = 0;
1435
- });
1619
+ }
1436
1620
  this.channels[9].bankMSB = 1;
1437
1621
  this.channels[9].bank = 128;
1438
1622
  }
1439
1623
  GM2SystemOn() {
1440
- this.channels.forEach((channel) => {
1624
+ for (let i = 0; i < this.channels.length; i++) {
1625
+ const channel = this.channels[i];
1441
1626
  channel.bankMSB = 121;
1442
1627
  channel.bankLSB = 0;
1443
1628
  channel.bank = 121 * 128;
1444
- });
1629
+ }
1445
1630
  this.channels[9].bankMSB = 120;
1446
1631
  this.channels[9].bank = 120 * 128;
1447
1632
  }
@@ -1582,10 +1767,8 @@ class MidyGM2 {
1582
1767
  }
1583
1768
  setReverbTime(value) {
1584
1769
  this.reverb.time = this.getReverbTime(value);
1585
- const { audioContext, channels, options } = this;
1586
- for (let i = 0; i < channels.length; i++) {
1587
- channels[i].reverbEffect = options.reverbAlgorithm(audioContext);
1588
- }
1770
+ const { audioContext, options } = this;
1771
+ this.reverbEffect = options.reverbAlgorithm(audioContext);
1589
1772
  }
1590
1773
  getReverbTime(value) {
1591
1774
  return Math.pow(Math.E, (value - 40) * 0.025);
@@ -1662,10 +1845,7 @@ class MidyGM2 {
1662
1845
  const now = this.audioContext.currentTime;
1663
1846
  const modRate = this.getChorusModRate(value);
1664
1847
  this.chorus.modRate = modRate;
1665
- for (let i = 0; i < this.channels.length; i++) {
1666
- const lfo = this.channels[i].chorusEffect.lfo;
1667
- lfo.frequency.setValueAtTime(modRate, now);
1668
- }
1848
+ this.chorusEffect.lfo.frequency.setValueAtTime(modRate, now);
1669
1849
  }
1670
1850
  getChorusModRate(value) {
1671
1851
  return value * 0.122; // Hz
@@ -1674,12 +1854,9 @@ class MidyGM2 {
1674
1854
  const now = this.audioContext.currentTime;
1675
1855
  const modDepth = this.getChorusModDepth(value);
1676
1856
  this.chorus.modDepth = modDepth;
1677
- for (let i = 0; i < this.channels.length; i++) {
1678
- const chorusEffect = this.channels[i].chorusEffect;
1679
- chorusEffect.lfoGain.gain
1680
- .cancelScheduledValues(now)
1681
- .setValueAtTime(modDepth / 2, now);
1682
- }
1857
+ this.chorusEffect.lfoGain.gain
1858
+ .cancelScheduledValues(now)
1859
+ .setValueAtTime(modDepth / 2, now);
1683
1860
  }
1684
1861
  getChorusModDepth(value) {
1685
1862
  return (value + 1) / 3200; // second
@@ -1688,14 +1865,11 @@ class MidyGM2 {
1688
1865
  const now = this.audioContext.currentTime;
1689
1866
  const feedback = this.getChorusFeedback(value);
1690
1867
  this.chorus.feedback = feedback;
1691
- for (let i = 0; i < this.channels.length; i++) {
1692
- const chorusEffect = this.channels[i].chorusEffect;
1693
- for (let j = 0; j < chorusEffect.feedbackGains.length; j++) {
1694
- const feedbackGain = chorusEffect.feedbackGains[j];
1695
- feedbackGain.gain
1696
- .cancelScheduledValues(now)
1697
- .setValueAtTime(feedback, now);
1698
- }
1868
+ const chorusEffect = this.chorusEffect;
1869
+ for (let i = 0; i < chorusEffect.feedbackGains.length; i++) {
1870
+ chorusEffect.feedbackGains[i].gain
1871
+ .cancelScheduledValues(now)
1872
+ .setValueAtTime(feedback, now);
1699
1873
  }
1700
1874
  }
1701
1875
  getChorusFeedback(value) {
@@ -1703,15 +1877,28 @@ class MidyGM2 {
1703
1877
  }
1704
1878
  setChorusSendToReverb(value) {
1705
1879
  const sendToReverb = this.getChorusSendToReverb(value);
1706
- if (0 < sendToReverb) {
1707
- const now = this.audioContext.currentTime;
1880
+ const sendGain = this.chorusEffect.sendGain;
1881
+ if (0 < this.chorus.sendToReverb) {
1708
1882
  this.chorus.sendToReverb = sendToReverb;
1709
- this.chorusEffect.sendGain.gain
1710
- .cancelScheduledValues(now)
1711
- .setValueAtTime(sendToReverb, now);
1883
+ if (0 < sendToReverb) {
1884
+ const now = this.audioContext.currentTime;
1885
+ sendGain.gain
1886
+ .cancelScheduledValues(now)
1887
+ .setValueAtTime(sendToReverb, now);
1888
+ }
1889
+ else {
1890
+ sendGain.disconnect();
1891
+ }
1712
1892
  }
1713
- else if (this.chorus.sendToReverb !== 0) {
1714
- this.chorusEffect.sendGain.disconnect(this.reverbEffect.input);
1893
+ else {
1894
+ this.chorus.sendToReverb = sendToReverb;
1895
+ if (0 < sendToReverb) {
1896
+ const now = this.audioContext.currentTime;
1897
+ sendGain.connect(this.reverbEffect.input);
1898
+ sendGain.gain
1899
+ .cancelScheduledValues(now)
1900
+ .setValueAtTime(sendToReverb, now);
1901
+ }
1715
1902
  }
1716
1903
  }
1717
1904
  getChorusSendToReverb(value) {
@@ -1751,7 +1938,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
1751
1938
  currentBufferSource: null,
1752
1939
  volume: 100 / 127,
1753
1940
  pan: 64,
1754
- portamentoTime: 0,
1941
+ portamentoTime: 1, // sec
1755
1942
  reverbSendLevel: 0,
1756
1943
  chorusSendLevel: 0,
1757
1944
  vibratoRate: 1,