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