@marmooo/midy 0.2.3 → 0.2.5

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", {
@@ -89,6 +141,12 @@ class Note {
89
141
  writable: true,
90
142
  value: void 0
91
143
  });
144
+ Object.defineProperty(this, "pressure", {
145
+ enumerable: true,
146
+ configurable: true,
147
+ writable: true,
148
+ value: 0
149
+ });
92
150
  this.noteNumber = noteNumber;
93
151
  this.velocity = velocity;
94
152
  this.startTime = startTime;
@@ -100,7 +158,7 @@ class Note {
100
158
  const defaultControllerState = {
101
159
  noteOnVelocity: { type: 2, defaultValue: 0 },
102
160
  noteOnKeyNumber: { type: 3, defaultValue: 0 },
103
- polyPressure: { type: 10, defaultValue: 0 },
161
+ polyphonicKeyPressure: { type: 10, defaultValue: 0 },
104
162
  channelPressure: { type: 13, defaultValue: 0 },
105
163
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
106
164
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
@@ -284,6 +342,18 @@ class Midy {
284
342
  writable: true,
285
343
  value: this.initSoundFontTable()
286
344
  });
345
+ Object.defineProperty(this, "audioBufferCounter", {
346
+ enumerable: true,
347
+ configurable: true,
348
+ writable: true,
349
+ value: new Map()
350
+ });
351
+ Object.defineProperty(this, "audioBufferCache", {
352
+ enumerable: true,
353
+ configurable: true,
354
+ writable: true,
355
+ value: new Map()
356
+ });
287
357
  Object.defineProperty(this, "isPlaying", {
288
358
  enumerable: true,
289
359
  configurable: true,
@@ -336,7 +406,7 @@ class Midy {
336
406
  enumerable: true,
337
407
  configurable: true,
338
408
  writable: true,
339
- value: new Map()
409
+ value: new SparseMap(128)
340
410
  });
341
411
  Object.defineProperty(this, "defaultOptions", {
342
412
  enumerable: true,
@@ -376,7 +446,7 @@ class Midy {
376
446
  initSoundFontTable() {
377
447
  const table = new Array(128);
378
448
  for (let i = 0; i < 128; i++) {
379
- table[i] = new Map();
449
+ table[i] = new SparseMap(128);
380
450
  }
381
451
  return table;
382
452
  }
@@ -430,14 +500,8 @@ class Midy {
430
500
  state: new ControllerState(),
431
501
  controlTable: this.initControlTable(),
432
502
  ...this.setChannelAudioNodes(audioContext),
433
- scheduledNotes: new Map(),
434
- sostenutoNotes: new Map(),
435
- polyphonicKeyPressure: {
436
- ...this.constructor.controllerDestinationSettings,
437
- },
438
- channelPressure: {
439
- ...this.constructor.controllerDestinationSettings,
440
- },
503
+ scheduledNotes: new SparseMap(128),
504
+ sostenutoNotes: new SparseMap(128),
441
505
  };
442
506
  });
443
507
  return channels;
@@ -471,9 +535,8 @@ class Midy {
471
535
  return audioBuffer;
472
536
  }
473
537
  }
474
- async createNoteBufferNode(voiceParams, isSF3) {
538
+ createNoteBufferNode(audioBuffer, voiceParams) {
475
539
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
476
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
477
540
  bufferSource.buffer = audioBuffer;
478
541
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
479
542
  if (bufferSource.loop) {
@@ -565,6 +628,7 @@ class Midy {
565
628
  await Promise.all(this.notePromises);
566
629
  this.notePromises = [];
567
630
  this.exclusiveClassMap.clear();
631
+ this.audioBufferCache.clear();
568
632
  resolve();
569
633
  return;
570
634
  }
@@ -580,8 +644,9 @@ class Midy {
580
644
  }
581
645
  else if (this.isStopping) {
582
646
  await this.stopNotes(0, true);
583
- this.exclusiveClassMap.clear();
584
647
  this.notePromises = [];
648
+ this.exclusiveClassMap.clear();
649
+ this.audioBufferCache.clear();
585
650
  resolve();
586
651
  this.isStopping = false;
587
652
  this.isPaused = false;
@@ -612,6 +677,9 @@ class Midy {
612
677
  secondToTicks(second, secondsPerBeat) {
613
678
  return second * this.ticksPerBeat / secondsPerBeat;
614
679
  }
680
+ getAudioBufferId(programNumber, noteNumber, velocity) {
681
+ return `${programNumber}:${noteNumber}:${velocity}`;
682
+ }
615
683
  extractMidiData(midi) {
616
684
  const instruments = new Set();
617
685
  const timeline = [];
@@ -633,6 +701,8 @@ class Midy {
633
701
  switch (event.type) {
634
702
  case "noteOn": {
635
703
  const channel = tmpChannels[event.channel];
704
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
705
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
636
706
  if (channel.programNumber < 0) {
637
707
  channel.programNumber = event.programNumber;
638
708
  switch (channel.bankMSB) {
@@ -682,6 +752,10 @@ class Midy {
682
752
  timeline.push(event);
683
753
  }
684
754
  }
755
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
756
+ if (count === 1)
757
+ this.audioBufferCounter.delete(audioBufferId);
758
+ }
685
759
  const priority = {
686
760
  controller: 0,
687
761
  sysEx: 1,
@@ -775,7 +849,7 @@ class Midy {
775
849
  return this.resumeTime + now - this.startTime - this.startDelay;
776
850
  }
777
851
  getActiveNotes(channel, time) {
778
- const activeNotes = new Map();
852
+ const activeNotes = new SparseMap(128);
779
853
  channel.scheduledNotes.forEach((noteList) => {
780
854
  const activeNote = this.getActiveNote(noteList, time);
781
855
  if (activeNote) {
@@ -942,28 +1016,31 @@ class Midy {
942
1016
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
943
1017
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
944
1018
  const pitch = pitchWheel * pitchWheelSensitivity;
945
- const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1019
+ const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
946
1020
  const pressure = pressureDepth * channel.state.channelPressure;
947
1021
  return tuning + pitch + pressure;
948
1022
  }
949
1023
  calcNoteDetune(channel, note) {
950
1024
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
951
1025
  }
952
- updateDetune(channel) {
953
- const now = this.audioContext.currentTime;
1026
+ updateChannelDetune(channel) {
954
1027
  channel.scheduledNotes.forEach((noteList) => {
955
1028
  for (let i = 0; i < noteList.length; i++) {
956
1029
  const note = noteList[i];
957
1030
  if (!note)
958
1031
  continue;
959
- const noteDetune = this.calcNoteDetune(channel, note);
960
- const detune = channel.detune + noteDetune;
961
- note.bufferSource.detune
962
- .cancelScheduledValues(now)
963
- .setValueAtTime(detune, now);
1032
+ this.updateDetune(channel, note, 0);
964
1033
  }
965
1034
  });
966
1035
  }
1036
+ updateDetune(channel, note, pressure) {
1037
+ const now = this.audioContext.currentTime;
1038
+ const noteDetune = this.calcNoteDetune(channel, note);
1039
+ const detune = channel.detune + noteDetune + pressure;
1040
+ note.bufferSource.detune
1041
+ .cancelScheduledValues(now)
1042
+ .setValueAtTime(detune, now);
1043
+ }
967
1044
  getPortamentoTime(channel) {
968
1045
  const factor = 5 * Math.log(10) / 127;
969
1046
  const time = channel.state.portamentoTime;
@@ -981,14 +1058,12 @@ class Midy {
981
1058
  .setValueAtTime(0, volDelay)
982
1059
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
983
1060
  }
984
- setVolumeEnvelope(channel, note) {
1061
+ setVolumeEnvelope(channel, note, pressure) {
985
1062
  const now = this.audioContext.currentTime;
986
1063
  const state = channel.state;
987
1064
  const { voiceParams, startTime } = note;
988
- const pressureDepth = channel.pressureTable[2] / 64;
989
- const pressure = 1 + pressureDepth * channel.state.channelPressure;
990
1065
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
991
- pressure;
1066
+ (1 + pressure);
992
1067
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
993
1068
  const volDelay = startTime + voiceParams.volDelay;
994
1069
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
@@ -1036,10 +1111,8 @@ class Midy {
1036
1111
  const { voiceParams, noteNumber, startTime } = note;
1037
1112
  const softPedalFactor = 1 -
1038
1113
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1039
- const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1040
- const pressure = pressureDepth * channel.state.channelPressure;
1041
- const baseCent = voiceParams.initialFilterFc + pressure;
1042
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1114
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1115
+ softPedalFactor *
1043
1116
  state.brightness * 2;
1044
1117
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1045
1118
  const sustainFreq = baseFreq +
@@ -1054,15 +1127,17 @@ class Midy {
1054
1127
  .setValueAtTime(adjustedBaseFreq, modDelay)
1055
1128
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1056
1129
  }
1057
- setFilterEnvelope(channel, note) {
1130
+ setFilterEnvelope(channel, note, pressure) {
1058
1131
  const now = this.audioContext.currentTime;
1059
1132
  const state = channel.state;
1060
1133
  const { voiceParams, noteNumber, startTime } = note;
1061
1134
  const softPedalFactor = 1 -
1062
1135
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1063
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1136
+ const baseCent = voiceParams.initialFilterFc + pressure;
1137
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1138
+ state.brightness * 2;
1139
+ const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1064
1140
  softPedalFactor * state.brightness * 2;
1065
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1066
1141
  const sustainFreq = baseFreq +
1067
1142
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1068
1143
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1089,9 +1164,9 @@ class Midy {
1089
1164
  gain: voiceParams.modLfoToFilterFc,
1090
1165
  });
1091
1166
  note.modulationDepth = new GainNode(this.audioContext);
1092
- this.setModLfoToPitch(channel, note);
1167
+ this.setModLfoToPitch(channel, note, 0);
1093
1168
  note.volumeDepth = new GainNode(this.audioContext);
1094
- this.setModLfoToVolume(channel, note);
1169
+ this.setModLfoToVolume(note, 0);
1095
1170
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1096
1171
  note.modulationLFO.connect(note.filterDepth);
1097
1172
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1104,8 +1179,7 @@ class Midy {
1104
1179
  const { voiceParams } = note;
1105
1180
  const state = channel.state;
1106
1181
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1107
- frequency: this.centToHz(voiceParams.freqVibLFO) *
1108
- state.vibratoRate,
1182
+ frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1109
1183
  });
1110
1184
  note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1111
1185
  note.vibratoDepth = new GainNode(this.audioContext);
@@ -1113,12 +1187,31 @@ class Midy {
1113
1187
  note.vibratoLFO.connect(note.vibratoDepth);
1114
1188
  note.vibratoDepth.connect(note.bufferSource.detune);
1115
1189
  }
1190
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1191
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1192
+ const cache = this.audioBufferCache.get(audioBufferId);
1193
+ if (cache) {
1194
+ cache.counter += 1;
1195
+ if (cache.maxCount <= cache.counter) {
1196
+ this.audioBufferCache.delete(audioBufferId);
1197
+ }
1198
+ return cache.audioBuffer;
1199
+ }
1200
+ else {
1201
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1202
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1203
+ const cache = { audioBuffer, maxCount, counter: 1 };
1204
+ this.audioBufferCache.set(audioBufferId, cache);
1205
+ return audioBuffer;
1206
+ }
1207
+ }
1116
1208
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1117
1209
  const state = channel.state;
1118
1210
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1119
1211
  const voiceParams = voice.getAllParams(controllerState);
1120
1212
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1121
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1213
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1214
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1122
1215
  note.volumeNode = new GainNode(this.audioContext);
1123
1216
  note.gainL = new GainNode(this.audioContext);
1124
1217
  note.gainR = new GainNode(this.audioContext);
@@ -1134,8 +1227,8 @@ class Midy {
1134
1227
  }
1135
1228
  else {
1136
1229
  note.portamento = false;
1137
- this.setVolumeEnvelope(channel, note);
1138
- this.setFilterEnvelope(channel, note);
1230
+ this.setVolumeEnvelope(channel, note, 0);
1231
+ this.setFilterEnvelope(channel, note, 0);
1139
1232
  }
1140
1233
  if (0 < state.vibratoDepth) {
1141
1234
  this.startVibrato(channel, note, startTime);
@@ -1178,10 +1271,10 @@ class Midy {
1178
1271
  if (soundFontIndex === undefined)
1179
1272
  return;
1180
1273
  const soundFont = this.soundFonts[soundFontIndex];
1181
- const isSF3 = soundFont.parsed.info.version.major === 3;
1182
1274
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1183
1275
  if (!voice)
1184
1276
  return;
1277
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1185
1278
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1186
1279
  note.gainL.connect(channel.gainL);
1187
1280
  note.gainR.connect(channel.gainR);
@@ -1350,16 +1443,12 @@ class Midy {
1350
1443
  handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
1351
1444
  const now = this.audioContext.currentTime;
1352
1445
  const channel = this.channels[channelNumber];
1353
- pressure /= 64;
1446
+ channel.state.polyphonicKeyPressure = pressure / 127;
1447
+ const table = channel.polyphonicKeyPressureTable;
1354
1448
  const activeNotes = this.getActiveNotes(channel, now);
1355
- if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1356
- if (activeNotes.has(noteNumber)) {
1357
- const activeNote = activeNotes.get(noteNumber);
1358
- const gain = activeNote.gainL.gain.value;
1359
- activeNote.volumeNode.gain
1360
- .cancelScheduledValues(now)
1361
- .setValueAtTime(gain * pressure, now);
1362
- }
1449
+ if (activeNotes.has(noteNumber)) {
1450
+ const note = activeNotes.get(noteNumber);
1451
+ this.applyDestinationSettings(channel, note, table);
1363
1452
  }
1364
1453
  // this.applyVoiceParams(channel, 10);
1365
1454
  }
@@ -1369,22 +1458,18 @@ class Midy {
1369
1458
  channel.program = program;
1370
1459
  }
1371
1460
  handleChannelPressure(channelNumber, value) {
1461
+ const now = this.audioContext.currentTime;
1372
1462
  const channel = this.channels[channelNumber];
1373
1463
  const prev = channel.state.channelPressure;
1374
1464
  const next = value / 127;
1375
1465
  channel.state.channelPressure = next;
1376
- if (channel.pressureTable[0] !== 64) {
1377
- const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1466
+ if (channel.channelPressureTable[0] !== 64) {
1467
+ const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1378
1468
  channel.detune += pressureDepth * (next - prev);
1379
1469
  }
1380
- const table = channel.pressureTable;
1381
- channel.scheduledNotes.forEach((noteList) => {
1382
- for (let i = 0; i < noteList.length; i++) {
1383
- const note = noteList[i];
1384
- if (!note)
1385
- continue;
1386
- this.applyDestinationSettings(channel, note, table);
1387
- }
1470
+ const table = channel.channelPressureTable;
1471
+ this.getActiveNotes(channel, now).forEach((note) => {
1472
+ this.applyDestinationSettings(channel, note, table);
1388
1473
  });
1389
1474
  // this.applyVoiceParams(channel, 13);
1390
1475
  }
@@ -1399,16 +1484,13 @@ class Midy {
1399
1484
  const next = (value - 8192) / 8192;
1400
1485
  state.pitchWheel = value / 16383;
1401
1486
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1402
- this.updateDetune(channel);
1487
+ this.updateChannelDetune(channel);
1403
1488
  this.applyVoiceParams(channel, 14);
1404
1489
  }
1405
- setModLfoToPitch(channel, note) {
1490
+ setModLfoToPitch(channel, note, pressure) {
1406
1491
  const now = this.audioContext.currentTime;
1407
- const pressureDepth = channel.pressureTable[3] / 127 * 600;
1408
- const pressure = pressureDepth * channel.state.channelPressure;
1409
1492
  const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1410
- const baseDepth = Math.abs(modLfoToPitch) +
1411
- channel.state.modulationDepth;
1493
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1412
1494
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1413
1495
  note.modulationDepth.gain
1414
1496
  .cancelScheduledValues(now)
@@ -1424,22 +1506,18 @@ class Midy {
1424
1506
  .cancelScheduledValues(now)
1425
1507
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1426
1508
  }
1427
- setModLfoToFilterFc(channel, note) {
1509
+ setModLfoToFilterFc(note, pressure) {
1428
1510
  const now = this.audioContext.currentTime;
1429
- const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1430
- const pressure = pressureDepth * channel.state.channelPressure;
1431
1511
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1432
1512
  note.filterDepth.gain
1433
1513
  .cancelScheduledValues(now)
1434
1514
  .setValueAtTime(modLfoToFilterFc, now);
1435
1515
  }
1436
- setModLfoToVolume(channel, note) {
1516
+ setModLfoToVolume(note, pressure) {
1437
1517
  const now = this.audioContext.currentTime;
1438
1518
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1439
1519
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1440
- const pressureDepth = channel.pressureTable[5] / 127;
1441
- const pressure = 1 + pressureDepth * channel.state.channelPressure;
1442
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1520
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1443
1521
  note.volumeDepth.gain
1444
1522
  .cancelScheduledValues(now)
1445
1523
  .setValueAtTime(volumeDepth, now);
@@ -1512,11 +1590,18 @@ class Midy {
1512
1590
  .cancelScheduledValues(now)
1513
1591
  .setValueAtTime(freqModLFO, now);
1514
1592
  }
1593
+ setFreqVibLFO(channel, note) {
1594
+ const now = this.audioContext.currentTime;
1595
+ const freqVibLFO = note.voiceParams.freqVibLFO;
1596
+ note.vibratoLFO.frequency
1597
+ .cancelScheduledValues(now)
1598
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1599
+ }
1515
1600
  createVoiceParamsHandlers() {
1516
1601
  return {
1517
1602
  modLfoToPitch: (channel, note, _prevValue) => {
1518
1603
  if (0 < channel.state.modulationDepth) {
1519
- this.setModLfoToPitch(channel, note);
1604
+ this.setModLfoToPitch(channel, note, 0);
1520
1605
  }
1521
1606
  },
1522
1607
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1526,12 +1611,12 @@ class Midy {
1526
1611
  },
1527
1612
  modLfoToFilterFc: (channel, note, _prevValue) => {
1528
1613
  if (0 < channel.state.modulationDepth) {
1529
- this.setModLfoToFilterFc(channel, note);
1614
+ this.setModLfoToFilterFc(note, 0);
1530
1615
  }
1531
1616
  },
1532
1617
  modLfoToVolume: (channel, note, _prevValue) => {
1533
1618
  if (0 < channel.state.modulationDepth) {
1534
- this.setModLfoToVolume(channel, note);
1619
+ this.setModLfoToVolume(note, 0);
1535
1620
  }
1536
1621
  },
1537
1622
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1557,11 +1642,7 @@ class Midy {
1557
1642
  },
1558
1643
  freqVibLFO: (channel, note, _prevValue) => {
1559
1644
  if (0 < channel.state.vibratoDepth) {
1560
- const now = this.audioContext.currentTime;
1561
- const freqVibLFO = note.voiceParams.freqVibLFO;
1562
- note.vibratoLFO.frequency
1563
- .cancelScheduledValues(now)
1564
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate, now);
1645
+ this.setFreqVibLFO(channel, note);
1565
1646
  }
1566
1647
  },
1567
1648
  };
@@ -1605,7 +1686,7 @@ class Midy {
1605
1686
  this.setPortamentoStartFilterEnvelope(channel, note);
1606
1687
  }
1607
1688
  else {
1608
- this.setFilterEnvelope(channel, note);
1689
+ this.setFilterEnvelope(channel, note, 0);
1609
1690
  }
1610
1691
  this.setPitchEnvelope(note);
1611
1692
  }
@@ -1619,7 +1700,7 @@ class Midy {
1619
1700
  if (key in voiceParams)
1620
1701
  noteVoiceParams[key] = voiceParams[key];
1621
1702
  }
1622
- this.setVolumeEnvelope(channel, note);
1703
+ this.setVolumeEnvelope(channel, note, 0);
1623
1704
  }
1624
1705
  }
1625
1706
  }
@@ -1799,8 +1880,7 @@ class Midy {
1799
1880
  channel.state.sostenutoPedal = value / 127;
1800
1881
  if (64 <= value) {
1801
1882
  const now = this.audioContext.currentTime;
1802
- const activeNotes = this.getActiveNotes(channel, now);
1803
- channel.sostenutoNotes = new Map(activeNotes);
1883
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1804
1884
  }
1805
1885
  else {
1806
1886
  this.releaseSostenutoPedal(channelNumber, value);
@@ -1840,7 +1920,7 @@ class Midy {
1840
1920
  continue;
1841
1921
  if (note.startTime < now)
1842
1922
  continue;
1843
- this.setVolumeEnvelope(channel, note);
1923
+ this.setVolumeEnvelope(channel, note, 0);
1844
1924
  }
1845
1925
  });
1846
1926
  }
@@ -1852,7 +1932,12 @@ class Midy {
1852
1932
  const note = noteList[i];
1853
1933
  if (!note)
1854
1934
  continue;
1855
- this.setFilterEnvelope(channel, note);
1935
+ if (note.portamento) {
1936
+ this.setPortamentoStartFilterEnvelope(channel, note);
1937
+ }
1938
+ else {
1939
+ this.setFilterEnvelope(channel, note, 0);
1940
+ }
1856
1941
  }
1857
1942
  });
1858
1943
  }
@@ -1864,7 +1949,7 @@ class Midy {
1864
1949
  const note = noteList[i];
1865
1950
  if (!note)
1866
1951
  continue;
1867
- this.setVolumeEnvelope(channel, note);
1952
+ this.setVolumeEnvelope(channel, note, 0);
1868
1953
  }
1869
1954
  });
1870
1955
  }
@@ -1873,21 +1958,53 @@ class Midy {
1873
1958
  channel.state.vibratoRate = vibratoRate / 64;
1874
1959
  if (channel.vibratoDepth <= 0)
1875
1960
  return;
1876
- const now = this.audioContext.currentTime;
1877
- const activeNotes = this.getActiveNotes(channel, now);
1878
- activeNotes.forEach((activeNote) => {
1879
- activeNote.vibratoLFO.frequency
1880
- .cancelScheduledValues(now)
1881
- .setValueAtTime(channel.state.vibratoRate, now);
1961
+ channel.scheduledNotes.forEach((noteList) => {
1962
+ for (let i = 0; i < noteList.length; i++) {
1963
+ const note = noteList[i];
1964
+ if (!note)
1965
+ continue;
1966
+ this.setVibLfoToPitch(channel, note);
1967
+ }
1882
1968
  });
1883
1969
  }
1884
1970
  setVibratoDepth(channelNumber, vibratoDepth) {
1885
1971
  const channel = this.channels[channelNumber];
1972
+ const prev = channel.state.vibratoDepth;
1886
1973
  channel.state.vibratoDepth = vibratoDepth / 64;
1974
+ if (0 < prev) {
1975
+ channel.scheduledNotes.forEach((noteList) => {
1976
+ for (let i = 0; i < noteList.length; i++) {
1977
+ const note = noteList[i];
1978
+ if (!note)
1979
+ continue;
1980
+ this.setFreqVibLFO(channel, note);
1981
+ }
1982
+ });
1983
+ }
1984
+ else {
1985
+ channel.scheduledNotes.forEach((noteList) => {
1986
+ for (let i = 0; i < noteList.length; i++) {
1987
+ const note = noteList[i];
1988
+ if (!note)
1989
+ continue;
1990
+ this.startVibrato(channel, note, note.startTime);
1991
+ }
1992
+ });
1993
+ }
1887
1994
  }
1888
1995
  setVibratoDelay(channelNumber, vibratoDelay) {
1889
1996
  const channel = this.channels[channelNumber];
1890
1997
  channel.state.vibratoDelay = vibratoDelay / 64;
1998
+ if (0 < channel.state.vibratoDepth) {
1999
+ channel.scheduledNotes.forEach((noteList) => {
2000
+ for (let i = 0; i < noteList.length; i++) {
2001
+ const note = noteList[i];
2002
+ if (!note)
2003
+ continue;
2004
+ this.startVibrato(channel, note, note.startTime);
2005
+ }
2006
+ });
2007
+ }
1891
2008
  }
1892
2009
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1893
2010
  const channel = this.channels[channelNumber];
@@ -2052,7 +2169,7 @@ class Midy {
2052
2169
  const next = value / 128;
2053
2170
  state.pitchWheelSensitivity = next;
2054
2171
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2055
- this.updateDetune(channel);
2172
+ this.updateChannelDetune(channel);
2056
2173
  this.applyVoiceParams(channel, 16);
2057
2174
  }
2058
2175
  handleFineTuningRPN(channelNumber) {
@@ -2067,7 +2184,7 @@ class Midy {
2067
2184
  const next = (value - 8192) / 8.192; // cent
2068
2185
  channel.fineTuning = next;
2069
2186
  channel.detune += next - prev;
2070
- this.updateDetune(channel);
2187
+ this.updateChannelDetune(channel);
2071
2188
  }
2072
2189
  handleCoarseTuningRPN(channelNumber) {
2073
2190
  const channel = this.channels[channelNumber];
@@ -2081,7 +2198,7 @@ class Midy {
2081
2198
  const next = (value - 64) * 100; // cent
2082
2199
  channel.coarseTuning = next;
2083
2200
  channel.detune += next - prev;
2084
- this.updateDetune(channel);
2201
+ this.updateChannelDetune(channel);
2085
2202
  }
2086
2203
  handleModulationDepthRangeRPN(channelNumber) {
2087
2204
  const channel = this.channels[channelNumber];
@@ -2112,7 +2229,7 @@ class Midy {
2112
2229
  const state = channel.state;
2113
2230
  for (let i = 0; i < stateTypes.length; i++) {
2114
2231
  const type = stateTypes[i];
2115
- state[type] = defaultControllerState[type];
2232
+ state[type] = defaultControllerState[type].defaultValue;
2116
2233
  }
2117
2234
  const settingTypes = [
2118
2235
  "rpnMSB",
@@ -2144,7 +2261,10 @@ class Midy {
2144
2261
  switch (data[3]) {
2145
2262
  case 8:
2146
2263
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2147
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2264
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2265
+ case 9:
2266
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2267
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false);
2148
2268
  default:
2149
2269
  console.warn(`Unsupported Exclusive Message: ${data}`);
2150
2270
  }
@@ -2206,8 +2326,10 @@ class Midy {
2206
2326
  case 8:
2207
2327
  switch (data[3]) {
2208
2328
  case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2209
- // TODO: realtime
2210
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2329
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true);
2330
+ case 9:
2331
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2332
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true);
2211
2333
  default:
2212
2334
  console.warn(`Unsupported Exclusive Message: ${data}`);
2213
2335
  }
@@ -2215,7 +2337,9 @@ class Midy {
2215
2337
  case 9:
2216
2338
  switch (data[3]) {
2217
2339
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2218
- return this.handleChannelPressureSysEx(data);
2340
+ return this.handlePressureSysEx(data, "channelPressureTable");
2341
+ case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2342
+ return this.handlePressureSysEx(data, "polyphonicKeyPressureTable");
2219
2343
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2220
2344
  return this.handleControlChangeSysEx(data);
2221
2345
  default:
@@ -2257,7 +2381,7 @@ class Midy {
2257
2381
  const next = (value - 8192) / 8.192; // cent
2258
2382
  this.masterFineTuning = next;
2259
2383
  channel.detune += next - prev;
2260
- this.updateDetune(channel);
2384
+ this.updateChannelDetune(channel);
2261
2385
  }
2262
2386
  handleMasterCoarseTuningSysEx(data) {
2263
2387
  const coarseTuning = data[4];
@@ -2268,7 +2392,7 @@ class Midy {
2268
2392
  const next = (value - 64) * 100; // cent
2269
2393
  this.masterCoarseTuning = next;
2270
2394
  channel.detune += next - prev;
2271
- this.updateDetune(channel);
2395
+ this.updateChannelDetune(channel);
2272
2396
  }
2273
2397
  handleGlobalParameterControlSysEx(data) {
2274
2398
  if (data[7] === 1) {
@@ -2475,8 +2599,8 @@ class Midy {
2475
2599
  }
2476
2600
  return bitmap;
2477
2601
  }
2478
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2479
- if (data.length < 18) {
2602
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2603
+ if (data.length < 19) {
2480
2604
  console.error("Data length is too short");
2481
2605
  return;
2482
2606
  }
@@ -2484,37 +2608,96 @@ class Midy {
2484
2608
  for (let i = 0; i < channelBitmap.length; i++) {
2485
2609
  if (!channelBitmap[i])
2486
2610
  continue;
2611
+ const channel = this.channels[i];
2487
2612
  for (let j = 0; j < 12; j++) {
2488
- const value = data[j + 7] - 64; // cent
2489
- this.channels[i].scaleOctaveTuningTable[j] = value;
2613
+ const centValue = data[j + 7] - 64;
2614
+ channel.scaleOctaveTuningTable[j] = centValue;
2490
2615
  }
2616
+ if (realtime)
2617
+ this.updateChannelDetune(channel);
2618
+ }
2619
+ }
2620
+ handleScaleOctaveTuning2ByteFormatSysEx(data, realtime) {
2621
+ if (data.length < 31) {
2622
+ console.error("Data length is too short");
2623
+ return;
2624
+ }
2625
+ const channelBitmap = this.getChannelBitmap(data);
2626
+ for (let i = 0; i < channelBitmap.length; i++) {
2627
+ if (!channelBitmap[i])
2628
+ continue;
2629
+ const channel = this.channels[i];
2630
+ for (let j = 0; j < 12; j++) {
2631
+ const index = 7 + j * 2;
2632
+ const msb = data[index] & 0x7F;
2633
+ const lsb = data[index + 1] & 0x7F;
2634
+ const value14bit = msb * 128 + lsb;
2635
+ const centValue = (value14bit - 8192) / 8.192;
2636
+ channel.scaleOctaveTuningTable[j] = centValue;
2637
+ }
2638
+ if (realtime)
2639
+ this.updateChannelDetune(channel);
2491
2640
  }
2492
2641
  }
2493
2642
  applyDestinationSettings(channel, note, table) {
2494
2643
  if (table[0] !== 64) {
2495
- this.updateDetune(channel);
2644
+ const polyphonicKeyPressure = (0 < note.pressure)
2645
+ ? channel.polyphonicKeyPressureTable[0] * note.pressure
2646
+ : 0;
2647
+ const pressure = (polyphonicKeyPressure - 64) / 37.5; // 2400 / 64;
2648
+ this.updateDetune(channel, note, pressure);
2496
2649
  }
2497
2650
  if (!note.portamento) {
2498
2651
  if (table[1] !== 64) {
2499
- this.setFilterEnvelope(channel, note);
2652
+ const channelPressure = channel.channelPressureTable[1] *
2653
+ channel.state.channelPressure;
2654
+ const polyphonicKeyPressure = (0 < note.pressure)
2655
+ ? channel.polyphonicKeyPressureTable[1] * note.pressure
2656
+ : 0;
2657
+ const pressure = (channelPressure + polyphonicKeyPressure - 128) * 15;
2658
+ this.setFilterEnvelope(channel, note, pressure);
2500
2659
  }
2501
2660
  if (table[2] !== 64) {
2502
- this.setVolumeEnvelope(channel, note);
2661
+ const channelPressure = channel.channelPressureTable[2] *
2662
+ channel.state.channelPressure;
2663
+ const polyphonicKeyPressure = (0 < note.pressure)
2664
+ ? channel.polyphonicKeyPressureTable[2] * note.pressure
2665
+ : 0;
2666
+ const pressure = (channelPressure + polyphonicKeyPressure) / 128;
2667
+ this.setVolumeEnvelope(channel, note, pressure);
2503
2668
  }
2504
2669
  }
2505
2670
  if (table[3] !== 0) {
2506
- this.setModLfoToPitch(channel, note);
2671
+ const channelPressure = channel.channelPressureTable[3] *
2672
+ channel.state.channelPressure;
2673
+ const polyphonicKeyPressure = (0 < note.pressure)
2674
+ ? channel.polyphonicKeyPressureTable[3] * note.pressure
2675
+ : 0;
2676
+ const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 600;
2677
+ this.setModLfoToPitch(channel, note, pressure);
2507
2678
  }
2508
2679
  if (table[4] !== 0) {
2509
- this.setModLfoToFilterFc(channel, note);
2680
+ const channelPressure = channel.channelPressureTable[4] *
2681
+ channel.state.channelPressure;
2682
+ const polyphonicKeyPressure = (0 < note.pressure)
2683
+ ? channel.polyphonicKeyPressureTable[4] * note.pressure
2684
+ : 0;
2685
+ const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2686
+ this.setModLfoToFilterFc(note, pressure);
2510
2687
  }
2511
2688
  if (table[5] !== 0) {
2512
- this.setModLfoToVolume(channel, note);
2689
+ const channelPressure = channel.channelPressureTable[5] *
2690
+ channel.state.channelPressure;
2691
+ const polyphonicKeyPressure = (0 < note.pressure)
2692
+ ? channel.polyphonicKeyPressureTable[5] * note.pressure
2693
+ : 0;
2694
+ const pressure = (channelPressure + polyphonicKeyPressure) / 254;
2695
+ this.setModLfoToVolume(note, pressure);
2513
2696
  }
2514
2697
  }
2515
- handleChannelPressureSysEx(data) {
2698
+ handleChannelPressureSysEx(data, tableName) {
2516
2699
  const channelNumber = data[4];
2517
- const table = this.channels[channelNumber].pressureTable;
2700
+ const table = this.channels[channelNumber][tableName];
2518
2701
  for (let i = 5; i < data.length - 1; i += 2) {
2519
2702
  const pp = data[i];
2520
2703
  const rr = data[i + 1];
@@ -2605,8 +2788,9 @@ Object.defineProperty(Midy, "channelSettings", {
2605
2788
  value: {
2606
2789
  currentBufferSource: null,
2607
2790
  detune: 0,
2608
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2609
- pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2791
+ scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
2792
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2793
+ polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2610
2794
  keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2611
2795
  program: 0,
2612
2796
  bank: 121 * 128,
@@ -2621,16 +2805,3 @@ Object.defineProperty(Midy, "channelSettings", {
2621
2805
  modulationDepthRange: 50, // cent
2622
2806
  }
2623
2807
  });
2624
- Object.defineProperty(Midy, "controllerDestinationSettings", {
2625
- enumerable: true,
2626
- configurable: true,
2627
- writable: true,
2628
- value: {
2629
- pitchControl: 0,
2630
- filterCutoffControl: 0,
2631
- amplitudeControl: 1,
2632
- lfoPitchDepth: 0,
2633
- lfoFilterDepth: 0,
2634
- lfoAmplitudeDepth: 0,
2635
- }
2636
- });