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