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