@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-GM1.d.ts +22 -4
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +120 -23
- package/esm/midy-GM2.d.ts +34 -24
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +194 -133
- package/esm/midy-GMLite.d.ts +23 -5
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +122 -23
- package/esm/midy.d.ts +37 -23
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +303 -132
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +22 -4
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +120 -23
- package/script/midy-GM2.d.ts +34 -24
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +194 -133
- package/script/midy-GMLite.d.ts +23 -5
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +122 -23
- package/script/midy.d.ts +37 -23
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +303 -132
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
|
|
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
|
|
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
|
|
431
|
-
sostenutoNotes: new
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1031
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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.
|
|
1348
|
-
const pressureDepth = (channel.
|
|
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.
|
|
1352
|
-
channel.
|
|
1353
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 <
|
|
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
|
|
2372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
2491
|
-
|
|
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
|
-
});
|