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