@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.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,
@@ -95,8 +113,6 @@ class Note {
95
113
  this.noteNumber = noteNumber;
96
114
  this.velocity = velocity;
97
115
  this.startTime = startTime;
98
- this.voice = voice;
99
- this.voiceParams = voiceParams;
100
116
  }
101
117
  }
102
118
  const drumExclusiveClassesByKit = new Array(57);
@@ -287,6 +303,18 @@ export class Midy {
287
303
  writable: true,
288
304
  value: 0
289
305
  });
306
+ Object.defineProperty(this, "lastActiveSensing", {
307
+ enumerable: true,
308
+ configurable: true,
309
+ writable: true,
310
+ value: 0
311
+ });
312
+ Object.defineProperty(this, "activeSensingThreshold", {
313
+ enumerable: true,
314
+ configurable: true,
315
+ writable: true,
316
+ value: 0.3
317
+ });
290
318
  Object.defineProperty(this, "noteCheckInterval", {
291
319
  enumerable: true,
292
320
  configurable: true,
@@ -327,7 +355,7 @@ export class Midy {
327
355
  enumerable: true,
328
356
  configurable: true,
329
357
  writable: true,
330
- value: this.initSoundFontTable()
358
+ value: Array.from({ length: 128 }, () => [])
331
359
  });
332
360
  Object.defineProperty(this, "voiceCounter", {
333
361
  enumerable: true,
@@ -341,6 +369,12 @@ export class Midy {
341
369
  writable: true,
342
370
  value: new Map()
343
371
  });
372
+ Object.defineProperty(this, "realtimeVoiceCache", {
373
+ enumerable: true,
374
+ configurable: true,
375
+ writable: true,
376
+ value: new Map()
377
+ });
344
378
  Object.defineProperty(this, "isPlaying", {
345
379
  enumerable: true,
346
380
  configurable: true,
@@ -414,8 +448,10 @@ export class Midy {
414
448
  length: 1,
415
449
  sampleRate: audioContext.sampleRate,
416
450
  });
451
+ this.messageHandlers = this.createMessageHandlers();
417
452
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
418
453
  this.controlChangeHandlers = this.createControlChangeHandlers();
454
+ this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
419
455
  this.channels = this.createChannels(audioContext);
420
456
  this.reverbEffect = this.createReverbEffect(audioContext);
421
457
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -425,21 +461,14 @@ export class Midy {
425
461
  this.scheduler.connect(audioContext.destination);
426
462
  this.GM2SystemOn();
427
463
  }
428
- initSoundFontTable() {
429
- const table = new Array(128);
430
- for (let i = 0; i < 128; i++) {
431
- table[i] = new Map();
432
- }
433
- return table;
434
- }
435
464
  addSoundFont(soundFont) {
436
465
  const index = this.soundFonts.length;
437
466
  this.soundFonts.push(soundFont);
438
467
  const presetHeaders = soundFont.parsed.presetHeaders;
468
+ const soundFontTable = this.soundFontTable;
439
469
  for (let i = 0; i < presetHeaders.length; i++) {
440
- const presetHeader = presetHeaders[i];
441
- const banks = this.soundFontTable[presetHeader.preset];
442
- banks.set(presetHeader.bank, index);
470
+ const { preset, bank } = presetHeaders[i];
471
+ soundFontTable[preset][bank] = index;
443
472
  }
444
473
  }
445
474
  async toUint8Array(input) {
@@ -517,13 +546,17 @@ export class Midy {
517
546
  this.GM2SystemOn();
518
547
  }
519
548
  getVoiceId(channel, noteNumber, velocity) {
520
- const bankNumber = this.calcBank(channel);
521
- const soundFontIndex = this.soundFontTable[channel.programNumber]
522
- .get(bankNumber);
549
+ const programNumber = channel.programNumber;
550
+ const bankTable = this.soundFontTable[programNumber];
551
+ if (!bankTable)
552
+ return;
553
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
554
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
555
+ const soundFontIndex = bankTable[bank];
523
556
  if (soundFontIndex === undefined)
524
557
  return;
525
558
  const soundFont = this.soundFonts[soundFontIndex];
526
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
559
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
527
560
  const { instrument, sampleID } = voice.generators;
528
561
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
529
562
  }
@@ -594,19 +627,22 @@ export class Midy {
594
627
  }
595
628
  return bufferSource;
596
629
  }
597
- async scheduleTimelineEvents(t, resumeTime, queueIndex) {
598
- while (queueIndex < this.timeline.length) {
599
- const event = this.timeline[queueIndex];
600
- if (event.startTime > t + this.lookAhead)
630
+ async scheduleTimelineEvents(scheduleTime, queueIndex) {
631
+ const timeOffset = this.resumeTime - this.startTime;
632
+ const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
633
+ const schedulingOffset = this.startDelay - timeOffset;
634
+ const timeline = this.timeline;
635
+ while (queueIndex < timeline.length) {
636
+ const event = timeline[queueIndex];
637
+ if (lookAheadCheckTime < event.startTime)
601
638
  break;
602
- const delay = this.startDelay - resumeTime;
603
- const startTime = event.startTime + delay;
639
+ const startTime = event.startTime + schedulingOffset;
604
640
  switch (event.type) {
605
641
  case "noteOn":
606
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
642
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
607
643
  break;
608
644
  case "noteOff": {
609
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
645
+ const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
610
646
  if (notePromise)
611
647
  this.notePromises.push(notePromise);
612
648
  break;
@@ -645,6 +681,7 @@ export class Midy {
645
681
  this.exclusiveClassNotes.fill(undefined);
646
682
  this.drumExclusiveClassNotes.fill(undefined);
647
683
  this.voiceCache.clear();
684
+ this.realtimeVoiceCache.clear();
648
685
  for (let i = 0; i < this.channels.length; i++) {
649
686
  this.channels[i].scheduledNotes = [];
650
687
  this.resetChannelStates(i);
@@ -678,13 +715,17 @@ export class Midy {
678
715
  this.isPaused = false;
679
716
  this.startTime = this.audioContext.currentTime;
680
717
  let queueIndex = this.getQueueIndex(this.resumeTime);
681
- let resumeTime = this.resumeTime - this.startTime;
682
718
  let finished = false;
683
719
  this.notePromises = [];
684
720
  while (queueIndex < this.timeline.length) {
685
721
  const now = this.audioContext.currentTime;
686
- const t = now + resumeTime;
687
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
722
+ if (0 < this.lastActiveSensing &&
723
+ this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
724
+ await this.stopNotes(0, true, now);
725
+ await this.audioContext.suspend();
726
+ finished = true;
727
+ break;
728
+ }
688
729
  if (this.isPausing) {
689
730
  await this.stopNotes(0, true, now);
690
731
  await this.audioContext.suspend();
@@ -703,16 +744,17 @@ export class Midy {
703
744
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
704
745
  this.updateStates(queueIndex, nextQueueIndex);
705
746
  queueIndex = nextQueueIndex;
706
- resumeTime = this.resumeTime - this.startTime;
707
747
  this.isSeeking = false;
708
748
  continue;
709
749
  }
750
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
710
751
  const waitTime = now + this.noteCheckInterval;
711
752
  await this.scheduleTask(() => { }, waitTime);
712
753
  }
713
754
  if (finished) {
714
755
  this.notePromises = [];
715
756
  this.resetAllStates();
757
+ this.lastActiveSensing = 0;
716
758
  }
717
759
  this.isPlaying = false;
718
760
  }
@@ -722,17 +764,17 @@ export class Midy {
722
764
  secondToTicks(second, secondsPerBeat) {
723
765
  return second * this.ticksPerBeat / secondsPerBeat;
724
766
  }
767
+ getSoundFontId(channel) {
768
+ const programNumber = channel.programNumber;
769
+ const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
770
+ const bank = bankNumber.toString().padStart(3, "0");
771
+ const program = programNumber.toString().padStart(3, "0");
772
+ return `${bank}:${program}`;
773
+ }
725
774
  extractMidiData(midi) {
726
775
  const instruments = new Set();
727
776
  const timeline = [];
728
- const tmpChannels = new Array(this.channels.length);
729
- for (let i = 0; i < tmpChannels.length; i++) {
730
- tmpChannels[i] = {
731
- programNumber: -1,
732
- bankMSB: this.channels[i].bankMSB,
733
- bankLSB: this.channels[i].bankLSB,
734
- };
735
- }
777
+ const channels = this.channels;
736
778
  for (let i = 0; i < midi.tracks.length; i++) {
737
779
  const track = midi.tracks[i];
738
780
  let currentTicks = 0;
@@ -742,48 +784,40 @@ export class Midy {
742
784
  event.ticks = currentTicks;
743
785
  switch (event.type) {
744
786
  case "noteOn": {
745
- const channel = tmpChannels[event.channel];
746
- if (channel.programNumber < 0) {
747
- channel.programNumber = event.programNumber;
748
- switch (channel.bankMSB) {
749
- case 120:
750
- instruments.add(`128:0`);
751
- break;
752
- case 121:
753
- instruments.add(`${channel.bankLSB}:0`);
754
- break;
755
- default: {
756
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
757
- instruments.add(`${bankNumber}:0`);
758
- }
759
- }
760
- channel.programNumber = 0;
761
- }
787
+ const channel = channels[event.channel];
788
+ instruments.add(this.getSoundFontId(channel));
762
789
  break;
763
790
  }
764
791
  case "controller":
765
792
  switch (event.controllerType) {
766
793
  case 0:
767
- tmpChannels[event.channel].bankMSB = event.value;
794
+ this.setBankMSB(event.channel, event.value);
768
795
  break;
769
796
  case 32:
770
- tmpChannels[event.channel].bankLSB = event.value;
797
+ this.setBankLSB(event.channel, event.value);
771
798
  break;
772
799
  }
773
800
  break;
774
801
  case "programChange": {
775
- const channel = tmpChannels[event.channel];
776
- channel.programNumber = event.programNumber;
777
- switch (channel.bankMSB) {
778
- case 120:
779
- instruments.add(`128:${channel.programNumber}`);
780
- break;
781
- case 121:
782
- instruments.add(`${channel.bankLSB}:${channel.programNumber}`);
783
- break;
784
- default: {
785
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
786
- instruments.add(`${bankNumber}:${channel.programNumber}`);
802
+ const channel = channels[event.channel];
803
+ this.setProgramChange(event.channel, event.programNumber);
804
+ instruments.add(this.getSoundFontId(channel));
805
+ break;
806
+ }
807
+ case "sysEx": {
808
+ const data = event.data;
809
+ if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
810
+ switch (data[3]) {
811
+ case 1:
812
+ this.GM1SystemOn(scheduleTime);
813
+ break;
814
+ case 2: // GM System Off
815
+ break;
816
+ case 3:
817
+ this.GM2SystemOn(scheduleTime);
818
+ break;
819
+ default:
820
+ console.warn(`Unsupported Exclusive Message: ${data}`);
787
821
  }
788
822
  }
789
823
  }
@@ -822,7 +856,7 @@ export class Midy {
822
856
  const channel = this.channels[channelNumber];
823
857
  const promises = [];
824
858
  this.processActiveNotes(channel, scheduleTime, (note) => {
825
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
859
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
826
860
  this.notePromises.push(promise);
827
861
  promises.push(promise);
828
862
  });
@@ -832,7 +866,7 @@ export class Midy {
832
866
  const channel = this.channels[channelNumber];
833
867
  const promises = [];
834
868
  this.processScheduledNotes(channel, (note) => {
835
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
869
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
836
870
  this.notePromises.push(promise);
837
871
  promises.push(promise);
838
872
  });
@@ -865,7 +899,7 @@ export class Midy {
865
899
  if (!this.isPlaying || this.isPaused)
866
900
  return;
867
901
  const now = this.audioContext.currentTime;
868
- this.resumeTime += now - this.startTime - this.startDelay;
902
+ this.resumeTime = now - this.startTime - this.startDelay;
869
903
  this.isPausing = true;
870
904
  await this.playPromise;
871
905
  this.isPausing = false;
@@ -891,11 +925,13 @@ export class Midy {
891
925
  if (totalTime < event.startTime)
892
926
  totalTime = event.startTime;
893
927
  }
894
- return totalTime;
928
+ return totalTime + this.startDelay;
895
929
  }
896
930
  currentTime() {
931
+ if (!this.isPlaying)
932
+ return this.resumeTime;
897
933
  const now = this.audioContext.currentTime;
898
- return this.resumeTime + now - this.startTime - this.startDelay;
934
+ return now + this.resumeTime - this.startTime;
899
935
  }
900
936
  processScheduledNotes(channel, callback) {
901
937
  const scheduledNotes = channel.scheduledNotes;
@@ -1107,7 +1143,7 @@ export class Midy {
1107
1143
  const noteDetune = this.calcNoteDetune(channel, note);
1108
1144
  const pitchControl = this.getPitchControl(channel, note);
1109
1145
  const detune = channel.detune + noteDetune + pitchControl;
1110
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1146
+ if (this.isPortamento(channel, note)) {
1111
1147
  const startTime = note.startTime;
1112
1148
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1113
1149
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
@@ -1327,31 +1363,42 @@ export class Midy {
1327
1363
  note.vibratoLFO.connect(note.vibratoDepth);
1328
1364
  note.vibratoDepth.connect(note.bufferSource.detune);
1329
1365
  }
1330
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1366
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1331
1367
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1332
- const cache = this.voiceCache.get(audioBufferId);
1333
- if (cache) {
1334
- cache.counter += 1;
1335
- if (cache.maxCount <= cache.counter) {
1336
- this.voiceCache.delete(audioBufferId);
1337
- }
1338
- return cache.audioBuffer;
1339
- }
1340
- else {
1341
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1368
+ if (realtime) {
1369
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1370
+ if (cachedAudioBuffer)
1371
+ return cachedAudioBuffer;
1342
1372
  const audioBuffer = await this.createAudioBuffer(voiceParams);
1343
- const cache = { audioBuffer, maxCount, counter: 1 };
1344
- this.voiceCache.set(audioBufferId, cache);
1373
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1345
1374
  return audioBuffer;
1346
1375
  }
1376
+ else {
1377
+ const cache = this.voiceCache.get(audioBufferId);
1378
+ if (cache) {
1379
+ cache.counter += 1;
1380
+ if (cache.maxCount <= cache.counter) {
1381
+ this.voiceCache.delete(audioBufferId);
1382
+ }
1383
+ return cache.audioBuffer;
1384
+ }
1385
+ else {
1386
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1387
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1388
+ const cache = { audioBuffer, maxCount, counter: 1 };
1389
+ this.voiceCache.set(audioBufferId, cache);
1390
+ return audioBuffer;
1391
+ }
1392
+ }
1347
1393
  }
1348
- async createNote(channel, voice, noteNumber, velocity, startTime) {
1394
+ async setNoteAudioNode(channel, note, realtime) {
1349
1395
  const now = this.audioContext.currentTime;
1396
+ const { noteNumber, velocity, startTime } = note;
1350
1397
  const state = channel.state;
1351
1398
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1352
- const voiceParams = voice.getAllParams(controllerState);
1353
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1354
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1399
+ const voiceParams = note.voice.getAllParams(controllerState);
1400
+ note.voiceParams = voiceParams;
1401
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1355
1402
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1356
1403
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1357
1404
  const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
@@ -1363,7 +1410,7 @@ export class Midy {
1363
1410
  if (prevNote && prevNote.noteNumber !== noteNumber) {
1364
1411
  note.portamentoNoteNumber = prevNote.noteNumber;
1365
1412
  }
1366
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1413
+ if (!channel.isDrum && this.isPortamento(channel, note)) {
1367
1414
  this.setPortamentoVolumeEnvelope(channel, note, now);
1368
1415
  this.setPortamentoFilterEnvelope(channel, note, now);
1369
1416
  this.setPortamentoPitchEnvelope(note, now);
@@ -1391,22 +1438,6 @@ export class Midy {
1391
1438
  note.bufferSource.start(startTime);
1392
1439
  return note;
1393
1440
  }
1394
- calcBank(channel) {
1395
- switch (this.mode) {
1396
- case "GM1":
1397
- if (channel.isDrum)
1398
- return 128;
1399
- return 0;
1400
- case "GM2":
1401
- if (channel.bankMSB === 121)
1402
- return 0;
1403
- if (channel.isDrum)
1404
- return 128;
1405
- return channel.bank;
1406
- default:
1407
- return channel.bank;
1408
- }
1409
- }
1410
1441
  handleExclusiveClass(note, channelNumber, startTime) {
1411
1442
  const exclusiveClass = note.voiceParams.exclusiveClass;
1412
1443
  if (exclusiveClass === 0)
@@ -1415,7 +1446,7 @@ export class Midy {
1415
1446
  if (prev) {
1416
1447
  const [prevNote, prevChannelNumber] = prev;
1417
1448
  if (prevNote && !prevNote.ending) {
1418
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1449
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1419
1450
  startTime, true);
1420
1451
  }
1421
1452
  }
@@ -1435,23 +1466,14 @@ export class Midy {
1435
1466
  channelNumber;
1436
1467
  const prevNote = this.drumExclusiveClassNotes[index];
1437
1468
  if (prevNote && !prevNote.ending) {
1438
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1469
+ this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1439
1470
  startTime, true);
1440
1471
  }
1441
1472
  this.drumExclusiveClassNotes[index] = note;
1442
1473
  }
1443
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1474
+ setNoteRouting(channelNumber, note, startTime) {
1444
1475
  const channel = this.channels[channelNumber];
1445
- const bankNumber = this.calcBank(channel, channelNumber);
1446
- const soundFontIndex = this.soundFontTable[channel.programNumber]
1447
- .get(bankNumber);
1448
- if (soundFontIndex === undefined)
1449
- return;
1450
- const soundFont = this.soundFonts[soundFontIndex];
1451
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1452
- if (!voice)
1453
- return;
1454
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1476
+ const { noteNumber, volumeEnvelopeNode } = note;
1455
1477
  if (channel.isDrum) {
1456
1478
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1457
1479
  let gainL = keyBasedGainLs[noteNumber];
@@ -1461,25 +1483,48 @@ export class Midy {
1461
1483
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1462
1484
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1463
1485
  }
1464
- note.volumeEnvelopeNode.connect(gainL);
1465
- note.volumeEnvelopeNode.connect(gainR);
1486
+ volumeEnvelopeNode.connect(gainL);
1487
+ volumeEnvelopeNode.connect(gainR);
1466
1488
  }
1467
1489
  else {
1468
- note.volumeEnvelopeNode.connect(channel.gainL);
1469
- note.volumeEnvelopeNode.connect(channel.gainR);
1490
+ volumeEnvelopeNode.connect(channel.gainL);
1491
+ volumeEnvelopeNode.connect(channel.gainR);
1470
1492
  }
1471
1493
  if (0.5 <= channel.state.sustainPedal) {
1472
1494
  channel.sustainNotes.push(note);
1473
1495
  }
1474
1496
  this.handleExclusiveClass(note, channelNumber, startTime);
1475
1497
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1498
+ }
1499
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
1500
+ const channel = this.channels[channelNumber];
1501
+ const realtime = startTime === undefined;
1502
+ if (realtime)
1503
+ startTime = this.audioContext.currentTime;
1504
+ const note = new Note(noteNumber, velocity, startTime);
1476
1505
  const scheduledNotes = channel.scheduledNotes;
1477
1506
  note.index = scheduledNotes.length;
1478
1507
  scheduledNotes.push(note);
1479
- }
1480
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1481
- scheduleTime ??= this.audioContext.currentTime;
1482
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1508
+ const programNumber = channel.programNumber;
1509
+ const bankTable = this.soundFontTable[programNumber];
1510
+ if (!bankTable)
1511
+ return;
1512
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1513
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1514
+ const soundFontIndex = bankTable[bank];
1515
+ if (soundFontIndex === undefined)
1516
+ return;
1517
+ const soundFont = this.soundFonts[soundFontIndex];
1518
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1519
+ if (!note.voice)
1520
+ return;
1521
+ await this.setNoteAudioNode(channel, note, realtime);
1522
+ this.setNoteRouting(channelNumber, note, startTime);
1523
+ note.pending = false;
1524
+ const off = note.offEvent;
1525
+ if (off) {
1526
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1527
+ }
1483
1528
  }
1484
1529
  disconnectNote(note) {
1485
1530
  note.bufferSource.disconnect();
@@ -1502,6 +1547,7 @@ export class Midy {
1502
1547
  }
1503
1548
  }
1504
1549
  releaseNote(channel, note, endTime) {
1550
+ endTime ??= this.audioContext.currentTime;
1505
1551
  const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1506
1552
  const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1507
1553
  const modRelease = endTime + note.voiceParams.modRelease;
@@ -1523,7 +1569,7 @@ export class Midy {
1523
1569
  }, stopTime);
1524
1570
  });
1525
1571
  }
1526
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1572
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1527
1573
  const channel = this.channels[channelNumber];
1528
1574
  const state = channel.state;
1529
1575
  if (!force) {
@@ -1542,6 +1588,10 @@ export class Midy {
1542
1588
  if (index < 0)
1543
1589
  return;
1544
1590
  const note = channel.scheduledNotes[index];
1591
+ if (note.pending) {
1592
+ note.offEvent = { velocity, startTime: endTime };
1593
+ return;
1594
+ }
1545
1595
  note.ending = true;
1546
1596
  this.setNoteIndex(channel, index);
1547
1597
  this.releaseNote(channel, note, endTime);
@@ -1572,16 +1622,12 @@ export class Midy {
1572
1622
  }
1573
1623
  return -1;
1574
1624
  }
1575
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1576
- scheduleTime ??= this.audioContext.currentTime;
1577
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1578
- }
1579
1625
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1580
1626
  const velocity = halfVelocity * 2;
1581
1627
  const channel = this.channels[channelNumber];
1582
1628
  const promises = [];
1583
1629
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1584
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1630
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1585
1631
  promises.push(promise);
1586
1632
  }
1587
1633
  channel.sustainNotes = [];
@@ -1595,33 +1641,48 @@ export class Midy {
1595
1641
  channel.state.sostenutoPedal = 0;
1596
1642
  for (let i = 0; i < sostenutoNotes.length; i++) {
1597
1643
  const note = sostenutoNotes[i];
1598
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1644
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1599
1645
  promises.push(promise);
1600
1646
  }
1601
1647
  channel.sostenutoNotes = [];
1602
1648
  return promises;
1603
1649
  }
1604
- handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1605
- const channelNumber = statusByte & 0x0F;
1606
- const messageType = statusByte & 0xF0;
1607
- switch (messageType) {
1608
- case 0x80:
1609
- return this.noteOff(channelNumber, data1, data2, scheduleTime);
1610
- case 0x90:
1611
- return this.noteOn(channelNumber, data1, data2, scheduleTime);
1612
- case 0xA0:
1613
- return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1614
- case 0xB0:
1615
- return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1616
- case 0xC0:
1617
- return this.setProgramChange(channelNumber, data1, scheduleTime);
1618
- case 0xD0:
1619
- return this.setChannelPressure(channelNumber, data1, scheduleTime);
1620
- case 0xE0:
1621
- return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1622
- default:
1623
- console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1650
+ createMessageHandlers() {
1651
+ const handlers = new Array(256);
1652
+ // Channel Message
1653
+ handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
1654
+ handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
1655
+ handlers[0xA0] = (data, scheduleTime) => this.setPolyphonicKeyPressure(data[0] & 0x0F, data[1], data[2], scheduleTime);
1656
+ handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
1657
+ handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
1658
+ handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
1659
+ handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
1660
+ // System Common Message
1661
+ // handlers[0xF1] = (_data, _scheduleTime) => {}; // MTC Quarter Frame
1662
+ // handlers[0xF2] = (_data, _scheduleTime) => {}; // Song Position Pointer
1663
+ // handlers[0xF3] = (_data, _scheduleTime) => {}; // Song Select
1664
+ // handlers[0xF6] = (_data, _scheduleTime) => {}; // Tune Request
1665
+ // handlers[0xF7] = (_data, _scheduleTime) => {}; // End of Exclusive (EOX)
1666
+ // System Real Time Message
1667
+ // handlers[0xF8] = (_data, _scheduleTime) => {}; // Timing Clock
1668
+ // handlers[0xFA] = (_data, _scheduleTime) => {}; // Start
1669
+ // handlers[0xFB] = (_data, _scheduleTime) => {}; // Continue
1670
+ // handlers[0xFC] = (_data, _scheduleTime) => {}; // Stop
1671
+ handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
1672
+ // handlers[0xFF] = (_data, _scheduleTime) => {}; // Reset
1673
+ return handlers;
1674
+ }
1675
+ handleMessage(data, scheduleTime) {
1676
+ const status = data[0];
1677
+ if (status === 0xF0) {
1678
+ return this.handleSysEx(data.subarray(1), scheduleTime);
1624
1679
  }
1680
+ const handler = this.messageHandlers[status];
1681
+ if (handler)
1682
+ handler(data, scheduleTime);
1683
+ }
1684
+ activeSensing() {
1685
+ this.lastActiveSensing = performance.now();
1625
1686
  }
1626
1687
  setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1627
1688
  const channel = this.channels[channelNumber];
@@ -1636,19 +1697,18 @@ export class Midy {
1636
1697
  }
1637
1698
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1638
1699
  const channel = this.channels[channelNumber];
1639
- channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1640
1700
  channel.programNumber = programNumber;
1641
1701
  if (this.mode === "GM2") {
1642
1702
  switch (channel.bankMSB) {
1643
1703
  case 120:
1644
1704
  channel.isDrum = true;
1705
+ channel.keyBasedTable.fill(-1);
1645
1706
  break;
1646
1707
  case 121:
1647
1708
  channel.isDrum = false;
1648
1709
  break;
1649
1710
  }
1650
1711
  }
1651
- channel.keyBasedTable.fill(-1);
1652
1712
  }
1653
1713
  setChannelPressure(channelNumber, value, scheduleTime) {
1654
1714
  const channel = this.channels[channelNumber];
@@ -1985,22 +2045,20 @@ export class Midy {
1985
2045
  this.updateModulation(channel, scheduleTime);
1986
2046
  }
1987
2047
  updatePortamento(channel, scheduleTime) {
2048
+ if (channel.isDrum)
2049
+ return;
1988
2050
  this.processScheduledNotes(channel, (note) => {
1989
- if (0.5 <= channel.state.portamento) {
1990
- if (0 <= note.portamentoNoteNumber) {
1991
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1992
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1993
- this.setPortamentoPitchEnvelope(note, scheduleTime);
1994
- this.updateDetune(channel, note, scheduleTime);
1995
- }
2051
+ if (this.isPortamento(channel, note)) {
2052
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2053
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2054
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
2055
+ this.updateDetune(channel, note, scheduleTime);
1996
2056
  }
1997
2057
  else {
1998
- if (0 <= note.portamentoNoteNumber) {
1999
- this.setVolumeEnvelope(channel, note, scheduleTime);
2000
- this.setFilterEnvelope(channel, note, scheduleTime);
2001
- this.setPitchEnvelope(note, scheduleTime);
2002
- this.updateDetune(channel, note, scheduleTime);
2003
- }
2058
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2059
+ this.setFilterEnvelope(channel, note, scheduleTime);
2060
+ this.setPitchEnvelope(note, scheduleTime);
2061
+ this.updateDetune(channel, note, scheduleTime);
2004
2062
  }
2005
2063
  });
2006
2064
  }
@@ -2106,6 +2164,9 @@ export class Midy {
2106
2164
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2107
2165
  }
2108
2166
  }
2167
+ isPortamento(channel, note) {
2168
+ return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
2169
+ }
2109
2170
  setPortamento(channelNumber, value, scheduleTime) {
2110
2171
  const channel = this.channels[channelNumber];
2111
2172
  if (channel.isDrum)
@@ -2142,7 +2203,7 @@ export class Midy {
2142
2203
  scheduleTime ??= this.audioContext.currentTime;
2143
2204
  state.softPedal = softPedal / 127;
2144
2205
  this.processScheduledNotes(channel, (note) => {
2145
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2206
+ if (this.isPortamento(channel, note)) {
2146
2207
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2147
2208
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2148
2209
  }
@@ -2200,7 +2261,7 @@ export class Midy {
2200
2261
  scheduleTime ??= this.audioContext.currentTime;
2201
2262
  state.brightness = brightness / 127;
2202
2263
  this.processScheduledNotes(channel, (note) => {
2203
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2264
+ if (this.isPortamento(channel, note)) {
2204
2265
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2205
2266
  }
2206
2267
  else {
@@ -2536,11 +2597,9 @@ export class Midy {
2536
2597
  const channel = this.channels[i];
2537
2598
  channel.bankMSB = 0;
2538
2599
  channel.bankLSB = 0;
2539
- channel.bank = 0;
2540
2600
  channel.isDrum = false;
2541
2601
  }
2542
2602
  this.channels[9].bankMSB = 1;
2543
- this.channels[9].bank = 128;
2544
2603
  this.channels[9].isDrum = true;
2545
2604
  }
2546
2605
  GM2SystemOn(scheduleTime) {
@@ -2551,11 +2610,9 @@ export class Midy {
2551
2610
  const channel = this.channels[i];
2552
2611
  channel.bankMSB = 121;
2553
2612
  channel.bankLSB = 0;
2554
- channel.bank = 121 * 128;
2555
2613
  channel.isDrum = false;
2556
2614
  }
2557
2615
  this.channels[9].bankMSB = 120;
2558
- this.channels[9].bank = 120 * 128;
2559
2616
  this.channels[9].isDrum = true;
2560
2617
  }
2561
2618
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
@@ -2613,16 +2670,11 @@ export class Midy {
2613
2670
  const volume = (data[5] * 128 + data[4]) / 16383;
2614
2671
  this.setMasterVolume(volume, scheduleTime);
2615
2672
  }
2616
- setMasterVolume(volume, scheduleTime) {
2673
+ setMasterVolume(value, scheduleTime) {
2617
2674
  scheduleTime ??= this.audioContext.currentTime;
2618
- if (volume < 0 && 1 < volume) {
2619
- console.error("Master Volume is out of range");
2620
- }
2621
- else {
2622
- this.masterVolume.gain
2623
- .cancelScheduledValues(scheduleTime)
2624
- .setValueAtTime(volume * volume, scheduleTime);
2625
- }
2675
+ this.masterVolume.gain
2676
+ .cancelScheduledValues(scheduleTime)
2677
+ .setValueAtTime(value * value, scheduleTime);
2626
2678
  }
2627
2679
  handleMasterFineTuningSysEx(data, scheduleTime) {
2628
2680
  const value = (data[5] * 128 + data[4]) / 16383;
@@ -3037,6 +3089,70 @@ export class Midy {
3037
3089
  const controlValue = channel.keyBasedTable[index];
3038
3090
  return controlValue;
3039
3091
  }
3092
+ createKeyBasedControllerHandlers() {
3093
+ const handlers = new Array(128);
3094
+ handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3095
+ handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3096
+ handlers[71] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3097
+ if (note.noteNumber === keyNumber) {
3098
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3099
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3100
+ note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3101
+ }
3102
+ });
3103
+ handlers[73] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3104
+ if (note.noteNumber === keyNumber) {
3105
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3106
+ }
3107
+ });
3108
+ handlers[74] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3109
+ if (note.noteNumber === keyNumber) {
3110
+ this.setFilterEnvelope(channel, note, scheduleTime);
3111
+ }
3112
+ });
3113
+ handlers[75] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3114
+ if (note.noteNumber === keyNumber) {
3115
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3116
+ }
3117
+ });
3118
+ handlers[76] = (channel, keyNumber, scheduleTime) => {
3119
+ if (channel.state.vibratoDepth <= 0)
3120
+ return;
3121
+ this.processScheduledNotes(channel, (note) => {
3122
+ if (note.noteNumber === keyNumber) {
3123
+ this.setFreqVibLFO(channel, note, scheduleTime);
3124
+ }
3125
+ });
3126
+ };
3127
+ handlers[77] = (channel, keyNumber, scheduleTime) => {
3128
+ if (channel.state.vibratoDepth <= 0)
3129
+ return;
3130
+ this.processScheduledNotes(channel, (note) => {
3131
+ if (note.noteNumber === keyNumber) {
3132
+ this.setVibLfoToPitch(channel, note, scheduleTime);
3133
+ }
3134
+ });
3135
+ };
3136
+ handlers[78] = (channel, keyNumber) => {
3137
+ if (channel.state.vibratoDepth <= 0)
3138
+ return;
3139
+ this.processScheduledNotes(channel, (note) => {
3140
+ if (note.noteNumber === keyNumber)
3141
+ this.setDelayVibLFO(channel, note);
3142
+ });
3143
+ };
3144
+ handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3145
+ if (note.noteNumber === keyNumber) {
3146
+ this.setReverbSend(channel, note, scheduleTime);
3147
+ }
3148
+ });
3149
+ handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3150
+ if (note.noteNumber === keyNumber) {
3151
+ this.setChorusSend(channel, note, scheduleTime);
3152
+ }
3153
+ });
3154
+ return handlers;
3155
+ }
3040
3156
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
3041
3157
  const channelNumber = data[4];
3042
3158
  const channel = this.channels[channelNumber];
@@ -3049,95 +3165,9 @@ export class Midy {
3049
3165
  const value = data[i + 1];
3050
3166
  const index = keyNumber * 128 + controllerType;
3051
3167
  table[index] = value;
3052
- switch (controllerType) {
3053
- case 7:
3054
- case 10:
3055
- this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3056
- break;
3057
- case 71:
3058
- this.processScheduledNotes(channel, (note) => {
3059
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3060
- if (note.noteNumber === keyNumber) {
3061
- const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3062
- note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3063
- }
3064
- });
3065
- break;
3066
- case 73:
3067
- this.processScheduledNotes(channel, (note) => {
3068
- if (note.noteNumber === keyNumber) {
3069
- if (0.5 <= channel.state.portamento &&
3070
- 0 <= note.portamentoNoteNumber) {
3071
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
3072
- }
3073
- else {
3074
- this.setVolumeEnvelope(channel, note, scheduleTime);
3075
- }
3076
- }
3077
- });
3078
- break;
3079
- case 74:
3080
- this.processScheduledNotes(channel, (note) => {
3081
- if (note.noteNumber === keyNumber) {
3082
- if (0.5 <= channel.state.portamento &&
3083
- 0 <= note.portamentoNoteNumber) {
3084
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
3085
- }
3086
- else {
3087
- this.setFilterEnvelope(channel, note, scheduleTime);
3088
- }
3089
- }
3090
- });
3091
- break;
3092
- case 75:
3093
- this.processScheduledNotes(channel, (note) => {
3094
- if (note.noteNumber === keyNumber) {
3095
- this.setVolumeEnvelope(channel, note, scheduleTime);
3096
- }
3097
- });
3098
- break;
3099
- case 76:
3100
- if (channel.state.vibratoDepth <= 0)
3101
- break;
3102
- this.processScheduledNotes(channel, (note) => {
3103
- if (note.noteNumber === keyNumber) {
3104
- this.setFreqVibLFO(channel, note, scheduleTime);
3105
- }
3106
- });
3107
- break;
3108
- case 77:
3109
- if (channel.state.vibratoDepth <= 0)
3110
- break;
3111
- this.processScheduledNotes(channel, (note) => {
3112
- if (note.noteNumber === keyNumber) {
3113
- this.setVibLfoToPitch(channel, note, scheduleTime);
3114
- }
3115
- });
3116
- break;
3117
- case 78:
3118
- if (channel.state.vibratoDepth <= 0)
3119
- break;
3120
- this.processScheduledNotes(channel, (note) => {
3121
- if (note.noteNumber === keyNumber) {
3122
- this.setDelayVibLFO(channel, note);
3123
- }
3124
- });
3125
- break;
3126
- case 91:
3127
- this.processScheduledNotes(channel, (note) => {
3128
- if (note.noteNumber === keyNumber) {
3129
- this.setReverbSend(channel, note, scheduleTime);
3130
- }
3131
- });
3132
- break;
3133
- case 93:
3134
- this.processScheduledNotes(channel, (note) => {
3135
- if (note.noteNumber === keyNumber) {
3136
- this.setChorusSend(channel, note, scheduleTime);
3137
- }
3138
- });
3139
- break;
3140
- }
3168
+ const handler = this.keyBasedControllerHandlers[controllerType];
3169
+ if (handler)
3170
+ handler(channel, keyNumber, scheduleTime);
3141
3171
  }
3142
3172
  }
3143
3173
  handleSysEx(data, scheduleTime) {
@@ -3178,7 +3208,6 @@ Object.defineProperty(Midy, "channelSettings", {
3178
3208
  scheduleIndex: 0,
3179
3209
  detune: 0,
3180
3210
  programNumber: 0,
3181
- bank: 121 * 128,
3182
3211
  bankMSB: 121,
3183
3212
  bankLSB: 0,
3184
3213
  dataMSB: 0,