Multiband Compressor?

Hey,

has anyone made a multiband compressor in SC? I’d be keen to use one but I don’t have anywhere near the DSP chops to make one myself.

Cheers,
Jordan

I’m not aware if someone implemented a class for this, but it would not be difficult to write an effect SynthDef that splits the input signal with UGens from the BandSplitter quark and applies Compander to each band with desired compressor settings.
However, as so many multiband compressors are out there, I don’t know if it makes sense, except you are wanting to do something very special.

… and, of course, there’s Christof Ressi’s VSTPlugin as a linkage option …

Thanks so much for such a helpful response!

Just came across this old thread. Related:

I -sort of- recreated the Soundgoodizer from FL Studio, which is basically a multiband compression preset:

1 Like

still needs polishing:

it’s actually stereo, but I forgot to change some names. BPF have 1/3 octave bandwidths, frequencies spaced 1/3 octave apart. too many to tune by hand, interesting to automate it somehow, or group them (3 or 4 together) if you want something ‘classical’

import("effect.lib");
import("filter.lib");
import("music.lib");

not(x) = abs(x - 1);
mute = not(checkbox("[0] mute [tooltip: Mute the whole band]"));
bypass(switch, block) = select2(switch, _, block);
bswitch = checkbox("[0] bypass [tooltip: Bypass the compressor, but not the bandpass filter]");

ratio = hslider("[9] Ratio [tooltip: Compression ratio]", 2, 1, 100, 1);
attack = hslider("[A] Attack (sec) [tooltip: Time before the compressor starts to kick in]", 0.012, 0, 1, 0.001);
release = hslider("[B] Release (sec) [tooltip: Time before the compressor releases the sound]", 1.25, 0, 10, 0.01);
safe = hslider("[6] Makeup-Threshold (db) [tooltip: Threshold correction, an anticlip measure]", 2, 0, 10, 0.1);

// Makeup gain sliders
makeup1 = hslider("[5] Makeup1 (db) [tooltip: Post amplification and threshold]", 13, -50, 50, 0.1);
makeup2 = hslider("[5] Makeup2 (db) [tooltip: Post amplification and threshold]", 10, -50, 50, 0.1);
makeup3 = hslider("[5] Makeup3 (db) [tooltip: Post amplification and threshold]", 4, -50, 50, 0.1);
makeup4 = hslider("[5] Makeup4 (db) [tooltip: Post amplification and threshold]", 8, -50, 50, 0.1);
makeup5 = hslider("[5] Makeup5 (db) [tooltip: Post amplification and threshold]", 11, -50, 50, 0.1);
makeup6 = hslider("[5] Makeup6 (db) [tooltip: Post amplification and threshold]", 14, -50, 50, 0.1);
makeup7 = hslider("[5] Makeup7 (db) [tooltip: Post amplification and threshold]", 16, -50, 50, 0.1);
makeup8 = hslider("[5] Makeup8 (db) [tooltip: Post amplification and threshold]", 18, -50, 50, 0.1);
makeup9 = hslider("[5] Makeup9 (db) [tooltip: Post amplification and threshold]", 20, -50, 50, 0.1);
makeup10 = hslider("[5] Makeup10 (db) [tooltip: Post amplification and threshold]", 22, -50, 50, 0.1);
makeup11 = hslider("[5] Makeup11 (db) [tooltip: Post amplification and threshold]", 24, -50, 50, 0.1);
makeup12 = hslider("[5] Makeup12 (db) [tooltip: Post amplification and threshold]", 26, -50, 50, 0.1);
makeup13 = hslider("[5] Makeup13 (db) [tooltip: Post amplification and threshold]", 28, -50, 50, 0.1);
makeup14 = hslider("[5] Makeup14 (db) [tooltip: Post amplification and threshold]", 30, -50, 50, 0.1);
makeup15 = hslider("[5] Makeup15 (db) [tooltip: Post amplification and threshold]", 32, -50, 50, 0.1);
makeup16 = hslider("[5] Makeup16 (db) [tooltip: Post amplification and threshold]", 34, -50, 50, 0.1);
makeup17 = hslider("[5] Makeup17 (db) [tooltip: Post amplification and threshold]", 36, -50, 50, 0.1);
makeup18 = hslider("[5] Makeup18 (db) [tooltip: Post amplification and threshold]", 38, -50, 50, 0.1);
makeup19 = hslider("[5] Makeup19 (db) [tooltip: Post amplification and threshold]", 40, -50, 50, 0.1);
makeup20 = hslider("[5] Makeup20 (db) [tooltip: Post amplification and threshold]", 42, -50, 50, 0.1);
makeup21 = hslider("[5] Makeup21 (db) [tooltip: Post amplification and threshold]", 44, -50, 50, 0.1);
makeup22 = hslider("[5] Makeup22 (db) [tooltip: Post amplification and threshold]", 46, -50, 50, 0.1);
makeup23 = hslider("[5] Makeup23 (db) [tooltip: Post amplification and threshold]", 48, -50, 50, 0.1);
makeup24 = hslider("[5] Makeup24 (db) [tooltip: Post amplification and threshold]", 50, -50, 50, 0.1);
makeup25 = hslider("[5] Makeup25 (db) [tooltip: Post amplification and threshold]", 52, -50, 60, 0.1);
makeup26 = hslider("[5] Makeup26 (db) [tooltip: Post amplification and threshold]", 54, -50, 60, 0.1);
makeup27 = hslider("[5] Makeup27 (db) [tooltip: Post amplification and threshold]", 56, -50, 60, 0.1);
makeup28 = hslider("[5] Makeup28 (db) [tooltip: Post amplification and threshold]", 58, -50, 60, 0.1);


// Define bandpass filters based on center frequencies and bandwidths
bandpass1 =  par(i, 2, bandpass(3, 25 - 5.8/2, 25 + 5.8/2));
bandpass2 =  par(i, 2, bandpass(3, 31.5 - 7.3/2, 31.5 + 7.3/2));
bandpass3 =  par(i, 2, bandpass(3, 40 - 9.2/2, 40 + 9.2/2));
bandpass4 =  par(i, 2, bandpass(3, 50 - 11.6/2, 50 + 11.6/2));
bandpass5 =  par(i, 2, bandpass(3, 63 - 14.5/2, 63 + 14.5/2));
bandpass6 =  par(i, 2, bandpass(3, 80 - 18.3/2, 80 + 18.3/2));
bandpass7 =  par(i, 2, bandpass(3, 100 - 23/2, 100 + 23/2));
bandpass8 =  par(i, 2, bandpass(3, 125 - 29/2, 125 + 29/2));
bandpass9 =  par(i, 2, bandpass(3, 160 - 37/2, 160 + 37/2));
bandpass10 = par(i, 2, bandpass(3, 200 - 46/2, 200 + 46/2));
bandpass11 = par(i, 2, bandpass(3, 250 - 58/2, 250 + 58/2));
bandpass12 = par(i, 2, bandpass(3, 315 - 73/2, 315 + 73/2));
bandpass13 = par(i, 2, bandpass(3, 400 - 92/2, 400 + 92/2));
bandpass14 = par(i, 2, bandpass(3, 500 - 116/2, 500 + 116/2));
bandpass15 = par(i, 2, bandpass(3, 630 - 145/2, 630 + 145/2));
bandpass16 = par(i, 2, bandpass(3, 800 - 183/2, 800 + 183/2));
bandpass17 = par(i, 2, bandpass(3, 1000 - 230/2, 1000 + 230/2));
bandpass18 = par(i, 2, bandpass(3, 1250 - 290/2, 1250 + 290/2));
bandpass19 = par(i, 2, bandpass(3, 1600 - 370/2, 1600 + 370/2));
bandpass20 = par(i, 2, bandpass(3, 2000 - 460/2, 2000 + 460/2));
bandpass21 = par(i, 2, bandpass(3, 2500 - 580/2, 2500 + 580/2));
bandpass22 = par(i, 2, bandpass(3, 3150 - 730/2, 3150 + 730/2));
bandpass23 = par(i, 2, bandpass(3, 4000 - 920/2, 4000 + 920/2));
bandpass24 = par(i, 2, bandpass(3, 5000 - 1160/2, 5000 + 1160/2));
bandpass25 = par(i, 2, bandpass(3, 6300 - 1450/2, 6300 + 1450/2));
bandpass26 = par(i, 2, bandpass(3, 8000 - 1830/2, 8000 + 1830/2));
bandpass27 = par(i, 2, bandpass(3, 10000 - 2300/2, 10000 + 2300/2));
bandpass28 = par(i, 2, bandpass(3, 12500 - 2900/2, 12500 + 2900/2));

// Makeup gains with smoothing and muting
makeupGain(bswitch, mute, makeup, safe) = mute * (not(bswitch) * (makeup - safe) : db2linear : smooth(0.999));

Makeup1 = makeupGain(bswitch, mute, makeup1, safe);
Makeup2 = makeupGain(bswitch, mute, makeup2, safe);
Makeup3 = makeupGain(bswitch, mute, makeup3, safe);
Makeup4 = makeupGain(bswitch, mute, makeup4, safe);
Makeup5 = makeupGain(bswitch, mute, makeup5, safe);
Makeup6 = makeupGain(bswitch, mute, makeup6, safe);
Makeup7 = makeupGain(bswitch, mute, makeup7, safe);
Makeup8 = makeupGain(bswitch, mute, makeup8, safe);
Makeup9 = makeupGain(bswitch, mute, makeup9, safe);
Makeup10 = makeupGain(bswitch, mute, makeup10, safe);
Makeup11 = makeupGain(bswitch, mute, makeup11, safe);
Makeup12 = makeupGain(bswitch, mute, makeup12, safe);
Makeup13 = makeupGain(bswitch, mute, makeup13, safe);
Makeup14 = makeupGain(bswitch, mute, makeup14, safe);
Makeup15 = makeupGain(bswitch, mute, makeup15, safe);
Makeup16 = makeupGain(bswitch, mute, makeup16, safe);
Makeup17 = makeupGain(bswitch, mute, makeup17, safe);
Makeup18 = makeupGain(bswitch, mute, makeup18, safe);
Makeup19 = makeupGain(bswitch, mute, makeup19, safe);
Makeup20 = makeupGain(bswitch, mute, makeup20, safe);
Makeup21 = makeupGain(bswitch, mute, makeup21, safe);
Makeup22 = makeupGain(bswitch, mute, makeup22, safe);
Makeup23 = makeupGain(bswitch, mute, makeup23, safe);
Makeup24 = makeupGain(bswitch, mute, makeup24, safe);
Makeup25 = makeupGain(bswitch, mute, makeup25, safe);
Makeup26 = makeupGain(bswitch, mute, makeup26, safe);
Makeup27 = makeupGain(bswitch, mute, makeup27, safe);
Makeup28 = makeupGain(bswitch, mute, makeup28, safe);

compressorMono(bandpass, makeup, push) = bandpass : bypass(bswitch, compressor_mono(ratio, -push, attack, release)) : *(makeup);

// Mono processing
gcomp1 = vgroup("[1] C1", compressorMono(bandpass1, Makeup1, makeup1));
gcomp2 = vgroup("[1] C2", compressorMono(bandpass2, Makeup2, makeup2));
gcomp3 = vgroup("[1] C3", compressorMono(bandpass3, Makeup3, makeup3));
gcomp4 = vgroup("[1] C4", compressorMono(bandpass4, Makeup4, makeup4));
gcomp5 = vgroup("[1] C5", compressorMono(bandpass5, Makeup5, makeup5));
gcomp6 = vgroup("[1] C6", compressorMono(bandpass6, Makeup6, makeup6));
gcomp7 = vgroup("[1] C7", compressorMono(bandpass7, Makeup7, makeup7));
gcomp8 = vgroup("[1] C8", compressorMono(bandpass8, Makeup8, makeup8));
gcomp9 = vgroup("[1] C9", compressorMono(bandpass9, Makeup9, makeup9));
gcomp10 = vgroup("[1] C10", compressorMono(bandpass10, Makeup10, makeup10));
gcomp11 = vgroup("[1] C11", compressorMono(bandpass11, Makeup11, makeup11));
gcomp12 = vgroup("[1] C12", compressorMono(bandpass12, Makeup12, makeup12));
gcomp13 = vgroup("[1] C13", compressorMono(bandpass13, Makeup13, makeup13));
gcomp14 = vgroup("[1] C14", compressorMono(bandpass14, Makeup14, makeup14));
gcomp15 = vgroup("[1] C15", compressorMono(bandpass15, Makeup15, makeup15));
gcomp16 = vgroup("[1] C16", compressorMono(bandpass16, Makeup16, makeup16));
gcomp17 = vgroup("[1] C17", compressorMono(bandpass17, Makeup17, makeup17));
gcomp18 = vgroup("[1] C18", compressorMono(bandpass18, Makeup18, makeup18));
gcomp19 = vgroup("[1] C19", compressorMono(bandpass19, Makeup19, makeup19));
gcomp20 = vgroup("[1] C20", compressorMono(bandpass20, Makeup20, makeup20));
gcomp21 = vgroup("[1] C21", compressorMono(bandpass21, Makeup21, makeup21));
gcomp22 = vgroup("[1] C22", compressorMono(bandpass22, Makeup22, makeup22));
gcomp23 = vgroup("[1] C23", compressorMono(bandpass23, Makeup23, makeup23));
gcomp24 = vgroup("[1] C24", compressorMono(bandpass24, Makeup24, makeup24));
gcomp25 = vgroup("[1] C25", compressorMono(bandpass25, Makeup25, makeup25));
gcomp26 = vgroup("[1] C26", compressorMono(bandpass26, Makeup26, makeup26));
gcomp27 = vgroup("[1] C27", compressorMono(bandpass27, Makeup27, makeup27));
gcomp28 = vgroup("[1] C28", compressorMono(bandpass28, Makeup28, makeup28));

process = (_,_)<: hgroup("", gcomp1, gcomp2, gcomp3, gcomp4, gcomp5, gcomp6, gcomp7, gcomp8, gcomp9, gcomp10, gcomp11, gcomp12, gcomp13, gcomp14, gcomp15, gcomp16, gcomp17, gcomp18, gcomp19, gcomp20, gcomp21, gcomp22, gcomp23, gcomp24, gcomp25, gcomp26, gcomp27, gcomp28) :>(_,_);

Does LPF.ar(LPF.ar…) yield a 24db/octave slope for the crossover? Also, is it possible to implement knee with variable slope (or hard knee/soft knee without variable slope) in this compressor design or does it take another design.

Yes, that’s precisely 24db/oct. I think a hard knee is straightforward with this design. But I think for a soft knee; you would need a hack and blend between uncompressed and compressed. Or rewrite the compressor code (untested to sketch an idea).

compressor = { |snd, attack, release, threshold, ratio, kneeWidth|
    var amplitudeDb, gainDb, kneeStart, kneeEnd, kneeGainDb, hardKneeGainDb, softKneeGainDb, gain;
    amplitudeDb = Amplitude.ar(snd, attack, release).ampdb;


    hardKneeGainDb = ((amplitudeDb - threshold) * (1 - (1 / ratio))).min(0);

    if (kneeWidth > 0) {

        kneeStart = threshold - (kneeWidth / 2);
        kneeEnd = threshold + (kneeWidth / 2);
        softKneeGainDb = Select.kr(amplitudeDb <= kneeStart, [
            hardKneeGainDb,
            (amplitudeDb - kneeStart).linlin(0, kneeWidth, 0, (amplitudeDb - kneeEnd) * (1 - (1 / ratio))).min(0)
        ]);

        kneeGainDb = softKneeGainDb.linlin(0, kneeWidth, hardKneeGainDb, softKneeGainDb);
    }  {
        kneeGainDb = hardKneeGainDb;
    };

    gainDb = kneeGainDb;
    gain = gainDb.dbamp;
    snd * gain;
};

By the way, gaps exist between the frequency zones due to how the frequency bands are split. The math is not quite right, or it was intended for demonstration or effect.

I did a 5-band compressor with a GUI as a little exercise. This is based on @nathan’s simple no-knee compressor design from another thread. The GUI has gain reduction meters for each band and adjustable attack, release, threshold, ratio and gain for each band. Crossover frequency can be adjusted with the mouse (normal: 1 Hz increments/decrements, with ‘shift’ held 100 Hz increments/decrements) or with text input (like any NumberBox). Feel free to improve on the design, e.g. adding knee to the compressor circuit.

(
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff");
d = (
	pars: [\atk, \rel, \thresh, \ratio, \gain],
	atk: { 0.01 }!5,
	rel: { 0.1 }!5,
	thresh: { -12 }!5,
	ratio: { 4 }!5,
	gain: { 1 }!5,
	freqs: [150, 500, 1500, 5000], // crossover frequencies
	specs: (atk: [0.005, 0.2], rel: [0.05, 0.5], thresh: [-80, 0], ratio: [1, 20], gain: [0, 2] ),
	masterGain: 1,
	play: { 
		Ndef(\multiComp).set(*[\atk, \rel, \thresh, \gain, \freqs, \masterGain].collect{|n|[n, d[n]]}.flat);
		d.changed(\multiComp)
	}
);

Ndef(\multiComp, {
	var atk = \atk.kr(0.01!5), rel = \rel.kr(0.1!5), thresh = \thresh.kr(-12!5), ratio = \ratio.kr(4!5);
	var gain = \gain.kr(1!5), freqs = \freqs.kr([300, 1000, 2000, 3200]), sigIn = In.ar(\bus.kr(0));
	var compressor = { |snd, attack, release, threshold, ratio|
		var amplitudeDb, gainDb;
		amplitudeDb = Amplitude.ar(snd, attack, release).ampdb;
		gainDb = ((amplitudeDb - threshold) * (1 / ratio - 1)).min(0);
		snd * gainDb.dbamp;
	};
	var sigs = [sigIn] ++ freqs.reverse.collect{|n, i| LPF.ar(LPF.ar(sigIn, n), n) };
	var bands = (freqs.size.collect{|i| sigs[i] - sigs[i + 1] } ++ [sigs.last]).reverse;
	var comp = bands.collect{|n, i| compressor.(n, atk[i], rel[i], thresh[i], ratio[i]) * gain[i] };
	5.do{|i| SendPeakRMS.kr(bands[i] - comp[i], 30, 0.1, "/redux", i) };
	ReplaceOut.ar(\bus.kr(0), comp.sum!2 * \masterGain.kr(1));
}).play;

~multiBandGui = {|ev|
	var v = View(nil, Rect(500, 0, 340, 820)).front.alwaysOnTop_(true);
	var update = {
		ev[\pars].do{|n, i|d[n].do{|m, j| { p[i][j].value = m }.defer } };
		ev[\freqs].do{|n, i| { p[6][i].value = n }.defer }
	};
	var p = { List.new }!7;
	v.decorator = FlowLayout(v.bounds).gap_(4@8);
	StaticText(v, 300@40).string_("Multi Band Compressor").font_(Font(\Arial, 18, true));
	v.decorator.nextLine;
	5.do{|i|
		StaticText(v, 60@20).string_("Band " ++ (i + 1)).font_(Font(\Arial, bold: true));
		ev[\pars].do{|n, j|
			p[j].add(EZKnob(v, label: n, controlSpec: ev[\specs][n].asSpec, layout: \vert2, action: {|knob| 
				d[n][i] = knob.();
				Ndef(\multiComp).set(n, d[n])
			}))
		};
		v.decorator.nextLine;
		StaticText(v, 60@20); // filler
		StaticText(v, 60@20).string_(\Reduction);
		p[5].add(LevelIndicator(v, 200@20).numSteps_(20).style_(\led));
		3.do{ v.decorator.nextLine };
	};
	StaticText(v, 60@20).string_("X-Over").font_(Font(\Arial, bold: true));
	4.do{ p[6].add(NumberBox(v, 60@20)) };
	update.();
	Ndef(\multiComp).addDependant(update);
	v.onClose_{ Ndef(\multiComp).removeDependant(update) };
	OSCdef(\redux, { |msg| { p[5][msg[2]].value = msg[3].ampdb.linlin(-80, 0, 0, 1) }.defer }, "/redux");
};
~multiBandGui.(d);
{ PlayBuf.ar(1, b, BufRateScale.kr(b), loop: 1) }.play;
)

// GUI can be closed and re-opened again by executing ~multiBandGui.(d);
// You can set values from the editor and update the GUI like this:

d[\atk] = { rrand(0.01, 0.2) }!5;
Ndef(\multiComp).set(\atk, d[\atk]).changed;
2 Likes

Cool. I haven’t tested my code. Does it sound as intended, or did I fuck up somewhere?

Thanks for sharing, I’ll check it out when possible

How did you choose and calculate the freqs and bw?

I haven’t actually tested your design yet, I will soon. I did a variation of @nathan’s model (can’t remember which thread is from). So basically create 4 signals with progressive lo-cuts and subtract the signals to get the isolated bands (see the Ndef above, var sigs, var bands for the exact implementation). I chose the starting crossover freqs semi-randomly as they can be adjusted from the GUI or by code. I am not sure about the optimal ranges for attack and release in the GUI (in code you can do whatever you want). Right now attack is in the range [0.005, 0.2] seconds and release is in the range [0.05, 0.5] seconds.

I imagine a perfect/sufficiently reconstructable filter bank would make sense for a multiband compressor. To be “perfect,” one would need to do some math, so in the process of parallelizing/serializer, the distance between frequencies and the curves of their bandwidth fill the spectrum in a “perfect” way.

(actually, there is a lot of theory in DSP about it, I’m simplifying a lot)

Like this:

But I also like to have the specs of classic vintage equipment. Those people did not kid with this stuff)))

I tried subtracting the sum of bands from the original sound, the resulting signal did not show up the meter, which is not the same as saying that they are totally identical but the difference was at least small enough not to show on the meter. If you have ways of making it ‘perfect’ I would be very interested, I don’t posses the math skills to improve this method myself. I like how multiband compression blurs the line between EQ and compression. For instance, one could adjust the gain of each band and the x-overs with a threshold of 0 to EQ the signal without compressing it. For this case (and in general) it is desirable that the sum of bands is equal (or very near equal) to the original.

For reference and while people are posting things, the multiband compressor I use. It’s still a work in progress, and I’m not really an expert on compressor design so … :woman_shrugging:

You probably can’t run it bc it has a lot of dependencies to private library stuff, but maybe useful to look at the signal chain if anyone is interested. It has both global compression settings and per-band ones, but I never really use it like that so i need to rethink that part a bit.

It does the think that a few compressors do where you set a min and max - below the min it expands, above the max it compresses, and in the middle it’s pass-thru.

1 Like

Still have not tested this code, but it should be very easy to add the knee control to my GUI design, I will mess around with. I just found this softknee compressor from @Wouter_Snoei. I have not tested it yet, but looking at the source code could probably give ideas about how to implement soft knee.

1 Like

The Faust example I posted here uses the specs of the Sonology BEA5 third-octave filter bank/matrix, I used this filters and loved it, (it is usually used grouping 3 or 4 bands together, not like I did, individually)

image

Do the missing parts interfere with something meaningful? You came up with the params by ear?

That’s an interesting code there. It works with RMS and uses other “mechanics” than my Snipett. Since I haven’t test mine, this one probably sounds way better))

Most of the missing stuff is GUI things, and the Pdef at the bottom - I THINK the only thing special in the SynthDef is SynthDef.channelized, which just generates version of the def for different channel counts.

To be totally honest, I did it partly by ear and partly just as a very utilitarian tool to control levels in the music that I’m making, which often has wildly out of control dynamics. I wanted some thing extremely neutral and generic to stick at the end of a signal chain - if I want something more colored or severe, that’s usually part of the sound design process and I’d do it elsewhere.

The matrixes of the filter I mentioned allow some very colored configuration/design. I will leave the manual for inspiration:

It’s possible to generate different synthdefs from a particular configuration of those matrixes, making this a fabulous candidate for a metaprogram synthdef generation code.

Or it may be possible just to use Faust; it will be less efficient but can work switching configurations, with the right skills.