I want to use a simple implementation of the Karplus-Strong algorithm to generate pluck sounds and chords.My existing code seems correct but is slow when more than a few plucks are being generated. Is there a way to improve the performance of this. In particular is there a better way to use numpy here, or would something like a collections.deque be faster.import numpy as npsample_rate = 44100damping = 0.999def generate(f, vol, nsamples):    Generate a Karplus-Strong pluck.    Arguments:    f -- the frequency, as an integer    vol -- volume, a float in [0.0, 1.0]    nsamples -- the number of samples to generate. To generate a pluck t    seconds long, one needs t * sample_rate samples. nsamples is an int.    Return value:    A numpy array of floats with length nsamples.        N = sample_rate // f    buf = np.random.rand(N) * 2 - 1    samples = np.empty(nsamples, dtype=float)    for i in range(nsamples):        samples[i] = buf[i % N]        avg = damping * 0.5 * (buf[i % N] + buf[(1 + i) % N])        buf[i % N] = avg    return samples * voldef generate_chord(f, nsamples):    Generate a chord of several plucks    Arguments:    f -- a (usually short) list of integer frequencies    nsamples -- the length of the chord, a chord of length t seconds needs    t * sample_rate, an integer.    Return value:    A numpy array        samples = np.zeros(nsamples, dtype=float)    for note in f:        samples += generate(note, 0.5, nsamples)    return samplesif __name__ == __main__:    import matplotlib.pyplot as plt    from scipy.io.wavfile import write    strum = generate_chord(  # A Major        [220, 220 * 5 // 4, 220 * 6 // 4, 220 * 2], sample_rate * 5)    plt.plot(strum)    plt.show()    write(pluck.wav, sample_rate, strum)This produces a sound file in pluck.wav with a waveform like this:
Karplus Strong pluck generation
python;numpy;audio
I was trying to improve your generate function and stumbled on a potential bug. You continuously update the buffer by adding two following values. This should be vectorizable easily by acting on the whole buffer at the same time, by doing something like this:for i in range(0, nsamples - N, N):    samples[i: i + N] = buf[:]    buf = damping * 0.5 * (buf + (np.roll(buf, -1)))# fractional bufferi += Nk = nsamples - iif k:    samples[i:] = buf[:k]This differs from your algorithm for every time step (i.e. one pass on the buf) only in the last element. This is because this code acts on the whole array at \$t_{i}\$ to generate the buffer at \$t_{i+1}\$, whereas your code uses the value of buf[0] already from \$t_{i+1}\$.If you can live with this difference (I can not check if it sounds any different, right now), the code execution time goes down from about 0.71 seconds to 0.20 seconds.