@marmooo/midy 0.1.6 → 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.
Files changed (37) hide show
  1. package/esm/midy-GM1.d.ts +53 -27
  2. package/esm/midy-GM1.d.ts.map +1 -1
  3. package/esm/midy-GM1.js +398 -146
  4. package/esm/midy-GM2.d.ts +55 -35
  5. package/esm/midy-GM2.d.ts.map +1 -1
  6. package/esm/midy-GM2.js +646 -244
  7. package/esm/midy-GMLite.d.ts +51 -26
  8. package/esm/midy-GMLite.d.ts.map +1 -1
  9. package/esm/midy-GMLite.js +379 -148
  10. package/esm/midy.d.ts +55 -40
  11. package/esm/midy.d.ts.map +1 -1
  12. package/esm/midy.js +662 -263
  13. package/package.json +5 -1
  14. package/script/midy-GM1.d.ts +53 -27
  15. package/script/midy-GM1.d.ts.map +1 -1
  16. package/script/midy-GM1.js +401 -149
  17. package/script/midy-GM2.d.ts +55 -35
  18. package/script/midy-GM2.d.ts.map +1 -1
  19. package/script/midy-GM2.js +649 -247
  20. package/script/midy-GMLite.d.ts +51 -26
  21. package/script/midy-GMLite.d.ts.map +1 -1
  22. package/script/midy-GMLite.js +382 -151
  23. package/script/midy.d.ts +55 -40
  24. package/script/midy.d.ts.map +1 -1
  25. package/script/midy.js +665 -266
  26. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts +0 -149
  27. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts.map +0 -1
  28. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js +0 -180
  29. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +0 -84
  30. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +0 -1
  31. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +0 -216
  32. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts +0 -149
  33. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts.map +0 -1
  34. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js +0 -190
  35. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +0 -84
  36. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +0 -1
  37. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +0 -221
package/esm/midy-GM1.js CHANGED
@@ -1,7 +1,7 @@
1
- import { parseMidi } from "./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js";
2
- import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js";
1
+ import { parseMidi } from "midi-file";
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", {
@@ -160,8 +226,15 @@ export class MidyGM1 {
160
226
  writable: true,
161
227
  value: []
162
228
  });
229
+ Object.defineProperty(this, "exclusiveClassMap", {
230
+ enumerable: true,
231
+ configurable: true,
232
+ writable: true,
233
+ value: new Map()
234
+ });
163
235
  this.audioContext = audioContext;
164
236
  this.masterGain = new GainNode(audioContext);
237
+ this.voiceParamsHandlers = this.createVoiceParamsHandlers();
165
238
  this.controlChangeHandlers = this.createControlChangeHandlers();
166
239
  this.channels = this.createChannels(audioContext);
167
240
  this.masterGain.connect(audioContext.destination);
@@ -204,7 +277,7 @@ export class MidyGM1 {
204
277
  this.totalTime = this.calcTotalTime();
205
278
  }
206
279
  setChannelAudioNodes(audioContext) {
207
- const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
280
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
208
281
  const gainL = new GainNode(audioContext, { gain: gainLeft });
209
282
  const gainR = new GainNode(audioContext, { gain: gainRight });
210
283
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -221,45 +294,50 @@ export class MidyGM1 {
221
294
  const channels = Array.from({ length: 16 }, () => {
222
295
  return {
223
296
  ...this.constructor.channelSettings,
224
- ...this.constructor.effectSettings,
297
+ state: new ControllerState(),
225
298
  ...this.setChannelAudioNodes(audioContext),
226
299
  scheduledNotes: new Map(),
227
300
  };
228
301
  });
229
302
  return channels;
230
303
  }
231
- async createNoteBuffer(instrumentKey, isSF3) {
232
- const sampleStart = instrumentKey.start;
233
- 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;
234
307
  if (isSF3) {
235
- const sample = instrumentKey.sample.slice(sampleStart, sampleEnd);
236
- const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
308
+ const sample = voiceParams.sample;
309
+ const start = sample.byteOffset + sampleStart;
310
+ const end = sample.byteOffset + sampleEnd;
311
+ const buffer = sample.buffer.slice(start, end);
312
+ const audioBuffer = await this.audioContext.decodeAudioData(buffer);
237
313
  return audioBuffer;
238
314
  }
239
315
  else {
240
- const sample = instrumentKey.sample.subarray(sampleStart, sampleEnd);
316
+ const sample = voiceParams.sample;
317
+ const start = sample.byteOffset + sampleStart;
318
+ const end = sample.byteOffset + sampleEnd;
319
+ const buffer = sample.buffer.slice(start, end);
241
320
  const audioBuffer = new AudioBuffer({
242
321
  numberOfChannels: 1,
243
322
  length: sample.length,
244
- sampleRate: instrumentKey.sampleRate,
323
+ sampleRate: voiceParams.sampleRate,
245
324
  });
246
325
  const channelData = audioBuffer.getChannelData(0);
247
- const int16Array = new Int16Array(sample.buffer);
326
+ const int16Array = new Int16Array(buffer);
248
327
  for (let i = 0; i < int16Array.length; i++) {
249
328
  channelData[i] = int16Array[i] / 32768;
250
329
  }
251
330
  return audioBuffer;
252
331
  }
253
332
  }
254
- async createNoteBufferNode(instrumentKey, isSF3) {
333
+ async createNoteBufferNode(voiceParams, isSF3) {
255
334
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
256
- const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
335
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
257
336
  bufferSource.buffer = audioBuffer;
258
- bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
337
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
259
338
  if (bufferSource.loop) {
260
- bufferSource.loopStart = instrumentKey.loopStart /
261
- instrumentKey.sampleRate;
262
- bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
339
+ bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
340
+ bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
263
341
  }
264
342
  return bufferSource;
265
343
  }
@@ -289,7 +367,7 @@ export class MidyGM1 {
289
367
  this.handleProgramChange(event.channel, event.programNumber);
290
368
  break;
291
369
  case "pitchBend":
292
- this.setPitchBend(event.channel, event.value);
370
+ this.setPitchBend(event.channel, event.value + 8192);
293
371
  break;
294
372
  case "sysEx":
295
373
  this.handleSysEx(event.data);
@@ -318,6 +396,7 @@ export class MidyGM1 {
318
396
  if (queueIndex >= this.timeline.length) {
319
397
  await Promise.all(this.notePromises);
320
398
  this.notePromises = [];
399
+ this.exclusiveClassMap.clear();
321
400
  resolve();
322
401
  return;
323
402
  }
@@ -333,6 +412,7 @@ export class MidyGM1 {
333
412
  }
334
413
  else if (this.isStopping) {
335
414
  await this.stopNotes(0, true);
415
+ this.exclusiveClassMap.clear();
336
416
  this.notePromises = [];
337
417
  resolve();
338
418
  this.isStopping = false;
@@ -341,6 +421,7 @@ export class MidyGM1 {
341
421
  }
342
422
  else if (this.isSeeking) {
343
423
  this.stopNotes(0, true);
424
+ this.exclusiveClassMap.clear();
344
425
  this.startTime = this.audioContext.currentTime;
345
426
  queueIndex = this.getQueueIndex(this.resumeTime);
346
427
  offset = this.resumeTime - this.startTime;
@@ -517,41 +598,50 @@ export class MidyGM1 {
517
598
  }
518
599
  calcSemitoneOffset(channel) {
519
600
  const tuning = channel.coarseTuning + channel.fineTuning;
520
- return channel.pitchBend * channel.pitchBendRange + tuning;
521
- }
522
- calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
523
- return instrumentKey.playbackRate(noteNumber) *
524
- 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;
525
605
  }
526
606
  setVolumeEnvelope(note) {
527
- const { instrumentKey, startTime } = note;
528
- const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
529
- const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
530
- const volDelay = startTime + instrumentKey.volDelay;
531
- const volAttack = volDelay + instrumentKey.volAttack;
532
- const volHold = volAttack + instrumentKey.volHold;
533
- 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;
534
615
  note.volumeNode.gain
535
- .cancelScheduledValues(startTime)
616
+ .cancelScheduledValues(now)
536
617
  .setValueAtTime(0, startTime)
537
618
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
538
619
  .exponentialRampToValueAtTime(attackVolume, volAttack)
539
620
  .setValueAtTime(attackVolume, volHold)
540
621
  .linearRampToValueAtTime(sustainVolume, volDecay);
541
622
  }
542
- setPitch(note, semitoneOffset) {
543
- const { instrumentKey, noteNumber, startTime } = note;
544
- const modEnvToPitch = instrumentKey.modEnvToPitch / 100;
545
- 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;
546
637
  if (modEnvToPitch === 0)
547
638
  return;
548
- const basePitch = note.bufferSource.playbackRate.value;
549
- const peekPitch = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset + modEnvToPitch);
550
- const modDelay = startTime + instrumentKey.modDelay;
551
- const modAttack = modDelay + instrumentKey.modAttack;
552
- const modHold = modAttack + instrumentKey.modHold;
553
- const modDecay = modHold + instrumentKey.modDecay;
554
- 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
555
645
  .setValueAtTime(basePitch, modDelay)
556
646
  .exponentialRampToValueAtTime(peekPitch, modAttack)
557
647
  .setValueAtTime(peekPitch, modHold)
@@ -563,20 +653,21 @@ export class MidyGM1 {
563
653
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
564
654
  }
565
655
  setFilterEnvelope(note) {
566
- const { instrumentKey, startTime } = note;
567
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
568
- 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);
569
660
  const sustainFreq = baseFreq +
570
- (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
661
+ (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
571
662
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
572
663
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
573
664
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
574
- const modDelay = startTime + instrumentKey.modDelay;
575
- const modAttack = modDelay + instrumentKey.modAttack;
576
- const modHold = modAttack + instrumentKey.modHold;
577
- 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;
578
669
  note.filterNode.frequency
579
- .cancelScheduledValues(startTime)
670
+ .cancelScheduledValues(now)
580
671
  .setValueAtTime(adjustedBaseFreq, startTime)
581
672
  .setValueAtTime(adjustedBaseFreq, modDelay)
582
673
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
@@ -584,25 +675,18 @@ export class MidyGM1 {
584
675
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
585
676
  }
586
677
  startModulation(channel, note, startTime) {
587
- const { instrumentKey } = note;
588
- const { modLfoToPitch, modLfoToVolume } = instrumentKey;
678
+ const { voiceParams } = note;
589
679
  note.modulationLFO = new OscillatorNode(this.audioContext, {
590
- frequency: this.centToHz(instrumentKey.freqModLFO),
680
+ frequency: this.centToHz(voiceParams.freqModLFO),
591
681
  });
592
682
  note.filterDepth = new GainNode(this.audioContext, {
593
- gain: instrumentKey.modLfoToFilterFc,
594
- });
595
- const modulationDepth = Math.abs(modLfoToPitch) + channel.modulationDepth;
596
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
597
- note.modulationDepth = new GainNode(this.audioContext, {
598
- gain: modulationDepth * modulationDepthSign,
683
+ gain: voiceParams.modLfoToFilterFc,
599
684
  });
600
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
601
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
602
- note.volumeDepth = new GainNode(this.audioContext, {
603
- gain: volumeDepth * volumeDepthSign,
604
- });
605
- 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);
606
690
  note.modulationLFO.connect(note.filterDepth);
607
691
  note.filterDepth.connect(note.filterNode.frequency);
608
692
  note.modulationLFO.connect(note.modulationDepth);
@@ -610,24 +694,23 @@ export class MidyGM1 {
610
694
  note.modulationLFO.connect(note.volumeDepth);
611
695
  note.volumeDepth.connect(note.volumeNode.gain);
612
696
  }
613
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
614
- const semitoneOffset = this.calcSemitoneOffset(channel);
615
- const note = new Note(noteNumber, velocity, startTime, instrumentKey);
616
- 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);
617
702
  note.volumeNode = new GainNode(this.audioContext);
618
703
  note.filterNode = new BiquadFilterNode(this.audioContext, {
619
704
  type: "lowpass",
620
- Q: instrumentKey.initialFilterQ / 10, // dB
705
+ Q: voiceParams.initialFilterQ / 10, // dB
621
706
  });
622
707
  this.setVolumeEnvelope(note);
623
708
  this.setFilterEnvelope(note);
624
- if (0 < channel.modulationDepth) {
625
- this.setPitch(note, semitoneOffset);
709
+ this.setPlaybackRate(note);
710
+ if (0 < state.modulationDepth) {
711
+ this.setPitch(channel, note);
626
712
  this.startModulation(channel, note, startTime);
627
713
  }
628
- else {
629
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
630
- }
631
714
  note.bufferSource.connect(note.filterNode);
632
715
  note.filterNode.connect(note.volumeNode);
633
716
  note.bufferSource.start(startTime);
@@ -635,18 +718,31 @@ export class MidyGM1 {
635
718
  }
636
719
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
637
720
  const channel = this.channels[channelNumber];
638
- const bankNumber = 0;
721
+ const bankNumber = channel.bank;
639
722
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
640
723
  if (soundFontIndex === undefined)
641
724
  return;
642
725
  const soundFont = this.soundFonts[soundFontIndex];
643
726
  const isSF3 = soundFont.parsed.info.version.major === 3;
644
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
645
- if (!instrumentKey)
727
+ const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
728
+ if (!voice)
646
729
  return;
647
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
730
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
648
731
  note.volumeNode.connect(channel.gainL);
649
732
  note.volumeNode.connect(channel.gainR);
733
+ const exclusiveClass = note.voiceParams.exclusiveClass;
734
+ if (exclusiveClass !== 0) {
735
+ if (this.exclusiveClassMap.has(exclusiveClass)) {
736
+ const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
737
+ const [prevNote, prevChannelNumber] = prevEntry;
738
+ if (!prevNote.ending) {
739
+ this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
740
+ startTime, undefined, // portamentoNoteNumber
741
+ true);
742
+ }
743
+ }
744
+ this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
745
+ }
650
746
  const scheduledNotes = channel.scheduledNotes;
651
747
  if (scheduledNotes.has(noteNumber)) {
652
748
  scheduledNotes.get(noteNumber).push(note);
@@ -659,15 +755,15 @@ export class MidyGM1 {
659
755
  const now = this.audioContext.currentTime;
660
756
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
661
757
  }
662
- stopNote(stopTime, endTime, scheduledNotes, index) {
758
+ stopNote(endTime, stopTime, scheduledNotes, index) {
663
759
  const note = scheduledNotes[index];
664
760
  note.volumeNode.gain
665
- .cancelScheduledValues(stopTime)
666
- .linearRampToValueAtTime(0, endTime);
761
+ .cancelScheduledValues(endTime)
762
+ .linearRampToValueAtTime(0, stopTime);
667
763
  note.ending = true;
668
764
  this.scheduleTask(() => {
669
765
  note.bufferSource.loop = false;
670
- }, endTime);
766
+ }, stopTime);
671
767
  return new Promise((resolve) => {
672
768
  note.bufferSource.onended = () => {
673
769
  scheduledNotes[index] = null;
@@ -685,12 +781,12 @@ export class MidyGM1 {
685
781
  }
686
782
  resolve();
687
783
  };
688
- note.bufferSource.stop(endTime);
784
+ note.bufferSource.stop(stopTime);
689
785
  });
690
786
  }
691
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, force) {
787
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
692
788
  const channel = this.channels[channelNumber];
693
- if (!force && channel.sustainPedal)
789
+ if (!force && 0.5 < channel.state.sustainPedal)
694
790
  return;
695
791
  if (!channel.scheduledNotes.has(noteNumber))
696
792
  return;
@@ -701,12 +797,13 @@ export class MidyGM1 {
701
797
  continue;
702
798
  if (note.ending)
703
799
  continue;
704
- const volEndTime = stopTime + note.instrumentKey.volRelease;
705
- const modRelease = stopTime + note.instrumentKey.modRelease;
800
+ const volRelease = endTime + note.voiceParams.volRelease;
801
+ const modRelease = endTime + note.voiceParams.modRelease;
706
802
  note.filterNode.frequency
707
- .cancelScheduledValues(stopTime)
803
+ .cancelScheduledValues(endTime)
708
804
  .linearRampToValueAtTime(0, modRelease);
709
- this.stopNote(stopTime, volEndTime, scheduledNotes, i);
805
+ const stopTime = Math.min(volRelease, modRelease);
806
+ return this.stopNote(endTime, stopTime, scheduledNotes, i);
710
807
  }
711
808
  }
712
809
  releaseNote(channelNumber, noteNumber, velocity) {
@@ -717,7 +814,7 @@ export class MidyGM1 {
717
814
  const velocity = halfVelocity * 2;
718
815
  const channel = this.channels[channelNumber];
719
816
  const promises = [];
720
- channel.sustainPedal = false;
817
+ channel.state.sustainPedal = halfVelocity;
721
818
  channel.scheduledNotes.forEach((noteList) => {
722
819
  for (let i = 0; i < noteList.length; i++) {
723
820
  const note = noteList[i];
@@ -753,17 +850,170 @@ export class MidyGM1 {
753
850
  channel.program = program;
754
851
  }
755
852
  handlePitchBendMessage(channelNumber, lsb, msb) {
756
- const pitchBend = msb * 128 + lsb - 8192;
853
+ const pitchBend = msb * 128 + lsb;
757
854
  this.setPitchBend(channelNumber, pitchBend);
758
855
  }
759
- setPitchBend(channelNumber, pitchBend) {
856
+ setPitchBend(channelNumber, value) {
760
857
  const channel = this.channels[channelNumber];
761
- const prevPitchBend = channel.pitchBend;
762
- channel.pitchBend = pitchBend / 8192;
763
- const detuneChange = (channel.pitchBend - prevPitchBend) *
764
- 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;
765
862
  this.updateDetune(channel, detuneChange);
766
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
+ }
767
1017
  createControlChangeHandlers() {
768
1018
  return {
769
1019
  1: this.setModulationDepth,
@@ -780,13 +1030,13 @@ export class MidyGM1 {
780
1030
  123: this.allNotesOff,
781
1031
  };
782
1032
  }
783
- handleControlChange(channelNumber, controller, value) {
784
- const handler = this.controlChangeHandlers[controller];
1033
+ handleControlChange(channelNumber, controllerType, value) {
1034
+ const handler = this.controlChangeHandlers[controllerType];
785
1035
  if (handler) {
786
1036
  handler.call(this, channelNumber, value);
787
1037
  }
788
1038
  else {
789
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
1039
+ console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
790
1040
  }
791
1041
  }
792
1042
  updateModulation(channel) {
@@ -797,11 +1047,10 @@ export class MidyGM1 {
797
1047
  if (!note)
798
1048
  continue;
799
1049
  if (note.modulationDepth) {
800
- note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
1050
+ note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
801
1051
  }
802
1052
  else {
803
- const semitoneOffset = this.calcSemitoneOffset(channel);
804
- this.setPitch(note, semitoneOffset);
1053
+ this.setPitch(channel, note);
805
1054
  this.startModulation(channel, note, now);
806
1055
  }
807
1056
  }
@@ -809,16 +1058,17 @@ export class MidyGM1 {
809
1058
  }
810
1059
  setModulationDepth(channelNumber, modulation) {
811
1060
  const channel = this.channels[channelNumber];
812
- channel.modulationDepth = (modulation / 127) * channel.modulationDepthRange;
1061
+ channel.state.modulationDepth = (modulation / 127) *
1062
+ channel.modulationDepthRange;
813
1063
  this.updateModulation(channel);
814
1064
  }
815
1065
  setVolume(channelNumber, volume) {
816
1066
  const channel = this.channels[channelNumber];
817
- channel.volume = volume / 127;
1067
+ channel.state.volume = volume / 127;
818
1068
  this.updateChannelVolume(channel);
819
1069
  }
820
1070
  panToGain(pan) {
821
- const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1071
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
822
1072
  return {
823
1073
  gainLeft: Math.cos(theta),
824
1074
  gainRight: Math.sin(theta),
@@ -826,12 +1076,12 @@ export class MidyGM1 {
826
1076
  }
827
1077
  setPan(channelNumber, pan) {
828
1078
  const channel = this.channels[channelNumber];
829
- channel.pan = pan;
1079
+ channel.state.pan = pan / 127;
830
1080
  this.updateChannelVolume(channel);
831
1081
  }
832
1082
  setExpression(channelNumber, expression) {
833
1083
  const channel = this.channels[channelNumber];
834
- channel.expression = expression / 127;
1084
+ channel.state.expression = expression / 127;
835
1085
  this.updateChannelVolume(channel);
836
1086
  }
837
1087
  dataEntryLSB(channelNumber, value) {
@@ -840,8 +1090,9 @@ export class MidyGM1 {
840
1090
  }
841
1091
  updateChannelVolume(channel) {
842
1092
  const now = this.audioContext.currentTime;
843
- const volume = channel.volume * channel.expression;
844
- 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);
845
1096
  channel.gainL.gain
846
1097
  .cancelScheduledValues(now)
847
1098
  .setValueAtTime(volume * gainLeft, now);
@@ -850,9 +1101,8 @@ export class MidyGM1 {
850
1101
  .setValueAtTime(volume * gainRight, now);
851
1102
  }
852
1103
  setSustainPedal(channelNumber, value) {
853
- const isOn = value >= 64;
854
- this.channels[channelNumber].sustainPedal = isOn;
855
- if (!isOn) {
1104
+ this.channels[channelNumber].state.sustainPedal = value / 127;
1105
+ if (value < 64) {
856
1106
  this.releaseSustainPedal(channelNumber, value);
857
1107
  }
858
1108
  }
@@ -909,7 +1159,7 @@ export class MidyGM1 {
909
1159
  this.channels[channelNumber].dataMSB = value;
910
1160
  this.handleRPN(channelNumber);
911
1161
  }
912
- updateDetune(channel, detuneChange) {
1162
+ updateDetune(channel, detune) {
913
1163
  const now = this.audioContext.currentTime;
914
1164
  channel.scheduledNotes.forEach((noteList) => {
915
1165
  for (let i = 0; i < noteList.length; i++) {
@@ -917,7 +1167,6 @@ export class MidyGM1 {
917
1167
  if (!note)
918
1168
  continue;
919
1169
  const { bufferSource } = note;
920
- const detune = bufferSource.detune.value + detuneChange;
921
1170
  bufferSource.detune
922
1171
  .cancelScheduledValues(now)
923
1172
  .setValueAtTime(detune, now);
@@ -930,13 +1179,13 @@ export class MidyGM1 {
930
1179
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
931
1180
  this.setPitchBendRange(channelNumber, pitchBendRange);
932
1181
  }
933
- setPitchBendRange(channelNumber, pitchBendRange) {
1182
+ setPitchBendRange(channelNumber, pitchWheelSensitivity) {
934
1183
  const channel = this.channels[channelNumber];
935
- const prevPitchBendRange = channel.pitchBendRange;
936
- channel.pitchBendRange = pitchBendRange;
937
- const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
938
- channel.pitchBend * 100;
939
- 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);
940
1189
  }
941
1190
  handleFineTuningRPN(channelNumber) {
942
1191
  const channel = this.channels[channelNumber];
@@ -968,7 +1217,26 @@ export class MidyGM1 {
968
1217
  return this.stopChannelNotes(channelNumber, 0, true);
969
1218
  }
970
1219
  resetAllControllers(channelNumber) {
971
- 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
+ }
972
1240
  }
973
1241
  allNotesOff(channelNumber) {
974
1242
  return this.stopChannelNotes(channelNumber, 0, false);
@@ -993,11 +1261,8 @@ export class MidyGM1 {
993
1261
  GM1SystemOn() {
994
1262
  for (let i = 0; i < this.channels.length; i++) {
995
1263
  const channel = this.channels[i];
996
- channel.bankMSB = 0;
997
- channel.bankLSB = 0;
998
1264
  channel.bank = 0;
999
1265
  }
1000
- this.channels[9].bankMSB = 1;
1001
1266
  this.channels[9].bank = 128;
1002
1267
  }
1003
1268
  handleUniversalRealTimeExclusiveMessage(data) {
@@ -1058,28 +1323,15 @@ Object.defineProperty(MidyGM1, "channelSettings", {
1058
1323
  configurable: true,
1059
1324
  writable: true,
1060
1325
  value: {
1061
- volume: 100 / 127,
1062
- pan: 64,
1326
+ currentBufferSource: null,
1327
+ program: 0,
1063
1328
  bank: 0,
1064
1329
  dataMSB: 0,
1065
1330
  dataLSB: 0,
1066
- program: 0,
1067
- pitchBend: 0,
1331
+ rpnMSB: 127,
1332
+ rpnLSB: 127,
1068
1333
  fineTuning: 0, // cb
1069
1334
  coarseTuning: 0, // cb
1070
1335
  modulationDepthRange: 50, // cent
1071
1336
  }
1072
1337
  });
1073
- Object.defineProperty(MidyGM1, "effectSettings", {
1074
- enumerable: true,
1075
- configurable: true,
1076
- writable: true,
1077
- value: {
1078
- expression: 1,
1079
- modulationDepth: 0,
1080
- sustainPedal: false,
1081
- rpnMSB: 127,
1082
- rpnLSB: 127,
1083
- pitchBendRange: 2,
1084
- }
1085
- });