@marmooo/midy 0.3.4 → 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
@@ -329,13 +329,13 @@ export class Midy {
329
329
  writable: true,
330
330
  value: this.initSoundFontTable()
331
331
  });
332
- Object.defineProperty(this, "audioBufferCounter", {
332
+ Object.defineProperty(this, "voiceCounter", {
333
333
  enumerable: true,
334
334
  configurable: true,
335
335
  writable: true,
336
336
  value: new Map()
337
337
  });
338
- Object.defineProperty(this, "audioBufferCache", {
338
+ Object.defineProperty(this, "voiceCache", {
339
339
  enumerable: true,
340
340
  configurable: true,
341
341
  writable: true,
@@ -432,13 +432,11 @@ export class Midy {
432
432
  const presetHeaders = soundFont.parsed.presetHeaders;
433
433
  for (let i = 0; i < presetHeaders.length; i++) {
434
434
  const presetHeader = presetHeaders[i];
435
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
436
- const banks = this.soundFontTable[presetHeader.preset];
437
- banks.set(presetHeader.bank, index);
438
- }
435
+ const banks = this.soundFontTable[presetHeader.preset];
436
+ banks.set(presetHeader.bank, index);
439
437
  }
440
438
  }
441
- async loadSoundFont(input) {
439
+ async toUint8Array(input) {
442
440
  let uint8Array;
443
441
  if (typeof input === "string") {
444
442
  const response = await fetch(input);
@@ -451,23 +449,32 @@ export class Midy {
451
449
  else {
452
450
  throw new TypeError("input must be a URL string or Uint8Array");
453
451
  }
454
- const parsed = parse(uint8Array);
455
- const soundFont = new SoundFont(parsed);
456
- this.addSoundFont(soundFont);
452
+ return uint8Array;
457
453
  }
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;
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
+ }
467
467
  }
468
468
  else {
469
- 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);
470
473
  }
474
+ }
475
+ async loadMIDI(input) {
476
+ this.voiceCounter.clear();
477
+ const uint8Array = await this.toUint8Array(input);
471
478
  const midi = parseMidi(uint8Array);
472
479
  this.ticksPerBeat = midi.header.ticksPerBeat;
473
480
  const midiData = this.extractMidiData(midi);
@@ -475,6 +482,45 @@ export class Midy {
475
482
  this.timeline = midiData.timeline;
476
483
  this.totalTime = this.calcTotalTime();
477
484
  }
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
+ }
478
524
  createChannelAudioNodes(audioContext) {
479
525
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
480
526
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -518,34 +564,12 @@ export class Midy {
518
564
  });
519
565
  return channels;
520
566
  }
521
- async createNoteBuffer(voiceParams, isSF3) {
567
+ async createAudioBuffer(voiceParams) {
568
+ const sample = voiceParams.sample;
522
569
  const sampleStart = voiceParams.start;
523
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
524
- if (isSF3) {
525
- const sample = voiceParams.sample;
526
- const start = sample.byteOffset + sampleStart;
527
- const end = sample.byteOffset + sampleEnd;
528
- const buffer = sample.buffer.slice(start, end);
529
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
530
- return audioBuffer;
531
- }
532
- else {
533
- const sample = voiceParams.sample;
534
- const start = sample.byteOffset + sampleStart;
535
- const end = sample.byteOffset + sampleEnd;
536
- const buffer = sample.buffer.slice(start, end);
537
- const audioBuffer = new AudioBuffer({
538
- numberOfChannels: 1,
539
- length: sample.length,
540
- sampleRate: voiceParams.sampleRate,
541
- });
542
- const channelData = audioBuffer.getChannelData(0);
543
- const int16Array = new Int16Array(buffer);
544
- for (let i = 0; i < int16Array.length; i++) {
545
- channelData[i] = int16Array[i] / 32768;
546
- }
547
- return audioBuffer;
548
- }
570
+ const sampleEnd = sample.data.length + voiceParams.end;
571
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
572
+ return audioBuffer;
549
573
  }
550
574
  isLoopDrum(channel, noteNumber) {
551
575
  const programNumber = channel.programNumber;
@@ -582,16 +606,16 @@ export class Midy {
582
606
  break;
583
607
  }
584
608
  case "noteAftertouch":
585
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
609
+ this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
586
610
  break;
587
611
  case "controller":
588
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
612
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
589
613
  break;
590
614
  case "programChange":
591
- this.handleProgramChange(event.channel, event.programNumber, startTime);
615
+ this.setProgramChange(event.channel, event.programNumber, startTime);
592
616
  break;
593
617
  case "channelAftertouch":
594
- this.handleChannelPressure(event.channel, event.amount, startTime);
618
+ this.setChannelPressure(event.channel, event.amount, startTime);
595
619
  break;
596
620
  case "pitchBend":
597
621
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -625,8 +649,9 @@ export class Midy {
625
649
  this.notePromises = [];
626
650
  this.exclusiveClassNotes.fill(undefined);
627
651
  this.drumExclusiveClassNotes.fill(undefined);
628
- this.audioBufferCache.clear();
652
+ this.voiceCache.clear();
629
653
  for (let i = 0; i < this.channels.length; i++) {
654
+ this.channels[i].scheduledNotes = [];
630
655
  this.resetAllStates(i);
631
656
  }
632
657
  resolve();
@@ -648,8 +673,9 @@ export class Midy {
648
673
  this.notePromises = [];
649
674
  this.exclusiveClassNotes.fill(undefined);
650
675
  this.drumExclusiveClassNotes.fill(undefined);
651
- this.audioBufferCache.clear();
676
+ this.voiceCache.clear();
652
677
  for (let i = 0; i < this.channels.length; i++) {
678
+ this.channels[i].scheduledNotes = [];
653
679
  this.resetAllStates(i);
654
680
  }
655
681
  this.isStopping = false;
@@ -682,11 +708,7 @@ export class Midy {
682
708
  secondToTicks(second, secondsPerBeat) {
683
709
  return second * this.ticksPerBeat / secondsPerBeat;
684
710
  }
685
- getAudioBufferId(programNumber, noteNumber, velocity) {
686
- return `${programNumber}:${noteNumber}:${velocity}`;
687
- }
688
711
  extractMidiData(midi) {
689
- this.audioBufferCounter.clear();
690
712
  const instruments = new Set();
691
713
  const timeline = [];
692
714
  const tmpChannels = new Array(this.channels.length);
@@ -707,8 +729,6 @@ export class Midy {
707
729
  switch (event.type) {
708
730
  case "noteOn": {
709
731
  const channel = tmpChannels[event.channel];
710
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
711
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
712
732
  if (channel.programNumber < 0) {
713
733
  channel.programNumber = event.programNumber;
714
734
  switch (channel.bankMSB) {
@@ -758,10 +778,6 @@ export class Midy {
758
778
  timeline.push(event);
759
779
  }
760
780
  }
761
- for (const [audioBufferId, count] of this.audioBufferCounter) {
762
- if (count === 1)
763
- this.audioBufferCounter.delete(audioBufferId);
764
- }
765
781
  const priority = {
766
782
  controller: 0,
767
783
  sysEx: 1,
@@ -801,12 +817,11 @@ export class Midy {
801
817
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
802
818
  const channel = this.channels[channelNumber];
803
819
  const promises = [];
804
- this.processScheduledNotes(channel, scheduleTime, (note) => {
820
+ this.processScheduledNotes(channel, (note) => {
805
821
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
806
822
  this.notePromises.push(promise);
807
823
  promises.push(promise);
808
824
  });
809
- channel.scheduledNotes = [];
810
825
  return Promise.all(promises);
811
826
  }
812
827
  stopNotes(velocity, force, scheduleTime) {
@@ -820,6 +835,8 @@ export class Midy {
820
835
  if (this.isPlaying || this.isPaused)
821
836
  return;
822
837
  this.resumeTime = 0;
838
+ if (this.voiceCounter.size === 0)
839
+ this.cacheVoiceIds();
823
840
  await this.playNotes();
824
841
  this.isPlaying = false;
825
842
  }
@@ -860,22 +877,20 @@ export class Midy {
860
877
  const now = this.audioContext.currentTime;
861
878
  return this.resumeTime + now - this.startTime - this.startDelay;
862
879
  }
863
- processScheduledNotes(channel, scheduleTime, callback) {
880
+ processScheduledNotes(channel, callback) {
864
881
  const scheduledNotes = channel.scheduledNotes;
865
- for (let i = 0; i < scheduledNotes.length; i++) {
882
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
866
883
  const note = scheduledNotes[i];
867
884
  if (!note)
868
885
  continue;
869
886
  if (note.ending)
870
887
  continue;
871
- if (note.startTime < scheduleTime)
872
- continue;
873
888
  callback(note);
874
889
  }
875
890
  }
876
891
  processActiveNotes(channel, scheduleTime, callback) {
877
892
  const scheduledNotes = channel.scheduledNotes;
878
- for (let i = 0; i < scheduledNotes.length; i++) {
893
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
879
894
  const note = scheduledNotes[i];
880
895
  if (!note)
881
896
  continue;
@@ -1066,7 +1081,7 @@ export class Midy {
1066
1081
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1067
1082
  }
1068
1083
  updateChannelDetune(channel, scheduleTime) {
1069
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1084
+ this.processScheduledNotes(channel, (note) => {
1070
1085
  this.updateDetune(channel, note, scheduleTime);
1071
1086
  });
1072
1087
  }
@@ -1294,31 +1309,31 @@ export class Midy {
1294
1309
  note.vibratoLFO.connect(note.vibratoDepth);
1295
1310
  note.vibratoDepth.connect(note.bufferSource.detune);
1296
1311
  }
1297
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1298
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1299
- 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);
1300
1315
  if (cache) {
1301
1316
  cache.counter += 1;
1302
1317
  if (cache.maxCount <= cache.counter) {
1303
- this.audioBufferCache.delete(audioBufferId);
1318
+ this.voiceCache.delete(audioBufferId);
1304
1319
  }
1305
1320
  return cache.audioBuffer;
1306
1321
  }
1307
1322
  else {
1308
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1309
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1323
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1324
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1310
1325
  const cache = { audioBuffer, maxCount, counter: 1 };
1311
- this.audioBufferCache.set(audioBufferId, cache);
1326
+ this.voiceCache.set(audioBufferId, cache);
1312
1327
  return audioBuffer;
1313
1328
  }
1314
1329
  }
1315
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1330
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1316
1331
  const now = this.audioContext.currentTime;
1317
1332
  const state = channel.state;
1318
1333
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1319
1334
  const voiceParams = voice.getAllParams(controllerState);
1320
1335
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1321
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1336
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1322
1337
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1323
1338
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1324
1339
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -1413,15 +1428,15 @@ export class Midy {
1413
1428
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1414
1429
  const channel = this.channels[channelNumber];
1415
1430
  const bankNumber = this.calcBank(channel, channelNumber);
1416
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1431
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1432
+ .get(bankNumber);
1417
1433
  if (soundFontIndex === undefined)
1418
1434
  return;
1419
1435
  const soundFont = this.soundFonts[soundFontIndex];
1420
1436
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1421
1437
  if (!voice)
1422
1438
  return;
1423
- const isSF3 = soundFont.parsed.info.version.major === 3;
1424
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1439
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1425
1440
  if (channel.isDrum) {
1426
1441
  const audioContext = this.audioContext;
1427
1442
  const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
@@ -1504,15 +1519,29 @@ export class Midy {
1504
1519
  return;
1505
1520
  }
1506
1521
  }
1507
- const note = this.findNoteOffTarget(channel, noteNumber);
1508
- if (!note)
1522
+ const index = this.findNoteOffIndex(channel, noteNumber);
1523
+ if (index < 0)
1509
1524
  return;
1525
+ const note = channel.scheduledNotes[index];
1510
1526
  note.ending = true;
1527
+ this.setNoteIndex(channel, index);
1511
1528
  this.releaseNote(channel, note, endTime);
1512
1529
  }
1513
- 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) {
1514
1543
  const scheduledNotes = channel.scheduledNotes;
1515
- for (let i = 0; i < scheduledNotes.length; i++) {
1544
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1516
1545
  const note = scheduledNotes[i];
1517
1546
  if (!note)
1518
1547
  continue;
@@ -1520,8 +1549,9 @@ export class Midy {
1520
1549
  continue;
1521
1550
  if (note.noteNumber !== noteNumber)
1522
1551
  continue;
1523
- return note;
1552
+ return i;
1524
1553
  }
1554
+ return -1;
1525
1555
  }
1526
1556
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1527
1557
  scheduleTime ??= this.audioContext.currentTime;
@@ -1561,31 +1591,31 @@ export class Midy {
1561
1591
  case 0x90:
1562
1592
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1563
1593
  case 0xA0:
1564
- return this.handlePolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1594
+ return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1565
1595
  case 0xB0:
1566
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1596
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1567
1597
  case 0xC0:
1568
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1598
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1569
1599
  case 0xD0:
1570
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1600
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1571
1601
  case 0xE0:
1572
1602
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1573
1603
  default:
1574
1604
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1575
1605
  }
1576
1606
  }
1577
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1607
+ setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1578
1608
  const channel = this.channels[channelNumber];
1579
1609
  const table = channel.polyphonicKeyPressureTable;
1580
1610
  this.processActiveNotes(channel, scheduleTime, (note) => {
1581
1611
  if (note.noteNumber === noteNumber) {
1582
1612
  note.pressure = pressure;
1583
- this.setControllerParameters(channel, note, table);
1613
+ this.setControllerParameters(channel, note, table, scheduleTime);
1584
1614
  }
1585
1615
  });
1586
1616
  this.applyVoiceParams(channel, 10);
1587
1617
  }
1588
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1618
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1589
1619
  const channel = this.channels[channelNumber];
1590
1620
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1591
1621
  channel.programNumber = programNumber;
@@ -1600,7 +1630,7 @@ export class Midy {
1600
1630
  }
1601
1631
  }
1602
1632
  }
1603
- handleChannelPressure(channelNumber, value, scheduleTime) {
1633
+ setChannelPressure(channelNumber, value, scheduleTime) {
1604
1634
  const channel = this.channels[channelNumber];
1605
1635
  if (channel.isDrum)
1606
1636
  return;
@@ -1614,7 +1644,7 @@ export class Midy {
1614
1644
  }
1615
1645
  const table = channel.channelPressureTable;
1616
1646
  this.processActiveNotes(channel, scheduleTime, (note) => {
1617
- this.setControllerParameters(channel, note, table);
1647
+ this.setControllerParameters(channel, note, table, scheduleTime);
1618
1648
  });
1619
1649
  this.applyVoiceParams(channel, 13);
1620
1650
  }
@@ -1672,7 +1702,7 @@ export class Midy {
1672
1702
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1673
1703
  let value = note.voiceParams.reverbEffectsSend;
1674
1704
  if (channel.isDrum) {
1675
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1705
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1676
1706
  if (0 <= keyBasedValue) {
1677
1707
  value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1678
1708
  }
@@ -1702,13 +1732,13 @@ export class Midy {
1702
1732
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1703
1733
  let value = note.voiceParams.chorusEffectsSend;
1704
1734
  if (channel.isDrum) {
1705
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1735
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1706
1736
  if (0 <= keyBasedValue) {
1707
1737
  value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1708
1738
  }
1709
1739
  }
1710
1740
  if (0 < prevValue) {
1711
- if (0 < vaule) {
1741
+ if (0 < value) {
1712
1742
  note.chorusEffectsSend.gain
1713
1743
  .cancelScheduledValues(scheduleTime)
1714
1744
  .setValueAtTime(value, scheduleTime);
@@ -1808,7 +1838,7 @@ export class Midy {
1808
1838
  return state;
1809
1839
  }
1810
1840
  applyVoiceParams(channel, controllerType, scheduleTime) {
1811
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1841
+ this.processScheduledNotes(channel, (note) => {
1812
1842
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1813
1843
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1814
1844
  let applyVolumeEnvelope = false;
@@ -1879,7 +1909,7 @@ export class Midy {
1879
1909
  handlers[127] = this.polyOn;
1880
1910
  return handlers;
1881
1911
  }
1882
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1912
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1883
1913
  const handler = this.controlChangeHandlers[controllerType];
1884
1914
  if (handler) {
1885
1915
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1896,7 +1926,7 @@ export class Midy {
1896
1926
  }
1897
1927
  updateModulation(channel, scheduleTime) {
1898
1928
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1899
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1929
+ this.processScheduledNotes(channel, (note) => {
1900
1930
  if (note.modulationDepth) {
1901
1931
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1902
1932
  }
@@ -1915,7 +1945,7 @@ export class Midy {
1915
1945
  this.updateModulation(channel, scheduleTime);
1916
1946
  }
1917
1947
  updatePortamento(channel, scheduleTime) {
1918
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1948
+ this.processScheduledNotes(channel, (note) => {
1919
1949
  if (0.5 <= channel.state.portamento) {
1920
1950
  if (0 <= note.portamentoNoteNumber) {
1921
1951
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -2000,11 +2030,11 @@ export class Midy {
2000
2030
  continue;
2001
2031
  if (!gainR)
2002
2032
  continue;
2003
- const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
2033
+ const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
2004
2034
  const volume = (0 <= keyBasedVolume)
2005
2035
  ? defaultVolume * keyBasedVolume / 64
2006
2036
  : defaultVolume;
2007
- const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
2037
+ const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
2008
2038
  const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2009
2039
  const { gainLeft, gainRight } = this.panToGain(pan);
2010
2040
  gainL.gain
@@ -2022,7 +2052,7 @@ export class Midy {
2022
2052
  scheduleTime ??= this.audioContext.currentTime;
2023
2053
  channel.state.sustainPedal = value / 127;
2024
2054
  if (64 <= value) {
2025
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2055
+ this.processScheduledNotes(channel, (note) => {
2026
2056
  channel.sustainNotes.push(note);
2027
2057
  });
2028
2058
  }
@@ -2065,7 +2095,7 @@ export class Midy {
2065
2095
  const state = channel.state;
2066
2096
  scheduleTime ??= this.audioContext.currentTime;
2067
2097
  state.softPedal = softPedal / 127;
2068
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2098
+ this.processScheduledNotes(channel, (note) => {
2069
2099
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2070
2100
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2071
2101
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2083,7 +2113,7 @@ export class Midy {
2083
2113
  scheduleTime ??= this.audioContext.currentTime;
2084
2114
  const state = channel.state;
2085
2115
  state.filterResonance = filterResonance / 127;
2086
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2116
+ this.processScheduledNotes(channel, (note) => {
2087
2117
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2088
2118
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2089
2119
  });
@@ -2101,10 +2131,10 @@ export class Midy {
2101
2131
  return;
2102
2132
  scheduleTime ??= this.audioContext.currentTime;
2103
2133
  channel.state.attackTime = attackTime / 127;
2104
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2134
+ this.processScheduledNotes(channel, (note) => {
2105
2135
  if (note.startTime < scheduleTime)
2106
2136
  return false;
2107
- this.setVolumeEnvelope(channel, note);
2137
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2108
2138
  });
2109
2139
  }
2110
2140
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2114,12 +2144,12 @@ export class Midy {
2114
2144
  const state = channel.state;
2115
2145
  scheduleTime ??= this.audioContext.currentTime;
2116
2146
  state.brightness = brightness / 127;
2117
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2147
+ this.processScheduledNotes(channel, (note) => {
2118
2148
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2119
2149
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2120
2150
  }
2121
2151
  else {
2122
- this.setFilterEnvelope(channel, note);
2152
+ this.setFilterEnvelope(channel, note, scheduleTime);
2123
2153
  }
2124
2154
  });
2125
2155
  }
@@ -2129,7 +2159,7 @@ export class Midy {
2129
2159
  return;
2130
2160
  scheduleTime ??= this.audioContext.currentTime;
2131
2161
  channel.state.decayTime = dacayTime / 127;
2132
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2162
+ this.processScheduledNotes(channel, (note) => {
2133
2163
  this.setVolumeEnvelope(channel, note, scheduleTime);
2134
2164
  });
2135
2165
  }
@@ -2141,7 +2171,7 @@ export class Midy {
2141
2171
  channel.state.vibratoRate = vibratoRate / 127;
2142
2172
  if (channel.vibratoDepth <= 0)
2143
2173
  return;
2144
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2174
+ this.processScheduledNotes(channel, (note) => {
2145
2175
  this.setVibLfoToPitch(channel, note, scheduleTime);
2146
2176
  });
2147
2177
  }
@@ -2153,12 +2183,12 @@ export class Midy {
2153
2183
  const prev = channel.state.vibratoDepth;
2154
2184
  channel.state.vibratoDepth = vibratoDepth / 127;
2155
2185
  if (0 < prev) {
2156
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2186
+ this.processScheduledNotes(channel, (note) => {
2157
2187
  this.setFreqVibLFO(channel, note, scheduleTime);
2158
2188
  });
2159
2189
  }
2160
2190
  else {
2161
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2191
+ this.processScheduledNotes(channel, (note) => {
2162
2192
  this.startVibrato(channel, note, scheduleTime);
2163
2193
  });
2164
2194
  }
@@ -2170,7 +2200,7 @@ export class Midy {
2170
2200
  scheduleTime ??= this.audioContext.currentTime;
2171
2201
  channel.state.vibratoDelay = vibratoDelay / 127;
2172
2202
  if (0 < channel.state.vibratoDepth) {
2173
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2203
+ this.processScheduledNotes(channel, (note) => {
2174
2204
  this.startVibrato(channel, note, scheduleTime);
2175
2205
  });
2176
2206
  }
@@ -2188,7 +2218,7 @@ export class Midy {
2188
2218
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2189
2219
  }
2190
2220
  else {
2191
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2221
+ this.processScheduledNotes(channel, (note) => {
2192
2222
  if (note.voiceParams.reverbEffectsSend <= 0)
2193
2223
  return false;
2194
2224
  if (note.reverbEffectsSend)
@@ -2198,7 +2228,7 @@ export class Midy {
2198
2228
  }
2199
2229
  else {
2200
2230
  if (0 < reverbSendLevel) {
2201
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2231
+ this.processScheduledNotes(channel, (note) => {
2202
2232
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2203
2233
  });
2204
2234
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2221,7 +2251,7 @@ export class Midy {
2221
2251
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2222
2252
  }
2223
2253
  else {
2224
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2254
+ this.processScheduledNotes(channel, (note) => {
2225
2255
  if (note.voiceParams.chorusEffectsSend <= 0)
2226
2256
  return false;
2227
2257
  if (note.chorusEffectsSend)
@@ -2231,7 +2261,7 @@ export class Midy {
2231
2261
  }
2232
2262
  else {
2233
2263
  if (0 < chorusSendLevel) {
2234
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2264
+ this.processScheduledNotes(channel, (note) => {
2235
2265
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2236
2266
  });
2237
2267
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2389,7 +2419,7 @@ export class Midy {
2389
2419
  const entries = Object.entries(defaultControllerState);
2390
2420
  for (const [key, { type, defaultValue }] of entries) {
2391
2421
  if (128 <= type) {
2392
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2422
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2393
2423
  }
2394
2424
  else {
2395
2425
  state[key] = defaultValue;
@@ -2422,7 +2452,7 @@ export class Midy {
2422
2452
  const key = keys[i];
2423
2453
  const { type, defaultValue } = defaultControllerState[key];
2424
2454
  if (128 <= type) {
2425
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2455
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2426
2456
  }
2427
2457
  else {
2428
2458
  state[key] = defaultValue;
@@ -2925,27 +2955,29 @@ export class Midy {
2925
2955
  : 0;
2926
2956
  return (channelPressure + polyphonicKeyPressure) / 254;
2927
2957
  }
2928
- setControllerParameters(channel, note, table) {
2958
+ setControllerParameters(channel, note, table, scheduleTime) {
2929
2959
  if (0 <= table[0])
2930
- this.updateDetune(channel, note);
2960
+ this.updateDetune(channel, note, scueduleTime);
2931
2961
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2932
- if (0 <= table[1])
2933
- this.setPortamentoFilterEnvelope(channel, note);
2934
- if (0 <= table[2])
2935
- 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
+ }
2936
2968
  }
2937
2969
  else {
2938
2970
  if (0 <= table[1])
2939
- this.setFilterEnvelope(channel, note);
2971
+ this.setFilterEnvelope(channel, note, scheduleTime);
2940
2972
  if (0 <= table[2])
2941
- this.setVolumeEnvelope(channel, note);
2973
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2942
2974
  }
2943
2975
  if (0 <= table[3])
2944
- this.setModLfoToPitch(channel, note);
2976
+ this.setModLfoToPitch(channel, note, scheduleTime);
2945
2977
  if (0 <= table[4])
2946
- this.setModLfoToFilterFc(channel, note);
2978
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2947
2979
  if (0 <= table[5])
2948
- this.setModLfoToVolume(channel, note);
2980
+ this.setModLfoToVolume(channel, note, scheduleTime);
2949
2981
  }
2950
2982
  handlePressureSysEx(data, tableName) {
2951
2983
  const channelNumber = data[4];
@@ -2968,8 +3000,8 @@ export class Midy {
2968
3000
  const slotSize = 6;
2969
3001
  const offset = controllerType * slotSize;
2970
3002
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2971
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2972
- this.setControllerParameters(channel, note, table);
3003
+ this.processScheduledNotes(channel, (note) => {
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;
@@ -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,