Karplus strong implementation issue

Hi everyone,
I found sound of karplus-strong to be really interesting. I’ve succeeded running simple code on stm32f4 discovery board. Now I would like to expand it’s functionality but my idea seems to be wrong.

Here’s the code which calculates nth value of karplus-strong

wavetable_pointer += WTB_LEN * pitch / SAMPLERATE;
if (wavetable_pointer > WTB_LEN)
    wavetable_pointer -= WTB_LEN;
current_sample = (uint16_t)  wavetable_pointer; 
current_value = noise[current_sample];
noise[current_sample] = 0.5f * (current_value + prev_value);
prev_value = current_value;
return current_value;

I’m using this function to fill the audio buffer. In basic example everything worked fine. I’ve triggered it once and the sound decayed in time.

Next I connected stm32 with midi keyboard, wrote parser for it and added basic envelope. With no modifications to karplus-strong part I can trigger any note for about one second, after that the sound decayed completely as noise table contains approximately only zeros. So I’ve figured out the solution that unfortunately doesn’t work. I’ve hardcoded constant copy of noise table. When the program finds new note to play I’m simply restoring the noise table:

for (int i = 0; i < WTB_LEN; i++) {
    noise[i] = noise_copy[i];
}

Unfortunately it messes all sound. Instead of guitar plucked timbre I got noise for given pitch.
Any hints on how to make it work?

I’m not really sure how to read that code.

EDIT: I didn’t know about this historical implementation. See my next post.

My old, incorrect answer

Karplus strong is basically a very short, pitch-tracking comb filter with a low pass in the feedback, fed by a burst of some sort of audio, e.g. noise.

The code you posted
A) reads noise from a wavetable which I can only assume was loaded into the noise array beforehand
B) Runs an averaging filter over that array multiple times

There are multiple problems, or maybe I don’t get how it’s supposed to work.

Regarding A):
Why are you reading the noise with different speeds depending on the pitch? Noise is noise, there’s no need to read it faster or slower. It’s random. You may want to look at pseudo random number generator a and ditch the table entirely.

Regarding B):
That’s not a comb filter. It’s an averaging filter. You need a comb filter that has a fractional delay so that it can track the pitch. I’ll post a link soon, give me a second

Okay, I think I now understand where this code comes from. Is it this example? Sound and Music - The Karplus-Strong Algortihm

What’s presented there is probably the simplest form of karplus strong, but it’s musically not very useful, IMO.

  1. The fixed size of the buffer means that pitch is static (not modulatable) and discrete (there will be many pitches where the integer-sized buffer is a little too long or a little to short to represent the correct pitch. The length would need to be fractional. E.g. 440Hz at 48kHz sampling rate would require a buffer with length of 109.0909… samples).
  2. The averaging filter does the job of the lowpass filtering but it’s also static, meaning that you can’t change the damping of the virtual string.
  3. The comb filter is - sort of - realized by repeating the same buffer over and over. However that means that the initial noise burst can’t be longer than the length of the buffer which is fine for simple string pluck sounds. However, you’re locking yourself out of a lot of “bowed” type of sounds.

Here’s how I think you should implement it:

                                 +------------+     
                                 |            |     
                           +-----+ Lowpass    +---+ 
                           |     |            |   | 
                           |     +------------+   | 
                           |                      | 
+-------+    +-----+       |     +------------+   | 
|       |    |     |       v     |            |   | 
| Noise +--->| Env +------>+---->| Frac Delay +---+----->
|       |    |     |             |            |       
+-------+    +-----+             +------------+       

For the noise, you can use a simple LFSR noise generator.
For the fractional delay, you can use linear interpolation for now, but eventually you’ll want to improve this with a higher order interpolation. The reason is that linear interpolation actually dampens the high frequencies depending on the fractional delay - resulting in some notes being more damped than others.
For the lowpass, a simple 1 pole lowpass filter will do just fine.

Historically, Karplus Strong synthesis is this: an array filled with noise, played in a loop, each sample being replaced by the average of this sample and the previous one in the array. Change the size of the array to change the pitch.

What’s wrong in your implementation is that you use a fixed sized array (of size WTB_LEN). The size doesn’t vary with the pitch. Instead, you skip or repeat samples in the array. As a result your averaging is not performed properly.

For example, if WTB_LEN * pitch / SAMPLERATE evaluates to the value 3, you will play and modify every third sample in the wavetable, then wavetable_pointer will wrap around and you’ll play samples which have not been averaged properly.

This leaves you with two strategies:

  • Stick to the original implementation, and use a variable size array, with an increment of 1.
  • Implement the averaging properly. If the increment is 4, 3 values of the noise array will have to be updated at each loop – ie you also need to average the samples that you skip.

Another issue with your implement: I assume the type of WTB_LEN * pitch / SAMPLERATE and wavetable_pointer is integer? Then you won’t get accurate pitches.

1 Like

Here’s some pseudo-code for my last post:

float generateNoise(); // implement LFSR here
float getAndAdvanceEnvelope(); // this is your evnelope code
float lowPass1P(float input); // implement 1pole lowpass filter here
float fractionalDelayRead(float delayInSamples); // implement fractional delay here
void fractionalDelayWrite(float input); // implement fractional delay here

float getNextSample(float currentPitch)
{
    const float noise = generateNoise();
    const float envelopeValue = getAndAdvanceEnvelope();
    const float noiseBurst = noise * envelopeValue;

    const float delayInSamples = sampleRate / currentPitch;
    const float output = fractionalDelayRead(delayInSamples);

    const float feedbackSignal = lowPass1P(output);
    const float delayInput = feedbackSignal + noiseBurst;
    fractionalDelayWrite(delayInput);

    return output;
}
1 Like

Thanks a lot for the responses!

@TheSlowGrowth
I’m testing audio ideas first using python. The algorithm was taken from: Understanding the Karplus-Strong with Python (Synthetic Guitar Sounds Included) | Frolian's blog
Thanks a lot for a code example. I will try to implement your solution and see the results. For sure I have to read more about filters you’ve mentioned.
I have a little experience in dsp, do you recommend any resources on that topic? I have some dsp books but there’s a lot of equations which are not clear to me yet. First I want to find some code examples, try to implement it in python and see results, make some plots, then I will try to convert it to embedded.

@pichenettes
So if I decide to stick to the original implementation then I have to calculate wavetable size and fill it with random numbers on each new note (pitch) event, right? As I have no experience I’m not sure how expensive it will be for the cpu?
Implementing averaging seems to be more efficient. For now I’m not so confident about the code and how it should look like but I will try to implement your advices and see the results.

wavetable_pointer is float that I’m casting to integer. What it means:

Then you won’t get accurate pitches.

I mean, I’m not sure if you try to say that the results will be slightly different from the estimated pitch, or they will be totally different

@TheSlowGrowth
Could you please explain what’s the difference between:
float fractionalDelayRead(float delayInSamples);
and
void fractionalDelayWrite(float input);
I see fractionalDelayWrite is void, I assume there’s some global state but still not sure what this function does

The most logical way of putting the delay in a function would probably look like this:

float fractionalDelay(float input, float delayInSamples);

However, due to the feedback you have to read the delay output before you can write to the delay input. That’s why I split it into two functions. I would implement them like this, assuming you’re using C++:

template<int maxDelayInSamples>
class FractionalDelay
{
public:
    FractionalDelay() :
        writeIdx_(0)
    {
        // clear buffer_ to 0's
    }

    float readRelativeWithoutAdvancing(float delayInSamples) const
    {
        const auto readIdxFractional = wrap(float(writeIdx_) - delayInSamples); 
        return /* interpolated readout here */;
    }

    void writeAndAdvance(float input)
    {
        buffer_[writeIdx_] = input;
        writeIdx_++;
        if (writeIdx_ >= maxDelayInSamples)
            writeIdx_ -= maxDelayInSamples;
    }
private:
    int writeIdx_;
    float buffer_[maxDelayInSamples];
};

You can do something similar in C by providing the buffer and the writeIdx as function arguments.

I think you did not read the python examples carefully.

Check the code for def karplus_strong(wavetable, n_samples). You will see that the increment is always 1.

Later, you can also observe that different notes are obtained by changing the wavetable size.

I think you tried to make a mish-mash of the karplus_strong function and the synthesize function.

If you fear that the random function is going to be computationally expensive, you can pre-compute a large table of random number, and copy from that same table every time a note starts. The cost will be that of copying samples from one memory location to another. If your sample rate is 48kHz and your minimum note frequency is 48 Hz, the worst case is copying 1000 numbers at each new note.

Ok. I couldn’t know from your code that it was a float, and “pointer” was very misleading, I thought it would be an integer.

If wavetable_pointer is an integer, some rounding errors will occur.

Hey people who contribute on this thread *!!!

Totally out of topic but…

just a small message to say thank you for all those precious informations|concepts|developments that are posted right here .

I can only understand 50% of the information wrote on this thread and most of the time, I’m just a lurker and don’t interact, but I know I will come back to those thread sooner or later once I progress in my journey.

MI forum is really a source of inspiration| education.

Thx for all the people ( and especially Emilie) for taking the time|energy to explain, develop, and share their knowledge with other

Those bites of codes, the schematics and the experience shared on this forum etc are an invaluable resource to understand, learn and progress this is really priceless!!!

*or other technical thread on MI

1 Like

Feel free to chime in and ask specific questions at any time. Everyone has to start somewhere. If you can phrase a specific question, you’re already on the right track.

Thx :pray:!
I will … at the right moment… l’habeas to do my homework first :grinning_face_with_smiling_eyes: