SuperCollider has a vast library of unit generators that can be assembled in unlimited ways, but sometimes even those aren't sufficient. You may have a need for an unusual signal processing algorithm, or you're running into efficiency problems that can be solved by condensing parts of your SynthDef into a single UGen.
UGens are defined in server plugins written in C++. Server plugins are not to be confused with quarks, which extend the SuperCollider language. UGens exist more or less independently of the interpreter and you don't need much familiarity with SC to write them.
Writing UGens is not too difficult, but it's arguably far less convenient and intuitive than the high-level tools that SuperCollider provides. You'll need a build system and a good amount of boilerplate code -- even fairly basic signal processing operations can require a lot of code. You don't have an instant live coding environment, and mistakes can easily crash the server. SuperCollider's UGens are stable and well-tested, and custom UGens are best viewed as a last resort for when the limitations of SC are impassable.
Before we proceed to the real UGens, we'll take a quick detour for the sake of completeness. A pseudo-UGen is a bit of SuperCollider code that abbreviates a certain configuration of UGens that gets used repeatedly. A pseudo-UGen is a class that superficially resembles a UGen class, but it only returns a composition of existing UGens. It has no efficiency savings, but it does save typing.
The below example has only a .ar
method, but you can just as easily have both .ar
and .kr
methods.
Examples of pseudo-UGens found in SC include BLowPass4 and BHiPass4, which break down into SOS UGens.
There are very few restrictions on what these classes can contain, but you should keep the following in mind:
A (real) UGen needs two components: a plugin for the server, and a class for the language. The class goes in an ordinary *.sc
file, and defines the interface for your UGen in the language. This class is generally just a few lines of code that ultimately call the class method UGen: -multiNew.
The server plugin is where the actual UGen behavior is defined. It is given by a dynamically loaded library written in C++, whose format is platform-dependent:
*.so
for *nix*.dll
for Windows*.scx
for macOSA plugin file can contain more than one UGen, and it can also define things other than UGens such as buffer fill ("/b_gen") commands.
When the server boots, it will look for plugin files in Platform.userExtensionDir
. Since sclang also looks for class files in the same location, the class file and the library file can go in the same place.
Plug-ins are loaded during the startup of the server, so it will have to be restarted after (re-)compiling a plugin. If you modify the plugin file but not the class file, you don't need to reboot the interpreter.
FAUST1 is an open source DSP language that describes real-time audio units. It can compile to SuperCollider plugins, providing an easy way to create UGens in SuperCollider.
FAUST provides a shell script useful for SuperCollider users called faust2supercollider
. This compiles a .dsp file into a class file and server plugin, which you can then drop into your extensions directory.
FAUST plugins are often quick to develop and can be painlessly ported to other environments. Unfortunately, they can't take advantage of all of the server's features, such as accessing Buffers or random number generators, and some UGens featuring very complex logic are difficult or impossible to write in FAUST. Furthermore, the FAUST compiler is quite intelligent but it might not always offer the best efficiency in its results. If a UGen you are developing hits these limitations, it is time to move on to handwritten C++.
To get an idea of the necessary ingredients for writing UGens, it's often best to poke around at complete examples. We've set up a GitHub repository at https://github.com/supercollider/example-plugins, which contains some example plugins numbered roughly by complexity. Each directory in that repository is self-contained with its own build system, so you can copy out a directory to form a starting point for your own UGens. The source codes of these plugins are heavily commented.
The first example, BoringMixer, is very minimal. The UGen is stateless and has only one calculation function, which is audio rate.
MySaw introduces states and multiple calculation functions. AnalogEcho introduces real-time memory management through internal buffers, and demonstrates how to do cubic interpolation from an array of samples.
The SC source code has a header file, include/plugin_interface/SC_PlugIn.h
, that gives you your interface to the server architecture as well as a bunch of helper functions. These are documented at Server Plugin API.
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.
These macros depend on a specific naming convention:
PluginName_Ctor
PluginName_Dtor
The meat of the UGen is its calculation function, which gets called every control period with the UGen object as an argument. (This is for control-rate and audio-rate UGens -- demand-rate is different.) In this function, the UGen reads from its inputs and writes to its outputs.
The calculation function is selected in the PluginName_Ctor
function with the SETCALC
macro. You can name the calculation function whatever you want, but the convention is PluginName_next
.
UGens often have multiple calculation functions, depending on the rate of the UGen itself and the rate of its inputs. For example, Phasor can be .ar or .kr, and its argument can be either .ar or .kr. So it has four calculation functions: Phasor_next_aa, Phasor_next_ak, Phasor_next_ka, and Phasor_next_kk. You don't need to be this thorough for your own UGens, however. For example, FreeVerb has only one calculation function. Who would want a control-rate reverb?
The most portable way to build plugins is using cmake2 , a cross-platform build system.
The examples in the example repository contain CMakeLists.txt
files.
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.
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
.
RTFree
the memory you RTAlloc
malloc/free
, you are reponsible for freeing all the memory you allocate. Remember to include RTFree
calls in your destructor functions.nullptr
nullptr
as early as possible in your constructor functions.ClearUnitIfMemFailed
nullptr
, it means that RTAlloc failed to allocate memory for it. The macro will then print an error message, set the UGen's calculation function to a no-op, and return from the calling function immediately. Since this can cause early exit from your constructor function, it is fundamental that all pointers are initialized to nullptr
as early as possible, as stated above.
ClearUnitIfMemFailed
can be passed a single pointer, or it can check multiple pointers at the same time, by chaining them with the &&
operator (see examples below).
ClearFFTUnitIfMemFailed
instead. This is because, on a failed allocation, ClearUnitIfMemFailed
would make them output 0
, which would be interpreted by the next UGen in the FFT chain as "FFT data is ready to be processed on buffer number 0", which is not the case. ClearFFTUnitIfMemFailed
will set their output to -1
instead, meaning that FFT data is not ready, and thus blocking further processing for the rest of the FFT chain. For more informations please see FFT Overview: How FFT UGens communicate.
ClearFFTUnitIfMemFailed
is defined in FFT_UGens.h
Minimal example, C style:
Or, in C++ class style:
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.
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.In order to prevent deadlocks, a simple deadlock prevention scheme is implemented, based on the following constraints.