Introduction

This book is a collection of essays, guides, and tips for intermediate to advanced SuperCollider users.

SuperCollider architecture

SuperCollider is first and foremost an audio programming language, but there's a lot more to it than just a textual interface. More specifically, SuperCollider consists of a audio server architected to support for on-the-fly definition and reuse of DSP algorithms. Uniquely, the audio is separated from a client, which provides the actual sequencing and control.

The best way to summarize how SC differs from its peers is that it's not a toy. It's robust, efficient, and meant to gracefully handle a wide range of sound synthesis models, no matter how extreme. Design decisions in SC have been made with these goals in mind. It's not necessarily perfect software, but if didn't do its job pretty dang well, I wouldn't be here writing this tutorial.

In this chapter, we'll discuss specific details of server-client separation and how the server works, and explain why they are important. We won't get very deep into the implementation of SC — just the architecture that you, the user, have to work with and understand to fully harness the power of the platform.

Real-time audio constraints

Real-time audio software is under tight time constraints. If a single buffer of samples takes too long to compute, a horrible glitch is audible. When you're performing a live work in front of hundreds of people, you generally do not want your audio to glitch.

Efficiency is a huge concern for audio software that advertises itself as suitable for real time performance. This is true today, and it was especially true for computers in 1996. For a lot of software, performance is convenience. For real-time audio, performance is reliability.

The little CPU meter that you see on many audio programs, SuperCollider included, is an indicator of how fast DSP is happening. If CPU usage is low and stable, you can probably rest easy knowing that the program won't glitch out. If it's seriously fluctuating, you are in trouble.

SuperCollider's server not only needs to be efficient, but it needs to be flexible. You have to be able to send it arbitrary sound synthesis networks on the fly, and it has to compile and run those algorithms under real-time constraints.

Server-client separation

In SuperCollider 1 and 2, there was no server-client separation. The language connected directly to audio driver callbacks, and every operation in the SuperCollider environment was in the audio thread, where everything must arrive exactly on time.

This means that not only did UGens have to race against the clock to prevent glitches, but so did an entire interpreted programming language. Even with a cleverly written language that had real-time safe garbage collection, there are some operations that simply cannot be done safely with this scheme — loading a large sound file from disk would usually cause some kind of glitch, so it would have to be done before a composition starts.

Modern audio software usually runs separate threads for audio and other non-DSP (such as graphics). However, back in the 90's, threads were not as powerful and as widespread on consumer machines.

In SuperCollider 3, the decision was made to split the language and server into two separate processes. Not only does this allow unsafe operations to be relegated to one process, but this also keeps a nice separation of concerns between audio processing and control, allowing for all manner of alternate clients, alternate servers, multiple clients, multiple servers, and servers running on a different machine.

The servers

The SuperCollider server programs are scsynth and supernova. They both do roughly the same thing. supernova is a newer, ground-up rewrite of scsynth that adds support for efficient parallel processing on multi-core CPUs, which we'll discuss later in this chapter.

Both are actively maintained, but supernova is more experimental and buggy due to its younger age. Furthermore, supernova is not supported on Windows yet, and was not packaged with the macOS binary build until version 3.10. If you're a new SC user, scsynth is recommended.

Since scsynth and supernova implement much of the same functionality, I will use "scsynth" as a shorthand for "scsynth or supernova" throughout these tutorials.

A quick OSC primer

The client talks to scsynth and vice versa using Open Sound Control (OSC). Despite the name, OSC is not very audio-specific at all — it's a lightweight binary message protocol that lets you send over arrays of common data types. OSC is usually sent over TCP or UDP.

An OSC message starts with an address, indicated with a forward slash. This is interpreted as the name of a "command" that the receiver understands. It is then followed by a heterogeneous array containing any mixture of strings, floats, integers, binary data, and timestamps.

For example, /s_new is a command that causes scsynth to create a new Synth node — analogous to a MIDI note on. In sclang, you can send a message to scsynth using the NetAddr:sendMsg instance method:

Server.default.addr.sendMsg(['/s_new', "piano", -1, 0, "velocity", 0.5]);

This OSC message tells scsynth to create a new Synth from the piano SynthDef, setting velocity to 0.5. It is roughly equivalent to

Synth(\piano, [velocity: 0.5])

There are a few more details like OSC bundles, but we won't go into them yet. Check the Open Sound Control official website, which clearly explains the protocol as well as its binary representation.

The signal processing hierarchy of scsynth

A simple real-time modular audio application might have the ability to create and destroy some pre-fab audio processing nodes on the fly, and functionality to modulate and rewire them.

It seems nice. Unfortunately, a naive implementation of this formula really starts to buckle when put under real-world conditions. Musicians want polyphony — spawning the same synthesis algorithms over and over again from a template like on a keyboard synthesizer. The ability to efficiently define and reuse DSP algorithms is a rarely mentioned, but very important area where SC really shines.

In this section, we'll look at scsynth's unique hierarchical approach to scaffolding of modular audio processing.

Layer 1: Signals and block-based processing

Currently, all signals in the server are 32-bit floating-points.

Very importantly, audio signals are processed in blocks, not individual samples. The size of the block is determined by a command-line option to scsynth. By default, sclang sets a block size of 64. By processing audio in blocks, DSP code can take advantage of vectorization and caching to help speed up computation. However, there is a disadvantage that single-sample feedback is intractable without making your own UGen in C++. (There is a hack to get around this using demand-rate UGens, but I won't discuss it here.)

As you might know from the beginner tutorial, SC signals aren't just audio — some are control-rate signals, conventionally denoted .kr in UGen classes. These signals define only one sample per block. Performing arithmetic operations on such signals is significantly faster since there's simply less signal to deal with. In DSP parlance, control rate signals are "downsampled processing."

Block-based sample processing is fairly common in well-written audio software, but marking some signals as audio and others as control is a somewhat SC-unique deal.

Layer 2: UGens and Units

A UGen (unit generator) is a chunk of DSP code, such as an oscillator or filter. When instantiated, it creates a unit, which is an object that processes input signals and produces output signals.

In compiled form, UGens physically live in plugins, which are shared libraries (.scx files on Windows and macOS, .so files on Linux). When scsynth boots up, it searches for plugin files and loads them. The connective tissue between scsynth and plugins is known as the plugin interface, a binary interface written in C.

Layer 3: SynthDefs and Synths

A SynthDef is a blueprint describing how UGens are connected together. When instantiated, the resulting object is a connection of UGens known as a Synth. Despite the name, a Synth is nowhere close to the physical synthesizer object. It's a generic, miniature DSP program that can do pretty much anything from producing sound to processing sound to analyzing it: whatever is possible with a combination of UGens.

The client can't directly manipulate UGens or units — it has to first create a SynthDef using /d_recv (SynthDef:add) or related commands, then instantiate that SynthDef as a Synth using /s_new (Synth.new). To be able to communicate SynthDefs, SuperCollider defines a binary file format for the client to write and the server to read. The SynthDef file format is designed to be very compact: a typical SynthDef may be less than a kilobyte.

SynthDefs can define parameters for the Synths they instantiate and plug them into UGens like other signals. However, these parameters can only modulate inputs to UGens — they can't add new UGens, take them away, or rewire the graph dynamically. It's a bummer sometimes, but it's the price we pay for efficient and stable modular audio software. Parameters may be directly modulated by the client using commands such as /n_set (Synth:set).

Synths also talk to the outside world using special UGens — the big ones are In and Out, which allow reading from and writing from buses. A bus in SC is a signal, uniquely identified by a global index, which any Synth can write to or read from. Buses that correspond to hardware inputs and outputs are the most important.

Once again, we have a class vs. instance distinction between SynthDefs and Synths. This particular class vs. instance distinction is very musically important, because it enables efficient polyphony by spawning multiple sound events from a template. Furthermore, as part of scsynth's aim of real-time safety, spawning and freeing Synths is a very efficient process. You can easily fire off hundreds of Synths per second for something like granular synthesis.

Layer 4: Synth order and Groups

As soon as your arrangement of Synths gets complex with multiple Synths writing to the same buses, order becomes very important. For example, if you have instrument Synths and a separate Synth for an effect, the effect needs to process its audio after the instrument. /s_new has a parameter which allows you to insert the Synth relative to another.

An entirely linear approach to Synth order gets a little bit flimsy the more complex the order is. Groups offer a solution for robust, hierarchical Synth order, and the hierarchy formed is known as the "server tree," and the Synths and Groups within as "nodes."

supernova has a special feature known as a "parallel group" (ParGroup) where you don't care about order, and instead giving supernova a chance to split the Synths' workload across different threads. Since many musical applications involve separate, superimposed sound events that don't interact with each other (multiple tracks, polyphony), this can result in tremendous CPU improvements when used properly.

Conclusion

SuperCollider built unlike pretty much any other audio software out there, but a lot of its quirky designs are well justified for its goals as an efficient and flexible sound synthesis platform. The language and server are built from the ground up for real-time safety, and the SynthDef/Synth distinction allows for arbitrary polyphony with low overhead.

sclang quick reference

sclang is not a hard language to pick up if you're familiar with the likes of JavaScript, Python, Ruby, or Lua. Its object model and syntax are derived from Smalltalk.

We're just going to breeze through sclang basics here. This isn't meant as a complete language spec, but just enough information so you can get on comfortable footing for writing programs in SC.

Since I'm showing you around a whole gaddang programming language in this chapter, it's going to get very long and very dull. As such, this probably isn't the most exciting place to start — the beginner's guide is much better if you haven't yet written a line of code. But once you start writing more complex programs and need a quick reference on how to do basic programming tasks, this is the chapter for you.

sclang: what's so special about it?

Some programmers come to SuperCollider and are understandably highly skeptical of the idea of learning a new programming language, especially one that was designed in the 90's. And I won't mince words with you: sclang is far from perfect. It has aged better than some of its contemporaries in the computer music sphere, but has some really odd design decisions and long-standing limitations. Due to server-client separation, others have written clients for SC in Scala, Python, Lua, Haskell, and many other familiar languages. Why should you bother with sclang?

First off, sclang is "soft real-time safe." sclang isn't under the constraints of an audio thread like scsynth is, but it does need to sequence events with musically accurate rhythm under normal conditions, so it is written with features like real-time safe garbage collection. Few interpreted languages are designed with this in mind (Lua is a major one).

Second, sclang has a feature known as multichannel expansion. These three lines are identical:

// Naive way:
[SinOsc.ar(440), SinOsc.ar(660), SinOsc.ar(880)]

// "Don't Repeat Yourself" way, like in most programming languages:
[440, 660, 880].collect { |freq| SinOsc.ar(freq) };

// Best way, using multichannel expansion:
SinOsc.ar([440, 660, 880])

Most programming languages would at best offer the second syntax. But the third one is true idiomatic sclang, and it's more terse and elegant. I know you may be thinking that focusing on syntax is shallow, but this makes a huge difference when actually working on sound synthesis in your artistic practice.

I'm not claiming that multichannel expansion as implemented in sclang has been executed perfectly, but it's still tremendously convenient a lot of the time. Being able to duplicate any part of a sound synthesis graph easily give sclang an advantage over many other programming languages and graphical dataflow languages.

Finally, if you are a SuperCollider user, you should be aware that sclang is the #1 SuperCollider client. By only learning an alternate client, you are distancing yourself from the SC community and limiting your ability to receive or give help. Most SC users are sclang users, and that's a very important network effect if you're new to the platform.

Don't get me wrong ­— I'm not here to condemn alternate clients and the hard work that goes into them. I'm sure you'll like some of them better than sclang. But especially for people new to SC, sclang is strongly recommended.

Running code in sclang

For beginning users who come to SC from other languages, the standard method of running code in SC from the IDE can be pretty weird and offputting. As a refresher, you write SuperCollider code in .scd files, which have a bunch of code blocks hanging out in space. Rather than running the whole thing at once, individual lines are run using Shift+Enter, and blocks of code delimited by parentheses are run using Ctrl+Enter (Cmd+Enter on macOS).

Weird? Yes. But it's less weird once you realize that the IDE is a glorified REPL. The workflow will be familiar if you've ever used Jupyter or an interactive interpreter like Python's. It's just that REPL is the most common way of interacting with SuperCollider.

It's actually possible — and may be preferable — to run sclang like an ordinary program from the command line. This can be done by running

sclang my_file.scd

and it will work fine. The IDE equivalent is to fire up the interpreter, open up my_file.scd, and select "Language → Evaluate File."

Your file needs to be set up in a special way, however. The parentheses used to delimit code blocks for interactive execution will not work as expected, and will usually cause syntax errors. The file should be a single parenthetical block, although without the enclosing parentheses. By default, sclang does not exit at the end of file evaluation, so you need to manually run, e.g., 0.exit to quit with return code 0. [TODO: provide an actual example!]

Why use REPL in the IDE if sclang can mostly be run as a normal programming language? Although the workflow is unusual compared to programming environments, it's not without precedent when viewed as artistic audio software. In your average modular synth software or hardware, the "work interface" and the "performance interface" are the same. Same for SC: you write your code and you run your code in the same place. [TODO: clarify]

One obvious reason to use the IDE is if you incorporate live coding into your work — it's a fully capable live coding interface, and can even be augmented with custom behavior specifically for that practice. But even if you don't, the IDE provides several user interface benefits, such as showing you a CPU meter for the server (very important to ensure code is suitable for live performance) and giving you an easy interface for volume control and recording to disk.

If the whole REPL thing is weird for you, I understand, but I assure you that you will get used to it. It's all part of SC being live performance software.

Comments

// two slashes,

/*
or multiline style.
*/

/* Multiline comments /* DO */ nest. */

Blocks & variables

When you execute code in between parentheses, execute code from a single line, or execute an entire file, that's a block. The final expression in a block is its return value. When you execute a block in the IDE, the post window prints out the block's return value, preceded by a ->.

(
1;
2;
3;
)
// -> 3

You can declare local variables using var. They're lexically scoped, so they behave similarly to a lot of your favorite languages.

(
var foo = 3, bar;
var baz = 50, quux = foo + baz;
quux;
)
// -> 53

All var statements have to go at the top of the block, and can't be interspersed with other code. (I know, it sucks.) An unassigned variable is given a value of nil.

A variable name always starts with a lowercase Latin letter, then any number of characters in a-z A-Z 0-9 _. The reserved keywords are quite few:

var arg classvar const pi true false nil inf

These identifiers are valid variable names, but I wouldn't recommend them, because then you won't have access to the actual builtins in the local scope:

this super thisProcess thisFunction thisFunctionDef thisMethod thisThread
currentEnvironment topEnvironment

Assigning to variables outside of var statements is done in the regular C way, like foo = 3;. Assignments return the assigned value, so they may be chained (foo = bar = 3). Operators like += are not supported in the spirit of minimalist Smalltalk syntax.

Variables are scoped to the block they are in, and they cease to exist outside of the block. How do we share information between blocks then?

Environment variables and interpreter globals

sclang doesn't have global variables. Instead, sclang has a syntactic feature known as "environment variables" (not to be confused with the OS-level environment variables like PATH) that provides a substitute for global scope. These are notated with the ~ prefix. In casual conversation, SC users often refer to them as "tilde variables."

~foo = 3;
~foo // -> 3

These variables persist across blocks. However, what they really supply is dynamic scoping. They are setting and getting key-value data in a global value called currentEnvironment, which can be manipulated and changed using various instance and class methods of Environment.

Some people like to use exclusively environment variables to get out of typing var. I don't recommend it. Local variables have garbage collection and are less prone to mistakes (for example, a typo'd local variable results in a syntax error, while a typo'd tilde variable returns nil). Only use environment variables when information needs to be passed between interactively executed blocks.

I lied — sclang does actually have global variables, but it has exactly 26 of them: the letters a through z.

x = 3;
x // -> 3

I strongly advise against using these in any production code. They're okay when you're doing nonserious tests, but please don't use them in real compositions.

Note that a single-letter variable declared locally (e.g. var x) isn't global, and just behaves like any other local variable.

Method calls

In sclang, pretty much every useful operation is performed by doing method calls on objects. The object we're calling on is referred to as the receiver, and it can take any number of arguments. The following are the most common syntaxes for method calls:

receiver.doSomething(1, 2, 3);
doSomething(receiver, 1, 2, 3);

The second one looks like a function call, but it's not. doSomething is actually a method name, and not referring to any variable in the local scope.

Method calls may take keyword arguments:

receiver.doSomething(foo: 1, bar: 2, baz: 3);

If no arguments are provided, the parentheses may be omitted (as they conventionally are):

"Hello World".postln();
"Hello World".postln;

Users coming from JavaScript, Python, or C++ may be used to . being for attribute retrieval in addition to method calls. In sclang, attribute getters are just method calls.

Mathematics

I'm sure you can guess what the nil, true, and false keywords are. Integers (32-bit signed) and floats (64-bit) are notated in pretty obvious ways:

0 -1 1443 0x07 3.0 4.0 1e8 1e-5

If a decimal point is provided, there must be at least one digit on each side. Expressions like .5 and 3. are syntax errors. This avoids some ambiguous parses.

Your favorite arithmetic operators work as expected. But unlike many other languages, infix operator precedence is always left to right. The order of operations is not respected. These are identical:

3 + 5 * 4
(3 + 5) * 4

You're probably groaning right now, but it's internally consistent and not unheard of in language design. One good reason for it is that sclang supports operator overloading, allowing you to define your own arbitrary operators like @|@<>@|@. How would we define the order of operations for those?

I expect you'll find these operators useful (they are syntactic sugar for method calls):

+ - / * ** == != < > <= >=

/ always graduates to a float even if both of its arguments are integers. Same for the exponentiation operator **.

There is no unary -. When you write -1, it's actually a single token. To do unary negation on variables and other things that aren't numeric literals, use the neg method.

These are good math functions to have under your belt — you can figure them out easy:

asInteger asFloat floor ceil round abs sgn
sin cos tan tanh

Functions

Functions are denoted with curly braces. They are first-class objects. The return value is simply the final line, and the function is called using the .value method:

var myFunction = {
    someOtherObject.doSomething;
    3
};
myFunction.value
// -> 3

myFunction.value can be shortened to myFunction.(), but not myFunction().

Function arguments are denoted using this funky pipe-delimited section:

var myFunction = { |foo, bar, baz = 5|
    foo + bar + baz;
};
myFunction.(3, 4);
// -> 12

There is an older way of writing this: { arg foo, bar, baz = 5; ... }, but it's going out of fashion.

Like in JavaScript, functions not strict about variable arguments. Unspecified arguments with no explicit default value are set to nil. If too many arguments are provided, they are simply ignored.

Conditionals

Boolean operators in sclang are a little different from other languages.

(0 == 1).not  // -> false

// Equivalent:
true.and(false)
true and: false

// The second is known as "key binary operator" syntax, and can be used in the
// special case where only one argument is passed to the method.

// 'and' and 'or' require their second argument to be wrapped in a function
// for short circuiting to occur:
doThisThing.() or: { doThatThing.() }

In keeping with sclang's minimal syntax, control structures are just method calls involving functions.

(
if(2 + 2 == 4, {
    "Nice!".postln;
}, {
    "What?".postln;
});
)

// Shortcut syntax that may be used because all arguments are functions.

(
if(2 + 2 == 4) {
    "Nice!".postln;
} {
    "What?".postln;
};
)

// 'if' produces a return value from the function/block that was run, so it
// actually doubles as a ternary operator.
if(2 + 2 == 4, { "Nice" }, { "What" }).postln;

// 'case' is a quick way to write a bunch of nested 'if's. The first condition
// that's true is evaluated.
case
    { x < 0 } { "It's negative".postln; }
    { x > 1000 } { "It's nonnegative, but too big".postln; }
    { x % 2 == 0 } { "It's an even number".postln; };

// 'switch' is very similar to ordinary switch statements. Like 'if' and
// 'case', it passes along the return value of the function that was actually
// evaluated.
switch(thing,
    \option1, { "Option 1" },
    \option2, { "Option 2" }
).postln;

// You can wrap the values in functions — saves a few commas.
switch(thing)
    { \option1 } { "Option 1".postln }
    { \option2 } { "Option 2".postln };

sclang is strict about only Booleans being placed in if, not, and, and or. If you try something like if(0, ...), you will receive an error that says "Non Boolean in test."

if, switch, and case are not keywords. They are methods.

Arrays

Creation, getting, and setting are the usual. Zero-indexed.

(
var myArray = [1, 3, 4, 4, 3];
myArray[0] = myArray[3];
myArray[1] = "hey";
"Array size is %".format(myArray.size).postln;
myArray;
)
// Array size is 5
// -> [ 4, hey, 4, 4, 3 ]

// NOTE: getting and setting indices are actually syntactic sugar for the ".at"
// and ".put" methods.

Retrieving an out-of-bounds index returns nil. Setting an out-of-bounds index produces an error.

Here are a few useful ways to create arrays:

(0..4)    // -> [ 0, 1, 2, 3, 4 ], inclusive of both endpoints
(1,3..10) // -> [ 1, 3, 5, 7, 9 ]
4.dup(8)  // -> [ 4, 4, 4, 4, 4, 4, 4, 4 ]
{ |i| i ** 2 }.dup(8)   // -> [ 0, 1, 4, 9, 16, 25, 36, 49 ]

sclang supports multichannel expansion. Binary and unary operators, when applied to arrays, are broadcast to the array's elements:

[1, 2, 3, 4] + 4   // -> [ 5, 6, 7, 8 ]
[3, 4, 5] * [1, 6, 8]  // -> [ 3, 24, 40 ]

This sounds small, but it becomes unbelievably convenient when working with multichannel sound design.

Appending to arrays is done using the .add method. However, for efficiency .add may or may not be in place. To avoid unexpected behavior, always assign the return value of .add back to the slot for the original array:

(
var myArray = [1, 2, 3];
myArray = myArray.add(4);
)
// -> [1, 2, 3, 4]

Slices are denoted with .. and are inclusive of both indices:

(
var myArray = [1, 5, 2, 0, 4, 4];
myArray[2..4]
)
// -> [ 2, 0, 4 ]

The following tools are also useful — I am certain you can figure out how they work from their names and very brief examples, and if not, you can consult the documentation:

["con", "cat"] ++ ["en", "a", "tion"]
newArray = array.copy
array.indexOf(thing)
array.includes(thing)
["heads", "tails"].choose
array.scramble

Iteration

sclang has a rich variety of methods for iterating and filtering arrays. Here is a very terse summary of them.

// Prints numbers 0 through 7 inclusive.
8.do { |i| "i".postln; };

// Remember, 8.do { ... } is just shorthand for 8.do({ ... }).

// Do something for each element. index is optional.
array.do { |elem| ... };
array.do { |elem, index| ... };

// Do something for each element and make a new array from the return values.
// Often called "map" in other environments
var newArray = array.collect { |elem| ... };

// Filter the array and keep only elements for which the given function returns
// true. The function should only return Booleans.
var newArray = array.select { |elem| elem % 2 == 0 };

// Return true iff the function returns true for any/every element.
// The function should only return Booleans.
[1, 2, 3].any { |elem| elem % 2 == 0 }
[1, 2, 3].every { |elem| elem % 2 == 0 }

These methods don't lazy evaluate on arrays, so chaining them isn't efficient like in functional programming environments. SC does have native coroutines via the Routine class.

Array methods get the job done most of the time, but while loops are also sometimes necessary:

var x = 3;
while { x < 10 } {
    x.postln;
    x = x + 1;
};

// More idiomatic: (3..9).do { |x| x.postln }

Loops can be broken out of using the block method, which is exceedingly rare but occasionally the best option:

block { |break|
    5.do { |i|
        if(i > 3) {
            break.value("hey"); // "hey" is the return value of block { ... }
        };
    };
};

Strings and symbols

"Strings are delimited with double quotes."
"Strings can be
multiline."
"Escape characters: \t \f \v \n \r \\"

sclang will tolerate non-ASCII bytes in strings, but they are just bytes, and Unicode isn't really understood. The SCIDE displays UTF-8, so if you're careful, non-ASCII characters will come out unscathed. Sorry about that.

Strings are effectively immutable arrays of Char objects. Char objects have a literal syntax: $a.

Symbols are similar to strings, but are efficient when reused, and not so efficient for textual operations. A symbol is therefore ideal for identifiers that are only meaningful within your program, while a string is intended for actual text input, manipulation, and output.

'Symbols are delimited by single quotes'

// But if the symbol is a valid identifier, there's a much simpler syntax:
\validIdentifier2000

Here are some useful things to do with strings and symbols — any ambiguities can be cleared up by consulting the docs:

// Querying:
"string".size

// Conversion:
"string".asSymbol
\symbol.asString
3.asString
"3".asInteger
"3".asFloat

// Modification:
"concaten" ++ "ation"
"with" + "space" + "in" + "between"
"path/" +/+ "/joining"
"You can format %.".format("strings")

Composing in SuperCollider

It’s easy to write code in SC that sounds great for around 10 seconds, but immediately tires itself out. SuperCollider users from beginners to experts have long struggled with the question: how do I turn sound design patches into complete works of music with a beginning, middle, and end?

SC being a programming language, the options are limitless and there’s no specific attempt to impose a workflow on you. But like all music software, it isn’t neutral. When incorporating SC into a compositional practice, the strengths and weaknesses of the SC interface need to be taken into account.

Here are some ideas on how you can incorporate SC into a compositional practice. Probably the most important point before we proceed: you don't need to constrain yourself to make your piece entirely in SC. Many people use it as just one layer in a larger mix.

Live interaction

SC particularly excels one thing: live interaction. Even if you only plan on making fixed-media works, don't discount the option of using real-time interaction in your process. It is totally okay to make a live patch and record yourself using it. The live element doesn't need to become a performance -- it can just be part of your compositional workflow.

One obvious one is to use a MIDI controller with SC. Large-scale compositional form can be created by turning knobs and pressing buttons.

Or you can make a GUI in sclang, and use that as a live interface.

Or you can analyze microphone input and use analysis parameters to modulate parameters of your patch.

Or you can use other hardware interfaces. Hook up sensors via Arduino and SerialPort. On a laptop, you can also make use of the mouse, keyboard, webcam, and accelerometer.

Or you can run blocks of code live -- even if you aren’t interested in live coding as a performance practice, this is often a very easy way to create section transitions.

Sequencing in sclang

Also be aware of SC's major weakness: note entry via arrays is really, really awkward. I don't recommend typing in melodies with Pbind unless they're very simple.

If you're composing music with relatively traditional notions of melody and harmony, you might want to look into using SC as a MIDI synthesizer rather than a compositional interface. Try composing MIDI in a DAW, routing the MIDI to SuperCollider, and routing SuperCollider's audio output back into the DAW. (Or, of course, hook up a MIDI keyboard and play it, but I already mentioned that.)

There are also specialized score programs such as IanniX and OSSIA that work great with large-scale sequencing in SC. Alternatively, depending on your needs, you could build a timeline view in SC's GUI features -- although that sounds like a ton of work to me.

You can also embrace text-based entry if you like it. One easy way to get sequencing in SC is to write a parser for a domain-specific language for entering notes, drum patterns, etc. I have programmed drum patterns using single characters like this: "k.k. s.th thkk s.t.". Try developing your own personalized sequencer notation that specifically fits your musical practice.

If your piece evolves slowly, you can also use a Routine and schedule when certain synths stop and start. This is a good choice for drone and ambient music.

Unsurprisingly, the SC environment is great for algorithmically generated melody, harmony, and rhythm, which could replace ordinary sequencing.

Other ideas

Use SC as an effect on an instrument.

Make a patch in SC and record yourself messing around with parameters. Use a DAW to sample your favorite parts and assemble them into a piece.

Design individual note sounds in SC. Record them and load them into a sampler. Use it in a larger work.

Conclusions

Unlike a lot of other music software, SC has a very open workflow and finding the best process is a matter of exploration.