@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,45 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGMLite = 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
+ this.noteNumber = noteNumber;
39
+ this.velocity = velocity;
40
+ this.startTime = startTime;
41
+ this.instrumentKey = instrumentKey;
42
+ }
43
+ }
6
44
  class MidyGMLite {
7
45
  constructor(audioContext) {
8
46
  Object.defineProperty(this, "ticksPerBeat", {
@@ -148,20 +186,16 @@ class MidyGMLite {
148
186
  this.totalTime = this.calcTotalTime();
149
187
  }
150
188
  setChannelAudioNodes(audioContext) {
151
- const gainNode = new GainNode(audioContext, {
152
- gain: MidyGMLite.channelSettings.volume,
153
- });
154
- const pannerNode = new StereoPannerNode(audioContext, {
155
- pan: MidyGMLite.channelSettings.pan,
156
- });
157
- const modulationEffect = this.createModulationEffect(audioContext);
158
- modulationEffect.lfo.start();
159
- pannerNode.connect(gainNode);
160
- gainNode.connect(this.masterGain);
189
+ const { gainLeft, gainRight } = this.panToGain(MidyGMLite.channelSettings.pan);
190
+ const gainL = new GainNode(audioContext, { gain: gainLeft });
191
+ const gainR = new GainNode(audioContext, { gain: gainRight });
192
+ const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
193
+ gainL.connect(merger, 0, 0);
194
+ gainR.connect(merger, 0, 1);
195
+ merger.connect(this.masterGain);
161
196
  return {
162
- gainNode,
163
- pannerNode,
164
- modulationEffect,
197
+ gainL,
198
+ gainR,
165
199
  };
166
200
  }
167
201
  createChannels(audioContext) {
@@ -171,16 +205,15 @@ class MidyGMLite {
171
205
  ...MidyGMLite.effectSettings,
172
206
  ...this.setChannelAudioNodes(audioContext),
173
207
  scheduledNotes: new Map(),
174
- sostenutoNotes: new Map(),
175
208
  };
176
209
  });
177
210
  return channels;
178
211
  }
179
- async createNoteBuffer(noteInfo, isSF3) {
180
- const sampleEnd = noteInfo.sample.length + noteInfo.end;
212
+ async createNoteBuffer(instrumentKey, isSF3) {
213
+ const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
181
214
  if (isSF3) {
182
- const sample = new Uint8Array(noteInfo.sample.length);
183
- sample.set(noteInfo.sample);
215
+ const sample = new Uint8Array(instrumentKey.sample.length);
216
+ sample.set(instrumentKey.sample);
184
217
  const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
185
218
  for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
186
219
  const channelData = audioBuffer.getChannelData(channel);
@@ -189,26 +222,27 @@ class MidyGMLite {
189
222
  return audioBuffer;
190
223
  }
191
224
  else {
192
- const sample = noteInfo.sample.subarray(0, sampleEnd);
225
+ const sample = instrumentKey.sample.subarray(0, sampleEnd);
193
226
  const floatSample = this.convertToFloat32Array(sample);
194
227
  const audioBuffer = new AudioBuffer({
195
228
  numberOfChannels: 1,
196
229
  length: sample.length,
197
- sampleRate: noteInfo.sampleRate,
230
+ sampleRate: instrumentKey.sampleRate,
198
231
  });
199
232
  const channelData = audioBuffer.getChannelData(0);
200
233
  channelData.set(floatSample);
201
234
  return audioBuffer;
202
235
  }
203
236
  }
204
- async createNoteBufferNode(noteInfo, isSF3) {
237
+ async createNoteBufferNode(instrumentKey, isSF3) {
205
238
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
206
- const audioBuffer = await this.createNoteBuffer(noteInfo, isSF3);
239
+ const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
207
240
  bufferSource.buffer = audioBuffer;
208
- bufferSource.loop = noteInfo.sampleModes % 2 !== 0;
241
+ bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
209
242
  if (bufferSource.loop) {
210
- bufferSource.loopStart = noteInfo.loopStart / noteInfo.sampleRate;
211
- bufferSource.loopEnd = noteInfo.loopEnd / noteInfo.sampleRate;
243
+ bufferSource.loopStart = instrumentKey.loopStart /
244
+ instrumentKey.sampleRate;
245
+ bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
212
246
  }
213
247
  return bufferSource;
214
248
  }
@@ -246,7 +280,7 @@ class MidyGMLite {
246
280
  this.handleProgramChange(event.channel, event.programNumber);
247
281
  break;
248
282
  case "pitchBend":
249
- this.handlePitchBend(event.channel, event.value);
283
+ this.setPitchBend(event.channel, event.value);
250
284
  break;
251
285
  case "sysEx":
252
286
  this.handleSysEx(event.data);
@@ -326,7 +360,6 @@ class MidyGMLite {
326
360
  const tmpChannels = new Array(16);
327
361
  for (let i = 0; i < tmpChannels.length; i++) {
328
362
  tmpChannels[i] = {
329
- durationTicks: new Map(),
330
363
  programNumber: -1,
331
364
  bank: this.channels[i].bank,
332
365
  };
@@ -343,16 +376,6 @@ class MidyGMLite {
343
376
  instruments.add(`${channel.bank}:0`);
344
377
  channel.programNumber = 0;
345
378
  }
346
- channel.durationTicks.set(event.noteNumber, {
347
- ticks: event.ticks,
348
- noteOn: event,
349
- });
350
- break;
351
- }
352
- case "noteOff": {
353
- const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
354
- .get(event.noteNumber);
355
- noteOn.durationTicks = event.ticks - ticks;
356
379
  break;
357
380
  }
358
381
  case "programChange": {
@@ -366,8 +389,8 @@ class MidyGMLite {
366
389
  });
367
390
  });
368
391
  const priority = {
369
- setTempo: 0,
370
- controller: 1,
392
+ controller: 0,
393
+ sysEx: 1,
371
394
  };
372
395
  timeline.sort((a, b) => {
373
396
  if (a.ticks !== b.ticks)
@@ -450,30 +473,26 @@ class MidyGMLite {
450
473
  const now = this.audioContext.currentTime;
451
474
  return this.resumeTime + now - this.startTime - this.startDelay;
452
475
  }
453
- getActiveNotes(channel) {
476
+ getActiveNotes(channel, time) {
454
477
  const activeNotes = new Map();
455
- channel.scheduledNotes.forEach((scheduledNotes) => {
456
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
478
+ channel.scheduledNotes.forEach((noteList) => {
479
+ const activeNote = this.getActiveNote(noteList, time);
457
480
  if (activeNote) {
458
481
  activeNotes.set(activeNote.noteNumber, activeNote);
459
482
  }
460
483
  });
461
484
  return activeNotes;
462
485
  }
463
- getActiveChannelNotes(scheduledNotes) {
464
- for (let i = 0; i < scheduledNotes; i++) {
465
- const scheduledNote = scheduledNotes[i];
466
- if (scheduledNote)
467
- return scheduledNote;
486
+ getActiveNote(noteList, time) {
487
+ for (let i = noteList.length - 1; i >= 0; i--) {
488
+ const note = noteList[i];
489
+ if (!note)
490
+ return;
491
+ if (time < note.startTime)
492
+ continue;
493
+ return (note.ending) ? null : note;
468
494
  }
469
- }
470
- createModulationEffect(audioContext) {
471
- const lfo = new OscillatorNode(audioContext, {
472
- frequency: 5,
473
- });
474
- return {
475
- lfo,
476
- };
495
+ return noteList[0];
477
496
  }
478
497
  connectNoteEffects(channel, gainNode) {
479
498
  gainNode.connect(channel.pannerNode);
@@ -485,73 +504,89 @@ class MidyGMLite {
485
504
  return 8.176 * Math.pow(2, cent / 1200);
486
505
  }
487
506
  calcSemitoneOffset(channel) {
488
- return channel.pitchBend * channel.pitchBendRange + tuning;
507
+ return channel.pitchBend * channel.pitchBendRange;
489
508
  }
490
- calcPlaybackRate(noteInfo, noteNumber, semitoneOffset) {
491
- return noteInfo.playbackRate(noteNumber) * Math.pow(2, semitoneOffset / 12);
509
+ calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
510
+ return instrumentKey.playbackRate(noteNumber) *
511
+ Math.pow(2, semitoneOffset / 12);
492
512
  }
493
- async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
494
- const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
495
- const semitoneOffset = this.calcSemitoneOffset(channel);
496
- bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
497
- // volume envelope
498
- const gainNode = new GainNode(this.audioContext, {
499
- gain: 0,
500
- });
513
+ setVolumeEnvelope(channel, note) {
514
+ const { instrumentKey, startTime, velocity } = note;
515
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
501
516
  let volume = (velocity / 127) * channel.volume * channel.expression;
502
517
  if (volume === 0)
503
518
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
504
- const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
505
- const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
506
- const volDelay = startTime + noteInfo.volDelay;
507
- const volAttack = volDelay + noteInfo.volAttack;
508
- const volHold = volAttack + noteInfo.volHold;
509
- const volDecay = volHold + noteInfo.volDecay;
510
- gainNode.gain
519
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
520
+ volume;
521
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
522
+ const volDelay = startTime + instrumentKey.volDelay;
523
+ const volAttack = volDelay + instrumentKey.volAttack;
524
+ const volHold = volAttack + instrumentKey.volHold;
525
+ const volDecay = volHold + instrumentKey.volDecay;
526
+ note.gainNode.gain
511
527
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
512
528
  .exponentialRampToValueAtTime(attackVolume, volAttack)
513
529
  .setValueAtTime(attackVolume, volHold)
514
530
  .linearRampToValueAtTime(sustainVolume, volDecay);
515
- // filter envelope
531
+ }
532
+ setFilterEnvelope(channel, note) {
533
+ const { instrumentKey, startTime, noteNumber } = note;
534
+ const softPedalFactor = 1 -
535
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
516
536
  const maxFreq = this.audioContext.sampleRate / 2;
517
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
518
- const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc);
519
- const sustainFreq = baseFreq +
520
- (peekFreq - baseFreq) * (1 - noteInfo.modSustain);
537
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
538
+ softPedalFactor;
539
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
540
+ const sustainFreq = (baseFreq +
541
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
542
+ const modDelay = startTime + instrumentKey.modDelay;
543
+ const modAttack = modDelay + instrumentKey.modAttack;
544
+ const modHold = modAttack + instrumentKey.modHold;
545
+ const modDecay = modHold + instrumentKey.modDecay;
521
546
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
522
547
  const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
523
548
  const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
524
- const filterNode = new BiquadFilterNode(this.audioContext, {
549
+ note.filterNode = new BiquadFilterNode(this.audioContext, {
525
550
  type: "lowpass",
526
- Q: noteInfo.initialFilterQ / 10, // dB
551
+ Q: instrumentKey.initialFilterQ / 10, // dB
527
552
  frequency: adjustedBaseFreq,
528
553
  });
529
- const modDelay = startTime + noteInfo.modDelay;
530
- const modAttack = modDelay + noteInfo.modAttack;
531
- const modHold = modAttack + noteInfo.modHold;
532
- const modDecay = modHold + noteInfo.modDecay;
533
- filterNode.frequency
554
+ note.filterNode.frequency
534
555
  .setValueAtTime(adjustedBaseFreq, modDelay)
535
556
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
536
557
  .setValueAtTime(adjustedPeekFreq, modHold)
537
558
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
538
- let lfoGain;
559
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
560
+ }
561
+ startModulation(channel, note, time) {
562
+ const { instrumentKey } = note;
563
+ note.modLFOGain = new GainNode(this.audioContext, {
564
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
565
+ });
566
+ note.modLFO = new OscillatorNode(this.audioContext, {
567
+ frequency: this.centToHz(instrumentKey.freqModLFO),
568
+ });
569
+ note.modLFO.start(time);
570
+ note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
571
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
572
+ note.modLFO.connect(note.modLFOGain);
573
+ note.modLFOGain.connect(note.bufferSource.detune);
574
+ }
575
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
576
+ const semitoneOffset = this.calcSemitoneOffset(channel);
577
+ const note = new Note(noteNumber, velocity, startTime, instrumentKey);
578
+ note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
579
+ note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
580
+ this.setVolumeEnvelope(channel, note);
581
+ this.setFilterEnvelope(channel, note);
539
582
  if (channel.modulation > 0) {
540
- const vibratoDelay = startTime + channel.vibratoDelay;
541
- const vibratoAttack = vibratoDelay + 0.1;
542
- lfoGain = new GainNode(this.audioContext, {
543
- gain: 0,
544
- });
545
- lfoGain.gain
546
- .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
547
- .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
548
- channel.modulationEffect.lfo.connect(lfoGain);
549
- lfoGain.connect(bufferSource.detune);
583
+ const delayModLFO = startTime + instrumentKey.delayModLFO;
584
+ this.startModulation(channel, note, delayModLFO);
550
585
  }
551
- bufferSource.connect(filterNode);
552
- filterNode.connect(gainNode);
553
- bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
554
- return { bufferSource, gainNode, filterNode, lfoGain };
586
+ note.bufferSource.connect(note.filterNode);
587
+ note.filterNode.connect(note.gainNode);
588
+ note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
589
+ return note;
555
590
  }
556
591
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
557
592
  const channel = this.channels[channelNumber];
@@ -561,27 +596,17 @@ class MidyGMLite {
561
596
  return;
562
597
  const soundFont = this.soundFonts[soundFontIndex];
563
598
  const isSF3 = soundFont.parsed.info.version.major === 3;
564
- const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
565
- if (!noteInfo)
599
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
600
+ if (!instrumentKey)
566
601
  return;
567
- const { bufferSource, gainNode, filterNode, lfoGain } = await this
568
- .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
569
- this.connectNoteEffects(channel, gainNode);
602
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
603
+ this.connectNoteEffects(channel, note.gainNode);
570
604
  const scheduledNotes = channel.scheduledNotes;
571
- const scheduledNote = {
572
- bufferSource,
573
- filterNode,
574
- gainNode,
575
- lfoGain,
576
- noteInfo,
577
- noteNumber,
578
- startTime,
579
- };
580
605
  if (scheduledNotes.has(noteNumber)) {
581
- scheduledNotes.get(noteNumber).push(scheduledNote);
606
+ scheduledNotes.get(noteNumber).push(note);
582
607
  }
583
608
  else {
584
- scheduledNotes.set(noteNumber, [scheduledNote]);
609
+ scheduledNotes.set(noteNumber, [note]);
585
610
  }
586
611
  }
587
612
  noteOn(channelNumber, noteNumber, velocity) {
@@ -601,15 +626,15 @@ class MidyGMLite {
601
626
  continue;
602
627
  if (targetNote.ending)
603
628
  continue;
604
- const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
629
+ const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
605
630
  const velocityRate = (velocity + 127) / 127;
606
- const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
631
+ const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
607
632
  gainNode.gain.cancelScheduledValues(stopTime);
608
633
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
609
634
  const maxFreq = this.audioContext.sampleRate / 2;
610
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
635
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
611
636
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
612
- const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
637
+ const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
613
638
  filterNode.frequency
614
639
  .cancelScheduledValues(stopTime)
615
640
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
@@ -623,8 +648,10 @@ class MidyGMLite {
623
648
  bufferSource.disconnect(0);
624
649
  filterNode.disconnect(0);
625
650
  gainNode.disconnect(0);
626
- if (lfoGain)
627
- lfoGain.disconnect(0);
651
+ if (modLFOGain)
652
+ modLFOGain.disconnect(0);
653
+ if (modLFO)
654
+ modLFO.stop();
628
655
  resolve();
629
656
  };
630
657
  bufferSource.stop(volEndTime);
@@ -675,20 +702,22 @@ class MidyGMLite {
675
702
  }
676
703
  handlePitchBendMessage(channelNumber, lsb, msb) {
677
704
  const pitchBend = msb * 128 + lsb;
678
- this.handlePitchBend(channelNumber, pitchBend);
705
+ this.setPitchBend(channelNumber, pitchBend);
679
706
  }
680
- handlePitchBend(channelNumber, pitchBend) {
707
+ setPitchBend(channelNumber, pitchBend) {
681
708
  const now = this.audioContext.currentTime;
682
709
  const channel = this.channels[channelNumber];
710
+ const prevPitchBend = channel.pitchBend;
683
711
  channel.pitchBend = (pitchBend - 8192) / 8192;
684
- const semitoneOffset = this.calcSemitoneOffset(channel);
685
- const activeNotes = this.getActiveNotes(channel);
712
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
713
+ channel.pitchBendRange * 100;
714
+ const activeNotes = this.getActiveNotes(channel, now);
686
715
  activeNotes.forEach((activeNote) => {
687
- const { bufferSource, noteInfo, noteNumber } = activeNote;
688
- const playbackRate = calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
689
- bufferSource.playbackRate
716
+ const { bufferSource } = activeNote;
717
+ const detune = bufferSource.detune.value + detuneChange;
718
+ bufferSource.detune
690
719
  .cancelScheduledValues(now)
691
- .setValueAtTime(playbackRate * pressure, now);
720
+ .setValueAtTime(detune, now);
692
721
  });
693
722
  }
694
723
  handleControlChange(channelNumber, controller, value) {
@@ -722,21 +751,37 @@ class MidyGMLite {
722
751
  }
723
752
  }
724
753
  setModulation(channelNumber, modulation) {
754
+ const now = this.audioContext.currentTime;
725
755
  const channel = this.channels[channelNumber];
726
756
  channel.modulation = (modulation / 127) *
727
757
  (channel.modulationDepthRange * 100);
758
+ const activeNotes = this.getActiveNotes(channel, now);
759
+ activeNotes.forEach((activeNote) => {
760
+ if (activeNote.modLFO) {
761
+ activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
762
+ channel.modulation, now);
763
+ }
764
+ else {
765
+ this.startModulation(channel, activeNote, now);
766
+ }
767
+ });
728
768
  }
729
769
  setVolume(channelNumber, volume) {
730
770
  const channel = this.channels[channelNumber];
731
771
  channel.volume = volume / 127;
732
772
  this.updateChannelGain(channel);
733
773
  }
774
+ panToGain(pan) {
775
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
776
+ return {
777
+ gainLeft: Math.cos(theta),
778
+ gainRight: Math.sin(theta),
779
+ };
780
+ }
734
781
  setPan(channelNumber, pan) {
735
- const now = this.audioContext.currentTime;
736
782
  const channel = this.channels[channelNumber];
737
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
738
- channel.pannerNode.pan.cancelScheduledValues(now);
739
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
783
+ channel.pan = pan;
784
+ this.updateChannelGain(channel);
740
785
  }
741
786
  setExpression(channelNumber, expression) {
742
787
  const channel = this.channels[channelNumber];
@@ -746,8 +791,13 @@ class MidyGMLite {
746
791
  updateChannelGain(channel) {
747
792
  const now = this.audioContext.currentTime;
748
793
  const volume = channel.volume * channel.expression;
749
- channel.gainNode.gain.cancelScheduledValues(now);
750
- channel.gainNode.gain.setValueAtTime(volume, now);
794
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
795
+ channel.gainL.gain
796
+ .cancelScheduledValues(now)
797
+ .setValueAtTime(volume * gainLeft, now);
798
+ channel.gainR.gain
799
+ .cancelScheduledValues(now)
800
+ .setValueAtTime(volume * gainRight, now);
751
801
  }
752
802
  setSustainPedal(channelNumber, value) {
753
803
  const isOn = value >= 64;
@@ -769,20 +819,39 @@ class MidyGMLite {
769
819
  const { dataMSB, dataLSB } = channel;
770
820
  switch (rpn) {
771
821
  case 0:
772
- channel.pitchBendRange = dataMSB + dataLSB / 100;
773
- break;
822
+ return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
774
823
  default:
775
824
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
776
825
  }
777
826
  }
827
+ handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
828
+ const pitchBendRange = dataMSB + dataLSB / 100;
829
+ this.setPitchBendRange(channelNumber, pitchBendRange);
830
+ }
831
+ setPitchBendRange(channelNumber, pitchBendRange) {
832
+ const now = this.audioContext.currentTime;
833
+ const channel = this.channels[channelNumber];
834
+ const prevPitchBendRange = channel.pitchBendRange;
835
+ channel.pitchBendRange = pitchBendRange;
836
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
837
+ channel.pitchBend * 100;
838
+ const activeNotes = this.getActiveNotes(channel, now);
839
+ activeNotes.forEach((activeNote) => {
840
+ const { bufferSource } = activeNote;
841
+ const detune = bufferSource.detune.value + detuneChange;
842
+ bufferSource.detune
843
+ .cancelScheduledValues(now)
844
+ .setValueAtTime(detune, now);
845
+ });
846
+ }
778
847
  allSoundOff(channelNumber) {
779
848
  const now = this.audioContext.currentTime;
780
849
  const channel = this.channels[channelNumber];
781
850
  const velocity = 0;
782
851
  const stopPedal = true;
783
852
  const promises = [];
784
- channel.scheduledNotes.forEach((scheduledNotes) => {
785
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
853
+ channel.scheduledNotes.forEach((noteList) => {
854
+ const activeNote = this.getActiveNote(noteList, now);
786
855
  if (activeNote) {
787
856
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
788
857
  promises.push(notePromise);
@@ -799,8 +868,8 @@ class MidyGMLite {
799
868
  const velocity = 0;
800
869
  const stopPedal = false;
801
870
  const promises = [];
802
- channel.scheduledNotes.forEach((scheduledNotes) => {
803
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
871
+ channel.scheduledNotes.forEach((noteList) => {
872
+ const activeNote = this.getActiveNote(noteList, now);
804
873
  if (activeNote) {
805
874
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
806
875
  promises.push(notePromise);
@@ -850,9 +919,9 @@ class MidyGMLite {
850
919
  }
851
920
  handleMasterVolumeSysEx(data) {
852
921
  const volume = (data[5] * 128 + data[4]) / 16383;
853
- this.handleMasterVolume(volume);
922
+ this.setMasterVolume(volume);
854
923
  }
855
- handleMasterVolume(volume) {
924
+ setMasterVolume(volume) {
856
925
  if (volume < 0 && 1 < volume) {
857
926
  console.error("Master Volume is out of range");
858
927
  }
@@ -894,10 +963,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
894
963
  writable: true,
895
964
  value: {
896
965
  volume: 100 / 127,
897
- pan: 0,
898
- vibratoRate: 5,
899
- vibratoDepth: 0.5,
900
- vibratoDelay: 2.5,
966
+ pan: 64,
901
967
  bank: 0,
902
968
  dataMSB: 0,
903
969
  dataLSB: 0,