@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-GM2.js CHANGED
@@ -1,5 +1,57 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
+ // 2-3 times faster than Map
4
+ class SparseMap {
5
+ constructor(size) {
6
+ this.data = new Array(size);
7
+ this.activeIndices = [];
8
+ }
9
+ set(key, value) {
10
+ if (this.data[key] === undefined) {
11
+ this.activeIndices.push(key);
12
+ }
13
+ this.data[key] = value;
14
+ }
15
+ get(key) {
16
+ return this.data[key];
17
+ }
18
+ delete(key) {
19
+ if (this.data[key] !== undefined) {
20
+ this.data[key] = undefined;
21
+ const index = this.activeIndices.indexOf(key);
22
+ if (index !== -1) {
23
+ this.activeIndices.splice(index, 1);
24
+ }
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ has(key) {
30
+ return this.data[key] !== undefined;
31
+ }
32
+ get size() {
33
+ return this.activeIndices.length;
34
+ }
35
+ clear() {
36
+ for (let i = 0; i < this.activeIndices.length; i++) {
37
+ const key = this.activeIndices[i];
38
+ this.data[key] = undefined;
39
+ }
40
+ this.activeIndices = [];
41
+ }
42
+ *[Symbol.iterator]() {
43
+ for (let i = 0; i < this.activeIndices.length; i++) {
44
+ const key = this.activeIndices[i];
45
+ yield [key, this.data[key]];
46
+ }
47
+ }
48
+ forEach(callback) {
49
+ for (let i = 0; i < this.activeIndices.length; i++) {
50
+ const key = this.activeIndices[i];
51
+ callback(this.data[key], key, this);
52
+ }
53
+ }
54
+ }
3
55
  class Note {
4
56
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
5
57
  Object.defineProperty(this, "bufferSource", {
@@ -97,7 +149,6 @@ class Note {
97
149
  const defaultControllerState = {
98
150
  noteOnVelocity: { type: 2, defaultValue: 0 },
99
151
  noteOnKeyNumber: { type: 3, defaultValue: 0 },
100
- polyPressure: { type: 10, defaultValue: 0 },
101
152
  channelPressure: { type: 13, defaultValue: 0 },
102
153
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
103
154
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
@@ -281,6 +332,18 @@ export class MidyGM2 {
281
332
  writable: true,
282
333
  value: this.initSoundFontTable()
283
334
  });
335
+ Object.defineProperty(this, "audioBufferCounter", {
336
+ enumerable: true,
337
+ configurable: true,
338
+ writable: true,
339
+ value: new Map()
340
+ });
341
+ Object.defineProperty(this, "audioBufferCache", {
342
+ enumerable: true,
343
+ configurable: true,
344
+ writable: true,
345
+ value: new Map()
346
+ });
284
347
  Object.defineProperty(this, "isPlaying", {
285
348
  enumerable: true,
286
349
  configurable: true,
@@ -333,7 +396,7 @@ export class MidyGM2 {
333
396
  enumerable: true,
334
397
  configurable: true,
335
398
  writable: true,
336
- value: new Map()
399
+ value: new SparseMap(128)
337
400
  });
338
401
  Object.defineProperty(this, "defaultOptions", {
339
402
  enumerable: true,
@@ -373,7 +436,7 @@ export class MidyGM2 {
373
436
  initSoundFontTable() {
374
437
  const table = new Array(128);
375
438
  for (let i = 0; i < 128; i++) {
376
- table[i] = new Map();
439
+ table[i] = new SparseMap(128);
377
440
  }
378
441
  return table;
379
442
  }
@@ -427,11 +490,8 @@ export class MidyGM2 {
427
490
  state: new ControllerState(),
428
491
  controlTable: this.initControlTable(),
429
492
  ...this.setChannelAudioNodes(audioContext),
430
- scheduledNotes: new Map(),
431
- sostenutoNotes: new Map(),
432
- channelPressure: {
433
- ...this.constructor.controllerDestinationSettings,
434
- },
493
+ scheduledNotes: new SparseMap(128),
494
+ sostenutoNotes: new SparseMap(128),
435
495
  };
436
496
  });
437
497
  return channels;
@@ -465,9 +525,8 @@ export class MidyGM2 {
465
525
  return audioBuffer;
466
526
  }
467
527
  }
468
- async createNoteBufferNode(voiceParams, isSF3) {
528
+ createNoteBufferNode(audioBuffer, voiceParams) {
469
529
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
470
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
471
530
  bufferSource.buffer = audioBuffer;
472
531
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
473
532
  if (bufferSource.loop) {
@@ -556,6 +615,7 @@ export class MidyGM2 {
556
615
  await Promise.all(this.notePromises);
557
616
  this.notePromises = [];
558
617
  this.exclusiveClassMap.clear();
618
+ this.audioBufferCache.clear();
559
619
  resolve();
560
620
  return;
561
621
  }
@@ -571,8 +631,9 @@ export class MidyGM2 {
571
631
  }
572
632
  else if (this.isStopping) {
573
633
  await this.stopNotes(0, true);
574
- this.exclusiveClassMap.clear();
575
634
  this.notePromises = [];
635
+ this.exclusiveClassMap.clear();
636
+ this.audioBufferCache.clear();
576
637
  resolve();
577
638
  this.isStopping = false;
578
639
  this.isPaused = false;
@@ -603,6 +664,9 @@ export class MidyGM2 {
603
664
  secondToTicks(second, secondsPerBeat) {
604
665
  return second * this.ticksPerBeat / secondsPerBeat;
605
666
  }
667
+ getAudioBufferId(programNumber, noteNumber, velocity) {
668
+ return `${programNumber}:${noteNumber}:${velocity}`;
669
+ }
606
670
  extractMidiData(midi) {
607
671
  const instruments = new Set();
608
672
  const timeline = [];
@@ -624,6 +688,8 @@ export class MidyGM2 {
624
688
  switch (event.type) {
625
689
  case "noteOn": {
626
690
  const channel = tmpChannels[event.channel];
691
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
692
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
627
693
  if (channel.programNumber < 0) {
628
694
  channel.programNumber = event.programNumber;
629
695
  switch (channel.bankMSB) {
@@ -673,6 +739,10 @@ export class MidyGM2 {
673
739
  timeline.push(event);
674
740
  }
675
741
  }
742
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
743
+ if (count === 1)
744
+ this.audioBufferCounter.delete(audioBufferId);
745
+ }
676
746
  const priority = {
677
747
  controller: 0,
678
748
  sysEx: 1,
@@ -766,7 +836,7 @@ export class MidyGM2 {
766
836
  return this.resumeTime + now - this.startTime - this.startDelay;
767
837
  }
768
838
  getActiveNotes(channel, time) {
769
- const activeNotes = new Map();
839
+ const activeNotes = new SparseMap(128);
770
840
  channel.scheduledNotes.forEach((noteList) => {
771
841
  const activeNote = this.getActiveNote(noteList, time);
772
842
  if (activeNote) {
@@ -933,28 +1003,31 @@ export class MidyGM2 {
933
1003
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
934
1004
  const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
935
1005
  const pitch = pitchWheel * pitchWheelSensitivity;
936
- const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1006
+ const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
937
1007
  const pressure = pressureDepth * channel.state.channelPressure;
938
1008
  return tuning + pitch + pressure;
939
1009
  }
940
1010
  calcNoteDetune(channel, note) {
941
1011
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
942
1012
  }
943
- updateDetune(channel) {
944
- const now = this.audioContext.currentTime;
1013
+ updateChannelDetune(channel) {
945
1014
  channel.scheduledNotes.forEach((noteList) => {
946
1015
  for (let i = 0; i < noteList.length; i++) {
947
1016
  const note = noteList[i];
948
1017
  if (!note)
949
1018
  continue;
950
- const noteDetune = this.calcNoteDetune(channel, note);
951
- const detune = channel.detune + noteDetune;
952
- note.bufferSource.detune
953
- .cancelScheduledValues(now)
954
- .setValueAtTime(detune, now);
1019
+ this.updateDetune(channel, note, 0);
955
1020
  }
956
1021
  });
957
1022
  }
1023
+ updateDetune(channel, note, pressure) {
1024
+ const now = this.audioContext.currentTime;
1025
+ const noteDetune = this.calcNoteDetune(channel, note);
1026
+ const detune = channel.detune + noteDetune + pressure;
1027
+ note.bufferSource.detune
1028
+ .cancelScheduledValues(now)
1029
+ .setValueAtTime(detune, now);
1030
+ }
958
1031
  getPortamentoTime(channel) {
959
1032
  const factor = 5 * Math.log(10) / 127;
960
1033
  const time = channel.state.portamentoTime;
@@ -972,14 +1045,11 @@ export class MidyGM2 {
972
1045
  .setValueAtTime(0, volDelay)
973
1046
  .linearRampToValueAtTime(sustainVolume, portamentoTime);
974
1047
  }
975
- setVolumeEnvelope(channel, note) {
1048
+ setVolumeEnvelope(note, pressure) {
976
1049
  const now = this.audioContext.currentTime;
977
- const state = channel.state;
978
1050
  const { voiceParams, startTime } = note;
979
- const pressureDepth = channel.pressureTable[2] / 64;
980
- const pressure = 1 + pressureDepth * channel.state.channelPressure;
981
1051
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
982
- pressure;
1052
+ (1 + pressure);
983
1053
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
984
1054
  const volDelay = startTime + voiceParams.volDelay;
985
1055
  const volAttack = volDelay + voiceParams.volAttack;
@@ -1027,10 +1097,8 @@ export class MidyGM2 {
1027
1097
  const { voiceParams, noteNumber, startTime } = note;
1028
1098
  const softPedalFactor = 1 -
1029
1099
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1030
- const pressureDepth = (channel.pressureTable[1] - 64) * 15;
1031
- const pressure = pressureDepth * channel.state.channelPressure;
1032
- const baseCent = voiceParams.initialFilterFc + pressure;
1033
- const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1100
+ const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1101
+ softPedalFactor;
1034
1102
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
1035
1103
  const sustainFreq = baseFreq +
1036
1104
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
@@ -1044,15 +1112,16 @@ export class MidyGM2 {
1044
1112
  .setValueAtTime(adjustedBaseFreq, modDelay)
1045
1113
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1046
1114
  }
1047
- setFilterEnvelope(channel, note) {
1115
+ setFilterEnvelope(channel, note, pressure) {
1048
1116
  const now = this.audioContext.currentTime;
1049
1117
  const state = channel.state;
1050
1118
  const { voiceParams, noteNumber, startTime } = note;
1051
1119
  const softPedalFactor = 1 -
1052
1120
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1053
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1121
+ const baseCent = voiceParams.initialFilterFc + pressure;
1122
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1123
+ const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1054
1124
  softPedalFactor;
1055
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
1056
1125
  const sustainFreq = baseFreq +
1057
1126
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1058
1127
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1079,9 +1148,9 @@ export class MidyGM2 {
1079
1148
  gain: voiceParams.modLfoToFilterFc,
1080
1149
  });
1081
1150
  note.modulationDepth = new GainNode(this.audioContext);
1082
- this.setModLfoToPitch(channel, note);
1151
+ this.setModLfoToPitch(channel, note, 0);
1083
1152
  note.volumeDepth = new GainNode(this.audioContext);
1084
- this.setModLfoToVolume(channel, note);
1153
+ this.setModLfoToVolume(note, 0);
1085
1154
  note.modulationLFO.start(startTime + voiceParams.delayModLFO);
1086
1155
  note.modulationLFO.connect(note.filterDepth);
1087
1156
  note.filterDepth.connect(note.filterNode.frequency);
@@ -1094,8 +1163,7 @@ export class MidyGM2 {
1094
1163
  const { voiceParams } = note;
1095
1164
  const state = channel.state;
1096
1165
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1097
- frequency: this.centToHz(voiceParams.freqVibLFO) *
1098
- state.vibratoRate,
1166
+ frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1099
1167
  });
1100
1168
  note.vibratoLFO.start(startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1101
1169
  note.vibratoDepth = new GainNode(this.audioContext);
@@ -1103,12 +1171,31 @@ export class MidyGM2 {
1103
1171
  note.vibratoLFO.connect(note.vibratoDepth);
1104
1172
  note.vibratoDepth.connect(note.bufferSource.detune);
1105
1173
  }
1174
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1175
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1176
+ const cache = this.audioBufferCache.get(audioBufferId);
1177
+ if (cache) {
1178
+ cache.counter += 1;
1179
+ if (cache.maxCount <= cache.counter) {
1180
+ this.audioBufferCache.delete(audioBufferId);
1181
+ }
1182
+ return cache.audioBuffer;
1183
+ }
1184
+ else {
1185
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1186
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1187
+ const cache = { audioBuffer, maxCount, counter: 1 };
1188
+ this.audioBufferCache.set(audioBufferId, cache);
1189
+ return audioBuffer;
1190
+ }
1191
+ }
1106
1192
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1107
1193
  const state = channel.state;
1108
1194
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1109
1195
  const voiceParams = voice.getAllParams(controllerState);
1110
1196
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1111
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1197
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1198
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1112
1199
  note.volumeNode = new GainNode(this.audioContext);
1113
1200
  note.gainL = new GainNode(this.audioContext);
1114
1201
  note.gainR = new GainNode(this.audioContext);
@@ -1124,8 +1211,8 @@ export class MidyGM2 {
1124
1211
  }
1125
1212
  else {
1126
1213
  note.portamento = false;
1127
- this.setVolumeEnvelope(channel, note);
1128
- this.setFilterEnvelope(channel, note);
1214
+ this.setVolumeEnvelope(note, 0);
1215
+ this.setFilterEnvelope(channel, note, 0);
1129
1216
  }
1130
1217
  if (0 < state.vibratoDepth) {
1131
1218
  this.startVibrato(channel, note, startTime);
@@ -1168,10 +1255,10 @@ export class MidyGM2 {
1168
1255
  if (soundFontIndex === undefined)
1169
1256
  return;
1170
1257
  const soundFont = this.soundFonts[soundFontIndex];
1171
- const isSF3 = soundFont.parsed.info.version.major === 3;
1172
1258
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1173
1259
  if (!voice)
1174
1260
  return;
1261
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1175
1262
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1176
1263
  note.gainL.connect(channel.gainL);
1177
1264
  note.gainR.connect(channel.gainR);
@@ -1340,47 +1427,21 @@ export class MidyGM2 {
1340
1427
  channel.program = program;
1341
1428
  }
1342
1429
  handleChannelPressure(channelNumber, value) {
1430
+ const now = this.audioContext.currentTime;
1343
1431
  const channel = this.channels[channelNumber];
1344
1432
  const prev = channel.state.channelPressure;
1345
1433
  const next = value / 127;
1346
1434
  channel.state.channelPressure = next;
1347
- if (channel.pressureTable[0] !== 64) {
1348
- const pressureDepth = (channel.pressureTable[0] - 64) / 37.5; // 2400 / 64;
1435
+ if (channel.channelPressureTable[0] !== 64) {
1436
+ const pressureDepth = (channel.channelPressureTable[0] - 64) / 37.5; // 2400 / 64;
1349
1437
  channel.detune += pressureDepth * (next - prev);
1350
1438
  }
1351
- const table = channel.pressureTable;
1352
- channel.scheduledNotes.forEach((noteList) => {
1353
- for (let i = 0; i < noteList.length; i++) {
1354
- const note = noteList[i];
1355
- if (!note)
1356
- continue;
1357
- this.applyDestinationSettings(channel, note, table);
1358
- }
1439
+ const table = channel.channelPressureTable;
1440
+ this.getActiveNotes(channel, now).forEach((note) => {
1441
+ this.applyDestinationSettings(channel, note, table);
1359
1442
  });
1360
1443
  // this.applyVoiceParams(channel, 13);
1361
1444
  }
1362
- setChannelPressure(channel, note) {
1363
- if (channel.pressureTable[0] !== 64) {
1364
- this.updateDetune(channel);
1365
- }
1366
- if (!note.portamento) {
1367
- if (channel.pressureTable[1] !== 64) {
1368
- this.setFilterEnvelope(channel, note);
1369
- }
1370
- if (channel.pressureTable[2] !== 64) {
1371
- this.setVolumeEnvelope(channel, note);
1372
- }
1373
- }
1374
- if (channel.pressureTable[3] !== 0) {
1375
- this.setModLfoToPitch(channel, note);
1376
- }
1377
- if (channel.pressureTable[4] !== 0) {
1378
- this.setModLfoToFilterFc(channel, note);
1379
- }
1380
- if (channel.pressureTable[5] !== 0) {
1381
- this.setModLfoToVolume(channel, note);
1382
- }
1383
- }
1384
1445
  handlePitchBendMessage(channelNumber, lsb, msb) {
1385
1446
  const pitchBend = msb * 128 + lsb;
1386
1447
  this.setPitchBend(channelNumber, pitchBend);
@@ -1392,16 +1453,13 @@ export class MidyGM2 {
1392
1453
  const next = (value - 8192) / 8192;
1393
1454
  state.pitchWheel = value / 16383;
1394
1455
  channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
1395
- this.updateDetune(channel);
1456
+ this.updateChannelDetune(channel);
1396
1457
  this.applyVoiceParams(channel, 14);
1397
1458
  }
1398
- setModLfoToPitch(channel, note) {
1459
+ setModLfoToPitch(channel, note, pressure) {
1399
1460
  const now = this.audioContext.currentTime;
1400
- const pressureDepth = channel.pressureTable[3] / 127 * 600;
1401
- const pressure = pressureDepth * channel.state.channelPressure;
1402
1461
  const modLfoToPitch = note.voiceParams.modLfoToPitch + pressure;
1403
- const baseDepth = Math.abs(modLfoToPitch) +
1404
- channel.state.modulationDepth;
1462
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1405
1463
  const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1406
1464
  note.modulationDepth.gain
1407
1465
  .cancelScheduledValues(now)
@@ -1417,22 +1475,18 @@ export class MidyGM2 {
1417
1475
  .cancelScheduledValues(now)
1418
1476
  .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
1419
1477
  }
1420
- setModLfoToFilterFc(channel, note) {
1478
+ setModLfoToFilterFc(note, pressure) {
1421
1479
  const now = this.audioContext.currentTime;
1422
- const pressureDepth = channel.pressureTable[4] / 127 * 2400;
1423
- const pressure = pressureDepth * channel.state.channelPressure;
1424
1480
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc + pressure;
1425
1481
  note.filterDepth.gain
1426
1482
  .cancelScheduledValues(now)
1427
1483
  .setValueAtTime(modLfoToFilterFc, now);
1428
1484
  }
1429
- setModLfoToVolume(channel, note) {
1485
+ setModLfoToVolume(note, pressure) {
1430
1486
  const now = this.audioContext.currentTime;
1431
1487
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1432
1488
  const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1433
- const pressureDepth = channel.pressureTable[5] / 127;
1434
- const pressure = 1 + pressureDepth * channel.state.channelPressure;
1435
- const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * pressure;
1489
+ const volumeDepth = baseDepth * Math.sign(modLfoToVolume) * (1 + pressure);
1436
1490
  note.volumeDepth.gain
1437
1491
  .cancelScheduledValues(now)
1438
1492
  .setValueAtTime(volumeDepth, now);
@@ -1505,11 +1559,18 @@ export class MidyGM2 {
1505
1559
  .cancelScheduledValues(now)
1506
1560
  .setValueAtTime(freqModLFO, now);
1507
1561
  }
1562
+ setFreqVibLFO(channel, note) {
1563
+ const now = this.audioContext.currentTime;
1564
+ const freqVibLFO = note.voiceParams.freqVibLFO;
1565
+ note.vibratoLFO.frequency
1566
+ .cancelScheduledValues(now)
1567
+ .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
1568
+ }
1508
1569
  createVoiceParamsHandlers() {
1509
1570
  return {
1510
1571
  modLfoToPitch: (channel, note, _prevValue) => {
1511
1572
  if (0 < channel.state.modulationDepth) {
1512
- this.setModLfoToPitch(channel, note);
1573
+ this.setModLfoToPitch(channel, note, 0);
1513
1574
  }
1514
1575
  },
1515
1576
  vibLfoToPitch: (channel, note, _prevValue) => {
@@ -1519,12 +1580,12 @@ export class MidyGM2 {
1519
1580
  },
1520
1581
  modLfoToFilterFc: (channel, note, _prevValue) => {
1521
1582
  if (0 < channel.state.modulationDepth) {
1522
- this.setModLfoToFilterFc(channel, note);
1583
+ this.setModLfoToFilterFc(note, 0);
1523
1584
  }
1524
1585
  },
1525
1586
  modLfoToVolume: (channel, note, _prevValue) => {
1526
1587
  if (0 < channel.state.modulationDepth) {
1527
- this.setModLfoToVolume(channel, note);
1588
+ this.setModLfoToVolume(note, 0);
1528
1589
  }
1529
1590
  },
1530
1591
  chorusEffectsSend: (channel, note, prevValue) => {
@@ -1550,11 +1611,7 @@ export class MidyGM2 {
1550
1611
  },
1551
1612
  freqVibLFO: (channel, note, _prevValue) => {
1552
1613
  if (0 < channel.state.vibratoDepth) {
1553
- const now = this.audioContext.currentTime;
1554
- const freqVibLFO = note.voiceParams.freqVibLFO;
1555
- note.vibratoLFO.frequency
1556
- .cancelScheduledValues(now)
1557
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate, now);
1614
+ this.setFreqVibLFO(channel, note);
1558
1615
  }
1559
1616
  },
1560
1617
  };
@@ -1598,7 +1655,7 @@ export class MidyGM2 {
1598
1655
  this.setPortamentoStartFilterEnvelope(channel, note);
1599
1656
  }
1600
1657
  else {
1601
- this.setFilterEnvelope(channel, note);
1658
+ this.setFilterEnvelope(channel, note, 0);
1602
1659
  }
1603
1660
  this.setPitchEnvelope(note);
1604
1661
  }
@@ -1612,7 +1669,7 @@ export class MidyGM2 {
1612
1669
  if (key in voiceParams)
1613
1670
  noteVoiceParams[key] = voiceParams[key];
1614
1671
  }
1615
- this.setVolumeEnvelope(channel, note);
1672
+ this.setVolumeEnvelope(note, 0);
1616
1673
  }
1617
1674
  }
1618
1675
  }
@@ -1651,7 +1708,7 @@ export class MidyGM2 {
1651
1708
  if (handler) {
1652
1709
  handler.call(this, channelNumber, value);
1653
1710
  const channel = this.channels[channelNumber];
1654
- this.applyVoiceParams(channel, controller + 128);
1711
+ this.applyVoiceParams(channel, controllerType + 128);
1655
1712
  this.applyControlTable(channel, controllerType);
1656
1713
  }
1657
1714
  else {
@@ -1782,8 +1839,7 @@ export class MidyGM2 {
1782
1839
  channel.state.sostenutoPedal = value / 127;
1783
1840
  if (64 <= value) {
1784
1841
  const now = this.audioContext.currentTime;
1785
- const activeNotes = this.getActiveNotes(channel, now);
1786
- channel.sostenutoNotes = new Map(activeNotes);
1842
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1787
1843
  }
1788
1844
  else {
1789
1845
  this.releaseSostenutoPedal(channelNumber, value);
@@ -1944,7 +2000,7 @@ export class MidyGM2 {
1944
2000
  const next = value / 128;
1945
2001
  state.pitchWheelSensitivity = next;
1946
2002
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
1947
- this.updateDetune(channel);
2003
+ this.updateChannelDetune(channel);
1948
2004
  this.applyVoiceParams(channel, 16);
1949
2005
  }
1950
2006
  handleFineTuningRPN(channelNumber) {
@@ -1959,7 +2015,7 @@ export class MidyGM2 {
1959
2015
  const next = (value - 8192) / 8.192; // cent
1960
2016
  channel.fineTuning = next;
1961
2017
  channel.detune += next - prev;
1962
- this.updateDetune(channel);
2018
+ this.updateChannelDetune(channel);
1963
2019
  }
1964
2020
  handleCoarseTuningRPN(channelNumber) {
1965
2021
  const channel = this.channels[channelNumber];
@@ -1973,7 +2029,7 @@ export class MidyGM2 {
1973
2029
  const next = (value - 64) * 100; // cent
1974
2030
  channel.coarseTuning = next;
1975
2031
  channel.detune += next - prev;
1976
- this.updateDetune(channel);
2032
+ this.updateChannelDetune(channel);
1977
2033
  }
1978
2034
  handleModulationDepthRangeRPN(channelNumber) {
1979
2035
  const channel = this.channels[channelNumber];
@@ -2004,7 +2060,7 @@ export class MidyGM2 {
2004
2060
  const state = channel.state;
2005
2061
  for (let i = 0; i < stateTypes.length; i++) {
2006
2062
  const type = stateTypes[i];
2007
- state[type] = defaultControllerState[type];
2063
+ state[type] = defaultControllerState[type].defaultValue;
2008
2064
  }
2009
2065
  const settingTypes = [
2010
2066
  "rpnMSB",
@@ -2036,7 +2092,7 @@ export class MidyGM2 {
2036
2092
  switch (data[3]) {
2037
2093
  case 8:
2038
2094
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2039
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2095
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2040
2096
  default:
2041
2097
  console.warn(`Unsupported Exclusive Message: ${data}`);
2042
2098
  }
@@ -2098,7 +2154,7 @@ export class MidyGM2 {
2098
2154
  case 9:
2099
2155
  switch (data[3]) {
2100
2156
  case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2101
- return this.handleChannelPressureSysEx(data);
2157
+ return this.handlePressureSysEx(data, "channelPressureTable");
2102
2158
  case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
2103
2159
  return this.handleControlChangeSysEx(data);
2104
2160
  default:
@@ -2140,7 +2196,7 @@ export class MidyGM2 {
2140
2196
  const next = (value - 8192) / 8.192; // cent
2141
2197
  this.masterFineTuning = next;
2142
2198
  channel.detune += next - prev;
2143
- this.updateDetune(channel);
2199
+ this.updateChannelDetune(channel);
2144
2200
  }
2145
2201
  handleMasterCoarseTuningSysEx(data) {
2146
2202
  const coarseTuning = data[4];
@@ -2151,7 +2207,7 @@ export class MidyGM2 {
2151
2207
  const next = (value - 64) * 100; // cent
2152
2208
  this.masterCoarseTuning = next;
2153
2209
  channel.detune += next - prev;
2154
- this.updateDetune(channel);
2210
+ this.updateChannelDetune(channel);
2155
2211
  }
2156
2212
  handleGlobalParameterControlSysEx(data) {
2157
2213
  if (data[7] === 1) {
@@ -2358,8 +2414,8 @@ export class MidyGM2 {
2358
2414
  }
2359
2415
  return bitmap;
2360
2416
  }
2361
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2362
- if (data.length < 18) {
2417
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2418
+ if (data.length < 19) {
2363
2419
  console.error("Data length is too short");
2364
2420
  return;
2365
2421
  }
@@ -2367,37 +2423,55 @@ export class MidyGM2 {
2367
2423
  for (let i = 0; i < channelBitmap.length; i++) {
2368
2424
  if (!channelBitmap[i])
2369
2425
  continue;
2426
+ const channel = this.channels[i];
2370
2427
  for (let j = 0; j < 12; j++) {
2371
- const value = data[j + 7] - 64; // cent
2372
- this.channels[i].scaleOctaveTuningTable[j] = value;
2428
+ const centValue = data[j + 7] - 64;
2429
+ channel.scaleOctaveTuningTable[j] = centValue;
2373
2430
  }
2431
+ if (realtime)
2432
+ this.updateChannelDetune(channel);
2374
2433
  }
2375
2434
  }
2376
2435
  applyDestinationSettings(channel, note, table) {
2377
2436
  if (table[0] !== 64) {
2378
- this.updateDetune(channel);
2437
+ this.updateDetune(channel, note, 0);
2379
2438
  }
2380
2439
  if (!note.portamento) {
2381
2440
  if (table[1] !== 64) {
2382
- this.setFilterEnvelope(channel, note);
2441
+ const channelPressure = channel.channelPressureTable[1] *
2442
+ channel.state.channelPressure;
2443
+ const pressure = (channelPressure - 64) * 15;
2444
+ this.setFilterEnvelope(channel, note, pressure);
2383
2445
  }
2384
2446
  if (table[2] !== 64) {
2385
- this.setVolumeEnvelope(channel, note);
2447
+ const channelPressure = channel.channelPressureTable[2] *
2448
+ channel.state.channelPressure;
2449
+ const pressure = channelPressure / 64;
2450
+ this.setVolumeEnvelope(note, pressure);
2386
2451
  }
2387
2452
  }
2388
2453
  if (table[3] !== 0) {
2389
- this.setModLfoToPitch(channel, note);
2454
+ const channelPressure = channel.channelPressureTable[3] *
2455
+ channel.state.channelPressure;
2456
+ const pressure = channelPressure / 127 * 600;
2457
+ this.setModLfoToPitch(channel, note, pressure);
2390
2458
  }
2391
2459
  if (table[4] !== 0) {
2392
- this.setModLfoToFilterFc(channel, note);
2460
+ const channelPressure = channel.channelPressureTable[4] *
2461
+ channel.state.channelPressure;
2462
+ const pressure = channelPressure / 127 * 2400;
2463
+ this.setModLfoToFilterFc(note, pressure);
2393
2464
  }
2394
2465
  if (table[5] !== 0) {
2395
- this.setModLfoToVolume(channel, note);
2466
+ const channelPressure = channel.channelPressureTable[5] *
2467
+ channel.state.channelPressure;
2468
+ const pressure = channelPressure / 127;
2469
+ this.setModLfoToVolume(note, pressure);
2396
2470
  }
2397
2471
  }
2398
- handleChannelPressureSysEx(data) {
2472
+ handleChannelPressureSysEx(data, tableName) {
2399
2473
  const channelNumber = data[4];
2400
- const table = this.channels[channelNumber].pressureTable;
2474
+ const table = this.channels[channelNumber][tableName];
2401
2475
  for (let i = 5; i < data.length - 1; i += 2) {
2402
2476
  const pp = data[i];
2403
2477
  const rr = data[i + 1];
@@ -2487,8 +2561,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2487
2561
  value: {
2488
2562
  currentBufferSource: null,
2489
2563
  detune: 0,
2490
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2491
- pressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2564
+ scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
2565
+ channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2492
2566
  keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2493
2567
  program: 0,
2494
2568
  bank: 121 * 128,
@@ -2503,16 +2577,3 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2503
2577
  modulationDepthRange: 50, // cent
2504
2578
  }
2505
2579
  });
2506
- Object.defineProperty(MidyGM2, "controllerDestinationSettings", {
2507
- enumerable: true,
2508
- configurable: true,
2509
- writable: true,
2510
- value: {
2511
- pitchControl: 0,
2512
- filterCutoffControl: 0,
2513
- amplitudeControl: 1,
2514
- lfoPitchDepth: 0,
2515
- lfoFilterDepth: 0,
2516
- lfoAmplitudeDepth: 0,
2517
- }
2518
- });