@marmooo/midy 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/midy.js CHANGED
@@ -44,24 +44,6 @@ class Note {
44
44
  writable: true,
45
45
  value: void 0
46
46
  });
47
- Object.defineProperty(this, "volumeNode", {
48
- enumerable: true,
49
- configurable: true,
50
- writable: true,
51
- value: void 0
52
- });
53
- Object.defineProperty(this, "gainL", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: void 0
58
- });
59
- Object.defineProperty(this, "gainR", {
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- value: void 0
64
- });
65
47
  Object.defineProperty(this, "modulationLFO", {
66
48
  enumerable: true,
67
49
  configurable: true,
@@ -214,6 +196,16 @@ class ControllerState {
214
196
  }
215
197
  }
216
198
  }
199
+ const volumeEnvelopeKeys = [
200
+ "volDelay",
201
+ "volAttack",
202
+ "volHold",
203
+ "volDecay",
204
+ "volSustain",
205
+ "volRelease",
206
+ "initialAttenuation",
207
+ ];
208
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
217
209
  const filterEnvelopeKeys = [
218
210
  "modEnvToPitch",
219
211
  "initialFilterFc",
@@ -223,22 +215,20 @@ const filterEnvelopeKeys = [
223
215
  "modHold",
224
216
  "modDecay",
225
217
  "modSustain",
226
- "modRelease",
227
- "playbackRate",
228
218
  ];
229
219
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
230
- const volumeEnvelopeKeys = [
231
- "volDelay",
232
- "volAttack",
233
- "volHold",
234
- "volDecay",
235
- "volSustain",
236
- "volRelease",
237
- "initialAttenuation",
220
+ const pitchEnvelopeKeys = [
221
+ "modEnvToPitch",
222
+ "modDelay",
223
+ "modAttack",
224
+ "modHold",
225
+ "modDecay",
226
+ "modSustain",
227
+ "playbackRate",
238
228
  ];
239
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
229
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
240
230
  export class Midy {
241
- constructor(audioContext, options = this.defaultOptions) {
231
+ constructor(audioContext) {
242
232
  Object.defineProperty(this, "mode", {
243
233
  enumerable: true,
244
234
  configurable: true,
@@ -262,6 +252,7 @@ export class Midy {
262
252
  configurable: true,
263
253
  writable: true,
264
254
  value: {
255
+ algorithm: "SchroederReverb",
265
256
  time: this.getReverbTime(64),
266
257
  feedback: 0.8,
267
258
  }
@@ -338,13 +329,13 @@ export class Midy {
338
329
  writable: true,
339
330
  value: this.initSoundFontTable()
340
331
  });
341
- Object.defineProperty(this, "audioBufferCounter", {
332
+ Object.defineProperty(this, "voiceCounter", {
342
333
  enumerable: true,
343
334
  configurable: true,
344
335
  writable: true,
345
336
  value: new Map()
346
337
  });
347
- Object.defineProperty(this, "audioBufferCache", {
338
+ Object.defineProperty(this, "voiceCache", {
348
339
  enumerable: true,
349
340
  configurable: true,
350
341
  writable: true,
@@ -410,30 +401,7 @@ export class Midy {
410
401
  writable: true,
411
402
  value: new Array(this.numChannels * drumExclusiveClassCount)
412
403
  });
413
- Object.defineProperty(this, "defaultOptions", {
414
- enumerable: true,
415
- configurable: true,
416
- writable: true,
417
- value: {
418
- reverbAlgorithm: (audioContext) => {
419
- const { time: rt60, feedback } = this.reverb;
420
- // const delay = this.calcDelay(rt60, feedback);
421
- // const impulse = this.createConvolutionReverbImpulse(
422
- // audioContext,
423
- // rt60,
424
- // delay,
425
- // );
426
- // return this.createConvolutionReverb(audioContext, impulse);
427
- const combFeedbacks = this.generateDistributedArray(feedback, 4);
428
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
429
- const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
430
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
431
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
432
- },
433
- }
434
- });
435
404
  this.audioContext = audioContext;
436
- this.options = { ...this.defaultOptions, ...options };
437
405
  this.masterVolume = new GainNode(audioContext);
438
406
  this.scheduler = new GainNode(audioContext, { gain: 0 });
439
407
  this.schedulerBuffer = new AudioBuffer({
@@ -443,7 +411,7 @@ export class Midy {
443
411
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
444
412
  this.controlChangeHandlers = this.createControlChangeHandlers();
445
413
  this.channels = this.createChannels(audioContext);
446
- this.reverbEffect = this.options.reverbAlgorithm(audioContext);
414
+ this.reverbEffect = this.createReverbEffect(audioContext);
447
415
  this.chorusEffect = this.createChorusEffect(audioContext);
448
416
  this.chorusEffect.output.connect(this.masterVolume);
449
417
  this.reverbEffect.output.connect(this.masterVolume);
@@ -464,13 +432,11 @@ export class Midy {
464
432
  const presetHeaders = soundFont.parsed.presetHeaders;
465
433
  for (let i = 0; i < presetHeaders.length; i++) {
466
434
  const presetHeader = presetHeaders[i];
467
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
468
- const banks = this.soundFontTable[presetHeader.preset];
469
- banks.set(presetHeader.bank, index);
470
- }
435
+ const banks = this.soundFontTable[presetHeader.preset];
436
+ banks.set(presetHeader.bank, index);
471
437
  }
472
438
  }
473
- async loadSoundFont(input) {
439
+ async toUint8Array(input) {
474
440
  let uint8Array;
475
441
  if (typeof input === "string") {
476
442
  const response = await fetch(input);
@@ -483,23 +449,32 @@ export class Midy {
483
449
  else {
484
450
  throw new TypeError("input must be a URL string or Uint8Array");
485
451
  }
486
- const parsed = parse(uint8Array);
487
- const soundFont = new SoundFont(parsed);
488
- this.addSoundFont(soundFont);
452
+ return uint8Array;
489
453
  }
490
- async loadMIDI(input) {
491
- let uint8Array;
492
- if (typeof input === "string") {
493
- const response = await fetch(input);
494
- const arrayBuffer = await response.arrayBuffer();
495
- uint8Array = new Uint8Array(arrayBuffer);
496
- }
497
- else if (input instanceof Uint8Array) {
498
- uint8Array = input;
454
+ async loadSoundFont(input) {
455
+ this.voiceCounter.clear();
456
+ if (Array.isArray(input)) {
457
+ const promises = new Array(input.length);
458
+ for (let i = 0; i < input.length; i++) {
459
+ promises[i] = this.toUint8Array(input[i]);
460
+ }
461
+ const uint8Arrays = await Promise.all(promises);
462
+ for (let i = 0; i < uint8Arrays.length; i++) {
463
+ const parsed = parse(uint8Arrays[i]);
464
+ const soundFont = new SoundFont(parsed);
465
+ this.addSoundFont(soundFont);
466
+ }
499
467
  }
500
468
  else {
501
- throw new TypeError("input must be a URL string or Uint8Array");
469
+ const uint8Array = await this.toUint8Array(input);
470
+ const parsed = parse(uint8Array);
471
+ const soundFont = new SoundFont(parsed);
472
+ this.addSoundFont(soundFont);
502
473
  }
474
+ }
475
+ async loadMIDI(input) {
476
+ this.voiceCounter.clear();
477
+ const uint8Array = await this.toUint8Array(input);
503
478
  const midi = parseMidi(uint8Array);
504
479
  this.ticksPerBeat = midi.header.ticksPerBeat;
505
480
  const midiData = this.extractMidiData(midi);
@@ -507,7 +482,46 @@ export class Midy {
507
482
  this.timeline = midiData.timeline;
508
483
  this.totalTime = this.calcTotalTime();
509
484
  }
510
- setChannelAudioNodes(audioContext) {
485
+ cacheVoiceIds() {
486
+ const timeline = this.timeline;
487
+ for (let i = 0; i < timeline.length; i++) {
488
+ const event = timeline[i];
489
+ switch (event.type) {
490
+ case "noteOn": {
491
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
492
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
493
+ break;
494
+ }
495
+ case "controller":
496
+ if (event.controllerType === 0) {
497
+ this.setBankMSB(event.channel, event.value);
498
+ }
499
+ else if (event.controllerType === 32) {
500
+ this.setBankLSB(event.channel, event.value);
501
+ }
502
+ break;
503
+ case "programChange":
504
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
505
+ }
506
+ }
507
+ for (const [audioBufferId, count] of this.voiceCounter) {
508
+ if (count === 1)
509
+ this.voiceCounter.delete(audioBufferId);
510
+ }
511
+ this.GM2SystemOn();
512
+ }
513
+ getVoiceId(channel, noteNumber, velocity) {
514
+ const bankNumber = this.calcBank(channel);
515
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
516
+ .get(bankNumber);
517
+ if (soundFontIndex === undefined)
518
+ return;
519
+ const soundFont = this.soundFonts[soundFontIndex];
520
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
521
+ const { instrument, sampleID } = voice.generators;
522
+ return `${soundFontIndex}:${instrument}:${sampleID}`;
523
+ }
524
+ createChannelAudioNodes(audioContext) {
511
525
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
512
526
  const gainL = new GainNode(audioContext, { gain: gainLeft });
513
527
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -522,10 +536,10 @@ export class Midy {
522
536
  };
523
537
  }
524
538
  resetChannelTable(channel) {
525
- this.resetControlTable(channel.controlTable);
539
+ channel.controlTable.fill(-1);
526
540
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
527
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
528
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
541
+ channel.channelPressureTable.fill(-1);
542
+ channel.polyphonicKeyPressureTable.fill(-1);
529
543
  channel.keyBasedInstrumentControlTable.fill(-1);
530
544
  }
531
545
  createChannels(audioContext) {
@@ -535,47 +549,27 @@ export class Midy {
535
549
  isDrum: false,
536
550
  state: new ControllerState(),
537
551
  ...this.constructor.channelSettings,
538
- ...this.setChannelAudioNodes(audioContext),
552
+ ...this.createChannelAudioNodes(audioContext),
539
553
  scheduledNotes: [],
540
554
  sustainNotes: [],
541
555
  sostenutoNotes: [],
542
556
  controlTable: this.initControlTable(),
543
557
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
544
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
545
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
558
+ channelPressureTable: new Int8Array(6).fill(-1),
559
+ polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
546
560
  keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
561
+ keyBasedGainLs: new Array(128),
562
+ keyBasedGainRs: new Array(128),
547
563
  };
548
564
  });
549
565
  return channels;
550
566
  }
551
- async createNoteBuffer(voiceParams, isSF3) {
567
+ async createAudioBuffer(voiceParams) {
568
+ const sample = voiceParams.sample;
552
569
  const sampleStart = voiceParams.start;
553
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
554
- if (isSF3) {
555
- const sample = voiceParams.sample;
556
- const start = sample.byteOffset + sampleStart;
557
- const end = sample.byteOffset + sampleEnd;
558
- const buffer = sample.buffer.slice(start, end);
559
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
560
- return audioBuffer;
561
- }
562
- else {
563
- const sample = voiceParams.sample;
564
- const start = sample.byteOffset + sampleStart;
565
- const end = sample.byteOffset + sampleEnd;
566
- const buffer = sample.buffer.slice(start, end);
567
- const audioBuffer = new AudioBuffer({
568
- numberOfChannels: 1,
569
- length: sample.length,
570
- sampleRate: voiceParams.sampleRate,
571
- });
572
- const channelData = audioBuffer.getChannelData(0);
573
- const int16Array = new Int16Array(buffer);
574
- for (let i = 0; i < int16Array.length; i++) {
575
- channelData[i] = int16Array[i] / 32768;
576
- }
577
- return audioBuffer;
578
- }
570
+ const sampleEnd = sample.data.length + voiceParams.end;
571
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
572
+ return audioBuffer;
579
573
  }
580
574
  isLoopDrum(channel, noteNumber) {
581
575
  const programNumber = channel.programNumber;
@@ -585,10 +579,9 @@ export class Midy {
585
579
  createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
586
580
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
587
581
  bufferSource.buffer = audioBuffer;
588
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
589
- if (channel.isDrum) {
590
- bufferSource.loop = this.isLoopDrum(channel, noteNumber);
591
- }
582
+ bufferSource.loop = channel.isDrum
583
+ ? this.isLoopDrum(channel, noteNumber)
584
+ : (voiceParams.sampleModes % 2 !== 0);
592
585
  if (bufferSource.loop) {
593
586
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
594
587
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -613,16 +606,16 @@ export class Midy {
613
606
  break;
614
607
  }
615
608
  case "noteAftertouch":
616
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
609
+ this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
617
610
  break;
618
611
  case "controller":
619
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
612
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
620
613
  break;
621
614
  case "programChange":
622
- this.handleProgramChange(event.channel, event.programNumber, startTime);
615
+ this.setProgramChange(event.channel, event.programNumber, startTime);
623
616
  break;
624
617
  case "channelAftertouch":
625
- this.handleChannelPressure(event.channel, event.amount, startTime);
618
+ this.setChannelPressure(event.channel, event.amount, startTime);
626
619
  break;
627
620
  case "pitchBend":
628
621
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -656,8 +649,9 @@ export class Midy {
656
649
  this.notePromises = [];
657
650
  this.exclusiveClassNotes.fill(undefined);
658
651
  this.drumExclusiveClassNotes.fill(undefined);
659
- this.audioBufferCache.clear();
652
+ this.voiceCache.clear();
660
653
  for (let i = 0; i < this.channels.length; i++) {
654
+ this.channels[i].scheduledNotes = [];
661
655
  this.resetAllStates(i);
662
656
  }
663
657
  resolve();
@@ -679,8 +673,9 @@ export class Midy {
679
673
  this.notePromises = [];
680
674
  this.exclusiveClassNotes.fill(undefined);
681
675
  this.drumExclusiveClassNotes.fill(undefined);
682
- this.audioBufferCache.clear();
676
+ this.voiceCache.clear();
683
677
  for (let i = 0; i < this.channels.length; i++) {
678
+ this.channels[i].scheduledNotes = [];
684
679
  this.resetAllStates(i);
685
680
  }
686
681
  this.isStopping = false;
@@ -713,11 +708,7 @@ export class Midy {
713
708
  secondToTicks(second, secondsPerBeat) {
714
709
  return second * this.ticksPerBeat / secondsPerBeat;
715
710
  }
716
- getAudioBufferId(programNumber, noteNumber, velocity) {
717
- return `${programNumber}:${noteNumber}:${velocity}`;
718
- }
719
711
  extractMidiData(midi) {
720
- this.audioBufferCounter.clear();
721
712
  const instruments = new Set();
722
713
  const timeline = [];
723
714
  const tmpChannels = new Array(this.channels.length);
@@ -738,8 +729,6 @@ export class Midy {
738
729
  switch (event.type) {
739
730
  case "noteOn": {
740
731
  const channel = tmpChannels[event.channel];
741
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
742
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
743
732
  if (channel.programNumber < 0) {
744
733
  channel.programNumber = event.programNumber;
745
734
  switch (channel.bankMSB) {
@@ -789,10 +778,6 @@ export class Midy {
789
778
  timeline.push(event);
790
779
  }
791
780
  }
792
- for (const [audioBufferId, count] of this.audioBufferCounter) {
793
- if (count === 1)
794
- this.audioBufferCounter.delete(audioBufferId);
795
- }
796
781
  const priority = {
797
782
  controller: 0,
798
783
  sysEx: 1,
@@ -837,7 +822,6 @@ export class Midy {
837
822
  this.notePromises.push(promise);
838
823
  promises.push(promise);
839
824
  });
840
- channel.scheduledNotes = [];
841
825
  return Promise.all(promises);
842
826
  }
843
827
  stopNotes(velocity, force, scheduleTime) {
@@ -851,6 +835,8 @@ export class Midy {
851
835
  if (this.isPlaying || this.isPaused)
852
836
  return;
853
837
  this.resumeTime = 0;
838
+ if (this.voiceCounter.size === 0)
839
+ this.cacheVoiceIds();
854
840
  await this.playNotes();
855
841
  this.isPlaying = false;
856
842
  }
@@ -893,7 +879,7 @@ export class Midy {
893
879
  }
894
880
  processScheduledNotes(channel, callback) {
895
881
  const scheduledNotes = channel.scheduledNotes;
896
- for (let i = 0; i < scheduledNotes.length; i++) {
882
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
897
883
  const note = scheduledNotes[i];
898
884
  if (!note)
899
885
  continue;
@@ -904,14 +890,14 @@ export class Midy {
904
890
  }
905
891
  processActiveNotes(channel, scheduleTime, callback) {
906
892
  const scheduledNotes = channel.scheduledNotes;
907
- for (let i = 0; i < scheduledNotes.length; i++) {
893
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
908
894
  const note = scheduledNotes[i];
909
895
  if (!note)
910
896
  continue;
911
897
  if (note.ending)
912
898
  continue;
913
899
  if (scheduleTime < note.startTime)
914
- continue;
900
+ break;
915
901
  callback(note);
916
902
  }
917
903
  }
@@ -1000,6 +986,22 @@ export class Midy {
1000
986
  const output = allpasses.at(-1);
1001
987
  return { input, output };
1002
988
  }
989
+ createReverbEffect(audioContext) {
990
+ const { algorithm, time: rt60, feedback } = this.reverb;
991
+ switch (algorithm) {
992
+ case "ConvolutionReverb": {
993
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
994
+ return this.createConvolutionReverb(audioContext, impulse);
995
+ }
996
+ case "SchroederReverb": {
997
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
998
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
999
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
1000
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
1001
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
1002
+ }
1003
+ }
1004
+ }
1003
1005
  createChorusEffect(audioContext) {
1004
1006
  const input = new GainNode(audioContext);
1005
1007
  const output = new GainNode(audioContext);
@@ -1064,9 +1066,16 @@ export class Midy {
1064
1066
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1065
1067
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1066
1068
  const pitch = pitchWheel * pitchWheelSensitivity;
1067
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1068
- const pressure = pressureDepth * channel.state.channelPressure;
1069
- return tuning + pitch + pressure;
1069
+ const channelPressureRaw = channel.channelPressureTable[0];
1070
+ if (0 <= channelPressureRaw) {
1071
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1072
+ const channelPressure = channelPressureDepth *
1073
+ channel.state.channelPressure;
1074
+ return tuning + pitch + channelPressure;
1075
+ }
1076
+ else {
1077
+ return tuning + pitch;
1078
+ }
1070
1079
  }
1071
1080
  calcNoteDetune(channel, note) {
1072
1081
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
@@ -1300,35 +1309,32 @@ export class Midy {
1300
1309
  note.vibratoLFO.connect(note.vibratoDepth);
1301
1310
  note.vibratoDepth.connect(note.bufferSource.detune);
1302
1311
  }
1303
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1304
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1305
- const cache = this.audioBufferCache.get(audioBufferId);
1312
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1313
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1314
+ const cache = this.voiceCache.get(audioBufferId);
1306
1315
  if (cache) {
1307
1316
  cache.counter += 1;
1308
1317
  if (cache.maxCount <= cache.counter) {
1309
- this.audioBufferCache.delete(audioBufferId);
1318
+ this.voiceCache.delete(audioBufferId);
1310
1319
  }
1311
1320
  return cache.audioBuffer;
1312
1321
  }
1313
1322
  else {
1314
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1315
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1323
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1324
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1316
1325
  const cache = { audioBuffer, maxCount, counter: 1 };
1317
- this.audioBufferCache.set(audioBufferId, cache);
1326
+ this.voiceCache.set(audioBufferId, cache);
1318
1327
  return audioBuffer;
1319
1328
  }
1320
1329
  }
1321
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1330
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1322
1331
  const now = this.audioContext.currentTime;
1323
1332
  const state = channel.state;
1324
- const controllerState = this.getControllerState(channel, noteNumber, velocity);
1333
+ const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1325
1334
  const voiceParams = voice.getAllParams(controllerState);
1326
1335
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1327
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1336
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1328
1337
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1329
- note.volumeNode = new GainNode(this.audioContext);
1330
- note.gainL = new GainNode(this.audioContext);
1331
- note.gainR = new GainNode(this.audioContext);
1332
1338
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1333
1339
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1334
1340
  type: "lowpass",
@@ -1361,9 +1367,6 @@ export class Midy {
1361
1367
  }
1362
1368
  note.bufferSource.connect(note.filterNode);
1363
1369
  note.filterNode.connect(note.volumeEnvelopeNode);
1364
- note.volumeEnvelopeNode.connect(note.volumeNode);
1365
- note.volumeNode.connect(note.gainL);
1366
- note.volumeNode.connect(note.gainR);
1367
1370
  if (0 < state.chorusSendLevel) {
1368
1371
  this.setChorusEffectsSend(channel, note, 0, now);
1369
1372
  }
@@ -1422,21 +1425,30 @@ export class Midy {
1422
1425
  }
1423
1426
  this.drumExclusiveClassNotes[index] = note;
1424
1427
  }
1425
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1428
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1426
1429
  const channel = this.channels[channelNumber];
1427
1430
  const bankNumber = this.calcBank(channel, channelNumber);
1428
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1431
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1432
+ .get(bankNumber);
1429
1433
  if (soundFontIndex === undefined)
1430
1434
  return;
1431
1435
  const soundFont = this.soundFonts[soundFontIndex];
1432
1436
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1433
1437
  if (!voice)
1434
1438
  return;
1435
- const isSF3 = soundFont.parsed.info.version.major === 3;
1436
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1437
- note.noteOffEvent = noteOffEvent;
1438
- note.gainL.connect(channel.gainL);
1439
- note.gainR.connect(channel.gainR);
1439
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1440
+ if (channel.isDrum) {
1441
+ const audioContext = this.audioContext;
1442
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1443
+ channel.keyBasedGainLs[noteNumber] = gainL;
1444
+ channel.keyBasedGainRs[noteNumber] = gainR;
1445
+ note.volumeEnvelopeNode.connect(gainL);
1446
+ note.volumeEnvelopeNode.connect(gainR);
1447
+ }
1448
+ else {
1449
+ note.volumeEnvelopeNode.connect(channel.gainL);
1450
+ note.volumeEnvelopeNode.connect(channel.gainR);
1451
+ }
1440
1452
  if (0.5 <= channel.state.sustainPedal) {
1441
1453
  channel.sustainNotes.push(note);
1442
1454
  }
@@ -1454,9 +1466,6 @@ export class Midy {
1454
1466
  note.bufferSource.disconnect();
1455
1467
  note.filterNode.disconnect();
1456
1468
  note.volumeEnvelopeNode.disconnect();
1457
- note.volumeNode.disconnect();
1458
- note.gainL.disconnect();
1459
- note.gainR.disconnect();
1460
1469
  if (note.modulationDepth) {
1461
1470
  note.volumeDepth.disconnect();
1462
1471
  note.modulationDepth.disconnect();
@@ -1510,15 +1519,29 @@ export class Midy {
1510
1519
  return;
1511
1520
  }
1512
1521
  }
1513
- const note = this.findNoteOffTarget(channel, noteNumber);
1514
- if (!note)
1522
+ const index = this.findNoteOffIndex(channel, noteNumber);
1523
+ if (index < 0)
1515
1524
  return;
1525
+ const note = channel.scheduledNotes[index];
1516
1526
  note.ending = true;
1527
+ this.setNoteIndex(channel, index);
1517
1528
  this.releaseNote(channel, note, endTime);
1518
1529
  }
1519
- findNoteOffTarget(channel, noteNumber) {
1530
+ setNoteIndex(channel, index) {
1531
+ let allEnds = true;
1532
+ for (let i = channel.scheduleIndex; i < index; i++) {
1533
+ const note = channel.scheduledNotes[i];
1534
+ if (note && !note.ending) {
1535
+ allEnds = false;
1536
+ break;
1537
+ }
1538
+ }
1539
+ if (allEnds)
1540
+ channel.scheduleIndex = index + 1;
1541
+ }
1542
+ findNoteOffIndex(channel, noteNumber) {
1520
1543
  const scheduledNotes = channel.scheduledNotes;
1521
- for (let i = 0; i < scheduledNotes.length; i++) {
1544
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1522
1545
  const note = scheduledNotes[i];
1523
1546
  if (!note)
1524
1547
  continue;
@@ -1526,8 +1549,9 @@ export class Midy {
1526
1549
  continue;
1527
1550
  if (note.noteNumber !== noteNumber)
1528
1551
  continue;
1529
- return note;
1552
+ return i;
1530
1553
  }
1554
+ return -1;
1531
1555
  }
1532
1556
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1533
1557
  scheduleTime ??= this.audioContext.currentTime;
@@ -1567,31 +1591,31 @@ export class Midy {
1567
1591
  case 0x90:
1568
1592
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1569
1593
  case 0xA0:
1570
- return this.handlePolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1594
+ return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1571
1595
  case 0xB0:
1572
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1596
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1573
1597
  case 0xC0:
1574
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1598
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1575
1599
  case 0xD0:
1576
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1600
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1577
1601
  case 0xE0:
1578
1602
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1579
1603
  default:
1580
1604
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1581
1605
  }
1582
1606
  }
1583
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1607
+ setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1584
1608
  const channel = this.channels[channelNumber];
1585
- channel.state.polyphonicKeyPressure = pressure / 127;
1586
1609
  const table = channel.polyphonicKeyPressureTable;
1587
1610
  this.processActiveNotes(channel, scheduleTime, (note) => {
1588
1611
  if (note.noteNumber === noteNumber) {
1589
- this.setControllerParameters(channel, note, table);
1612
+ note.pressure = pressure;
1613
+ this.setControllerParameters(channel, note, table, scheduleTime);
1590
1614
  }
1591
1615
  });
1592
1616
  this.applyVoiceParams(channel, 10);
1593
1617
  }
1594
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1618
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1595
1619
  const channel = this.channels[channelNumber];
1596
1620
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1597
1621
  channel.programNumber = programNumber;
@@ -1606,20 +1630,21 @@ export class Midy {
1606
1630
  }
1607
1631
  }
1608
1632
  }
1609
- handleChannelPressure(channelNumber, value, scheduleTime) {
1633
+ setChannelPressure(channelNumber, value, scheduleTime) {
1610
1634
  const channel = this.channels[channelNumber];
1611
1635
  if (channel.isDrum)
1612
1636
  return;
1613
1637
  const prev = channel.state.channelPressure;
1614
1638
  const next = value / 127;
1615
1639
  channel.state.channelPressure = next;
1616
- if (channel.channelPressureTable[0] !== 64) {
1617
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1618
- channel.detune += pressureDepth * (next - prev);
1640
+ const channelPressureRaw = channel.channelPressureTable[0];
1641
+ if (0 <= channelPressureRaw) {
1642
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1643
+ channel.detune += channelPressureDepth * (next - prev);
1619
1644
  }
1620
1645
  const table = channel.channelPressureTable;
1621
1646
  this.processActiveNotes(channel, scheduleTime, (note) => {
1622
- this.setControllerParameters(channel, note, table);
1647
+ this.setControllerParameters(channel, note, table, scheduleTime);
1623
1648
  });
1624
1649
  this.applyVoiceParams(channel, 13);
1625
1650
  }
@@ -1675,10 +1700,12 @@ export class Midy {
1675
1700
  .setValueAtTime(volumeDepth, scheduleTime);
1676
1701
  }
1677
1702
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1678
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1679
1703
  let value = note.voiceParams.reverbEffectsSend;
1680
- if (0 <= keyBasedValue) {
1681
- value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1704
+ if (channel.isDrum) {
1705
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1706
+ if (0 <= keyBasedValue) {
1707
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1708
+ }
1682
1709
  }
1683
1710
  if (0 < prevValue) {
1684
1711
  if (0 < value) {
@@ -1696,20 +1723,22 @@ export class Midy {
1696
1723
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1697
1724
  gain: value,
1698
1725
  });
1699
- note.volumeNode.connect(note.reverbEffectsSend);
1726
+ note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1700
1727
  }
1701
1728
  note.reverbEffectsSend.connect(this.reverbEffect.input);
1702
1729
  }
1703
1730
  }
1704
1731
  }
1705
1732
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1706
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1707
1733
  let value = note.voiceParams.chorusEffectsSend;
1708
- if (0 <= keyBasedValue) {
1709
- value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1734
+ if (channel.isDrum) {
1735
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1736
+ if (0 <= keyBasedValue) {
1737
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1738
+ }
1710
1739
  }
1711
1740
  if (0 < prevValue) {
1712
- if (0 < vaule) {
1741
+ if (0 < value) {
1713
1742
  note.chorusEffectsSend.gain
1714
1743
  .cancelScheduledValues(scheduleTime)
1715
1744
  .setValueAtTime(value, scheduleTime);
@@ -1724,7 +1753,7 @@ export class Midy {
1724
1753
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1725
1754
  gain: value,
1726
1755
  });
1727
- note.volumeNode.connect(note.chorusEffectsSend);
1756
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1728
1757
  }
1729
1758
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1730
1759
  }
@@ -1799,21 +1828,22 @@ export class Midy {
1799
1828
  },
1800
1829
  };
1801
1830
  }
1802
- getControllerState(channel, noteNumber, velocity) {
1831
+ getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
1803
1832
  const state = new Float32Array(channel.state.array.length);
1804
1833
  state.set(channel.state.array);
1805
1834
  state[2] = velocity / 127;
1806
1835
  state[3] = noteNumber / 127;
1807
- state[10] = state.polyphonicKeyPressure / 127;
1836
+ state[10] = polyphonicKeyPressure / 127;
1808
1837
  state[13] = state.channelPressure / 127;
1809
1838
  return state;
1810
1839
  }
1811
1840
  applyVoiceParams(channel, controllerType, scheduleTime) {
1812
1841
  this.processScheduledNotes(channel, (note) => {
1813
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1842
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1814
1843
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1815
- let appliedFilterEnvelope = false;
1816
- let appliedVolumeEnvelope = false;
1844
+ let applyVolumeEnvelope = false;
1845
+ let applyFilterEnvelope = false;
1846
+ let applyPitchEnvelope = false;
1817
1847
  for (const [key, value] of Object.entries(voiceParams)) {
1818
1848
  const prevValue = note.voiceParams[key];
1819
1849
  if (value === prevValue)
@@ -1822,37 +1852,23 @@ export class Midy {
1822
1852
  if (key in this.voiceParamsHandlers) {
1823
1853
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1824
1854
  }
1825
- else if (filterEnvelopeKeySet.has(key)) {
1826
- if (appliedFilterEnvelope)
1827
- continue;
1828
- appliedFilterEnvelope = true;
1829
- const noteVoiceParams = note.voiceParams;
1830
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1831
- const key = filterEnvelopeKeys[i];
1832
- if (key in voiceParams)
1833
- noteVoiceParams[key] = voiceParams[key];
1834
- }
1835
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1836
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1837
- }
1838
- else {
1839
- this.setFilterEnvelope(channel, note, scheduleTime);
1840
- }
1841
- this.setPitchEnvelope(note, scheduleTime);
1842
- }
1843
- else if (volumeEnvelopeKeySet.has(key)) {
1844
- if (appliedVolumeEnvelope)
1845
- continue;
1846
- appliedVolumeEnvelope = true;
1847
- const noteVoiceParams = note.voiceParams;
1848
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1849
- const key = volumeEnvelopeKeys[i];
1850
- if (key in voiceParams)
1851
- noteVoiceParams[key] = voiceParams[key];
1852
- }
1853
- this.setVolumeEnvelope(channel, note, scheduleTime);
1855
+ else {
1856
+ if (volumeEnvelopeKeySet.has(key))
1857
+ applyVolumeEnvelope = true;
1858
+ if (filterEnvelopeKeySet.has(key))
1859
+ applyFilterEnvelope = true;
1860
+ if (pitchEnvelopeKeySet.has(key))
1861
+ applyPitchEnvelope = true;
1854
1862
  }
1855
1863
  }
1864
+ if (applyVolumeEnvelope) {
1865
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1866
+ }
1867
+ if (applyFilterEnvelope) {
1868
+ this.setFilterEnvelope(channel, note, scheduleTime);
1869
+ }
1870
+ if (applyPitchEnvelope)
1871
+ this.setPitchEnvelope(note, scheduleTime);
1856
1872
  });
1857
1873
  }
1858
1874
  createControlChangeHandlers() {
@@ -1893,13 +1909,13 @@ export class Midy {
1893
1909
  handlers[127] = this.polyOn;
1894
1910
  return handlers;
1895
1911
  }
1896
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1912
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1897
1913
  const handler = this.controlChangeHandlers[controllerType];
1898
1914
  if (handler) {
1899
1915
  handler.call(this, channelNumber, value, scheduleTime);
1900
1916
  const channel = this.channels[channelNumber];
1901
1917
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1902
- this.applyControlTable(channel, controllerType);
1918
+ this.applyControlTable(channel, controllerType, scheduleTime);
1903
1919
  }
1904
1920
  else {
1905
1921
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1956,22 +1972,12 @@ export class Midy {
1956
1972
  return;
1957
1973
  this.updatePortamento(channel, scheduleTime);
1958
1974
  }
1959
- setKeyBasedVolume(channel, scheduleTime) {
1960
- this.processScheduledNotes(channel, (note) => {
1961
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1962
- if (0 <= keyBasedValue) {
1963
- note.volumeNode.gain
1964
- .cancelScheduledValues(scheduleTime)
1965
- .setValueAtTime(keyBasedValue / 127, scheduleTime);
1966
- }
1967
- });
1968
- }
1969
1975
  setVolume(channelNumber, volume, scheduleTime) {
1970
1976
  scheduleTime ??= this.audioContext.currentTime;
1971
1977
  const channel = this.channels[channelNumber];
1972
1978
  channel.state.volume = volume / 127;
1973
1979
  this.updateChannelVolume(channel, scheduleTime);
1974
- this.setKeyBasedVolume(channel, scheduleTime);
1980
+ this.updateKeyBasedVolume(channel, scheduleTime);
1975
1981
  }
1976
1982
  panToGain(pan) {
1977
1983
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1980,26 +1986,12 @@ export class Midy {
1980
1986
  gainRight: Math.sin(theta),
1981
1987
  };
1982
1988
  }
1983
- setKeyBasedPan(channel, scheduleTime) {
1984
- this.processScheduledNotes(channel, (note) => {
1985
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1986
- if (0 <= keyBasedValue) {
1987
- const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1988
- note.gainL.gain
1989
- .cancelScheduledValues(scheduleTime)
1990
- .setValueAtTime(gainLeft, scheduleTime);
1991
- note.gainR.gain
1992
- .cancelScheduledValues(scheduleTime)
1993
- .setValueAtTime(gainRight, scheduleTime);
1994
- }
1995
- });
1996
- }
1997
1989
  setPan(channelNumber, pan, scheduleTime) {
1998
1990
  scheduleTime ??= this.audioContext.currentTime;
1999
1991
  const channel = this.channels[channelNumber];
2000
1992
  channel.state.pan = pan / 127;
2001
1993
  this.updateChannelVolume(channel, scheduleTime);
2002
- this.setKeyBasedPan(channel, scheduleTime);
1994
+ this.updateKeyBasedVolume(channel, scheduleTime);
2003
1995
  }
2004
1996
  setExpression(channelNumber, expression, scheduleTime) {
2005
1997
  scheduleTime ??= this.audioContext.currentTime;
@@ -2025,6 +2017,34 @@ export class Midy {
2025
2017
  .cancelScheduledValues(scheduleTime)
2026
2018
  .setValueAtTime(volume * gainRight, scheduleTime);
2027
2019
  }
2020
+ updateKeyBasedVolume(channel, scheduleTime) {
2021
+ if (!channel.isDrum)
2022
+ return;
2023
+ const state = channel.state;
2024
+ const defaultVolume = state.volume * state.expression;
2025
+ const defaultPan = state.pan;
2026
+ for (let i = 0; i < 128; i++) {
2027
+ const gainL = channel.keyBasedGainLs[i];
2028
+ const gainR = channel.keyBasedGainLs[i];
2029
+ if (!gainL)
2030
+ continue;
2031
+ if (!gainR)
2032
+ continue;
2033
+ const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
2034
+ const volume = (0 <= keyBasedVolume)
2035
+ ? defaultVolume * keyBasedVolume / 64
2036
+ : defaultVolume;
2037
+ const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
2038
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2039
+ const { gainLeft, gainRight } = this.panToGain(pan);
2040
+ gainL.gain
2041
+ .cancelScheduledValues(scheduleTime)
2042
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2043
+ gainR.gain
2044
+ .cancelScheduledValues(scheduleTime)
2045
+ .setValueAtTime(volume * gainRight, scheduleTime);
2046
+ }
2047
+ }
2028
2048
  setSustainPedal(channelNumber, value, scheduleTime) {
2029
2049
  const channel = this.channels[channelNumber];
2030
2050
  if (channel.isDrum)
@@ -2114,7 +2134,7 @@ export class Midy {
2114
2134
  this.processScheduledNotes(channel, (note) => {
2115
2135
  if (note.startTime < scheduleTime)
2116
2136
  return false;
2117
- this.setVolumeEnvelope(channel, note);
2137
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2118
2138
  });
2119
2139
  }
2120
2140
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2129,7 +2149,7 @@ export class Midy {
2129
2149
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2130
2150
  }
2131
2151
  else {
2132
- this.setFilterEnvelope(channel, note);
2152
+ this.setFilterEnvelope(channel, note, scheduleTime);
2133
2153
  }
2134
2154
  });
2135
2155
  }
@@ -2399,7 +2419,7 @@ export class Midy {
2399
2419
  const entries = Object.entries(defaultControllerState);
2400
2420
  for (const [key, { type, defaultValue }] of entries) {
2401
2421
  if (128 <= type) {
2402
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2422
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2403
2423
  }
2404
2424
  else {
2405
2425
  state[key] = defaultValue;
@@ -2432,7 +2452,7 @@ export class Midy {
2432
2452
  const key = keys[i];
2433
2453
  const { type, defaultValue } = defaultControllerState[key];
2434
2454
  if (128 <= type) {
2435
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2455
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2436
2456
  }
2437
2457
  else {
2438
2458
  state[key] = defaultValue;
@@ -2656,8 +2676,7 @@ export class Midy {
2656
2676
  setReverbType(type) {
2657
2677
  this.reverb.time = this.getReverbTimeFromType(type);
2658
2678
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2659
- const { audioContext, options } = this;
2660
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2679
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2661
2680
  }
2662
2681
  getReverbTimeFromType(type) {
2663
2682
  switch (type) {
@@ -2679,8 +2698,7 @@ export class Midy {
2679
2698
  }
2680
2699
  setReverbTime(value) {
2681
2700
  this.reverb.time = this.getReverbTime(value);
2682
- const { audioContext, options } = this;
2683
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2701
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2684
2702
  }
2685
2703
  getReverbTime(value) {
2686
2704
  return Math.exp((value - 40) * 0.025);
@@ -2875,66 +2893,91 @@ export class Midy {
2875
2893
  }
2876
2894
  }
2877
2895
  getPitchControl(channel, note) {
2878
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2896
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
2897
+ if (polyphonicKeyPressureRaw < 0)
2898
+ return 0;
2899
+ const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
2879
2900
  note.pressure;
2880
2901
  return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2881
2902
  }
2882
2903
  getFilterCutoffControl(channel, note) {
2883
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2884
- channel.state.channelPressure;
2885
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2886
- note.pressure;
2904
+ const channelPressureRaw = channel.channelPressureTable[1];
2905
+ const channelPressure = (0 <= channelPressureRaw)
2906
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2907
+ : 0;
2908
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[1];
2909
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2910
+ ? (polyphonicKeyPressureRaw - 64) * note.pressure
2911
+ : 0;
2887
2912
  return (channelPressure + polyphonicKeyPressure) * 15;
2888
2913
  }
2889
2914
  getAmplitudeControl(channel, note) {
2890
- const channelPressure = channel.channelPressureTable[2] *
2891
- channel.state.channelPressure;
2892
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2893
- note.pressure;
2915
+ const channelPressureRaw = channel.channelPressureTable[2];
2916
+ const channelPressure = (0 <= channelPressureRaw)
2917
+ ? channelPressureRaw * channel.state.channelPressure
2918
+ : 0;
2919
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
2920
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2921
+ ? polyphonicKeyPressureRaw * note.pressure
2922
+ : 0;
2894
2923
  return (channelPressure + polyphonicKeyPressure) / 128;
2895
2924
  }
2896
2925
  getLFOPitchDepth(channel, note) {
2897
- const channelPressure = channel.channelPressureTable[3] *
2898
- channel.state.channelPressure;
2899
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2900
- note.pressure;
2926
+ const channelPressureRaw = channel.channelPressureTable[3];
2927
+ const channelPressure = (0 <= channelPressureRaw)
2928
+ ? channelPressureRaw * channel.state.channelPressure
2929
+ : 0;
2930
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
2931
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2932
+ ? polyphonicKeyPressureRaw * note.pressure
2933
+ : 0;
2901
2934
  return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2902
2935
  }
2903
2936
  getLFOFilterDepth(channel, note) {
2904
- const channelPressure = channel.channelPressureTable[4] *
2905
- channel.state.channelPressure;
2906
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2907
- note.pressure;
2937
+ const channelPressureRaw = channel.channelPressureTable[4];
2938
+ const channelPressure = (0 <= channelPressureRaw)
2939
+ ? channelPressureRaw * channel.state.channelPressure
2940
+ : 0;
2941
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
2942
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2943
+ ? polyphonicKeyPressureRaw * note.pressure
2944
+ : 0;
2908
2945
  return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2909
2946
  }
2910
2947
  getLFOAmplitudeDepth(channel, note) {
2911
- const channelPressure = channel.channelPressureTable[5] *
2912
- channel.state.channelPressure;
2913
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2914
- note.pressure;
2948
+ const channelPressureRaw = channel.channelPressureTable[5];
2949
+ const channelPressure = (0 <= channelPressureRaw)
2950
+ ? channelPressureRaw * channel.state.channelPressure
2951
+ : 0;
2952
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[5];
2953
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2954
+ ? polyphonicKeyPressureRaw * note.pressure
2955
+ : 0;
2915
2956
  return (channelPressure + polyphonicKeyPressure) / 254;
2916
2957
  }
2917
- setControllerParameters(channel, note, table) {
2918
- if (table[0] !== 64)
2919
- this.updateDetune(channel, note);
2958
+ setControllerParameters(channel, note, table, scheduleTime) {
2959
+ if (0 <= table[0])
2960
+ this.updateDetune(channel, note, scueduleTime);
2920
2961
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2921
- if (table[1] !== 64)
2922
- this.setPortamentoFilterEnvelope(channel, note);
2923
- if (table[2] !== 64)
2924
- this.setPortamentoVolumeEnvelope(channel, note);
2962
+ if (0 <= table[1]) {
2963
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2964
+ }
2965
+ if (0 <= table[2]) {
2966
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2967
+ }
2925
2968
  }
2926
2969
  else {
2927
- if (table[1] !== 64)
2928
- this.setFilterEnvelope(channel, note);
2929
- if (table[2] !== 64)
2930
- this.setVolumeEnvelope(channel, note);
2931
- }
2932
- if (table[3] !== 0)
2933
- this.setModLfoToPitch(channel, note);
2934
- if (table[4] !== 0)
2935
- this.setModLfoToFilterFc(channel, note);
2936
- if (table[5] !== 0)
2937
- this.setModLfoToVolume(channel, note);
2970
+ if (0 <= table[1])
2971
+ this.setFilterEnvelope(channel, note, scheduleTime);
2972
+ if (0 <= table[2])
2973
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2974
+ }
2975
+ if (0 <= table[3])
2976
+ this.setModLfoToPitch(channel, note, scheduleTime);
2977
+ if (0 <= table[4])
2978
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2979
+ if (0 <= table[5])
2980
+ this.setModLfoToVolume(channel, note, scheduleTime);
2938
2981
  }
2939
2982
  handlePressureSysEx(data, tableName) {
2940
2983
  const channelNumber = data[4];
@@ -2949,27 +2992,16 @@ export class Midy {
2949
2992
  }
2950
2993
  }
2951
2994
  initControlTable() {
2952
- const channelCount = 128;
2995
+ const ccCount = 128;
2953
2996
  const slotSize = 6;
2954
- const table = new Uint8Array(channelCount * slotSize);
2955
- return this.resetControlTable(table);
2997
+ return new Int8Array(ccCount * slotSize).fill(-1);
2956
2998
  }
2957
- resetControlTable(table) {
2958
- const channelCount = 128;
2959
- const slotSize = 6;
2960
- const defaultValues = [64, 64, 64, 0, 0, 0];
2961
- for (let ch = 0; ch < channelCount; ch++) {
2962
- const offset = ch * slotSize;
2963
- table.set(defaultValues, offset);
2964
- }
2965
- return table;
2966
- }
2967
- applyControlTable(channel, controllerType) {
2999
+ applyControlTable(channel, controllerType, scheduleTime) {
2968
3000
  const slotSize = 6;
2969
3001
  const offset = controllerType * slotSize;
2970
3002
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2971
3003
  this.processScheduledNotes(channel, (note) => {
2972
- this.setControllerParameters(channel, note, table);
3004
+ this.setControllerParameters(channel, note, table, scheduleTime);
2973
3005
  });
2974
3006
  }
2975
3007
  handleControlChangeSysEx(data) {
@@ -2985,7 +3017,7 @@ export class Midy {
2985
3017
  table[pp] = rr;
2986
3018
  }
2987
3019
  }
2988
- getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
3020
+ getKeyBasedValue(channel, keyNumber, controllerType) {
2989
3021
  const index = keyNumber * 128 + controllerType;
2990
3022
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2991
3023
  return controlValue;
@@ -2993,7 +3025,7 @@ export class Midy {
2993
3025
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2994
3026
  const channelNumber = data[4];
2995
3027
  const channel = this.channels[channelNumber];
2996
- if (channel.isDrum)
3028
+ if (!channel.isDrum)
2997
3029
  return;
2998
3030
  const keyNumber = data[5];
2999
3031
  const table = channel.keyBasedInstrumentControlTable;
@@ -3003,7 +3035,7 @@ export class Midy {
3003
3035
  const index = keyNumber * 128 + controllerType;
3004
3036
  table[index] = value;
3005
3037
  }
3006
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3038
+ this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3007
3039
  }
3008
3040
  handleSysEx(data, scheduleTime) {
3009
3041
  switch (data[0]) {
@@ -3040,6 +3072,7 @@ Object.defineProperty(Midy, "channelSettings", {
3040
3072
  configurable: true,
3041
3073
  writable: true,
3042
3074
  value: {
3075
+ scheduleIndex: 0,
3043
3076
  detune: 0,
3044
3077
  programNumber: 0,
3045
3078
  bank: 121 * 128,