@marmooo/midy 0.4.6 → 0.4.7
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 +13 -4
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +251 -157
- 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 +29 -11
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +328 -221
- 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 +13 -4
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +251 -157
- 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 +29 -11
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +328 -221
package/esm/midy.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { parseMidi } from "midi-file";
|
|
2
2
|
import { parse, SoundFont } from "@marmooo/soundfont-parser";
|
|
3
|
+
import { OggVorbisDecoderWebWorker } from "@wasm-audio-decoders/ogg-vorbis";
|
|
4
|
+
let decoderPromise = null;
|
|
5
|
+
let decoderQueue = Promise.resolve();
|
|
6
|
+
function initDecoder() {
|
|
7
|
+
if (!decoderPromise) {
|
|
8
|
+
const instance = new OggVorbisDecoderWebWorker();
|
|
9
|
+
decoderPromise = instance.ready.then(() => instance);
|
|
10
|
+
}
|
|
11
|
+
return decoderPromise;
|
|
12
|
+
}
|
|
3
13
|
class Note {
|
|
4
14
|
constructor(noteNumber, velocity, startTime) {
|
|
5
15
|
Object.defineProperty(this, "voice", {
|
|
@@ -38,49 +48,55 @@ class Note {
|
|
|
38
48
|
writable: true,
|
|
39
49
|
value: void 0
|
|
40
50
|
});
|
|
41
|
-
Object.defineProperty(this, "
|
|
51
|
+
Object.defineProperty(this, "filterEnvelopeNode", {
|
|
42
52
|
enumerable: true,
|
|
43
53
|
configurable: true,
|
|
44
54
|
writable: true,
|
|
45
55
|
value: void 0
|
|
46
56
|
});
|
|
47
|
-
Object.defineProperty(this, "
|
|
57
|
+
Object.defineProperty(this, "volumeEnvelopeNode", {
|
|
48
58
|
enumerable: true,
|
|
49
59
|
configurable: true,
|
|
50
60
|
writable: true,
|
|
51
61
|
value: void 0
|
|
52
62
|
});
|
|
53
|
-
Object.defineProperty(this, "
|
|
63
|
+
Object.defineProperty(this, "volumeNode", {
|
|
54
64
|
enumerable: true,
|
|
55
65
|
configurable: true,
|
|
56
66
|
writable: true,
|
|
57
67
|
value: void 0
|
|
58
|
-
});
|
|
59
|
-
Object.defineProperty(this, "
|
|
68
|
+
}); // polyphonic key pressure
|
|
69
|
+
Object.defineProperty(this, "modLfo", {
|
|
60
70
|
enumerable: true,
|
|
61
71
|
configurable: true,
|
|
62
72
|
writable: true,
|
|
63
73
|
value: void 0
|
|
64
|
-
});
|
|
65
|
-
Object.defineProperty(this, "
|
|
74
|
+
}); // CC#1 modulation LFO
|
|
75
|
+
Object.defineProperty(this, "modLfoToPitch", {
|
|
66
76
|
enumerable: true,
|
|
67
77
|
configurable: true,
|
|
68
78
|
writable: true,
|
|
69
79
|
value: void 0
|
|
70
80
|
});
|
|
71
|
-
Object.defineProperty(this, "
|
|
81
|
+
Object.defineProperty(this, "modLfoToFilterFc", {
|
|
72
82
|
enumerable: true,
|
|
73
83
|
configurable: true,
|
|
74
84
|
writable: true,
|
|
75
85
|
value: void 0
|
|
76
86
|
});
|
|
77
|
-
Object.defineProperty(this, "
|
|
87
|
+
Object.defineProperty(this, "modLfoToVolume", {
|
|
78
88
|
enumerable: true,
|
|
79
89
|
configurable: true,
|
|
80
90
|
writable: true,
|
|
81
91
|
value: void 0
|
|
82
92
|
});
|
|
83
|
-
Object.defineProperty(this, "
|
|
93
|
+
Object.defineProperty(this, "vibLfo", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: void 0
|
|
98
|
+
}); // vibrato LFO
|
|
99
|
+
Object.defineProperty(this, "vibLfoToPitch", {
|
|
84
100
|
enumerable: true,
|
|
85
101
|
configurable: true,
|
|
86
102
|
writable: true,
|
|
@@ -253,7 +269,20 @@ const pitchEnvelopeKeys = [
|
|
|
253
269
|
"playbackRate",
|
|
254
270
|
];
|
|
255
271
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
272
|
+
const effectParameters = [
|
|
273
|
+
2400 / 64, // cent
|
|
274
|
+
9600 / 64, // cent
|
|
275
|
+
1 / 64,
|
|
276
|
+
600 / 127, // cent
|
|
277
|
+
2400 / 127, // cent
|
|
278
|
+
1 / 127,
|
|
279
|
+
];
|
|
280
|
+
const pressureBaselines = new Int8Array([64, 64, 0, 0, 0, 0]);
|
|
256
281
|
const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
|
|
282
|
+
const defaultControlValues = new Int8Array([
|
|
283
|
+
...[-1, -1, -1, -1, -1, -1],
|
|
284
|
+
...defaultPressureValues,
|
|
285
|
+
]);
|
|
257
286
|
function cbToRatio(cb) {
|
|
258
287
|
return Math.pow(10, cb / 200);
|
|
259
288
|
}
|
|
@@ -402,6 +431,12 @@ export class Midy extends EventTarget {
|
|
|
402
431
|
writable: true,
|
|
403
432
|
value: new Map()
|
|
404
433
|
});
|
|
434
|
+
Object.defineProperty(this, "decodeMethod", {
|
|
435
|
+
enumerable: true,
|
|
436
|
+
configurable: true,
|
|
437
|
+
writable: true,
|
|
438
|
+
value: "wasm-audio-decoders"
|
|
439
|
+
});
|
|
405
440
|
Object.defineProperty(this, "isPlaying", {
|
|
406
441
|
enumerable: true,
|
|
407
442
|
configurable: true,
|
|
@@ -521,6 +556,9 @@ export class Midy extends EventTarget {
|
|
|
521
556
|
noteToChannel: new Map(),
|
|
522
557
|
}
|
|
523
558
|
});
|
|
559
|
+
this.decoder = new OggVorbisDecoderWebWorker();
|
|
560
|
+
this.decoderReady = this.decoder.ready;
|
|
561
|
+
this.decoderQueue = Promise.resolve();
|
|
524
562
|
this.audioContext = audioContext;
|
|
525
563
|
this.masterVolume = new GainNode(audioContext);
|
|
526
564
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
@@ -532,6 +570,7 @@ export class Midy extends EventTarget {
|
|
|
532
570
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
533
571
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
534
572
|
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
573
|
+
this.effectHandlers = this.createEffectHandlers();
|
|
535
574
|
this.channels = this.createChannels(audioContext);
|
|
536
575
|
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
537
576
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
@@ -659,7 +698,7 @@ export class Midy extends EventTarget {
|
|
|
659
698
|
};
|
|
660
699
|
}
|
|
661
700
|
resetChannelTable(channel) {
|
|
662
|
-
channel.controlTable.
|
|
701
|
+
channel.controlTable.set(defaultControlValues);
|
|
663
702
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
664
703
|
channel.channelPressureTable.set(defaultPressureValues);
|
|
665
704
|
channel.polyphonicKeyPressureTable.set(defaultPressureValues);
|
|
@@ -676,7 +715,7 @@ export class Midy extends EventTarget {
|
|
|
676
715
|
scheduledNotes: [],
|
|
677
716
|
sustainNotes: [],
|
|
678
717
|
sostenutoNotes: [],
|
|
679
|
-
controlTable:
|
|
718
|
+
controlTable: new Int8Array(defaultControlValues),
|
|
680
719
|
scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
|
|
681
720
|
channelPressureTable: new Int8Array(defaultPressureValues),
|
|
682
721
|
polyphonicKeyPressureTable: new Int8Array(defaultPressureValues),
|
|
@@ -687,11 +726,57 @@ export class Midy extends EventTarget {
|
|
|
687
726
|
});
|
|
688
727
|
return channels;
|
|
689
728
|
}
|
|
729
|
+
decodeOggVorbis(sample) {
|
|
730
|
+
const task = decoderQueue.then(async () => {
|
|
731
|
+
const decoder = await initDecoder();
|
|
732
|
+
const slice = sample.data.slice();
|
|
733
|
+
const { channelData, sampleRate, errors } = await decoder.decodeFile(slice);
|
|
734
|
+
if (0 < errors.length) {
|
|
735
|
+
throw new Error(errors.join(", "));
|
|
736
|
+
}
|
|
737
|
+
const audioBuffer = new AudioBuffer({
|
|
738
|
+
numberOfChannels: channelData.length,
|
|
739
|
+
length: channelData[0].length,
|
|
740
|
+
sampleRate,
|
|
741
|
+
});
|
|
742
|
+
for (let ch = 0; ch < channelData.length; ch++) {
|
|
743
|
+
audioBuffer.getChannelData(ch).set(channelData[ch]);
|
|
744
|
+
}
|
|
745
|
+
return audioBuffer;
|
|
746
|
+
});
|
|
747
|
+
decoderQueue = task.catch(() => { });
|
|
748
|
+
return task;
|
|
749
|
+
}
|
|
690
750
|
async createAudioBuffer(voiceParams) {
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
751
|
+
const sample = voiceParams.sample;
|
|
752
|
+
if (sample.type === "compressed") {
|
|
753
|
+
switch (this.decodeMethod) {
|
|
754
|
+
case "decodeAudioData": {
|
|
755
|
+
// https://jakearchibald.com/2016/sounds-fun/
|
|
756
|
+
// https://github.com/WebAudio/web-audio-api/issues/1091
|
|
757
|
+
// decodeAudioData() has priming issues on Safari
|
|
758
|
+
const arrayBuffer = sample.data.slice().buffer;
|
|
759
|
+
return await this.audioContext.decodeAudioData(arrayBuffer);
|
|
760
|
+
}
|
|
761
|
+
case "wasm-audio-decoders":
|
|
762
|
+
return await this.decodeOggVorbis(sample);
|
|
763
|
+
default:
|
|
764
|
+
throw new Error(`Unknown decodeMethod: ${this.decodeMethod}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
const data = sample.data;
|
|
769
|
+
const end = data.length + voiceParams.end;
|
|
770
|
+
const subarray = data.subarray(voiceParams.start, end);
|
|
771
|
+
const pcm = sample.decodePCM(subarray);
|
|
772
|
+
const audioBuffer = new AudioBuffer({
|
|
773
|
+
numberOfChannels: 1,
|
|
774
|
+
length: pcm.length,
|
|
775
|
+
sampleRate: sample.sampleHeader.sampleRate,
|
|
776
|
+
});
|
|
777
|
+
audioBuffer.getChannelData(0).set(pcm);
|
|
778
|
+
return audioBuffer;
|
|
779
|
+
}
|
|
695
780
|
}
|
|
696
781
|
isLoopDrum(channel, noteNumber) {
|
|
697
782
|
const programNumber = channel.programNumber;
|
|
@@ -1005,8 +1090,8 @@ export class Midy extends EventTarget {
|
|
|
1005
1090
|
}
|
|
1006
1091
|
stopNotes(velocity, force, scheduleTime) {
|
|
1007
1092
|
const channels = this.channels;
|
|
1008
|
-
for (let
|
|
1009
|
-
this.stopChannelNotes(
|
|
1093
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
1094
|
+
this.stopChannelNotes(ch, velocity, force, scheduleTime);
|
|
1010
1095
|
}
|
|
1011
1096
|
const stopPromise = Promise.all(this.notePromises);
|
|
1012
1097
|
this.notePromises = [];
|
|
@@ -1281,16 +1366,8 @@ export class Midy extends EventTarget {
|
|
|
1281
1366
|
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
1282
1367
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
1283
1368
|
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1287
|
-
const channelPressure = channelPressureDepth *
|
|
1288
|
-
channel.state.channelPressure;
|
|
1289
|
-
return tuning + pitch + channelPressure;
|
|
1290
|
-
}
|
|
1291
|
-
else {
|
|
1292
|
-
return tuning + pitch;
|
|
1293
|
-
}
|
|
1369
|
+
const effect = this.getChannelPitchControl(channel);
|
|
1370
|
+
return tuning + pitch + effect;
|
|
1294
1371
|
}
|
|
1295
1372
|
updateChannelDetune(channel, scheduleTime) {
|
|
1296
1373
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1308,7 +1385,7 @@ export class Midy extends EventTarget {
|
|
|
1308
1385
|
calcNoteDetune(channel, note) {
|
|
1309
1386
|
const noteDetune = note.voiceParams.detune +
|
|
1310
1387
|
this.calcScaleOctaveTuning(channel, note);
|
|
1311
|
-
const pitchControl = this.
|
|
1388
|
+
const pitchControl = this.getNotePitchControl(channel, note);
|
|
1312
1389
|
return channel.detune + noteDetune + pitchControl;
|
|
1313
1390
|
}
|
|
1314
1391
|
getPortamentoTime(channel, note) {
|
|
@@ -1377,7 +1454,7 @@ export class Midy extends EventTarget {
|
|
|
1377
1454
|
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1378
1455
|
const { voiceParams, startTime } = note;
|
|
1379
1456
|
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1380
|
-
(1 + this.
|
|
1457
|
+
(1 + this.getChannelAmplitudeControl(channel));
|
|
1381
1458
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1382
1459
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1383
1460
|
note.volumeEnvelopeNode.gain
|
|
@@ -1385,15 +1462,17 @@ export class Midy extends EventTarget {
|
|
|
1385
1462
|
.exponentialRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1386
1463
|
}
|
|
1387
1464
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1388
|
-
const { voiceParams, startTime } = note;
|
|
1465
|
+
const { voiceParams, startTime, noteNumber } = note;
|
|
1389
1466
|
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1390
|
-
(1 + this.
|
|
1467
|
+
(1 + this.getChannelAmplitudeControl(channel));
|
|
1391
1468
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1392
1469
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1393
|
-
const attackTime = this.getRelativeKeyBasedValue(channel,
|
|
1470
|
+
const attackTime = this.getRelativeKeyBasedValue(channel, noteNumber, 73) *
|
|
1471
|
+
2;
|
|
1394
1472
|
const volAttack = volDelay + voiceParams.volAttack * attackTime;
|
|
1395
1473
|
const volHold = volAttack + voiceParams.volHold;
|
|
1396
|
-
const decayTime = this.getRelativeKeyBasedValue(channel,
|
|
1474
|
+
const decayTime = this.getRelativeKeyBasedValue(channel, noteNumber, 75) *
|
|
1475
|
+
2;
|
|
1397
1476
|
const decayDuration = voiceParams.volDecay * decayTime;
|
|
1398
1477
|
note.volumeEnvelopeNode.gain
|
|
1399
1478
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1403,6 +1482,12 @@ export class Midy extends EventTarget {
|
|
|
1403
1482
|
.setValueAtTime(attackVolume, volHold)
|
|
1404
1483
|
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
1405
1484
|
}
|
|
1485
|
+
setVolumeNode(channel, note, scheduleTime) {
|
|
1486
|
+
const depth = 1 + this.getNoteAmplitudeControl(channel, note);
|
|
1487
|
+
note.volumeNode.gain
|
|
1488
|
+
.cancelScheduledValues(scheduleTime)
|
|
1489
|
+
.setValueAtTime(depth, scheduleTime);
|
|
1490
|
+
}
|
|
1406
1491
|
setPortamentoDetune(channel, note, scheduleTime) {
|
|
1407
1492
|
if (channel.portamentoControl) {
|
|
1408
1493
|
const state = channel.state;
|
|
@@ -1464,9 +1549,10 @@ export class Midy extends EventTarget {
|
|
|
1464
1549
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1465
1550
|
}
|
|
1466
1551
|
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1467
|
-
const { voiceParams, startTime } = note;
|
|
1552
|
+
const { voiceParams, startTime, noteNumber } = note;
|
|
1468
1553
|
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1469
|
-
const brightness = this.getRelativeKeyBasedValue(channel,
|
|
1554
|
+
const brightness = this.getRelativeKeyBasedValue(channel, noteNumber, 74) *
|
|
1555
|
+
2;
|
|
1470
1556
|
const scale = softPedalFactor * brightness;
|
|
1471
1557
|
const baseCent = voiceParams.initialFilterFc +
|
|
1472
1558
|
this.getFilterCutoffControl(channel, note);
|
|
@@ -1479,14 +1565,14 @@ export class Midy extends EventTarget {
|
|
|
1479
1565
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1480
1566
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1481
1567
|
note.adjustedBaseFreq = adjustedSustainFreq;
|
|
1482
|
-
note.
|
|
1568
|
+
note.filterEnvelopeNode.frequency
|
|
1483
1569
|
.cancelScheduledValues(scheduleTime)
|
|
1484
1570
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1485
1571
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
1486
1572
|
.exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
|
|
1487
1573
|
}
|
|
1488
1574
|
setFilterEnvelope(channel, note, scheduleTime) {
|
|
1489
|
-
const { voiceParams, startTime } = note;
|
|
1575
|
+
const { voiceParams, startTime, noteNumber } = note;
|
|
1490
1576
|
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
1491
1577
|
const baseCent = voiceParams.initialFilterFc +
|
|
1492
1578
|
this.getFilterCutoffControl(channel, note);
|
|
@@ -1494,7 +1580,8 @@ export class Midy extends EventTarget {
|
|
|
1494
1580
|
const sustainCent = baseCent +
|
|
1495
1581
|
modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
1496
1582
|
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1497
|
-
const brightness = this.getRelativeKeyBasedValue(channel,
|
|
1583
|
+
const brightness = this.getRelativeKeyBasedValue(channel, noteNumber, 74) *
|
|
1584
|
+
2;
|
|
1498
1585
|
const scale = softPedalFactor * brightness;
|
|
1499
1586
|
const baseFreq = this.centToHz(baseCent) * scale;
|
|
1500
1587
|
const peekFreq = this.centToHz(peekCent) * scale;
|
|
@@ -1505,9 +1592,9 @@ export class Midy extends EventTarget {
|
|
|
1505
1592
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1506
1593
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
1507
1594
|
const modHold = modAttack + voiceParams.modHold;
|
|
1508
|
-
const decayDuration =
|
|
1595
|
+
const decayDuration = voiceParams.modDecay;
|
|
1509
1596
|
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
1510
|
-
note.
|
|
1597
|
+
note.filterEnvelopeNode.frequency
|
|
1511
1598
|
.cancelScheduledValues(scheduleTime)
|
|
1512
1599
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1513
1600
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
@@ -1518,36 +1605,37 @@ export class Midy extends EventTarget {
|
|
|
1518
1605
|
startModulation(channel, note, scheduleTime) {
|
|
1519
1606
|
const audioContext = this.audioContext;
|
|
1520
1607
|
const { voiceParams } = note;
|
|
1521
|
-
note.
|
|
1608
|
+
note.modLfo = new OscillatorNode(audioContext, {
|
|
1522
1609
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
1523
1610
|
});
|
|
1524
|
-
note.
|
|
1611
|
+
note.modLfoToFilterFc = new GainNode(audioContext, {
|
|
1525
1612
|
gain: voiceParams.modLfoToFilterFc,
|
|
1526
1613
|
});
|
|
1527
|
-
note.
|
|
1614
|
+
note.modLfoToPitch = new GainNode(audioContext);
|
|
1528
1615
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1529
|
-
note.
|
|
1616
|
+
note.modLfoToVolume = new GainNode(audioContext);
|
|
1530
1617
|
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
1531
|
-
note.
|
|
1532
|
-
note.
|
|
1533
|
-
note.
|
|
1534
|
-
note.
|
|
1535
|
-
note.
|
|
1536
|
-
note.
|
|
1537
|
-
note.
|
|
1618
|
+
note.modLfo.start(note.startTime + voiceParams.delayModLFO);
|
|
1619
|
+
note.modLfo.connect(note.modLfoToFilterFc);
|
|
1620
|
+
note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
|
|
1621
|
+
note.modLfo.connect(note.modLfoToPitch);
|
|
1622
|
+
note.modLfoToPitch.connect(note.bufferSource.detune);
|
|
1623
|
+
note.modLfo.connect(note.modLfoToVolume);
|
|
1624
|
+
note.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
|
|
1538
1625
|
}
|
|
1539
1626
|
startVibrato(channel, note, scheduleTime) {
|
|
1540
|
-
const { voiceParams } = note;
|
|
1541
|
-
const vibratoRate = this.getRelativeKeyBasedValue(channel,
|
|
1542
|
-
|
|
1543
|
-
|
|
1627
|
+
const { voiceParams, noteNumber } = note;
|
|
1628
|
+
const vibratoRate = this.getRelativeKeyBasedValue(channel, noteNumber, 76) *
|
|
1629
|
+
2;
|
|
1630
|
+
const vibratoDelay = this.getRelativeKeyBasedValue(channel, noteNumber, 78) * 2;
|
|
1631
|
+
note.vibLfo = new OscillatorNode(this.audioContext, {
|
|
1544
1632
|
frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
|
|
1545
1633
|
});
|
|
1546
|
-
note.
|
|
1547
|
-
note.
|
|
1634
|
+
note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
|
|
1635
|
+
note.vibLfoToPitch = new GainNode(this.audioContext);
|
|
1548
1636
|
this.setVibLfoToPitch(channel, note, scheduleTime);
|
|
1549
|
-
note.
|
|
1550
|
-
note.
|
|
1637
|
+
note.vibLfo.connect(note.vibLfoToPitch);
|
|
1638
|
+
note.vibLfoToPitch.connect(note.bufferSource.detune);
|
|
1551
1639
|
}
|
|
1552
1640
|
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
1553
1641
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
@@ -1588,8 +1676,9 @@ export class Midy extends EventTarget {
|
|
|
1588
1676
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1589
1677
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1590
1678
|
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1591
|
-
|
|
1592
|
-
|
|
1679
|
+
note.volumeNode = new GainNode(audioContext);
|
|
1680
|
+
const filterResonance = this.getRelativeKeyBasedValue(channel, noteNumber, 71);
|
|
1681
|
+
note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
|
|
1593
1682
|
type: "lowpass",
|
|
1594
1683
|
Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
|
|
1595
1684
|
});
|
|
@@ -1597,6 +1686,7 @@ export class Midy extends EventTarget {
|
|
|
1597
1686
|
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1598
1687
|
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1599
1688
|
}
|
|
1689
|
+
this.setVolumeNode(channel, note, now);
|
|
1600
1690
|
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
1601
1691
|
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1602
1692
|
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
@@ -1619,8 +1709,9 @@ export class Midy extends EventTarget {
|
|
|
1619
1709
|
channel.currentBufferSource.stop(startTime);
|
|
1620
1710
|
channel.currentBufferSource = note.bufferSource;
|
|
1621
1711
|
}
|
|
1622
|
-
note.bufferSource.connect(note.
|
|
1623
|
-
note.
|
|
1712
|
+
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
1713
|
+
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
1714
|
+
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
1624
1715
|
this.setChorusSend(channel, note, now);
|
|
1625
1716
|
this.setReverbSend(channel, note, now);
|
|
1626
1717
|
if (voiceParams.sample.type === "compressed") {
|
|
@@ -1667,7 +1758,7 @@ export class Midy extends EventTarget {
|
|
|
1667
1758
|
}
|
|
1668
1759
|
setNoteRouting(channelNumber, note, startTime) {
|
|
1669
1760
|
const channel = this.channels[channelNumber];
|
|
1670
|
-
const { noteNumber,
|
|
1761
|
+
const { noteNumber, volumeNode } = note;
|
|
1671
1762
|
if (channel.isDrum) {
|
|
1672
1763
|
const { keyBasedGainLs, keyBasedGainRs } = channel;
|
|
1673
1764
|
let gainL = keyBasedGainLs[noteNumber];
|
|
@@ -1677,12 +1768,12 @@ export class Midy extends EventTarget {
|
|
|
1677
1768
|
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
1678
1769
|
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
1679
1770
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1771
|
+
volumeNode.connect(gainL);
|
|
1772
|
+
volumeNode.connect(gainR);
|
|
1682
1773
|
}
|
|
1683
1774
|
else {
|
|
1684
|
-
|
|
1685
|
-
|
|
1775
|
+
volumeNode.connect(channel.gainL);
|
|
1776
|
+
volumeNode.connect(channel.gainR);
|
|
1686
1777
|
}
|
|
1687
1778
|
if (0.5 <= channel.state.sustainPedal) {
|
|
1688
1779
|
channel.sustainNotes.push(note);
|
|
@@ -1714,6 +1805,8 @@ export class Midy extends EventTarget {
|
|
|
1714
1805
|
scheduledNotes.push(note);
|
|
1715
1806
|
const programNumber = channel.programNumber;
|
|
1716
1807
|
const bankTable = this.soundFontTable[programNumber];
|
|
1808
|
+
if (!bankTable)
|
|
1809
|
+
return;
|
|
1717
1810
|
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
1718
1811
|
if (bankTable[bank] === undefined) {
|
|
1719
1812
|
if (channel.isDrum)
|
|
@@ -1734,16 +1827,17 @@ export class Midy extends EventTarget {
|
|
|
1734
1827
|
}
|
|
1735
1828
|
disconnectNote(note) {
|
|
1736
1829
|
note.bufferSource.disconnect();
|
|
1737
|
-
note.
|
|
1830
|
+
note.filterEnvelopeNode.disconnect();
|
|
1738
1831
|
note.volumeEnvelopeNode.disconnect();
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
note.
|
|
1742
|
-
note.
|
|
1832
|
+
note.volumeNode.disconnect();
|
|
1833
|
+
if (note.modLfoToPitch) {
|
|
1834
|
+
note.modLfoToVolume.disconnect();
|
|
1835
|
+
note.modLfoToPitch.disconnect();
|
|
1836
|
+
note.modLfo.stop();
|
|
1743
1837
|
}
|
|
1744
|
-
if (note.
|
|
1745
|
-
note.
|
|
1746
|
-
note.
|
|
1838
|
+
if (note.vibLfoToPitch) {
|
|
1839
|
+
note.vibLfoToPitch.disconnect();
|
|
1840
|
+
note.vibLfo.stop();
|
|
1747
1841
|
}
|
|
1748
1842
|
if (note.reverbSend) {
|
|
1749
1843
|
note.reverbSend.disconnect();
|
|
@@ -1754,10 +1848,10 @@ export class Midy extends EventTarget {
|
|
|
1754
1848
|
}
|
|
1755
1849
|
releaseNote(channel, note, endTime) {
|
|
1756
1850
|
endTime ??= this.audioContext.currentTime;
|
|
1757
|
-
const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
|
|
1851
|
+
const releaseTime = this.getRelativeKeyBasedValue(channel, note.noteNumber, 72) * 2;
|
|
1758
1852
|
const volDuration = note.voiceParams.volRelease * releaseTime;
|
|
1759
1853
|
const volRelease = endTime + volDuration;
|
|
1760
|
-
note.
|
|
1854
|
+
note.filterEnvelopeNode.frequency
|
|
1761
1855
|
.cancelScheduledValues(endTime)
|
|
1762
1856
|
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1763
1857
|
note.volumeEnvelopeNode.gain
|
|
@@ -1927,11 +2021,10 @@ export class Midy extends EventTarget {
|
|
|
1927
2021
|
return;
|
|
1928
2022
|
if (!(0 <= scheduleTime))
|
|
1929
2023
|
scheduleTime = this.audioContext.currentTime;
|
|
1930
|
-
const table = channel.polyphonicKeyPressureTable;
|
|
1931
2024
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1932
2025
|
if (note.noteNumber === noteNumber) {
|
|
1933
2026
|
note.pressure = pressure;
|
|
1934
|
-
this.
|
|
2027
|
+
this.setPressureEffects(channel, note, scheduleTime);
|
|
1935
2028
|
}
|
|
1936
2029
|
});
|
|
1937
2030
|
this.applyVoiceParams(channel, 10, scheduleTime);
|
|
@@ -1957,27 +2050,22 @@ export class Midy extends EventTarget {
|
|
|
1957
2050
|
}
|
|
1958
2051
|
}
|
|
1959
2052
|
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
2053
|
+
if (!(0 <= scheduleTime))
|
|
2054
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1960
2055
|
this.applyToMPEChannels(channelNumber, (ch) => {
|
|
1961
2056
|
this.applyChannelPressure(ch, value, scheduleTime);
|
|
1962
2057
|
});
|
|
1963
2058
|
}
|
|
1964
2059
|
applyChannelPressure(channelNumber, value, scheduleTime) {
|
|
1965
|
-
if (!(0 <= scheduleTime))
|
|
1966
|
-
scheduleTime = this.audioContext.currentTime;
|
|
1967
2060
|
const channel = this.channels[channelNumber];
|
|
1968
2061
|
if (channel.isDrum)
|
|
1969
2062
|
return;
|
|
1970
|
-
const prev = channel
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
if (0 <= channelPressureRaw) {
|
|
1975
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1976
|
-
channel.detune += channelPressureDepth * (next - prev);
|
|
1977
|
-
}
|
|
1978
|
-
const table = channel.channelPressureTable;
|
|
2063
|
+
const prev = this.calcChannelPressureEffectValue(channel, 0);
|
|
2064
|
+
channel.state.channelPressure = value / 127;
|
|
2065
|
+
const next = this.calcChannelPressureEffectValue(channel, 0);
|
|
2066
|
+
channel.detune += next - prev;
|
|
1979
2067
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1980
|
-
this.
|
|
2068
|
+
this.setPressureEffects(channel, note, scheduleTime);
|
|
1981
2069
|
});
|
|
1982
2070
|
this.applyVoiceParams(channel, 13, scheduleTime);
|
|
1983
2071
|
}
|
|
@@ -2012,7 +2100,7 @@ export class Midy extends EventTarget {
|
|
|
2012
2100
|
this.getLFOPitchDepth(channel, note);
|
|
2013
2101
|
const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
|
|
2014
2102
|
const depth = baseDepth * Math.sign(modLfoToPitch);
|
|
2015
|
-
note.
|
|
2103
|
+
note.modLfoToPitch.gain
|
|
2016
2104
|
.cancelScheduledValues(scheduleTime)
|
|
2017
2105
|
.setValueAtTime(depth, scheduleTime);
|
|
2018
2106
|
}
|
|
@@ -2021,13 +2109,12 @@ export class Midy extends EventTarget {
|
|
|
2021
2109
|
}
|
|
2022
2110
|
}
|
|
2023
2111
|
setVibLfoToPitch(channel, note, scheduleTime) {
|
|
2024
|
-
if (note.
|
|
2025
|
-
const vibratoDepth = this.
|
|
2026
|
-
2;
|
|
2112
|
+
if (note.vibLfoToPitch) {
|
|
2113
|
+
const vibratoDepth = this.getRelativeKeyBasedValue(channel, note.noteNumber, 77) * 2;
|
|
2027
2114
|
const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
|
|
2028
2115
|
const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
|
|
2029
2116
|
const depth = baseDepth * Math.sign(vibLfoToPitch);
|
|
2030
|
-
note.
|
|
2117
|
+
note.vibLfoToPitch.gain
|
|
2031
2118
|
.cancelScheduledValues(scheduleTime)
|
|
2032
2119
|
.setValueAtTime(depth, scheduleTime);
|
|
2033
2120
|
}
|
|
@@ -2038,18 +2125,18 @@ export class Midy extends EventTarget {
|
|
|
2038
2125
|
setModLfoToFilterFc(channel, note, scheduleTime) {
|
|
2039
2126
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
|
|
2040
2127
|
this.getLFOFilterDepth(channel, note);
|
|
2041
|
-
note.
|
|
2128
|
+
note.modLfoToFilterFc.gain
|
|
2042
2129
|
.cancelScheduledValues(scheduleTime)
|
|
2043
2130
|
.setValueAtTime(modLfoToFilterFc, scheduleTime);
|
|
2044
2131
|
}
|
|
2045
2132
|
setModLfoToVolume(channel, note, scheduleTime) {
|
|
2046
2133
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
2047
2134
|
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
2048
|
-
const
|
|
2135
|
+
const depth = baseDepth * Math.sign(modLfoToVolume) *
|
|
2049
2136
|
(1 + this.getLFOAmplitudeDepth(channel, note));
|
|
2050
|
-
note.
|
|
2137
|
+
note.modLfoToVolume.gain
|
|
2051
2138
|
.cancelScheduledValues(scheduleTime)
|
|
2052
|
-
.setValueAtTime(
|
|
2139
|
+
.setValueAtTime(depth, scheduleTime);
|
|
2053
2140
|
}
|
|
2054
2141
|
setReverbSend(channel, note, scheduleTime) {
|
|
2055
2142
|
let value = note.voiceParams.reverbEffectsSend *
|
|
@@ -2062,7 +2149,7 @@ export class Midy extends EventTarget {
|
|
|
2062
2149
|
if (!note.reverbSend) {
|
|
2063
2150
|
if (0 < value) {
|
|
2064
2151
|
note.reverbSend = new GainNode(this.audioContext, { gain: value });
|
|
2065
|
-
note.
|
|
2152
|
+
note.volumeNode.connect(note.reverbSend);
|
|
2066
2153
|
note.reverbSend.connect(this.reverbEffect.input);
|
|
2067
2154
|
}
|
|
2068
2155
|
}
|
|
@@ -2071,11 +2158,11 @@ export class Midy extends EventTarget {
|
|
|
2071
2158
|
.cancelScheduledValues(scheduleTime)
|
|
2072
2159
|
.setValueAtTime(value, scheduleTime);
|
|
2073
2160
|
if (0 < value) {
|
|
2074
|
-
note.
|
|
2161
|
+
note.volumeNode.connect(note.reverbSend);
|
|
2075
2162
|
}
|
|
2076
2163
|
else {
|
|
2077
2164
|
try {
|
|
2078
|
-
note.
|
|
2165
|
+
note.volumeNode.disconnect(note.reverbSend);
|
|
2079
2166
|
}
|
|
2080
2167
|
catch { /* empty */ }
|
|
2081
2168
|
}
|
|
@@ -2092,7 +2179,7 @@ export class Midy extends EventTarget {
|
|
|
2092
2179
|
if (!note.chorusSend) {
|
|
2093
2180
|
if (0 < value) {
|
|
2094
2181
|
note.chorusSend = new GainNode(this.audioContext, { gain: value });
|
|
2095
|
-
note.
|
|
2182
|
+
note.volumeNode.connect(note.chorusSend);
|
|
2096
2183
|
note.chorusSend.connect(this.chorusEffect.input);
|
|
2097
2184
|
}
|
|
2098
2185
|
}
|
|
@@ -2101,11 +2188,11 @@ export class Midy extends EventTarget {
|
|
|
2101
2188
|
.cancelScheduledValues(scheduleTime)
|
|
2102
2189
|
.setValueAtTime(value, scheduleTime);
|
|
2103
2190
|
if (0 < value) {
|
|
2104
|
-
note.
|
|
2191
|
+
note.volumeNode.connect(note.chorusSend);
|
|
2105
2192
|
}
|
|
2106
2193
|
else {
|
|
2107
2194
|
try {
|
|
2108
|
-
note.
|
|
2195
|
+
note.volumeNode.disconnect(note.chorusSend);
|
|
2109
2196
|
}
|
|
2110
2197
|
catch { /* empty */ }
|
|
2111
2198
|
}
|
|
@@ -2114,29 +2201,29 @@ export class Midy extends EventTarget {
|
|
|
2114
2201
|
setDelayModLFO(note) {
|
|
2115
2202
|
const startTime = note.startTime + note.voiceParams.delayModLFO;
|
|
2116
2203
|
try {
|
|
2117
|
-
note.
|
|
2204
|
+
note.modLfo.start(startTime);
|
|
2118
2205
|
}
|
|
2119
2206
|
catch { /* empty */ }
|
|
2120
2207
|
}
|
|
2121
2208
|
setFreqModLFO(note, scheduleTime) {
|
|
2122
2209
|
const freqModLFO = note.voiceParams.freqModLFO;
|
|
2123
|
-
note.
|
|
2210
|
+
note.modLfo.frequency
|
|
2124
2211
|
.cancelScheduledValues(scheduleTime)
|
|
2125
2212
|
.setValueAtTime(freqModLFO, scheduleTime);
|
|
2126
2213
|
}
|
|
2127
2214
|
setDelayVibLFO(channel, note) {
|
|
2128
|
-
const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
|
|
2215
|
+
const vibratoDelay = this.getRelativeKeyBasedValue(channel, note.noteNumber, 78) * 2;
|
|
2129
2216
|
const value = note.voiceParams.delayVibLFO;
|
|
2130
2217
|
const startTime = note.startTime + value * vibratoDelay;
|
|
2131
2218
|
try {
|
|
2132
|
-
note.
|
|
2219
|
+
note.vibLfo.start(startTime);
|
|
2133
2220
|
}
|
|
2134
2221
|
catch { /* empty */ }
|
|
2135
2222
|
}
|
|
2136
2223
|
setFreqVibLFO(channel, note, scheduleTime) {
|
|
2137
|
-
const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
|
|
2224
|
+
const vibratoRate = this.getRelativeKeyBasedValue(channel, note.noteNumber, 76) * 2;
|
|
2138
2225
|
const freqVibLFO = note.voiceParams.freqVibLFO;
|
|
2139
|
-
note.
|
|
2226
|
+
note.vibLfo.frequency
|
|
2140
2227
|
.cancelScheduledValues(scheduleTime)
|
|
2141
2228
|
.setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
|
|
2142
2229
|
}
|
|
@@ -2185,7 +2272,7 @@ export class Midy extends EventTarget {
|
|
|
2185
2272
|
},
|
|
2186
2273
|
delayVibLFO: (channel, note, _scheduleTime) => {
|
|
2187
2274
|
if (0 < channel.state.vibratoDepth) {
|
|
2188
|
-
setDelayVibLFO(channel, note);
|
|
2275
|
+
this.setDelayVibLFO(channel, note);
|
|
2189
2276
|
}
|
|
2190
2277
|
},
|
|
2191
2278
|
freqVibLFO: (channel, note, scheduleTime) => {
|
|
@@ -2292,6 +2379,8 @@ export class Midy extends EventTarget {
|
|
|
2292
2379
|
return handlers;
|
|
2293
2380
|
}
|
|
2294
2381
|
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
2382
|
+
if (!(0 <= scheduleTime))
|
|
2383
|
+
scheduleTime = this.audioContext.currentTime;
|
|
2295
2384
|
this.applyToMPEChannels(channelNumber, (ch) => {
|
|
2296
2385
|
this.applyControlChange(ch, controllerType, value, scheduleTime);
|
|
2297
2386
|
});
|
|
@@ -2302,7 +2391,9 @@ export class Midy extends EventTarget {
|
|
|
2302
2391
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
2303
2392
|
const channel = this.channels[channelNumber];
|
|
2304
2393
|
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
2305
|
-
this.
|
|
2394
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2395
|
+
this.setControlChangeEffects(channel, note, scheduleTime);
|
|
2396
|
+
});
|
|
2306
2397
|
}
|
|
2307
2398
|
else {
|
|
2308
2399
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -2384,7 +2475,7 @@ export class Midy extends EventTarget {
|
|
|
2384
2475
|
}
|
|
2385
2476
|
}
|
|
2386
2477
|
panToGain(pan) {
|
|
2387
|
-
const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
|
|
2478
|
+
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
2388
2479
|
return {
|
|
2389
2480
|
gainLeft: Math.cos(theta),
|
|
2390
2481
|
gainRight: Math.sin(theta),
|
|
@@ -2429,7 +2520,8 @@ export class Midy extends EventTarget {
|
|
|
2429
2520
|
const volume = volumeMSB + volumeLSB / 128;
|
|
2430
2521
|
const expression = expressionMSB + expressionLSB / 128;
|
|
2431
2522
|
const pan = panMSB + panLSB / 128;
|
|
2432
|
-
const
|
|
2523
|
+
const effect = this.getChannelAmplitudeControl(channel);
|
|
2524
|
+
const gain = volume * expression * (1 + effect);
|
|
2433
2525
|
const { gainLeft, gainRight } = this.panToGain(pan);
|
|
2434
2526
|
channel.gainL.gain
|
|
2435
2527
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -2539,19 +2631,11 @@ export class Midy extends EventTarget {
|
|
|
2539
2631
|
const state = channel.state;
|
|
2540
2632
|
state.filterResonance = ccValue / 127;
|
|
2541
2633
|
this.processScheduledNotes(channel, (note) => {
|
|
2542
|
-
const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
|
|
2634
|
+
const filterResonance = this.getRelativeKeyBasedValue(channel, note.noteNumber, 71);
|
|
2543
2635
|
const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
|
|
2544
|
-
note.
|
|
2636
|
+
note.filterEnvelopeNode.Q.setValueAtTime(Q, scheduleTime);
|
|
2545
2637
|
});
|
|
2546
2638
|
}
|
|
2547
|
-
getRelativeKeyBasedValue(channel, note, controllerType) {
|
|
2548
|
-
const ccState = channel.state.array[128 + controllerType];
|
|
2549
|
-
const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, controllerType);
|
|
2550
|
-
if (keyBasedValue < 0)
|
|
2551
|
-
return ccState;
|
|
2552
|
-
const keyValue = ccState + keyBasedValue / 127 - 0.5;
|
|
2553
|
-
return keyValue < 0 ? keyValue : 0;
|
|
2554
|
-
}
|
|
2555
2639
|
setReleaseTime(channelNumber, releaseTime, scheduleTime) {
|
|
2556
2640
|
const channel = this.channels[channelNumber];
|
|
2557
2641
|
if (channel.isDrum)
|
|
@@ -2824,6 +2908,7 @@ export class Midy extends EventTarget {
|
|
|
2824
2908
|
this.updateModulation(channel, scheduleTime);
|
|
2825
2909
|
}
|
|
2826
2910
|
handleMIDIPolyphonicExpressionRPN(channelNumber, _scheduleTime) {
|
|
2911
|
+
const channel = this.channels[channelNumber];
|
|
2827
2912
|
this.setMIDIPolyphonicExpression(channelNumber, channel.dataMSB);
|
|
2828
2913
|
}
|
|
2829
2914
|
setMIDIPolyphonicExpression(channelNumber, value) {
|
|
@@ -3355,98 +3440,118 @@ export class Midy extends EventTarget {
|
|
|
3355
3440
|
this.updateChannelDetune(channel, scheduleTime);
|
|
3356
3441
|
}
|
|
3357
3442
|
}
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3443
|
+
calcEffectValue(channel, note, destination) {
|
|
3444
|
+
return this.calcChannelEffectValue(channel, destination) +
|
|
3445
|
+
this.calcNoteEffectValue(channel, note, destination);
|
|
3446
|
+
}
|
|
3447
|
+
calcChannelEffectValue(channel, destination) {
|
|
3448
|
+
return this.calcControlChangeEffectValue(channel, destination) +
|
|
3449
|
+
this.calcChannelPressureEffectValue(channel, destination);
|
|
3450
|
+
}
|
|
3451
|
+
calcControlChangeEffectValue(channel, destination) {
|
|
3452
|
+
const controlType = channel.controlTable[destination];
|
|
3453
|
+
if (controlType < 0)
|
|
3454
|
+
return 0;
|
|
3455
|
+
const pressure = channel.state.array[controlType];
|
|
3456
|
+
if (pressure <= 0)
|
|
3457
|
+
return 0;
|
|
3458
|
+
const baseline = pressureBaselines[destination];
|
|
3459
|
+
const tableValue = channel.controlTable[destination + 6];
|
|
3460
|
+
const value = (tableValue - baseline) * pressure;
|
|
3461
|
+
return value * effectParameters[destination];
|
|
3462
|
+
}
|
|
3463
|
+
calcChannelPressureEffectValue(channel, destination) {
|
|
3464
|
+
const pressure = channel.state.channelPressure;
|
|
3465
|
+
if (pressure <= 0)
|
|
3361
3466
|
return 0;
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3364
|
-
|
|
3467
|
+
const baseline = pressureBaselines[destination];
|
|
3468
|
+
const tableValue = channel.channelPressureTable[destination];
|
|
3469
|
+
const value = (tableValue - baseline) * pressure;
|
|
3470
|
+
return value * effectParameters[destination];
|
|
3471
|
+
}
|
|
3472
|
+
calcNoteEffectValue(channel, note, destination) {
|
|
3473
|
+
const pressure = note.pressure;
|
|
3474
|
+
if (pressure <= 0)
|
|
3475
|
+
return 0;
|
|
3476
|
+
const baseline = pressureBaselines[destination];
|
|
3477
|
+
const tableValue = channel.polyphonicKeyPressureTable[destination];
|
|
3478
|
+
const value = (tableValue - baseline) * pressure / 127;
|
|
3479
|
+
return value * effectParameters[destination];
|
|
3480
|
+
}
|
|
3481
|
+
getChannelPitchControl(channel) {
|
|
3482
|
+
return this.calcChannelEffectValue(channel, 0);
|
|
3483
|
+
}
|
|
3484
|
+
getNotePitchControl(channel, note) {
|
|
3485
|
+
return this.calcNoteEffectValue(channel, note, 0);
|
|
3486
|
+
}
|
|
3487
|
+
getPitchControl(channel, note) {
|
|
3488
|
+
return this.calcEffectValue(channel, note, 0);
|
|
3365
3489
|
}
|
|
3366
3490
|
getFilterCutoffControl(channel, note) {
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
: 0;
|
|
3375
|
-
return (channelPressure + polyphonicKeyPressure) * 15;
|
|
3491
|
+
return this.calcEffectValue(channel, note, 1);
|
|
3492
|
+
}
|
|
3493
|
+
getChannelAmplitudeControl(channel) {
|
|
3494
|
+
return this.calcChannelEffectValue(channel, 2);
|
|
3495
|
+
}
|
|
3496
|
+
getNoteAmplitudeControl(channel, note) {
|
|
3497
|
+
return this.calcNoteEffectValue(channel, note, 2);
|
|
3376
3498
|
}
|
|
3377
3499
|
getAmplitudeControl(channel, note) {
|
|
3378
|
-
|
|
3379
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
3380
|
-
? channel.state.channelPressure * 127 / channelPressureRaw
|
|
3381
|
-
: 0;
|
|
3382
|
-
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
|
|
3383
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3384
|
-
? note.pressure / polyphonicKeyPressureRaw
|
|
3385
|
-
: 0;
|
|
3386
|
-
return channelPressure + polyphonicKeyPressure;
|
|
3500
|
+
return this.calcEffectValue(channel, note, 2);
|
|
3387
3501
|
}
|
|
3388
3502
|
getLFOPitchDepth(channel, note) {
|
|
3389
|
-
|
|
3390
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
3391
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
3392
|
-
: 0;
|
|
3393
|
-
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
|
|
3394
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3395
|
-
? polyphonicKeyPressureRaw * note.pressure
|
|
3396
|
-
: 0;
|
|
3397
|
-
return (channelPressure + polyphonicKeyPressure) / 254 * 600;
|
|
3503
|
+
return this.calcEffectValue(channel, note, 3);
|
|
3398
3504
|
}
|
|
3399
3505
|
getLFOFilterDepth(channel, note) {
|
|
3400
|
-
|
|
3401
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
3402
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
3403
|
-
: 0;
|
|
3404
|
-
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
|
|
3405
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3406
|
-
? polyphonicKeyPressureRaw * note.pressure
|
|
3407
|
-
: 0;
|
|
3408
|
-
return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
|
|
3506
|
+
return this.calcEffectValue(channel, note, 4);
|
|
3409
3507
|
}
|
|
3410
3508
|
getLFOAmplitudeDepth(channel, note) {
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3417
|
-
? polyphonicKeyPressureRaw * note.pressure
|
|
3418
|
-
: 0;
|
|
3419
|
-
return (channelPressure + polyphonicKeyPressure) / 254;
|
|
3420
|
-
}
|
|
3421
|
-
setEffects(channel, note, table, scheduleTime) {
|
|
3422
|
-
if (0 < table[0]) {
|
|
3509
|
+
return this.calcEffectValue(channel, note, 5);
|
|
3510
|
+
}
|
|
3511
|
+
createEffectHandlers() {
|
|
3512
|
+
const handlers = new Array(6);
|
|
3513
|
+
handlers[0] = (channel, note, scheduleTime) => {
|
|
3423
3514
|
if (this.isPortamento(channel, note)) {
|
|
3424
3515
|
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
3425
3516
|
}
|
|
3426
3517
|
else {
|
|
3427
3518
|
this.setDetune(channel, note, scheduleTime);
|
|
3428
3519
|
}
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
|
-
if (0
|
|
3520
|
+
};
|
|
3521
|
+
handlers[1] = (channel, note, scheduleTime) => {
|
|
3522
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
3432
3523
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
3433
3524
|
}
|
|
3434
|
-
|
|
3435
|
-
this.
|
|
3525
|
+
else {
|
|
3526
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
3436
3527
|
}
|
|
3528
|
+
};
|
|
3529
|
+
handlers[2] = (channel, note, scheduleTime) => this.setVolumeNode(channel, note, scheduleTime);
|
|
3530
|
+
handlers[3] = (channel, note, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3531
|
+
handlers[4] = (channel, note, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3532
|
+
handlers[5] = (channel, note, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3533
|
+
return handlers;
|
|
3534
|
+
}
|
|
3535
|
+
setControlChangeEffects(channel, note, scheduleTime) {
|
|
3536
|
+
const handlers = this.effectHandlers;
|
|
3537
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3538
|
+
const baseline = pressureBaselines[i];
|
|
3539
|
+
const tableValue = channel.controlTable[i + 6];
|
|
3540
|
+
if (baseline === tableValue)
|
|
3541
|
+
continue;
|
|
3542
|
+
handlers[i](channel, note, scheduleTime);
|
|
3437
3543
|
}
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3544
|
+
}
|
|
3545
|
+
setPressureEffects(channel, note, tableName, scheduleTime) {
|
|
3546
|
+
const handlers = this.effectHandlers;
|
|
3547
|
+
const table = channel[tableName];
|
|
3548
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3549
|
+
const baseline = pressureBaselines[i];
|
|
3550
|
+
const tableValue = table[i];
|
|
3551
|
+
if (baseline === tableValue)
|
|
3552
|
+
continue;
|
|
3553
|
+
handlers[i](channel, note, scheduleTime);
|
|
3443
3554
|
}
|
|
3444
|
-
if (0 < table[3])
|
|
3445
|
-
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3446
|
-
if (0 < table[4])
|
|
3447
|
-
this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3448
|
-
if (0 < table[5])
|
|
3449
|
-
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3450
3555
|
}
|
|
3451
3556
|
handlePressureSysEx(data, tableName, scheduleTime) {
|
|
3452
3557
|
const channelNumber = data[4];
|
|
@@ -3458,39 +3563,41 @@ export class Midy extends EventTarget {
|
|
|
3458
3563
|
const pp = data[i];
|
|
3459
3564
|
const rr = data[i + 1];
|
|
3460
3565
|
table[pp] = rr;
|
|
3566
|
+
const handler = this.effectHandlers[pp];
|
|
3567
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3568
|
+
if (handler)
|
|
3569
|
+
handler(channel, note, scheduleTime);
|
|
3570
|
+
});
|
|
3461
3571
|
}
|
|
3462
|
-
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3463
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
3464
|
-
});
|
|
3465
|
-
}
|
|
3466
|
-
initControlTable() {
|
|
3467
|
-
const ccCount = 128;
|
|
3468
|
-
const slotSize = 6;
|
|
3469
|
-
return new Int8Array(ccCount * slotSize).fill(-1);
|
|
3470
|
-
}
|
|
3471
|
-
setControlChangeEffects(channel, controllerType, scheduleTime) {
|
|
3472
|
-
const slotSize = 6;
|
|
3473
|
-
const offset = controllerType * slotSize;
|
|
3474
|
-
const table = channel.controlTable.subarray(offset, offset + slotSize);
|
|
3475
|
-
this.processScheduledNotes(channel, (note) => {
|
|
3476
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
3477
|
-
});
|
|
3478
3572
|
}
|
|
3479
3573
|
handleControlChangeSysEx(data, scheduleTime) {
|
|
3480
3574
|
const channelNumber = data[4];
|
|
3481
3575
|
const channel = this.channels[channelNumber];
|
|
3482
3576
|
if (channel.isDrum)
|
|
3483
3577
|
return;
|
|
3484
|
-
const slotSize = 6;
|
|
3485
|
-
const controllerType = data[5];
|
|
3486
|
-
const offset = controllerType * slotSize;
|
|
3487
3578
|
const table = channel.controlTable;
|
|
3579
|
+
table.set(defaultControlValues);
|
|
3580
|
+
const controllerType = data[5];
|
|
3488
3581
|
for (let i = 6; i < data.length; i += 2) {
|
|
3489
3582
|
const pp = data[i];
|
|
3490
3583
|
const rr = data[i + 1];
|
|
3491
|
-
table[
|
|
3584
|
+
table[pp] = controllerType;
|
|
3585
|
+
table[pp + 6] = rr;
|
|
3586
|
+
const handler = this.effectHandlers[pp];
|
|
3587
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3588
|
+
if (handler)
|
|
3589
|
+
handler(channel, note, scheduleTime);
|
|
3590
|
+
});
|
|
3492
3591
|
}
|
|
3493
|
-
|
|
3592
|
+
}
|
|
3593
|
+
getRelativeKeyBasedValue(channel, keyNumber, controllerType) {
|
|
3594
|
+
const ccState = channel.state.array[128 + controllerType];
|
|
3595
|
+
if (!channel.isDrum)
|
|
3596
|
+
return ccState;
|
|
3597
|
+
const keyBasedValue = this.getKeyBasedValue(channel, keyNumber, controllerType);
|
|
3598
|
+
if (keyBasedValue < 0)
|
|
3599
|
+
return ccState;
|
|
3600
|
+
return ccState * keyBasedValue / 64;
|
|
3494
3601
|
}
|
|
3495
3602
|
getKeyBasedValue(channel, keyNumber, controllerType) {
|
|
3496
3603
|
const index = keyNumber * 128 + controllerType;
|
|
@@ -3503,9 +3610,9 @@ export class Midy extends EventTarget {
|
|
|
3503
3610
|
handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
3504
3611
|
handlers[71] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|
|
3505
3612
|
if (note.noteNumber === keyNumber) {
|
|
3506
|
-
const filterResonance = this.getRelativeKeyBasedValue(channel,
|
|
3613
|
+
const filterResonance = this.getRelativeKeyBasedValue(channel, keyNumber, 71);
|
|
3507
3614
|
const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
|
|
3508
|
-
note.
|
|
3615
|
+
note.filterEnvelopeNode.Q.setValueAtTime(Q, scheduleTime);
|
|
3509
3616
|
}
|
|
3510
3617
|
});
|
|
3511
3618
|
handlers[73] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|