Using YAML for Configuration

At some point, a program might require more arguments on startup than a command line call can manage without complications. In that case it can help to work with configuration files, which are passed to the binary when starting it. This can be done in many ways, including plain text files or markup languages like XML, Markdown, JSON or others.

The examples in this module use YAML for passing configuration data. YAML is human-readable and easy to edit, which allows quick changes to configurations.


The YamlMan Class

The YAML manager class YamlMan is used in all following examples which make use of YAML for parsing configuration parameters in adapted versions.

The Constructor

The constructor of the class is passed a file path to a YAML file as argument:

YamlMan::YamlMan(std::string filepath)

The following behavior is specific for each example, storing the values from the YAML file to member variables:

YAML::Node config = YAML::LoadFile(filepath);

// access YAML nodes:
param1 = config["param1"].as<std::string>();
param2 = config["param2"].as<int>();
param3 = config["param3"].as<double>();

Simple Waveguides in C++

Waveguides can be used to model acoustical oscillators, such as strings, pipes or drumheads. The theory of digital waveguides is covered in the sound synthesis introduction.

The WaveGuide Class

The WaveGuide class implements all necessary components for a waveguide string emulation. Inside the class, the actual waveguide-buffers are realized as pointers to double arrays.

From ``WaveGuide.h``:

/// length of the delay lines
int     l_delayLine;

/// leftward delay line
double  *dl_l;

/// rightward delay line
double  *dl_r;

The pointer arrays need to be initialized in the constructor of the WaveGuide class.

From ``WaveGuide.cpp``:

dl_l = new double[l_delayLine];
dl_r = new double[l_delayLine];

for (int i=0; i<l_delayLine-1; i++)
{
    dl_l[i] = 0.0;
    dl_r[i] = 0.0;
}

Plucking the String

The method void WaveGuide::excite(double pos, double shift) is called for plucking the string, respectively exciting it. It receives a plucking position and a shift parameter. In two loops, the method fills the two waveguides with the excitation function. In this example, a simple triangular function is chosen.

From ``WaveGuide.cpp``:

// set positive slope until plucking index
for (int i=0; i<idx; i++)
{
    dl_l[i] = 0.5* ((double) i / (double)(idx));
    dl_r[i] = 0.5* ((double) i / (double)(idx));
}
// set negative slope from plucking index to end
for (int i=idx; i<l_delayLine; i++)
{
    dl_l[i] = 0.5*(1.0 - ((double) i / (double) (l_delayLine-idx)));
    dl_r[i] = 0.5*(1.0 - ((double) i / (double) (l_delayLine-idx)));
}

Oscillating

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

Exercise I

Make the buffer length and filter length command line or realtime-controllable parameters.

Exercise II

Implement a fractional noise buffer for arbitrary pitches.

Using MIDI with RtMidi

Although the MIDI protocol is quite old and has several drawbacks, it is still widely used and is appropriate for many applications. Read the MIDI section in the Computer Music Basics for a deeper introduction.

The development system used in this class relies on the RtMidi framework. This allows the inclusion of any ALSA MIDI device on Linux systems and hence any USB MIDI device. The RtMidi Tutorial gives a thorough introduction to the use of the library.


ALSA MIDI

The Advanced Linux Sound Architecture (ALSA) makes audio- and MIDI interfaces accessible for software. As an API it is part of the Linux kernel. Other frameworks, like JACK or Pulseaudio work on a higher level and rely on ALSA.

Finding your ALSA MIDI Devices

After connecting a MIDI device to an USB port, it should be available via ALSA. All ALSA MIDI devices can be listed with the following shell command:

$ amidi -l

The output of this request can look as follows:

Dir     Device        Name
IO      hw:1 ,0 ,0   nanoKONTROL MIDI 1
IO      hw:2 ,0 ,0   PCR-1 MIDI 1
I       hw:2 ,0 ,1   PCR-1 MIDI 2

In this case, two USB MIDI devices are connected. They can be addressed by their MIDI device ID (hw:0/1).


The MIDI Tester Example

The MIDI tester example can be used to print all incoming MIDI messages to the console. This can be helpful for reverse-engineering MIDI devices to figure out their controller numbers.

The MIDI Manager Class

The MIDI Manager class introduced in this test example is used as a template for following examples which use MIDI. For receiving messages, RtMidi offers a queued MIDI input and a user callback mode. In the latter case, each incoming message triggers a callback function. For the queued mode, as used here, incoming messages are collected until retrieved by an additional process.

The midiMessage struct is used to store incoming messages. It holds the three standard MIDI message bytes plus a Boolean for the processing state.

/// struct for holding a MIDI message
typedef struct  {
    int byte1             = -1;
    int byte2             = -1;
    double byte3          = -1;
    bool hasBeenProcessed = false;

}midiMessage;

Using OSC with the liblo

The OSC protocol is a wide spread means for communication between software components or systems, not only suited for music applications. Read more in the OSC chapter of the Computer Music Basics. There is a large variety of OSC libraries available in C/C++. The examples in this class are based on the liblo, a lightweight OSC implementation for POSIX systems.

Installing the Library

On Ubuntu systems, as the ones used in this class, the liblo library is installed with the following command:

$ sudo apt-get install liblo-dev

Including the Library

The liblo comes with additional C++11 wrappers to offer an object-oriented workflow. This feature is also used in the examples of this class. The following lines include both headers:

#include <lo/lo.h>
#include <lo/lo_cpp.h>

The GainExample

The GainExample is based on the ThroughExample, adding the capability to control the gain of the passed through signal with OSC messages.


Passing Command Line Arguments

The main function of this example accepts the OSC port to listen to as a command line argument. This is realized with a string comparison. The compiled binary is then started with an extra argument for the port:

$ ./gain_example -p 6666

The OSC Manager Class

The OSC-ready examples in these tutorials rely on a basic class for receiving OSC messages and making them accessible to other program parts. It opens a server thread, which listens to incoming messages in the background. With the add_method function, OSC paths and arguments specifications can be linked to a callback function.

// create new server
st = new lo::ServerThread ( p );

// / Add the example handler to the server !
st->add_method("/gain", "f", gain_callback, this);

st -> start ();

Inside the callback function gain_callback, the incoming value is stored to the member variable gain of the OscMan class.

statCast->gain = argv[0]->f;

The Processing Function

At the beginning of each call of the processing function, the recent incoming OSC messages are read from the OSC Manager:

// get the recent gain value from the OSC manager
double gain = oscman->get_gain();

The gain values are applied later in the processing function, when copying the input buffers to the output buffers:

out[chanCNT][sampCNT] = in[chanCNT][sampCNT] * gain;

Compiling

When compiling with g++, the liblo library needs to be linked in addition to the JACK library:

$ g++ -Wall -std=c++11 src/main.cpp src/gain_example.cpp src/oscman.cpp -ljack -llo -o gain_example

Working with the g++ Compiler

Compiling a Program

Examples and projects in this modules can be compiled with g++ from the GNU Compiler Collection. With the proper libraries installed, g++ can be called directly for small to medium sized projects. The first example, just passing through the audio, is compiled with the following command:

g++ -Wall src/gain_example.cpp src/oscman.cpp -ljack -o gain_example

The compiler gets the extra argument Wall to print all warnings. All source (cpp) files are passed to the compiler, followed by all libraries which need to be linked (linker arguments). The name of the binary or executable is specified after the -o flag.


Build Scripts

Past Projects - Sound Synthesis in C++

The following projects have been realized within the Sound Synthesis seminar in the past.

Wave Digital Filter (WDF) Tonestack