Downloads

hw1.tar.gz - tarball containing code, makefile, readme, and RtAudio sources
sig-gen.cpp - just the code

Summary

sig-gen is a command-line signal generator application. It can generate sine, pulse, sawtooth, and triangle waveforms, plus impulse trains and white noise. By default, the signal is generated at 440Hz, but the frequency can also be specified as a command line argument. The pulse with of the pulse wave can be specified in the range [0.0, 1.0], where 0.5 yields a square wave. In saw mode, the width parameter skews the shape of a triangle wave, where 0.5 is a symmetrical triangle. Lastly, the program can optionally accept audio input and ring-modulate it with the specified signal.

The assignment to create sig-gen presented an opportunity to design a modular signal processing environment. The result of this exercise is the begining of a system that can easily be extended to do many other kinds of signal processing in a modular fashion. Here's some sample code creating one oscillator at 1kHz and one input source running through a ring modulator:

int main(int argc, const char** argv)
{
    RtAudioDriver driver;
    InputSource in;
    SinOsc s(1000);
    Multiplier m(&in, &s);
    AudioServer::GetInstance()->AddClient(&m, 0);
    AudioServer::GetInstance()->AddClient(&m, 1);

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

    return 0;
}


Strategy

I started by abstracting the signal generator unit as an abstract class called "AudioClient", which is the type of object used by the audio driver to distribute callbacks and collect rendered audio. Concrete classes must implement a function called Render in which to generate audio.

The idea of the audio driver is abstracted in a singleton class called "AudioServer". AudioServer receives callbacks from an audio driver wrapper (such as RtAudio), and maintains lists of audio clients, one list per channel. AudioClients can be registered with the AudioServer, which essentially connects the clients to the DAC. Of course, not all AudioClients should necessarily be connected to the output; some might be used as input to others. In this case, the client that requires input will cause the input client to render its audio.

Challenges

One technical challenge presented itself when multiple connections were made with the same AudioClient. For example, if one audio client should play through two DAC channels, it is regiestered with the AudioServer twice, once for each channel. Without any handling of this case in place, the client will receive two callbacks for every one submitted by the driver. The solution to this problem is to introduce the notion of time. The Server maintains a count that increments each time a callback has completed. Each connected client also maintains a counter which is syncrhonized with the server's counter after it produces audio. If a client is asked to process audio, but its count is equal to the server's, then it simply returns a copy of the last audio buffer it rendered. Thus there is a distinction between processing a buffer and rendering new material. The AudioClient base class manages processing, and concrete AudioClients do rendering.