Karplus-Strong in C++

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
int buffer_pos=0;

/// noise buffer
double  *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 karplus_strong_example class:

// allocate noise buffer
noise_buffer = new double [l_buff];
for (int i=0; i<l_buff; i++)
  noise_buffer[i]=0.0;

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++)
    {
      out[chanCNT][sampCNT]=noise_buffer[buffer_pos];
    }


    // increment buffer position
     buffer_pos++;
     if (buffer_pos>=l_buff)
      buffer_pos=0;
}

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++)
  {
    if(buffer_pos+smoothCNT<l_buff)
      sum+=noise_buffer[buffer_pos+smoothCNT];
    else
      sum+=noise_buffer[smoothCNT];
  }
  noise_buffer[buffer_pos] = gain*(sum/l_smooth);

Compiling

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 karplus_strong.


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.


Exercises