-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathbus_comp.jsfx
More file actions
1072 lines (956 loc) · 35.5 KB
/
bus_comp.jsfx
File metadata and controls
1072 lines (956 loc) · 35.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
desc: Bus Compressor
version: 1.8.3
author: chokehold
tags: processing compressor dynamics gain
link: https://github.com/chkhld/jsfx/
screenshot: https://github.com/chkhld/jsfx/blob/main/assets/screenshots/bus_comp.png
about:
# Bus Compressor
A compressor primarily intended for signal densification, just to
avoid the word "glue". Use this wherever several different signal
sources meet and are combined into a single bus.
Input gain allows "boosting" the signal into the compressor, this
helps to bring background signals up, "make cymbals breathe" etc.
The Threshold, Ratio, Attack and Release settings are pretty much
as in any other compressor as well. If the Release is set to Auto,
the compressor will use a second release envelope, which helps to
keep a signal stable over long periods of time, but still lets it
"live" over shorter periods of time.
If the Sidechain is switched to External, the compressor will use
inputs 3+4 as the Left and Right key signals. There's a high pass
filter that lets the detector circuit focus on higher frequencies
to avoid pumping from kick drums and bass where not desired.
The channel routing switches between stereo and Mid/Side mode. In
stereo mode, the L channel is processed as the L channel, and the
R channel is processed as the R channel. In Mid/Side setting, the
L+R channels will be converted into M+S channels. M channel holds
the mid/center information of the signal, e.g. kick/snare/vocals,
and S channel holds the side/stereo information, e.g. guitars and
drum overheads. So in Mid/Side mode, the left sample holds center
information and the right sample holds the stereo information. In
M+S mode, the stereo field can appear "widened", depending on how
much each channel is compressed.
With stereo linking turned off the L+R channels will be processed
individually. Fading in stereo linking will make the detector act
on a mix of the L+R channels increasingly. If you have incoherent
signals (e.g. two separate guitars) on both channels, you'll most
likely not want any stereo linking. Coherent signals, e.g. drums,
will need stereo linking or their stereo image will suffer badly.
NOTE: if stereo liking is set to 100%, there won't be an audible
difference between L+R and M+S mode.
Instability will add a bit of low-level noise to the signal, both
the audible path as well as the detector path. The noise is auto-
blanked, meaning it won't hiss in quieter sections of the project.
It is very quiet (~ -89 dBfs RMS), so you probably won't be able
to hear it. But it adds different inconsistencies to every signal
path, audible + key/sidechain, like real-world analog devices do.
This results in ever so gentle "errors" in the compression, plus
it imparts an esoterically slight "graininess" onto the material.
The saturation circuit is gain-reduction dependent, so the harder
the compressor squashes the signal, the more soft saturation will
be introduced into the material.
Hard 0 dBfs output clipping may be selected if so desired but may
be a bit harsh and buzzy sounding. Use the Makeup gain to "boost"
the material into this, or don't.
Finally, Dry/wet mix fades between the unprocessed and processed
signals, this is also known as parallel or New York compression.
// ----------------------------------------------------------------------------
slider1: dBGain=0<-12, 12, 0.01>Input gain [dB]
slider2: compThresh=0<-60, 0, 0.01>Threshold [dB]
slider3: rawRatio=3<0, 6, {1.5,2,3,4,5,10,20}>Ratio [x:1]
slider4: rawAttack=4<0, 5, {0.1,0.3,1,3,10,30}>Attack [ms]
slider5: rawRelease=6<0, 6, {0.1,0.2,0.4,0.8,1.6,3.2,Auto}>Release [s]
slider6: sidechain=0<0, 1, {Internal (inputs 1+2),External (inputs 3+4)}>Sidechain
slider7: scFreq=75<20, 500, 1>SC high pass [Hz]
slider8: midSide=0<0,1,{Stereo (L+R),Mid/Side (M+S)}>Channel routing
slider9: linkAmount=100<0, 100, 1>Stereo link [%]
slider10:instability=100<0,100,0.01>Instability [%]
slider11:saturation=100<0,100,0.01>Saturation [%]
slider12:dBTrim=0<-24, 24, 0.01>Makeup gain [dB]
slider13:clip=0<0,1,{Deactivated,Hard clip 0 dBfs}>Clip output
slider14:pctMix=100<0,100,0.01>Dry/wet mix [%]
in_pin:Input L
in_pin:Input R
in_pin:Sidechain L
in_pin:Sidechain R
out_pin:Output L
out_pin:Output R
// Don't need these because of UI metering
options:no_meter
@init // ----------------------------------------------------------------------
// Stop Reaper from re-initializing the plugin every time playback is reset
ext_noinit = 1;
// Various convenience constants
M_LN10_20 = 8.68588963806503655302257837833210164588794011607333;
floatFloor = 0.0000000630957; // dBToGain --> ~ -144 dBfs
noiseFloor = 0.00001258925412; // dBToGain --> ~ -98 dBfs
halfPi = $PI * 0.5;
satSpeed = 50.0; // ms timing for saturation envelope follower
rcpSqrt2 = 0.7071067812; // Reciprocal constant 1.0 / sqrt(2.0)
compFeedback = 0.15;
// Used to buffer input/output samples for GUI interaction
bufferInput = 1000; // Raw input values
bufferKey = 1002; // Sidechain key signal
bufferGR = 1004; // Gain reduction as absolute float gain factors
bufferOutput = 1006; // Raw output values
//
bufferClipInput = 1008;
bufferClipKey = 1010;
bufferClipOutput = 1012;
// Buffer for noise generation states
noiseState = 10000;
// Converts signed dB values to normalized float gain factors.
// Divisions are slow, so: dB/20 --> dB * 1/20 --> dB * 0.05
function dBToGain (decibels) (pow(10, decibels * 0.05));
//
// Converts float gain factors to dB values
function gainTodB (float) local (below)
(
float = abs(float);
below = float < floatFloor;
float = below * floatFloor + (!below) * float;
(log(float) * M_LN10_20);
);
// Stereo L+R and Mid/Side M+S conversion functions
//
function lrToM (sampleLeft, sampleRight) ((sampleLeft + sampleRight) * rcpSqrt2);
function lrToS (sampleLeft, sampleRight) ((sampleLeft - sampleRight) * rcpSqrt2);
function msToL (sampleMid, sampleSide) ((sampleMid + sampleSide) * rcpSqrt2);
function msToR (sampleMid, sampleSide) ((sampleMid - sampleSide) * rcpSqrt2);
// DC Blocker to filter near-static frequency content
// that would otherwise "offset" the waveform.
function dcBlocker () instance (stateIn, stateOut)
(
stateOut *= 0.99988487;
stateOut += this - stateIn;
stateIn = this;
this = stateOut;
);
// SAMPLE RANDOMIZATION
//
// Returns a randomized sample value between [-limit,+limit]
// which can be used as basic white noise.
//
function random (limit) (rand() * 2.0 * limit - limit);
// PINK NOISE
//
// "Warm" sounding, volume falls off at -3 dBfs per octave across the
// spectrum. Often said to be similar to the optimal mix balance, and
// to be generally pleasing to the human ear.
//
// Implemented after Larry Trammell's "newpink" method:
// http://www.ridgerat-tech.us/tech/newpink.htm
//
function tickPink (channel) local (offset, break, sample)
(
offset = channel * 50;
break = 0;
sample = rand();
break == 0 && sample <= 0.00198 ? (noiseState[offset+1] = random(1) * 3.8024; break = 1);
break == 0 && sample <= 0.01478 ? (noiseState[offset+2] = random(1) * 2.9694; break = 1);
break == 0 && sample <= 0.06378 ? (noiseState[offset+3] = random(1) * 2.5970; break = 1);
break == 0 && sample <= 0.23378 ? (noiseState[offset+4] = random(1) * 3.0870; break = 1);
break == 0 && sample <= 0.91578 ? (noiseState[offset+5] = random(1) * 3.4006; break = 1);
noiseState[offset] = 0;
noiseState[offset] += noiseState[offset+1];
noiseState[offset] += noiseState[offset+2];
noiseState[offset] += noiseState[offset+3];
noiseState[offset] += noiseState[offset+4];
noiseState[offset] += noiseState[offset+5];
noiseState[offset] * dBToGain(-15);
);
// Hard clipping at 0 dBfs, restricts samples to range [-1,+1]
function hardclip ()
(
this = max(-1, min(1, this));
);
// SINE CLIPPING -------------------------------------------------------------
//
// The output of the sine function is within the range [-1,+1] as long as its
// input stays inside the range [-1/2π,+1/2π]. This is perfect to create soft
// overdrive, with less distortion than common hyperbolic tangent saturation.
//
// This type of clipping doesn't use a ceiling, if you want one then you have
// to boost the signal into this function first, and attenuate it later on by
// the factor 1/boost.
//
function sineClip (sample) local (above, below, overs, polarity)
(
// Evaluate whether the sample is outside of range [-1/2π,+1/2π]
above = (sample > halfPi);
below = (sample < -halfPi);
overs = (above || below);
// If the sample is outside [-1/2π,+1/2π] then output the sample's polarity,
// which will always be either -1 or +1, making it a perfect 0 dBfs ceiling
// value for hard clipping. If the sample is inside [-1/2π,+1/2π], then run
// it through the sin() function, which returns values in range [-1,+1] for
// peak sample values up to 1/2π, so well over the usual 0 dBfs hard limit.
//
sample = overs * sign(sample) + !overs * sin(sample);
);
// SATURATION PROCESS
//
// Nothing special here, just adds a certain amount of soft
// clipped "warm tube drive" (sigh) onto the signal.
//
function saturate (drive) local (boost)
(
boost = 1.0 + (1.0 - drive);
this = (dryMix * this) + (satMix * sineClip(this * boost));
);
// SIGNAL INSTABILITY
//
// Generates a noisefloor at ~ -89 dBfs RMS to make the signal
// become a little instable, as would be the case in an analog
// circuit. The noise is added to both the audible signal and
// the sidechain circuit, but it will be auto-blanked if input
// is quiet, so it will not "hiss" in the project when paused.
//
function variation (channel) local (channel)
(
noise = tickPink(channel) * noiseFloor * variationLevel;
noise;
);
// SIDECHAIN FILTER
//
// Simple Biquad High Pass filter used in the sidechain circuit.
// Implemented after Andrew Simper's State Variable Filter paper.
// https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf
//
function eqHP (Hz, Q) instance (a1, a2, a3, m0, m1, m2) local (g, k)
(
g = tan(halfPi * (Hz / srate)); k = 1/Q;
a1 = 1/(1.0 + g * (g + k)); a2 = a1 * g; a3 = a2 * g;
m0 = 1.0; m1 = -k; m2 = -1.0;
);
//
function eqTick (sample) instance (v1, v2, v3, ic1eq, ic2eq)
(
v3 = sample - ic2eq; v1 = this.a1 * ic1eq + this.a2 * v3;
v2 = ic2eq + this.a2 * ic1eq + this.a3 * v3;
ic1eq = 2.0 * v1 - ic1eq; ic2eq = 2.0 * v2 - ic2eq;
(this.m0 * sample + this.m1 * v1 + this.m2 * v2);
);
// PRIMITIVE ENVELOPE FOLLOWER
//
// Follows the envelope of a signal at the speed set with the
// msTime argument. Higher ms values mean slower response to
// the signal, lower ms values mean faster response.
//
function envSetup (msTime) instance (coeff) local ()
(
coeff = exp(-1000 / (msTime * srate));
);
//
// It's possible to do the calculations in dB or in gain factors.
// Because this compressor uses many dB values, I decided against
// constantly converting values back and forth, but to just run
// the envelopes on dB values.
//
// The sample should already be abs()-ed by here to dBfs.
//
// The envelope is NOT kept in this variable, but in the PARENT
// variable containing this one. This is the case so one variable
// could hold one envelope value, but two follower instances with
// different timings which could both work on the same envelope.
//
function envTick (dBsample) instance (coeff) local (active)
(
active = (coeff != 0);
(!active * dBsample) + (active * (dBsample + coeff * (this..envelope - dBsample)));
);
// ATTACK / RELEASE ENVELOPE
//
// This will turn a variable into a full envelope container that
// holds an envelope state as well as two time coefficients used
// for separate attack and release timings.
//
function attRelSetup (msAttack, msRelease) instance (coeffAtt, coeffRel) local ()
(
// Set attack and release time coefficients
coeffAtt = exp(-1000 / (msAttack * srate));
coeffRel = exp(-1000 / (msRelease * srate));
);
//
// This calculates the new envelope state for the current sample.
// If the current sample is above the current envelope state, let
// the attack envelope run. And if the current sample is below the
// the current envelope state, then let the release envelope run.
//
// The sample should already be abs()-ed by here to dBfs
//
function attRelTick (dBsample) instance (envelope, coeffAtt, coeffRel) local (above, change)
(
above = (dBsample > envelope);
change = envelope - dBsample;
// If above, calculate attack + if not above, calculate release
envelope = (above * (dBsample + coeffAtt * change)) + (!above * (dBsample + coeffRel * change));
);
// ATTACK / DUAL RELEASE ENVELOPE
//
// Same as an attack/release envelope, but this has a second release stage,
// which allows for less static and more "program dependent" compression.
//
function attRelRelSetup (msAttack, msRelease1, msRelease2) instance (coeffAtt, coeffRel, release2) local ()
(
// Set attack and release time coefficients
coeffAtt = exp(-1000 / (msAttack * srate));
coeffRel = exp(-1000 / (msRelease1 * srate));
release2.envSetup(msRelease2);
);
//
function attRelRelTick (dBsample) instance (envelope, coeffAtt, coeffRel, release2) local (above, change)
(
above = (dBsample > envelope);
change = envelope - dBsample;
// If above, calculate attack + if not above, calculate release
envelope = (above * (dBsample + coeffAtt * change)) + (!above * (dBsample + coeffRel * change));
// If not above, run second release envelope
!above ? envelope = release2.envTick(envelope);
);
// SATURATION ENVELOPE FOLLOWER
//
// To make the saturation smoother, this envelope follower will
// will "slow down" the amount of saturation that is applied to
// the compressed signal.
//
function satSetup (msSpeed) instance (satEnv) local ()
(
satEnv.envSetup(msSpeed);
);
//
function satTick (sampleGR) instance (satEnv, envelope) local ()
(
envelope = satEnv.envTick(sampleGR);
envelope;
);
// GAIN CALCULATOR
//
// From all the various levels, this will calculate more
// values required to calculate with later on.
//
function gainCalcSetup (dBThreshold, fullRatio, dBKnee) instance (threshold, ratio, knee, kneeWidth, kneeUpper, kneeLower) local ()
(
threshold = dBThreshold; // signed dBfs
ratio = 1/fullRatio; // 1/x --> compression < 1, expansion > 1
knee = dBKnee;
kneeWidth = knee * 0.5;
kneeUpper = threshold + kneeWidth;
kneeLower = threshold - kneeWidth;
);
//
function gainCalcTick (dBsample) instance (ratio, knee, kneeLower, kneeUpper, threshold) local (dBReduction, slope)
(
dBReduction = dBsample;
slope = 1.0 - ratio;
// If the signal is inside the confies of the set Soft Knee,
// calculate the appropriate "soft" reduction here.
(knee > 0.0) && (dBsample > kneeLower) && (dBsample < kneeUpper) ?
(
slope *= ((dBsample - kneeLower) / knee) * 0.5;
dBReduction = slope * (kneeLower - dBsample);
):(
dBReduction = min(0.0, slope * (threshold - dBsample));
);
// Return the gain reduction factor, i.e. NOT a sample value
dBToGain(dBReduction);
);
// COMPRESSOR
//
// Finally, now all the individual components created
// earlier are combined into a single big compressor.
//
// Full ratio: >1 for compression, <1 for expansion
// dBThreshold: signed dBfs
// dBKnee: absolute/positive dB
// pctFeedback: % of previous GR detector feedback
//
function compSetup (msAttack, msRelease, dBThreshold, fullRatio, dBKnee, pctFeedback) instance (attRel, attRelRel, calc, feedback) local ()
(
attRel.attRelSetup(msAttack, msRelease);
attRelRel.attRelRelSetup(msAttack, msRelease, 100);
calc.gainCalcSetup(dbThreshold, fullRatio, dBKnee);
feedback = pctFeedback * 0.01;
);
//
function compTick (sample) instance (GR, feedback, attRel, attRelRel, calc) local (feedbackFactor, keyGain, keyDecibels)
(
// The amount of previous GR to apply to this key sample,
// this is essentially a 1-sample-delayed feedback.
feedbackFactor = 1.0 - ((1.0 - GR) * feedback);
// The sidechain key sample value as a float gain factor.
// Doesn't need to be made absolute, this already happens
// outside after filtering, when the linking is prepared.
keyGain = sample * feedbackFactor;
// Turn the key sample [-1;1] into a dBfs value
keyDecibels = gainTodB(keyGain);
// Send the dBfs sample value into the envelope followers
// and get new envelope states. Both are being calculated
// here to avoid clicks and similar issues when switching
// between Manual and Auto release settings.
attRel.attRelTick(keyDecibels);
attRelRel.attRelRelTick(keyDecibels);
// Calculate the required gain reduction for this input
// sample based on user-specified parameters. This will
// output the GR value as a float gain factor, NOT in dB.
GR = calc.gainCalcTick(rawRelease < 6 ? attRel.envelope : attRelRel.envelope);
// This return value is the float factor gain adjustment
// that needs to be applied to the signal sample, it is
// NOT an actual sample value.
GR;
);
// CONVENIENCE VARIABLES -----------------------------------------------------
//
// Makes handling channel buffers more accessible
L = 0;
R = 1;
M = 2;
S = 4;
channelNames = 100;
channelNames[L] = "L";
channelNames[R] = "R";
channelNames[M] = "M";
channelNames[S] = "S";
//
// The speed (dB per second) at which the meters return to 0 (well... -144)
falloff = dBToGain(-36 / srate);
//
// Falloff level at which clipping markers clear again
clipClear = dBToGain(-54); // 1.5 sec
//
// Used in @gfx to check if size has changed
lastWidth = -1;
lastHeight = -1;
//
// Various buffered dB values as float gain factors
dB_Neg48 = dBToGain(-48);
dB_Neg36 = dBToGain(-36);
dB_Neg24 = dBToGain(-24);
dB_Neg12 = dBToGain(-12);
dB_Neg06 = dBToGain(-6);
dB_Neg03 = dBToGain(-3);
dB_Zero0 = dBToGain(0);
//
// Transparency values; I got tired of changing them repetitively
alphaMeterBars = 0.75;
alphaMeterClip = 0.75;
alphaMeterText = 0.75;
alphaMarkerHard = 0.8;
alphaMarkerText = 0.8;
// Set up the dynamic saturation envelope followers
satL.satSetup(satSpeed);
satR.satSetup(satSpeed);
@slider // --------------------------------------------------------------------
// Set the compressor ratio
rawRatio == 0 ? compRatio = 1.5;
rawRatio == 1 ? compRatio = 2;
rawRatio == 2 ? compRatio = 3;
rawRatio == 3 ? compRatio = 4;
rawRatio == 4 ? compRatio = 5;
rawRatio == 5 ? compRatio = 10;
rawRatio == 6 ? compRatio = 20;
// Set the compressor attack time
rawAttack == 0 ? compAttack = 0.1;
rawAttack == 1 ? compAttack = 0.3;
rawAttack == 2 ? compAttack = 1;
rawAttack == 3 ? compAttack = 3;
rawAttack == 4 ? compAttack = 10;
rawAttack == 5 ? compAttack = 30;
// Set the compressor release time
rawRelease == 0 ? compRelease = 100;
rawRelease == 1 ? compRelease = 200;
rawRelease == 2 ? compRelease = 400;
rawRelease == 3 ? compRelease = 800;
rawRelease == 4 ? compRelease = 1600;
rawRelease == 5 ? compRelease = 3200;
rawRelease == 6 ? compRelease = 2400; // sets 2nd release time internally
// Sidechain instability "noise" level
variationLevel = instability * 0.01; // [0,100] --> [0,1]
// Knee level changes depending on Ratio setting.
// Higher ratio = narrow knee, lower ratio = wide knee.
compKnee = 20.0 / compRatio;
// Prepare two compressor instances, one for each channel.
compL.compSetup(compAttack, compRelease, compThresh, compRatio, compKnee, compFeedback);
compR.compSetup(compAttack, compRelease, compThresh, compRatio, compKnee, compFeedback);
// Calculate amount of stereo-linking in the key signal
lnkMix = linkAmount * 0.01;
splMix = 1.0 - lnkMix;
// Configure the sidechain high-pass filters
filterL.eqHP(scFreq, 1.5);
filterR.eqHP(scFreq, 1.5);
// Turn input/output dB gain values into float factors
gainIn = dBToGain(dBGain);
gainOut = dBToGain(dBTrim);
//
// Below only required for GUI drawing
gainThreshold = dBToGain(compThresh);
// The amount of saturation added to the signal
satMix = saturation * 0.01;
dryMix = 1.0 - satMix;
// The amount of dry and processed signal to blend
wetDry = pctMix * 0.01;
dryWet = 1.0 - wetDry;
@gfx 0 200 // ------------------------------------------------------------------
// Reset the canvas to black
gfx_dest = -1;
gfx_clear = 0;
gfx_a = 1.0;
// RECALCULATE DIMENSIONS AND RELATIONS --------------------------------------
//
// Only really needs to happen if screen size changes
(lastWidth != gfx_w) || (lastHeight != gfx_h) ?
(
// Height for marker/label numbers (1px pad + 8px font + 1px pad, and that twice)
markers_h = 20;
// The main display area that contains the level metering bars
display_w = gfx_w - 8; // -4px from right leaves room for overs indication
display_h = gfx_h - 2*markers_h; // 1x top, 1x bottom
display_y = markers_h; // Top-most Y coordinate
// There are 2 channels with 4 meter bars each = 8 meter bars in total
pad_h = 2; // Transparent/black padding between meter bars
pad_lane_h = 2; // Transparent/black padding between stereo pairs
lane_h = (display_h / 8) - pad_lane_h; // "One metering bar of any kind"
meter_h = lane_h - pad_h; // Each only needs 1x pad underneath
half_lane_h = lane_h / 2; // Used for vertically centering text
// These are gfx_x values for various elments.
X_0dB = display_w;
X_Over = X_0dB + 1;
X_Neg48 = X_0dB * dB_Neg48;
X_Neg36 = X_0dB * dB_Neg36;
X_Neg24 = X_0dB * dB_Neg24;
X_Neg12 = X_0dB * dB_Neg12;
X_Neg06 = X_0dB * dB_Neg06;
X_Neg03 = X_0dB * dB_Neg03;
// Update size flags to not calculate again next cycle
lastWidth = gfx_w;
lastHeight = gfx_h;
);
// THRESHOLD MARKER POSITIONS
//
// Can't be offloaded to the one-time calculation above, because these will
// change without the size of the UI changing.
//
X_Threshold = X_0dB * gainThreshold;
//
// Required to calculate label widths below
lbl_w = lbl_h = 0;
//
// Compute the X position of where the Threshold label should go
//
// Text 0.00 and left of line marker
(compThresh == 0) ?
(
gfx_measurestr("0.00x", lbl_w, lbl_h); // Added x as spacer
X_LabelThreshold = X_Threshold - lbl_w;
);
//
// Text -X.XX and left of line marker
(compThresh > -3) && (compThresh < 0) ?
(
gfx_measurestr("-0.00x", lbl_w, lbl_h); // Added x as spacer
X_LabelThreshold = X_Threshold - lbl_w;
);
//
// Text -XX.XX and right of line marker
(compThresh <= -3) ?
(
gfx_measurestr("x", lbl_w, lbl_h); // Added xx as spacer
X_LabelThreshold = X_Threshold + lbl_w + 2;
);
// VERTICAL dB MARKER LINES --------------------------------------------------
//
// For -48 to -3 dBfs
gfx_set(1,1,1,0.35); // Dark gray
//
// -48 dBfs
gfx_x = X_Neg48;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
//
// -36 dBfs
gfx_x = X_Neg36;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
//
// -24 dBfs
gfx_x = X_Neg24;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
//
// -12 dBfs
gfx_x = X_Neg12;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
//
// -6 dBfs
gfx_x = X_Neg06;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
//
// -3 dBfs
gfx_x = X_Neg03;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
//
// Somewhat lighter, only for 0 dBfs -------------- 0 dBfs
gfx_set(1,1,1,0.6); // Gray
//
// 0 dBfs
gfx_x = X_0dB;
gfx_y = 0;
gfx_lineto(gfx_x,gfx_h,1);
// TEXT dB MARKER LABELS -----------------------------------------------------
//
gfx_measurestr("-48", lbl_w, lbl_h);
//
// For -36 to -3 dBfs
gfx_set(1,1,1,0.35); // Dark gray
//
// TOP LABELS ------------------------
//
gfx_y = 4;
gfx_setfont(0);
//
gfx_x = X_Neg36 + 2;
gfx_drawstr("-36");
//
gfx_x = X_Neg24 + 8;
gfx_drawstr("-24");
//
gfx_x = X_Neg12 + 8;
gfx_drawstr("-12");
//
gfx_x = X_Neg06 + 8;
gfx_drawstr("-6");
//
gfx_x = X_Neg03 + 8;
gfx_drawstr("-3");
//
gfx_x = X_0dB - 14;
gfx_drawstr("0");
//
// BOTTOM LABELS -----------------------
//
gfx_y = display_h + markers_h + lbl_h;
gfx_setfont(0);
//
gfx_x = X_Neg36 + 2;
gfx_drawstr("-36");
//
gfx_x = X_Neg24 + 8;
gfx_drawstr("-24");
//
gfx_x = X_Neg12 + 8;
gfx_drawstr("-12");
//
gfx_x = X_Neg06 + 8;
gfx_drawstr("-6");
//
gfx_x = X_Neg03 + 8;
gfx_drawstr("-3");
//
gfx_x = X_0dB - 14;
gfx_drawstr("0");
// THRESHOLD MARKER ----------------------------------------------------------
//
// Line marker
gfx_x = X_Threshold;
gfx_y = 0;
gfx_set(1,0.5,0,alphaMarkerHard); // Red or Orange
gfx_rectto(gfx_x+3,gfx_h);
//
// Marker label ------------ TOP
gfx_x = X_LabelThreshold;
gfx_y = lbl_h + 4;
gfx_set(1,0.5,0,alphaMarkerText); // Red or Orange
gfx_printf("%02.2f", compThresh);
//
// Marker label ------------ BOTTOM
gfx_x = X_LabelThreshold;
gfx_y = gfx_h - markers_h - 1;
gfx_printf("%02.2f", compThresh);
// LEVEL METERING LANES ------------------------------------------------------
//
// Every channel has multiple meters: raw in, key/sidechain, GR, output.
// Meters are grouped per type: input L/R, key L/R, etc.
// Draw each channel separately, leave space between bars for other channel.
//
// Cycle through both plugin channels
channel = L;
while (channel <= R)
(
// SAMPLE PREPARATION --------------
//
// The processing block always writes up-to-date samples into buffers.
//
// Fetch the buffered (absolute) samples and restrict them to a useful range
splIn = max(dB_Neg48, min(dB_Zero0, bufferInput[channel]));
splKey = max(dB_Neg48, min(dB_Zero0, bufferKey[channel]));
splGR = max(dB_Neg48, min(dB_Zero0, bufferGR[channel]));
splOut = max(dB_Neg48, min(dB_Zero0, bufferOutput[channel]));
//
// Prepare clipping state buffers (reset to 0 if below clearing level)
bufferClipInput[channel] = (bufferClipInput[channel] > clipClear) * bufferClipInput[channel];
bufferClipKey[channel] = (bufferClipKey[channel] > clipClear) * bufferClipKey[channel];
bufferClipOutput[channel] = (bufferClipOutput[channel] > clipClear) * bufferClipOutput[channel];
// METER BAR DRAWING ---------------
//
// Figure out the Y positions for each meter
meterOffset = (channel * lane_h) + pad_lane_h;
meterInput_y = display_y + meterOffset;
meterKey_y = display_y + meterOffset + ((lane_h + pad_lane_h) * 2);
meterGR_y = display_y + meterOffset + ((lane_h + pad_lane_h) * 4);
meterOut_y = display_y + meterOffset + ((lane_h + pad_lane_h) * 6);
//
(splIn > dB_Neg48) ? // INPUT METER ----------------------------------------
(
X_SampleIn = max(0, min(display_w, X_0dB * splIn));
gfx_y = meterInput_y;
gfx_x = 0;
gfx_set(0,0.75,0,alphaMeterBars); // Green
gfx_rectto(X_SampleIn, meterInput_y+meter_h);
// Clipping marker -----
bufferClipInput[channel] ?
(
gfx_y = meterInput_y;
gfx_x = X_Over;
gfx_set(1,0,0,alphaMeterClip); // Red
gfx_rectto(gfx_w, meterInput_y+meter_h);
);
);
//
(splKey > dB_Neg48) ? // SIDECHAIN METER -----------------------------------
(
X_SampleKey = max(0, min(display_w, X_0dB * splKey));
gfx_y = meterKey_y;
gfx_x = 0;
gfx_set(1,0.8,0,alphaMeterBars); // Orange Yellow
gfx_rectto(X_SampleKey, meterKey_y+meter_h);
// Clipping marker -----
bufferClipKey[channel] ?
(
gfx_y = meterKey_y;
gfx_x = X_Over;
gfx_set(1,0,0,alphaMeterClip); // Red
gfx_rectto(gfx_w, meterKey_y+meter_h);
);
);
//
(splGR < 1) ? // GR METER ----------------------------
(
X_SampleGR = max(0, min(display_w, X_0dB * splGR));
gfx_y = meterGR_y;
gfx_x = X_SampleGR;
gfx_set(1,0.5,0,alphaMeterBars); // Purple
gfx_rectto(display_w+1, meterGR_y+meter_h);
// GR doesn't need clipping marker...
);
//
(splOut > dB_Neg48) ? // OUTPUT METER --------------------------------------
(
X_SampleOut = max(0, min(display_w, X_0dB * splOut));
gfx_y = meterOut_y;
gfx_x = 0;
gfx_set(0,0.7,1,alphaMeterBars); // Blue
gfx_rectto(X_SampleOut, meterOut_y+meter_h);
// Clipping marker -----
bufferClipOutput[channel] ?
(
gfx_y = meterOut_y;
gfx_x = X_Over;
gfx_set(1,0,0,alphaMeterClip); // Red
gfx_rectto(gfx_w, meterOut_y+meter_h);
);
);
// METER BAR LABELS --------------------------------------------------------
//
// Get dimensions
gfx_measurestr("XX", lbl_w, lbl_h);
lbl_offset_y = half_lane_h - (lbl_h / 2);
//
// INPUT LABEL ---------------------
gfx_y = meterInput_y + lbl_offset_y;
gfx_x = 12;
gfx_set(0,0,0,alphaMeterText); // Transparent black
gfx_drawstr("Input ");
gfx_drawstr(channelNames[channel]);
//
// KEY LABEL -----------------------
gfx_y = meterKey_y + lbl_offset_y;
gfx_x = 12;
gfx_set(0,0,0,alphaMeterText); // Transparent black
gfx_drawstr("Key ");
gfx_drawstr(channelNames[channel + (midSide * 2)]);
//
// G-R LABEL -----------------------
gfx_y = meterGR_y + lbl_offset_y;
gfx_x = 12;
gfx_set(0.5,0.5,0.5,alphaMeterText); // Transparent gray
gfx_drawstr("GR ");
gfx_drawstr(channelNames[channel + (midSide * 2)]);
//
// OUTPUT LABEL --------------------
gfx_y = meterOut_y + lbl_offset_y;
gfx_x = 12;
gfx_set(0,0,0,alphaMeterText); // Transparent black
gfx_drawstr("Output ");
gfx_drawstr(channelNames[channel]);
// Advance to next channel
channel += 1;
);
@sample // ---------------------------------------------------------------------
// Safety mechanism to force @gfx recalculation (once) if UI size was changed
(gfx_w != lastWidth) || (gfx_h != lastHeight) ?
(
lastWidth = -1;
lastHeight = -1;
);
// Store the dry input samples here for later dry/wet mixing
dryL = spl0;
dryR = spl1;
// Store dry input samples (absolute) in buffer for GUI evaluation
bufferInput[L] = max(bufferInput[L]*falloff, abs(spl0));
bufferInput[R] = max(bufferInput[R]*falloff, abs(spl1));
//
// Check if the input samples are clipping and store into a buffer
bufferClipInput[L] = max(bufferClipInput[L]*falloff, (bufferInput[L]>1));
bufferClipInput[R] = max(bufferClipInput[R]*falloff, (bufferInput[R]>1));
// Input gain. Branching is slow, so it's faster to
// just do this multiplication instead of running
// a check to see if it's needed, i.e. dBGain != 0
//
spl0 *= gainIn;
spl1 *= gainIn;
// If Mid/Side mode is set, convert L+R samples to M+S.
// This will turn the left sample into the mid sample,
// and the right sample into the side sample.
(midSide == 1) ?
(
// Can mis-use these as buffers here, since they're
// not yet required by other parts of the process.
keyL = lrToM(spl0, spl1);
keyR = lrToS(spl0, spl1);
spl0 = keyL;
spl1 = keyR;
);
// Use channel 1+2 inputs if internal sidechain is
// selected, otherwise use channel 3+4 input samples
// for external sidechain. When using external input,
// apply input gain to the key signal too.
//
scExternal = (sidechain == 1);
keyL = !scExternal * spl0 + scExternal * (spl2 * gainIn);
keyR = !scExternal * spl1 + scExternal * (spl3 * gainIn);
// Add sample value offsets to make the compression
// more unstable, this is like circuit noise.
//
(instability > 0) ?
(
// If the input samples carry a signal, add noise.
// If they don't, then add no noise.
spl0 += (spl0 != 0) * variation(0);
spl1 += (spl1 != 0) * variation(1);
// If the key samples carry a signal, add noise
// If they don't, then add no noise.
keyL += (keyL != 0) * variation(2);
keyR += (keyR != 0) * variation(3);
);
// Filter sidechain samples (if filter cutoff > 20 Hz)
//
(scFreq > 20) ?
(
keyL = filterL.eqTick(keyL);
keyR = filterR.eqTick(keyR);
);
// Make key signal absolute from this point on
keyL = abs(keyL);
keyR = abs(keyR);
// Stereo-link the detector signal?
(lnkMix > 0) ?
(
// Take average of both key channels
linked = (abs(keyL) + abs(keyR)) * 0.5 * lnkMix;
//linked = sqrt(sqr(keyL) + sqr(keyR)) * lnkMix;
// |
// +--> Smarter method, but becomes too loud when linked
// Adjust mix volume of un-linked samples
keyL *= splMix;
keyR *= splMix;
// Add linked sample on top
keyL += linked;
keyR += linked;
);
// Write absolute values of the sidechain key samples to a buffer
bufferKey[L] = max(bufferKey[L]*falloff, abs(keyL));
bufferKey[R] = max(bufferKey[R]*falloff, abs(keyR));
//
// Check if the sidechain samples are clipping and store into a buffer
bufferClipKey[L] = max(bufferClipKey[L]*falloff, (bufferKey[L]>1));
bufferClipKey[R] = max(bufferClipKey[R]*falloff, (bufferKey[R]>1));
// Run the comp calculations on whatever mixture
// of the two input channels is left in the keys,
// then apply the resulting GR to the signal.
//
spl0 *= compL.compTick(keyL);
spl1 *= compR.compTick(keyR);