@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.
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);
@@ -527,50 +593,74 @@ export class MidyGM1 {
527
593
  cbToRatio(cb) {
528
594
  return Math.pow(10, cb / 200);
529
595
  }
596
+ rateToCent(rate) {
597
+ return 1200 * Math.log2(rate);
598
+ }
599
+ centToRate(cent) {
600
+ return Math.pow(2, cent / 1200);
601
+ }
530
602
  centToHz(cent) {
531
- return 8.176 * Math.pow(2, cent / 1200);
603
+ return 8.176 * this.centToRate(cent);
532
604
  }
533
- calcSemitoneOffset(channel) {
605
+ calcChannelDetune(channel) {
534
606
  const tuning = channel.coarseTuning + channel.fineTuning;
535
- return channel.pitchBend * channel.pitchBendRange + tuning;
607
+ const pitchWheel = channel.state.pitchWheel * 2 - 1;
608
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
609
+ const pitch = pitchWheel * pitchWheelSensitivity;
610
+ return tuning + pitch;
536
611
  }
537
- calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
538
- return instrumentKey.playbackRate(noteNumber) *
539
- Math.pow(2, semitoneOffset / 12);
612
+ updateDetune(channel) {
613
+ const now = this.audioContext.currentTime;
614
+ channel.scheduledNotes.forEach((noteList) => {
615
+ for (let i = 0; i < noteList.length; i++) {
616
+ const note = noteList[i];
617
+ if (!note)
618
+ continue;
619
+ note.bufferSource.detune
620
+ .cancelScheduledValues(now)
621
+ .setValueAtTime(channel.detune, now);
622
+ }
623
+ });
540
624
  }
541
625
  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;
626
+ const now = this.audioContext.currentTime;
627
+ const { voiceParams, startTime } = note;
628
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
629
+ const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
630
+ const volDelay = startTime + voiceParams.volDelay;
631
+ const volAttack = volDelay + voiceParams.volAttack;
632
+ const volHold = volAttack + voiceParams.volHold;
633
+ const volDecay = volHold + voiceParams.volDecay;
549
634
  note.volumeNode.gain
550
- .cancelScheduledValues(startTime)
635
+ .cancelScheduledValues(now)
551
636
  .setValueAtTime(0, startTime)
552
637
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
553
638
  .exponentialRampToValueAtTime(attackVolume, volAttack)
554
639
  .setValueAtTime(attackVolume, volHold)
555
640
  .linearRampToValueAtTime(sustainVolume, volDecay);
556
641
  }
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);
642
+ setPitchEnvelope(note) {
643
+ const now = this.audioContext.currentTime;
644
+ const { voiceParams } = note;
645
+ const baseRate = voiceParams.playbackRate;
646
+ note.bufferSource.playbackRate
647
+ .cancelScheduledValues(now)
648
+ .setValueAtTime(baseRate, now);
649
+ const modEnvToPitch = voiceParams.modEnvToPitch;
561
650
  if (modEnvToPitch === 0)
562
651
  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
570
- .setValueAtTime(basePitch, modDelay)
571
- .exponentialRampToValueAtTime(peekPitch, modAttack)
572
- .setValueAtTime(peekPitch, modHold)
573
- .linearRampToValueAtTime(basePitch, modDecay);
652
+ const basePitch = this.rateToCent(baseRate);
653
+ const peekPitch = basePitch + modEnvToPitch;
654
+ const peekRate = this.centToRate(peekPitch);
655
+ const modDelay = startTime + voiceParams.modDelay;
656
+ const modAttack = modDelay + voiceParams.modAttack;
657
+ const modHold = modAttack + voiceParams.modHold;
658
+ const modDecay = modHold + voiceParams.modDecay;
659
+ note.bufferSource.playbackRate
660
+ .setValueAtTime(baseRate, modDelay)
661
+ .exponentialRampToValueAtTime(peekRate, modAttack)
662
+ .setValueAtTime(peekRate, modHold)
663
+ .linearRampToValueAtTime(baseRate, modDecay);
574
664
  }
575
665
  clampCutoffFrequency(frequency) {
576
666
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -578,20 +668,21 @@ export class MidyGM1 {
578
668
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
579
669
  }
580
670
  setFilterEnvelope(note) {
581
- const { instrumentKey, startTime } = note;
582
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
583
- const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc);
671
+ const now = this.audioContext.currentTime;
672
+ const { voiceParams, startTime } = note;
673
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc);
674
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
584
675
  const sustainFreq = baseFreq +
585
- (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
676
+ (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
586
677
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
587
678
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
588
679
  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;
680
+ const modDelay = startTime + voiceParams.modDelay;
681
+ const modAttack = modDelay + voiceParams.modAttack;
682
+ const modHold = modAttack + voiceParams.modHold;
683
+ const modDecay = modHold + voiceParams.modDecay;
593
684
  note.filterNode.frequency
594
- .cancelScheduledValues(startTime)
685
+ .cancelScheduledValues(now)
595
686
  .setValueAtTime(adjustedBaseFreq, startTime)
596
687
  .setValueAtTime(adjustedBaseFreq, modDelay)
597
688
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
@@ -599,25 +690,18 @@ export class MidyGM1 {
599
690
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
600
691
  }
601
692
  startModulation(channel, note, startTime) {
602
- const { instrumentKey } = note;
603
- const { modLfoToPitch, modLfoToVolume } = instrumentKey;
693
+ const { voiceParams } = note;
604
694
  note.modulationLFO = new OscillatorNode(this.audioContext, {
605
- frequency: this.centToHz(instrumentKey.freqModLFO),
695
+ frequency: this.centToHz(voiceParams.freqModLFO),
606
696
  });
607
697
  note.filterDepth = new GainNode(this.audioContext, {
608
- gain: instrumentKey.modLfoToFilterFc,
698
+ gain: voiceParams.modLfoToFilterFc,
609
699
  });
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,
619
- });
620
- note.modulationLFO.start(startTime + instrumentKey.delayModLFO);
700
+ note.modulationDepth = new GainNode(this.audioContext);
701
+ this.setModLfoToPitch(channel, note);
702
+ note.volumeDepth = new GainNode(this.audioContext);
703
+ this.setModLfoToVolume(note);
704
+ note.modulationLFO.start(startTime + voiceParams.delayModLFO);
621
705
  note.modulationLFO.connect(note.filterDepth);
622
706
  note.filterDepth.connect(note.filterNode.frequency);
623
707
  note.modulationLFO.connect(note.modulationDepth);
@@ -625,24 +709,22 @@ export class MidyGM1 {
625
709
  note.modulationLFO.connect(note.volumeDepth);
626
710
  note.volumeDepth.connect(note.volumeNode.gain);
627
711
  }
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);
712
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
713
+ const state = channel.state;
714
+ const voiceParams = voice.getAllParams(state.array);
715
+ const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
716
+ note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
632
717
  note.volumeNode = new GainNode(this.audioContext);
633
718
  note.filterNode = new BiquadFilterNode(this.audioContext, {
634
719
  type: "lowpass",
635
- Q: instrumentKey.initialFilterQ / 10, // dB
720
+ Q: voiceParams.initialFilterQ / 10, // dB
636
721
  });
637
722
  this.setVolumeEnvelope(note);
638
723
  this.setFilterEnvelope(note);
639
- if (0 < channel.modulationDepth) {
640
- this.setPitch(note, semitoneOffset);
724
+ this.setPitchEnvelope(note);
725
+ if (0 < state.modulationDepth) {
641
726
  this.startModulation(channel, note, startTime);
642
727
  }
643
- else {
644
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
645
- }
646
728
  note.bufferSource.connect(note.filterNode);
647
729
  note.filterNode.connect(note.volumeNode);
648
730
  note.bufferSource.start(startTime);
@@ -650,19 +732,19 @@ export class MidyGM1 {
650
732
  }
651
733
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
652
734
  const channel = this.channels[channelNumber];
653
- const bankNumber = 0;
735
+ const bankNumber = channel.bank;
654
736
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
655
737
  if (soundFontIndex === undefined)
656
738
  return;
657
739
  const soundFont = this.soundFonts[soundFontIndex];
658
740
  const isSF3 = soundFont.parsed.info.version.major === 3;
659
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
660
- if (!instrumentKey)
741
+ const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
742
+ if (!voice)
661
743
  return;
662
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
744
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
663
745
  note.volumeNode.connect(channel.gainL);
664
746
  note.volumeNode.connect(channel.gainR);
665
- const exclusiveClass = instrumentKey.exclusiveClass;
747
+ const exclusiveClass = note.voiceParams.exclusiveClass;
666
748
  if (exclusiveClass !== 0) {
667
749
  if (this.exclusiveClassMap.has(exclusiveClass)) {
668
750
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
@@ -718,7 +800,7 @@ export class MidyGM1 {
718
800
  }
719
801
  scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
720
802
  const channel = this.channels[channelNumber];
721
- if (!force && channel.sustainPedal)
803
+ if (!force && 0.5 < channel.state.sustainPedal)
722
804
  return;
723
805
  if (!channel.scheduledNotes.has(noteNumber))
724
806
  return;
@@ -729,8 +811,8 @@ export class MidyGM1 {
729
811
  continue;
730
812
  if (note.ending)
731
813
  continue;
732
- const volRelease = endTime + note.instrumentKey.volRelease;
733
- const modRelease = endTime + note.instrumentKey.modRelease;
814
+ const volRelease = endTime + note.voiceParams.volRelease;
815
+ const modRelease = endTime + note.voiceParams.modRelease;
734
816
  note.filterNode.frequency
735
817
  .cancelScheduledValues(endTime)
736
818
  .linearRampToValueAtTime(0, modRelease);
@@ -746,7 +828,7 @@ export class MidyGM1 {
746
828
  const velocity = halfVelocity * 2;
747
829
  const channel = this.channels[channelNumber];
748
830
  const promises = [];
749
- channel.sustainPedal = false;
831
+ channel.state.sustainPedal = halfVelocity;
750
832
  channel.scheduledNotes.forEach((noteList) => {
751
833
  for (let i = 0; i < noteList.length; i++) {
752
834
  const note = noteList[i];
@@ -782,16 +864,171 @@ export class MidyGM1 {
782
864
  channel.program = program;
783
865
  }
784
866
  handlePitchBendMessage(channelNumber, lsb, msb) {
785
- const pitchBend = msb * 128 + lsb - 8192;
867
+ const pitchBend = msb * 128 + lsb;
786
868
  this.setPitchBend(channelNumber, pitchBend);
787
869
  }
788
- setPitchBend(channelNumber, pitchBend) {
870
+ setPitchBend(channelNumber, value) {
789
871
  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;
794
- this.updateDetune(channel, detuneChange);
872
+ const state = channel.state;
873
+ const prev = state.pitchWheel * 2 - 1;
874
+ const next = (value - 8192) / 8192;
875
+ state.pitchWheel = value / 16383;
876
+ channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
877
+ this.updateDetune(channel);
878
+ this.applyVoiceParams(channel, 14);
879
+ }
880
+ setModLfoToPitch(channel, note) {
881
+ const now = this.audioContext.currentTime;
882
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
883
+ const modulationDepth = Math.abs(modLfoToPitch) +
884
+ channel.state.modulationDepth;
885
+ const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
886
+ note.modulationDepth.gain
887
+ .cancelScheduledValues(now)
888
+ .setValueAtTime(modulationDepth * modulationDepthSign, now);
889
+ }
890
+ setVibLfoToPitch(channel, note) {
891
+ const now = this.audioContext.currentTime;
892
+ const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
893
+ const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
894
+ 2;
895
+ const vibratoDepthSign = 0 < vibLfoToPitch;
896
+ note.vibratoDepth.gain
897
+ .cancelScheduledValues(now)
898
+ .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
899
+ }
900
+ setModLfoToFilterFc(note) {
901
+ const now = this.audioContext.currentTime;
902
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
903
+ note.filterDepth.gain
904
+ .cancelScheduledValues(now)
905
+ .setValueAtTime(modLfoToFilterFc, now);
906
+ }
907
+ setModLfoToVolume(note) {
908
+ const now = this.audioContext.currentTime;
909
+ const modLfoToVolume = note.voiceParams.modLfoToVolume;
910
+ const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
911
+ const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
912
+ note.volumeDepth.gain
913
+ .cancelScheduledValues(now)
914
+ .setValueAtTime(volumeDepth * volumeDepthSign, now);
915
+ }
916
+ setDelayModLFO(note) {
917
+ const now = this.audioContext.currentTime;
918
+ const startTime = note.startTime;
919
+ if (startTime < now)
920
+ return;
921
+ note.modulationLFO.stop(now);
922
+ note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
923
+ note.modulationLFO.connect(note.filterDepth);
924
+ }
925
+ setFreqModLFO(note) {
926
+ const now = this.audioContext.currentTime;
927
+ const freqModLFO = note.voiceParams.freqModLFO;
928
+ note.modulationLFO.frequency
929
+ .cancelScheduledValues(now)
930
+ .setValueAtTime(freqModLFO, now);
931
+ }
932
+ createVoiceParamsHandlers() {
933
+ return {
934
+ modLfoToPitch: (channel, note, _prevValue) => {
935
+ if (0 < channel.state.modulationDepth) {
936
+ this.setModLfoToPitch(channel, note);
937
+ }
938
+ },
939
+ vibLfoToPitch: (channel, note, _prevValue) => {
940
+ if (0 < channel.state.vibratoDepth) {
941
+ this.setVibLfoToPitch(channel, note);
942
+ }
943
+ },
944
+ modLfoToFilterFc: (channel, note, _prevValue) => {
945
+ if (0 < channel.state.modulationDepth)
946
+ this.setModLfoToFilterFc(note);
947
+ },
948
+ modLfoToVolume: (channel, note) => {
949
+ if (0 < channel.state.modulationDepth)
950
+ this.setModLfoToVolume(note);
951
+ },
952
+ chorusEffectsSend: (_channel, _note, _prevValue) => { },
953
+ reverbEffectsSend: (_channel, _note, _prevValue) => { },
954
+ delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
955
+ freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
956
+ delayVibLFO: (channel, note, prevValue) => {
957
+ if (0 < channel.state.vibratoDepth) {
958
+ const now = this.audioContext.currentTime;
959
+ const prevStartTime = note.startTime +
960
+ prevValue * channel.state.vibratoDelay * 2;
961
+ if (now < prevStartTime)
962
+ return;
963
+ const startTime = note.startTime +
964
+ value * channel.state.vibratoDelay * 2;
965
+ note.vibratoLFO.stop(now);
966
+ note.vibratoLFO.start(startTime);
967
+ }
968
+ },
969
+ freqVibLFO: (channel, note, _prevValue) => {
970
+ if (0 < channel.state.vibratoDepth) {
971
+ const now = this.audioContext.currentTime;
972
+ note.vibratoLFO.frequency
973
+ .cancelScheduledValues(now)
974
+ .setValueAtTime(value * sate.vibratoRate, now);
975
+ }
976
+ },
977
+ };
978
+ }
979
+ getControllerState(channel, noteNumber, velocity) {
980
+ const state = new Float32Array(channel.state.array.length);
981
+ state.set(channel.state.array);
982
+ state[2] = velocity / 127;
983
+ state[3] = noteNumber / 127;
984
+ return state;
985
+ }
986
+ applyVoiceParams(channel, controllerType) {
987
+ channel.scheduledNotes.forEach((noteList) => {
988
+ for (let i = 0; i < noteList.length; i++) {
989
+ const note = noteList[i];
990
+ if (!note)
991
+ continue;
992
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
993
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
994
+ let appliedFilterEnvelope = false;
995
+ let appliedVolumeEnvelope = false;
996
+ for (const [key, value] of Object.entries(voiceParams)) {
997
+ const prevValue = note.voiceParams[key];
998
+ if (value === prevValue)
999
+ continue;
1000
+ note.voiceParams[key] = value;
1001
+ if (key in this.voiceParamsHandlers) {
1002
+ this.voiceParamsHandlers[key](channel, note, prevValue);
1003
+ }
1004
+ else if (filterEnvelopeKeySet.has(key)) {
1005
+ if (appliedFilterEnvelope)
1006
+ continue;
1007
+ appliedFilterEnvelope = true;
1008
+ const noteVoiceParams = note.voiceParams;
1009
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1010
+ const key = filterEnvelopeKeys[i];
1011
+ if (key in voiceParams)
1012
+ noteVoiceParams[key] = voiceParams[key];
1013
+ }
1014
+ this.setFilterEnvelope(channel, note);
1015
+ this.setPitchEnvelope(note);
1016
+ }
1017
+ else if (volumeEnvelopeKeySet.has(key)) {
1018
+ if (appliedVolumeEnvelope)
1019
+ continue;
1020
+ appliedVolumeEnvelope = true;
1021
+ const noteVoiceParams = note.voiceParams;
1022
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1023
+ const key = volumeEnvelopeKeys[i];
1024
+ if (key in voiceParams)
1025
+ noteVoiceParams[key] = voiceParams[key];
1026
+ }
1027
+ this.setVolumeEnvelope(channel, note);
1028
+ }
1029
+ }
1030
+ }
1031
+ });
795
1032
  }
796
1033
  createControlChangeHandlers() {
797
1034
  return {
@@ -809,13 +1046,13 @@ export class MidyGM1 {
809
1046
  123: this.allNotesOff,
810
1047
  };
811
1048
  }
812
- handleControlChange(channelNumber, controller, value) {
813
- const handler = this.controlChangeHandlers[controller];
1049
+ handleControlChange(channelNumber, controllerType, value) {
1050
+ const handler = this.controlChangeHandlers[controllerType];
814
1051
  if (handler) {
815
1052
  handler.call(this, channelNumber, value);
816
1053
  }
817
1054
  else {
818
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
1055
+ console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
819
1056
  }
820
1057
  }
821
1058
  updateModulation(channel) {
@@ -826,11 +1063,10 @@ export class MidyGM1 {
826
1063
  if (!note)
827
1064
  continue;
828
1065
  if (note.modulationDepth) {
829
- note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
1066
+ note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
830
1067
  }
831
1068
  else {
832
- const semitoneOffset = this.calcSemitoneOffset(channel);
833
- this.setPitch(note, semitoneOffset);
1069
+ this.setPitchEnvelope(note);
834
1070
  this.startModulation(channel, note, now);
835
1071
  }
836
1072
  }
@@ -838,16 +1074,17 @@ export class MidyGM1 {
838
1074
  }
839
1075
  setModulationDepth(channelNumber, modulation) {
840
1076
  const channel = this.channels[channelNumber];
841
- channel.modulationDepth = (modulation / 127) * channel.modulationDepthRange;
1077
+ channel.state.modulationDepth = (modulation / 127) *
1078
+ channel.modulationDepthRange;
842
1079
  this.updateModulation(channel);
843
1080
  }
844
1081
  setVolume(channelNumber, volume) {
845
1082
  const channel = this.channels[channelNumber];
846
- channel.volume = volume / 127;
1083
+ channel.state.volume = volume / 127;
847
1084
  this.updateChannelVolume(channel);
848
1085
  }
849
1086
  panToGain(pan) {
850
- const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1087
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
851
1088
  return {
852
1089
  gainLeft: Math.cos(theta),
853
1090
  gainRight: Math.sin(theta),
@@ -855,12 +1092,12 @@ export class MidyGM1 {
855
1092
  }
856
1093
  setPan(channelNumber, pan) {
857
1094
  const channel = this.channels[channelNumber];
858
- channel.pan = pan;
1095
+ channel.state.pan = pan / 127;
859
1096
  this.updateChannelVolume(channel);
860
1097
  }
861
1098
  setExpression(channelNumber, expression) {
862
1099
  const channel = this.channels[channelNumber];
863
- channel.expression = expression / 127;
1100
+ channel.state.expression = expression / 127;
864
1101
  this.updateChannelVolume(channel);
865
1102
  }
866
1103
  dataEntryLSB(channelNumber, value) {
@@ -869,8 +1106,9 @@ export class MidyGM1 {
869
1106
  }
870
1107
  updateChannelVolume(channel) {
871
1108
  const now = this.audioContext.currentTime;
872
- const volume = channel.volume * channel.expression;
873
- const { gainLeft, gainRight } = this.panToGain(channel.pan);
1109
+ const state = channel.state;
1110
+ const volume = state.volume * state.expression;
1111
+ const { gainLeft, gainRight } = this.panToGain(state.pan);
874
1112
  channel.gainL.gain
875
1113
  .cancelScheduledValues(now)
876
1114
  .setValueAtTime(volume * gainLeft, now);
@@ -879,9 +1117,8 @@ export class MidyGM1 {
879
1117
  .setValueAtTime(volume * gainRight, now);
880
1118
  }
881
1119
  setSustainPedal(channelNumber, value) {
882
- const isOn = value >= 64;
883
- this.channels[channelNumber].sustainPedal = isOn;
884
- if (!isOn) {
1120
+ this.channels[channelNumber].state.sustainPedal = value / 127;
1121
+ if (value < 64) {
885
1122
  this.releaseSustainPedal(channelNumber, value);
886
1123
  }
887
1124
  }
@@ -938,66 +1175,74 @@ export class MidyGM1 {
938
1175
  this.channels[channelNumber].dataMSB = value;
939
1176
  this.handleRPN(channelNumber);
940
1177
  }
941
- updateDetune(channel, detuneChange) {
942
- const now = this.audioContext.currentTime;
943
- channel.scheduledNotes.forEach((noteList) => {
944
- for (let i = 0; i < noteList.length; i++) {
945
- const note = noteList[i];
946
- if (!note)
947
- continue;
948
- const { bufferSource } = note;
949
- const detune = bufferSource.detune.value + detuneChange;
950
- bufferSource.detune
951
- .cancelScheduledValues(now)
952
- .setValueAtTime(detune, now);
953
- }
954
- });
955
- }
956
1178
  handlePitchBendRangeRPN(channelNumber) {
957
1179
  const channel = this.channels[channelNumber];
958
1180
  this.limitData(channel, 0, 127, 0, 99);
959
1181
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
960
1182
  this.setPitchBendRange(channelNumber, pitchBendRange);
961
1183
  }
962
- setPitchBendRange(channelNumber, pitchBendRange) {
1184
+ setPitchBendRange(channelNumber, value) {
963
1185
  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);
1186
+ const state = channel.state;
1187
+ const prev = state.pitchWheelSensitivity;
1188
+ const next = value / 128;
1189
+ state.pitchWheelSensitivity = next;
1190
+ channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1191
+ this.updateDetune(channel);
1192
+ this.applyVoiceParams(channel, 16);
969
1193
  }
970
1194
  handleFineTuningRPN(channelNumber) {
971
1195
  const channel = this.channels[channelNumber];
972
1196
  this.limitData(channel, 0, 127, 0, 127);
973
- const fineTuning = (channel.dataMSB * 128 + channel.dataLSB - 8192) / 8192;
1197
+ const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
974
1198
  this.setFineTuning(channelNumber, fineTuning);
975
1199
  }
976
- setFineTuning(channelNumber, fineTuning) {
1200
+ setFineTuning(channelNumber, value) {
977
1201
  const channel = this.channels[channelNumber];
978
- const prevFineTuning = channel.fineTuning;
979
- channel.fineTuning = fineTuning;
980
- const detuneChange = channel.fineTuning - prevFineTuning;
981
- this.updateDetune(channel, detuneChange);
1202
+ const prev = channel.fineTuning;
1203
+ const next = (value - 8192) / 8.192; // cent
1204
+ channel.fineTuning = next;
1205
+ channel.detune += next - prev;
1206
+ this.updateDetune(channel);
982
1207
  }
983
1208
  handleCoarseTuningRPN(channelNumber) {
984
1209
  const channel = this.channels[channelNumber];
985
1210
  this.limitDataMSB(channel, 0, 127);
986
- const coarseTuning = channel.dataMSB - 64;
987
- this.setFineTuning(channelNumber, coarseTuning);
1211
+ const coarseTuning = channel.dataMSB;
1212
+ this.setCoarseTuning(channelNumber, coarseTuning);
988
1213
  }
989
- setCoarseTuning(channelNumber, coarseTuning) {
1214
+ setCoarseTuning(channelNumber, value) {
990
1215
  const channel = this.channels[channelNumber];
991
- const prevCoarseTuning = channel.coarseTuning;
992
- channel.coarseTuning = coarseTuning;
993
- const detuneChange = channel.coarseTuning - prevCoarseTuning;
994
- this.updateDetune(channel, detuneChange);
1216
+ const prev = channel.coarseTuning;
1217
+ const next = (value - 64) * 100; // cent
1218
+ channel.coarseTuning = next;
1219
+ channel.detune += next - prev;
1220
+ this.updateDetune(channel);
995
1221
  }
996
1222
  allSoundOff(channelNumber) {
997
1223
  return this.stopChannelNotes(channelNumber, 0, true);
998
1224
  }
999
1225
  resetAllControllers(channelNumber) {
1000
- Object.assign(this.channels[channelNumber], this.effectSettings);
1226
+ const stateTypes = [
1227
+ "expression",
1228
+ "modulationDepth",
1229
+ "sustainPedal",
1230
+ "pitchWheelSensitivity",
1231
+ ];
1232
+ const channel = this.channels[channelNumber];
1233
+ const state = channel.state;
1234
+ for (let i = 0; i < stateTypes.length; i++) {
1235
+ const type = stateTypes[i];
1236
+ state[type] = defaultControllerState[type];
1237
+ }
1238
+ const settingTypes = [
1239
+ "rpnMSB",
1240
+ "rpnLSB",
1241
+ ];
1242
+ for (let i = 0; i < settingTypes.length; i++) {
1243
+ const type = settingTypes[i];
1244
+ channel[type] = this.constructor.channelSettings[type];
1245
+ }
1001
1246
  }
1002
1247
  allNotesOff(channelNumber) {
1003
1248
  return this.stopChannelNotes(channelNumber, 0, false);
@@ -1022,11 +1267,8 @@ export class MidyGM1 {
1022
1267
  GM1SystemOn() {
1023
1268
  for (let i = 0; i < this.channels.length; i++) {
1024
1269
  const channel = this.channels[i];
1025
- channel.bankMSB = 0;
1026
- channel.bankLSB = 0;
1027
1270
  channel.bank = 0;
1028
1271
  }
1029
- this.channels[9].bankMSB = 1;
1030
1272
  this.channels[9].bank = 128;
1031
1273
  }
1032
1274
  handleUniversalRealTimeExclusiveMessage(data) {
@@ -1087,28 +1329,16 @@ Object.defineProperty(MidyGM1, "channelSettings", {
1087
1329
  configurable: true,
1088
1330
  writable: true,
1089
1331
  value: {
1090
- volume: 100 / 127,
1091
- pan: 64,
1332
+ currentBufferSource: null,
1333
+ detune: 0,
1334
+ program: 0,
1092
1335
  bank: 0,
1093
1336
  dataMSB: 0,
1094
1337
  dataLSB: 0,
1095
- program: 0,
1096
- pitchBend: 0,
1338
+ rpnMSB: 127,
1339
+ rpnLSB: 127,
1097
1340
  fineTuning: 0, // cb
1098
1341
  coarseTuning: 0, // cb
1099
1342
  modulationDepthRange: 50, // cent
1100
1343
  }
1101
1344
  });
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
- });