@marmooo/midy 0.2.4 → 0.2.6

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.
@@ -3,6 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGM2 = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
+ // 2-3 times faster than Map
7
+ class SparseMap {
8
+ constructor(size) {
9
+ this.data = new Array(size);
10
+ this.activeIndices = [];
11
+ }
12
+ set(key, value) {
13
+ if (this.data[key] === undefined) {
14
+ this.activeIndices.push(key);
15
+ }
16
+ this.data[key] = value;
17
+ }
18
+ get(key) {
19
+ return this.data[key];
20
+ }
21
+ delete(key) {
22
+ if (this.data[key] !== undefined) {
23
+ this.data[key] = undefined;
24
+ const index = this.activeIndices.indexOf(key);
25
+ if (index !== -1) {
26
+ this.activeIndices.splice(index, 1);
27
+ }
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ has(key) {
33
+ return this.data[key] !== undefined;
34
+ }
35
+ get size() {
36
+ return this.activeIndices.length;
37
+ }
38
+ clear() {
39
+ for (let i = 0; i < this.activeIndices.length; i++) {
40
+ const key = this.activeIndices[i];
41
+ this.data[key] = undefined;
42
+ }
43
+ this.activeIndices = [];
44
+ }
45
+ *[Symbol.iterator]() {
46
+ for (let i = 0; i < this.activeIndices.length; i++) {
47
+ const key = this.activeIndices[i];
48
+ yield [key, this.data[key]];
49
+ }
50
+ }
51
+ forEach(callback) {
52
+ for (let i = 0; i < this.activeIndices.length; i++) {
53
+ const key = this.activeIndices[i];
54
+ callback(this.data[key], key, this);
55
+ }
56
+ }
57
+ }
6
58
  class Note {
7
59
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
60
  Object.defineProperty(this, "bufferSource", {
@@ -17,31 +69,37 @@ class Note {
17
69
  writable: true,
18
70
  value: void 0
19
71
  });
72
+ Object.defineProperty(this, "filterDepth", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: void 0
77
+ });
20
78
  Object.defineProperty(this, "volumeEnvelopeNode", {
21
79
  enumerable: true,
22
80
  configurable: true,
23
81
  writable: true,
24
82
  value: void 0
25
83
  });
26
- Object.defineProperty(this, "volumeNode", {
84
+ Object.defineProperty(this, "volumeDepth", {
27
85
  enumerable: true,
28
86
  configurable: true,
29
87
  writable: true,
30
88
  value: void 0
31
89
  });
32
- Object.defineProperty(this, "gainL", {
90
+ Object.defineProperty(this, "volumeNode", {
33
91
  enumerable: true,
34
92
  configurable: true,
35
93
  writable: true,
36
94
  value: void 0
37
95
  });
38
- Object.defineProperty(this, "gainR", {
96
+ Object.defineProperty(this, "gainL", {
39
97
  enumerable: true,
40
98
  configurable: true,
41
99
  writable: true,
42
100
  value: void 0
43
101
  });
44
- Object.defineProperty(this, "volumeDepth", {
102
+ Object.defineProperty(this, "gainR", {
45
103
  enumerable: true,
46
104
  configurable: true,
47
105
  writable: true,
@@ -283,6 +341,18 @@ class MidyGM2 {
283
341
  writable: true,
284
342
  value: this.initSoundFontTable()
285
343
  });
344
+ Object.defineProperty(this, "audioBufferCounter", {
345
+ enumerable: true,
346
+ configurable: true,
347
+ writable: true,
348
+ value: new Map()
349
+ });
350
+ Object.defineProperty(this, "audioBufferCache", {
351
+ enumerable: true,
352
+ configurable: true,
353
+ writable: true,
354
+ value: new Map()
355
+ });
286
356
  Object.defineProperty(this, "isPlaying", {
287
357
  enumerable: true,
288
358
  configurable: true,
@@ -335,7 +405,7 @@ class MidyGM2 {
335
405
  enumerable: true,
336
406
  configurable: true,
337
407
  writable: true,
338
- value: new Map()
408
+ value: new SparseMap(128)
339
409
  });
340
410
  Object.defineProperty(this, "defaultOptions", {
341
411
  enumerable: true,
@@ -375,7 +445,7 @@ class MidyGM2 {
375
445
  initSoundFontTable() {
376
446
  const table = new Array(128);
377
447
  for (let i = 0; i < 128; i++) {
378
- table[i] = new Map();
448
+ table[i] = new SparseMap(128);
379
449
  }
380
450
  return table;
381
451
  }
@@ -429,8 +499,11 @@ class MidyGM2 {
429
499
  state: new ControllerState(),
430
500
  controlTable: this.initControlTable(),
431
501
  ...this.setChannelAudioNodes(audioContext),
432
- scheduledNotes: new Map(),
433
- sostenutoNotes: new Map(),
502
+ scheduledNotes: new SparseMap(128),
503
+ sostenutoNotes: new SparseMap(128),
504
+ scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
505
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
506
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
434
507
  };
435
508
  });
436
509
  return channels;
@@ -464,9 +537,8 @@ class MidyGM2 {
464
537
  return audioBuffer;
465
538
  }
466
539
  }
467
- async createNoteBufferNode(voiceParams, isSF3) {
540
+ createNoteBufferNode(audioBuffer, voiceParams) {
468
541
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
469
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
470
542
  bufferSource.buffer = audioBuffer;
471
543
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
472
544
  if (bufferSource.loop) {
@@ -498,10 +570,11 @@ class MidyGM2 {
498
570
  const event = this.timeline[queueIndex];
499
571
  if (event.startTime > t + this.lookAhead)
500
572
  break;
573
+ const startTime = event.startTime + this.startDelay - offset;
501
574
  switch (event.type) {
502
575
  case "noteOn":
503
576
  if (event.velocity !== 0) {
504
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
577
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
505
578
  break;
506
579
  }
507
580
  /* falls through */
@@ -509,26 +582,26 @@ class MidyGM2 {
509
582
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
510
583
  if (portamentoTarget)
511
584
  portamentoTarget.portamento = true;
512
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
585
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, portamentoTarget?.noteNumber, false);
513
586
  if (notePromise) {
514
587
  this.notePromises.push(notePromise);
515
588
  }
516
589
  break;
517
590
  }
518
591
  case "controller":
519
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
592
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
520
593
  break;
521
594
  case "programChange":
522
- this.handleProgramChange(event.channel, event.programNumber);
595
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
523
596
  break;
524
597
  case "channelAftertouch":
525
- this.handleChannelPressure(event.channel, event.amount);
598
+ this.handleChannelPressure(event.channel, event.amount, startTime);
526
599
  break;
527
600
  case "pitchBend":
528
- this.setPitchBend(event.channel, event.value + 8192);
601
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
529
602
  break;
530
603
  case "sysEx":
531
- this.handleSysEx(event.data);
604
+ this.handleSysEx(event.data, startTime);
532
605
  }
533
606
  queueIndex++;
534
607
  }
@@ -555,6 +628,7 @@ class MidyGM2 {
555
628
  await Promise.all(this.notePromises);
556
629
  this.notePromises = [];
557
630
  this.exclusiveClassMap.clear();
631
+ this.audioBufferCache.clear();
558
632
  resolve();
559
633
  return;
560
634
  }
@@ -570,8 +644,9 @@ class MidyGM2 {
570
644
  }
571
645
  else if (this.isStopping) {
572
646
  await this.stopNotes(0, true);
573
- this.exclusiveClassMap.clear();
574
647
  this.notePromises = [];
648
+ this.exclusiveClassMap.clear();
649
+ this.audioBufferCache.clear();
575
650
  resolve();
576
651
  this.isStopping = false;
577
652
  this.isPaused = false;
@@ -602,6 +677,9 @@ class MidyGM2 {
602
677
  secondToTicks(second, secondsPerBeat) {
603
678
  return second * this.ticksPerBeat / secondsPerBeat;
604
679
  }
680
+ getAudioBufferId(programNumber, noteNumber, velocity) {
681
+ return `${programNumber}:${noteNumber}:${velocity}`;
682
+ }
605
683
  extractMidiData(midi) {
606
684
  const instruments = new Set();
607
685
  const timeline = [];
@@ -623,6 +701,8 @@ class MidyGM2 {
623
701
  switch (event.type) {
624
702
  case "noteOn": {
625
703
  const channel = tmpChannels[event.channel];
704
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
705
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
626
706
  if (channel.programNumber < 0) {
627
707
  channel.programNumber = event.programNumber;
628
708
  switch (channel.bankMSB) {
@@ -672,6 +752,10 @@ class MidyGM2 {
672
752
  timeline.push(event);
673
753
  }
674
754
  }
755
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
756
+ if (count === 1)
757
+ this.audioBufferCounter.delete(audioBufferId);
758
+ }
675
759
  const priority = {
676
760
  controller: 0,
677
761
  sysEx: 1,
@@ -764,8 +848,20 @@ class MidyGM2 {
764
848
  const now = this.audioContext.currentTime;
765
849
  return this.resumeTime + now - this.startTime - this.startDelay;
766
850
  }
851
+ processScheduledNotes(channel, scheduleTime, callback) {
852
+ channel.scheduledNotes.forEach((noteList) => {
853
+ for (let i = 0; i < noteList.length; i++) {
854
+ const note = noteList[i];
855
+ if (!note)
856
+ continue;
857
+ if (scheduleTime < note.startTime)
858
+ continue;
859
+ callback(note);
860
+ }
861
+ });
862
+ }
767
863
  getActiveNotes(channel, time) {
768
- const activeNotes = new Map();
864
+ const activeNotes = new SparseMap(128);
769
865
  channel.scheduledNotes.forEach((noteList) => {
770
866
  const activeNote = this.getActiveNote(noteList, time);
771
867
  if (activeNote) {
@@ -945,14 +1041,14 @@ class MidyGM2 {
945
1041
  const note = noteList[i];
946
1042
  if (!note)
947
1043
  continue;
948
- this.updateDetune(channel, note, 0);
1044
+ this.updateDetune(channel, note);
949
1045
  }
950
1046
  });
951
1047
  }
952
- updateDetune(channel, note, pressure) {
1048
+ updateDetune(channel, note) {
953
1049
  const now = this.audioContext.currentTime;
954
1050
  const noteDetune = this.calcNoteDetune(channel, note);
955
- const detune = channel.detune + noteDetune + pressure;
1051
+ const detune = channel.detune + noteDetune;
956
1052
  note.bufferSource.detune
957
1053
  .cancelScheduledValues(now)
958
1054
  .setValueAtTime(detune, now);
@@ -974,11 +1070,11 @@ class MidyGM2 {
974
1070
  .setValueAtTime(0, volDelay)
975
1071
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
976
1072
  }
977
- setVolumeEnvelope(note, pressure) {
1073
+ setVolumeEnvelope(channel, note) {
978
1074
  const now = this.audioContext.currentTime;
979
1075
  const { voiceParams, startTime } = note;
980
1076
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
981
- (1 + pressure);
1077
+ (1 + this.getAmplitudeControl(channel));
982
1078
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
983
1079
  const volDelay = startTime + voiceParams.volDelay;
984
1080
  const volAttack = volDelay + voiceParams.volAttack;
@@ -992,20 +1088,20 @@ class MidyGM2 {
992
1088
  .setValueAtTime(attackVolume, volHold)
993
1089
  .linearRampToValueAtTime(sustainVolume, volDecay);
994
1090
  }
995
- setPitchEnvelope(note) {
996
- const now = this.audioContext.currentTime;
1091
+ setPitchEnvelope(note, scheduleTime) {
1092
+ scheduleTime ??= this.audioContext.currentTime;
997
1093
  const { voiceParams } = note;
998
1094
  const baseRate = voiceParams.playbackRate;
999
1095
  note.bufferSource.playbackRate
1000
- .cancelScheduledValues(now)
1001
- .setValueAtTime(baseRate, now);
1096
+ .cancelScheduledValues(scheduleTime)
1097
+ .setValueAtTime(baseRate, scheduleTime);
1002
1098
  const modEnvToPitch = voiceParams.modEnvToPitch;
1003
1099
  if (modEnvToPitch === 0)
1004
1100
  return;
1005
1101
  const basePitch = this.rateToCent(baseRate);
1006
1102
  const peekPitch = basePitch + modEnvToPitch;
1007
1103
  const peekRate = this.centToRate(peekPitch);
1008
- const modDelay = startTime + voiceParams.modDelay;
1104
+ const modDelay = note.startTime + voiceParams.modDelay;
1009
1105
  const modAttack = modDelay + voiceParams.modAttack;
1010
1106
  const modHold = modAttack + voiceParams.modHold;
1011
1107
  const modDecay = modHold + voiceParams.modDecay;
@@ -1041,13 +1137,14 @@ class MidyGM2 {
1041
1137
  .setValueAtTime(adjustedBaseFreq, modDelay)
1042
1138
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1043
1139
  }
1044
- setFilterEnvelope(channel, note, pressure) {
1140
+ setFilterEnvelope(channel, note) {
1045
1141
  const now = this.audioContext.currentTime;
1046
1142
  const state = channel.state;
1047
1143
  const { voiceParams, noteNumber, startTime } = note;
1048
1144
  const softPedalFactor = 1 -
1049
1145
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1050
- const baseCent = voiceParams.initialFilterFc + pressure;
1146
+ const baseCent = voiceParams.initialFilterFc +
1147
+ this.getFilterCutoffControl(channel);
1051
1148
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1052
1149
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1053
1150
  softPedalFactor;
@@ -1077,9 +1174,9 @@ class MidyGM2 {
1077
1174
  gain: voiceParams.modLfoToFilterFc,
1078
1175
  });
1079
1176
  note.modulationDepth = new GainNode(this.audioContext);
1080
- this.setModLfoToPitch(channel, note, 0);
1177
+ this.setModLfoToPitch(channel, note);
1081
1178
  note.volumeDepth = new GainNode(this.audioContext);
1082
- this.setModLfoToVolume(note, 0);
1179
+ this.setModLfoToVolume(channel, note);
1083
1180
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1084
1181
  note.modulationLFO.connect(note.filterDepth);
1085
1182
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1100,12 +1197,31 @@ class MidyGM2 {
1100
1197
  note.vibratoLFO.connect(note.vibratoDepth);
1101
1198
  note.vibratoDepth.connect(note.bufferSource.detune);
1102
1199
  }
1200
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1201
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1202
+ const cache = this.audioBufferCache.get(audioBufferId);
1203
+ if (cache) {
1204
+ cache.counter += 1;
1205
+ if (cache.maxCount <= cache.counter) {
1206
+ this.audioBufferCache.delete(audioBufferId);
1207
+ }
1208
+ return cache.audioBuffer;
1209
+ }
1210
+ else {
1211
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1212
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1213
+ const cache = { audioBuffer, maxCount, counter: 1 };
1214
+ this.audioBufferCache.set(audioBufferId, cache);
1215
+ return audioBuffer;
1216
+ }
1217
+ }
1103
1218
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1104
1219
  const state = channel.state;
1105
1220
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1106
1221
  const voiceParams = voice.getAllParams(controllerState);
1107
1222
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1108
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1223
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1224
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1109
1225
  note.volumeNode = new GainNode(this.audioContext);
1110
1226
  note.gainL = new GainNode(this.audioContext);
1111
1227
  note.gainR = new GainNode(this.audioContext);
@@ -1121,8 +1237,8 @@ class MidyGM2 {
1121
1237
  }
1122
1238
  else {
1123
1239
  note.portamento = false;
1124
- this.setVolumeEnvelope(note, 0);
1125
- this.setFilterEnvelope(channel, note, 0);
1240
+ this.setVolumeEnvelope(channel, note);
1241
+ this.setFilterEnvelope(channel, note);
1126
1242
  }
1127
1243
  if (0 < state.vibratoDepth) {
1128
1244
  this.startVibrato(channel, note, startTime);
@@ -1165,10 +1281,10 @@ class MidyGM2 {
1165
1281
  if (soundFontIndex === undefined)
1166
1282
  return;
1167
1283
  const soundFont = this.soundFonts[soundFontIndex];
1168
- const isSF3 = soundFont.parsed.info.version.major === 3;
1169
1284
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1170
1285
  if (!voice)
1171
1286
  return;
1287
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1172
1288
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1173
1289
  note.gainL.connect(channel.gainL);
1174
1290
  note.gainR.connect(channel.gainR);
@@ -1336,7 +1452,9 @@ class MidyGM2 {
1336
1452
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1337
1453
  channel.program = program;
1338
1454
  }
1339
- handleChannelPressure(channelNumber, value) {
1455
+ handleChannelPressure(channelNumber, value, startTime) {
1456
+ if (!startTime)
1457
+ startTime = this.audioContext.currentTime;
1340
1458
  const channel = this.channels[channelNumber];
1341
1459
  const prev = channel.state.channelPressure;
1342
1460
  const next = value / 127;
@@ -1346,13 +1464,8 @@ class MidyGM2 {
1346
1464
  channel.detune += pressureDepth * (next - prev);
1347
1465
  }
1348
1466
  const table = channel.channelPressureTable;
1349
- channel.scheduledNotes.forEach((noteList) => {
1350
- for (let i = 0; i < noteList.length; i++) {
1351
- const note = noteList[i];
1352
- if (!note)
1353
- continue;
1354
- this.applyDestinationSettings(channel, note, table);
1355
- }
1467
+ this.getActiveNotes(channel, startTime).forEach((note) => {
1468
+ this.setControllerParameters(channel, note, table);
1356
1469
  });
1357
1470
  // this.applyVoiceParams(channel, 13);
1358
1471
  }
@@ -1370,9 +1483,10 @@ class MidyGM2 {
1370
1483
  this.updateChannelDetune(channel);
1371
1484
  this.applyVoiceParams(channel, 14);
1372
1485
  }
1373
- setModLfoToPitch(channel, note, pressure) {
1486
+ setModLfoToPitch(channel, note) {
1374
1487
  const now = this.audioContext.currentTime;
1375
- const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1488
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1489
+ this.getLFOPitchDepth(channel);
1376
1490
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1377
1491
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1378
1492
  note.modulationDepth.gain
@@ -1389,18 +1503,20 @@ class MidyGM2 {
1389
1503
  .cancelScheduledValues(now)
1390
1504
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1391
1505
  }
1392
- setModLfoToFilterFc(note, pressure) {
1506
+ setModLfoToFilterFc(channel, note) {
1393
1507
  const now = this.audioContext.currentTime;
1394
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1508
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1509
+ this.getLFOFilterDepth(channel);
1395
1510
  note.filterDepth.gain
1396
1511
  .cancelScheduledValues(now)
1397
1512
  .setValueAtTime(modLfoToFilterFc, now);
1398
1513
  }
1399
- setModLfoToVolume(note, pressure) {
1514
+ setModLfoToVolume(channel, note) {
1400
1515
  const now = this.audioContext.currentTime;
1401
1516
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1402
1517
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1403
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1518
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1519
+ (1 + this.getLFOAmplitudeDepth(channel));
1404
1520
  note.volumeDepth.gain
1405
1521
  .cancelScheduledValues(now)
1406
1522
  .setValueAtTime(volumeDepth, now);
@@ -1484,7 +1600,7 @@ class MidyGM2 {
1484
1600
  return {
1485
1601
  modLfoToPitch: (channel, note, _prevValue) => {
1486
1602
  if (0 < channel.state.modulationDepth) {
1487
- this.setModLfoToPitch(channel, note, 0);
1603
+ this.setModLfoToPitch(channel, note);
1488
1604
  }
1489
1605
  },
1490
1606
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1494,12 +1610,12 @@ class MidyGM2 {
1494
1610
  },
1495
1611
  modLfoToFilterFc: (channel, note, _prevValue) => {
1496
1612
  if (0 < channel.state.modulationDepth) {
1497
- this.setModLfoToFilterFc(note, 0);
1613
+ this.setModLfoToFilterFc(channel, note);
1498
1614
  }
1499
1615
  },
1500
1616
  modLfoToVolume: (channel, note, _prevValue) => {
1501
1617
  if (0 < channel.state.modulationDepth) {
1502
- this.setModLfoToVolume(note, 0);
1618
+ this.setModLfoToVolume(channel, note);
1503
1619
  }
1504
1620
  },
1505
1621
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1569,7 +1685,7 @@ class MidyGM2 {
1569
1685
  this.setPortamentoStartFilterEnvelope(channel, note);
1570
1686
  }
1571
1687
  else {
1572
- this.setFilterEnvelope(channel, note, 0);
1688
+ this.setFilterEnvelope(channel, note);
1573
1689
  }
1574
1690
  this.setPitchEnvelope(note);
1575
1691
  }
@@ -1583,7 +1699,7 @@ class MidyGM2 {
1583
1699
  if (key in voiceParams)
1584
1700
  noteVoiceParams[key] = voiceParams[key];
1585
1701
  }
1586
- this.setVolumeEnvelope(note, 0);
1702
+ this.setVolumeEnvelope(channel, note);
1587
1703
  }
1588
1704
  }
1589
1705
  }
@@ -1617,10 +1733,10 @@ class MidyGM2 {
1617
1733
  127: this.polyOn,
1618
1734
  };
1619
1735
  }
1620
- handleControlChange(channelNumber, controllerType, value) {
1736
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1621
1737
  const handler = this.controlChangeHandlers[controllerType];
1622
1738
  if (handler) {
1623
- handler.call(this, channelNumber, value);
1739
+ handler.call(this, channelNumber, value, startTime);
1624
1740
  const channel = this.channels[channelNumber];
1625
1741
  this.applyVoiceParams(channel, controllerType + 128);
1626
1742
  this.applyControlTable(channel, controllerType);
@@ -1632,55 +1748,45 @@ class MidyGM2 {
1632
1748
  setBankMSB(channelNumber, msb) {
1633
1749
  this.channels[channelNumber].bankMSB = msb;
1634
1750
  }
1635
- updateModulation(channel) {
1636
- const now = this.audioContext.currentTime;
1751
+ updateModulation(channel, scheduleTime) {
1752
+ scheduleTime ??= this.audioContext.currentTime;
1637
1753
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1638
- channel.scheduledNotes.forEach((noteList) => {
1639
- for (let i = 0; i < noteList.length; i++) {
1640
- const note = noteList[i];
1641
- if (!note)
1642
- continue;
1643
- if (note.modulationDepth) {
1644
- note.modulationDepth.gain.setValueAtTime(depth, now);
1645
- }
1646
- else {
1647
- this.setPitchEnvelope(note);
1648
- this.startModulation(channel, note, now);
1649
- }
1754
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1755
+ if (note.modulationDepth) {
1756
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1757
+ }
1758
+ else {
1759
+ this.setPitchEnvelope(note, scheduleTime);
1760
+ this.startModulation(channel, note, scheduleTime);
1650
1761
  }
1651
1762
  });
1652
1763
  }
1653
- setModulationDepth(channelNumber, modulation) {
1764
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1654
1765
  const channel = this.channels[channelNumber];
1655
1766
  channel.state.modulationDepth = modulation / 127;
1656
- this.updateModulation(channel);
1767
+ this.updateModulation(channel, scheduleTime);
1657
1768
  }
1658
1769
  setPortamentoTime(channelNumber, portamentoTime) {
1659
1770
  const channel = this.channels[channelNumber];
1660
1771
  const factor = 5 * Math.log(10) / 127;
1661
1772
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1662
1773
  }
1663
- setKeyBasedVolume(channel) {
1664
- const now = this.audioContext.currentTime;
1665
- channel.scheduledNotes.forEach((noteList) => {
1666
- for (let i = 0; i < noteList.length; i++) {
1667
- const note = noteList[i];
1668
- if (!note)
1669
- continue;
1670
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1671
- if (keyBasedValue === 0)
1672
- continue;
1774
+ setKeyBasedVolume(channel, scheduleTime) {
1775
+ scheduleTime ??= this.audioContext.currentTime;
1776
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1777
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1778
+ if (keyBasedValue !== 0) {
1673
1779
  note.volumeNode.gain
1674
- .cancelScheduledValues(now)
1675
- .setValueAtTime(1 + keyBasedValue, now);
1780
+ .cancelScheduledValues(scheduleTime)
1781
+ .setValueAtTime(1 + keyBasedValue, scheduleTime);
1676
1782
  }
1677
1783
  });
1678
1784
  }
1679
- setVolume(channelNumber, volume) {
1785
+ setVolume(channelNumber, volume, scheduleTime) {
1680
1786
  const channel = this.channels[channelNumber];
1681
1787
  channel.state.volume = volume / 127;
1682
- this.updateChannelVolume(channel);
1683
- this.setKeyBasedVolume(channel);
1788
+ this.updateChannelVolume(channel, scheduleTime);
1789
+ this.setKeyBasedVolume(channel, scheduleTime);
1684
1790
  }
1685
1791
  panToGain(pan) {
1686
1792
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1689,36 +1795,31 @@ class MidyGM2 {
1689
1795
  gainRight: Math.sin(theta),
1690
1796
  };
1691
1797
  }
1692
- setKeyBasedPan(channel) {
1693
- const now = this.audioContext.currentTime;
1694
- channel.scheduledNotes.forEach((noteList) => {
1695
- for (let i = 0; i < noteList.length; i++) {
1696
- const note = noteList[i];
1697
- if (!note)
1698
- continue;
1699
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1700
- if (keyBasedValue === 0)
1701
- continue;
1798
+ setKeyBasedPan(channel, scheduleTime) {
1799
+ scheduleTime ??= this.audioContext.currentTime;
1800
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1801
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1802
+ if (keyBasedValue !== 0) {
1702
1803
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1703
1804
  note.gainL.gain
1704
- .cancelScheduledValues(now)
1705
- .setValueAtTime(gainLeft, now);
1805
+ .cancelScheduledValues(scheduleTime)
1806
+ .setValueAtTime(gainLeft, scheduleTime);
1706
1807
  note.gainR.gain
1707
- .cancelScheduledValues(now)
1708
- .setValueAtTime(gainRight, now);
1808
+ .cancelScheduledValues(scheduleTime)
1809
+ .setValueAtTime(gainRight, scheduleTime);
1709
1810
  }
1710
1811
  });
1711
1812
  }
1712
- setPan(channelNumber, pan) {
1813
+ setPan(channelNumber, pan, scheduleTime) {
1713
1814
  const channel = this.channels[channelNumber];
1714
1815
  channel.state.pan = pan / 127;
1715
- this.updateChannelVolume(channel);
1716
- this.setKeyBasedPan(channel);
1816
+ this.updateChannelVolume(channel, scheduleTime);
1817
+ this.setKeyBasedPan(channel, scheduleTime);
1717
1818
  }
1718
- setExpression(channelNumber, expression) {
1819
+ setExpression(channelNumber, expression, scheduleTime) {
1719
1820
  const channel = this.channels[channelNumber];
1720
1821
  channel.state.expression = expression / 127;
1721
- this.updateChannelVolume(channel);
1822
+ this.updateChannelVolume(channel, scheduleTime);
1722
1823
  }
1723
1824
  setBankLSB(channelNumber, lsb) {
1724
1825
  this.channels[channelNumber].bankLSB = lsb;
@@ -1753,8 +1854,7 @@ class MidyGM2 {
1753
1854
  channel.state.sostenutoPedal = value / 127;
1754
1855
  if (64 <= value) {
1755
1856
  const now = this.audioContext.currentTime;
1756
- const activeNotes = this.getActiveNotes(channel, now);
1757
- channel.sostenutoNotes = new Map(activeNotes);
1857
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1758
1858
  }
1759
1859
  else {
1760
1860
  this.releaseSostenutoPedal(channelNumber, value);
@@ -2007,7 +2107,7 @@ class MidyGM2 {
2007
2107
  switch (data[3]) {
2008
2108
  case 8:
2009
2109
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2010
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2110
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2011
2111
  default:
2012
2112
  console.warn(`Unsupported Exclusive Message: ${data}`);
2013
2113
  }
@@ -2329,8 +2429,8 @@ class MidyGM2 {
2329
2429
  }
2330
2430
  return bitmap;
2331
2431
  }
2332
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2333
- if (data.length < 18) {
2432
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2433
+ if (data.length < 19) {
2334
2434
  console.error("Data length is too short");
2335
2435
  return;
2336
2436
  }
@@ -2338,48 +2438,55 @@ class MidyGM2 {
2338
2438
  for (let i = 0; i < channelBitmap.length; i++) {
2339
2439
  if (!channelBitmap[i])
2340
2440
  continue;
2441
+ const channel = this.channels[i];
2341
2442
  for (let j = 0; j < 12; j++) {
2342
- const value = data[j + 7] - 64; // cent
2343
- this.channels[i].scaleOctaveTuningTable[j] = value;
2443
+ const centValue = data[j + 7] - 64;
2444
+ channel.scaleOctaveTuningTable[j] = centValue;
2344
2445
  }
2345
- }
2346
- }
2347
- applyDestinationSettings(channel, note, table) {
2348
- if (table[0] !== 64) {
2349
- this.updateDetune(channel, note, 0);
2350
- }
2446
+ if (realtime)
2447
+ this.updateChannelDetune(channel);
2448
+ }
2449
+ }
2450
+ getFilterCutoffControl(channel) {
2451
+ const channelPressure = (channel.channelPressureTable[1] - 64) *
2452
+ channel.state.channelPressure;
2453
+ return channelPressure * 15;
2454
+ }
2455
+ getAmplitudeControl(channel) {
2456
+ const channelPressure = channel.channelPressureTable[2] *
2457
+ channel.state.channelPressure;
2458
+ return channelPressure / 64;
2459
+ }
2460
+ getLFOPitchDepth(channel) {
2461
+ const channelPressure = channel.channelPressureTable[3] *
2462
+ channel.state.channelPressure;
2463
+ return channelPressure / 127 * 600;
2464
+ }
2465
+ getLFOFilterDepth(channel) {
2466
+ const channelPressure = channel.channelPressureTable[4] *
2467
+ channel.state.channelPressure;
2468
+ return channelPressure / 127 * 2400;
2469
+ }
2470
+ getLFOAmplitudeDepth(channel) {
2471
+ const channelPressure = channel.channelPressureTable[5] *
2472
+ channel.state.channelPressure;
2473
+ return channelPressure / 127;
2474
+ }
2475
+ setControllerParameters(channel, note, table) {
2476
+ if (table[0] !== 64)
2477
+ this.updateDetune(channel, note);
2351
2478
  if (!note.portamento) {
2352
- if (table[1] !== 64) {
2353
- const channelPressure = channel.channelPressureTable[1] *
2354
- channel.state.channelPressure;
2355
- const pressure = (channelPressure - 64) * 15;
2356
- this.setFilterEnvelope(channel, note, pressure);
2357
- }
2358
- if (table[2] !== 64) {
2359
- const channelPressure = channel.channelPressureTable[2] *
2360
- channel.state.channelPressure;
2361
- const pressure = channelPressure / 64;
2362
- this.setVolumeEnvelope(note, pressure);
2363
- }
2364
- }
2365
- if (table[3] !== 0) {
2366
- const channelPressure = channel.channelPressureTable[3] *
2367
- channel.state.channelPressure;
2368
- const pressure = channelPressure / 127 * 600;
2369
- this.setModLfoToPitch(channel, note, pressure);
2370
- }
2371
- if (table[4] !== 0) {
2372
- const channelPressure = channel.channelPressureTable[4] *
2373
- channel.state.channelPressure;
2374
- const pressure = channelPressure / 127 * 2400;
2375
- this.setModLfoToFilterFc(note, pressure);
2376
- }
2377
- if (table[5] !== 0) {
2378
- const channelPressure = channel.channelPressureTable[5] *
2379
- channel.state.channelPressure;
2380
- const pressure = channelPressure / 127;
2381
- this.setModLfoToVolume(note, pressure);
2382
- }
2479
+ if (table[1] !== 64)
2480
+ this.setFilterEnvelope(channel, note);
2481
+ if (table[2] !== 64)
2482
+ this.setVolumeEnvelope(channel, note);
2483
+ }
2484
+ if (table[3] !== 0)
2485
+ this.setModLfoToPitch(channel, note);
2486
+ if (table[4] !== 0)
2487
+ this.setModLfoToFilterFc(channel, note);
2488
+ if (table[5] !== 0)
2489
+ this.setModLfoToVolume(channel, note);
2383
2490
  }
2384
2491
  handleChannelPressureSysEx(data, tableName) {
2385
2492
  const channelNumber = data[4];
@@ -2410,7 +2517,7 @@ class MidyGM2 {
2410
2517
  const note = noteList[i];
2411
2518
  if (!note)
2412
2519
  continue;
2413
- this.applyDestinationSettings(channel, note, table);
2520
+ this.setControllerParameters(channel, note, table);
2414
2521
  }
2415
2522
  });
2416
2523
  }
@@ -2474,9 +2581,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2474
2581
  value: {
2475
2582
  currentBufferSource: null,
2476
2583
  detune: 0,
2477
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2478
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2479
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2480
2584
  program: 0,
2481
2585
  bank: 121 * 128,
2482
2586
  bankMSB: 121,