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;
}
}
