@marmooo/midy 0.0.4 → 0.0.6

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 (33) hide show
  1. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.d.ts +13 -6
  2. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.d.ts.map +1 -0
  3. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.js +5 -5
  4. package/esm/midy-GM1.d.ts +36 -42
  5. package/esm/midy-GM1.d.ts.map +1 -1
  6. package/esm/midy-GM1.js +214 -148
  7. package/esm/midy-GM2.d.ts +133 -25
  8. package/esm/midy-GM2.d.ts.map +1 -1
  9. package/esm/midy-GM2.js +230 -163
  10. package/esm/midy-GMLite.d.ts +37 -43
  11. package/esm/midy-GMLite.d.ts.map +1 -1
  12. package/esm/midy-GMLite.js +215 -149
  13. package/esm/midy.d.ts +41 -33
  14. package/esm/midy.d.ts.map +1 -1
  15. package/esm/midy.js +263 -179
  16. package/package.json +1 -1
  17. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.d.ts +13 -6
  18. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.d.ts.map +1 -0
  19. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.js +5 -5
  20. package/script/midy-GM1.d.ts +36 -42
  21. package/script/midy-GM1.d.ts.map +1 -1
  22. package/script/midy-GM1.js +214 -148
  23. package/script/midy-GM2.d.ts +133 -25
  24. package/script/midy-GM2.d.ts.map +1 -1
  25. package/script/midy-GM2.js +230 -163
  26. package/script/midy-GMLite.d.ts +37 -43
  27. package/script/midy-GMLite.d.ts.map +1 -1
  28. package/script/midy-GMLite.js +215 -149
  29. package/script/midy.d.ts +41 -33
  30. package/script/midy.d.ts.map +1 -1
  31. package/script/midy.js +263 -179
  32. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +0 -1
  33. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +0 -1
package/script/midy.js CHANGED
@@ -2,7 +2,57 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = void 0;
4
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.1/+esm.js");
5
+ const _esm_js_2 = require("./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.js");
6
+ class Note {
7
+ constructor(noteNumber, velocity, startTime, instrumentKey) {
8
+ Object.defineProperty(this, "bufferSource", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: void 0
13
+ });
14
+ Object.defineProperty(this, "gainNode", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: void 0
19
+ });
20
+ Object.defineProperty(this, "filterNode", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
26
+ Object.defineProperty(this, "modLFO", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: void 0
31
+ });
32
+ Object.defineProperty(this, "modLFOGain", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ Object.defineProperty(this, "vibLFO", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: void 0
43
+ });
44
+ Object.defineProperty(this, "vibLFOGain", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: void 0
49
+ });
50
+ this.noteNumber = noteNumber;
51
+ this.velocity = velocity;
52
+ this.startTime = startTime;
53
+ this.instrumentKey = instrumentKey;
54
+ }
55
+ }
6
56
  class Midy {
7
57
  constructor(audioContext) {
8
58
  Object.defineProperty(this, "ticksPerBeat", {
@@ -178,25 +228,23 @@ class Midy {
178
228
  this.totalTime = this.calcTotalTime();
179
229
  }
180
230
  setChannelAudioNodes(audioContext) {
181
- const gainNode = new GainNode(audioContext, {
182
- gain: Midy.channelSettings.volume,
183
- });
184
- const pannerNode = new StereoPannerNode(audioContext, {
185
- pan: Midy.channelSettings.pan,
186
- });
187
- const modulationEffect = this.createModulationEffect(audioContext);
231
+ const { gainLeft, gainRight } = this.panToGain(Midy.channelSettings.pan);
232
+ const gainL = new GainNode(audioContext, { gain: gainLeft });
233
+ const gainR = new GainNode(audioContext, { gain: gainRight });
234
+ const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
235
+ gainL.connect(merger, 0, 0);
236
+ gainR.connect(merger, 0, 1);
237
+ merger.connect(this.masterGain);
188
238
  const reverbEffect = this.createReverbEffect(audioContext);
189
239
  const chorusEffect = this.createChorusEffect(audioContext);
190
- modulationEffect.lfo.start();
191
240
  chorusEffect.lfo.start();
192
- reverbEffect.dryGain.connect(pannerNode);
193
- reverbEffect.wetGain.connect(pannerNode);
194
- pannerNode.connect(gainNode);
195
- gainNode.connect(this.masterGain);
241
+ reverbEffect.dryGain.connect(gainL);
242
+ reverbEffect.dryGain.connect(gainR);
243
+ reverbEffect.wetGain.connect(gainL);
244
+ reverbEffect.wetGain.connect(gainR);
196
245
  return {
197
- gainNode,
198
- pannerNode,
199
- modulationEffect,
246
+ gainL,
247
+ gainR,
200
248
  reverbEffect,
201
249
  chorusEffect,
202
250
  };
@@ -219,11 +267,11 @@ class Midy {
219
267
  });
220
268
  return channels;
221
269
  }
222
- async createNoteBuffer(noteInfo, isSF3) {
223
- const sampleEnd = noteInfo.sample.length + noteInfo.end;
270
+ async createNoteBuffer(instrumentKey, isSF3) {
271
+ const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
224
272
  if (isSF3) {
225
- const sample = new Uint8Array(noteInfo.sample.length);
226
- sample.set(noteInfo.sample);
273
+ const sample = new Uint8Array(instrumentKey.sample.length);
274
+ sample.set(instrumentKey.sample);
227
275
  const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
228
276
  for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
229
277
  const channelData = audioBuffer.getChannelData(channel);
@@ -232,26 +280,27 @@ class Midy {
232
280
  return audioBuffer;
233
281
  }
234
282
  else {
235
- const sample = noteInfo.sample.subarray(0, sampleEnd);
283
+ const sample = instrumentKey.sample.subarray(0, sampleEnd);
236
284
  const floatSample = this.convertToFloat32Array(sample);
237
285
  const audioBuffer = new AudioBuffer({
238
286
  numberOfChannels: 1,
239
287
  length: sample.length,
240
- sampleRate: noteInfo.sampleRate,
288
+ sampleRate: instrumentKey.sampleRate,
241
289
  });
242
290
  const channelData = audioBuffer.getChannelData(0);
243
291
  channelData.set(floatSample);
244
292
  return audioBuffer;
245
293
  }
246
294
  }
247
- async createNoteBufferNode(noteInfo, isSF3) {
295
+ async createNoteBufferNode(instrumentKey, isSF3) {
248
296
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
249
- const audioBuffer = await this.createNoteBuffer(noteInfo, isSF3);
297
+ const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
250
298
  bufferSource.buffer = audioBuffer;
251
- bufferSource.loop = noteInfo.sampleModes % 2 !== 0;
299
+ bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
252
300
  if (bufferSource.loop) {
253
- bufferSource.loopStart = noteInfo.loopStart / noteInfo.sampleRate;
254
- bufferSource.loopEnd = noteInfo.loopEnd / noteInfo.sampleRate;
301
+ bufferSource.loopStart = instrumentKey.loopStart /
302
+ instrumentKey.sampleRate;
303
+ bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
255
304
  }
256
305
  return bufferSource;
257
306
  }
@@ -295,7 +344,7 @@ class Midy {
295
344
  this.handleChannelPressure(event.channel, event.amount);
296
345
  break;
297
346
  case "pitchBend":
298
- this.handlePitchBend(event.channel, event.value);
347
+ this.setPitchBend(event.channel, event.value);
299
348
  break;
300
349
  case "sysEx":
301
350
  this.handleSysEx(event.data);
@@ -375,7 +424,6 @@ class Midy {
375
424
  const tmpChannels = new Array(16);
376
425
  for (let i = 0; i < tmpChannels.length; i++) {
377
426
  tmpChannels[i] = {
378
- durationTicks: new Map(),
379
427
  programNumber: -1,
380
428
  bankMSB: this.channels[i].bankMSB,
381
429
  bankLSB: this.channels[i].bankLSB,
@@ -405,16 +453,6 @@ class Midy {
405
453
  }
406
454
  channel.programNumber = 0;
407
455
  }
408
- channel.durationTicks.set(event.noteNumber, {
409
- ticks: event.ticks,
410
- noteOn: event,
411
- });
412
- break;
413
- }
414
- case "noteOff": {
415
- const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
416
- .get(event.noteNumber);
417
- noteOn.durationTicks = event.ticks - ticks;
418
456
  break;
419
457
  }
420
458
  case "controller":
@@ -449,8 +487,8 @@ class Midy {
449
487
  });
450
488
  });
451
489
  const priority = {
452
- setTempo: 0,
453
- controller: 1,
490
+ controller: 0,
491
+ sysEx: 1,
454
492
  };
455
493
  timeline.sort((a, b) => {
456
494
  if (a.ticks !== b.ticks)
@@ -533,30 +571,26 @@ class Midy {
533
571
  const now = this.audioContext.currentTime;
534
572
  return this.resumeTime + now - this.startTime - this.startDelay;
535
573
  }
536
- getActiveNotes(channel) {
574
+ getActiveNotes(channel, time) {
537
575
  const activeNotes = new Map();
538
- channel.scheduledNotes.forEach((scheduledNotes) => {
539
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
576
+ channel.scheduledNotes.forEach((noteList) => {
577
+ const activeNote = this.getActiveNote(noteList, time);
540
578
  if (activeNote) {
541
579
  activeNotes.set(activeNote.noteNumber, activeNote);
542
580
  }
543
581
  });
544
582
  return activeNotes;
545
583
  }
546
- getActiveChannelNotes(scheduledNotes) {
547
- for (let i = 0; i < scheduledNotes; i++) {
548
- const scheduledNote = scheduledNotes[i];
549
- if (scheduledNote)
550
- return scheduledNote;
584
+ getActiveNote(noteList, time) {
585
+ for (let i = noteList.length - 1; i >= 0; i--) {
586
+ const note = noteList[i];
587
+ if (!note)
588
+ return;
589
+ if (time < note.startTime)
590
+ continue;
591
+ return (note.ending) ? null : note;
551
592
  }
552
- }
553
- createModulationEffect(audioContext) {
554
- const lfo = new OscillatorNode(audioContext, {
555
- frequency: 5,
556
- });
557
- return {
558
- lfo,
559
- };
593
+ return noteList[0];
560
594
  }
561
595
  createReverbEffect(audioContext, options = {}) {
562
596
  const { decay = 0.8, preDecay = 0, } = options;
@@ -592,12 +626,8 @@ class Midy {
592
626
  }
593
627
  createChorusEffect(audioContext, options = {}) {
594
628
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
595
- const lfo = new OscillatorNode(audioContext, {
596
- frequency: chorusRate,
597
- });
598
- const lfoGain = new GainNode(audioContext, {
599
- gain: chorusDepth,
600
- });
629
+ const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
630
+ const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
601
631
  const chorusGains = [];
602
632
  const delayNodes = [];
603
633
  const baseGain = 1 / chorusCount;
@@ -608,9 +638,7 @@ class Midy {
608
638
  maxDelayTime: delayTime,
609
639
  });
610
640
  delayNodes.push(delayNode);
611
- const chorusGain = new GainNode(audioContext, {
612
- gain: baseGain,
613
- });
641
+ const chorusGain = new GainNode(audioContext, { gain: baseGain });
614
642
  chorusGains.push(chorusGain);
615
643
  lfo.connect(lfoGain);
616
644
  lfoGain.connect(delayNode.delayTime);
@@ -666,77 +694,108 @@ class Midy {
666
694
  const tuning = masterTuning + channelTuning;
667
695
  return channel.pitchBend * channel.pitchBendRange + tuning;
668
696
  }
669
- calcPlaybackRate(noteInfo, noteNumber, semitoneOffset) {
670
- return noteInfo.playbackRate(noteNumber) * Math.pow(2, semitoneOffset / 12);
697
+ calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
698
+ return instrumentKey.playbackRate(noteNumber) *
699
+ Math.pow(2, semitoneOffset / 12);
671
700
  }
672
- async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
673
- const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
674
- const semitoneOffset = this.calcSemitoneOffset(channel);
675
- bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
676
- // volume envelope
677
- const gainNode = new GainNode(this.audioContext, {
678
- gain: 0,
679
- });
701
+ setVolumeEnvelope(channel, note) {
702
+ const { instrumentKey, startTime, velocity } = note;
703
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
680
704
  let volume = (velocity / 127) * channel.volume * channel.expression;
681
705
  if (volume === 0)
682
706
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
683
- const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
684
- const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
685
- const volDelay = startTime + noteInfo.volDelay;
686
- const volAttack = volDelay + noteInfo.volAttack;
687
- const volHold = volAttack + noteInfo.volHold;
688
- const volDecay = volHold + noteInfo.volDecay;
689
- gainNode.gain
707
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
708
+ volume;
709
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
710
+ const volDelay = startTime + instrumentKey.volDelay;
711
+ const volAttack = volDelay + instrumentKey.volAttack;
712
+ const volHold = volAttack + instrumentKey.volHold;
713
+ const volDecay = volHold + instrumentKey.volDecay;
714
+ note.gainNode.gain
690
715
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
691
716
  .exponentialRampToValueAtTime(attackVolume, volAttack)
692
717
  .setValueAtTime(attackVolume, volHold)
693
718
  .linearRampToValueAtTime(sustainVolume, volDecay);
694
- // filter envelope
719
+ }
720
+ setFilterEnvelope(channel, note) {
721
+ const { instrumentKey, startTime, noteNumber } = note;
695
722
  const softPedalFactor = 1 -
696
723
  (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
697
724
  const maxFreq = this.audioContext.sampleRate / 2;
698
- const baseFreq = this.centToHz(noteInfo.initialFilterFc) * softPedalFactor;
699
- const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc) * softPedalFactor;
725
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
726
+ softPedalFactor;
727
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
700
728
  const sustainFreq = (baseFreq +
701
- (peekFreq - baseFreq) * (1 - noteInfo.modSustain)) * softPedalFactor;
729
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
730
+ const modDelay = startTime + instrumentKey.modDelay;
731
+ const modAttack = modDelay + instrumentKey.modAttack;
732
+ const modHold = modAttack + instrumentKey.modHold;
733
+ const modDecay = modHold + instrumentKey.modDecay;
702
734
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
703
735
  const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
704
736
  const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
705
- const filterNode = new BiquadFilterNode(this.audioContext, {
737
+ note.filterNode = new BiquadFilterNode(this.audioContext, {
706
738
  type: "lowpass",
707
- Q: noteInfo.initialFilterQ / 10, // dB
739
+ Q: instrumentKey.initialFilterQ / 10, // dB
708
740
  frequency: adjustedBaseFreq,
709
741
  });
710
- const modDelay = startTime + noteInfo.modDelay;
711
- const modAttack = modDelay + noteInfo.modAttack;
712
- const modHold = modAttack + noteInfo.modHold;
713
- const modDecay = modHold + noteInfo.modDecay;
714
- filterNode.frequency
742
+ note.filterNode.frequency
715
743
  .setValueAtTime(adjustedBaseFreq, modDelay)
716
744
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
717
745
  .setValueAtTime(adjustedPeekFreq, modHold)
718
746
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
719
- let lfoGain;
747
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
748
+ }
749
+ startModulation(channel, note, time) {
750
+ const { instrumentKey } = note;
751
+ note.modLFOGain = new GainNode(this.audioContext, {
752
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
753
+ });
754
+ note.modLFO = new OscillatorNode(this.audioContext, {
755
+ frequency: this.centToHz(instrumentKey.freqModLFO),
756
+ });
757
+ note.modLFO.start(time);
758
+ note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
759
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
760
+ note.modLFO.connect(note.modLFOGain);
761
+ note.modLFOGain.connect(note.bufferSource.detune);
762
+ }
763
+ startVibrato(channel, note, time) {
764
+ const { instrumentKey } = note;
765
+ note.vibLFOGain = new GainNode(this.audioContext, {
766
+ gain: channel.vibratoDepth,
767
+ });
768
+ note.vibLFO = new OscillatorNode(this.audioContext, {
769
+ frequency: this.centToHz(instrumentKey.freqModLFO) +
770
+ channel.vibratoRate,
771
+ });
772
+ note.vibLFO.start(time + channel.vibratoDelay);
773
+ note.vibLFO.connect(note.vibLFOGain);
774
+ note.vibLFOGain.connect(note.bufferSource.detune);
775
+ }
776
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
777
+ const semitoneOffset = this.calcSemitoneOffset(channel);
778
+ const note = new Note(noteNumber, velocity, startTime, instrumentKey);
779
+ note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
780
+ note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
781
+ this.setVolumeEnvelope(channel, note);
782
+ this.setFilterEnvelope(channel, note);
720
783
  if (channel.modulation > 0) {
721
- const vibratoDelay = startTime + channel.vibratoDelay;
722
- const vibratoAttack = vibratoDelay + 0.1;
723
- lfoGain = new GainNode(this.audioContext, {
724
- gain: 0,
725
- });
726
- lfoGain.gain
727
- .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
728
- .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
729
- channel.modulationEffect.lfo.connect(lfoGain);
730
- lfoGain.connect(bufferSource.detune);
784
+ const delayModLFO = startTime + instrumentKey.delayModLFO;
785
+ this.startModulation(channel, note, delayModLFO);
786
+ }
787
+ if (channel.vibratoDepth > 0) {
788
+ const delayVibLFO = startTime + instrumentKey.delayVibLFO;
789
+ this.startVibrato(channel, note, delayVibLFO);
731
790
  }
732
- bufferSource.connect(filterNode);
733
- filterNode.connect(gainNode);
734
791
  if (this.mono && channel.currentBufferSource) {
735
792
  channel.currentBufferSource.stop(startTime);
736
- channel.currentBufferSource = bufferSource;
793
+ channel.currentBufferSource = note.bufferSource;
737
794
  }
738
- bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
739
- return { bufferSource, gainNode, filterNode, lfoGain };
795
+ note.bufferSource.connect(note.filterNode);
796
+ note.filterNode.connect(note.gainNode);
797
+ note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
798
+ return note;
740
799
  }
741
800
  calcBank(channel, channelNumber) {
742
801
  if (channel.bankMSB === 121) {
@@ -755,36 +814,20 @@ class Midy {
755
814
  return;
756
815
  const soundFont = this.soundFonts[soundFontIndex];
757
816
  const isSF3 = soundFont.parsed.info.version.major === 3;
758
- const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
759
- if (!noteInfo)
817
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
818
+ if (!instrumentKey)
760
819
  return;
761
- const { bufferSource, gainNode, filterNode, lfoGain } = await this
762
- .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
763
- this.connectNoteEffects(channel, gainNode);
820
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
821
+ this.connectNoteEffects(channel, note.gainNode);
764
822
  if (channel.sostenutoPedal) {
765
- channel.sostenutoNotes.set(noteNumber, {
766
- gainNode,
767
- filterNode,
768
- bufferSource,
769
- noteNumber,
770
- noteInfo,
771
- });
823
+ channel.sostenutoNotes.set(noteNumber, note);
772
824
  }
773
825
  const scheduledNotes = channel.scheduledNotes;
774
- const scheduledNote = {
775
- bufferSource,
776
- filterNode,
777
- gainNode,
778
- lfoGain,
779
- noteInfo,
780
- noteNumber,
781
- startTime,
782
- };
783
826
  if (scheduledNotes.has(noteNumber)) {
784
- scheduledNotes.get(noteNumber).push(scheduledNote);
827
+ scheduledNotes.get(noteNumber).push(note);
785
828
  }
786
829
  else {
787
- scheduledNotes.set(noteNumber, [scheduledNote]);
830
+ scheduledNotes.set(noteNumber, [note]);
788
831
  }
789
832
  }
790
833
  noteOn(channelNumber, noteNumber, velocity) {
@@ -806,15 +849,15 @@ class Midy {
806
849
  continue;
807
850
  if (targetNote.ending)
808
851
  continue;
809
- const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
852
+ const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, vibLFO, vibLFOGain, instrumentKey, } = targetNote;
810
853
  const velocityRate = (velocity + 127) / 127;
811
- const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
854
+ const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
812
855
  gainNode.gain.cancelScheduledValues(stopTime);
813
856
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
814
857
  const maxFreq = this.audioContext.sampleRate / 2;
815
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
858
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
816
859
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
817
- const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
860
+ const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
818
861
  filterNode.frequency
819
862
  .cancelScheduledValues(stopTime)
820
863
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
@@ -828,8 +871,14 @@ class Midy {
828
871
  bufferSource.disconnect(0);
829
872
  filterNode.disconnect(0);
830
873
  gainNode.disconnect(0);
831
- if (lfoGain)
832
- lfoGain.disconnect(0);
874
+ if (modLFOGain)
875
+ modLFOGain.disconnect(0);
876
+ if (vibLFOGain)
877
+ vibLFOGain.disconnect(0);
878
+ if (modLFO)
879
+ modLFO.stop();
880
+ if (vibLFO)
881
+ vibLFO.stop();
833
882
  resolve();
834
883
  };
835
884
  bufferSource.stop(volEndTime);
@@ -895,7 +944,7 @@ class Midy {
895
944
  const now = this.audioContext.currentTime;
896
945
  const channel = this.channels[channelNumber];
897
946
  pressure /= 64;
898
- const activeNotes = this.getActiveNotes(channel);
947
+ const activeNotes = this.getActiveNotes(channel, now);
899
948
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
900
949
  if (activeNotes.has(noteNumber)) {
901
950
  const activeNote = activeNotes.get(noteNumber);
@@ -916,7 +965,7 @@ class Midy {
916
965
  const channel = this.channels[channelNumber];
917
966
  pressure /= 64;
918
967
  channel.channelPressure = pressure;
919
- const activeNotes = this.getActiveNotes(channel);
968
+ const activeNotes = this.getActiveNotes(channel, now);
920
969
  if (channel.channelPressure.amplitudeControl !== 1) {
921
970
  activeNotes.forEach((activeNote) => {
922
971
  const gain = activeNote.gainNode.gain.value;
@@ -928,20 +977,22 @@ class Midy {
928
977
  }
929
978
  handlePitchBendMessage(channelNumber, lsb, msb) {
930
979
  const pitchBend = msb * 128 + lsb;
931
- this.handlePitchBend(channelNumber, pitchBend);
980
+ this.setPitchBend(channelNumber, pitchBend);
932
981
  }
933
- handlePitchBend(channelNumber, pitchBend) {
982
+ setPitchBend(channelNumber, pitchBend) {
934
983
  const now = this.audioContext.currentTime;
935
984
  const channel = this.channels[channelNumber];
985
+ const prevPitchBend = channel.pitchBend;
936
986
  channel.pitchBend = (pitchBend - 8192) / 8192;
937
- const semitoneOffset = this.calcSemitoneOffset(channel);
938
- const activeNotes = this.getActiveNotes(channel);
987
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
988
+ channel.pitchBendRange * 100;
989
+ const activeNotes = this.getActiveNotes(channel, now);
939
990
  activeNotes.forEach((activeNote) => {
940
- const { bufferSource, noteInfo, noteNumber } = activeNote;
941
- const playbackRate = calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
942
- bufferSource.playbackRate
991
+ const { bufferSource } = activeNote;
992
+ const detune = bufferSource.detune.value + detuneChange;
993
+ bufferSource.detune
943
994
  .cancelScheduledValues(now)
944
- .setValueAtTime(playbackRate * pressure, now);
995
+ .setValueAtTime(detune, now);
945
996
  });
946
997
  }
947
998
  handleControlChange(channelNumber, controller, value) {
@@ -1013,9 +1064,20 @@ class Midy {
1013
1064
  this.channels[channelNumber].bankMSB = msb;
1014
1065
  }
1015
1066
  setModulation(channelNumber, modulation) {
1067
+ const now = this.audioContext.currentTime;
1016
1068
  const channel = this.channels[channelNumber];
1017
1069
  channel.modulation = (modulation / 127) *
1018
1070
  (channel.modulationDepthRange * 100);
1071
+ const activeNotes = this.getActiveNotes(channel, now);
1072
+ activeNotes.forEach((activeNote) => {
1073
+ if (activeNote.modLFO) {
1074
+ activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
1075
+ channel.modulation, now);
1076
+ }
1077
+ else {
1078
+ this.startModulation(channel, activeNote, now);
1079
+ }
1080
+ });
1019
1081
  }
1020
1082
  setPortamentoTime(channelNumber, portamentoTime) {
1021
1083
  this.channels[channelNumber].portamentoTime = portamentoTime / 127;
@@ -1025,12 +1087,17 @@ class Midy {
1025
1087
  channel.volume = volume / 127;
1026
1088
  this.updateChannelGain(channel);
1027
1089
  }
1090
+ panToGain(pan) {
1091
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1092
+ return {
1093
+ gainLeft: Math.cos(theta),
1094
+ gainRight: Math.sin(theta),
1095
+ };
1096
+ }
1028
1097
  setPan(channelNumber, pan) {
1029
- const now = this.audioContext.currentTime;
1030
1098
  const channel = this.channels[channelNumber];
1031
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
1032
- channel.pannerNode.pan.cancelScheduledValues(now);
1033
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
1099
+ channel.pan = pan;
1100
+ this.updateChannelGain(channel);
1034
1101
  }
1035
1102
  setExpression(channelNumber, expression) {
1036
1103
  const channel = this.channels[channelNumber];
@@ -1043,8 +1110,13 @@ class Midy {
1043
1110
  updateChannelGain(channel) {
1044
1111
  const now = this.audioContext.currentTime;
1045
1112
  const volume = channel.volume * channel.expression;
1046
- channel.gainNode.gain.cancelScheduledValues(now);
1047
- channel.gainNode.gain.setValueAtTime(volume, now);
1113
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
1114
+ channel.gainL.gain
1115
+ .cancelScheduledValues(now)
1116
+ .setValueAtTime(volume * gainLeft, now);
1117
+ channel.gainR.gain
1118
+ .cancelScheduledValues(now)
1119
+ .setValueAtTime(volume * gainRight, now);
1048
1120
  }
1049
1121
  setSustainPedal(channelNumber, value) {
1050
1122
  const isOn = value >= 64;
@@ -1076,7 +1148,8 @@ class Midy {
1076
1148
  const channel = this.channels[channelNumber];
1077
1149
  channel.sostenutoPedal = isOn;
1078
1150
  if (isOn) {
1079
- const activeNotes = this.getActiveNotes(channel);
1151
+ const now = this.audioContext.currentTime;
1152
+ const activeNotes = this.getActiveNotes(channel, now);
1080
1153
  channel.sostenutoNotes = new Map(activeNotes);
1081
1154
  }
1082
1155
  else {
@@ -1088,20 +1161,12 @@ class Midy {
1088
1161
  channel.softPedal = softPedal / 127;
1089
1162
  }
1090
1163
  setVibratoRate(channelNumber, vibratoRate) {
1091
- const now = this.audioContext.currentTime;
1092
1164
  const channel = this.channels[channelNumber];
1093
1165
  channel.vibratoRate = vibratoRate / 127 * 4 + 3; // 3-7Hz
1094
- channel.modulationEffect.lfo.frequency
1095
- .cancelScheduledValues(now)
1096
- .setValueAtTime(channel.vibratoRate, now);
1097
1166
  }
1098
1167
  setVibratoDepth(channelNumber, vibratoDepth) {
1099
- const now = this.audioContext.currentTime;
1100
1168
  const channel = this.channels[channelNumber];
1101
1169
  channel.vibratoDepth = vibratoDepth / 127;
1102
- channel.modulationEffect.lfoGain.gain
1103
- .cancelScheduledValues(now)
1104
- .setValueAtTime(channel.vibratoDepth, now);
1105
1170
  }
1106
1171
  setVibratoDelay(channelNumber, vibratoDelay) {
1107
1172
  // Access Virus: 0-10sec
@@ -1161,8 +1226,7 @@ class Midy {
1161
1226
  const { dataMSB, dataLSB } = channel;
1162
1227
  switch (rpn) {
1163
1228
  case 0:
1164
- channel.pitchBendRange = dataMSB + dataLSB / 100;
1165
- break;
1229
+ return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
1166
1230
  case 1:
1167
1231
  channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
1168
1232
  break;
@@ -1176,14 +1240,34 @@ class Midy {
1176
1240
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1177
1241
  }
1178
1242
  }
1243
+ handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
1244
+ const pitchBendRange = dataMSB + dataLSB / 100;
1245
+ this.setPitchBendRange(channelNumber, pitchBendRange);
1246
+ }
1247
+ setPitchBendRange(channelNumber, pitchBendRange) {
1248
+ const now = this.audioContext.currentTime;
1249
+ const channel = this.channels[channelNumber];
1250
+ const prevPitchBendRange = channel.pitchBendRange;
1251
+ channel.pitchBendRange = pitchBendRange;
1252
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
1253
+ channel.pitchBend * 100;
1254
+ const activeNotes = this.getActiveNotes(channel, now);
1255
+ activeNotes.forEach((activeNote) => {
1256
+ const { bufferSource } = activeNote;
1257
+ const detune = bufferSource.detune.value + detuneChange;
1258
+ bufferSource.detune
1259
+ .cancelScheduledValues(now)
1260
+ .setValueAtTime(detune, now);
1261
+ });
1262
+ }
1179
1263
  allSoundOff(channelNumber) {
1180
1264
  const now = this.audioContext.currentTime;
1181
1265
  const channel = this.channels[channelNumber];
1182
1266
  const velocity = 0;
1183
1267
  const stopPedal = true;
1184
1268
  const promises = [];
1185
- channel.scheduledNotes.forEach((scheduledNotes) => {
1186
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
1269
+ channel.scheduledNotes.forEach((noteList) => {
1270
+ const activeNote = this.getActiveNote(noteList, now);
1187
1271
  if (activeNote) {
1188
1272
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1189
1273
  promises.push(notePromise);
@@ -1200,8 +1284,8 @@ class Midy {
1200
1284
  const velocity = 0;
1201
1285
  const stopPedal = false;
1202
1286
  const promises = [];
1203
- channel.scheduledNotes.forEach((scheduledNotes) => {
1204
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
1287
+ channel.scheduledNotes.forEach((noteList) => {
1288
+ const activeNote = this.getActiveNote(noteList, now);
1205
1289
  if (activeNote) {
1206
1290
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1207
1291
  promises.push(notePromise);
@@ -1266,9 +1350,9 @@ class Midy {
1266
1350
  case 1:
1267
1351
  return this.handleMasterVolumeSysEx(data);
1268
1352
  case 3:
1269
- return this.handleMasterFineTuning(data);
1353
+ return this.handleMasterFineTuningSysEx(data);
1270
1354
  case 4:
1271
- return this.handleMasterCoarseTuning(data);
1355
+ return this.handleMasterCoarseTuningSysEx(data);
1272
1356
  // case 5: // TODO: Global Parameter Control
1273
1357
  default:
1274
1358
  console.warn(`Unsupported Exclusive Message ${data}`);
@@ -1310,9 +1394,9 @@ class Midy {
1310
1394
  }
1311
1395
  handleMasterVolumeSysEx(data) {
1312
1396
  const volume = (data[5] * 128 + data[4]) / 16383;
1313
- this.handleMasterVolume(volume);
1397
+ this.setMasterVolume(volume);
1314
1398
  }
1315
- handleMasterVolume(volume) {
1399
+ setMasterVolume(volume) {
1316
1400
  if (volume < 0 && 1 < volume) {
1317
1401
  console.error("Master Volume is out of range");
1318
1402
  }
@@ -1324,9 +1408,9 @@ class Midy {
1324
1408
  }
1325
1409
  handleMasterFineTuningSysEx(data) {
1326
1410
  const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
1327
- this.handleMasterFineTuning(fineTuning);
1411
+ this.setMasterFineTuning(fineTuning);
1328
1412
  }
1329
- handleMasterFineTuning(fineTuning) {
1413
+ setMasterFineTuning(fineTuning) {
1330
1414
  if (fineTuning < -1 && 1 < fineTuning) {
1331
1415
  console.error("Master Fine Tuning value is out of range");
1332
1416
  }
@@ -1336,9 +1420,9 @@ class Midy {
1336
1420
  }
1337
1421
  handleMasterCoarseTuningSysEx(data) {
1338
1422
  const coarseTuning = data[4];
1339
- this.handleMasterCoarseTuning(coarseTuning);
1423
+ this.setMasterCoarseTuning(coarseTuning);
1340
1424
  }
1341
- handleMasterCoarseTuning(coarseTuning) {
1425
+ setMasterCoarseTuning(coarseTuning) {
1342
1426
  if (coarseTuning < 0 && 127 < coarseTuning) {
1343
1427
  console.error("Master Coarse Tuning value is out of range");
1344
1428
  }
@@ -1379,7 +1463,7 @@ Object.defineProperty(Midy, "channelSettings", {
1379
1463
  value: {
1380
1464
  currentBufferSource: null,
1381
1465
  volume: 100 / 127,
1382
- pan: 0,
1466
+ pan: 64,
1383
1467
  portamentoTime: 0,
1384
1468
  reverb: 0,
1385
1469
  chorus: 0,