Limiter, clipper, and envelope follower (C++)

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

Building on the principle of the envelope follower, here is a new class that will automatically limit the output if it would be clipped. Instead of clipping, it follows the envelope of the output (using the follower) and scales the samples down to fit. This preserves the audio content in a way that doesn't cause distortion like straight clipping.

You can control the attack and decay parameters of the limiter. The attack determines how quickly the limiter will respond to a sudden increase in output level. I have found that attack=10ms and decay=500ms works very well for my application.

This C++ example demonstrates the use of template parameters to allow the same piece of code to work with either floats or doubles (without needing to make a duplicate of the code). As well as allowing the same code to work with interleaved audio data (any number of channels) or linear, via the "skip" parameter. Note that even in this case, the compiler produces fully optimized output in the case where the template is instantiated for a compile-time constant value of skip.

In Limiter::Process() you can see the envelope class getting called for one sample, this shows how even calling a function for a single sample can get fully optimized out by the compiler if code is structured correctly.

Code: Select all

class EnvelopeFollower
{
public:
	EnvelopeFollower();

	void Setup( double attackMs, double releaseMs, int sampleRate );

	template<class T, int skip>
	void Process( size_t count, const T *src );

	double envelope;

protected:
	double a;
	double r;
};

//-----------------------

class Clipper
{
public:
	Clipper();

	template<class T, int skip>
	void Process( size_t count, T *dest );

	bool bClipped;
};

//-----------------------

struct Limiter
{
	void Setup( double attackMs, double releaseMs, int sampleRate );

	template<class T, int skip>
	void Process( size_t nSamples, T *dest );

private:
	EnvelopeFollower e;
};

//-----------------------

inline EnvelopeFollower::EnvelopeFollower()
{
	envelope=0;
}

inline void EnvelopeFollower::Setup( double attackMs, double releaseMs, int sampleRate )
{
	a = pow( 0.01, 1.0 / ( attackMs * sampleRate * 0.001 ) );
	r = pow( 0.01, 1.0 / ( releaseMs * sampleRate * 0.001 ) );
}

template<class T, int skip>
void EnvelopeFollower::Process( size_t count, const T *src )
{
	while( count-- )
	{
		double v=::fabs( *src );
		src+=skip;
		if( v>envelope )
			envelope = a * ( envelope - v ) + v;
		else
			envelope = r * ( envelope - v ) + v;
	}
}

//-----------------------

inline Clipper::Clipper()
{
	bClipped=false;
}

template<class T, int skip>
void Clipper::Process( size_t count, T *dest )
{
	bClipped=false;
	while( count-- )
	{
		if( *dest>1 ) 
		{
			*dest=1;
			bClipped=true;
		}
		else if( *dest<-1 )
		{
			*dest=-1;
			bClipped=true;
		}
		dest+=skip;
	}
}

//-----------------------

inline void Limiter::Setup( double attackMs, double releaseMs, int sampleRate )
{
	e.Setup( attackMs, releaseMs, sampleRate );
}

template<class T, int skip>
void Limiter::Process( size_t count, T *dest )
{
	while( count-- )
	{
		T v=*dest;
		// don't worry, this should get optimized
		e.Process<T, skip>( 1, &v );
		if( e.envelope>1 )
			*dest=*dest/e.envelope;
		dest+=skip;
	}
}

Post

One thing worth mentioning is that I am using one instance of the class for both channels of stereo data. I don't want the left and right sides to get scaled differently, that would be silly. So I call Process() once on my interleaved stereo data with twice the number of frames, and skip=1.

Post

Where would one tap this to get the amount of GR that the limiter is putting on? For a meter..

Post

robrokken wrote:Where would one tap this to get the amount of GR that the limiter is putting on?
This should be someting like

Code: Select all

double Limiter::getLinearGR()
{
  return e.envelope>1. ? 1/e.envelope : 1.;
}
@thevinn:
Wouldn't it be better to also make the envelope follower's members templated instead of doubles. The way you do it atm you'll have float to double conversions in your EnvelopeFollower::Process():

Code: Select all

double v=::fabs( *src );
Also you might get denormalisation issues in your EnvelopeFollower's release.


Chris

Post

Well that code is from 2009! I think DspFilters has a better one in it (not sure though).

Think of this more as a code example rather than a finished product. Tweak as needed to suit your needs!

Post Reply

Return to “DSP and Plugin Development”