@marmooo/midy 0.1.7 → 0.2.0

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.
@@ -4,7 +4,7 @@ exports.MidyGM1 = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
6
  class Note {
7
- constructor(noteNumber, velocity, startTime, instrumentKey) {
7
+ constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
8
  Object.defineProperty(this, "bufferSource", {
9
9
  enumerable: true,
10
10
  configurable: true,
@@ -56,9 +56,75 @@ class Note {
56
56
  this.noteNumber = noteNumber;
57
57
  this.velocity = velocity;
58
58
  this.startTime = startTime;
59
- this.instrumentKey = instrumentKey;
59
+ this.voice = voice;
60
+ this.voiceParams = voiceParams;
60
61
  }
61
62
  }
63
+ // normalized to 0-1 for use with the SF2 modulator model
64
+ const defaultControllerState = {
65
+ noteOnVelocity: { type: 2, defaultValue: 0 },
66
+ noteOnKeyNumber: { type: 3, defaultValue: 0 },
67
+ pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
68
+ pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
69
+ link: { type: 127, defaultValue: 0 },
70
+ // bankMSB: { type: 128 + 0, defaultValue: 121, },
71
+ modulationDepth: { type: 128 + 1, defaultValue: 0 },
72
+ // dataMSB: { type: 128 + 6, defaultValue: 0, },
73
+ volume: { type: 128 + 7, defaultValue: 100 / 127 },
74
+ pan: { type: 128 + 10, defaultValue: 0.5 },
75
+ expression: { type: 128 + 11, defaultValue: 1 },
76
+ // bankLSB: { type: 128 + 32, defaultValue: 0, },
77
+ // dataLSB: { type: 128 + 38, defaultValue: 0, },
78
+ sustainPedal: { type: 128 + 64, defaultValue: 0 },
79
+ // rpnLSB: { type: 128 + 100, defaultValue: 127 },
80
+ // rpnMSB: { type: 128 + 101, defaultValue: 127 },
81
+ // allSoundOff: { type: 128 + 120, defaultValue: 0 },
82
+ // resetAllControllers: { type: 128 + 121, defaultValue: 0 },
83
+ // allNotesOff: { type: 128 + 123, defaultValue: 0 },
84
+ };
85
+ class ControllerState {
86
+ constructor() {
87
+ Object.defineProperty(this, "array", {
88
+ enumerable: true,
89
+ configurable: true,
90
+ writable: true,
91
+ value: new Float32Array(256)
92
+ });
93
+ const entries = Object.entries(defaultControllerState);
94
+ for (const [name, { type, defaultValue }] of entries) {
95
+ this.array[type] = defaultValue;
96
+ Object.defineProperty(this, name, {
97
+ get: () => this.array[type],
98
+ set: (value) => this.array[type] = value,
99
+ enumerable: true,
100
+ configurable: true,
101
+ });
102
+ }
103
+ }
104
+ }
105
+ const filterEnvelopeKeys = [
106
+ "modEnvToPitch",
107
+ "initialFilterFc",
108
+ "modEnvToFilterFc",
109
+ "modDelay",
110
+ "modAttack",
111
+ "modHold",
112
+ "modDecay",
113
+ "modSustain",
114
+ "modRelease",
115
+ "playbackRate",
116
+ ];
117
+ const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
118
+ const volumeEnvelopeKeys = [
119
+ "volDelay",
120
+ "volAttack",
121
+ "volHold",
122
+ "volDecay",
123
+ "volSustain",
124
+ "volRelease",
125
+ "initialAttenuation",
126
+ ];
127
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
62
128
  class MidyGM1 {
63
129
  constructor(audioContext) {
64
130
  Object.defineProperty(this, "ticksPerBeat", {
@@ -171,6 +237,7 @@ class MidyGM1 {
171
237
  });
172
238
  this.audioContext = audioContext;
173
239
  this.masterGain = new GainNode(audioContext);
240
+ this.voiceParamsHandlers = this.createVoiceParamsHandlers();
174
241
  this.controlChangeHandlers = this.createControlChangeHandlers();
175
242
  this.channels = this.createChannels(audioContext);
176
243
  this.masterGain.connect(audioContext.destination);
@@ -213,7 +280,7 @@ class MidyGM1 {
213
280
  this.totalTime = this.calcTotalTime();
214
281
  }
215
282
  setChannelAudioNodes(audioContext) {
216
- const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
283
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
217
284
  const gainL = new GainNode(audioContext, { gain: gainLeft });
218
285
  const gainR = new GainNode(audioContext, { gain: gainRight });
219
286
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -230,18 +297,18 @@ class MidyGM1 {
230
297
  const channels = Array.from({ length: 16 }, () => {
231
298
  return {
232
299
  ...this.constructor.channelSettings,
233
- ...this.constructor.effectSettings,
300
+ state: new ControllerState(),
234
301
  ...this.setChannelAudioNodes(audioContext),
235
302
  scheduledNotes: new Map(),
236
303
  };
237
304
  });
238
305
  return channels;
239
306
  }
240
- async createNoteBuffer(instrumentKey, isSF3) {
241
- const sampleStart = instrumentKey.start;
242
- const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
307
+ async createNoteBuffer(voiceParams, isSF3) {
308
+ const sampleStart = voiceParams.start;
309
+ const sampleEnd = voiceParams.sample.length + voiceParams.end;
243
310
  if (isSF3) {
244
- const sample = instrumentKey.sample;
311
+ const sample = voiceParams.sample;
245
312
  const start = sample.byteOffset + sampleStart;
246
313
  const end = sample.byteOffset + sampleEnd;
247
314
  const buffer = sample.buffer.slice(start, end);
@@ -249,14 +316,14 @@ class MidyGM1 {
249
316
  return audioBuffer;
250
317
  }
251
318
  else {
252
- const sample = instrumentKey.sample;
319
+ const sample = voiceParams.sample;
253
320
  const start = sample.byteOffset + sampleStart;
254
321
  const end = sample.byteOffset + sampleEnd;
255
322
  const buffer = sample.buffer.slice(start, end);
256
323
  const audioBuffer = new AudioBuffer({
257
324
  numberOfChannels: 1,
258
325
  length: sample.length,
259
- sampleRate: instrumentKey.sampleRate,
326
+ sampleRate: voiceParams.sampleRate,
260
327
  });
261
328
  const channelData = audioBuffer.getChannelData(0);
262
329
  const int16Array = new Int16Array(buffer);
@@ -266,15 +333,14 @@ class MidyGM1 {
266
333
  return audioBuffer;
267
334
  }
268
335
  }
269
- async createNoteBufferNode(instrumentKey, isSF3) {
336
+ async createNoteBufferNode(voiceParams, isSF3) {
270
337
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
271
- const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
338
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
272
339
  bufferSource.buffer = audioBuffer;
273
- bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
340
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
274
341
  if (bufferSource.loop) {
275
- bufferSource.loopStart = instrumentKey.loopStart /
276
- instrumentKey.sampleRate;
277
- bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
342
+ bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
343
+ bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
278
344
  }
279
345
  return bufferSource;
280
346
  }
@@ -304,7 +370,7 @@ class MidyGM1 {
304
370
  this.handleProgramChange(event.channel, event.programNumber);
305
371
  break;
306
372
  case "pitchBend":
307
- this.setPitchBend(event.channel, event.value);
373
+ this.setPitchBend(event.channel, event.value + 8192);
308
374
  break;
309
375
  case "sysEx":
310
376
  this.handleSysEx(event.data);
@@ -535,41 +601,50 @@ class MidyGM1 {
535
601
  }
536
602
  calcSemitoneOffset(channel) {
537
603
  const tuning = channel.coarseTuning + channel.fineTuning;
538
- return channel.pitchBend * channel.pitchBendRange + tuning;
539
- }
540
- calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
541
- return instrumentKey.playbackRate(noteNumber) *
542
- Math.pow(2, semitoneOffset / 12);
604
+ const pitchWheel = channel.state.pitchWheel * 2 - 1;
605
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
606
+ const pitch = pitchWheel * pitchWheelSensitivity;
607
+ return tuning + pitch;
543
608
  }
544
609
  setVolumeEnvelope(note) {
545
- const { instrumentKey, startTime } = note;
546
- const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
547
- const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
548
- const volDelay = startTime + instrumentKey.volDelay;
549
- const volAttack = volDelay + instrumentKey.volAttack;
550
- const volHold = volAttack + instrumentKey.volHold;
551
- const volDecay = volHold + instrumentKey.volDecay;
610
+ const now = this.audioContext.currentTime;
611
+ const { voiceParams, startTime } = note;
612
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
613
+ const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
614
+ const volDelay = startTime + voiceParams.volDelay;
615
+ const volAttack = volDelay + voiceParams.volAttack;
616
+ const volHold = volAttack + voiceParams.volHold;
617
+ const volDecay = volHold + voiceParams.volDecay;
552
618
  note.volumeNode.gain
553
- .cancelScheduledValues(startTime)
619
+ .cancelScheduledValues(now)
554
620
  .setValueAtTime(0, startTime)
555
621
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
556
622
  .exponentialRampToValueAtTime(attackVolume, volAttack)
557
623
  .setValueAtTime(attackVolume, volHold)
558
624
  .linearRampToValueAtTime(sustainVolume, volDecay);
559
625
  }
560
- setPitch(note, semitoneOffset) {
561
- const { instrumentKey, noteNumber, startTime } = note;
562
- const modEnvToPitch = instrumentKey.modEnvToPitch / 100;
563
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
626
+ setPlaybackRate(note) {
627
+ const now = this.audioContext.currentTime;
628
+ note.bufferSource.playbackRate
629
+ .cancelScheduledValues(now)
630
+ .setValueAtTime(note.voiceParams.playbackRate, now);
631
+ }
632
+ setPitch(channel, note) {
633
+ const now = this.audioContext.currentTime;
634
+ const { startTime } = note;
635
+ const basePitch = this.calcSemitoneOffset(channel) * 100;
636
+ note.bufferSource.detune
637
+ .cancelScheduledValues(now)
638
+ .setValueAtTime(basePitch, startTime);
639
+ const modEnvToPitch = note.voiceParams.modEnvToPitch;
564
640
  if (modEnvToPitch === 0)
565
641
  return;
566
- const basePitch = note.bufferSource.playbackRate.value;
567
- const peekPitch = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset + modEnvToPitch);
568
- const modDelay = startTime + instrumentKey.modDelay;
569
- const modAttack = modDelay + instrumentKey.modAttack;
570
- const modHold = modAttack + instrumentKey.modHold;
571
- const modDecay = modHold + instrumentKey.modDecay;
572
- note.bufferSource.playbackRate.value
642
+ const peekPitch = basePitch + modEnvToPitch;
643
+ const modDelay = startTime + voiceParams.modDelay;
644
+ const modAttack = modDelay + voiceParams.modAttack;
645
+ const modHold = modAttack + voiceParams.modHold;
646
+ const modDecay = modHold + voiceParams.modDecay;
647
+ note.bufferSource.detune
573
648
  .setValueAtTime(basePitch, modDelay)
574
649
  .exponentialRampToValueAtTime(peekPitch, modAttack)
575
650
  .setValueAtTime(peekPitch, modHold)
@@ -581,20 +656,21 @@ class MidyGM1 {
581
656
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
582
657
  }
583
658
  setFilterEnvelope(note) {
584
- const { instrumentKey, startTime } = note;
585
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
586
- const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc);
659
+ const now = this.audioContext.currentTime;
660
+ const { voiceParams, startTime } = note;
661
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc);
662
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
587
663
  const sustainFreq = baseFreq +
588
- (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
664
+ (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
589
665
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
590
666
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
591
667
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
592
- const modDelay = startTime + instrumentKey.modDelay;
593
- const modAttack = modDelay + instrumentKey.modAttack;
594
- const modHold = modAttack + instrumentKey.modHold;
595
- const modDecay = modHold + instrumentKey.modDecay;
668
+ const modDelay = startTime + voiceParams.modDelay;
669
+ const modAttack = modDelay + voiceParams.modAttack;
670
+ const modHold = modAttack + voiceParams.modHold;
671
+ const modDecay = modHold + voiceParams.modDecay;
596
672
  note.filterNode.frequency
597
- .cancelScheduledValues(startTime)
673
+ .cancelScheduledValues(now)
598
674
  .setValueAtTime(adjustedBaseFreq, startTime)
599
675
  .setValueAtTime(adjustedBaseFreq, modDelay)
600
676
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
@@ -602,25 +678,18 @@ class MidyGM1 {
602
678
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
603
679
  }
604
680
  startModulation(channel, note, startTime) {
605
- const { instrumentKey } = note;
606
- const { modLfoToPitch, modLfoToVolume } = instrumentKey;
681
+ const { voiceParams } = note;
607
682
  note.modulationLFO = new OscillatorNode(this.audioContext, {
608
- frequency: this.centToHz(instrumentKey.freqModLFO),
683
+ frequency: this.centToHz(voiceParams.freqModLFO),
609
684
  });
610
685
  note.filterDepth = new GainNode(this.audioContext, {
611
- gain: instrumentKey.modLfoToFilterFc,
612
- });
613
- const modulationDepth = Math.abs(modLfoToPitch) + channel.modulationDepth;
614
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
615
- note.modulationDepth = new GainNode(this.audioContext, {
616
- gain: modulationDepth * modulationDepthSign,
617
- });
618
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
619
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
620
- note.volumeDepth = new GainNode(this.audioContext, {
621
- gain: volumeDepth * volumeDepthSign,
686
+ gain: voiceParams.modLfoToFilterFc,
622
687
  });
623
- note.modulationLFO.start(startTime + instrumentKey.delayModLFO);
688
+ note.modulationDepth = new GainNode(this.audioContext);
689
+ this.setModLfoToPitch(channel, note);
690
+ note.volumeDepth = new GainNode(this.audioContext);
691
+ this.setModLfoToVolume(note);
692
+ note.modulationLFO.start(startTime + voiceParams.delayModLFO);
624
693
  note.modulationLFO.connect(note.filterDepth);
625
694
  note.filterDepth.connect(note.filterNode.frequency);
626
695
  note.modulationLFO.connect(note.modulationDepth);
@@ -628,24 +697,23 @@ class MidyGM1 {
628
697
  note.modulationLFO.connect(note.volumeDepth);
629
698
  note.volumeDepth.connect(note.volumeNode.gain);
630
699
  }
631
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
632
- const semitoneOffset = this.calcSemitoneOffset(channel);
633
- const note = new Note(noteNumber, velocity, startTime, instrumentKey);
634
- note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
700
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
701
+ const state = channel.state;
702
+ const voiceParams = voice.getAllParams(state.array);
703
+ const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
704
+ note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
635
705
  note.volumeNode = new GainNode(this.audioContext);
636
706
  note.filterNode = new BiquadFilterNode(this.audioContext, {
637
707
  type: "lowpass",
638
- Q: instrumentKey.initialFilterQ / 10, // dB
708
+ Q: voiceParams.initialFilterQ / 10, // dB
639
709
  });
640
710
  this.setVolumeEnvelope(note);
641
711
  this.setFilterEnvelope(note);
642
- if (0 < channel.modulationDepth) {
643
- this.setPitch(note, semitoneOffset);
712
+ this.setPlaybackRate(note);
713
+ if (0 < state.modulationDepth) {
714
+ this.setPitch(channel, note);
644
715
  this.startModulation(channel, note, startTime);
645
716
  }
646
- else {
647
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
648
- }
649
717
  note.bufferSource.connect(note.filterNode);
650
718
  note.filterNode.connect(note.volumeNode);
651
719
  note.bufferSource.start(startTime);
@@ -653,19 +721,19 @@ class MidyGM1 {
653
721
  }
654
722
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
655
723
  const channel = this.channels[channelNumber];
656
- const bankNumber = 0;
724
+ const bankNumber = channel.bank;
657
725
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
658
726
  if (soundFontIndex === undefined)
659
727
  return;
660
728
  const soundFont = this.soundFonts[soundFontIndex];
661
729
  const isSF3 = soundFont.parsed.info.version.major === 3;
662
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
663
- if (!instrumentKey)
730
+ const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
731
+ if (!voice)
664
732
  return;
665
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
733
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
666
734
  note.volumeNode.connect(channel.gainL);
667
735
  note.volumeNode.connect(channel.gainR);
668
- const exclusiveClass = instrumentKey.exclusiveClass;
736
+ const exclusiveClass = note.voiceParams.exclusiveClass;
669
737
  if (exclusiveClass !== 0) {
670
738
  if (this.exclusiveClassMap.has(exclusiveClass)) {
671
739
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
@@ -721,7 +789,7 @@ class MidyGM1 {
721
789
  }
722
790
  scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
723
791
  const channel = this.channels[channelNumber];
724
- if (!force && channel.sustainPedal)
792
+ if (!force && 0.5 < channel.state.sustainPedal)
725
793
  return;
726
794
  if (!channel.scheduledNotes.has(noteNumber))
727
795
  return;
@@ -732,8 +800,8 @@ class MidyGM1 {
732
800
  continue;
733
801
  if (note.ending)
734
802
  continue;
735
- const volRelease = endTime + note.instrumentKey.volRelease;
736
- const modRelease = endTime + note.instrumentKey.modRelease;
803
+ const volRelease = endTime + note.voiceParams.volRelease;
804
+ const modRelease = endTime + note.voiceParams.modRelease;
737
805
  note.filterNode.frequency
738
806
  .cancelScheduledValues(endTime)
739
807
  .linearRampToValueAtTime(0, modRelease);
@@ -749,7 +817,7 @@ class MidyGM1 {
749
817
  const velocity = halfVelocity * 2;
750
818
  const channel = this.channels[channelNumber];
751
819
  const promises = [];
752
- channel.sustainPedal = false;
820
+ channel.state.sustainPedal = halfVelocity;
753
821
  channel.scheduledNotes.forEach((noteList) => {
754
822
  for (let i = 0; i < noteList.length; i++) {
755
823
  const note = noteList[i];
@@ -785,17 +853,170 @@ class MidyGM1 {
785
853
  channel.program = program;
786
854
  }
787
855
  handlePitchBendMessage(channelNumber, lsb, msb) {
788
- const pitchBend = msb * 128 + lsb - 8192;
856
+ const pitchBend = msb * 128 + lsb;
789
857
  this.setPitchBend(channelNumber, pitchBend);
790
858
  }
791
- setPitchBend(channelNumber, pitchBend) {
859
+ setPitchBend(channelNumber, value) {
792
860
  const channel = this.channels[channelNumber];
793
- const prevPitchBend = channel.pitchBend;
794
- channel.pitchBend = pitchBend / 8192;
795
- const detuneChange = (channel.pitchBend - prevPitchBend) *
796
- channel.pitchBendRange * 100;
861
+ const state = channel.state;
862
+ state.pitchWheel = value / 16383;
863
+ const pitchWheel = (value - 8192) / 8192;
864
+ const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
797
865
  this.updateDetune(channel, detuneChange);
798
866
  }
867
+ setModLfoToPitch(channel, note) {
868
+ const now = this.audioContext.currentTime;
869
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
870
+ const modulationDepth = Math.abs(modLfoToPitch) +
871
+ channel.state.modulationDepth;
872
+ const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
873
+ note.modulationDepth.gain
874
+ .cancelScheduledValues(now)
875
+ .setValueAtTime(modulationDepth * modulationDepthSign, now);
876
+ }
877
+ setModLfoToVolume(note) {
878
+ const now = this.audioContext.currentTime;
879
+ const modLfoToVolume = note.voiceParams.modLfoToVolume;
880
+ const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
881
+ const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
882
+ note.volumeDepth.gain
883
+ .cancelScheduledValues(now)
884
+ .setValueAtTime(volumeDepth * volumeDepthSign, now);
885
+ }
886
+ setVibLfoToPitch(channel, note) {
887
+ const now = this.audioContext.currentTime;
888
+ const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
889
+ const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
890
+ 2;
891
+ const vibratoDepthSign = 0 < vibLfoToPitch;
892
+ note.vibratoDepth.gain
893
+ .cancelScheduledValues(now)
894
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
895
+ }
896
+ setModLfoToFilterFc(note) {
897
+ const now = this.audioContext.currentTime;
898
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
899
+ note.filterDepth.gain
900
+ .cancelScheduledValues(now)
901
+ .setValueAtTime(modLfoToFilterFc, now);
902
+ }
903
+ setDelayModLFO(note) {
904
+ const now = this.audioContext.currentTime;
905
+ const startTime = note.startTime;
906
+ if (startTime < now)
907
+ return;
908
+ note.modulationLFO.stop(now);
909
+ note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
910
+ note.modulationLFO.connect(note.filterDepth);
911
+ }
912
+ setFreqModLFO(note) {
913
+ const now = this.audioContext.currentTime;
914
+ const freqModLFO = note.voiceParams.freqModLFO;
915
+ note.modulationLFO.frequency
916
+ .cancelScheduledValues(now)
917
+ .setValueAtTime(freqModLFO, now);
918
+ }
919
+ createVoiceParamsHandlers() {
920
+ return {
921
+ modLfoToPitch: (channel, note, _prevValue) => {
922
+ if (0 < channel.state.modulationDepth) {
923
+ this.setModLfoToPitch(channel, note);
924
+ }
925
+ },
926
+ vibLfoToPitch: (channel, note, _prevValue) => {
927
+ if (0 < channel.state.vibratoDepth) {
928
+ this.setVibLfoToPitch(channel, note);
929
+ }
930
+ },
931
+ modLfoToFilterFc: (channel, note, _prevValue) => {
932
+ if (0 < channel.state.modulationDepth)
933
+ this.setModLfoToFilterFc(note);
934
+ },
935
+ modLfoToVolume: (channel, note) => {
936
+ if (0 < channel.state.modulationDepth)
937
+ this.setModLfoToVolume(note);
938
+ },
939
+ chorusEffectsSend: (_channel, _note, _prevValue) => { },
940
+ reverbEffectsSend: (_channel, _note, _prevValue) => { },
941
+ delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
942
+ freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
943
+ delayVibLFO: (channel, note, prevValue) => {
944
+ if (0 < channel.state.vibratoDepth) {
945
+ const now = this.audioContext.currentTime;
946
+ const prevStartTime = note.startTime +
947
+ prevValue * channel.state.vibratoDelay * 2;
948
+ if (now < prevStartTime)
949
+ return;
950
+ const startTime = note.startTime +
951
+ value * channel.state.vibratoDelay * 2;
952
+ note.vibratoLFO.stop(now);
953
+ note.vibratoLFO.start(startTime);
954
+ }
955
+ },
956
+ freqVibLFO: (channel, note, _prevValue) => {
957
+ if (0 < channel.state.vibratoDepth) {
958
+ const now = this.audioContext.currentTime;
959
+ note.vibratoLFO.frequency
960
+ .cancelScheduledValues(now)
961
+ .setValueAtTime(value * sate.vibratoRate, now);
962
+ }
963
+ },
964
+ };
965
+ }
966
+ getControllerState(channel, noteNumber, velocity) {
967
+ const state = new Float32Array(channel.state.array.length);
968
+ state.set(channel.state.array);
969
+ state[2] = velocity / 127;
970
+ state[3] = noteNumber / 127;
971
+ return state;
972
+ }
973
+ applyVoiceParams(channel, controllerType) {
974
+ channel.scheduledNotes.forEach((noteList) => {
975
+ for (let i = 0; i < noteList.length; i++) {
976
+ const note = noteList[i];
977
+ if (!note)
978
+ continue;
979
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
980
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
981
+ let appliedFilterEnvelope = false;
982
+ let appliedVolumeEnvelope = false;
983
+ for (const [key, value] of Object.entries(voiceParams)) {
984
+ const prevValue = note.voiceParams[key];
985
+ if (value === prevValue)
986
+ continue;
987
+ note.voiceParams[key] = value;
988
+ if (key in this.voiceParamsHandlers) {
989
+ this.voiceParamsHandlers[key](channel, note, prevValue);
990
+ }
991
+ else if (filterEnvelopeKeySet.has(key)) {
992
+ if (appliedFilterEnvelope)
993
+ continue;
994
+ appliedFilterEnvelope = true;
995
+ const noteVoiceParams = note.voiceParams;
996
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
997
+ const key = filterEnvelopeKeys[i];
998
+ if (key in voiceParams)
999
+ noteVoiceParams[key] = voiceParams[key];
1000
+ }
1001
+ this.setFilterEnvelope(channel, note);
1002
+ this.setPitch(channel, note);
1003
+ }
1004
+ else if (volumeEnvelopeKeySet.has(key)) {
1005
+ if (appliedVolumeEnvelope)
1006
+ continue;
1007
+ appliedVolumeEnvelope = true;
1008
+ const noteVoiceParams = note.voiceParams;
1009
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1010
+ const key = volumeEnvelopeKeys[i];
1011
+ if (key in voiceParams)
1012
+ noteVoiceParams[key] = voiceParams[key];
1013
+ }
1014
+ this.setVolumeEnvelope(channel, note);
1015
+ }
1016
+ }
1017
+ }
1018
+ });
1019
+ }
799
1020
  createControlChangeHandlers() {
800
1021
  return {
801
1022
  1: this.setModulationDepth,
@@ -812,13 +1033,13 @@ class MidyGM1 {
812
1033
  123: this.allNotesOff,
813
1034
  };
814
1035
  }
815
- handleControlChange(channelNumber, controller, value) {
816
- const handler = this.controlChangeHandlers[controller];
1036
+ handleControlChange(channelNumber, controllerType, value) {
1037
+ const handler = this.controlChangeHandlers[controllerType];
817
1038
  if (handler) {
818
1039
  handler.call(this, channelNumber, value);
819
1040
  }
820
1041
  else {
821
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
1042
+ console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
822
1043
  }
823
1044
  }
824
1045
  updateModulation(channel) {
@@ -829,11 +1050,10 @@ class MidyGM1 {
829
1050
  if (!note)
830
1051
  continue;
831
1052
  if (note.modulationDepth) {
832
- note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
1053
+ note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
833
1054
  }
834
1055
  else {
835
- const semitoneOffset = this.calcSemitoneOffset(channel);
836
- this.setPitch(note, semitoneOffset);
1056
+ this.setPitch(channel, note);
837
1057
  this.startModulation(channel, note, now);
838
1058
  }
839
1059
  }
@@ -841,16 +1061,17 @@ class MidyGM1 {
841
1061
  }
842
1062
  setModulationDepth(channelNumber, modulation) {
843
1063
  const channel = this.channels[channelNumber];
844
- channel.modulationDepth = (modulation / 127) * channel.modulationDepthRange;
1064
+ channel.state.modulationDepth = (modulation / 127) *
1065
+ channel.modulationDepthRange;
845
1066
  this.updateModulation(channel);
846
1067
  }
847
1068
  setVolume(channelNumber, volume) {
848
1069
  const channel = this.channels[channelNumber];
849
- channel.volume = volume / 127;
1070
+ channel.state.volume = volume / 127;
850
1071
  this.updateChannelVolume(channel);
851
1072
  }
852
1073
  panToGain(pan) {
853
- const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1074
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
854
1075
  return {
855
1076
  gainLeft: Math.cos(theta),
856
1077
  gainRight: Math.sin(theta),
@@ -858,12 +1079,12 @@ class MidyGM1 {
858
1079
  }
859
1080
  setPan(channelNumber, pan) {
860
1081
  const channel = this.channels[channelNumber];
861
- channel.pan = pan;
1082
+ channel.state.pan = pan / 127;
862
1083
  this.updateChannelVolume(channel);
863
1084
  }
864
1085
  setExpression(channelNumber, expression) {
865
1086
  const channel = this.channels[channelNumber];
866
- channel.expression = expression / 127;
1087
+ channel.state.expression = expression / 127;
867
1088
  this.updateChannelVolume(channel);
868
1089
  }
869
1090
  dataEntryLSB(channelNumber, value) {
@@ -872,8 +1093,9 @@ class MidyGM1 {
872
1093
  }
873
1094
  updateChannelVolume(channel) {
874
1095
  const now = this.audioContext.currentTime;
875
- const volume = channel.volume * channel.expression;
876
- const { gainLeft, gainRight } = this.panToGain(channel.pan);
1096
+ const state = channel.state;
1097
+ const volume = state.volume * state.expression;
1098
+ const { gainLeft, gainRight } = this.panToGain(state.pan);
877
1099
  channel.gainL.gain
878
1100
  .cancelScheduledValues(now)
879
1101
  .setValueAtTime(volume * gainLeft, now);
@@ -882,9 +1104,8 @@ class MidyGM1 {
882
1104
  .setValueAtTime(volume * gainRight, now);
883
1105
  }
884
1106
  setSustainPedal(channelNumber, value) {
885
- const isOn = value >= 64;
886
- this.channels[channelNumber].sustainPedal = isOn;
887
- if (!isOn) {
1107
+ this.channels[channelNumber].state.sustainPedal = value / 127;
1108
+ if (value < 64) {
888
1109
  this.releaseSustainPedal(channelNumber, value);
889
1110
  }
890
1111
  }
@@ -941,7 +1162,7 @@ class MidyGM1 {
941
1162
  this.channels[channelNumber].dataMSB = value;
942
1163
  this.handleRPN(channelNumber);
943
1164
  }
944
- updateDetune(channel, detuneChange) {
1165
+ updateDetune(channel, detune) {
945
1166
  const now = this.audioContext.currentTime;
946
1167
  channel.scheduledNotes.forEach((noteList) => {
947
1168
  for (let i = 0; i < noteList.length; i++) {
@@ -949,7 +1170,6 @@ class MidyGM1 {
949
1170
  if (!note)
950
1171
  continue;
951
1172
  const { bufferSource } = note;
952
- const detune = bufferSource.detune.value + detuneChange;
953
1173
  bufferSource.detune
954
1174
  .cancelScheduledValues(now)
955
1175
  .setValueAtTime(detune, now);
@@ -962,13 +1182,13 @@ class MidyGM1 {
962
1182
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
963
1183
  this.setPitchBendRange(channelNumber, pitchBendRange);
964
1184
  }
965
- setPitchBendRange(channelNumber, pitchBendRange) {
1185
+ setPitchBendRange(channelNumber, pitchWheelSensitivity) {
966
1186
  const channel = this.channels[channelNumber];
967
- const prevPitchBendRange = channel.pitchBendRange;
968
- channel.pitchBendRange = pitchBendRange;
969
- const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
970
- channel.pitchBend * 100;
971
- this.updateDetune(channel, detuneChange);
1187
+ const state = channel.state;
1188
+ state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
1189
+ const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
1190
+ this.updateDetune(channel, detune);
1191
+ this.applyVoiceParams(channel, 16);
972
1192
  }
973
1193
  handleFineTuningRPN(channelNumber) {
974
1194
  const channel = this.channels[channelNumber];
@@ -1000,7 +1220,26 @@ class MidyGM1 {
1000
1220
  return this.stopChannelNotes(channelNumber, 0, true);
1001
1221
  }
1002
1222
  resetAllControllers(channelNumber) {
1003
- Object.assign(this.channels[channelNumber], this.effectSettings);
1223
+ const stateTypes = [
1224
+ "expression",
1225
+ "modulationDepth",
1226
+ "sustainPedal",
1227
+ "pitchWheelSensitivity",
1228
+ ];
1229
+ const channel = this.channels[channelNumber];
1230
+ const state = channel.state;
1231
+ for (let i = 0; i < stateTypes.length; i++) {
1232
+ const type = stateTypes[i];
1233
+ state[type] = defaultControllerState[type];
1234
+ }
1235
+ const settingTypes = [
1236
+ "rpnMSB",
1237
+ "rpnLSB",
1238
+ ];
1239
+ for (let i = 0; i < settingTypes.length; i++) {
1240
+ const type = settingTypes[i];
1241
+ channel[type] = this.constructor.channelSettings[type];
1242
+ }
1004
1243
  }
1005
1244
  allNotesOff(channelNumber) {
1006
1245
  return this.stopChannelNotes(channelNumber, 0, false);
@@ -1025,11 +1264,8 @@ class MidyGM1 {
1025
1264
  GM1SystemOn() {
1026
1265
  for (let i = 0; i < this.channels.length; i++) {
1027
1266
  const channel = this.channels[i];
1028
- channel.bankMSB = 0;
1029
- channel.bankLSB = 0;
1030
1267
  channel.bank = 0;
1031
1268
  }
1032
- this.channels[9].bankMSB = 1;
1033
1269
  this.channels[9].bank = 128;
1034
1270
  }
1035
1271
  handleUniversalRealTimeExclusiveMessage(data) {
@@ -1091,28 +1327,15 @@ Object.defineProperty(MidyGM1, "channelSettings", {
1091
1327
  configurable: true,
1092
1328
  writable: true,
1093
1329
  value: {
1094
- volume: 100 / 127,
1095
- pan: 64,
1330
+ currentBufferSource: null,
1331
+ program: 0,
1096
1332
  bank: 0,
1097
1333
  dataMSB: 0,
1098
1334
  dataLSB: 0,
1099
- program: 0,
1100
- pitchBend: 0,
1335
+ rpnMSB: 127,
1336
+ rpnLSB: 127,
1101
1337
  fineTuning: 0, // cb
1102
1338
  coarseTuning: 0, // cb
1103
1339
  modulationDepthRange: 50, // cent
1104
1340
  }
1105
1341
  });
1106
- Object.defineProperty(MidyGM1, "effectSettings", {
1107
- enumerable: true,
1108
- configurable: true,
1109
- writable: true,
1110
- value: {
1111
- expression: 1,
1112
- modulationDepth: 0,
1113
- sustainPedal: false,
1114
- rpnMSB: 127,
1115
- rpnLSB: 127,
1116
- pitchBendRange: 2,
1117
- }
1118
- });