@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.MidyGM1 = 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 MidyGM1 {
7
45
  constructor(audioContext) {
8
46
  Object.defineProperty(this, "ticksPerBeat", {
@@ -148,20 +186,16 @@ class MidyGM1 {
148
186
  this.totalTime = this.calcTotalTime();
149
187
  }
150
188
  setChannelAudioNodes(audioContext) {
151
- const gainNode = new GainNode(audioContext, {
152
- gain: MidyGM1.channelSettings.volume,
153
- });
154
- const pannerNode = new StereoPannerNode(audioContext, {
155
- pan: MidyGM1.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(MidyGM1.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 MidyGM1 {
171
205
  ...MidyGM1.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 MidyGM1 {
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 MidyGM1 {
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 MidyGM1 {
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 MidyGM1 {
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 MidyGM1 {
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 MidyGM1 {
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);
@@ -488,71 +507,87 @@ class MidyGM1 {
488
507
  const tuning = channel.coarseTuning + channel.fineTuning;
489
508
  return channel.pitchBend * channel.pitchBendRange + tuning;
490
509
  }
491
- calcPlaybackRate(noteInfo, noteNumber, semitoneOffset) {
492
- return noteInfo.playbackRate(noteNumber) * Math.pow(2, semitoneOffset / 12);
510
+ calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
511
+ return instrumentKey.playbackRate(noteNumber) *
512
+ Math.pow(2, semitoneOffset / 12);
493
513
  }
494
- async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
495
- const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
496
- const semitoneOffset = this.calcSemitoneOffset(channel);
497
- bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
498
- // volume envelope
499
- const gainNode = new GainNode(this.audioContext, {
500
- gain: 0,
501
- });
514
+ setVolumeEnvelope(channel, note) {
515
+ const { instrumentKey, startTime, velocity } = note;
516
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
502
517
  let volume = (velocity / 127) * channel.volume * channel.expression;
503
518
  if (volume === 0)
504
519
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
505
- const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
506
- const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
507
- const volDelay = startTime + noteInfo.volDelay;
508
- const volAttack = volDelay + noteInfo.volAttack;
509
- const volHold = volAttack + noteInfo.volHold;
510
- const volDecay = volHold + noteInfo.volDecay;
511
- gainNode.gain
520
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
521
+ volume;
522
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
523
+ const volDelay = startTime + instrumentKey.volDelay;
524
+ const volAttack = volDelay + instrumentKey.volAttack;
525
+ const volHold = volAttack + instrumentKey.volHold;
526
+ const volDecay = volHold + instrumentKey.volDecay;
527
+ note.gainNode.gain
512
528
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
513
529
  .exponentialRampToValueAtTime(attackVolume, volAttack)
514
530
  .setValueAtTime(attackVolume, volHold)
515
531
  .linearRampToValueAtTime(sustainVolume, volDecay);
516
- // filter envelope
532
+ }
533
+ setFilterEnvelope(channel, note) {
534
+ const { instrumentKey, startTime, noteNumber } = note;
535
+ const softPedalFactor = 1 -
536
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
517
537
  const maxFreq = this.audioContext.sampleRate / 2;
518
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
519
- const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc);
520
- const sustainFreq = baseFreq +
521
- (peekFreq - baseFreq) * (1 - noteInfo.modSustain);
538
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
539
+ softPedalFactor;
540
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
541
+ const sustainFreq = (baseFreq +
542
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
543
+ const modDelay = startTime + instrumentKey.modDelay;
544
+ const modAttack = modDelay + instrumentKey.modAttack;
545
+ const modHold = modAttack + instrumentKey.modHold;
546
+ const modDecay = modHold + instrumentKey.modDecay;
522
547
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
523
548
  const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
524
549
  const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
525
- const filterNode = new BiquadFilterNode(this.audioContext, {
550
+ note.filterNode = new BiquadFilterNode(this.audioContext, {
526
551
  type: "lowpass",
527
- Q: noteInfo.initialFilterQ / 10, // dB
552
+ Q: instrumentKey.initialFilterQ / 10, // dB
528
553
  frequency: adjustedBaseFreq,
529
554
  });
530
- const modDelay = startTime + noteInfo.modDelay;
531
- const modAttack = modDelay + noteInfo.modAttack;
532
- const modHold = modAttack + noteInfo.modHold;
533
- const modDecay = modHold + noteInfo.modDecay;
534
- filterNode.frequency
555
+ note.filterNode.frequency
535
556
  .setValueAtTime(adjustedBaseFreq, modDelay)
536
557
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
537
558
  .setValueAtTime(adjustedPeekFreq, modHold)
538
559
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
539
- let lfoGain;
560
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
561
+ }
562
+ startModulation(channel, note, time) {
563
+ const { instrumentKey } = note;
564
+ note.modLFOGain = new GainNode(this.audioContext, {
565
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
566
+ });
567
+ note.modLFO = new OscillatorNode(this.audioContext, {
568
+ frequency: this.centToHz(instrumentKey.freqModLFO),
569
+ });
570
+ note.modLFO.start(time);
571
+ note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
572
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
573
+ note.modLFO.connect(note.modLFOGain);
574
+ note.modLFOGain.connect(note.bufferSource.detune);
575
+ }
576
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
577
+ const semitoneOffset = this.calcSemitoneOffset(channel);
578
+ const note = new Note(noteNumber, velocity, startTime, instrumentKey);
579
+ note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
580
+ note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
581
+ this.setVolumeEnvelope(channel, note);
582
+ this.setFilterEnvelope(channel, note);
540
583
  if (channel.modulation > 0) {
541
- const vibratoDelay = startTime + channel.vibratoDelay;
542
- const vibratoAttack = vibratoDelay + 0.1;
543
- lfoGain = new GainNode(this.audioContext, {
544
- gain: 0,
545
- });
546
- lfoGain.gain
547
- .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
548
- .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
549
- channel.modulationEffect.lfo.connect(lfoGain);
550
- lfoGain.connect(bufferSource.detune);
584
+ const delayModLFO = startTime + instrumentKey.delayModLFO;
585
+ this.startModulation(channel, note, delayModLFO);
551
586
  }
552
- bufferSource.connect(filterNode);
553
- filterNode.connect(gainNode);
554
- bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
555
- return { bufferSource, gainNode, filterNode, lfoGain };
587
+ note.bufferSource.connect(note.filterNode);
588
+ note.filterNode.connect(note.gainNode);
589
+ note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
590
+ return note;
556
591
  }
557
592
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
558
593
  const channel = this.channels[channelNumber];
@@ -562,27 +597,17 @@ class MidyGM1 {
562
597
  return;
563
598
  const soundFont = this.soundFonts[soundFontIndex];
564
599
  const isSF3 = soundFont.parsed.info.version.major === 3;
565
- const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
566
- if (!noteInfo)
600
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
601
+ if (!instrumentKey)
567
602
  return;
568
- const { bufferSource, gainNode, filterNode, lfoGain } = await this
569
- .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
570
- this.connectNoteEffects(channel, gainNode);
603
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
604
+ this.connectNoteEffects(channel, note.gainNode);
571
605
  const scheduledNotes = channel.scheduledNotes;
572
- const scheduledNote = {
573
- bufferSource,
574
- filterNode,
575
- gainNode,
576
- lfoGain,
577
- noteInfo,
578
- noteNumber,
579
- startTime,
580
- };
581
606
  if (scheduledNotes.has(noteNumber)) {
582
- scheduledNotes.get(noteNumber).push(scheduledNote);
607
+ scheduledNotes.get(noteNumber).push(note);
583
608
  }
584
609
  else {
585
- scheduledNotes.set(noteNumber, [scheduledNote]);
610
+ scheduledNotes.set(noteNumber, [note]);
586
611
  }
587
612
  }
588
613
  noteOn(channelNumber, noteNumber, velocity) {
@@ -602,15 +627,15 @@ class MidyGM1 {
602
627
  continue;
603
628
  if (targetNote.ending)
604
629
  continue;
605
- const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
630
+ const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
606
631
  const velocityRate = (velocity + 127) / 127;
607
- const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
632
+ const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
608
633
  gainNode.gain.cancelScheduledValues(stopTime);
609
634
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
610
635
  const maxFreq = this.audioContext.sampleRate / 2;
611
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
636
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
612
637
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
613
- const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
638
+ const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
614
639
  filterNode.frequency
615
640
  .cancelScheduledValues(stopTime)
616
641
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
@@ -624,8 +649,10 @@ class MidyGM1 {
624
649
  bufferSource.disconnect(0);
625
650
  filterNode.disconnect(0);
626
651
  gainNode.disconnect(0);
627
- if (lfoGain)
628
- lfoGain.disconnect(0);
652
+ if (modLFOGain)
653
+ modLFOGain.disconnect(0);
654
+ if (modLFO)
655
+ modLFO.stop();
629
656
  resolve();
630
657
  };
631
658
  bufferSource.stop(volEndTime);
@@ -676,20 +703,22 @@ class MidyGM1 {
676
703
  }
677
704
  handlePitchBendMessage(channelNumber, lsb, msb) {
678
705
  const pitchBend = msb * 128 + lsb;
679
- this.handlePitchBend(channelNumber, pitchBend);
706
+ this.setPitchBend(channelNumber, pitchBend);
680
707
  }
681
- handlePitchBend(channelNumber, pitchBend) {
708
+ setPitchBend(channelNumber, pitchBend) {
682
709
  const now = this.audioContext.currentTime;
683
710
  const channel = this.channels[channelNumber];
711
+ const prevPitchBend = channel.pitchBend;
684
712
  channel.pitchBend = (pitchBend - 8192) / 8192;
685
- const semitoneOffset = this.calcSemitoneOffset(channel);
686
- const activeNotes = this.getActiveNotes(channel);
713
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
714
+ channel.pitchBendRange * 100;
715
+ const activeNotes = this.getActiveNotes(channel, now);
687
716
  activeNotes.forEach((activeNote) => {
688
- const { bufferSource, noteInfo, noteNumber } = activeNote;
689
- const playbackRate = calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
690
- bufferSource.playbackRate
717
+ const { bufferSource } = activeNote;
718
+ const detune = bufferSource.detune.value + detuneChange;
719
+ bufferSource.detune
691
720
  .cancelScheduledValues(now)
692
- .setValueAtTime(playbackRate * pressure, now);
721
+ .setValueAtTime(detune, now);
693
722
  });
694
723
  }
695
724
  handleControlChange(channelNumber, controller, value) {
@@ -723,21 +752,37 @@ class MidyGM1 {
723
752
  }
724
753
  }
725
754
  setModulation(channelNumber, modulation) {
755
+ const now = this.audioContext.currentTime;
726
756
  const channel = this.channels[channelNumber];
727
757
  channel.modulation = (modulation / 127) *
728
758
  (channel.modulationDepthRange * 100);
759
+ const activeNotes = this.getActiveNotes(channel, now);
760
+ activeNotes.forEach((activeNote) => {
761
+ if (activeNote.modLFO) {
762
+ activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
763
+ channel.modulation, now);
764
+ }
765
+ else {
766
+ this.startModulation(channel, activeNote, now);
767
+ }
768
+ });
729
769
  }
730
770
  setVolume(channelNumber, volume) {
731
771
  const channel = this.channels[channelNumber];
732
772
  channel.volume = volume / 127;
733
773
  this.updateChannelGain(channel);
734
774
  }
775
+ panToGain(pan) {
776
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
777
+ return {
778
+ gainLeft: Math.cos(theta),
779
+ gainRight: Math.sin(theta),
780
+ };
781
+ }
735
782
  setPan(channelNumber, pan) {
736
- const now = this.audioContext.currentTime;
737
783
  const channel = this.channels[channelNumber];
738
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
739
- channel.pannerNode.pan.cancelScheduledValues(now);
740
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
784
+ channel.pan = pan;
785
+ this.updateChannelGain(channel);
741
786
  }
742
787
  setExpression(channelNumber, expression) {
743
788
  const channel = this.channels[channelNumber];
@@ -747,8 +792,13 @@ class MidyGM1 {
747
792
  updateChannelGain(channel) {
748
793
  const now = this.audioContext.currentTime;
749
794
  const volume = channel.volume * channel.expression;
750
- channel.gainNode.gain.cancelScheduledValues(now);
751
- channel.gainNode.gain.setValueAtTime(volume, now);
795
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
796
+ channel.gainL.gain
797
+ .cancelScheduledValues(now)
798
+ .setValueAtTime(volume * gainLeft, now);
799
+ channel.gainR.gain
800
+ .cancelScheduledValues(now)
801
+ .setValueAtTime(volume * gainRight, now);
752
802
  }
753
803
  setSustainPedal(channelNumber, value) {
754
804
  const isOn = value >= 64;
@@ -770,8 +820,7 @@ class MidyGM1 {
770
820
  const { dataMSB, dataLSB } = channel;
771
821
  switch (rpn) {
772
822
  case 0:
773
- channel.pitchBendRange = dataMSB + dataLSB / 100;
774
- break;
823
+ return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
775
824
  case 1:
776
825
  channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
777
826
  break;
@@ -782,14 +831,34 @@ class MidyGM1 {
782
831
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
783
832
  }
784
833
  }
834
+ handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
835
+ const pitchBendRange = dataMSB + dataLSB / 100;
836
+ this.setPitchBendRange(channelNumber, pitchBendRange);
837
+ }
838
+ setPitchBendRange(channelNumber, pitchBendRange) {
839
+ const now = this.audioContext.currentTime;
840
+ const channel = this.channels[channelNumber];
841
+ const prevPitchBendRange = channel.pitchBendRange;
842
+ channel.pitchBendRange = pitchBendRange;
843
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
844
+ channel.pitchBend * 100;
845
+ const activeNotes = this.getActiveNotes(channel, now);
846
+ activeNotes.forEach((activeNote) => {
847
+ const { bufferSource } = activeNote;
848
+ const detune = bufferSource.detune.value + detuneChange;
849
+ bufferSource.detune
850
+ .cancelScheduledValues(now)
851
+ .setValueAtTime(detune, now);
852
+ });
853
+ }
785
854
  allSoundOff(channelNumber) {
786
855
  const now = this.audioContext.currentTime;
787
856
  const channel = this.channels[channelNumber];
788
857
  const velocity = 0;
789
858
  const stopPedal = true;
790
859
  const promises = [];
791
- channel.scheduledNotes.forEach((scheduledNotes) => {
792
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
860
+ channel.scheduledNotes.forEach((noteList) => {
861
+ const activeNote = this.getActiveNote(noteList, now);
793
862
  if (activeNote) {
794
863
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
795
864
  promises.push(notePromise);
@@ -806,8 +875,8 @@ class MidyGM1 {
806
875
  const velocity = 0;
807
876
  const stopPedal = false;
808
877
  const promises = [];
809
- channel.scheduledNotes.forEach((scheduledNotes) => {
810
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
878
+ channel.scheduledNotes.forEach((noteList) => {
879
+ const activeNote = this.getActiveNote(noteList, now);
811
880
  if (activeNote) {
812
881
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
813
882
  promises.push(notePromise);
@@ -857,9 +926,9 @@ class MidyGM1 {
857
926
  }
858
927
  handleMasterVolumeSysEx(data) {
859
928
  const volume = (data[5] * 128 + data[4]) / 16383;
860
- this.handleMasterVolume(volume);
929
+ this.setMasterVolume(volume);
861
930
  }
862
- handleMasterVolume(volume) {
931
+ setMasterVolume(volume) {
863
932
  if (volume < 0 && 1 < volume) {
864
933
  console.error("Master Volume is out of range");
865
934
  }
@@ -901,10 +970,7 @@ Object.defineProperty(MidyGM1, "channelSettings", {
901
970
  writable: true,
902
971
  value: {
903
972
  volume: 100 / 127,
904
- pan: 0,
905
- vibratoRate: 5,
906
- vibratoDepth: 0.5,
907
- vibratoDelay: 2.5,
973
+ pan: 64,
908
974
  bank: 0,
909
975
  dataMSB: 0,
910
976
  dataLSB: 0,