@marmooo/midy 0.4.6 → 0.4.8
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 +3 -0
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +115 -53
- package/esm/midy-GM2.d.ts +16 -4
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +255 -159
- package/esm/midy-GMLite.d.ts +2 -0
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +109 -53
- package/esm/midy.d.ts +34 -11
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +352 -223
- package/package.json +3 -2
- package/script/midy-GM1.d.ts +3 -0
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +115 -53
- package/script/midy-GM2.d.ts +16 -4
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +255 -159
- package/script/midy-GMLite.d.ts +2 -0
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +109 -53
- package/script/midy.d.ts +34 -11
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +352 -223
package/script/midy.js
CHANGED
|
@@ -3,6 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Midy = void 0;
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
|
+
const ogg_vorbis_1 = require("@wasm-audio-decoders/ogg-vorbis");
|
|
7
|
+
let decoderPromise = null;
|
|
8
|
+
let decoderQueue = Promise.resolve();
|
|
9
|
+
function initDecoder() {
|
|
10
|
+
if (!decoderPromise) {
|
|
11
|
+
const instance = new ogg_vorbis_1.OggVorbisDecoderWebWorker();
|
|
12
|
+
decoderPromise = instance.ready.then(() => instance);
|
|
13
|
+
}
|
|
14
|
+
return decoderPromise;
|
|
15
|
+
}
|
|
6
16
|
class Note {
|
|
7
17
|
constructor(noteNumber, velocity, startTime) {
|
|
8
18
|
Object.defineProperty(this, "voice", {
|
|
@@ -41,49 +51,55 @@ class Note {
|
|
|
41
51
|
writable: true,
|
|
42
52
|
value: void 0
|
|
43
53
|
});
|
|
44
|
-
Object.defineProperty(this, "
|
|
54
|
+
Object.defineProperty(this, "filterEnvelopeNode", {
|
|
45
55
|
enumerable: true,
|
|
46
56
|
configurable: true,
|
|
47
57
|
writable: true,
|
|
48
58
|
value: void 0
|
|
49
59
|
});
|
|
50
|
-
Object.defineProperty(this, "
|
|
60
|
+
Object.defineProperty(this, "volumeEnvelopeNode", {
|
|
51
61
|
enumerable: true,
|
|
52
62
|
configurable: true,
|
|
53
63
|
writable: true,
|
|
54
64
|
value: void 0
|
|
55
65
|
});
|
|
56
|
-
Object.defineProperty(this, "
|
|
66
|
+
Object.defineProperty(this, "volumeNode", {
|
|
57
67
|
enumerable: true,
|
|
58
68
|
configurable: true,
|
|
59
69
|
writable: true,
|
|
60
70
|
value: void 0
|
|
61
|
-
});
|
|
62
|
-
Object.defineProperty(this, "
|
|
71
|
+
}); // polyphonic key pressure
|
|
72
|
+
Object.defineProperty(this, "modLfo", {
|
|
63
73
|
enumerable: true,
|
|
64
74
|
configurable: true,
|
|
65
75
|
writable: true,
|
|
66
76
|
value: void 0
|
|
67
|
-
});
|
|
68
|
-
Object.defineProperty(this, "
|
|
77
|
+
}); // CC#1 modulation LFO
|
|
78
|
+
Object.defineProperty(this, "modLfoToPitch", {
|
|
69
79
|
enumerable: true,
|
|
70
80
|
configurable: true,
|
|
71
81
|
writable: true,
|
|
72
82
|
value: void 0
|
|
73
83
|
});
|
|
74
|
-
Object.defineProperty(this, "
|
|
84
|
+
Object.defineProperty(this, "modLfoToFilterFc", {
|
|
75
85
|
enumerable: true,
|
|
76
86
|
configurable: true,
|
|
77
87
|
writable: true,
|
|
78
88
|
value: void 0
|
|
79
89
|
});
|
|
80
|
-
Object.defineProperty(this, "
|
|
90
|
+
Object.defineProperty(this, "modLfoToVolume", {
|
|
81
91
|
enumerable: true,
|
|
82
92
|
configurable: true,
|
|
83
93
|
writable: true,
|
|
84
94
|
value: void 0
|
|
85
95
|
});
|
|
86
|
-
Object.defineProperty(this, "
|
|
96
|
+
Object.defineProperty(this, "vibLfo", {
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true,
|
|
99
|
+
writable: true,
|
|
100
|
+
value: void 0
|
|
101
|
+
}); // vibrato LFO
|
|
102
|
+
Object.defineProperty(this, "vibLfoToPitch", {
|
|
87
103
|
enumerable: true,
|
|
88
104
|
configurable: true,
|
|
89
105
|
writable: true,
|
|
@@ -256,7 +272,20 @@ const pitchEnvelopeKeys = [
|
|
|
256
272
|
"playbackRate",
|
|
257
273
|
];
|
|
258
274
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
275
|
+
const effectParameters = [
|
|
276
|
+
2400 / 64, // cent
|
|
277
|
+
9600 / 64, // cent
|
|
278
|
+
1 / 64,
|
|
279
|
+
600 / 127, // cent
|
|
280
|
+
2400 / 127, // cent
|
|
281
|
+
1 / 127,
|
|
282
|
+
];
|
|
283
|
+
const pressureBaselines = new Int8Array([64, 64, 0, 0, 0, 0]);
|
|
259
284
|
const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
|
|
285
|
+
const defaultControlValues = new Int8Array([
|
|
286
|
+
...[-1, -1, -1, -1, -1, -1],
|
|
287
|
+
...defaultPressureValues,
|
|
288
|
+
]);
|
|
260
289
|
function cbToRatio(cb) {
|
|
261
290
|
return Math.pow(10, cb / 200);
|
|
262
291
|
}
|
|
@@ -405,6 +434,12 @@ class Midy extends EventTarget {
|
|
|
405
434
|
writable: true,
|
|
406
435
|
value: new Map()
|
|
407
436
|
});
|
|
437
|
+
Object.defineProperty(this, "decodeMethod", {
|
|
438
|
+
enumerable: true,
|
|
439
|
+
configurable: true,
|
|
440
|
+
writable: true,
|
|
441
|
+
value: "wasm-audio-decoders"
|
|
442
|
+
});
|
|
408
443
|
Object.defineProperty(this, "isPlaying", {
|
|
409
444
|
enumerable: true,
|
|
410
445
|
configurable: true,
|
|
@@ -524,6 +559,9 @@ class Midy extends EventTarget {
|
|
|
524
559
|
noteToChannel: new Map(),
|
|
525
560
|
}
|
|
526
561
|
});
|
|
562
|
+
this.decoder = new ogg_vorbis_1.OggVorbisDecoderWebWorker();
|
|
563
|
+
this.decoderReady = this.decoder.ready;
|
|
564
|
+
this.decoderQueue = Promise.resolve();
|
|
527
565
|
this.audioContext = audioContext;
|
|
528
566
|
this.masterVolume = new GainNode(audioContext);
|
|
529
567
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
@@ -535,6 +573,7 @@ class Midy extends EventTarget {
|
|
|
535
573
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
536
574
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
537
575
|
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
576
|
+
this.effectHandlers = this.createEffectHandlers();
|
|
538
577
|
this.channels = this.createChannels(audioContext);
|
|
539
578
|
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
540
579
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
@@ -662,7 +701,7 @@ class Midy extends EventTarget {
|
|
|
662
701
|
};
|
|
663
702
|
}
|
|
664
703
|
resetChannelTable(channel) {
|
|
665
|
-
channel.controlTable.
|
|
704
|
+
channel.controlTable.set(defaultControlValues);
|
|
666
705
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
667
706
|
channel.channelPressureTable.set(defaultPressureValues);
|
|
668
707
|
channel.polyphonicKeyPressureTable.set(defaultPressureValues);
|
|
@@ -679,7 +718,7 @@ class Midy extends EventTarget {
|
|
|
679
718
|
scheduledNotes: [],
|
|
680
719
|
sustainNotes: [],
|
|
681
720
|
sostenutoNotes: [],
|
|
682
|
-
controlTable:
|
|
721
|
+
controlTable: new Int8Array(defaultControlValues),
|
|
683
722
|
scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
|
|
684
723
|
channelPressureTable: new Int8Array(defaultPressureValues),
|
|
685
724
|
polyphonicKeyPressureTable: new Int8Array(defaultPressureValues),
|
|
@@ -690,11 +729,57 @@ class Midy extends EventTarget {
|
|
|
690
729
|
});
|
|
691
730
|
return channels;
|
|
692
731
|
}
|
|
732
|
+
decodeOggVorbis(sample) {
|
|
733
|
+
const task = decoderQueue.then(async () => {
|
|
734
|
+
const decoder = await initDecoder();
|
|
735
|
+
const slice = sample.data.slice();
|
|
736
|
+
const { channelData, sampleRate, errors } = await decoder.decodeFile(slice);
|
|
737
|
+
if (0 < errors.length) {
|
|
738
|
+
throw new Error(errors.join(", "));
|
|
739
|
+
}
|
|
740
|
+
const audioBuffer = new AudioBuffer({
|
|
741
|
+
numberOfChannels: channelData.length,
|
|
742
|
+
length: channelData[0].length,
|
|
743
|
+
sampleRate,
|
|
744
|
+
});
|
|
745
|
+
for (let ch = 0; ch < channelData.length; ch++) {
|
|
746
|
+
audioBuffer.getChannelData(ch).set(channelData[ch]);
|
|
747
|
+
}
|
|
748
|
+
return audioBuffer;
|
|
749
|
+
});
|
|
750
|
+
decoderQueue = task.catch(() => { });
|
|
751
|
+
return task;
|
|
752
|
+
}
|
|
693
753
|
async createAudioBuffer(voiceParams) {
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
754
|
+
const sample = voiceParams.sample;
|
|
755
|
+
if (sample.type === "compressed") {
|
|
756
|
+
switch (this.decodeMethod) {
|
|
757
|
+
case "decodeAudioData": {
|
|
758
|
+
// https://jakearchibald.com/2016/sounds-fun/
|
|
759
|
+
// https://github.com/WebAudio/web-audio-api/issues/1091
|
|
760
|
+
// decodeAudioData() has priming issues on Safari
|
|
761
|
+
const arrayBuffer = sample.data.slice().buffer;
|
|
762
|
+
return await this.audioContext.decodeAudioData(arrayBuffer);
|
|
763
|
+
}
|
|
764
|
+
case "wasm-audio-decoders":
|
|
765
|
+
return await this.decodeOggVorbis(sample);
|
|
766
|
+
default:
|
|
767
|
+
throw new Error(`Unknown decodeMethod: ${this.decodeMethod}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
const data = sample.data;
|
|
772
|
+
const end = data.length + voiceParams.end;
|
|
773
|
+
const subarray = data.subarray(voiceParams.start, end);
|
|
774
|
+
const pcm = sample.decodePCM(subarray);
|
|
775
|
+
const audioBuffer = new AudioBuffer({
|
|
776
|
+
numberOfChannels: 1,
|
|
777
|
+
length: pcm.length,
|
|
778
|
+
sampleRate: sample.sampleHeader.sampleRate,
|
|
779
|
+
});
|
|
780
|
+
audioBuffer.getChannelData(0).set(pcm);
|
|
781
|
+
return audioBuffer;
|
|
782
|
+
}
|
|
698
783
|
}
|
|
699
784
|
isLoopDrum(channel, noteNumber) {
|
|
700
785
|
const programNumber = channel.programNumber;
|
|
@@ -1008,8 +1093,8 @@ class Midy extends EventTarget {
|
|
|
1008
1093
|
}
|
|
1009
1094
|
stopNotes(velocity, force, scheduleTime) {
|
|
1010
1095
|
const channels = this.channels;
|
|
1011
|
-
for (let
|
|
1012
|
-
this.stopChannelNotes(
|
|
1096
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
1097
|
+
this.stopChannelNotes(ch, velocity, force, scheduleTime);
|
|
1013
1098
|
}
|
|
1014
1099
|
const stopPromise = Promise.all(this.notePromises);
|
|
1015
1100
|
this.notePromises = [];
|
|
@@ -1284,16 +1369,8 @@ class Midy extends EventTarget {
|
|
|
1284
1369
|
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
1285
1370
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
1286
1371
|
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1290
|
-
const channelPressure = channelPressureDepth *
|
|
1291
|
-
channel.state.channelPressure;
|
|
1292
|
-
return tuning + pitch + channelPressure;
|
|
1293
|
-
}
|
|
1294
|
-
else {
|
|
1295
|
-
return tuning + pitch;
|
|
1296
|
-
}
|
|
1372
|
+
const effect = this.getChannelPitchControl(channel);
|
|
1373
|
+
return tuning + pitch + effect;
|
|
1297
1374
|
}
|
|
1298
1375
|
updateChannelDetune(channel, scheduleTime) {
|
|
1299
1376
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1311,7 +1388,7 @@ class Midy extends EventTarget {
|
|
|
1311
1388
|
calcNoteDetune(channel, note) {
|
|
1312
1389
|
const noteDetune = note.voiceParams.detune +
|
|
1313
1390
|
this.calcScaleOctaveTuning(channel, note);
|
|
1314
|
-
const pitchControl = this.
|
|
1391
|
+
const pitchControl = this.getNotePitchControl(channel, note);
|
|
1315
1392
|
return channel.detune + noteDetune + pitchControl;
|
|
1316
1393
|
}
|
|
1317
1394
|
getPortamentoTime(channel, note) {
|
|
@@ -1380,7 +1457,7 @@ class Midy extends EventTarget {
|
|
|
1380
1457
|
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1381
1458
|
const { voiceParams, startTime } = note;
|
|
1382
1459
|
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1383
|
-
(1 + this.
|
|
1460
|
+
(1 + this.getChannelAmplitudeControl(channel));
|
|
1384
1461
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1385
1462
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1386
1463
|
note.volumeEnvelopeNode.gain
|
|
@@ -1388,15 +1465,17 @@ class Midy extends EventTarget {
|
|
|
1388
1465
|
.exponentialRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1389
1466
|
}
|
|
1390
1467
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1391
|
-
const { voiceParams, startTime } = note;
|
|
1468
|
+
const { voiceParams, startTime, noteNumber } = note;
|
|
1392
1469
|
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1393
|
-
(1 + this.
|
|
1470
|
+
(1 + this.getChannelAmplitudeControl(channel));
|
|
1394
1471
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1395
1472
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1396
|
-
const attackTime = this.getRelativeKeyBasedValue(channel,
|
|
1473
|
+
const attackTime = this.getRelativeKeyBasedValue(channel, noteNumber, 73) *
|
|
1474
|
+
2;
|
|
1397
1475
|
const volAttack = volDelay + voiceParams.volAttack * attackTime;
|
|
1398
1476
|
const volHold = volAttack + voiceParams.volHold;
|
|
1399
|
-
const decayTime = this.getRelativeKeyBasedValue(channel,
|
|
1477
|
+
const decayTime = this.getRelativeKeyBasedValue(channel, noteNumber, 75) *
|
|
1478
|
+
2;
|
|
1400
1479
|
const decayDuration = voiceParams.volDecay * decayTime;
|
|
1401
1480
|
note.volumeEnvelopeNode.gain
|
|
1402
1481
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1406,6 +1485,12 @@ class Midy extends EventTarget {
|
|
|
1406
1485
|
.setValueAtTime(attackVolume, volHold)
|
|
1407
1486
|
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
1408
1487
|
}
|
|
1488
|
+
setVolumeNode(channel, note, scheduleTime) {
|
|
1489
|
+
const depth = 1 + this.getNoteAmplitudeControl(channel, note);
|
|
1490
|
+
note.volumeNode.gain
|
|
1491
|
+
.cancelScheduledValues(scheduleTime)
|
|
1492
|
+
.setValueAtTime(depth, scheduleTime);
|
|
1493
|
+
}
|
|
1409
1494
|
setPortamentoDetune(channel, note, scheduleTime) {
|
|
1410
1495
|
if (channel.portamentoControl) {
|
|
1411
1496
|
const state = channel.state;
|
|
@@ -1467,9 +1552,10 @@ class Midy extends EventTarget {
|
|
|
1467
1552
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1468
1553
|
}
|
|
1469
1554
|
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1470
|
-
const { voiceParams, startTime } = note;
|
|
1555
|
+
const { voiceParams, startTime, noteNumber } = note;
|
|
1471
1556
|
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1472
|
-
const brightness = this.getRelativeKeyBasedValue(channel,
|
|
1557
|
+
const brightness = this.getRelativeKeyBasedValue(channel, noteNumber, 74) *
|
|
1558
|
+
2;
|
|
1473
1559
|
const scale = softPedalFactor * brightness;
|
|
1474
1560
|
const baseCent = voiceParams.initialFilterFc +
|
|
1475
1561
|
this.getFilterCutoffControl(channel, note);
|
|
@@ -1482,14 +1568,14 @@ class Midy extends EventTarget {
|
|
|
1482
1568
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1483
1569
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1484
1570
|
note.adjustedBaseFreq = adjustedSustainFreq;
|
|
1485
|
-
note.
|
|
1571
|
+
note.filterEnvelopeNode.frequency
|
|
1486
1572
|
.cancelScheduledValues(scheduleTime)
|
|
1487
1573
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1488
1574
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
1489
1575
|
.exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
|
|
1490
1576
|
}
|
|
1491
1577
|
setFilterEnvelope(channel, note, scheduleTime) {
|
|
1492
|
-
const { voiceParams, startTime } = note;
|
|
1578
|
+
const { voiceParams, startTime, noteNumber } = note;
|
|
1493
1579
|
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
1494
1580
|
const baseCent = voiceParams.initialFilterFc +
|
|
1495
1581
|
this.getFilterCutoffControl(channel, note);
|
|
@@ -1497,7 +1583,8 @@ class Midy extends EventTarget {
|
|
|
1497
1583
|
const sustainCent = baseCent +
|
|
1498
1584
|
modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
1499
1585
|
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1500
|
-
const brightness = this.getRelativeKeyBasedValue(channel,
|
|
1586
|
+
const brightness = this.getRelativeKeyBasedValue(channel, noteNumber, 74) *
|
|
1587
|
+
2;
|
|
1501
1588
|
const scale = softPedalFactor * brightness;
|
|
1502
1589
|
const baseFreq = this.centToHz(baseCent) * scale;
|
|
1503
1590
|
const peekFreq = this.centToHz(peekCent) * scale;
|
|
@@ -1508,9 +1595,9 @@ class Midy extends EventTarget {
|
|
|
1508
1595
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1509
1596
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
1510
1597
|
const modHold = modAttack + voiceParams.modHold;
|
|
1511
|
-
const decayDuration =
|
|
1598
|
+
const decayDuration = voiceParams.modDecay;
|
|
1512
1599
|
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
1513
|
-
note.
|
|
1600
|
+
note.filterEnvelopeNode.frequency
|
|
1514
1601
|
.cancelScheduledValues(scheduleTime)
|
|
1515
1602
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1516
1603
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
@@ -1521,36 +1608,37 @@ class Midy extends EventTarget {
|
|
|
1521
1608
|
startModulation(channel, note, scheduleTime) {
|
|
1522
1609
|
const audioContext = this.audioContext;
|
|
1523
1610
|
const { voiceParams } = note;
|
|
1524
|
-
note.
|
|
1611
|
+
note.modLfo = new OscillatorNode(audioContext, {
|
|
1525
1612
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
1526
1613
|
});
|
|
1527
|
-
note.
|
|
1614
|
+
note.modLfoToFilterFc = new GainNode(audioContext, {
|
|
1528
1615
|
gain: voiceParams.modLfoToFilterFc,
|
|
1529
1616
|
});
|
|
1530
|
-
note.
|
|
1617
|
+
note.modLfoToPitch = new GainNode(audioContext);
|
|
1531
1618
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1532
|
-
note.
|
|
1619
|
+
note.modLfoToVolume = new GainNode(audioContext);
|
|
1533
1620
|
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
1534
|
-
note.
|
|
1535
|
-
note.
|
|
1536
|
-
note.
|
|
1537
|
-
note.
|
|
1538
|
-
note.
|
|
1539
|
-
note.
|
|
1540
|
-
note.
|
|
1621
|
+
note.modLfo.start(note.startTime + voiceParams.delayModLFO);
|
|
1622
|
+
note.modLfo.connect(note.modLfoToFilterFc);
|
|
1623
|
+
note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
|
|
1624
|
+
note.modLfo.connect(note.modLfoToPitch);
|
|
1625
|
+
note.modLfoToPitch.connect(note.bufferSource.detune);
|
|
1626
|
+
note.modLfo.connect(note.modLfoToVolume);
|
|
1627
|
+
note.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
|
|
1541
1628
|
}
|
|
1542
1629
|
startVibrato(channel, note, scheduleTime) {
|
|
1543
|
-
const { voiceParams } = note;
|
|
1544
|
-
const vibratoRate = this.getRelativeKeyBasedValue(channel,
|
|
1545
|
-
|
|
1546
|
-
|
|
1630
|
+
const { voiceParams, noteNumber } = note;
|
|
1631
|
+
const vibratoRate = this.getRelativeKeyBasedValue(channel, noteNumber, 76) *
|
|
1632
|
+
2;
|
|
1633
|
+
const vibratoDelay = this.getRelativeKeyBasedValue(channel, noteNumber, 78) * 2;
|
|
1634
|
+
note.vibLfo = new OscillatorNode(this.audioContext, {
|
|
1547
1635
|
frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
|
|
1548
1636
|
});
|
|
1549
|
-
note.
|
|
1550
|
-
note.
|
|
1637
|
+
note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
|
|
1638
|
+
note.vibLfoToPitch = new GainNode(this.audioContext);
|
|
1551
1639
|
this.setVibLfoToPitch(channel, note, scheduleTime);
|
|
1552
|
-
note.
|
|
1553
|
-
note.
|
|
1640
|
+
note.vibLfo.connect(note.vibLfoToPitch);
|
|
1641
|
+
note.vibLfoToPitch.connect(note.bufferSource.detune);
|
|
1554
1642
|
}
|
|
1555
1643
|
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
1556
1644
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
@@ -1591,8 +1679,9 @@ class Midy extends EventTarget {
|
|
|
1591
1679
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1592
1680
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1593
1681
|
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1594
|
-
|
|
1595
|
-
|
|
1682
|
+
note.volumeNode = new GainNode(audioContext);
|
|
1683
|
+
const filterResonance = this.getRelativeKeyBasedValue(channel, noteNumber, 71);
|
|
1684
|
+
note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
|
|
1596
1685
|
type: "lowpass",
|
|
1597
1686
|
Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
|
|
1598
1687
|
});
|
|
@@ -1600,6 +1689,7 @@ class Midy extends EventTarget {
|
|
|
1600
1689
|
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1601
1690
|
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1602
1691
|
}
|
|
1692
|
+
this.setVolumeNode(channel, note, now);
|
|
1603
1693
|
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
1604
1694
|
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1605
1695
|
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
@@ -1622,8 +1712,9 @@ class Midy extends EventTarget {
|
|
|
1622
1712
|
channel.currentBufferSource.stop(startTime);
|
|
1623
1713
|
channel.currentBufferSource = note.bufferSource;
|
|
1624
1714
|
}
|
|
1625
|
-
note.bufferSource.connect(note.
|
|
1626
|
-
note.
|
|
1715
|
+
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
1716
|
+
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
1717
|
+
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
1627
1718
|
this.setChorusSend(channel, note, now);
|
|
1628
1719
|
this.setReverbSend(channel, note, now);
|
|
1629
1720
|
if (voiceParams.sample.type === "compressed") {
|
|
@@ -1670,7 +1761,7 @@ class Midy extends EventTarget {
|
|
|
1670
1761
|
}
|
|
1671
1762
|
setNoteRouting(channelNumber, note, startTime) {
|
|
1672
1763
|
const channel = this.channels[channelNumber];
|
|
1673
|
-
const { noteNumber,
|
|
1764
|
+
const { noteNumber, volumeNode } = note;
|
|
1674
1765
|
if (channel.isDrum) {
|
|
1675
1766
|
const { keyBasedGainLs, keyBasedGainRs } = channel;
|
|
1676
1767
|
let gainL = keyBasedGainLs[noteNumber];
|
|
@@ -1680,12 +1771,12 @@ class Midy extends EventTarget {
|
|
|
1680
1771
|
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
1681
1772
|
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
1682
1773
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1774
|
+
volumeNode.connect(gainL);
|
|
1775
|
+
volumeNode.connect(gainR);
|
|
1685
1776
|
}
|
|
1686
1777
|
else {
|
|
1687
|
-
|
|
1688
|
-
|
|
1778
|
+
volumeNode.connect(channel.gainL);
|
|
1779
|
+
volumeNode.connect(channel.gainR);
|
|
1689
1780
|
}
|
|
1690
1781
|
if (0.5 <= channel.state.sustainPedal) {
|
|
1691
1782
|
channel.sustainNotes.push(note);
|
|
@@ -1717,6 +1808,8 @@ class Midy extends EventTarget {
|
|
|
1717
1808
|
scheduledNotes.push(note);
|
|
1718
1809
|
const programNumber = channel.programNumber;
|
|
1719
1810
|
const bankTable = this.soundFontTable[programNumber];
|
|
1811
|
+
if (!bankTable)
|
|
1812
|
+
return;
|
|
1720
1813
|
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
1721
1814
|
if (bankTable[bank] === undefined) {
|
|
1722
1815
|
if (channel.isDrum)
|
|
@@ -1737,16 +1830,17 @@ class Midy extends EventTarget {
|
|
|
1737
1830
|
}
|
|
1738
1831
|
disconnectNote(note) {
|
|
1739
1832
|
note.bufferSource.disconnect();
|
|
1740
|
-
note.
|
|
1833
|
+
note.filterEnvelopeNode.disconnect();
|
|
1741
1834
|
note.volumeEnvelopeNode.disconnect();
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
note.
|
|
1745
|
-
note.
|
|
1835
|
+
note.volumeNode.disconnect();
|
|
1836
|
+
if (note.modLfoToPitch) {
|
|
1837
|
+
note.modLfoToVolume.disconnect();
|
|
1838
|
+
note.modLfoToPitch.disconnect();
|
|
1839
|
+
note.modLfo.stop();
|
|
1746
1840
|
}
|
|
1747
|
-
if (note.
|
|
1748
|
-
note.
|
|
1749
|
-
note.
|
|
1841
|
+
if (note.vibLfoToPitch) {
|
|
1842
|
+
note.vibLfoToPitch.disconnect();
|
|
1843
|
+
note.vibLfo.stop();
|
|
1750
1844
|
}
|
|
1751
1845
|
if (note.reverbSend) {
|
|
1752
1846
|
note.reverbSend.disconnect();
|
|
@@ -1757,10 +1851,10 @@ class Midy extends EventTarget {
|
|
|
1757
1851
|
}
|
|
1758
1852
|
releaseNote(channel, note, endTime) {
|
|
1759
1853
|
endTime ??= this.audioContext.currentTime;
|
|
1760
|
-
const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
|
|
1854
|
+
const releaseTime = this.getRelativeKeyBasedValue(channel, note.noteNumber, 72) * 2;
|
|
1761
1855
|
const volDuration = note.voiceParams.volRelease * releaseTime;
|
|
1762
1856
|
const volRelease = endTime + volDuration;
|
|
1763
|
-
note.
|
|
1857
|
+
note.filterEnvelopeNode.frequency
|
|
1764
1858
|
.cancelScheduledValues(endTime)
|
|
1765
1859
|
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1766
1860
|
note.volumeEnvelopeNode.gain
|
|
@@ -1930,11 +2024,10 @@ class Midy extends EventTarget {
|
|
|
1930
2024
|
return;
|
|
1931
2025
|
if (!(0 <= scheduleTime))
|
|
1932
2026
|
scheduleTime = this.audioContext.currentTime;
|
|
1933
|
-
const table = channel.polyphonicKeyPressureTable;
|
|
1934
2027
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1935
2028
|
if (note.noteNumber === noteNumber) {
|
|
1936
2029
|
note.pressure = pressure;
|
|
1937
|
-
this.
|
|
2030
|
+
this.setPolyphonicKeyPressureEffects(channel, note, scheduleTime);
|
|
1938
2031
|
}
|
|
1939
2032
|
});
|
|
1940
2033
|
this.applyVoiceParams(channel, 10, scheduleTime);
|
|
@@ -1960,27 +2053,22 @@ class Midy extends EventTarget {
|
|
|
1960
2053
|
}
|
|
1961
2054
|
}
|
|
1962
2055
|
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
2056
|
+
if (!(0 <= scheduleTime))
|
|
2057
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1963
2058
|
this.applyToMPEChannels(channelNumber, (ch) => {
|
|
1964
2059
|
this.applyChannelPressure(ch, value, scheduleTime);
|
|
1965
2060
|
});
|
|
1966
2061
|
}
|
|
1967
2062
|
applyChannelPressure(channelNumber, value, scheduleTime) {
|
|
1968
|
-
if (!(0 <= scheduleTime))
|
|
1969
|
-
scheduleTime = this.audioContext.currentTime;
|
|
1970
2063
|
const channel = this.channels[channelNumber];
|
|
1971
2064
|
if (channel.isDrum)
|
|
1972
2065
|
return;
|
|
1973
|
-
const prev = channel
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
if (0 <= channelPressureRaw) {
|
|
1978
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1979
|
-
channel.detune += channelPressureDepth * (next - prev);
|
|
1980
|
-
}
|
|
1981
|
-
const table = channel.channelPressureTable;
|
|
2066
|
+
const prev = this.calcChannelPressureEffectValue(channel, 0);
|
|
2067
|
+
channel.state.channelPressure = value / 127;
|
|
2068
|
+
const next = this.calcChannelPressureEffectValue(channel, 0);
|
|
2069
|
+
channel.detune += next - prev;
|
|
1982
2070
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1983
|
-
this.
|
|
2071
|
+
this.setChannelPressureEffects(channel, note, scheduleTime);
|
|
1984
2072
|
});
|
|
1985
2073
|
this.applyVoiceParams(channel, 13, scheduleTime);
|
|
1986
2074
|
}
|
|
@@ -2015,7 +2103,7 @@ class Midy extends EventTarget {
|
|
|
2015
2103
|
this.getLFOPitchDepth(channel, note);
|
|
2016
2104
|
const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
|
|
2017
2105
|
const depth = baseDepth * Math.sign(modLfoToPitch);
|
|
2018
|
-
note.
|
|
2106
|
+
note.modLfoToPitch.gain
|
|
2019
2107
|
.cancelScheduledValues(scheduleTime)
|
|
2020
2108
|
.setValueAtTime(depth, scheduleTime);
|
|
2021
2109
|
}
|
|
@@ -2024,13 +2112,12 @@ class Midy extends EventTarget {
|
|
|
2024
2112
|
}
|
|
2025
2113
|
}
|
|
2026
2114
|
setVibLfoToPitch(channel, note, scheduleTime) {
|
|
2027
|
-
if (note.
|
|
2028
|
-
const vibratoDepth = this.
|
|
2029
|
-
2;
|
|
2115
|
+
if (note.vibLfoToPitch) {
|
|
2116
|
+
const vibratoDepth = this.getRelativeKeyBasedValue(channel, note.noteNumber, 77) * 2;
|
|
2030
2117
|
const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
|
|
2031
2118
|
const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
|
|
2032
2119
|
const depth = baseDepth * Math.sign(vibLfoToPitch);
|
|
2033
|
-
note.
|
|
2120
|
+
note.vibLfoToPitch.gain
|
|
2034
2121
|
.cancelScheduledValues(scheduleTime)
|
|
2035
2122
|
.setValueAtTime(depth, scheduleTime);
|
|
2036
2123
|
}
|
|
@@ -2041,18 +2128,18 @@ class Midy extends EventTarget {
|
|
|
2041
2128
|
setModLfoToFilterFc(channel, note, scheduleTime) {
|
|
2042
2129
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
|
|
2043
2130
|
this.getLFOFilterDepth(channel, note);
|
|
2044
|
-
note.
|
|
2131
|
+
note.modLfoToFilterFc.gain
|
|
2045
2132
|
.cancelScheduledValues(scheduleTime)
|
|
2046
2133
|
.setValueAtTime(modLfoToFilterFc, scheduleTime);
|
|
2047
2134
|
}
|
|
2048
2135
|
setModLfoToVolume(channel, note, scheduleTime) {
|
|
2049
2136
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
2050
2137
|
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
2051
|
-
const
|
|
2138
|
+
const depth = baseDepth * Math.sign(modLfoToVolume) *
|
|
2052
2139
|
(1 + this.getLFOAmplitudeDepth(channel, note));
|
|
2053
|
-
note.
|
|
2140
|
+
note.modLfoToVolume.gain
|
|
2054
2141
|
.cancelScheduledValues(scheduleTime)
|
|
2055
|
-
.setValueAtTime(
|
|
2142
|
+
.setValueAtTime(depth, scheduleTime);
|
|
2056
2143
|
}
|
|
2057
2144
|
setReverbSend(channel, note, scheduleTime) {
|
|
2058
2145
|
let value = note.voiceParams.reverbEffectsSend *
|
|
@@ -2065,7 +2152,7 @@ class Midy extends EventTarget {
|
|
|
2065
2152
|
if (!note.reverbSend) {
|
|
2066
2153
|
if (0 < value) {
|
|
2067
2154
|
note.reverbSend = new GainNode(this.audioContext, { gain: value });
|
|
2068
|
-
note.
|
|
2155
|
+
note.volumeNode.connect(note.reverbSend);
|
|
2069
2156
|
note.reverbSend.connect(this.reverbEffect.input);
|
|
2070
2157
|
}
|
|
2071
2158
|
}
|
|
@@ -2074,11 +2161,11 @@ class Midy extends EventTarget {
|
|
|
2074
2161
|
.cancelScheduledValues(scheduleTime)
|
|
2075
2162
|
.setValueAtTime(value, scheduleTime);
|
|
2076
2163
|
if (0 < value) {
|
|
2077
|
-
note.
|
|
2164
|
+
note.volumeNode.connect(note.reverbSend);
|
|
2078
2165
|
}
|
|
2079
2166
|
else {
|
|
2080
2167
|
try {
|
|
2081
|
-
note.
|
|
2168
|
+
note.volumeNode.disconnect(note.reverbSend);
|
|
2082
2169
|
}
|
|
2083
2170
|
catch { /* empty */ }
|
|
2084
2171
|
}
|
|
@@ -2095,7 +2182,7 @@ class Midy extends EventTarget {
|
|
|
2095
2182
|
if (!note.chorusSend) {
|
|
2096
2183
|
if (0 < value) {
|
|
2097
2184
|
note.chorusSend = new GainNode(this.audioContext, { gain: value });
|
|
2098
|
-
note.
|
|
2185
|
+
note.volumeNode.connect(note.chorusSend);
|
|
2099
2186
|
note.chorusSend.connect(this.chorusEffect.input);
|
|
2100
2187
|
}
|
|
2101
2188
|
}
|
|
@@ -2104,11 +2191,11 @@ class Midy extends EventTarget {
|
|
|
2104
2191
|
.cancelScheduledValues(scheduleTime)
|
|
2105
2192
|
.setValueAtTime(value, scheduleTime);
|
|
2106
2193
|
if (0 < value) {
|
|
2107
|
-
note.
|
|
2194
|
+
note.volumeNode.connect(note.chorusSend);
|
|
2108
2195
|
}
|
|
2109
2196
|
else {
|
|
2110
2197
|
try {
|
|
2111
|
-
note.
|
|
2198
|
+
note.volumeNode.disconnect(note.chorusSend);
|
|
2112
2199
|
}
|
|
2113
2200
|
catch { /* empty */ }
|
|
2114
2201
|
}
|
|
@@ -2117,29 +2204,29 @@ class Midy extends EventTarget {
|
|
|
2117
2204
|
setDelayModLFO(note) {
|
|
2118
2205
|
const startTime = note.startTime + note.voiceParams.delayModLFO;
|
|
2119
2206
|
try {
|
|
2120
|
-
note.
|
|
2207
|
+
note.modLfo.start(startTime);
|
|
2121
2208
|
}
|
|
2122
2209
|
catch { /* empty */ }
|
|
2123
2210
|
}
|
|
2124
2211
|
setFreqModLFO(note, scheduleTime) {
|
|
2125
2212
|
const freqModLFO = note.voiceParams.freqModLFO;
|
|
2126
|
-
note.
|
|
2213
|
+
note.modLfo.frequency
|
|
2127
2214
|
.cancelScheduledValues(scheduleTime)
|
|
2128
2215
|
.setValueAtTime(freqModLFO, scheduleTime);
|
|
2129
2216
|
}
|
|
2130
2217
|
setDelayVibLFO(channel, note) {
|
|
2131
|
-
const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
|
|
2218
|
+
const vibratoDelay = this.getRelativeKeyBasedValue(channel, note.noteNumber, 78) * 2;
|
|
2132
2219
|
const value = note.voiceParams.delayVibLFO;
|
|
2133
2220
|
const startTime = note.startTime + value * vibratoDelay;
|
|
2134
2221
|
try {
|
|
2135
|
-
note.
|
|
2222
|
+
note.vibLfo.start(startTime);
|
|
2136
2223
|
}
|
|
2137
2224
|
catch { /* empty */ }
|
|
2138
2225
|
}
|
|
2139
2226
|
setFreqVibLFO(channel, note, scheduleTime) {
|
|
2140
|
-
const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
|
|
2227
|
+
const vibratoRate = this.getRelativeKeyBasedValue(channel, note.noteNumber, 76) * 2;
|
|
2141
2228
|
const freqVibLFO = note.voiceParams.freqVibLFO;
|
|
2142
|
-
note.
|
|
2229
|
+
note.vibLfo.frequency
|
|
2143
2230
|
.cancelScheduledValues(scheduleTime)
|
|
2144
2231
|
.setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
|
|
2145
2232
|
}
|
|
@@ -2188,7 +2275,7 @@ class Midy extends EventTarget {
|
|
|
2188
2275
|
},
|
|
2189
2276
|
delayVibLFO: (channel, note, _scheduleTime) => {
|
|
2190
2277
|
if (0 < channel.state.vibratoDepth) {
|
|
2191
|
-
setDelayVibLFO(channel, note);
|
|
2278
|
+
this.setDelayVibLFO(channel, note);
|
|
2192
2279
|
}
|
|
2193
2280
|
},
|
|
2194
2281
|
freqVibLFO: (channel, note, scheduleTime) => {
|
|
@@ -2295,6 +2382,8 @@ class Midy extends EventTarget {
|
|
|
2295
2382
|
return handlers;
|
|
2296
2383
|
}
|
|
2297
2384
|
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
2385
|
+
if (!(0 <= scheduleTime))
|
|
2386
|
+
scheduleTime = this.audioContext.currentTime;
|
|
2298
2387
|
this.applyToMPEChannels(channelNumber, (ch) => {
|
|
2299
2388
|
this.applyControlChange(ch, controllerType, value, scheduleTime);
|
|
2300
2389
|
});
|
|
@@ -2305,7 +2394,9 @@ class Midy extends EventTarget {
|
|
|
2305
2394
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
2306
2395
|
const channel = this.channels[channelNumber];
|
|
2307
2396
|
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
2308
|
-
this.
|
|
2397
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2398
|
+
this.setControlChangeEffects(channel, note, scheduleTime);
|
|
2399
|
+
});
|
|
2309
2400
|
}
|
|
2310
2401
|
else {
|
|
2311
2402
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -2377,6 +2468,9 @@ class Midy extends EventTarget {
|
|
|
2377
2468
|
const intPart = Math.trunc(value);
|
|
2378
2469
|
state.volumeMSB = intPart / 127;
|
|
2379
2470
|
state.volumeLSB = value - intPart;
|
|
2471
|
+
this.applyVolume(channel, scheduleTime);
|
|
2472
|
+
}
|
|
2473
|
+
applyVolume(channel, scheduleTime) {
|
|
2380
2474
|
if (channel.isDrum) {
|
|
2381
2475
|
for (let i = 0; i < 128; i++) {
|
|
2382
2476
|
this.updateKeyBasedVolume(channel, i, scheduleTime);
|
|
@@ -2387,7 +2481,7 @@ class Midy extends EventTarget {
|
|
|
2387
2481
|
}
|
|
2388
2482
|
}
|
|
2389
2483
|
panToGain(pan) {
|
|
2390
|
-
const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
|
|
2484
|
+
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
2391
2485
|
return {
|
|
2392
2486
|
gainLeft: Math.cos(theta),
|
|
2393
2487
|
gainRight: Math.sin(theta),
|
|
@@ -2432,7 +2526,8 @@ class Midy extends EventTarget {
|
|
|
2432
2526
|
const volume = volumeMSB + volumeLSB / 128;
|
|
2433
2527
|
const expression = expressionMSB + expressionLSB / 128;
|
|
2434
2528
|
const pan = panMSB + panLSB / 128;
|
|
2435
|
-
const
|
|
2529
|
+
const effect = this.getChannelAmplitudeControl(channel);
|
|
2530
|
+
const gain = volume * expression * (1 + effect);
|
|
2436
2531
|
const { gainLeft, gainRight } = this.panToGain(pan);
|
|
2437
2532
|
channel.gainL.gain
|
|
2438
2533
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -2542,19 +2637,11 @@ class Midy extends EventTarget {
|
|
|
2542
2637
|
const state = channel.state;
|
|
2543
2638
|
state.filterResonance = ccValue / 127;
|
|
2544
2639
|
this.processScheduledNotes(channel, (note) => {
|
|
2545
|
-
const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
|
|
2640
|
+
const filterResonance = this.getRelativeKeyBasedValue(channel, note.noteNumber, 71);
|
|
2546
2641
|
const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
|
|
2547
|
-
note.
|
|
2642
|
+
note.filterEnvelopeNode.Q.setValueAtTime(Q, scheduleTime);
|
|
2548
2643
|
});
|
|
2549
2644
|
}
|
|
2550
|
-
getRelativeKeyBasedValue(channel, note, controllerType) {
|
|
2551
|
-
const ccState = channel.state.array[128 + controllerType];
|
|
2552
|
-
const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, controllerType);
|
|
2553
|
-
if (keyBasedValue < 0)
|
|
2554
|
-
return ccState;
|
|
2555
|
-
const keyValue = ccState + keyBasedValue / 127 - 0.5;
|
|
2556
|
-
return keyValue < 0 ? keyValue : 0;
|
|
2557
|
-
}
|
|
2558
2645
|
setReleaseTime(channelNumber, releaseTime, scheduleTime) {
|
|
2559
2646
|
const channel = this.channels[channelNumber];
|
|
2560
2647
|
if (channel.isDrum)
|
|
@@ -2827,6 +2914,7 @@ class Midy extends EventTarget {
|
|
|
2827
2914
|
this.updateModulation(channel, scheduleTime);
|
|
2828
2915
|
}
|
|
2829
2916
|
handleMIDIPolyphonicExpressionRPN(channelNumber, _scheduleTime) {
|
|
2917
|
+
const channel = this.channels[channelNumber];
|
|
2830
2918
|
this.setMIDIPolyphonicExpression(channelNumber, channel.dataMSB);
|
|
2831
2919
|
}
|
|
2832
2920
|
setMIDIPolyphonicExpression(channelNumber, value) {
|
|
@@ -3047,9 +3135,9 @@ class Midy extends EventTarget {
|
|
|
3047
3135
|
case 9:
|
|
3048
3136
|
switch (data[3]) {
|
|
3049
3137
|
case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
3050
|
-
return this.
|
|
3138
|
+
return this.handleChannelPressureSysEx(data, scheduelTime);
|
|
3051
3139
|
case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
3052
|
-
return this.
|
|
3140
|
+
return this.handlePolyphonicKeyPressureSysEx(data, scheduleTime);
|
|
3053
3141
|
case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
3054
3142
|
return this.handleControlChangeSysEx(data, scheduleTime);
|
|
3055
3143
|
default:
|
|
@@ -3358,98 +3446,137 @@ class Midy extends EventTarget {
|
|
|
3358
3446
|
this.updateChannelDetune(channel, scheduleTime);
|
|
3359
3447
|
}
|
|
3360
3448
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3449
|
+
calcEffectValue(channel, note, destination) {
|
|
3450
|
+
return this.calcChannelEffectValue(channel, destination) +
|
|
3451
|
+
this.calcNoteEffectValue(channel, note, destination);
|
|
3452
|
+
}
|
|
3453
|
+
calcChannelEffectValue(channel, destination) {
|
|
3454
|
+
return this.calcControlChangeEffectValue(channel, destination) +
|
|
3455
|
+
this.calcChannelPressureEffectValue(channel, destination);
|
|
3456
|
+
}
|
|
3457
|
+
calcControlChangeEffectValue(channel, destination) {
|
|
3458
|
+
const controlType = channel.controlTable[destination];
|
|
3459
|
+
if (controlType < 0)
|
|
3460
|
+
return 0;
|
|
3461
|
+
const pressure = channel.state.array[controlType];
|
|
3462
|
+
if (pressure <= 0)
|
|
3463
|
+
return 0;
|
|
3464
|
+
const baseline = pressureBaselines[destination];
|
|
3465
|
+
const tableValue = channel.controlTable[destination + 6];
|
|
3466
|
+
const value = (tableValue - baseline) * pressure;
|
|
3467
|
+
return value * effectParameters[destination];
|
|
3468
|
+
}
|
|
3469
|
+
calcChannelPressureEffectValue(channel, destination) {
|
|
3470
|
+
const pressure = channel.state.channelPressure;
|
|
3471
|
+
if (pressure <= 0)
|
|
3472
|
+
return 0;
|
|
3473
|
+
const baseline = pressureBaselines[destination];
|
|
3474
|
+
const tableValue = channel.channelPressureTable[destination];
|
|
3475
|
+
const value = (tableValue - baseline) * pressure;
|
|
3476
|
+
return value * effectParameters[destination];
|
|
3477
|
+
}
|
|
3478
|
+
calcNoteEffectValue(channel, note, destination) {
|
|
3479
|
+
const pressure = note.pressure;
|
|
3480
|
+
if (pressure <= 0)
|
|
3364
3481
|
return 0;
|
|
3365
|
-
const
|
|
3366
|
-
|
|
3367
|
-
|
|
3482
|
+
const baseline = pressureBaselines[destination];
|
|
3483
|
+
const tableValue = channel.polyphonicKeyPressureTable[destination];
|
|
3484
|
+
const value = (tableValue - baseline) * pressure / 127;
|
|
3485
|
+
return value * effectParameters[destination];
|
|
3486
|
+
}
|
|
3487
|
+
getChannelPitchControl(channel) {
|
|
3488
|
+
return this.calcChannelEffectValue(channel, 0);
|
|
3489
|
+
}
|
|
3490
|
+
getNotePitchControl(channel, note) {
|
|
3491
|
+
return this.calcNoteEffectValue(channel, note, 0);
|
|
3492
|
+
}
|
|
3493
|
+
getPitchControl(channel, note) {
|
|
3494
|
+
return this.calcEffectValue(channel, note, 0);
|
|
3368
3495
|
}
|
|
3369
3496
|
getFilterCutoffControl(channel, note) {
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
: 0;
|
|
3378
|
-
return (channelPressure + polyphonicKeyPressure) * 15;
|
|
3497
|
+
return this.calcEffectValue(channel, note, 1);
|
|
3498
|
+
}
|
|
3499
|
+
getChannelAmplitudeControl(channel) {
|
|
3500
|
+
return this.calcChannelEffectValue(channel, 2);
|
|
3501
|
+
}
|
|
3502
|
+
getNoteAmplitudeControl(channel, note) {
|
|
3503
|
+
return this.calcNoteEffectValue(channel, note, 2);
|
|
3379
3504
|
}
|
|
3380
3505
|
getAmplitudeControl(channel, note) {
|
|
3381
|
-
|
|
3382
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
3383
|
-
? channel.state.channelPressure * 127 / channelPressureRaw
|
|
3384
|
-
: 0;
|
|
3385
|
-
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
|
|
3386
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3387
|
-
? note.pressure / polyphonicKeyPressureRaw
|
|
3388
|
-
: 0;
|
|
3389
|
-
return channelPressure + polyphonicKeyPressure;
|
|
3506
|
+
return this.calcEffectValue(channel, note, 2);
|
|
3390
3507
|
}
|
|
3391
3508
|
getLFOPitchDepth(channel, note) {
|
|
3392
|
-
|
|
3393
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
3394
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
3395
|
-
: 0;
|
|
3396
|
-
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
|
|
3397
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3398
|
-
? polyphonicKeyPressureRaw * note.pressure
|
|
3399
|
-
: 0;
|
|
3400
|
-
return (channelPressure + polyphonicKeyPressure) / 254 * 600;
|
|
3509
|
+
return this.calcEffectValue(channel, note, 3);
|
|
3401
3510
|
}
|
|
3402
3511
|
getLFOFilterDepth(channel, note) {
|
|
3403
|
-
|
|
3404
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
3405
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
3406
|
-
: 0;
|
|
3407
|
-
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
|
|
3408
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3409
|
-
? polyphonicKeyPressureRaw * note.pressure
|
|
3410
|
-
: 0;
|
|
3411
|
-
return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
|
|
3512
|
+
return this.calcEffectValue(channel, note, 4);
|
|
3412
3513
|
}
|
|
3413
3514
|
getLFOAmplitudeDepth(channel, note) {
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3420
|
-
? polyphonicKeyPressureRaw * note.pressure
|
|
3421
|
-
: 0;
|
|
3422
|
-
return (channelPressure + polyphonicKeyPressure) / 254;
|
|
3423
|
-
}
|
|
3424
|
-
setEffects(channel, note, table, scheduleTime) {
|
|
3425
|
-
if (0 < table[0]) {
|
|
3515
|
+
return this.calcEffectValue(channel, note, 5);
|
|
3516
|
+
}
|
|
3517
|
+
createEffectHandlers() {
|
|
3518
|
+
const handlers = new Array(6);
|
|
3519
|
+
handlers[0] = (channel, note, _tableName, scheduleTime) => {
|
|
3426
3520
|
if (this.isPortamento(channel, note)) {
|
|
3427
3521
|
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
3428
3522
|
}
|
|
3429
3523
|
else {
|
|
3430
3524
|
this.setDetune(channel, note, scheduleTime);
|
|
3431
3525
|
}
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
|
-
if (0
|
|
3526
|
+
};
|
|
3527
|
+
handlers[1] = (channel, note, _tableName, scheduleTime) => {
|
|
3528
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
3435
3529
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
3436
3530
|
}
|
|
3437
|
-
|
|
3438
|
-
this.
|
|
3531
|
+
else {
|
|
3532
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
handlers[2] = (channel, note, tableName, scheduleTime) => {
|
|
3536
|
+
if (tableName === "polyphonicKeyPressureTable") {
|
|
3537
|
+
this.setVolumeNode(channel, note, scheduleTime);
|
|
3439
3538
|
}
|
|
3539
|
+
else {
|
|
3540
|
+
this.applyVolume(channel, scheduleTime);
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
handlers[3] = (channel, note, _tableName, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3544
|
+
handlers[4] = (channel, note, _tableName, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3545
|
+
handlers[5] = (channel, note, _tableName, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3546
|
+
return handlers;
|
|
3547
|
+
}
|
|
3548
|
+
setControlChangeEffects(channel, note, scheduleTime) {
|
|
3549
|
+
const handlers = this.effectHandlers;
|
|
3550
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3551
|
+
const baseline = pressureBaselines[i];
|
|
3552
|
+
const tableValue = channel.controlTable[i + 6];
|
|
3553
|
+
if (baseline === tableValue)
|
|
3554
|
+
continue;
|
|
3555
|
+
handlers[i](channel, note, "controlTable", scheduleTime);
|
|
3440
3556
|
}
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3557
|
+
}
|
|
3558
|
+
setChannelPressureEffects(channel, note, scheduleTime) {
|
|
3559
|
+
this.setPressureEffects(channel, note, "channelPressureTable", scheduleTime);
|
|
3560
|
+
}
|
|
3561
|
+
setPolyphonicKeyPressureEffects(channel, note, scheduleTime) {
|
|
3562
|
+
this.setPressureEffects(channel, note, "polyphonicKeyPressureTable", scheduleTime);
|
|
3563
|
+
}
|
|
3564
|
+
setPressureEffects(channel, note, tableName, scheduleTime) {
|
|
3565
|
+
const handlers = this.effectHandlers;
|
|
3566
|
+
const table = channel[tableName];
|
|
3567
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3568
|
+
const baseline = pressureBaselines[i];
|
|
3569
|
+
const tableValue = table[i];
|
|
3570
|
+
if (baseline === tableValue)
|
|
3571
|
+
continue;
|
|
3572
|
+
handlers[i](channel, note, tableName, scheduleTime);
|
|
3446
3573
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3574
|
+
}
|
|
3575
|
+
handleChannelPressureSysEx(data, scheduleTime) {
|
|
3576
|
+
this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
|
|
3577
|
+
}
|
|
3578
|
+
handlePolyphonicKeyPressureSysEx(data, scheduleTime) {
|
|
3579
|
+
this.handlePressureSysEx(data, "polyphonicKeyPressureTable", scheduleTime);
|
|
3453
3580
|
}
|
|
3454
3581
|
handlePressureSysEx(data, tableName, scheduleTime) {
|
|
3455
3582
|
const channelNumber = data[4];
|
|
@@ -3461,39 +3588,41 @@ class Midy extends EventTarget {
|
|
|
3461
3588
|
const pp = data[i];
|
|
3462
3589
|
const rr = data[i + 1];
|
|
3463
3590
|
table[pp] = rr;
|
|
3591
|
+
const handler = this.effectHandlers[pp];
|
|
3592
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3593
|
+
if (handler)
|
|
3594
|
+
handler(channel, note, tableName, scheduleTime);
|
|
3595
|
+
});
|
|
3464
3596
|
}
|
|
3465
|
-
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3466
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
3467
|
-
});
|
|
3468
|
-
}
|
|
3469
|
-
initControlTable() {
|
|
3470
|
-
const ccCount = 128;
|
|
3471
|
-
const slotSize = 6;
|
|
3472
|
-
return new Int8Array(ccCount * slotSize).fill(-1);
|
|
3473
|
-
}
|
|
3474
|
-
setControlChangeEffects(channel, controllerType, scheduleTime) {
|
|
3475
|
-
const slotSize = 6;
|
|
3476
|
-
const offset = controllerType * slotSize;
|
|
3477
|
-
const table = channel.controlTable.subarray(offset, offset + slotSize);
|
|
3478
|
-
this.processScheduledNotes(channel, (note) => {
|
|
3479
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
3480
|
-
});
|
|
3481
3597
|
}
|
|
3482
3598
|
handleControlChangeSysEx(data, scheduleTime) {
|
|
3483
3599
|
const channelNumber = data[4];
|
|
3484
3600
|
const channel = this.channels[channelNumber];
|
|
3485
3601
|
if (channel.isDrum)
|
|
3486
3602
|
return;
|
|
3487
|
-
const slotSize = 6;
|
|
3488
|
-
const controllerType = data[5];
|
|
3489
|
-
const offset = controllerType * slotSize;
|
|
3490
3603
|
const table = channel.controlTable;
|
|
3604
|
+
table.set(defaultControlValues);
|
|
3605
|
+
const controllerType = data[5];
|
|
3491
3606
|
for (let i = 6; i < data.length; i += 2) {
|
|
3492
3607
|
const pp = data[i];
|
|
3493
3608
|
const rr = data[i + 1];
|
|
3494
|
-
table[
|
|
3609
|
+
table[pp] = controllerType;
|
|
3610
|
+
table[pp + 6] = rr;
|
|
3611
|
+
const handler = this.effectHandlers[pp];
|
|
3612
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3613
|
+
if (handler)
|
|
3614
|
+
handler(channel, note, "controlTable", scheduleTime);
|
|
3615
|
+
});
|
|
3495
3616
|
}
|
|
3496
|
-
|
|
3617
|
+
}
|
|
3618
|
+
getRelativeKeyBasedValue(channel, keyNumber, controllerType) {
|
|
3619
|
+
const ccState = channel.state.array[128 + controllerType];
|
|
3620
|
+
if (!channel.isDrum)
|
|
3621
|
+
return ccState;
|
|
3622
|
+
const keyBasedValue = this.getKeyBasedValue(channel, keyNumber, controllerType);
|
|
3623
|
+
if (keyBasedValue < 0)
|
|
3624
|
+
return ccState;
|
|
3625
|
+
return ccState * keyBasedValue / 64;
|
|
3497
3626
|
}
|
|
3498
3627
|
getKeyBasedValue(channel, keyNumber, controllerType) {
|
|
3499
3628
|
const index = keyNumber * 128 + controllerType;
|
|
@@ -3506,9 +3635,9 @@ class Midy extends EventTarget {
|
|
|
3506
3635
|
handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
3507
3636
|
handlers[71] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|
|
3508
3637
|
if (note.noteNumber === keyNumber) {
|
|
3509
|
-
const filterResonance = this.getRelativeKeyBasedValue(channel,
|
|
3638
|
+
const filterResonance = this.getRelativeKeyBasedValue(channel, keyNumber, 71);
|
|
3510
3639
|
const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
|
|
3511
|
-
note.
|
|
3640
|
+
note.filterEnvelopeNode.Q.setValueAtTime(Q, scheduleTime);
|
|
3512
3641
|
}
|
|
3513
3642
|
});
|
|
3514
3643
|
handlers[73] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|