@marmooo/midy 0.3.2 → 0.3.4

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
@@ -8,11 +8,11 @@ class Note {
8
8
  writable: true,
9
9
  value: -1
10
10
  });
11
- Object.defineProperty(this, "noteOffEvent", {
11
+ Object.defineProperty(this, "ending", {
12
12
  enumerable: true,
13
13
  configurable: true,
14
14
  writable: true,
15
- value: void 0
15
+ value: false
16
16
  });
17
17
  Object.defineProperty(this, "bufferSource", {
18
18
  enumerable: true,
@@ -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
  }
@@ -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);
@@ -470,24 +438,44 @@ export class Midy {
470
438
  }
471
439
  }
472
440
  }
473
- async loadSoundFont(soundFontUrl) {
474
- const response = await fetch(soundFontUrl);
475
- const arrayBuffer = await response.arrayBuffer();
476
- const parsed = parse(new Uint8Array(arrayBuffer));
441
+ async loadSoundFont(input) {
442
+ let uint8Array;
443
+ if (typeof input === "string") {
444
+ const response = await fetch(input);
445
+ const arrayBuffer = await response.arrayBuffer();
446
+ uint8Array = new Uint8Array(arrayBuffer);
447
+ }
448
+ else if (input instanceof Uint8Array) {
449
+ uint8Array = input;
450
+ }
451
+ else {
452
+ throw new TypeError("input must be a URL string or Uint8Array");
453
+ }
454
+ const parsed = parse(uint8Array);
477
455
  const soundFont = new SoundFont(parsed);
478
456
  this.addSoundFont(soundFont);
479
457
  }
480
- async loadMIDI(midiUrl) {
481
- const response = await fetch(midiUrl);
482
- const arrayBuffer = await response.arrayBuffer();
483
- const midi = parseMidi(new Uint8Array(arrayBuffer));
458
+ async loadMIDI(input) {
459
+ let uint8Array;
460
+ if (typeof input === "string") {
461
+ const response = await fetch(input);
462
+ const arrayBuffer = await response.arrayBuffer();
463
+ uint8Array = new Uint8Array(arrayBuffer);
464
+ }
465
+ else if (input instanceof Uint8Array) {
466
+ uint8Array = input;
467
+ }
468
+ else {
469
+ throw new TypeError("input must be a URL string or Uint8Array");
470
+ }
471
+ const midi = parseMidi(uint8Array);
484
472
  this.ticksPerBeat = midi.header.ticksPerBeat;
485
473
  const midiData = this.extractMidiData(midi);
486
474
  this.instruments = midiData.instruments;
487
475
  this.timeline = midiData.timeline;
488
476
  this.totalTime = this.calcTotalTime();
489
477
  }
490
- setChannelAudioNodes(audioContext) {
478
+ createChannelAudioNodes(audioContext) {
491
479
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
492
480
  const gainL = new GainNode(audioContext, { gain: gainLeft });
493
481
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -502,11 +490,11 @@ export class Midy {
502
490
  };
503
491
  }
504
492
  resetChannelTable(channel) {
505
- this.resetControlTable(channel.controlTable);
493
+ channel.controlTable.fill(-1);
506
494
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
507
- channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
508
- channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
509
- channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
495
+ channel.channelPressureTable.fill(-1);
496
+ channel.polyphonicKeyPressureTable.fill(-1);
497
+ channel.keyBasedInstrumentControlTable.fill(-1);
510
498
  }
511
499
  createChannels(audioContext) {
512
500
  const channels = Array.from({ length: this.numChannels }, () => {
@@ -515,15 +503,17 @@ export class Midy {
515
503
  isDrum: false,
516
504
  state: new ControllerState(),
517
505
  ...this.constructor.channelSettings,
518
- ...this.setChannelAudioNodes(audioContext),
506
+ ...this.createChannelAudioNodes(audioContext),
519
507
  scheduledNotes: [],
520
508
  sustainNotes: [],
521
509
  sostenutoNotes: [],
522
510
  controlTable: this.initControlTable(),
523
511
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
524
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
525
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
526
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
512
+ channelPressureTable: new Int8Array(6).fill(-1),
513
+ polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
514
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
515
+ keyBasedGainLs: new Array(128),
516
+ keyBasedGainRs: new Array(128),
527
517
  };
528
518
  });
529
519
  return channels;
@@ -557,10 +547,17 @@ export class Midy {
557
547
  return audioBuffer;
558
548
  }
559
549
  }
560
- createBufferSource(voiceParams, audioBuffer) {
550
+ isLoopDrum(channel, noteNumber) {
551
+ const programNumber = channel.programNumber;
552
+ return ((programNumber === 48 && noteNumber === 88) ||
553
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
554
+ }
555
+ createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
561
556
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
562
557
  bufferSource.buffer = audioBuffer;
563
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
558
+ bufferSource.loop = channel.isDrum
559
+ ? this.isLoopDrum(channel, noteNumber)
560
+ : (voiceParams.sampleModes % 2 !== 0);
564
561
  if (bufferSource.loop) {
565
562
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
566
563
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -575,12 +572,13 @@ export class Midy {
575
572
  const delay = this.startDelay - resumeTime;
576
573
  const startTime = event.startTime + delay;
577
574
  switch (event.type) {
578
- case "noteOn": {
579
- const noteOffEvent = {
580
- ...event.noteOffEvent,
581
- startTime: event.noteOffEvent.startTime + delay,
582
- };
583
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
575
+ case "noteOn":
576
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
577
+ break;
578
+ case "noteOff": {
579
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
580
+ if (notePromise)
581
+ this.notePromises.push(notePromise);
584
582
  break;
585
583
  }
586
584
  case "noteAftertouch":
@@ -688,6 +686,7 @@ export class Midy {
688
686
  return `${programNumber}:${noteNumber}:${velocity}`;
689
687
  }
690
688
  extractMidiData(midi) {
689
+ this.audioBufferCounter.clear();
691
690
  const instruments = new Set();
692
691
  const timeline = [];
693
692
  const tmpChannels = new Array(this.channels.length);
@@ -787,38 +786,13 @@ export class Midy {
787
786
  prevTempoTicks = event.ticks;
788
787
  }
789
788
  }
790
- const activeNotes = new Array(this.channels.length * 128);
791
- for (let i = 0; i < activeNotes.length; i++) {
792
- activeNotes[i] = [];
793
- }
794
- for (let i = 0; i < timeline.length; i++) {
795
- const event = timeline[i];
796
- switch (event.type) {
797
- case "noteOn": {
798
- const index = event.channel * 128 + event.noteNumber;
799
- activeNotes[index].push(event);
800
- break;
801
- }
802
- case "noteOff": {
803
- const index = event.channel * 128 + event.noteNumber;
804
- const noteOn = activeNotes[index].pop();
805
- if (noteOn) {
806
- noteOn.noteOffEvent = event;
807
- }
808
- else {
809
- const eventString = JSON.stringify(event, null, 2);
810
- console.warn(`noteOff without matching noteOn: ${eventString}`);
811
- }
812
- }
813
- }
814
- }
815
789
  return { instruments, timeline };
816
790
  }
817
791
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
818
792
  const channel = this.channels[channelNumber];
819
793
  const promises = [];
820
794
  this.processActiveNotes(channel, scheduleTime, (note) => {
821
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
795
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
822
796
  this.notePromises.push(promise);
823
797
  promises.push(promise);
824
798
  });
@@ -827,8 +801,8 @@ export class Midy {
827
801
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
828
802
  const channel = this.channels[channelNumber];
829
803
  const promises = [];
830
- this.processScheduledNotes(channel, (note) => {
831
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
804
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
805
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
832
806
  this.notePromises.push(promise);
833
807
  promises.push(promise);
834
808
  });
@@ -886,7 +860,7 @@ export class Midy {
886
860
  const now = this.audioContext.currentTime;
887
861
  return this.resumeTime + now - this.startTime - this.startDelay;
888
862
  }
889
- processScheduledNotes(channel, callback) {
863
+ processScheduledNotes(channel, scheduleTime, callback) {
890
864
  const scheduledNotes = channel.scheduledNotes;
891
865
  for (let i = 0; i < scheduledNotes.length; i++) {
892
866
  const note = scheduledNotes[i];
@@ -894,6 +868,8 @@ export class Midy {
894
868
  continue;
895
869
  if (note.ending)
896
870
  continue;
871
+ if (note.startTime < scheduleTime)
872
+ continue;
897
873
  callback(note);
898
874
  }
899
875
  }
@@ -905,11 +881,8 @@ export class Midy {
905
881
  continue;
906
882
  if (note.ending)
907
883
  continue;
908
- const noteOffEvent = note.noteOffEvent;
909
- if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
910
- continue;
911
884
  if (scheduleTime < note.startTime)
912
- continue;
885
+ break;
913
886
  callback(note);
914
887
  }
915
888
  }
@@ -998,6 +971,22 @@ export class Midy {
998
971
  const output = allpasses.at(-1);
999
972
  return { input, output };
1000
973
  }
974
+ createReverbEffect(audioContext) {
975
+ const { algorithm, time: rt60, feedback } = this.reverb;
976
+ switch (algorithm) {
977
+ case "ConvolutionReverb": {
978
+ const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
979
+ return this.createConvolutionReverb(audioContext, impulse);
980
+ }
981
+ case "SchroederReverb": {
982
+ const combFeedbacks = this.generateDistributedArray(feedback, 4);
983
+ const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
984
+ const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
985
+ const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
986
+ return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
987
+ }
988
+ }
989
+ }
1001
990
  createChorusEffect(audioContext) {
1002
991
  const input = new GainNode(audioContext);
1003
992
  const output = new GainNode(audioContext);
@@ -1062,15 +1051,22 @@ export class Midy {
1062
1051
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
1063
1052
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
1064
1053
  const pitch = pitchWheel * pitchWheelSensitivity;
1065
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1066
- const pressure = pressureDepth * channel.state.channelPressure;
1067
- return tuning + pitch + pressure;
1054
+ const channelPressureRaw = channel.channelPressureTable[0];
1055
+ if (0 <= channelPressureRaw) {
1056
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1057
+ const channelPressure = channelPressureDepth *
1058
+ channel.state.channelPressure;
1059
+ return tuning + pitch + channelPressure;
1060
+ }
1061
+ else {
1062
+ return tuning + pitch;
1063
+ }
1068
1064
  }
1069
1065
  calcNoteDetune(channel, note) {
1070
1066
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1071
1067
  }
1072
1068
  updateChannelDetune(channel, scheduleTime) {
1073
- this.processScheduledNotes(channel, (note) => {
1069
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1074
1070
  this.updateDetune(channel, note, scheduleTime);
1075
1071
  });
1076
1072
  }
@@ -1220,9 +1216,8 @@ export class Midy {
1220
1216
  }
1221
1217
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1222
1218
  const state = channel.state;
1223
- const { voiceParams, noteNumber, startTime } = note;
1224
- const softPedalFactor = 1 -
1225
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1219
+ const { voiceParams, startTime } = note;
1220
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1226
1221
  const baseCent = voiceParams.initialFilterFc +
1227
1222
  this.getFilterCutoffControl(channel, note);
1228
1223
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
@@ -1242,9 +1237,8 @@ export class Midy {
1242
1237
  }
1243
1238
  setFilterEnvelope(channel, note, scheduleTime) {
1244
1239
  const state = channel.state;
1245
- const { voiceParams, noteNumber, startTime } = note;
1246
- const softPedalFactor = 1 -
1247
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1240
+ const { voiceParams, startTime } = note;
1241
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1248
1242
  const baseCent = voiceParams.initialFilterFc +
1249
1243
  this.getFilterCutoffControl(channel, note);
1250
1244
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
@@ -1321,14 +1315,11 @@ export class Midy {
1321
1315
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1322
1316
  const now = this.audioContext.currentTime;
1323
1317
  const state = channel.state;
1324
- const controllerState = this.getControllerState(channel, noteNumber, velocity);
1318
+ const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1325
1319
  const voiceParams = voice.getAllParams(controllerState);
1326
1320
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1327
1321
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1328
- note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1329
- note.volumeNode = new GainNode(this.audioContext);
1330
- note.gainL = new GainNode(this.audioContext);
1331
- note.gainR = new GainNode(this.audioContext);
1322
+ note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1332
1323
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1333
1324
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1334
1325
  type: "lowpass",
@@ -1361,13 +1352,10 @@ export class Midy {
1361
1352
  }
1362
1353
  note.bufferSource.connect(note.filterNode);
1363
1354
  note.filterNode.connect(note.volumeEnvelopeNode);
1364
- note.volumeEnvelopeNode.connect(note.volumeNode);
1365
- note.volumeNode.connect(note.gainL);
1366
- note.volumeNode.connect(note.gainR);
1367
- if (0 < channel.chorusSendLevel) {
1355
+ if (0 < state.chorusSendLevel) {
1368
1356
  this.setChorusEffectsSend(channel, note, 0, now);
1369
1357
  }
1370
- if (0 < channel.reverbSendLevel) {
1358
+ if (0 < state.reverbSendLevel) {
1371
1359
  this.setReverbEffectsSend(channel, note, 0, now);
1372
1360
  }
1373
1361
  note.bufferSource.start(startTime);
@@ -1397,7 +1385,7 @@ export class Midy {
1397
1385
  if (prev) {
1398
1386
  const [prevNote, prevChannelNumber] = prev;
1399
1387
  if (prevNote && !prevNote.ending) {
1400
- this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1388
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1401
1389
  startTime, true);
1402
1390
  }
1403
1391
  }
@@ -1417,19 +1405,12 @@ export class Midy {
1417
1405
  channelNumber;
1418
1406
  const prevNote = this.drumExclusiveClassNotes[index];
1419
1407
  if (prevNote && !prevNote.ending) {
1420
- this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1408
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1421
1409
  startTime, true);
1422
1410
  }
1423
1411
  this.drumExclusiveClassNotes[index] = note;
1424
1412
  }
1425
- isDrumNoteOffException(channel, noteNumber) {
1426
- if (!channel.isDrum)
1427
- return false;
1428
- const programNumber = channel.programNumber;
1429
- return !((programNumber === 48 && noteNumber === 88) ||
1430
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1431
- }
1432
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1413
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1433
1414
  const channel = this.channels[channelNumber];
1434
1415
  const bankNumber = this.calcBank(channel, channelNumber);
1435
1416
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1441,9 +1422,18 @@ export class Midy {
1441
1422
  return;
1442
1423
  const isSF3 = soundFont.parsed.info.version.major === 3;
1443
1424
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1444
- note.noteOffEvent = noteOffEvent;
1445
- note.gainL.connect(channel.gainL);
1446
- note.gainR.connect(channel.gainR);
1425
+ if (channel.isDrum) {
1426
+ const audioContext = this.audioContext;
1427
+ const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
1428
+ channel.keyBasedGainLs[noteNumber] = gainL;
1429
+ channel.keyBasedGainRs[noteNumber] = gainR;
1430
+ note.volumeEnvelopeNode.connect(gainL);
1431
+ note.volumeEnvelopeNode.connect(gainR);
1432
+ }
1433
+ else {
1434
+ note.volumeEnvelopeNode.connect(channel.gainL);
1435
+ note.volumeEnvelopeNode.connect(channel.gainR);
1436
+ }
1447
1437
  if (0.5 <= channel.state.sustainPedal) {
1448
1438
  channel.sustainNotes.push(note);
1449
1439
  }
@@ -1452,31 +1442,6 @@ export class Midy {
1452
1442
  const scheduledNotes = channel.scheduledNotes;
1453
1443
  note.index = scheduledNotes.length;
1454
1444
  scheduledNotes.push(note);
1455
- if (this.isDrumNoteOffException(channel, noteNumber)) {
1456
- const stopTime = startTime + note.bufferSource.buffer.duration;
1457
- const promise = new Promise((resolve) => {
1458
- note.bufferSource.onended = () => {
1459
- scheduledNotes[note.index] = undefined;
1460
- this.disconnectNote(note);
1461
- resolve();
1462
- };
1463
- note.bufferSource.stop(stopTime);
1464
- });
1465
- this.notePromises.push(promise);
1466
- }
1467
- else if (noteOffEvent) {
1468
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1469
- const portamentoTime = this.getPortamentoTime(channel, note);
1470
- const portamentoEndTime = startTime + portamentoTime;
1471
- const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
1472
- Math.max(noteOffEvent.startTime, portamentoEndTime), false);
1473
- this.notePromises.push(notePromise);
1474
- }
1475
- else {
1476
- const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
1477
- this.notePromises.push(notePromise);
1478
- }
1479
- }
1480
1445
  }
1481
1446
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1482
1447
  scheduleTime ??= this.audioContext.currentTime;
@@ -1486,9 +1451,6 @@ export class Midy {
1486
1451
  note.bufferSource.disconnect();
1487
1452
  note.filterNode.disconnect();
1488
1453
  note.volumeEnvelopeNode.disconnect();
1489
- note.volumeNode.disconnect();
1490
- note.gainL.disconnect();
1491
- note.gainR.disconnect();
1492
1454
  if (note.modulationDepth) {
1493
1455
  note.volumeDepth.disconnect();
1494
1456
  note.modulationDepth.disconnect();
@@ -1505,42 +1467,48 @@ export class Midy {
1505
1467
  note.chorusEffectsSend.disconnect();
1506
1468
  }
1507
1469
  }
1508
- stopNote(channel, note, endTime, stopTime) {
1470
+ releaseNote(channel, note, endTime) {
1471
+ const volRelease = endTime +
1472
+ note.voiceParams.volRelease * channel.state.releaseTime * 2;
1473
+ const modRelease = endTime + note.voiceParams.modRelease;
1474
+ const stopTime = Math.min(volRelease, modRelease);
1475
+ note.filterNode.frequency
1476
+ .cancelScheduledValues(endTime)
1477
+ .linearRampToValueAtTime(0, modRelease);
1509
1478
  note.volumeEnvelopeNode.gain
1510
1479
  .cancelScheduledValues(endTime)
1511
- .linearRampToValueAtTime(0, stopTime);
1512
- note.ending = true;
1513
- this.scheduleTask(() => {
1514
- note.bufferSource.loop = false;
1515
- }, stopTime);
1480
+ .linearRampToValueAtTime(0, volRelease);
1516
1481
  return new Promise((resolve) => {
1517
- note.bufferSource.onended = () => {
1518
- channel.scheduledNotes[note.index] = undefined;
1482
+ this.scheduleTask(() => {
1483
+ const bufferSource = note.bufferSource;
1484
+ bufferSource.loop = false;
1485
+ bufferSource.stop(stopTime);
1519
1486
  this.disconnectNote(note);
1487
+ channel.scheduledNotes[note.index] = undefined;
1520
1488
  resolve();
1521
- };
1522
- note.bufferSource.stop(stopTime);
1489
+ }, stopTime);
1523
1490
  });
1524
1491
  }
1525
- scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1492
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1526
1493
  const channel = this.channels[channelNumber];
1527
- if (this.isDrumNoteOffException(channel, note.noteNumber))
1528
- return;
1529
1494
  const state = channel.state;
1530
1495
  if (!force) {
1531
- if (0.5 <= state.sustainPedal)
1532
- return;
1533
- if (0.5 <= channel.state.sostenutoPedal)
1534
- return;
1496
+ if (channel.isDrum) {
1497
+ if (!this.isLoopDrum(channel, noteNumber))
1498
+ return;
1499
+ }
1500
+ else {
1501
+ if (0.5 <= state.sustainPedal)
1502
+ return;
1503
+ if (0.5 <= state.sostenutoPedal)
1504
+ return;
1505
+ }
1535
1506
  }
1536
- const volRelease = endTime +
1537
- note.voiceParams.volRelease * channel.state.releaseTime * 2;
1538
- const modRelease = endTime + note.voiceParams.modRelease;
1539
- note.filterNode.frequency
1540
- .cancelScheduledValues(endTime)
1541
- .linearRampToValueAtTime(0, modRelease);
1542
- const stopTime = Math.min(volRelease, modRelease);
1543
- return this.stopNote(channel, note, endTime, stopTime);
1507
+ const note = this.findNoteOffTarget(channel, noteNumber);
1508
+ if (!note)
1509
+ return;
1510
+ note.ending = true;
1511
+ this.releaseNote(channel, note, endTime);
1544
1512
  }
1545
1513
  findNoteOffTarget(channel, noteNumber) {
1546
1514
  const scheduledNotes = channel.scheduledNotes;
@@ -1557,16 +1525,14 @@ export class Midy {
1557
1525
  }
1558
1526
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1559
1527
  scheduleTime ??= this.audioContext.currentTime;
1560
- const channel = this.channels[channelNumber];
1561
- const note = this.findNoteOffTarget(channel, noteNumber);
1562
- return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1528
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1563
1529
  }
1564
1530
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1565
1531
  const velocity = halfVelocity * 2;
1566
1532
  const channel = this.channels[channelNumber];
1567
1533
  const promises = [];
1568
1534
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1569
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1535
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1570
1536
  promises.push(promise);
1571
1537
  }
1572
1538
  channel.sustainNotes = [];
@@ -1580,7 +1546,7 @@ export class Midy {
1580
1546
  channel.state.sostenutoPedal = 0;
1581
1547
  for (let i = 0; i < sostenutoNotes.length; i++) {
1582
1548
  const note = sostenutoNotes[i];
1583
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
1549
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1584
1550
  promises.push(promise);
1585
1551
  }
1586
1552
  channel.sostenutoNotes = [];
@@ -1610,10 +1576,10 @@ export class Midy {
1610
1576
  }
1611
1577
  handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1612
1578
  const channel = this.channels[channelNumber];
1613
- channel.state.polyphonicKeyPressure = pressure / 127;
1614
1579
  const table = channel.polyphonicKeyPressureTable;
1615
1580
  this.processActiveNotes(channel, scheduleTime, (note) => {
1616
1581
  if (note.noteNumber === noteNumber) {
1582
+ note.pressure = pressure;
1617
1583
  this.setControllerParameters(channel, note, table);
1618
1584
  }
1619
1585
  });
@@ -1641,9 +1607,10 @@ export class Midy {
1641
1607
  const prev = channel.state.channelPressure;
1642
1608
  const next = value / 127;
1643
1609
  channel.state.channelPressure = next;
1644
- if (channel.channelPressureTable[0] !== 64) {
1645
- const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1646
- channel.detune += pressureDepth * (next - prev);
1610
+ const channelPressureRaw = channel.channelPressureTable[0];
1611
+ if (0 <= channelPressureRaw) {
1612
+ const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
1613
+ channel.detune += channelPressureDepth * (next - prev);
1647
1614
  }
1648
1615
  const table = channel.channelPressureTable;
1649
1616
  this.processActiveNotes(channel, scheduleTime, (note) => {
@@ -1703,10 +1670,15 @@ export class Midy {
1703
1670
  .setValueAtTime(volumeDepth, scheduleTime);
1704
1671
  }
1705
1672
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1673
+ let value = note.voiceParams.reverbEffectsSend;
1674
+ if (channel.isDrum) {
1675
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1676
+ if (0 <= keyBasedValue) {
1677
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1678
+ }
1679
+ }
1706
1680
  if (0 < prevValue) {
1707
- if (0 < note.voiceParams.reverbEffectsSend) {
1708
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1709
- const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1681
+ if (0 < value) {
1710
1682
  note.reverbEffectsSend.gain
1711
1683
  .cancelScheduledValues(scheduleTime)
1712
1684
  .setValueAtTime(value, scheduleTime);
@@ -1716,22 +1688,27 @@ export class Midy {
1716
1688
  }
1717
1689
  }
1718
1690
  else {
1719
- if (0 < note.voiceParams.reverbEffectsSend) {
1691
+ if (0 < value) {
1720
1692
  if (!note.reverbEffectsSend) {
1721
1693
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1722
- gain: note.voiceParams.reverbEffectsSend,
1694
+ gain: value,
1723
1695
  });
1724
- note.volumeNode.connect(note.reverbEffectsSend);
1696
+ note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
1725
1697
  }
1726
1698
  note.reverbEffectsSend.connect(this.reverbEffect.input);
1727
1699
  }
1728
1700
  }
1729
1701
  }
1730
1702
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1703
+ let value = note.voiceParams.chorusEffectsSend;
1704
+ if (channel.isDrum) {
1705
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1706
+ if (0 <= keyBasedValue) {
1707
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1708
+ }
1709
+ }
1731
1710
  if (0 < prevValue) {
1732
- if (0 < note.voiceParams.chorusEffectsSend) {
1733
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1734
- const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1711
+ if (0 < vaule) {
1735
1712
  note.chorusEffectsSend.gain
1736
1713
  .cancelScheduledValues(scheduleTime)
1737
1714
  .setValueAtTime(value, scheduleTime);
@@ -1741,12 +1718,12 @@ export class Midy {
1741
1718
  }
1742
1719
  }
1743
1720
  else {
1744
- if (0 < note.voiceParams.chorusEffectsSend) {
1721
+ if (0 < value) {
1745
1722
  if (!note.chorusEffectsSend) {
1746
1723
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1747
- gain: note.voiceParams.chorusEffectsSend,
1724
+ gain: value,
1748
1725
  });
1749
- note.volumeNode.connect(note.chorusEffectsSend);
1726
+ note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
1750
1727
  }
1751
1728
  note.chorusEffectsSend.connect(this.chorusEffect.input);
1752
1729
  }
@@ -1821,21 +1798,22 @@ export class Midy {
1821
1798
  },
1822
1799
  };
1823
1800
  }
1824
- getControllerState(channel, noteNumber, velocity) {
1801
+ getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
1825
1802
  const state = new Float32Array(channel.state.array.length);
1826
1803
  state.set(channel.state.array);
1827
1804
  state[2] = velocity / 127;
1828
1805
  state[3] = noteNumber / 127;
1829
- state[10] = state.polyphonicKeyPressure / 127;
1806
+ state[10] = polyphonicKeyPressure / 127;
1830
1807
  state[13] = state.channelPressure / 127;
1831
1808
  return state;
1832
1809
  }
1833
1810
  applyVoiceParams(channel, controllerType, scheduleTime) {
1834
- this.processScheduledNotes(channel, (note) => {
1835
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1811
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1812
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1836
1813
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1837
- let appliedFilterEnvelope = false;
1838
- let appliedVolumeEnvelope = false;
1814
+ let applyVolumeEnvelope = false;
1815
+ let applyFilterEnvelope = false;
1816
+ let applyPitchEnvelope = false;
1839
1817
  for (const [key, value] of Object.entries(voiceParams)) {
1840
1818
  const prevValue = note.voiceParams[key];
1841
1819
  if (value === prevValue)
@@ -1844,37 +1822,23 @@ export class Midy {
1844
1822
  if (key in this.voiceParamsHandlers) {
1845
1823
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1846
1824
  }
1847
- else if (filterEnvelopeKeySet.has(key)) {
1848
- if (appliedFilterEnvelope)
1849
- continue;
1850
- appliedFilterEnvelope = true;
1851
- const noteVoiceParams = note.voiceParams;
1852
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1853
- const key = filterEnvelopeKeys[i];
1854
- if (key in voiceParams)
1855
- noteVoiceParams[key] = voiceParams[key];
1856
- }
1857
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1858
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1859
- }
1860
- else {
1861
- this.setFilterEnvelope(channel, note, scheduleTime);
1862
- }
1863
- this.setPitchEnvelope(note, scheduleTime);
1864
- }
1865
- else if (volumeEnvelopeKeySet.has(key)) {
1866
- if (appliedVolumeEnvelope)
1867
- continue;
1868
- appliedVolumeEnvelope = true;
1869
- const noteVoiceParams = note.voiceParams;
1870
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1871
- const key = volumeEnvelopeKeys[i];
1872
- if (key in voiceParams)
1873
- noteVoiceParams[key] = voiceParams[key];
1874
- }
1875
- this.setVolumeEnvelope(channel, note, scheduleTime);
1825
+ else {
1826
+ if (volumeEnvelopeKeySet.has(key))
1827
+ applyVolumeEnvelope = true;
1828
+ if (filterEnvelopeKeySet.has(key))
1829
+ applyFilterEnvelope = true;
1830
+ if (pitchEnvelopeKeySet.has(key))
1831
+ applyPitchEnvelope = true;
1876
1832
  }
1877
1833
  }
1834
+ if (applyVolumeEnvelope) {
1835
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1836
+ }
1837
+ if (applyFilterEnvelope) {
1838
+ this.setFilterEnvelope(channel, note, scheduleTime);
1839
+ }
1840
+ if (applyPitchEnvelope)
1841
+ this.setPitchEnvelope(note, scheduleTime);
1878
1842
  });
1879
1843
  }
1880
1844
  createControlChangeHandlers() {
@@ -1921,7 +1885,7 @@ export class Midy {
1921
1885
  handler.call(this, channelNumber, value, scheduleTime);
1922
1886
  const channel = this.channels[channelNumber];
1923
1887
  this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
1924
- this.applyControlTable(channel, controllerType);
1888
+ this.applyControlTable(channel, controllerType, scheduleTime);
1925
1889
  }
1926
1890
  else {
1927
1891
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
@@ -1932,7 +1896,7 @@ export class Midy {
1932
1896
  }
1933
1897
  updateModulation(channel, scheduleTime) {
1934
1898
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1935
- this.processScheduledNotes(channel, (note) => {
1899
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1936
1900
  if (note.modulationDepth) {
1937
1901
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1938
1902
  }
@@ -1951,7 +1915,7 @@ export class Midy {
1951
1915
  this.updateModulation(channel, scheduleTime);
1952
1916
  }
1953
1917
  updatePortamento(channel, scheduleTime) {
1954
- this.processScheduledNotes(channel, (note) => {
1918
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1955
1919
  if (0.5 <= channel.state.portamento) {
1956
1920
  if (0 <= note.portamentoNoteNumber) {
1957
1921
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -1978,22 +1942,12 @@ export class Midy {
1978
1942
  return;
1979
1943
  this.updatePortamento(channel, scheduleTime);
1980
1944
  }
1981
- setKeyBasedVolume(channel, scheduleTime) {
1982
- this.processScheduledNotes(channel, (note) => {
1983
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1984
- if (keyBasedValue !== 0) {
1985
- note.volumeNode.gain
1986
- .cancelScheduledValues(scheduleTime)
1987
- .setValueAtTime(1 + keyBasedValue, scheduleTime);
1988
- }
1989
- });
1990
- }
1991
1945
  setVolume(channelNumber, volume, scheduleTime) {
1992
1946
  scheduleTime ??= this.audioContext.currentTime;
1993
1947
  const channel = this.channels[channelNumber];
1994
1948
  channel.state.volume = volume / 127;
1995
1949
  this.updateChannelVolume(channel, scheduleTime);
1996
- this.setKeyBasedVolume(channel, scheduleTime);
1950
+ this.updateKeyBasedVolume(channel, scheduleTime);
1997
1951
  }
1998
1952
  panToGain(pan) {
1999
1953
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -2002,26 +1956,12 @@ export class Midy {
2002
1956
  gainRight: Math.sin(theta),
2003
1957
  };
2004
1958
  }
2005
- setKeyBasedPan(channel, scheduleTime) {
2006
- this.processScheduledNotes(channel, (note) => {
2007
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
2008
- if (keyBasedValue !== 0) {
2009
- const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
2010
- note.gainL.gain
2011
- .cancelScheduledValues(scheduleTime)
2012
- .setValueAtTime(gainLeft, scheduleTime);
2013
- note.gainR.gain
2014
- .cancelScheduledValues(scheduleTime)
2015
- .setValueAtTime(gainRight, scheduleTime);
2016
- }
2017
- });
2018
- }
2019
1959
  setPan(channelNumber, pan, scheduleTime) {
2020
1960
  scheduleTime ??= this.audioContext.currentTime;
2021
1961
  const channel = this.channels[channelNumber];
2022
1962
  channel.state.pan = pan / 127;
2023
1963
  this.updateChannelVolume(channel, scheduleTime);
2024
- this.setKeyBasedPan(channel, scheduleTime);
1964
+ this.updateKeyBasedVolume(channel, scheduleTime);
2025
1965
  }
2026
1966
  setExpression(channelNumber, expression, scheduleTime) {
2027
1967
  scheduleTime ??= this.audioContext.currentTime;
@@ -2047,6 +1987,34 @@ export class Midy {
2047
1987
  .cancelScheduledValues(scheduleTime)
2048
1988
  .setValueAtTime(volume * gainRight, scheduleTime);
2049
1989
  }
1990
+ updateKeyBasedVolume(channel, scheduleTime) {
1991
+ if (!channel.isDrum)
1992
+ return;
1993
+ const state = channel.state;
1994
+ const defaultVolume = state.volume * state.expression;
1995
+ const defaultPan = state.pan;
1996
+ for (let i = 0; i < 128; i++) {
1997
+ const gainL = channel.keyBasedGainLs[i];
1998
+ const gainR = channel.keyBasedGainLs[i];
1999
+ if (!gainL)
2000
+ continue;
2001
+ if (!gainR)
2002
+ continue;
2003
+ const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
2004
+ const volume = (0 <= keyBasedVolume)
2005
+ ? defaultVolume * keyBasedVolume / 64
2006
+ : defaultVolume;
2007
+ const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
2008
+ const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2009
+ const { gainLeft, gainRight } = this.panToGain(pan);
2010
+ gainL.gain
2011
+ .cancelScheduledValues(scheduleTime)
2012
+ .setValueAtTime(volume * gainLeft, scheduleTime);
2013
+ gainR.gain
2014
+ .cancelScheduledValues(scheduleTime)
2015
+ .setValueAtTime(volume * gainRight, scheduleTime);
2016
+ }
2017
+ }
2050
2018
  setSustainPedal(channelNumber, value, scheduleTime) {
2051
2019
  const channel = this.channels[channelNumber];
2052
2020
  if (channel.isDrum)
@@ -2054,7 +2022,7 @@ export class Midy {
2054
2022
  scheduleTime ??= this.audioContext.currentTime;
2055
2023
  channel.state.sustainPedal = value / 127;
2056
2024
  if (64 <= value) {
2057
- this.processScheduledNotes(channel, (note) => {
2025
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2058
2026
  channel.sustainNotes.push(note);
2059
2027
  });
2060
2028
  }
@@ -2087,6 +2055,9 @@ export class Midy {
2087
2055
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
2088
2056
  }
2089
2057
  }
2058
+ getSoftPedalFactor(channel, note) {
2059
+ return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
2060
+ }
2090
2061
  setSoftPedal(channelNumber, softPedal, scheduleTime) {
2091
2062
  const channel = this.channels[channelNumber];
2092
2063
  if (channel.isDrum)
@@ -2094,7 +2065,7 @@ export class Midy {
2094
2065
  const state = channel.state;
2095
2066
  scheduleTime ??= this.audioContext.currentTime;
2096
2067
  state.softPedal = softPedal / 127;
2097
- this.processScheduledNotes(channel, (note) => {
2068
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2098
2069
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2099
2070
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2100
2071
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2112,7 +2083,7 @@ export class Midy {
2112
2083
  scheduleTime ??= this.audioContext.currentTime;
2113
2084
  const state = channel.state;
2114
2085
  state.filterResonance = filterResonance / 127;
2115
- this.processScheduledNotes(channel, (note) => {
2086
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2116
2087
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2117
2088
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2118
2089
  });
@@ -2130,7 +2101,7 @@ export class Midy {
2130
2101
  return;
2131
2102
  scheduleTime ??= this.audioContext.currentTime;
2132
2103
  channel.state.attackTime = attackTime / 127;
2133
- this.processScheduledNotes(channel, (note) => {
2104
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2134
2105
  if (note.startTime < scheduleTime)
2135
2106
  return false;
2136
2107
  this.setVolumeEnvelope(channel, note);
@@ -2143,7 +2114,7 @@ export class Midy {
2143
2114
  const state = channel.state;
2144
2115
  scheduleTime ??= this.audioContext.currentTime;
2145
2116
  state.brightness = brightness / 127;
2146
- this.processScheduledNotes(channel, (note) => {
2117
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2147
2118
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2148
2119
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2149
2120
  }
@@ -2158,7 +2129,7 @@ export class Midy {
2158
2129
  return;
2159
2130
  scheduleTime ??= this.audioContext.currentTime;
2160
2131
  channel.state.decayTime = dacayTime / 127;
2161
- this.processScheduledNotes(channel, (note) => {
2132
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2162
2133
  this.setVolumeEnvelope(channel, note, scheduleTime);
2163
2134
  });
2164
2135
  }
@@ -2170,7 +2141,7 @@ export class Midy {
2170
2141
  channel.state.vibratoRate = vibratoRate / 127;
2171
2142
  if (channel.vibratoDepth <= 0)
2172
2143
  return;
2173
- this.processScheduledNotes(channel, (note) => {
2144
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2174
2145
  this.setVibLfoToPitch(channel, note, scheduleTime);
2175
2146
  });
2176
2147
  }
@@ -2182,12 +2153,12 @@ export class Midy {
2182
2153
  const prev = channel.state.vibratoDepth;
2183
2154
  channel.state.vibratoDepth = vibratoDepth / 127;
2184
2155
  if (0 < prev) {
2185
- this.processScheduledNotes(channel, (note) => {
2156
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2186
2157
  this.setFreqVibLFO(channel, note, scheduleTime);
2187
2158
  });
2188
2159
  }
2189
2160
  else {
2190
- this.processScheduledNotes(channel, (note) => {
2161
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2191
2162
  this.startVibrato(channel, note, scheduleTime);
2192
2163
  });
2193
2164
  }
@@ -2199,7 +2170,7 @@ export class Midy {
2199
2170
  scheduleTime ??= this.audioContext.currentTime;
2200
2171
  channel.state.vibratoDelay = vibratoDelay / 127;
2201
2172
  if (0 < channel.state.vibratoDepth) {
2202
- this.processScheduledNotes(channel, (note) => {
2173
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2203
2174
  this.startVibrato(channel, note, scheduleTime);
2204
2175
  });
2205
2176
  }
@@ -2217,16 +2188,17 @@ export class Midy {
2217
2188
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2218
2189
  }
2219
2190
  else {
2220
- this.processScheduledNotes(channel, (note) => {
2191
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2221
2192
  if (note.voiceParams.reverbEffectsSend <= 0)
2222
2193
  return false;
2223
- note.reverbEffectsSend.disconnect();
2194
+ if (note.reverbEffectsSend)
2195
+ note.reverbEffectsSend.disconnect();
2224
2196
  });
2225
2197
  }
2226
2198
  }
2227
2199
  else {
2228
2200
  if (0 < reverbSendLevel) {
2229
- this.processScheduledNotes(channel, (note) => {
2201
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2230
2202
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2231
2203
  });
2232
2204
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2249,16 +2221,17 @@ export class Midy {
2249
2221
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2250
2222
  }
2251
2223
  else {
2252
- this.processScheduledNotes(channel, (note) => {
2224
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2253
2225
  if (note.voiceParams.chorusEffectsSend <= 0)
2254
2226
  return false;
2255
- note.chorusEffectsSend.disconnect();
2227
+ if (note.chorusEffectsSend)
2228
+ note.chorusEffectsSend.disconnect();
2256
2229
  });
2257
2230
  }
2258
2231
  }
2259
2232
  else {
2260
2233
  if (0 < chorusSendLevel) {
2261
- this.processScheduledNotes(channel, (note) => {
2234
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2262
2235
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2263
2236
  });
2264
2237
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2673,8 +2646,7 @@ export class Midy {
2673
2646
  setReverbType(type) {
2674
2647
  this.reverb.time = this.getReverbTimeFromType(type);
2675
2648
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
2676
- const { audioContext, options } = this;
2677
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2649
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2678
2650
  }
2679
2651
  getReverbTimeFromType(type) {
2680
2652
  switch (type) {
@@ -2696,8 +2668,7 @@ export class Midy {
2696
2668
  }
2697
2669
  setReverbTime(value) {
2698
2670
  this.reverb.time = this.getReverbTime(value);
2699
- const { audioContext, options } = this;
2700
- this.reverbEffect = options.reverbAlgorithm(audioContext);
2671
+ this.reverbEffect = this.createReverbEffect(this.audioContext);
2701
2672
  }
2702
2673
  getReverbTime(value) {
2703
2674
  return Math.exp((value - 40) * 0.025);
@@ -2892,65 +2863,88 @@ export class Midy {
2892
2863
  }
2893
2864
  }
2894
2865
  getPitchControl(channel, note) {
2895
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2866
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
2867
+ if (polyphonicKeyPressureRaw < 0)
2868
+ return 0;
2869
+ const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
2896
2870
  note.pressure;
2897
2871
  return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2898
2872
  }
2899
2873
  getFilterCutoffControl(channel, note) {
2900
- const channelPressure = (channel.channelPressureTable[1] - 64) *
2901
- channel.state.channelPressure;
2902
- const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2903
- note.pressure;
2874
+ const channelPressureRaw = channel.channelPressureTable[1];
2875
+ const channelPressure = (0 <= channelPressureRaw)
2876
+ ? (channelPressureRaw - 64) * channel.state.channelPressure
2877
+ : 0;
2878
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[1];
2879
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2880
+ ? (polyphonicKeyPressureRaw - 64) * note.pressure
2881
+ : 0;
2904
2882
  return (channelPressure + polyphonicKeyPressure) * 15;
2905
2883
  }
2906
2884
  getAmplitudeControl(channel, note) {
2907
- const channelPressure = channel.channelPressureTable[2] *
2908
- channel.state.channelPressure;
2909
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2910
- note.pressure;
2885
+ const channelPressureRaw = channel.channelPressureTable[2];
2886
+ const channelPressure = (0 <= channelPressureRaw)
2887
+ ? channelPressureRaw * channel.state.channelPressure
2888
+ : 0;
2889
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
2890
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2891
+ ? polyphonicKeyPressureRaw * note.pressure
2892
+ : 0;
2911
2893
  return (channelPressure + polyphonicKeyPressure) / 128;
2912
2894
  }
2913
2895
  getLFOPitchDepth(channel, note) {
2914
- const channelPressure = channel.channelPressureTable[3] *
2915
- channel.state.channelPressure;
2916
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2917
- note.pressure;
2896
+ const channelPressureRaw = channel.channelPressureTable[3];
2897
+ const channelPressure = (0 <= channelPressureRaw)
2898
+ ? channelPressureRaw * channel.state.channelPressure
2899
+ : 0;
2900
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
2901
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2902
+ ? polyphonicKeyPressureRaw * note.pressure
2903
+ : 0;
2918
2904
  return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2919
2905
  }
2920
2906
  getLFOFilterDepth(channel, note) {
2921
- const channelPressure = channel.channelPressureTable[4] *
2922
- channel.state.channelPressure;
2923
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2924
- note.pressure;
2907
+ const channelPressureRaw = channel.channelPressureTable[4];
2908
+ const channelPressure = (0 <= channelPressureRaw)
2909
+ ? channelPressureRaw * channel.state.channelPressure
2910
+ : 0;
2911
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
2912
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2913
+ ? polyphonicKeyPressureRaw * note.pressure
2914
+ : 0;
2925
2915
  return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2926
2916
  }
2927
2917
  getLFOAmplitudeDepth(channel, note) {
2928
- const channelPressure = channel.channelPressureTable[5] *
2929
- channel.state.channelPressure;
2930
- const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2931
- note.pressure;
2918
+ const channelPressureRaw = channel.channelPressureTable[5];
2919
+ const channelPressure = (0 <= channelPressureRaw)
2920
+ ? channelPressureRaw * channel.state.channelPressure
2921
+ : 0;
2922
+ const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[5];
2923
+ const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
2924
+ ? polyphonicKeyPressureRaw * note.pressure
2925
+ : 0;
2932
2926
  return (channelPressure + polyphonicKeyPressure) / 254;
2933
2927
  }
2934
2928
  setControllerParameters(channel, note, table) {
2935
- if (table[0] !== 64)
2929
+ if (0 <= table[0])
2936
2930
  this.updateDetune(channel, note);
2937
2931
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2938
- if (table[1] !== 64)
2932
+ if (0 <= table[1])
2939
2933
  this.setPortamentoFilterEnvelope(channel, note);
2940
- if (table[2] !== 64)
2934
+ if (0 <= table[2])
2941
2935
  this.setPortamentoVolumeEnvelope(channel, note);
2942
2936
  }
2943
2937
  else {
2944
- if (table[1] !== 64)
2938
+ if (0 <= table[1])
2945
2939
  this.setFilterEnvelope(channel, note);
2946
- if (table[2] !== 64)
2940
+ if (0 <= table[2])
2947
2941
  this.setVolumeEnvelope(channel, note);
2948
2942
  }
2949
- if (table[3] !== 0)
2943
+ if (0 <= table[3])
2950
2944
  this.setModLfoToPitch(channel, note);
2951
- if (table[4] !== 0)
2945
+ if (0 <= table[4])
2952
2946
  this.setModLfoToFilterFc(channel, note);
2953
- if (table[5] !== 0)
2947
+ if (0 <= table[5])
2954
2948
  this.setModLfoToVolume(channel, note);
2955
2949
  }
2956
2950
  handlePressureSysEx(data, tableName) {
@@ -2966,26 +2960,15 @@ export class Midy {
2966
2960
  }
2967
2961
  }
2968
2962
  initControlTable() {
2969
- const channelCount = 128;
2963
+ const ccCount = 128;
2970
2964
  const slotSize = 6;
2971
- const table = new Uint8Array(channelCount * slotSize);
2972
- return this.resetControlTable(table);
2965
+ return new Int8Array(ccCount * slotSize).fill(-1);
2973
2966
  }
2974
- resetControlTable(table) {
2975
- const channelCount = 128;
2976
- const slotSize = 6;
2977
- const defaultValues = [64, 64, 64, 0, 0, 0];
2978
- for (let ch = 0; ch < channelCount; ch++) {
2979
- const offset = ch * slotSize;
2980
- table.set(defaultValues, offset);
2981
- }
2982
- return table;
2983
- }
2984
- applyControlTable(channel, controllerType) {
2967
+ applyControlTable(channel, controllerType, scheduleTime) {
2985
2968
  const slotSize = 6;
2986
2969
  const offset = controllerType * slotSize;
2987
2970
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2988
- this.processScheduledNotes(channel, (note) => {
2971
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
2989
2972
  this.setControllerParameters(channel, note, table);
2990
2973
  });
2991
2974
  }
@@ -3005,12 +2988,12 @@ export class Midy {
3005
2988
  getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
3006
2989
  const index = keyNumber * 128 + controllerType;
3007
2990
  const controlValue = channel.keyBasedInstrumentControlTable[index];
3008
- return (controlValue + 64) / 64;
2991
+ return controlValue;
3009
2992
  }
3010
2993
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
3011
2994
  const channelNumber = data[4];
3012
2995
  const channel = this.channels[channelNumber];
3013
- if (channel.isDrum)
2996
+ if (!channel.isDrum)
3014
2997
  return;
3015
2998
  const keyNumber = data[5];
3016
2999
  const table = channel.keyBasedInstrumentControlTable;
@@ -3018,7 +3001,7 @@ export class Midy {
3018
3001
  const controllerType = data[i];
3019
3002
  const value = data[i + 1];
3020
3003
  const index = keyNumber * 128 + controllerType;
3021
- table[index] = value - 64;
3004
+ table[index] = value;
3022
3005
  }
3023
3006
  this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3024
3007
  }