The Karplus-Strong algorithm is a proto-physical model. The underlying theory is covered in the Karplus-Strong Section of the Sound Synthesis Introduction. Although the resulting sounds are very interesting, the Karplus-Strong algorithm is easy to implement, especially in C/C++. It is based on a single buffer, filled with noise, and a moving average smoothing.
The Noise Buffer
Besides the general framework of all examples in this teaching unit, the
karplus_strong_example needs just a few additional elements, defined in the class`s header:
// the buffer length
int l_buff = 600;
// the 'playback position' in the buffer
/// noise buffer
/// length of moving average filter
int l_smooth = 10;
// feedback gain
double gain =1.0;
Note that the pitch of the resulting sound is hard-coded in this example, since it is based only on the sampling rate of the system and the buffer length. In contrast to the original Karplus-Strong algorithm, this version uses an arbitrary length for the moving average filter, instead of only two samples. This results in a faster decay of high frequency components.
Initializing the Buffer
Since the noise buffer is implemented as a pointer to an array of doubles,
it first needs to be allocated and initialized. This happens in the constructor of the
// allocate noise buffer
noise_buffer = new double [l_buff];
for (int i=0; i<l_buff; i++)
Plucking the Algorithm
Each time the Karplus-Strong algorithm is excited, or plucked, the buffer needs to be filled with a sequence of random noise. At each call of the JACK callback function (
process), it is checked, whether a new event has been triggered via MIDI or OSC.
If that is true, the playback position of the buffer is set to
0 and each sample of the
noise_buffer is filled with a random double between -1 and 1:
cout << "Filling buffer!";
buffer_pos = 0;
for(int i=0; i<=l_buff; i++)
noise_buffer[i]= rand() % 2 - 1;
Running Through the Buffer
The sound is generated by directly writing the samples of the
noise_buffer to the JACK output buffer. This is managed in a circular fashion with the
buffer_pos counter. Wrapping the counter to the buffer size makes the process circular. This example uses a stereo output with the mono signal.
for(int sampCNT=0; sampCNT<nframes; sampCNT++)
// write all input samples to output
for(int chanCNT=0; chanCNT<nChannels; chanCNT++)
// increment buffer position
Smoothing the Buffer
The above version results in a never-ending oscillation, a white tone. The timbre of this tone changes with every triggering, since a unique random sequence is used each time.
With the additional smoothing, the tone will decay and lose the high spectral components, gradually.
This is done as follows:
// smoothing the buffer
double sum = 0;
for(int smoothCNT=0; smoothCNT<l_smooth; smoothCNT++)
noise_buffer[buffer_pos] = gain*(sum/l_smooth);
To compile the KarplusStrongExample, run the following command line:
g++ -Wall -L/usr/lib src/yamlman.cpp src/main.cpp src/karplus_strong_example.cpp src/oscman.cpp src/midiman.cpp -ljack -llo -lyaml-cpp -lsndfile -lrtmidi -o karplus_strong
This call of the g++ compiler includes all necessary libraries and creates the binary
Running the Example
The binary can be started with the following command line:
./karplus_strong -c config.yml -m "OSC"
This will use the configurations from the YAML file and wait for OSC input. The easiest way of triggering the synth via OSC is to use the Puredata patch from the example's directory.
Make the buffer length and filter length command line or realtime-controllable parameters.
Implement a fractional noise buffer for arbitrary pitches.