@marmooo/midy 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/script/midy.js CHANGED
@@ -4,7 +4,19 @@ exports.Midy = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
6
  class Note {
7
- constructor(noteNumber, velocity, startTime, voice, voiceParams) {
7
+ constructor(noteNumber, velocity, startTime) {
8
+ Object.defineProperty(this, "voice", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: void 0
13
+ });
14
+ Object.defineProperty(this, "voiceParams", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: void 0
19
+ });
8
20
  Object.defineProperty(this, "index", {
9
21
  enumerable: true,
10
22
  configurable: true,
@@ -17,6 +29,12 @@ class Note {
17
29
  writable: true,
18
30
  value: false
19
31
  });
32
+ Object.defineProperty(this, "pending", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: true
37
+ });
20
38
  Object.defineProperty(this, "bufferSource", {
21
39
  enumerable: true,
22
40
  configurable: true,
@@ -98,8 +116,6 @@ class Note {
98
116
  this.noteNumber = noteNumber;
99
117
  this.velocity = velocity;
100
118
  this.startTime = startTime;
101
- this.voice = voice;
102
- this.voiceParams = voiceParams;
103
119
  }
104
120
  }
105
121
  const drumExclusiveClassesByKit = new Array(57);
@@ -290,6 +306,18 @@ class Midy {
290
306
  writable: true,
291
307
  value: 0
292
308
  });
309
+ Object.defineProperty(this, "lastActiveSensing", {
310
+ enumerable: true,
311
+ configurable: true,
312
+ writable: true,
313
+ value: 0
314
+ });
315
+ Object.defineProperty(this, "activeSensingThreshold", {
316
+ enumerable: true,
317
+ configurable: true,
318
+ writable: true,
319
+ value: 0.3
320
+ });
293
321
  Object.defineProperty(this, "noteCheckInterval", {
294
322
  enumerable: true,
295
323
  configurable: true,
@@ -330,7 +358,7 @@ class Midy {
330
358
  enumerable: true,
331
359
  configurable: true,
332
360
  writable: true,
333
- value: this.initSoundFontTable()
361
+ value: Array.from({ length: 128 }, () => [])
334
362
  });
335
363
  Object.defineProperty(this, "voiceCounter", {
336
364
  enumerable: true,
@@ -344,6 +372,12 @@ class Midy {
344
372
  writable: true,
345
373
  value: new Map()
346
374
  });
375
+ Object.defineProperty(this, "realtimeVoiceCache", {
376
+ enumerable: true,
377
+ configurable: true,
378
+ writable: true,
379
+ value: new Map()
380
+ });
347
381
  Object.defineProperty(this, "isPlaying", {
348
382
  enumerable: true,
349
383
  configurable: true,
@@ -417,8 +451,10 @@ class Midy {
417
451
  length: 1,
418
452
  sampleRate: audioContext.sampleRate,
419
453
  });
454
+ this.messageHandlers = this.createMessageHandlers();
420
455
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
421
456
  this.controlChangeHandlers = this.createControlChangeHandlers();
457
+ this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
422
458
  this.channels = this.createChannels(audioContext);
423
459
  this.reverbEffect = this.createReverbEffect(audioContext);
424
460
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -428,21 +464,14 @@ class Midy {
428
464
  this.scheduler.connect(audioContext.destination);
429
465
  this.GM2SystemOn();
430
466
  }
431
- initSoundFontTable() {
432
- const table = new Array(128);
433
- for (let i = 0; i < 128; i++) {
434
- table[i] = new Map();
435
- }
436
- return table;
437
- }
438
467
  addSoundFont(soundFont) {
439
468
  const index = this.soundFonts.length;
440
469
  this.soundFonts.push(soundFont);
441
470
  const presetHeaders = soundFont.parsed.presetHeaders;
471
+ const soundFontTable = this.soundFontTable;
442
472
  for (let i = 0; i < presetHeaders.length; i++) {
443
- const presetHeader = presetHeaders[i];
444
- const banks = this.soundFontTable[presetHeader.preset];
445
- banks.set(presetHeader.bank, index);
473
+ const { preset, bank } = presetHeaders[i];
474
+ soundFontTable[preset][bank] = index;
446
475
  }
447
476
  }
448
477
  async toUint8Array(input) {
@@ -520,13 +549,17 @@ class Midy {
520
549
  this.GM2SystemOn();
521
550
  }
522
551
  getVoiceId(channel, noteNumber, velocity) {
523
- const bankNumber = this.calcBank(channel);
524
- const soundFontIndex = this.soundFontTable[channel.programNumber]
525
- .get(bankNumber);
552
+ const programNumber = channel.programNumber;
553
+ const bankTable = this.soundFontTable[programNumber];
554
+ if (!bankTable)
555
+ return;
556
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
557
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
558
+ const soundFontIndex = bankTable[bank];
526
559
  if (soundFontIndex === undefined)
527
560
  return;
528
561
  const soundFont = this.soundFonts[soundFontIndex];
529
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
562
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
530
563
  const { instrument, sampleID } = voice.generators;
531
564
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
532
565
  }
@@ -597,19 +630,22 @@ class Midy {
597
630
  }
598
631
  return bufferSource;
599
632
  }
600
- async scheduleTimelineEvents(t, resumeTime, queueIndex) {
601
- while (queueIndex < this.timeline.length) {
602
- const event = this.timeline[queueIndex];
603
- if (event.startTime > t + this.lookAhead)
633
+ async scheduleTimelineEvents(scheduleTime, queueIndex) {
634
+ const timeOffset = this.resumeTime - this.startTime;
635
+ const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
636
+ const schedulingOffset = this.startDelay - timeOffset;
637
+ const timeline = this.timeline;
638
+ while (queueIndex < timeline.length) {
639
+ const event = timeline[queueIndex];
640
+ if (lookAheadCheckTime < event.startTime)
604
641
  break;
605
- const delay = this.startDelay - resumeTime;
606
- const startTime = event.startTime + delay;
642
+ const startTime = event.startTime + schedulingOffset;
607
643
  switch (event.type) {
608
644
  case "noteOn":
609
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
645
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
610
646
  break;
611
647
  case "noteOff": {
612
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
648
+ const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
613
649
  if (notePromise)
614
650
  this.notePromises.push(notePromise);
615
651
  break;
@@ -648,6 +684,7 @@ class Midy {
648
684
  this.exclusiveClassNotes.fill(undefined);
649
685
  this.drumExclusiveClassNotes.fill(undefined);
650
686
  this.voiceCache.clear();
687
+ this.realtimeVoiceCache.clear();
651
688
  for (let i = 0; i < this.channels.length; i++) {
652
689
  this.channels[i].scheduledNotes = [];
653
690
  this.resetChannelStates(i);
@@ -681,13 +718,17 @@ class Midy {
681
718
  this.isPaused = false;
682
719
  this.startTime = this.audioContext.currentTime;
683
720
  let queueIndex = this.getQueueIndex(this.resumeTime);
684
- let resumeTime = this.resumeTime - this.startTime;
685
721
  let finished = false;
686
722
  this.notePromises = [];
687
723
  while (queueIndex < this.timeline.length) {
688
724
  const now = this.audioContext.currentTime;
689
- const t = now + resumeTime;
690
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
725
+ if (0 < this.lastActiveSensing &&
726
+ this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
727
+ await this.stopNotes(0, true, now);
728
+ await this.audioContext.suspend();
729
+ finished = true;
730
+ break;
731
+ }
691
732
  if (this.isPausing) {
692
733
  await this.stopNotes(0, true, now);
693
734
  await this.audioContext.suspend();
@@ -706,16 +747,17 @@ class Midy {
706
747
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
707
748
  this.updateStates(queueIndex, nextQueueIndex);
708
749
  queueIndex = nextQueueIndex;
709
- resumeTime = this.resumeTime - this.startTime;
710
750
  this.isSeeking = false;
711
751
  continue;
712
752
  }
753
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
713
754
  const waitTime = now + this.noteCheckInterval;
714
755
  await this.scheduleTask(() => { }, waitTime);
715
756
  }
716
757
  if (finished) {
717
758
  this.notePromises = [];
718
759
  this.resetAllStates();
760
+ this.lastActiveSensing = 0;
719
761
  }
720
762
  this.isPlaying = false;
721
763
  }
@@ -725,17 +767,17 @@ class Midy {
725
767
  secondToTicks(second, secondsPerBeat) {
726
768
  return second * this.ticksPerBeat / secondsPerBeat;
727
769
  }
770
+ getSoundFontId(channel) {
771
+ const programNumber = channel.programNumber;
772
+ const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
773
+ const bank = bankNumber.toString().padStart(3, "0");
774
+ const program = programNumber.toString().padStart(3, "0");
775
+ return `${bank}:${program}`;
776
+ }
728
777
  extractMidiData(midi) {
729
778
  const instruments = new Set();
730
779
  const timeline = [];
731
- const tmpChannels = new Array(this.channels.length);
732
- for (let i = 0; i < tmpChannels.length; i++) {
733
- tmpChannels[i] = {
734
- programNumber: -1,
735
- bankMSB: this.channels[i].bankMSB,
736
- bankLSB: this.channels[i].bankLSB,
737
- };
738
- }
780
+ const channels = this.channels;
739
781
  for (let i = 0; i < midi.tracks.length; i++) {
740
782
  const track = midi.tracks[i];
741
783
  let currentTicks = 0;
@@ -745,48 +787,40 @@ class Midy {
745
787
  event.ticks = currentTicks;
746
788
  switch (event.type) {
747
789
  case "noteOn": {
748
- const channel = tmpChannels[event.channel];
749
- if (channel.programNumber < 0) {
750
- channel.programNumber = event.programNumber;
751
- switch (channel.bankMSB) {
752
- case 120:
753
- instruments.add(`128:0`);
754
- break;
755
- case 121:
756
- instruments.add(`${channel.bankLSB}:0`);
757
- break;
758
- default: {
759
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
760
- instruments.add(`${bankNumber}:0`);
761
- }
762
- }
763
- channel.programNumber = 0;
764
- }
790
+ const channel = channels[event.channel];
791
+ instruments.add(this.getSoundFontId(channel));
765
792
  break;
766
793
  }
767
794
  case "controller":
768
795
  switch (event.controllerType) {
769
796
  case 0:
770
- tmpChannels[event.channel].bankMSB = event.value;
797
+ this.setBankMSB(event.channel, event.value);
771
798
  break;
772
799
  case 32:
773
- tmpChannels[event.channel].bankLSB = event.value;
800
+ this.setBankLSB(event.channel, event.value);
774
801
  break;
775
802
  }
776
803
  break;
777
804
  case "programChange": {
778
- const channel = tmpChannels[event.channel];
779
- channel.programNumber = event.programNumber;
780
- switch (channel.bankMSB) {
781
- case 120:
782
- instruments.add(`128:${channel.programNumber}`);
783
- break;
784
- case 121:
785
- instruments.add(`${channel.bankLSB}:${channel.programNumber}`);
786
- break;
787
- default: {
788
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
789
- instruments.add(`${bankNumber}:${channel.programNumber}`);
805
+ const channel = channels[event.channel];
806
+ this.setProgramChange(event.channel, event.programNumber);
807
+ instruments.add(this.getSoundFontId(channel));
808
+ break;
809
+ }
810
+ case "sysEx": {
811
+ const data = event.data;
812
+ if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
813
+ switch (data[3]) {
814
+ case 1:
815
+ this.GM1SystemOn(scheduleTime);
816
+ break;
817
+ case 2: // GM System Off
818
+ break;
819
+ case 3:
820
+ this.GM2SystemOn(scheduleTime);
821
+ break;
822
+ default:
823
+ console.warn(`Unsupported Exclusive Message: ${data}`);
790
824
  }
791
825
  }
792
826
  }
@@ -825,7 +859,7 @@ class Midy {
825
859
  const channel = this.channels[channelNumber];
826
860
  const promises = [];
827
861
  this.processActiveNotes(channel, scheduleTime, (note) => {
828
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
862
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
829
863
  this.notePromises.push(promise);
830
864
  promises.push(promise);
831
865
  });
@@ -835,7 +869,7 @@ class Midy {
835
869
  const channel = this.channels[channelNumber];
836
870
  const promises = [];
837
871
  this.processScheduledNotes(channel, (note) => {
838
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
872
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
839
873
  this.notePromises.push(promise);
840
874
  promises.push(promise);
841
875
  });
@@ -868,7 +902,7 @@ class Midy {
868
902
  if (!this.isPlaying || this.isPaused)
869
903
  return;
870
904
  const now = this.audioContext.currentTime;
871
- this.resumeTime += now - this.startTime - this.startDelay;
905
+ this.resumeTime = now - this.startTime - this.startDelay;
872
906
  this.isPausing = true;
873
907
  await this.playPromise;
874
908
  this.isPausing = false;
@@ -894,11 +928,13 @@ class Midy {
894
928
  if (totalTime < event.startTime)
895
929
  totalTime = event.startTime;
896
930
  }
897
- return totalTime;
931
+ return totalTime + this.startDelay;
898
932
  }
899
933
  currentTime() {
934
+ if (!this.isPlaying)
935
+ return this.resumeTime;
900
936
  const now = this.audioContext.currentTime;
901
- return this.resumeTime + now - this.startTime - this.startDelay;
937
+ return now + this.resumeTime - this.startTime;
902
938
  }
903
939
  processScheduledNotes(channel, callback) {
904
940
  const scheduledNotes = channel.scheduledNotes;
@@ -1110,7 +1146,7 @@ class Midy {
1110
1146
  const noteDetune = this.calcNoteDetune(channel, note);
1111
1147
  const pitchControl = this.getPitchControl(channel, note);
1112
1148
  const detune = channel.detune + noteDetune + pitchControl;
1113
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1149
+ if (this.isPortamento(channel, note)) {
1114
1150
  const startTime = note.startTime;
1115
1151
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1116
1152
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
@@ -1330,31 +1366,42 @@ class Midy {
1330
1366
  note.vibratoLFO.connect(note.vibratoDepth);
1331
1367
  note.vibratoDepth.connect(note.bufferSource.detune);
1332
1368
  }
1333
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1369
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1334
1370
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1335
- const cache = this.voiceCache.get(audioBufferId);
1336
- if (cache) {
1337
- cache.counter += 1;
1338
- if (cache.maxCount <= cache.counter) {
1339
- this.voiceCache.delete(audioBufferId);
1340
- }
1341
- return cache.audioBuffer;
1342
- }
1343
- else {
1344
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1371
+ if (realtime) {
1372
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1373
+ if (cachedAudioBuffer)
1374
+ return cachedAudioBuffer;
1345
1375
  const audioBuffer = await this.createAudioBuffer(voiceParams);
1346
- const cache = { audioBuffer, maxCount, counter: 1 };
1347
- this.voiceCache.set(audioBufferId, cache);
1376
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1348
1377
  return audioBuffer;
1349
1378
  }
1379
+ else {
1380
+ const cache = this.voiceCache.get(audioBufferId);
1381
+ if (cache) {
1382
+ cache.counter += 1;
1383
+ if (cache.maxCount <= cache.counter) {
1384
+ this.voiceCache.delete(audioBufferId);
1385
+ }
1386
+ return cache.audioBuffer;
1387
+ }
1388
+ else {
1389
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1390
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1391
+ const cache = { audioBuffer, maxCount, counter: 1 };
1392
+ this.voiceCache.set(audioBufferId, cache);
1393
+ return audioBuffer;
1394
+ }
1395
+ }
1350
1396
  }
1351
- async createNote(channel, voice, noteNumber, velocity, startTime) {
1397
+ async setNoteAudioNode(channel, note, realtime) {
1352
1398
  const now = this.audioContext.currentTime;
1399
+ const { noteNumber, velocity, startTime } = note;
1353
1400
  const state = channel.state;
1354
1401
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1355
- const voiceParams = voice.getAllParams(controllerState);
1356
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1357
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1402
+ const voiceParams = note.voice.getAllParams(controllerState);
1403
+ note.voiceParams = voiceParams;
1404
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1358
1405
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1359
1406
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1360
1407
  const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
@@ -1366,7 +1413,7 @@ class Midy {
1366
1413
  if (prevNote && prevNote.noteNumber !== noteNumber) {
1367
1414
  note.portamentoNoteNumber = prevNote.noteNumber;
1368
1415
  }
1369
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1416
+ if (!channel.isDrum && this.isPortamento(channel, note)) {
1370
1417
  this.setPortamentoVolumeEnvelope(channel, note, now);
1371
1418
  this.setPortamentoFilterEnvelope(channel, note, now);
1372
1419
  this.setPortamentoPitchEnvelope(note, now);
@@ -1394,22 +1441,6 @@ class Midy {
1394
1441
  note.bufferSource.start(startTime);
1395
1442
  return note;
1396
1443
  }
1397
- calcBank(channel) {
1398
- switch (this.mode) {
1399
- case "GM1":
1400
- if (channel.isDrum)
1401
- return 128;
1402
- return 0;
1403
- case "GM2":
1404
- if (channel.bankMSB === 121)
1405
- return 0;
1406
- if (channel.isDrum)
1407
- return 128;
1408
- return channel.bank;
1409
- default:
1410
- return channel.bank;
1411
- }
1412
- }
1413
1444
  handleExclusiveClass(note, channelNumber, startTime) {
1414
1445
  const exclusiveClass = note.voiceParams.exclusiveClass;
1415
1446
  if (exclusiveClass === 0)
@@ -1418,7 +1449,7 @@ class Midy {
1418
1449
  if (prev) {
1419
1450
  const [prevNote, prevChannelNumber] = prev;
1420
1451
  if (prevNote && !prevNote.ending) {
1421
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1452
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1422
1453
  startTime, true);
1423
1454
  }
1424
1455
  }
@@ -1438,23 +1469,14 @@ class Midy {
1438
1469
  channelNumber;
1439
1470
  const prevNote = this.drumExclusiveClassNotes[index];
1440
1471
  if (prevNote && !prevNote.ending) {
1441
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1472
+ this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1442
1473
  startTime, true);
1443
1474
  }
1444
1475
  this.drumExclusiveClassNotes[index] = note;
1445
1476
  }
1446
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1477
+ setNoteRouting(channelNumber, note, startTime) {
1447
1478
  const channel = this.channels[channelNumber];
1448
- const bankNumber = this.calcBank(channel, channelNumber);
1449
- const soundFontIndex = this.soundFontTable[channel.programNumber]
1450
- .get(bankNumber);
1451
- if (soundFontIndex === undefined)
1452
- return;
1453
- const soundFont = this.soundFonts[soundFontIndex];
1454
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1455
- if (!voice)
1456
- return;
1457
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1479
+ const { noteNumber, volumeEnvelopeNode } = note;
1458
1480
  if (channel.isDrum) {
1459
1481
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1460
1482
  let gainL = keyBasedGainLs[noteNumber];
@@ -1464,25 +1486,48 @@ class Midy {
1464
1486
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1465
1487
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1466
1488
  }
1467
- note.volumeEnvelopeNode.connect(gainL);
1468
- note.volumeEnvelopeNode.connect(gainR);
1489
+ volumeEnvelopeNode.connect(gainL);
1490
+ volumeEnvelopeNode.connect(gainR);
1469
1491
  }
1470
1492
  else {
1471
- note.volumeEnvelopeNode.connect(channel.gainL);
1472
- note.volumeEnvelopeNode.connect(channel.gainR);
1493
+ volumeEnvelopeNode.connect(channel.gainL);
1494
+ volumeEnvelopeNode.connect(channel.gainR);
1473
1495
  }
1474
1496
  if (0.5 <= channel.state.sustainPedal) {
1475
1497
  channel.sustainNotes.push(note);
1476
1498
  }
1477
1499
  this.handleExclusiveClass(note, channelNumber, startTime);
1478
1500
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1501
+ }
1502
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
1503
+ const channel = this.channels[channelNumber];
1504
+ const realtime = startTime === undefined;
1505
+ if (realtime)
1506
+ startTime = this.audioContext.currentTime;
1507
+ const note = new Note(noteNumber, velocity, startTime);
1479
1508
  const scheduledNotes = channel.scheduledNotes;
1480
1509
  note.index = scheduledNotes.length;
1481
1510
  scheduledNotes.push(note);
1482
- }
1483
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1484
- scheduleTime ??= this.audioContext.currentTime;
1485
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1511
+ const programNumber = channel.programNumber;
1512
+ const bankTable = this.soundFontTable[programNumber];
1513
+ if (!bankTable)
1514
+ return;
1515
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1516
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1517
+ const soundFontIndex = bankTable[bank];
1518
+ if (soundFontIndex === undefined)
1519
+ return;
1520
+ const soundFont = this.soundFonts[soundFontIndex];
1521
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1522
+ if (!note.voice)
1523
+ return;
1524
+ await this.setNoteAudioNode(channel, note, realtime);
1525
+ this.setNoteRouting(channelNumber, note, startTime);
1526
+ note.pending = false;
1527
+ const off = note.offEvent;
1528
+ if (off) {
1529
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1530
+ }
1486
1531
  }
1487
1532
  disconnectNote(note) {
1488
1533
  note.bufferSource.disconnect();
@@ -1505,6 +1550,7 @@ class Midy {
1505
1550
  }
1506
1551
  }
1507
1552
  releaseNote(channel, note, endTime) {
1553
+ endTime ??= this.audioContext.currentTime;
1508
1554
  const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1509
1555
  const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1510
1556
  const modRelease = endTime + note.voiceParams.modRelease;
@@ -1526,7 +1572,7 @@ class Midy {
1526
1572
  }, stopTime);
1527
1573
  });
1528
1574
  }
1529
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1575
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1530
1576
  const channel = this.channels[channelNumber];
1531
1577
  const state = channel.state;
1532
1578
  if (!force) {
@@ -1545,6 +1591,10 @@ class Midy {
1545
1591
  if (index < 0)
1546
1592
  return;
1547
1593
  const note = channel.scheduledNotes[index];
1594
+ if (note.pending) {
1595
+ note.offEvent = { velocity, startTime: endTime };
1596
+ return;
1597
+ }
1548
1598
  note.ending = true;
1549
1599
  this.setNoteIndex(channel, index);
1550
1600
  this.releaseNote(channel, note, endTime);
@@ -1575,16 +1625,12 @@ class Midy {
1575
1625
  }
1576
1626
  return -1;
1577
1627
  }
1578
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1579
- scheduleTime ??= this.audioContext.currentTime;
1580
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1581
- }
1582
1628
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1583
1629
  const velocity = halfVelocity * 2;
1584
1630
  const channel = this.channels[channelNumber];
1585
1631
  const promises = [];
1586
1632
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1587
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1633
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1588
1634
  promises.push(promise);
1589
1635
  }
1590
1636
  channel.sustainNotes = [];
@@ -1598,33 +1644,48 @@ class Midy {
1598
1644
  channel.state.sostenutoPedal = 0;
1599
1645
  for (let i = 0; i < sostenutoNotes.length; i++) {
1600
1646
  const note = sostenutoNotes[i];
1601
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1647
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1602
1648
  promises.push(promise);
1603
1649
  }
1604
1650
  channel.sostenutoNotes = [];
1605
1651
  return promises;
1606
1652
  }
1607
- handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1608
- const channelNumber = statusByte & 0x0F;
1609
- const messageType = statusByte & 0xF0;
1610
- switch (messageType) {
1611
- case 0x80:
1612
- return this.noteOff(channelNumber, data1, data2, scheduleTime);
1613
- case 0x90:
1614
- return this.noteOn(channelNumber, data1, data2, scheduleTime);
1615
- case 0xA0:
1616
- return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1617
- case 0xB0:
1618
- return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1619
- case 0xC0:
1620
- return this.setProgramChange(channelNumber, data1, scheduleTime);
1621
- case 0xD0:
1622
- return this.setChannelPressure(channelNumber, data1, scheduleTime);
1623
- case 0xE0:
1624
- return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1625
- default:
1626
- console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1653
+ createMessageHandlers() {
1654
+ const handlers = new Array(256);
1655
+ // Channel Message
1656
+ handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
1657
+ handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
1658
+ handlers[0xA0] = (data, scheduleTime) => this.setPolyphonicKeyPressure(data[0] & 0x0F, data[1], data[2], scheduleTime);
1659
+ handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
1660
+ handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
1661
+ handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
1662
+ handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
1663
+ // System Common Message
1664
+ // handlers[0xF1] = (_data, _scheduleTime) => {}; // MTC Quarter Frame
1665
+ // handlers[0xF2] = (_data, _scheduleTime) => {}; // Song Position Pointer
1666
+ // handlers[0xF3] = (_data, _scheduleTime) => {}; // Song Select
1667
+ // handlers[0xF6] = (_data, _scheduleTime) => {}; // Tune Request
1668
+ // handlers[0xF7] = (_data, _scheduleTime) => {}; // End of Exclusive (EOX)
1669
+ // System Real Time Message
1670
+ // handlers[0xF8] = (_data, _scheduleTime) => {}; // Timing Clock
1671
+ // handlers[0xFA] = (_data, _scheduleTime) => {}; // Start
1672
+ // handlers[0xFB] = (_data, _scheduleTime) => {}; // Continue
1673
+ // handlers[0xFC] = (_data, _scheduleTime) => {}; // Stop
1674
+ handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
1675
+ // handlers[0xFF] = (_data, _scheduleTime) => {}; // Reset
1676
+ return handlers;
1677
+ }
1678
+ handleMessage(data, scheduleTime) {
1679
+ const status = data[0];
1680
+ if (status === 0xF0) {
1681
+ return this.handleSysEx(data.subarray(1), scheduleTime);
1627
1682
  }
1683
+ const handler = this.messageHandlers[status];
1684
+ if (handler)
1685
+ handler(data, scheduleTime);
1686
+ }
1687
+ activeSensing() {
1688
+ this.lastActiveSensing = performance.now();
1628
1689
  }
1629
1690
  setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1630
1691
  const channel = this.channels[channelNumber];
@@ -1639,19 +1700,18 @@ class Midy {
1639
1700
  }
1640
1701
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1641
1702
  const channel = this.channels[channelNumber];
1642
- channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1643
1703
  channel.programNumber = programNumber;
1644
1704
  if (this.mode === "GM2") {
1645
1705
  switch (channel.bankMSB) {
1646
1706
  case 120:
1647
1707
  channel.isDrum = true;
1708
+ channel.keyBasedTable.fill(-1);
1648
1709
  break;
1649
1710
  case 121:
1650
1711
  channel.isDrum = false;
1651
1712
  break;
1652
1713
  }
1653
1714
  }
1654
- channel.keyBasedTable.fill(-1);
1655
1715
  }
1656
1716
  setChannelPressure(channelNumber, value, scheduleTime) {
1657
1717
  const channel = this.channels[channelNumber];
@@ -1988,22 +2048,20 @@ class Midy {
1988
2048
  this.updateModulation(channel, scheduleTime);
1989
2049
  }
1990
2050
  updatePortamento(channel, scheduleTime) {
2051
+ if (channel.isDrum)
2052
+ return;
1991
2053
  this.processScheduledNotes(channel, (note) => {
1992
- if (0.5 <= channel.state.portamento) {
1993
- if (0 <= note.portamentoNoteNumber) {
1994
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1995
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1996
- this.setPortamentoPitchEnvelope(note, scheduleTime);
1997
- this.updateDetune(channel, note, scheduleTime);
1998
- }
2054
+ if (this.isPortamento(channel, note)) {
2055
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2056
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2057
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
2058
+ this.updateDetune(channel, note, scheduleTime);
1999
2059
  }
2000
2060
  else {
2001
- if (0 <= note.portamentoNoteNumber) {
2002
- this.setVolumeEnvelope(channel, note, scheduleTime);
2003
- this.setFilterEnvelope(channel, note, scheduleTime);
2004
- this.setPitchEnvelope(note, scheduleTime);
2005
- this.updateDetune(channel, note, scheduleTime);
2006
- }
2061
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2062
+ this.setFilterEnvelope(channel, note, scheduleTime);
2063
+ this.setPitchEnvelope(note, scheduleTime);
2064
+ this.updateDetune(channel, note, scheduleTime);
2007
2065
  }
2008
2066
  });
2009
2067
  }
@@ -2109,6 +2167,9 @@ class Midy {
2109
2167
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2110
2168
  }
2111
2169
  }
2170
+ isPortamento(channel, note) {
2171
+ return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
2172
+ }
2112
2173
  setPortamento(channelNumber, value, scheduleTime) {
2113
2174
  const channel = this.channels[channelNumber];
2114
2175
  if (channel.isDrum)
@@ -2145,7 +2206,7 @@ class Midy {
2145
2206
  scheduleTime ??= this.audioContext.currentTime;
2146
2207
  state.softPedal = softPedal / 127;
2147
2208
  this.processScheduledNotes(channel, (note) => {
2148
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2209
+ if (this.isPortamento(channel, note)) {
2149
2210
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2150
2211
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2151
2212
  }
@@ -2203,7 +2264,7 @@ class Midy {
2203
2264
  scheduleTime ??= this.audioContext.currentTime;
2204
2265
  state.brightness = brightness / 127;
2205
2266
  this.processScheduledNotes(channel, (note) => {
2206
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2267
+ if (this.isPortamento(channel, note)) {
2207
2268
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2208
2269
  }
2209
2270
  else {
@@ -2539,11 +2600,9 @@ class Midy {
2539
2600
  const channel = this.channels[i];
2540
2601
  channel.bankMSB = 0;
2541
2602
  channel.bankLSB = 0;
2542
- channel.bank = 0;
2543
2603
  channel.isDrum = false;
2544
2604
  }
2545
2605
  this.channels[9].bankMSB = 1;
2546
- this.channels[9].bank = 128;
2547
2606
  this.channels[9].isDrum = true;
2548
2607
  }
2549
2608
  GM2SystemOn(scheduleTime) {
@@ -2554,11 +2613,9 @@ class Midy {
2554
2613
  const channel = this.channels[i];
2555
2614
  channel.bankMSB = 121;
2556
2615
  channel.bankLSB = 0;
2557
- channel.bank = 121 * 128;
2558
2616
  channel.isDrum = false;
2559
2617
  }
2560
2618
  this.channels[9].bankMSB = 120;
2561
- this.channels[9].bank = 120 * 128;
2562
2619
  this.channels[9].isDrum = true;
2563
2620
  }
2564
2621
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
@@ -2616,16 +2673,11 @@ class Midy {
2616
2673
  const volume = (data[5] * 128 + data[4]) / 16383;
2617
2674
  this.setMasterVolume(volume, scheduleTime);
2618
2675
  }
2619
- setMasterVolume(volume, scheduleTime) {
2676
+ setMasterVolume(value, scheduleTime) {
2620
2677
  scheduleTime ??= this.audioContext.currentTime;
2621
- if (volume < 0 && 1 < volume) {
2622
- console.error("Master Volume is out of range");
2623
- }
2624
- else {
2625
- this.masterVolume.gain
2626
- .cancelScheduledValues(scheduleTime)
2627
- .setValueAtTime(volume * volume, scheduleTime);
2628
- }
2678
+ this.masterVolume.gain
2679
+ .cancelScheduledValues(scheduleTime)
2680
+ .setValueAtTime(value * value, scheduleTime);
2629
2681
  }
2630
2682
  handleMasterFineTuningSysEx(data, scheduleTime) {
2631
2683
  const value = (data[5] * 128 + data[4]) / 16383;
@@ -3040,6 +3092,70 @@ class Midy {
3040
3092
  const controlValue = channel.keyBasedTable[index];
3041
3093
  return controlValue;
3042
3094
  }
3095
+ createKeyBasedControllerHandlers() {
3096
+ const handlers = new Array(128);
3097
+ handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3098
+ handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3099
+ handlers[71] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3100
+ if (note.noteNumber === keyNumber) {
3101
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3102
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3103
+ note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3104
+ }
3105
+ });
3106
+ handlers[73] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3107
+ if (note.noteNumber === keyNumber) {
3108
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3109
+ }
3110
+ });
3111
+ handlers[74] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3112
+ if (note.noteNumber === keyNumber) {
3113
+ this.setFilterEnvelope(channel, note, scheduleTime);
3114
+ }
3115
+ });
3116
+ handlers[75] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3117
+ if (note.noteNumber === keyNumber) {
3118
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3119
+ }
3120
+ });
3121
+ handlers[76] = (channel, keyNumber, scheduleTime) => {
3122
+ if (channel.state.vibratoDepth <= 0)
3123
+ return;
3124
+ this.processScheduledNotes(channel, (note) => {
3125
+ if (note.noteNumber === keyNumber) {
3126
+ this.setFreqVibLFO(channel, note, scheduleTime);
3127
+ }
3128
+ });
3129
+ };
3130
+ handlers[77] = (channel, keyNumber, scheduleTime) => {
3131
+ if (channel.state.vibratoDepth <= 0)
3132
+ return;
3133
+ this.processScheduledNotes(channel, (note) => {
3134
+ if (note.noteNumber === keyNumber) {
3135
+ this.setVibLfoToPitch(channel, note, scheduleTime);
3136
+ }
3137
+ });
3138
+ };
3139
+ handlers[78] = (channel, keyNumber) => {
3140
+ if (channel.state.vibratoDepth <= 0)
3141
+ return;
3142
+ this.processScheduledNotes(channel, (note) => {
3143
+ if (note.noteNumber === keyNumber)
3144
+ this.setDelayVibLFO(channel, note);
3145
+ });
3146
+ };
3147
+ handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3148
+ if (note.noteNumber === keyNumber) {
3149
+ this.setReverbSend(channel, note, scheduleTime);
3150
+ }
3151
+ });
3152
+ handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3153
+ if (note.noteNumber === keyNumber) {
3154
+ this.setChorusSend(channel, note, scheduleTime);
3155
+ }
3156
+ });
3157
+ return handlers;
3158
+ }
3043
3159
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
3044
3160
  const channelNumber = data[4];
3045
3161
  const channel = this.channels[channelNumber];
@@ -3052,95 +3168,9 @@ class Midy {
3052
3168
  const value = data[i + 1];
3053
3169
  const index = keyNumber * 128 + controllerType;
3054
3170
  table[index] = value;
3055
- switch (controllerType) {
3056
- case 7:
3057
- case 10:
3058
- this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3059
- break;
3060
- case 71:
3061
- this.processScheduledNotes(channel, (note) => {
3062
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3063
- if (note.noteNumber === keyNumber) {
3064
- const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3065
- note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3066
- }
3067
- });
3068
- break;
3069
- case 73:
3070
- this.processScheduledNotes(channel, (note) => {
3071
- if (note.noteNumber === keyNumber) {
3072
- if (0.5 <= channel.state.portamento &&
3073
- 0 <= note.portamentoNoteNumber) {
3074
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
3075
- }
3076
- else {
3077
- this.setVolumeEnvelope(channel, note, scheduleTime);
3078
- }
3079
- }
3080
- });
3081
- break;
3082
- case 74:
3083
- this.processScheduledNotes(channel, (note) => {
3084
- if (note.noteNumber === keyNumber) {
3085
- if (0.5 <= channel.state.portamento &&
3086
- 0 <= note.portamentoNoteNumber) {
3087
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
3088
- }
3089
- else {
3090
- this.setFilterEnvelope(channel, note, scheduleTime);
3091
- }
3092
- }
3093
- });
3094
- break;
3095
- case 75:
3096
- this.processScheduledNotes(channel, (note) => {
3097
- if (note.noteNumber === keyNumber) {
3098
- this.setVolumeEnvelope(channel, note, scheduleTime);
3099
- }
3100
- });
3101
- break;
3102
- case 76:
3103
- if (channel.state.vibratoDepth <= 0)
3104
- break;
3105
- this.processScheduledNotes(channel, (note) => {
3106
- if (note.noteNumber === keyNumber) {
3107
- this.setFreqVibLFO(channel, note, scheduleTime);
3108
- }
3109
- });
3110
- break;
3111
- case 77:
3112
- if (channel.state.vibratoDepth <= 0)
3113
- break;
3114
- this.processScheduledNotes(channel, (note) => {
3115
- if (note.noteNumber === keyNumber) {
3116
- this.setVibLfoToPitch(channel, note, scheduleTime);
3117
- }
3118
- });
3119
- break;
3120
- case 78:
3121
- if (channel.state.vibratoDepth <= 0)
3122
- break;
3123
- this.processScheduledNotes(channel, (note) => {
3124
- if (note.noteNumber === keyNumber) {
3125
- this.setDelayVibLFO(channel, note);
3126
- }
3127
- });
3128
- break;
3129
- case 91:
3130
- this.processScheduledNotes(channel, (note) => {
3131
- if (note.noteNumber === keyNumber) {
3132
- this.setReverbSend(channel, note, scheduleTime);
3133
- }
3134
- });
3135
- break;
3136
- case 93:
3137
- this.processScheduledNotes(channel, (note) => {
3138
- if (note.noteNumber === keyNumber) {
3139
- this.setChorusSend(channel, note, scheduleTime);
3140
- }
3141
- });
3142
- break;
3143
- }
3171
+ const handler = this.keyBasedControllerHandlers[controllerType];
3172
+ if (handler)
3173
+ handler(channel, keyNumber, scheduleTime);
3144
3174
  }
3145
3175
  }
3146
3176
  handleSysEx(data, scheduleTime) {
@@ -3182,7 +3212,6 @@ Object.defineProperty(Midy, "channelSettings", {
3182
3212
  scheduleIndex: 0,
3183
3213
  detune: 0,
3184
3214
  programNumber: 0,
3185
- bank: 121 * 128,
3186
3215
  bankMSB: 121,
3187
3216
  bankLSB: 0,
3188
3217
  dataMSB: 0,