@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
@@ -2,7 +2,57 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGM2 = 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 MidyGM2 {
7
57
  constructor(audioContext) {
8
58
  Object.defineProperty(this, "ticksPerBeat", {
@@ -184,10 +234,8 @@ class MidyGM2 {
184
234
  const pannerNode = new StereoPannerNode(audioContext, {
185
235
  pan: MidyGM2.channelSettings.pan,
186
236
  });
187
- const modulationEffect = this.createModulationEffect(audioContext);
188
237
  const reverbEffect = this.createReverbEffect(audioContext);
189
238
  const chorusEffect = this.createChorusEffect(audioContext);
190
- modulationEffect.lfo.start();
191
239
  chorusEffect.lfo.start();
192
240
  reverbEffect.dryGain.connect(pannerNode);
193
241
  reverbEffect.wetGain.connect(pannerNode);
@@ -196,7 +244,6 @@ class MidyGM2 {
196
244
  return {
197
245
  gainNode,
198
246
  pannerNode,
199
- modulationEffect,
200
247
  reverbEffect,
201
248
  chorusEffect,
202
249
  };
@@ -204,23 +251,23 @@ class MidyGM2 {
204
251
  createChannels(audioContext) {
205
252
  const channels = Array.from({ length: 16 }, () => {
206
253
  return {
207
- ...Midy.channelSettings,
208
- ...Midy.effectSettings,
254
+ ...MidyGM2.channelSettings,
255
+ ...MidyGM2.effectSettings,
209
256
  ...this.setChannelAudioNodes(audioContext),
210
257
  scheduledNotes: new Map(),
211
258
  sostenutoNotes: new Map(),
212
259
  channelPressure: {
213
- ...Midy.controllerDestinationSettings,
260
+ ...MidyGM2.controllerDestinationSettings,
214
261
  },
215
262
  };
216
263
  });
217
264
  return channels;
218
265
  }
219
- async createNoteBuffer(noteInfo, isSF3) {
220
- const sampleEnd = noteInfo.sample.length + noteInfo.end;
266
+ async createNoteBuffer(instrumentKey, isSF3) {
267
+ const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
221
268
  if (isSF3) {
222
- const sample = new Uint8Array(noteInfo.sample.length);
223
- sample.set(noteInfo.sample);
269
+ const sample = new Uint8Array(instrumentKey.sample.length);
270
+ sample.set(instrumentKey.sample);
224
271
  const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
225
272
  for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
226
273
  const channelData = audioBuffer.getChannelData(channel);
@@ -229,26 +276,27 @@ class MidyGM2 {
229
276
  return audioBuffer;
230
277
  }
231
278
  else {
232
- const sample = noteInfo.sample.subarray(0, sampleEnd);
279
+ const sample = instrumentKey.sample.subarray(0, sampleEnd);
233
280
  const floatSample = this.convertToFloat32Array(sample);
234
281
  const audioBuffer = new AudioBuffer({
235
282
  numberOfChannels: 1,
236
283
  length: sample.length,
237
- sampleRate: noteInfo.sampleRate,
284
+ sampleRate: instrumentKey.sampleRate,
238
285
  });
239
286
  const channelData = audioBuffer.getChannelData(0);
240
287
  channelData.set(floatSample);
241
288
  return audioBuffer;
242
289
  }
243
290
  }
244
- async createNoteBufferNode(noteInfo, isSF3) {
291
+ async createNoteBufferNode(instrumentKey, isSF3) {
245
292
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
246
- const audioBuffer = await this.createNoteBuffer(noteInfo, isSF3);
293
+ const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
247
294
  bufferSource.buffer = audioBuffer;
248
- bufferSource.loop = noteInfo.sampleModes % 2 !== 0;
295
+ bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
249
296
  if (bufferSource.loop) {
250
- bufferSource.loopStart = noteInfo.loopStart / noteInfo.sampleRate;
251
- bufferSource.loopEnd = noteInfo.loopEnd / noteInfo.sampleRate;
297
+ bufferSource.loopStart = instrumentKey.loopStart /
298
+ instrumentKey.sampleRate;
299
+ bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
252
300
  }
253
301
  return bufferSource;
254
302
  }
@@ -289,7 +337,7 @@ class MidyGM2 {
289
337
  this.handleChannelPressure(event.channel, event.amount);
290
338
  break;
291
339
  case "pitchBend":
292
- this.handlePitchBend(event.channel, event.value);
340
+ this.setPitchBend(event.channel, event.value);
293
341
  break;
294
342
  case "sysEx":
295
343
  this.handleSysEx(event.data);
@@ -369,7 +417,6 @@ class MidyGM2 {
369
417
  const tmpChannels = new Array(16);
370
418
  for (let i = 0; i < tmpChannels.length; i++) {
371
419
  tmpChannels[i] = {
372
- durationTicks: new Map(),
373
420
  programNumber: -1,
374
421
  bankMSB: this.channels[i].bankMSB,
375
422
  bankLSB: this.channels[i].bankLSB,
@@ -399,16 +446,6 @@ class MidyGM2 {
399
446
  }
400
447
  channel.programNumber = 0;
401
448
  }
402
- channel.durationTicks.set(event.noteNumber, {
403
- ticks: event.ticks,
404
- noteOn: event,
405
- });
406
- break;
407
- }
408
- case "noteOff": {
409
- const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
410
- .get(event.noteNumber);
411
- noteOn.durationTicks = event.ticks - ticks;
412
449
  break;
413
450
  }
414
451
  case "controller":
@@ -443,8 +480,8 @@ class MidyGM2 {
443
480
  });
444
481
  });
445
482
  const priority = {
446
- setTempo: 0,
447
- controller: 1,
483
+ controller: 0,
484
+ sysEx: 1,
448
485
  };
449
486
  timeline.sort((a, b) => {
450
487
  if (a.ticks !== b.ticks)
@@ -527,30 +564,26 @@ class MidyGM2 {
527
564
  const now = this.audioContext.currentTime;
528
565
  return this.resumeTime + now - this.startTime - this.startDelay;
529
566
  }
530
- getActiveNotes(channel) {
567
+ getActiveNotes(channel, time) {
531
568
  const activeNotes = new Map();
532
- channel.scheduledNotes.forEach((scheduledNotes) => {
533
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
569
+ channel.scheduledNotes.forEach((noteList) => {
570
+ const activeNote = this.getActiveNote(noteList, time);
534
571
  if (activeNote) {
535
572
  activeNotes.set(activeNote.noteNumber, activeNote);
536
573
  }
537
574
  });
538
575
  return activeNotes;
539
576
  }
540
- getActiveChannelNotes(scheduledNotes) {
541
- for (let i = 0; i < scheduledNotes; i++) {
542
- const scheduledNote = scheduledNotes[i];
543
- if (scheduledNote)
544
- return scheduledNote;
577
+ getActiveNote(noteList, time) {
578
+ for (let i = noteList.length - 1; i >= 0; i--) {
579
+ const note = noteList[i];
580
+ if (!note)
581
+ return;
582
+ if (time < note.startTime)
583
+ continue;
584
+ return (note.ending) ? null : note;
545
585
  }
546
- }
547
- createModulationEffect(audioContext) {
548
- const lfo = new OscillatorNode(audioContext, {
549
- frequency: 5,
550
- });
551
- return {
552
- lfo,
553
- };
586
+ return noteList[0];
554
587
  }
555
588
  createReverbEffect(audioContext, options = {}) {
556
589
  const { decay = 0.8, preDecay = 0, } = options;
@@ -586,12 +619,8 @@ class MidyGM2 {
586
619
  }
587
620
  createChorusEffect(audioContext, options = {}) {
588
621
  const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
589
- const lfo = new OscillatorNode(audioContext, {
590
- frequency: chorusRate,
591
- });
592
- const lfoGain = new GainNode(audioContext, {
593
- gain: chorusDepth,
594
- });
622
+ const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
623
+ const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
595
624
  const chorusGains = [];
596
625
  const delayNodes = [];
597
626
  const baseGain = 1 / chorusCount;
@@ -602,9 +631,7 @@ class MidyGM2 {
602
631
  maxDelayTime: delayTime,
603
632
  });
604
633
  delayNodes.push(delayNode);
605
- const chorusGain = new GainNode(audioContext, {
606
- gain: baseGain,
607
- });
634
+ const chorusGain = new GainNode(audioContext, { gain: baseGain });
608
635
  chorusGains.push(chorusGain);
609
636
  lfo.connect(lfoGain);
610
637
  lfoGain.connect(delayNode.delayTime);
@@ -660,77 +687,91 @@ class MidyGM2 {
660
687
  const tuning = masterTuning + channelTuning;
661
688
  return channel.pitchBend * channel.pitchBendRange + tuning;
662
689
  }
663
- calcPlaybackRate(noteInfo, noteNumber, semitoneOffset) {
664
- return noteInfo.playbackRate(noteNumber) * Math.pow(2, semitoneOffset / 12);
690
+ calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
691
+ return instrumentKey.playbackRate(noteNumber) *
692
+ Math.pow(2, semitoneOffset / 12);
665
693
  }
666
- async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
667
- const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
668
- const semitoneOffset = this.calcSemitoneOffset(channel);
669
- bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
670
- // volume envelope
671
- const gainNode = new GainNode(this.audioContext, {
672
- gain: 0,
673
- });
694
+ setVolumeEnvelope(channel, note) {
695
+ const { instrumentKey, startTime, velocity } = note;
696
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
674
697
  let volume = (velocity / 127) * channel.volume * channel.expression;
675
698
  if (volume === 0)
676
699
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
677
- const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
678
- const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
679
- const volDelay = startTime + noteInfo.volDelay;
680
- const volAttack = volDelay + noteInfo.volAttack;
681
- const volHold = volAttack + noteInfo.volHold;
682
- const volDecay = volHold + noteInfo.volDecay;
683
- gainNode.gain
700
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
701
+ volume;
702
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
703
+ const volDelay = startTime + instrumentKey.volDelay;
704
+ const volAttack = volDelay + instrumentKey.volAttack;
705
+ const volHold = volAttack + instrumentKey.volHold;
706
+ const volDecay = volHold + instrumentKey.volDecay;
707
+ note.gainNode.gain
684
708
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
685
709
  .exponentialRampToValueAtTime(attackVolume, volAttack)
686
710
  .setValueAtTime(attackVolume, volHold)
687
711
  .linearRampToValueAtTime(sustainVolume, volDecay);
688
- // filter envelope
712
+ }
713
+ setFilterEnvelope(channel, note) {
714
+ const { instrumentKey, startTime, noteNumber } = note;
689
715
  const softPedalFactor = 1 -
690
716
  (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
691
717
  const maxFreq = this.audioContext.sampleRate / 2;
692
- const baseFreq = this.centToHz(noteInfo.initialFilterFc) * softPedalFactor;
693
- const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc) * softPedalFactor;
718
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
719
+ softPedalFactor;
720
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
694
721
  const sustainFreq = (baseFreq +
695
- (peekFreq - baseFreq) * (1 - noteInfo.modSustain)) * softPedalFactor;
722
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
723
+ const modDelay = startTime + instrumentKey.modDelay;
724
+ const modAttack = modDelay + instrumentKey.modAttack;
725
+ const modHold = modAttack + instrumentKey.modHold;
726
+ const modDecay = modHold + instrumentKey.modDecay;
696
727
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
697
728
  const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
698
729
  const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
699
- const filterNode = new BiquadFilterNode(this.audioContext, {
730
+ note.filterNode = new BiquadFilterNode(this.audioContext, {
700
731
  type: "lowpass",
701
- Q: noteInfo.initialFilterQ / 10, // dB
732
+ Q: instrumentKey.initialFilterQ / 10, // dB
702
733
  frequency: adjustedBaseFreq,
703
734
  });
704
- const modDelay = startTime + noteInfo.modDelay;
705
- const modAttack = modDelay + noteInfo.modAttack;
706
- const modHold = modAttack + noteInfo.modHold;
707
- const modDecay = modHold + noteInfo.modDecay;
708
- filterNode.frequency
735
+ note.filterNode.frequency
709
736
  .setValueAtTime(adjustedBaseFreq, modDelay)
710
737
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
711
738
  .setValueAtTime(adjustedPeekFreq, modHold)
712
739
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
713
- let lfoGain;
740
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
741
+ }
742
+ startModulation(channel, note, time) {
743
+ const { instrumentKey } = note;
744
+ note.modLFOGain = new GainNode(this.audioContext, {
745
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
746
+ });
747
+ note.modLFO = new OscillatorNode(this.audioContext, {
748
+ frequency: this.centToHz(instrumentKey.freqModLFO),
749
+ });
750
+ note.modLFO.start(time);
751
+ note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
752
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
753
+ note.modLFO.connect(note.modLFOGain);
754
+ note.modLFOGain.connect(note.bufferSource.detune);
755
+ }
756
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
757
+ const semitoneOffset = this.calcSemitoneOffset(channel);
758
+ const note = new Note(noteNumber, velocity, startTime, instrumentKey);
759
+ note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
760
+ note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
761
+ this.setVolumeEnvelope(channel, note);
762
+ this.setFilterEnvelope(channel, note);
714
763
  if (channel.modulation > 0) {
715
- const vibratoDelay = startTime + channel.vibratoDelay;
716
- const vibratoAttack = vibratoDelay + 0.1;
717
- lfoGain = new GainNode(this.audioContext, {
718
- gain: 0,
719
- });
720
- lfoGain.gain
721
- .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
722
- .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
723
- channel.modulationEffect.lfo.connect(lfoGain);
724
- lfoGain.connect(bufferSource.detune);
764
+ const delayModLFO = startTime + instrumentKey.delayModLFO;
765
+ this.startModulation(channel, note, delayModLFO);
725
766
  }
726
- bufferSource.connect(filterNode);
727
- filterNode.connect(gainNode);
728
767
  if (this.mono && channel.currentBufferSource) {
729
768
  channel.currentBufferSource.stop(startTime);
730
- channel.currentBufferSource = bufferSource;
769
+ channel.currentBufferSource = note.bufferSource;
731
770
  }
732
- bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
733
- return { bufferSource, gainNode, filterNode, lfoGain };
771
+ note.bufferSource.connect(note.filterNode);
772
+ note.filterNode.connect(note.gainNode);
773
+ note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
774
+ return note;
734
775
  }
735
776
  calcBank(channel, channelNumber) {
736
777
  if (channel.bankMSB === 121) {
@@ -749,36 +790,20 @@ class MidyGM2 {
749
790
  return;
750
791
  const soundFont = this.soundFonts[soundFontIndex];
751
792
  const isSF3 = soundFont.parsed.info.version.major === 3;
752
- const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
753
- if (!noteInfo)
793
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
794
+ if (!instrumentKey)
754
795
  return;
755
- const { bufferSource, gainNode, filterNode, lfoGain } = await this
756
- .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
757
- this.connectNoteEffects(channel, gainNode);
796
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
797
+ this.connectNoteEffects(channel, note.gainNode);
758
798
  if (channel.sostenutoPedal) {
759
- channel.sostenutoNotes.set(noteNumber, {
760
- gainNode,
761
- filterNode,
762
- bufferSource,
763
- noteNumber,
764
- noteInfo,
765
- });
799
+ channel.sostenutoNotes.set(noteNumber, note);
766
800
  }
767
801
  const scheduledNotes = channel.scheduledNotes;
768
- const scheduledNote = {
769
- bufferSource,
770
- filterNode,
771
- gainNode,
772
- lfoGain,
773
- noteInfo,
774
- noteNumber,
775
- startTime,
776
- };
777
802
  if (scheduledNotes.has(noteNumber)) {
778
- scheduledNotes.get(noteNumber).push(scheduledNote);
803
+ scheduledNotes.get(noteNumber).push(note);
779
804
  }
780
805
  else {
781
- scheduledNotes.set(noteNumber, [scheduledNote]);
806
+ scheduledNotes.set(noteNumber, [note]);
782
807
  }
783
808
  }
784
809
  noteOn(channelNumber, noteNumber, velocity) {
@@ -800,15 +825,15 @@ class MidyGM2 {
800
825
  continue;
801
826
  if (targetNote.ending)
802
827
  continue;
803
- const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
828
+ const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
804
829
  const velocityRate = (velocity + 127) / 127;
805
- const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
830
+ const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
806
831
  gainNode.gain.cancelScheduledValues(stopTime);
807
832
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
808
833
  const maxFreq = this.audioContext.sampleRate / 2;
809
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
834
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
810
835
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
811
- const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
836
+ const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
812
837
  filterNode.frequency
813
838
  .cancelScheduledValues(stopTime)
814
839
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
@@ -822,8 +847,10 @@ class MidyGM2 {
822
847
  bufferSource.disconnect(0);
823
848
  filterNode.disconnect(0);
824
849
  gainNode.disconnect(0);
825
- if (lfoGain)
826
- lfoGain.disconnect(0);
850
+ if (modLFOGain)
851
+ modLFOGain.disconnect(0);
852
+ if (modLFO)
853
+ modLFO.stop();
827
854
  resolve();
828
855
  };
829
856
  bufferSource.stop(volEndTime);
@@ -893,7 +920,7 @@ class MidyGM2 {
893
920
  const channel = this.channels[channelNumber];
894
921
  pressure /= 64;
895
922
  channel.channelPressure = pressure;
896
- const activeNotes = this.getActiveNotes(channel);
923
+ const activeNotes = this.getActiveNotes(channel, now);
897
924
  if (channel.channelPressure.amplitudeControl !== 1) {
898
925
  activeNotes.forEach((activeNote) => {
899
926
  const gain = activeNote.gainNode.gain.value;
@@ -905,20 +932,22 @@ class MidyGM2 {
905
932
  }
906
933
  handlePitchBendMessage(channelNumber, lsb, msb) {
907
934
  const pitchBend = msb * 128 + lsb;
908
- this.handlePitchBend(channelNumber, pitchBend);
935
+ this.setPitchBend(channelNumber, pitchBend);
909
936
  }
910
- handlePitchBend(channelNumber, pitchBend) {
937
+ setPitchBend(channelNumber, pitchBend) {
911
938
  const now = this.audioContext.currentTime;
912
939
  const channel = this.channels[channelNumber];
940
+ const prevPitchBend = channel.pitchBend;
913
941
  channel.pitchBend = (pitchBend - 8192) / 8192;
914
- const semitoneOffset = this.calcSemitoneOffset(channel);
915
- const activeNotes = this.getActiveNotes(channel);
942
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
943
+ channel.pitchBendRange * 100;
944
+ const activeNotes = this.getActiveNotes(channel, now);
916
945
  activeNotes.forEach((activeNote) => {
917
- const { bufferSource, noteInfo, noteNumber } = activeNote;
918
- const playbackRate = calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
919
- bufferSource.playbackRate
946
+ const { bufferSource } = activeNote;
947
+ const detune = bufferSource.detune.value + detuneChange;
948
+ bufferSource.detune
920
949
  .cancelScheduledValues(now)
921
- .setValueAtTime(playbackRate * pressure, now);
950
+ .setValueAtTime(detune, now);
922
951
  });
923
952
  }
924
953
  handleControlChange(channelNumber, controller, value) {
@@ -979,9 +1008,20 @@ class MidyGM2 {
979
1008
  this.channels[channelNumber].bankMSB = msb;
980
1009
  }
981
1010
  setModulation(channelNumber, modulation) {
1011
+ const now = this.audioContext.currentTime;
982
1012
  const channel = this.channels[channelNumber];
983
1013
  channel.modulation = (modulation / 127) *
984
1014
  (channel.modulationDepthRange * 100);
1015
+ const activeNotes = this.getActiveNotes(channel, now);
1016
+ activeNotes.forEach((activeNote) => {
1017
+ if (activeNote.modLFO) {
1018
+ activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
1019
+ channel.modulation, now);
1020
+ }
1021
+ else {
1022
+ this.startModulation(channel, activeNote, now);
1023
+ }
1024
+ });
985
1025
  }
986
1026
  setPortamentoTime(channelNumber, portamentoTime) {
987
1027
  this.channels[channelNumber].portamentoTime = portamentoTime / 127;
@@ -991,12 +1031,17 @@ class MidyGM2 {
991
1031
  channel.volume = volume / 127;
992
1032
  this.updateChannelGain(channel);
993
1033
  }
1034
+ panToGain(pan) {
1035
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1036
+ return {
1037
+ gainLeft: Math.cos(theta),
1038
+ gainRight: Math.sin(theta),
1039
+ };
1040
+ }
994
1041
  setPan(channelNumber, pan) {
995
- const now = this.audioContext.currentTime;
996
1042
  const channel = this.channels[channelNumber];
997
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
998
- channel.pannerNode.pan.cancelScheduledValues(now);
999
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
1043
+ channel.pan = pan;
1044
+ this.updateChannelGain(channel);
1000
1045
  }
1001
1046
  setExpression(channelNumber, expression) {
1002
1047
  const channel = this.channels[channelNumber];
@@ -1009,8 +1054,13 @@ class MidyGM2 {
1009
1054
  updateChannelGain(channel) {
1010
1055
  const now = this.audioContext.currentTime;
1011
1056
  const volume = channel.volume * channel.expression;
1012
- channel.gainNode.gain.cancelScheduledValues(now);
1013
- channel.gainNode.gain.setValueAtTime(volume, now);
1057
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
1058
+ channel.gainL.gain
1059
+ .cancelScheduledValues(now)
1060
+ .setValueAtTime(volume * gainLeft, now);
1061
+ channel.gainR.gain
1062
+ .cancelScheduledValues(now)
1063
+ .setValueAtTime(volume * gainRight, now);
1014
1064
  }
1015
1065
  setSustainPedal(channelNumber, value) {
1016
1066
  const isOn = value >= 64;
@@ -1042,7 +1092,8 @@ class MidyGM2 {
1042
1092
  const channel = this.channels[channelNumber];
1043
1093
  channel.sostenutoPedal = isOn;
1044
1094
  if (isOn) {
1045
- const activeNotes = this.getActiveNotes(channel);
1095
+ const now = this.audioContext.currentTime;
1096
+ const activeNotes = this.getActiveNotes(channel, now);
1046
1097
  channel.sostenutoNotes = new Map(activeNotes);
1047
1098
  }
1048
1099
  else {
@@ -1066,8 +1117,7 @@ class MidyGM2 {
1066
1117
  const { dataMSB, dataLSB } = channel;
1067
1118
  switch (rpn) {
1068
1119
  case 0:
1069
- channel.pitchBendRange = dataMSB + dataLSB / 100;
1070
- break;
1120
+ return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
1071
1121
  case 1:
1072
1122
  channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
1073
1123
  break;
@@ -1081,14 +1131,34 @@ class MidyGM2 {
1081
1131
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1082
1132
  }
1083
1133
  }
1134
+ handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
1135
+ const pitchBendRange = dataMSB + dataLSB / 100;
1136
+ this.setPitchBendRange(channelNumber, pitchBendRange);
1137
+ }
1138
+ setPitchBendRange(channelNumber, pitchBendRange) {
1139
+ const now = this.audioContext.currentTime;
1140
+ const channel = this.channels[channelNumber];
1141
+ const prevPitchBendRange = channel.pitchBendRange;
1142
+ channel.pitchBendRange = pitchBendRange;
1143
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
1144
+ channel.pitchBend * 100;
1145
+ const activeNotes = this.getActiveNotes(channel, now);
1146
+ activeNotes.forEach((activeNote) => {
1147
+ const { bufferSource } = activeNote;
1148
+ const detune = bufferSource.detune.value + detuneChange;
1149
+ bufferSource.detune
1150
+ .cancelScheduledValues(now)
1151
+ .setValueAtTime(detune, now);
1152
+ });
1153
+ }
1084
1154
  allSoundOff(channelNumber) {
1085
1155
  const now = this.audioContext.currentTime;
1086
1156
  const channel = this.channels[channelNumber];
1087
1157
  const velocity = 0;
1088
1158
  const stopPedal = true;
1089
1159
  const promises = [];
1090
- channel.scheduledNotes.forEach((scheduledNotes) => {
1091
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
1160
+ channel.scheduledNotes.forEach((noteList) => {
1161
+ const activeNote = this.getActiveNote(noteList, now);
1092
1162
  if (activeNote) {
1093
1163
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1094
1164
  promises.push(notePromise);
@@ -1105,8 +1175,8 @@ class MidyGM2 {
1105
1175
  const velocity = 0;
1106
1176
  const stopPedal = false;
1107
1177
  const promises = [];
1108
- channel.scheduledNotes.forEach((scheduledNotes) => {
1109
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
1178
+ channel.scheduledNotes.forEach((noteList) => {
1179
+ const activeNote = this.getActiveNote(noteList, now);
1110
1180
  if (activeNote) {
1111
1181
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1112
1182
  promises.push(notePromise);
@@ -1171,9 +1241,9 @@ class MidyGM2 {
1171
1241
  case 1:
1172
1242
  return this.handleMasterVolumeSysEx(data);
1173
1243
  case 3:
1174
- return this.handleMasterFineTuning(data);
1244
+ return this.handleMasterFineTuningSysEx(data);
1175
1245
  case 4:
1176
- return this.handleMasterCoarseTuning(data);
1246
+ return this.handleMasterCoarseTuningSysEx(data);
1177
1247
  // case 5: // TODO: Global Parameter Control
1178
1248
  default:
1179
1249
  console.warn(`Unsupported Exclusive Message ${data}`);
@@ -1215,9 +1285,9 @@ class MidyGM2 {
1215
1285
  }
1216
1286
  handleMasterVolumeSysEx(data) {
1217
1287
  const volume = (data[5] * 128 + data[4]) / 16383;
1218
- this.handleMasterVolume(volume);
1288
+ this.setMasterVolume(volume);
1219
1289
  }
1220
- handleMasterVolume(volume) {
1290
+ setMasterVolume(volume) {
1221
1291
  if (volume < 0 && 1 < volume) {
1222
1292
  console.error("Master Volume is out of range");
1223
1293
  }
@@ -1229,9 +1299,9 @@ class MidyGM2 {
1229
1299
  }
1230
1300
  handleMasterFineTuningSysEx(data) {
1231
1301
  const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
1232
- this.handleMasterFineTuning(fineTuning);
1302
+ this.setMasterFineTuning(fineTuning);
1233
1303
  }
1234
- handleMasterFineTuning(fineTuning) {
1304
+ setMasterFineTuning(fineTuning) {
1235
1305
  if (fineTuning < -1 && 1 < fineTuning) {
1236
1306
  console.error("Master Fine Tuning value is out of range");
1237
1307
  }
@@ -1241,9 +1311,9 @@ class MidyGM2 {
1241
1311
  }
1242
1312
  handleMasterCoarseTuningSysEx(data) {
1243
1313
  const coarseTuning = data[4];
1244
- this.handleMasterCoarseTuning(coarseTuning);
1314
+ this.setMasterCoarseTuning(coarseTuning);
1245
1315
  }
1246
- handleMasterCoarseTuning(coarseTuning) {
1316
+ setMasterCoarseTuning(coarseTuning) {
1247
1317
  if (coarseTuning < 0 && 127 < coarseTuning) {
1248
1318
  console.error("Master Coarse Tuning value is out of range");
1249
1319
  }
@@ -1288,9 +1358,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
1288
1358
  portamentoTime: 0,
1289
1359
  reverb: 0,
1290
1360
  chorus: 0,
1291
- vibratoRate: 5,
1292
- vibratoDepth: 0.5,
1293
- vibratoDelay: 2.5,
1294
1361
  bank: 121 * 128,
1295
1362
  bankMSB: 121,
1296
1363
  bankLSB: 0,