Compander fix?

Hello community,

I can ear that Compander Ugen isn’t sound very good.
according to Nathan Ho “It downsamples the gain control signal to control rate to save CPU usage, and although it does linear interpolation, the aliasing artifacts are pretty audible to me at a standard block size of 64, and very audible if I try to set fast attack and release.”

I vaguely remember a user who shared a link, here on this forum, to an improved version of Compander.
Do you know where to find it ?
I’ve looked everywhere without success.

Thank you

The only thing I can think of off the top of my head is this:

DCompressor.

Lately I’ve been using ZL Compressor with VSTPlugin.

hjh

What is the performance like for VSTs - compared to built in UGens (such as Compander)?

Thank you @cian and @jamshark70 for your answers.

I already tried DCompressor but I’m not really convince.
I’m looking for something light to use in tidal.
VSTplugin isn’t an option for my case.

I think I will try to export compressor_stereo from faust.

Well DCompressor is mono, so if you’re looking for a stereo compressor then the Faust one would certainly be more suitable.

But if you’re looking for something that will shape the sound (e.g. for drum kicks), then you’re probably looking for something different. Both of those compressors are intended to be characterless. They’re also both hard knee compressors. There’s also a lookahead in Faust that can be useful (I use it, when I remember, at the end of my signal chain as a master bus compressor).

If you know what you’re after it’s also fairly easy to build your own compressor in supercollider using envelope follower. I think there was a post on here that showed how.

From my post on negative compression, here is a textbook hard-knee feedforward compressor.

(
var compGain;
compGain = { |in, threshold, slope, attack = 0.05, release = 0.3|
    var inArray, amplitude, timeConstantToRT60;
    inArray = if(in.isArray) { in } { [in] };
    timeConstantToRT60 = log(-60.dbamp) * -1;
    amplitude = (inArray.abs.lagud(attack * timeConstantToRT60, release * timeConstantToRT60).sum / inArray.size.sqrt).max(-100.dbamp).ampdb;
    ((amplitude - threshold).max(0) * (slope - 1)).lag(attack * timeConstantToRT60).dbamp;
};

{
    var snd;
    snd = SinOsc.ar(440) * Env.perc(0.01, 1.0).ar;
    snd = snd * compGain.(snd, threshold: -10, slope: 1 / 4, attack: 0.1, release: 0.3);
    (snd * -6.dbamp) ! 2;
}.play(fadeTime: 0);
)

Usage notes:

  • Slope is the reciprocal of ratio, so slope: 1 / 4 is a 4:1 compressor. Slope 0 is infinity:1 ratio. Negative slopes allow negative compression (non-monotonic gain control).
  • .lag(attack) at the end of the gain signal is optional but makes it a little smoother. I’ve seen analog compressors that do something like that.
  • For sidechaining plug a different signal into the first arg of compGain.
  • Pre-EQ on the first arg of compGain allows you to control frequency selectivity. Either way definitely LeakDC the signal before.
  • For stereo link compression pass in a stereo signal to the first arg of compGain. The output will be mono signal. For “dual mono” compression run compGain separately on each channel.
  • “Attack” and “release” are the “time constants” given in seconds. Time constant is defined as the time for exponential decay to decay to 1/e. However lagud and lag are defined using RT60, so I threw in a unit conversion. That step is optional if you prefer to work in RT60.

Overall have fun with it, it’s not a black box, try inserting other filters or nonlinear functions in there. There are many ways to implement a compressor. One example would be “program dependence” which is a marketing term for changing the release time so that it’s faster for quieter signals.

EDIT: I meant to use lagud, not Amplitude here.

3 Likes

Is it ok like this ?

SynthDef("nathanHoCompressor", { |out, nCompslope=0.25, nCompthreshold=(-10), nCompattack=0.1, nComprelease=4, nCompstereomode=0|
	var signal, compGain, stereolink, dualmono;
	signal = In.ar(out, 2);

	compGain = { |in, threshold, slope, attack = 0.05, release = 0.3|
		var inArray, amplitude, tc;
		tc = log(-60.dbamp) * -1;
		inArray = if(in.isArray) { in } { [in] };
		amplitude = (inArray.abs.lagud(attack * tc, release * tc).sum / inArray.size.sqrt).max(-100.dbamp).ampdb;
		((amplitude - threshold).max(0) * (slope - 1)).lag(attack).dbamp;
	};

	stereolink = signal * compGain.(in: LeakDC.ar(signal), threshold: nCompthreshold, slope: nCompslope, attack: nCompattack, release: nComprelease);

	dualmono = signal.collect({ |channel|
		channel * compGain.(in: LeakDC.ar(channel), threshold: nCompthreshold, slope: nCompslope, attack: nCompattack, release: nComprelease);
	});

	signal = Select.ar(nCompstereomode.clip(0, 1), [dualmono, stereolink]);

	ReplaceOut.ar(out, signal);
}, [\ir]).add;

If it sounds good, it is good.

As far as code goes, I wouldn’t bother making dual-mono vs. stereo-link switchable, I’d just go with one or the other depending on what sounds better to you.

Also I think .lagud is based on RT60 (time to decay to -60 dB) so the attack/release units might be a little different from what you’re used to with other compressors which are mostly based on time constant (time to decay to 1/e, conventional in electrical engineering), so prior to plugging them into lagud I think you’d want to multiply both attack and release by log(-60.dbamp) * -1 or about 6.9. It’s optional but it will probably have better parity with the settings on commercial compressors. I’m gonna update my post real quick.

Thank you, I update my post too

It all depends on the plugin. VST compressors will of course cost more CPU than a UGen, but it’s worth it for the visual feedback too. (Also, in a DAW, I wouldn’t hesitate to have 10-12 compressors covering drum tracks and 2-4 compressors on lead vocals.)

In a quick measure, I got 20-30 instances of ZL Compressor to reach 20% CPU, and 150 instances of Compander. But ZL sounds better and is easier to tune.

A small detail is that Amplitude actually specifies RT20 attack and decay convergence times – unit->m_clampcoef = clamp == 0.0 ? 0.0 : exp(log1 / (clamp * SAMPLERATE)); where log1 = log(0.1) = log(-20.dbamp). Not sure how much difference that makes to this compressor though.

hjh

Thank you for the info.

What would be the math for a correct conversion in this regard ?
rt20_to_rt60 = ???

It would just be:

TimeConstantToRT20 = log(-20.dbamp) * -1;

But given that log(-20.dbamp) is just 0.1, you can get a tiny bit more efficiency with:

TimeConstantToRT20 = log(0.1) * -1;

I think your math work to convert time constant to RT20 but IIUC according to @jamshark70 here we want to convert RT20 to RT60.
@nathan what do you think ?

I might be misunderstanding, as I didn’t take the time to look closely, but I think James was pointing out that Amplitude kicks in at -20 dbs, rather than -60. So I think you’d only need to change that number. -20 also seems more reasonable (-60 db is basically silence).