Tuning a modified Karplus-Strong loop
-
- KVRist
- Topic Starter
- 134 posts since 13 Apr, 2016
I've been playing around with KS and ran into a tuning problem that I don't understand.
As I understand it, the classic tuned KS algorithm is to create a delay line with a buffer size based on the integer wavelength, and then add an allPass filter to it's output to compensate for the fractional delay portion.
That works, and when I fill the buffer with a single sine wave (or any other wave), it's perfectly in tune.
But I don't like the way the higher notes decay shorter and I also don't like the sound of the filtered output, thanks to the averaging of the last two output samples.
My idea was to fill the buffer with a single sine wave (or other wave) and NOT average the last two output samples...basically keeping the delayLine intact...and then add my own filtering and adsr to the output for more control.
But when I do that, (removing the output averaging and putting it back into the delayLine), it changes the tuning slightly sharp and I don't know why, or how to compensate for it. I still run it through the allPass, but it doesn't seem to be enough.
Anyone have any ideas what I'm doing wrong here?
As I understand it, the classic tuned KS algorithm is to create a delay line with a buffer size based on the integer wavelength, and then add an allPass filter to it's output to compensate for the fractional delay portion.
That works, and when I fill the buffer with a single sine wave (or any other wave), it's perfectly in tune.
But I don't like the way the higher notes decay shorter and I also don't like the sound of the filtered output, thanks to the averaging of the last two output samples.
My idea was to fill the buffer with a single sine wave (or other wave) and NOT average the last two output samples...basically keeping the delayLine intact...and then add my own filtering and adsr to the output for more control.
But when I do that, (removing the output averaging and putting it back into the delayLine), it changes the tuning slightly sharp and I don't know why, or how to compensate for it. I still run it through the allPass, but it doesn't seem to be enough.
Anyone have any ideas what I'm doing wrong here?
- KVRist
- 347 posts since 20 Apr, 2005 from Moscow, Russian Federation
A quick answer: if by "averaging last two samples" you mean the usual [.5 .5] FIR (like here https://ccrma.stanford.edu/~jos/pasp/Ka ... rithm.html) then its delay is 0.5 samples and you need to adjust your overall delay time accordingly. If it's something else we need more details.
(E.g. initially you say it like you used only a delay and an allpass but did not mention any loss/dump/decay filters and then it took me to be confused to guess why you have any decay at all).
(E.g. initially you say it like you used only a delay and an allpass but did not mention any loss/dump/decay filters and then it took me to be confused to guess why you have any decay at all).
-
- KVRist
- Topic Starter
- 134 posts since 13 Apr, 2016
Thanks for your reply and sorry about the lack of details.
I calculated the following:
I have nothing else in the loop, just the main delay line, the allPass and the [.5 .5] filter.
What I'm looking to do is remove that [.5 .5] filter so the waveform in the delayLine stays intact, and use filtering external to the loop.
I thought that the allPass made up for the difference between the wavelength actually needed and the integer delayLine. But I guess the [.5 .5] filter adds more delay. What I don't know is how to calculate the difference needed.
I calculated the following:
Code: Select all
float waveLength = sampleRate / frequency;
int delayInSamples = (int)std::floor(waveLength); // set the delayLine buffer size
float fracDelay = waveLength - (float)delayInSamples;
float allPassDelay = (1.0f - fracDelay) / (1.0f + fracDelay); // set the allPass delay time
What I'm looking to do is remove that [.5 .5] filter so the waveform in the delayLine stays intact, and use filtering external to the loop.
I thought that the allPass made up for the difference between the wavelength actually needed and the integer delayLine. But I guess the [.5 .5] filter adds more delay. What I don't know is how to calculate the difference needed.
- KVRist
- 347 posts since 20 Apr, 2005 from Moscow, Russian Federation
I have nothing else in the loop, just the main delay line, the allPass and the [.5 .5] filter.
Then it's like I mentioned above: the [.5 .5] filter introduces 0.5 samples delay. So if you remove this filter, the overall delay of the remaining units should be increased by same 0.5 samples. Though I wonder how you get correct tuning w/o taking this half of the sample into account when the loss filter is there... Either way, are your sure you don't have any +/-1 delay error in you main integer delay line implementation? Such misalign could be easy to made if the [.5 .5] filter was integrated into it. E.g. it may depend on which of these two samples you treat as the actuall delay line output before and now (It's easy to debug by feeding the delay line with a single impulse and examining the output of the whole loop).
Another tip (most likely not related to your current problem though, since it should be an issue regardless of the loss filter being there or not):
fracDelay having the [0...1] range (as it is in your code) is usually not a very good idea - because when this delay value approaches 0, the allpass coefficient is right near 1 and the filter becomes quite numerically unstable (too "resonant" at higher frequencies) also resulting in notably large detuning in the HF range.
Usually it's more convenient to have fracDelay to be in [0.418...1.418] (or similar) range - there the allpass is more stable and a tuning drift in HFs is minimal. This could be very important if you're going to feed the buffer with some wideband waveform/excitation (see for example https://ccrma.stanford.edu/~juhan/pubs/jnam-dafx09.pdf section 2.1).
Then it's like I mentioned above: the [.5 .5] filter introduces 0.5 samples delay. So if you remove this filter, the overall delay of the remaining units should be increased by same 0.5 samples. Though I wonder how you get correct tuning w/o taking this half of the sample into account when the loss filter is there... Either way, are your sure you don't have any +/-1 delay error in you main integer delay line implementation? Such misalign could be easy to made if the [.5 .5] filter was integrated into it. E.g. it may depend on which of these two samples you treat as the actuall delay line output before and now (It's easy to debug by feeding the delay line with a single impulse and examining the output of the whole loop).
Another tip (most likely not related to your current problem though, since it should be an issue regardless of the loss filter being there or not):
fracDelay having the [0...1] range (as it is in your code) is usually not a very good idea - because when this delay value approaches 0, the allpass coefficient is right near 1 and the filter becomes quite numerically unstable (too "resonant" at higher frequencies) also resulting in notably large detuning in the HF range.
Usually it's more convenient to have fracDelay to be in [0.418...1.418] (or similar) range - there the allpass is more stable and a tuning drift in HFs is minimal. This could be very important if you're going to feed the buffer with some wideband waveform/excitation (see for example https://ccrma.stanford.edu/~juhan/pubs/jnam-dafx09.pdf section 2.1).
-
- KVRian
- 1000 posts since 1 Dec, 2004
Jon Dattorro has a good paper on this:
https://ccrma.stanford.edu/~dattorro/Ef ... nPart2.pdf
(his implementation doesn't have the 0.5+0.5 averaging either)
I've implemented the whole thing too... using a [0.5...1.5] range for the fractional delay (which puts the parameter in the -0.333...0.2 range I think) works well for me too. My delay time calculation:
https://ccrma.stanford.edu/~dattorro/Ef ... nPart2.pdf
(his implementation doesn't have the 0.5+0.5 averaging either)
I've implemented the whole thing too... using a [0.5...1.5] range for the fractional delay (which puts the parameter in the -0.333...0.2 range I think) works well for me too. My delay time calculation:
Code: Select all
int samples = (int)(time - 1.5f) + 1; // <0 rounds upwards!
float frac = time - samples;
float param = (frac - 1) / (frac + 1);