@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/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", {
@@ -86,6 +138,12 @@ class Note {
86
138
  writable: true,
87
139
  value: void 0
88
140
  });
141
+ Object.defineProperty(this, "pressure", {
142
+ enumerable: true,
143
+ configurable: true,
144
+ writable: true,
145
+ value: 0
146
+ });
89
147
  this.noteNumber = noteNumber;
90
148
  this.velocity = velocity;
91
149
  this.startTime = startTime;
@@ -97,7 +155,7 @@ class Note {
97
155
  const defaultControllerState = {
98
156
  noteOnVelocity: { type: 2, defaultValue: 0 },
99
157
  noteOnKeyNumber: { type: 3, defaultValue: 0 },
100
- polyPressure: { type: 10, defaultValue: 0 },
158
+ polyphonicKeyPressure: { type: 10, defaultValue: 0 },
101
159
  channelPressure: { type: 13, defaultValue: 0 },
102
160
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
103
161
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
@@ -281,6 +339,18 @@ export class Midy {
281
339
  writable: true,
282
340
  value: this.initSoundFontTable()
283
341
  });
342
+ Object.defineProperty(this, "audioBufferCounter", {
343
+ enumerable: true,
344
+ configurable: true,
345
+ writable: true,
346
+ value: new Map()
347
+ });
348
+ Object.defineProperty(this, "audioBufferCache", {
349
+ enumerable: true,
350
+ configurable: true,
351
+ writable: true,
352
+ value: new Map()
353
+ });
284
354
  Object.defineProperty(this, "isPlaying", {
285
355
  enumerable: true,
286
356
  configurable: true,
@@ -333,7 +403,7 @@ export class Midy {
333
403
  enumerable: true,
334
404
  configurable: true,
335
405
  writable: true,
336
- value: new Map()
406
+ value: new SparseMap(128)
337
407
  });
338
408
  Object.defineProperty(this, "defaultOptions", {
339
409
  enumerable: true,
@@ -373,7 +443,7 @@ export class Midy {
373
443
  initSoundFontTable() {
374
444
  const table = new Array(128);
375
445
  for (let i = 0; i < 128; i++) {
376
- table[i] = new Map();
446
+ table[i] = new SparseMap(128);
377
447
  }
378
448
  return table;
379
449
  }
@@ -427,14 +497,8 @@ export class Midy {
427
497
  state: new ControllerState(),
428
498
  controlTable: this.initControlTable(),
429
499
  ...this.setChannelAudioNodes(audioContext),
430
- scheduledNotes: new Map(),
431
- sostenutoNotes: new Map(),
432
- polyphonicKeyPressure: {
433
- ...this.constructor.controllerDestinationSettings,
434
- },
435
- channelPressure: {
436
- ...this.constructor.controllerDestinationSettings,
437
- },
500
+ scheduledNotes: new SparseMap(128),
501
+ sostenutoNotes: new SparseMap(128),
438
502
  };
439
503
  });
440
504
  return channels;
@@ -468,9 +532,8 @@ export class Midy {
468
532
  return audioBuffer;
469
533
  }
470
534
  }
471
- async createNoteBufferNode(voiceParams, isSF3) {
535
+ createNoteBufferNode(audioBuffer, voiceParams) {
472
536
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
473
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
474
537
  bufferSource.buffer = audioBuffer;
475
538
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
476
539
  if (bufferSource.loop) {
@@ -562,6 +625,7 @@ export class Midy {
562
625
  await Promise.all(this.notePromises);
563
626
  this.notePromises = [];
564
627
  this.exclusiveClassMap.clear();
628
+ this.audioBufferCache.clear();
565
629
  resolve();
566
630
  return;
567
631
  }
@@ -577,8 +641,9 @@ export class Midy {
577
641
  }
578
642
  else if (this.isStopping) {
579
643
  await this.stopNotes(0, true);
580
- this.exclusiveClassMap.clear();
581
644
  this.notePromises = [];
645
+ this.exclusiveClassMap.clear();
646
+ this.audioBufferCache.clear();
582
647
  resolve();
583
648
  this.isStopping = false;
584
649
  this.isPaused = false;
@@ -609,6 +674,9 @@ export class Midy {
609
674
  secondToTicks(second, secondsPerBeat) {
610
675
  return second * this.ticksPerBeat / secondsPerBeat;
611
676
  }
677
+ getAudioBufferId(programNumber, noteNumber, velocity) {
678
+ return `${programNumber}:${noteNumber}:${velocity}`;
679
+ }
612
680
  extractMidiData(midi) {
613
681
  const instruments = new Set();
614
682
  const timeline = [];
@@ -630,6 +698,8 @@ export class Midy {
630
698
  switch (event.type) {
631
699
  case "noteOn": {
632
700
  const channel = tmpChannels[event.channel];
701
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
702
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
633
703
  if (channel.programNumber < 0) {
634
704
  channel.programNumber = event.programNumber;
635
705
  switch (channel.bankMSB) {
@@ -679,6 +749,10 @@ export class Midy {
679
749
  timeline.push(event);
680
750
  }
681
751
  }
752
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
753
+ if (count === 1)
754
+ this.audioBufferCounter.delete(audioBufferId);
755
+ }
682
756
  const priority = {
683
757
  controller: 0,
684
758
  sysEx: 1,
@@ -772,7 +846,7 @@ export class Midy {
772
846
  return this.resumeTime + now - this.startTime - this.startDelay;
773
847
  }
774
848
  getActiveNotes(channel, time) {
775
- const activeNotes = new Map();
849
+ const activeNotes = new SparseMap(128);
776
850
  channel.scheduledNotes.forEach((noteList) => {
777
851
  const activeNote = this.getActiveNote(noteList, time);
778
852
  if (activeNote) {
@@ -939,28 +1013,31 @@ export class Midy {
939
1013
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
940
1014
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
941
1015
  const pitch = pitchWheel * pitchWheelSensitivity;
942
- const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1016
+ const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
943
1017
  const pressure = pressureDepth * channel.state.channelPressure;
944
1018
  return tuning + pitch + pressure;
945
1019
  }
946
1020
  calcNoteDetune(channel, note) {
947
1021
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
948
1022
  }
949
- updateDetune(channel) {
950
- const now = this.audioContext.currentTime;
1023
+ updateChannelDetune(channel) {
951
1024
  channel.scheduledNotes.forEach((noteList) => {
952
1025
  for (let i = 0; i < noteList.length; i++) {
953
1026
  const note = noteList[i];
954
1027
  if (!note)
955
1028
  continue;
956
- const noteDetune = this.calcNoteDetune(channel, note);
957
- const detune = channel.detune + noteDetune;
958
- note.bufferSource.detune
959
- .cancelScheduledValues(now)
960
- .setValueAtTime(detune, now);
1029
+ this.updateDetune(channel, note, 0);
961
1030
  }
962
1031
  });
963
1032
  }
1033
+ updateDetune(channel, note, pressure) {
1034
+ const now = this.audioContext.currentTime;
1035
+ const noteDetune = this.calcNoteDetune(channel, note);
1036
+ const detune = channel.detune + noteDetune + pressure;
1037
+ note.bufferSource.detune
1038
+ .cancelScheduledValues(now)
1039
+ .setValueAtTime(detune, now);
1040
+ }
964
1041
  getPortamentoTime(channel) {
965
1042
  const factor = 5 * Math.log(10) / 127;
966
1043
  const time = channel.state.portamentoTime;
@@ -978,14 +1055,12 @@ export class Midy {
978
1055
  .setValueAtTime(0, volDelay)
979
1056
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
980
1057
  }
981
- setVolumeEnvelope(channel, note) {
1058
+ setVolumeEnvelope(channel, note, pressure) {
982
1059
  const now = this.audioContext.currentTime;
983
1060
  const state = channel.state;
984
1061
  const { voiceParams, startTime } = note;
985
- const pressureDepth = channel.pressureTable[2] / 64;
986
- const pressure = 1 + pressureDepth * channel.state.channelPressure;
987
1062
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
988
- pressure;
1063
+ (1 + pressure);
989
1064
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
990
1065
  const volDelay = startTime + voiceParams.volDelay;
991
1066
  const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
@@ -1033,10 +1108,8 @@ export class Midy {
1033
1108
  const { voiceParams, noteNumber, startTime } = note;
1034
1109
  const softPedalFactor = 1 -
1035
1110
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1036
- const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1037
- const pressure = pressureDepth * channel.state.channelPressure;
1038
- const baseCent = voiceParams.initialFilterFc + pressure;
1039
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1111
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1112
+ softPedalFactor *
1040
1113
  state.brightness * 2;
1041
1114
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1042
1115
  const sustainFreq = baseFreq +
@@ -1051,15 +1124,17 @@ export class Midy {
1051
1124
  .setValueAtTime(adjustedBaseFreq, modDelay)
1052
1125
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1053
1126
  }
1054
- setFilterEnvelope(channel, note) {
1127
+ setFilterEnvelope(channel, note, pressure) {
1055
1128
  const now = this.audioContext.currentTime;
1056
1129
  const state = channel.state;
1057
1130
  const { voiceParams, noteNumber, startTime } = note;
1058
1131
  const softPedalFactor = 1 -
1059
1132
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1060
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1133
+ const baseCent = voiceParams.initialFilterFc + pressure;
1134
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1135
+ state.brightness * 2;
1136
+ const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1061
1137
  softPedalFactor * state.brightness * 2;
1062
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1063
1138
  const sustainFreq = baseFreq +
1064
1139
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1065
1140
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1086,9 +1161,9 @@ export class Midy {
1086
1161
  gain: voiceParams.modLfoToFilterFc,
1087
1162
  });
1088
1163
  note.modulationDepth = new GainNode(this.audioContext);
1089
- this.setModLfoToPitch(channel, note);
1164
+ this.setModLfoToPitch(channel, note, 0);
1090
1165
  note.volumeDepth = new GainNode(this.audioContext);
1091
- this.setModLfoToVolume(channel, note);
1166
+ this.setModLfoToVolume(note, 0);
1092
1167
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1093
1168
  note.modulationLFO.connect(note.filterDepth);
1094
1169
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1101,8 +1176,7 @@ export class Midy {
1101
1176
  const { voiceParams } = note;
1102
1177
  const state = channel.state;
1103
1178
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1104
- frequency: this.centToHz(voiceParams.freqVibLFO) *
1105
- state.vibratoRate,
1179
+ frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1106
1180
  });
1107
1181
  note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1108
1182
  note.vibratoDepth = new GainNode(this.audioContext);
@@ -1110,12 +1184,31 @@ export class Midy {
1110
1184
  note.vibratoLFO.connect(note.vibratoDepth);
1111
1185
  note.vibratoDepth.connect(note.bufferSource.detune);
1112
1186
  }
1187
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1188
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1189
+ const cache = this.audioBufferCache.get(audioBufferId);
1190
+ if (cache) {
1191
+ cache.counter += 1;
1192
+ if (cache.maxCount <= cache.counter) {
1193
+ this.audioBufferCache.delete(audioBufferId);
1194
+ }
1195
+ return cache.audioBuffer;
1196
+ }
1197
+ else {
1198
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1199
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1200
+ const cache = { audioBuffer, maxCount, counter: 1 };
1201
+ this.audioBufferCache.set(audioBufferId, cache);
1202
+ return audioBuffer;
1203
+ }
1204
+ }
1113
1205
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1114
1206
  const state = channel.state;
1115
1207
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1116
1208
  const voiceParams = voice.getAllParams(controllerState);
1117
1209
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1118
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1210
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1211
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1119
1212
  note.volumeNode = new GainNode(this.audioContext);
1120
1213
  note.gainL = new GainNode(this.audioContext);
1121
1214
  note.gainR = new GainNode(this.audioContext);
@@ -1131,8 +1224,8 @@ export class Midy {
1131
1224
  }
1132
1225
  else {
1133
1226
  note.portamento = false;
1134
- this.setVolumeEnvelope(channel, note);
1135
- this.setFilterEnvelope(channel, note);
1227
+ this.setVolumeEnvelope(channel, note, 0);
1228
+ this.setFilterEnvelope(channel, note, 0);
1136
1229
  }
1137
1230
  if (0 < state.vibratoDepth) {
1138
1231
  this.startVibrato(channel, note, startTime);
@@ -1175,10 +1268,10 @@ export class Midy {
1175
1268
  if (soundFontIndex === undefined)
1176
1269
  return;
1177
1270
  const soundFont = this.soundFonts[soundFontIndex];
1178
- const isSF3 = soundFont.parsed.info.version.major === 3;
1179
1271
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1180
1272
  if (!voice)
1181
1273
  return;
1274
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1182
1275
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1183
1276
  note.gainL.connect(channel.gainL);
1184
1277
  note.gainR.connect(channel.gainR);
@@ -1347,16 +1440,12 @@ export class Midy {
1347
1440
  handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
1348
1441
  const now = this.audioContext.currentTime;
1349
1442
  const channel = this.channels[channelNumber];
1350
- pressure /= 64;
1443
+ channel.state.polyphonicKeyPressure = pressure / 127;
1444
+ const table = channel.polyphonicKeyPressureTable;
1351
1445
  const activeNotes = this.getActiveNotes(channel, now);
1352
- if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
1353
- if (activeNotes.has(noteNumber)) {
1354
- const activeNote = activeNotes.get(noteNumber);
1355
- const gain = activeNote.gainL.gain.value;
1356
- activeNote.volumeNode.gain
1357
- .cancelScheduledValues(now)
1358
- .setValueAtTime(gain * pressure, now);
1359
- }
1446
+ if (activeNotes.has(noteNumber)) {
1447
+ const note = activeNotes.get(noteNumber);
1448
+ this.applyDestinationSettings(channel, note, table);
1360
1449
  }
1361
1450
  // this.applyVoiceParams(channel, 10);
1362
1451
  }
@@ -1366,22 +1455,18 @@ export class Midy {
1366
1455
  channel.program = program;
1367
1456
  }
1368
1457
  handleChannelPressure(channelNumber, value) {
1458
+ const now = this.audioContext.currentTime;
1369
1459
  const channel = this.channels[channelNumber];
1370
1460
  const prev = channel.state.channelPressure;
1371
1461
  const next = value / 127;
1372
1462
  channel.state.channelPressure = next;
1373
- if (channel.pressureTable[0] !== 64) {
1374
- const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1463
+ if (channel.channelPressureTable[0] !== 64) {
1464
+ const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1375
1465
  channel.detune += pressureDepth * (next - prev);
1376
1466
  }
1377
- const table = channel.pressureTable;
1378
- channel.scheduledNotes.forEach((noteList) => {
1379
- for (let i = 0; i < noteList.length; i++) {
1380
- const note = noteList[i];
1381
- if (!note)
1382
- continue;
1383
- this.applyDestinationSettings(channel, note, table);
1384
- }
1467
+ const table = channel.channelPressureTable;
1468
+ this.getActiveNotes(channel, now).forEach((note) => {
1469
+ this.applyDestinationSettings(channel, note, table);
1385
1470
  });
1386
1471
  // this.applyVoiceParams(channel, 13);
1387
1472
  }
@@ -1396,16 +1481,13 @@ export class Midy {
1396
1481
  const next = (value - 8192) / 8192;
1397
1482
  state.pitchWheel = value / 16383;
1398
1483
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1399
- this.updateDetune(channel);
1484
+ this.updateChannelDetune(channel);
1400
1485
  this.applyVoiceParams(channel, 14);
1401
1486
  }
1402
- setModLfoToPitch(channel, note) {
1487
+ setModLfoToPitch(channel, note, pressure) {
1403
1488
  const now = this.audioContext.currentTime;
1404
- const pressureDepth = channel.pressureTable[3] / 127 * 600;
1405
- const pressure = pressureDepth * channel.state.channelPressure;
1406
1489
  const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1407
- const baseDepth = Math.abs(modLfoToPitch) +
1408
- channel.state.modulationDepth;
1490
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1409
1491
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1410
1492
  note.modulationDepth.gain
1411
1493
  .cancelScheduledValues(now)
@@ -1421,22 +1503,18 @@ export class Midy {
1421
1503
  .cancelScheduledValues(now)
1422
1504
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1423
1505
  }
1424
- setModLfoToFilterFc(channel, note) {
1506
+ setModLfoToFilterFc(note, pressure) {
1425
1507
  const now = this.audioContext.currentTime;
1426
- const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1427
- const pressure = pressureDepth * channel.state.channelPressure;
1428
1508
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1429
1509
  note.filterDepth.gain
1430
1510
  .cancelScheduledValues(now)
1431
1511
  .setValueAtTime(modLfoToFilterFc, now);
1432
1512
  }
1433
- setModLfoToVolume(channel, note) {
1513
+ setModLfoToVolume(note, pressure) {
1434
1514
  const now = this.audioContext.currentTime;
1435
1515
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1436
1516
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1437
- const pressureDepth = channel.pressureTable[5] / 127;
1438
- const pressure = 1 + pressureDepth * channel.state.channelPressure;
1439
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1517
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1440
1518
  note.volumeDepth.gain
1441
1519
  .cancelScheduledValues(now)
1442
1520
  .setValueAtTime(volumeDepth, now);
@@ -1509,11 +1587,18 @@ export class Midy {
1509
1587
  .cancelScheduledValues(now)
1510
1588
  .setValueAtTime(freqModLFO, now);
1511
1589
  }
1590
+ setFreqVibLFO(channel, note) {
1591
+ const now = this.audioContext.currentTime;
1592
+ const freqVibLFO = note.voiceParams.freqVibLFO;
1593
+ note.vibratoLFO.frequency
1594
+ .cancelScheduledValues(now)
1595
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1596
+ }
1512
1597
  createVoiceParamsHandlers() {
1513
1598
  return {
1514
1599
  modLfoToPitch: (channel, note, _prevValue) => {
1515
1600
  if (0 < channel.state.modulationDepth) {
1516
- this.setModLfoToPitch(channel, note);
1601
+ this.setModLfoToPitch(channel, note, 0);
1517
1602
  }
1518
1603
  },
1519
1604
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1523,12 +1608,12 @@ export class Midy {
1523
1608
  },
1524
1609
  modLfoToFilterFc: (channel, note, _prevValue) => {
1525
1610
  if (0 < channel.state.modulationDepth) {
1526
- this.setModLfoToFilterFc(channel, note);
1611
+ this.setModLfoToFilterFc(note, 0);
1527
1612
  }
1528
1613
  },
1529
1614
  modLfoToVolume: (channel, note, _prevValue) => {
1530
1615
  if (0 < channel.state.modulationDepth) {
1531
- this.setModLfoToVolume(channel, note);
1616
+ this.setModLfoToVolume(note, 0);
1532
1617
  }
1533
1618
  },
1534
1619
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1554,11 +1639,7 @@ export class Midy {
1554
1639
  },
1555
1640
  freqVibLFO: (channel, note, _prevValue) => {
1556
1641
  if (0 < channel.state.vibratoDepth) {
1557
- const now = this.audioContext.currentTime;
1558
- const freqVibLFO = note.voiceParams.freqVibLFO;
1559
- note.vibratoLFO.frequency
1560
- .cancelScheduledValues(now)
1561
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate, now);
1642
+ this.setFreqVibLFO(channel, note);
1562
1643
  }
1563
1644
  },
1564
1645
  };
@@ -1602,7 +1683,7 @@ export class Midy {
1602
1683
  this.setPortamentoStartFilterEnvelope(channel, note);
1603
1684
  }
1604
1685
  else {
1605
- this.setFilterEnvelope(channel, note);
1686
+ this.setFilterEnvelope(channel, note, 0);
1606
1687
  }
1607
1688
  this.setPitchEnvelope(note);
1608
1689
  }
@@ -1616,7 +1697,7 @@ export class Midy {
1616
1697
  if (key in voiceParams)
1617
1698
  noteVoiceParams[key] = voiceParams[key];
1618
1699
  }
1619
- this.setVolumeEnvelope(channel, note);
1700
+ this.setVolumeEnvelope(channel, note, 0);
1620
1701
  }
1621
1702
  }
1622
1703
  }
@@ -1796,8 +1877,7 @@ export class Midy {
1796
1877
  channel.state.sostenutoPedal = value / 127;
1797
1878
  if (64 <= value) {
1798
1879
  const now = this.audioContext.currentTime;
1799
- const activeNotes = this.getActiveNotes(channel, now);
1800
- channel.sostenutoNotes = new Map(activeNotes);
1880
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1801
1881
  }
1802
1882
  else {
1803
1883
  this.releaseSostenutoPedal(channelNumber, value);
@@ -1837,7 +1917,7 @@ export class Midy {
1837
1917
  continue;
1838
1918
  if (note.startTime < now)
1839
1919
  continue;
1840
- this.setVolumeEnvelope(channel, note);
1920
+ this.setVolumeEnvelope(channel, note, 0);
1841
1921
  }
1842
1922
  });
1843
1923
  }
@@ -1849,7 +1929,12 @@ export class Midy {
1849
1929
  const note = noteList[i];
1850
1930
  if (!note)
1851
1931
  continue;
1852
- this.setFilterEnvelope(channel, note);
1932
+ if (note.portamento) {
1933
+ this.setPortamentoStartFilterEnvelope(channel, note);
1934
+ }
1935
+ else {
1936
+ this.setFilterEnvelope(channel, note, 0);
1937
+ }
1853
1938
  }
1854
1939
  });
1855
1940
  }
@@ -1861,7 +1946,7 @@ export class Midy {
1861
1946
  const note = noteList[i];
1862
1947
  if (!note)
1863
1948
  continue;
1864
- this.setVolumeEnvelope(channel, note);
1949
+ this.setVolumeEnvelope(channel, note, 0);
1865
1950
  }
1866
1951
  });
1867
1952
  }
@@ -1870,21 +1955,53 @@ export class Midy {
1870
1955
  channel.state.vibratoRate = vibratoRate / 64;
1871
1956
  if (channel.vibratoDepth <= 0)
1872
1957
  return;
1873
- const now = this.audioContext.currentTime;
1874
- const activeNotes = this.getActiveNotes(channel, now);
1875
- activeNotes.forEach((activeNote) => {
1876
- activeNote.vibratoLFO.frequency
1877
- .cancelScheduledValues(now)
1878
- .setValueAtTime(channel.state.vibratoRate, now);
1958
+ channel.scheduledNotes.forEach((noteList) => {
1959
+ for (let i = 0; i < noteList.length; i++) {
1960
+ const note = noteList[i];
1961
+ if (!note)
1962
+ continue;
1963
+ this.setVibLfoToPitch(channel, note);
1964
+ }
1879
1965
  });
1880
1966
  }
1881
1967
  setVibratoDepth(channelNumber, vibratoDepth) {
1882
1968
  const channel = this.channels[channelNumber];
1969
+ const prev = channel.state.vibratoDepth;
1883
1970
  channel.state.vibratoDepth = vibratoDepth / 64;
1971
+ if (0 < prev) {
1972
+ channel.scheduledNotes.forEach((noteList) => {
1973
+ for (let i = 0; i < noteList.length; i++) {
1974
+ const note = noteList[i];
1975
+ if (!note)
1976
+ continue;
1977
+ this.setFreqVibLFO(channel, note);
1978
+ }
1979
+ });
1980
+ }
1981
+ else {
1982
+ channel.scheduledNotes.forEach((noteList) => {
1983
+ for (let i = 0; i < noteList.length; i++) {
1984
+ const note = noteList[i];
1985
+ if (!note)
1986
+ continue;
1987
+ this.startVibrato(channel, note, note.startTime);
1988
+ }
1989
+ });
1990
+ }
1884
1991
  }
1885
1992
  setVibratoDelay(channelNumber, vibratoDelay) {
1886
1993
  const channel = this.channels[channelNumber];
1887
1994
  channel.state.vibratoDelay = vibratoDelay / 64;
1995
+ if (0 < channel.state.vibratoDepth) {
1996
+ channel.scheduledNotes.forEach((noteList) => {
1997
+ for (let i = 0; i < noteList.length; i++) {
1998
+ const note = noteList[i];
1999
+ if (!note)
2000
+ continue;
2001
+ this.startVibrato(channel, note, note.startTime);
2002
+ }
2003
+ });
2004
+ }
1888
2005
  }
1889
2006
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1890
2007
  const channel = this.channels[channelNumber];
@@ -2049,7 +2166,7 @@ export class Midy {
2049
2166
  const next = value / 128;
2050
2167
  state.pitchWheelSensitivity = next;
2051
2168
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2052
- this.updateDetune(channel);
2169
+ this.updateChannelDetune(channel);
2053
2170
  this.applyVoiceParams(channel, 16);
2054
2171
  }
2055
2172
  handleFineTuningRPN(channelNumber) {
@@ -2064,7 +2181,7 @@ export class Midy {
2064
2181
  const next = (value - 8192) / 8.192; // cent
2065
2182
  channel.fineTuning = next;
2066
2183
  channel.detune += next - prev;
2067
- this.updateDetune(channel);
2184
+ this.updateChannelDetune(channel);
2068
2185
  }
2069
2186
  handleCoarseTuningRPN(channelNumber) {
2070
2187
  const channel = this.channels[channelNumber];
@@ -2078,7 +2195,7 @@ export class Midy {
2078
2195
  const next = (value - 64) * 100; // cent
2079
2196
  channel.coarseTuning = next;
2080
2197
  channel.detune += next - prev;
2081
- this.updateDetune(channel);
2198
+ this.updateChannelDetune(channel);
2082
2199
  }
2083
2200
  handleModulationDepthRangeRPN(channelNumber) {
2084
2201
  const channel = this.channels[channelNumber];
@@ -2109,7 +2226,7 @@ export class Midy {
2109
2226
  const state = channel.state;
2110
2227
  for (let i = 0; i < stateTypes.length; i++) {
2111
2228
  const type = stateTypes[i];
2112
- state[type] = defaultControllerState[type];
2229
+ state[type] = defaultControllerState[type].defaultValue;
2113
2230
  }
2114
2231
  const settingTypes = [
2115
2232
  "rpnMSB",
@@ -2141,7 +2258,10 @@ export class Midy {
2141
2258
  switch (data[3]) {
2142
2259
  case 8:
2143
2260
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2144
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2261
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2262
+ case 9:
2263
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2264
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false);
2145
2265
  default:
2146
2266
  console.warn(`Unsupported Exclusive Message: ${data}`);
2147
2267
  }
@@ -2203,8 +2323,10 @@ export class Midy {
2203
2323
  case 8:
2204
2324
  switch (data[3]) {
2205
2325
  case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2206
- // TODO: realtime
2207
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2326
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true);
2327
+ case 9:
2328
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2329
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true);
2208
2330
  default:
2209
2331
  console.warn(`Unsupported Exclusive Message: ${data}`);
2210
2332
  }
@@ -2212,7 +2334,9 @@ export class Midy {
2212
2334
  case 9:
2213
2335
  switch (data[3]) {
2214
2336
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2215
- return this.handleChannelPressureSysEx(data);
2337
+ return this.handlePressureSysEx(data, "channelPressureTable");
2338
+ case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2339
+ return this.handlePressureSysEx(data, "polyphonicKeyPressureTable");
2216
2340
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2217
2341
  return this.handleControlChangeSysEx(data);
2218
2342
  default:
@@ -2254,7 +2378,7 @@ export class Midy {
2254
2378
  const next = (value - 8192) / 8.192; // cent
2255
2379
  this.masterFineTuning = next;
2256
2380
  channel.detune += next - prev;
2257
- this.updateDetune(channel);
2381
+ this.updateChannelDetune(channel);
2258
2382
  }
2259
2383
  handleMasterCoarseTuningSysEx(data) {
2260
2384
  const coarseTuning = data[4];
@@ -2265,7 +2389,7 @@ export class Midy {
2265
2389
  const next = (value - 64) * 100; // cent
2266
2390
  this.masterCoarseTuning = next;
2267
2391
  channel.detune += next - prev;
2268
- this.updateDetune(channel);
2392
+ this.updateChannelDetune(channel);
2269
2393
  }
2270
2394
  handleGlobalParameterControlSysEx(data) {
2271
2395
  if (data[7] === 1) {
@@ -2472,8 +2596,8 @@ export class Midy {
2472
2596
  }
2473
2597
  return bitmap;
2474
2598
  }
2475
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2476
- if (data.length < 18) {
2599
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2600
+ if (data.length < 19) {
2477
2601
  console.error("Data length is too short");
2478
2602
  return;
2479
2603
  }
@@ -2481,37 +2605,96 @@ export class Midy {
2481
2605
  for (let i = 0; i < channelBitmap.length; i++) {
2482
2606
  if (!channelBitmap[i])
2483
2607
  continue;
2608
+ const channel = this.channels[i];
2484
2609
  for (let j = 0; j < 12; j++) {
2485
- const value = data[j + 7] - 64; // cent
2486
- this.channels[i].scaleOctaveTuningTable[j] = value;
2610
+ const centValue = data[j + 7] - 64;
2611
+ channel.scaleOctaveTuningTable[j] = centValue;
2487
2612
  }
2613
+ if (realtime)
2614
+ this.updateChannelDetune(channel);
2615
+ }
2616
+ }
2617
+ handleScaleOctaveTuning2ByteFormatSysEx(data, realtime) {
2618
+ if (data.length < 31) {
2619
+ console.error("Data length is too short");
2620
+ return;
2621
+ }
2622
+ const channelBitmap = this.getChannelBitmap(data);
2623
+ for (let i = 0; i < channelBitmap.length; i++) {
2624
+ if (!channelBitmap[i])
2625
+ continue;
2626
+ const channel = this.channels[i];
2627
+ for (let j = 0; j < 12; j++) {
2628
+ const index = 7 + j * 2;
2629
+ const msb = data[index] & 0x7F;
2630
+ const lsb = data[index + 1] & 0x7F;
2631
+ const value14bit = msb * 128 + lsb;
2632
+ const centValue = (value14bit - 8192) / 8.192;
2633
+ channel.scaleOctaveTuningTable[j] = centValue;
2634
+ }
2635
+ if (realtime)
2636
+ this.updateChannelDetune(channel);
2488
2637
  }
2489
2638
  }
2490
2639
  applyDestinationSettings(channel, note, table) {
2491
2640
  if (table[0] !== 64) {
2492
- this.updateDetune(channel);
2641
+ const polyphonicKeyPressure = (0 < note.pressure)
2642
+ ? channel.polyphonicKeyPressureTable[0] * note.pressure
2643
+ : 0;
2644
+ const pressure = (polyphonicKeyPressure - 64) / 37.5; // 2400 / 64;
2645
+ this.updateDetune(channel, note, pressure);
2493
2646
  }
2494
2647
  if (!note.portamento) {
2495
2648
  if (table[1] !== 64) {
2496
- this.setFilterEnvelope(channel, note);
2649
+ const channelPressure = channel.channelPressureTable[1] *
2650
+ channel.state.channelPressure;
2651
+ const polyphonicKeyPressure = (0 < note.pressure)
2652
+ ? channel.polyphonicKeyPressureTable[1] * note.pressure
2653
+ : 0;
2654
+ const pressure = (channelPressure + polyphonicKeyPressure - 128) * 15;
2655
+ this.setFilterEnvelope(channel, note, pressure);
2497
2656
  }
2498
2657
  if (table[2] !== 64) {
2499
- this.setVolumeEnvelope(channel, note);
2658
+ const channelPressure = channel.channelPressureTable[2] *
2659
+ channel.state.channelPressure;
2660
+ const polyphonicKeyPressure = (0 < note.pressure)
2661
+ ? channel.polyphonicKeyPressureTable[2] * note.pressure
2662
+ : 0;
2663
+ const pressure = (channelPressure + polyphonicKeyPressure) / 128;
2664
+ this.setVolumeEnvelope(channel, note, pressure);
2500
2665
  }
2501
2666
  }
2502
2667
  if (table[3] !== 0) {
2503
- this.setModLfoToPitch(channel, note);
2668
+ const channelPressure = channel.channelPressureTable[3] *
2669
+ channel.state.channelPressure;
2670
+ const polyphonicKeyPressure = (0 < note.pressure)
2671
+ ? channel.polyphonicKeyPressureTable[3] * note.pressure
2672
+ : 0;
2673
+ const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 600;
2674
+ this.setModLfoToPitch(channel, note, pressure);
2504
2675
  }
2505
2676
  if (table[4] !== 0) {
2506
- this.setModLfoToFilterFc(channel, note);
2677
+ const channelPressure = channel.channelPressureTable[4] *
2678
+ channel.state.channelPressure;
2679
+ const polyphonicKeyPressure = (0 < note.pressure)
2680
+ ? channel.polyphonicKeyPressureTable[4] * note.pressure
2681
+ : 0;
2682
+ const pressure = (channelPressure + polyphonicKeyPressure) / 254 * 2400;
2683
+ this.setModLfoToFilterFc(note, pressure);
2507
2684
  }
2508
2685
  if (table[5] !== 0) {
2509
- this.setModLfoToVolume(channel, note);
2686
+ const channelPressure = channel.channelPressureTable[5] *
2687
+ channel.state.channelPressure;
2688
+ const polyphonicKeyPressure = (0 < note.pressure)
2689
+ ? channel.polyphonicKeyPressureTable[5] * note.pressure
2690
+ : 0;
2691
+ const pressure = (channelPressure + polyphonicKeyPressure) / 254;
2692
+ this.setModLfoToVolume(note, pressure);
2510
2693
  }
2511
2694
  }
2512
- handleChannelPressureSysEx(data) {
2695
+ handleChannelPressureSysEx(data, tableName) {
2513
2696
  const channelNumber = data[4];
2514
- const table = this.channels[channelNumber].pressureTable;
2697
+ const table = this.channels[channelNumber][tableName];
2515
2698
  for (let i = 5; i < data.length - 1; i += 2) {
2516
2699
  const pp = data[i];
2517
2700
  const rr = data[i + 1];
@@ -2601,8 +2784,9 @@ Object.defineProperty(Midy, "channelSettings", {
2601
2784
  value: {
2602
2785
  currentBufferSource: null,
2603
2786
  detune: 0,
2604
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2605
- pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2787
+ scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
2788
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2789
+ polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2606
2790
  keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2607
2791
  program: 0,
2608
2792
  bank: 121 * 128,
@@ -2617,16 +2801,3 @@ Object.defineProperty(Midy, "channelSettings", {
2617
2801
  modulationDepthRange: 50, // cent
2618
2802
  }
2619
2803
  });
2620
- Object.defineProperty(Midy, "controllerDestinationSettings", {
2621
- enumerable: true,
2622
- configurable: true,
2623
- writable: true,
2624
- value: {
2625
- pitchControl: 0,
2626
- filterCutoffControl: 0,
2627
- amplitudeControl: 1,
2628
- lfoPitchDepth: 0,
2629
- lfoFilterDepth: 0,
2630
- lfoAmplitudeDepth: 0,
2631
- }
2632
- });