@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
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGMLite = void 0;
4
- const _esm_js_1 = require("./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js");
5
- const _esm_js_2 = require("./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js");
4
+ const midi_file_1 = require("midi-file");
5
+ const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
6
  class Note {
7
- constructor(noteNumber, velocity, startTime, instrumentKey) {
7
+ constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
8
  Object.defineProperty(this, "bufferSource", {
9
9
  enumerable: true,
10
10
  configurable: true,
@@ -44,9 +44,75 @@ class Note {
44
44
  this.noteNumber = noteNumber;
45
45
  this.velocity = velocity;
46
46
  this.startTime = startTime;
47
- this.instrumentKey = instrumentKey;
47
+ this.voice = voice;
48
+ this.voiceParams = voiceParams;
48
49
  }
49
50
  }
51
+ // normalized to 0-1 for use with the SF2 modulator model
52
+ const defaultControllerState = {
53
+ noteOnVelocity: { type: 2, defaultValue: 0 },
54
+ noteOnKeyNumber: { type: 3, defaultValue: 0 },
55
+ pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
56
+ pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
57
+ link: { type: 127, defaultValue: 0 },
58
+ // bankMSB: { type: 128 + 0, defaultValue: 121, },
59
+ modulationDepth: { type: 128 + 1, defaultValue: 0 },
60
+ // dataMSB: { type: 128 + 6, defaultValue: 0, },
61
+ volume: { type: 128 + 7, defaultValue: 100 / 127 },
62
+ pan: { type: 128 + 10, defaultValue: 0.5 },
63
+ expression: { type: 128 + 11, defaultValue: 1 },
64
+ // bankLSB: { type: 128 + 32, defaultValue: 0, },
65
+ // dataLSB: { type: 128 + 38, defaultValue: 0, },
66
+ sustainPedal: { type: 128 + 64, defaultValue: 0 },
67
+ // rpnLSB: { type: 128 + 100, defaultValue: 127 },
68
+ // rpnMSB: { type: 128 + 101, defaultValue: 127 },
69
+ // allSoundOff: { type: 128 + 120, defaultValue: 0 },
70
+ // resetAllControllers: { type: 128 + 121, defaultValue: 0 },
71
+ // allNotesOff: { type: 128 + 123, defaultValue: 0 },
72
+ };
73
+ class ControllerState {
74
+ constructor() {
75
+ Object.defineProperty(this, "array", {
76
+ enumerable: true,
77
+ configurable: true,
78
+ writable: true,
79
+ value: new Float32Array(256)
80
+ });
81
+ const entries = Object.entries(defaultControllerState);
82
+ for (const [name, { type, defaultValue }] of entries) {
83
+ this.array[type] = defaultValue;
84
+ Object.defineProperty(this, name, {
85
+ get: () => this.array[type],
86
+ set: (value) => this.array[type] = value,
87
+ enumerable: true,
88
+ configurable: true,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ const filterEnvelopeKeys = [
94
+ "modEnvToPitch",
95
+ "initialFilterFc",
96
+ "modEnvToFilterFc",
97
+ "modDelay",
98
+ "modAttack",
99
+ "modHold",
100
+ "modDecay",
101
+ "modSustain",
102
+ "modRelease",
103
+ "playbackRate",
104
+ ];
105
+ const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
106
+ const volumeEnvelopeKeys = [
107
+ "volDelay",
108
+ "volAttack",
109
+ "volHold",
110
+ "volDecay",
111
+ "volSustain",
112
+ "volRelease",
113
+ "initialAttenuation",
114
+ ];
115
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
50
116
  class MidyGMLite {
51
117
  constructor(audioContext) {
52
118
  Object.defineProperty(this, "ticksPerBeat", {
@@ -151,8 +217,15 @@ class MidyGMLite {
151
217
  writable: true,
152
218
  value: []
153
219
  });
220
+ Object.defineProperty(this, "exclusiveClassMap", {
221
+ enumerable: true,
222
+ configurable: true,
223
+ writable: true,
224
+ value: new Map()
225
+ });
154
226
  this.audioContext = audioContext;
155
227
  this.masterGain = new GainNode(audioContext);
228
+ this.voiceParamsHandlers = this.createVoiceParamsHandlers();
156
229
  this.controlChangeHandlers = this.createControlChangeHandlers();
157
230
  this.channels = this.createChannels(audioContext);
158
231
  this.masterGain.connect(audioContext.destination);
@@ -180,14 +253,14 @@ class MidyGMLite {
180
253
  async loadSoundFont(soundFontUrl) {
181
254
  const response = await fetch(soundFontUrl);
182
255
  const arrayBuffer = await response.arrayBuffer();
183
- const parsed = (0, _esm_js_2.parse)(new Uint8Array(arrayBuffer));
184
- const soundFont = new _esm_js_2.SoundFont(parsed);
256
+ const parsed = (0, soundfont_parser_1.parse)(new Uint8Array(arrayBuffer));
257
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
185
258
  this.addSoundFont(soundFont);
186
259
  }
187
260
  async loadMIDI(midiUrl) {
188
261
  const response = await fetch(midiUrl);
189
262
  const arrayBuffer = await response.arrayBuffer();
190
- const midi = (0, _esm_js_1.parseMidi)(new Uint8Array(arrayBuffer));
263
+ const midi = (0, midi_file_1.parseMidi)(new Uint8Array(arrayBuffer));
191
264
  this.ticksPerBeat = midi.header.ticksPerBeat;
192
265
  const midiData = this.extractMidiData(midi);
193
266
  this.instruments = midiData.instruments;
@@ -195,7 +268,7 @@ class MidyGMLite {
195
268
  this.totalTime = this.calcTotalTime();
196
269
  }
197
270
  setChannelAudioNodes(audioContext) {
198
- const { gainLeft, gainRight } = this.panToGain(this.constructor.channelSettings.pan);
271
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
199
272
  const gainL = new GainNode(audioContext, { gain: gainLeft });
200
273
  const gainR = new GainNode(audioContext, { gain: gainRight });
201
274
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -212,45 +285,50 @@ class MidyGMLite {
212
285
  const channels = Array.from({ length: 16 }, () => {
213
286
  return {
214
287
  ...this.constructor.channelSettings,
215
- ...this.constructor.effectSettings,
288
+ state: new ControllerState(),
216
289
  ...this.setChannelAudioNodes(audioContext),
217
290
  scheduledNotes: new Map(),
218
291
  };
219
292
  });
220
293
  return channels;
221
294
  }
222
- async createNoteBuffer(instrumentKey, isSF3) {
223
- const sampleStart = instrumentKey.start;
224
- const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
295
+ async createNoteBuffer(voiceParams, isSF3) {
296
+ const sampleStart = voiceParams.start;
297
+ const sampleEnd = voiceParams.sample.length + voiceParams.end;
225
298
  if (isSF3) {
226
- const sample = instrumentKey.sample.slice(sampleStart, sampleEnd);
227
- const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
299
+ const sample = voiceParams.sample;
300
+ const start = sample.byteOffset + sampleStart;
301
+ const end = sample.byteOffset + sampleEnd;
302
+ const buffer = sample.buffer.slice(start, end);
303
+ const audioBuffer = await this.audioContext.decodeAudioData(buffer);
228
304
  return audioBuffer;
229
305
  }
230
306
  else {
231
- const sample = instrumentKey.sample.subarray(sampleStart, sampleEnd);
307
+ const sample = voiceParams.sample;
308
+ const start = sample.byteOffset + sampleStart;
309
+ const end = sample.byteOffset + sampleEnd;
310
+ const buffer = sample.buffer.slice(start, end);
232
311
  const audioBuffer = new AudioBuffer({
233
312
  numberOfChannels: 1,
234
313
  length: sample.length,
235
- sampleRate: instrumentKey.sampleRate,
314
+ sampleRate: voiceParams.sampleRate,
236
315
  });
237
316
  const channelData = audioBuffer.getChannelData(0);
238
- const int16Array = new Int16Array(sample.buffer);
317
+ const int16Array = new Int16Array(buffer);
239
318
  for (let i = 0; i < int16Array.length; i++) {
240
319
  channelData[i] = int16Array[i] / 32768;
241
320
  }
242
321
  return audioBuffer;
243
322
  }
244
323
  }
245
- async createNoteBufferNode(instrumentKey, isSF3) {
324
+ async createNoteBufferNode(voiceParams, isSF3) {
246
325
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
247
- const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
326
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
248
327
  bufferSource.buffer = audioBuffer;
249
- bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
328
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
250
329
  if (bufferSource.loop) {
251
- bufferSource.loopStart = instrumentKey.loopStart /
252
- instrumentKey.sampleRate;
253
- bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
330
+ bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
331
+ bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
254
332
  }
255
333
  return bufferSource;
256
334
  }
@@ -280,7 +358,7 @@ class MidyGMLite {
280
358
  this.handleProgramChange(event.channel, event.programNumber);
281
359
  break;
282
360
  case "pitchBend":
283
- this.setPitchBend(event.channel, event.value);
361
+ this.setPitchBend(event.channel, event.value + 8192);
284
362
  break;
285
363
  case "sysEx":
286
364
  this.handleSysEx(event.data);
@@ -309,6 +387,7 @@ class MidyGMLite {
309
387
  if (queueIndex >= this.timeline.length) {
310
388
  await Promise.all(this.notePromises);
311
389
  this.notePromises = [];
390
+ this.exclusiveClassMap.clear();
312
391
  resolve();
313
392
  return;
314
393
  }
@@ -324,6 +403,7 @@ class MidyGMLite {
324
403
  }
325
404
  else if (this.isStopping) {
326
405
  await this.stopNotes(0, true);
406
+ this.exclusiveClassMap.clear();
327
407
  this.notePromises = [];
328
408
  resolve();
329
409
  this.isStopping = false;
@@ -332,6 +412,7 @@ class MidyGMLite {
332
412
  }
333
413
  else if (this.isSeeking) {
334
414
  this.stopNotes(0, true);
415
+ this.exclusiveClassMap.clear();
335
416
  this.startTime = this.audioContext.currentTime;
336
417
  queueIndex = this.getQueueIndex(this.resumeTime);
337
418
  offset = this.resumeTime - this.startTime;
@@ -507,41 +588,49 @@ class MidyGMLite {
507
588
  return 8.176 * Math.pow(2, cent / 1200);
508
589
  }
509
590
  calcSemitoneOffset(channel) {
510
- return channel.pitchBend * channel.pitchBendRange;
511
- }
512
- calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
513
- return instrumentKey.playbackRate(noteNumber) *
514
- Math.pow(2, semitoneOffset / 12);
591
+ const pitchWheel = channel.state.pitchWheel * 2 - 1;
592
+ const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
593
+ return pitchWheel * pitchWheelSensitivity;
515
594
  }
516
595
  setVolumeEnvelope(note) {
517
- const { instrumentKey, startTime } = note;
518
- const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
519
- const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
520
- const volDelay = startTime + instrumentKey.volDelay;
521
- const volAttack = volDelay + instrumentKey.volAttack;
522
- const volHold = volAttack + instrumentKey.volHold;
523
- const volDecay = volHold + instrumentKey.volDecay;
596
+ const now = this.audioContext.currentTime;
597
+ const { voiceParams, startTime } = note;
598
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
599
+ const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
600
+ const volDelay = startTime + voiceParams.volDelay;
601
+ const volAttack = volDelay + voiceParams.volAttack;
602
+ const volHold = volAttack + voiceParams.volHold;
603
+ const volDecay = volHold + voiceParams.volDecay;
524
604
  note.volumeNode.gain
525
- .cancelScheduledValues(startTime)
605
+ .cancelScheduledValues(now)
526
606
  .setValueAtTime(0, startTime)
527
607
  .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
528
608
  .exponentialRampToValueAtTime(attackVolume, volAttack)
529
609
  .setValueAtTime(attackVolume, volHold)
530
610
  .linearRampToValueAtTime(sustainVolume, volDecay);
531
611
  }
532
- setPitch(note, semitoneOffset) {
533
- const { instrumentKey, noteNumber, startTime } = note;
534
- const modEnvToPitch = instrumentKey.modEnvToPitch / 100;
535
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
612
+ setPlaybackRate(note) {
613
+ const now = this.audioContext.currentTime;
614
+ note.bufferSource.playbackRate
615
+ .cancelScheduledValues(now)
616
+ .setValueAtTime(note.voiceParams.playbackRate, now);
617
+ }
618
+ setPitch(channel, note) {
619
+ const now = this.audioContext.currentTime;
620
+ const { startTime } = note;
621
+ const basePitch = this.calcSemitoneOffset(channel) * 100;
622
+ note.bufferSource.detune
623
+ .cancelScheduledValues(now)
624
+ .setValueAtTime(basePitch, startTime);
625
+ const modEnvToPitch = note.voiceParams.modEnvToPitch;
536
626
  if (modEnvToPitch === 0)
537
627
  return;
538
- const basePitch = note.bufferSource.playbackRate.value;
539
- const peekPitch = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset + modEnvToPitch);
540
- const modDelay = startTime + instrumentKey.modDelay;
541
- const modAttack = modDelay + instrumentKey.modAttack;
542
- const modHold = modAttack + instrumentKey.modHold;
543
- const modDecay = modHold + instrumentKey.modDecay;
544
- note.bufferSource.playbackRate.value
628
+ const peekPitch = basePitch + modEnvToPitch;
629
+ const modDelay = startTime + voiceParams.modDelay;
630
+ const modAttack = modDelay + voiceParams.modAttack;
631
+ const modHold = modAttack + voiceParams.modHold;
632
+ const modDecay = modHold + voiceParams.modDecay;
633
+ note.bufferSource.detune
545
634
  .setValueAtTime(basePitch, modDelay)
546
635
  .exponentialRampToValueAtTime(peekPitch, modAttack)
547
636
  .setValueAtTime(peekPitch, modHold)
@@ -553,20 +642,21 @@ class MidyGMLite {
553
642
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
554
643
  }
555
644
  setFilterEnvelope(note) {
556
- const { instrumentKey, startTime } = note;
557
- const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
558
- const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc);
645
+ const now = this.audioContext.currentTime;
646
+ const { voiceParams, startTime } = note;
647
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc);
648
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
559
649
  const sustainFreq = baseFreq +
560
- (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
650
+ (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
561
651
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
562
652
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
563
653
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
564
- const modDelay = startTime + instrumentKey.modDelay;
565
- const modAttack = modDelay + instrumentKey.modAttack;
566
- const modHold = modAttack + instrumentKey.modHold;
567
- const modDecay = modHold + instrumentKey.modDecay;
654
+ const modDelay = startTime + voiceParams.modDelay;
655
+ const modAttack = modDelay + voiceParams.modAttack;
656
+ const modHold = modAttack + voiceParams.modHold;
657
+ const modDecay = modHold + voiceParams.modDecay;
568
658
  note.filterNode.frequency
569
- .cancelScheduledValues(startTime)
659
+ .cancelScheduledValues(now)
570
660
  .setValueAtTime(adjustedBaseFreq, startTime)
571
661
  .setValueAtTime(adjustedBaseFreq, modDelay)
572
662
  .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
@@ -574,25 +664,18 @@ class MidyGMLite {
574
664
  .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
575
665
  }
576
666
  startModulation(channel, note, startTime) {
577
- const { instrumentKey } = note;
578
- const { modLfoToPitch, modLfoToVolume } = instrumentKey;
667
+ const { voiceParams } = note;
579
668
  note.modulationLFO = new OscillatorNode(this.audioContext, {
580
- frequency: this.centToHz(instrumentKey.freqModLFO),
669
+ frequency: this.centToHz(voiceParams.freqModLFO),
581
670
  });
582
671
  note.filterDepth = new GainNode(this.audioContext, {
583
- gain: instrumentKey.modLfoToFilterFc,
672
+ gain: voiceParams.modLfoToFilterFc,
584
673
  });
585
- const modulationDepth = Math.abs(modLfoToPitch) + channel.modulationDepth;
586
- const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
587
- note.modulationDepth = new GainNode(this.audioContext, {
588
- gain: modulationDepth * modulationDepthSign,
589
- });
590
- const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
591
- const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
592
- note.volumeDepth = new GainNode(this.audioContext, {
593
- gain: volumeDepth * volumeDepthSign,
594
- });
595
- note.modulationLFO.start(startTime + instrumentKey.delayModLFO);
674
+ note.modulationDepth = new GainNode(this.audioContext);
675
+ this.setModLfoToPitch(channel, note);
676
+ note.volumeDepth = new GainNode(this.audioContext);
677
+ this.setModLfoToVolume(note);
678
+ note.modulationLFO.start(startTime + voiceParams.delayModLFO);
596
679
  note.modulationLFO.connect(note.filterDepth);
597
680
  note.filterDepth.connect(note.filterNode.frequency);
598
681
  note.modulationLFO.connect(note.modulationDepth);
@@ -600,24 +683,23 @@ class MidyGMLite {
600
683
  note.modulationLFO.connect(note.volumeDepth);
601
684
  note.volumeDepth.connect(note.volumeNode.gain);
602
685
  }
603
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
604
- const semitoneOffset = this.calcSemitoneOffset(channel);
605
- const note = new Note(noteNumber, velocity, startTime, instrumentKey);
606
- note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
686
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
687
+ const state = channel.state;
688
+ const voiceParams = voice.getAllParams(state.array);
689
+ const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
690
+ note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
607
691
  note.volumeNode = new GainNode(this.audioContext);
608
692
  note.filterNode = new BiquadFilterNode(this.audioContext, {
609
693
  type: "lowpass",
610
- Q: instrumentKey.initialFilterQ / 10, // dB
694
+ Q: voiceParams.initialFilterQ / 10, // dB
611
695
  });
612
696
  this.setVolumeEnvelope(note);
613
697
  this.setFilterEnvelope(note);
614
- if (0 < channel.modulationDepth) {
615
- this.setPitch(note, semitoneOffset);
698
+ this.setPlaybackRate(note);
699
+ if (0 < state.modulationDepth) {
700
+ this.setPitch(channel, note);
616
701
  this.startModulation(channel, note, startTime);
617
702
  }
618
- else {
619
- note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
620
- }
621
703
  note.bufferSource.connect(note.filterNode);
622
704
  note.filterNode.connect(note.volumeNode);
623
705
  note.bufferSource.start(startTime);
@@ -631,12 +713,25 @@ class MidyGMLite {
631
713
  return;
632
714
  const soundFont = this.soundFonts[soundFontIndex];
633
715
  const isSF3 = soundFont.parsed.info.version.major === 3;
634
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
635
- if (!instrumentKey)
716
+ const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
717
+ if (!voice)
636
718
  return;
637
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
719
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
638
720
  note.volumeNode.connect(channel.gainL);
639
721
  note.volumeNode.connect(channel.gainR);
722
+ const exclusiveClass = note.voiceParams.exclusiveClass;
723
+ if (exclusiveClass !== 0) {
724
+ if (this.exclusiveClassMap.has(exclusiveClass)) {
725
+ const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
726
+ const [prevNote, prevChannelNumber] = prevEntry;
727
+ if (!prevNote.ending) {
728
+ this.scheduleNoteRelease(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
729
+ startTime, undefined, // portamentoNoteNumber
730
+ true);
731
+ }
732
+ }
733
+ this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
734
+ }
640
735
  const scheduledNotes = channel.scheduledNotes;
641
736
  if (scheduledNotes.has(noteNumber)) {
642
737
  scheduledNotes.get(noteNumber).push(note);
@@ -649,15 +744,15 @@ class MidyGMLite {
649
744
  const now = this.audioContext.currentTime;
650
745
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
651
746
  }
652
- stopNote(stopTime, endTime, scheduledNotes, index) {
747
+ stopNote(endTime, stopTime, scheduledNotes, index) {
653
748
  const note = scheduledNotes[index];
654
749
  note.volumeNode.gain
655
- .cancelScheduledValues(stopTime)
656
- .linearRampToValueAtTime(0, endTime);
750
+ .cancelScheduledValues(endTime)
751
+ .linearRampToValueAtTime(0, stopTime);
657
752
  note.ending = true;
658
753
  this.scheduleTask(() => {
659
754
  note.bufferSource.loop = false;
660
- }, endTime);
755
+ }, stopTime);
661
756
  return new Promise((resolve) => {
662
757
  note.bufferSource.onended = () => {
663
758
  scheduledNotes[index] = null;
@@ -669,18 +764,14 @@ class MidyGMLite {
669
764
  note.modulationDepth.disconnect();
670
765
  note.modulationLFO.stop();
671
766
  }
672
- if (note.vibratoDepth) {
673
- note.vibratoDepth.disconnect();
674
- note.vibratoLFO.stop();
675
- }
676
767
  resolve();
677
768
  };
678
- note.bufferSource.stop(endTime);
769
+ note.bufferSource.stop(stopTime);
679
770
  });
680
771
  }
681
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, force) {
772
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
682
773
  const channel = this.channels[channelNumber];
683
- if (!force && channel.sustainPedal)
774
+ if (!force && 0.5 < channel.state.sustainPedal)
684
775
  return;
685
776
  if (!channel.scheduledNotes.has(noteNumber))
686
777
  return;
@@ -691,12 +782,13 @@ class MidyGMLite {
691
782
  continue;
692
783
  if (note.ending)
693
784
  continue;
694
- const volEndTime = stopTime + note.instrumentKey.volRelease;
695
- const modRelease = stopTime + note.instrumentKey.modRelease;
785
+ const volRelease = endTime + note.voiceParams.volRelease;
786
+ const modRelease = endTime + note.voiceParams.modRelease;
696
787
  note.filterNode.frequency
697
- .cancelScheduledValues(stopTime)
788
+ .cancelScheduledValues(endTime)
698
789
  .linearRampToValueAtTime(0, modRelease);
699
- this.stopNote(stopTime, volEndTime, scheduledNotes, i);
790
+ const stopTime = Math.min(volRelease, modRelease);
791
+ return this.stopNote(endTime, stopTime, scheduledNotes, i);
700
792
  }
701
793
  }
702
794
  releaseNote(channelNumber, noteNumber, velocity) {
@@ -707,7 +799,7 @@ class MidyGMLite {
707
799
  const velocity = halfVelocity * 2;
708
800
  const channel = this.channels[channelNumber];
709
801
  const promises = [];
710
- channel.sustainPedal = false;
802
+ channel.state.sustainPedal = halfVelocity;
711
803
  channel.scheduledNotes.forEach((noteList) => {
712
804
  for (let i = 0; i < noteList.length; i++) {
713
805
  const note = noteList[i];
@@ -743,17 +835,137 @@ class MidyGMLite {
743
835
  channel.program = program;
744
836
  }
745
837
  handlePitchBendMessage(channelNumber, lsb, msb) {
746
- const pitchBend = msb * 128 + lsb - 8192;
838
+ const pitchBend = msb * 128 + lsb;
747
839
  this.setPitchBend(channelNumber, pitchBend);
748
840
  }
749
- setPitchBend(channelNumber, pitchBend) {
841
+ setPitchBend(channelNumber, value) {
750
842
  const channel = this.channels[channelNumber];
751
- const prevPitchBend = channel.pitchBend;
752
- channel.pitchBend = pitchBend / 8192;
753
- const detuneChange = (channel.pitchBend - prevPitchBend) *
754
- channel.pitchBendRange * 100;
843
+ const state = channel.state;
844
+ state.pitchWheel = value / 16383;
845
+ const pitchWheel = (value - 8192) / 8192;
846
+ const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
755
847
  this.updateDetune(channel, detuneChange);
756
848
  }
849
+ setModLfoToPitch(channel, note) {
850
+ const now = this.audioContext.currentTime;
851
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
852
+ const modulationDepth = Math.abs(modLfoToPitch) +
853
+ channel.state.modulationDepth;
854
+ const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
855
+ note.modulationDepth.gain
856
+ .cancelScheduledValues(now)
857
+ .setValueAtTime(modulationDepth * modulationDepthSign, now);
858
+ }
859
+ setModLfoToVolume(note) {
860
+ const now = this.audioContext.currentTime;
861
+ const modLfoToVolume = note.voiceParams.modLfoToVolume;
862
+ const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
863
+ const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
864
+ note.volumeDepth.gain
865
+ .cancelScheduledValues(now)
866
+ .setValueAtTime(volumeDepth * volumeDepthSign, now);
867
+ }
868
+ setModLfoToFilterFc(note) {
869
+ const now = this.audioContext.currentTime;
870
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
871
+ note.filterDepth.gain
872
+ .cancelScheduledValues(now)
873
+ .setValueAtTime(modLfoToFilterFc, now);
874
+ }
875
+ setDelayModLFO(note) {
876
+ const now = this.audioContext.currentTime;
877
+ const startTime = note.startTime;
878
+ if (startTime < now)
879
+ return;
880
+ note.modulationLFO.stop(now);
881
+ note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
882
+ note.modulationLFO.connect(note.filterDepth);
883
+ }
884
+ setFreqModLFO(note) {
885
+ const now = this.audioContext.currentTime;
886
+ const freqModLFO = note.voiceParams.freqModLFO;
887
+ note.modulationLFO.frequency
888
+ .cancelScheduledValues(now)
889
+ .setValueAtTime(freqModLFO, now);
890
+ }
891
+ createVoiceParamsHandlers() {
892
+ return {
893
+ modLfoToPitch: (channel, note, _prevValue) => {
894
+ if (0 < channel.state.modulationDepth) {
895
+ this.setModLfoToPitch(channel, note);
896
+ }
897
+ },
898
+ vibLfoToPitch: (_channel, _note, _prevValue) => { },
899
+ modLfoToFilterFc: (channel, note, _prevValue) => {
900
+ if (0 < channel.state.modulationDepth)
901
+ this.setModLfoToFilterFc(note);
902
+ },
903
+ modLfoToVolume: (channel, note) => {
904
+ if (0 < channel.state.modulationDepth)
905
+ this.setModLfoToVolume(note);
906
+ },
907
+ chorusEffectsSend: (_channel, _note, _prevValue) => { },
908
+ reverbEffectsSend: (_channel, _note, _prevValue) => { },
909
+ delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
910
+ freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
911
+ delayVibLFO: (_channel, _note, _prevValue) => { },
912
+ freqVibLFO: (_channel, _note, _prevValue) => { },
913
+ };
914
+ }
915
+ getControllerState(channel, noteNumber, velocity) {
916
+ const state = new Float32Array(channel.state.array.length);
917
+ state.set(channel.state.array);
918
+ state[2] = velocity / 127;
919
+ state[3] = noteNumber / 127;
920
+ return state;
921
+ }
922
+ applyVoiceParams(channel, controllerType) {
923
+ channel.scheduledNotes.forEach((noteList) => {
924
+ for (let i = 0; i < noteList.length; i++) {
925
+ const note = noteList[i];
926
+ if (!note)
927
+ continue;
928
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
929
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
930
+ let appliedFilterEnvelope = false;
931
+ let appliedVolumeEnvelope = false;
932
+ for (const [key, value] of Object.entries(voiceParams)) {
933
+ const prevValue = note.voiceParams[key];
934
+ if (value === prevValue)
935
+ continue;
936
+ note.voiceParams[key] = value;
937
+ if (key in this.voiceParamsHandlers) {
938
+ this.voiceParamsHandlers[key](channel, note, prevValue);
939
+ }
940
+ else if (filterEnvelopeKeySet.has(key)) {
941
+ if (appliedFilterEnvelope)
942
+ continue;
943
+ appliedFilterEnvelope = true;
944
+ const noteVoiceParams = note.voiceParams;
945
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
946
+ const key = filterEnvelopeKeys[i];
947
+ if (key in voiceParams)
948
+ noteVoiceParams[key] = voiceParams[key];
949
+ }
950
+ this.setFilterEnvelope(channel, note);
951
+ this.setPitch(channel, note);
952
+ }
953
+ else if (volumeEnvelopeKeySet.has(key)) {
954
+ if (appliedVolumeEnvelope)
955
+ continue;
956
+ appliedVolumeEnvelope = true;
957
+ const noteVoiceParams = note.voiceParams;
958
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
959
+ const key = volumeEnvelopeKeys[i];
960
+ if (key in voiceParams)
961
+ noteVoiceParams[key] = voiceParams[key];
962
+ }
963
+ this.setVolumeEnvelope(channel, note);
964
+ }
965
+ }
966
+ }
967
+ });
968
+ }
757
969
  createControlChangeHandlers() {
758
970
  return {
759
971
  1: this.setModulationDepth,
@@ -770,13 +982,13 @@ class MidyGMLite {
770
982
  123: this.allNotesOff,
771
983
  };
772
984
  }
773
- handleControlChange(channelNumber, controller, value) {
774
- const handler = this.controlChangeHandlers[controller];
985
+ handleControlChange(channelNumber, controllerType, value) {
986
+ const handler = this.controlChangeHandlers[controllerType];
775
987
  if (handler) {
776
988
  handler.call(this, channelNumber, value);
777
989
  }
778
990
  else {
779
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
991
+ console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
780
992
  }
781
993
  }
782
994
  updateModulation(channel) {
@@ -787,11 +999,10 @@ class MidyGMLite {
787
999
  if (!note)
788
1000
  continue;
789
1001
  if (note.modulationDepth) {
790
- note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
1002
+ note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
791
1003
  }
792
1004
  else {
793
- const semitoneOffset = this.calcSemitoneOffset(channel);
794
- this.setPitch(note, semitoneOffset);
1005
+ this.setPitch(channel, note);
795
1006
  this.startModulation(channel, note, now);
796
1007
  }
797
1008
  }
@@ -799,16 +1010,17 @@ class MidyGMLite {
799
1010
  }
800
1011
  setModulationDepth(channelNumber, modulation) {
801
1012
  const channel = this.channels[channelNumber];
802
- channel.modulationDepth = (modulation / 127) * channel.modulationDepthRange;
1013
+ channel.state.modulationDepth = (modulation / 127) *
1014
+ channel.modulationDepthRange;
803
1015
  this.updateModulation(channel);
804
1016
  }
805
1017
  setVolume(channelNumber, volume) {
806
1018
  const channel = this.channels[channelNumber];
807
- channel.volume = volume / 127;
1019
+ channel.state.volume = volume / 127;
808
1020
  this.updateChannelVolume(channel);
809
1021
  }
810
1022
  panToGain(pan) {
811
- const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
1023
+ const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
812
1024
  return {
813
1025
  gainLeft: Math.cos(theta),
814
1026
  gainRight: Math.sin(theta),
@@ -816,12 +1028,12 @@ class MidyGMLite {
816
1028
  }
817
1029
  setPan(channelNumber, pan) {
818
1030
  const channel = this.channels[channelNumber];
819
- channel.pan = pan;
1031
+ channel.state.pan = pan / 127;
820
1032
  this.updateChannelVolume(channel);
821
1033
  }
822
1034
  setExpression(channelNumber, expression) {
823
1035
  const channel = this.channels[channelNumber];
824
- channel.expression = expression / 127;
1036
+ channel.state.expression = expression / 127;
825
1037
  this.updateChannelVolume(channel);
826
1038
  }
827
1039
  dataEntryLSB(channelNumber, value) {
@@ -830,8 +1042,9 @@ class MidyGMLite {
830
1042
  }
831
1043
  updateChannelVolume(channel) {
832
1044
  const now = this.audioContext.currentTime;
833
- const volume = channel.volume * channel.expression;
834
- const { gainLeft, gainRight } = this.panToGain(channel.pan);
1045
+ const state = channel.state;
1046
+ const volume = state.volume * state.expression;
1047
+ const { gainLeft, gainRight } = this.panToGain(state.pan);
835
1048
  channel.gainL.gain
836
1049
  .cancelScheduledValues(now)
837
1050
  .setValueAtTime(volume * gainLeft, now);
@@ -840,12 +1053,29 @@ class MidyGMLite {
840
1053
  .setValueAtTime(volume * gainRight, now);
841
1054
  }
842
1055
  setSustainPedal(channelNumber, value) {
843
- const isOn = value >= 64;
844
- this.channels[channelNumber].sustainPedal = isOn;
845
- if (!isOn) {
1056
+ this.channels[channelNumber].state.sustainPedal = value / 127;
1057
+ if (value < 64) {
846
1058
  this.releaseSustainPedal(channelNumber, value);
847
1059
  }
848
1060
  }
1061
+ limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
1062
+ if (maxLSB < channel.dataLSB) {
1063
+ channel.dataMSB++;
1064
+ channel.dataLSB = minLSB;
1065
+ }
1066
+ else if (channel.dataLSB < 0) {
1067
+ channel.dataMSB--;
1068
+ channel.dataLSB = maxLSB;
1069
+ }
1070
+ if (maxMSB < channel.dataMSB) {
1071
+ channel.dataMSB = maxMSB;
1072
+ channel.dataLSB = maxLSB;
1073
+ }
1074
+ else if (channel.dataMSB < 0) {
1075
+ channel.dataMSB = minMSB;
1076
+ channel.dataLSB = minLSB;
1077
+ }
1078
+ }
849
1079
  handleRPN(channelNumber) {
850
1080
  const channel = this.channels[channelNumber];
851
1081
  const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
@@ -867,7 +1097,7 @@ class MidyGMLite {
867
1097
  this.channels[channelNumber].dataMSB = value;
868
1098
  this.handleRPN(channelNumber);
869
1099
  }
870
- updateDetune(channel, detuneChange) {
1100
+ updateDetune(channel, detune) {
871
1101
  const now = this.audioContext.currentTime;
872
1102
  channel.scheduledNotes.forEach((noteList) => {
873
1103
  for (let i = 0; i < noteList.length; i++) {
@@ -875,7 +1105,6 @@ class MidyGMLite {
875
1105
  if (!note)
876
1106
  continue;
877
1107
  const { bufferSource } = note;
878
- const detune = bufferSource.detune.value + detuneChange;
879
1108
  bufferSource.detune
880
1109
  .cancelScheduledValues(now)
881
1110
  .setValueAtTime(detune, now);
@@ -888,19 +1117,38 @@ class MidyGMLite {
888
1117
  const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
889
1118
  this.setPitchBendRange(channelNumber, pitchBendRange);
890
1119
  }
891
- setPitchBendRange(channelNumber, pitchBendRange) {
1120
+ setPitchBendRange(channelNumber, pitchWheelSensitivity) {
892
1121
  const channel = this.channels[channelNumber];
893
- const prevPitchBendRange = channel.pitchBendRange;
894
- channel.pitchBendRange = pitchBendRange;
895
- const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
896
- channel.pitchBend * 100;
897
- this.updateDetune(channel, detuneChange);
1122
+ const state = channel.state;
1123
+ state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
1124
+ const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
1125
+ this.updateDetune(channel, detune);
1126
+ this.applyVoiceParams(channel, 16);
898
1127
  }
899
1128
  allSoundOff(channelNumber) {
900
1129
  return this.stopChannelNotes(channelNumber, 0, true);
901
1130
  }
902
1131
  resetAllControllers(channelNumber) {
903
- Object.assign(this.channels[channelNumber], this.effectSettings);
1132
+ const stateTypes = [
1133
+ "expression",
1134
+ "modulationDepth",
1135
+ "sustainPedal",
1136
+ "pitchWheelSensitivity",
1137
+ ];
1138
+ const channel = this.channels[channelNumber];
1139
+ const state = channel.state;
1140
+ for (let i = 0; i < stateTypes.length; i++) {
1141
+ const type = stateTypes[i];
1142
+ state[type] = defaultControllerState[type];
1143
+ }
1144
+ const settingTypes = [
1145
+ "rpnMSB",
1146
+ "rpnLSB",
1147
+ ];
1148
+ for (let i = 0; i < settingTypes.length; i++) {
1149
+ const type = settingTypes[i];
1150
+ channel[type] = this.constructor.channelSettings[type];
1151
+ }
904
1152
  }
905
1153
  allNotesOff(channelNumber) {
906
1154
  return this.stopChannelNotes(channelNumber, 0, false);
@@ -925,11 +1173,8 @@ class MidyGMLite {
925
1173
  GM1SystemOn() {
926
1174
  for (let i = 0; i < this.channels.length; i++) {
927
1175
  const channel = this.channels[i];
928
- channel.bankMSB = 0;
929
- channel.bankLSB = 0;
930
1176
  channel.bank = 0;
931
1177
  }
932
- this.channels[9].bankMSB = 1;
933
1178
  this.channels[9].bank = 128;
934
1179
  }
935
1180
  handleUniversalRealTimeExclusiveMessage(data) {
@@ -991,26 +1236,12 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
991
1236
  configurable: true,
992
1237
  writable: true,
993
1238
  value: {
994
- volume: 100 / 127,
995
- pan: 64,
1239
+ currentBufferSource: null,
1240
+ program: 0,
996
1241
  bank: 0,
997
1242
  dataMSB: 0,
998
1243
  dataLSB: 0,
999
- program: 0,
1000
- pitchBend: 0,
1001
- modulationDepthRange: 50, // cent
1002
- }
1003
- });
1004
- Object.defineProperty(MidyGMLite, "effectSettings", {
1005
- enumerable: true,
1006
- configurable: true,
1007
- writable: true,
1008
- value: {
1009
- expression: 1,
1010
- modulationDepth: 0,
1011
- sustainPedal: false,
1012
1244
  rpnMSB: 127,
1013
1245
  rpnLSB: 127,
1014
- pitchBendRange: 2,
1015
1246
  }
1016
1247
  });