@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.
@@ -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);
@@ -519,41 +585,49 @@ export class MidyGMLite {
519
585
  return 8.176 * Math.pow(2, cent / 1200);
520
586
  }
521
587
  calcSemitoneOffset(channel) {
522
- return channel.pitchBend * channel.pitchBendRange;
523
- }
524
- calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
525
- return instrumentKey.playbackRate(noteNumber) *
526
- Math.pow(2, semitoneOffset / 12);
588
+ const pitchWheel = channel.state.pitchWheel * 2 - 1;
589
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
590
+ return pitchWheel * pitchWheelSensitivity;
527
591
  }
528
592
  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;
593
+ const now = this.audioContext.currentTime;
594
+ const { voiceParams, startTime } = note;
595
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
596
+ const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
597
+ const volDelay = startTime + voiceParams.volDelay;
598
+ const volAttack = volDelay + voiceParams.volAttack;
599
+ const volHold = volAttack + voiceParams.volHold;
600
+ const volDecay = volHold + voiceParams.volDecay;
536
601
  note.volumeNode.gain
537
- .cancelScheduledValues(startTime)
602
+ .cancelScheduledValues(now)
538
603
  .setValueAtTime(0, startTime)
539
604
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
540
605
  .exponentialRampToValueAtTime(attackVolume, volAttack)
541
606
  .setValueAtTime(attackVolume, volHold)
542
607
  .linearRampToValueAtTime(sustainVolume, volDecay);
543
608
  }
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);
609
+ setPlaybackRate(note) {
610
+ const now = this.audioContext.currentTime;
611
+ note.bufferSource.playbackRate
612
+ .cancelScheduledValues(now)
613
+ .setValueAtTime(note.voiceParams.playbackRate, now);
614
+ }
615
+ setPitch(channel, note) {
616
+ const now = this.audioContext.currentTime;
617
+ const { startTime } = note;
618
+ const basePitch = this.calcSemitoneOffset(channel) * 100;
619
+ note.bufferSource.detune
620
+ .cancelScheduledValues(now)
621
+ .setValueAtTime(basePitch, startTime);
622
+ const modEnvToPitch = note.voiceParams.modEnvToPitch;
548
623
  if (modEnvToPitch === 0)
549
624
  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
625
+ const peekPitch = basePitch + modEnvToPitch;
626
+ const modDelay = startTime + voiceParams.modDelay;
627
+ const modAttack = modDelay + voiceParams.modAttack;
628
+ const modHold = modAttack + voiceParams.modHold;
629
+ const modDecay = modHold + voiceParams.modDecay;
630
+ note.bufferSource.detune
557
631
  .setValueAtTime(basePitch, modDelay)
558
632
  .exponentialRampToValueAtTime(peekPitch, modAttack)
559
633
  .setValueAtTime(peekPitch, modHold)
@@ -565,20 +639,21 @@ export class MidyGMLite {
565
639
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
566
640
  }
567
641
  setFilterEnvelope(note) {
568
- const { instrumentKey, startTime } = note;
569
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
570
- const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc);
642
+ const now = this.audioContext.currentTime;
643
+ const { voiceParams, startTime } = note;
644
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc);
645
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
571
646
  const sustainFreq = baseFreq +
572
- (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
647
+ (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
573
648
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
574
649
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
575
650
  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;
651
+ const modDelay = startTime + voiceParams.modDelay;
652
+ const modAttack = modDelay + voiceParams.modAttack;
653
+ const modHold = modAttack + voiceParams.modHold;
654
+ const modDecay = modHold + voiceParams.modDecay;
580
655
  note.filterNode.frequency
581
- .cancelScheduledValues(startTime)
656
+ .cancelScheduledValues(now)
582
657
  .setValueAtTime(adjustedBaseFreq, startTime)
583
658
  .setValueAtTime(adjustedBaseFreq, modDelay)
584
659
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
@@ -586,25 +661,18 @@ export class MidyGMLite {
586
661
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
587
662
  }
588
663
  startModulation(channel, note, startTime) {
589
- const { instrumentKey } = note;
590
- const { modLfoToPitch, modLfoToVolume } = instrumentKey;
664
+ const { voiceParams } = note;
591
665
  note.modulationLFO = new OscillatorNode(this.audioContext, {
592
- frequency: this.centToHz(instrumentKey.freqModLFO),
666
+ frequency: this.centToHz(voiceParams.freqModLFO),
593
667
  });
594
668
  note.filterDepth = new GainNode(this.audioContext, {
595
- gain: instrumentKey.modLfoToFilterFc,
669
+ gain: voiceParams.modLfoToFilterFc,
596
670
  });
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,
606
- });
607
- note.modulationLFO.start(startTime + instrumentKey.delayModLFO);
671
+ note.modulationDepth = new GainNode(this.audioContext);
672
+ this.setModLfoToPitch(channel, note);
673
+ note.volumeDepth = new GainNode(this.audioContext);
674
+ this.setModLfoToVolume(note);
675
+ note.modulationLFO.start(startTime + voiceParams.delayModLFO);
608
676
  note.modulationLFO.connect(note.filterDepth);
609
677
  note.filterDepth.connect(note.filterNode.frequency);
610
678
  note.modulationLFO.connect(note.modulationDepth);
@@ -612,24 +680,23 @@ export class MidyGMLite {
612
680
  note.modulationLFO.connect(note.volumeDepth);
613
681
  note.volumeDepth.connect(note.volumeNode.gain);
614
682
  }
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);
683
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
684
+ const state = channel.state;
685
+ const voiceParams = voice.getAllParams(state.array);
686
+ const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
687
+ note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
619
688
  note.volumeNode = new GainNode(this.audioContext);
620
689
  note.filterNode = new BiquadFilterNode(this.audioContext, {
621
690
  type: "lowpass",
622
- Q: instrumentKey.initialFilterQ / 10, // dB
691
+ Q: voiceParams.initialFilterQ / 10, // dB
623
692
  });
624
693
  this.setVolumeEnvelope(note);
625
694
  this.setFilterEnvelope(note);
626
- if (0 < channel.modulationDepth) {
627
- this.setPitch(note, semitoneOffset);
695
+ this.setPlaybackRate(note);
696
+ if (0 < state.modulationDepth) {
697
+ this.setPitch(channel, note);
628
698
  this.startModulation(channel, note, startTime);
629
699
  }
630
- else {
631
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
632
- }
633
700
  note.bufferSource.connect(note.filterNode);
634
701
  note.filterNode.connect(note.volumeNode);
635
702
  note.bufferSource.start(startTime);
@@ -643,13 +710,13 @@ export class MidyGMLite {
643
710
  return;
644
711
  const soundFont = this.soundFonts[soundFontIndex];
645
712
  const isSF3 = soundFont.parsed.info.version.major === 3;
646
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
647
- if (!instrumentKey)
713
+ const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
714
+ if (!voice)
648
715
  return;
649
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
716
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
650
717
  note.volumeNode.connect(channel.gainL);
651
718
  note.volumeNode.connect(channel.gainR);
652
- const exclusiveClass = instrumentKey.exclusiveClass;
719
+ const exclusiveClass = note.voiceParams.exclusiveClass;
653
720
  if (exclusiveClass !== 0) {
654
721
  if (this.exclusiveClassMap.has(exclusiveClass)) {
655
722
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
@@ -694,10 +761,6 @@ export class MidyGMLite {
694
761
  note.modulationDepth.disconnect();
695
762
  note.modulationLFO.stop();
696
763
  }
697
- if (note.vibratoDepth) {
698
- note.vibratoDepth.disconnect();
699
- note.vibratoLFO.stop();
700
- }
701
764
  resolve();
702
765
  };
703
766
  note.bufferSource.stop(stopTime);
@@ -705,7 +768,7 @@ export class MidyGMLite {
705
768
  }
706
769
  scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
707
770
  const channel = this.channels[channelNumber];
708
- if (!force && channel.sustainPedal)
771
+ if (!force && 0.5 < channel.state.sustainPedal)
709
772
  return;
710
773
  if (!channel.scheduledNotes.has(noteNumber))
711
774
  return;
@@ -716,8 +779,8 @@ export class MidyGMLite {
716
779
  continue;
717
780
  if (note.ending)
718
781
  continue;
719
- const volRelease = endTime + note.instrumentKey.volRelease;
720
- const modRelease = endTime + note.instrumentKey.modRelease;
782
+ const volRelease = endTime + note.voiceParams.volRelease;
783
+ const modRelease = endTime + note.voiceParams.modRelease;
721
784
  note.filterNode.frequency
722
785
  .cancelScheduledValues(endTime)
723
786
  .linearRampToValueAtTime(0, modRelease);
@@ -733,7 +796,7 @@ export class MidyGMLite {
733
796
  const velocity = halfVelocity * 2;
734
797
  const channel = this.channels[channelNumber];
735
798
  const promises = [];
736
- channel.sustainPedal = false;
799
+ channel.state.sustainPedal = halfVelocity;
737
800
  channel.scheduledNotes.forEach((noteList) => {
738
801
  for (let i = 0; i < noteList.length; i++) {
739
802
  const note = noteList[i];
@@ -769,17 +832,137 @@ export class MidyGMLite {
769
832
  channel.program = program;
770
833
  }
771
834
  handlePitchBendMessage(channelNumber, lsb, msb) {
772
- const pitchBend = msb * 128 + lsb - 8192;
835
+ const pitchBend = msb * 128 + lsb;
773
836
  this.setPitchBend(channelNumber, pitchBend);
774
837
  }
775
- setPitchBend(channelNumber, pitchBend) {
838
+ setPitchBend(channelNumber, value) {
776
839
  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;
840
+ const state = channel.state;
841
+ state.pitchWheel = value / 16383;
842
+ const pitchWheel = (value - 8192) / 8192;
843
+ const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
781
844
  this.updateDetune(channel, detuneChange);
782
845
  }
846
+ setModLfoToPitch(channel, note) {
847
+ const now = this.audioContext.currentTime;
848
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
849
+ const modulationDepth = Math.abs(modLfoToPitch) +
850
+ channel.state.modulationDepth;
851
+ const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
852
+ note.modulationDepth.gain
853
+ .cancelScheduledValues(now)
854
+ .setValueAtTime(modulationDepth * modulationDepthSign, now);
855
+ }
856
+ setModLfoToVolume(note) {
857
+ const now = this.audioContext.currentTime;
858
+ const modLfoToVolume = note.voiceParams.modLfoToVolume;
859
+ const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
860
+ const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
861
+ note.volumeDepth.gain
862
+ .cancelScheduledValues(now)
863
+ .setValueAtTime(volumeDepth * volumeDepthSign, now);
864
+ }
865
+ setModLfoToFilterFc(note) {
866
+ const now = this.audioContext.currentTime;
867
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
868
+ note.filterDepth.gain
869
+ .cancelScheduledValues(now)
870
+ .setValueAtTime(modLfoToFilterFc, now);
871
+ }
872
+ setDelayModLFO(note) {
873
+ const now = this.audioContext.currentTime;
874
+ const startTime = note.startTime;
875
+ if (startTime < now)
876
+ return;
877
+ note.modulationLFO.stop(now);
878
+ note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
879
+ note.modulationLFO.connect(note.filterDepth);
880
+ }
881
+ setFreqModLFO(note) {
882
+ const now = this.audioContext.currentTime;
883
+ const freqModLFO = note.voiceParams.freqModLFO;
884
+ note.modulationLFO.frequency
885
+ .cancelScheduledValues(now)
886
+ .setValueAtTime(freqModLFO, now);
887
+ }
888
+ createVoiceParamsHandlers() {
889
+ return {
890
+ modLfoToPitch: (channel, note, _prevValue) => {
891
+ if (0 < channel.state.modulationDepth) {
892
+ this.setModLfoToPitch(channel, note);
893
+ }
894
+ },
895
+ vibLfoToPitch: (_channel, _note, _prevValue) => { },
896
+ modLfoToFilterFc: (channel, note, _prevValue) => {
897
+ if (0 < channel.state.modulationDepth)
898
+ this.setModLfoToFilterFc(note);
899
+ },
900
+ modLfoToVolume: (channel, note) => {
901
+ if (0 < channel.state.modulationDepth)
902
+ this.setModLfoToVolume(note);
903
+ },
904
+ chorusEffectsSend: (_channel, _note, _prevValue) => { },
905
+ reverbEffectsSend: (_channel, _note, _prevValue) => { },
906
+ delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
907
+ freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
908
+ delayVibLFO: (_channel, _note, _prevValue) => { },
909
+ freqVibLFO: (_channel, _note, _prevValue) => { },
910
+ };
911
+ }
912
+ getControllerState(channel, noteNumber, velocity) {
913
+ const state = new Float32Array(channel.state.array.length);
914
+ state.set(channel.state.array);
915
+ state[2] = velocity / 127;
916
+ state[3] = noteNumber / 127;
917
+ return state;
918
+ }
919
+ applyVoiceParams(channel, controllerType) {
920
+ channel.scheduledNotes.forEach((noteList) => {
921
+ for (let i = 0; i < noteList.length; i++) {
922
+ const note = noteList[i];
923
+ if (!note)
924
+ continue;
925
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
926
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
927
+ let appliedFilterEnvelope = false;
928
+ let appliedVolumeEnvelope = false;
929
+ for (const [key, value] of Object.entries(voiceParams)) {
930
+ const prevValue = note.voiceParams[key];
931
+ if (value === prevValue)
932
+ continue;
933
+ note.voiceParams[key] = value;
934
+ if (key in this.voiceParamsHandlers) {
935
+ this.voiceParamsHandlers[key](channel, note, prevValue);
936
+ }
937
+ else if (filterEnvelopeKeySet.has(key)) {
938
+ if (appliedFilterEnvelope)
939
+ continue;
940
+ appliedFilterEnvelope = true;
941
+ const noteVoiceParams = note.voiceParams;
942
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
943
+ const key = filterEnvelopeKeys[i];
944
+ if (key in voiceParams)
945
+ noteVoiceParams[key] = voiceParams[key];
946
+ }
947
+ this.setFilterEnvelope(channel, note);
948
+ this.setPitch(channel, note);
949
+ }
950
+ else if (volumeEnvelopeKeySet.has(key)) {
951
+ if (appliedVolumeEnvelope)
952
+ continue;
953
+ appliedVolumeEnvelope = true;
954
+ const noteVoiceParams = note.voiceParams;
955
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
956
+ const key = volumeEnvelopeKeys[i];
957
+ if (key in voiceParams)
958
+ noteVoiceParams[key] = voiceParams[key];
959
+ }
960
+ this.setVolumeEnvelope(channel, note);
961
+ }
962
+ }
963
+ }
964
+ });
965
+ }
783
966
  createControlChangeHandlers() {
784
967
  return {
785
968
  1: this.setModulationDepth,
@@ -796,13 +979,13 @@ export class MidyGMLite {
796
979
  123: this.allNotesOff,
797
980
  };
798
981
  }
799
- handleControlChange(channelNumber, controller, value) {
800
- const handler = this.controlChangeHandlers[controller];
982
+ handleControlChange(channelNumber, controllerType, value) {
983
+ const handler = this.controlChangeHandlers[controllerType];
801
984
  if (handler) {
802
985
  handler.call(this, channelNumber, value);
803
986
  }
804
987
  else {
805
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
988
+ console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
806
989
  }
807
990
  }
808
991
  updateModulation(channel) {
@@ -813,11 +996,10 @@ export class MidyGMLite {
813
996
  if (!note)
814
997
  continue;
815
998
  if (note.modulationDepth) {
816
- note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
999
+ note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
817
1000
  }
818
1001
  else {
819
- const semitoneOffset = this.calcSemitoneOffset(channel);
820
- this.setPitch(note, semitoneOffset);
1002
+ this.setPitch(channel, note);
821
1003
  this.startModulation(channel, note, now);
822
1004
  }
823
1005
  }
@@ -825,16 +1007,17 @@ export class MidyGMLite {
825
1007
  }
826
1008
  setModulationDepth(channelNumber, modulation) {
827
1009
  const channel = this.channels[channelNumber];
828
- channel.modulationDepth = (modulation / 127) * channel.modulationDepthRange;
1010
+ channel.state.modulationDepth = (modulation / 127) *
1011
+ channel.modulationDepthRange;
829
1012
  this.updateModulation(channel);
830
1013
  }
831
1014
  setVolume(channelNumber, volume) {
832
1015
  const channel = this.channels[channelNumber];
833
- channel.volume = volume / 127;
1016
+ channel.state.volume = volume / 127;
834
1017
  this.updateChannelVolume(channel);
835
1018
  }
836
1019
  panToGain(pan) {
837
- const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1020
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
838
1021
  return {
839
1022
  gainLeft: Math.cos(theta),
840
1023
  gainRight: Math.sin(theta),
@@ -842,12 +1025,12 @@ export class MidyGMLite {
842
1025
  }
843
1026
  setPan(channelNumber, pan) {
844
1027
  const channel = this.channels[channelNumber];
845
- channel.pan = pan;
1028
+ channel.state.pan = pan / 127;
846
1029
  this.updateChannelVolume(channel);
847
1030
  }
848
1031
  setExpression(channelNumber, expression) {
849
1032
  const channel = this.channels[channelNumber];
850
- channel.expression = expression / 127;
1033
+ channel.state.expression = expression / 127;
851
1034
  this.updateChannelVolume(channel);
852
1035
  }
853
1036
  dataEntryLSB(channelNumber, value) {
@@ -856,8 +1039,9 @@ export class MidyGMLite {
856
1039
  }
857
1040
  updateChannelVolume(channel) {
858
1041
  const now = this.audioContext.currentTime;
859
- const volume = channel.volume * channel.expression;
860
- const { gainLeft, gainRight } = this.panToGain(channel.pan);
1042
+ const state = channel.state;
1043
+ const volume = state.volume * state.expression;
1044
+ const { gainLeft, gainRight } = this.panToGain(state.pan);
861
1045
  channel.gainL.gain
862
1046
  .cancelScheduledValues(now)
863
1047
  .setValueAtTime(volume * gainLeft, now);
@@ -866,12 +1050,29 @@ export class MidyGMLite {
866
1050
  .setValueAtTime(volume * gainRight, now);
867
1051
  }
868
1052
  setSustainPedal(channelNumber, value) {
869
- const isOn = value >= 64;
870
- this.channels[channelNumber].sustainPedal = isOn;
871
- if (!isOn) {
1053
+ this.channels[channelNumber].state.sustainPedal = value / 127;
1054
+ if (value < 64) {
872
1055
  this.releaseSustainPedal(channelNumber, value);
873
1056
  }
874
1057
  }
1058
+ limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1059
+ if (maxLSB < channel.dataLSB) {
1060
+ channel.dataMSB++;
1061
+ channel.dataLSB = minLSB;
1062
+ }
1063
+ else if (channel.dataLSB < 0) {
1064
+ channel.dataMSB--;
1065
+ channel.dataLSB = maxLSB;
1066
+ }
1067
+ if (maxMSB < channel.dataMSB) {
1068
+ channel.dataMSB = maxMSB;
1069
+ channel.dataLSB = maxLSB;
1070
+ }
1071
+ else if (channel.dataMSB < 0) {
1072
+ channel.dataMSB = minMSB;
1073
+ channel.dataLSB = minLSB;
1074
+ }
1075
+ }
875
1076
  handleRPN(channelNumber) {
876
1077
  const channel = this.channels[channelNumber];
877
1078
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
@@ -893,7 +1094,7 @@ export class MidyGMLite {
893
1094
  this.channels[channelNumber].dataMSB = value;
894
1095
  this.handleRPN(channelNumber);
895
1096
  }
896
- updateDetune(channel, detuneChange) {
1097
+ updateDetune(channel, detune) {
897
1098
  const now = this.audioContext.currentTime;
898
1099
  channel.scheduledNotes.forEach((noteList) => {
899
1100
  for (let i = 0; i < noteList.length; i++) {
@@ -901,7 +1102,6 @@ export class MidyGMLite {
901
1102
  if (!note)
902
1103
  continue;
903
1104
  const { bufferSource } = note;
904
- const detune = bufferSource.detune.value + detuneChange;
905
1105
  bufferSource.detune
906
1106
  .cancelScheduledValues(now)
907
1107
  .setValueAtTime(detune, now);
@@ -914,19 +1114,38 @@ export class MidyGMLite {
914
1114
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
915
1115
  this.setPitchBendRange(channelNumber, pitchBendRange);
916
1116
  }
917
- setPitchBendRange(channelNumber, pitchBendRange) {
1117
+ setPitchBendRange(channelNumber, pitchWheelSensitivity) {
918
1118
  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);
1119
+ const state = channel.state;
1120
+ state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
1121
+ const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
1122
+ this.updateDetune(channel, detune);
1123
+ this.applyVoiceParams(channel, 16);
924
1124
  }
925
1125
  allSoundOff(channelNumber) {
926
1126
  return this.stopChannelNotes(channelNumber, 0, true);
927
1127
  }
928
1128
  resetAllControllers(channelNumber) {
929
- Object.assign(this.channels[channelNumber], this.effectSettings);
1129
+ const stateTypes = [
1130
+ "expression",
1131
+ "modulationDepth",
1132
+ "sustainPedal",
1133
+ "pitchWheelSensitivity",
1134
+ ];
1135
+ const channel = this.channels[channelNumber];
1136
+ const state = channel.state;
1137
+ for (let i = 0; i < stateTypes.length; i++) {
1138
+ const type = stateTypes[i];
1139
+ state[type] = defaultControllerState[type];
1140
+ }
1141
+ const settingTypes = [
1142
+ "rpnMSB",
1143
+ "rpnLSB",
1144
+ ];
1145
+ for (let i = 0; i < settingTypes.length; i++) {
1146
+ const type = settingTypes[i];
1147
+ channel[type] = this.constructor.channelSettings[type];
1148
+ }
930
1149
  }
931
1150
  allNotesOff(channelNumber) {
932
1151
  return this.stopChannelNotes(channelNumber, 0, false);
@@ -951,11 +1170,8 @@ export class MidyGMLite {
951
1170
  GM1SystemOn() {
952
1171
  for (let i = 0; i < this.channels.length; i++) {
953
1172
  const channel = this.channels[i];
954
- channel.bankMSB = 0;
955
- channel.bankLSB = 0;
956
1173
  channel.bank = 0;
957
1174
  }
958
- this.channels[9].bankMSB = 1;
959
1175
  this.channels[9].bank = 128;
960
1176
  }
961
1177
  handleUniversalRealTimeExclusiveMessage(data) {
@@ -1016,26 +1232,12 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1016
1232
  configurable: true,
1017
1233
  writable: true,
1018
1234
  value: {
1019
- volume: 100 / 127,
1020
- pan: 64,
1235
+ currentBufferSource: null,
1236
+ program: 0,
1021
1237
  bank: 0,
1022
1238
  dataMSB: 0,
1023
1239
  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
1240
  rpnMSB: 127,
1038
1241
  rpnLSB: 127,
1039
- pitchBendRange: 2,
1040
1242
  }
1041
1243
  });