Parameter smoothing for delay line?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Awesome. Will give all of these suggestions a try. Thank you!

Post

I want to visit this one more time because I must be doing something wrong.

My delay line consists of an array where the "write" pointer advances forward 1 step with every step of "while(--sampleFrames>0)".

My "read" pointer follows behind the "write" pointer by a delay value that I called "offset" which is an integer value.

Normally the delay is static, i.e., both the read pointer and write pointer advance 1 step per sample but are separated by the offset (delay) amount.

Now, if I apply a One Pole filter to the offset value - and the user moves the delay control fast enough - that offset value can increase faster than 1 step per sample and the output will actually play BACKWARDS during the initial part of the transition. As the one pole flattens out to the final value the audio will reverse direction and again play forward at the new delay time. Not what I had in mind for parameter smoothing!

So, I guess my question is - how do you keep the read/write pointers always moving forward even while changing the offset value? You have to make sure "offset" can never increase by more than 1 step per loop otherwise the "read" pointer can be standing still or going backwards in time!

How is this normally done? What am I not understanding here?

Post

Though it might not be the effect you desire-- I'm not telling you how to judge the proper behavior of your effect--

On the other hand, if you grab the playback head of a movable head tape delay like some of the echoplex boxes, wouldn't it do the same temporary backwards thing?

Can't recall whether all echoplex tape delay units used a movable playback head. Maybe some of them used adjustable capstan motor speed. Several other brands of tape delay boxes used fixed heads and adjustable tape speed.

You could crossfade between old and new delay offsets, as previously mentioned.

If you perhaps limit the advancement of the read pointer until the new offset is reached-- For instance if the read pointer is only advanced at half the normal rate of the write pointer until new offset reached-- In that case, rather that temporarily playing backwards, it would play an octave low (and half as fast) until the new offset is reached.

Post

Fender19 wrote:Now, if I apply a One Pole filter to the offset value - and the user moves the delay control fast enough - that offset value can increase faster than 1 step per sample and the output will actually play BACKWARDS during the initial part of the transition. As the one pole flattens out to the final value the audio will reverse direction and again play forward at the new delay time. Not what I had in mind for parameter smoothing!

So, I guess my question is - how do you keep the read/write pointers always moving forward even while changing the offset value? You have to make sure "offset" can never increase by more than 1 step per loop otherwise the "read" pointer can be standing still or going backwards in time!

How is this normally done? What am I not understanding here?
Like Jim said, this is how analog delays such as tape echo work—if you move the head continuously, of course the audio will play backwards if you are moving the head backwards in time—there is no getting around that (and it's fun, if you want to sound analog). Iƒ you traveled back in time, continuously, and could watch the world, you'd see people walking backwards, until you arrived at your time destination, when they would start moving forward again. As I mentioned earlier in the thread, the alternative is to jump to the destination and de-glitch, if that's the effect you want.
My audio DSP blog: earlevel.com

Post

Note that the quick and easy way to handle lossy integrators is to note they're an exponential decay.

Code: Select all

// these functions are much used for other purposes
float nroot(float n, float r) { return powf(n, 1.0f / r); }
float apow(float n, float r) { return logf(n) / logf(r); }

namespace lossy_integrator
{
	// only really useful for calculating exponential decay
	float get_fraction(float coefficient, float time) { return powf(1.0f - coefficient, time); }
	float get_coefficient(float fraction, float time) { return 1.0f - nroot(fraction, time); }
	float get_time(float coefficient, float fraction) { return apow(fraction, 1.0f - coefficient); }
};
This allows you to specify that you want to reach a certain percentage within the destination value in a specific number of steps.

Say you want to get to 1/100 in 1000 samples:

Code: Select all

coefficient = lossy_integrator::get_coefficient(1.0f / 100.0f, 1000.0f);
Of course these are all just helpful "wrapper" functions for very basic computations. You might want to build them into an object implementing "lossyIntegrator" or similar.

For the specific case you're using it, I actually have a "parameter filter" object that I can instantiate. It is a template where you specify the maximum number of filtered parameters to eliminate dynamic allocation.

Code: Select all

parameter_filter<16> pf;
I've actually got templates for types as well, so it works on various int formats and so on.

The object itself has push/pop state functions. Rather than a stack I use a single storage just to be able to get the same coefficients across multiple channels.

It is used like:

Code: Select all

pf.add(&destination_parameter, target_value, optional_coefficient);
This way you can go through a loop executing each filter and keep track of the time (see the get_time function) for each to reach within the specified fraction you need. After each filter runs for that length of time just set the destination to equal the target value. While you aren't automating several parameters at once, the overhead is minimal. Just a check of an int, no more.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

earlevel wrote: Iƒ you traveled back in time, continuously, and could watch the world, you'd see people walking backwards, until you arrived at your time destination, when they would start moving forward again. As I mentioned earlier in the thread, the alternative is to jump to the destination and de-glitch, if that's the effect you want.
Unfortunately-- Truly artifact-free and continuously variable translation along the time axis requires incredibly precise biasing of the flux capacitors.

Some references indicate that such precise biasing was first achieved circa 1895 by George Wells, though it remains one of the more difficult and cpu-hungry tasks to this very day!

Post

OK, then, I guess I DO understand it! I just have to keep an eye on the read/write pointer relationship to make sure it does what I want.

Thank you.

Post

Hi all,
I'm trying your suggestions on my delay. I implemented a one pole filter and I'm using it on the delay time knob values. But I still hear zipper noise when I change that knob :( :dog: . Anyone can help me on exactly how implement the smoothing function?

my filter function for smoothing:

Code: Select all

a0 = 0.1;
b1 = 1 - a0;
...
inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
When the knob changes value I initialize the filter and call the smooth function until the target value is reached.

Thanks!

Post

luketre wrote:

Code: Select all

a0 = 0.1;
b1 = 1 - a0;
...
inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
Your smoothing time constant is VERY short. Remember you're working at audio rates, with decay of 0.9 per sample, the half-time is somewhere around 6.5 samples, which at 44.1kHz sampling is around 0.147 ms. This is far too short to properly filter out typical zipper.

To calculate exponential time constants, try: a0 = 1 - exp(-1 / (time * samplerate)) where time (in seconds) is essentially the time to reach 0.63 of the target value. You could probably try something like time=0.05 (=50ms) as a starting point, then adjust for taste.

Post

Not sure about an all-pass for smoothing but I use a 2 pole low pass set to 20hz to remove audio components from the control signals.

http://denniscronin.net/dsp/vst.html

See the "flange" one which has a couple smoothed control inputs. I have to evaluate the control filter on every sample but you could probably optimize that out. Doesn't seem to have much impact tho given today's CPU speeds.

Post

ChewingAluminumFoil wrote:Not sure about an all-pass for smoothing but I use a 2 pole low pass set to 20hz to remove audio components from the control signals.
Note that you may want to ensure that you don't have overshoot in your step response. A one-pole (or two of them if you need more smoothing) ensures that.
My audio DSP blog: earlevel.com

Post

mystran wrote:
luketre wrote:

Code: Select all

a0 = 0.1;
b1 = 1 - a0;
...
inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
Your smoothing time constant is VERY short. Remember you're working at audio rates, with decay of 0.9 per sample, the half-time is somewhere around 6.5 samples, which at 44.1kHz sampling is around 0.147 ms. This is far too short to properly filter out typical zipper.

To calculate exponential time constants, try: a0 = 1 - exp(-1 / (time * samplerate)) where time (in seconds) is essentially the time to reach 0.63 of the target value. You could probably try something like time=0.05 (=50ms) as a starting point, then adjust for taste.
Thank you mystran, I've understand your reply and checked it here https://ccrma.stanford.edu/~jos/fp/Time ... _Pole.html. Unfortunately, my zipper noise still remains. I'll give a try by increasing more the decaytime.
Last edited by luketre on Wed Jun 25, 2014 6:25 am, edited 1 time in total.

Post

ChewingAluminumFoil wrote:Not sure about an all-pass for smoothing but I use a 2 pole low pass set to 20hz to remove audio components from the control signals.

http://denniscronin.net/dsp/vst.html

See the "flange" one which has a couple smoothed control inputs. I have to evaluate the control filter on every sample but you could probably optimize that out. Doesn't seem to have much impact tho given today's CPU speeds.
Thank you. I'll try your solution also.

Post

luketre wrote:Hi all,
I'm trying your suggestions on my delay. I implemented a one pole filter and I'm using it on the delay time knob values. But I still hear zipper noise when I change that knob :( :dog: . Anyone can help me on exactly how implement the smoothing function?

my filter function for smoothing:

Code: Select all

a0 = 0.1;
b1 = 1 - a0;
...
inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
When the knob changes value I initialize the filter and call the smooth function until the target value is reached.

Thanks!
You may just be stating it in an awkward way that you don't intend, but...you do not want to initialize the filter when it changes. You'll have many incoming changes as the knob turns, and the filter should just continue to roll. If you're running it at the sample rate, b1 should be in the 0.999 area (and a0 = 1 - b1). Looking at Echo Farm, it looks like I used 10 Hz for most of the knobs, and I wanted the delay knob to be very slushy to get the effect of a tape delay changing, so it's 0.7 Hz. That's about 0.9986 and 0.9999, respectively, at 44.1 kHz. In other words, your a0 should be a couple of orders of magnitude smaller.

If you're running the filter at 44.1 kHz, for instance, you have your filter cutoff at 740 Hz, and that's why you're getting zipper noise. But you also can't be resetting the filter when the knob moves—that will give you zipper noise too.
My audio DSP blog: earlevel.com

Post

Use an abstraction that can allow you to control the delay in line with your intended behaviour.

Your behaviour has a maximum pitch increase and decrease, and thus you should model your delay using a pitch bend.

The play head then determines how many sample to read based on the pitch :)

Post Reply

Return to “DSP and Plugin Development”