Chapter Four: Synthesis

14. A Digital Synthesis Language Sampler | Page 12

SuperCollider (SC)
Prepared by Eli Fieldsteel
Director of the University of Illinois Experimental Music Studios

History

SuperCollider (SC) is an environment and programming language for sound, typically described as a platform for “real-time audio synthesis and algorithmic composition,” created and originally released by software engineer James McCartney in 1996. In 2002, McCartney generously made the project available to the public as free, open-source software. In the two decades that followed, SC has attracted an increasingly large following of users from various artistic and scientific disciplines. The project is now maintained on GitHub, supported by an active community of volunteer developers, and runs on macOS, Windows, and Linux. At the time of writing, the latest version of SC is 3.12.2.

Features

Among SC’s most distinguishing features is its de-coupling of the programming code from the audio synthesis. SC appears as one program, but actually exists as two: an interpreted, object-oriented programming language called sclang (often called “the language”), and a lean, powerful audio server application called scsynth (often called “the server”). These two programs communicate by exchanging Open Sound Control (OSC) messages. In most cases, these two applications run together on the same computer, but this networked approach offers unique advantages and facilitates otherwise difficult-to-achieve creative practices. It is possible to control scsynth using any OSC-compliant software, provided it is running on either the same computer or the same local area network. The live coding platform Sonic Pi, for instance, is a Ruby-based system that runs scsynth in the background. The language/server separation also lends itself to interesting multi-computer installations and performances, e.g. laptop ensembles or pieces in which the audience can participate with smartphones. This separation also means that if one program crashes, the other program is generally unaffected. Beginning with version 3.6, SC includes the SC IDE (Integrated Development Environment), a visual interface that enhances the coding experience by providing an array of powerful features, such as document tabs, an embedded help browser, and the ability to split the text editor horizontally/vertically (prior to version 3.6, the SC interface resembled ultra-minimal text-editing software). The centerpiece of the IDE is the code editor,  which is surrounded by three components that can be freely detached and rearranged: the help browser, the post window, and the document list.

SC IDE

SC is listed in one of its help files as having over 300 unit generators (UGens), although this figure is probably a drastic undercount today. In lieu of a precise quantity, it is more meaningful to say that the UGen collection facilitates a rich and vibrant family of signal generation and processing techniques, easily matching and perhaps exceeding sibling environments such as Csound, Max, and Kyma.

SC is an interpreted language, as opposed to a compiled language. The practical consequence of this design is that code is run on a line-by-line basis, rather than having to write an entire program before execution. Therefore, using SC is often a freer and more dynamic experience than other languages, as it allows (and perhaps encourages) the user to make musical choices in the heat of the moment. It is a solid choice for live coding and similar real-time improvisational practices. Of course, projects in SC can also be structured to accommodate deterministic structures, akin to Csound’s orchestra-score paradigm. One of the greatest strengths of SC (and maybe, one of its greatest learning obstacles) is that it does not impose any particular workflow style on the user. Two SC projects by two different composers may appear vastly different not only in terms of code and sound content, but also style and architecture.

Sample Code with Explanation (readers may wish to download SC to work along with this section):

Writing and Evaluating Code

Before making sound, it is helpful to get familiar with basic requirements of writing and evaluating code. The end of a SC code statement is always designated with a semicolon. When a list of arguments is provided to an object, the arguments must be separated by commas. Single-line comments are preceded with a double forward slash, and a multi-line comment is designated with a slash-asterisk at the beginning, and an asterisk-slash at the end. With very few exceptions, SC is indifferent to whitespace, so spaces and return characters can be freely used.

Two different keystrokes are used to evaluate code. One is shift-enter, which evaluates a single line of code. Because SC is a dynamically interpreted language, it uses the placement of the mouse cursor to determine which line of code to run. Note that the beginning and end of a “line” of code is determined by the presence of return characters, not by semicolons. In many cases, however, you may want to run several lines of code as a single unit. To do this, we enclose these multiple lines of code in parentheses, with each parenthesis isolated on its own line, then place the mouse cursor anywhere in the enclosure and press command-enter (macOS) or control-enter (Windows/Linux). Whenever code is evaluated, the result appears in the post window.

Note: throughout the rest of this document, ctrl is used to signify the control key on computers running Windows and Linux. If you are running macOS, this abbreviation refers to the command key.

5.squared + 1; //place cursor here & press shift-enter
/*
place the cursor somewhere
in the enclosure below and
press ctrl-enter
*/
    (
    var num = 5;
    num = num.squared;
    num = num + 1;
    )

Variables

Like other programming languages, SC allows the creation of named variables, so that the data stored within then can subsequently be referenced and used with ease. In the example above, we declare a local variable named num using a var statement, and we initialize it with a value of 5 (initialization of variables is optional). We then use it to manipulate the value, ending up with 26. Local variables are only usable within the scope in which they are created, so after evaluating the code clump above, the num variable no longer exists. Additionally, variables must begin with a lowercase alphabetic character, may contain uppercase/lowercase letters, numbers, and underscores, and must be declared at the top of a code chunk.

Local variables are tidy and convenient for quick tasks, but we may sometimes want a more persistent variable. We can instead use so-called environment variables, which function with global scope under normal circumstances. An environment variable is created by preceding a variable name with a tilde. After evaluating the following line, you will find that ~num retains its value, even when evaluated in a different tab. This value is only lost if we quit and reopen SC, or if we recompile the class library (or if we manually change it to something else, of course).

~num = 9;

Additionally, as a convenience for quick testing and experimenting, the 26 lowercase alphabetic characters also function as global variables, and do not require a tilde, although lowercase s is conventionally reserved as a reference to the audio server, as we will see later.

Functions

A function is delineated with an enclosure of curly braces. The primary purpose of functions is to allow encapsulation of individual tasks so that they can be easily called upon later. At the very top of a function (before any variables are declared), we have the option to use an arg statement to declare one or more comma-separated arguments, which serve as inputs to the function. When evaluated, a function will return the value of its last statement (an explicit return designation is not required):

(
   ~math = {
       arg input;
       var number;
       number = input.squared;
       number = number + 1;
   };
)

 

To call a function, type its name, followed by a period and a parenthetical enclosure of comma-separated input argument values:

~math.(5); //returns 26

Arrays

An array is an ordered collection of items, delineated by an enclosure of square brackets, in which each item is addressable by integer index (starting at zero). Often, these items are numbers, but an array can contain any combination of data types. Items in an array can be accessed using the at method, or by following the name of the array with a square-bracketed index value:

x = [4, -3, 8.2, 0, 17]; //create the array
x.at(2); //return the 2nd item
x[0]; //alternative syntax: return the 0th item

Most mathematical operations that are defined for regular numbers will also work on arrays, resulting in the operation being applied to every item in the array:

x + 5; //each array item summed with 5

There are also some methods which are specifically designed for arrays and other types of collections:

x.reverse; //reverse the order
x.scramble; //randomly reorder the array
x.mirror; //extend the array by pivoting on the last item and reversing

Note that these methods do not alter the array, but rather return a modified version. If desired, we can alter the array by setting the same variable name equal to the modified version:

x = x.scramble; //now permanently reordered

The dup method creates an array of copies of an item. The exclamation mark serves as a syntax shortcut:

8.dup(4); //returns [ 8, 8, 8, 8 ]
8!4; //alternative syntax

If the duplicated item is a function, the function will be evaluated once for each array entry created. This can be useful for creating, among other things, collections of random values. Note that if we remove the curly braces in the following example, a random value is generated once, and then duplicated to populate the array.

{ rrand(0, 9) }!8; //8 random values between 0 and 9.
 rrand(0, 9)!8; //8 copies of one random value

As we will see shortly, arrays are particularly useful on the audio server, which will interpret an array as a multichannel signal.

Making Sound

The first step toward making sound is to boot the audio server. When SC starts up, a reference to scsynth is automatically stored in the variable s, so the first line in the following example is typically not necessary. After running the code below, the post window will display some information about the server configuration, and the server numbers in the bottom-right corner of the IDE should change from white to green.

(
s = Server.local;
s.boot;
)

The simplest way to make sound is to create a function, fill it with some combination of UGens, and apply the play method. We have already seen a small example of sound-producing SC code earlier in this chapter, which plays a 220 Hz tone in the left speaker, and a 440 Hz tone in the right speaker:

{ [SinOsc.ar(220, 0, 0.2), SinOsc.ar(440, 0, 0.2)] }.play;

But wait! How do we make it stop!? By far, the most useful keyboard shortcut is ctrl-period, which destroys all signal-processing units on scsynth.

Although this example is very simple, there are a few important features to unpack and observe. First, the SinOsc UGen (a sine wave oscillator) makes use of the ar method, which means these two sine wave oscillators are running at the audio rate. The kr and ir methods can be used to specify control rate (by default, one control cycle contains 64 audio samples) and initialization rate for some UGens. Each SinOsc is supplied with three arguments, which represent frequency in Hertz, phase in radians, and amplitude on a normalized scale from 0 to 1. Naturally, other UGens expect their own unique sequence of argument values (for example, the PinkNoise UGen has no frequency input). We also note that these two SinOsc UGens are enclosed within an array, thus producing a two-channel signal. SC will attempt to play the 0th signal to your 0th hardware output channel (typically the left speaker) and distributes subsequent signals to numerically ascending hardware output channels.

Among the most powerful features of scsynth is the mechanism by which arrays “propagate” through a series of UGens. When an array of values is provided as a UGen argument, the UGen itself expands into an array of several UGens, in which the original array values are distributed across the array of UGens. This feature is called multichannel expansion, and its primary consequence is that it enables concise text representation of large multichannel signals. For instance, the previous UGen function can be rewritten as follows, in which the array [220, 440] causes the SinOsc to transform into an array of two SinOsc UGens, with one at each frequency.

( 
  { 
        var sig, freq = [220, 440]; 
        sig = SinOsc.ar(freq, 0, 0.2); 
  }.play; 
)

A more complex and interesting sound example follows. We begin with a frequency of 55 Hz, which is transformed via multichannel expansion into an array of control-rate noise generators, each of which uniquely meanders around 55 Hz, capped at a deviation of ±0.2 semitones. This array is fed to the frequency input of a sawtooth oscillator (Saw), which becomes an array of eight uniquely detuned sawtooth oscillators. We then pass this bank of sawtooth oscillators through a bank of resonant low-pass filters (RLPF), which each have a uniquely varying cutoff frequency. Finally, we use Splay to distribute and spatialize this array of eight signals across a stereophonic field. Without this step, we would only hear the first two oscillators when using a stereo speaker setup.

( 
  x = { 
        arg freq = 55, amp = 0.1; 
        var sig, detune, cutoff; 
        detune = LFNoise1.kr(2!8).range(-0.2, 0.2).midiratio; 
        freq = freq * detune; 
        sig = Saw.ar(freq); 
        cutoff = LFNoise0.kr(4!8).exprange(100, 4000); 
        sig = RLPF.ar(sig, cutoff, 0.05); 
        sig = Splay.ar(sig) * amp; 
  }.play; 
)

Because we have stored the result of the function-dot-play construction in the global variable x, we can use a set message to update a parameter in real-time. The midicps method is used to convert MIDI note numbers to Hertz values.

x.set(\freq, 110);
x.set(\freq, 29.midicps);

Likewise, we can set the amplitude, which was declared as an input argument named amp and applied to the audio signal via multiplication. The dbamp method can be used to convert from dBFS to a normalized range between 0 and 1.

x.set(\amp, 0.02);
x.set(\amp, -20.dbamp);

A sound-generating process created with function-dot-play can be faded out with the release method, along with a duration value in seconds:

x.release(3);

The open-endedness of SC provides great power and flexibility, but also comes with inherent risk—there are remarkably few safeguards in place to protect you from harming your ears with accidentally large amplitude values! Frequency values, too, can also carry risk (negative frequency values cause some UGens to exhibit unstable behavior). It is ultimately your responsibility to supply safe, sensible values to scsynth, but there are some options to prevent explosive chaos. The simplest of these options is the Limiter UGen, which can be deployed at the end of a sound function:

( 
  { 
      var sig, freq = 330, amp = 0.1; 
      sig = SinOsc.ar(freq!2); 
      //possibly dangerous code goes here 
      sig = Limiter.ar(sig) * amp; 
  }.play; 
)

Envelopes

A generative UGen, such as SinOsc or PinkNoise, will generate a signal forever, until stopped by some external event like a release message or ctrl-period. In the real world, virtually every sound produced by an object has a finite duration, over the course of which its amplitude will rise and fall based on the object’s physical properties and the way in which it is physically excited. In the world of creative audio, we often model these amplitude changes using an envelope generator. In SC, the simplest envelope generator is the Line UGen, which outputs a signal that moves from one value to another over a duration.

(
  {
      var sig, env, freq = [220, 440]; 
      env = Line.kr(0, -80, 2, doneAction:2).dbamp; 
      sig = SinOsc.ar(freq, 0, 0.2); 
      sig = sig * env; 
  }.play; 
)

What does doneAction:2 mean? This bit of code tells scsynth that when this UGen has reached its endpoint, it is formally "finished" and that the sound-generating process it belongs to should be destroyed. Without this piece of information, the sound process will continue to (silently) exist on the audio server, where it will continue consume a small amount of CPU power, until we press ctrl-period. If we carelessly let many such stale processes accumulate, they will eventually overwhelm the server, resulting in sluggish behavior and audio glitches.

EnvGen is a more flexible UGen for creating custom breakpoint envelope shapes. It requires an instance of Env, which specifies the envelope shape via three arrays. The first array represents the amplitude values of the envelope points, the second represents the durations (in seconds) between points, and the third array represents the curvature values of the segments (zero is linear, ± values will “bend” the segments in either direction). Be aware that the first array always has one additional number, because when there are n envelope segments, there are n + 1 points that connect them. doneAction:2 is again necessary to ensure that the sound process is removed from the server when it is complete.

( 
  { 
      var sig, env, freq = [220, 440]; 
      env = EnvGen.kr( 
            Env.new( 
                  [0, 1, 0.15, 0.15, 0], 
                  [0.02, 0.5, 1.5, 1], 
                  [1, -2, 0, -2] 
            ), doneAction:2 
      ); 
      sig = SinOsc.ar(freq, 0, 0.2); 
      sig = sig * env; 
  }.play; 
)

 

Relatedly, we can plot an Env to visualize how its values influence its shape:

Env.new([0, 1, 0.15, 0.15, 0], [0.02, 0.5, 1.5, 1], [1, -2, 0, -2]).plot;


Advanced Example with Audio

To demonstrate the power a few lines of SC code can have, below is an advanced example using the random impulse generator Dust, with the audio results.

SuperCollider Code Audio Result
s.boot; 
   
(

  x = { 
  var sig, trig, buf, index, note; 
      trig = Decay2.ar(Dust.ar(0.15!8), 0.001, 0.003); 
      buf = LocalBuf.newFrom([ 0, 2, 5, 7, 10 ]); 
      index = LFNoise1.kr(0.3!8).range(0, 26); 
      note = 40 + DegreeToKey.kr(buf, index).lag(0.08); 
      sig = Ringz.ar(trig, note.midicps, 0.5); 
      sig = LPF.ar(tanh(sig) * 0.4, 2500); 
      sig = sig.blend(DelayL.ar(sig, 0.002, SinOsc.kr(3).exprange(0.001, 0.002)), 0.5); 
      sig = sig.blend(CombL.ar(sig, 0.5, LFNoise1.kr(0.01!8).range(0.2,0.5), 10), 0.25); 
      sig = Splay.ar(sig, 0.6); 
      sig = sig.blend(LPF.ar(GVerb.ar(sig.sum, 300, 5), 1000), 0.3); 
  }.play; 
) 
   
x.release(10);

 

The Help Browser

If you want to learn more about a UGen or method, you can search for it in the help browser. If you place your cursor on the name of a class (such as EnvGen) and press ctrl-d, the help page for that class will appear in the help browser. Each help file includes a list of related classes, a description of that class, the methods it understands, a description of the arguments it expects, and (usually) some example code. You can also use the keyboard shortcut shift-ctrl-d to bring up a help browser search bar.

These code examples in this section barely scratch the surface of what SC can do, but hopefully they provide material that invites tinkering and exploration on your own. Beyond simple generative sound functions, SC has numerous signal processing UGens (e.g. delays and reverbs), the ability to capture and process live microphone signals, MIDI, OSC, and HID functionality, FFT-based spectral tools, graphical user interface classes, and a powerful library of “Pattern” classes that enable high-level algorithmic sequencing.

Resources

The main SuperCollider page (includes download links):
http://supercollider.github.io/

The Official SuperCollider Forum:
https://scsynth.org

A list of SuperCollider Tutorials on the GitHub Wiki:
https://github.com/supercollider/supercollider/wiki/Tutorials

Scott Wilson, David Cottle & Nick Collins. The SuperCollider Book, MIT Press, 2011.
(At the time of writing this, a newer version of this book is scheduled to be released very soon)
https://mitpress.mit.edu/books/supercollider-book

Eli Fieldsteel. SuperCollider Tutorials on YouTube:
https://www.youtube.com/playlist?list=PLPYzvS8A_rTaNDweXe6PX4CXSGq4iEWYC

Eli Fieldsteel. SuperCollider "Mini" Tutorials on YouTube:
https://www.youtube.com/playlist?list=PLPYzvS8A_rTY5fYKmASs9j-g83332BMzr

Bruno Ruviaro. A Gentle Introduction to SuperCollider:
https://works.bepress.com/bruno-ruviaro/3/

User-uploaded SuperCollider code examples:
http://sccode.org/

SuperCollider on Reddit:
https://www.reddit.com/r/supercollider/

An archive of the retired SC-Users Mailing List, where you can search for answers to commonly asked questions:
https://listarc.cal.bham.ac.uk/