Endlessly repeating...

Reasons to love SuperCollider

28 January 2010

When I first started to get into the nuts and bolts of sound generation (rather than tweaking someone else's plugins), I did what a lot of other people do and downloaded Pure Data. It's a great environment to start playing around in, you connect an oscillator block to a filter block to an output block and you've got a little subtractive synth and it's clear how the signal flows through it. That's absolutely great for learning how things work and, to my mind, Pd is one of the gems of open-source audio software. Having said that, Pd, Max/MSP and every other draw lines between the building blocks audio systems have a problem - spaghetti. The building blocks lend themselves well to simple signal flows but when you get a hankering to play with 16 operator FM it's not quite as easy to see how everything fits together and it requires a hell of a lot of mousing around to put the thing together. Sure there are ways to organize subsections and that helps a lot (and is good practice generally) but I tend not to start a project with a clear view of where I'm going so once things have gone a bit Italian it's too late. Take all this with a pinch of salt of course - there are people out there who do wonderful, complex work with Pd, I'm just not one of them.

Anyway, the point of that ramble was to say that there's a need sometimes for a more abstract approach than blocks and wires. That's where SuperCollider comes in. Programmers have been representing complex problems as text for many years now and SuperCollider brings that power to audio (without the need to take a degree level class in DSP). To highlight the power of text I wanted to share this little example that I wrote after being inspired by this example of data based reverberation.

SynthDef(\dataverb, { |t_trig, freq, amp = 0.5 att = 0.1, dec = 0.8, rtime = 0.04|
    var steps = 24, d, a, w, f, sig, controlEnv;
    d = (1..steps - 1).collect({|n| rtime * n}).addFirst(0);
    a = (2..steps).collect({|n| amp / n}).addFirst(amp);
    w = [0.5, 0.6, 0.7, 0.4];
    f = [freq, freq * 0.999, freq * 1.001, freq / 2, freq * 1.5];
    sig = Mix.new(
        Pulse.ar(f, w, mul: EnvGen.kr(Env.perc(att, dec), DelayN.kr(t_trig, d, d), a))
    );
    controlEnv = EnvGen.kr(Env.new([0, 0], [d.last + att + dec]), doneAction: 2);
    Out.ar(0, sig!2);
})

Those 12 lines of code represent 24 channels of a delay, an envelope and an oscillator all subsequently mixed down. The magic comes from SuperCollider's ability to take an array as one of the creation arguments of a sound generating block and create an instance of the block for each element of the array. In the example above the line

d = (1..steps - 1).collect({|n| rtime * n}).addFirst(0);

creates an array of 24 gradually increasing delay times. When this is provided as an argument to the delay block (DelayN.kr(t_trig, d, d)) you get 24 delays. No fuss, no patch cables. Want more? Just change the value of the steps variable. Want to individually filter each step, it's only a couple more lines of code.

To get an idea of what it sounds like have a listen to this:

If you haven't looked into the power of the SuperCollider language yet, I hope this inspires you to do so.