Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
322 lines (283 sloc) 11.6 KB
// 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.
//
// author: chokehold
// url: https://github.com/chkhld/jsfx/
// tags: processing gate expander dynamics
//
desc: Gate/Expander
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));
);
You can’t perform that action at this time.