-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathtrack_comp.jsfx
More file actions
1017 lines (913 loc) · 35.1 KB
/
track_comp.jsfx
File metadata and controls
1017 lines (913 loc) · 35.1 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: Track 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/track_compressor.png
about:
# Track Compressor
Flexible compressor with very low CPU footprint.
The idea was to make everything I care about in a compressor available in one plugin.
It can switch between internal and external key/"sidechain" signals that go into the detector.
There are stereo setups as well as mono modes, two of them are designed to receive the actual
signal on one channel and the key signal on the opposite channel.
The detector mode can be faded between feed-forward (modern) and feed-back (classic). FF at 0%
will calculate a gain reduction factor for each sample independently. FB at 100% lets previous
GR from the former sample/s flow into new samples' GR. This makes the overall behaviour of the
compressor more stable and predictable, but also slower to respond to fast/large changes.
There'e an RMS averager that will calculate the average sample value over a settable window of
time. Setting it to 0 ms window will make the compressor respond to each peak sample directly.
Similar to feedback topology, an increased RMS window will make the behaviour more predictable
and stable, but a smaller/no RMS window catches faster transients and attenuates more quickly.
The "knee" refers to the point at which the compressor kicks in and starts compressing. "Hard
knee" means zero compression happens up to a certain point, and as soon as that point is hit,
the compression immediately kicks in at full amount. "Soft knee" means the ratio of compression
will start already below the set threshold at a low ratio amount, and increases gradually above
the cutoff point to reach its full ratio. This makes the transfer curve of the compression look
like a round curve rather than a hard fold, hence "soft" knee.
A hard knee can be considered modern, digital, maybe a bit brutish, since it does nothing until
required, and then suddenly kicks in at full ratio. A soft knee can be considered maybe more
analog and vintage, due to the variable ratio and the slightly (perceived) imprecise triggering
that already starts below the set threshold mark.
The Range parameter controls the maximum amount of gain reduction to apply. If this is set to
-40 dB, the range parameter is considered "off" and the full Gain Reduction range of up to -400
dB of Gain Reduction will be applied if need be. If this is set lower than -40 dB then only the
specified gain reduction amount between 0 and -40 dB will be applied.
Stereo linking uses the averaged value of both sidechain (internal or external) channels for
reference. At 0% there is no stereo linking, which means the compressor operates in dual mono
mode, this is good to get two independent signals levelled. At 100%, there is total linking,
which means that both key signals will evenly feed into the detector, so the signal will be
compressed equally on both channels. This is good for things like grouped drums with several
kit elements spanning the entire stereo field.
There's a sidechain HP filter that will cut the lows off the detector signal, this is useful
to make the compressor respond less to bass-heavy signal parts like kick drums or distorted
guitar palm mutes and helps reduce pumping. If it's set to 20 Hz, it's in bypass. To have it
filter the key signal, turn it up to somewhere above 20 Hz.
Automatic Makeup Gain compensation will factor in an amount of post-compression gain increase.
The intention of this is to bring the volume of a signal that was attenuated with compression
back up to somewhere around where it used to be. This is done with a static formula, so there
can be settings where it results in output either a lot louder or well quieter than the input
signal was, so it is not reliable, be careful. (This is generally the case, not just in mine.)
The saturation is dynamic and depends on the amount of gain reduction that is applied for each
compressor tick. So the more the signal is attenuated by the compressor, the higher saturation
coloration will be.
Finally, the Dry/Wet mix defines the balance of uncompressed and compressed signal. Blending
happens 100/0 - 50/50 - 0/100, so at the extreme values the signal will either be 100% dry (0%)
or 100% wet (100%). In between, there will be a mixture of both the unprocessed input and the
processed output.
// ----------------------------------------------------------------------------
in_pin:Input L
in_pin:Input R
in_pin:External SC / L
in_pin:External SC / R
out_pin:Output L
out_pin:Output R
slider1: dBGain=0<-12, 12, 0.01> Input Gain [dB]
slider2: compFeedbk=25<0, 100, 0.01> Feedback [%]
slider3: compThresh=0<-60, 0, 0.01> Threshold [dB]
slider4: compRatio=4<0.1, 20, 0.1> Ratio [x:1]
slider5: compAttack=10<0.001, 100, 0.01> Attack [ms]
slider6: compRelease=200<20, 1500, 0.01> Release [ms]
slider7: compWindow=0<0,150,0.01> RMS Window [ms]
slider8: compKnee=4.5<0,12,0.01> Hard/Soft Knee [dB]
slider9: compRange=-40<-40, 0, 0.01> Comp. Range [dB]
slider10:linkAmount=50<0, 100, 1> Stereo Link [%]
slider11:scFreq=70<20, 350, 1> SC High Pass [Hz]
slider12:autoGain=0<0,1,{Activated,Deactivated}> Auto Makeup Gain
slider13:saturation=25<0,100,0.01> Saturation [%]
slider14:dBTrim=0<-12, 12, 0.01> Output Gain [dB]
slider15:pctMix=100<0,100,0.01> Dry/Wet Mix [%]
slider16:routing=0<0, 3,{Stereo (internal SC),Stereo (external SC 3-4),Mono (L),Mono (R),Mono (signal L / key R),Mono (key L / signal R)}> Routing
// Don't need these because of UI metering
options:no_meter
@init // -----------------------------------------------------------------------
// Stop Reaper from re-initialising the plugin every time playback is reset
ext_noinit = 1;
// Various convenience constants
M_LN10_20 = 8.68588963806503655302257837833210164588794011607333;
floatFloor = 0.0000000630957; // dBToGain --> ~ -144 dBfs
halfPi = $PI * 0.5;
satSpeed = 50.0; // ms timing for saturation envelope follower
// 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;
// Converts signed dB values to normalised 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);
);
// 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;
);
// 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));
);
// 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 so many different dB values, like
// Range and Knee, I decided to not constantly convert the 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));
);
// RMS AVERAGER
//
// Turns a simple envelope follower into an RMS averager.
// This is like a brake for incoming samples, so instead of
// shooting every incoming sample directly into the envelope
// detector, this will average incoming values over a period
// of time, a so called "window". The length of the window is
// set in ms and defines over how much time the samples will
// be smoothed.
//
function rmsSetup (msWindow) instance (rmsEnv) local ()
(
rmsEnv.envSetup(msWindow);
);
//
function rmsTick (sample) instance (rmsEnv, envelope) local ()
(
envelope = rmsEnv.envTick(sample * sample);
sqrt(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. Included
// are hard/soft knee limits and ratios, as well as the
// makeup gain amount.
//
// The formula to calculate the gain compensation value was
// implemented after Tom Duffy at Tascam and Charles Hoffman
// at Black Ghost Audio.
// https://music.columbia.edu/pipermail/music-dsp/2009-September/068027.html
// https://www.blackghostaudio.com/blog/the-ultimate-guide-to-compression
//
function gainCalcSetup (dBThreshold, fullRatio, dBKnee, dBRange) instance (threshold, ratio, makeup, knee, kneeWidth, kneeUpper, kneeLower, range)
(
threshold = dBThreshold; // signed dBfs
ratio = 1/fullRatio; // 1/x --> compression < 1, expansion > 1
makeup = dBToGain(abs((threshold + (abs(threshold) * ratio)) * 0.4)); // 0.5 went too loud too quickly
knee = dBKnee;
kneeWidth = knee * 0.5;
kneeUpper = threshold + kneeWidth;
kneeLower = threshold - kneeWidth;
range = dBRange;
);
//
function gainCalcTick (dBsample) instance (ratio, knee, kneeLower, kneeUpper, threshold, range)
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));
);
// Make sure the reduction is not higher than the specified
// range in Decibels. If set to -40 dB on the UI, the value
// of the range variable will be 0 here, so the calculated
// reduction amount will "win" and be used in full instead.
dBToGain(max(dBReduction, range));
);
// 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, dBRange: signed dBfs
// dBKnee: absolute/positive dB
// pctFeedback: % of previous GR detector feedback
//
function compSetup (msAttack, msRelease, msWindow, dBThreshold, fullRatio, dBKnee, dBRange, pctFeedback, autoMakeup) instance (attRel, rms, calc, feedback, autogain)
(
attRel.attRelSetup(msAttack, msRelease);
rms.rmsSetup(msWindow);
calc.gainCalcSetup(dbThreshold, fullRatio, dBKnee, dBRange);
feedback = pctFeedback * 0.01;
autogain = autoMakeup;
);
//
function compTick (sample) instance (GR, feedback, rms, attRel, calc, autogain)
local (feedbackFactor, keyGain, keyDecibels, gainAdjust)
(
// 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;
// If an RMS window is set up, run the key sample through
// the RMS averager envelope first.
rms.rmsEnv.coeff > 0 ? keyGain = rms.rmsTick(keyGain);
// Turn the key sample [-1;1] into a dBfs value
keyDecibels = gainTodB(keyGain);
// Send the dBfs sample value into the attack/release
// envelope follower and get a new envelope state.
attRel.attRelTick(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(attRel.envelope);
// Factor in automatic gain compensation if set
gainAdjust = (autogain == 0) ? calc.makeup : 1.0;
// 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 * gainAdjust;
);
// CONVENIENCE VARIABLES -----------------------------------------------------
//
// Makes handling channel buffers more accessible
L = 0;
R = 1;
channelNames = 100;
channelNames[L] = "L";
channelNames[R] = "R";
//
// 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 // --------------------------------------------------------------------
// The maximum amount of gain reduction to apply
maxRange = (compRange == -40) ? -400 : compRange;
// Prepare two compressor instances, one for each channel.
compL.compSetup(compAttack, compRelease, compWindow, compThresh, compRatio, compKnee, maxRange, compFeedbk, autoGain);
compR.compSetup(compAttack, compRelease, compWindow, compThresh, compRatio, compKnee, maxRange, compFeedbk, autoGain);
// 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.
//
// 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]);
//
// 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]);
//
// 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;
);
// STEREO ROUTING MODES
//
(routing < 2) ?
(
// Storing dry samples here for later dry/wet mixing
dryL = spl0;
dryR = spl1;
// Store dry samples in buffer for later dry/wet mixing
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;
// Use channel 1+2 inputs if internal sidechain is
// selected, otherwise use channel 3+4 input samples
// for external sidechain.
//
scExternal = (routing == 1);
keyL = !scExternal * spl0 + scExternal * (spl2 * gainIn);
keyR = !scExternal * spl1 + scExternal * (spl3 * gainIn);
// Filter sidechain samples (if 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);
// Write the gain reduction to buffers as absolute float gain factors
bufferGR[L] = abs(compL.GR);
bufferGR[R] = abs(compR.GR);
// Send the compressor gain reduction values to the
// saturation envelope followers to slow them down.
saturationL = satL.satTick(compL.GR);
saturationR = satR.satTick(compR.GR);
)
: // MONO ROUTING MODES
(
// Mono L
// Ignores channel R input completely, only processes
// channel L and duplicates output to channel R
(routing == 2) ?
(
dryL = dryR = spl0;
sampleInput = spl0 * gainIn; // Apply input gain
sampleKey = sampleInput;
);
//
// Mono R
// Ignores channel L input completely, only processes
// channel R and duplicates output to channel L
(routing == 3) ?
(
dryL = dryR = spl1;
sampleInput = spl1 * gainIn; // Apply input gain
sampleKey = sampleInput;
);
//
// Mono L <- R
// Mono signal on channel L, mono key/sidechain on channel R
(routing == 4) ?
(
dryL = dryR = spl0;
sampleInput = spl0 * gainIn; // Apply input gain
sampleKey = spl1 * gainIn; // Apply input gain
);
//
// Mono L -> R
// Mono signal on channel R, mono key/sidechain on channel L
(routing == 5) ?
(
dryL = dryR = spl1;
sampleInput = spl1 * gainIn; // Apply input gain
sampleKey = spl0 * gainIn; // Apply input gain
);
// Store dry samples in buffer for later dry/wet mixing
bufferInput[L] = max(bufferInput[L]*falloff, abs(dryL));
bufferInput[R] = bufferInput[L];
//
// Check if the input samples are clipping and store into a buffer
bufferClipInput[L] = max(bufferClipInput[L]*falloff, (bufferInput[L]>1));
bufferClipInput[R] = bufferClipInput[L];
// Filter sidechain sample (if cutoff > 20 Hz)
//
(scFreq > 20) ?
(
sampleKey = filterL.eqTick(sampleKey);
);
// Make key signal absolute from this point on
sampleKey = abs(sampleKey);
// Write absolute values of the sidechain key samples to a buffer
bufferKey[L] = max(bufferKey[L]*falloff, abs(sampleKey));
bufferKey[R] = bufferKey[L];
//
// Check if the sidechain samples are clipping and store into a buffer
bufferClipKey[L] = max(bufferClipKey[L]*falloff, (bufferKey[L]>1));
bufferClipKey[R] = bufferClipKey[L];
// 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.
sampleInput *= compL.compTick(sampleKey);
// Write the gain reduction to buffers as absolute float gain factors
bufferGR[L] = abs(compL.GR);
bufferGR[R] = bufferGR[L];
// Update the output samples with the current state
spl0 = spl1 = sampleInput;
// Send the compressor gain reduction values to the
// saturation envelope followers to slow them down.
saturationL = saturationR = satL.satTick(compL.GR);
);
// Saturation stage
(saturation > 0) ?
(
// Add saturation to the samples
spl0.saturate(saturationL);
spl1.saturate(saturationR);
// Run the DC blocker on each sample
spl0.dcBlocker();
spl1.dcBlocker();
);
// Output gain. Probably faster to just multiply
// rather than using conditional branching. The
// automatic gain compensation has already been
// handled and factored in earlier by compTick().
spl0 *= gainOut;
spl1 *= gainOut;
// Dry/wet mix
//
// Mixes unprocessed and processed signals this way:
// - 0.0: 100% dry