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