@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.
@@ -4,7 +4,19 @@ exports.MidyGM2 = 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,
@@ -92,8 +110,6 @@ class Note {
92
110
  this.noteNumber = noteNumber;
93
111
  this.velocity = velocity;
94
112
  this.startTime = startTime;
95
- this.voice = voice;
96
- this.voiceParams = voiceParams;
97
113
  }
98
114
  }
99
115
  const drumExclusiveClassesByKit = new Array(57);
@@ -275,6 +291,18 @@ class MidyGM2 {
275
291
  writable: true,
276
292
  value: 0
277
293
  });
294
+ Object.defineProperty(this, "lastActiveSensing", {
295
+ enumerable: true,
296
+ configurable: true,
297
+ writable: true,
298
+ value: 0
299
+ });
300
+ Object.defineProperty(this, "activeSensingThreshold", {
301
+ enumerable: true,
302
+ configurable: true,
303
+ writable: true,
304
+ value: 0.3
305
+ });
278
306
  Object.defineProperty(this, "noteCheckInterval", {
279
307
  enumerable: true,
280
308
  configurable: true,
@@ -315,7 +343,7 @@ class MidyGM2 {
315
343
  enumerable: true,
316
344
  configurable: true,
317
345
  writable: true,
318
- value: this.initSoundFontTable()
346
+ value: Array.from({ length: 128 }, () => [])
319
347
  });
320
348
  Object.defineProperty(this, "voiceCounter", {
321
349
  enumerable: true,
@@ -329,6 +357,12 @@ class MidyGM2 {
329
357
  writable: true,
330
358
  value: new Map()
331
359
  });
360
+ Object.defineProperty(this, "realtimeVoiceCache", {
361
+ enumerable: true,
362
+ configurable: true,
363
+ writable: true,
364
+ value: new Map()
365
+ });
332
366
  Object.defineProperty(this, "isPlaying", {
333
367
  enumerable: true,
334
368
  configurable: true,
@@ -402,8 +436,10 @@ class MidyGM2 {
402
436
  length: 1,
403
437
  sampleRate: audioContext.sampleRate,
404
438
  });
439
+ this.messageHandlers = this.createMessageHandlers();
405
440
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
406
441
  this.controlChangeHandlers = this.createControlChangeHandlers();
442
+ this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
407
443
  this.channels = this.createChannels(audioContext);
408
444
  this.reverbEffect = this.createReverbEffect(audioContext);
409
445
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -413,21 +449,14 @@ class MidyGM2 {
413
449
  this.scheduler.connect(audioContext.destination);
414
450
  this.GM2SystemOn();
415
451
  }
416
- initSoundFontTable() {
417
- const table = new Array(128);
418
- for (let i = 0; i < 128; i++) {
419
- table[i] = new Map();
420
- }
421
- return table;
422
- }
423
452
  addSoundFont(soundFont) {
424
453
  const index = this.soundFonts.length;
425
454
  this.soundFonts.push(soundFont);
426
455
  const presetHeaders = soundFont.parsed.presetHeaders;
456
+ const soundFontTable = this.soundFontTable;
427
457
  for (let i = 0; i < presetHeaders.length; i++) {
428
- const presetHeader = presetHeaders[i];
429
- const banks = this.soundFontTable[presetHeader.preset];
430
- banks.set(presetHeader.bank, index);
458
+ const { preset, bank } = presetHeaders[i];
459
+ soundFontTable[preset][bank] = index;
431
460
  }
432
461
  }
433
462
  async toUint8Array(input) {
@@ -505,13 +534,17 @@ class MidyGM2 {
505
534
  this.GM2SystemOn();
506
535
  }
507
536
  getVoiceId(channel, noteNumber, velocity) {
508
- const bankNumber = this.calcBank(channel);
509
- const soundFontIndex = this.soundFontTable[channel.programNumber]
510
- .get(bankNumber);
537
+ const programNumber = channel.programNumber;
538
+ const bankTable = this.soundFontTable[programNumber];
539
+ if (!bankTable)
540
+ return;
541
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
542
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
543
+ const soundFontIndex = bankTable[bank];
511
544
  if (soundFontIndex === undefined)
512
545
  return;
513
546
  const soundFont = this.soundFonts[soundFontIndex];
514
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
547
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
515
548
  const { instrument, sampleID } = voice.generators;
516
549
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
517
550
  }
@@ -580,19 +613,22 @@ class MidyGM2 {
580
613
  }
581
614
  return bufferSource;
582
615
  }
583
- async scheduleTimelineEvents(t, resumeTime, queueIndex) {
584
- while (queueIndex < this.timeline.length) {
585
- const event = this.timeline[queueIndex];
586
- if (event.startTime > t + this.lookAhead)
616
+ async scheduleTimelineEvents(scheduleTime, queueIndex) {
617
+ const timeOffset = this.resumeTime - this.startTime;
618
+ const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
619
+ const schedulingOffset = this.startDelay - timeOffset;
620
+ const timeline = this.timeline;
621
+ while (queueIndex < timeline.length) {
622
+ const event = timeline[queueIndex];
623
+ if (lookAheadCheckTime < event.startTime)
587
624
  break;
588
- const delay = this.startDelay - resumeTime;
589
- const startTime = event.startTime + delay;
625
+ const startTime = event.startTime + schedulingOffset;
590
626
  switch (event.type) {
591
627
  case "noteOn":
592
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
628
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
593
629
  break;
594
630
  case "noteOff": {
595
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
631
+ const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
596
632
  if (notePromise)
597
633
  this.notePromises.push(notePromise);
598
634
  break;
@@ -628,6 +664,7 @@ class MidyGM2 {
628
664
  this.exclusiveClassNotes.fill(undefined);
629
665
  this.drumExclusiveClassNotes.fill(undefined);
630
666
  this.voiceCache.clear();
667
+ this.realtimeVoiceCache.clear();
631
668
  for (let i = 0; i < this.channels.length; i++) {
632
669
  this.channels[i].scheduledNotes = [];
633
670
  this.resetChannelStates(i);
@@ -661,13 +698,17 @@ class MidyGM2 {
661
698
  this.isPaused = false;
662
699
  this.startTime = this.audioContext.currentTime;
663
700
  let queueIndex = this.getQueueIndex(this.resumeTime);
664
- let resumeTime = this.resumeTime - this.startTime;
665
701
  let finished = false;
666
702
  this.notePromises = [];
667
703
  while (queueIndex < this.timeline.length) {
668
704
  const now = this.audioContext.currentTime;
669
- const t = now + resumeTime;
670
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
705
+ if (0 < this.lastActiveSensing &&
706
+ this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
707
+ await this.stopNotes(0, true, now);
708
+ await this.audioContext.suspend();
709
+ finished = true;
710
+ break;
711
+ }
671
712
  if (this.isPausing) {
672
713
  await this.stopNotes(0, true, now);
673
714
  await this.audioContext.suspend();
@@ -686,16 +727,17 @@ class MidyGM2 {
686
727
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
687
728
  this.updateStates(queueIndex, nextQueueIndex);
688
729
  queueIndex = nextQueueIndex;
689
- resumeTime = this.resumeTime - this.startTime;
690
730
  this.isSeeking = false;
691
731
  continue;
692
732
  }
733
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
693
734
  const waitTime = now + this.noteCheckInterval;
694
735
  await this.scheduleTask(() => { }, waitTime);
695
736
  }
696
737
  if (finished) {
697
738
  this.notePromises = [];
698
739
  this.resetAllStates();
740
+ this.lastActiveSensing = 0;
699
741
  }
700
742
  this.isPlaying = false;
701
743
  }
@@ -705,17 +747,17 @@ class MidyGM2 {
705
747
  secondToTicks(second, secondsPerBeat) {
706
748
  return second * this.ticksPerBeat / secondsPerBeat;
707
749
  }
750
+ getSoundFontId(channel) {
751
+ const programNumber = channel.programNumber;
752
+ const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
753
+ const bank = bankNumber.toString().padStart(3, "0");
754
+ const program = programNumber.toString().padStart(3, "0");
755
+ return `${bank}:${program}`;
756
+ }
708
757
  extractMidiData(midi) {
709
758
  const instruments = new Set();
710
759
  const timeline = [];
711
- const tmpChannels = new Array(this.channels.length);
712
- for (let i = 0; i < tmpChannels.length; i++) {
713
- tmpChannels[i] = {
714
- programNumber: -1,
715
- bankMSB: this.channels[i].bankMSB,
716
- bankLSB: this.channels[i].bankLSB,
717
- };
718
- }
760
+ const channels = this.channels;
719
761
  for (let i = 0; i < midi.tracks.length; i++) {
720
762
  const track = midi.tracks[i];
721
763
  let currentTicks = 0;
@@ -725,48 +767,40 @@ class MidyGM2 {
725
767
  event.ticks = currentTicks;
726
768
  switch (event.type) {
727
769
  case "noteOn": {
728
- const channel = tmpChannels[event.channel];
729
- if (channel.programNumber < 0) {
730
- channel.programNumber = event.programNumber;
731
- switch (channel.bankMSB) {
732
- case 120:
733
- instruments.add(`128:0`);
734
- break;
735
- case 121:
736
- instruments.add(`${channel.bankLSB}:0`);
737
- break;
738
- default: {
739
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
740
- instruments.add(`${bankNumber}:0`);
741
- }
742
- }
743
- channel.programNumber = 0;
744
- }
770
+ const channel = channels[event.channel];
771
+ instruments.add(this.getSoundFontId(channel));
745
772
  break;
746
773
  }
747
774
  case "controller":
748
775
  switch (event.controllerType) {
749
776
  case 0:
750
- tmpChannels[event.channel].bankMSB = event.value;
777
+ this.setBankMSB(event.channel, event.value);
751
778
  break;
752
779
  case 32:
753
- tmpChannels[event.channel].bankLSB = event.value;
780
+ this.setBankLSB(event.channel, event.value);
754
781
  break;
755
782
  }
756
783
  break;
757
784
  case "programChange": {
758
- const channel = tmpChannels[event.channel];
759
- channel.programNumber = event.programNumber;
760
- switch (channel.bankMSB) {
761
- case 120:
762
- instruments.add(`128:${channel.programNumber}`);
763
- break;
764
- case 121:
765
- instruments.add(`${channel.bankLSB}:${channel.programNumber}`);
766
- break;
767
- default: {
768
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
769
- instruments.add(`${bankNumber}:${channel.programNumber}`);
785
+ const channel = channels[event.channel];
786
+ this.setProgramChange(event.channel, event.programNumber);
787
+ instruments.add(this.getSoundFontId(channel));
788
+ break;
789
+ }
790
+ case "sysEx": {
791
+ const data = event.data;
792
+ if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
793
+ switch (data[3]) {
794
+ case 1:
795
+ this.GM1SystemOn(scheduleTime);
796
+ break;
797
+ case 2: // GM System Off
798
+ break;
799
+ case 3:
800
+ this.GM2SystemOn(scheduleTime);
801
+ break;
802
+ default:
803
+ console.warn(`Unsupported Exclusive Message: ${data}`);
770
804
  }
771
805
  }
772
806
  }
@@ -805,7 +839,7 @@ class MidyGM2 {
805
839
  const channel = this.channels[channelNumber];
806
840
  const promises = [];
807
841
  this.processActiveNotes(channel, scheduleTime, (note) => {
808
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
842
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
809
843
  this.notePromises.push(promise);
810
844
  promises.push(promise);
811
845
  });
@@ -815,7 +849,7 @@ class MidyGM2 {
815
849
  const channel = this.channels[channelNumber];
816
850
  const promises = [];
817
851
  this.processScheduledNotes(channel, (note) => {
818
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
852
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
819
853
  this.notePromises.push(promise);
820
854
  promises.push(promise);
821
855
  });
@@ -848,7 +882,7 @@ class MidyGM2 {
848
882
  if (!this.isPlaying || this.isPaused)
849
883
  return;
850
884
  const now = this.audioContext.currentTime;
851
- this.resumeTime += now - this.startTime - this.startDelay;
885
+ this.resumeTime = now - this.startTime - this.startDelay;
852
886
  this.isPausing = true;
853
887
  await this.playPromise;
854
888
  this.isPausing = false;
@@ -874,11 +908,13 @@ class MidyGM2 {
874
908
  if (totalTime < event.startTime)
875
909
  totalTime = event.startTime;
876
910
  }
877
- return totalTime;
911
+ return totalTime + this.startDelay;
878
912
  }
879
913
  currentTime() {
914
+ if (!this.isPlaying)
915
+ return this.resumeTime;
880
916
  const now = this.audioContext.currentTime;
881
- return this.resumeTime + now - this.startTime - this.startDelay;
917
+ return now + this.resumeTime - this.startTime;
882
918
  }
883
919
  processScheduledNotes(channel, callback) {
884
920
  const scheduledNotes = channel.scheduledNotes;
@@ -1089,7 +1125,7 @@ class MidyGM2 {
1089
1125
  updateDetune(channel, note, scheduleTime) {
1090
1126
  const noteDetune = this.calcNoteDetune(channel, note);
1091
1127
  const detune = channel.detune + noteDetune;
1092
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1128
+ if (this.isPortamento(channel, note)) {
1093
1129
  const startTime = note.startTime;
1094
1130
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1095
1131
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
@@ -1305,31 +1341,42 @@ class MidyGM2 {
1305
1341
  note.vibratoLFO.connect(note.vibratoDepth);
1306
1342
  note.vibratoDepth.connect(note.bufferSource.detune);
1307
1343
  }
1308
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1344
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1309
1345
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1310
- const cache = this.voiceCache.get(audioBufferId);
1311
- if (cache) {
1312
- cache.counter += 1;
1313
- if (cache.maxCount <= cache.counter) {
1314
- this.voiceCache.delete(audioBufferId);
1315
- }
1316
- return cache.audioBuffer;
1317
- }
1318
- else {
1319
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1346
+ if (realtime) {
1347
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1348
+ if (cachedAudioBuffer)
1349
+ return cachedAudioBuffer;
1320
1350
  const audioBuffer = await this.createAudioBuffer(voiceParams);
1321
- const cache = { audioBuffer, maxCount, counter: 1 };
1322
- this.voiceCache.set(audioBufferId, cache);
1351
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1323
1352
  return audioBuffer;
1324
1353
  }
1354
+ else {
1355
+ const cache = this.voiceCache.get(audioBufferId);
1356
+ if (cache) {
1357
+ cache.counter += 1;
1358
+ if (cache.maxCount <= cache.counter) {
1359
+ this.voiceCache.delete(audioBufferId);
1360
+ }
1361
+ return cache.audioBuffer;
1362
+ }
1363
+ else {
1364
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1365
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1366
+ const cache = { audioBuffer, maxCount, counter: 1 };
1367
+ this.voiceCache.set(audioBufferId, cache);
1368
+ return audioBuffer;
1369
+ }
1370
+ }
1325
1371
  }
1326
- async createNote(channel, voice, noteNumber, velocity, startTime) {
1372
+ async setNoteAudioNode(channel, note, realtime) {
1327
1373
  const now = this.audioContext.currentTime;
1374
+ const { noteNumber, velocity, startTime } = note;
1328
1375
  const state = channel.state;
1329
1376
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1330
- const voiceParams = voice.getAllParams(controllerState);
1331
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1332
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1377
+ const voiceParams = note.voice.getAllParams(controllerState);
1378
+ note.voiceParams = voiceParams;
1379
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1333
1380
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1334
1381
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1335
1382
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -1340,7 +1387,7 @@ class MidyGM2 {
1340
1387
  if (prevNote && prevNote.noteNumber !== noteNumber) {
1341
1388
  note.portamentoNoteNumber = prevNote.noteNumber;
1342
1389
  }
1343
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1390
+ if (!channel.isDrum && this.isPortamento(channel, note)) {
1344
1391
  this.setPortamentoVolumeEnvelope(channel, note, now);
1345
1392
  this.setPortamentoFilterEnvelope(channel, note, now);
1346
1393
  this.setPortamentoPitchEnvelope(note, now);
@@ -1368,22 +1415,6 @@ class MidyGM2 {
1368
1415
  note.bufferSource.start(startTime);
1369
1416
  return note;
1370
1417
  }
1371
- calcBank(channel) {
1372
- switch (this.mode) {
1373
- case "GM1":
1374
- if (channel.isDrum)
1375
- return 128;
1376
- return 0;
1377
- case "GM2":
1378
- if (channel.bankMSB === 121)
1379
- return 0;
1380
- if (channel.isDrum)
1381
- return 128;
1382
- return channel.bank;
1383
- default:
1384
- return channel.bank;
1385
- }
1386
- }
1387
1418
  handleExclusiveClass(note, channelNumber, startTime) {
1388
1419
  const exclusiveClass = note.voiceParams.exclusiveClass;
1389
1420
  if (exclusiveClass === 0)
@@ -1392,7 +1423,7 @@ class MidyGM2 {
1392
1423
  if (prev) {
1393
1424
  const [prevNote, prevChannelNumber] = prev;
1394
1425
  if (prevNote && !prevNote.ending) {
1395
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1426
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1396
1427
  startTime, true);
1397
1428
  }
1398
1429
  }
@@ -1412,23 +1443,14 @@ class MidyGM2 {
1412
1443
  channelNumber;
1413
1444
  const prevNote = this.drumExclusiveClassNotes[index];
1414
1445
  if (prevNote && !prevNote.ending) {
1415
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1446
+ this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1416
1447
  startTime, true);
1417
1448
  }
1418
1449
  this.drumExclusiveClassNotes[index] = note;
1419
1450
  }
1420
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1451
+ setNoteRouting(channelNumber, note, startTime) {
1421
1452
  const channel = this.channels[channelNumber];
1422
- const bankNumber = this.calcBank(channel, channelNumber);
1423
- const soundFontIndex = this.soundFontTable[channel.programNumber]
1424
- .get(bankNumber);
1425
- if (soundFontIndex === undefined)
1426
- return;
1427
- const soundFont = this.soundFonts[soundFontIndex];
1428
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1429
- if (!voice)
1430
- return;
1431
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1453
+ const { noteNumber, volumeEnvelopeNode } = note;
1432
1454
  if (channel.isDrum) {
1433
1455
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1434
1456
  let gainL = keyBasedGainLs[noteNumber];
@@ -1438,25 +1460,48 @@ class MidyGM2 {
1438
1460
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1439
1461
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1440
1462
  }
1441
- note.volumeEnvelopeNode.connect(gainL);
1442
- note.volumeEnvelopeNode.connect(gainR);
1463
+ volumeEnvelopeNode.connect(gainL);
1464
+ volumeEnvelopeNode.connect(gainR);
1443
1465
  }
1444
1466
  else {
1445
- note.volumeEnvelopeNode.connect(channel.gainL);
1446
- note.volumeEnvelopeNode.connect(channel.gainR);
1467
+ volumeEnvelopeNode.connect(channel.gainL);
1468
+ volumeEnvelopeNode.connect(channel.gainR);
1447
1469
  }
1448
1470
  if (0.5 <= channel.state.sustainPedal) {
1449
1471
  channel.sustainNotes.push(note);
1450
1472
  }
1451
1473
  this.handleExclusiveClass(note, channelNumber, startTime);
1452
1474
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1475
+ }
1476
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
1477
+ const channel = this.channels[channelNumber];
1478
+ const realtime = startTime === undefined;
1479
+ if (realtime)
1480
+ startTime = this.audioContext.currentTime;
1481
+ const note = new Note(noteNumber, velocity, startTime);
1453
1482
  const scheduledNotes = channel.scheduledNotes;
1454
1483
  note.index = scheduledNotes.length;
1455
1484
  scheduledNotes.push(note);
1456
- }
1457
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1458
- scheduleTime ??= this.audioContext.currentTime;
1459
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1485
+ const programNumber = channel.programNumber;
1486
+ const bankTable = this.soundFontTable[programNumber];
1487
+ if (!bankTable)
1488
+ return;
1489
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1490
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1491
+ const soundFontIndex = bankTable[bank];
1492
+ if (soundFontIndex === undefined)
1493
+ return;
1494
+ const soundFont = this.soundFonts[soundFontIndex];
1495
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1496
+ if (!note.voice)
1497
+ return;
1498
+ await this.setNoteAudioNode(channel, note, realtime);
1499
+ this.setNoteRouting(channelNumber, note, startTime);
1500
+ note.pending = false;
1501
+ const off = note.offEvent;
1502
+ if (off) {
1503
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1504
+ }
1460
1505
  }
1461
1506
  disconnectNote(note) {
1462
1507
  note.bufferSource.disconnect();
@@ -1479,6 +1524,7 @@ class MidyGM2 {
1479
1524
  }
1480
1525
  }
1481
1526
  releaseNote(channel, note, endTime) {
1527
+ endTime ??= this.audioContext.currentTime;
1482
1528
  const volRelease = endTime + note.voiceParams.volRelease;
1483
1529
  const modRelease = endTime + note.voiceParams.modRelease;
1484
1530
  const stopTime = Math.min(volRelease, modRelease);
@@ -1499,7 +1545,7 @@ class MidyGM2 {
1499
1545
  }, stopTime);
1500
1546
  });
1501
1547
  }
1502
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1548
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1503
1549
  const channel = this.channels[channelNumber];
1504
1550
  const state = channel.state;
1505
1551
  if (!force) {
@@ -1518,6 +1564,10 @@ class MidyGM2 {
1518
1564
  if (index < 0)
1519
1565
  return;
1520
1566
  const note = channel.scheduledNotes[index];
1567
+ if (note.pending) {
1568
+ note.offEvent = { velocity, startTime: endTime };
1569
+ return;
1570
+ }
1521
1571
  note.ending = true;
1522
1572
  this.setNoteIndex(channel, index);
1523
1573
  this.releaseNote(channel, note, endTime);
@@ -1548,16 +1598,12 @@ class MidyGM2 {
1548
1598
  }
1549
1599
  return -1;
1550
1600
  }
1551
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1552
- scheduleTime ??= this.audioContext.currentTime;
1553
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1554
- }
1555
1601
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1556
1602
  const velocity = halfVelocity * 2;
1557
1603
  const channel = this.channels[channelNumber];
1558
1604
  const promises = [];
1559
1605
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1560
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1606
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1561
1607
  promises.push(promise);
1562
1608
  }
1563
1609
  channel.sustainNotes = [];
@@ -1571,47 +1617,51 @@ class MidyGM2 {
1571
1617
  channel.state.sostenutoPedal = 0;
1572
1618
  for (let i = 0; i < sostenutoNotes.length; i++) {
1573
1619
  const note = sostenutoNotes[i];
1574
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1620
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1575
1621
  promises.push(promise);
1576
1622
  }
1577
1623
  channel.sostenutoNotes = [];
1578
1624
  return promises;
1579
1625
  }
1580
- handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1581
- const channelNumber = statusByte & 0x0F;
1582
- const messageType = statusByte & 0xF0;
1583
- switch (messageType) {
1584
- case 0x80:
1585
- return this.noteOff(channelNumber, data1, data2, scheduleTime);
1586
- case 0x90:
1587
- return this.noteOn(channelNumber, data1, data2, scheduleTime);
1588
- case 0xB0:
1589
- return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1590
- case 0xC0:
1591
- return this.setProgramChange(channelNumber, data1, scheduleTime);
1592
- case 0xD0:
1593
- return this.setChannelPressure(channelNumber, data1, scheduleTime);
1594
- case 0xE0:
1595
- return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1596
- default:
1597
- console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1626
+ createMessageHandlers() {
1627
+ const handlers = new Array(256);
1628
+ // Channel Message
1629
+ handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
1630
+ handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
1631
+ handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
1632
+ handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
1633
+ handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
1634
+ handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
1635
+ // System Real Time Message
1636
+ handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
1637
+ return handlers;
1638
+ }
1639
+ handleMessage(data, scheduleTime) {
1640
+ const status = data[0];
1641
+ if (status === 0xF0) {
1642
+ return this.handleSysEx(data.subarray(1), scheduleTime);
1598
1643
  }
1644
+ const handler = this.messageHandlers[status];
1645
+ if (handler)
1646
+ handler(data, scheduleTime);
1647
+ }
1648
+ activeSensing() {
1649
+ this.lastActiveSensing = performance.now();
1599
1650
  }
1600
1651
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1601
1652
  const channel = this.channels[channelNumber];
1602
- channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1603
1653
  channel.programNumber = programNumber;
1604
1654
  if (this.mode === "GM2") {
1605
1655
  switch (channel.bankMSB) {
1606
1656
  case 120:
1607
1657
  channel.isDrum = true;
1658
+ channel.keyBasedTable.fill(-1);
1608
1659
  break;
1609
1660
  case 121:
1610
1661
  channel.isDrum = false;
1611
1662
  break;
1612
1663
  }
1613
1664
  }
1614
- channel.keyBasedTable.fill(-1);
1615
1665
  }
1616
1666
  setChannelPressure(channelNumber, value, scheduleTime) {
1617
1667
  const channel = this.channels[channelNumber];
@@ -1936,22 +1986,20 @@ class MidyGM2 {
1936
1986
  this.updateModulation(channel, scheduleTime);
1937
1987
  }
1938
1988
  updatePortamento(channel, scheduleTime) {
1989
+ if (channel.isDrum)
1990
+ return;
1939
1991
  this.processScheduledNotes(channel, (note) => {
1940
- if (0.5 <= channel.state.portamento) {
1941
- if (0 <= note.portamentoNoteNumber) {
1942
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1943
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1944
- this.setPortamentoPitchEnvelope(note, scheduleTime);
1945
- this.updateDetune(channel, note, scheduleTime);
1946
- }
1992
+ if (this.isPortamento(channel, note)) {
1993
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1994
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1995
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1996
+ this.updateDetune(channel, note, scheduleTime);
1947
1997
  }
1948
1998
  else {
1949
- if (0 <= note.portamentoNoteNumber) {
1950
- this.setVolumeEnvelope(channel, note, scheduleTime);
1951
- this.setFilterEnvelope(channel, note, scheduleTime);
1952
- this.setPitchEnvelope(note, scheduleTime);
1953
- this.updateDetune(channel, note, scheduleTime);
1954
- }
1999
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2000
+ this.setFilterEnvelope(channel, note, scheduleTime);
2001
+ this.setPitchEnvelope(note, scheduleTime);
2002
+ this.updateDetune(channel, note, scheduleTime);
1955
2003
  }
1956
2004
  });
1957
2005
  }
@@ -2057,6 +2105,9 @@ class MidyGM2 {
2057
2105
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2058
2106
  }
2059
2107
  }
2108
+ isPortamento(channel, note) {
2109
+ return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
2110
+ }
2060
2111
  setPortamento(channelNumber, value, scheduleTime) {
2061
2112
  const channel = this.channels[channelNumber];
2062
2113
  if (channel.isDrum)
@@ -2093,7 +2144,7 @@ class MidyGM2 {
2093
2144
  scheduleTime ??= this.audioContext.currentTime;
2094
2145
  state.softPedal = softPedal / 127;
2095
2146
  this.processScheduledNotes(channel, (note) => {
2096
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2147
+ if (this.isPortamento(channel, note)) {
2097
2148
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2098
2149
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2099
2150
  }
@@ -2361,11 +2412,9 @@ class MidyGM2 {
2361
2412
  const channel = this.channels[i];
2362
2413
  channel.bankMSB = 0;
2363
2414
  channel.bankLSB = 0;
2364
- channel.bank = 0;
2365
2415
  channel.isDrum = false;
2366
2416
  }
2367
2417
  this.channels[9].bankMSB = 1;
2368
- this.channels[9].bank = 128;
2369
2418
  this.channels[9].isDrum = true;
2370
2419
  }
2371
2420
  GM2SystemOn(scheduleTime) {
@@ -2376,11 +2425,9 @@ class MidyGM2 {
2376
2425
  const channel = this.channels[i];
2377
2426
  channel.bankMSB = 121;
2378
2427
  channel.bankLSB = 0;
2379
- channel.bank = 121 * 128;
2380
2428
  channel.isDrum = false;
2381
2429
  }
2382
2430
  this.channels[9].bankMSB = 120;
2383
- this.channels[9].bank = 120 * 128;
2384
2431
  this.channels[9].isDrum = true;
2385
2432
  }
2386
2433
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
@@ -2425,16 +2472,11 @@ class MidyGM2 {
2425
2472
  const volume = (data[5] * 128 + data[4]) / 16383;
2426
2473
  this.setMasterVolume(volume, scheduleTime);
2427
2474
  }
2428
- setMasterVolume(volume, scheduleTime) {
2475
+ setMasterVolume(value, scheduleTime) {
2429
2476
  scheduleTime ??= this.audioContext.currentTime;
2430
- if (volume < 0 && 1 < volume) {
2431
- console.error("Master Volume is out of range");
2432
- }
2433
- else {
2434
- this.masterVolume.gain
2435
- .cancelScheduledValues(scheduleTime)
2436
- .setValueAtTime(volume * volume, scheduleTime);
2437
- }
2477
+ this.masterVolume.gain
2478
+ .cancelScheduledValues(scheduleTime)
2479
+ .setValueAtTime(value * value, scheduleTime);
2438
2480
  }
2439
2481
  handleMasterFineTuningSysEx(data, scheduleTime) {
2440
2482
  const value = (data[5] * 128 + data[4]) / 16383;
@@ -2797,6 +2839,22 @@ class MidyGM2 {
2797
2839
  const controlValue = channel.keyBasedTable[index];
2798
2840
  return controlValue;
2799
2841
  }
2842
+ createKeyBasedControllerHandlers() {
2843
+ const handlers = new Array(128);
2844
+ handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2845
+ handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2846
+ handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
2847
+ if (note.noteNumber === keyNumber) {
2848
+ this.setReverbSend(channel, note, scheduleTime);
2849
+ }
2850
+ });
2851
+ handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
2852
+ if (note.noteNumber === keyNumber) {
2853
+ this.setChorusSend(channel, note, scheduleTime);
2854
+ }
2855
+ });
2856
+ return handlers;
2857
+ }
2800
2858
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2801
2859
  const channelNumber = data[4];
2802
2860
  const channel = this.channels[channelNumber];
@@ -2809,26 +2867,9 @@ class MidyGM2 {
2809
2867
  const value = data[i + 1];
2810
2868
  const index = keyNumber * 128 + controllerType;
2811
2869
  table[index] = value;
2812
- switch (controllerType) {
2813
- case 7:
2814
- case 10:
2815
- this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2816
- break;
2817
- case 91:
2818
- this.processScheduledNotes(channel, (note) => {
2819
- if (note.noteNumber === keyNumber) {
2820
- this.setReverbSend(channel, note, scheduleTime);
2821
- }
2822
- });
2823
- break;
2824
- case 93:
2825
- this.processScheduledNotes(channel, (note) => {
2826
- if (note.noteNumber === keyNumber) {
2827
- this.setChorusSend(channel, note, scheduleTime);
2828
- }
2829
- });
2830
- break;
2831
- }
2870
+ const handler = this.keyBasedControllerHandlers[controllerType];
2871
+ if (handler)
2872
+ handler(channel, keyNumber, scheduleTime);
2832
2873
  }
2833
2874
  }
2834
2875
  handleSysEx(data, scheduleTime) {
@@ -2870,7 +2911,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2870
2911
  scheduleIndex: 0,
2871
2912
  detune: 0,
2872
2913
  programNumber: 0,
2873
- bank: 121 * 128,
2874
2914
  bankMSB: 121,
2875
2915
  bankLSB: 0,
2876
2916
  dataMSB: 0,