@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/script/midy.js CHANGED
@@ -3,6 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = 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,
@@ -290,6 +348,18 @@ class Midy {
290
348
  writable: true,
291
349
  value: this.initSoundFontTable()
292
350
  });
351
+ Object.defineProperty(this, "audioBufferCounter", {
352
+ enumerable: true,
353
+ configurable: true,
354
+ writable: true,
355
+ value: new Map()
356
+ });
357
+ Object.defineProperty(this, "audioBufferCache", {
358
+ enumerable: true,
359
+ configurable: true,
360
+ writable: true,
361
+ value: new Map()
362
+ });
293
363
  Object.defineProperty(this, "isPlaying", {
294
364
  enumerable: true,
295
365
  configurable: true,
@@ -342,7 +412,7 @@ class Midy {
342
412
  enumerable: true,
343
413
  configurable: true,
344
414
  writable: true,
345
- value: new Map()
415
+ value: new SparseMap(128)
346
416
  });
347
417
  Object.defineProperty(this, "defaultOptions", {
348
418
  enumerable: true,
@@ -382,7 +452,7 @@ class Midy {
382
452
  initSoundFontTable() {
383
453
  const table = new Array(128);
384
454
  for (let i = 0; i < 128; i++) {
385
- table[i] = new Map();
455
+ table[i] = new SparseMap(128);
386
456
  }
387
457
  return table;
388
458
  }
@@ -436,8 +506,12 @@ class Midy {
436
506
  state: new ControllerState(),
437
507
  controlTable: this.initControlTable(),
438
508
  ...this.setChannelAudioNodes(audioContext),
439
- scheduledNotes: new Map(),
440
- sostenutoNotes: new Map(),
509
+ scheduledNotes: new SparseMap(128),
510
+ sostenutoNotes: new SparseMap(128),
511
+ scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
512
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
513
+ polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
514
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
441
515
  };
442
516
  });
443
517
  return channels;
@@ -471,9 +545,8 @@ class Midy {
471
545
  return audioBuffer;
472
546
  }
473
547
  }
474
- async createNoteBufferNode(voiceParams, isSF3) {
548
+ createNoteBufferNode(audioBuffer, voiceParams) {
475
549
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
476
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
477
550
  bufferSource.buffer = audioBuffer;
478
551
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
479
552
  if (bufferSource.loop) {
@@ -505,10 +578,11 @@ class Midy {
505
578
  const event = this.timeline[queueIndex];
506
579
  if (event.startTime > t + this.lookAhead)
507
580
  break;
581
+ const startTime = event.startTime + this.startDelay - offset;
508
582
  switch (event.type) {
509
583
  case "noteOn":
510
584
  if (event.velocity !== 0) {
511
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
585
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
512
586
  break;
513
587
  }
514
588
  /* falls through */
@@ -516,29 +590,29 @@ class Midy {
516
590
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
517
591
  if (portamentoTarget)
518
592
  portamentoTarget.portamento = true;
519
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
593
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, portamentoTarget?.noteNumber, false);
520
594
  if (notePromise) {
521
595
  this.notePromises.push(notePromise);
522
596
  }
523
597
  break;
524
598
  }
525
599
  case "noteAftertouch":
526
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount);
600
+ this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
527
601
  break;
528
602
  case "controller":
529
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
603
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
530
604
  break;
531
605
  case "programChange":
532
- this.handleProgramChange(event.channel, event.programNumber);
606
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
533
607
  break;
534
608
  case "channelAftertouch":
535
- this.handleChannelPressure(event.channel, event.amount);
609
+ this.handleChannelPressure(event.channel, event.amount, startTime);
536
610
  break;
537
611
  case "pitchBend":
538
- this.setPitchBend(event.channel, event.value + 8192);
612
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
539
613
  break;
540
614
  case "sysEx":
541
- this.handleSysEx(event.data);
615
+ this.handleSysEx(event.data, startTime);
542
616
  }
543
617
  queueIndex++;
544
618
  }
@@ -565,6 +639,7 @@ class Midy {
565
639
  await Promise.all(this.notePromises);
566
640
  this.notePromises = [];
567
641
  this.exclusiveClassMap.clear();
642
+ this.audioBufferCache.clear();
568
643
  resolve();
569
644
  return;
570
645
  }
@@ -580,8 +655,9 @@ class Midy {
580
655
  }
581
656
  else if (this.isStopping) {
582
657
  await this.stopNotes(0, true);
583
- this.exclusiveClassMap.clear();
584
658
  this.notePromises = [];
659
+ this.exclusiveClassMap.clear();
660
+ this.audioBufferCache.clear();
585
661
  resolve();
586
662
  this.isStopping = false;
587
663
  this.isPaused = false;
@@ -612,6 +688,9 @@ class Midy {
612
688
  secondToTicks(second, secondsPerBeat) {
613
689
  return second * this.ticksPerBeat / secondsPerBeat;
614
690
  }
691
+ getAudioBufferId(programNumber, noteNumber, velocity) {
692
+ return `${programNumber}:${noteNumber}:${velocity}`;
693
+ }
615
694
  extractMidiData(midi) {
616
695
  const instruments = new Set();
617
696
  const timeline = [];
@@ -633,6 +712,8 @@ class Midy {
633
712
  switch (event.type) {
634
713
  case "noteOn": {
635
714
  const channel = tmpChannels[event.channel];
715
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
716
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
636
717
  if (channel.programNumber < 0) {
637
718
  channel.programNumber = event.programNumber;
638
719
  switch (channel.bankMSB) {
@@ -682,6 +763,10 @@ class Midy {
682
763
  timeline.push(event);
683
764
  }
684
765
  }
766
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
767
+ if (count === 1)
768
+ this.audioBufferCounter.delete(audioBufferId);
769
+ }
685
770
  const priority = {
686
771
  controller: 0,
687
772
  sysEx: 1,
@@ -774,8 +859,20 @@ class Midy {
774
859
  const now = this.audioContext.currentTime;
775
860
  return this.resumeTime + now - this.startTime - this.startDelay;
776
861
  }
862
+ processScheduledNotes(channel, scheduleTime, callback) {
863
+ channel.scheduledNotes.forEach((noteList) => {
864
+ for (let i = 0; i < noteList.length; i++) {
865
+ const note = noteList[i];
866
+ if (!note)
867
+ continue;
868
+ if (scheduleTime < note.startTime)
869
+ continue;
870
+ callback(note);
871
+ }
872
+ });
873
+ }
777
874
  getActiveNotes(channel, time) {
778
- const activeNotes = new Map();
875
+ const activeNotes = new SparseMap(128);
779
876
  channel.scheduledNotes.forEach((noteList) => {
780
877
  const activeNote = this.getActiveNote(noteList, time);
781
878
  if (activeNote) {
@@ -955,14 +1052,15 @@ class Midy {
955
1052
  const note = noteList[i];
956
1053
  if (!note)
957
1054
  continue;
958
- this.updateDetune(channel, note, 0);
1055
+ this.updateDetune(channel, note);
959
1056
  }
960
1057
  });
961
1058
  }
962
- updateDetune(channel, note, pressure) {
1059
+ updateDetune(channel, note) {
963
1060
  const now = this.audioContext.currentTime;
964
1061
  const noteDetune = this.calcNoteDetune(channel, note);
965
- const detune = channel.detune + noteDetune + pressure;
1062
+ const pitchControl = this.getPitchControl(channel, note);
1063
+ const detune = channel.detune + noteDetune + pitchControl;
966
1064
  note.bufferSource.detune
967
1065
  .cancelScheduledValues(now)
968
1066
  .setValueAtTime(detune, now);
@@ -984,12 +1082,12 @@ class Midy {
984
1082
  .setValueAtTime(0, volDelay)
985
1083
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
986
1084
  }
987
- setVolumeEnvelope(channel, note, pressure) {
1085
+ setVolumeEnvelope(channel, note) {
988
1086
  const now = this.audioContext.currentTime;
989
1087
  const state = channel.state;
990
1088
  const { voiceParams, startTime } = note;
991
1089
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
992
- (1 + pressure);
1090
+ (1 + this.getAmplitudeControl(channel, note));
993
1091
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
994
1092
  const volDelay = startTime + voiceParams.volDelay;
995
1093
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
@@ -1003,20 +1101,20 @@ class Midy {
1003
1101
  .setValueAtTime(attackVolume, volHold)
1004
1102
  .linearRampToValueAtTime(sustainVolume, volDecay);
1005
1103
  }
1006
- setPitchEnvelope(note) {
1007
- const now = this.audioContext.currentTime;
1104
+ setPitchEnvelope(note, scheduleTime) {
1105
+ scheduleTime ??= this.audioContext.currentTime;
1008
1106
  const { voiceParams } = note;
1009
1107
  const baseRate = voiceParams.playbackRate;
1010
1108
  note.bufferSource.playbackRate
1011
- .cancelScheduledValues(now)
1012
- .setValueAtTime(baseRate, now);
1109
+ .cancelScheduledValues(scheduleTime)
1110
+ .setValueAtTime(baseRate, scheduleTime);
1013
1111
  const modEnvToPitch = voiceParams.modEnvToPitch;
1014
1112
  if (modEnvToPitch === 0)
1015
1113
  return;
1016
1114
  const basePitch = this.rateToCent(baseRate);
1017
1115
  const peekPitch = basePitch + modEnvToPitch;
1018
1116
  const peekRate = this.centToRate(peekPitch);
1019
- const modDelay = startTime + voiceParams.modDelay;
1117
+ const modDelay = note.startTime + voiceParams.modDelay;
1020
1118
  const modAttack = modDelay + voiceParams.modAttack;
1021
1119
  const modHold = modAttack + voiceParams.modHold;
1022
1120
  const modDecay = modHold + voiceParams.modDecay;
@@ -1053,13 +1151,14 @@ class Midy {
1053
1151
  .setValueAtTime(adjustedBaseFreq, modDelay)
1054
1152
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1055
1153
  }
1056
- setFilterEnvelope(channel, note, pressure) {
1154
+ setFilterEnvelope(channel, note) {
1057
1155
  const now = this.audioContext.currentTime;
1058
1156
  const state = channel.state;
1059
1157
  const { voiceParams, noteNumber, startTime } = note;
1060
1158
  const softPedalFactor = 1 -
1061
1159
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1062
- const baseCent = voiceParams.initialFilterFc + pressure;
1160
+ const baseCent = voiceParams.initialFilterFc +
1161
+ this.getFilterCutoffControl(channel, note);
1063
1162
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1064
1163
  state.brightness * 2;
1065
1164
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
@@ -1090,9 +1189,9 @@ class Midy {
1090
1189
  gain: voiceParams.modLfoToFilterFc,
1091
1190
  });
1092
1191
  note.modulationDepth = new GainNode(this.audioContext);
1093
- this.setModLfoToPitch(channel, note, 0);
1192
+ this.setModLfoToPitch(channel, note);
1094
1193
  note.volumeDepth = new GainNode(this.audioContext);
1095
- this.setModLfoToVolume(note, 0);
1194
+ this.setModLfoToVolume(channel, note);
1096
1195
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1097
1196
  note.modulationLFO.connect(note.filterDepth);
1098
1197
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1113,12 +1212,31 @@ class Midy {
1113
1212
  note.vibratoLFO.connect(note.vibratoDepth);
1114
1213
  note.vibratoDepth.connect(note.bufferSource.detune);
1115
1214
  }
1215
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1216
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1217
+ const cache = this.audioBufferCache.get(audioBufferId);
1218
+ if (cache) {
1219
+ cache.counter += 1;
1220
+ if (cache.maxCount <= cache.counter) {
1221
+ this.audioBufferCache.delete(audioBufferId);
1222
+ }
1223
+ return cache.audioBuffer;
1224
+ }
1225
+ else {
1226
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1227
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1228
+ const cache = { audioBuffer, maxCount, counter: 1 };
1229
+ this.audioBufferCache.set(audioBufferId, cache);
1230
+ return audioBuffer;
1231
+ }
1232
+ }
1116
1233
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1117
1234
  const state = channel.state;
1118
1235
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1119
1236
  const voiceParams = voice.getAllParams(controllerState);
1120
1237
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1121
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1238
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1239
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1122
1240
  note.volumeNode = new GainNode(this.audioContext);
1123
1241
  note.gainL = new GainNode(this.audioContext);
1124
1242
  note.gainR = new GainNode(this.audioContext);
@@ -1134,8 +1252,8 @@ class Midy {
1134
1252
  }
1135
1253
  else {
1136
1254
  note.portamento = false;
1137
- this.setVolumeEnvelope(channel, note, 0);
1138
- this.setFilterEnvelope(channel, note, 0);
1255
+ this.setVolumeEnvelope(channel, note);
1256
+ this.setFilterEnvelope(channel, note);
1139
1257
  }
1140
1258
  if (0 < state.vibratoDepth) {
1141
1259
  this.startVibrato(channel, note, startTime);
@@ -1178,10 +1296,10 @@ class Midy {
1178
1296
  if (soundFontIndex === undefined)
1179
1297
  return;
1180
1298
  const soundFont = this.soundFonts[soundFontIndex];
1181
- const isSF3 = soundFont.parsed.info.version.major === 3;
1182
1299
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1183
1300
  if (!voice)
1184
1301
  return;
1302
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1185
1303
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1186
1304
  note.gainL.connect(channel.gainL);
1187
1305
  note.gainR.connect(channel.gainR);
@@ -1347,15 +1465,16 @@ class Midy {
1347
1465
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1348
1466
  }
1349
1467
  }
1350
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
1351
- const now = this.audioContext.currentTime;
1468
+ handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, startTime) {
1469
+ if (!startTime)
1470
+ startTime = this.audioContext.currentTime;
1352
1471
  const channel = this.channels[channelNumber];
1353
1472
  channel.state.polyphonicKeyPressure = pressure / 127;
1354
1473
  const table = channel.polyphonicKeyPressureTable;
1355
- const activeNotes = this.getActiveNotes(channel, now);
1474
+ const activeNotes = this.getActiveNotes(channel, startTime);
1356
1475
  if (activeNotes.has(noteNumber)) {
1357
1476
  const note = activeNotes.get(noteNumber);
1358
- this.applyDestinationSettings(channel, note, table);
1477
+ this.setControllerParameters(channel, note, table);
1359
1478
  }
1360
1479
  // this.applyVoiceParams(channel, 10);
1361
1480
  }
@@ -1364,7 +1483,9 @@ class Midy {
1364
1483
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1365
1484
  channel.program = program;
1366
1485
  }
1367
- handleChannelPressure(channelNumber, value) {
1486
+ handleChannelPressure(channelNumber, value, startTime) {
1487
+ if (!startTime)
1488
+ startTime = this.audioContext.currentTime;
1368
1489
  const channel = this.channels[channelNumber];
1369
1490
  const prev = channel.state.channelPressure;
1370
1491
  const next = value / 127;
@@ -1374,13 +1495,8 @@ class Midy {
1374
1495
  channel.detune += pressureDepth * (next - prev);
1375
1496
  }
1376
1497
  const table = channel.channelPressureTable;
1377
- channel.scheduledNotes.forEach((noteList) => {
1378
- for (let i = 0; i < noteList.length; i++) {
1379
- const note = noteList[i];
1380
- if (!note)
1381
- continue;
1382
- this.applyDestinationSettings(channel, note, table);
1383
- }
1498
+ this.getActiveNotes(channel, startTime).forEach((note) => {
1499
+ this.setControllerParameters(channel, note, table);
1384
1500
  });
1385
1501
  // this.applyVoiceParams(channel, 13);
1386
1502
  }
@@ -1398,9 +1514,10 @@ class Midy {
1398
1514
  this.updateChannelDetune(channel);
1399
1515
  this.applyVoiceParams(channel, 14);
1400
1516
  }
1401
- setModLfoToPitch(channel, note, pressure) {
1517
+ setModLfoToPitch(channel, note) {
1402
1518
  const now = this.audioContext.currentTime;
1403
- const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1519
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1520
+ this.getLFOPitchDepth(channel, note);
1404
1521
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1405
1522
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1406
1523
  note.modulationDepth.gain
@@ -1417,18 +1534,20 @@ class Midy {
1417
1534
  .cancelScheduledValues(now)
1418
1535
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1419
1536
  }
1420
- setModLfoToFilterFc(note, pressure) {
1537
+ setModLfoToFilterFc(channel, note) {
1421
1538
  const now = this.audioContext.currentTime;
1422
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1539
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1540
+ this.getLFOFilterDepth(channel, note);
1423
1541
  note.filterDepth.gain
1424
1542
  .cancelScheduledValues(now)
1425
1543
  .setValueAtTime(modLfoToFilterFc, now);
1426
1544
  }
1427
- setModLfoToVolume(note, pressure) {
1545
+ setModLfoToVolume(channel, note) {
1428
1546
  const now = this.audioContext.currentTime;
1429
1547
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1430
1548
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1431
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1549
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1550
+ (1 + this.getLFOAmplitudeDepth(channel, note));
1432
1551
  note.volumeDepth.gain
1433
1552
  .cancelScheduledValues(now)
1434
1553
  .setValueAtTime(volumeDepth, now);
@@ -1512,7 +1631,7 @@ class Midy {
1512
1631
  return {
1513
1632
  modLfoToPitch: (channel, note, _prevValue) => {
1514
1633
  if (0 < channel.state.modulationDepth) {
1515
- this.setModLfoToPitch(channel, note, 0);
1634
+ this.setModLfoToPitch(channel, note);
1516
1635
  }
1517
1636
  },
1518
1637
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1522,12 +1641,12 @@ class Midy {
1522
1641
  },
1523
1642
  modLfoToFilterFc: (channel, note, _prevValue) => {
1524
1643
  if (0 < channel.state.modulationDepth) {
1525
- this.setModLfoToFilterFc(note, 0);
1644
+ this.setModLfoToFilterFc(channel, note);
1526
1645
  }
1527
1646
  },
1528
1647
  modLfoToVolume: (channel, note, _prevValue) => {
1529
1648
  if (0 < channel.state.modulationDepth) {
1530
- this.setModLfoToVolume(note, 0);
1649
+ this.setModLfoToVolume(channel, note);
1531
1650
  }
1532
1651
  },
1533
1652
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1597,7 +1716,7 @@ class Midy {
1597
1716
  this.setPortamentoStartFilterEnvelope(channel, note);
1598
1717
  }
1599
1718
  else {
1600
- this.setFilterEnvelope(channel, note, 0);
1719
+ this.setFilterEnvelope(channel, note);
1601
1720
  }
1602
1721
  this.setPitchEnvelope(note);
1603
1722
  }
@@ -1611,7 +1730,7 @@ class Midy {
1611
1730
  if (key in voiceParams)
1612
1731
  noteVoiceParams[key] = voiceParams[key];
1613
1732
  }
1614
- this.setVolumeEnvelope(channel, note, 0);
1733
+ this.setVolumeEnvelope(channel, note);
1615
1734
  }
1616
1735
  }
1617
1736
  }
@@ -1655,10 +1774,10 @@ class Midy {
1655
1774
  127: this.polyOn,
1656
1775
  };
1657
1776
  }
1658
- handleControlChange(channelNumber, controllerType, value) {
1777
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1659
1778
  const handler = this.controlChangeHandlers[controllerType];
1660
1779
  if (handler) {
1661
- handler.call(this, channelNumber, value);
1780
+ handler.call(this, channelNumber, value, startTime);
1662
1781
  const channel = this.channels[channelNumber];
1663
1782
  this.applyVoiceParams(channel, controllerType + 128);
1664
1783
  this.applyControlTable(channel, controllerType);
@@ -1670,55 +1789,45 @@ class Midy {
1670
1789
  setBankMSB(channelNumber, msb) {
1671
1790
  this.channels[channelNumber].bankMSB = msb;
1672
1791
  }
1673
- updateModulation(channel) {
1674
- const now = this.audioContext.currentTime;
1792
+ updateModulation(channel, scheduleTime) {
1793
+ scheduleTime ??= this.audioContext.currentTime;
1675
1794
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1676
- channel.scheduledNotes.forEach((noteList) => {
1677
- for (let i = 0; i < noteList.length; i++) {
1678
- const note = noteList[i];
1679
- if (!note)
1680
- continue;
1681
- if (note.modulationDepth) {
1682
- note.modulationDepth.gain.setValueAtTime(depth, now);
1683
- }
1684
- else {
1685
- this.setPitchEnvelope(note);
1686
- this.startModulation(channel, note, now);
1687
- }
1795
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1796
+ if (note.modulationDepth) {
1797
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1798
+ }
1799
+ else {
1800
+ this.setPitchEnvelope(note, scheduleTime);
1801
+ this.startModulation(channel, note, scheduleTime);
1688
1802
  }
1689
1803
  });
1690
1804
  }
1691
- setModulationDepth(channelNumber, modulation) {
1805
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1692
1806
  const channel = this.channels[channelNumber];
1693
1807
  channel.state.modulationDepth = modulation / 127;
1694
- this.updateModulation(channel);
1808
+ this.updateModulation(channel, scheduleTime);
1695
1809
  }
1696
1810
  setPortamentoTime(channelNumber, portamentoTime) {
1697
1811
  const channel = this.channels[channelNumber];
1698
1812
  const factor = 5 * Math.log(10) / 127;
1699
1813
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1700
1814
  }
1701
- setKeyBasedVolume(channel) {
1702
- const now = this.audioContext.currentTime;
1703
- channel.scheduledNotes.forEach((noteList) => {
1704
- for (let i = 0; i < noteList.length; i++) {
1705
- const note = noteList[i];
1706
- if (!note)
1707
- continue;
1708
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1709
- if (keyBasedValue === 0)
1710
- continue;
1815
+ setKeyBasedVolume(channel, scheduleTime) {
1816
+ scheduleTime ??= this.audioContext.currentTime;
1817
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1818
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1819
+ if (keyBasedValue !== 0) {
1711
1820
  note.volumeNode.gain
1712
- .cancelScheduledValues(now)
1713
- .setValueAtTime(1 + keyBasedValue, now);
1821
+ .cancelScheduledValues(scheduleTime)
1822
+ .setValueAtTime(1 + keyBasedValue, scheduleTime);
1714
1823
  }
1715
1824
  });
1716
1825
  }
1717
- setVolume(channelNumber, volume) {
1826
+ setVolume(channelNumber, volume, scheduleTime) {
1718
1827
  const channel = this.channels[channelNumber];
1719
1828
  channel.state.volume = volume / 127;
1720
- this.updateChannelVolume(channel);
1721
- this.setKeyBasedVolume(channel);
1829
+ this.updateChannelVolume(channel, scheduleTime);
1830
+ this.setKeyBasedVolume(channel, scheduleTime);
1722
1831
  }
1723
1832
  panToGain(pan) {
1724
1833
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1727,36 +1836,31 @@ class Midy {
1727
1836
  gainRight: Math.sin(theta),
1728
1837
  };
1729
1838
  }
1730
- setKeyBasedPan(channel) {
1731
- const now = this.audioContext.currentTime;
1732
- channel.scheduledNotes.forEach((noteList) => {
1733
- for (let i = 0; i < noteList.length; i++) {
1734
- const note = noteList[i];
1735
- if (!note)
1736
- continue;
1737
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1738
- if (keyBasedValue === 0)
1739
- continue;
1839
+ setKeyBasedPan(channel, scheduleTime) {
1840
+ scheduleTime ??= this.audioContext.currentTime;
1841
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1842
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1843
+ if (keyBasedValue !== 0) {
1740
1844
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1741
1845
  note.gainL.gain
1742
- .cancelScheduledValues(now)
1743
- .setValueAtTime(gainLeft, now);
1846
+ .cancelScheduledValues(scheduleTime)
1847
+ .setValueAtTime(gainLeft, scheduleTime);
1744
1848
  note.gainR.gain
1745
- .cancelScheduledValues(now)
1746
- .setValueAtTime(gainRight, now);
1849
+ .cancelScheduledValues(scheduleTime)
1850
+ .setValueAtTime(gainRight, scheduleTime);
1747
1851
  }
1748
1852
  });
1749
1853
  }
1750
- setPan(channelNumber, pan) {
1854
+ setPan(channelNumber, pan, scheduleTime) {
1751
1855
  const channel = this.channels[channelNumber];
1752
1856
  channel.state.pan = pan / 127;
1753
- this.updateChannelVolume(channel);
1754
- this.setKeyBasedPan(channel);
1857
+ this.updateChannelVolume(channel, scheduleTime);
1858
+ this.setKeyBasedPan(channel, scheduleTime);
1755
1859
  }
1756
- setExpression(channelNumber, expression) {
1860
+ setExpression(channelNumber, expression, scheduleTime) {
1757
1861
  const channel = this.channels[channelNumber];
1758
1862
  channel.state.expression = expression / 127;
1759
- this.updateChannelVolume(channel);
1863
+ this.updateChannelVolume(channel, scheduleTime);
1760
1864
  }
1761
1865
  setBankLSB(channelNumber, lsb) {
1762
1866
  this.channels[channelNumber].bankLSB = lsb;
@@ -1791,8 +1895,7 @@ class Midy {
1791
1895
  channel.state.sostenutoPedal = value / 127;
1792
1896
  if (64 <= value) {
1793
1897
  const now = this.audioContext.currentTime;
1794
- const activeNotes = this.getActiveNotes(channel, now);
1795
- channel.sostenutoNotes = new Map(activeNotes);
1898
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1796
1899
  }
1797
1900
  else {
1798
1901
  this.releaseSostenutoPedal(channelNumber, value);
@@ -1832,7 +1935,7 @@ class Midy {
1832
1935
  continue;
1833
1936
  if (note.startTime < now)
1834
1937
  continue;
1835
- this.setVolumeEnvelope(channel, note, 0);
1938
+ this.setVolumeEnvelope(channel, note);
1836
1939
  }
1837
1940
  });
1838
1941
  }
@@ -1848,7 +1951,7 @@ class Midy {
1848
1951
  this.setPortamentoStartFilterEnvelope(channel, note);
1849
1952
  }
1850
1953
  else {
1851
- this.setFilterEnvelope(channel, note, 0);
1954
+ this.setFilterEnvelope(channel, note);
1852
1955
  }
1853
1956
  }
1854
1957
  });
@@ -1861,7 +1964,7 @@ class Midy {
1861
1964
  const note = noteList[i];
1862
1965
  if (!note)
1863
1966
  continue;
1864
- this.setVolumeEnvelope(channel, note, 0);
1967
+ this.setVolumeEnvelope(channel, note);
1865
1968
  }
1866
1969
  });
1867
1970
  }
@@ -2173,7 +2276,10 @@ class Midy {
2173
2276
  switch (data[3]) {
2174
2277
  case 8:
2175
2278
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2176
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2279
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2280
+ case 9:
2281
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2282
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false);
2177
2283
  default:
2178
2284
  console.warn(`Unsupported Exclusive Message: ${data}`);
2179
2285
  }
@@ -2235,8 +2341,10 @@ class Midy {
2235
2341
  case 8:
2236
2342
  switch (data[3]) {
2237
2343
  case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2238
- // TODO: realtime
2239
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2344
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true);
2345
+ case 9:
2346
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2347
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true);
2240
2348
  default:
2241
2349
  console.warn(`Unsupported Exclusive Message: ${data}`);
2242
2350
  }
@@ -2506,8 +2614,8 @@ class Midy {
2506
2614
  }
2507
2615
  return bitmap;
2508
2616
  }
2509
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2510
- if (data.length < 18) {
2617
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2618
+ if (data.length < 19) {
2511
2619
  console.error("Data length is too short");
2512
2620
  return;
2513
2621
  }
@@ -2515,67 +2623,92 @@ class Midy {
2515
2623
  for (let i = 0; i < channelBitmap.length; i++) {
2516
2624
  if (!channelBitmap[i])
2517
2625
  continue;
2626
+ const channel = this.channels[i];
2518
2627
  for (let j = 0; j < 12; j++) {
2519
- const value = data[j + 7] - 64; // cent
2520
- this.channels[i].scaleOctaveTuningTable[j] = value;
2628
+ const centValue = data[j + 7] - 64;
2629
+ channel.scaleOctaveTuningTable[j] = centValue;
2521
2630
  }
2631
+ if (realtime)
2632
+ this.updateChannelDetune(channel);
2522
2633
  }
2523
2634
  }
2524
- applyDestinationSettings(channel, note, table) {
2525
- if (table[0] !== 64) {
2526
- const polyphonicKeyPressure = (0 < note.pressure)
2527
- ? channel.polyphonicKeyPressureTable[0] * note.pressure
2528
- : 0;
2529
- const pressure = (polyphonicKeyPressure - 64) / 37.5; // 2400 / 64;
2530
- this.updateDetune(channel, note, pressure);
2635
+ handleScaleOctaveTuning2ByteFormatSysEx(data, realtime) {
2636
+ if (data.length < 31) {
2637
+ console.error("Data length is too short");
2638
+ return;
2531
2639
  }
2532
- if (!note.portamento) {
2533
- if (table[1] !== 64) {
2534
- const channelPressure = channel.channelPressureTable[1] *
2535
- channel.state.channelPressure;
2536
- const polyphonicKeyPressure = (0 < note.pressure)
2537
- ? channel.polyphonicKeyPressureTable[1] * note.pressure
2538
- : 0;
2539
- const pressure = (channelPressure + polyphonicKeyPressure - 128) * 15;
2540
- this.setFilterEnvelope(channel, note, pressure);
2541
- }
2542
- if (table[2] !== 64) {
2543
- const channelPressure = channel.channelPressureTable[2] *
2544
- channel.state.channelPressure;
2545
- const polyphonicKeyPressure = (0 < note.pressure)
2546
- ? channel.polyphonicKeyPressureTable[2] * note.pressure
2547
- : 0;
2548
- const pressure = (channelPressure + polyphonicKeyPressure) / 128;
2549
- this.setVolumeEnvelope(channel, note, pressure);
2640
+ const channelBitmap = this.getChannelBitmap(data);
2641
+ for (let i = 0; i < channelBitmap.length; i++) {
2642
+ if (!channelBitmap[i])
2643
+ continue;
2644
+ const channel = this.channels[i];
2645
+ for (let j = 0; j < 12; j++) {
2646
+ const index = 7 + j * 2;
2647
+ const msb = data[index] & 0x7F;
2648
+ const lsb = data[index + 1] & 0x7F;
2649
+ const value14bit = msb * 128 + lsb;
2650
+ const centValue = (value14bit - 8192) / 8.192;
2651
+ channel.scaleOctaveTuningTable[j] = centValue;
2550
2652
  }
2551
- }
2552
- if (table[3] !== 0) {
2553
- const channelPressure = channel.channelPressureTable[3] *
2554
- channel.state.channelPressure;
2555
- const polyphonicKeyPressure = (0 < note.pressure)
2556
- ? channel.polyphonicKeyPressureTable[3] * note.pressure
2557
- : 0;
2558
- const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 600;
2559
- this.setModLfoToPitch(channel, note, pressure);
2560
- }
2561
- if (table[4] !== 0) {
2562
- const channelPressure = channel.channelPressureTable[4] *
2563
- channel.state.channelPressure;
2564
- const polyphonicKeyPressure = (0 < note.pressure)
2565
- ? channel.polyphonicKeyPressureTable[4] * note.pressure
2566
- : 0;
2567
- const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2568
- this.setModLfoToFilterFc(note, pressure);
2569
- }
2570
- if (table[5] !== 0) {
2571
- const channelPressure = channel.channelPressureTable[5] *
2572
- channel.state.channelPressure;
2573
- const polyphonicKeyPressure = (0 < note.pressure)
2574
- ? channel.polyphonicKeyPressureTable[5] * note.pressure
2575
- : 0;
2576
- const pressure = (channelPressure + polyphonicKeyPressure) / 254;
2577
- this.setModLfoToVolume(note, pressure);
2578
- }
2653
+ if (realtime)
2654
+ this.updateChannelDetune(channel);
2655
+ }
2656
+ }
2657
+ getPitchControl(channel, note) {
2658
+ const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2659
+ note.pressure;
2660
+ return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2661
+ }
2662
+ getFilterCutoffControl(channel, note) {
2663
+ const channelPressure = (channel.channelPressureTable[1] - 64) *
2664
+ channel.state.channelPressure;
2665
+ const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2666
+ note.pressure;
2667
+ return (channelPressure + polyphonicKeyPressure) * 15;
2668
+ }
2669
+ getAmplitudeControl(channel, note) {
2670
+ const channelPressure = channel.channelPressureTable[2] *
2671
+ channel.state.channelPressure;
2672
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2673
+ note.pressure;
2674
+ return (channelPressure + polyphonicKeyPressure) / 128;
2675
+ }
2676
+ getLFOPitchDepth(channel, note) {
2677
+ const channelPressure = channel.channelPressureTable[3] *
2678
+ channel.state.channelPressure;
2679
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2680
+ note.pressure;
2681
+ return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2682
+ }
2683
+ getLFOFilterDepth(channel, note) {
2684
+ const channelPressure = channel.channelPressureTable[4] *
2685
+ channel.state.channelPressure;
2686
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2687
+ note.pressure;
2688
+ return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2689
+ }
2690
+ getLFOAmplitudeDepth(channel, note) {
2691
+ const channelPressure = channel.channelPressureTable[5] *
2692
+ channel.state.channelPressure;
2693
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2694
+ note.pressure;
2695
+ return (channelPressure + polyphonicKeyPressure) / 254;
2696
+ }
2697
+ setControllerParameters(channel, note, table) {
2698
+ if (table[0] !== 64)
2699
+ this.updateDetune(channel, note);
2700
+ if (!note.portamento) {
2701
+ if (table[1] !== 64)
2702
+ this.setFilterEnvelope(channel, note);
2703
+ if (table[2] !== 64)
2704
+ this.setVolumeEnvelope(channel, note);
2705
+ }
2706
+ if (table[3] !== 0)
2707
+ this.setModLfoToPitch(channel, note);
2708
+ if (table[4] !== 0)
2709
+ this.setModLfoToFilterFc(channel, note);
2710
+ if (table[5] !== 0)
2711
+ this.setModLfoToVolume(channel, note);
2579
2712
  }
2580
2713
  handleChannelPressureSysEx(data, tableName) {
2581
2714
  const channelNumber = data[4];
@@ -2606,7 +2739,7 @@ class Midy {
2606
2739
  const note = noteList[i];
2607
2740
  if (!note)
2608
2741
  continue;
2609
- this.applyDestinationSettings(channel, note, table);
2742
+ this.setControllerParameters(channel, note, table);
2610
2743
  }
2611
2744
  });
2612
2745
  }
@@ -2670,10 +2803,6 @@ Object.defineProperty(Midy, "channelSettings", {
2670
2803
  value: {
2671
2804
  currentBufferSource: null,
2672
2805
  detune: 0,
2673
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2674
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2675
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2676
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2677
2806
  program: 0,
2678
2807
  bank: 121 * 128,
2679
2808
  bankMSB: 121,