Audio Programming with Cinder Part II

Part II: Adding Gamma to Cinder

Cinder has many great tools for programming visuals, however its audio functions leave much to be desired. As a solution, this tutorial will focus on adding the Gamma “generic synthesis” library  to our application. Gamma is authored by my friend and colleague Dr. Lance Putnam and includes dozens of efficient tools for signal processing that we’ll use throughout the rest of this tutorial (and hopefully after).

1. Download the latest version of Gamma here (where is says “Stable Release”). Unzip the folder and cd into it from a terminal. Then build the library by typing “make” and install it to your /usr/local folders by typing “sudo make install” and entering your password. You should now have Gamma’s header files included in /usr/local/include/Gamma and the compiled library (libGamma.a) inside of /usr/local/lib. Gamma also relies on PortAudio and libsndfile, so it will also include libportaudio.a and libsndfile.a in /usr/local/lib.

2. Inside of the Gamma open the examples folder. You should see a file called “examples.h”. Drag this file into the source folder of your Cinder Xcode project and check “Copy item into destination group’s folder.” We then need to include the examples.h file which in turn includes header files for most of Gamma:

#include "examples.h"

If we try to run our app at this point, it’s not going to build because we haven’t told the Xcode project where to look for these Gamma header files that we’re telling it to include. To do this click on your project’s icon in the project navigator window (on the left hand side of the screen where it says “CinderGamma” with a little blue icon). This should open up a menu with our project settings. Click the “Build Settings” tab and in the search bar enter “Header Search Path”. Double click to the right of the Header Search Path line and a little box should pop up. Click the “+” icon to add a folder. Then type in “/usr/local/include” and hit enter. Now if you click “Run” your project should build successfully.

3. In order to actually use any of Gamma’s classes and functions, we need to link our project to the compiled libGamma.a library. Click on your project’s icon again to open the project’s settings. Click the “Build Settings” tab and in the search bar enter “Library Search Path”. Double click to the right of the Library Search Paths line and a little box should pop up. Click the “+” icon to add a folder. Then type in “/usr/local/lib” and hit enter. Now we should be all ready to use Gamma. (Sometimes you need to explicitly tell Xcode what libraries to link to. If you’re getting linking errors after this step, search for “flags” in project settings and double click next to other linker flags. Then hit the “+” icon and add “-lGamma”)

4. Now we can use Gamma’s wide range of unit generators to make noise. As an initial test, let’s re-create that sine tone we made in the last part using Gamma’s Sine class. First we add an instance of Sine to our app class:

Sine<> mySine;

We then initialize Gamma’s master Sync to the sample rate we’re using and ititialize our sine tone generator to 300 hz using the freq(x) function:

Sync::master().spu(44100.0);
mySine.freq(300.0);

And finally we need to update our audio callback to generate and store a new sample from our generator each frame:

float tempVal = mySine();
ioBuffer->mData[ i * ioBuffer->mNumberChannels ] = tempVal;
ioBuffer->mData[ i * ioBuffer->mNumberChannels + 1 ] = tempVal;

Gamma uses the generator pattern quite extensively, which means that the () operator is overloaded, enabling us to both generate a new sample and return the current one just by calling mySine(). It might look confusing but it will become extremely convenient in our next step. Here’s the full program which generates a 300 hz sine tone using Gamma:

#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
#include "cinder/audio/Output.h"
#include "cinder/audio/Callback.h"
#include "examples.h"

using namespace ci;
using namespace ci::app;
using namespace std;

class CinderGammaApp : public AppBasic {
public:
    void setup();
    void mouseDown( MouseEvent event );
    void update();
    void draw();
    void myAudioCallback( uint64_t inSampleOffset, uint32_t ioSampleCount, ci::audio::Buffer32f * ioBuffer );
private:
    float phaseIncrement;
    float phase;
    Sine<> mySine;
};

void CinderGammaApp::setup()
{
    audio::Output::play( audio::createCallback( this, & CinderGammaApp::myAudioCallback ) );
    Sync::master().spu(44100.0);
    mySine.freq(300.0);
}

void CinderGammaApp::mouseDown( MouseEvent event )
{
}

void CinderGammaApp::update()
{
}

void CinderGammaApp::draw()
{
    // clear out the window with green
    gl::clear( Color( 0, 0.7, 0.0 ) );
}

void CinderGammaApp::myAudioCallback( uint64_t inSampleOffset, uint32_t ioSampleCount, audio::Buffer32f * ioBuffer )
{
    for ( uint32_t i = 0; i < ioSampleCount; i++ ) {
        phase += phaseIncrement;
        float tempVal = mySine();
        ioBuffer->mData[ i * ioBuffer->mNumberChannels ] = tempVal;
        ioBuffer->mData[ i * ioBuffer->mNumberChannels + 1 ] = tempVal;
    }
}

CINDER_APP_BASIC( CinderGammaApp, RendererGl )

5. Ok this is great but now that we have access to Gamma, let’s make something more complicated that a sine tone. We’re going to make an FM struct that stores a carrier frequency, a mod frequency, a depth, and the carrier and modulator sine tone generators themselves. We also give it an overloaded () function which updates sets the carrier frequency based on the modulator’s phase times depth and returns the next sample:

struct FM {
    float carrierFreq;
    float modFreq;
    float depth;
    Sine<> carrier;
    Sine<> mod;
    FM(){
    }
    float operator()(){
        mod.freq(modFreq);
        carrier.freq(carrierFreq+(mod()*depth));
        return carrier();
    }
};

We also need to add an instance of this FM struct to our main app.

FM myFM;

In setup() we should initialize the values we want for our carrier frequency, mod frequency, and depth:

myFM.carrierFreq = 300.0;
myFM.modFreq = 20.0;
myFM.depth = 20.0;

And then finally we make sure this is called in our audio loop. The overloaded () operator allows us to just call myFM() every frame which both updates our synth and returns the next sample (remember- this is called the generator pattern and it rocks).

float tempVal = myFM();
ioBuffer->mData[ i * ioBuffer->mNumberChannels ] = tempVal;
ioBuffer->mData[ i * ioBuffer->mNumberChannels + 1 ] = tempVal;

Here’s the complete program which outputs a frequency modulated sine tone. Later on in part three we’re going to add a user interface so that we can actually control our synth in real time:

#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
#include "cinder/audio/Output.h"
#include "cinder/audio/Callback.h"
#include "examples.h"

using namespace ci;
using namespace ci::app;
using namespace std;

struct FM {
    float carrierFreq;
    float modFreq;
    float depth;
    Sine<> carrier;
    Sine<> mod;
    FM(){
    }
    float operator()(){
        mod.freq(modFreq);
        carrier.freq(carrierFreq+(mod()*depth));
        return carrier();
    }
};

class CinderGammaApp : public AppBasic {
public:
    void setup();
    void mouseDown( MouseEvent event );
    void update();
    void draw();
    void myAudioCallback( uint64_t inSampleOffset, uint32_t ioSampleCount, ci::audio::Buffer32f * ioBuffer );
private:
    FM myFM;
};

void CinderGammaApp::setup()
{
    audio::Output::play( audio::createCallback( this, & CinderGammaApp::myAudioCallback ) );
    Sync::master().spu(44100.0);

    myFM.carrierFreq = 300.0;
    myFM.modFreq = 20.0;
    myFM.depth = 20.0;
}

void CinderGammaApp::mouseDown( MouseEvent event )
{
}

void CinderGammaApp::update()
{
}

void CinderGammaApp::draw()
{
    // clear out the window with green
    gl::clear( Color( 0, 0.7, 0.0 ) );
}

void CinderGammaApp::myAudioCallback( uint64_t inSampleOffset, uint32_t ioSampleCount, audio::Buffer32f * ioBuffer )
{
    for ( uint32_t i = 0; i < ioSampleCount; i++ ) {
        float tempVal = myFM();
        ioBuffer->mData[ i * ioBuffer->mNumberChannels ] = tempVal;
        ioBuffer->mData[ i * ioBuffer->mNumberChannels + 1 ] = tempVal;
    }
}
CINDER_APP_BASIC( CinderGammaApp, RendererGl )

Leave a Reply

Your email address will not be published. Required fields are marked *