SuperCollider GUIDES

Writing Unit Generators

Get started with writing unit generators

How Unit Generator plug-ins work.

The server loads unit generator plug-ins when it starts up. Unit Generator plug-ins are dynamically loaded libraries written in C++. Each library may contain one or multiple unit generator definitions. A plug-in can also define things other than unit generators such as buffer fill ("/b_gen") commands. Plug-ins are loaded during the startup of the synthesis server. Therefore the server will have to be restarted after (re-)compiling the plugin.

The Entry Point

When the library is loaded the server calls a function in the library, which is defined by the PluginLoad()macro. This entry point has two responsibilities:

Unit Generators are defined by calling a function in the InterfaceTable and passing it the name of the unit generator, the size of its C data struct, and pointers to functions for constructing and destructing it. There are 4 macros, which can be used to simplify the process.

DefineSimpleUnit
Define a `simple' unit generator
DefineDtorUnit
Define a unit generator with a descructor
DefineSimpleCantAliasUnit
Define a `simple' unit generator, whose input and output buffers cannot alias
DefineDtorCantAliasUnit
Define a unit generator with a destructor, whose input and output buffers cannot alias

These macros depend on a specific naming convention:

A Simple Unit Generator Plugin

Unit generator plugins require two parts: A C++ part, which implements the server-side code that is loaded as a dynamically loaded library, and an SCLang class, that is required to build the SynthDef. The following example implements a simple Sawtooth oscillator

C++-side Definition of Unit Generators

The following code shows the C++ source of a simple unit generator.

#include "SC_PlugIn.h"

// InterfaceTable contains pointers to functions in the host (server).
static InterfaceTable *ft;

// declare struct to hold unit generator state
struct MySaw : public Unit
{
    double mPhase; // phase of the oscillator, from -1 to 1.
    float mFreqMul; // a constant for multiplying frequency
};

// declare unit generator functions
static void MySaw_next_a(MySaw *unit, int inNumSamples);
static void MySaw_next_k(MySaw *unit, int inNumSamples);
static void MySaw_Ctor(MySaw* unit);


//////////////////////////////////////////////////////////////////

// Ctor is called to initialize the unit generator.
// It only executes once.

// A Ctor usually does 3 things.
// 1. set the calculation function.
// 2. initialize the unit generator state variables.
// 3. calculate one sample of output.
void MySaw_Ctor(MySaw* unit)
{
    // 1. set the calculation function.
    if (INRATE(0) == calc_FullRate) {
        // if the frequency argument is audio rate
        SETCALC(MySaw_next_a);
    } else {
        // if the frequency argument is control rate (or a scalar).
        SETCALC(MySaw_next_k);
    }

    // 2. initialize the unit generator state variables.
    // initialize a constant for multiplying the frequency
    unit->mFreqMul = 2.0 * SAMPLEDUR;
    // get initial phase of oscillator
    unit->mPhase = IN0(1);

    // 3. calculate one sample of output.
    MySaw_next_k(unit, 1);
}


//////////////////////////////////////////////////////////////////

// The calculation function executes once per control period
// which is typically 64 samples.

// calculation function for an audio rate frequency argument
void MySaw_next_a(MySaw *unit, int inNumSamples)
{
    // get the pointer to the output buffer
    float *out = OUT(0);

    // get the pointer to the input buffer
    float *freq = IN(0);

    // get phase and freqmul constant from struct and store it in a
    // local variable.
    // The optimizer will cause them to be loaded it into a register.
    float freqmul = unit->mFreqMul;
    double phase = unit->mPhase;

    // perform a loop for the number of samples in the control period.
    // If this unit is audio rate then inNumSamples will be 64 or whatever
    // the block size is. If this unit is control rate then inNumSamples will
    // be 1.
    for (int i=0; i < inNumSamples; ++i)
    {
        // out must be written last for in place operation
        float z = phase;
        phase += freq[i] * freqmul;

        // these if statements wrap the phase a +1 or -1.
        if (phase >= 1.f) phase -= 2.f;
        else if (phase <= -1.f) phase += 2.f;

        // write the output
        out[i] = z;
    }

    // store the phase back to the struct
    unit->mPhase = phase;
}

//////////////////////////////////////////////////////////////////

// calculation function for a control rate frequency argument
void MySaw_next_k(MySaw *unit, int inNumSamples)
{
    // get the pointer to the output buffer
    float *out = OUT(0);

    // freq is control rate, so calculate it once.
    float freq = IN0(0) * unit->mFreqMul;

    // get phase from struct and store it in a local variable.
    // The optimizer will cause it to be loaded it into a register.
    double phase = unit->mPhase;

    // since the frequency is not changing then we can simplify the loops
    // by separating the cases of positive or negative frequencies.
    // This will make them run faster because there is less code inside the loop.
    if (freq >= 0.f) {
        // positive frequencies
        for (int i=0; i < inNumSamples; ++i)
        {
            out[i] = phase;
            phase += freq;
            if (phase >= 1.f) phase -= 2.f;
        }
    } else {
        // negative frequencies
        for (int i=0; i < inNumSamples; ++i)
        {
            out[i] = phase;
            phase += freq;
            if (phase <= -1.f) phase += 2.f;
        }
    }

    // store the phase back to the struct
    unit->mPhase = phase;
}


// the entry point is called by the host when the plug-in is loaded
PluginLoad(MySaw)
{
    // InterfaceTable *inTable implicitly given as argument to the load function
    ft = inTable; // store pointer to InterfaceTable

    DefineSimpleUnit(MySaw);
}

SCLang-side Definition of Unit Generators

SuperCollider requires an SCLang class in order to build SynthDefs.

The arguments to the MySaw UGen are freq and iphase. The multiNew method handles multi channel expansion. The madd method provides support for the mul and add arguments. It will create a MulAdd UGen if necessary. You could write the class without mul and add arguments, but providing them makes it more convenient for the user. See Writing Classes for details on writing sclang classes.

// without mul and add.
MySaw : UGen {
    *ar { arg freq = 440.0, iphase = 0.0;
        ^this.multiNew('audio', freq, iphase)
    }
    *kr { arg freq = 440.0, iphase = 0.0;
        ^this.multiNew('control', freq, iphase)
    }
}

Building Unit Generator Plugins

The most portable way to build plugins is using cmake1 , a cross-platform build system. In order build the example with cmake, the following code should go into a CMakeLists.txt file.

cmake_minimum_required (VERSION 2.8)
project (MySaw)

include_directories(${SC_PATH}/include/plugin_interface)
include_directories(${SC_PATH}/include/common)
include_directories(${SC_PATH}/external_libraries/libsndfile/)

set(CMAKE_SHARED_MODULE_PREFIX "")
if(APPLE OR WIN32)
set(CMAKE_SHARED_MODULE_SUFFIX ".scx")
endif()

add_library(MySaw MODULE MySaw.cpp)

Coding Guidelines

Unit generator plugins are called from the real-time context, which means that special care needs to be taken in order to avoid audio dropouts.

Memory Allocation
Do not allocate memory from the OS via malloc / free or new/ delete. Instead you should use the real-time memory allocator via RTAlloc / RTFree.
STL Containers
It is generally not recommended to use STL containers, since they internally allocate memory. The only way the STL containers can be used is by providing an Allocator, which maps to the allocating functions of the server.
Blocking API Calls
Unit generators should not call any code, which could block the execution of the current thread. In particular, system calls should be avoided. If synchronization with other threads is required, this has to be done in a lock-free manner.

Thread Safety

There are two different implementations of the SuperCollider server. scsynth is the traditional server and supernova is a new implementation with support for multi-processor audio synthesis. Since the plugins in supernova can be called at the same time from multiple threads, write access to global data structures needs to be synchronized.

Shared Global Data Structures
Unit generators should not share data structures, which are written to. While it it safe to use global data structures for read-only purposes (e.g. different unit generators could use the same constant wavetable), the data structures that are modified by the unit generators should not be shared among different instances.
Resource Locking
SuperCollider's buffers and busses are global data structures, and access needs to be synchronized. This is done internally by using reader-writer spinlocks. This is done by using the ACQUIRE_, RELEASE_, and LOCK_ macros, which are defined in SC_Unit.h. As exception, buffers in the wavetable format are not required to be locked.

Deadlock Prevention

In order to prevent deadlocks, a simple deadlock prevention scheme is implemented, based on the following constraints.