@marmooo/midy 0.1.7 → 0.2.1

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.
@@ -1,7 +1,7 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
3
  class Note {
4
- constructor(noteNumber, velocity, startTime, instrumentKey) {
4
+ constructor(noteNumber, velocity, startTime, voice, voiceParams) {
5
5
  Object.defineProperty(this, "bufferSource", {
6
6
  enumerable: true,
7
7
  configurable: true,
@@ -41,9 +41,75 @@ class Note {
41
41
  this.noteNumber = noteNumber;
42
42
  this.velocity = velocity;
43
43
  this.startTime = startTime;
44
- this.instrumentKey = instrumentKey;
44
+ this.voice = voice;
45
+ this.voiceParams = voiceParams;
45
46
  }
46
47
  }
48
+ // normalized to 0-1 for use with the SF2 modulator model
49
+ const defaultControllerState = {
50
+ noteOnVelocity: { type: 2, defaultValue: 0 },
51
+ noteOnKeyNumber: { type: 3, defaultValue: 0 },
52
+ pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
53
+ pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
54
+ link: { type: 127, defaultValue: 0 },
55
+ // bankMSB: { type: 128 + 0, defaultValue: 121, },
56
+ modulationDepth: { type: 128 + 1, defaultValue: 0 },
57
+ // dataMSB: { type: 128 + 6, defaultValue: 0, },
58
+ volume: { type: 128 + 7, defaultValue: 100 / 127 },
59
+ pan: { type: 128 + 10, defaultValue: 0.5 },
60
+ expression: { type: 128 + 11, defaultValue: 1 },
61
+ // bankLSB: { type: 128 + 32, defaultValue: 0, },
62
+ // dataLSB: { type: 128 + 38, defaultValue: 0, },
63
+ sustainPedal: { type: 128 + 64, defaultValue: 0 },
64
+ // rpnLSB: { type: 128 + 100, defaultValue: 127 },
65
+ // rpnMSB: { type: 128 + 101, defaultValue: 127 },
66
+ // allSoundOff: { type: 128 + 120, defaultValue: 0 },
67
+ // resetAllControllers: { type: 128 + 121, defaultValue: 0 },
68
+ // allNotesOff: { type: 128 + 123, defaultValue: 0 },
69
+ };
70
+ class ControllerState {
71
+ constructor() {
72
+ Object.defineProperty(this, "array", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: new Float32Array(256)
77
+ });
78
+ const entries = Object.entries(defaultControllerState);
79
+ for (const [name, { type, defaultValue }] of entries) {
80
+ this.array[type] = defaultValue;
81
+ Object.defineProperty(this, name, {
82
+ get: () => this.array[type],
83
+ set: (value) => this.array[type] = value,
84
+ enumerable: true,
85
+ configurable: true,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ const filterEnvelopeKeys = [
91
+ "modEnvToPitch",
92
+ "initialFilterFc",
93
+ "modEnvToFilterFc",
94
+ "modDelay",
95
+ "modAttack",
96
+ "modHold",
97
+ "modDecay",
98
+ "modSustain",
99
+ "modRelease",
100
+ "playbackRate",
101
+ ];
102
+ const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
103
+ const volumeEnvelopeKeys = [
104
+ "volDelay",
105
+ "volAttack",
106
+ "volHold",
107
+ "volDecay",
108
+ "volSustain",
109
+ "volRelease",
110
+ "initialAttenuation",
111
+ ];
112
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
47
113
  export class MidyGMLite {
48
114
  constructor(audioContext) {
49
115
  Object.defineProperty(this, "ticksPerBeat", {
@@ -156,6 +222,7 @@ export class MidyGMLite {
156
222
  });
157
223
  this.audioContext = audioContext;
158
224
  this.masterGain = new GainNode(audioContext);
225
+ this.voiceParamsHandlers = this.createVoiceParamsHandlers();
159
226
  this.controlChangeHandlers = this.createControlChangeHandlers();
160
227
  this.channels = this.createChannels(audioContext);
161
228
  this.masterGain.connect(audioContext.destination);
@@ -198,7 +265,7 @@ export class MidyGMLite {
198
265
  this.totalTime = this.calcTotalTime();
199
266
  }
200
267
  setChannelAudioNodes(audioContext) {
201
- const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
268
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
202
269
  const gainL = new GainNode(audioContext, { gain: gainLeft });
203
270
  const gainR = new GainNode(audioContext, { gain: gainRight });
204
271
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -215,18 +282,18 @@ export class MidyGMLite {
215
282
  const channels = Array.from({ length: 16 }, () => {
216
283
  return {
217
284
  ...this.constructor.channelSettings,
218
- ...this.constructor.effectSettings,
285
+ state: new ControllerState(),
219
286
  ...this.setChannelAudioNodes(audioContext),
220
287
  scheduledNotes: new Map(),
221
288
  };
222
289
  });
223
290
  return channels;
224
291
  }
225
- async createNoteBuffer(instrumentKey, isSF3) {
226
- const sampleStart = instrumentKey.start;
227
- const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
292
+ async createNoteBuffer(voiceParams, isSF3) {
293
+ const sampleStart = voiceParams.start;
294
+ const sampleEnd = voiceParams.sample.length + voiceParams.end;
228
295
  if (isSF3) {
229
- const sample = instrumentKey.sample;
296
+ const sample = voiceParams.sample;
230
297
  const start = sample.byteOffset + sampleStart;
231
298
  const end = sample.byteOffset + sampleEnd;
232
299
  const buffer = sample.buffer.slice(start, end);
@@ -234,14 +301,14 @@ export class MidyGMLite {
234
301
  return audioBuffer;
235
302
  }
236
303
  else {
237
- const sample = instrumentKey.sample;
304
+ const sample = voiceParams.sample;
238
305
  const start = sample.byteOffset + sampleStart;
239
306
  const end = sample.byteOffset + sampleEnd;
240
307
  const buffer = sample.buffer.slice(start, end);
241
308
  const audioBuffer = new AudioBuffer({
242
309
  numberOfChannels: 1,
243
310
  length: sample.length,
244
- sampleRate: instrumentKey.sampleRate,
311
+ sampleRate: voiceParams.sampleRate,
245
312
  });
246
313
  const channelData = audioBuffer.getChannelData(0);
247
314
  const int16Array = new Int16Array(buffer);
@@ -251,15 +318,14 @@ export class MidyGMLite {
251
318
  return audioBuffer;
252
319
  }
253
320
  }
254
- async createNoteBufferNode(instrumentKey, isSF3) {
321
+ async createNoteBufferNode(voiceParams, isSF3) {
255
322
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
256
- const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
323
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
257
324
  bufferSource.buffer = audioBuffer;
258
- bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
325
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
259
326
  if (bufferSource.loop) {
260
- bufferSource.loopStart = instrumentKey.loopStart /
261
- instrumentKey.sampleRate;
262
- bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
327
+ bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
328
+ bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
263
329
  }
264
330
  return bufferSource;
265
331
  }
@@ -289,7 +355,7 @@ export class MidyGMLite {
289
355
  this.handleProgramChange(event.channel, event.programNumber);
290
356
  break;
291
357
  case "pitchBend":
292
- this.setPitchBend(event.channel, event.value);
358
+ this.setPitchBend(event.channel, event.value + 8192);
293
359
  break;
294
360
  case "sysEx":
295
361
  this.handleSysEx(event.data);
@@ -515,49 +581,72 @@ export class MidyGMLite {
515
581
  cbToRatio(cb) {
516
582
  return Math.pow(10, cb / 200);
517
583
  }
584
+ rateToCent(rate) {
585
+ return 1200 * Math.log2(rate);
586
+ }
587
+ centToRate(cent) {
588
+ return Math.pow(2, cent / 1200);
589
+ }
518
590
  centToHz(cent) {
519
- return 8.176 * Math.pow(2, cent / 1200);
591
+ return 8.176 * this.centToRate(cent);
520
592
  }
521
- calcSemitoneOffset(channel) {
522
- return channel.pitchBend * channel.pitchBendRange;
593
+ calcChannelDetune(channel) {
594
+ const pitchWheel = channel.state.pitchWheel * 2 - 1;
595
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
596
+ return pitchWheel * pitchWheelSensitivity;
523
597
  }
524
- calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
525
- return instrumentKey.playbackRate(noteNumber) *
526
- Math.pow(2, semitoneOffset / 12);
598
+ updateDetune(channel) {
599
+ const now = this.audioContext.currentTime;
600
+ channel.scheduledNotes.forEach((noteList) => {
601
+ for (let i = 0; i < noteList.length; i++) {
602
+ const note = noteList[i];
603
+ if (!note)
604
+ continue;
605
+ note.bufferSource.detune
606
+ .cancelScheduledValues(now)
607
+ .setValueAtTime(channel.detune, now);
608
+ }
609
+ });
527
610
  }
528
611
  setVolumeEnvelope(note) {
529
- const { instrumentKey, startTime } = note;
530
- const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
531
- const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
532
- const volDelay = startTime + instrumentKey.volDelay;
533
- const volAttack = volDelay + instrumentKey.volAttack;
534
- const volHold = volAttack + instrumentKey.volHold;
535
- const volDecay = volHold + instrumentKey.volDecay;
612
+ const now = this.audioContext.currentTime;
613
+ const { voiceParams, startTime } = note;
614
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
615
+ const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
616
+ const volDelay = startTime + voiceParams.volDelay;
617
+ const volAttack = volDelay + voiceParams.volAttack;
618
+ const volHold = volAttack + voiceParams.volHold;
619
+ const volDecay = volHold + voiceParams.volDecay;
536
620
  note.volumeNode.gain
537
- .cancelScheduledValues(startTime)
621
+ .cancelScheduledValues(now)
538
622
  .setValueAtTime(0, startTime)
539
623
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
540
624
  .exponentialRampToValueAtTime(attackVolume, volAttack)
541
625
  .setValueAtTime(attackVolume, volHold)
542
626
  .linearRampToValueAtTime(sustainVolume, volDecay);
543
627
  }
544
- setPitch(note, semitoneOffset) {
545
- const { instrumentKey, noteNumber, startTime } = note;
546
- const modEnvToPitch = instrumentKey.modEnvToPitch / 100;
547
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
628
+ setPitchEnvelope(note) {
629
+ const now = this.audioContext.currentTime;
630
+ const { voiceParams } = note;
631
+ const baseRate = voiceParams.playbackRate;
632
+ note.bufferSource.playbackRate
633
+ .cancelScheduledValues(now)
634
+ .setValueAtTime(baseRate, now);
635
+ const modEnvToPitch = voiceParams.modEnvToPitch;
548
636
  if (modEnvToPitch === 0)
549
637
  return;
550
- const basePitch = note.bufferSource.playbackRate.value;
551
- const peekPitch = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset + modEnvToPitch);
552
- const modDelay = startTime + instrumentKey.modDelay;
553
- const modAttack = modDelay + instrumentKey.modAttack;
554
- const modHold = modAttack + instrumentKey.modHold;
555
- const modDecay = modHold + instrumentKey.modDecay;
556
- note.bufferSource.playbackRate.value
557
- .setValueAtTime(basePitch, modDelay)
558
- .exponentialRampToValueAtTime(peekPitch, modAttack)
559
- .setValueAtTime(peekPitch, modHold)
560
- .linearRampToValueAtTime(basePitch, modDecay);
638
+ const basePitch = this.rateToCent(baseRate);
639
+ const peekPitch = basePitch + modEnvToPitch;
640
+ const peekRate = this.centToRate(peekPitch);
641
+ const modDelay = startTime + voiceParams.modDelay;
642
+ const modAttack = modDelay + voiceParams.modAttack;
643
+ const modHold = modAttack + voiceParams.modHold;
644
+ const modDecay = modHold + voiceParams.modDecay;
645
+ note.bufferSource.playbackRate
646
+ .setValueAtTime(baseRate, modDelay)
647
+ .exponentialRampToValueAtTime(peekRate, modAttack)
648
+ .setValueAtTime(peekRate, modHold)
649
+ .linearRampToValueAtTime(baseRate, modDecay);
561
650
  }
562
651
  clampCutoffFrequency(frequency) {
563
652
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -565,20 +654,21 @@ export class MidyGMLite {
565
654
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
566
655
  }
567
656
  setFilterEnvelope(note) {
568
- const { instrumentKey, startTime } = note;
569
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
570
- const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc);
657
+ const now = this.audioContext.currentTime;
658
+ const { voiceParams, startTime } = note;
659
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc);
660
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
571
661
  const sustainFreq = baseFreq +
572
- (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
662
+ (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
573
663
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
574
664
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
575
665
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
576
- const modDelay = startTime + instrumentKey.modDelay;
577
- const modAttack = modDelay + instrumentKey.modAttack;
578
- const modHold = modAttack + instrumentKey.modHold;
579
- const modDecay = modHold + instrumentKey.modDecay;
666
+ const modDelay = startTime + voiceParams.modDelay;
667
+ const modAttack = modDelay + voiceParams.modAttack;
668
+ const modHold = modAttack + voiceParams.modHold;
669
+ const modDecay = modHold + voiceParams.modDecay;
580
670
  note.filterNode.frequency
581
- .cancelScheduledValues(startTime)
671
+ .cancelScheduledValues(now)
582
672
  .setValueAtTime(adjustedBaseFreq, startTime)
583
673
  .setValueAtTime(adjustedBaseFreq, modDelay)
584
674
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
@@ -586,25 +676,18 @@ export class MidyGMLite {
586
676
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
587
677
  }
588
678
  startModulation(channel, note, startTime) {
589
- const { instrumentKey } = note;
590
- const { modLfoToPitch, modLfoToVolume } = instrumentKey;
679
+ const { voiceParams } = note;
591
680
  note.modulationLFO = new OscillatorNode(this.audioContext, {
592
- frequency: this.centToHz(instrumentKey.freqModLFO),
681
+ frequency: this.centToHz(voiceParams.freqModLFO),
593
682
  });
594
683
  note.filterDepth = new GainNode(this.audioContext, {
595
- gain: instrumentKey.modLfoToFilterFc,
596
- });
597
- const modulationDepth = Math.abs(modLfoToPitch) + channel.modulationDepth;
598
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
599
- note.modulationDepth = new GainNode(this.audioContext, {
600
- gain: modulationDepth * modulationDepthSign,
601
- });
602
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
603
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
604
- note.volumeDepth = new GainNode(this.audioContext, {
605
- gain: volumeDepth * volumeDepthSign,
684
+ gain: voiceParams.modLfoToFilterFc,
606
685
  });
607
- note.modulationLFO.start(startTime + instrumentKey.delayModLFO);
686
+ note.modulationDepth = new GainNode(this.audioContext);
687
+ this.setModLfoToPitch(channel, note);
688
+ note.volumeDepth = new GainNode(this.audioContext);
689
+ this.setModLfoToVolume(note);
690
+ note.modulationLFO.start(startTime + voiceParams.delayModLFO);
608
691
  note.modulationLFO.connect(note.filterDepth);
609
692
  note.filterDepth.connect(note.filterNode.frequency);
610
693
  note.modulationLFO.connect(note.modulationDepth);
@@ -612,24 +695,22 @@ export class MidyGMLite {
612
695
  note.modulationLFO.connect(note.volumeDepth);
613
696
  note.volumeDepth.connect(note.volumeNode.gain);
614
697
  }
615
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
616
- const semitoneOffset = this.calcSemitoneOffset(channel);
617
- const note = new Note(noteNumber, velocity, startTime, instrumentKey);
618
- note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
698
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
699
+ const state = channel.state;
700
+ const voiceParams = voice.getAllParams(state.array);
701
+ const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
702
+ note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
619
703
  note.volumeNode = new GainNode(this.audioContext);
620
704
  note.filterNode = new BiquadFilterNode(this.audioContext, {
621
705
  type: "lowpass",
622
- Q: instrumentKey.initialFilterQ / 10, // dB
706
+ Q: voiceParams.initialFilterQ / 10, // dB
623
707
  });
624
708
  this.setVolumeEnvelope(note);
625
709
  this.setFilterEnvelope(note);
626
- if (0 < channel.modulationDepth) {
627
- this.setPitch(note, semitoneOffset);
710
+ this.setPitchEnvelope(note);
711
+ if (0 < state.modulationDepth) {
628
712
  this.startModulation(channel, note, startTime);
629
713
  }
630
- else {
631
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
632
- }
633
714
  note.bufferSource.connect(note.filterNode);
634
715
  note.filterNode.connect(note.volumeNode);
635
716
  note.bufferSource.start(startTime);
@@ -643,13 +724,13 @@ export class MidyGMLite {
643
724
  return;
644
725
  const soundFont = this.soundFonts[soundFontIndex];
645
726
  const isSF3 = soundFont.parsed.info.version.major === 3;
646
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
647
- if (!instrumentKey)
727
+ const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
728
+ if (!voice)
648
729
  return;
649
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
730
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
650
731
  note.volumeNode.connect(channel.gainL);
651
732
  note.volumeNode.connect(channel.gainR);
652
- const exclusiveClass = instrumentKey.exclusiveClass;
733
+ const exclusiveClass = note.voiceParams.exclusiveClass;
653
734
  if (exclusiveClass !== 0) {
654
735
  if (this.exclusiveClassMap.has(exclusiveClass)) {
655
736
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
@@ -694,10 +775,6 @@ export class MidyGMLite {
694
775
  note.modulationDepth.disconnect();
695
776
  note.modulationLFO.stop();
696
777
  }
697
- if (note.vibratoDepth) {
698
- note.vibratoDepth.disconnect();
699
- note.vibratoLFO.stop();
700
- }
701
778
  resolve();
702
779
  };
703
780
  note.bufferSource.stop(stopTime);
@@ -705,7 +782,7 @@ export class MidyGMLite {
705
782
  }
706
783
  scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
707
784
  const channel = this.channels[channelNumber];
708
- if (!force && channel.sustainPedal)
785
+ if (!force && 0.5 < channel.state.sustainPedal)
709
786
  return;
710
787
  if (!channel.scheduledNotes.has(noteNumber))
711
788
  return;
@@ -716,8 +793,8 @@ export class MidyGMLite {
716
793
  continue;
717
794
  if (note.ending)
718
795
  continue;
719
- const volRelease = endTime + note.instrumentKey.volRelease;
720
- const modRelease = endTime + note.instrumentKey.modRelease;
796
+ const volRelease = endTime + note.voiceParams.volRelease;
797
+ const modRelease = endTime + note.voiceParams.modRelease;
721
798
  note.filterNode.frequency
722
799
  .cancelScheduledValues(endTime)
723
800
  .linearRampToValueAtTime(0, modRelease);
@@ -733,7 +810,7 @@ export class MidyGMLite {
733
810
  const velocity = halfVelocity * 2;
734
811
  const channel = this.channels[channelNumber];
735
812
  const promises = [];
736
- channel.sustainPedal = false;
813
+ channel.state.sustainPedal = halfVelocity;
737
814
  channel.scheduledNotes.forEach((noteList) => {
738
815
  for (let i = 0; i < noteList.length; i++) {
739
816
  const note = noteList[i];
@@ -769,16 +846,138 @@ export class MidyGMLite {
769
846
  channel.program = program;
770
847
  }
771
848
  handlePitchBendMessage(channelNumber, lsb, msb) {
772
- const pitchBend = msb * 128 + lsb - 8192;
849
+ const pitchBend = msb * 128 + lsb;
773
850
  this.setPitchBend(channelNumber, pitchBend);
774
851
  }
775
- setPitchBend(channelNumber, pitchBend) {
852
+ setPitchBend(channelNumber, value) {
776
853
  const channel = this.channels[channelNumber];
777
- const prevPitchBend = channel.pitchBend;
778
- channel.pitchBend = pitchBend / 8192;
779
- const detuneChange = (channel.pitchBend - prevPitchBend) *
780
- channel.pitchBendRange * 100;
781
- this.updateDetune(channel, detuneChange);
854
+ const state = channel.state;
855
+ const prev = state.pitchWheel * 2 - 1;
856
+ const next = (value - 8192) / 8192;
857
+ state.pitchWheel = value / 16383;
858
+ channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
859
+ this.updateDetune(channel);
860
+ this.applyVoiceParams(channel, 14);
861
+ }
862
+ setModLfoToPitch(channel, note) {
863
+ const now = this.audioContext.currentTime;
864
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
865
+ const modulationDepth = Math.abs(modLfoToPitch) +
866
+ channel.state.modulationDepth;
867
+ const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
868
+ note.modulationDepth.gain
869
+ .cancelScheduledValues(now)
870
+ .setValueAtTime(modulationDepth * modulationDepthSign, now);
871
+ }
872
+ setModLfoToFilterFc(note) {
873
+ const now = this.audioContext.currentTime;
874
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
875
+ note.filterDepth.gain
876
+ .cancelScheduledValues(now)
877
+ .setValueAtTime(modLfoToFilterFc, now);
878
+ }
879
+ setModLfoToVolume(note) {
880
+ const now = this.audioContext.currentTime;
881
+ const modLfoToVolume = note.voiceParams.modLfoToVolume;
882
+ const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
883
+ const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
884
+ note.volumeDepth.gain
885
+ .cancelScheduledValues(now)
886
+ .setValueAtTime(volumeDepth * volumeDepthSign, now);
887
+ }
888
+ setDelayModLFO(note) {
889
+ const now = this.audioContext.currentTime;
890
+ const startTime = note.startTime;
891
+ if (startTime < now)
892
+ return;
893
+ note.modulationLFO.stop(now);
894
+ note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
895
+ note.modulationLFO.connect(note.filterDepth);
896
+ }
897
+ setFreqModLFO(note) {
898
+ const now = this.audioContext.currentTime;
899
+ const freqModLFO = note.voiceParams.freqModLFO;
900
+ note.modulationLFO.frequency
901
+ .cancelScheduledValues(now)
902
+ .setValueAtTime(freqModLFO, now);
903
+ }
904
+ createVoiceParamsHandlers() {
905
+ return {
906
+ modLfoToPitch: (channel, note, _prevValue) => {
907
+ if (0 < channel.state.modulationDepth) {
908
+ this.setModLfoToPitch(channel, note);
909
+ }
910
+ },
911
+ vibLfoToPitch: (_channel, _note, _prevValue) => { },
912
+ modLfoToFilterFc: (channel, note, _prevValue) => {
913
+ if (0 < channel.state.modulationDepth)
914
+ this.setModLfoToFilterFc(note);
915
+ },
916
+ modLfoToVolume: (channel, note) => {
917
+ if (0 < channel.state.modulationDepth)
918
+ this.setModLfoToVolume(note);
919
+ },
920
+ chorusEffectsSend: (_channel, _note, _prevValue) => { },
921
+ reverbEffectsSend: (_channel, _note, _prevValue) => { },
922
+ delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
923
+ freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
924
+ delayVibLFO: (_channel, _note, _prevValue) => { },
925
+ freqVibLFO: (_channel, _note, _prevValue) => { },
926
+ };
927
+ }
928
+ getControllerState(channel, noteNumber, velocity) {
929
+ const state = new Float32Array(channel.state.array.length);
930
+ state.set(channel.state.array);
931
+ state[2] = velocity / 127;
932
+ state[3] = noteNumber / 127;
933
+ return state;
934
+ }
935
+ applyVoiceParams(channel, controllerType) {
936
+ channel.scheduledNotes.forEach((noteList) => {
937
+ for (let i = 0; i < noteList.length; i++) {
938
+ const note = noteList[i];
939
+ if (!note)
940
+ continue;
941
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
942
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
943
+ let appliedFilterEnvelope = false;
944
+ let appliedVolumeEnvelope = false;
945
+ for (const [key, value] of Object.entries(voiceParams)) {
946
+ const prevValue = note.voiceParams[key];
947
+ if (value === prevValue)
948
+ continue;
949
+ note.voiceParams[key] = value;
950
+ if (key in this.voiceParamsHandlers) {
951
+ this.voiceParamsHandlers[key](channel, note, prevValue);
952
+ }
953
+ else if (filterEnvelopeKeySet.has(key)) {
954
+ if (appliedFilterEnvelope)
955
+ continue;
956
+ appliedFilterEnvelope = true;
957
+ const noteVoiceParams = note.voiceParams;
958
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
959
+ const key = filterEnvelopeKeys[i];
960
+ if (key in voiceParams)
961
+ noteVoiceParams[key] = voiceParams[key];
962
+ }
963
+ this.setFilterEnvelope(channel, note);
964
+ this.setPitchEnvelope(note);
965
+ }
966
+ else if (volumeEnvelopeKeySet.has(key)) {
967
+ if (appliedVolumeEnvelope)
968
+ continue;
969
+ appliedVolumeEnvelope = true;
970
+ const noteVoiceParams = note.voiceParams;
971
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
972
+ const key = volumeEnvelopeKeys[i];
973
+ if (key in voiceParams)
974
+ noteVoiceParams[key] = voiceParams[key];
975
+ }
976
+ this.setVolumeEnvelope(channel, note);
977
+ }
978
+ }
979
+ }
980
+ });
782
981
  }
783
982
  createControlChangeHandlers() {
784
983
  return {
@@ -796,13 +995,13 @@ export class MidyGMLite {
796
995
  123: this.allNotesOff,
797
996
  };
798
997
  }
799
- handleControlChange(channelNumber, controller, value) {
800
- const handler = this.controlChangeHandlers[controller];
998
+ handleControlChange(channelNumber, controllerType, value) {
999
+ const handler = this.controlChangeHandlers[controllerType];
801
1000
  if (handler) {
802
1001
  handler.call(this, channelNumber, value);
803
1002
  }
804
1003
  else {
805
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
1004
+ console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
806
1005
  }
807
1006
  }
808
1007
  updateModulation(channel) {
@@ -813,11 +1012,10 @@ export class MidyGMLite {
813
1012
  if (!note)
814
1013
  continue;
815
1014
  if (note.modulationDepth) {
816
- note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
1015
+ note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
817
1016
  }
818
1017
  else {
819
- const semitoneOffset = this.calcSemitoneOffset(channel);
820
- this.setPitch(note, semitoneOffset);
1018
+ this.setPitchEnvelope(note);
821
1019
  this.startModulation(channel, note, now);
822
1020
  }
823
1021
  }
@@ -825,16 +1023,17 @@ export class MidyGMLite {
825
1023
  }
826
1024
  setModulationDepth(channelNumber, modulation) {
827
1025
  const channel = this.channels[channelNumber];
828
- channel.modulationDepth = (modulation / 127) * channel.modulationDepthRange;
1026
+ channel.state.modulationDepth = (modulation / 127) *
1027
+ channel.modulationDepthRange;
829
1028
  this.updateModulation(channel);
830
1029
  }
831
1030
  setVolume(channelNumber, volume) {
832
1031
  const channel = this.channels[channelNumber];
833
- channel.volume = volume / 127;
1032
+ channel.state.volume = volume / 127;
834
1033
  this.updateChannelVolume(channel);
835
1034
  }
836
1035
  panToGain(pan) {
837
- const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1036
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
838
1037
  return {
839
1038
  gainLeft: Math.cos(theta),
840
1039
  gainRight: Math.sin(theta),
@@ -842,12 +1041,12 @@ export class MidyGMLite {
842
1041
  }
843
1042
  setPan(channelNumber, pan) {
844
1043
  const channel = this.channels[channelNumber];
845
- channel.pan = pan;
1044
+ channel.state.pan = pan / 127;
846
1045
  this.updateChannelVolume(channel);
847
1046
  }
848
1047
  setExpression(channelNumber, expression) {
849
1048
  const channel = this.channels[channelNumber];
850
- channel.expression = expression / 127;
1049
+ channel.state.expression = expression / 127;
851
1050
  this.updateChannelVolume(channel);
852
1051
  }
853
1052
  dataEntryLSB(channelNumber, value) {
@@ -856,8 +1055,9 @@ export class MidyGMLite {
856
1055
  }
857
1056
  updateChannelVolume(channel) {
858
1057
  const now = this.audioContext.currentTime;
859
- const volume = channel.volume * channel.expression;
860
- const { gainLeft, gainRight } = this.panToGain(channel.pan);
1058
+ const state = channel.state;
1059
+ const volume = state.volume * state.expression;
1060
+ const { gainLeft, gainRight } = this.panToGain(state.pan);
861
1061
  channel.gainL.gain
862
1062
  .cancelScheduledValues(now)
863
1063
  .setValueAtTime(volume * gainLeft, now);
@@ -866,12 +1066,29 @@ export class MidyGMLite {
866
1066
  .setValueAtTime(volume * gainRight, now);
867
1067
  }
868
1068
  setSustainPedal(channelNumber, value) {
869
- const isOn = value >= 64;
870
- this.channels[channelNumber].sustainPedal = isOn;
871
- if (!isOn) {
1069
+ this.channels[channelNumber].state.sustainPedal = value / 127;
1070
+ if (value < 64) {
872
1071
  this.releaseSustainPedal(channelNumber, value);
873
1072
  }
874
1073
  }
1074
+ limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1075
+ if (maxLSB < channel.dataLSB) {
1076
+ channel.dataMSB++;
1077
+ channel.dataLSB = minLSB;
1078
+ }
1079
+ else if (channel.dataLSB < 0) {
1080
+ channel.dataMSB--;
1081
+ channel.dataLSB = maxLSB;
1082
+ }
1083
+ if (maxMSB < channel.dataMSB) {
1084
+ channel.dataMSB = maxMSB;
1085
+ channel.dataLSB = maxLSB;
1086
+ }
1087
+ else if (channel.dataMSB < 0) {
1088
+ channel.dataMSB = minMSB;
1089
+ channel.dataLSB = minLSB;
1090
+ }
1091
+ }
875
1092
  handleRPN(channelNumber) {
876
1093
  const channel = this.channels[channelNumber];
877
1094
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
@@ -893,40 +1110,44 @@ export class MidyGMLite {
893
1110
  this.channels[channelNumber].dataMSB = value;
894
1111
  this.handleRPN(channelNumber);
895
1112
  }
896
- updateDetune(channel, detuneChange) {
897
- const now = this.audioContext.currentTime;
898
- channel.scheduledNotes.forEach((noteList) => {
899
- for (let i = 0; i < noteList.length; i++) {
900
- const note = noteList[i];
901
- if (!note)
902
- continue;
903
- const { bufferSource } = note;
904
- const detune = bufferSource.detune.value + detuneChange;
905
- bufferSource.detune
906
- .cancelScheduledValues(now)
907
- .setValueAtTime(detune, now);
908
- }
909
- });
910
- }
911
1113
  handlePitchBendRangeRPN(channelNumber) {
912
1114
  const channel = this.channels[channelNumber];
913
1115
  this.limitData(channel, 0, 127, 0, 99);
914
1116
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
915
1117
  this.setPitchBendRange(channelNumber, pitchBendRange);
916
1118
  }
917
- setPitchBendRange(channelNumber, pitchBendRange) {
1119
+ setPitchBendRange(channelNumber, pitchWheelSensitivity) {
918
1120
  const channel = this.channels[channelNumber];
919
- const prevPitchBendRange = channel.pitchBendRange;
920
- channel.pitchBendRange = pitchBendRange;
921
- const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
922
- channel.pitchBend * 100;
923
- this.updateDetune(channel, detuneChange);
1121
+ const state = channel.state;
1122
+ state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
1123
+ const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
1124
+ this.updateDetune(channel, detune);
1125
+ this.applyVoiceParams(channel, 16);
924
1126
  }
925
1127
  allSoundOff(channelNumber) {
926
1128
  return this.stopChannelNotes(channelNumber, 0, true);
927
1129
  }
928
1130
  resetAllControllers(channelNumber) {
929
- Object.assign(this.channels[channelNumber], this.effectSettings);
1131
+ const stateTypes = [
1132
+ "expression",
1133
+ "modulationDepth",
1134
+ "sustainPedal",
1135
+ "pitchWheelSensitivity",
1136
+ ];
1137
+ const channel = this.channels[channelNumber];
1138
+ const state = channel.state;
1139
+ for (let i = 0; i < stateTypes.length; i++) {
1140
+ const type = stateTypes[i];
1141
+ state[type] = defaultControllerState[type];
1142
+ }
1143
+ const settingTypes = [
1144
+ "rpnMSB",
1145
+ "rpnLSB",
1146
+ ];
1147
+ for (let i = 0; i < settingTypes.length; i++) {
1148
+ const type = settingTypes[i];
1149
+ channel[type] = this.constructor.channelSettings[type];
1150
+ }
930
1151
  }
931
1152
  allNotesOff(channelNumber) {
932
1153
  return this.stopChannelNotes(channelNumber, 0, false);
@@ -951,11 +1172,8 @@ export class MidyGMLite {
951
1172
  GM1SystemOn() {
952
1173
  for (let i = 0; i < this.channels.length; i++) {
953
1174
  const channel = this.channels[i];
954
- channel.bankMSB = 0;
955
- channel.bankLSB = 0;
956
1175
  channel.bank = 0;
957
1176
  }
958
- this.channels[9].bankMSB = 1;
959
1177
  this.channels[9].bank = 128;
960
1178
  }
961
1179
  handleUniversalRealTimeExclusiveMessage(data) {
@@ -1016,26 +1234,13 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1016
1234
  configurable: true,
1017
1235
  writable: true,
1018
1236
  value: {
1019
- volume: 100 / 127,
1020
- pan: 64,
1237
+ currentBufferSource: null,
1238
+ detune: 0,
1239
+ program: 0,
1021
1240
  bank: 0,
1022
1241
  dataMSB: 0,
1023
1242
  dataLSB: 0,
1024
- program: 0,
1025
- pitchBend: 0,
1026
- modulationDepthRange: 50, // cent
1027
- }
1028
- });
1029
- Object.defineProperty(MidyGMLite, "effectSettings", {
1030
- enumerable: true,
1031
- configurable: true,
1032
- writable: true,
1033
- value: {
1034
- expression: 1,
1035
- modulationDepth: 0,
1036
- sustainPedal: false,
1037
1243
  rpnMSB: 127,
1038
1244
  rpnLSB: 127,
1039
- pitchBendRange: 2,
1040
1245
  }
1041
1246
  });