camsr wrote:Unless you delay the signal to calculate feedforward and feedback values, it is always a compromise.

Yes, however even without delaying anything there are some tricks to mitigate the compromise. Some of the simple ones I'm going to share with you all.

The code examples will be in

JesuSonic. It is either available as

standalone or as

VST plug-in. However the code is very simple to read and thus can easily be transfered into any other language.

The most simple envelope follower is a simple RC filter working on a rectified signal such as:

- Code: Select all
`/* Copyright (c) 2012, ro.cking`

* All rights reserved.

*

* Permission to use, copy, modify, and/or distribute this software for any

* purpose without fee is hereby granted, provided that the above

* copyright notice and this permission notice appear in all copies.

*

* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES

* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF

* MERCHANTABILITY, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE

* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL

* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR

* PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS

* ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF

* THIS SOFTWARE.

*/

desc: RC (Envelope Follower)

@init

/* rc smoothing filter */

C = exp(-1/(25/1000*srate));

@sample

/* input is absolute value of left channel; so this is mono only! */

in = abs(spl0);

/* smooth via a rc low pass */

out = in>out ? in : in + C * (out-in);

/* output the envelope on both channels */

spl0 = spl1 = out;

This as already seen in this thread has the problem of ripple vs response.

A very simple trick is to use 3 (or more) peak followers in parallel that are reset in a round robin fashion as presented by

Harry Bissell, Royal Oak also available as

pdf.

- Code: Select all
`/* Copyright (c) 2012, ro.cking`

* All rights reserved.

*

* Permission to use, copy, modify, and/or distribute this software for any

* purpose without fee is hereby granted, provided that the above

* copyright notice and this permission notice appear in all copies.

*

* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES

* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF

* MERCHANTABILITY, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE

* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL

* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR

* PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS

* ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF

* THIS SOFTWARE.

*/

/*

* Based on http://www.edn.com/design/analog/4344656/

* Envelope-follower-combines-fast-response-low-ripple

*/

desc: Triple Peak Hold (Envelope Follower)

@init

/* total peak hold chunk length; is good down to 50Hz !! */

t = srate/50;

/* total peak hold chunk length; is good down to 20Hz !! */

//t = srate/20;

/* peak hold 1 */

p1 = 0;

t1 = 0 * t/3;

/* peak hold 2 */

p2 = 0;

t2 = 1 * t/3;

/* peak hold 3 */

p3 = 0;

t3 = 2 * t/3;

/* rc smoothing filter; tune this to the total peak hold chunk length */

C = exp(-1/(20/1000*srate));

@sample

/* input is absolute value of left channel; so this is mono only! */

in = abs(spl0);

/* get new maximas */

p1 = max(p1, in);

p2 = max(p2, in);

p3 = max(p3, in);

/* get maximum of all peak holds */

p = max(max(p1,p2),p3);

/* increment peak hold times and conditionally reset the peak holds */

(t1+=1) >= t ? (p1 = t1 = 0);

(t2+=1) >= t ? (p2 = t2 = 0);

(t3+=1) >= t ? (p3 = t3 = 0);

/* smooth the peak hold output via a rc low pass */

out = p>out ? p : p + C * (out-p);

/* output the envelope on both channels */

spl0 = spl1 = out;

This also suffers ripple vs response. However with the peak hold time and also the number of peak holds combined with the RC smoothing filter it is very configurable and if you are willing to pay the processing cycles for additional peak holds can cover a vast range of frequencies with good ripple and response.

Then a more musical approach is to use the actual pitch of the signal to determine whether the envelope should decay fast or slow. This is mostly used in compressors for automatic release functions, however it will also work wonderful on the sine waves posted in this thread. A quick test revealed it will be smooth down to 1Hz! But still fast at 1000Hz. However it will not be fast at 1Hz, but in musical terms this is wanted behavior. But without further ado here it is.

- Code: Select all
`/* Copyright (c) 2012, ro.cking`

* All rights reserved.

*

* Permission to use, copy, modify, and/or distribute this software for any

* purpose without fee is hereby granted, provided that the above

* copyright notice and this permission notice appear in all copies.

*

* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES

* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF

* MERCHANTABILITY, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE

* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL

* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR

* PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS

* ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF

* THIS SOFTWARE.

*/

desc: Pitch Aware (Envelope Follower)

@init

/* rc hp to remove dc */

Chp = exp(-2*$pi*20/srate);

/* filtering to smooth cylce length changes */

Cl = exp(-1/(5/1000*srate));

/* initial pitch */

pitch = 1000;

@sample

/* input is left channel; so this is mono only! */

in = spl0;

/* calculate lp coef; lp at the base freq so upper hmx dont interfere */

Clp = exp(-2*$pi*pitch/srate);

/* do lp 234523w3t5 to remove hmx freqs or formants */

in = ylp = in + Clp*(ylp-in);

/* do hp to remove dc */

in -= yhp = in + Chp*(yhp-in);

/* count length between to zero crossings */

old_s = s;

s = sign(in);

old_s < s ? (

l = cnt;

cnt = 0;

);

/* filter this length to get a more steady read out */

len = l + Cl*(len-l);

/* calc pitch, cap this >20Hz so the lp ref 234523w3t5 isn't filtering too low */

pitch = max(srate/len,20);

cnt+=1;

/* input for envelope is absolute value of left channel; this is mono! */

in = abs(spl0);

/* calculate decay according to cycle length; after 20 cycles it will drop

* to 36%, this should be smooth enough but yet fast enough

*/

rel = exp(-1/(len*20));

/* rc smoothing */

out = in > out ? in : in + rel*(out-in);

/* assign envelope to outputs */

spl0 = spl1 = out;

If this is for a compressor a simple RC envelope follower will most likely be enough. Believe me I've been there and done that even developed "better" approaches than the above presented and acoustically the only provide a minimal improvement over the RC approach at a heavy cost.

As you are trying to develop a compressor you might find the following free source code useful:

- Code: Select all
`/* Copyright (c) 2012`

* All rights reserved.

*

* Permission to use, copy, modify, and/or distribute this software for any

* purpose with or without fee is hereby granted.

*

* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES

* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF

* MERCHANTABILITY, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE

* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL

* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR

* PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS

* ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF

* THIS SOFTWARE.

*/

desc:FreeComp

slider1:0<-60,6,1>Threshold (dB)

slider2:1<1,100,1>Ratio (n:1)

slider3:20<0,250,1>Attack (ms)

slider4:200<25,2500,1>Release (ms)

slider5:0<0,30,1>Output (dB)

@init

/* set values for sample n=-1, that is what the state was/is before we got input

* this is more formal than anything, but if we do not set these we might have fade-ins

* or other weird effects when starting processing, e.g. the gain reduction is at max

* because we forgot to reset the 'final_gain' value ... see @sample section for what

* it does, same goes for the 'env' (envelope)

*/

final_gain = 1;

env = 0;

@slider

/********************

* to 1) ENVELOPE

*******************/

/* RC filter coefficient for envelope decay, we set this to 1/4 of the release

* value to get a good smooth envelope, but still allow for _very_ fast release

* timings

*/

env_rel = exp(-1/ (0.25*slider4*srate) );

/**************************

* to 2) TRANFER FUNCTION

***************************/

/* the threshold's amplitude value */

thresh = 10^(slider1/20); /* convert dB from slider1 to amplitude */

/* pre-calculate values as described in @sample comments*/

transA = (1/slider2)-1;

transB = 10^(slider5/20) * pow(thresh,1-(1/slider2));

/* output_gain's amplitude value */

output_gain = 10^(slider5/20);

/*************************

* to 3) GAIN FOLLOWER

*********************/

/* calculate the RC coefficients for the attack and release filter parts */

att_coef = exp(-1 / (slider3/1000*srate) );

rel_coef = exp(-1 / (slider4/1000*srate) );

@sample

/***************************************************

* 1) ENVELOPE

* First step for an compressor is to get the level, that is

* the envelope of the signal that will then later be used

* to determine whether and also how much to compress etc..

* Note: there are a lot of different ways to get this level:

* 1) envelope follower (a.k.a. detector) (there are also different kinds of that)

* 2) RMS averaging

* 3) and many more

* ... we will stick with simple here

************************************/

det = max( abs(spl0),abs(spl1) ); /* the detector input for the

* envelope follower */

det += 10e-30; /* add tiny DC offset (-600dB) to prevent denormals

* Note: JesuSonic doesn't need this because it prevents denormals

* internally itself, but when you are going to port this code

* you will need it

*/

env = det >= env ? det : /* ramp up instantly on attack so we

* have a peak compressor */

det+env_rel*(env-det); /* otherwise we use a RC filter to

* produce a smooth decay

* We need 'env_rel' for this which we calculated in @slider

* Note: a simple decay like 'env=env*env_rel'

* would be "wrong" here (or a bad approach)

* while this is arguable

* I tell you why I think it's bad:

* imagine 'env' only being a _tad_ BIGGER

* than 'det' but 'env*env_rel' being SMALLER

* than 'det' what you would get would be a constantly

* readjusting envelope follower, which

* our instant ramp up (to make the follower

* peak detect) would make even worse

* ... anyway so far the envelope follower

*/

/**************************************

* 2) TRANSFER FUNCTION

* The transfer function is the "command center" of a compressor

* it basically decides on how much gain reduction should be applied

* for a given input (level).

* As always there are many different ways to do it, but in general

* _any_ waveshaping function can be used here, but again we gonna

* do it simple and use the typical equation:

* Out[dB] = Thresh[dB]+(In[dB]-Thresh[dB])*Ratio[1/(n:1)], if In[dB] > Thresh[dB]

* In[dB] , otherwise

* (1)

* Note: that these figures are all in dB and when we use the

* sample values (which are given in amplitude) like this:

* 'out = in>thresh ? thresh+(in-thresh)*ratio:in;'

* (using the variables we declared in @slider)

* we would get "wrong" results. We would get a curve that is

* only linear in the amplitude space but in dB the curve would be inverse

* non-linear, that is the gain reduction would _decrease_ at higher input

* levels .... however many call this "vintage" or something, whatever

* it could be used as a quick trade of for a CPU saving ratio implementation.

* Anyway here we gonna use the above "correct" equation. So we need to

* convert the values into dB and then apply equation.

* But first we need to change the equation to give us the 'gain'

* we will need and we do this simply by subtracting In[dB] from

* the left side of (1) which gives us:

* Gain[dB] = -(Ratio[1/(n:1)]-1)*(Thresh[dB]-In[dB]), if In[dB] > Thresh[dB]

* 0 , otherwise

* (2)

* Now we can already add our final output gain to (2) (to save

* the extra scaling at the output later on) by simply adding it to

* (2), and we have:

* Gain[dB] = Output_Gain[dB]-(Ratio[1/(n:1)]-1)*(Thresh[dB]-In[dB]), if In[dB] > Thresh[dB]

* Output_Gain[dB] , otherwise

* (3)

* Next we need to convert (3) to work with values in amplitude instead

* of dB, so we need to convert the entire equation to give:

* Gain[Amp] = 10^((20*log10(Output_Gain[Amp])-(Ratio[1/(n:1)]-1)*(20*log10(Thresh[dB])-20*log10(In[dB])))/20), if In[dB] > Thresh[dB]

* 10^((20*log10(Output_Gain[Amp]) , otherwise

* which we can transform into:

* Gain[Amp] = In[Amp]^(Ratio[1/(n:1)]-1)*Output_Gain[Amp]*Thresh[Amp]^(1-Ratio[n:1]), In[Amp] > Thresh[Amp]

* Output_Gain[Amp] , otherwise

* (4)

* As we can see (4) now consists of "only" one raise to the Ratio'th power and a

* multiplication because everything else can be precomputed, yay!

* So we can pre-compute the two values:

*

* TransA = (Ratio[n:1]-1) (5)

*

* and

*

* TransB = (Output_Gain[Amp]*Thresh[Amp]^(1-Ratio[n:1]) (6)

*

* Combining (4), (5) and (6) we get

* Gain[Amp] = In[Amp]^TransA * TransB , In[Amp] > Thresh[Amp]

* Output_Gain[Amp] , else (7)

* which is the final transfer function that we will implement.

* (Note: there might be much easier and faster ways to calculate a transfer curve)

**************************************/

gain = env > thresh ? pow(env,transA)*transB: /* Transfer function as developed

* thresh, transA and B pre-calced in @slider

*/

output_gain; /* output_gain also calculated in @slider */

/***********************************************

* 3) GAIN FOLLOWER (the attack and release)

* Now the final step applying the attack and the release.

* There are also various ways to apply attack and release. Sometimes they

* are already calculated in the envelope, so the envelope will ramp up and

* decay according to the attack and release times, however, I don't consider

* this a good approach because then the attack and release also affect the

* non-linear transfer curve which they in my opinion shouldn't, but whatever

* sounds good IS good, but since mostly _everything_ sounds good I actually

* go by measurements most of the time and found out that by getting a good

* and smooth envelope you get a smoother overall operation in the compressor,

* read less distortion, which is better, but a lot of people argued this in the

* past so I won't go into detail but rather will give you another reason for

* _not_ botch the release and attack into the envelope is that when keeping

* the gain follower separate you can exchange the "modules" as you like and

* can work on improving each part individually.

* Anyway there are two main types of attack and release curves:

* Linear (where the gain in/decreases linearly overtime)

* Exponential (where the gain in/decreases exponentially overtime)

*

* We will use an exponential curve, because it is smoother, easy to implement

* and is in a sense level independent, because unlike a linear curve that

* changes x dB over time, an exponential curve actually changes to x % of the initial

* value over time.

* A simple exponential curve can be done again with a simple RC filter, like the

* envelope, however we use two cases one that deals with lowering (decaying)

* the gain, that is the _attack_ (important here on a compressor the decay of

* gain, that is the reduction of gain is the attack and _not_ the release (a.k.a.

* decay)) and the increase of gain again, which is the release.

* Equation would be something like: (using the real variables we will use in the code)

* final_gain = gain+att_coef*(final_gain-gain) , if gain < final_gain

* gain+rel_coef*(final_gain-gain) , else

************************************************/

final_gain = gain < final_gain ? gain+att_coef*(final_gain-gain):

gain+rel_coef*(final_gain-gain);

/***********************

* 4) "VCA"

* Luckily we are in the digital domain so applying gain is as simple as multiplication, literally.

*******************/

spl0 *= final_gain;

spl1 *= final_gain;

/*********************************

* ADDITIONAL NOTES

*

* The algorithm as implemented here is a particularly bad music processor.

* ---------------------------------------------------------

* A linear attack and release curve (linear in in/decrease of the _level's amplitude_) can be implemented with:

* @slider

* [...]

* lin_att = 1/(slider3/1000*srate);

* lin_rel = 1/(slider4/1000*srate);

* [...]

* @sample

* [...] // replace old 3) GAIN FOLLOWER section with

* final_gain = gain<final_gain ? max(gain,final_gain-lin_att):

* min(gain,final_gain+lin_rel);

* [...]

*

* ----------------------------------------------------------

* A linear attack and release curve (linear in in/decrease of _level in dB_) can be implemented with:

* [...]

* @sample

* [...] // replace old 3) GAIN FOLLOWER section with

* final_gain = gain<final_gain ? max(gain,final_gain*lin_att):

* min(gain,final_gain/lin_rel);

* [...]

*

* ----------------------------------------------------------

* Ratios < 1 will instantly turn this compressor in an expander ;) (just remember

* that in that case the attack and release are swapped).

* ----------------------------------------------------------

* A Gate implementation can be achieved simply by changing the transfer function

* to have gain of 0 (amplitude) when below the threshold and 1 (~0dB) when above

* it. Just like with the expander, attack and release need to be swapped. A hold

* is mostly always wanted when implementing a gate as well.

* ----------------------------------------------------------

* Implementing a little hold either in the gain follower or the envelope follower can

* decrease distortion (due to (unnecessary much) amplitude modulation) at lower frequencies

* but also making the (release) response of compressor slower. The current implementation

* however should already be suitable for most applications. In fact the envelope's decay

* (in combination with the gain followers release) is in some way already acting as a

* hold.

* Note: some people disagree with this and think that reducing the amplitude modulation

* to the necessary minimum will inevitable make the compressors response way too slow

* to be usable. Anyway judge for yourself.

* -----------------------------------------------------------

* TESTING the most underrated thing in programming.

* While I was doing my research I've seen a few compressors that had "flaws" in

* there implementation that would have been instantly discovered by the following

* simple tests (I wrote "flaws" because maybe it was/is intended behavior):

* Transfer function: disable the gain follower section and apply the raw gain from the

* transfer function to the signal, then plot the curve and make sure it is what you

* intended; a useful tool for this is Christian Budde's VST Analyser

* (http://www.savioursofsoul.de/Christian/?page_id=106). This is especially advised

* when you try implementing a soft knee curve.

* Attack and Release curve: Use a white noise (or a sine wave) signal that

* starts with a level below the threshold than instantly has its level increased to

* go over the threshold and after a while instantly falling back down to its initial

* level. Now by feeding this test signal through your compressor you can see (by looking

* at the output waveform) what your attack and release are really doing.

*****************************/

Anyway good luck.

EDIT: Sorry all for the rather longish post.

Last edited by ro.cking on Fri Dec 21, 2012 3:04 am, edited 1 time in total.