Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
443 lines (378 sloc) 13.1 KB
// Consolidator
//
// The idea is simple: boost a dynamic signal into this processor at ridiculous
// amounts of gain, compensate with the makeup gain, turn the mix knob down.
//
// This is a set of 3 compressors chained in a row, each with its own character.
// The compressor thresholds are adjusted in relation to increasing input boost,
// so don't be afraid to turn up that boost. Things will become louder at first
// but don't be scared, keep pushing that input boost for some serious squash.
//
// The sidechain high pass filter is applied to every compressor stage. This is
// only a sidechain filter that processes the key signal, it will not alter the
// program material path.
//
// Stereo linking is calculated at each compressor stage independently. Linking
// L+R channels in the detector makes the stereo image more stable, but it robs
// the compressors of possible gain reduction. Rule of thumb: to squash signals
// as much as possible ("maximize"), use low or no stereo linking. To help keep
// a signal's stereo field intact, use high or full stereo linking.
//
// Makeup gain is just a simple volume adjustment. Don't get scared when things
// get loud, just bring down the makeup gain in the end.
//
// The dry/wet parameter mixes the unprocessed input signal with the output. If
// things get too squashy and flattened for your liking, just turn down the wet
// amount and mix in some of the dry signal. Instant parallel/NY compression.
//
// author: chokehold
// url: https://github.com/chkhld/jsfx/
// tags: utility gain volume trim
//
desc: Consolidator
slider1:operation=2<0, 2, {Gentle,Dynamic,Hard}>Operation
slider2:dBGain=0<0, 24, 0.01>Input boost [dB]
slider3:scFreq=75<20, 500, 1>SC high pass [Hz]
slider4:midSide=0<0,1,{Stereo (L+R),Mid/Side (M+S)}>Channel routing
slider5:linkAmount=0<0, 100, 1>Stereo link [%]
slider6:dBTrim=0<-12, 12, 0.01>Makeup gain [dB]
slider7:pctMix=100<0,100,0.01>Dry/wet mix [%]
in_pin:Input L
in_pin:Input R
out_pin:Output L
out_pin:Output R
@init
// Various convenience constants
M_LN10_20 = 8.68588963806503655302257837833210164588794011607333;
floatFloor = 0.0000000630957; // dBToGain --> ~ -144 dBfs
halfPi = $PI * 0.5;
rcpSqrt2 = 0.7071067812; // Reciprocal constant 1.0 / sqrt(2.0)
// Converts dB values to float gain factors
function dBToGain (decibels) (10.0 ^ (decibels / 20.0));
// 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);
// Accelerated 1/value calculation
function fastReciprocal (value) (sqr(invsqrt(value)));
// SIDECHAIN FILTER
//
// Simple Biquad HP filters 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 = fastReciprocal(Q);
a1 = fastReciprocal(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);
);
// 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 * fastReciprocal(msAttack * srate));
coeffRel = exp(-1000 * fastReciprocal(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));
);
// 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 = fastReciprocal(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 float factor
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
//
function compSetup (msAttack, msRelease, dBThreshold, fullRatio, dBKnee) instance (attRel, calc) local ()
(
attRel.attRelSetup(msAttack, msRelease);
calc.gainCalcSetup(dbThreshold, fullRatio, dBKnee);
);
//
function compTick (sample) instance (GR, attRel, calc) local ()
(
// Turn the key sample [-1;1] into a dBfs value and send it
// into the envelope follower.
attRel.attRelTick(gainTodB(sample));
// 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);
// 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;
);
@slider
// Calculate amount of stereo-linking in the key signals
lnkMix = linkAmount * 0.01;
splMix = 1.0 - lnkMix;
// Configure the sidechain high-pass filters
filter1L.eqHP(scFreq, 1.5);
filter1R.eqHP(scFreq, 1.5);
filter2L.eqHP(scFreq, 1.5);
filter2R.eqHP(scFreq, 1.5);
filter3L.eqHP(scFreq, 1.5);
filter3R.eqHP(scFreq, 1.5);
// Turn input/output dB gain values into float factors
gainIn = dBToGain(dBGain);
gainOut = dBToGain(dBTrim);
// The amount of dry and processed signal to blend
wetDry = pctMix * 0.01;
dryWet = 1.0 - wetDry;
// Operation: Gentle
operation == 0 ?
(
attack1 = 100;
release1 = 1000;
threshold1 = min(-(dBGain * 2),0);
ratio1 = 1.5;
knee1 = 12;
attack2 = 25;
release2 = 1000;
threshold2 = min(-(dBGain * 0.5),0);
ratio2 = 2;
knee2 = 12;
attack3 = 1;
release3 = 1000;
threshold3 = min(-(dBGain * 0.25),0);
ratio3 = 3;
knee3 = 6;
);
// Operation: Normal
operation == 1 ?
(
attack1 = 200;
release1 = 200;
threshold1 = min(-(dBGain),0);
ratio1 = 1.5;
knee1 = 6;
attack2 = 20;
release2 = 50;
threshold2 = min(-(dBGain),0);
ratio2 = 2;
knee2 = 12;
attack3 = 1;
release3 = 1000;
threshold3 = min(-(dBGain * 0.75),0);
ratio3 = 2;
knee3 = 3;
);
// Operation: Hard
operation == 2 ?
(
attack1 = 150;
release1 = 10;
threshold1 = min(-(dBGain * 2),0);
ratio1 = 2;
knee1 = 12;
attack2 = 20;
release2 = 10;
threshold2 = min(-(dBGain),0);
ratio2 = 4;
knee2 = 12;
attack3 = 1;
release3 = 500;
threshold3 = min(-(dBGain * 0.5),0);
ratio3 = 8;
knee3 = 3;
);
// Set up stage 1 compressors
comp1L.compSetup(attack1, release1, threshold1, ratio1, knee1);
comp1R.compSetup(attack1, release1, threshold1, ratio1, knee1);
// Set up stage 2 compressors
comp2L.compSetup(attack2, release2, threshold2, ratio2, knee2);
comp2R.compSetup(attack2, release2, threshold2, ratio2, knee2);
// Set up stage 3 compressors
comp3L.compSetup(attack3, release3, threshold3, ratio3, knee3);
comp3R.compSetup(attack3, release3, threshold3, ratio3, knee3);
@sample
// Storing dry samples here for later dry/wet mixing
dryL = spl0;
dryR = spl1;
// 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;
);
//
// COMPRESSOR STAGE #1
//
// Use channel 1+2 inputs as sidechain key
keyL = spl0;
keyR = spl1;
// Filter sidechain samples and make them positive.
keyL = abs(filter1L.eqTick(keyL));
keyR = abs(filter1R.eqTick(keyR));
// Stereo-link the detector signal?
lnkMix > 0 ?
(
// If stereo-linked, take average of both channels
linked = sqrt(sqr(keyL) + sqr(keyR)) * lnkMix;
// Adjust mix volume of un-linked samples
keyL *= splMix;
keyR *= splMix;
// Add linked samples on top
keyL += linked;
keyR += linked;
);
// Run the comp calculations and apply gain reduction
spl0 *= comp1L.compTick(keyL);
spl1 *= comp1R.compTick(keyR);
//
// COMPRESSOR STAGE #2
//
// Use channel 1+2 inputs as sidechain key
keyL = spl0;
keyR = spl1;
// Filter sidechain samples and make them positive.
keyL = abs(filter2L.eqTick(keyL));
keyR = abs(filter2R.eqTick(keyR));
// Stereo-link the detector signal?
lnkMix > 0 ?
(
// If stereo-linked, take average of both channels
linked = sqrt(sqr(keyL) + sqr(keyR)) * lnkMix;
// Adjust mix volume of un-linked samples
keyL *= splMix;
keyR *= splMix;
// Add linked samples on top
keyL += linked;
keyR += linked;
);
// Run the comp calculations and apply gain reduction
spl0 *= comp2L.compTick(keyL);
spl1 *= comp2R.compTick(keyR);
//
// COMPRESSOR STAGE #3
//
// Use channel 1+2 inputs as sidechain key
keyL = spl0;
keyR = spl1;
// Filter sidechain samples and make them positive.
keyL = abs(filter3L.eqTick(keyL));
keyR = abs(filter3R.eqTick(keyR));
// Stereo-link the detector signal?
lnkMix > 0 ?
(
// If stereo-linked, take average of both channels
linked = sqrt(sqr(keyL) + sqr(keyR)) * lnkMix;
// Adjust mix volume of un-linked samples
keyL *= splMix;
keyR *= splMix;
// Add linked samples on top
keyL += linked;
keyR += linked;
);
// Run the comp calculations and apply gain reduction
spl0 *= comp3L.compTick(keyL);
spl1 *= comp3R.compTick(keyR);
// If Mid/Side processing is selected
midSide == 1 ?
(
// Can mis-use these as buffers here, since they're
// no longer required by other parts of the process.
keyL = spl0;
keyR = spl1;
// Convert the M+S samples back to L+R samples
spl0 = msToL(keyL, keyR);
spl1 = msToR(keyL, keyR);
);
// Makeup gain. Probably faster to just multiply than
// doing conditional branching.
spl0 *= gainOut;
spl1 *= gainOut;
// Dry/wet mix
//
// Mixes unprocessed and processed signals this way:
// - 0.0: 100% dry
// - 0.5: 50% dry + 50% wet
// - 1.0: 100% wet
//
wetDry < 1 ?
(
spl0 = dryWet * dryL + wetDry * spl0;
spl1 = dryWet * dryR + wetDry * spl1;
);
You can’t perform that action at this time.