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);
4 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;
}

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

1 Like

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)