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