@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/esm/midy.js CHANGED
@@ -1,5 +1,55 @@
1
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.1/+esm.js";
2
+ import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.js";
3
+ class Note {
4
+ constructor(noteNumber, velocity, startTime, instrumentKey) {
5
+ Object.defineProperty(this, "bufferSource", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: void 0
10
+ });
11
+ Object.defineProperty(this, "gainNode", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
17
+ Object.defineProperty(this, "filterNode", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: void 0
22
+ });
23
+ Object.defineProperty(this, "modLFO", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: void 0
28
+ });
29
+ Object.defineProperty(this, "modLFOGain", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: void 0
34
+ });
35
+ Object.defineProperty(this, "vibLFO", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: void 0
40
+ });
41
+ Object.defineProperty(this, "vibLFOGain", {
42
+ enumerable: true,
43
+ configurable: true,
44
+ writable: true,
45
+ value: void 0
46
+ });
47
+ this.noteNumber = noteNumber;
48
+ this.velocity = velocity;
49
+ this.startTime = startTime;
50
+ this.instrumentKey = instrumentKey;
51
+ }
52
+ }
3
53
  export class Midy {
4
54
  constructor(audioContext) {
5
55
  Object.defineProperty(this, "ticksPerBeat", {
@@ -175,25 +225,23 @@ export class Midy {
175
225
  this.totalTime = this.calcTotalTime();
176
226
  }
177
227
  setChannelAudioNodes(audioContext) {
178
- const gainNode = new GainNode(audioContext, {
179
- gain: Midy.channelSettings.volume,
180
- });
181
- const pannerNode = new StereoPannerNode(audioContext, {
182
- pan: Midy.channelSettings.pan,
183
- });
184
- const modulationEffect = this.createModulationEffect(audioContext);
228
+ const { gainLeft, gainRight } = this.panToGain(Midy.channelSettings.pan);
229
+ const gainL = new GainNode(audioContext, { gain: gainLeft });
230
+ const gainR = new GainNode(audioContext, { gain: gainRight });
231
+ const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
232
+ gainL.connect(merger, 0, 0);
233
+ gainR.connect(merger, 0, 1);
234
+ merger.connect(this.masterGain);
185
235
  const reverbEffect = this.createReverbEffect(audioContext);
186
236
  const chorusEffect = this.createChorusEffect(audioContext);
187
- modulationEffect.lfo.start();
188
237
  chorusEffect.lfo.start();
189
- reverbEffect.dryGain.connect(pannerNode);
190
- reverbEffect.wetGain.connect(pannerNode);
191
- pannerNode.connect(gainNode);
192
- gainNode.connect(this.masterGain);
238
+ reverbEffect.dryGain.connect(gainL);
239
+ reverbEffect.dryGain.connect(gainR);
240
+ reverbEffect.wetGain.connect(gainL);
241
+ reverbEffect.wetGain.connect(gainR);
193
242
  return {
194
- gainNode,
195
- pannerNode,
196
- modulationEffect,
243
+ gainL,
244
+ gainR,
197
245
  reverbEffect,
198
246
  chorusEffect,
199
247
  };
@@ -216,11 +264,11 @@ export class Midy {
216
264
  });
217
265
  return channels;
218
266
  }
219
- async createNoteBuffer(noteInfo, isSF3) {
220
- const sampleEnd = noteInfo.sample.length + noteInfo.end;
267
+ async createNoteBuffer(instrumentKey, isSF3) {
268
+ const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
221
269
  if (isSF3) {
222
- const sample = new Uint8Array(noteInfo.sample.length);
223
- sample.set(noteInfo.sample);
270
+ const sample = new Uint8Array(instrumentKey.sample.length);
271
+ sample.set(instrumentKey.sample);
224
272
  const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
225
273
  for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
226
274
  const channelData = audioBuffer.getChannelData(channel);
@@ -229,26 +277,27 @@ export class Midy {
229
277
  return audioBuffer;
230
278
  }
231
279
  else {
232
- const sample = noteInfo.sample.subarray(0, sampleEnd);
280
+ const sample = instrumentKey.sample.subarray(0, sampleEnd);
233
281
  const floatSample = this.convertToFloat32Array(sample);
234
282
  const audioBuffer = new AudioBuffer({
235
283
  numberOfChannels: 1,
236
284
  length: sample.length,
237
- sampleRate: noteInfo.sampleRate,
285
+ sampleRate: instrumentKey.sampleRate,
238
286
  });
239
287
  const channelData = audioBuffer.getChannelData(0);
240
288
  channelData.set(floatSample);
241
289
  return audioBuffer;
242
290
  }
243
291
  }
244
- async createNoteBufferNode(noteInfo, isSF3) {
292
+ async createNoteBufferNode(instrumentKey, isSF3) {
245
293
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
246
- const audioBuffer = await this.createNoteBuffer(noteInfo, isSF3);
294
+ const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
247
295
  bufferSource.buffer = audioBuffer;
248
- bufferSource.loop = noteInfo.sampleModes % 2 !== 0;
296
+ bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
249
297
  if (bufferSource.loop) {
250
- bufferSource.loopStart = noteInfo.loopStart / noteInfo.sampleRate;
251
- bufferSource.loopEnd = noteInfo.loopEnd / noteInfo.sampleRate;
298
+ bufferSource.loopStart = instrumentKey.loopStart /
299
+ instrumentKey.sampleRate;
300
+ bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
252
301
  }
253
302
  return bufferSource;
254
303
  }
@@ -292,7 +341,7 @@ export class Midy {
292
341
  this.handleChannelPressure(event.channel, event.amount);
293
342
  break;
294
343
  case "pitchBend":
295
- this.handlePitchBend(event.channel, event.value);
344
+ this.setPitchBend(event.channel, event.value);
296
345
  break;
297
346
  case "sysEx":
298
347
  this.handleSysEx(event.data);
@@ -372,7 +421,6 @@ export class Midy {
372
421
  const tmpChannels = new Array(16);
373
422
  for (let i = 0; i < tmpChannels.length; i++) {
374
423
  tmpChannels[i] = {
375
- durationTicks: new Map(),
376
424
  programNumber: -1,
377
425
  bankMSB: this.channels[i].bankMSB,
378
426
  bankLSB: this.channels[i].bankLSB,
@@ -402,16 +450,6 @@ export class Midy {
402
450
  }
403
451
  channel.programNumber = 0;
404
452
  }
405
- channel.durationTicks.set(event.noteNumber, {
406
- ticks: event.ticks,
407
- noteOn: event,
408
- });
409
- break;
410
- }
411
- case "noteOff": {
412
- const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
413
- .get(event.noteNumber);
414
- noteOn.durationTicks = event.ticks - ticks;
415
453
  break;
416
454
  }
417
455
  case "controller":
@@ -446,8 +484,8 @@ export class Midy {
446
484
  });
447
485
  });
448
486
  const priority = {
449
- setTempo: 0,
450
- controller: 1,
487
+ controller: 0,
488
+ sysEx: 1,
451
489
  };
452
490
  timeline.sort((a, b) => {
453
491
  if (a.ticks !== b.ticks)
@@ -530,30 +568,26 @@ export class Midy {
530
568
  const now = this.audioContext.currentTime;
531
569
  return this.resumeTime + now - this.startTime - this.startDelay;
532
570
  }
533
- getActiveNotes(channel) {
571
+ getActiveNotes(channel, time) {
534
572
  const activeNotes = new Map();
535
- channel.scheduledNotes.forEach((scheduledNotes) => {
536
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
573
+ channel.scheduledNotes.forEach((noteList) => {
574
+ const activeNote = this.getActiveNote(noteList, time);
537
575
  if (activeNote) {
538
576
  activeNotes.set(activeNote.noteNumber, activeNote);
539
577
  }
540
578
  });
541
579
  return activeNotes;
542
580
  }
543
- getActiveChannelNotes(scheduledNotes) {
544
- for (let i = 0; i < scheduledNotes; i++) {
545
- const scheduledNote = scheduledNotes[i];
546
- if (scheduledNote)
547
- return scheduledNote;
581
+ getActiveNote(noteList, time) {
582
+ for (let i = noteList.length - 1; i >= 0; i--) {
583
+ const note = noteList[i];
584
+ if (!note)
585
+ return;
586
+ if (time < note.startTime)
587
+ continue;
588
+ return (note.ending) ? null : note;
548
589
  }
549
- }
550
- createModulationEffect(audioContext) {
551
- const lfo = new OscillatorNode(audioContext, {
552
- frequency: 5,
553
- });
554
- return {
555
- lfo,
556
- };
590
+ return noteList[0];
557
591
  }
558
592
  createReverbEffect(audioContext, options = {}) {
559
593
  const { decay = 0.8, preDecay = 0, } = options;
@@ -589,12 +623,8 @@ export class Midy {
589
623
  }
590
624
  createChorusEffect(audioContext, options = {}) {
591
625
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
592
- const lfo = new OscillatorNode(audioContext, {
593
- frequency: chorusRate,
594
- });
595
- const lfoGain = new GainNode(audioContext, {
596
- gain: chorusDepth,
597
- });
626
+ const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
627
+ const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
598
628
  const chorusGains = [];
599
629
  const delayNodes = [];
600
630
  const baseGain = 1 / chorusCount;
@@ -605,9 +635,7 @@ export class Midy {
605
635
  maxDelayTime: delayTime,
606
636
  });
607
637
  delayNodes.push(delayNode);
608
- const chorusGain = new GainNode(audioContext, {
609
- gain: baseGain,
610
- });
638
+ const chorusGain = new GainNode(audioContext, { gain: baseGain });
611
639
  chorusGains.push(chorusGain);
612
640
  lfo.connect(lfoGain);
613
641
  lfoGain.connect(delayNode.delayTime);
@@ -663,77 +691,108 @@ export class Midy {
663
691
  const tuning = masterTuning + channelTuning;
664
692
  return channel.pitchBend * channel.pitchBendRange + tuning;
665
693
  }
666
- calcPlaybackRate(noteInfo, noteNumber, semitoneOffset) {
667
- return noteInfo.playbackRate(noteNumber) * Math.pow(2, semitoneOffset / 12);
694
+ calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
695
+ return instrumentKey.playbackRate(noteNumber) *
696
+ Math.pow(2, semitoneOffset / 12);
668
697
  }
669
- async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
670
- const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
671
- const semitoneOffset = this.calcSemitoneOffset(channel);
672
- bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
673
- // volume envelope
674
- const gainNode = new GainNode(this.audioContext, {
675
- gain: 0,
676
- });
698
+ setVolumeEnvelope(channel, note) {
699
+ const { instrumentKey, startTime, velocity } = note;
700
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
677
701
  let volume = (velocity / 127) * channel.volume * channel.expression;
678
702
  if (volume === 0)
679
703
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
680
- const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
681
- const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
682
- const volDelay = startTime + noteInfo.volDelay;
683
- const volAttack = volDelay + noteInfo.volAttack;
684
- const volHold = volAttack + noteInfo.volHold;
685
- const volDecay = volHold + noteInfo.volDecay;
686
- gainNode.gain
704
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
705
+ volume;
706
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
707
+ const volDelay = startTime + instrumentKey.volDelay;
708
+ const volAttack = volDelay + instrumentKey.volAttack;
709
+ const volHold = volAttack + instrumentKey.volHold;
710
+ const volDecay = volHold + instrumentKey.volDecay;
711
+ note.gainNode.gain
687
712
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
688
713
  .exponentialRampToValueAtTime(attackVolume, volAttack)
689
714
  .setValueAtTime(attackVolume, volHold)
690
715
  .linearRampToValueAtTime(sustainVolume, volDecay);
691
- // filter envelope
716
+ }
717
+ setFilterEnvelope(channel, note) {
718
+ const { instrumentKey, startTime, noteNumber } = note;
692
719
  const softPedalFactor = 1 -
693
720
  (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
694
721
  const maxFreq = this.audioContext.sampleRate / 2;
695
- const baseFreq = this.centToHz(noteInfo.initialFilterFc) * softPedalFactor;
696
- const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc) * softPedalFactor;
722
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
723
+ softPedalFactor;
724
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
697
725
  const sustainFreq = (baseFreq +
698
- (peekFreq - baseFreq) * (1 - noteInfo.modSustain)) * softPedalFactor;
726
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
727
+ const modDelay = startTime + instrumentKey.modDelay;
728
+ const modAttack = modDelay + instrumentKey.modAttack;
729
+ const modHold = modAttack + instrumentKey.modHold;
730
+ const modDecay = modHold + instrumentKey.modDecay;
699
731
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
700
732
  const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
701
733
  const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
702
- const filterNode = new BiquadFilterNode(this.audioContext, {
734
+ note.filterNode = new BiquadFilterNode(this.audioContext, {
703
735
  type: "lowpass",
704
- Q: noteInfo.initialFilterQ / 10, // dB
736
+ Q: instrumentKey.initialFilterQ / 10, // dB
705
737
  frequency: adjustedBaseFreq,
706
738
  });
707
- const modDelay = startTime + noteInfo.modDelay;
708
- const modAttack = modDelay + noteInfo.modAttack;
709
- const modHold = modAttack + noteInfo.modHold;
710
- const modDecay = modHold + noteInfo.modDecay;
711
- filterNode.frequency
739
+ note.filterNode.frequency
712
740
  .setValueAtTime(adjustedBaseFreq, modDelay)
713
741
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
714
742
  .setValueAtTime(adjustedPeekFreq, modHold)
715
743
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
716
- let lfoGain;
744
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
745
+ }
746
+ startModulation(channel, note, time) {
747
+ const { instrumentKey } = note;
748
+ note.modLFOGain = new GainNode(this.audioContext, {
749
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
750
+ });
751
+ note.modLFO = new OscillatorNode(this.audioContext, {
752
+ frequency: this.centToHz(instrumentKey.freqModLFO),
753
+ });
754
+ note.modLFO.start(time);
755
+ note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
756
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
757
+ note.modLFO.connect(note.modLFOGain);
758
+ note.modLFOGain.connect(note.bufferSource.detune);
759
+ }
760
+ startVibrato(channel, note, time) {
761
+ const { instrumentKey } = note;
762
+ note.vibLFOGain = new GainNode(this.audioContext, {
763
+ gain: channel.vibratoDepth,
764
+ });
765
+ note.vibLFO = new OscillatorNode(this.audioContext, {
766
+ frequency: this.centToHz(instrumentKey.freqModLFO) +
767
+ channel.vibratoRate,
768
+ });
769
+ note.vibLFO.start(time + channel.vibratoDelay);
770
+ note.vibLFO.connect(note.vibLFOGain);
771
+ note.vibLFOGain.connect(note.bufferSource.detune);
772
+ }
773
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
774
+ const semitoneOffset = this.calcSemitoneOffset(channel);
775
+ const note = new Note(noteNumber, velocity, startTime, instrumentKey);
776
+ note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
777
+ note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
778
+ this.setVolumeEnvelope(channel, note);
779
+ this.setFilterEnvelope(channel, note);
717
780
  if (channel.modulation > 0) {
718
- const vibratoDelay = startTime + channel.vibratoDelay;
719
- const vibratoAttack = vibratoDelay + 0.1;
720
- lfoGain = new GainNode(this.audioContext, {
721
- gain: 0,
722
- });
723
- lfoGain.gain
724
- .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
725
- .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
726
- channel.modulationEffect.lfo.connect(lfoGain);
727
- lfoGain.connect(bufferSource.detune);
781
+ const delayModLFO = startTime + instrumentKey.delayModLFO;
782
+ this.startModulation(channel, note, delayModLFO);
783
+ }
784
+ if (channel.vibratoDepth > 0) {
785
+ const delayVibLFO = startTime + instrumentKey.delayVibLFO;
786
+ this.startVibrato(channel, note, delayVibLFO);
728
787
  }
729
- bufferSource.connect(filterNode);
730
- filterNode.connect(gainNode);
731
788
  if (this.mono && channel.currentBufferSource) {
732
789
  channel.currentBufferSource.stop(startTime);
733
- channel.currentBufferSource = bufferSource;
790
+ channel.currentBufferSource = note.bufferSource;
734
791
  }
735
- bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
736
- return { bufferSource, gainNode, filterNode, lfoGain };
792
+ note.bufferSource.connect(note.filterNode);
793
+ note.filterNode.connect(note.gainNode);
794
+ note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
795
+ return note;
737
796
  }
738
797
  calcBank(channel, channelNumber) {
739
798
  if (channel.bankMSB === 121) {
@@ -752,36 +811,20 @@ export class Midy {
752
811
  return;
753
812
  const soundFont = this.soundFonts[soundFontIndex];
754
813
  const isSF3 = soundFont.parsed.info.version.major === 3;
755
- const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
756
- if (!noteInfo)
814
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
815
+ if (!instrumentKey)
757
816
  return;
758
- const { bufferSource, gainNode, filterNode, lfoGain } = await this
759
- .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
760
- this.connectNoteEffects(channel, gainNode);
817
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
818
+ this.connectNoteEffects(channel, note.gainNode);
761
819
  if (channel.sostenutoPedal) {
762
- channel.sostenutoNotes.set(noteNumber, {
763
- gainNode,
764
- filterNode,
765
- bufferSource,
766
- noteNumber,
767
- noteInfo,
768
- });
820
+ channel.sostenutoNotes.set(noteNumber, note);
769
821
  }
770
822
  const scheduledNotes = channel.scheduledNotes;
771
- const scheduledNote = {
772
- bufferSource,
773
- filterNode,
774
- gainNode,
775
- lfoGain,
776
- noteInfo,
777
- noteNumber,
778
- startTime,
779
- };
780
823
  if (scheduledNotes.has(noteNumber)) {
781
- scheduledNotes.get(noteNumber).push(scheduledNote);
824
+ scheduledNotes.get(noteNumber).push(note);
782
825
  }
783
826
  else {
784
- scheduledNotes.set(noteNumber, [scheduledNote]);
827
+ scheduledNotes.set(noteNumber, [note]);
785
828
  }
786
829
  }
787
830
  noteOn(channelNumber, noteNumber, velocity) {
@@ -803,15 +846,15 @@ export class Midy {
803
846
  continue;
804
847
  if (targetNote.ending)
805
848
  continue;
806
- const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
849
+ const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, vibLFO, vibLFOGain, instrumentKey, } = targetNote;
807
850
  const velocityRate = (velocity + 127) / 127;
808
- const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
851
+ const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
809
852
  gainNode.gain.cancelScheduledValues(stopTime);
810
853
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
811
854
  const maxFreq = this.audioContext.sampleRate / 2;
812
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
855
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
813
856
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
814
- const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
857
+ const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
815
858
  filterNode.frequency
816
859
  .cancelScheduledValues(stopTime)
817
860
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
@@ -825,8 +868,14 @@ export class Midy {
825
868
  bufferSource.disconnect(0);
826
869
  filterNode.disconnect(0);
827
870
  gainNode.disconnect(0);
828
- if (lfoGain)
829
- lfoGain.disconnect(0);
871
+ if (modLFOGain)
872
+ modLFOGain.disconnect(0);
873
+ if (vibLFOGain)
874
+ vibLFOGain.disconnect(0);
875
+ if (modLFO)
876
+ modLFO.stop();
877
+ if (vibLFO)
878
+ vibLFO.stop();
830
879
  resolve();
831
880
  };
832
881
  bufferSource.stop(volEndTime);
@@ -892,7 +941,7 @@ export class Midy {
892
941
  const now = this.audioContext.currentTime;
893
942
  const channel = this.channels[channelNumber];
894
943
  pressure /= 64;
895
- const activeNotes = this.getActiveNotes(channel);
944
+ const activeNotes = this.getActiveNotes(channel, now);
896
945
  if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
897
946
  if (activeNotes.has(noteNumber)) {
898
947
  const activeNote = activeNotes.get(noteNumber);
@@ -913,7 +962,7 @@ export class Midy {
913
962
  const channel = this.channels[channelNumber];
914
963
  pressure /= 64;
915
964
  channel.channelPressure = pressure;
916
- const activeNotes = this.getActiveNotes(channel);
965
+ const activeNotes = this.getActiveNotes(channel, now);
917
966
  if (channel.channelPressure.amplitudeControl !== 1) {
918
967
  activeNotes.forEach((activeNote) => {
919
968
  const gain = activeNote.gainNode.gain.value;
@@ -925,20 +974,22 @@ export class Midy {
925
974
  }
926
975
  handlePitchBendMessage(channelNumber, lsb, msb) {
927
976
  const pitchBend = msb * 128 + lsb;
928
- this.handlePitchBend(channelNumber, pitchBend);
977
+ this.setPitchBend(channelNumber, pitchBend);
929
978
  }
930
- handlePitchBend(channelNumber, pitchBend) {
979
+ setPitchBend(channelNumber, pitchBend) {
931
980
  const now = this.audioContext.currentTime;
932
981
  const channel = this.channels[channelNumber];
982
+ const prevPitchBend = channel.pitchBend;
933
983
  channel.pitchBend = (pitchBend - 8192) / 8192;
934
- const semitoneOffset = this.calcSemitoneOffset(channel);
935
- const activeNotes = this.getActiveNotes(channel);
984
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
985
+ channel.pitchBendRange * 100;
986
+ const activeNotes = this.getActiveNotes(channel, now);
936
987
  activeNotes.forEach((activeNote) => {
937
- const { bufferSource, noteInfo, noteNumber } = activeNote;
938
- const playbackRate = calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
939
- bufferSource.playbackRate
988
+ const { bufferSource } = activeNote;
989
+ const detune = bufferSource.detune.value + detuneChange;
990
+ bufferSource.detune
940
991
  .cancelScheduledValues(now)
941
- .setValueAtTime(playbackRate * pressure, now);
992
+ .setValueAtTime(detune, now);
942
993
  });
943
994
  }
944
995
  handleControlChange(channelNumber, controller, value) {
@@ -1010,9 +1061,20 @@ export class Midy {
1010
1061
  this.channels[channelNumber].bankMSB = msb;
1011
1062
  }
1012
1063
  setModulation(channelNumber, modulation) {
1064
+ const now = this.audioContext.currentTime;
1013
1065
  const channel = this.channels[channelNumber];
1014
1066
  channel.modulation = (modulation / 127) *
1015
1067
  (channel.modulationDepthRange * 100);
1068
+ const activeNotes = this.getActiveNotes(channel, now);
1069
+ activeNotes.forEach((activeNote) => {
1070
+ if (activeNote.modLFO) {
1071
+ activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
1072
+ channel.modulation, now);
1073
+ }
1074
+ else {
1075
+ this.startModulation(channel, activeNote, now);
1076
+ }
1077
+ });
1016
1078
  }
1017
1079
  setPortamentoTime(channelNumber, portamentoTime) {
1018
1080
  this.channels[channelNumber].portamentoTime = portamentoTime / 127;
@@ -1022,12 +1084,17 @@ export class Midy {
1022
1084
  channel.volume = volume / 127;
1023
1085
  this.updateChannelGain(channel);
1024
1086
  }
1087
+ panToGain(pan) {
1088
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1089
+ return {
1090
+ gainLeft: Math.cos(theta),
1091
+ gainRight: Math.sin(theta),
1092
+ };
1093
+ }
1025
1094
  setPan(channelNumber, pan) {
1026
- const now = this.audioContext.currentTime;
1027
1095
  const channel = this.channels[channelNumber];
1028
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
1029
- channel.pannerNode.pan.cancelScheduledValues(now);
1030
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
1096
+ channel.pan = pan;
1097
+ this.updateChannelGain(channel);
1031
1098
  }
1032
1099
  setExpression(channelNumber, expression) {
1033
1100
  const channel = this.channels[channelNumber];
@@ -1040,8 +1107,13 @@ export class Midy {
1040
1107
  updateChannelGain(channel) {
1041
1108
  const now = this.audioContext.currentTime;
1042
1109
  const volume = channel.volume * channel.expression;
1043
- channel.gainNode.gain.cancelScheduledValues(now);
1044
- channel.gainNode.gain.setValueAtTime(volume, now);
1110
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
1111
+ channel.gainL.gain
1112
+ .cancelScheduledValues(now)
1113
+ .setValueAtTime(volume * gainLeft, now);
1114
+ channel.gainR.gain
1115
+ .cancelScheduledValues(now)
1116
+ .setValueAtTime(volume * gainRight, now);
1045
1117
  }
1046
1118
  setSustainPedal(channelNumber, value) {
1047
1119
  const isOn = value >= 64;
@@ -1073,7 +1145,8 @@ export class Midy {
1073
1145
  const channel = this.channels[channelNumber];
1074
1146
  channel.sostenutoPedal = isOn;
1075
1147
  if (isOn) {
1076
- const activeNotes = this.getActiveNotes(channel);
1148
+ const now = this.audioContext.currentTime;
1149
+ const activeNotes = this.getActiveNotes(channel, now);
1077
1150
  channel.sostenutoNotes = new Map(activeNotes);
1078
1151
  }
1079
1152
  else {
@@ -1085,20 +1158,12 @@ export class Midy {
1085
1158
  channel.softPedal = softPedal / 127;
1086
1159
  }
1087
1160
  setVibratoRate(channelNumber, vibratoRate) {
1088
- const now = this.audioContext.currentTime;
1089
1161
  const channel = this.channels[channelNumber];
1090
1162
  channel.vibratoRate = vibratoRate / 127 * 4 + 3; // 3-7Hz
1091
- channel.modulationEffect.lfo.frequency
1092
- .cancelScheduledValues(now)
1093
- .setValueAtTime(channel.vibratoRate, now);
1094
1163
  }
1095
1164
  setVibratoDepth(channelNumber, vibratoDepth) {
1096
- const now = this.audioContext.currentTime;
1097
1165
  const channel = this.channels[channelNumber];
1098
1166
  channel.vibratoDepth = vibratoDepth / 127;
1099
- channel.modulationEffect.lfoGain.gain
1100
- .cancelScheduledValues(now)
1101
- .setValueAtTime(channel.vibratoDepth, now);
1102
1167
  }
1103
1168
  setVibratoDelay(channelNumber, vibratoDelay) {
1104
1169
  // Access Virus: 0-10sec
@@ -1158,8 +1223,7 @@ export class Midy {
1158
1223
  const { dataMSB, dataLSB } = channel;
1159
1224
  switch (rpn) {
1160
1225
  case 0:
1161
- channel.pitchBendRange = dataMSB + dataLSB / 100;
1162
- break;
1226
+ return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
1163
1227
  case 1:
1164
1228
  channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
1165
1229
  break;
@@ -1173,14 +1237,34 @@ export class Midy {
1173
1237
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1174
1238
  }
1175
1239
  }
1240
+ handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
1241
+ const pitchBendRange = dataMSB + dataLSB / 100;
1242
+ this.setPitchBendRange(channelNumber, pitchBendRange);
1243
+ }
1244
+ setPitchBendRange(channelNumber, pitchBendRange) {
1245
+ const now = this.audioContext.currentTime;
1246
+ const channel = this.channels[channelNumber];
1247
+ const prevPitchBendRange = channel.pitchBendRange;
1248
+ channel.pitchBendRange = pitchBendRange;
1249
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
1250
+ channel.pitchBend * 100;
1251
+ const activeNotes = this.getActiveNotes(channel, now);
1252
+ activeNotes.forEach((activeNote) => {
1253
+ const { bufferSource } = activeNote;
1254
+ const detune = bufferSource.detune.value + detuneChange;
1255
+ bufferSource.detune
1256
+ .cancelScheduledValues(now)
1257
+ .setValueAtTime(detune, now);
1258
+ });
1259
+ }
1176
1260
  allSoundOff(channelNumber) {
1177
1261
  const now = this.audioContext.currentTime;
1178
1262
  const channel = this.channels[channelNumber];
1179
1263
  const velocity = 0;
1180
1264
  const stopPedal = true;
1181
1265
  const promises = [];
1182
- channel.scheduledNotes.forEach((scheduledNotes) => {
1183
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
1266
+ channel.scheduledNotes.forEach((noteList) => {
1267
+ const activeNote = this.getActiveNote(noteList, now);
1184
1268
  if (activeNote) {
1185
1269
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1186
1270
  promises.push(notePromise);
@@ -1197,8 +1281,8 @@ export class Midy {
1197
1281
  const velocity = 0;
1198
1282
  const stopPedal = false;
1199
1283
  const promises = [];
1200
- channel.scheduledNotes.forEach((scheduledNotes) => {
1201
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
1284
+ channel.scheduledNotes.forEach((noteList) => {
1285
+ const activeNote = this.getActiveNote(noteList, now);
1202
1286
  if (activeNote) {
1203
1287
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1204
1288
  promises.push(notePromise);
@@ -1263,9 +1347,9 @@ export class Midy {
1263
1347
  case 1:
1264
1348
  return this.handleMasterVolumeSysEx(data);
1265
1349
  case 3:
1266
- return this.handleMasterFineTuning(data);
1350
+ return this.handleMasterFineTuningSysEx(data);
1267
1351
  case 4:
1268
- return this.handleMasterCoarseTuning(data);
1352
+ return this.handleMasterCoarseTuningSysEx(data);
1269
1353
  // case 5: // TODO: Global Parameter Control
1270
1354
  default:
1271
1355
  console.warn(`Unsupported Exclusive Message ${data}`);
@@ -1307,9 +1391,9 @@ export class Midy {
1307
1391
  }
1308
1392
  handleMasterVolumeSysEx(data) {
1309
1393
  const volume = (data[5] * 128 + data[4]) / 16383;
1310
- this.handleMasterVolume(volume);
1394
+ this.setMasterVolume(volume);
1311
1395
  }
1312
- handleMasterVolume(volume) {
1396
+ setMasterVolume(volume) {
1313
1397
  if (volume < 0 && 1 < volume) {
1314
1398
  console.error("Master Volume is out of range");
1315
1399
  }
@@ -1321,9 +1405,9 @@ export class Midy {
1321
1405
  }
1322
1406
  handleMasterFineTuningSysEx(data) {
1323
1407
  const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
1324
- this.handleMasterFineTuning(fineTuning);
1408
+ this.setMasterFineTuning(fineTuning);
1325
1409
  }
1326
- handleMasterFineTuning(fineTuning) {
1410
+ setMasterFineTuning(fineTuning) {
1327
1411
  if (fineTuning < -1 && 1 < fineTuning) {
1328
1412
  console.error("Master Fine Tuning value is out of range");
1329
1413
  }
@@ -1333,9 +1417,9 @@ export class Midy {
1333
1417
  }
1334
1418
  handleMasterCoarseTuningSysEx(data) {
1335
1419
  const coarseTuning = data[4];
1336
- this.handleMasterCoarseTuning(coarseTuning);
1420
+ this.setMasterCoarseTuning(coarseTuning);
1337
1421
  }
1338
- handleMasterCoarseTuning(coarseTuning) {
1422
+ setMasterCoarseTuning(coarseTuning) {
1339
1423
  if (coarseTuning < 0 && 127 < coarseTuning) {
1340
1424
  console.error("Master Coarse Tuning value is out of range");
1341
1425
  }
@@ -1375,7 +1459,7 @@ Object.defineProperty(Midy, "channelSettings", {
1375
1459
  value: {
1376
1460
  currentBufferSource: null,
1377
1461
  volume: 100 / 127,
1378
- pan: 0,
1462
+ pan: 64,
1379
1463
  portamentoTime: 0,
1380
1464
  reverb: 0,
1381
1465
  chorus: 0,