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