Supercollider
Supercollider is a platform for audio synthesis and algorithmic composition, used by musicians, artists and researchers working with sound.
It's free and open source software for Windows, MacOS and Linux.
Features
scsynth
– A real-time audio serversclang
– An interpreted programming languagescide
– An editor for sclang with an integrated help system
Documentation is provided (at https://doc.sccode.org/), and a lot more user examples at https://sccode.org/. Plus, sc-140, an amazing album with code examples!
Basic Tutorial
Type
4.squared
then, making sure your mouse cursor is placed somewhere on this line, press [shift]+[return], which tells the interpreter to execute the current line. You'll see a brief flash of color over the code, and the number 16 will appear in the post window.
Alternatively, type:
squared(4)
The parentheses used to contain the receiver are one type of enclosure in SC. Others include [square brackets], {curly braces}, double quotes,
and ‘single quotes.' Each type of enclosure has its own significance, and some have multiple uses.
The semicolon is the statement terminator.
Further, you can combine multiple methods into a single statement, using two different syntax styles:
4.squared.reciprocal;
or
reciprocal(squared(4));
Or even, for a two-argument function:
3.pow(4);
instead of just:
pow(3, 4);
Posting a Value
The postln method has the effect of printing its receiver to the post window, followed by a new line.
Variables
Variables declared using a var statement are local variables; they are local to the evaluated code of which they are a part. This means that once evaluation is complete, variables created in this way will no longer exist.
If we want to retain a variable, to be used again in the future as part of a separate code evaluation, we can use an environment variable, created by preceding the variable name with a tilde character (~). Alternatively, we can use one of the twenty-six lowercase alphabetic characters, which are reserved as interpreter variables. Both environment and interpreter variables can be used without a local declaration, and both behave with global scope, that is, they will retain their value (even across multiple code documents) as long as the interpreter remains active.
The Class Browser
The class browser is a graphical tool for browsing the class tree. The browse method can be applied to any class name (e.g. Array.browse
), which invokes the browser and displays information for that class.
Introduction to Supercollider's Classes and Methods
Integers and Floats
The Integer
and Float
classes represent numerical values. Some programming languages are quite particular about the distinction between integers and floats, balking at certain operations that attempt to combine them, but SC is flexible, and will typically convert automatically as needed. Integers and floats are closely related on the class tree and therefore share many methods, but there are a few methods which apply to one and not the other.
We'll begin our tour by introducing the class
method, which returns the class of its receiver. This method is not exclusive to integers and floats; in fact, every object knows how to respond to it! However, it's relevant here in showing the classes to which different numbers belong.
4.class; // -> Integer 4.0.class; // -> Float
There are many methods that perform mathematical operations with numbers. Those that perform an operation involving two values are called binary operators, while those that perform an operation on a single receiver, like taking the square root of a number, are called unary operators.
abs(x) | Absolute value. Non-negative value of x, i.e., distance from zero. |
ceil(x) | Round up to the nearest greater whole number. |
floor(x) | Round up to the nearest smaller whole number. |
neg(x) | Negation. Positive numbers become negative and vice-versa. |
reciprocal(x) | Return 1 divided by x. |
sqrt(x) | Return the square root of x. |
squared(x) | Return x raised to the power of 2. |
x + y | |
x - y | |
x * y | |
x / y | Division |
x ** y or x.pow(y) | Return x raised to the power of y |
x % y or x.mod(y); | modulo |
x.round(y) |
Precedence of operations
// symbolic operators have equal precedence, applied left-to-right: 4 + 2 * 3; // -> 18 // parentheses have precedence over symbolic operators: 4 + (2 * 3); // -> 10 // methods have precedence over symbolic operators: 4 + 2.pow(3); // -> 12 // parentheses have precedence over methods: (4 + 2).pow(3); // -> 216 // parentheses first, then methods, then binary operators: 1 + (4 + 2).pow(3); // -> 217
Strings
A string is an ordered sequence of characters, delineated by an enclosure of double quotes. A string can contain any text, including letters, numbers, symbols, and even non-printing characters like tabs and new lines.
Examples of strings and usage of the escape character
// a typical string "Click the green button to start."; // using the escape character to include quotation marks "The phrase \"practice makes perfect\" is one I try to remember."; // using the escape character to include new lines "This string\nwill print on\nmultiple lines.";
Common string methods and operations
"Hello" + "there!"; // -> "Hello there!" "Some" ++ "times"; // -> "Sometimes" "I'm a string.".size; // return the number of characters in the string "I'm a string.".reverse; // reverse the order of the characters "I'm a string.".scramble; // randomize the order of the characters "I'm a string.".drop(2); // remove the first two characters "I'm a string.".drop(-2); // remove the last two characters
Symbols
A symbol is like a string, in that it's composed of a sequence of characters and commonly used to name or label things. Where a string is used, a symbol can often be substituted, and vice-versa. A symbol is written in one of two ways: by preceding the sequence of characters with a backslash (e.g., \freq
), or by enclosing it in single quotes (e.g., 'freq'
). These styles are largely interchangeable, but the quote enclosure is the safer option of the two. Symbols that begin with or include certain characters will trigger syntax errors if using the backslash style.
Unlike a string, a symbol is an irreducible unit; it is not possible to access or manipulate the individual characters in a symbol, and all symbols return zero in response to the size method. It is, however, possible to convert back and forth between symbols and strings using the methods asSymbol
and asString
(see below). Symbols, being slightly more optimized than strings, are the preferable choice when used as names or labels for objects.
"hello".asSymbol.class; // -> Symbol \hello.asString.class; // -> String
Booleans
There are exactly two instances of the Boolean class: true
and false
.
Common binary operators that return Boolean values:
x == y x != y x > y x < y x >= y x <= y
nil
Like true and false, nil
is a reserved keyword in the SC language, and is the singular instance of the Nil
class. Most commonly, it represents the value of a variable that hasn't been given a value assignment, or something that doesn't exist. We rarely use nil
explicitly, but it shows up frequently, so it's helpful to be familiar. The isNil
method can be useful for confirming whether a variable has a value (attempting to call methods on an uninitialized variable is a common source of error messages).
( var num; num.isNil.postln; // check the variable — initially, it's nil num = 2; // make an assignment num.isNil.postln; // check again — it's no longer nil )
Arrays
An array is an ordered collection of objects. Syntactically, objects stored in an array are separated by commas and surrounded by an enclosure of square brackets. Arrays are like strings in that both are ordered lists, but while strings can only contain text characters, an array can contain anything. In fact, arrays can (and often do) contain other arrays. Arrays are among the most frequently used objects, because they allow us to express an arbitrarily large collection as a singular unit. Arrays have lots of musical applications; we might use one to contain pitch information for a musical scale, a sequence of rhythmic values, and so on.
We can access an item stored in an array by using the at method and providing the numerical index. Indices begin at zero. As an alternative, we can follow an array with a square bracket enclosure containing the desired index.
x = [4, "freq", \note, 7.5, true]; x.at(3); // -> 7.5 (return the item stored at index 3) x[3]; // alternate syntax
Most unary and binary operators defined for numbers can also be applied to arrays, if they contain numbers. Several examples appear below. If we apply a binary operator to a number and an array, the operation is applied to the number and each item in the array, and the new array is returned. A binary operation between two arrays of the same size returns a new array of the same size in which the binary operation has been applied to each pair of items. If the arrays are different sizes, the operation is applied to corresponding pairs of items, but the smaller array will repeat itself as many times as needed to accommodate the larger array (this behavior is called wrapping
).
[50, 60, 70].squared; // -> [2500, 3600, 4900] 1 + [50, 60, 70]; // -> [51, 61, 71] [1, 2, 3] + [50, 60, 70]; // -> [51, 62, 73] [1, 2] + [50, 60, 70]; // -> [51, 62, 71]
The dup
method, defined for all objects, returns an array of copies of its receiver. An integer, provided as an argument, determines the size of the array. The exclamation mark can also be used as a symbolic shortcut.
7.dup; // -> [7, 7] (default size is 2) 7.dup(4); // -> [7, 7, 7, 7] 7 ! 4; // -> [7, 7, 7, 7] (alternate syntax)
Arrays are a must-learn feature, rich with many convenient methods and uses
Functions
A function is delineated by an enclosure of curly braces. Once a function is defined, we can evaluate it with value
, or by following it with a period and a parenthetical enclosure. When evaluated, a function returns the value of the last expression it contains.
( f = { var num = 4; num = num.squared; num = num.reciprocal; }; ) f.value; // -> 0.0625 f.(); // alternate syntax for evaluating
Defining and evaluating a function with an argument:
( f = { arg input = 4; var num; num = input.squared; num = num.reciprocal; }; ) f.(5); // -> 0.04 (evaluate, passing in a different value as the input) f.(); // -> 0.0625 (evaluate using the default value)
The code example below shows a syntax alternative that replaces the arg
keyword with an enclosure of vertical bar characters (sometimes called pipes
) and declares multiple arguments, converting the code into a function. When executing a function with multiple arguments, the argument values must be separated by commas, and will be interpreted in the same order as they appear in the declaration.
( g = { |thingA = 7, thingB = 5| var result; thingA = thingA.squared; thingB = thingB.reciprocal; result = thingA + thingB; }; ) g.(3, 2); // -> 9.5 (thingA = 3, thingB = 2);
Arguments and Variables
In some respects, arguments and variables are similar: each is a named container that holds a value. Variables are ordinary, named containers that provide the convenience of storing and referencing data. An argument, on the other hand, can only be declared at the very beginning of a function, and serves the specific purpose of allowing some input that can be passed or routed into the function during execution. Variable declarations can only occur at the beginning of a parenthetically enclosed multi-line code block, or at the beginning of a function. If a function declares arguments and variables, the argument declaration must come first. It's not possible to spontaneously declare additional variables or arguments somewhere in the middle of your code.
Getting and Setting Attributes
Objects have attributes. As a simplified real-world example, a car has a color, a number of doors, a transmission that may be manual or automatic, etc. In SC, we interact with an object's attributes by applying methods to that object. Retrieving an attribute is called getting, and changing an attribute is called setting. To get an attribute, we simply call the method that returns the value of that attribute. For setting an attribute, there are two options: we can follow the getter method with an equals symbol to assign a new value to it, or we can follow the getter method with an underscore and the new value enclosed in parentheses.
The following pseudo-code demonstrates essential syntax styles for getting and setting. Note that an advantage of the underscore syntax is that it allows us to chain multiple setter calls into a single expression.
x = Car.new; // make a new car x.color = "red"; // set the color x.numDoors_(4).transmission_("manual"); // set two more attributes x.numDoors; // get the number of doors (returns 4)
Literals
x = House.new(30, 40); // create a house with specific dimensions x.color_("blue"); // set the color x.hasGarage_(true); // set whether it has a garage
We don't have to type Float.new(5.2)
or Symbol.new(\freq)
. Instead, we just type the object as is. Classes like these, which have a direct, syntactical representation through code, are called literals. When we type the number seven, it is literally the number seven. But an object like a house can't be directly represented with code; there is no house
symbol in our standard character set. So, we must use the more abstract approach of typing x = House.new, while its literal representation remains in our imagination.
Integers, floats, strings, symbols, Booleans, and functions are all examples of literals. Arrays, for the record, exist in more of a grey area; they have a direct representation via square brackets, but we may sometimes create one with Array.new
or a related method. There is also a distinction between literal arrays and non-literal arrays, but which is not relevant here. The point is that many of the objects we'll encounter in this pages are not literals and require creation via some method call to their class.
Omitting the new
Method
The new
method is so commonly used for creating new instances of classes that we can usually omit it, and the interpreter will make the right assumption. For example, using our imaginary house
class, we could write x = House(30, 40)
instead of x = House.new(30, 40)
. However, if creating a new instance without providing any arguments, we can omit new
but cannot omit the parentheses, even if they are empty. For example, x = House.new()
and x = House()
are both valid, but x = House
will be problematic. In this third case, the interpreter will store the house class, instead of a new house instance.
Randomness
Conditional Logic
if
One of the most common conditional methods is if, which includes three components: (1) an expression that represents the test condition, which must return a Boolean value, (2) a function to be evaluated if the condition is true, and (3) an optional function to be evaluated if false.
The code example below demonstrates the use of conditional logic to model a coin flip (a value of 1 represents heads
), in three styles that vary in syntax and whitespace. The second style tends to be preferable to the first, because it places the if
at the beginning of the expression, mirroring how the sentence it represents would be spoken in English.
Because the entire expression is somewhat long, the multi-line approach can improve readability. Note that in the first expression, the parentheses around the test condition are required to give precedence to the binary operator == over the if method. Without parentheses, the if method is applied to the number 1 instead of the full Boolean expression, which produces an error
// "receiver-dot-method" syntax: ([0, 1].choose == 1).if({\heads.postln}, {\tails.postln}); // "method(receiver)" syntax: if([0, 1].choose == 1, {\heads.postln}, {\tails.postln}); // structured as a multi-line block: ( if( [0, 1].choose == 1, {\heads.postln}, {\tails.postln} ); )
And/Or
The methods and and or (representable using binary operators && and ||), allow us to check multiple conditions. For example, if ice cream is on sale, and they have chocolate, then I'll buy two. The code example below models a two-coin flip in which both must be heads
for the result to be considered true. Again, parentheses around each conditional test are required to ensure correct order of operations.
( if( ([0, 1].choose == 1) && ([0, 1].choose == 1), {"both heads".postln}, {"at least one tails".postln} ); )
Case and Switch
Say we roll a six-sided die and want to perform one of six unique actions depending on the outcome. A single if
statement is insufficient because it envisions only two outcomes. If we insisted on using if, we'd need to nest
several if's inside of each other. Even with a small handful of possible outcomes, the code quickly spirals into an unreadable mess.
Alternatively, a case
statement (see below) accepts an arbitrary number of function pairs. The first function in each pair must contain a Boolean expression, and the second function contains code to be evaluated if its partner function is true. If a test condition is false, the interpreter moves onto the next pair and tries again. As soon as a test returns true, the interpreter executes the partner function and exits the case block, abandoning any remaining conditional tests. If all tests are false, the interpreter returns nil
.
( var roll = rrand(1, 6); case( {roll == 1}, {\red.postln}, {roll == 2}, {\orange.postln}, {roll == 3}, {\yellow.postln}, {roll == 4}, {\green.postln}, {roll == 5}, {\blue.postln}, {roll == 6}, {\purple.postln} ); )
A switch statement is similar to case, but with a slightly different syntax, shown in Code Example 1.27. We begin with some value—not necessarily a Boolean—and provide an arbitrary number of value-function pairs. The interpreter will check for equality between the starting value and each of the paired values. For the first comparison that returns true, the corresponding function is evaluated.
( var roll = rrand(1, 6); switch( roll, 1, {\red.postln}, 2, {\orange.postln}, 3, {\yellow.postln}, 4, {\green.postln}, 5, {\blue.postln}, 6, {\purple.postln} ); )
Iteration
One of the most attractive aspects of computer programming is its ability to handle repetitive tasks. Iteration refers to techniques that allow a repetitive task to be expressed and executed concisely. Music is full of repetitive structures and benefits greatly from iteration. More generally, if you ever find yourself typing a nearly identical chunk of code many times over, or relying heavily on copy/paste, this could be a sign that you should be using iteration.
Two general-purpose iteration methods, do
and collect
, often make good choices for iterative tasks. Both are applied to some collection—usually an array—and both accept a function as their sole argument. The function is evaluated once for each item in the collection. A primary difference between these two methods is that do
returns its receiver, while collect
returns a modified collection of the same size, populated using values returned by the function. Thus, do
is a good choice when we don't care about the values returned by the function, and instead simply want to do
some action a certain number of times. On the other hand, collect is a good choice when we want to modify or interact with an existing collection and capture the result.
At the beginning of an iteration function, we can optionally declare two arguments, which represent each item in the collection and its index as the function is repeatedly executed. By declaring these arguments, we give ourselves access to the collection items within the function.
In code example (a), we iterate over an array of four items, and for each item, we post a string. In this case, the items in the array are irrelevant; the result will be the same as long as the size of the array is four. Performing an action some number of times is so common, that do
is also defined for integers. When do is applied to some integer n, the receiver will be interpreted as the array [0, 1, … n-1], thus providing a shorter alternative, depicted in code example (b). In code example (c), we declare two arguments and post them, to visualize the values of these arguments.
(a) [30, 40, 50, 60].do({"this is a test".postln}); (b) 4.do({"this is a test".postln}); (c) [30, 40, 50, 60].do({|item, index| [item, index].postln});
A simple usage of collect
is shown right below. We iterate over the array, and for each item, return the item multiplied by its index. collect
returns this new array.
x = [30, 40, 50, 60].collect({|item, index| item * index}); // -> the array [0, 40, 100, 180] is now stored in x
Numerous other iteration methods exist, several of which are depicted next. The isPrime
method is featured here, which returns true if its receiver is a prime number, and otherwise returns false.
x = [101, 102, 103, 104, 105, 106, 107]; // return the subset of the array for which the function returns true: x.select({ |n| n.isPrime }); // -> [101, 103, 107] // return the first item for which the function returns true: x.detect({ |n| n.isPrime }); // -> 101 // return true if the function returns true for at least one item: x.any({ |n| n.isPrime }); // -> true // return true if the function returns true for every item: x.every({ |n| n.isPrime }); // -> false // return the number of items for which the function returns true: x.count({ |n| n.isPrime }); // -> 3
Essentials of Making Sound
Once you're all set, you can launch the server by evaluating:
s.boot;
By default, the keyboard shortcut [cmd]+[b] will also boot the server.
As you run this line, information will appear in the post window. If the boot is successful, the numbers in the server status bar in the bottom-right corner of the IDE will turn green.
If the server numbers don't turn green, the server has not successfully booted. Boot failures are relatively uncommon, but when they do occur, they are rarely cause for alarm and almost always quickly rectifiable. For example, if you're using separate hardware devices for audio input/output, the server will not boot if these devices are running at different sample rates. Alternatively, if a running server is unexpectedly interrupted (e.g., if the USB cable for your audio interface becomes unplugged), an attempt to reboot may produce an error that reads Exception in World_OpenUDP: unable to bind udp socket,
or ERROR: server failed to start.
This message appears because there is likely a hanging instance of the audio server application that must be destroyed, which can be done by evaluating Server.killAll
before rebooting. In rarer cases, a boot failure may be resolved by recompiling the SC class library, quitting and reopening the SC environment, or—as a last resort—restarting your computer.
Assuming you've already booted the server, you can check the sample rate and block size by evaluating the following expressions:
s.sampleRate; s.options.blockSize;
Unit Generators
Unit generators (UGens) are objects that represent digital signal calculations on the audio server. They are the basic building blocks for sound processes, akin to modules on an analog synthesizer. Each UGen performs a specific task, like generating a sawtooth wave, applying a low-pass filter, playing back an audio file, and so on. The following displays a roughly categorized list of some of the simplest and most commonly used UGens. The purposes of some UGens are obvious from their names (WhiteNoise generates white noise), while others, like Dust, are more cryptic. The documentation includes a guide file titled Tour of UGens.
Catagory | Ugens |
Oscillators | SinOsc, Pulse, Saw, Blip, LFPulse, LFSaw, LFTri, VarSaw |
Noise Generators | LFNoise0, LFNoise1, PinkNoise, WhiteNoise |
Envelopes | Line, XLine, EnvGen |
Filters | LPF, HPF, BPF, BRF |
Triggers | Impulse, Dust, Trig |
Sound File Players | PlayBuf, BufRd |
Stereo Panners | Pan2, Balance2 |
Musical fluency doesn't demand intimate familiarity with every UGen. UGens are a means to an end, so you'll only need to get acquainted with those that help achieve your goals.
UGen Rates
A UGen runs at a particular rate, depending on the class method used to create the instance. Instead of using new
to create a UGen instance, we use ar
, kr
, or ir
, which represent audio rate, control rate, and initialization rate.
An audio rate UGen produces output values at the sample rate, which is the highest-resolution signal available to us. If you want to hear a signal through your speakers, it must run at the audio rate. Generally, if you require a high frequency signal, a fast-moving signal, or anything you want to monitor directly, you should use ar
.
Control rate UGens run at the control rate. This rate is equal to sample rate divided by block size. If the sample rate is 48,000 and the block size is 64, then the control rate calculates 48,000 ÷ 64 = 750 samples per second, outputting one at the start of each control cycle. Control rate UGens have a lower resolution but consume less processing power. They are useful when you need a relatively slow-moving signal, like a gradual envelope or a low-frequency oscillator. Because the control rate is a proportionally reduced sample rate, the Nyquist frequency is similarly reduced. In this case, the highest control rate frequency that can be faithfully represented is 750 ÷ 2 = 375 Hz. Therefore, if you require an oscillator with a frequency higher than 375 Hz, kr
is a poor choice. In fact, signal integrity degrades as the frequency of a signal approaches the Nyquist frequency, so ar is a good choice even if a UGen's frequency is slightly below the Nyquist frequency.
Consider a sine oscillator with a frequency of 1 Hz, used to control the cutoff frequency of a filter. It's possible to run this oscillator at the audio rate, but this would provide more resolution than we need. If using the control rate instead, we'd calculate 750 samples per cycle, which is more than enough to visually represent one cycle of a sine wave. In fact, as few as 20 or 30 points would probably be enough to see
a sinusoidal shape. This is an example in which we can take advantage of control rate UGens to reduce the server's processing load, without sacrificing sound quality.
If you're unsure of which rate to use in a particular situation, ar
is usually the safer choice.
Initialization rate UGens are the rarest of the three. In fact, many UGens won't understand the ir
method. A UGen at the initialization rate produces exactly one sample when created and holds that value indefinitely. So, the initialization rate is not really a rate at all; it's simply a means of initializing a UGen so that it behaves like a constant. The CPU savings ir provides are obvious, but what's the point of a signal that's stuck on a specific value? There are a few situations in which this choice makes sense. One example is the SampleRate UGen, a UGen that outputs the sample rate of the server. In some signal algorithms, we need to perform a calculation involving the sample rate, which may vary from one system to another. Generally, the sample rate does not (and should not) change while the server is booted, so it's inefficient and unnecessary to repeatedly calculate it.
UGen Arguments
As already discussed, some methods, like pow
, require arguments to function correctly. UGens are no different. The expression SinOsc.ar()
produces no errors, but questions remain. What is the frequency of this sine oscillator? What is its amplitude? The specific arguments that ar
, kr
, and ir
accept, and their default values, depend on the type of UGen.
In the Class Methods section of the SinOsc help file, we read that this UGen can run at the audio or control rate, and the arguments are the same for both methods. Four input values are expected: freq, phase, mul, and add. freq determines the frequency of the oscillator, measured in Hz. phase controls an offset amount, measured in radians, used to make the oscillator begin at a specific point within its cycle. mul is a value that is multiplied by every sample in the output signal, and add is a value added to every sample in the output signal.
In the case of SinOsc, one cycle is equal to 2π radians. In SC, we use the special keyword pi
to represent π. Expressions such as pi/4
, 3pi/2
, etc., are valid.
The discussion of mul brings up important considerations regarding monitoring level and loudness. When using SC (or any digital audio platform, really), it's smart to calibrate your system volume before you start creating. First, turn your system volume down so it's almost silent, then run the following line of code, which plays a two-channel pink noise signal:
{PinkNoise.ar(mul: 1) ! 2}.play;
As the noise plays, slowly turn up your system volume until the noise sounds strong and healthy. It shouldn't be painful, but it should be unambiguously loud, perhaps even slightly annoying. Once you've set this volume level, consider your system calibrated
and don't modify your hardware levels again. This configuration will encourage you to create signal levels in SC that are comfortable and present a minimal risk of distorting.
By contrast, a bad workflow involves setting your system volume too low, which encourages compensation with higher mul values. This configuration gives the misleading impression that your signal levels are low, when they're actually quite high, with almost no headroom. In this setup, you'll inevitably find yourself in the frustrating situation of having signals that seem too quiet, but with levels that are constantly in the red.
Playing and Stopping Simple Sounds
Function-dot-play refers to a simple code structure that allows us to quickly make sound. This construct involves a function that contains one or more UGens and receives the play method. In the example below we have an audio rate sine UGen with user-specified arguments. If you evaluate this line, you should hear a 300 Hz tone in your left speaker.
{SinOsc.ar(300, 0, 0.1, 0)}.play;
Press [cmd]+[period] to stop the sound. Take a moment to memorize this keyboard shortcut!
In the example, since listening to sound in only one speaker can be uncomfortable, particularly on headphones. SC interprets an array of UGens as a multichannel signal. So, if the function contains an array of two SinOsc UGens, the first signal will be routed to the left speaker, and the second to the right. The duplication shortcut, as shown in the example, is a quick way to create such an array. Multichannel signals will be explored later on.
{SinOsc.ar(300, 0, 0.1, 0) ! 2}.play;
The following code shows an alternative, using a more verbose style:
{SinOsc.ar(freq: 300, phase: 0, mul: 0.1, add: 0) ! 2}.play;
This longer but more descriptive approach of specifying argument values applies to all methods, not just those associated with UGens.
Changing a Sound While Playing
Suppose we want to change the frequency of an oscillator while it's playing. To enable real-time changes to a sound, we need to make a few changes to the examples in the previous section. First, we must declare an argument at the beginning of the UGen function (just as we did with ordinary functions in the previous chapter) and incorporate it into the appropriate UGen. Argument names are flexible, and don't have to match the name of the UGen argument for which they're being used. However, freq
is certainly a good choice, because it's short and meaningful at a glance. It's also wise to provide a default value in the declaration.
When calling play
, we must assign the resulting sound process to a variable, so that we can communicate with it later. While the sound is playing, we can alter it using the set
method and providing the name and value of the parameter we want to change. Next you can see this sequence of actions.
( x = { |freq = 300| SinOsc.ar(freq, mul: 0.1) ! 2; }.play; ) x.set(\freq, 400); // change the frequency
We can declare as many arguments as we need. Now we shall add a second argument that controls signal amplitude and demonstrate a variety of set
messages.
( x = { |freq = 300, amp = 0.1| SinOsc.ar(freq, mul: amp) ! 2; }.play; ) x.set(\freq, 400, \amp, 0.4); // modify both arguments x.set(\amp, 0.05, \freq, 500); // order of name/value pairs doesn't matter x.set(\freq, 600); // modify only one argument
It's often desirable to separate creating and playing a UGen function into two discrete actions. Next we shall define a UGen function and store it in the interpreter variable f. Then, we play it, storing the resulting sound process in the interpreter variable x. The former is simply the object that defines the sound, while the latter is the active sound process that understands set messages. It's important not to confuse the two.
( // define the sound f = { |freq = 300, amp = 0.1| SinOsc.ar(freq, mul: amp) ! 2; }; ) x = f.play; // play the sound x.set(\freq, 400, \amp, 0.3); // change the sound f.set(\freq, 500, \amp, 0.05); // no effect if applied to the function
Even if arguments in a UGen function have default values, we can override them when playing the function. The play method has an args argument, which accepts an array of name-value pairs, shown below:
( f = { |freq = 300, amp = 0.1| SinOsc.ar(freq, mul: amp) ! 2; }; ) x = f.play(args: [freq: 800, amp: 0.2]); // override default arguments x.set(\freq, 600, \amp, 0.05); // set messages work normally
Other Ways to Stop a Sound
The [cmd]+[period] shortcut is useful for stopping sound, but it's indiscriminate. As an alternative, we can free
a sound process. With this method, we can have multiple sound processes playing simultaneously, and remove them one-by-one (see below). This example also highlights the value of defining a function and playing it separately; specifically, we can spawn multiple sound processes from one function.
( f = { |freq = 300, amp = 0.1| SinOsc.ar(freq, mul: amp) ! 2; }; ) x = f.play(args: [freq: 350]); y = f.play(args: [freq: 450]); y.free; x.free;
Like [cmd]+[period], freeing a sound process also results in a hard stop, which may not be what we want. When using function-dot-play, we can also use release
to create a gradual fade, optionally providing a fade duration measured in seconds
( f = { |freq = 300, amp = 0.1| SinOsc.ar(freq, mul: amp) ! 2; }; ) x = f.play; x.release(2);
Math Operations with UGens
We've already established that a UGen is essentially a sequence of numbers, therefore most math operations defined for floats and integers can also be applied to UGens. Signal summation, for example, is a fundamental technique that forms the basis of audio mixing and additive synthesis. When two signals are summed, their corresponding samples are summed, and the result is a new waveform in which both signals can usually be perceived. Let's add a sine wave and pink noise together.
( x = { var sig; sig = SinOsc.ar(300, mul: 0.15); sig = sig + PinkNoise.ar(mul: 0.1); sig = sig ! 2; }.play; ) x.release(2);
When a binary operator is used between a number and a UGen, the operation is applied to the number and every sample value produced by the UGen. This being the case, multiplication and addition can be used instead of providing argument values for mul
and add
.
( x = { var sig; sig = SinOsc.ar(300) * 0.15; sig = sig + (PinkNoise.ar * 0.1); sig = sig ! 2; }.play; ) x.release(2);
A UGen Function Plays the Last Expression
Just as ordinary functions return the value of their last expression when evaluated, the output signal from a UGen function is also determined by its last expression. In the first of the following two functions, we'll only hear pink noise, despite creating a sine wave. In the second function, the last expression is the sum of both signals, which is what we hear.
( { var sig0, sig1; sig0 = SinOsc.ar(300, mul: 0.15) ! 2; sig1 = PinkNoise.ar(mul: 0.1) ! 2; }.play; ) ( { var sig0, sig1; sig0 = SinOsc.ar(300, mul: 0.15) ! 2; sig1 = PinkNoise.ar(mul: 0.1) ! 2; sig0 + sig1; }.play; )
Multiplying one signal by another is also common. We shall be multiplying pink noise by a low-frequency sine wave, producing a sound like ocean waves. This is a simple example of signal modulation, which involves the use of one signal to influence some aspect of another. A phase value of 3π/2 causes the sine oscillator to begin at the lowest point in its cycle, and the multiplication/addition values scale and shift the output values to a new range between 0 and 0.2.
( x = { var sig, lfo; lfo = SinOsc.kr(freq: 1/5, phase: 3pi/2, mul: 0.1, add: 0.1); sig = PinkNoise.ar * lfo; sig = sig ! 2; }.play; ) x.release(2);
When we want a UGen's output to range between some arbitrary minimum and maximum, using mul/add sometimes involves cumbersome mental math. Even worse, the actual range of the UGen isn't immediately clear from looking at the code. A better approach involves using one of several range-mapping methods, such as range
. This method lets us explicitly provide a minimum and maximum, avoiding the need to deal with mul/add. The table below lists some common range-mapping methods.
( x = { var sig, lfo; lfo = SinOsc.kr(freq: 0.2, phase: 3pi/2).range(0, 0.2); sig = PinkNoise.ar * lfo; sig = sig ! 2; }.play; ) x.release(2);
Method | Description |
.range(x, y) | Linearly map the output range between x and y |
.exprange(x, y) | Exponentially map the output range between x and y. Arguments must be either both positive or both negative, and neither can be 0. |
.curverange(x, y, n) | Map the output range between x and y using a custom warp value n. Positive values create exponential-like behavior, negative values create logarithmic-like behavior. |
.unipolar(x) | Map the output range between 0 and x. |
.bipolar(x) | Map the output range between ±x. |
Range-mapping vs. mul/add
Range-mapping methods are designed as alternatives to mul/add arguments, and they assume the range of the UGen to which they apply has not been previously altered. You can specify a UGen's range using one approach or the other, but you should never apply both approaches at the same time. If you do, a range-mapping operation will be applied twice in a row, producing erroneous numbers and possibly startling sound!
As you start exploring UGen functions of your own, remember that just because a math operation can be used doesn't necessarily mean it should. Dividing one signal by another, for example, is dangerous! This calculation may involve division by some extremely small value (or even zero), which is likely to generate a dramatic amplitude spike, or something similarly unpleasant. Experimentation is encouraged, but you should proceed with purpose and mindfulness. Mute or turn your system volume down first before you try something unpredictable.
Envelopes
If we play a function containing some oscillator or noise generator, and then step away for a coffee, we'd return sometime later to find that sound still going. In music, an infinite-length sound isn't particularly useful. Instead, we usually like sounds to have definitive beginnings and ends, so that we can structure them in time.
An envelope is a signal with a customizable shape and duration, typically constructed from individual line segments joined end-to-end. Envelopes are often used to control the amplitude of another signal, enabling fades instead of abrupt starts and stops. By using release, we've already been relying on a built-in envelope that accompanies the function-dot-play construct. When controlling signal amplitude, an envelope typically starts at zero, ramps up to some positive value, possibly stays there for a while, and eventually comes back down to zero. The first segment is called the attack, the stable portion in the middle is the sustain, and the final descent is the release. Many variations exist; an ADSR
envelope, for example, has a decay segment between the attack and sustain.
It's important to recognize that the ADSR envelope is just one specific example that happens to be useful for modeling envelope characteristics of many real-world sounds. Ultimately, an envelope is just a signal with a customizable shape, which can be used to control any aspect of a signal algorithm, not just amplitude.
Line
and XLine
The UGens Line
and XLine
provide simple envelope shapes. Line
generates a signal that travels linearly from one value to another over a duration in seconds. XLine
is similar but features an exponentially curved trajectory. Like the exprand
and exprange
methods, the start and end values for XLine
must have the same sign and neither can be zero. Note that XLine
cannot end at zero, but it can get close enough that the difference is unnoticeable.
( { var sig, env; env = Line.kr(start: 0.3, end: 0, dur: 0.5); sig = SinOsc.ar(350) * env; sig = sig ! 2; }.play; ) ( { var sig, env; env = XLine.kr(start: 0.3, end: 0.0001, dur: 0.5); sig = SinOsc.ar(350) * env; sig = sig ! 2; }.play; )
DoneAction
Line and XLine include an argument, named doneAction
, which appears in UGens that have an inherently finite duration. In SC, a doneAction represents an action that the audio server takes when the UGen that contains the doneAction has finished. These actions can be specified by integer, and a complete list of available actions and their meanings appears in the help file for the Done UGen. Most of the descriptions in this table may look completely meaningless to you, but if so, don't worry. In practice, we rarely employ a doneAction other than zero (do nothing) or two (free the enclosing synth). The default doneAction is zero, and while taking no action sounds harmless, it carries consequences. To demonstrate, evaluate either of the two code examples below many times in a row. As you do, you'll notice your CPU usage will gradually creep upwards.
Why does this happen? Because a doneAction of zero tells the server to do nothing when the envelope is complete, the envelope remains active on the server and continues to output its final value indefinitely. These zero or near-zero values are multiplied by the sine oscillator, which results in a silent or near-silent signal. The server is indifferent to whether a sound process is silent; it only knows that it was instructed to do nothing when the envelope finished. If you evaluate this code over and over, you'll create more and more non-terminating sound processes. Eventually, the server will become overwhelmed, and additional sounds will start glitching (if you've followed these instructions and ramped up your CPU numbers, now is a good time to press [cmd]+[period] to remove these ghost
sounds).
From a practical perspective, when our envelope reaches its end, we consider the sound to be totally finished. So, it makes sense to specify 2 for the doneAction. When running the code in Code Example 2.15, the server automatically frees the sound when the envelope is done. Evaluate this code as many times as you like, and although it won't sound any different, you'll notice that CPU usage will not creep upwards as it did before.
( { var sig, env; env = XLine.kr(start: 0.3, end: 0.0001, dur: 0.5, doneAction: 2); sig = SinOsc.ar(350) * env; sig = sig ! 2; }.play; )
Knowing which doneAction to specify is an important skill, essential for automating the cleanup of stale sounds and optimizing usage of the audio server's resources.
Env
and EnvGen
Lines are useful for simple envelopes, but don't provide much flexibility. Once a Line or XLine starts, it cannot be restarted, modified, or looped; it merely travels from start to end, and triggers a doneAction
when finished. In most cases, it's preferable to use the more flexible EnvGen
. The shape of an EnvGen is determined by an instance of a language-side class called Env
, provided as the envelope signal's first argument. An instance of Env created with new
expects three arguments: an array of level values, an array of segment durations, and an array of curve specifications. We can also plot an Env to visualize its shape.
( e = Env.new( levels: [0, 1, 0], times: [1, 3], curve: [0, 0] ); e.plot; )
Let's unpack the meaning of the numbers in the previous code. The first array contains envelope levels, which are values that the envelope signal will visit as time progresses: the envelope starts at 0, travels to 1, and returns to 0. The second array specifies durations of the segments between these levels: the attack is 1 second long, and the release is 3 seconds. The final array determines segment curvatures. Zero represents linearity, while positive/negative values will bend
the segments. Note that when an Env is created this way, the size of the first array is always one greater than either of the other two arrays. Take a moment to modify the code from the example, to better understand how the numbers influence the envelope's shape. This example illustrates how EnvGen
and Env
work together to create an envelope signal in a UGen function. Keywords are used for clarity. Because it is inherently finite, EnvGen
accepts a doneAction
. As before, it makes sense to specify a doneAction of 2 to automate the cleanup process.
When using numbers to specify segment curves, it can be hard to remember how a segment will bend depending on the sign of the number. The rule is: positive values cause a segment to be more horizontal at first, and more vertical toward the end. Negative values cause the segment to be more vertical at first, becoming more horizontal toward the end.
Certain symbols can also be used to specify a segment curve, such as \lin, \exp, \sin, and others. A table of valid options appears in the Env help file, under the section that explains the class method new.
( { var sig, env; env = EnvGen.kr( envelope: Env.new( levels: [0, 1, 0], times: [1, 3], curve: [0, 0] ), doneAction: 2 ); sig = SinOsc.ar(350) * 0.3; sig = sig * env; sig = sig ! 2; }.play; )
Envelopes can be divided into two categories: those with fixed durations, and those that can be sustained indefinitely. The envelopes we've seen so far belong to the first category, but in the real world, many musical sounds have amplitude envelopes with indefinite durations. When a violinist bows a string, we won't know when the sound will stop until the bow is lifted. An envelope that models this behavior is called a gated envelope. It has a parameter, called a gate
, which determines how and when the envelope signal progresses along its trajectory. When the gate value transitions from zero to positive, the envelope begins and sustains at a point along the way. When the gate becomes zero again, the envelope continues from its sustain point and finishes the rest of its journey. Like a real-world gate, we describe this parameter as being open (positive) or closed (zero).
To create a sustaining envelope, we can add a fourth argument to Env.new()
: an integer representing an index into the levels array, indicating the value at which the envelope will sustain. In SC terminology, this level is called the release node. In the example, the release node is 2, which means the envelope signal will sustain at a level of 0.2 while the gate remains open. Because gate is a parameter we'd like to change while the sound is playing, it must be declared as an argument, and supplied to the EnvGen. In effect, this example creates an ADSR envelope: the attack travels from 0 to 1 over 0.02 seconds, the decay drops to a level of 0.2 over the next 0.3 seconds, and the signal remains at 0.2 until the gate closes, which triggers a one-second release.
( f = { |gate = 1| var sig, env; env = EnvGen.kr( envelope: Env.new( [0, 1, 0.2, 0], [0.02, 0.3, 1], [0, -1, -4], 2 ), gate: gate, doneAction: 2 ); sig = SinOsc.ar(350) * 0.3; sig = sig * env; sig = sig ! 2; }; ) x = f.play; x.set(\gate, 0);
In some cases, we may want to retrigger an envelope, opening and closing its gate at will, to selectively allow sound to pass through. If so, a doneAction of 2 is a poor choice, because we don't necessarily want the sound process to be destroyed if the envelope reaches its end. Instead, a 0 doneAction (the default) is the correct choice, as demonstrated next, which causes the envelope to idle
at its end point until it is retriggered.
It's worth being extra clear about the specific behavior of an envelope in response to gate changes when a release node has been specified:
- A zero-to-positive gate transition causes the envelope to move from its current level to the second level in the levels array, using its first duration and first curve value. Note that the envelope never revisits its first level, which is only used for initialization.
- A positive-to-zero gate transition causes the envelope to move from its current value to the value immediately after the release node, using the duration and curve values at the same index as the release node.
( f = { |gate = 1| var sig, env; env = EnvGen.kr( Env.new( [0, 1, 0.2, 0], [0.02, 0.3, 1], [0, -1, -4], 2 ), gate ); sig = SinOsc.ar(350) * 0.3; sig = sig * env; sig = sig ! 2; }; ) x = f.play; x.set(\gate, 0); // fade to silence but do not free x.set(\gate, 1); // reopen the gate to restart the envelope x.set(\gate, 0); // fade to silence again x.free; // free when finished
This retriggering ability may also be useful for fixed-duration envelopes. It's possible but clumsy to retrigger a fixed-duration envelope with a standard gate argument, because it requires manually closing the gate before reopening. As a solution, we can precede the gate argument name with t_
, which transforms it into a trigger-type
argument, which responds differently to set messages. When a trigger-type argument is set to a non-zero value, it holds that value for a single control cycle, and then almost immediately snaps
back to zero. It's like a real-world gate that's been augmented with a powerful spring, slamming shut immediately after being opened. Below we demonstrate the use of trigger-type arguments. Note that the default gate value is zero, which means the envelope will idle at its starting level (zero) until the gate is opened.
( x = { |t_gate = 0| var sig, env; env = EnvGen.kr( Env.new( [0, 1, 0], [0.02, 0.3], [0, -4], ), t_gate, ); sig = SinOsc.ar(350) * 0.3; sig = sig * env; sig = sig ! 2; }.play; ) x.set(\t_gate, 1); // evaluate repeatedly x.free; // free when finished
EnvGen, in partnership with Env, is one of the more complex UGens in the class library, with many variations and subtleties. Both help files contain additional information.
Multichannel Signals*
SynthDef
and Synth
Alternate Expression of Frequency and Amplitude
Oscillators with a frequency parameter expect a value in Hertz. When thinking about musical pitch, Hertz isn't always the preferred unit of measurement. Using MIDI note numbers can be a convenient alternative. In this system, 60 corresponds to middle C (roughly 261.6 Hz), and an increment/decrement of one corresponds to a semitone shift in 12-tone equal temperament. We can convert from MIDI to Hertz using midicps
and convert in the other direction with cpsmidi
. Non-integer MIDI note numbers are valid and represent a pitch proportionally between two equal-tempered semitones.
60.midicps; // -> 261.6255653006 500.cpsmidi; // -> 71.213094853649
midiratio
and ratiomidi
are similarly useful methods that convert back and forth between an interval, measured in semitones, and the frequency ratio that interval represents. For example, 3.midiratio represents the ratio between an F and the D immediately below it, because these two pitches are three semitones apart. Negative semitone values represent pitch movement in the opposite direction.
// the ratio that raises a frequency by one semitone 1.midiratio; // -> 1.0594630943591 // the ratio 8/5 is slightly more than 8 equal-tempered semitones (8/5).ratiomidi // -> 8.1368628613517
Similarly, when expressing signal amplitude, a normalized range between zero and one isn't always the most intuitive choice. The ampdb
method converts a normalized amplitude to a decibel value and dbamp
does the opposite. A value of zero dB corresponds to a nominal amplitude value of one. If your audio system is properly calibrated, a decibel value around -20 dB should produce a comfortable monitoring level, and a typical signal will become inaudible around -80 dB.
-15.dbamp; // -> 0.17782794100389 0.3.ampdb; // -> -10.457574905607
For efficiency reasons, it is preferable not to build these methods into a SynthDef
, and instead call them when creating or modifying a Synth
, so that the server does not have to repeatedly perform these calculations.
( SynthDef.new(\test, { arg freq = 350, amp = 0.2, atk = 0.01, dec = 0.3, slev = 0.4, rel = 1, gate = 1, out = 0; var sig, env; env = EnvGen.kr( Env.adsr(atk, dec, slev, rel), gate, doneAction: 2 ); sig = SinOsc.ar(freq + [0, 1]); sig = sig * env; sig = sig * amp; Out.ar(out, sig); }).add; ) x = Synth(\test, [freq: 60.midicps, amp: -20.dbamp]); x.set(\freq, 62.midicps); // increase pitch by 2 semitones x.set(\amp, -12.dbamp); // increase level by 8 dB x.set(\gate, 0);
Synthesis*
Synthesis refers to creative applications of combining and interconnecting signal generators, typically relying on oscillators and noise, with a goal of building unique timbres and textures. This section explores synthesis categories.
Additive Synthesis*
Modulation Synthesis*
Wavetable Synthesis*
Filters and Subtractive Synthesis*
Modal Synthesis*
Physical objects vibrate when disturbed. Common musical examples include plucking a string or striking something with a mallet. The vibrational patterns of some objects, like bells and chimes, are composed of a complex sum of sinusoidal vibrations that decay over a relatively long duration. Other objects, like blocks of wood, exhibit periodic vibrations that decay almost instantly. Modal synthesis refers to the practice of creating (or recreating) the sound of a physical object by simulating its natural modes of vibration. Superficially, this technique is like additive synthesis, but involves injecting excitation signals into resonant filters, rather than summing sine generators.
Resonz
and Ringz
are resonant filters that provide an entryway into modal synthesis. Resonz
is a band-pass filter with a constant gain at zero decibels. This means that as the bandwidth decreases, the sense of resonance increases, but spectral content at the center frequency will remain at its input level, while surrounding content is attenuated. It virtually indistinguishable from BPF in terms of usage and sound. Ringz
, on the other hand, has a variable gain that depends on the bandwidth, specified indirectly as a 60 dB decay time. As this decay time increases, bandwidth narrows, a sense of resonance increases, and spectral content at the center frequency undergoes a potentially dramatic increase in amplitude. The difference between Resonz and Ringz is subtle but has significant consequences.
In terms of practical usage, because of its variable-gain design, Ringz
is intended to be driven by single-sample impulses. Even an excitation signal a few samples long has the potential to overload Ringz
and produce a distorted output signal. Longer signals, such as sustained noise, can technically be fed to an instance of Ringz
, but the amplitude of the excitation signal and/or the output signal must be drastically reduced in order to compensate for the increase in level, particularly if the decay time is long. Resonz
, by contrast, is designed to accept sustained excitation signals and is more likely to need an amplitude boost to compensate for low levels, particularly in narrow bandwidth situations. Feeding single-sample impulses into Resonz
is fine, but the level of the output signal will likely be quite low.
( { var sig, exc; exc = Impulse.ar(1); sig = Ringz.ar( in: exc, freq: 800, decaytime: 1/3 ); sig = sig * 0.2 ! 2; }.play; ) ( { var sig, exc; exc = PinkNoise.ar(1); sig = Resonz.ar( in: exc, freq: 800, bwr: 0.001, mul: 1 / 0.001.sqrt ); sig = sig * 0.5 ! 2; }.play; )
Klank
and DynKlank
encapsulate fixed and dynamic banks of Ringz
resonators, offering a slightly more convenient and efficient option than applying multichannel expansion to an instance of Ringz (see below). These UGens require a Ref
array containing internal arrays of frequencies, amplitudes, and decay times of simulated resonances. The frequencies can be scaled and shifted, and the decay times can also be scaled.
( { var sig, exc, freqs, amps, decays; freqs = [211, 489, 849, 857, 3139, 4189, 10604, 15767]; amps = [0.75, 0.46, 0.24, 0.17, 0.03, 0.019, 0.002, 0.001]; decays = [3.9, 3.4, 3.3, 2.5, 2.2, 1.5, 1.3, 1.0]; exc = Impulse.ar(0.5); sig = Klank.ar( `[freqs, amps, decays], // <- note the backtick character exc, ); sig = sig * 0.25 ! 2; }.play; )
Waveform Distortion*
Sampling*
Sampling refers to creative practices that rely on recorded sound, typically involving modified playback of audio files stored in blocks of memory on the audio server. In a sense, sampling and synthesis are two sides of the same signal-generating coin. Synthesis relies on mathematical algorithms, while sampling is based on the use of content that has already been produced and captured. Most sound sources found throughout creative audio practices are rooted in one of these two categories.
Sampling opens a door to a world of sound that is difficult or impossible to create using synthesis techniques alone. Anything captured with a microphone and rendered to a file instantly becomes a wellspring of creative potential: a recording of wildlife can become a surreal ambient backdrop, or a recording of a broken elevator can be chopped into weird percussion samples. Instead of using dozens of sine waves or filters to simulate a gong, why not use the real thing?
Before loading sampled audio files into software, it's wise to practice good sample hygiene. Unnecessary silence should be trimmed from the beginnings and ends of the files, samples should be as free as possible from background noise and other unwanted sounds, and the peak amplitudes of similar samples should be normalized to a consistent level. Normalization level is partly a matter of personal preference, but –12 dBFS is usually a reasonable target, which makes good use of available bit depth while reserving ample headroom for mixing with other sounds.
Overview*
*
*
*
*
Sequencing
Overview
A musical composition can be viewed as a sequence of sections, a section as a sequence of phrases, and a phrase as a sequence of notes. Thinking in this modular way, that is, conceptualizing a project as smaller sequential units that can be freely combined, is an excellent way to approach large-scale projects in SC, and in programming languages more generally.
SC provides a wealth of sequencing options. The Pattern library, for example, is home to hundreds of classes that define many types of sequences, which can be nested and combined to form complex, composite structures. The Stream
class is also a focal point, which provides sequencing infrastructure through its subclasses, notably Routine
and EventStreamPlayer
. Clock
classes provide an implicit musical grid on which events can be scheduled, and play a central role in sequencing as well.
It's important to make a distinction between processes that define sequences, and processes that perform sequences. As an analogy, consider the difference between a notated musical score and a live musical performance of that score. The score provides detailed performance instructions, and the sound of the music can even be imagined by studying it. However, the score is not the same thing as a performance. One score can spawn an infinite number of performances, which may be slightly or significantly different from each other. In SC, a pattern or function can be used to define a sequence, while some type of stream is used to perform it.
Routines and Clocks
When we evaluate a function, the encapsulated code statements are executed in order, but these executions occur so quickly that they seem to happen all at once. When the function in the listing below is evaluated, three tones are produced, and we hear a chord.
s.boot; ( ~eventA = {SinOsc.ar(60.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; ~eventB = {SinOsc.ar(70.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; ~eventC = {SinOsc.ar(75.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; f = { ~eventA.play; ~eventB.play; ~eventC.play; }; ) f.();
How would we play these tones one-by-one, to create a melody? The Routine
class, introduced next, provides one option for timed sequences. A routine is a special type of state-aware function, capable of pausing and resuming mid-execution. A routine encapsulates a function, and within this function, either the yield
or wait
method designates a pause (yield
is used throughout this section, but these methods are synonymous when applied to a number). Once a routine is created, we can manually step through it by calling next on the routine. On each next, the routine begins evaluation, suspends when it encounters a pause, and continues from that point when another next is received. If a routine has reached its end, next returns nil
, but a routine can be reset at any time, which effectively rewinds
it to the beginning.
( ~eventA = {SinOsc.ar(60.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; ~eventB = {SinOsc.ar(70.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; ~eventC = {SinOsc.ar(75.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; f = { ~eventA.play; 1.yield; ~eventB.play; 1.yield; ~eventC.play; 1.yield; }; r = Routine(f); ) r.next; // evaluate repeatedly r.reset; // return to the beginning at any time
Note that yield must be called from within a routine, so the function f
in the code above cannot be evaluated by itself:
f.(); // -> ERROR: yield was called outside of a routine.
The convenience method r
can be applied to a function and will return a routine that contains that function. Thus, a routine can be written in any of the following ways:
Routine({"hello".postln; 1.yield;}); {"hello".postln; 1.yield;}.r; r({"hello".postln; 1.yield;});
When stepping through a routine, each yield
returns its receiver, in the same way that calling value
on a function returns its last expression. Thus, the returned value from each next call is equal to each yielded item (this is also why each next causes the number one to appear in the post window). In the foregoing code, the thing we yield is irrelevant, and we have no interest in it. It could be any object; the number one is chosen arbitrarily. In this example, we are more interested in the actions that occur between yields, specifically, the production of sound.
However, the ability of a routine to return values illustrates another usage. Suppose we want a process that generates MIDI note numbers that start at 48 and increment by a random number of semitones between one and four, until the total range exceeds three octaves. We can do so by defining an appropriate function and yielding values of interest. A while
loop is useful, as it allows us to repeatedly apply an increment until our end condition is met (see below).
Using a routine and while loop to generate randomly incremented MIDI note numbers
( ~noteFunc = { var num = 48; while({num < 84}, { num.yield; num = num + rrand(1, 4); }); }; ~noteGen = Routine(~noteFunc); ) ~noteGen.next; // evaluate repeatedly
Note that this sequence is not predetermined, because each next
performs a new application of the rrand
function. Thus, if ~noteGen
is reset, it will generate a new random sequence, probably different than the previous.
We often want a routine to advance on its own. A routine will execute automatically in response to play
, as shown next. In this case, yield
values are treated as pause durations (measured in seconds, under default conditions).
Iteration, demonstrated afterwards, is a useful tool for creating timed repetitions.
( ~eventA = {SinOsc.ar(60.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; ~eventB = {SinOsc.ar(70.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; ~eventC = {SinOsc.ar(75.midicps ! 2) * Line.kr(0.1, 0, 1, 2)}; f = { ~eventA.play; 1.yield; ~eventB.play; 1.yield; ~eventC.play; 1.yield; }; r = Routine(f); ) r.play;
Using iteration inside a routine to create repetitions
( ~playTone = { |freq| {SinOsc.ar(freq ! 2) * Line.kr(0.1, 0, 1, 2)}.play; }; f = { 3.do({ ~playTone.(72.midicps); 0.2.yield; ~playTone.(62.midicps); 0.4.yield; }); }; r = Routine(f); ) r.play;
When using iteration in a routine, number.do
can be replaced with loop
or inf.do
(a special keyword that represents infinity) to repeat a block of code indefinitely, as illustrated. A playing routine can be stopped at any time using stop—particularly important to keep in mind if a routine has no end! Stopping all routines is also one of the side-effects of pressing [cmd]+[period]. Once a routine is stopped, it cannot be resumed with play
or next
unless it is first reset.
An infinite-length routine.
( ~playTone = { |freq| {SinOsc.ar(freq ! 2) * Line.kr(0.1, 0, 0.2, 2)}.play; }; r = Routine({ loop({ ~playTone.(72.midicps); 0.4.yield; [62, 63, 64].do({ |n| ~playTone.(n.midicps); (0.4 / 3).yield; }); }); }); ) r.play; r.stop;
The Danger of Infinite Loops
When using play
or next
on an infinite-length routine, make sure there's at least one yield
/wait
! Executing an infinite routine with no built-in pauses will immediately overwhelm your CPU and may even crash the language. While stuck in a loop, SC will struggle to receive even the most basic types of input, like pressing [cmd]+[period] or clicking drop-down menus, and a forced quit may be necessary. Some versions of SC may attempt to auto-recover unsaved code in the event of a crash, but it's not wise to rely too heavily on this feature.
When a routine is played, the resulting process exists in its own temporal space, called a thread, which is independent from the parent thread in which it was created. Thus, when multiple routines are played using back-to-back code statements (as they are in ~r_parallel
), each exists independently, unaware of the others' existences. However, when serial behavior is desired, embedInStream
can be used instead of play
, which situates the subroutine in the parent thread. In this case, the parent routine and the two subroutines are all part of the same thread and exist along one temporal continuum. Thus, each subroutine begins only when the previous subroutine has finished.
Nesting routines inside of other routines so that they play in parallel or in series
( ~playTone = { |freq| {SinOsc.ar(freq ! 2) * Line.kr(0.1, 0, 0.2, 2)}.play; }; ~sub0 = { 2.do({ ~playTone.(67.midicps); 0.15.yield; ~playTone.(69.midicps); 0.15.yield; }); 0.5.yield; }; ~sub1 = { 3.do({ ~playTone.(75.midicps); 0.5.yield; }); 1.yield; }; ~r_parallel = Routine({ Routine(~sub0).play; Routine(~sub1).play; }); ~r_series = Routine({ Routine(~sub0).embedInStream; Routine(~sub1).embedInStream; }); ) ~r_parallel.play; // subroutines execute simultaneously ~r_series.play; // subroutines play one after the other
Modular thinking is important and valuable. Before setting out to build some glorious routines, take a moment to conceptualize the musical structure they'll represent. Break down your structures into the simpler units, build subroutines (perhaps sub-subroutines), and combine them appropriately. A large, unwieldy, irreducible routine is usually harder to debug than one built from modular parts.
TempoClock
Previously, yield values were interpreted as durations, measured in seconds. Unless you're working at a tempo of 60 (or perhaps 120) beats per minute, specifying durations in seconds is inconvenient, and requires extra math. To determine the duration of an eighth note at 132 bpm, for example, we first divide 60 by the tempo to produce a value that represents seconds per beat. If a quarter note is considered one beat, we then divide by two to get the duration of an eighth note:
(60 / 132 / 2).yield; // an eighth rest at 132 bpm
Manual calculation of durations based on tempo is cluttered and does not do a good job of visually conveying metric structure. TempoClock
, one of three clock objects (its siblings are SystemClock
and AppClock
), provides an elegant solution. These three clocks handle the general task of scheduling things at specific times. AppClock
is the least accurate of the three, but it can schedule certain types of actions that its siblings cannot, notably, interacting with graphical user interfaces. SystemClock
and TempoClock
are more accurate, but TempoClock
has the additional benefit of being able to express time in terms of beats at a particular tempo. When the interpreter is launched or rebooted, a default instance of TempoClock
is automatically created:
TempoClock.default;
The default TempoClock
runs at 60 bpm, and the current beat can be retrieved via the beats
method:
TempoClock.default.beats; // evaluate repeatedly
When a routine is played, it is scheduled on a clock. If no clock is specified, it plays on the default TempoClock
. This is why, throughout the previous section, yield
values can be treated as durations, even though they technically specify a number of beats.
When creating your own instance of TempoClock
, the first argument is interpreted as a value in beats per second. If you want to specify tempo in beats per minute, divide that value by 60:
t = TempoClock(132/60); // a TempoClock running at 132 bpm t.beats; // evaluate repeatedly to get the current beat
A routine can be scheduled on a specific clock by providing that clock as the first argument for play
. In the code below, yield
times represent beat values, relative to the clock on which the routine plays. The tempo of a TempoClock
can be changed at any time using the tempo
method, also depicted in this same example. Note that tempo
only affects durations between onsets and has no effect on the durations of the notes themselves (which in this case are determined by Line
).
A routine that plays three notes in sequence, with timings based on beat durations at a specific tempo
( t = TempoClock(132/60); ~playTone = { |freq| {SinOsc.ar(freq ! 2) * Line.kr(0.1, 0, 1, 2)}.play; }; r = Routine({ [60, 70, 75].do({ |n| ~playTone.(n.midicps); (1/2).yield; }); }); ) r.play(t); t.tempo = 112/60; r.reset.play(t); // now eighth notes at 112 bpm
We often want to synchronize several timed processes, so that they begin together and/or exist in rhythmic alignment. Manual attempts to synchronize a routine with one that's already playing will rarely succeed. Even if both routines are played on the same clock, their performances are not guaranteed to align with each other. By default, when a routine is played on a clock, it begins immediately. To schedule a routine to begin on a certain beat, we can specify quant
information along with the clock, as shown below.
( t = TempoClock(132/60); ~playTone = { |freq| {SinOsc.ar(freq ! 2) * Line.kr(0.1, 0, 0.2, 2)}.play; }; ~r0 = Routine({ loop({ [60, 63, 65, 67].do({ |n| ~playTone.(n.midicps); (1/2).yield; }); }); }); ~r1 = Routine({ loop({ [70, 72, 75, 77].do({ |n| ~playTone.(n.midicps); (1/2).yield; }); }); }); ) ~r0.play(t, quant: 4); // begin playing on next beat multiple of four ~r1.play(t, quant: 4); // will be beat-aligned with ~r0
In some cases, you may want a rhythmic process to be quantized to a particular beat but shifted in time to begin some number of beats after or before that beat. In this case, an array of two values can be provided for quant
, as demostrated below. The first value represents a beat multiple, and the second value represents a number of beats to shift along the timeline. Negative values result in earlier scheduling.
~r0.reset.play(t, quant: [4, 0]); // plays on the next beat multiple of four ~r1.reset.play(t, quant: [4, 1]); // plays one beat after the next beat multiple of four
As an alternative to stopping all routines individually, all actions scheduled on a TempoClock can be removed by calling clear on the clock:
t.clear;
When a large-scale routine is constructed as a singular entity composed of subroutines, there is less of a need to control quantization information, because the timing aspects can be configured in advance. However, quantization is essential for real-time applications, such as live coding, in which new components may be added spontaneously.
There is generally no harm in allowing a TempoClock
to continue existing in the background (any outstanding clocks are destroyed when quitting SC), but a TempoClock
can be stopped at any time with stop
. This is a hard stop that destroys the clock, after which resuming or querying the current beat is no longer possible. To resume, a new clock must be created. By default, [cmd]+[period] will destroy all user-created clocks, but a clock can be configured to survive [cmd]+[period]
by setting its permanent
attribute to true
:
t = TempoClock(132/60).permanent_(true); // press [cmd]+[period]… t.beats; // the clock remains t.permanent = false; // press [cmd]+[period]… t.beats; // the clock is destroyed
Patterns
Patterns, which exist as a family of classes that all begin with capital P, provide flexible, concise tools for expressing musical sequences. A pattern defines a sequence but does not perform that sequence. To retrieve a pattern's output, we can use asStream
to return a routine, which can then be evaluated with next
. In contrast to creating such routines ourselves, patterns are simpler, pre-packaged units with known behaviors, and tend to save time.
Patterns are, in a sense, a language of their own within the SC language, and it takes time to get familiar. At first, it can be difficult to find the right pattern (or combination of patterns) to express an idea. But, when learning a foreign language, you don't need to memorize the dictionary to become fluent. You only need to learn a small handful of important words, enough to form a few coherent, useful sentences, and the rest will follow with practice and exposure. Patterns are among the most thoroughly documented classes, and there are multiple tutorials built into the help browser.
Value Sequences
Many patterns define sequences of values. Often, these values are numbers, but may be any type of data. Consider Pseries
, demonstrated below, which represents an arithmetic sequence that begins at 50, repeatedly adds 7, and generates a total of six values. Creating this stream from scratch using a routine, shown before, requires considerably more labor.
A pattern-generated arithmetic sequence
( ~pat = Pseries(start: 50, step: 7, length: 6); ~seq = ~pat.asStream; ) ~seq.next; // evaluate repeatedly
A routine-generated arithmetic sequence
( ~pat = { var num = 50, inc = 7, count = 0; while({count < 6}, { num.yield; num = num + inc; count = count + 1; }); }; ~seq = Routine(~pat); ) ~seq.next; // evaluate repeatedly
Patterns are described as stateless.
They represent a sequence but are distinct from and completely unaware of its actualization. To emphasize, observe the result when trying to extract values directly from a pattern:
~pat = Pseries(50, 7, 6); ~pat.next; // -> returns "a Pseries"
nextN
returns an array of values from a sequence, and all
returns an array containing all of them. To demonstrate, we also introduce Pwhite
, which defines a sequence of random values selected from a range with a uniform distribution. Like rrand
, Pwhite
defines a sequence of integers if its boundaries are integers, and floats if either boundary is a float.
Retrieving an array of multiple values from a sequence
( ~pat = Pwhite(lo: 48, hi: 72, length: 10); ~seq = ~pat.asStream; ) ~seq.nextN(4); // evaluate repeatedly ~seq.reset; ~seq.all;
Infinite-length sequences are possible, as shown below, created by specifying inf
for the pattern length or number of repeats. Here, we introduce Prand
, which randomly selects an item from an array.
( ~pat = Prand(list: [4, 12, 17], repeats: inf); ~seq = ~pat.asStream; ) ~seq.next; // an inexhaustible supply
Calling all
on an Infinite-length Stream
Like playing an infinite-length routine with no yields, calling all on an infinite-length stream will crash the program!
The following table provides a list of commonly encountered patterns used to generate numerical sequences. Note that some patterns, like Pseries
and Pwhite
, can only define sequences with numerical output, while others, like Pseq
and Prand
, output items from an array and can therefore define sequences that output any kind of data.
Pattern | Description |
Pseq(list, repeats, offset) | Sequentially outputs values from list array, with an optional offset to a specific index. |
Pwhite(lo, hi, length) | Random values between lo and hi with a uniform distribution. |
Pexprand(lo, hi, length) | Random values between lo and hi with an exponential distribution. |
Pbrown(lo, hi, step, length) | Random values between lo and hi, but never deviating from the previous value by more than ±step. |
Prand(list, repeats) | Randomly outputs values from list array. |
Pxrand(list, repeats) | Randomly outputs values from list array, but never selects the same item twice in a row. |
Pwrand(list, weights, repeats) | Randomly outputs values from list array, according to a second array of weights that must sum to one. |
Pseries(start, step, length) | Arithmetic series. Begins at start and incrementally adds step. |
Pgeom(start, grow, length) | Geometric series. Begins at start and repeatedly multiplies by grow. |
Mathematical operations and methods that apply to numbers can also be applied to patterns that specify numerical sequences. Imagine creating a sequence that outputs random values between ±1.0, but also wanting that sequence to alternate between positive and negative values. One solution involves multiplying one pattern by another, as shown next. The result is a composite pattern that defines a sequence in which corresponding pairs of output values are multiplied. The example afterwards offers another solution, involving nesting patterns inside of another. When an array-based pattern (such as Pseq
or Prand
) encounters another pattern as part of its output, it embeds the entire output of that pattern before moving on to its next item (similar to the embedInStream
method for routines).
Multiplication of one pattern by another
( ~pat = Pwhite(0.0, 1.0, inf) * Pseq([-1, 1], inf); ~seq = ~pat.asStream.nextN(8); )
Nesting patterns inside other patterns
( ~pat = Pseq([ Pwhite(-1.0, 0.0, 1), Pwhite(0.0, 1.0, 1) ], inf); ~seq = ~pat.asStream.nextN(10); )
The Event Model
Value patterns are useful for generating sequences of numbers and other data types, but more is required to generate sequences of sound. Pbind
, introduced in the next section, is a pattern capable of defining sound sequences, which relies on the Event
class, discussed only briefly before as a storage device for buffers. As a reminder, an Event
is a type of unordered collection in which each item is paired with a key,
specified as a symbol. An Event
can be created with the new
method, or with an enclosure of parentheses. The following code statements use an Event
to model quantity and type of fish in an aquarium. We begin with five guppies and eight goldfish:
a = (guppy: 5, goldfish: 8);
If we acquire three fish of a new breed, we can update the Event
by adding a new key-value association:
a[\clownfish] = 3;
If a sixth guppy appears, we can update the value at that key. Because each key in an Event
is unique, this expression overwrites the previous key, rather than creating a second identical key:
a[\guppy] = 6;
To retrieve the number of goldfish, we access the item stored at the appropriate key:
a[\goldfish]; // -> 8
An Event is not just a storage device; it is commonly used to model an action taken in response to a play message, in which its key-value pairs represent parameters and values necessary to that action. Often, this action is the creation of a new Synth
, but Events model other actions, like musical rests, set messages, or sending MIDI data. There are numerous pre-made Event types that represent useful actions, each pre-filled with sensible default values. A complete list of built-in Event types can be retrieved by evaluating:
Event.eventTypes.keys.postcs;\
Post Window Techniques
postcs
(short for post compile string
) is useful for printing the entirety of a large body of text in the post window. By contrast, postln
truncates its receiver if deemed too long. Additionally, by appending the empty symbol (a single backslash) after the semicolon, we can suppress the double-posting
that sometimes occurs as a result of the interpreter always posting the result of the last evaluated statement. Compare the following four expressions:
(0..999).postln; // truncated double-post (0..999).postln;\ // truncated single-post (0..999).postcs; // non-truncated double-post (0..999).postcs;\ // non-truncated single-post (ideal!)
postcs is also useful for visualizing the internals of a function:
( ~func = { |input| input = input + 2; }; ) ~func.postcs;\ // print the entire function definition ~func.postln;\ // only prints "a Function"
Each Event type represents a different type of action and thus expects a unique set of keys. Most Event types are rarely used and largely irrelevant for creative applications. The most common, by far, is the note Event, which models the creation of a Synth
. This is the default Event type if unspecified. The components and behaviors of the note Event are given default values that are so comprehensive, that even playing an empty Event generates a Synth
and produces a sound, if the server is booted:
().play;
On evaluation, the post window displays the resulting Event, which looks roughly like this:
-> ('instrument': default, 'msgFunc': a Function, 'amp': 0.1, 'server': localhost, 'sustain': 0.8, 'isPlaying': true, 'freq': 261.6255653006, 'hasGate': true, 'id': [1000])
The default note Event includes a freq key with a value of approximately 261.6 (middle C), an amp key with a value of 0.1, and a few other items that are relatively unimportant at the moment. We can override these default values by providing our own. For example, we can specify a higher and slightly louder pitch:
(freq: 625, amp: 0.4).play;
Where does this sound come from? The instrument key specifies the SynthDef
to be used, and there is a \default SynthDef
, automatically added when the server boots. This default SynthDef
is primarily used to support code examples in pattern help files, and can be found in the Event source code within the makeDefaultSynthDef
method. When a SynthDef
name is provided for an Event's instrument key, that SynthDef
's arguments also become meaningful Event keys. Event keys that don't match SynthDef
arguments and aren't part of the Event definition will have no effect. For example, the default SynthDef
has a pan position argument, but no envelope parameters. Thus, in the following line, pan will shift the sound toward the left, but atk does nothing:
(freq: 625, amp: 0.3, pan: -0.85, atk: 0.5).play;
The default SynthDef
is rarely used for anything beyond simple demonstrations. In the following excerpt an Event creates a Synth using a SynthDef adapted from other code.
Using an Event to play a sound using a custom SynthDef. On creation, key-value pairs in the Event are supplied as Synth arguments
( SynthDef(\bpf_brown, { arg atk = 0.02, rel = 2, freq = 800, rq = 0.005, pan = 0, amp = 1, out = 0; var sig, env; env = Env([0, 1, 0], [atk, rel], [1, -2]).kr(2); sig = BrownNoise.ar(0.8); sig = BPF.ar(sig, freq, rq, 1 / rq.sqrt); sig = Pan2.ar(sig, pan, amp) * env; Out.ar(out, sig); }).add; ) (instrument: \bpf_brown, freq: 500, atk: 2, rel: 4, amp: 0.6).play;
A few additional details about internal Event mechanisms are worth discussing. Throughout earlier sections, the argument names freq
and amp
were regularly used to represent the frequency and amplitude of a sound. We can technically name these parameters whatever we like, but these specific names are chosen to take advantage of a flexible system of pitch and volume specification built into the Event paradigm. In addition to specifying amplitude via amp, we can also specify amplitude as a value in decibels, using the db key:
(instrument: \bpf_brown, db: -3).play; (instrument: \bpf_brown, db: -20).play;
How is this possible? Despite the fact that db is not one of our SynthDef arguments, the Event knows how to convert and apply this value correctly. We can understand this behavior more clearly by examining some internals of the Event paradigm:
Event.parentEvents.default[\db]; // default db value = -20.0 Event.parentEvents.default[\amp].postcs; // -> the function {~db.dbamp}
In the absence of user-provided values, the default db value of -20.0 is converted to a normalized amplitude of 0.1. If a db value is provided, that value is converted to an amplitude. If an amp value is directly provided, it intercepts
the conversion process, temporarily overwriting the function that performs the conversion, such that the amp value is directly provided to the Synth. It's important to remember that this flexibility is only available if the SynthDef includes an argument named amp
that is used in the conventional sense (as a scalar that controls output level). If, for example, our SynthDef used a variable named vol
for amplitude control, then neither amp nor db would have any effect on the sound if provided in the Event, akin to misspelling a SynthDef argument name when creating a Synth. In this case, our only option for level control would be vol,
and we would not have access to this two-tier specification structure. To specify level as a decibel value, we would have to perform the conversion ourselves, for example:
(vol: -20.dbamp).play;
The situation with freq is similar, but with more options. If freq
is declared as a SynthDef argument that controls pitch, then four layers of pitch specification become available. These four options are somewhat intertwined, but are roughly expressible as follows:
- 1.
degree,
along withscale
andmtranspose,
allows modal expression of pitch as a scale degree, with the possibility of modal transposition. - 2.
note,
along withroot,
octave,
gtranspose,
stepsPerOctave,
andoctaveRatio,
allows specification of pitch as a scale degree within an equal-tempered framework, with a customizable octave ratio and arbitrary number of divisions per octave. - 3.
midinote,
along withctranspose
andharmonic,
allows specification of pitch as MIDI note numbers (non-integers are allowed), with the option for chromatic transposition and specification of a harmonic partial above a fundamental. - 4.
freq,
along withdetune,
allows specification of a frequency value measured in Hertz, with an optional offset amount added to this value.
Use of four different pitch specifications to play middle C, followed by the D immediately above it.
(degree: 0).play; (degree: 1).play; // modal transposition by scale degree (note: 0).play; (note: 2).play; // chromatic transposition by semitones (midinote: 60).play; (midinote: 62).play; // MIDI note numbers (freq: 261.626).play; (freq: 293.665).play; // Hertz
Flats and Sharps with Scale Degrees
The degree key has additional flexibility for specifying pitch. Altering a degree value by ±0.1 produces a transposition by one semitone, akin to notating a sharp or flat symbol on a musical score:
(degree: 0).play; (degree: 0.1).play; // sharp
Similarly, s
(for sharp) and b
(for bemol or flat) can be appended to an integer, which has the same result:
(degree: 0).play; (degree: 0b).play; // flat
Again, these pitch options are only available if an argument named freq
is declared in the SynthDef and used conventionally. In doing so, pitch information is specifiable at any of these four tiers, and calculations propagate through these tiers from degree
to detunedFreq.
The functions that perform these calculations can also be examined:
Event.parentEvents.default[\degree]; // default = 0 Event.parentEvents.default[\note].postcs; Event.parentEvents.default[\midinote].postcs; Event.parentEvents.default[\freq].postcs; Event.parentEvents.default[\detunedFreq].postcs;
Lastly, we shall demonstrate an interesting feature of Events. If a SynthDef includes a gated envelope, we must manually close the gate when creating a Synth, but the gate closes automatically when playing an Event.
( SynthDef(\bpf_brown, { arg atk = 0.02, rel = 2, gate = 1, freq = 800, rq = 0.005, pan = 0, amp = 1, out = 0; var sig, env; env = Env.asr(atk, 1, rel).kr(2, gate); sig = BrownNoise.ar(0.8); sig = BPF.ar(sig, freq, rq, 1 / rq.sqrt); sig = Pan2.ar(sig, pan, amp) * env; Out.ar(out, sig); }).add; ) x = Synth(\bpf_brown, [freq: 500, amp: 0.2]); x.set(\gate, 0); // manual gate closure (instrument: \bpf_brown, freq: 500, amp: 0.2).play; // automatic gate closure
The default note Event includes a sustain key, which represents a duration after which a (\gate, 0) message is automatically sent to the Synth. The sustain value is measured in beats, the duration of which is determined by the clock on which the Event is scheduled (if no clock is specified, the default TempoClock is used, which runs at 60 bpm). This mechanism assumes the SynthDef has an argument named gate,
used to release some sort of envelope with a terminating doneAction. If this argument has a different name or is used for another purpose, the Synth will become stuck in an on
state, probably requiring [cmd]+[period]. The backend of the Event paradigm is complex, and perplexing situations (e.g., stuck notes) may arise from time to time.
Although the Event paradigm runs deeper than discussed here, this general introduction should be enough to help you get started with Event sequencing using the primary Event pattern, Pbind. For readers seeking more detailed information on Event features and behaviors, the Event help file provides additional information, particularly in a section titled Useful keys for notes. Relatedly, the code below can be evaluated to print a list of keys that are built into the Event paradigm.
( Event.partialEvents.keys.do({ |n| n.postln; Event.partialEvents[n].keys.postln; \.postln; });\ )
Event Sequences with Pbind
Pbind combines value patterns with Events, establishing a high-level framework for expressing musical sequences. A Pbind contains a list of pairs, each of which consists of an Event key and value pattern. When a stream is generated from a Pbind, and asked to perform its output, the result is a sequence of Events. Each Event contains keys from the Pbind, and each key is paired with the next value defined by its corresponding value pattern. This behavior of binding
values to keys is what gives Pbind its name. The code below provides an example. To perform the Events, we convert the pattern to a stream, call next
to yield an Event, and play it to generate a Synth. The only difference between this example and the stream examples from previous sections is that here, we must provide an empty starting Event to be populated (hence the additional set of parentheses inside of next
).
( p = Pbind( \midinote, Pseq([55, 57, 60], 2), \db, Pwhite(-20.0, -10.0, 6), \pan, Prand([-0.5, 0, 0.5], 6) ); ) ~seq = p.asStream; ~seq.next(()).play; // evaluate repeatedly, returns nil when finished ~seq.reset; // can be reset at any time
In the foregoing code, each internal value pattern specifies exactly six values, so the stream produces six Events. Because the Pbind does not specify otherwise, the created Events are note-type Events that use the default SynthDef. The midinote
and db
keys undergo internal calculations to yield freq
and amp
values, which are supplied to each Synth.
Manually extracting and playing Events one-by-one is rarely done, intended here as only an illustrative example. More commonly, we play a Pbind, which returns an EventStreamPlayer, a type of stream that performs an Event sequence. As demonstrated next, an EventStreamPlayer works by automating the Event extraction process, and scheduling the Events to be played on a clock, using the default TempoClock if unspecified, thus generating a timed musical sequence.
( p = Pbind( \midinote, Pseq([55, 57, 60], 2), \db, Pwhite(-20.0, -10.0, 6), \pan, Prand([-0.5, 0, 0.5], 6) ); ~seq = p.play; )
Debugging Pattern Values
Visualizing a Pbind's output can be helpful for debugging code that doesn't work properly, and also for understanding pattern behavior in general. postln does not work in this context. Instead, trace can be applied to any pattern, which prints its output to the post window as a stream performs it:
( p = Pbind( \midinote, Pseq([55, 57, 60], 2).trace, ); ~seq = p.play; )
The onset of each Synth occurs one second after the preceding onset. But where does this timing information originate? Timing information is typically provided using the dur key, which specifies a duration in beats and has a default value of one:
Event.partialEvents.durEvent[\dur]; // -> 1.0
We can provide our own timing information, which may be a value pattern, as shown below:
( p = Pbind( \dur, Pseq([0.75, 0.25, 0.75, 0.25, 0.5, 0.5], 1), \midinote, Pseq([55, 57, 60], 2), \db, Pwhite(-20.0, -10.0, 6), \pan, Prand([-0.5, 0, 0.5], 6) ); ~seq = p.play; )
Pbind can specify an infinite-length Event stream. If each internal value pattern is infinite, the Event stream will also be infinite. If at least one internal value pattern is finite, the length of the Event stream will be equal to the length of the shortest value pattern. If an ordinary number is provided for one of Pbind's keys, it will be interpreted as an infinite-length value stream that repeatedly outputs that number. An EventStreamPlayer can be stopped at any time with stop
. Unlike routines, a stopped EventStreamPlayer can be resumed with resume
, causing it to continue from where it left off. The following code demonstrates these concepts: the midinote pattern is the only finite value pattern, which produces 24 values and thus determines the length of the Event stream. The db value −15 is interpreted as an infinite stream that repeatedly yields −15.
( p = Pbind( \dur, Pseq([0.75, 0.25, 0.75, 0.25, 0.5, 0.5], inf), \midinote, Pseq([55, 57, 60], 8), \db, -15 ); ~seq = p.play; ) ~seq.stop; ~seq.resume;
In a Pbind, only values or value patterns should be paired with Event keys. It may be tempting to use a method like rrand to generate random values for a key. However, this will result in a random value being generated once when the Pbind is created and used for every Event in the output stream, as demonstrated next, whereas the correct approach is to use the pattern or pattern combination that represents the desired behavior (in this case, Pwhite).
An incorrect approach for creating randomness in an Event stream.
( p = Pbind( \dur, 0.2, \midinote, rrand(50, 90), // <- should use Pwhite(50, 90) instead ); ~seq = p.play; )
Recalling techniques already introduced, an EventStreamPlayer can be scheduled on a specific TempoClock and quantized to a particular beat:
Rhythmically quantizing two EventStreamPlayers on a custom TempoClock
( t = TempoClock(90/60); p = Pbind( \dur, 0.25, \midinote, Pwhite(48, 60, inf), ); q = Pbind( \dur, 0.25, \midinote, Pwhite(72, 84, inf), ); ) ~seq_p = p.play(t, quant:4); // scheduled on next beat multiple of 4 ~seq_q = q.play(t, quant:4); // synchronizes with ~seq_p
Combined with a general understanding of Events, patterns, and streams, Pbind unlocks an endless supply of ideas for musical sequences.
Additional Techniques for Pattern Composition
As you explore musical sequences, you'll inevitably find yourself wanting to build larger ideas from smaller, individual patterns. This section covers a few relevant features of patterns, aiming to facilitate expression of more complex sequences.
The Rest Event
There are many types of pre-made Events, but so far, we've only been using note Events. In some cases, the note Event by itself is insufficient and awkward. Consider, for example, a musical phrase which begins with a rest. How should we represent this phrase with patterns?
t = TempoClock.new(112/60); ( Pbind( \dur, Pseq([1/2, 1/2, 1/2, 1/4, 1/2, 1/2, 1/2], 1), \sustain, 0.1, \degree, Pseq([4, 5, 7, 4, 5, 7, 8], 1), ).play(t); )
This code is a poor choice, because it treats the phrase as if it begins on its first eighth note. Rhythmic misalignment would likely occur if we tried to quantize this pattern. A better approach involves using rest Events. A rest Event does exactly what its name suggests: when played, it does nothing for a specific number of beats. Event types are specified using the type key, and providing the type name as a symbol. A two-beat rest Event, for example, looks like this:
(type: \rest, dur: 2).play;
To employ rests in an Event sequence, we can embed instances of the Rest class in a Pbind's dur pattern. Each rest is given a duration, measured in beats:
t = TempoClock.new(112/60); ( Pbind( \dur, Pseq([ Pseq([Rest(1/4), 1/4], 4), // bar 1 Pseq([1/4, Rest(1/4), 1/4, Rest(1/4), 1/4, Rest(3/4)]) // bar 2 ], 1), \sustain, 0.1, \degree, Pseq([ 0, 4, 0, 5, 0, 7, 0, 4, // bar 1 5, 0, 7, 0, 8, 0, 0, 0 // bar 2 ], 1), ).play(t); )
When this code is quantized, the true downbeat of the phrase occurs on the target beat. Note that the pitch values nicely resemble the metric layout of the notated pitches. The pitch values that align with rest Events are ultimately meaningless since they do not sound. Zeroes are used for visual clarity, but any value is fine.
Other options exist for expressing a sequential mix of notes and rests. Rest Events can be generated by using a pattern to determine type values. This approach lets us use a constant dur value equal to the smallest beat subdivision necessary to express the rhythm, and the type pattern handles the determination of Event type (see next). Another option involves supplying symbols for a pitch-related pattern (such as degree, note, midinote, or freq). If the pitch value of a note Event is a symbol, the Event becomes a rest Event. The simplest option is to use the empty symbol, expressed as a single backslash (see example after next).
Pattern manipulation of the type key to express a musical phrase
t = TempoClock.new(112/60); ( Pbind( \type, Pseq([ Pseq([\rest, \note], 4), // bar 1 Pseq([\note, \rest], 2), \note, Pseq([\rest], 3) // bar 2 ], 1), \dur, 1/4, \sustain, 0.1, \degree, Pseq([ 0, 4, 0, 5, 0, 7, 0, 4, // bar 1 5, 0, 7, 0, 8, 0, 0, 0 // bar 2 ], 1), ).play(t); )
Using symbols in a pitch pattern to express the same musical phrase
t = TempoClock.new(112/60); ( Pbind( \dur, 1/4, \sustain, 0.1, \degree, Pseq([ \, 4, \, 5, \, 7, \, 4, // bar 1 5, \, 7, \, 8, \, \, \ // bar 2 ], 1), ).play(t); )
Though it's possible to achieve a similar result by strategically providing zeroes in an amplitude pattern, this is less efficient. In this case, every Event will produce a Synth, and every Synth consumes some processing power, regardless of its amplitude.
Limiting Pattern Output with Pfin/Pfindur
It's sometimes convenient to define an infinite length pattern but specify a finite output at performance time, therefore avoiding the need to manually stop the stream. The example below shows the use of Pfin
to limit the number of values a pattern will output. Pfindur
is similar, but instead of constraining based on Event quantity, Pfindur
limits a stream based on the number of beats that have elapsed, shown after next. The duration of one beat is, as usual, governed by the clock on which the stream plays.
Using Pfin to limit the output of an Event stream to 16 Events
( p = Pbind( \dur, 1/8, \sustain, 0.02, \freq, Pexprand(200, 4000, inf), ); q = Pfin(16, p); ~seq = q.play; // stops after 16 events )
Using Pfindur to limit the output of an Event stream to three beats.
( p = Pbind( \dur, 1/8, \sustain, 0.02, \freq, Pexprand(200, 4000, inf), ); q = Pfindur(3, p); ~seq = q.play; // stops after 3 beats )
Pfin and Pfindur are helpful in allowing us to focus on the finer musical details of a pattern during the compositional process, working in an infinite mindset and not concerning ourselves with total duration. When the sound is just right, we can simply wrap the pattern in one of these limiters to constrain its lifespan as needed.
Composite Patterns with Pseq/Ppar/Ptpar
Once you have several Pbinds that represent musical phrases, a logical next step involves combining them into composite structures that represent longer musical phrases, large-scale sections, and perhaps eventually, an entire musical composition. Consider the code below, which represents a four-note sequence, and this same sequence shifted up by two scale degrees:
( ~p0 = Pbind( \dur, 1/6, \degree, Pseq([0, 2, 3, 5], 1), \sustain, 0.02, ); ~p1 = Pbind( \dur, 1/6, \degree, Pseq([2, 4, 5, 7], 1), \sustain, 0.02, ); )
Suppose we wanted to play these phrases twice in sequence, for a total of four phrases. Pseq provides an elegant solution, shown next, which demonstrates its ability to sequence Events, and not just numerical values. A composite pattern need not be deterministic; any value pattern that retrieves items from an array can be used for this purpose. If Prand([~p0, ~p1], 4)
is substituted for Pseq
, for example, the Event stream will play four phrases, randomly chosen from these two.
( ~p_seq = Pseq([~p0, ~p1], 2); ~player = ~p_seq.play; )
To play these two phrases, we could simply enclose ~p0.play
and ~p1.play
in the same block and evaluate them with one keystroke, but this approach only sounds the patterns together—it does not express these two patterns as a singular unit. Ppar
(see below) is a better option, which takes an array of several Event patterns, and returns one Event pattern in which the individual patterns are essentially superimposed into a single sequence. Like Pseq, Ppar also accepts a repeats value. Ptpar is similar (see example afterwards) but allows us to specify a timing offset for each individual pattern used to compose the parallel composite pattern.
Use of Ppar to play Event patterns in parallel. This example relies on the two patterns previously created
( ~p_par = Ppar([~p0, ~p1], 3); ~seq = ~p_par.play; )
Use of Ptpar to play Event patterns in parallel with individual timing offsets. The second pattern begins one-twelfth of a beat after the first. This example relies on the two patterns already created
( p = Ptpar([ 0, Pseq([~p1], 3), 1/12, Pseq([~p0], 3) ], 1); ~seq = p.play; )
As you may be able to guess from the previous examples, the composite patterns returned by Pseq, Ppar, and Ptpar retain the ability to be further sequenced into even larger composite patterns. In some cases, the final performance structure of a composition involves a parent Pseq, which contains Pseq/Ppar patterns that represent large-scale sections, which contain Pseq/Ppar patterns that represent sub-sections, and so forth, all the way down to note-level details. The central idea is that patterns should be treated as modular building blocks, which can be freely combined and may represent all sequential aspects of a composition. Conceptualizing time-based structures within the pattern framework provides great flexibility, allowing complex sequential ideas to be expressed and performed relatively easily.
Real-Time Pattern Control
Real-time control is an essential feature for performing electronic music. We've seen a simple example in previous sections: a Synth argument can be set
while it is running, which can influence its sound and behavior. It's also possible to manipulate characteristics of an EventStreamPlayer
while it's playing, by swapping out one value pattern for another. This ability mostly relies on a handful of pattern objects that revolve around the concept of proxies. In SC, a proxy is a placeholder that references a piece of data we want to be able to change dynamically. Proxies enable fluid musical transitions and allow us to make spontaneous decisions on-the-fly, helping us avoid the need to prepare extensive sequential structures in advance.
The PatternProxy
class provides core functionality for real-time pattern control, but in practice, we typically use one of its subclasses, such as Pdefn
, Pdef
, or Pbindef
. These classes are part of a larger def-type
family that includes Ndef
, Tdef
, MIDIdef
, OSCdef
, and others. These proxies share a common syntax by which they are created and referenced. On creation, a def-type object includes a name (provided as a symbol), followed by its data. The data can be dynamically overwritten and can be referenced by name. Next a pseudo-code example is provided. Proxies are the core element of live coding in SC.
ThingDef(\name, data); // create proxy and provide initial data ThingDef(\name, newData); // overwrite proxy with new data ThingDef(\name); // reference current proxy data
Pdefn
Pdefn serves as a proxy for a single value pattern, deployed by wrapping it around the desired pattern. In the example, it serves as a placeholder for the degree pattern. While the EventStreamPlayer ~seq is playing, the pitch information can be dynamically changed by overwriting the proxy data with a new value pattern.
( p = Pbind( \dur, 0.2, \sustain, 0.02, \degree, Pdefn(\deg0, Pwhite(-4, 8, inf)), ); ~seq = p.play; ) Pdefn(\deg0, Pseq([0, 2, 3], inf));
Note that if an infinite value pattern is replaced with a finite one, the EventStreamPlayer to which that pattern belongs also becomes finite. One practical application of this feature, exemplified below, is to fade out an EventStreamPlayer by replacing its amplitude pattern with one that gradually decreases to silence or near-silence over a finite period of time.
( p = Pbind( \dur, 0.2, \sustain, 0.02, \degree, Pwhite(-4, 8, inf), \db, Pdefn(\db0, -20), ); ~seq = p.play; ) Pdefn(\db0, Pseries(-20, -1, 40));
Multiple Pdefn objects can be independently used and manipulated in the context of one Pbind. In this case, care should be taken to ensure that no two Pdefns share the same name. If they do, one will overwrite the other, much in the same way that an Event cannot contain two different pieces of data at the same key. It's also possible to deploy one Pdefn in several different places. A change to a Pdefn propagates through all its implementations, as demonstrated below.
( Pdefn(\deg0, Pseq([0, 4, 1, 5], inf)); p = Pbind( \dur, 0.2, \sustain, 0.02, \degree, Pdefn(\deg0), ); q = Pbind( \dur, 0.2, \sustain, 0.02, \degree, Pdefn(\deg0) + 2, ); ~seq = Ppar([p, q]).play; ) Pdefn(\deg0, Pseq([-3, -2, 0],inf));
Pdefn can be quantized using techniques similar to those previously discussed, but the syntax is slightly different, demonstrated beñpw. Instead of specifying quant as an argument, each Pdefn has its own quant
attribute, accessible by applying the quant method to the Pdefn and setting a desired value.
( t = TempoClock(128/60); p = Pbind( \dur, 1/4, \sustain, 0.02, \note, Pdefn(\note0, Pseq([0, \, \, \, 1, 2, 3, 4], inf)), ); ~seq = p.play(t, quant: 4); ) ( Pdefn(\note0, Pseq([7, \, 4, \, 1, \, \, \], inf) ).quant_(4); )
Pdefn remembers
its quantization value, so it's unnecessary (but harmless) to re-specify this information for subsequent Pdefn changes meant to adhere to the same value. We can nullify a quant value by setting it to nil
.
The example below shows a few additional techniques. Pdefn's data can be queried with source
(optionally appending postcs
for verbosity), and a Pdefn
's data can be erased with clear
. We can also iterate over all Pdefn
objects to erase each one.
Pdefn(\note0).source; // -> a Pseq Pdefn(\note0).source.postcs; // -> full Pseq code Pdefn(\note0).clear; // erase Pdefn data Pdefn(\note0).source; // -> nil Pdefn.all.do({ |n| n.clear });
Pdef
Pdef, demonstrated below, is similar to Pdefn, and features many of the same techniques. The primary difference is that Pdef is a proxy for an Event pattern, rather than a value pattern. Typically, the data of a Pdef is a Pbind, but may also be a Pseq or Ppar that represents a composite of several Pbinds. When an Event pattern is encapsulated within a Pdef, any part of it can be changed while the Pdef is playing, without interruption of the Event stream. Pdef is generally useful in that it avoids the need to create multiple, independent Pdefns within a Pbind.
( t = TempoClock(128/60); Pdef(\seq, Pbind( \dur, 0.25, \sustain, 0.02, \degree, Pseq([0, 2, 4, 5], inf), ) ).clock_(t).quant_(4); Pdef(\seq).play; ) ( Pdef(\seq, // swap the old Pbind for a new one Pbind( \dur, Pseq([0.5, 0.25, 0.25, 0.5, 0.5], inf), \sustain, 0.5, \degree, Pxrand([-4, -2, 0, 2, 3], inf), ) ); )
In the foregoing code, note that we set clock and quant attributes for the Pdef before playing it, which are retained and remembered whenever the Pdef's source changes. All the Pdefn methods shown are valid for Pdef as well.
Pbindef
Pbindef is nearly the same as Pdef; it's a proxy for Event patterns, and it retains all the same methods and attributes of Pdef (in fact, Pbindef is a subclass of Pdef). These two classes even share the same namespace in which their data is stored, in other words, Pdef(\x)
and Pbindef(\x)
refer to the same object. The difference between these two classes is that Pbindef allows key-value pairs in its Event pattern to be modified on an individual basis, instead of requiring the entire Pbind
source code to be present when a change is applied. Instead of placing a Pbind inside a proxy pattern, the syntax of a Pbindef involves merging
Pdef and Pbind into a single entity, much in the same way that their names form a portmanteau. The example below demonstrates various features of Pbindef, including changing one or more value-patterns in real-time, adding new value patterns, and substituting a finite value pattern to create a fade-out. Although this example does not use the quant and clock attributes, they can be applied to Pbindef using the same techniques that appeared before.
( Pbindef(\seqA, \dur, Pexprand(0.05, 2, inf), \degree, Prand([0, 1, 2, 4, 5], inf), \mtranspose, Prand([-7, 0, 7], inf), \sustain, 4, \amp, Pexprand(0.02, 0.1, inf), ).play; ) // change degree pattern: Pbindef(\seqA, \degree, Prand([0, 1, 3.1, 4.1, 5], inf)); // change dur and sustain pattern in one expression: Pbindef(\seqA, \dur, 0.3, \sustain, Pseq([2, 0.02], inf)); // add a new value pattern: Pbindef(\seqA, \pan, Pwhite(-0.8, 0.8, inf)); // fade out with finite amp pattern: Pbindef(\seqA, \amp, Pgeom(0.05, 0.85, 30));
In the code above, a finite Pgeom modifies the Event stream so that it ends after 30 Events. However, it's possible to restart the stream by playing the Pbindef again. In this case, it remembers its key-value pairs, and will generate another thirty Events with the same decreasing amp pattern. Of course, we can replace the amplitude pattern with an infinite value pattern before restarting playback, and the Event stream will continue indefinitely once more. The semi-permanence of Pbindef's key-value pairs can be a source of confusion, particularly if switching between different pitch or amplitude tiers. Consider the Pbindef in the code below. It begins with explicit freq values, which override internal calculations that propagate upward from degree values. As a result, if we try to dynamically switch from frequency to degrees, the degree values will have no effect, because the Pbindef remembers
the original frequency values. Thus, if we want to switch to specifying pitch in degrees, we also need to set the old frequency pattern to nil
.
( Pbindef(\seqB, \dur, 0.2, \sustain, 0.02, \freq, Pexprand(500, 1200, inf) ).play; ) // degree values are ignored: Pbindef(\seqB, \degree, Pwhite(0, 7, inf)); // degree values take effect Pbindef(\seqB, \freq, nil, \degree, Pwhite(0, 7, inf));