Algorithms for modulated delay

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Hey folks, this one has puzzled me for a long time. Let's say you have a delay buffer (essentially float*) and there are no constraints on the delay length (so you cannot state it will be say 200ms to 250ms in which case it could be easily optimizable using 2 buffers) and you want to modulate the delay using some external engine, so again you cannot predict what the next delay size will be. The aim is perfect audio quality and low CPU consumption. So far I can see just these methods:

a) When the delay increases, add zeroes, if it decreases remove some samples.
This obviously sucks, lots of zipper noise...

b) Resize the delay buffer using some kind of interpolation (probably just linear or cubic).
Good, but takes a lot of CPU if the delay is large (like more than a few seconds).

Any others? There is the possibility of some sort multiwrite delay, where the output will be interpolated from the input and also when inserting samples to the delay, one would potentially write multiple samples, but the problem is this won't be easy if there are no constraints on the delay length.
Vojtech
MeldaProduction MSoundFactory MDrummer MCompleteBundle The best plugins in the world :D

Post

I don't know what b is supposed to do?
a) is the only way, but then if you have a jump in the delay, you will have a problem. So it is not a proper solution.
Actually, there is no proper solution if the delay can vary to any value.

Post

b) Matches the physical interpretation of a moving listener integrating over "plane-wave" space at variable speeds provided that the head of the buffer corresponds with physical position of a point-source. e.g. a listener moves away from a sound-source <=> tape reader of buffer moves away from tape head and so you always resample back to whatever was equivalent to 1 block of data.

By modeling all tape movements in terms of moving listener and static sound-source, you get the implicit constraints that the tape reader can never read past the tape head (distance between sound-source and listener is non-negative), tape-head will never write multi-samples (sound-source is never moving and so you need not resample the entire tape to make room), tape length can be made long enough to capture entire duration of RT60 in atmosphere (super-distant listener will hear nothing), super-sonic listener can be modeled (basically integrating over tape along +- directions perhaps several times)

Post

Yes, the problem is the CPU consumption of b). Any ideas how to improve it?
Vojtech
MeldaProduction MSoundFactory MDrummer MCompleteBundle The best plugins in the world :D

Post

MeldaProduction wrote:Yes, the problem is the CPU consumption of b). Any ideas how to improve it?
If audio quality is a higher priority, then vary the order of the Lagrange interpolation methods (linear, quadratic, cubic, etc) according to delta change in delay taps. For large jumps, windowed sinc is possible but also ridiculously expensive.

If CPU usage is higher priority, then oversample input stream at a fixed rate (fast algorithms exist here), then do linear interpolation for variable delta delay changes, followed by downsample so that the high-frequency loss isn't as apparent?

Post

MeldaProduction wrote: b) Resize the delay buffer using some kind of interpolation (probably just linear or cubic).
Good, but takes a lot of CPU if the delay is large (like more than a few seconds).
Why would the delay length matter in terms of CPU? You just keep as much input in a "long" ring buffer as you might possibly need in the worst case. Then you calculate the read position backwards from your current write offset and perform an interpolated read (using whatever method) per output sample.

Obviously if your modulation causing a lot of frequency modulation then aliasing can become a problem and you should probably be prepared to either output at varying sampling rates or vary the interpolation kernel cutoff.. but like... I wouldn't ever bother actually resampling the whole buffer, because you can do that "on-demand" from straight history buffer for just those samples you're actually going to use.

Post

nonnaci: The problem is it is very demanding even for linear interpolation when the delay length is big enough, since every time it changes, it needs to resample the whole thing.

mystran: That's an interesting idea. It wouldn't have the classic "tape" pitched response, which I actually quite like, but that's fine, a different character. The problem is it would take a hell of a lot more CPU when not modulated. I tried things like that many times years ago, but I always ended up with unacceptable amount of zipper noise. The oversampling could help indeed, but then the question is whether it would actually be saving CPU, probably yes when modulated. The years ago I tried various "differential filtering" methods and it just didn't work...
Vojtech
MeldaProduction MSoundFactory MDrummer MCompleteBundle The best plugins in the world :D

Post

I'm not sure I understand why delay length affects the computation time of linear interpolation. Linear interpolation only ever involves interpolating between two samples, right? So just interpolate on a per-sample basis. Why would you need to resample the whole buffer at once?
MeldaProduction wrote:(so you cannot state it will be say 200ms to 250ms in which case it could be easily optimizable using 2 buffers)
I'm not super knowledgeable in this area. How would you use 2 buffers to optimize a modulated delay when the delay size is bounded?

Also, for a), would it be reasonable to just always add and remove samples from directly in front of the write pointer so that the read pointer is not interrupted?

Post

That's because the b) idea was about resizing the entire buffer. Like stretching images. The great thing about this is that it essentially simulates the tape and from my experience it is pretty much entirely zipper-noise free. The idea about 2 buffers is that the first delay doesn't need to be modulated, so it is very fast in every way and the other one is, but is small, so the buffer stretching is quick.

As for "a) not stretching the delay buffer": One could simply move the read pointer and keep the delay buffer long, but the problem is that the read pointer would need to move slow enough and in practice I found that it would need to move very very slowly... too slow imho... otherwise the zipper noise starts to occur.
Vojtech
MeldaProduction MSoundFactory MDrummer MCompleteBundle The best plugins in the world :D

Post

MeldaProduction wrote:nonnaci: The problem is it is very demanding even for linear interpolation when the delay length is big enough, since every time it changes, it needs to resample the whole thing.
You don't resample the input (the contents of the delay buffer), you resample the output (what you read from the delay buffer). You're changing the rate you read the buffer, not changing the rate that the buffer was originally written after the fact, right? Imagine it's a tape loop that runs at a constant speed, but the play head can be slid forward or back in position/time. Moving the play head doesn't change what's on the tape.
Last edited by SMH on Mon Jun 26, 2017 2:41 pm, edited 1 time in total.

Post

If I understand you correctly, you are trying to cope with a situation where you have, say, a 250ms circular buffer which is producing a delay of 250ms, and you are then asked to increase the delay to, say, 500ms - and you need to find a 'nice' way to handle the fact that you don't have the data from 250ms-500ms ago.

You could just reallocate and fill with zeros and set the read pointer to 500ms, but this would give 250ms of silence which would sound glitchy.

Your resampling proposal is to allocate a 500ms buffer and stretch the 250ms buffer to fill it, and then immediately read with a 500ms delay. In this case, instead of a 250ms silence, you would hear the delayed sound drop down an octave and halve in tempo for 500ms and then revert back to being normal pitch.

But rather than resample the buffer, you could just reallocate it to 500ms (you don't even need to initialise the extra samples in the new buffer) and let it fill as normal, one sample per tick - but limit the slew rate of your read pointer so that it never retreats from the write pointer faster than, say, half a sample per tick. The effect would be that after 250ms your 500ms buffer would be full again, and after 500ms the read pointer would be at the end of the buffer. For the duration of those 500ms while the read pointer was retreating at half a sample per tick, the delayed sound would again drop by an octave and halve in tempo, just as before, but with no need for resampling - just interpolation.

As a bonus, instead of just limiting the slew rate you could make it accelerate and decelerate nicely, which would be much harder to do in the resampling model.

Post

kryptonaut wrote:If I understand you correctly, you are trying to cope with a situation where you have, say, a 250ms circular buffer which is producing a delay of 250ms, and you are then asked to increase the delay to, say, 500ms - and you need to find a 'nice' way to handle the fact that you don't have the data from 250ms-500ms ago.

You could just reallocate and fill with zeros and set the read pointer to 500ms, but this would give 250ms of silence which would sound glitchy.

Your resampling proposal is to allocate a 500ms buffer and stretch the 250ms buffer to fill it, and then immediately read with a 500ms delay. In this case, instead of a 250ms silence, you would hear the delayed sound drop down an octave and halve in tempo for 500ms and then revert back to being normal pitch.

But rather than resample the buffer, you could just reallocate it to 500ms (you don't even need to initialise the extra samples in the new buffer) and let it fill as normal, one sample per tick - but limit the slew rate of your read pointer so that it never retreats from the write pointer faster than, say, half a sample per tick. The effect would be that after 250ms your 500ms buffer would be full again, and after 500ms the read pointer would be at the end of the buffer. For the duration of those 500ms while the read pointer was retreating at half a sample per tick, the delayed sound would again drop by an octave and halve in tempo, just as before, but with no need for resampling - just interpolation.

As a bonus, instead of just limiting the slew rate you could make it accelerate and decelerate nicely, which would be much harder to do in the resampling model.
But why not just always have the buffer filled to the longest possible delay time and tap it wherever you want the delay? RAM is cheap and plentiful. I'm pretty sure this is how most every digital delay works.

Post

kryptonaut basically suggests what I proposed before - slow down the pointer enough. But the problem is, whenever I tried this, it just needed to be extremely slow (what is extreme here is a bit of a question here, but well, it was too slow for me :D ).

SMH, I understand, but the problem is that doesn't necessarily fix the issue - you'd need pretty heavy constraints. Imagine you have a range of 1ms to 60 seconds. Ok, so first, you allocate buffer of 60 seconds, that's a lot and caches especially don't like that. Also you need to know the 60 seconds is the limit. But ok, all of this is manageable. But then you just need the pointer to move slowly enough. Or we could perform the output resampling, but that doesn't really help the problem - imagine you are a really crazy person making the delay jump from 100ms to 10 seconds and back all the time. With slowly moving pointer, you wouldn't succeed at all. If you jump and oversample the output, you'd have a complete discontinuity there, so I don't think any reasonable oversampling could save you. And the oversampling itself takes a lot of CPU as well, while the delay itself when not modulated takes almost nothing. Sure this is an extreme example, but with the full delay buffer rescaling it actually works! :) And it takes additional CPU only when modulating it, when it is static, it's superfast.
Vojtech
MeldaProduction MSoundFactory MDrummer MCompleteBundle The best plugins in the world :D

Post

MeldaProduction wrote: SMH, I understand, but the problem is that doesn't necessarily fix the issue - you'd need pretty heavy constraints. Imagine you have a range of 1ms to 60 seconds. Ok, so first, you allocate buffer of 60 seconds, that's a lot and caches especially don't like that. Also you need to know the 60 seconds is the limit. But ok, all of this is manageable. But then you just need the pointer to move slowly enough. Or we could perform the output resampling, but that doesn't really help the problem - imagine you are a really crazy person making the delay jump from 100ms to 10 seconds and back all the time. With slowly moving pointer, you wouldn't succeed at all. If you jump and oversample the output, you'd have a complete discontinuity there, so I don't think any reasonable oversampling could save you. And the oversampling itself takes a lot of CPU as well, while the delay itself when not modulated takes almost nothing. Sure this is an extreme example, but with the full delay buffer rescaling it actually works! :) And it takes additional CPU only when modulating it, when it is static, it's superfast.
Why do you care if your delay performs better at 60 seconds than at 1 ms? Who would expect that? It's not like a delay effect uses a lot of CPU power. You're proposing to use a lot more CPU to avoid the very minor cost of a cache miss.

And *something* has to be the time limit. You can't instantly jump to a delay time that's longer than what you recorded, because you overwrote that part of the circular buffer already. Your buffer size is for the maximum possible delay time. If it's 60 seconds then it's 60 seconds. If it's a day it's a day. Who cares?

And your "crazy" example *should* make an audible click, because it's an instantaneous jump in delay time. (Who said "oversampling"? REsampling.) It's also not common (or useful) to modulate delays with waveforms with instantaneous transitions.

Post

The cache miss was just a little example of a negative. The thing is, I know what I'm after isn't physically possible, but isn't what we do exactly that - trying what was impossible? ;)

Sorry for the confusion related to resampling. Now, I must misunderstand something - imagine you have a 20 seconds delay at one point, let's round it to a million samples. The user then changes the delay (slowly :) ) to 100ms (say 4000 samples). So at this point resampling as I understand it would mean that the output buffer, lets say 1024 samples, would actually need to be processing 250k samples. I mean again you can just get the interpolated taps, but the question is if that could even be called resampling. It's more like interpolation. And it just wouldn't work unless the delay modulation was slow enough.
Vojtech
MeldaProduction MSoundFactory MDrummer MCompleteBundle The best plugins in the world :D

Post Reply

Return to “DSP and Plugin Development”