@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.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,
@@ -287,6 +345,18 @@ export class Midy {
287
345
  writable: true,
288
346
  value: this.initSoundFontTable()
289
347
  });
348
+ Object.defineProperty(this, "audioBufferCounter", {
349
+ enumerable: true,
350
+ configurable: true,
351
+ writable: true,
352
+ value: new Map()
353
+ });
354
+ Object.defineProperty(this, "audioBufferCache", {
355
+ enumerable: true,
356
+ configurable: true,
357
+ writable: true,
358
+ value: new Map()
359
+ });
290
360
  Object.defineProperty(this, "isPlaying", {
291
361
  enumerable: true,
292
362
  configurable: true,
@@ -339,7 +409,7 @@ export class Midy {
339
409
  enumerable: true,
340
410
  configurable: true,
341
411
  writable: true,
342
- value: new Map()
412
+ value: new SparseMap(128)
343
413
  });
344
414
  Object.defineProperty(this, "defaultOptions", {
345
415
  enumerable: true,
@@ -379,7 +449,7 @@ export class Midy {
379
449
  initSoundFontTable() {
380
450
  const table = new Array(128);
381
451
  for (let i = 0; i < 128; i++) {
382
- table[i] = new Map();
452
+ table[i] = new SparseMap(128);
383
453
  }
384
454
  return table;
385
455
  }
@@ -433,8 +503,12 @@ export class Midy {
433
503
  state: new ControllerState(),
434
504
  controlTable: this.initControlTable(),
435
505
  ...this.setChannelAudioNodes(audioContext),
436
- scheduledNotes: new Map(),
437
- sostenutoNotes: new Map(),
506
+ scheduledNotes: new SparseMap(128),
507
+ sostenutoNotes: new SparseMap(128),
508
+ scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
509
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
510
+ polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
511
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
438
512
  };
439
513
  });
440
514
  return channels;
@@ -468,9 +542,8 @@ export class Midy {
468
542
  return audioBuffer;
469
543
  }
470
544
  }
471
- async createNoteBufferNode(voiceParams, isSF3) {
545
+ createNoteBufferNode(audioBuffer, voiceParams) {
472
546
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
473
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
474
547
  bufferSource.buffer = audioBuffer;
475
548
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
476
549
  if (bufferSource.loop) {
@@ -502,10 +575,11 @@ export class Midy {
502
575
  const event = this.timeline[queueIndex];
503
576
  if (event.startTime > t + this.lookAhead)
504
577
  break;
578
+ const startTime = event.startTime + this.startDelay - offset;
505
579
  switch (event.type) {
506
580
  case "noteOn":
507
581
  if (event.velocity !== 0) {
508
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
582
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
509
583
  break;
510
584
  }
511
585
  /* falls through */
@@ -513,29 +587,29 @@ export class Midy {
513
587
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
514
588
  if (portamentoTarget)
515
589
  portamentoTarget.portamento = true;
516
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
590
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, portamentoTarget?.noteNumber, false);
517
591
  if (notePromise) {
518
592
  this.notePromises.push(notePromise);
519
593
  }
520
594
  break;
521
595
  }
522
596
  case "noteAftertouch":
523
- this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount);
597
+ this.handlePolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
524
598
  break;
525
599
  case "controller":
526
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
600
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
527
601
  break;
528
602
  case "programChange":
529
- this.handleProgramChange(event.channel, event.programNumber);
603
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
530
604
  break;
531
605
  case "channelAftertouch":
532
- this.handleChannelPressure(event.channel, event.amount);
606
+ this.handleChannelPressure(event.channel, event.amount, startTime);
533
607
  break;
534
608
  case "pitchBend":
535
- this.setPitchBend(event.channel, event.value + 8192);
609
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
536
610
  break;
537
611
  case "sysEx":
538
- this.handleSysEx(event.data);
612
+ this.handleSysEx(event.data, startTime);
539
613
  }
540
614
  queueIndex++;
541
615
  }
@@ -562,6 +636,7 @@ export class Midy {
562
636
  await Promise.all(this.notePromises);
563
637
  this.notePromises = [];
564
638
  this.exclusiveClassMap.clear();
639
+ this.audioBufferCache.clear();
565
640
  resolve();
566
641
  return;
567
642
  }
@@ -577,8 +652,9 @@ export class Midy {
577
652
  }
578
653
  else if (this.isStopping) {
579
654
  await this.stopNotes(0, true);
580
- this.exclusiveClassMap.clear();
581
655
  this.notePromises = [];
656
+ this.exclusiveClassMap.clear();
657
+ this.audioBufferCache.clear();
582
658
  resolve();
583
659
  this.isStopping = false;
584
660
  this.isPaused = false;
@@ -609,6 +685,9 @@ export class Midy {
609
685
  secondToTicks(second, secondsPerBeat) {
610
686
  return second * this.ticksPerBeat / secondsPerBeat;
611
687
  }
688
+ getAudioBufferId(programNumber, noteNumber, velocity) {
689
+ return `${programNumber}:${noteNumber}:${velocity}`;
690
+ }
612
691
  extractMidiData(midi) {
613
692
  const instruments = new Set();
614
693
  const timeline = [];
@@ -630,6 +709,8 @@ export class Midy {
630
709
  switch (event.type) {
631
710
  case "noteOn": {
632
711
  const channel = tmpChannels[event.channel];
712
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
713
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
633
714
  if (channel.programNumber < 0) {
634
715
  channel.programNumber = event.programNumber;
635
716
  switch (channel.bankMSB) {
@@ -679,6 +760,10 @@ export class Midy {
679
760
  timeline.push(event);
680
761
  }
681
762
  }
763
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
764
+ if (count === 1)
765
+ this.audioBufferCounter.delete(audioBufferId);
766
+ }
682
767
  const priority = {
683
768
  controller: 0,
684
769
  sysEx: 1,
@@ -771,8 +856,20 @@ export class Midy {
771
856
  const now = this.audioContext.currentTime;
772
857
  return this.resumeTime + now - this.startTime - this.startDelay;
773
858
  }
859
+ processScheduledNotes(channel, scheduleTime, callback) {
860
+ channel.scheduledNotes.forEach((noteList) => {
861
+ for (let i = 0; i < noteList.length; i++) {
862
+ const note = noteList[i];
863
+ if (!note)
864
+ continue;
865
+ if (scheduleTime < note.startTime)
866
+ continue;
867
+ callback(note);
868
+ }
869
+ });
870
+ }
774
871
  getActiveNotes(channel, time) {
775
- const activeNotes = new Map();
872
+ const activeNotes = new SparseMap(128);
776
873
  channel.scheduledNotes.forEach((noteList) => {
777
874
  const activeNote = this.getActiveNote(noteList, time);
778
875
  if (activeNote) {
@@ -952,14 +1049,15 @@ export class Midy {
952
1049
  const note = noteList[i];
953
1050
  if (!note)
954
1051
  continue;
955
- this.updateDetune(channel, note, 0);
1052
+ this.updateDetune(channel, note);
956
1053
  }
957
1054
  });
958
1055
  }
959
- updateDetune(channel, note, pressure) {
1056
+ updateDetune(channel, note) {
960
1057
  const now = this.audioContext.currentTime;
961
1058
  const noteDetune = this.calcNoteDetune(channel, note);
962
- const detune = channel.detune + noteDetune + pressure;
1059
+ const pitchControl = this.getPitchControl(channel, note);
1060
+ const detune = channel.detune + noteDetune + pitchControl;
963
1061
  note.bufferSource.detune
964
1062
  .cancelScheduledValues(now)
965
1063
  .setValueAtTime(detune, now);
@@ -981,12 +1079,12 @@ export class Midy {
981
1079
  .setValueAtTime(0, volDelay)
982
1080
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
983
1081
  }
984
- setVolumeEnvelope(channel, note, pressure) {
1082
+ setVolumeEnvelope(channel, note) {
985
1083
  const now = this.audioContext.currentTime;
986
1084
  const state = channel.state;
987
1085
  const { voiceParams, startTime } = note;
988
1086
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
989
- (1 + pressure);
1087
+ (1 + this.getAmplitudeControl(channel, note));
990
1088
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
991
1089
  const volDelay = startTime + voiceParams.volDelay;
992
1090
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
@@ -1000,20 +1098,20 @@ export class Midy {
1000
1098
  .setValueAtTime(attackVolume, volHold)
1001
1099
  .linearRampToValueAtTime(sustainVolume, volDecay);
1002
1100
  }
1003
- setPitchEnvelope(note) {
1004
- const now = this.audioContext.currentTime;
1101
+ setPitchEnvelope(note, scheduleTime) {
1102
+ scheduleTime ??= this.audioContext.currentTime;
1005
1103
  const { voiceParams } = note;
1006
1104
  const baseRate = voiceParams.playbackRate;
1007
1105
  note.bufferSource.playbackRate
1008
- .cancelScheduledValues(now)
1009
- .setValueAtTime(baseRate, now);
1106
+ .cancelScheduledValues(scheduleTime)
1107
+ .setValueAtTime(baseRate, scheduleTime);
1010
1108
  const modEnvToPitch = voiceParams.modEnvToPitch;
1011
1109
  if (modEnvToPitch === 0)
1012
1110
  return;
1013
1111
  const basePitch = this.rateToCent(baseRate);
1014
1112
  const peekPitch = basePitch + modEnvToPitch;
1015
1113
  const peekRate = this.centToRate(peekPitch);
1016
- const modDelay = startTime + voiceParams.modDelay;
1114
+ const modDelay = note.startTime + voiceParams.modDelay;
1017
1115
  const modAttack = modDelay + voiceParams.modAttack;
1018
1116
  const modHold = modAttack + voiceParams.modHold;
1019
1117
  const modDecay = modHold + voiceParams.modDecay;
@@ -1050,13 +1148,14 @@ export class Midy {
1050
1148
  .setValueAtTime(adjustedBaseFreq, modDelay)
1051
1149
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1052
1150
  }
1053
- setFilterEnvelope(channel, note, pressure) {
1151
+ setFilterEnvelope(channel, note) {
1054
1152
  const now = this.audioContext.currentTime;
1055
1153
  const state = channel.state;
1056
1154
  const { voiceParams, noteNumber, startTime } = note;
1057
1155
  const softPedalFactor = 1 -
1058
1156
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1059
- const baseCent = voiceParams.initialFilterFc + pressure;
1157
+ const baseCent = voiceParams.initialFilterFc +
1158
+ this.getFilterCutoffControl(channel, note);
1060
1159
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1061
1160
  state.brightness * 2;
1062
1161
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
@@ -1087,9 +1186,9 @@ export class Midy {
1087
1186
  gain: voiceParams.modLfoToFilterFc,
1088
1187
  });
1089
1188
  note.modulationDepth = new GainNode(this.audioContext);
1090
- this.setModLfoToPitch(channel, note, 0);
1189
+ this.setModLfoToPitch(channel, note);
1091
1190
  note.volumeDepth = new GainNode(this.audioContext);
1092
- this.setModLfoToVolume(note, 0);
1191
+ this.setModLfoToVolume(channel, note);
1093
1192
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1094
1193
  note.modulationLFO.connect(note.filterDepth);
1095
1194
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1110,12 +1209,31 @@ export class Midy {
1110
1209
  note.vibratoLFO.connect(note.vibratoDepth);
1111
1210
  note.vibratoDepth.connect(note.bufferSource.detune);
1112
1211
  }
1212
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1213
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1214
+ const cache = this.audioBufferCache.get(audioBufferId);
1215
+ if (cache) {
1216
+ cache.counter += 1;
1217
+ if (cache.maxCount <= cache.counter) {
1218
+ this.audioBufferCache.delete(audioBufferId);
1219
+ }
1220
+ return cache.audioBuffer;
1221
+ }
1222
+ else {
1223
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1224
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1225
+ const cache = { audioBuffer, maxCount, counter: 1 };
1226
+ this.audioBufferCache.set(audioBufferId, cache);
1227
+ return audioBuffer;
1228
+ }
1229
+ }
1113
1230
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1114
1231
  const state = channel.state;
1115
1232
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1116
1233
  const voiceParams = voice.getAllParams(controllerState);
1117
1234
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1118
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1235
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1236
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1119
1237
  note.volumeNode = new GainNode(this.audioContext);
1120
1238
  note.gainL = new GainNode(this.audioContext);
1121
1239
  note.gainR = new GainNode(this.audioContext);
@@ -1131,8 +1249,8 @@ export class Midy {
1131
1249
  }
1132
1250
  else {
1133
1251
  note.portamento = false;
1134
- this.setVolumeEnvelope(channel, note, 0);
1135
- this.setFilterEnvelope(channel, note, 0);
1252
+ this.setVolumeEnvelope(channel, note);
1253
+ this.setFilterEnvelope(channel, note);
1136
1254
  }
1137
1255
  if (0 < state.vibratoDepth) {
1138
1256
  this.startVibrato(channel, note, startTime);
@@ -1175,10 +1293,10 @@ export class Midy {
1175
1293
  if (soundFontIndex === undefined)
1176
1294
  return;
1177
1295
  const soundFont = this.soundFonts[soundFontIndex];
1178
- const isSF3 = soundFont.parsed.info.version.major === 3;
1179
1296
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1180
1297
  if (!voice)
1181
1298
  return;
1299
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1182
1300
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1183
1301
  note.gainL.connect(channel.gainL);
1184
1302
  note.gainR.connect(channel.gainR);
@@ -1344,15 +1462,16 @@ export class Midy {
1344
1462
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1345
1463
  }
1346
1464
  }
1347
- handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
1348
- const now = this.audioContext.currentTime;
1465
+ handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure, startTime) {
1466
+ if (!startTime)
1467
+ startTime = this.audioContext.currentTime;
1349
1468
  const channel = this.channels[channelNumber];
1350
1469
  channel.state.polyphonicKeyPressure = pressure / 127;
1351
1470
  const table = channel.polyphonicKeyPressureTable;
1352
- const activeNotes = this.getActiveNotes(channel, now);
1471
+ const activeNotes = this.getActiveNotes(channel, startTime);
1353
1472
  if (activeNotes.has(noteNumber)) {
1354
1473
  const note = activeNotes.get(noteNumber);
1355
- this.applyDestinationSettings(channel, note, table);
1474
+ this.setControllerParameters(channel, note, table);
1356
1475
  }
1357
1476
  // this.applyVoiceParams(channel, 10);
1358
1477
  }
@@ -1361,7 +1480,9 @@ export class Midy {
1361
1480
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1362
1481
  channel.program = program;
1363
1482
  }
1364
- handleChannelPressure(channelNumber, value) {
1483
+ handleChannelPressure(channelNumber, value, startTime) {
1484
+ if (!startTime)
1485
+ startTime = this.audioContext.currentTime;
1365
1486
  const channel = this.channels[channelNumber];
1366
1487
  const prev = channel.state.channelPressure;
1367
1488
  const next = value / 127;
@@ -1371,13 +1492,8 @@ export class Midy {
1371
1492
  channel.detune += pressureDepth * (next - prev);
1372
1493
  }
1373
1494
  const table = channel.channelPressureTable;
1374
- channel.scheduledNotes.forEach((noteList) => {
1375
- for (let i = 0; i < noteList.length; i++) {
1376
- const note = noteList[i];
1377
- if (!note)
1378
- continue;
1379
- this.applyDestinationSettings(channel, note, table);
1380
- }
1495
+ this.getActiveNotes(channel, startTime).forEach((note) => {
1496
+ this.setControllerParameters(channel, note, table);
1381
1497
  });
1382
1498
  // this.applyVoiceParams(channel, 13);
1383
1499
  }
@@ -1395,9 +1511,10 @@ export class Midy {
1395
1511
  this.updateChannelDetune(channel);
1396
1512
  this.applyVoiceParams(channel, 14);
1397
1513
  }
1398
- setModLfoToPitch(channel, note, pressure) {
1514
+ setModLfoToPitch(channel, note) {
1399
1515
  const now = this.audioContext.currentTime;
1400
- const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1516
+ const modLfoToPitch = note.voiceParams.modLfoToPitch +
1517
+ this.getLFOPitchDepth(channel, note);
1401
1518
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1402
1519
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1403
1520
  note.modulationDepth.gain
@@ -1414,18 +1531,20 @@ export class Midy {
1414
1531
  .cancelScheduledValues(now)
1415
1532
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1416
1533
  }
1417
- setModLfoToFilterFc(note, pressure) {
1534
+ setModLfoToFilterFc(channel, note) {
1418
1535
  const now = this.audioContext.currentTime;
1419
- const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1536
+ const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
1537
+ this.getLFOFilterDepth(channel, note);
1420
1538
  note.filterDepth.gain
1421
1539
  .cancelScheduledValues(now)
1422
1540
  .setValueAtTime(modLfoToFilterFc, now);
1423
1541
  }
1424
- setModLfoToVolume(note, pressure) {
1542
+ setModLfoToVolume(channel, note) {
1425
1543
  const now = this.audioContext.currentTime;
1426
1544
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1427
1545
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1428
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1546
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1547
+ (1 + this.getLFOAmplitudeDepth(channel, note));
1429
1548
  note.volumeDepth.gain
1430
1549
  .cancelScheduledValues(now)
1431
1550
  .setValueAtTime(volumeDepth, now);
@@ -1509,7 +1628,7 @@ export class Midy {
1509
1628
  return {
1510
1629
  modLfoToPitch: (channel, note, _prevValue) => {
1511
1630
  if (0 < channel.state.modulationDepth) {
1512
- this.setModLfoToPitch(channel, note, 0);
1631
+ this.setModLfoToPitch(channel, note);
1513
1632
  }
1514
1633
  },
1515
1634
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1519,12 +1638,12 @@ export class Midy {
1519
1638
  },
1520
1639
  modLfoToFilterFc: (channel, note, _prevValue) => {
1521
1640
  if (0 < channel.state.modulationDepth) {
1522
- this.setModLfoToFilterFc(note, 0);
1641
+ this.setModLfoToFilterFc(channel, note);
1523
1642
  }
1524
1643
  },
1525
1644
  modLfoToVolume: (channel, note, _prevValue) => {
1526
1645
  if (0 < channel.state.modulationDepth) {
1527
- this.setModLfoToVolume(note, 0);
1646
+ this.setModLfoToVolume(channel, note);
1528
1647
  }
1529
1648
  },
1530
1649
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1594,7 +1713,7 @@ export class Midy {
1594
1713
  this.setPortamentoStartFilterEnvelope(channel, note);
1595
1714
  }
1596
1715
  else {
1597
- this.setFilterEnvelope(channel, note, 0);
1716
+ this.setFilterEnvelope(channel, note);
1598
1717
  }
1599
1718
  this.setPitchEnvelope(note);
1600
1719
  }
@@ -1608,7 +1727,7 @@ export class Midy {
1608
1727
  if (key in voiceParams)
1609
1728
  noteVoiceParams[key] = voiceParams[key];
1610
1729
  }
1611
- this.setVolumeEnvelope(channel, note, 0);
1730
+ this.setVolumeEnvelope(channel, note);
1612
1731
  }
1613
1732
  }
1614
1733
  }
@@ -1652,10 +1771,10 @@ export class Midy {
1652
1771
  127: this.polyOn,
1653
1772
  };
1654
1773
  }
1655
- handleControlChange(channelNumber, controllerType, value) {
1774
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1656
1775
  const handler = this.controlChangeHandlers[controllerType];
1657
1776
  if (handler) {
1658
- handler.call(this, channelNumber, value);
1777
+ handler.call(this, channelNumber, value, startTime);
1659
1778
  const channel = this.channels[channelNumber];
1660
1779
  this.applyVoiceParams(channel, controllerType + 128);
1661
1780
  this.applyControlTable(channel, controllerType);
@@ -1667,55 +1786,45 @@ export class Midy {
1667
1786
  setBankMSB(channelNumber, msb) {
1668
1787
  this.channels[channelNumber].bankMSB = msb;
1669
1788
  }
1670
- updateModulation(channel) {
1671
- const now = this.audioContext.currentTime;
1789
+ updateModulation(channel, scheduleTime) {
1790
+ scheduleTime ??= this.audioContext.currentTime;
1672
1791
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1673
- channel.scheduledNotes.forEach((noteList) => {
1674
- for (let i = 0; i < noteList.length; i++) {
1675
- const note = noteList[i];
1676
- if (!note)
1677
- continue;
1678
- if (note.modulationDepth) {
1679
- note.modulationDepth.gain.setValueAtTime(depth, now);
1680
- }
1681
- else {
1682
- this.setPitchEnvelope(note);
1683
- this.startModulation(channel, note, now);
1684
- }
1792
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1793
+ if (note.modulationDepth) {
1794
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1795
+ }
1796
+ else {
1797
+ this.setPitchEnvelope(note, scheduleTime);
1798
+ this.startModulation(channel, note, scheduleTime);
1685
1799
  }
1686
1800
  });
1687
1801
  }
1688
- setModulationDepth(channelNumber, modulation) {
1802
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1689
1803
  const channel = this.channels[channelNumber];
1690
1804
  channel.state.modulationDepth = modulation / 127;
1691
- this.updateModulation(channel);
1805
+ this.updateModulation(channel, scheduleTime);
1692
1806
  }
1693
1807
  setPortamentoTime(channelNumber, portamentoTime) {
1694
1808
  const channel = this.channels[channelNumber];
1695
1809
  const factor = 5 * Math.log(10) / 127;
1696
1810
  channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1697
1811
  }
1698
- setKeyBasedVolume(channel) {
1699
- const now = this.audioContext.currentTime;
1700
- channel.scheduledNotes.forEach((noteList) => {
1701
- for (let i = 0; i < noteList.length; i++) {
1702
- const note = noteList[i];
1703
- if (!note)
1704
- continue;
1705
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1706
- if (keyBasedValue === 0)
1707
- continue;
1812
+ setKeyBasedVolume(channel, scheduleTime) {
1813
+ scheduleTime ??= this.audioContext.currentTime;
1814
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1815
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1816
+ if (keyBasedValue !== 0) {
1708
1817
  note.volumeNode.gain
1709
- .cancelScheduledValues(now)
1710
- .setValueAtTime(1 + keyBasedValue, now);
1818
+ .cancelScheduledValues(scheduleTime)
1819
+ .setValueAtTime(1 + keyBasedValue, scheduleTime);
1711
1820
  }
1712
1821
  });
1713
1822
  }
1714
- setVolume(channelNumber, volume) {
1823
+ setVolume(channelNumber, volume, scheduleTime) {
1715
1824
  const channel = this.channels[channelNumber];
1716
1825
  channel.state.volume = volume / 127;
1717
- this.updateChannelVolume(channel);
1718
- this.setKeyBasedVolume(channel);
1826
+ this.updateChannelVolume(channel, scheduleTime);
1827
+ this.setKeyBasedVolume(channel, scheduleTime);
1719
1828
  }
1720
1829
  panToGain(pan) {
1721
1830
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1724,36 +1833,31 @@ export class Midy {
1724
1833
  gainRight: Math.sin(theta),
1725
1834
  };
1726
1835
  }
1727
- setKeyBasedPan(channel) {
1728
- const now = this.audioContext.currentTime;
1729
- channel.scheduledNotes.forEach((noteList) => {
1730
- for (let i = 0; i < noteList.length; i++) {
1731
- const note = noteList[i];
1732
- if (!note)
1733
- continue;
1734
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1735
- if (keyBasedValue === 0)
1736
- continue;
1836
+ setKeyBasedPan(channel, scheduleTime) {
1837
+ scheduleTime ??= this.audioContext.currentTime;
1838
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1839
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1840
+ if (keyBasedValue !== 0) {
1737
1841
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1738
1842
  note.gainL.gain
1739
- .cancelScheduledValues(now)
1740
- .setValueAtTime(gainLeft, now);
1843
+ .cancelScheduledValues(scheduleTime)
1844
+ .setValueAtTime(gainLeft, scheduleTime);
1741
1845
  note.gainR.gain
1742
- .cancelScheduledValues(now)
1743
- .setValueAtTime(gainRight, now);
1846
+ .cancelScheduledValues(scheduleTime)
1847
+ .setValueAtTime(gainRight, scheduleTime);
1744
1848
  }
1745
1849
  });
1746
1850
  }
1747
- setPan(channelNumber, pan) {
1851
+ setPan(channelNumber, pan, scheduleTime) {
1748
1852
  const channel = this.channels[channelNumber];
1749
1853
  channel.state.pan = pan / 127;
1750
- this.updateChannelVolume(channel);
1751
- this.setKeyBasedPan(channel);
1854
+ this.updateChannelVolume(channel, scheduleTime);
1855
+ this.setKeyBasedPan(channel, scheduleTime);
1752
1856
  }
1753
- setExpression(channelNumber, expression) {
1857
+ setExpression(channelNumber, expression, scheduleTime) {
1754
1858
  const channel = this.channels[channelNumber];
1755
1859
  channel.state.expression = expression / 127;
1756
- this.updateChannelVolume(channel);
1860
+ this.updateChannelVolume(channel, scheduleTime);
1757
1861
  }
1758
1862
  setBankLSB(channelNumber, lsb) {
1759
1863
  this.channels[channelNumber].bankLSB = lsb;
@@ -1788,8 +1892,7 @@ export class Midy {
1788
1892
  channel.state.sostenutoPedal = value / 127;
1789
1893
  if (64 <= value) {
1790
1894
  const now = this.audioContext.currentTime;
1791
- const activeNotes = this.getActiveNotes(channel, now);
1792
- channel.sostenutoNotes = new Map(activeNotes);
1895
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1793
1896
  }
1794
1897
  else {
1795
1898
  this.releaseSostenutoPedal(channelNumber, value);
@@ -1829,7 +1932,7 @@ export class Midy {
1829
1932
  continue;
1830
1933
  if (note.startTime < now)
1831
1934
  continue;
1832
- this.setVolumeEnvelope(channel, note, 0);
1935
+ this.setVolumeEnvelope(channel, note);
1833
1936
  }
1834
1937
  });
1835
1938
  }
@@ -1845,7 +1948,7 @@ export class Midy {
1845
1948
  this.setPortamentoStartFilterEnvelope(channel, note);
1846
1949
  }
1847
1950
  else {
1848
- this.setFilterEnvelope(channel, note, 0);
1951
+ this.setFilterEnvelope(channel, note);
1849
1952
  }
1850
1953
  }
1851
1954
  });
@@ -1858,7 +1961,7 @@ export class Midy {
1858
1961
  const note = noteList[i];
1859
1962
  if (!note)
1860
1963
  continue;
1861
- this.setVolumeEnvelope(channel, note, 0);
1964
+ this.setVolumeEnvelope(channel, note);
1862
1965
  }
1863
1966
  });
1864
1967
  }
@@ -2170,7 +2273,10 @@ export class Midy {
2170
2273
  switch (data[3]) {
2171
2274
  case 8:
2172
2275
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2173
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2276
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2277
+ case 9:
2278
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2279
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false);
2174
2280
  default:
2175
2281
  console.warn(`Unsupported Exclusive Message: ${data}`);
2176
2282
  }
@@ -2232,8 +2338,10 @@ export class Midy {
2232
2338
  case 8:
2233
2339
  switch (data[3]) {
2234
2340
  case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2235
- // TODO: realtime
2236
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2341
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true);
2342
+ case 9:
2343
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2344
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true);
2237
2345
  default:
2238
2346
  console.warn(`Unsupported Exclusive Message: ${data}`);
2239
2347
  }
@@ -2503,8 +2611,8 @@ export class Midy {
2503
2611
  }
2504
2612
  return bitmap;
2505
2613
  }
2506
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2507
- if (data.length < 18) {
2614
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2615
+ if (data.length < 19) {
2508
2616
  console.error("Data length is too short");
2509
2617
  return;
2510
2618
  }
@@ -2512,67 +2620,92 @@ export class Midy {
2512
2620
  for (let i = 0; i < channelBitmap.length; i++) {
2513
2621
  if (!channelBitmap[i])
2514
2622
  continue;
2623
+ const channel = this.channels[i];
2515
2624
  for (let j = 0; j < 12; j++) {
2516
- const value = data[j + 7] - 64; // cent
2517
- this.channels[i].scaleOctaveTuningTable[j] = value;
2625
+ const centValue = data[j + 7] - 64;
2626
+ channel.scaleOctaveTuningTable[j] = centValue;
2518
2627
  }
2628
+ if (realtime)
2629
+ this.updateChannelDetune(channel);
2519
2630
  }
2520
2631
  }
2521
- applyDestinationSettings(channel, note, table) {
2522
- if (table[0] !== 64) {
2523
- const polyphonicKeyPressure = (0 < note.pressure)
2524
- ? channel.polyphonicKeyPressureTable[0] * note.pressure
2525
- : 0;
2526
- const pressure = (polyphonicKeyPressure - 64) / 37.5; // 2400 / 64;
2527
- this.updateDetune(channel, note, pressure);
2632
+ handleScaleOctaveTuning2ByteFormatSysEx(data, realtime) {
2633
+ if (data.length < 31) {
2634
+ console.error("Data length is too short");
2635
+ return;
2528
2636
  }
2529
- if (!note.portamento) {
2530
- if (table[1] !== 64) {
2531
- const channelPressure = channel.channelPressureTable[1] *
2532
- channel.state.channelPressure;
2533
- const polyphonicKeyPressure = (0 < note.pressure)
2534
- ? channel.polyphonicKeyPressureTable[1] * note.pressure
2535
- : 0;
2536
- const pressure = (channelPressure + polyphonicKeyPressure - 128) * 15;
2537
- this.setFilterEnvelope(channel, note, pressure);
2538
- }
2539
- if (table[2] !== 64) {
2540
- const channelPressure = channel.channelPressureTable[2] *
2541
- channel.state.channelPressure;
2542
- const polyphonicKeyPressure = (0 < note.pressure)
2543
- ? channel.polyphonicKeyPressureTable[2] * note.pressure
2544
- : 0;
2545
- const pressure = (channelPressure + polyphonicKeyPressure) / 128;
2546
- this.setVolumeEnvelope(channel, note, pressure);
2637
+ const channelBitmap = this.getChannelBitmap(data);
2638
+ for (let i = 0; i < channelBitmap.length; i++) {
2639
+ if (!channelBitmap[i])
2640
+ continue;
2641
+ const channel = this.channels[i];
2642
+ for (let j = 0; j < 12; j++) {
2643
+ const index = 7 + j * 2;
2644
+ const msb = data[index] & 0x7F;
2645
+ const lsb = data[index + 1] & 0x7F;
2646
+ const value14bit = msb * 128 + lsb;
2647
+ const centValue = (value14bit - 8192) / 8.192;
2648
+ channel.scaleOctaveTuningTable[j] = centValue;
2547
2649
  }
2548
- }
2549
- if (table[3] !== 0) {
2550
- const channelPressure = channel.channelPressureTable[3] *
2551
- channel.state.channelPressure;
2552
- const polyphonicKeyPressure = (0 < note.pressure)
2553
- ? channel.polyphonicKeyPressureTable[3] * note.pressure
2554
- : 0;
2555
- const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 600;
2556
- this.setModLfoToPitch(channel, note, pressure);
2557
- }
2558
- if (table[4] !== 0) {
2559
- const channelPressure = channel.channelPressureTable[4] *
2560
- channel.state.channelPressure;
2561
- const polyphonicKeyPressure = (0 < note.pressure)
2562
- ? channel.polyphonicKeyPressureTable[4] * note.pressure
2563
- : 0;
2564
- const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2565
- this.setModLfoToFilterFc(note, pressure);
2566
- }
2567
- if (table[5] !== 0) {
2568
- const channelPressure = channel.channelPressureTable[5] *
2569
- channel.state.channelPressure;
2570
- const polyphonicKeyPressure = (0 < note.pressure)
2571
- ? channel.polyphonicKeyPressureTable[5] * note.pressure
2572
- : 0;
2573
- const pressure = (channelPressure + polyphonicKeyPressure) / 254;
2574
- this.setModLfoToVolume(note, pressure);
2575
- }
2650
+ if (realtime)
2651
+ this.updateChannelDetune(channel);
2652
+ }
2653
+ }
2654
+ getPitchControl(channel, note) {
2655
+ const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[0] - 64) *
2656
+ note.pressure;
2657
+ return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
2658
+ }
2659
+ getFilterCutoffControl(channel, note) {
2660
+ const channelPressure = (channel.channelPressureTable[1] - 64) *
2661
+ channel.state.channelPressure;
2662
+ const polyphonicKeyPressure = (channel.polyphonicKeyPressureTable[1] - 64) *
2663
+ note.pressure;
2664
+ return (channelPressure + polyphonicKeyPressure) * 15;
2665
+ }
2666
+ getAmplitudeControl(channel, note) {
2667
+ const channelPressure = channel.channelPressureTable[2] *
2668
+ channel.state.channelPressure;
2669
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[2] *
2670
+ note.pressure;
2671
+ return (channelPressure + polyphonicKeyPressure) / 128;
2672
+ }
2673
+ getLFOPitchDepth(channel, note) {
2674
+ const channelPressure = channel.channelPressureTable[3] *
2675
+ channel.state.channelPressure;
2676
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[3] *
2677
+ note.pressure;
2678
+ return (channelPressure + polyphonicKeyPressure) / 254 * 600;
2679
+ }
2680
+ getLFOFilterDepth(channel, note) {
2681
+ const channelPressure = channel.channelPressureTable[4] *
2682
+ channel.state.channelPressure;
2683
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[4] *
2684
+ note.pressure;
2685
+ return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2686
+ }
2687
+ getLFOAmplitudeDepth(channel, note) {
2688
+ const channelPressure = channel.channelPressureTable[5] *
2689
+ channel.state.channelPressure;
2690
+ const polyphonicKeyPressure = channel.polyphonicKeyPressureTable[5] *
2691
+ note.pressure;
2692
+ return (channelPressure + polyphonicKeyPressure) / 254;
2693
+ }
2694
+ setControllerParameters(channel, note, table) {
2695
+ if (table[0] !== 64)
2696
+ this.updateDetune(channel, note);
2697
+ if (!note.portamento) {
2698
+ if (table[1] !== 64)
2699
+ this.setFilterEnvelope(channel, note);
2700
+ if (table[2] !== 64)
2701
+ this.setVolumeEnvelope(channel, note);
2702
+ }
2703
+ if (table[3] !== 0)
2704
+ this.setModLfoToPitch(channel, note);
2705
+ if (table[4] !== 0)
2706
+ this.setModLfoToFilterFc(channel, note);
2707
+ if (table[5] !== 0)
2708
+ this.setModLfoToVolume(channel, note);
2576
2709
  }
2577
2710
  handleChannelPressureSysEx(data, tableName) {
2578
2711
  const channelNumber = data[4];
@@ -2603,7 +2736,7 @@ export class Midy {
2603
2736
  const note = noteList[i];
2604
2737
  if (!note)
2605
2738
  continue;
2606
- this.applyDestinationSettings(channel, note, table);
2739
+ this.setControllerParameters(channel, note, table);
2607
2740
  }
2608
2741
  });
2609
2742
  }
@@ -2666,10 +2799,6 @@ Object.defineProperty(Midy, "channelSettings", {
2666
2799
  value: {
2667
2800
  currentBufferSource: null,
2668
2801
  detune: 0,
2669
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2670
- channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2671
- polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2672
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2673
2802
  program: 0,
2674
2803
  bank: 121 * 128,
2675
2804
  bankMSB: 121,