Tutorial: SuperCollider server plugins in C++

Tutorial: SuperCollider server plugins in C++

A tutorial on how to write SuperCollider server plugins in C++, by Mads Kjeldgaard, Notam

Example code and PDF available here

Introduction

In SuperCollider, a unit generator (UGen) is a building block for creating sound patches. These building blocks may be something as simple as a sine wave generator or as complex as an algorithmic reverb. With these, the user of SuperCollider may put together synthesis and sound manipulation patches for creating almost any sound imaginable.

SuperCollider comes prepackaged with a plethora of different UGens.

Expanding the selection of UGens is done by installing plugins - collections of user contributed UGens (the words UGen, unit generators and plugins are often used interchangeably in SuperCollider).

The sc3-plugins repository contains a large library of community developed plugins (of varying age and quality) and other notable examples of plugins include IEM’s vstplugin plugin and The Fluid Corpus Manipulation Project.

This is all made possible by SuperCollider’s plugin API that provides a simple way to write plugins in C or C++ and hook them in to SuperCollider’s synthesis server as one of many building blocks. Each addition of a plugin makes possible an abundance of new possibilities for the user when combined with the existing plugins.

In this tutorial series, we will cover some of the basics of writing such plugins using the C++ interface available to plugin authors (if you prefer writing your plugin in pure C, then I would advise reading Dan Stowell’s excellent chapter on just that in the MIT SuperCollider book).

Before continuing I would like to extend a big thank you to the ever friendly and generous SuperCollider community for helping me understand these things a bit better, and to my workplace Notam for allowing me time to work on this.

On that note: If you need help with plugin development, there is an entire subforum dedicated to the subject over on the SuperCollider forum.

The code to accompany this tutorial may be found here.

Why write a plugin for SuperCollider?

Most users of SuperCollider will never experience the need for writing a plugin simply because the library of UGens that comes with SuperCollider is so rich in selection and effective in performance.

That said, there are some reasons for doing it anyway:

  1. Learning: It’s a great way to learn or get better at C++ or DSP (Digital signal processing) because it lets you focus on the sound and the algorithms, with fairly short compilation times and the ability to quickly test it out in SuperCollider.
  2. Contributing: You get to expand the ever evolving world of sonic possibilities in SuperCollider and contribute to a lively and creative community that has been going for more than 25 years.
  3. Expanding sonic possibilities: There may be some functionality you would love to see added to SuperCollider (see the flucoma project for a really impressive example of this).

Part 1: Demystifying the SuperCollider plugin

Before actually creating a plugin, let’s take a bit of a detour to have a look at what a plugin project contains and how to use the CMake build generator system to turn our code into UGen objects for SuperCollider.

Quick starting a plugin project

The fastest way to create a plugin project is to use the SuperCollider development team’s cookiecutter template. By running this script in a terminal and answering the questionnaire it presents you with, you should be able to quickly generate all the files and project structure necessary for a plugin. See the cookiecutter template’s project page for the most updated information about how to use this, but in short it boils down to first installing the cookiecutter tool which is a python library:

$ python3.7 -m pip install cookiecutter

Then you can run the cookiecutter command line tool and supply it with the URL of SuperCollider’s cookiecutter template:

$ cookiecutter https://github.com/supercollider/cookiecutter-supercollider-plugin

A typical project structure

A typical project structure for a plugin looks something like this:

|-- CMakeLists.txt

|-- cmake_modules
|   |-- SuperColliderCompilerConfig.cmake
|   |-- SuperColliderServerPlugin.cmake
|
|-- LICENSE
|
|-- plugins
|   |-- SimpleGain
|       |-- SimpleGain.cpp
|       |-- SimpleGain.hpp
|       |-- SimpleGain.sc
|       |-- SimpleGain.schelp
|
|-- README.md

Let’s take it from the bottom up:
First there is a readme. This explains what the plugin is and how to install it.
Then there is a subfolder called plugins - this is where we will be spending most of our time writing plugins. This folder contains another subfolder called SimpleGain which is the name of this particular plugin, and then the four files you need for a plugin:

  1. A .hpp file that declares your C++ code and imports all the necessary files.
  2. A .cpp file - this C++ implementation file is where we define our algorithms (typically in a setup function (a constructor), an optional teardown function (a destructor) and a calculation function that is called on each block of sound samples we put in to our plugin).
  3. A .sc SuperCollider class file that bridges our C++ code to the SuperCollider language
  4. A .schelp file for the SuperCollider help system which explains the usage of the plugin and demonstrates its usage in one or more examples.

The cmake_modules subfolder and the CMakeLists.txt file are used for building and compiling our project (we will get back to this later).

And then lastly, there is a LICENSE file containing the license for the plugin’s code (SuperCollider itself is licensed under the GPL-3 license).

CMake is your friend

Building, compiling and installing your plugin(s) is one process handled by a program called CMake.

It is easy to get freaked out by CMake. At first, using it may seem like a semi-esoteric experience but CMake is actually your friend. In a SuperCollider plugin project, the main job of CMake is to keep track of your build options, what platform your are on and where the files needed are - and then, with that information at hand, construct a build system, compile your code and install it for you.

It is not strictly necessary to go deep in to the world of CMake to write a SuperCollider plugin but familiarizing yourself with some of the absolute basics can be helpful, which is why we will spend a bit of time doing just that before actually starting on our plugin (should you feel the need to go deep, then Craig Scott’s book “Professional CMake” is a nice resource).

CMake’ing a plugin project

CMake needs a copy of the SuperCollider source code to be able to build a plugin project.

The path to this is supplied to CMake by using the -DSC_PATH flag like so:

-DSC_PATH=/path/to/supercollider/sourcecode

The reason for this is that the SuperCollider source code contains all the actual library code you need to make your plugin work - including the plugin API, so the compiler needs these to be able to create the necessary objects.

CMake also needs to know where your plugin code is. This is done in the CMakeLists.txt file at the root of your project where the paths (and other useful options) are defined. You generally only need to change this file if you rename your files, move them or want to add more files.

This is done using two CMake commands:

  • set(variablename ...variable_contents)
  • sc_add_server_plugin(destination name cpp_files sc_files schelp_files)

Here is an example of what that might look like in your plugin project:

set(SimpleGain_cpp_files
    plugins/SimpleGain/SimpleGain.hpp
    plugins/SimpleGain/SimpleGain.cpp
)
set(SimpleGain_sc_files
    plugins/SimpleGain/SimpleGain.sc
)
set(SimpleGain_schelp_files
    plugins/SimpleGain/SimpleGain.schelp
)

sc_add_server_plugin(
  "${project_name}" # destination directory
  "SimpleGain" # target name
  "${SimpleGain_cpp_files}"
  "${SimpleGain_sc_files}"
  "${SimpleGain_schelp_files}"
)

When you add more plugin files to your project you simply have to repeat the chunk of CMake code above in your CMakeLists.txt, modified to contain information about the newly added files.

Build and install using CMake

What makes CMake really nice is that it makes it possible to build, compile and install your project with the help of a few terminal commands.

The workflow for this is divided into these steps:

Step 1: Create a build directory and generate a build system.

From the root of your project, run:

mkdir build # Create a build sub directory
cmake -B "build" -S . -DSC_PATH="/path/to/sc/sourcecode" \
	-DCMAKE_INSTALL_PREFIX="/path/to/your/extensions/dir"

Once this is done, CMake will fill the subfolder build with a bunch of files and data that is needed every time you compile your code (and it also serves as a sort of cache or memory).

Note always wrap the path arguments in quotes, CMake is very particular about this.

Step 2: Compile (and optionally install) the plugin(s).

To compile and install all you need to do is run the following command from the root of your project (repeat this command every time you change your code and want to see if it compiles and installs correctly):

cmake --build build --config "Release" --target install

Step 3: Try it out in SuperCollider

If compilation was successful, you should now have compiled and installed your project to your SuperCollider extensions directory.

If you have SuperCollider open, recompile the class library and search the help system for your plugin to verify that it showed up.

In the next part of this tutorial series, we will go deeper into the actual code of a plugin.

Part 2: Creating a ramp generator plugin

In this part of the tutorial we will start work on a plugin project. We will create a very simple oscillator that ramps from 0.0 to 1.0. This is sometimes known as a phasor as it is often used internally in code to read through lookup tables or as a way to have an internal clock that steps through data. It is also known as a sawtooth oscillator and may be used at audio rate to create sound signals with many (harsh) overtones.

Control, audio and scalar rate: A note on calculation rates

But first: We need to talk about calculation rates.

You may or may not be aware that UGens in SuperCollider often have multiple different output rates at which they may run, these are denoted by the class method they are called with. Some run at audio rate, some at control rate, others at demand rate (which we will not cover here) and others again only at scalar rate (abbreviated ir sometimes for initialization rate).

Here are examples of how that might look in SuperCollider:

// Control rate
SinOsc.kr(100);

// Audio rate
SinOsc.ar(100);

// Scalar rate
SampleRate.ir();

These three different calculation rates are optimized for different purposes:

  1. Scalar rate, also known as initialization rate is useful if your plugin should output a value when it is initialized and then do nothing else.
  2. Audio rate is self-explanatory, but what it means in practice is your calculation function will receive a block of audio samples, loop over each sample, do something with the data and then return an output block of samples. Every time your plugin’s calculation function is called it is supplied with an argument containing the value of the number of samples to process. This is typically 64 samples, but the user may change this in the server options.
  3. Control rate. This is a more performant alternative to audio rate calculation that only produces one value per block of samples. It is equivalent to setting the number of samples in the calculation function to 1.

Here is a little table to help you remember this:

Rate sclang method name Update rate
Audio rate *ar numSamples/sampleblock
Control rate *kr 1/sampleblock
Scalar *ir at UGen initialization

Ramping values

A ramp generator is a simple algorithm that counts up to a certain threshold, then wraps back to where it began. This technique is seen in sawtooth and phasor oscillators. Not only is the signal itself useful in modulating parameters as an LFO, but the core algorithm is often used in other oscillator functions to index into wavetables and similar things.

The idea is simple, really: Imagine you are counting from 0 to 10 and then every time you reach 10, you start over at 0. If you were to plot this on a graph you would see a ramp signal (which looks like a sawtooth).

We will be using the same concept in our ramp UGen but instead of counting whole numbers we will be counting floats, which is the type of number that comes out of a UGen.

So, instead of counting from 0 to 10, we will be counting from 0.0 to 1.0.

Creating the RampUpGen SuperCollider class

If you followed the previous tutorial in this series, you will know that the easiest way to generate the scaffolding for a plugin is using the supercollider plugin cookiecutter template. Running this will generate all the files needed for a plugin and going forward I will assume you’ve done that and that you’ve called your plugin RampUpGen.

The first thing to write is the SuperCollider class that will be responsible for calling our C++ code. The file is called something like RampUpGen.sc.

It defines the UGen’s audio rate (*ar) and control rate (*kr) methods and sets the frequency argument’s default value.

Another important (and often overlooked) aspect of the SuperCollider class interface for plugins is that they are tasked with checking the rates of the inputs to the parameters and ensure an error is raised if for example a user inputs an audio rate signal in an input that was written for control rate.

RampUpGen : UGen {
	// Audio rate output
	*ar { |frequency=1.0|
		^this.multiNew('audio', frequency);
	}

	// Control rate output
	*kr { |frequency=1.0|
		^this.multiNew('control', frequency);
	}

	// Check all inputs' rates
	checkInputs {

		// Check the rate of the frequency argument
		if(inputs.at(0) == \audio, {
			"You're not supposed to use an audio rate as input to frequency.".error
		});

		// Checks if inputs are valid UGen inputs (and not a GUI slider or something)
		^this.checkValidInputs;
	}

}

Important note on multichannel UGens:

Our SuperCollider class above inherits from UGen - this results in a 1 channel UGen. If you need more outputs than that, you need to inherit from MultiOutUGen instead (don’t forget to call this.initOutputs(numChannels, rate) somewhere in your class if you do end up using this, it will (silently) fail otherwise).

Setting up the header file

Next step is to open up RampUpGen.hpp in a text editor. This is your header file for your plugin where the correct files are imported and your new C++ class RampUpGen inherits its functionality from the SCUnit class. The class should look something like this:

class RampUpGen : public SCUnit {
public:
  RampUpGen();
private:
  // Calc function
  void next(int nSamples);
};

As you can see above, it defines a public constructor for the class RampUpGen() and a private calculation function called next(int nSamples). If you create a UGen that allocates memory (for example delay based UGens), then you should also define a destructor function which is the same name as the constructor but with a tilde prefixed: ~RampUpGen(). It is omitted here since we don’t need it.

We only need to make one small adjustment to this. Under the private: keyword, declare a member variable that we can use to store our phase value in between calls to the next calculation function:

// Ramp generator phase storage. Initialized to 0.0.
double m_phase{0.0};

Creating the calculation function

It’s now time to define the behaviour of our plugin. Move into the RampUpGen.cpp file.

The core of our plugin will be the calculation function RampUpGen::next(int nSamples).

Let’s define this function.

First, we need to figure out how much to increment our counter at every clock tick.

This is done by dividing the frequency of our ramp generator with the samplerate (which is available to us using the sampleRate() function that comes with the plugin API) of our UGen.

We will let the user of our UGen decide what frequency our ramp generator should be running at.

This is done by polling the value that is supplied via the frequency parameter in the SuperCollider class defined above.

For this purpose, we use the in0(int argumentInputNum) method that comes with the plugin interface - it takes the index of the input argument as a parameter, in this case there is only one input parameter frequency so this will be equal to 0.

One last thing to note here is that we will poll this argument at control rate, that is: It only inputs one value per sample block (see explanation above). To get the full sample block input for audio rate parameters you use the in(int argumentInputNum) method.

// First UGen input is the frequency parameter
const float frequency = in0(0);

// Calculate increment value.
// Double precision is important in phase values
// because division errors are accumulated as well
double increment = static_cast<double>(frequency) / sampleRate();

Every time our calculation function is called, we need to increase our output by increment amount.

m_phase += increment;

Then, once our ramp generator reaches 1.0, we reset it to 0.0 to make it wrap back around at the beginning of the ramp.

const double minvalue = 0.0;
const double maxvalue = 1.0;

// Wrap
if (m_phase > maxvalue) {
	// Wrap phase value
	m_phase = minvalue + (m_phase - maxvalue);
} else if (m_phase < minvalue) {
	// in case phase is below minimum value
	m_phase = maxvalue - std::fabs(m_phase);
}

That’s it for the calculation function.

Documentation

The last thing we need to do is to document the functionality of our UGen so that it is usable for others. This is done in SuperCollider help file that will go with our plugin called RampUp.schelp. See the writing help and / or scdoc syntax help files for more information on how to do this. Be sure to add some good and clear examples to this.

The schelp file could look something like this:

class:: RampUpGen
summary:: A simple ramp generator
related:: Classes/Saw
categories:: UGens

description::

A simple ramp generator

classmethods::

method::kr

argument::frequency

Set the frequency of the ramp generator.

examples::

code::

// Scope the ramp generator at 1 hz
{ RampUpGen.kr(1.0) }.scope

::

Compile the plugin and try it out in SuperCollider

Now a first version of your plugin should be finished. Build and compile your plugin using the CMake commands discussed earlier in this tutorial, recompile the SuperCollider class library and try messing around with it.

Part 3: Finishing touches

Before our plugin is done, there are some nice little things we can do to touch it up and make it even better.

Using enums to keep track of inputs

Inputs and outputs in the C++ side of your UGen are represented as integers. The frequency-parameter in our RampUpGen UGen is represented by 0 since it’s the first input to the UGen. To make it more readable and easier to keep track of parameters when you add more of them later on, a simple trick is to use enums. At it’s most basic, an enum may be used as a collection of aliases for integers. Using this for your parameters makes your code easier to read and maintain.

In your header file, under the private:-keyword, add the following:

enum Inputs { Frequency };

Then, in your plugin code, whenever you need the input number for frequency you simply type Frequency. Another nice thing is that you only have to reorder the items in this enum if you reorder your parameters, and the changes will automatically propagate.

Delegating functionality to functions

Another thing that can help keep your code clean is to delegate some of the functionality to methods outside of the calculation function. Because we are about to add a second calculation function, we can extract a bit of the ramp code to its own method and then call that in the calculation function.

To do this, add in your header file:

inline float progressPhasor(float frequency);

And then implement it in your .cpp-file:

inline float RampUpGen::progressPhasor(float frequency) {
  // Calculate increment value.
  // Double precision is important in phase values
  // because division errors are accumulated as well
  double increment = static_cast<double>(frequency) / sampleRate();

  m_phase += increment;

  const double minvalue = 0.0;
  const double maxvalue = 1.0;

  // Wrap the phasor if it goes beyond the boundaries
  if (m_phase > maxvalue) {
    m_phase = minvalue + (m_phase - maxvalue);
  } else if (m_phase < minvalue) {
    // in case phase is below minimum value
    m_phase = maxvalue - std::fabs(m_phase);
  }

  return m_phase;
}

Interpolating control rate parameters

Sometimes the user of a plugin may wish to modulate one of the UGen’s parameters. In an environment such as SuperCollider, this is to be expected. When doing this, you may end up hearing crunchy or glitchy effects in the sound of your UGen. This is caused by a lack of interpolation in the control signal between each sample being processed. This results in steppy jumps between values instead of smooth trajectories. Let’s fix this!

The plugin API contains a very useful function for exactly this purpose. The trick is to use the type SlopeSignal<float> to represent and contain our input parameter’s value. This is produced using the makeSlope(current_value, previous_value) function. And then, on each iteration of our calculation function’s for-loop, we use the .consume() method to slowly step from the previous sample block’s value to the current one. And then, after the for-loop we store the final value in a member variable that will be used next time the block is being processed.

This is what our calculation function looks like with parameter interpolation:

void RampUpGen::next(int nSamples) {
  const float frequencyParam = in(Frequency)[0];
  SlopeSignal<float> slopedFrequency =
      makeSlope(frequencyParam, m_frequency_past);
  float *outbuf = out(0);

  for (int i = 0; i < nSamples; ++i) {
    // Calculate increment value.
    // Double precision is important in phase values
    // because division errors are accumulated as well
    double increment =
        static_cast<double>(slopedFrequency.consume()) / sampleRate();

    m_phase += increment;

    const double minvalue = 0.0;
    const double maxvalue = 1.0;

    // Wrap the phasor if it goes beyond the boundaries
    if (m_phase > maxvalue) {
      m_phase = minvalue + (m_phase - maxvalue);
    } else if (m_phase < minvalue) {
      // in case phase is below minimum value
      m_phase = maxvalue - std::fabs(m_phase);
    }

    outbuf[i] = m_phase;
  }

  // Store final value of frequency slope to
  // be used next time the calculation
  // function runs
  m_frequency_past = slopedFrequency.value;
}

Setting calculation function based on input rates

Let’s take this a step further. We actually only need to interpolate the frequency parameter if the parameter’s input is control rate. If the input is audio rate, we get a full block’s worth of frequency-parameter and thus we don’t need the interpolation.

The way to work around this and similar problems with multiple situations for the same plugin is to use multiple calculation functions, one for each calculation rate input for example. In our example we only have one parameter so we can make a next_a and next_k version of that function, one for audio rate inputs to the frequency-parameter and one for control (and scalar) rate inputs.

The Server Plugin API comes with a function you can use to poll an input number to see what rate it is running at:

inRate(int inputNumber)

This is useful when used in combination with the constants (calc_ScalarRate for scalar, calc_BufRate for control and calc_FullRate for audio rate) that represent each of the input rates (these also come with the API):

RampUpGen::RampUpGen() {

  // Set the UGen's calculation function depending on the rate of the first
  // argument (frequency)
  if (inRate(Frequency) == calc_FullRate) {
    mCalcFunc = make_calc_function<RampUpGen, &RampUpGen::next_a>();

    // Calculate first value
    next_a(1);
  } else {
    mCalcFunc = make_calc_function<RampUpGen, &RampUpGen::next_k>();

    // Calculate first value
    next_k(1);
  };
}

And then our two calculation functions (next_a for audio rate frequency-parameter and next_k for control rate):

// Calculation function for audio rate frequency input
void RampUpGen::next_a(int nSamples) {
  const float *frequency = in(Frequency);
  float *outbuf = out(0);

  for (int i = 0; i < nSamples; ++i) {
    // Calculate increment value.
    // Double precision is important in phase values
    // because division errors are accumulated as well
    double increment = static_cast<double>(frequency[i]) / sampleRate();

    m_phase += increment;

    const double minvalue = 0.0;
    const double maxvalue = 1.0;

    // Wrap the phasor if it goes beyond the boundaries
    if (m_phase > maxvalue) {
      m_phase = minvalue + (m_phase - maxvalue);
    } else if (m_phase < minvalue) {
      // in case phase is below minimum value
      m_phase = maxvalue - std::fabs(m_phase);
    }

    outbuf[i] = m_phase;
  }
}

// Calculation function for control rate frequency input
void RampUpGen::next_k(int nSamples) {
  const float frequencyParam = in(Frequency)[0];
  SlopeSignal<float> slopedFrequency =
      makeSlope(frequencyParam, m_frequency_past);
  float *outbuf = out(0);

  for (int i = 0; i < nSamples; ++i) {
    // Calculate increment value.
    // Double precision is important in phase values
    // because division errors are accumulated as well
    double increment =
        static_cast<double>(slopedFrequency.consume()) / sampleRate();

    m_phase += increment;

    const double minvalue = 0.0;
    const double maxvalue = 1.0;

    // Wrap the phasor if it goes beyond the boundaries
    if (m_phase > maxvalue) {
      m_phase = minvalue + (m_phase - maxvalue);
    } else if (m_phase < minvalue) {
      // in case phase is below minimum value
      m_phase = maxvalue - std::fabs(m_phase);
    }

    outbuf[i] = m_phase;
  }

  // Store final value of frequency slope to be used next time the calculation
  // function runs
  m_frequency_past = slopedFrequency.value;
}

General tips and troubleshooting

Tools that may come in handy

Make the compiler more strict

Adding the following section to your CMakeLists.txt will make the compiler a lot more strict and give you warnings on unused variables etcetera. This may be overwhelming at first but it helps make your code cleaner.

# Set compiler pickyness for Unix builds
# ... This picks up a ton of extra little things
if(CMAKE_COMPILER_IS_GNUCXX)
  add_compile_options(-Wall -Wextra -pedantic)
endif(CMAKE_COMPILER_IS_GNUCXX)

Common issues

One of the difficulties of coding plugins in C++ is that when something is wrong, you have to debug - you rarely get nice error messages to help you out. That said, here are some of the problems and causes that have caused me trouble in 9/10 situations.

Problem: The server exits as soon as the plugin is initialized in a patch.

Whenever I have experienced this, it has come down to problems in the way I have allocated memory.

This could also be caused by something dividing by zero somewhere in your code.

Another common mistake I make is to inherit the wrong class in the .sc-file containing the SuperCollider interface for my C++ code. This error may occur if, for example, you are inheriting from MultiOutUGen but haven’t defined how many channels to use.

Problem: The sound is crunchy when I modulate the parameters of my plugin

You probably haven’t used interpolation for control rate parameters. See the previous part of this tutorial about smoothing control rate signals using SlopeSignal.

Problem: [Esoteric things] are happening

Make sure that all variables are initialized correctly. C++ allows you to use variables even though you haven’t initialized them with any values - this will lead to the variable being filled with random junk from your computer’s memory that can easily cause strange behaviour.


This tutorial was written with the support of Notam - The Norwegian Centre for Technology in Arts and Music.

20 Likes

Resources

1 Like

Contributing

Contributions are very welcome.

If you see any errors or omissions, feel free to fix them by either creating an issue or (even better!) a pull request on the github repo for this tutorial.

Thanks!

Hi Mads. Thanks so much for this tutorial!
I’ve followed the instructions but keep on getting this error message: “exception in GraphDef_Recv: UGen ‘RampUpGen’ not installed.” I’m sure there might be a crucial step I’m missing (being a total novice).

Hi Héctor - what cmake commands did you use to build and install? Best

The ones specified:

cmake -B “build” -S . -DSC_PATH="/path/to/sc/sourcecode"
-DCMAKE_INSTALL_PREFIX="/path/to/your/extensions/dir"

and

cmake --build build --config “Release” --target install

However, I’m working on a Linux terminal and trying to test the plugin on SuperCollider on Windows, but I’m not getting any scx files. Anything I should add to either cmake command?

Hi Héctor. I am not sure if you can compile for Windows from WSL ( I don’t use windows so can’t say really but I’d recommend running cmake from the windows side of things). Was the compile succesful otherwise?

Hi Mark. The compile was successful otherwise, I’ll try to do it again from PowerShell or something and let you know if I have any luck.

I’d like to contribute with a draft “reference” to the C++ plugin interface. I’m just copy/pasting function signatures and their comments from include/plugin_interface/SC_Plugin.hpp, reordering them a little bit, and let’s see if a list makes it easier to access this info.

Traditionally, UGens are written C-style, as a set of three functions and a Unit struct. The cookie-cutter approach brings in a nice C++ interface, through the SCUnit class. Here is a list of things SCUnit can do for you:

Getting inputs and outputs:

/// get input signal at index
const float* in(int index)  const
/// get input signal at index (to be used with ZXP)
const float* zin(int index);
/// get first sample of input signal
float in0(int index) const;

/// get output signal at index
float* out(int index) const;
/// get output signal at index (to be used with ZXP)
float* zout(int index) const;
/// get reference to first sample of output signal
float& out0(int index) const;

/// get number of inputs
int numInputs() const;
/// get number of outputs
int numOutputs() const;

Getting input rates:

/// get rate of input signal
int inRate(int index) const;
/// test if input signal at index is scalar rate
bool isScalarRateIn(int index) const;
/// test if input signal at index is demand rate
bool isDemandRateIn(int index) const;
/// test if input signal at index is control rate
bool isControlRateIn(int index) const;
/// test if input signal at index is audio rate
bool isAudioRateIn(int index) const;

Sample/Control Rates and Block Size:

/// get sample rate of ugen
double sampleRate() const;
/// get sample duration (1 / sampleRate)
double sampleDur() const;
/// get sampling rate of audio signal
double fullSampleRate() const;

/// get control rate
double controlRate();
/// get duration of a control block
double controlDur() const;

/// get buffer size of ugen
int bufferSize() const;
/// get buffer size of audio signals
int fullBufferSize() const;
/// get the blocksize of the input
int inBufferSize(int index) cons;

Slopes:

/// calculate slope value (used internally by SlopeSignal)
template <typename FloatType>
FloatType calcSlope(FloatType next, FloatType prev) const;

Make and register calc functions:

template <typename UnitType, void (UnitType::*PointerToMember)(int)> static UnitCalcFunc
make_calc_function(void);

/// set calc function & compute initial sample
template <typename UnitType, void (UnitType::*PointerToMember)(int)>
void set_calc_function(void);

/// set calc function & compute initial sample
template <typename UnitType, void (UnitType::*VectorCalcFunc)(int), void (UnitType::*ScalarCalcFunc)(int)>
void set_vector_calc_function(void);
3 Likes

Wonderful - thank you! Can I add this as an appendix to the pdf?

1 Like

Sure! And sorry for the late reply!

Using the tutorial I managed to successfully make a first plugin :smiley:
Thanks!

Some further questions:

  • Could it be extended with the instructions to make github build releases from tags (I vaguely remember seeing something pass by, but I don’t remember when/where)?

  • Would it make sense to add this tutorial to the GitHub - supercollider/learn: Official SuperCollider tutorial project? (This was just an idea to keep tutorial stuff somewhat bundled together - I have no real preference here)

1 Like

Wonderful to hear and great feedback!

The github actions instructions are here: Automatically build, compile and release SuperCollider plugins using Github Actions :: Mads Kjeldgaard — Composer and developer

Please post a link to the plugins here if you are sharing them at some point. Very curious!

@madskjeldgaard thanks a lot for this! Super helpful!

One question about the development of SC: should the main C++ be more documented or this is something to avoid ? I am curious to know how this work for other FLOSS projects, I was thinking if this, on the one hand, could lead to a more dense and difficult code to read/work or if this, on the other hand, would help other developers to quickly understand, remember and fix code sections.

BTW, I though that the Blip UGen would be something simpler, somehow like a sync function, but I found a really dense code. Can someone explain basically how the Blip UGen works (OscUGens.cpp) ?

void Blip_Ctor(Blip* unit) {
    SETCALC(Blip_next);
    unit->m_freqin = ZIN0(0);
    unit->m_numharm = (int32)ZIN0(1);

    unit->m_cpstoinc = ft->mSineSize * SAMPLEDUR * 65536. * 0.5;
    int32 N = unit->m_numharm;
    int32 maxN = (int32)((SAMPLERATE * 0.5) / unit->m_freqin);
    if (N > maxN)
        N = maxN;
    if (N < 1)
        N = 1;
    unit->m_N = N;
    unit->m_scale = 0.5 / N;
    unit->m_phase = 0;

    Blip_next(unit, 1);
}

void Blip_next(Blip* unit, int inNumSamples) {
    float* out = ZOUT(0);
    float freqin = ZIN0(0);
    int numharm = (int32)ZIN0(1);

    int32 phase = unit->m_phase;

    float* numtbl = ft->mSine;
    float* dentbl = ft->mCosecant;

    int32 freq, N, prevN;
    float scale, prevscale;
    bool crossfade;
    if (numharm != unit->m_numharm || freqin != unit->m_freqin) {
        N = numharm;
        int32 maxN = (int32)((SAMPLERATE * 0.5) / freqin);
        if (N > maxN) {
            float maxfreqin;
            N = maxN;
            maxfreqin = sc_max(unit->m_freqin, freqin);
            freq = (int32)(unit->m_cpstoinc * maxfreqin);
        } else {
            if (N < 1) {
                N = 1;
            }
            freq = (int32)(unit->m_cpstoinc * freqin);
        }
        crossfade = N != unit->m_N;
        prevN = unit->m_N;
        prevscale = unit->m_scale;
        unit->m_N = N;
        unit->m_scale = scale = 0.5 / N;
    } else {
        N = unit->m_N;
        freq = (int32)(unit->m_cpstoinc * freqin);
        scale = unit->m_scale;
        crossfade = false;
    }
    int32 N2 = 2 * N + 1;

    if (crossfade) {
        int32 prevN2 = 2 * prevN + 1;
        float xfade_slope = unit->mRate->mSlopeFactor;
        float xfade = 0.f;
        LOOP1(
            inNumSamples, float* tbl = (float*)((char*)dentbl + ((phase >> xlobits) & xlomask13)); float t0 = tbl[0];
            float t1 = tbl[1]; if (t0 == kBadValue || t1 == kBadValue) {
                tbl = (float*)((char*)numtbl + ((phase >> xlobits) & xlomask13));
                t0 = tbl[0];
                t1 = tbl[1];
                float pfrac = PhaseFrac(phase);
                float denom = t0 + (t1 - t0) * pfrac;
                if (std::abs(denom) < 0.0005f) {
                    ZXP(out) = 1.f;
                } else {
                    int32 rphase = phase * prevN2;
                    pfrac = PhaseFrac(rphase);
                    tbl = (float*)((char*)numtbl + ((rphase >> xlobits) & xlomask13));
                    float numer = lininterp(pfrac, tbl[0], tbl[1]);
                    float n1 = (numer / denom - 1.f) * prevscale;

                    rphase = phase * N2;
                    pfrac = PhaseFrac(rphase);
                    tbl = (float*)((char*)numtbl + ((rphase >> xlobits) & xlomask13));
                    numer = lininterp(pfrac, tbl[0], tbl[1]);
                    float n2 = (numer / denom - 1.f) * scale;

                    ZXP(out) = lininterp(xfade, n1, n2);
                }
            } else {
                float pfrac = PhaseFrac(phase);
                float denom = t0 + (t1 - t0) * pfrac;

                int32 rphase = phase * prevN2;
                pfrac = PhaseFrac(rphase);
                float* tbl = (float*)((char*)numtbl + ((rphase >> xlobits) & xlomask13));
                float numer = lininterp(pfrac, tbl[0], tbl[1]);
                float n1 = (numer * denom - 1.f) * prevscale;

                rphase = phase * N2;
                pfrac = PhaseFrac(rphase);
                tbl = (float*)((char*)numtbl + ((rphase >> xlobits) & xlomask13));
                numer = lininterp(pfrac, tbl[0], tbl[1]);
                float n2 = (numer * denom - 1.f) * scale;

                ZXP(out) = lininterp(xfade, n1, n2);
            } phase += freq;
            xfade += xfade_slope;);
    } else {
        // hmm, if freq is above sr/4 then revert to sine table osc w/ no interpolation ?
        // why bother, it isn't a common choice for a fundamental.
        LOOP1(
            inNumSamples, float* tbl = (float*)((char*)dentbl + ((phase >> xlobits) & xlomask13)); float t0 = tbl[0];
            float t1 = tbl[1]; if (t0 == kBadValue || t1 == kBadValue) {
                tbl = (float*)((char*)numtbl + ((phase >> xlobits) & xlomask13));
                t0 = tbl[0];
                t1 = tbl[1];
                float pfrac = PhaseFrac(phase);
                float denom = t0 + (t1 - t0) * pfrac;
                if (std::abs(denom) < 0.0005f) {
                    ZXP(out) = 1.f;
                } else {
                    int32 rphase = phase * N2;
                    pfrac = PhaseFrac(rphase);
                    tbl = (float*)((char*)numtbl + ((rphase >> xlobits) & xlomask13));
                    float numer = lininterp(pfrac, tbl[0], tbl[1]);
                    ZXP(out) = (numer / denom - 1.f) * scale;
                }
            } else {
                float pfrac = PhaseFrac(phase);
                float denom = t0 + (t1 - t0) * pfrac;
                int32 rphase = phase * N2;
                pfrac = PhaseFrac(rphase);
                tbl = (float*)((char*)numtbl + ((rphase >> xlobits) & xlomask13));
                float numer = lininterp(pfrac, tbl[0], tbl[1]);
                ZXP(out) = (numer * denom - 1.f) * scale;
            } phase += freq;);
    }

    unit->m_phase = phase;
    unit->m_freqin = freqin;
    unit->m_numharm = numharm;
}

I believe this code was adapted from Csound’s buzz and gbuzz opcodes, which use Discrete Summation Formulae.

1 Like