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.

EDIT:

The contents of the tutorial has been moved to Github. Feel free to contribute to it there and to keep the discussion alive here :slight_smile:

Download the latest release of the tutorial as an ebook/pdf: Release Update the tutorial Ā· notam02/supercollider-plugin-tutorial Ā· GitHub


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

33 Likes

Resources

3 Likes

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);
5 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)

2 Likes

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!

1 Like

@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;
}

Hi. Thanks for the tutorial. I can’t seem to find any information on the syntax/specification for the .sc file. Can you point me to a resource on that topic?

Hi ! The .SC file is a class file. There’s a small guide on how to write classes here

http://doc.sccode.org/Guides/WritingClasses.html

Hi All,

Fist of all thanks to Mads for this tutorial, unfortunately I’m a bit stuck at generating the cookie cutter template…

I’m on an M1 Mac, and I made sure to install python3.7 first, but maybe there is a problem between that version and M1 Macs? I’m a noob at python so any pointers appreciated!

Here’s how it looks when I try:

$ cookiecutter https://github.com/supercollider/cookiecutter-supercollider-plugin
You've downloaded /Users/bimjozz/.cookiecutters/cookiecutter-supercollider-plugin before. Is it okay to delete and re-download it? [yes]: 
full_path_to_supercollider_source [/home/wendy/supercollider (if you haven't cloned it yet, do that first! Press Ctrl-C to exit this script)]: /Users/bimjozz/tmp/supercollider
project_name [Simple Gain]: Simple Ramp
project_namespace [SimpleRamp]: 
repo_name [simpleramp]: 
plugin_name [SimpleRamp]: 
plugin_description [A simple audio volume gain plugin]: A simple ramp generator
full_name [Wendy Carlos]: 
github_username [wendy.carlos]: 
email [wendy.carlos@site.com]: 

Running pre-project-generation hook...

Checking Python version...

Checking for SuperCollider repository...

Running post-project-generation hook...

Initializing new Git repository
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint: 
hint: 	git config --global init.defaultBranch <name>
hint: 
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint: 
hint: 	git branch -m <name>
Initialized empty Git repository in /Users/bimjozz/simpleramp/.git/

Running CMake generation script
Traceback (most recent call last):
  File "/var/folders/1y/vvdm91yj107606d5q6zpm0580000gn/T/tmpg6zzilfa.py", line 25, in <module>
    '--install-cmake'
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 339, in call
    with Popen(*popenargs, **kwargs) as p:
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 800, in __init__
    restore_signals, start_new_session)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1551, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'python': 'python'
ERROR: Stopping generation because post_gen_project hook script didn't exit successfully
Hook script failed (exit status: 1)

Just bumping this question from a year ago. I have the same error.

Sam