-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathgate_expander.jsfx
323 lines (272 loc) · 11.6 KB
/
gate_expander.jsfx
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
desc: Gate/Expander
version: 1.8.2
author: chokehold
tags: processing gate expander dynamics gain
link: https://github.com/chkhld/jsfx/
screenshot: https://github.com/chkhld/jsfx/blob/main/assets/screenshots/gate_expander.png
about:
# Gate/Expander
A combined noise gate and expander. Both are actually quite
the same, with the main difference that when a gate closes,
it closes all the way to silence - but expanders only close
down to certain level. A gate is like an extreme flavour of
expander, and an expander is like a soft type of nosie gate.
Why do you need both, and when would you use which? Simple.
Things like distorted electric guitars may need to get rid
of some hissing, and having any remainder of the hiss still
in your signal won't be of any profit, so it's a clear vote
for the gate that fades all the way to silence.
Things like acoustic drum kits live and breathe through the
combination of all their various microphone recordings. All
of those microphones will have recorded "bleed" from other
kit pieces in the room, e.g. there will be cymbal noise on
the kick and vice versa. Rigorously cutting away everything
from every track that wasn't intended to be ther will make
the combined mix of those tracks sound artificial and void
of any life, and it will throw your mix out of balance. It
just doesn't feel natural if everything is super clean and
trimmed, and suddenly there's a hit on the tom that sets a
few gates off that shouldn't have been set off, and right
then is when your entire panorama mix and equalization are
done for. So in such cases, it may be more desirable to not
let the attenuation happen down to full silence, but rather
only for a limited range. So if many microphones are active
at the same time, your mix won't fall out of proportion.
Needless to say: the Exp. Range parameter will only work if
the plugin is in expander mode, it has no effect in a gate.
The Hysteresis is something like a little grace period when
the gate/expander is about to close. Instead of stubbornly
insisting on shutting the volume down becaue the signal has
just fallen below the magic threshold, the Hysteresis will
let the signal fall just a few more dB below the threshold
before the release envelope is triggered. This can be very
useful for transient-rich material which may be quite loud
at first, but then immediately loses its energy. Having a
Hysteresis set will still require the transient to go past
the defined threshold to open the gate/expander, but when
the signal falls again, the gate/expander will stay open a
little bit longer to let non-transient signal also pass.
A quick word on the channel configurations:
- Stereo (internal SC)
Regular stereo/dual mono routing, the incoming audio is
also the key signal and "gates itself".
- Stereo (external SC)
Stereo/dual mono routing, but only signal coming through
plugin channels 3+4 will be used as the key source that
triggers the gate/expander envelope.
- Mono (L) amd Mono (R)
Will route the selected input channel to all outputs,
the incoming signal will be used to "gate itself".
- Mono (signal L / key R) and Mono (key L / signal R)
Similar to the previous, only the channel marked with
"signal" will be routed to both outputs, and the other
channel marked as "key" will be used as the triggering
key signal to gate it.
The stereo linking features is only active in the first two
routing options, i.e. Stereo with internal/external SC. It
uses "louder channel" priorization, so whichever channel is
louder will open/close the gate/expander for both channels.
Just for fun, I added a primitive high-pass filter into the
sidechain. This will be applied to any key input, internal
external, stereo, mono. Any side-chain input sample will be
filtered.
// ----------------------------------------------------------------------------
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:operation=0<0,1,{Gate [fade to silence],Expander [fade to range]}> Operation
slider2:gateThresh=-60<-60,0,0.1> Threshold [dB]
slider3:gateRange=-40<-40,0,0.1> Exp. Range [dB]
slider4:gateHyst=-3<-12,0,0.01> Hysteresis [dB]
slider5:gateAttack=5<1,50,0.1> Attack [ms]
slider6:gateRelease=300<50, 2500, 0.1> Release [ms]
slider7:linkAmount=0<0,100,1> Stereo Link [%]
slider8:scFreq=70<20,350,1> SC High Pass [Hz]
slider9: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
@init
halfPi = $PI * 0.5;
// Sample rate / 1000 = number of samples per millisecond
msRate = srate * 0.001;
// Global variables that will later hold the values
// the channel envelope followers will follow.
keyL = 0.0;
keyR = 0.0;
// Helper functions
function dBToGain (decibels) (10.0 ^ (decibels / 20.0));
function fastReciprocal (value) (sqr(invsqrt(value)));
// Simple envelope follower, setup stage
function envSetup (msAttack, msRelease, dBThreshold, dBHysteresis) instance (envelope, attack, release, hysteresis, threshold) local ()
(
attack = exp(-1000 * fastReciprocal(msAttack * srate));
release = exp(-1000 * fastReciprocal(msRelease * srate));
hysteresis = dBToGain(dBHysteresis);
threshold = dBToGain(dBThreshold);
);
// Simple envelope follower, processing stage
function envFollow (signal) instance (envelope, attack, release, hysteresis) local ()
(
// Conditional branching sucks, e.g. "if (x) then do (y) else if".
// Evaluating conditional branches is a slow process and can become
// taxing on the CPU if done too much. The problem lies not in the
// conditions, but in the branches. What I do here removes branches
// and just turns an if/else statement into conditions that evaluate
// to true/false - or 1/0 - and therefor can be used as factors in a
// simple row of additions and multiplications. It leads to the same
// result, but it's faster in per-sample processes.
//
// Take the first example:
//
// ((signal > envelope) * attack)
// + ((signal <= envelope) * release))
//
// This is a simple addition of two sides, the left one and the right.
// If "signal > envelope" is true, it evaluates to 1. If that happens,
// the "signal <= envelope" will be false and evaluate to 0. This will
// simplify the above calculation like so:
//
// ((1) * attack) + ((0) * release) --> attack + 0
//
// If it were the other way round, and "signal <= envelope" were true,
// then that would make "signal > envelope" false and invert it all.
//
// ((0) * attack) + ((1) * release) --> 0 + release
//
// The result is exactly what conditional branching would have given,
// but without doing any actual branching. :)
//
// Either way, this next bit is "if input louder than envelope, start
// attack stage - else if input quieter than envelope, start release
// stage.
envelope = (((signal > envelope) * attack) + ((signal <= envelope) * release)) * (envelope - signal) + signal;
envelope;
);
// Gate configuration function
function gateSetup (msAttack, msRelease, dBThreshold, dBRange, dBHysteresis)
(
this.envSetup(msAttack, msRelease, dBThreshold, dBHysteresis);
// Difference between gate and expander: if Gate mode is selected, the
// "final volume level" for the low end of the envelope will be zero,
// that is absolute silence. But when Expander mode is selected, then
// the actual range parameter from the UI will be used here.
this.range = (operation == 1) ? dBToGain(dBRange) : 0;
);
// This is the gate function that processes every incoming sample
function tickGate (sample) instance (threshold, gain, envelope, range, hysteresis) local ()
(
// Make the incoming sample all-positive, because the further used gain
// factors like threshold and hysteresis are also positive values. Use
// it to check if the sample is above or below threshold (+hysteresis)
// and to decide whether to let the envelope rise to 1 or fall to range/0.
this.envFollow((abs(sample) < (threshold * hysteresis)) ? range : 1);
// After the envelope value was calculated, make sure the gain reduction
// does not surpass the "maximum reduction" range parameter. If Gate mode
// is active, the range parameter will be 0 i.e. total silence.
gain = max(envelope, range);
gain;
);
// Sidechain high-pass filter processing function
//
// Implemented after Andy Simper's (Cytomic) Biquad Paper. If you want to
// know what these do and how and why, go find his PDF (it's everywhere on
// the Web) and read it. :)
//
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);
);
// Sidechain high-pass filter coefficient calculation
function eqHP (Hz, Q) instance (a1, a2, a3, m0, m1, m2) local (g, k)
(
g = tan(halfPi * (Hz / srate)); k = 1.0 / Q;
a1 = 1.0 / (1.0 + g * (g + k)); a2 = a1 * g; a3 = a2 * g;
m0 = 1.0; m1 = -k; m2 = -1.0;
);
@slider
// Just passing UI values into the configuration method
gateL.gateSetup(gateAttack, gateRelease, gateThresh, gateRange, gateHyst);
gateR.gateSetup(gateAttack, gateRelease, gateThresh, gateRange, gateHyst);
// Calculates the amount of stereo-linked signal used in the key signal.
lnkMix = linkAmount * 0.01;
splMix = 1.0 - lnkMix;
// Make the sidechain high-pass filters
filterL.eqHP(scFreq, 1.5);
filterR.eqHP(scFreq, 1.5);
@block
@sample
// Stereo int SC
routing == 0 ?
(
// Store filtered sidechain samples
keyL = filterL.eqTick(spl0);
keyR = filterR.eqTick(spl1);
// Link louder
(lnkMix > 0) ?
(
// If stereo-linked, pick louder channel
linked = max(keyL, keyR) * lnkMix;
keyL *= splMix;
keyR *= splMix;
keyL += linked;
keyR += linked;
);
// Run the gate calculations on whatever mixture
// of the two input channels is left in the keys.
spl0 *= gateL.tickGate(keyL);
spl1 *= gateR.tickGate(keyR);
);
// Stereo ext SC
(routing == 1) ?
(
// Store filtered sidechain samples
keyL = filterL.eqTick(spl2);
keyR = filterR.eqTick(spl3);
// Link louder
lnkMix > 0 ?
(
// If stereo-linked, pick louder channel
linked = max(keyL, keyR) * lnkMix;
keyL *= splMix;
keyR *= splMix;
keyL += linked;
keyR += linked;
);
// Run the gate calculations on whatever mixture
// of the two input channels is left in the keys.
spl0 *= gateL.tickGate(keyL);
spl1 *= gateR.tickGate(keyR);
);
// Mono L
routing == 2 ?
(
// Process L channel and pass its result
// through to R channel
spl1 = spl0 *= gateL.tickGate(filterL.eqTick(spl0));
);
// Mono R
routing == 3 ?
(
// Process R channel and pass its result
// through to L channel
spl0 = spl1 *= gateR.tickGate(filterR.eqTick(spl1));
);
// Mono L <- R
routing == 4 ?
(
// Process L channel using R channel as key.
// Duplicate result into channel R.
spl1 = spl0 *= gateL.tickGate(filterL.eqTick(spl1));
);
// Mono L -> R
routing == 5 ?
(
// Process R channel using R channel as key.
// Duplicate result into channel L.
spl0 = spl1 *= gateR.tickGate(filterR.eqTick(spl0));
);