How do you do general parameter smoothing?

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

Post

Hello,

I've been trying to figure out an efficient way to do general parameter smoothing for a while now, but I just can't get it down. In Reaktor, I would just use the Mnogo Smusas macro (found in some of NI's ensembles) and it worked perfectly. Well now I'm trying to work with C++ (using JUCE) and I can't get it right. From what I understand, Mnogo Smusas just interpolates across the previous and current values over a specified amount of time, in a rather neat way. How would you do this in C++? Has anyone made a class specifically for this? Any help or tips would be greatly appreciated. I can't even make a simple gain control without hearing crackles! :P

Some questions/thoughts that occur to me. I assume that you would "process" the smoothing on the audio process (or maybe event timers?) if you have a ramp that crossfades between previous and current values over time. Is this right? Well how would you make it only process the smoothing when the parameters actually change? I was thinking that just a simple duplicate filter would work (only processing if current != previous), but I'm not too sure about that. Also, I'm wondering if you would process on every sample. When I think about a ramp going from 0->1 over a specified amount of time, I think about a ramp oscillator or phase accumulator, which I believe needs to be processed every sample to get the correct amount of time (or frequency).

I think all I really need is to figure out how to trigger a single cycle ramp oscillator when a parameter changes and use that to control the crossfade position for previous and current values. Any ideas or working code? I know some of you have to have a good way to do this.

I used smothers for a lot of things in Reaktor and I'd love to be able to do it here.

Post

For smoothing, I use a one pole lowpass filter and set the target just above (or below) the actual value, depending on whether the parameter value is greater than or less than the previous value. I have code that detects when the value crosses the actual target value. At that point, smoothing stops, and all values fetched from the smoother after that are the actual target value.

Another approach is to just let the lowpass filter run forever. If you set the target to the actual value you're aiming for, the filter will never quite reach it (asymptote) but get closer and closer to the point where it's good enough. If you take this approach, watch out for denormals.

You can check before each buffer to see if a smoother is done. If so, to make your code less complicated, you could pass along an empty iterator object that simply returns the target value. Template methods can be good for this.

Something like:

Code: Select all

void Plugin::Process(/* buffers and stuff here */)
{
    if(ampSmoother.IsDirty())
    {
        DoProcess(/* pass along buffers */ ampSmoother);        
    }
    else
    {
        DoProcess(/* pass along buffers */ ampSmoother.GetEmptyIterator());                
    }
}

// And in your header file:

template<Class T>
void Plugin::DoProcess((/* buffers and stuff here */, T &ampSmoother)
{
    for(int i = 0; i < bufferSize; i++)
    {
        *out = *in * ampSmoother();

        in++;
        out++;
        ampSmoother++;
    }
}
There are probably other (read better ways), but this approach has worked for me.

Post

Leslie Sanford wrote: Another approach is to just let the lowpass filter run forever. If you set the target to the actual value you're aiming for, the filter will never quite reach it (asymptote) but get closer and closer to the point where it's good enough. If you take this approach, watch out for denormals.
The filter feedback will eventually settle, if you use SSE denormal prevention. So it's not quite infinite decay.
Edit: This is true when the filter input is 0 (at least for the filter I use), I am going to test the case the input is a DC signal.

A lowpass is ok for gain smoothing. Other functions might not behave as expected if the smoothing is not linear.

Post

Here is what I use. Nothing fancy, just a linear interpolation over a predefined number of steps.

Code: Select all

template<class T>
class SmoothedValue
{
public:
	SmoothedValue(T v, int steps=64): _v(v), _target(v), _delta(0), _ksteps(steps), _nsteps(0)
	{
	}
	~SmoothedValue()
	{
	}
	
	void setValue(T v)
	{
		_nsteps=0;
		_target = v;
		_v = v;
		_delta = 0;
	}

	void setTargetValue(T target)
	{
		_nsteps = _ksteps;
		_target = target;
		_delta = (_target-_v)/(T)_nsteps;	
	}

	T getValue() const
	{
		return _v;
	}

	T getTargetValue() const
	{
		return _target;
	}
	
	void tick()
	{
		if (_nsteps) {
			_v = _target-_delta*(T)_nsteps; 
			_nsteps--;
		}
	}

	bool isStillSmoothing() const
	{
		return (_nsteps!=0);
	}
private:
	T _target;
	T _delta;
	T _v;
	int _nsteps;
	const int _ksteps;
};

Post

Another One Pole LPF smoother (from a comment that I left in musicdsp.org few months ago).

You can define the smoothing time in ms. so maybe for parameters like delay time and that sort of things you want longer smoothing times, for most of the parameters I use something around 10 ms.

Just instantiate one of these in any onInit function then update it every sample in the audio process function.

Hope this helps!

Code: Select all

class CParamSmooth
{
public:
    CParamSmooth(float smoothingTimeInMs, float samplingRate)
    {
        const float c_twoPi = 6.283185307179586476925286766559f;
        
        a = exp(-c_twoPi / (smoothingTimeInMs * 0.001f * samplingRate));
        b = 1.0f - a;
        z = 0.0f;
    }

    ~CParamSmooth()
    {
        
    }

    inline float process(float in)
    {
        z = (in * b) + (z * a);
        return z;
    }

private:
    float a;
    float b;
    float z;
};

Post

There is also slew rate limiting. This can work at sample rate, but it does not attempt to reduce aliasing.

Post

Thanks guys. I'll definitely be checking those out.

Do any of y'all know how to make a triggered ramp that goes from 0 to 1 over a specified amount of time and stops at 1? For some reason, I just can't get that to work. I think it could be useful for a lot of things besides just smoothing. Maybe I should make a new post for that if I don't get anything in this one.

Post

Fs = <sampling rate>;
t = <time>; // in seconds
c = 1.0/(t * Fs); // increment coeff
p = 0.0; // memory. initialise/retrigger to 0.0

Code: Select all

// process:
double y = p; // output sample
p += c;
if (p >= 1.0) { p = 1.0; }
return y;
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post

Jordan Harris wrote:Thanks guys. I'll definitely be checking those out.

Do any of y'all know how to make a triggered ramp that goes from 0 to 1 over a specified amount of time and stops at 1? For some reason, I just can't get that to work. I think it could be useful for a lot of things besides just smoothing. Maybe I should make a new post for that if I don't get anything in this one.
Using the class above:

Code: Select all

SmoothedValue<float> v(0.0f, (int) sampleRate);
v.setTargetValue(1.0f);

Post

Sorry to revive this old (and very useful) thread, but I was wondering if anyone had any methods for efficiently smoothing boolean parameters. I have a few on/off buttons that, you guessed it, switch between two functions. The functions are fairly expensive so I'd rather not run both of them and interpolate between them. There must be some efficient technique to deal with bools. Or ints.

Anyone?

Post

joshb wrote:Sorry to revive this old (and very useful) thread, but I was wondering if anyone had any methods for efficiently smoothing boolean parameters. I have a few on/off buttons that, you guessed it, switch between two functions. The functions are fairly expensive so I'd rather not run both of them and interpolate between them. There must be some efficient technique to deal with bools. Or ints.
You can't smooth discrete values, it just doesn't work. You can convert a boolean or integer into a floating point value (or something similar) and smooth that. Then you have a continuous parameter and what you do with that is up to you.. but you can't smooth it while it's discrete, it just doesn't work.

Post

Maybe you can just fade out to zero the last value you got from the first function for any number of samples, and use only the output of the second function that you fade in from zero to its actual value at the same time ? Same principle than crossfading between the two functions, but here we don't care about recalculating the output of the obsolete one, we just use the last value it produced. Might be enough to reduce the switching artefacts if the cross fade curve is exponential. Or maybe you could use a fast approximation of the first function instead of the real one just for the crossfade...

Post

Ivan_C, what you describe looks very like linear interpolation.
See you here and there... Youtube, Google Play, SoundCloud...

Post Reply

Return to “DSP and Plugin Development”