Downloads

Plucky.tar.gz - tarball containing code, makefile, readme, and Rt[ Audio | Midi ] sources
Sirius_Pluckage.aif - A musical statement created with Plucky

Summary

Plucky is a Karplus-Strong plucked string model that runs on the command line. It utilizes RtAudio for audio output and RtMidi for MIDI input. It can also be controlled by the computer keyboard. See the Readme for compilation and usage info

The assignment to create Plucky prompted the extension of the signal processing evironment created for sig-gen in hw1. Since Plucky needed to be polyphonic, a module called "Poly" was created to fit within the existing AudioClient framework. The assignment also involved supporting MIDI input, so the client/server relationship was mimiced to this end. Here's some sample code showing the creation of a polyphonic Karplus-Strong instrument that accepts MIDI and is connected to the output

int main(int argc, const char** argv)
{
    RtAudioDriver driver;
    RtMidiDriver midiDriver;
    Poly p;
    for (int i = 0; i < MAX_VOICES; ++i)
    {
       Karplus* string = new Karplus();
       p.AddVoice(string);
    }
    AudioServer::GetInstance()->AddClient(&p, 0);
    AudioServer::GetInstance()->AddClient(&p, 1);
    MidiServer::GetInstance()->AddClient(&p, 0);

    for (;;)
    {
       sleep(1);
    }

    return 0;
}


Strategy

The fundamental concept behind enabling polyphony is to have an object that sits in the audio processing graph and encapsulates sound generating "voices". The class Poly was created as an AudioClient and a MidiClient, and the abstract class Voice was created to serve as an interface for any audio source that should 1) be polyphonic and 2) react to midi. Poly is responsible for managing a resource pool of voices, a task which involves triggering available voices with new note messges, stealing from the pool of active voices, and correctly shutting off notes when a note-off is received. This is all implemented by way of a double-ended queue and a map. The queue simply provides a naive voice-stealing mechanism, and the map is used to relate incoming note-off messages to notes that are playing.

Challenges

Several technical challenges were presented with the question of how to modularly implement polyphony. The first issue was to merge a semantically elegant solution with the obvious task at hand (to map midi notes to voices). In my opinion, the most readable version of Poly would be used as follows:

Poly<Karplus> p(MAX_VOICES);


This is an expressive way to do the same as above, but it brings the limitation that the number of voices cannot be changed dynamically, and only one kind of voice can be used at a time. To be fair, Poly is intedented to handle only once kind of Voice, but nothing stops the user from violating that policy (violations should be encouraged!!). Poly does not really care about the desired number of voices. It simply takes ownership of Voice objects and utilizes them as needed (when notes are played).

Another challenge was voice-allocation. The voice-allocation scheme involves a FIFO queue and a map. The map, which is normally used to shut notes off, is also referenced on note-on to determine if a given note is already playing. In that case, the note is simply re-triggered. When the maximum number of voices has been reached, the voice used is the oldest voice in the queue. The challenge here was that a stream of short notes could eventually eclipse a single long note, so an additional check for "free" voices is made before grabbing the front of the queue. Unfortunately, this involves traversing the queue every time a note is played. I am now considering how to replicate this functionality in a more efficient manner using a priority queue.