@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
@@ -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 MidyGMLite {
4
42
  constructor(audioContext) {
5
43
  Object.defineProperty(this, "ticksPerBeat", {
@@ -145,20 +183,16 @@ export class MidyGMLite {
145
183
  this.totalTime = this.calcTotalTime();
146
184
  }
147
185
  setChannelAudioNodes(audioContext) {
148
- const gainNode = new GainNode(audioContext, {
149
- gain: MidyGMLite.channelSettings.volume,
150
- });
151
- const pannerNode = new StereoPannerNode(audioContext, {
152
- pan: MidyGMLite.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(MidyGMLite.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 MidyGMLite {
168
202
  ...MidyGMLite.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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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 MidyGMLite {
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);
@@ -482,73 +501,89 @@ export class MidyGMLite {
482
501
  return 8.176 * Math.pow(2, cent / 1200);
483
502
  }
484
503
  calcSemitoneOffset(channel) {
485
- return channel.pitchBend * channel.pitchBendRange + tuning;
504
+ return channel.pitchBend * channel.pitchBendRange;
486
505
  }
487
- calcPlaybackRate(noteInfo, noteNumber, semitoneOffset) {
488
- return noteInfo.playbackRate(noteNumber) * Math.pow(2, semitoneOffset / 12);
506
+ calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
507
+ return instrumentKey.playbackRate(noteNumber) *
508
+ Math.pow(2, semitoneOffset / 12);
489
509
  }
490
- async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
491
- const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
492
- const semitoneOffset = this.calcSemitoneOffset(channel);
493
- bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
494
- // volume envelope
495
- const gainNode = new GainNode(this.audioContext, {
496
- gain: 0,
497
- });
510
+ setVolumeEnvelope(channel, note) {
511
+ const { instrumentKey, startTime, velocity } = note;
512
+ note.gainNode = new GainNode(this.audioContext, { gain: 0 });
498
513
  let volume = (velocity / 127) * channel.volume * channel.expression;
499
514
  if (volume === 0)
500
515
  volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
501
- const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
502
- const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
503
- const volDelay = startTime + noteInfo.volDelay;
504
- const volAttack = volDelay + noteInfo.volAttack;
505
- const volHold = volAttack + noteInfo.volHold;
506
- const volDecay = volHold + noteInfo.volDecay;
507
- gainNode.gain
516
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
517
+ volume;
518
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
519
+ const volDelay = startTime + instrumentKey.volDelay;
520
+ const volAttack = volDelay + instrumentKey.volAttack;
521
+ const volHold = volAttack + instrumentKey.volHold;
522
+ const volDecay = volHold + instrumentKey.volDecay;
523
+ note.gainNode.gain
508
524
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
509
525
  .exponentialRampToValueAtTime(attackVolume, volAttack)
510
526
  .setValueAtTime(attackVolume, volHold)
511
527
  .linearRampToValueAtTime(sustainVolume, volDecay);
512
- // filter envelope
528
+ }
529
+ setFilterEnvelope(channel, note) {
530
+ const { instrumentKey, startTime, noteNumber } = note;
531
+ const softPedalFactor = 1 -
532
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
513
533
  const maxFreq = this.audioContext.sampleRate / 2;
514
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
515
- const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc);
516
- const sustainFreq = baseFreq +
517
- (peekFreq - baseFreq) * (1 - noteInfo.modSustain);
534
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
535
+ softPedalFactor;
536
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
537
+ const sustainFreq = (baseFreq +
538
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
539
+ const modDelay = startTime + instrumentKey.modDelay;
540
+ const modAttack = modDelay + instrumentKey.modAttack;
541
+ const modHold = modAttack + instrumentKey.modHold;
542
+ const modDecay = modHold + instrumentKey.modDecay;
518
543
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
519
544
  const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
520
545
  const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
521
- const filterNode = new BiquadFilterNode(this.audioContext, {
546
+ note.filterNode = new BiquadFilterNode(this.audioContext, {
522
547
  type: "lowpass",
523
- Q: noteInfo.initialFilterQ / 10, // dB
548
+ Q: instrumentKey.initialFilterQ / 10, // dB
524
549
  frequency: adjustedBaseFreq,
525
550
  });
526
- const modDelay = startTime + noteInfo.modDelay;
527
- const modAttack = modDelay + noteInfo.modAttack;
528
- const modHold = modAttack + noteInfo.modHold;
529
- const modDecay = modHold + noteInfo.modDecay;
530
- filterNode.frequency
551
+ note.filterNode.frequency
531
552
  .setValueAtTime(adjustedBaseFreq, modDelay)
532
553
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
533
554
  .setValueAtTime(adjustedPeekFreq, modHold)
534
555
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
535
- let lfoGain;
556
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
557
+ }
558
+ startModulation(channel, note, time) {
559
+ const { instrumentKey } = note;
560
+ note.modLFOGain = new GainNode(this.audioContext, {
561
+ gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
562
+ });
563
+ note.modLFO = new OscillatorNode(this.audioContext, {
564
+ frequency: this.centToHz(instrumentKey.freqModLFO),
565
+ });
566
+ note.modLFO.start(time);
567
+ note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
568
+ note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
569
+ note.modLFO.connect(note.modLFOGain);
570
+ note.modLFOGain.connect(note.bufferSource.detune);
571
+ }
572
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
573
+ const semitoneOffset = this.calcSemitoneOffset(channel);
574
+ const note = new Note(noteNumber, velocity, startTime, instrumentKey);
575
+ note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
576
+ note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
577
+ this.setVolumeEnvelope(channel, note);
578
+ this.setFilterEnvelope(channel, note);
536
579
  if (channel.modulation > 0) {
537
- const vibratoDelay = startTime + channel.vibratoDelay;
538
- const vibratoAttack = vibratoDelay + 0.1;
539
- lfoGain = new GainNode(this.audioContext, {
540
- gain: 0,
541
- });
542
- lfoGain.gain
543
- .setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
544
- .exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
545
- channel.modulationEffect.lfo.connect(lfoGain);
546
- lfoGain.connect(bufferSource.detune);
580
+ const delayModLFO = startTime + instrumentKey.delayModLFO;
581
+ this.startModulation(channel, note, delayModLFO);
547
582
  }
548
- bufferSource.connect(filterNode);
549
- filterNode.connect(gainNode);
550
- bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
551
- return { bufferSource, gainNode, filterNode, lfoGain };
583
+ note.bufferSource.connect(note.filterNode);
584
+ note.filterNode.connect(note.gainNode);
585
+ note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
586
+ return note;
552
587
  }
553
588
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
554
589
  const channel = this.channels[channelNumber];
@@ -558,27 +593,17 @@ export class MidyGMLite {
558
593
  return;
559
594
  const soundFont = this.soundFonts[soundFontIndex];
560
595
  const isSF3 = soundFont.parsed.info.version.major === 3;
561
- const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
562
- if (!noteInfo)
596
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
597
+ if (!instrumentKey)
563
598
  return;
564
- const { bufferSource, gainNode, filterNode, lfoGain } = await this
565
- .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
566
- this.connectNoteEffects(channel, gainNode);
599
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
600
+ this.connectNoteEffects(channel, note.gainNode);
567
601
  const scheduledNotes = channel.scheduledNotes;
568
- const scheduledNote = {
569
- bufferSource,
570
- filterNode,
571
- gainNode,
572
- lfoGain,
573
- noteInfo,
574
- noteNumber,
575
- startTime,
576
- };
577
602
  if (scheduledNotes.has(noteNumber)) {
578
- scheduledNotes.get(noteNumber).push(scheduledNote);
603
+ scheduledNotes.get(noteNumber).push(note);
579
604
  }
580
605
  else {
581
- scheduledNotes.set(noteNumber, [scheduledNote]);
606
+ scheduledNotes.set(noteNumber, [note]);
582
607
  }
583
608
  }
584
609
  noteOn(channelNumber, noteNumber, velocity) {
@@ -598,15 +623,15 @@ export class MidyGMLite {
598
623
  continue;
599
624
  if (targetNote.ending)
600
625
  continue;
601
- const { bufferSource, filterNode, gainNode, lfoGain, noteInfo } = targetNote;
626
+ const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
602
627
  const velocityRate = (velocity + 127) / 127;
603
- const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
628
+ const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
604
629
  gainNode.gain.cancelScheduledValues(stopTime);
605
630
  gainNode.gain.linearRampToValueAtTime(0, volEndTime);
606
631
  const maxFreq = this.audioContext.sampleRate / 2;
607
- const baseFreq = this.centToHz(noteInfo.initialFilterFc);
632
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
608
633
  const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
609
- const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
634
+ const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
610
635
  filterNode.frequency
611
636
  .cancelScheduledValues(stopTime)
612
637
  .linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
@@ -620,8 +645,10 @@ export class MidyGMLite {
620
645
  bufferSource.disconnect(0);
621
646
  filterNode.disconnect(0);
622
647
  gainNode.disconnect(0);
623
- if (lfoGain)
624
- lfoGain.disconnect(0);
648
+ if (modLFOGain)
649
+ modLFOGain.disconnect(0);
650
+ if (modLFO)
651
+ modLFO.stop();
625
652
  resolve();
626
653
  };
627
654
  bufferSource.stop(volEndTime);
@@ -672,20 +699,22 @@ export class MidyGMLite {
672
699
  }
673
700
  handlePitchBendMessage(channelNumber, lsb, msb) {
674
701
  const pitchBend = msb * 128 + lsb;
675
- this.handlePitchBend(channelNumber, pitchBend);
702
+ this.setPitchBend(channelNumber, pitchBend);
676
703
  }
677
- handlePitchBend(channelNumber, pitchBend) {
704
+ setPitchBend(channelNumber, pitchBend) {
678
705
  const now = this.audioContext.currentTime;
679
706
  const channel = this.channels[channelNumber];
707
+ const prevPitchBend = channel.pitchBend;
680
708
  channel.pitchBend = (pitchBend - 8192) / 8192;
681
- const semitoneOffset = this.calcSemitoneOffset(channel);
682
- const activeNotes = this.getActiveNotes(channel);
709
+ const detuneChange = (channel.pitchBend - prevPitchBend) *
710
+ channel.pitchBendRange * 100;
711
+ const activeNotes = this.getActiveNotes(channel, now);
683
712
  activeNotes.forEach((activeNote) => {
684
- const { bufferSource, noteInfo, noteNumber } = activeNote;
685
- const playbackRate = calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
686
- bufferSource.playbackRate
713
+ const { bufferSource } = activeNote;
714
+ const detune = bufferSource.detune.value + detuneChange;
715
+ bufferSource.detune
687
716
  .cancelScheduledValues(now)
688
- .setValueAtTime(playbackRate * pressure, now);
717
+ .setValueAtTime(detune, now);
689
718
  });
690
719
  }
691
720
  handleControlChange(channelNumber, controller, value) {
@@ -719,21 +748,37 @@ export class MidyGMLite {
719
748
  }
720
749
  }
721
750
  setModulation(channelNumber, modulation) {
751
+ const now = this.audioContext.currentTime;
722
752
  const channel = this.channels[channelNumber];
723
753
  channel.modulation = (modulation / 127) *
724
754
  (channel.modulationDepthRange * 100);
755
+ const activeNotes = this.getActiveNotes(channel, now);
756
+ activeNotes.forEach((activeNote) => {
757
+ if (activeNote.modLFO) {
758
+ activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
759
+ channel.modulation, now);
760
+ }
761
+ else {
762
+ this.startModulation(channel, activeNote, now);
763
+ }
764
+ });
725
765
  }
726
766
  setVolume(channelNumber, volume) {
727
767
  const channel = this.channels[channelNumber];
728
768
  channel.volume = volume / 127;
729
769
  this.updateChannelGain(channel);
730
770
  }
771
+ panToGain(pan) {
772
+ const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
773
+ return {
774
+ gainLeft: Math.cos(theta),
775
+ gainRight: Math.sin(theta),
776
+ };
777
+ }
731
778
  setPan(channelNumber, pan) {
732
- const now = this.audioContext.currentTime;
733
779
  const channel = this.channels[channelNumber];
734
- channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
735
- channel.pannerNode.pan.cancelScheduledValues(now);
736
- channel.pannerNode.pan.setValueAtTime(channel.pan, now);
780
+ channel.pan = pan;
781
+ this.updateChannelGain(channel);
737
782
  }
738
783
  setExpression(channelNumber, expression) {
739
784
  const channel = this.channels[channelNumber];
@@ -743,8 +788,13 @@ export class MidyGMLite {
743
788
  updateChannelGain(channel) {
744
789
  const now = this.audioContext.currentTime;
745
790
  const volume = channel.volume * channel.expression;
746
- channel.gainNode.gain.cancelScheduledValues(now);
747
- channel.gainNode.gain.setValueAtTime(volume, now);
791
+ const { gainLeft, gainRight } = this.panToGain(channel.pan);
792
+ channel.gainL.gain
793
+ .cancelScheduledValues(now)
794
+ .setValueAtTime(volume * gainLeft, now);
795
+ channel.gainR.gain
796
+ .cancelScheduledValues(now)
797
+ .setValueAtTime(volume * gainRight, now);
748
798
  }
749
799
  setSustainPedal(channelNumber, value) {
750
800
  const isOn = value >= 64;
@@ -766,20 +816,39 @@ export class MidyGMLite {
766
816
  const { dataMSB, dataLSB } = channel;
767
817
  switch (rpn) {
768
818
  case 0:
769
- channel.pitchBendRange = dataMSB + dataLSB / 100;
770
- break;
819
+ return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
771
820
  default:
772
821
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
773
822
  }
774
823
  }
824
+ handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
825
+ const pitchBendRange = dataMSB + dataLSB / 100;
826
+ this.setPitchBendRange(channelNumber, pitchBendRange);
827
+ }
828
+ setPitchBendRange(channelNumber, pitchBendRange) {
829
+ const now = this.audioContext.currentTime;
830
+ const channel = this.channels[channelNumber];
831
+ const prevPitchBendRange = channel.pitchBendRange;
832
+ channel.pitchBendRange = pitchBendRange;
833
+ const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
834
+ channel.pitchBend * 100;
835
+ const activeNotes = this.getActiveNotes(channel, now);
836
+ activeNotes.forEach((activeNote) => {
837
+ const { bufferSource } = activeNote;
838
+ const detune = bufferSource.detune.value + detuneChange;
839
+ bufferSource.detune
840
+ .cancelScheduledValues(now)
841
+ .setValueAtTime(detune, now);
842
+ });
843
+ }
775
844
  allSoundOff(channelNumber) {
776
845
  const now = this.audioContext.currentTime;
777
846
  const channel = this.channels[channelNumber];
778
847
  const velocity = 0;
779
848
  const stopPedal = true;
780
849
  const promises = [];
781
- channel.scheduledNotes.forEach((scheduledNotes) => {
782
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
850
+ channel.scheduledNotes.forEach((noteList) => {
851
+ const activeNote = this.getActiveNote(noteList, now);
783
852
  if (activeNote) {
784
853
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
785
854
  promises.push(notePromise);
@@ -796,8 +865,8 @@ export class MidyGMLite {
796
865
  const velocity = 0;
797
866
  const stopPedal = false;
798
867
  const promises = [];
799
- channel.scheduledNotes.forEach((scheduledNotes) => {
800
- const activeNote = this.getActiveChannelNotes(scheduledNotes);
868
+ channel.scheduledNotes.forEach((noteList) => {
869
+ const activeNote = this.getActiveNote(noteList, now);
801
870
  if (activeNote) {
802
871
  const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
803
872
  promises.push(notePromise);
@@ -847,9 +916,9 @@ export class MidyGMLite {
847
916
  }
848
917
  handleMasterVolumeSysEx(data) {
849
918
  const volume = (data[5] * 128 + data[4]) / 16383;
850
- this.handleMasterVolume(volume);
919
+ this.setMasterVolume(volume);
851
920
  }
852
- handleMasterVolume(volume) {
921
+ setMasterVolume(volume) {
853
922
  if (volume < 0 && 1 < volume) {
854
923
  console.error("Master Volume is out of range");
855
924
  }
@@ -890,10 +959,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
890
959
  writable: true,
891
960
  value: {
892
961
  volume: 100 / 127,
893
- pan: 0,
894
- vibratoRate: 5,
895
- vibratoDepth: 0.5,
896
- vibratoDelay: 2.5,
962
+ pan: 64,
897
963
  bank: 0,
898
964
  dataMSB: 0,
899
965
  dataLSB: 0,