Parameter smoothing for delay line?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

I'm sure this is old news and has been asked X times before but I can't find what I'm looking for in "search" - or even Google. (Maybe I'm calling it the wrong term?)

Is there a standard/customary approach to smooth changes in delay line-based processes to avoid glitching noises when the delay time parameter is changing?

This would be something used in chorus, flangers, etc., as well as during user control changes of simple echos, etc.

I've tried smoothing parameter changes with simple IIR. It helps, but does not eliminate the problem (and creates new problems). What is the approach typically used for this?

Post

Yes
https://ccrma.stanford.edu/~dattorro/Ef ... nPart2.pdf
Long story short: For modulated delay lines (chorus, reverb, tape echo, physical modeling) use a 1-pole allpass IIR filter.

If you need to interpolate a large, instantaneous change in delay time (like changing a long delay time) instead of a bunch of smooth changes, it might be better to have a 2-tap delay line and do a crossfade.

Post

Thank you.

Post

Basically two methods: if you want a pitch shift, just smooth the delay length and interpolate for fractional samples. If you don't want a pitch shift, calculate the new length and do a fade (temporarily mixing two taps).

Post

mystran wrote:Basically two methods: if you want a pitch shift, just smooth the delay length and interpolate for fractional samples. If you don't want a pitch shift, calculate the new length and do a fade (temporarily mixing two taps).
My current need is just to smooth a simple delay line during user control (parameter) changes. Whatever I use I do not want it affect the audio with interpolation or some other "always on" filtering means.

What I'm not getting is how to filter the parameter changes (in a VST plugin). The parameter changes take place outside the frame processing loop - but yet you need the "time" element of the processing loop to enact some sort of filter. So, how/where is this done?

Post

Well, a simple crossfade example--

For instance maybe you have decided to do a 10 ms linear crossfade on the parameter change. For instance if the host happened to be running at 44.1K samplerate, 128 sample buffers, then the crossfade needs to happen over a span of 441 samples, so it will take several buffers to complete the crossfade.

If you decide to just do the crossfades starting at buffer boundaries, maybe something in the ballpark of this crude pseudocode--

Midi messages in vst can tell the plugin what sample offset in the buffer to begin responding to each midi message. The plugin doesn't have to start all notes and control changes exactly at buffer boundaries. I can't recall if other possible flavors of param changes can be time-stamped thataway in VST. I mainly pay attention to MIDI in my hosting.

Code: Select all

====
When you notice a param change--

double InterpFactor; //instance global
double InterpStep; //instance global

double FadeSeconds;
double FadeSpanInSamps;

SaveNewestSettingsInNewParmsObject();
InterpFactor = 1; //this is usually <= 0.0 except when crossfading
FadeSeconds = 0.01; //10 ms
FadeSpanInSamps = SampleRate * FadeSeconds;
InterpStep = 1.0 / FadeSpanInSamps;

====

In your rendering--

if (InterpFactor <= 0.0) //no recent parm changes, no crossfade in effect
  CalculateOutputBufferUsingOldParms();
else //need to calculate two buffers with the two sets of parameters and crossfade
{
  CalcTempOutBufferWithOldParms(); //fill temp buf OldParmBuffer[]
  CalcTempOutBufferWithNewParms(); //fill temp buf NewParmBuffer[]
  for (i = 0; i < NumSampsInBuffer; ++i)
  {
    if (InterpFactor <= 0.0) //finished crossfade within this loop
      OutputBuffer[i] = NewParmBuffer[i];
    else
    {
      OutputBuffer[i] = OldParmBuffer[i] * InterpFactor + NewParmBuffer[i] * (1.0 - InterpFactor);
      InterpFactor -= InterpStep; //decrement InterpFactor toward zero
    }
  }
  if (InterpFactor <= 0.0) //it reached the end of crossfade in this buffer loop
    OldParms = NewParms; //update the OldParms after crossfade finished
}

Post

There are a number of ways to go—you just have to decide on the behavior you want. For example, in Echo Farm (PT plug-in) and the DL4 delay pedal (shared DSP code), I gave the analog delays analog behavior by smoothing the delay parameter with a one-pole lowpass. This makes the pitch drop or rise (depending on increasing or decreasing delay) and settle back into pitch, like it does with a tape echo or other continuous analog delay.

But for digital delays, I ramped the delays down and back up per change, so that it gets ducked repeatedly while the knob is in motion. The pitch doesn't change, and I was simply looking for "digital delay" behavior to go with digital delay emulation. (Note, this doesn't require multiple delays. Echo Farm and DL4 use two delay lines in order to let the old delay effect to repeat out when switching to another, but the current, single delay line per channel is used with the ducking effect.) Think of how many radios duck the audio while you're spinning the tuning knob—you do hear a kind of sucking in and out of the level while the knob's turning, but not the blasts of noise you'd get without the ducking. The effect on the digital delays is smoother, but maybe that description give you some idea.

You probably don't need this, but maybe someone does—this is exactly the sort of filter I used for various parameters, including the delay smoothing for analog:

http://www.earlevel.com/main/2012/12/15 ... le-filter/
My audio DSP blog: earlevel.com

Post

JCJR wrote: I can't recall if other possible flavors of param changes can be time-stamped thataway in VST.
In VST2, no such time-stamps. Those few hosts that actually care (not very many), just split process blocks so the changes can be done in betwee.

Post

mystran wrote:
JCJR wrote: I can't recall if other possible flavors of param changes can be time-stamped thataway in VST.
In VST2, no such time-stamps. Those few hosts that actually care (not very many), just split process blocks so the changes can be done in between.
Thanks, mystran!

Post

Thanks all.

I actually went with a little different, and very simple, approach to what was suggested above. I created a new variable I called "newoffset" (pretty clever name, eh? LOL) which is set by the parameter change. I then ramped the current value up or down to it in the "process()" function like this:

Code: Select all

        if(offset < newoffset) offset++;//delay change smooth to new value
        if(offset > newoffset) offset--;
So with each sample that is processed the delay pointer (which is offset by the delay value of "offset") can only change by one increment up or down (rather than jumping several at once). It ramps smoothly, and linearly, to the new offset value. The bigger the change the longer it takes to get there. Higher sample rates will ramp faster but - so what - it's still a graduated transition vs. jumps.

It's not perfect but it is simple and took care of the clicks and pops during user delay time changes. The best part is it doesn't affect the audio at all once settled.

BTW - I initialized "offset" to 0 in the constructor to make sure it didn't have too far to go to any new value. I also protect the range with limits elsewhere in the code.

Post

Fender19 wrote:I actually went with a little different, and very simple, approach to what was suggested above...

Code: Select all

        if(offset < newoffset) offset++;//delay change smooth to new value
        if(offset > newoffset) offset--;
Great...but I suggest that you do yourself a favor and at least try the one-pole. It's actually simpler, and the behavior sounds more natural (you aren't limited by the slew rate for bigger moves, and it has a satisfying deceleration into the target). If you look at the link to code I posted, it's a single, branchless "z1 = in * a0 + z1 * b1", where z1 is the delay element and output for each iteration. a0 is just 1 - b1, so you could rearrange it that way if you want. I give the calculation for b1 (exp(-2.0 * M_PI * Fc);), but in a nut shell it's just a constant that gives you the rate of change you're happy with, so you could just plug in a number—a little less than 1.

BTW, a one-pole filter won't overshoot, so it behaves the way you want for this purpose.
My audio DSP blog: earlevel.com

Post

earlevel wrote:BTW, a one-pole filter won't overshoot, so it behaves the way you want for this purpose.
Overshoot may not be a problem but I have found that UNDERSHOOT can be. Since the delay line pointers are integers when cast to float and smoothed with IIR the final value cast back to integer may settle +/-1 sample off from what your control wants it to be.

At least that's what I found when I've tried IIR smoothing before. Maybe I've misunderstood the approach? I will give it another try. Thank you for the code and reference.

Post

Fender19 wrote:
earlevel wrote:BTW, a one-pole filter won't overshoot, so it behaves the way you want for this purpose.
Overshoot may not be a problem but I have found that UNDERSHOOT can be. Since the delay line pointers are integers when cast to float and smoothed with IIR the final value cast back to integer may settle +/-1 sample off from what your control wants it to be.
Ah, so your delay line doesn't support fractional delays times (if I understand correctly)...well, you can round or something...

Of course fractional delay times are a big improvement if you want to support various time-based effects (flanging, etc.). There are more sophisticated ways, but if you want to experiment cheaply, try simple linear interpolation, using a float time value in samples (interpolating between the sample indexed by the integer part and the next one, using the fractional part). Now, I hear gasps, but linear interp is good for lower freqs, not so good for the higher, but you can cheat a little by forcing your time settings to sample boundaries (so that it's normal at or very close to a sample boundary, but can still slide continuously through time while being modulated); do this with your nominal delay setting but let everything from there on (the one-pole and any wow-and-flutter or other modulation) "float".

First, it's not as bad as you'd think, since for much of music the high frequencies are at lower amplitudes (and it helps if you're emulating an analog delay with HF rolloff). And second, if it's not good enough, you can just drop in an improved interpolated delay line later.

Anyway, just a tip, not telling you how to do your thing. But since you're going to the effort of trying to handle time changes in a somewhat natural manner, I think you'll enjoy the improvement with a fractional delay line.
My audio DSP blog: earlevel.com

Post

earlevel wrote:Ah, so your delay line doesn't support fractional delays times (if I understand correctly)...well, you can round or something....
Yes, I am using a simple delay line where the read pointer is X offset behind the write pointer in a rotating buffer. It's normally a static offset. The only time it's "modulated" is when the user is making delay parameter changes during which the original code gave nasty clicks and pops.
earlevel wrote:Anyway, just a tip, not telling you how to do your thing.
I appreciate any/all tips. :tu:

Post

AUTO-ADMIN: Non-MP3, WAV, OGG, SoundCloud, YouTube, Vimeo, Twitter and Facebook links in this post have been protected automatically. Once the member reaches 5 posts the links will function as normal.
Have you tried cubic interpolation? It has this almost buttery flavor to it, I used it in my pitch shifting delay and it has been pretty "smooth".

Code: Select all (#)

double CubicInterp (double frac, double w, double x, double y, double z)
{
	double c1 = (y - w) * 0.5f;
	double c3 = (x - y) * 1.5f + (z - w) * 0.5f;
	double c2 = c1 + w - x - c3;
	return ((c3 * frac + c2) * frac + c1) * frac + x;
}
And yeah I would also try slapping a OnePole on there and see how things go, even something like this might work

Code: Select all (#)

struct Smooth {
    Smooth() : value(0), target(0), factor(0.003) {}
    
    double operator()() {
        double y = value;
        value += (target - value) * factor;
        return y;
    }
    
    void operator=(double target) {
        this->target = target;
    }
    
    double value, target, factor;
};
=)

Post Reply

Return to “DSP and Plugin Development”