@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/esm/midy-GM2.js CHANGED
@@ -1,7 +1,19 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
3
  class Note {
4
- constructor(noteNumber, velocity, startTime, voice, voiceParams) {
4
+ constructor(noteNumber, velocity, startTime) {
5
+ Object.defineProperty(this, "voice", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: void 0
10
+ });
11
+ Object.defineProperty(this, "voiceParams", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
5
17
  Object.defineProperty(this, "index", {
6
18
  enumerable: true,
7
19
  configurable: true,
@@ -14,6 +26,12 @@ class Note {
14
26
  writable: true,
15
27
  value: false
16
28
  });
29
+ Object.defineProperty(this, "pending", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: true
34
+ });
17
35
  Object.defineProperty(this, "bufferSource", {
18
36
  enumerable: true,
19
37
  configurable: true,
@@ -89,8 +107,6 @@ class Note {
89
107
  this.noteNumber = noteNumber;
90
108
  this.velocity = velocity;
91
109
  this.startTime = startTime;
92
- this.voice = voice;
93
- this.voiceParams = voiceParams;
94
110
  }
95
111
  }
96
112
  const drumExclusiveClassesByKit = new Array(57);
@@ -272,6 +288,18 @@ export class MidyGM2 {
272
288
  writable: true,
273
289
  value: 0
274
290
  });
291
+ Object.defineProperty(this, "lastActiveSensing", {
292
+ enumerable: true,
293
+ configurable: true,
294
+ writable: true,
295
+ value: 0
296
+ });
297
+ Object.defineProperty(this, "activeSensingThreshold", {
298
+ enumerable: true,
299
+ configurable: true,
300
+ writable: true,
301
+ value: 0.3
302
+ });
275
303
  Object.defineProperty(this, "noteCheckInterval", {
276
304
  enumerable: true,
277
305
  configurable: true,
@@ -312,7 +340,7 @@ export class MidyGM2 {
312
340
  enumerable: true,
313
341
  configurable: true,
314
342
  writable: true,
315
- value: this.initSoundFontTable()
343
+ value: Array.from({ length: 128 }, () => [])
316
344
  });
317
345
  Object.defineProperty(this, "voiceCounter", {
318
346
  enumerable: true,
@@ -326,6 +354,12 @@ export class MidyGM2 {
326
354
  writable: true,
327
355
  value: new Map()
328
356
  });
357
+ Object.defineProperty(this, "realtimeVoiceCache", {
358
+ enumerable: true,
359
+ configurable: true,
360
+ writable: true,
361
+ value: new Map()
362
+ });
329
363
  Object.defineProperty(this, "isPlaying", {
330
364
  enumerable: true,
331
365
  configurable: true,
@@ -399,8 +433,10 @@ export class MidyGM2 {
399
433
  length: 1,
400
434
  sampleRate: audioContext.sampleRate,
401
435
  });
436
+ this.messageHandlers = this.createMessageHandlers();
402
437
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
403
438
  this.controlChangeHandlers = this.createControlChangeHandlers();
439
+ this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
404
440
  this.channels = this.createChannels(audioContext);
405
441
  this.reverbEffect = this.createReverbEffect(audioContext);
406
442
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -410,21 +446,14 @@ export class MidyGM2 {
410
446
  this.scheduler.connect(audioContext.destination);
411
447
  this.GM2SystemOn();
412
448
  }
413
- initSoundFontTable() {
414
- const table = new Array(128);
415
- for (let i = 0; i < 128; i++) {
416
- table[i] = new Map();
417
- }
418
- return table;
419
- }
420
449
  addSoundFont(soundFont) {
421
450
  const index = this.soundFonts.length;
422
451
  this.soundFonts.push(soundFont);
423
452
  const presetHeaders = soundFont.parsed.presetHeaders;
453
+ const soundFontTable = this.soundFontTable;
424
454
  for (let i = 0; i < presetHeaders.length; i++) {
425
- const presetHeader = presetHeaders[i];
426
- const banks = this.soundFontTable[presetHeader.preset];
427
- banks.set(presetHeader.bank, index);
455
+ const { preset, bank } = presetHeaders[i];
456
+ soundFontTable[preset][bank] = index;
428
457
  }
429
458
  }
430
459
  async toUint8Array(input) {
@@ -502,13 +531,17 @@ export class MidyGM2 {
502
531
  this.GM2SystemOn();
503
532
  }
504
533
  getVoiceId(channel, noteNumber, velocity) {
505
- const bankNumber = this.calcBank(channel);
506
- const soundFontIndex = this.soundFontTable[channel.programNumber]
507
- .get(bankNumber);
534
+ const programNumber = channel.programNumber;
535
+ const bankTable = this.soundFontTable[programNumber];
536
+ if (!bankTable)
537
+ return;
538
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
539
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
540
+ const soundFontIndex = bankTable[bank];
508
541
  if (soundFontIndex === undefined)
509
542
  return;
510
543
  const soundFont = this.soundFonts[soundFontIndex];
511
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
544
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
512
545
  const { instrument, sampleID } = voice.generators;
513
546
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
514
547
  }
@@ -577,19 +610,22 @@ export class MidyGM2 {
577
610
  }
578
611
  return bufferSource;
579
612
  }
580
- async scheduleTimelineEvents(t, resumeTime, queueIndex) {
581
- while (queueIndex < this.timeline.length) {
582
- const event = this.timeline[queueIndex];
583
- if (event.startTime > t + this.lookAhead)
613
+ async scheduleTimelineEvents(scheduleTime, queueIndex) {
614
+ const timeOffset = this.resumeTime - this.startTime;
615
+ const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
616
+ const schedulingOffset = this.startDelay - timeOffset;
617
+ const timeline = this.timeline;
618
+ while (queueIndex < timeline.length) {
619
+ const event = timeline[queueIndex];
620
+ if (lookAheadCheckTime < event.startTime)
584
621
  break;
585
- const delay = this.startDelay - resumeTime;
586
- const startTime = event.startTime + delay;
622
+ const startTime = event.startTime + schedulingOffset;
587
623
  switch (event.type) {
588
624
  case "noteOn":
589
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
625
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
590
626
  break;
591
627
  case "noteOff": {
592
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
628
+ const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
593
629
  if (notePromise)
594
630
  this.notePromises.push(notePromise);
595
631
  break;
@@ -625,6 +661,7 @@ export class MidyGM2 {
625
661
  this.exclusiveClassNotes.fill(undefined);
626
662
  this.drumExclusiveClassNotes.fill(undefined);
627
663
  this.voiceCache.clear();
664
+ this.realtimeVoiceCache.clear();
628
665
  for (let i = 0; i < this.channels.length; i++) {
629
666
  this.channels[i].scheduledNotes = [];
630
667
  this.resetChannelStates(i);
@@ -658,13 +695,17 @@ export class MidyGM2 {
658
695
  this.isPaused = false;
659
696
  this.startTime = this.audioContext.currentTime;
660
697
  let queueIndex = this.getQueueIndex(this.resumeTime);
661
- let resumeTime = this.resumeTime - this.startTime;
662
698
  let finished = false;
663
699
  this.notePromises = [];
664
700
  while (queueIndex < this.timeline.length) {
665
701
  const now = this.audioContext.currentTime;
666
- const t = now + resumeTime;
667
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
702
+ if (0 < this.lastActiveSensing &&
703
+ this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
704
+ await this.stopNotes(0, true, now);
705
+ await this.audioContext.suspend();
706
+ finished = true;
707
+ break;
708
+ }
668
709
  if (this.isPausing) {
669
710
  await this.stopNotes(0, true, now);
670
711
  await this.audioContext.suspend();
@@ -683,16 +724,17 @@ export class MidyGM2 {
683
724
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
684
725
  this.updateStates(queueIndex, nextQueueIndex);
685
726
  queueIndex = nextQueueIndex;
686
- resumeTime = this.resumeTime - this.startTime;
687
727
  this.isSeeking = false;
688
728
  continue;
689
729
  }
730
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
690
731
  const waitTime = now + this.noteCheckInterval;
691
732
  await this.scheduleTask(() => { }, waitTime);
692
733
  }
693
734
  if (finished) {
694
735
  this.notePromises = [];
695
736
  this.resetAllStates();
737
+ this.lastActiveSensing = 0;
696
738
  }
697
739
  this.isPlaying = false;
698
740
  }
@@ -702,17 +744,17 @@ export class MidyGM2 {
702
744
  secondToTicks(second, secondsPerBeat) {
703
745
  return second * this.ticksPerBeat / secondsPerBeat;
704
746
  }
747
+ getSoundFontId(channel) {
748
+ const programNumber = channel.programNumber;
749
+ const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
750
+ const bank = bankNumber.toString().padStart(3, "0");
751
+ const program = programNumber.toString().padStart(3, "0");
752
+ return `${bank}:${program}`;
753
+ }
705
754
  extractMidiData(midi) {
706
755
  const instruments = new Set();
707
756
  const timeline = [];
708
- const tmpChannels = new Array(this.channels.length);
709
- for (let i = 0; i < tmpChannels.length; i++) {
710
- tmpChannels[i] = {
711
- programNumber: -1,
712
- bankMSB: this.channels[i].bankMSB,
713
- bankLSB: this.channels[i].bankLSB,
714
- };
715
- }
757
+ const channels = this.channels;
716
758
  for (let i = 0; i < midi.tracks.length; i++) {
717
759
  const track = midi.tracks[i];
718
760
  let currentTicks = 0;
@@ -722,48 +764,40 @@ export class MidyGM2 {
722
764
  event.ticks = currentTicks;
723
765
  switch (event.type) {
724
766
  case "noteOn": {
725
- const channel = tmpChannels[event.channel];
726
- if (channel.programNumber < 0) {
727
- channel.programNumber = event.programNumber;
728
- switch (channel.bankMSB) {
729
- case 120:
730
- instruments.add(`128:0`);
731
- break;
732
- case 121:
733
- instruments.add(`${channel.bankLSB}:0`);
734
- break;
735
- default: {
736
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
737
- instruments.add(`${bankNumber}:0`);
738
- }
739
- }
740
- channel.programNumber = 0;
741
- }
767
+ const channel = channels[event.channel];
768
+ instruments.add(this.getSoundFontId(channel));
742
769
  break;
743
770
  }
744
771
  case "controller":
745
772
  switch (event.controllerType) {
746
773
  case 0:
747
- tmpChannels[event.channel].bankMSB = event.value;
774
+ this.setBankMSB(event.channel, event.value);
748
775
  break;
749
776
  case 32:
750
- tmpChannels[event.channel].bankLSB = event.value;
777
+ this.setBankLSB(event.channel, event.value);
751
778
  break;
752
779
  }
753
780
  break;
754
781
  case "programChange": {
755
- const channel = tmpChannels[event.channel];
756
- channel.programNumber = event.programNumber;
757
- switch (channel.bankMSB) {
758
- case 120:
759
- instruments.add(`128:${channel.programNumber}`);
760
- break;
761
- case 121:
762
- instruments.add(`${channel.bankLSB}:${channel.programNumber}`);
763
- break;
764
- default: {
765
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
766
- instruments.add(`${bankNumber}:${channel.programNumber}`);
782
+ const channel = channels[event.channel];
783
+ this.setProgramChange(event.channel, event.programNumber);
784
+ instruments.add(this.getSoundFontId(channel));
785
+ break;
786
+ }
787
+ case "sysEx": {
788
+ const data = event.data;
789
+ if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
790
+ switch (data[3]) {
791
+ case 1:
792
+ this.GM1SystemOn(scheduleTime);
793
+ break;
794
+ case 2: // GM System Off
795
+ break;
796
+ case 3:
797
+ this.GM2SystemOn(scheduleTime);
798
+ break;
799
+ default:
800
+ console.warn(`Unsupported Exclusive Message: ${data}`);
767
801
  }
768
802
  }
769
803
  }
@@ -802,7 +836,7 @@ export class MidyGM2 {
802
836
  const channel = this.channels[channelNumber];
803
837
  const promises = [];
804
838
  this.processActiveNotes(channel, scheduleTime, (note) => {
805
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
839
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
806
840
  this.notePromises.push(promise);
807
841
  promises.push(promise);
808
842
  });
@@ -812,7 +846,7 @@ export class MidyGM2 {
812
846
  const channel = this.channels[channelNumber];
813
847
  const promises = [];
814
848
  this.processScheduledNotes(channel, (note) => {
815
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
849
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
816
850
  this.notePromises.push(promise);
817
851
  promises.push(promise);
818
852
  });
@@ -845,7 +879,7 @@ export class MidyGM2 {
845
879
  if (!this.isPlaying || this.isPaused)
846
880
  return;
847
881
  const now = this.audioContext.currentTime;
848
- this.resumeTime += now - this.startTime - this.startDelay;
882
+ this.resumeTime = now - this.startTime - this.startDelay;
849
883
  this.isPausing = true;
850
884
  await this.playPromise;
851
885
  this.isPausing = false;
@@ -871,11 +905,13 @@ export class MidyGM2 {
871
905
  if (totalTime < event.startTime)
872
906
  totalTime = event.startTime;
873
907
  }
874
- return totalTime;
908
+ return totalTime + this.startDelay;
875
909
  }
876
910
  currentTime() {
911
+ if (!this.isPlaying)
912
+ return this.resumeTime;
877
913
  const now = this.audioContext.currentTime;
878
- return this.resumeTime + now - this.startTime - this.startDelay;
914
+ return now + this.resumeTime - this.startTime;
879
915
  }
880
916
  processScheduledNotes(channel, callback) {
881
917
  const scheduledNotes = channel.scheduledNotes;
@@ -1086,7 +1122,7 @@ export class MidyGM2 {
1086
1122
  updateDetune(channel, note, scheduleTime) {
1087
1123
  const noteDetune = this.calcNoteDetune(channel, note);
1088
1124
  const detune = channel.detune + noteDetune;
1089
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1125
+ if (this.isPortamento(channel, note)) {
1090
1126
  const startTime = note.startTime;
1091
1127
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1092
1128
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
@@ -1302,31 +1338,42 @@ export class MidyGM2 {
1302
1338
  note.vibratoLFO.connect(note.vibratoDepth);
1303
1339
  note.vibratoDepth.connect(note.bufferSource.detune);
1304
1340
  }
1305
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1341
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1306
1342
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1307
- const cache = this.voiceCache.get(audioBufferId);
1308
- if (cache) {
1309
- cache.counter += 1;
1310
- if (cache.maxCount <= cache.counter) {
1311
- this.voiceCache.delete(audioBufferId);
1312
- }
1313
- return cache.audioBuffer;
1314
- }
1315
- else {
1316
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1343
+ if (realtime) {
1344
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1345
+ if (cachedAudioBuffer)
1346
+ return cachedAudioBuffer;
1317
1347
  const audioBuffer = await this.createAudioBuffer(voiceParams);
1318
- const cache = { audioBuffer, maxCount, counter: 1 };
1319
- this.voiceCache.set(audioBufferId, cache);
1348
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1320
1349
  return audioBuffer;
1321
1350
  }
1351
+ else {
1352
+ const cache = this.voiceCache.get(audioBufferId);
1353
+ if (cache) {
1354
+ cache.counter += 1;
1355
+ if (cache.maxCount <= cache.counter) {
1356
+ this.voiceCache.delete(audioBufferId);
1357
+ }
1358
+ return cache.audioBuffer;
1359
+ }
1360
+ else {
1361
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1362
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1363
+ const cache = { audioBuffer, maxCount, counter: 1 };
1364
+ this.voiceCache.set(audioBufferId, cache);
1365
+ return audioBuffer;
1366
+ }
1367
+ }
1322
1368
  }
1323
- async createNote(channel, voice, noteNumber, velocity, startTime) {
1369
+ async setNoteAudioNode(channel, note, realtime) {
1324
1370
  const now = this.audioContext.currentTime;
1371
+ const { noteNumber, velocity, startTime } = note;
1325
1372
  const state = channel.state;
1326
1373
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1327
- const voiceParams = voice.getAllParams(controllerState);
1328
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1329
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1374
+ const voiceParams = note.voice.getAllParams(controllerState);
1375
+ note.voiceParams = voiceParams;
1376
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1330
1377
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1331
1378
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1332
1379
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -1337,7 +1384,7 @@ export class MidyGM2 {
1337
1384
  if (prevNote && prevNote.noteNumber !== noteNumber) {
1338
1385
  note.portamentoNoteNumber = prevNote.noteNumber;
1339
1386
  }
1340
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1387
+ if (!channel.isDrum && this.isPortamento(channel, note)) {
1341
1388
  this.setPortamentoVolumeEnvelope(channel, note, now);
1342
1389
  this.setPortamentoFilterEnvelope(channel, note, now);
1343
1390
  this.setPortamentoPitchEnvelope(note, now);
@@ -1365,22 +1412,6 @@ export class MidyGM2 {
1365
1412
  note.bufferSource.start(startTime);
1366
1413
  return note;
1367
1414
  }
1368
- calcBank(channel) {
1369
- switch (this.mode) {
1370
- case "GM1":
1371
- if (channel.isDrum)
1372
- return 128;
1373
- return 0;
1374
- case "GM2":
1375
- if (channel.bankMSB === 121)
1376
- return 0;
1377
- if (channel.isDrum)
1378
- return 128;
1379
- return channel.bank;
1380
- default:
1381
- return channel.bank;
1382
- }
1383
- }
1384
1415
  handleExclusiveClass(note, channelNumber, startTime) {
1385
1416
  const exclusiveClass = note.voiceParams.exclusiveClass;
1386
1417
  if (exclusiveClass === 0)
@@ -1389,7 +1420,7 @@ export class MidyGM2 {
1389
1420
  if (prev) {
1390
1421
  const [prevNote, prevChannelNumber] = prev;
1391
1422
  if (prevNote && !prevNote.ending) {
1392
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1423
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1393
1424
  startTime, true);
1394
1425
  }
1395
1426
  }
@@ -1409,23 +1440,14 @@ export class MidyGM2 {
1409
1440
  channelNumber;
1410
1441
  const prevNote = this.drumExclusiveClassNotes[index];
1411
1442
  if (prevNote && !prevNote.ending) {
1412
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1443
+ this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1413
1444
  startTime, true);
1414
1445
  }
1415
1446
  this.drumExclusiveClassNotes[index] = note;
1416
1447
  }
1417
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1448
+ setNoteRouting(channelNumber, note, startTime) {
1418
1449
  const channel = this.channels[channelNumber];
1419
- const bankNumber = this.calcBank(channel, channelNumber);
1420
- const soundFontIndex = this.soundFontTable[channel.programNumber]
1421
- .get(bankNumber);
1422
- if (soundFontIndex === undefined)
1423
- return;
1424
- const soundFont = this.soundFonts[soundFontIndex];
1425
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1426
- if (!voice)
1427
- return;
1428
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1450
+ const { noteNumber, volumeEnvelopeNode } = note;
1429
1451
  if (channel.isDrum) {
1430
1452
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1431
1453
  let gainL = keyBasedGainLs[noteNumber];
@@ -1435,25 +1457,48 @@ export class MidyGM2 {
1435
1457
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1436
1458
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1437
1459
  }
1438
- note.volumeEnvelopeNode.connect(gainL);
1439
- note.volumeEnvelopeNode.connect(gainR);
1460
+ volumeEnvelopeNode.connect(gainL);
1461
+ volumeEnvelopeNode.connect(gainR);
1440
1462
  }
1441
1463
  else {
1442
- note.volumeEnvelopeNode.connect(channel.gainL);
1443
- note.volumeEnvelopeNode.connect(channel.gainR);
1464
+ volumeEnvelopeNode.connect(channel.gainL);
1465
+ volumeEnvelopeNode.connect(channel.gainR);
1444
1466
  }
1445
1467
  if (0.5 <= channel.state.sustainPedal) {
1446
1468
  channel.sustainNotes.push(note);
1447
1469
  }
1448
1470
  this.handleExclusiveClass(note, channelNumber, startTime);
1449
1471
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1472
+ }
1473
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
1474
+ const channel = this.channels[channelNumber];
1475
+ const realtime = startTime === undefined;
1476
+ if (realtime)
1477
+ startTime = this.audioContext.currentTime;
1478
+ const note = new Note(noteNumber, velocity, startTime);
1450
1479
  const scheduledNotes = channel.scheduledNotes;
1451
1480
  note.index = scheduledNotes.length;
1452
1481
  scheduledNotes.push(note);
1453
- }
1454
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1455
- scheduleTime ??= this.audioContext.currentTime;
1456
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1482
+ const programNumber = channel.programNumber;
1483
+ const bankTable = this.soundFontTable[programNumber];
1484
+ if (!bankTable)
1485
+ return;
1486
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1487
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1488
+ const soundFontIndex = bankTable[bank];
1489
+ if (soundFontIndex === undefined)
1490
+ return;
1491
+ const soundFont = this.soundFonts[soundFontIndex];
1492
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1493
+ if (!note.voice)
1494
+ return;
1495
+ await this.setNoteAudioNode(channel, note, realtime);
1496
+ this.setNoteRouting(channelNumber, note, startTime);
1497
+ note.pending = false;
1498
+ const off = note.offEvent;
1499
+ if (off) {
1500
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1501
+ }
1457
1502
  }
1458
1503
  disconnectNote(note) {
1459
1504
  note.bufferSource.disconnect();
@@ -1476,6 +1521,7 @@ export class MidyGM2 {
1476
1521
  }
1477
1522
  }
1478
1523
  releaseNote(channel, note, endTime) {
1524
+ endTime ??= this.audioContext.currentTime;
1479
1525
  const volRelease = endTime + note.voiceParams.volRelease;
1480
1526
  const modRelease = endTime + note.voiceParams.modRelease;
1481
1527
  const stopTime = Math.min(volRelease, modRelease);
@@ -1496,7 +1542,7 @@ export class MidyGM2 {
1496
1542
  }, stopTime);
1497
1543
  });
1498
1544
  }
1499
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1545
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1500
1546
  const channel = this.channels[channelNumber];
1501
1547
  const state = channel.state;
1502
1548
  if (!force) {
@@ -1515,6 +1561,10 @@ export class MidyGM2 {
1515
1561
  if (index < 0)
1516
1562
  return;
1517
1563
  const note = channel.scheduledNotes[index];
1564
+ if (note.pending) {
1565
+ note.offEvent = { velocity, startTime: endTime };
1566
+ return;
1567
+ }
1518
1568
  note.ending = true;
1519
1569
  this.setNoteIndex(channel, index);
1520
1570
  this.releaseNote(channel, note, endTime);
@@ -1545,16 +1595,12 @@ export class MidyGM2 {
1545
1595
  }
1546
1596
  return -1;
1547
1597
  }
1548
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1549
- scheduleTime ??= this.audioContext.currentTime;
1550
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1551
- }
1552
1598
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1553
1599
  const velocity = halfVelocity * 2;
1554
1600
  const channel = this.channels[channelNumber];
1555
1601
  const promises = [];
1556
1602
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1557
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1603
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1558
1604
  promises.push(promise);
1559
1605
  }
1560
1606
  channel.sustainNotes = [];
@@ -1568,47 +1614,51 @@ export class MidyGM2 {
1568
1614
  channel.state.sostenutoPedal = 0;
1569
1615
  for (let i = 0; i < sostenutoNotes.length; i++) {
1570
1616
  const note = sostenutoNotes[i];
1571
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1617
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1572
1618
  promises.push(promise);
1573
1619
  }
1574
1620
  channel.sostenutoNotes = [];
1575
1621
  return promises;
1576
1622
  }
1577
- handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1578
- const channelNumber = statusByte & 0x0F;
1579
- const messageType = statusByte & 0xF0;
1580
- switch (messageType) {
1581
- case 0x80:
1582
- return this.noteOff(channelNumber, data1, data2, scheduleTime);
1583
- case 0x90:
1584
- return this.noteOn(channelNumber, data1, data2, scheduleTime);
1585
- case 0xB0:
1586
- return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1587
- case 0xC0:
1588
- return this.setProgramChange(channelNumber, data1, scheduleTime);
1589
- case 0xD0:
1590
- return this.setChannelPressure(channelNumber, data1, scheduleTime);
1591
- case 0xE0:
1592
- return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1593
- default:
1594
- console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1623
+ createMessageHandlers() {
1624
+ const handlers = new Array(256);
1625
+ // Channel Message
1626
+ handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
1627
+ handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
1628
+ handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
1629
+ handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
1630
+ handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
1631
+ handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
1632
+ // System Real Time Message
1633
+ handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
1634
+ return handlers;
1635
+ }
1636
+ handleMessage(data, scheduleTime) {
1637
+ const status = data[0];
1638
+ if (status === 0xF0) {
1639
+ return this.handleSysEx(data.subarray(1), scheduleTime);
1595
1640
  }
1641
+ const handler = this.messageHandlers[status];
1642
+ if (handler)
1643
+ handler(data, scheduleTime);
1644
+ }
1645
+ activeSensing() {
1646
+ this.lastActiveSensing = performance.now();
1596
1647
  }
1597
1648
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1598
1649
  const channel = this.channels[channelNumber];
1599
- channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1600
1650
  channel.programNumber = programNumber;
1601
1651
  if (this.mode === "GM2") {
1602
1652
  switch (channel.bankMSB) {
1603
1653
  case 120:
1604
1654
  channel.isDrum = true;
1655
+ channel.keyBasedTable.fill(-1);
1605
1656
  break;
1606
1657
  case 121:
1607
1658
  channel.isDrum = false;
1608
1659
  break;
1609
1660
  }
1610
1661
  }
1611
- channel.keyBasedTable.fill(-1);
1612
1662
  }
1613
1663
  setChannelPressure(channelNumber, value, scheduleTime) {
1614
1664
  const channel = this.channels[channelNumber];
@@ -1933,22 +1983,20 @@ export class MidyGM2 {
1933
1983
  this.updateModulation(channel, scheduleTime);
1934
1984
  }
1935
1985
  updatePortamento(channel, scheduleTime) {
1986
+ if (channel.isDrum)
1987
+ return;
1936
1988
  this.processScheduledNotes(channel, (note) => {
1937
- if (0.5 <= channel.state.portamento) {
1938
- if (0 <= note.portamentoNoteNumber) {
1939
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1940
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1941
- this.setPortamentoPitchEnvelope(note, scheduleTime);
1942
- this.updateDetune(channel, note, scheduleTime);
1943
- }
1989
+ if (this.isPortamento(channel, note)) {
1990
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1991
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1992
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1993
+ this.updateDetune(channel, note, scheduleTime);
1944
1994
  }
1945
1995
  else {
1946
- if (0 <= note.portamentoNoteNumber) {
1947
- this.setVolumeEnvelope(channel, note, scheduleTime);
1948
- this.setFilterEnvelope(channel, note, scheduleTime);
1949
- this.setPitchEnvelope(note, scheduleTime);
1950
- this.updateDetune(channel, note, scheduleTime);
1951
- }
1996
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1997
+ this.setFilterEnvelope(channel, note, scheduleTime);
1998
+ this.setPitchEnvelope(note, scheduleTime);
1999
+ this.updateDetune(channel, note, scheduleTime);
1952
2000
  }
1953
2001
  });
1954
2002
  }
@@ -2054,6 +2102,9 @@ export class MidyGM2 {
2054
2102
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2055
2103
  }
2056
2104
  }
2105
+ isPortamento(channel, note) {
2106
+ return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
2107
+ }
2057
2108
  setPortamento(channelNumber, value, scheduleTime) {
2058
2109
  const channel = this.channels[channelNumber];
2059
2110
  if (channel.isDrum)
@@ -2090,7 +2141,7 @@ export class MidyGM2 {
2090
2141
  scheduleTime ??= this.audioContext.currentTime;
2091
2142
  state.softPedal = softPedal / 127;
2092
2143
  this.processScheduledNotes(channel, (note) => {
2093
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2144
+ if (this.isPortamento(channel, note)) {
2094
2145
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2095
2146
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2096
2147
  }
@@ -2358,11 +2409,9 @@ export class MidyGM2 {
2358
2409
  const channel = this.channels[i];
2359
2410
  channel.bankMSB = 0;
2360
2411
  channel.bankLSB = 0;
2361
- channel.bank = 0;
2362
2412
  channel.isDrum = false;
2363
2413
  }
2364
2414
  this.channels[9].bankMSB = 1;
2365
- this.channels[9].bank = 128;
2366
2415
  this.channels[9].isDrum = true;
2367
2416
  }
2368
2417
  GM2SystemOn(scheduleTime) {
@@ -2373,11 +2422,9 @@ export class MidyGM2 {
2373
2422
  const channel = this.channels[i];
2374
2423
  channel.bankMSB = 121;
2375
2424
  channel.bankLSB = 0;
2376
- channel.bank = 121 * 128;
2377
2425
  channel.isDrum = false;
2378
2426
  }
2379
2427
  this.channels[9].bankMSB = 120;
2380
- this.channels[9].bank = 120 * 128;
2381
2428
  this.channels[9].isDrum = true;
2382
2429
  }
2383
2430
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
@@ -2422,16 +2469,11 @@ export class MidyGM2 {
2422
2469
  const volume = (data[5] * 128 + data[4]) / 16383;
2423
2470
  this.setMasterVolume(volume, scheduleTime);
2424
2471
  }
2425
- setMasterVolume(volume, scheduleTime) {
2472
+ setMasterVolume(value, scheduleTime) {
2426
2473
  scheduleTime ??= this.audioContext.currentTime;
2427
- if (volume < 0 && 1 < volume) {
2428
- console.error("Master Volume is out of range");
2429
- }
2430
- else {
2431
- this.masterVolume.gain
2432
- .cancelScheduledValues(scheduleTime)
2433
- .setValueAtTime(volume * volume, scheduleTime);
2434
- }
2474
+ this.masterVolume.gain
2475
+ .cancelScheduledValues(scheduleTime)
2476
+ .setValueAtTime(value * value, scheduleTime);
2435
2477
  }
2436
2478
  handleMasterFineTuningSysEx(data, scheduleTime) {
2437
2479
  const value = (data[5] * 128 + data[4]) / 16383;
@@ -2794,6 +2836,22 @@ export class MidyGM2 {
2794
2836
  const controlValue = channel.keyBasedTable[index];
2795
2837
  return controlValue;
2796
2838
  }
2839
+ createKeyBasedControllerHandlers() {
2840
+ const handlers = new Array(128);
2841
+ handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2842
+ handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2843
+ handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
2844
+ if (note.noteNumber === keyNumber) {
2845
+ this.setReverbSend(channel, note, scheduleTime);
2846
+ }
2847
+ });
2848
+ handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
2849
+ if (note.noteNumber === keyNumber) {
2850
+ this.setChorusSend(channel, note, scheduleTime);
2851
+ }
2852
+ });
2853
+ return handlers;
2854
+ }
2797
2855
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2798
2856
  const channelNumber = data[4];
2799
2857
  const channel = this.channels[channelNumber];
@@ -2806,26 +2864,9 @@ export class MidyGM2 {
2806
2864
  const value = data[i + 1];
2807
2865
  const index = keyNumber * 128 + controllerType;
2808
2866
  table[index] = value;
2809
- switch (controllerType) {
2810
- case 7:
2811
- case 10:
2812
- this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
2813
- break;
2814
- case 91:
2815
- this.processScheduledNotes(channel, (note) => {
2816
- if (note.noteNumber === keyNumber) {
2817
- this.setReverbSend(channel, note, scheduleTime);
2818
- }
2819
- });
2820
- break;
2821
- case 93:
2822
- this.processScheduledNotes(channel, (note) => {
2823
- if (note.noteNumber === keyNumber) {
2824
- this.setChorusSend(channel, note, scheduleTime);
2825
- }
2826
- });
2827
- break;
2828
- }
2867
+ const handler = this.keyBasedControllerHandlers[controllerType];
2868
+ if (handler)
2869
+ handler(channel, keyNumber, scheduleTime);
2829
2870
  }
2830
2871
  }
2831
2872
  handleSysEx(data, scheduleTime) {
@@ -2866,7 +2907,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2866
2907
  scheduleIndex: 0,
2867
2908
  detune: 0,
2868
2909
  programNumber: 0,
2869
- bank: 121 * 128,
2870
2910
  bankMSB: 121,
2871
2911
  bankLSB: 0,
2872
2912
  dataMSB: 0,