Developing Supercollider Plugins For Norns- Where to Start?

this is specifically for PD… does PD change the block size per render call on ‘mobile platforms’

anyway, .what I do is start with a buf size that would normally be the largest i think is likely to be the case.
IF I find i need to extend it, then I extend it… but I don’t reduce it. (actually iirc, in this specific case, brds is chunking it into blocks of max 24, so its a bit of a moot point)
my aim here, is to not start creating very large buffers, as on a slow pc/rPI perhaps they set the buffer to 1024… and I really dont wanna allocate that ‘just in case’

for sure this is not ideal (nor ‘best practice’) but I seem to remember when I looked at this, the issue was PD only tells you the block size, during the render call … not during setup, I guess because setup is only called when the object is created, so if the audio buffer is changed on the fly, then it doesn’t get called again.

so if you only find out on the render call, you really dont have a lot of choice other than to reallocate when you find out - in the unlikely event that it will change.

but for sure, no arguments that its important not to do time-unbounded ops on the audio thread.

in the specific case of puredata, my understanding is that it does always double buffer to DEFDACBLKSIZE. (also true for libpd objc wrapper, but uptream puredata AudioUnit wrappers are still stubs.)

yes, but given a patch can use block~ to change the blocksize, I don’t think thats something you can use in an external… it needs to use the size of the buffer supplied in the render call.
anyway… this is all a bit off-topic … (the brds~ code is now ‘fixed’)


back on topic, porting to SC…
as @toneburst points out, its probably better to port Plaits rather than Braids now.

also you might want to consider a different direction,

on axoloti, rather than port ‘Braids’ as a single module - Johannes, created a number of modules which represented the individual oscillators, so that they could be re-combined in different ways. ( * )
I didn’t do this… for the simple reason for Orac I wanted a single module for the user to include.
but in a programming environment, I think the break down approach is a little more interesting/flexible.

( * ) Emile writes such clean / great code this was actually very simple

Yeah, again sorry for the derail. I picked braids for a few reasons, but didn’t realize how much of a pain the int -> float conversion would be. I figure a few exercises that would be helpful/illustrative of how to work with real time audio buffers:

  1. risset tone generator
  2. clipping distortion (basically just up scale and and then clamp)
  3. a simple delay

at this point i’m less interested in having a recreation of plaits/braids and more interested in getting a better handle of real time DSP.

yes, well, i certainly did not ask for an interrogation of pure data’s block scheduler internals with my pretty basic and (i thought) non-controversial observation. and i’m no expert in PD anyways :slight_smile: .thank you for informing me about block~. fwiw, i agree that always re-allocating upwards seems safest if you absolutely do need to allocate.

strategies for managing buffering layers for algos, definitely seems on topic for any plugin format. it’s a common requirement (any time you want to resample / interpolate / delay / &c) and the general solution is some kind of ring buffer. (i don’t see why the MI oscillators actually need their own output buffer, except that it’s less work than updating each FooRender() function to convert to float… ok right, moving on. )

like, here is an external i just made last week for fun. it is a chaotic oscillator that requires 2 layers of internal buffering: one for signal analysis (it works by weighted-averaging of a chaotic map history), and one for interpolation.

{https://github.com/catfact/rad/blob/master/source/projects/rad.worb_tilde/rad.worb_tilde.cpp}
{https://github.com/catfact/rad-lib/blob/cb95d10cc5bb33ef738eb90531c172591a8a76fe/worb.h}
{https://github.com/catfact/rad-lib/blob/cb95d10cc5bb33ef738eb90531c172591a8a76fe/interpolator.h}
{https://github.com/catfact/rad-lib/blob/cb95d10cc5bb33ef738eb90531c172591a8a76fe/generator.h}

implementation-wise, i’ve found the above structure to be widely useful for a broad class of oscillators and colored-noise generators: specify an update rate (frequency of “highest harmonic”,) a generator function that is called at the update rate, and a signal interpolation method.

i have a bunch more similar weird oscillators using same pattern; when i get a minute, i will wrap them in SuperCollider UGens as well. that would be a good opportunity to work on a good UGen template.

this discussion has made me realize that i was being a bit too glib in simply recommending a look at example-plugins and the official guides. there seems to be room for a clean UGen template that uses only the stuff relevant for a direct implementation of an audio algorithm.

given infinite time it sure would be nice to have a generic C++ wrapper system that targeted the plugin formats of pd/max/sc. (Faust is nice, but it’s not always the most expedient wray to express something.)


ok, these are fun questions:

going backawrds,

  1. clipping distortion is the simplest here, at base it is as you say:
y = min(1, max(-1, x * gain)) 

but of course hard clipping introduces infinite bandwidth expansion, and in general one uses some “soft clipping” shape to constrain the generation of harmonics. if the clipping function is a polynomial then the poly order constrains the order of harmonics, which is nice.

whatever function you use, static waveshaping is nice and simple because it requires no signal history at all.

here’s a kind of “bestiary” of waveshaping functions i’ve (mostly) collected or (occasionally) made up over the years, which include old standbys like tanh. they are expressed as python code, sometimes with multiple parameters and sometimes just gain. (in a plugin, some would be much to expensive to compute directly and i would use a LUT.)

some of these produce folding at higher parameter values.

def shaper_tsq(x, t):
    # two-stage quadratic
    # t is softclip threshold in (0, 1) exclusive
    g = 2.0
    ax = abs(x)
    sx = np.sign(x)
    t_1 = t - 1.
    a = g / (2. * t_1)
    b = g * t - (a * t_1 * t_1)
    if ax > t:
        q = ax - 1.
        y = a * q * q + b
        return y * sx / 2
    else:
        return x * g / 2


def shaper_bram(x, a):
    ax = np.abs(x)
    return x * (ax + a) / (x * x + (a - 1) * ax + 1)

def shaper_bram2(x, a):
    ax = np.abs(x)
    sx = np.sign(x)

    if ax < a:
        return x
    if ax > 1:
        return sx * (a + 1) / 2

    return sx * (a + (ax - a) / (1 + ((ax - a) / (1 - a)) ** 2))


def shaper_cubic(x, a):
    g = 2 ** a
    x = x * g
    y = ((3 * x / 2) - ((x * x * x) / 2))
    return y / g


def shaper_expo(x, a):
    sx = np.sign(x)
    return sx * (1 - (np.abs(x - sx) ** a))


def shaper_sine(x, a):
    # param = logarithmic pre-gain
    x = x * (2 ** a)
    return np.sin(np.pi * x / 2)


def shaper_reciprocal(x, a):
    # param = pregain
    x = x * (2 ** a)
    return np.sign(x) * (1 - (1 / (np.abs(x) + 1)))


def shaper_tanh(x, a):
    # param = pregain
    x = x * (2 ** a)
    return np.tanh(x)

def shaper_ulaw(x, a):
    ax = abs(x)
    sx = np.sign(x)
    return sx * np.log(1 + a * ax) / np.log(1 + a)

def shaper_alaw(x, a):
    ax = abs(x)
    sx = np.sign(x)
    denom = 1 + np.log(a)
    if ax < (1 / a):
        return sx * a * ax / denom
    else:
        return sx * (1 + np.log(a * ax)) / denom

# including to show useful ranges
test_shaper(shaper_bram, [1, 2, 3, 5, 7, 8, 9, 10])
test_shaper(shaper_bram2, [0.999, 0.8, 0.7, 0.5, 0.3, 0.15, 0.05, 0.001])
test_shaper(shaper_tsq, [0.9, 0.8, 0.7, 0.5, 0.3, 0.2, 0.1, 0.001], 1)
test_shaper(shaper_cubic, [-1, -0.5, 0, 0.25, 0.5], 1)
test_shaper(shaper_expo, [1, 2, 3, 4, 5, 6], 1)
test_shaper(shaper_sine, [-1, -0.5, -0.25, 0, 0.25, 0.5], 1)
test_shaper(shaper_reciprocal, [1, 2, 3, 4, 6, 8, 9, 10, 11, 12], 1)
test_shaper(shaper_tanh, [0, 0.5, 1, 2, 3, 4], 1)
test_shaper(shaper_alaw, np.exp(np.linspace(0, np.log(100), 10)))
test_shaper(shaper_ulaw, np.exp(np.linspace(0, np.log(300), 10)))

and here are their transfer functions and spectra. (spectra display is a little wonky, oh well.)

shaper_alaw shaper_bram shaper_bram2 shaper_cubic shaper_expo shaper_reciprocal shaper_sine shaper_tanh shaper_tsq shaper_ulaw

ooh and here’s a wonderful trick from robert bristow-johnson: family of polynomials approximating integral of (1 - x^2)^N by binomial expansion… this gives you a really nice progression of odd-order distortion
(matlab code for the moment i’m afraid)
rbjpoly.m (446 Bytes)

  1. simple delay is also, well, simple. if delay time is a multiple of 1 sample, it is just a peek backwards into a ringbuffer. (see peek method in worb.h linked above for example.) for fractional delays, interpolate between neighboring samples with whatever interpolation is appropriate. (usually linear, or hermite spline, occasionally allpass if you want specific phase distortion effects, like in reverbs and phasers.)

  2. risset glissando is the most complex here. it is basically simple (some sine waves and some rate / amplitude functions) but tuning them can take some time. (i’ll see if i can dig up some old notes on those…)

6 Likes

FWIW, I’ve ported a few of the mutable modules to SuperCollider (among those is Plaits).
Sources are here:

UGens comiled for macos (scroll down all the way):
https://vboehm.net/downloads/

Sound demos:

11 Likes

That sounds good!

I’ve not really attempted making a plugin myself, having been sucked in to other stuff, since starting this discussion, but when I do get around to it, some kind of basic “insert your audio-rate algorithm here” template for custom SuperCollider UGens would be super useful, I think.

1 Like

Wow, cool!!

How feasible would it be to build these for Norns’ Arm processor?

hm, I guess it shouldn’t be too hard, but I haven’t tired it - so have no idea really.

If I have some free time later I will share the steps needed to get stuff building on Norns, major life event stuff happening today so more than likely I will get around to it tomorrow. It’s pretty straight forward if you’ve use make before.

3 Likes

I compiled some other ugens a long while back. I might give these a go later today if I have time.

3 Likes

oh man, a clouds ugen would be awesome!

1 Like

so I just complied MiClouds but not entirely sure what to do next to test it

I forget where I need to install these (and do I include the cpp files, or just the .so and .sc files?)

@Justmat do you wanna try?

EDIT (a few hours later): I have a very basic MiRings engine working.

4 Likes

Volker – these are awesome TY!

Clouds engine WIP

Thanks geplanteobsoleszenz!!

Edit: FWIW - I couldn’t get Plaits to compile on my pi, so would love to chat with someone to help figure that out.

(and I totally stole this cloud from @Justmat’s Showers :laughing: Thx Matt! )

6 Likes

@shreeswifty thanks, Patrick

@okyeron great you got it working!
Can’t really help with the pi, but what’s the compile error you’re getting?

here’s where it barfed on make

[ 96%] Building CXX object CMakeFiles/MiPlaits.dir/MiPlaits.cpp.o
[100%] Linking CXX shared module MiPlaits.so
c++: error: Accelerate: No such file or directory
c++: error: unrecognized command line option ‘-framework’
make[2]: *** [CMakeFiles/MiPlaits.dir/build.make:549: MiPlaits.so] Error 1
make[1]: *** [CMakeFiles/Makefile2:73: CMakeFiles/MiPlaits.dir/all] Error 2
make: *** [Makefile:84: all] Error 2

gcc (Raspbian 8.3.0-6+rpi1) 8.3.0
pulled supercollider source today but not sure what version that source is. (3.11.0?)

EDIT: Accelerate is MacOS only I’m reading??

EDIT 2: can I just delete target_link_libraries(${PROJECT_NAME} PUBLIC "-framework Accelerate") from CMakeLists.txt?

EDIT 3: removing that seems to have done the trick and it finished compiling without errors

EDIT 4: MiVerb hits this error:

[ 50%] Building CXX object CMakeFiles/MiVerb.dir/MiVerb.cpp.o
/home/we/mi-UGens/MiVerb/MiVerb.cpp:31:10: fatal error: SC_PlugIn.h: No such file or directory
 #include "SC_PlugIn.h"
          ^~~~~~~~~~~~~
compilation terminated.

which is odd since the others have that and compiled ok?

1 Like

ah, yes, I used apple’s vDSP_sve for trigger detection to speed up the vector summing. You can comment it out in the cmakeLists file. In the source you should probably delete the accelerate include at the beginning and replace the vDSP_sve call with a simple for loop which sums all values from the trigger input.

1 Like

EDIT 4: MiVerb hits this error:

sorry, that’s a leftover - comment out the “set(SC_PATH /Users/vb…” in cmakeLists.txt and provide your own path to sc sources.

2 Likes