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