Supercollider 02 – Functions

audio, supercollider

Day Two of 30 Days of Supercollider

A word of warning about this series as a whole: this should not be taken as a comprehensive, step-by-step tutorial on how to use Supercollider. There are better resources out there for that. This will be a loose collection of deep, or not-so-deep, dives into different topics. A lot of it is just documenting stuff for myself. Teaching is the best way to learn.

One of the first things you’ll learn about in Supercollider is functions, because the most common way demonstrated to play sounds at first is to wrap them in a function. But it took me quite a while to wrap my head around functions in Supercollider. Like how code is evaluated in the IDE, Supercollider goes way off the beaten track with functions.

Functions are defined by code inside curly brackets. The last value in the function is the return value. Here is an empty function:

{ }

If you evaluate that line, you’ll see -> a Function in the Post Window, telling you that it is a function.

Fairly often you’ll want to assign a function to a variable. You can do that like this:

f = {};

I’m just going to use the single letter f throughout this post. You can use other names but there are some rules around all that which I’m not going to get into here. Another day. For now just use f, or another single letter.

Say you want a function that returns a value, like 42, you can do this:

f = { 42; };

// or...

(
f = {
    42;
};
)

Note the parentheses around the second version. They’re not necessary, but as described in the first post in this series, it makes it so you can evaluate the entire function by putting your cursor in that region and hitting Ctrl-Enter/Cmd-Enter.

By the way, semicolons are not always required on the end of lines, but more often then not if you leave one off, you’ll wind up with an error that will be really tough to debug. It will just run two lines together and try to parse them like that. You can get away with it if it’s the last line of code in a block or you’re only evaluating a single line. Otherwise, best to use them.

Now, what do you do with functions? You call them. So you’d probably guess to do something like this:

f();

But that will give you an error:

ERROR: syntax error, unexpected ';', expecting BEGINCLOSEDFUNC or '{'
  in interpreted text
  line 1 char 4:

  f(); 
     ^
-----------------------------------
ERROR: Command line parse failed
-> nil

Sometimes you’ll see advice to do something like this:

f.();

And sure enough, that works, outputting what you’d expect:

-> 42

Now that just looks like some funky syntax decision, but what’s actually going on is a lot deeper. I eventually came to the realization that functions in Supercollider are not really functions like in other languages that are directly callable. I find it easier to think of them as special objects that have a few methods that can be called. You probably shouldn’t talk about it in those terms because nobody else does, but if you’re coming from another language, that may help you make sense of them.

Supercollider docs actually say:

A Function is an expression which defines operations to be performed when it is sent the value message.

So, yeah… value. That starts to look more normal:

f.value();

And that works! It turns out that f.() is really just an alias for f.value(). Better, better. Also, most of the time, you don’t need the parentheses. This works too:

f.value;

You will need to use parentheses when you pass arguments to functions though. So let’s cover arguments next.

Arguments are defined at the top of a function, before any other code. Use the key word arg followed by the argument name.

(
f = {
    arg foo;
    foo * 2;
};
)

When you evaluate it, you can now call it with value, the argument inside parentheses:

f.value(8);

As expected, this will print -> 16 in the Post Window.

Multiple arguments can be added to the same arg line with commas:

(
f = {
    arg foo, bar;
    foo * bar;
};
)

And now you can call it, passing in two args:

f.value(8, 3);

And this should print -> 24.

Arguments can also use default values, just set them in the arg line:

(
f = {
    arg foo = 10, bar = 3;
    foo * bar;
};
)

Now you can call this with 0, 1 or both arguments.

f.value();     // 30 - using both defaults
f.value(7);    // 21 - using only the second default
f.value(7, 2); // 14 - using no defaults

Like some other languages, such as Python, you can also use named arguments, or a mix of named and unnamed.

f.value(7, 3);           // unnamed
f.value(7, bar: 3);      // unnamed and named
f.value(foo: 7, bar: 3); // both named

With named args you can order them however you want and even skip arguments, assuming they have defaults. But once you use one named argument, all the rest must be named.

f.value(bar: 3, foo: 7); // opposite order
f.value(bar: 3);        // skip the first arg
f.value(foo: 7, 3);     // illegal! will throw an error

Lastly, there’s an alternate way to specify arguments. Rather than the arg key word, you can enclose the arguments line in a pair of pipe | characters.

(
f = {
    | foo = 10, bar = 3 |
    foo * bar;
};
)

That’s just a matter of preference. However you want to do it is up to you.

One last thing I want to go over on functions: functions that play sound. This one confused me for quite a while. Without going into too much detail just yet, there are various classes called Unit Generators that are mostly used to generate sound. SinOsc is a common one. It generates a sine wave oscillator – a really basic sound. Normally you call the ar method of that class to generate a sound of a particular frequency, such as SinOsc.ar(400) to generate a 400hz tone.

But that line of code does not play the sound. You’ll most often see something like this:

{ SinOsc.ar(400); }.play;

You can type that in and evaluate it and hear the sound. We’ll go more into unit generators later.

But I could not wrap my head around this one for a while. So SinOsc.ar(400) creates the unit generator. I’d expect that you’d play it like so:

SinOsc.ar(400).play;

But that gives you an error that the play message is not understood. But you have a function… the last line of the function returns that generator, and then you call play on that generator. How is that different from just calling it directly? I finally understood it though. It turns out that like value, play is a special message that you can send to a function. The details get a bit deep, but the bottom line is that when you call play on a function, it tries to evaluate the return value of that function as something that it can send to the server and play as a sound. Just calling play on the generator itself doesn’t work because play is a message you send to a function, not a generator. It’s a bit more complex than that, and I understand a good bit of what’s actually going on there, but I’m not going to try to explain it in this post. Enough for one day.

One thought on “Supercollider 02 – Functions

  1. This will be an index of the articles I post about Supercollider. Warning: I don’t know a lot about Supercollider yet. This will be a journal of my discoveries as much as anything else. I pro…

Leave a Reply