One String
A buffer of noise and a two-sample average. That is a plucked string, and it cannot be tuned properly, for a reason you cannot fix.
What it is
Six strings. Pluck them. Drag across the middle for a rounder attack, nearer the edges for a brighter one.
There’s no oscillator here and no wavetable. Each note is a short buffer of white noise being averaged with itself, over and over.
How it works
Karplus-Strong, and it’s genuinely two lines. Fill a buffer of N samples with noise. Then repeatedly: output the oldest sample, replace it with the average of itself and its neighbour, move on. That’s the whole instrument.
The buffer length sets the pitch, because the contents cycle every N samples, so the
frequency is exactly sampleRate / N. The averaging is a one-pole lowpass applied once per
round trip, so each lap through the buffer removes a little more of the high content.
That’s why the pluck starts as a bright noise burst and settles into a warm tone, and the tests measure it. The spectral centroid falls by more than 20% between the attack and a second and a half later, while the pitch stays put.
What surprised me
You can’t tune it. Not properly.
The delay line is a whole number of samples, so the only available pitches are sampleRate
divided by an integer. At 44,100 Hz, asking for 440 gives you a delay of 100 samples and a
pitch of 441 Hz, 3.9 cents sharp. There’s nothing between 441 and 445.45.
It gets worse as you go up, because there are fewer samples to round. At 1760 Hz the delay is 25 samples, and half a sample of error is 2%, more than a third of a semitone. A high note on this instrument is audibly wrong, and no amount of care in the rest of the algorithm helps, because the pitch is quantised by the sample rate itself.
The fix real implementations use is a fractional delay: interpolate between two samples at the end of the buffer so the effective length can be 100.23. That’s a whole day’s work on its own, and the fact that it’s necessary rather than a refinement is the part I hadn’t appreciated.
There was also a bug in my measuring. My pitch detector reported every string about 1% flat. Day 7 taught me that autocorrelation reports multiples of the true period, and my fix there was to take the smallest lag scoring near the best, which stops octave errors but lets a neighbouring lag win instead. It now only considers submultiples of the best lag, which is what the octave problem actually is.
What I would do next
Fractional delay by linear interpolation, and see how much of the sharpness goes away.