@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/script/midy.js CHANGED
@@ -332,13 +332,13 @@ class Midy {
332
332
  writable: true,
333
333
  value: this.initSoundFontTable()
334
334
  });
335
- Object.defineProperty(this, "audioBufferCounter", {
335
+ Object.defineProperty(this, "voiceCounter", {
336
336
  enumerable: true,
337
337
  configurable: true,
338
338
  writable: true,
339
339
  value: new Map()
340
340
  });
341
- Object.defineProperty(this, "audioBufferCache", {
341
+ Object.defineProperty(this, "voiceCache", {
342
342
  enumerable: true,
343
343
  configurable: true,
344
344
  writable: true,
@@ -435,13 +435,11 @@ class Midy {
435
435
  const presetHeaders = soundFont.parsed.presetHeaders;
436
436
  for (let i = 0; i < presetHeaders.length; i++) {
437
437
  const presetHeader = presetHeaders[i];
438
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
439
- const banks = this.soundFontTable[presetHeader.preset];
440
- banks.set(presetHeader.bank, index);
441
- }
438
+ const banks = this.soundFontTable[presetHeader.preset];
439
+ banks.set(presetHeader.bank, index);
442
440
  }
443
441
  }
444
- async loadSoundFont(input) {
442
+ async toUint8Array(input) {
445
443
  let uint8Array;
446
444
  if (typeof input === "string") {
447
445
  const response = await fetch(input);
@@ -454,23 +452,32 @@ class Midy {
454
452
  else {
455
453
  throw new TypeError("input must be a URL string or Uint8Array");
456
454
  }
457
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
458
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
459
- this.addSoundFont(soundFont);
455
+ return uint8Array;
460
456
  }
461
- async loadMIDI(input) {
462
- let uint8Array;
463
- if (typeof input === "string") {
464
- const response = await fetch(input);
465
- const arrayBuffer = await response.arrayBuffer();
466
- uint8Array = new Uint8Array(arrayBuffer);
467
- }
468
- else if (input instanceof Uint8Array) {
469
- uint8Array = input;
457
+ async loadSoundFont(input) {
458
+ this.voiceCounter.clear();
459
+ if (Array.isArray(input)) {
460
+ const promises = new Array(input.length);
461
+ for (let i = 0; i < input.length; i++) {
462
+ promises[i] = this.toUint8Array(input[i]);
463
+ }
464
+ const uint8Arrays = await Promise.all(promises);
465
+ for (let i = 0; i < uint8Arrays.length; i++) {
466
+ const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
467
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
468
+ this.addSoundFont(soundFont);
469
+ }
470
470
  }
471
471
  else {
472
- throw new TypeError("input must be a URL string or Uint8Array");
472
+ const uint8Array = await this.toUint8Array(input);
473
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
474
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
475
+ this.addSoundFont(soundFont);
473
476
  }
477
+ }
478
+ async loadMIDI(input) {
479
+ this.voiceCounter.clear();
480
+ const uint8Array = await this.toUint8Array(input);
474
481
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
475
482
  this.ticksPerBeat = midi.header.ticksPerBeat;
476
483
  const midiData = this.extractMidiData(midi);
@@ -478,6 +485,45 @@ class Midy {
478
485
  this.timeline = midiData.timeline;
479
486
  this.totalTime = this.calcTotalTime();
480
487
  }
488
+ cacheVoiceIds() {
489
+ const timeline = this.timeline;
490
+ for (let i = 0; i < timeline.length; i++) {
491
+ const event = timeline[i];
492
+ switch (event.type) {
493
+ case "noteOn": {
494
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
495
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
496
+ break;
497
+ }
498
+ case "controller":
499
+ if (event.controllerType === 0) {
500
+ this.setBankMSB(event.channel, event.value);
501
+ }
502
+ else if (event.controllerType === 32) {
503
+ this.setBankLSB(event.channel, event.value);
504
+ }
505
+ break;
506
+ case "programChange":
507
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
508
+ }
509
+ }
510
+ for (const [audioBufferId, count] of this.voiceCounter) {
511
+ if (count === 1)
512
+ this.voiceCounter.delete(audioBufferId);
513
+ }
514
+ this.GM2SystemOn();
515
+ }
516
+ getVoiceId(channel, noteNumber, velocity) {
517
+ const bankNumber = this.calcBank(channel);
518
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
519
+ .get(bankNumber);
520
+ if (soundFontIndex === undefined)
521
+ return;
522
+ const soundFont = this.soundFonts[soundFontIndex];
523
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
524
+ const { instrument, sampleID } = voice.generators;
525
+ return `${soundFontIndex}:${instrument}:${sampleID}`;
526
+ }
481
527
  createChannelAudioNodes(audioContext) {
482
528
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
483
529
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -521,34 +567,12 @@ class Midy {
521
567
  });
522
568
  return channels;
523
569
  }
524
- async createNoteBuffer(voiceParams, isSF3) {
570
+ async createAudioBuffer(voiceParams) {
571
+ const sample = voiceParams.sample;
525
572
  const sampleStart = voiceParams.start;
526
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
527
- if (isSF3) {
528
- const sample = voiceParams.sample;
529
- const start = sample.byteOffset + sampleStart;
530
- const end = sample.byteOffset + sampleEnd;
531
- const buffer = sample.buffer.slice(start, end);
532
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
533
- return audioBuffer;
534
- }
535
- else {
536
- const sample = voiceParams.sample;
537
- const start = sample.byteOffset + sampleStart;
538
- const end = sample.byteOffset + sampleEnd;
539
- const buffer = sample.buffer.slice(start, end);
540
- const audioBuffer = new AudioBuffer({
541
- numberOfChannels: 1,
542
- length: sample.length,
543
- sampleRate: voiceParams.sampleRate,
544
- });
545
- const channelData = audioBuffer.getChannelData(0);
546
- const int16Array = new Int16Array(buffer);
547
- for (let i = 0; i < int16Array.length; i++) {
548
- channelData[i] = int16Array[i] / 32768;
549
- }
550
- return audioBuffer;
551
- }
573
+ const sampleEnd = sample.data.length + voiceParams.end;
574
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
575
+ return audioBuffer;
552
576
  }
553
577
  isLoopDrum(channel, noteNumber) {
554
578
  const programNumber = channel.programNumber;
@@ -585,16 +609,16 @@ class Midy {
585
609
  break;
586
610
  }
587
611
  case "noteAftertouch":
588
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
612
+ this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
589
613
  break;
590
614
  case "controller":
591
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
615
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
592
616
  break;
593
617
  case "programChange":
594
- this.handleProgramChange(event.channel, event.programNumber, startTime);
618
+ this.setProgramChange(event.channel, event.programNumber, startTime);
595
619
  break;
596
620
  case "channelAftertouch":
597
- this.handleChannelPressure(event.channel, event.amount, startTime);
621
+ this.setChannelPressure(event.channel, event.amount, startTime);
598
622
  break;
599
623
  case "pitchBend":
600
624
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -628,8 +652,9 @@ class Midy {
628
652
  this.notePromises = [];
629
653
  this.exclusiveClassNotes.fill(undefined);
630
654
  this.drumExclusiveClassNotes.fill(undefined);
631
- this.audioBufferCache.clear();
655
+ this.voiceCache.clear();
632
656
  for (let i = 0; i < this.channels.length; i++) {
657
+ this.channels[i].scheduledNotes = [];
633
658
  this.resetAllStates(i);
634
659
  }
635
660
  resolve();
@@ -651,8 +676,9 @@ class Midy {
651
676
  this.notePromises = [];
652
677
  this.exclusiveClassNotes.fill(undefined);
653
678
  this.drumExclusiveClassNotes.fill(undefined);
654
- this.audioBufferCache.clear();
679
+ this.voiceCache.clear();
655
680
  for (let i = 0; i < this.channels.length; i++) {
681
+ this.channels[i].scheduledNotes = [];
656
682
  this.resetAllStates(i);
657
683
  }
658
684
  this.isStopping = false;
@@ -685,11 +711,7 @@ class Midy {
685
711
  secondToTicks(second, secondsPerBeat) {
686
712
  return second * this.ticksPerBeat / secondsPerBeat;
687
713
  }
688
- getAudioBufferId(programNumber, noteNumber, velocity) {
689
- return `${programNumber}:${noteNumber}:${velocity}`;
690
- }
691
714
  extractMidiData(midi) {
692
- this.audioBufferCounter.clear();
693
715
  const instruments = new Set();
694
716
  const timeline = [];
695
717
  const tmpChannels = new Array(this.channels.length);
@@ -710,8 +732,6 @@ class Midy {
710
732
  switch (event.type) {
711
733
  case "noteOn": {
712
734
  const channel = tmpChannels[event.channel];
713
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
714
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
715
735
  if (channel.programNumber < 0) {
716
736
  channel.programNumber = event.programNumber;
717
737
  switch (channel.bankMSB) {
@@ -761,10 +781,6 @@ class Midy {
761
781
  timeline.push(event);
762
782
  }
763
783
  }
764
- for (const [audioBufferId, count] of this.audioBufferCounter) {
765
- if (count === 1)
766
- this.audioBufferCounter.delete(audioBufferId);
767
- }
768
784
  const priority = {
769
785
  controller: 0,
770
786
  sysEx: 1,
@@ -804,12 +820,11 @@ class Midy {
804
820
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
805
821
  const channel = this.channels[channelNumber];
806
822
  const promises = [];
807
- this.processScheduledNotes(channel, scheduleTime, (note) => {
823
+ this.processScheduledNotes(channel, (note) => {
808
824
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
809
825
  this.notePromises.push(promise);
810
826
  promises.push(promise);
811
827
  });
812
- channel.scheduledNotes = [];
813
828
  return Promise.all(promises);
814
829
  }
815
830
  stopNotes(velocity, force, scheduleTime) {
@@ -823,6 +838,8 @@ class Midy {
823
838
  if (this.isPlaying || this.isPaused)
824
839
  return;
825
840
  this.resumeTime = 0;
841
+ if (this.voiceCounter.size === 0)
842
+ this.cacheVoiceIds();
826
843
  await this.playNotes();
827
844
  this.isPlaying = false;
828
845
  }
@@ -863,22 +880,20 @@ class Midy {
863
880
  const now = this.audioContext.currentTime;
864
881
  return this.resumeTime + now - this.startTime - this.startDelay;
865
882
  }
866
- processScheduledNotes(channel, scheduleTime, callback) {
883
+ processScheduledNotes(channel, callback) {
867
884
  const scheduledNotes = channel.scheduledNotes;
868
- for (let i = 0; i < scheduledNotes.length; i++) {
885
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
869
886
  const note = scheduledNotes[i];
870
887
  if (!note)
871
888
  continue;
872
889
  if (note.ending)
873
890
  continue;
874
- if (note.startTime < scheduleTime)
875
- continue;
876
891
  callback(note);
877
892
  }
878
893
  }
879
894
  processActiveNotes(channel, scheduleTime, callback) {
880
895
  const scheduledNotes = channel.scheduledNotes;
881
- for (let i = 0; i < scheduledNotes.length; i++) {
896
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
882
897
  const note = scheduledNotes[i];
883
898
  if (!note)
884
899
  continue;
@@ -1069,7 +1084,7 @@ class Midy {
1069
1084
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1070
1085
  }
1071
1086
  updateChannelDetune(channel, scheduleTime) {
1072
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1087
+ this.processScheduledNotes(channel, (note) => {
1073
1088
  this.updateDetune(channel, note, scheduleTime);
1074
1089
  });
1075
1090
  }
@@ -1297,31 +1312,31 @@ class Midy {
1297
1312
  note.vibratoLFO.connect(note.vibratoDepth);
1298
1313
  note.vibratoDepth.connect(note.bufferSource.detune);
1299
1314
  }
1300
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1301
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1302
- const cache = this.audioBufferCache.get(audioBufferId);
1315
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1316
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1317
+ const cache = this.voiceCache.get(audioBufferId);
1303
1318
  if (cache) {
1304
1319
  cache.counter += 1;
1305
1320
  if (cache.maxCount <= cache.counter) {
1306
- this.audioBufferCache.delete(audioBufferId);
1321
+ this.voiceCache.delete(audioBufferId);
1307
1322
  }
1308
1323
  return cache.audioBuffer;
1309
1324
  }
1310
1325
  else {
1311
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1312
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1326
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1327
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1313
1328
  const cache = { audioBuffer, maxCount, counter: 1 };
1314
- this.audioBufferCache.set(audioBufferId, cache);
1329
+ this.voiceCache.set(audioBufferId, cache);
1315
1330
  return audioBuffer;
1316
1331
  }
1317
1332
  }
1318
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1333
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
1319
1334
  const now = this.audioContext.currentTime;
1320
1335
  const state = channel.state;
1321
1336
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1322
1337
  const voiceParams = voice.getAllParams(controllerState);
1323
1338
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1324
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1339
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1325
1340
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1326
1341
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1327
1342
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -1416,15 +1431,15 @@ class Midy {
1416
1431
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1417
1432
  const channel = this.channels[channelNumber];
1418
1433
  const bankNumber = this.calcBank(channel, channelNumber);
1419
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1434
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
1435
+ .get(bankNumber);
1420
1436
  if (soundFontIndex === undefined)
1421
1437
  return;
1422
1438
  const soundFont = this.soundFonts[soundFontIndex];
1423
1439
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1424
1440
  if (!voice)
1425
1441
  return;
1426
- const isSF3 = soundFont.parsed.info.version.major === 3;
1427
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1442
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1428
1443
  if (channel.isDrum) {
1429
1444
  const audioContext = this.audioContext;
1430
1445
  const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
@@ -1507,15 +1522,29 @@ class Midy {
1507
1522
  return;
1508
1523
  }
1509
1524
  }
1510
- const note = this.findNoteOffTarget(channel, noteNumber);
1511
- if (!note)
1525
+ const index = this.findNoteOffIndex(channel, noteNumber);
1526
+ if (index < 0)
1512
1527
  return;
1528
+ const note = channel.scheduledNotes[index];
1513
1529
  note.ending = true;
1530
+ this.setNoteIndex(channel, index);
1514
1531
  this.releaseNote(channel, note, endTime);
1515
1532
  }
1516
- findNoteOffTarget(channel, noteNumber) {
1533
+ setNoteIndex(channel, index) {
1534
+ let allEnds = true;
1535
+ for (let i = channel.scheduleIndex; i < index; i++) {
1536
+ const note = channel.scheduledNotes[i];
1537
+ if (note && !note.ending) {
1538
+ allEnds = false;
1539
+ break;
1540
+ }
1541
+ }
1542
+ if (allEnds)
1543
+ channel.scheduleIndex = index + 1;
1544
+ }
1545
+ findNoteOffIndex(channel, noteNumber) {
1517
1546
  const scheduledNotes = channel.scheduledNotes;
1518
- for (let i = 0; i < scheduledNotes.length; i++) {
1547
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
1519
1548
  const note = scheduledNotes[i];
1520
1549
  if (!note)
1521
1550
  continue;
@@ -1523,8 +1552,9 @@ class Midy {
1523
1552
  continue;
1524
1553
  if (note.noteNumber !== noteNumber)
1525
1554
  continue;
1526
- return note;
1555
+ return i;
1527
1556
  }
1557
+ return -1;
1528
1558
  }
1529
1559
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1530
1560
  scheduleTime ??= this.audioContext.currentTime;
@@ -1564,31 +1594,31 @@ class Midy {
1564
1594
  case 0x90:
1565
1595
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1566
1596
  case 0xA0:
1567
- return this.handlePolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1597
+ return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1568
1598
  case 0xB0:
1569
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1599
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1570
1600
  case 0xC0:
1571
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1601
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1572
1602
  case 0xD0:
1573
- return this.handleChannelPressure(channelNumber, data1, scheduleTime);
1603
+ return this.setChannelPressure(channelNumber, data1, scheduleTime);
1574
1604
  case 0xE0:
1575
1605
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1576
1606
  default:
1577
1607
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1578
1608
  }
1579
1609
  }
1580
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1610
+ setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1581
1611
  const channel = this.channels[channelNumber];
1582
1612
  const table = channel.polyphonicKeyPressureTable;
1583
1613
  this.processActiveNotes(channel, scheduleTime, (note) => {
1584
1614
  if (note.noteNumber === noteNumber) {
1585
1615
  note.pressure = pressure;
1586
- this.setControllerParameters(channel, note, table);
1616
+ this.setControllerParameters(channel, note, table, scheduleTime);
1587
1617
  }
1588
1618
  });
1589
1619
  this.applyVoiceParams(channel, 10);
1590
1620
  }
1591
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1621
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1592
1622
  const channel = this.channels[channelNumber];
1593
1623
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1594
1624
  channel.programNumber = programNumber;
@@ -1603,7 +1633,7 @@ class Midy {
1603
1633
  }
1604
1634
  }
1605
1635
  }
1606
- handleChannelPressure(channelNumber, value, scheduleTime) {
1636
+ setChannelPressure(channelNumber, value, scheduleTime) {
1607
1637
  const channel = this.channels[channelNumber];
1608
1638
  if (channel.isDrum)
1609
1639
  return;
@@ -1617,7 +1647,7 @@ class Midy {
1617
1647
  }
1618
1648
  const table = channel.channelPressureTable;
1619
1649
  this.processActiveNotes(channel, scheduleTime, (note) => {
1620
- this.setControllerParameters(channel, note, table);
1650
+ this.setControllerParameters(channel, note, table, scheduleTime);
1621
1651
  });
1622
1652
  this.applyVoiceParams(channel, 13);
1623
1653
  }
@@ -1675,7 +1705,7 @@ class Midy {
1675
1705
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1676
1706
  let value = note.voiceParams.reverbEffectsSend;
1677
1707
  if (channel.isDrum) {
1678
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1708
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
1679
1709
  if (0 <= keyBasedValue) {
1680
1710
  value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1681
1711
  }
@@ -1705,13 +1735,13 @@ class Midy {
1705
1735
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1706
1736
  let value = note.voiceParams.chorusEffectsSend;
1707
1737
  if (channel.isDrum) {
1708
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1738
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
1709
1739
  if (0 <= keyBasedValue) {
1710
1740
  value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1711
1741
  }
1712
1742
  }
1713
1743
  if (0 < prevValue) {
1714
- if (0 < vaule) {
1744
+ if (0 < value) {
1715
1745
  note.chorusEffectsSend.gain
1716
1746
  .cancelScheduledValues(scheduleTime)
1717
1747
  .setValueAtTime(value, scheduleTime);
@@ -1811,7 +1841,7 @@ class Midy {
1811
1841
  return state;
1812
1842
  }
1813
1843
  applyVoiceParams(channel, controllerType, scheduleTime) {
1814
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1844
+ this.processScheduledNotes(channel, (note) => {
1815
1845
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
1816
1846
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1817
1847
  let applyVolumeEnvelope = false;
@@ -1882,7 +1912,7 @@ class Midy {
1882
1912
  handlers[127] = this.polyOn;
1883
1913
  return handlers;
1884
1914
  }
1885
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1915
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1886
1916
  const handler = this.controlChangeHandlers[controllerType];
1887
1917
  if (handler) {
1888
1918
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1899,7 +1929,7 @@ class Midy {
1899
1929
  }
1900
1930
  updateModulation(channel, scheduleTime) {
1901
1931
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1902
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1932
+ this.processScheduledNotes(channel, (note) => {
1903
1933
  if (note.modulationDepth) {
1904
1934
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1905
1935
  }
@@ -1918,7 +1948,7 @@ class Midy {
1918
1948
  this.updateModulation(channel, scheduleTime);
1919
1949
  }
1920
1950
  updatePortamento(channel, scheduleTime) {
1921
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1951
+ this.processScheduledNotes(channel, (note) => {
1922
1952
  if (0.5 <= channel.state.portamento) {
1923
1953
  if (0 <= note.portamentoNoteNumber) {
1924
1954
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
@@ -2003,11 +2033,11 @@ class Midy {
2003
2033
  continue;
2004
2034
  if (!gainR)
2005
2035
  continue;
2006
- const keyBasedVolume = this.getKeyBasedInstrumentControlValue(channel, i, 7);
2036
+ const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
2007
2037
  const volume = (0 <= keyBasedVolume)
2008
2038
  ? defaultVolume * keyBasedVolume / 64
2009
2039
  : defaultVolume;
2010
- const keyBasedPan = this.getKeyBasedInstrumentControlValue(channel, i, 10);
2040
+ const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
2011
2041
  const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2012
2042
  const { gainLeft, gainRight } = this.panToGain(pan);
2013
2043
  gainL.gain
@@ -2025,7 +2055,7 @@ class Midy {
2025
2055
  scheduleTime ??= this.audioContext.currentTime;
2026
2056
  channel.state.sustainPedal = value / 127;
2027
2057
  if (64 <= value) {
2028
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2058
+ this.processScheduledNotes(channel, (note) => {
2029
2059
  channel.sustainNotes.push(note);
2030
2060
  });
2031
2061
  }
@@ -2068,7 +2098,7 @@ class Midy {
2068
2098
  const state = channel.state;
2069
2099
  scheduleTime ??= this.audioContext.currentTime;
2070
2100
  state.softPedal = softPedal / 127;
2071
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2101
+ this.processScheduledNotes(channel, (note) => {
2072
2102
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2073
2103
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2074
2104
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
@@ -2086,7 +2116,7 @@ class Midy {
2086
2116
  scheduleTime ??= this.audioContext.currentTime;
2087
2117
  const state = channel.state;
2088
2118
  state.filterResonance = filterResonance / 127;
2089
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2119
+ this.processScheduledNotes(channel, (note) => {
2090
2120
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2091
2121
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2092
2122
  });
@@ -2104,10 +2134,10 @@ class Midy {
2104
2134
  return;
2105
2135
  scheduleTime ??= this.audioContext.currentTime;
2106
2136
  channel.state.attackTime = attackTime / 127;
2107
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2137
+ this.processScheduledNotes(channel, (note) => {
2108
2138
  if (note.startTime < scheduleTime)
2109
2139
  return false;
2110
- this.setVolumeEnvelope(channel, note);
2140
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2111
2141
  });
2112
2142
  }
2113
2143
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2117,12 +2147,12 @@ class Midy {
2117
2147
  const state = channel.state;
2118
2148
  scheduleTime ??= this.audioContext.currentTime;
2119
2149
  state.brightness = brightness / 127;
2120
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2150
+ this.processScheduledNotes(channel, (note) => {
2121
2151
  if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2122
2152
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2123
2153
  }
2124
2154
  else {
2125
- this.setFilterEnvelope(channel, note);
2155
+ this.setFilterEnvelope(channel, note, scheduleTime);
2126
2156
  }
2127
2157
  });
2128
2158
  }
@@ -2132,7 +2162,7 @@ class Midy {
2132
2162
  return;
2133
2163
  scheduleTime ??= this.audioContext.currentTime;
2134
2164
  channel.state.decayTime = dacayTime / 127;
2135
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2165
+ this.processScheduledNotes(channel, (note) => {
2136
2166
  this.setVolumeEnvelope(channel, note, scheduleTime);
2137
2167
  });
2138
2168
  }
@@ -2144,7 +2174,7 @@ class Midy {
2144
2174
  channel.state.vibratoRate = vibratoRate / 127;
2145
2175
  if (channel.vibratoDepth <= 0)
2146
2176
  return;
2147
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2177
+ this.processScheduledNotes(channel, (note) => {
2148
2178
  this.setVibLfoToPitch(channel, note, scheduleTime);
2149
2179
  });
2150
2180
  }
@@ -2156,12 +2186,12 @@ class Midy {
2156
2186
  const prev = channel.state.vibratoDepth;
2157
2187
  channel.state.vibratoDepth = vibratoDepth / 127;
2158
2188
  if (0 < prev) {
2159
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2189
+ this.processScheduledNotes(channel, (note) => {
2160
2190
  this.setFreqVibLFO(channel, note, scheduleTime);
2161
2191
  });
2162
2192
  }
2163
2193
  else {
2164
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2194
+ this.processScheduledNotes(channel, (note) => {
2165
2195
  this.startVibrato(channel, note, scheduleTime);
2166
2196
  });
2167
2197
  }
@@ -2173,7 +2203,7 @@ class Midy {
2173
2203
  scheduleTime ??= this.audioContext.currentTime;
2174
2204
  channel.state.vibratoDelay = vibratoDelay / 127;
2175
2205
  if (0 < channel.state.vibratoDepth) {
2176
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2206
+ this.processScheduledNotes(channel, (note) => {
2177
2207
  this.startVibrato(channel, note, scheduleTime);
2178
2208
  });
2179
2209
  }
@@ -2191,7 +2221,7 @@ class Midy {
2191
2221
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
2192
2222
  }
2193
2223
  else {
2194
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2224
+ this.processScheduledNotes(channel, (note) => {
2195
2225
  if (note.voiceParams.reverbEffectsSend <= 0)
2196
2226
  return false;
2197
2227
  if (note.reverbEffectsSend)
@@ -2201,7 +2231,7 @@ class Midy {
2201
2231
  }
2202
2232
  else {
2203
2233
  if (0 < reverbSendLevel) {
2204
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2234
+ this.processScheduledNotes(channel, (note) => {
2205
2235
  this.setReverbEffectsSend(channel, note, 0, scheduleTime);
2206
2236
  });
2207
2237
  state.reverbSendLevel = reverbSendLevel / 127;
@@ -2224,7 +2254,7 @@ class Midy {
2224
2254
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
2225
2255
  }
2226
2256
  else {
2227
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2257
+ this.processScheduledNotes(channel, (note) => {
2228
2258
  if (note.voiceParams.chorusEffectsSend <= 0)
2229
2259
  return false;
2230
2260
  if (note.chorusEffectsSend)
@@ -2234,7 +2264,7 @@ class Midy {
2234
2264
  }
2235
2265
  else {
2236
2266
  if (0 < chorusSendLevel) {
2237
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2267
+ this.processScheduledNotes(channel, (note) => {
2238
2268
  this.setChorusEffectsSend(channel, note, 0, scheduleTime);
2239
2269
  });
2240
2270
  state.chorusSendLevel = chorusSendLevel / 127;
@@ -2392,7 +2422,7 @@ class Midy {
2392
2422
  const entries = Object.entries(defaultControllerState);
2393
2423
  for (const [key, { type, defaultValue }] of entries) {
2394
2424
  if (128 <= type) {
2395
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2425
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2396
2426
  }
2397
2427
  else {
2398
2428
  state[key] = defaultValue;
@@ -2425,7 +2455,7 @@ class Midy {
2425
2455
  const key = keys[i];
2426
2456
  const { type, defaultValue } = defaultControllerState[key];
2427
2457
  if (128 <= type) {
2428
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2458
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2429
2459
  }
2430
2460
  else {
2431
2461
  state[key] = defaultValue;
@@ -2928,27 +2958,29 @@ class Midy {
2928
2958
  : 0;
2929
2959
  return (channelPressure + polyphonicKeyPressure) / 254;
2930
2960
  }
2931
- setControllerParameters(channel, note, table) {
2961
+ setControllerParameters(channel, note, table, scheduleTime) {
2932
2962
  if (0 <= table[0])
2933
- this.updateDetune(channel, note);
2963
+ this.updateDetune(channel, note, scueduleTime);
2934
2964
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2935
- if (0 <= table[1])
2936
- this.setPortamentoFilterEnvelope(channel, note);
2937
- if (0 <= table[2])
2938
- this.setPortamentoVolumeEnvelope(channel, note);
2965
+ if (0 <= table[1]) {
2966
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2967
+ }
2968
+ if (0 <= table[2]) {
2969
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2970
+ }
2939
2971
  }
2940
2972
  else {
2941
2973
  if (0 <= table[1])
2942
- this.setFilterEnvelope(channel, note);
2974
+ this.setFilterEnvelope(channel, note, scheduleTime);
2943
2975
  if (0 <= table[2])
2944
- this.setVolumeEnvelope(channel, note);
2976
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2945
2977
  }
2946
2978
  if (0 <= table[3])
2947
- this.setModLfoToPitch(channel, note);
2979
+ this.setModLfoToPitch(channel, note, scheduleTime);
2948
2980
  if (0 <= table[4])
2949
- this.setModLfoToFilterFc(channel, note);
2981
+ this.setModLfoToFilterFc(channel, note, scheduleTime);
2950
2982
  if (0 <= table[5])
2951
- this.setModLfoToVolume(channel, note);
2983
+ this.setModLfoToVolume(channel, note, scheduleTime);
2952
2984
  }
2953
2985
  handlePressureSysEx(data, tableName) {
2954
2986
  const channelNumber = data[4];
@@ -2971,8 +3003,8 @@ class Midy {
2971
3003
  const slotSize = 6;
2972
3004
  const offset = controllerType * slotSize;
2973
3005
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2974
- this.processScheduledNotes(channel, scheduleTime, (note) => {
2975
- this.setControllerParameters(channel, note, table);
3006
+ this.processScheduledNotes(channel, (note) => {
3007
+ this.setControllerParameters(channel, note, table, scheduleTime);
2976
3008
  });
2977
3009
  }
2978
3010
  handleControlChangeSysEx(data) {
@@ -2988,7 +3020,7 @@ class Midy {
2988
3020
  table[pp] = rr;
2989
3021
  }
2990
3022
  }
2991
- getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
3023
+ getKeyBasedValue(channel, keyNumber, controllerType) {
2992
3024
  const index = keyNumber * 128 + controllerType;
2993
3025
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2994
3026
  return controlValue;
@@ -3006,7 +3038,7 @@ class Midy {
3006
3038
  const index = keyNumber * 128 + controllerType;
3007
3039
  table[index] = value;
3008
3040
  }
3009
- this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3041
+ this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
3010
3042
  }
3011
3043
  handleSysEx(data, scheduleTime) {
3012
3044
  switch (data[0]) {
@@ -3044,6 +3076,7 @@ Object.defineProperty(Midy, "channelSettings", {
3044
3076
  configurable: true,
3045
3077
  writable: true,
3046
3078
  value: {
3079
+ scheduleIndex: 0,
3047
3080
  detune: 0,
3048
3081
  programNumber: 0,
3049
3082
  bank: 121 * 128,