@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.
package/esm/midy-GM2.js CHANGED
@@ -1,5 +1,57 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
+ // 2-3 times faster than Map
4
+ class SparseMap {
5
+ constructor(size) {
6
+ this.data = new Array(size);
7
+ this.activeIndices = [];
8
+ }
9
+ set(key, value) {
10
+ if (this.data[key] === undefined) {
11
+ this.activeIndices.push(key);
12
+ }
13
+ this.data[key] = value;
14
+ }
15
+ get(key) {
16
+ return this.data[key];
17
+ }
18
+ delete(key) {
19
+ if (this.data[key] !== undefined) {
20
+ this.data[key] = undefined;
21
+ const index = this.activeIndices.indexOf(key);
22
+ if (index !== -1) {
23
+ this.activeIndices.splice(index, 1);
24
+ }
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ has(key) {
30
+ return this.data[key] !== undefined;
31
+ }
32
+ get size() {
33
+ return this.activeIndices.length;
34
+ }
35
+ clear() {
36
+ for (let i = 0; i < this.activeIndices.length; i++) {
37
+ const key = this.activeIndices[i];
38
+ this.data[key] = undefined;
39
+ }
40
+ this.activeIndices = [];
41
+ }
42
+ *[Symbol.iterator]() {
43
+ for (let i = 0; i < this.activeIndices.length; i++) {
44
+ const key = this.activeIndices[i];
45
+ yield [key, this.data[key]];
46
+ }
47
+ }
48
+ forEach(callback) {
49
+ for (let i = 0; i < this.activeIndices.length; i++) {
50
+ const key = this.activeIndices[i];
51
+ callback(this.data[key], key, this);
52
+ }
53
+ }
54
+ }
3
55
  class Note {
4
56
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
5
57
  Object.defineProperty(this, "bufferSource", {
@@ -14,31 +66,37 @@ class Note {
14
66
  writable: true,
15
67
  value: void 0
16
68
  });
69
+ Object.defineProperty(this, "filterDepth", {
70
+ enumerable: true,
71
+ configurable: true,
72
+ writable: true,
73
+ value: void 0
74
+ });
17
75
  Object.defineProperty(this, "volumeEnvelopeNode", {
18
76
  enumerable: true,
19
77
  configurable: true,
20
78
  writable: true,
21
79
  value: void 0
22
80
  });
23
- Object.defineProperty(this, "volumeNode", {
81
+ Object.defineProperty(this, "volumeDepth", {
24
82
  enumerable: true,
25
83
  configurable: true,
26
84
  writable: true,
27
85
  value: void 0
28
86
  });
29
- Object.defineProperty(this, "gainL", {
87
+ Object.defineProperty(this, "volumeNode", {
30
88
  enumerable: true,
31
89
  configurable: true,
32
90
  writable: true,
33
91
  value: void 0
34
92
  });
35
- Object.defineProperty(this, "gainR", {
93
+ Object.defineProperty(this, "gainL", {
36
94
  enumerable: true,
37
95
  configurable: true,
38
96
  writable: true,
39
97
  value: void 0
40
98
  });
41
- Object.defineProperty(this, "volumeDepth", {
99
+ Object.defineProperty(this, "gainR", {
42
100
  enumerable: true,
43
101
  configurable: true,
44
102
  writable: true,
@@ -280,6 +338,18 @@ export class MidyGM2 {
280
338
  writable: true,
281
339
  value: this.initSoundFontTable()
282
340
  });
341
+ Object.defineProperty(this, "audioBufferCounter", {
342
+ enumerable: true,
343
+ configurable: true,
344
+ writable: true,
345
+ value: new Map()
346
+ });
347
+ Object.defineProperty(this, "audioBufferCache", {
348
+ enumerable: true,
349
+ configurable: true,
350
+ writable: true,
351
+ value: new Map()
352
+ });
283
353
  Object.defineProperty(this, "isPlaying", {
284
354
  enumerable: true,
285
355
  configurable: true,
@@ -332,7 +402,7 @@ export class MidyGM2 {
332
402
  enumerable: true,
333
403
  configurable: true,
334
404
  writable: true,
335
- value: new Map()
405
+ value: new SparseMap(128)
336
406
  });
337
407
  Object.defineProperty(this, "defaultOptions", {
338
408
  enumerable: true,
@@ -372,7 +442,7 @@ export class MidyGM2 {
372
442
  initSoundFontTable() {
373
443
  const table = new Array(128);
374
444
  for (let i = 0; i < 128; i++) {
375
- table[i] = new Map();
445
+ table[i] = new SparseMap(128);
376
446
  }
377
447
  return table;
378
448
  }
@@ -426,8 +496,11 @@ export class MidyGM2 {
426
496
  state: new ControllerState(),
427
497
  controlTable: this.initControlTable(),
428
498
  ...this.setChannelAudioNodes(audioContext),
429
- scheduledNotes: new Map(),
430
- sostenutoNotes: new Map(),
499
+ scheduledNotes: new SparseMap(128),
500
+ sostenutoNotes: new SparseMap(128),
501
+ scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
502
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
503
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
431
504
  };
432
505
  });
433
506
  return channels;
@@ -461,9 +534,8 @@ export class MidyGM2 {
461
534
  return audioBuffer;
462
535
  }
463
536
  }
464
- async createNoteBufferNode(voiceParams, isSF3) {
537
+ createNoteBufferNode(audioBuffer, voiceParams) {
465
538
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
466
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
467
539
  bufferSource.buffer = audioBuffer;
468
540
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
469
541
  if (bufferSource.loop) {
@@ -495,10 +567,11 @@ export class MidyGM2 {
495
567
  const event = this.timeline[queueIndex];
496
568
  if (event.startTime > t + this.lookAhead)
497
569
  break;
570
+ const startTime = event.startTime + this.startDelay - offset;
498
571
  switch (event.type) {
499
572
  case "noteOn":
500
573
  if (event.velocity !== 0) {
501
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
574
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
502
575
  break;
503
576
  }
504
577
  /* falls through */
@@ -506,26 +579,26 @@ export class MidyGM2 {
506
579
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
507
580
  if (portamentoTarget)
508
581
  portamentoTarget.portamento = true;
509
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
582
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, portamentoTarget?.noteNumber, false);
510
583
  if (notePromise) {
511
584
  this.notePromises.push(notePromise);
512
585
  }
513
586
  break;
514
587
  }
515
588
  case "controller":
516
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
589
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
517
590
  break;
518
591
  case "programChange":
519
- this.handleProgramChange(event.channel, event.programNumber);
592
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
520
593
  break;
521
594
  case "channelAftertouch":
522
- this.handleChannelPressure(event.channel, event.amount);
595
+ this.handleChannelPressure(event.channel, event.amount, startTime);
523
596
  break;
524
597
  case "pitchBend":
525
- this.setPitchBend(event.channel, event.value + 8192);
598
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
526
599
  break;
527
600
  case "sysEx":
528
- this.handleSysEx(event.data);
601
+ this.handleSysEx(event.data, startTime);
529
602
  }
530
603
  queueIndex++;
531
604
  }
@@ -552,6 +625,7 @@ export class MidyGM2 {
552
625
  await Promise.all(this.notePromises);
553
626
  this.notePromises = [];
554
627
  this.exclusiveClassMap.clear();
628
+ this.audioBufferCache.clear();
555
629
  resolve();
556
630
  return;
557
631
  }
@@ -567,8 +641,9 @@ export class MidyGM2 {
567
641
  }
568
642
  else if (this.isStopping) {
569
643
  await this.stopNotes(0, true);
570
- this.exclusiveClassMap.clear();
571
644
  this.notePromises = [];
645
+ this.exclusiveClassMap.clear();
646
+ this.audioBufferCache.clear();
572
647
  resolve();
573
648
  this.isStopping = false;
574
649
  this.isPaused = false;
@@ -599,6 +674,9 @@ export class MidyGM2 {
599
674
  secondToTicks(second, secondsPerBeat) {
600
675
  return second * this.ticksPerBeat / secondsPerBeat;
601
676
  }
677
+ getAudioBufferId(programNumber, noteNumber, velocity) {
678
+ return `${programNumber}:${noteNumber}:${velocity}`;
679
+ }
602
680
  extractMidiData(midi) {
603
681
  const instruments = new Set();
604
682
  const timeline = [];
@@ -620,6 +698,8 @@ export class MidyGM2 {
620
698
  switch (event.type) {
621
699
  case "noteOn": {
622
700
  const channel = tmpChannels[event.channel];
701
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
702
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
623
703
  if (channel.programNumber < 0) {
624
704
  channel.programNumber = event.programNumber;
625
705
  switch (channel.bankMSB) {
@@ -669,6 +749,10 @@ export class MidyGM2 {
669
749
  timeline.push(event);
670
750
  }
671
751
  }
752
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
753
+ if (count === 1)
754
+ this.audioBufferCounter.delete(audioBufferId);
755
+ }
672
756
  const priority = {
673
757
  controller: 0,
674
758
  sysEx: 1,
@@ -761,8 +845,20 @@ export class MidyGM2 {
761
845
  const now = this.audioContext.currentTime;
762
846
  return this.resumeTime + now - this.startTime - this.startDelay;
763
847
  }
848
+ processScheduledNotes(channel, scheduleTime, callback) {
849
+ channel.scheduledNotes.forEach((noteList) => {
850
+ for (let i = 0; i < noteList.length; i++) {
851
+ const note = noteList[i];
852
+ if (!note)
853
+ continue;
854
+ if (scheduleTime < note.startTime)
855
+ continue;
856
+ callback(note);
857
+ }
858
+ });
859
+ }
764
860
  getActiveNotes(channel, time) {
765
- const activeNotes = new Map();
861
+ const activeNotes = new SparseMap(128);
766
862
  channel.scheduledNotes.forEach((noteList) => {
767
863
  const activeNote = this.getActiveNote(noteList, time);
768
864
  if (activeNote) {
@@ -942,14 +1038,14 @@ export class MidyGM2 {
942
1038
  const note = noteList[i];
943
1039
  if (!note)
944
1040
  continue;
945
- this.updateDetune(channel, note, 0);
1041
+ this.updateDetune(channel, note);
946
1042
  }
947
1043
  });
948
1044
  }
949
- updateDetune(channel, note, pressure) {
1045
+ updateDetune(channel, note) {
950
1046
  const now = this.audioContext.currentTime;
951
1047
  const noteDetune = this.calcNoteDetune(channel, note);
952
- const detune = channel.detune + noteDetune + pressure;
1048
+ const detune = channel.detune + noteDetune;
953
1049
  note.bufferSource.detune
954
1050
  .cancelScheduledValues(now)
955
1051
  .setValueAtTime(detune, now);
@@ -971,11 +1067,11 @@ export class MidyGM2 {
971
1067
  .setValueAtTime(0, volDelay)
972
1068
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
973
1069
  }
974
- setVolumeEnvelope(note, pressure) {
1070
+ setVolumeEnvelope(channel, note) {
975
1071
  const now = this.audioContext.currentTime;
976
1072
  const { voiceParams, startTime } = note;
977
1073
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
978
- (1 + pressure);
1074
+ (1 + this.getAmplitudeControl(channel));
979
1075
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
980
1076
  const volDelay = startTime + voiceParams.volDelay;
981
1077
  const volAttack = volDelay + voiceParams.volAttack;
@@ -989,20 +1085,20 @@ export class MidyGM2 {
989
1085
  .setValueAtTime(attackVolume, volHold)
990
1086
  .linearRampToValueAtTime(sustainVolume, volDecay);
991
1087
  }
992
- setPitchEnvelope(note) {
993
- const now = this.audioContext.currentTime;
1088
+ setPitchEnvelope(note, scheduleTime) {
1089
+ scheduleTime ??= this.audioContext.currentTime;
994
1090
  const { voiceParams } = note;
995
1091
  const baseRate = voiceParams.playbackRate;
996
1092
  note.bufferSource.playbackRate
997
- .cancelScheduledValues(now)
998
- .setValueAtTime(baseRate, now);
1093
+ .cancelScheduledValues(scheduleTime)
1094
+ .setValueAtTime(baseRate, scheduleTime);
999
1095
  const modEnvToPitch = voiceParams.modEnvToPitch;
1000
1096
  if (modEnvToPitch === 0)
1001
1097
  return;
1002
1098
  const basePitch = this.rateToCent(baseRate);
1003
1099
  const peekPitch = basePitch + modEnvToPitch;
1004
1100
  const peekRate = this.centToRate(peekPitch);
1005
- const modDelay = startTime + voiceParams.modDelay;
1101
+ const modDelay = note.startTime + voiceParams.modDelay;
1006
1102
  const modAttack = modDelay + voiceParams.modAttack;
1007
1103
  const modHold = modAttack + voiceParams.modHold;
1008
1104
  const modDecay = modHold + voiceParams.modDecay;
@@ -1038,13 +1134,14 @@ export class MidyGM2 {
1038
1134
  .setValueAtTime(adjustedBaseFreq, modDelay)
1039
1135
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1040
1136
  }
1041
- setFilterEnvelope(channel, note, pressure) {
1137
+ setFilterEnvelope(channel, note) {
1042
1138
  const now = this.audioContext.currentTime;
1043
1139
  const state = channel.state;
1044
1140
  const { voiceParams, noteNumber, startTime } = note;
1045
1141
  const softPedalFactor = 1 -
1046
1142
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1047
- const baseCent = voiceParams.initialFilterFc + pressure;
1143
+ const baseCent = voiceParams.initialFilterFc +
1144
+ this.getFilterCutoffControl(channel);
1048
1145
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1049
1146
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1050
1147
  softPedalFactor;
@@ -1074,9 +1171,9 @@ export class MidyGM2 {
1074
1171
  gain: voiceParams.modLfoToFilterFc,
1075
1172
  });
1076
1173
  note.modulationDepth = new GainNode(this.audioContext);
1077
- this.setModLfoToPitch(channel, note, 0);
1174
+ this.setModLfoToPitch(channel, note);
1078
1175
  note.volumeDepth = new GainNode(this.audioContext);
1079
- this.setModLfoToVolume(note, 0);
1176
+ this.setModLfoToVolume(channel, note);
1080
1177
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1081
1178
  note.modulationLFO.connect(note.filterDepth);
1082
1179
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1097,12 +1194,31 @@ export class MidyGM2 {
1097
1194
  note.vibratoLFO.connect(note.vibratoDepth);
1098
1195
  note.vibratoDepth.connect(note.bufferSource.detune);
1099
1196
  }
1197
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1198
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1199
+ const cache = this.audioBufferCache.get(audioBufferId);
1200
+ if (cache) {
1201
+ cache.counter += 1;
1202
+ if (cache.maxCount <= cache.counter) {
1203
+ this.audioBufferCache.delete(audioBufferId);
1204
+ }
1205
+ return cache.audioBuffer;
1206
+ }
1207
+ else {
1208
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1209
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1210
+ const cache = { audioBuffer, maxCount, counter: 1 };
1211
+ this.audioBufferCache.set(audioBufferId, cache);
1212
+ return audioBuffer;
1213
+ }
1214
+ }
1100
1215
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1101
1216
  const state = channel.state;
1102
1217
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1103
1218
  const voiceParams = voice.getAllParams(controllerState);
1104
1219
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1105
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1220
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1221
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1106
1222
  note.volumeNode = new GainNode(this.audioContext);
1107
1223
  note.gainL = new GainNode(this.audioContext);
1108
1224
  note.gainR = new GainNode(this.audioContext);
@@ -1118,8 +1234,8 @@ export class MidyGM2 {
1118
1234
  }
1119
1235
  else {
1120
1236
  note.portamento = false;
1121
- this.setVolumeEnvelope(note, 0);
1122
- this.setFilterEnvelope(channel, note, 0);
1237
+ this.setVolumeEnvelope(channel, note);
1238
+ this.setFilterEnvelope(channel, note);
1123
1239
  }
1124
1240
  if (0 < state.vibratoDepth) {
1125
1241
  this.startVibrato(channel, note, startTime);
@@ -1162,10 +1278,10 @@ export class MidyGM2 {
1162
1278
  if (soundFontIndex === undefined)
1163
1279
  return;
1164
1280
  const soundFont = this.soundFonts[soundFontIndex];
1165
- const isSF3 = soundFont.parsed.info.version.major === 3;
1166
1281
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1167
1282
  if (!voice)
1168
1283
  return;
1284
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1169
1285
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1170
1286
  note.gainL.connect(channel.gainL);
1171
1287
  note.gainR.connect(channel.gainR);
@@ -1333,7 +1449,9 @@ export class MidyGM2 {
1333
1449
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1334
1450
  channel.program = program;
1335
1451
  }
1336
- handleChannelPressure(channelNumber, value) {
1452
+ handleChannelPressure(channelNumber, value, startTime) {
1453
+ if (!startTime)
1454
+ startTime = this.audioContext.currentTime;
1337
1455
  const channel = this.channels[channelNumber];
1338
1456
  const prev = channel.state.channelPressure;
1339
1457
  const next = value / 127;
@@ -1343,13 +1461,8 @@ export class MidyGM2 {
1343
1461
  channel.detune += pressureDepth * (next - prev);
1344
1462
  }
1345
1463
  const table = channel.channelPressureTable;
1346
- channel.scheduledNotes.forEach((noteList) => {
1347
- for (let i = 0; i < noteList.length; i++) {
1348
- const note = noteList[i];
1349
- if (!note)
1350
- continue;
1351
- this.applyDestinationSettings(channel, note, table);
1352
- }
1464
+ this.getActiveNotes(channel, startTime).forEach((note) => {
1465
+ this.setControllerParameters(channel, note, table);
1353
1466
  });
1354
1467
  // this.applyVoiceParams(channel, 13);
1355
1468
  }
@@ -1367,9 +1480,10 @@ export class MidyGM2 {
1367
1480
  this.updateChannelDetune(channel);
1368
1481
  this.applyVoiceParams(channel, 14);
1369
1482
  }
1370
- setModLfoToPitch(channel, note, pressure) {
1483
+ setModLfoToPitch(channel, note) {
1371
1484
  const now = this.audioContext.currentTime;
1372
- const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1485
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1486
+ this.getLFOPitchDepth(channel);
1373
1487
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1374
1488
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1375
1489
  note.modulationDepth.gain
@@ -1386,18 +1500,20 @@ export class MidyGM2 {
1386
1500
  .cancelScheduledValues(now)
1387
1501
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1388
1502
  }
1389
- setModLfoToFilterFc(note, pressure) {
1503
+ setModLfoToFilterFc(channel, note) {
1390
1504
  const now = this.audioContext.currentTime;
1391
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1505
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1506
+ this.getLFOFilterDepth(channel);
1392
1507
  note.filterDepth.gain
1393
1508
  .cancelScheduledValues(now)
1394
1509
  .setValueAtTime(modLfoToFilterFc, now);
1395
1510
  }
1396
- setModLfoToVolume(note, pressure) {
1511
+ setModLfoToVolume(channel, note) {
1397
1512
  const now = this.audioContext.currentTime;
1398
1513
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1399
1514
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1400
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1515
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1516
+ (1 + this.getLFOAmplitudeDepth(channel));
1401
1517
  note.volumeDepth.gain
1402
1518
  .cancelScheduledValues(now)
1403
1519
  .setValueAtTime(volumeDepth, now);
@@ -1481,7 +1597,7 @@ export class MidyGM2 {
1481
1597
  return {
1482
1598
  modLfoToPitch: (channel, note, _prevValue) => {
1483
1599
  if (0 < channel.state.modulationDepth) {
1484
- this.setModLfoToPitch(channel, note, 0);
1600
+ this.setModLfoToPitch(channel, note);
1485
1601
  }
1486
1602
  },
1487
1603
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1491,12 +1607,12 @@ export class MidyGM2 {
1491
1607
  },
1492
1608
  modLfoToFilterFc: (channel, note, _prevValue) => {
1493
1609
  if (0 < channel.state.modulationDepth) {
1494
- this.setModLfoToFilterFc(note, 0);
1610
+ this.setModLfoToFilterFc(channel, note);
1495
1611
  }
1496
1612
  },
1497
1613
  modLfoToVolume: (channel, note, _prevValue) => {
1498
1614
  if (0 < channel.state.modulationDepth) {
1499
- this.setModLfoToVolume(note, 0);
1615
+ this.setModLfoToVolume(channel, note);
1500
1616
  }
1501
1617
  },
1502
1618
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1566,7 +1682,7 @@ export class MidyGM2 {
1566
1682
  this.setPortamentoStartFilterEnvelope(channel, note);
1567
1683
  }
1568
1684
  else {
1569
- this.setFilterEnvelope(channel, note, 0);
1685
+ this.setFilterEnvelope(channel, note);
1570
1686
  }
1571
1687
  this.setPitchEnvelope(note);
1572
1688
  }
@@ -1580,7 +1696,7 @@ export class MidyGM2 {
1580
1696
  if (key in voiceParams)
1581
1697
  noteVoiceParams[key] = voiceParams[key];
1582
1698
  }
1583
- this.setVolumeEnvelope(note, 0);
1699
+ this.setVolumeEnvelope(channel, note);
1584
1700
  }
1585
1701
  }
1586
1702
  }
@@ -1614,10 +1730,10 @@ export class MidyGM2 {
1614
1730
  127: this.polyOn,
1615
1731
  };
1616
1732
  }
1617
- handleControlChange(channelNumber, controllerType, value) {
1733
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1618
1734
  const handler = this.controlChangeHandlers[controllerType];
1619
1735
  if (handler) {
1620
- handler.call(this, channelNumber, value);
1736
+ handler.call(this, channelNumber, value, startTime);
1621
1737
  const channel = this.channels[channelNumber];
1622
1738
  this.applyVoiceParams(channel, controllerType + 128);
1623
1739
  this.applyControlTable(channel, controllerType);
@@ -1629,55 +1745,45 @@ export class MidyGM2 {
1629
1745
  setBankMSB(channelNumber, msb) {
1630
1746
  this.channels[channelNumber].bankMSB = msb;
1631
1747
  }
1632
- updateModulation(channel) {
1633
- const now = this.audioContext.currentTime;
1748
+ updateModulation(channel, scheduleTime) {
1749
+ scheduleTime ??= this.audioContext.currentTime;
1634
1750
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1635
- channel.scheduledNotes.forEach((noteList) => {
1636
- for (let i = 0; i < noteList.length; i++) {
1637
- const note = noteList[i];
1638
- if (!note)
1639
- continue;
1640
- if (note.modulationDepth) {
1641
- note.modulationDepth.gain.setValueAtTime(depth, now);
1642
- }
1643
- else {
1644
- this.setPitchEnvelope(note);
1645
- this.startModulation(channel, note, now);
1646
- }
1751
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1752
+ if (note.modulationDepth) {
1753
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1754
+ }
1755
+ else {
1756
+ this.setPitchEnvelope(note, scheduleTime);
1757
+ this.startModulation(channel, note, scheduleTime);
1647
1758
  }
1648
1759
  });
1649
1760
  }
1650
- setModulationDepth(channelNumber, modulation) {
1761
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1651
1762
  const channel = this.channels[channelNumber];
1652
1763
  channel.state.modulationDepth = modulation / 127;
1653
- this.updateModulation(channel);
1764
+ this.updateModulation(channel, scheduleTime);
1654
1765
  }
1655
1766
  setPortamentoTime(channelNumber, portamentoTime) {
1656
1767
  const channel = this.channels[channelNumber];
1657
1768
  const factor = 5 * Math.log(10) / 127;
1658
1769
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1659
1770
  }
1660
- setKeyBasedVolume(channel) {
1661
- const now = this.audioContext.currentTime;
1662
- channel.scheduledNotes.forEach((noteList) => {
1663
- for (let i = 0; i < noteList.length; i++) {
1664
- const note = noteList[i];
1665
- if (!note)
1666
- continue;
1667
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1668
- if (keyBasedValue === 0)
1669
- continue;
1771
+ setKeyBasedVolume(channel, scheduleTime) {
1772
+ scheduleTime ??= this.audioContext.currentTime;
1773
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1774
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1775
+ if (keyBasedValue !== 0) {
1670
1776
  note.volumeNode.gain
1671
- .cancelScheduledValues(now)
1672
- .setValueAtTime(1 + keyBasedValue, now);
1777
+ .cancelScheduledValues(scheduleTime)
1778
+ .setValueAtTime(1 + keyBasedValue, scheduleTime);
1673
1779
  }
1674
1780
  });
1675
1781
  }
1676
- setVolume(channelNumber, volume) {
1782
+ setVolume(channelNumber, volume, scheduleTime) {
1677
1783
  const channel = this.channels[channelNumber];
1678
1784
  channel.state.volume = volume / 127;
1679
- this.updateChannelVolume(channel);
1680
- this.setKeyBasedVolume(channel);
1785
+ this.updateChannelVolume(channel, scheduleTime);
1786
+ this.setKeyBasedVolume(channel, scheduleTime);
1681
1787
  }
1682
1788
  panToGain(pan) {
1683
1789
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1686,36 +1792,31 @@ export class MidyGM2 {
1686
1792
  gainRight: Math.sin(theta),
1687
1793
  };
1688
1794
  }
1689
- setKeyBasedPan(channel) {
1690
- const now = this.audioContext.currentTime;
1691
- channel.scheduledNotes.forEach((noteList) => {
1692
- for (let i = 0; i < noteList.length; i++) {
1693
- const note = noteList[i];
1694
- if (!note)
1695
- continue;
1696
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1697
- if (keyBasedValue === 0)
1698
- continue;
1795
+ setKeyBasedPan(channel, scheduleTime) {
1796
+ scheduleTime ??= this.audioContext.currentTime;
1797
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1798
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1799
+ if (keyBasedValue !== 0) {
1699
1800
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1700
1801
  note.gainL.gain
1701
- .cancelScheduledValues(now)
1702
- .setValueAtTime(gainLeft, now);
1802
+ .cancelScheduledValues(scheduleTime)
1803
+ .setValueAtTime(gainLeft, scheduleTime);
1703
1804
  note.gainR.gain
1704
- .cancelScheduledValues(now)
1705
- .setValueAtTime(gainRight, now);
1805
+ .cancelScheduledValues(scheduleTime)
1806
+ .setValueAtTime(gainRight, scheduleTime);
1706
1807
  }
1707
1808
  });
1708
1809
  }
1709
- setPan(channelNumber, pan) {
1810
+ setPan(channelNumber, pan, scheduleTime) {
1710
1811
  const channel = this.channels[channelNumber];
1711
1812
  channel.state.pan = pan / 127;
1712
- this.updateChannelVolume(channel);
1713
- this.setKeyBasedPan(channel);
1813
+ this.updateChannelVolume(channel, scheduleTime);
1814
+ this.setKeyBasedPan(channel, scheduleTime);
1714
1815
  }
1715
- setExpression(channelNumber, expression) {
1816
+ setExpression(channelNumber, expression, scheduleTime) {
1716
1817
  const channel = this.channels[channelNumber];
1717
1818
  channel.state.expression = expression / 127;
1718
- this.updateChannelVolume(channel);
1819
+ this.updateChannelVolume(channel, scheduleTime);
1719
1820
  }
1720
1821
  setBankLSB(channelNumber, lsb) {
1721
1822
  this.channels[channelNumber].bankLSB = lsb;
@@ -1750,8 +1851,7 @@ export class MidyGM2 {
1750
1851
  channel.state.sostenutoPedal = value / 127;
1751
1852
  if (64 <= value) {
1752
1853
  const now = this.audioContext.currentTime;
1753
- const activeNotes = this.getActiveNotes(channel, now);
1754
- channel.sostenutoNotes = new Map(activeNotes);
1854
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1755
1855
  }
1756
1856
  else {
1757
1857
  this.releaseSostenutoPedal(channelNumber, value);
@@ -2004,7 +2104,7 @@ export class MidyGM2 {
2004
2104
  switch (data[3]) {
2005
2105
  case 8:
2006
2106
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2007
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2107
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2008
2108
  default:
2009
2109
  console.warn(`Unsupported Exclusive Message: ${data}`);
2010
2110
  }
@@ -2326,8 +2426,8 @@ export class MidyGM2 {
2326
2426
  }
2327
2427
  return bitmap;
2328
2428
  }
2329
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2330
- if (data.length < 18) {
2429
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2430
+ if (data.length < 19) {
2331
2431
  console.error("Data length is too short");
2332
2432
  return;
2333
2433
  }
@@ -2335,48 +2435,55 @@ export class MidyGM2 {
2335
2435
  for (let i = 0; i < channelBitmap.length; i++) {
2336
2436
  if (!channelBitmap[i])
2337
2437
  continue;
2438
+ const channel = this.channels[i];
2338
2439
  for (let j = 0; j < 12; j++) {
2339
- const value = data[j + 7] - 64; // cent
2340
- this.channels[i].scaleOctaveTuningTable[j] = value;
2440
+ const centValue = data[j + 7] - 64;
2441
+ channel.scaleOctaveTuningTable[j] = centValue;
2341
2442
  }
2342
- }
2343
- }
2344
- applyDestinationSettings(channel, note, table) {
2345
- if (table[0] !== 64) {
2346
- this.updateDetune(channel, note, 0);
2347
- }
2443
+ if (realtime)
2444
+ this.updateChannelDetune(channel);
2445
+ }
2446
+ }
2447
+ getFilterCutoffControl(channel) {
2448
+ const channelPressure = (channel.channelPressureTable[1] - 64) *
2449
+ channel.state.channelPressure;
2450
+ return channelPressure * 15;
2451
+ }
2452
+ getAmplitudeControl(channel) {
2453
+ const channelPressure = channel.channelPressureTable[2] *
2454
+ channel.state.channelPressure;
2455
+ return channelPressure / 64;
2456
+ }
2457
+ getLFOPitchDepth(channel) {
2458
+ const channelPressure = channel.channelPressureTable[3] *
2459
+ channel.state.channelPressure;
2460
+ return channelPressure / 127 * 600;
2461
+ }
2462
+ getLFOFilterDepth(channel) {
2463
+ const channelPressure = channel.channelPressureTable[4] *
2464
+ channel.state.channelPressure;
2465
+ return channelPressure / 127 * 2400;
2466
+ }
2467
+ getLFOAmplitudeDepth(channel) {
2468
+ const channelPressure = channel.channelPressureTable[5] *
2469
+ channel.state.channelPressure;
2470
+ return channelPressure / 127;
2471
+ }
2472
+ setControllerParameters(channel, note, table) {
2473
+ if (table[0] !== 64)
2474
+ this.updateDetune(channel, note);
2348
2475
  if (!note.portamento) {
2349
- if (table[1] !== 64) {
2350
- const channelPressure = channel.channelPressureTable[1] *
2351
- channel.state.channelPressure;
2352
- const pressure = (channelPressure - 64) * 15;
2353
- this.setFilterEnvelope(channel, note, pressure);
2354
- }
2355
- if (table[2] !== 64) {
2356
- const channelPressure = channel.channelPressureTable[2] *
2357
- channel.state.channelPressure;
2358
- const pressure = channelPressure / 64;
2359
- this.setVolumeEnvelope(note, pressure);
2360
- }
2361
- }
2362
- if (table[3] !== 0) {
2363
- const channelPressure = channel.channelPressureTable[3] *
2364
- channel.state.channelPressure;
2365
- const pressure = channelPressure / 127 * 600;
2366
- this.setModLfoToPitch(channel, note, pressure);
2367
- }
2368
- if (table[4] !== 0) {
2369
- const channelPressure = channel.channelPressureTable[4] *
2370
- channel.state.channelPressure;
2371
- const pressure = channelPressure / 127 * 2400;
2372
- this.setModLfoToFilterFc(note, pressure);
2373
- }
2374
- if (table[5] !== 0) {
2375
- const channelPressure = channel.channelPressureTable[5] *
2376
- channel.state.channelPressure;
2377
- const pressure = channelPressure / 127;
2378
- this.setModLfoToVolume(note, pressure);
2379
- }
2476
+ if (table[1] !== 64)
2477
+ this.setFilterEnvelope(channel, note);
2478
+ if (table[2] !== 64)
2479
+ this.setVolumeEnvelope(channel, note);
2480
+ }
2481
+ if (table[3] !== 0)
2482
+ this.setModLfoToPitch(channel, note);
2483
+ if (table[4] !== 0)
2484
+ this.setModLfoToFilterFc(channel, note);
2485
+ if (table[5] !== 0)
2486
+ this.setModLfoToVolume(channel, note);
2380
2487
  }
2381
2488
  handleChannelPressureSysEx(data, tableName) {
2382
2489
  const channelNumber = data[4];
@@ -2407,7 +2514,7 @@ export class MidyGM2 {
2407
2514
  const note = noteList[i];
2408
2515
  if (!note)
2409
2516
  continue;
2410
- this.applyDestinationSettings(channel, note, table);
2517
+ this.setControllerParameters(channel, note, table);
2411
2518
  }
2412
2519
  });
2413
2520
  }
@@ -2470,9 +2577,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2470
2577
  value: {
2471
2578
  currentBufferSource: null,
2472
2579
  detune: 0,
2473
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2474
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2475
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2476
2580
  program: 0,
2477
2581
  bank: 121 * 128,
2478
2582
  bankMSB: 121,