Parameter smoothing for delay line?
-
- KVRian
- Topic Starter
- 626 posts since 30 Aug, 2012
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?
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?
-
- KVRAF
- 3080 posts since 17 Apr, 2005 from S.E. TN
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.
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.
-
- KVRian
- 653 posts since 4 Apr, 2010
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.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?
My audio DSP blog: earlevel.com
- KVRAF
- 12555 posts since 7 Dec, 2004
Note that the quick and easy way to handle lossy integrators is to note they're an exponential decay.
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:
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.
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:
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.
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); }
};
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);
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;
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);
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.
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.
-
- KVRAF
- 3080 posts since 17 Apr, 2005 from S.E. TN
Unfortunately-- Truly artifact-free and continuously variable translation along the time axis requires incredibly precise biasing of the flux capacitors.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.
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!
-
- KVRer
- 6 posts since 10 Jun, 2014 from Milan
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 . Anyone can help me on exactly how implement the smoothing function?
my filter function for smoothing:
When the knob changes value I initialize the filter and call the smooth function until the target value is reached.
Thanks!
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 . 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; };
Thanks!
- KVRAF
- 7893 posts since 12 Feb, 2006 from Helsinki, Finland
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.luketre wrote:Code: Select all
a0 = 0.1; b1 = 1 - a0; ... inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
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.
-
ChewingAluminumFoil ChewingAluminumFoil https://www.kvraudio.com/forum/memberlist.php?mode=viewprofile&u=248970
- KVRist
- 73 posts since 28 Jan, 2011 from Scottsdale, AZ
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.
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.
-
- KVRian
- 653 posts since 4 Apr, 2010
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.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.
My audio DSP blog: earlevel.com
-
- KVRer
- 6 posts since 10 Jun, 2014 from Milan
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.mystran wrote: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.luketre wrote:Code: Select all
a0 = 0.1; b1 = 1 - a0; ... inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
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.
Last edited by luketre on Wed Jun 25, 2014 6:25 am, edited 1 time in total.
-
- KVRer
- 6 posts since 10 Jun, 2014 from Milan
Thank you. I'll try your solution also.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.
-
- KVRian
- 653 posts since 4 Apr, 2010
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.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 . Anyone can help me on exactly how implement the smoothing function?
my filter function for smoothing:
When the knob changes value I initialize the filter and call the smooth function until the target value is reached.Code: Select all
a0 = 0.1; b1 = 1 - a0; ... inline float smooth(){ z1 = (x * a0) + (z1 * b1); return z1; };
Thanks!
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
- KVRist
- 168 posts since 19 Apr, 2014 from London
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
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