From Fedora Project Wiki

< User:Crantila‎ | FSC‎ | Synthesizers/SuperCollider

Revision as of 06:21, 6 July 2010 by Crantila (talk | contribs) (wrote a lot)

Address: User:Crantila/FSC/Synthesizers/SuperCollider/Basic_Programming

As with any programming language, you will start learning SuperCollider with the basic commands, that are of little use by themselves. However, since the language is so flexible, even the most basic commands can be combined in ways that create highly complex behaviours. The example program, "Method One," was written with the goal of illustrating how a single sound-generating object can be used to create an entire composition. This tutorial does not begin with audio-generating code, which helps to emphasize that SuperCollider is primarily a programming language.

This portion of the Guide is designed as a "reference textbook," which you can use both to learn the SuperCollider language in the first place, and to remind yourself about the language's features afterwards.

The Guide will probably be most effective when read only in small portions at once.


First Steps

The Different Parts of SuperCollider

As you discovered when installing SuperCollider, there are actually many different components involved with SuperCollider. Here is a list of some of them, with brief descriptions of their purpose:

  • Programming language: this is an abstract set of rules and guidelines that allow you to write down instructions for producing sounds.
  • Interpreter: this is what is run in GEdit; it transforms the "programming language" instructions written by you into useful instructions for the server; also called the "client."
  • Server: this is what synthesizes the sound, according to instructions sent to it by the interpreter.
  • Library: these contain commands and the instructions to be executed when you call the commands; the interpreter looks up commands in the library when you call them.

This modular design allows for several advanced capabilities and features. Any particular element could theoretically be replaced without affecting other elements, as long as the methods of communication remain the same. As long as the programming language is the same, portions of the library can be modified, removed, or added at will; this happens often, and Planet CCRMA at Home provides a collection of library extensions. One of the most exciting capabilities is the ability to run the interpreter and server on different physical computers. The networking component is built into these components - they always communicate by UDP or TCP, even when run on the same computer! Although this ability is not used in this Guide, it is not difficult.

The most important thing to remember is that the SuperCollider interpreter is what deals with the programs you write. The SuperCollider server is controlled by the interpreter, but is an independent program. For simple things, like the Hello World Programs below, the server is not even used - after all, there is no audio for it to synthesize.

"Hello, World!"

The first program that one traditionally makes when learning a new programming language is called "The Hello World Program." This is a simple and trivial application that simply prints outs the phrase, "Hello, World!" (or any variation of it). It might seem useless at first, but the ability to provide feedback to an application's user is very important, and this is essentially what the Hello World Program does.

Here is the program in SuperCollider:

"Hello, World!".postln;

Here is an extension to that program:

"Hello, World!".postln;
"Hello, SC!".postln;

As with all examples in this Guide, you should paste these programs into GEdit, and execute them with SuperCollider. Look at the output produced by the programs, but don't worry about it for now.

These programs are very small, but it highlights some key concepts of the SuperCollider language, described below.

Return Values

Every SuperCollider program must provide the interpreter with a value (some information) when it has carried out all of its instructions. This value is called a "return value," because it is the value given by a program when it "returns" control to the interpreter. In a SuperCollider program, it is the last value stated in a program that automatically becomes the return value - no special command is required. When program execution ends, and control is returned to the SuperCollider interpreter, the interpreter outputs the return value in the "SuperCollider output" pane.

In the single-line Hello World Program above, the program produces the following output:

Hello, World!
Hello, World!

The program appears to have been executed twice, but that is not the case. The first "Hello, World!" is printed by the program. The second "Hello, World!" appears because "Hello, World!.postln is the last (in this case, the only) value of the program. It is "returned" by the program, and the interpreter prints it.

In the two-line Hello World Program above, the program produces the following output:

Hello, World!
Hello, SC!
Hello, SC!

This makes it more clear that the program is not being executed twice, and that it is the last value of a program that is returned to the interpreter.

Try executing the following single-line programs. Look at the output produced by each, and determine whether it is printed by the program itself, the interpreter, or both.

  • "Hello, World!".postln;
  • "Hello, World!";
  • 5.postln;
  • 5;

Can you modify the two-line Hello World Program so that each line is printed only once?

Note: In reality, every "function" must return a value. Functions are described later in this Guide; the difference is not yet important.

Statements

A "statement" is a single instruction, which is always ended with a semicolon. Exactly what constitutes a statement will become clear as you gain experience, and including semicolons will quickly become an automatic action.

In the Hello World Programs above, all of the statements contain the single instruction to post a line to the output screen. What happens when you remove the first semicolon, which marks the end of the first statement? The SuperCollider interpreter produces an unhelpful error message, and tells you that an error occurred after the forgotten semicolon. This is why it is important to always remember statement-concluding semicolons.

Data Types: Numbers and Strings

In many programming languages, it is the programmer's responsibility to determine the type of data that is being used, and how it should be stored. The SuperCollider interpreter takes advantage of the power of modern computers, and deals with this on our behalf. This greatly simplifies basic tasks, because there are only two kinds of data to worry about, and they make perfect sense:

  • Numbers: These are numbers, written simply as numbers. Anything that can be done with real-world numbers can also be done with SuperCollider's numbers. They can be as large or small, positive or negative as you want. They can have any number of digits on either side of the decimal point.
  • Strings: These are a string of characters, written between two double-quote characters like "this." The double-quote characters are required so that SuperCollider knows where to begin and end the string of characters. A string of character can contain as many characters as you like, including one character and no characters. If you want to include a double-quote character in a string, you should put a blackslash before it. The following is interpreted by SuperCollider as a string with only a double-quote character: "\""

Here are some examples of numbers and strings:

  • 5;
  • 18920982341;
  • 0.00000000000001;
  • "characters";
  • "@";
  • "";
  • "6";

Is the last example a number or a string? You and I recognize that it is a number inside a string, but SuperCollider will only treat it as a string. You can do string things with it, but you cannot do number things with it. You cannot add "6" to something, for example. Notice also that each example ends with a semicolon, which makes them complete statements. The statements don't do anything but represent themselves.

Try executing the following single-line programs. Think about why the SuperCollider interpreter produces the output that it does.

  • 6 + 3;
  • "6" + 3;
  • "six" + 3;

Simultaneous Execution

Complex SuperCollider programs contain many parts, which all do different things. Sometimes, executing all of these together doesn't make sense, and it can be difficult to know which portions of the program are supposed to be executed when. To help with this, the interpreter allows you to mark portions of your program between ( and ) so that you will know to execute them together.

Here is an example:

(
  "Hello, Fred!".postln;
  "Hello, Wilma!".postln;
)
(
  "Goodbye, Fred!".postln;
  "Goodbye, Wilma!".postln;
)

It doesn't make sense to say "hello" and "goodbye" at the same time, so separating these sections with parentheses will serve as a reminder. In case we try to execute all of the code at once, the SuperCollider interpreter will give us an error.

Variables and Functions

The concepts in this section are related to the mathematical terms with the same names. This is a modern-day result of the first uses of computers and programming languages: the calculation of complex mathematical problems.

Variables

A variable is a symbol that can be assigned an arbitrary value. A "symbol" is a series of alphabetic and numeric characters, separated by whitespace (a space, a line-break, or the end of the file). When a variable is "assigned" a value, the variable name (the symbol) is understood to be a substitute for the assigned value.

Consider a traffic light, which has three possible symbols: green, yellow, and red. When you are driving, and you encounter a traffic light, you might see that its red symbol is activated (the red light is illuminated). What you see is a red light, but you understand that it means you should stop your car. Red lights in general do not make you stop - it is specifically red traffic lights, because we know that it is a symbol meaning to stop.

SuperCollider's variables work in the same way: you tell the interpreter that you want to use a symbol, like cheese. Then you assign cheese a value, like 5. After that point, whenever you use cheese, the interpreter will automatically know that what you really mean is 5.

Run the following two programs. They should result in the same output.

(
  5 + 5;
)
(
  var cheese;
  cheese = 5;
  cheese + cheese;
)

In the first example, the program calculates the value of 5 + 5, which is 10, and returns that to the interpreter, which prints it out. In the second example, the program tells the interpreter that it wants to use a variable called cheese then it assigns cheese the value 5. Finally, the program calculates cheese + cheese, which it understands as meaning 5 + 5, and returns 10 to the interpreter, which prints it out.

This trivial use of a variable does nothing but complicate the process of adding 5 to itself. Soon you will see that variables can greatly simplify your programs.

Using Variables

There are three words that describe the key stages of using a variable: declaration, initialization, and assignment.

A variable must be declared before use, so that the interpreter knows that you want to use that symbol as a variable. All variables must be declared before any statement that does not declare a variable; in other words, you should declare your variables before doing anything else. Variable names are declared like this:

var variableName;

Variables can also be declared in lists, like this:

var variableOne, variableTwo;

Variables can be assigned a value at any time after they have been declared. Any single object can be assigned to a variable. If a variable is already assigned a value, any subsequent assignment will erase the previous assignment; the previously-assigned value will is not retrievable.

The first assignment to a variable is said to "initialize" the variable. Initialization is a special kind of assignment, because a variable cannot be used before it is initialized. If a program attempts to use an un-initialized variable, the SuperCollider interpreter will cause an error. For this reason, you should always initialize a variable when you declare it. There is a special way to do this:

var variableName = nil;

Since you can't always assign a useful value, you can pick an arbitrary one. Assigning "nil" is common practice, because it means "nothing," but without actually being nothing (this avoids some errors). Assigning zero is another possibility; it is standard practice in many programming languages, and will avoid most errors, even if the variable is eventually supposed to hold another kind of object. Intialization and declaration of multiple variables can also be done as a list:

var variableOne = 0, variableTwo = 0;

Single-letter variable names have a special purpose in SuperCollider. They are already declared, so you don't have to declare them. They are also already initialized to "nil", so you don't have to do that either. These variable names are intended to be used as a quick fix, while you're experimenting with how to make a program work. You should not use them in good-quality programs.

The single-letter variable "s" is automatically assigned to the server on the computer running the interpreter. You should avoid re-assigning that variable.

Variable names should always begin with a lower-case letter.

Use variables to write programs that do the following tasks:

  1. Perform arithmetic with an uninitialized variable. An error should appear when the program is executed.
  2. Calculate the value of y, if all other values are known, for the quadratic equation: y = a * x * x + b * x + c
  3. Re-write the Hello World Program so that it will say "Hello" to a name stored in a variable. Remember that you can use the interpreter to automatically output the last line of a function.

Functions

A Function is a statement, or a series of statements, that we want to use many times. When a Function is assigned to a variable, you can execute the Function as many times as you wish. Any statements that happen between braces { like this; } are treated as a Function. Functions are executed by passing them the "value" message, as in the following example.

Here is a Function that is not assigned to a variable, and is executed once.

{ "Hello, World!".postln; }.value;

Notice that there are two semicolons: one after the statement within the Function, and one after the "value" message that tells the Function to execute.

Here is a Function with identical function, assigned to a variable, and executed twice.

var myFunction = { "Hello, World!".postln; }; // note two semicolons
myFunction.value;
myFunction.value;

Function Arguments

The most useful aspect of Functions is that they can produce varying results, depending on their input. For whatever reason, the input accepted by a Function is called an "argument." SuperCollider's Functions can accept any number of arguments - zero, one, or many. Argument values (called "parameters") are provided to a Function by adding them in parentheses after the name of the Function, separated with commas, like this: exampleFunction( 5, 7, 9 ); Argument variables are declared as the first statement in a Function, like this: arg oneNumber, twoNumber;

This program is significantly more complicated than previous examples, but it shows how useful Functions can be. Notice how the braces in that example are on different lines than the rest of the Function, which gives us more space within the Function to complete some useful work.

(
   var greeter =
   {
      arg name;
      ( "Hello" + name ).postln;
   };
   
   greeter.value( "Samantha" );
   greeter.value( "Jermain" );
   nil;
)

Here is how the program works:

  1. A variable named greeter is declared, and assigned a Function.
  2. The Function contains an argument called name, and outputs "Hello" plus the name given to it.
  3. The parentheses here ( "Hello" + name ) ensure that the two strings are added together before the "postln" message prints them out.
  4. The greeter variable is used to call the Function with two different names.
  5. The nil; statement is optional, and does not affect the operation of the program. What it does is return a "nothing" value to the interpreter after program execution completes, so that the last message is not repeated.

Since every argument has a name, SuperCollider allows you to use that name when executing the Function. This example executes the greeter Function from the last example:

greeter.value( name:"Myung-Whun" );

This is more useful if there are many arguments, and you do not remember the order that they appear in the Function's definition.

SuperCollider also allows you to specify default values for arguments, so that they do not need to be specified. This allows optional customization of a Function's behaviour, and is therefore very powerful.

This example modifies the one above by adding default-value arguments, and by calling arguments with their name. As you can see, I've been tricking you a bit: postln is actually a Function, but a special kind, explained later.

(
   var greeter =
   {
      arg name, greeting = "Hello";
      postln( greeting + name );
   };
   
   greeter.value( "Samantha" );
   greeter.value( "Jermain", "Goodbye" );
   greeter.value( name:"Myung-Whun" );
   greeter.value( greeting:"Bienvenue", name:"Marcel" );
   nil;
)

Any value can be used as a parameter, as long as the Function expects it. In fact, even Functions can be used as parameters for Functions!

Function Return Values

All SuperCollider Functions return a value to the interpreter when they have finished execution. As with programs, the value returned is the value of the last statement in the Function. The return value of a Function can be captured, assigned to a variable, and used again later.

This example assigns the result of a Function to a variable.

(
   var mysticalMath =
   {
      arg input = 0;
      input * 23;
   };
   var someNumber = 9;
   
   someNumber = mysticalMath.value( someNumber );
   someNumber.postln;
   nil;
)

Here is how the program works:

  1. A Function and variable are created, and assigned values.
  2. This line someNumber = mysticalMath.value( someNumber ); executes the mysticalMath Function, which multiplies its argument by 23 and returns the value. Then, it assigns the return value of the Function to someNumber. In any statement that contains an assignment, the assignment is always done last. In other words, the Function in this example will always be given an argument of 9, and only after the Function completes execution and returns a value will that value be assigned to someNumber.
  3. The new value of someNumber is displayed.

The program could have been shortened like this:

(
   var mysticalMath =
   {
      arg input = 0;
      input * 23;
   };
   
   var someNumber = mysticalMath.value( 9 );
   someNumber.postln;
   nil;
)

It could have been shortened even more like this:

(
   var mysticalMath =
   {
      arg input = 0;
      input * 23;
   };
   
   mysticalMath.value( 9 ).postln;
   nil;
)

Experiment with the shortened versions of the program, ensuring that you know why they work.

Variable Scope

A variable is only valid within its "scope." A variable's scope is determined by where it is declared. It will always last between either ( and ) or { and }, and applies to all statements within that block of code. Variable names can be re-declared in some contexts, which can be confusing.

Consider the scope of the variables in this example:

(
   var zero = 0;
   var function =
   {
      var zero = 8;
      var sixteen = 16;
      zero.postln; // always prints 8
   };
   
   function.value;
   zero.postln; // always prints 0
   sixteen.postln; // always causes an error
)

Because function declares its own copy of zero, it is modified independently of the variable zero declared before the Function. Every time function is executed, it re-declares its own zero, and the interpreter keeps it separate from any other variables with the same name. When function has finished executing, the interpreter destroys its variables. Variables declared inside any Function are only ever accessible from within that Function. This is why, when we try to execute sixteen.postln;, the interpreter encounters an error: sixteen exists only within function, and is not accessible outside the Function. By the way, in order to excute this example, you will need to remove the error-causing reference to sixteen.

Now consider the scope of the variables in this example:

(
   var zero = 0;
   var function =
   {
      var sixteen = 16;
      zero = 8;
      zero.postln; // always prints 8
      sixteen.postln;
   };
   
   function.value;
   zero.postln; // always prints 8
)

Why does the last line always print 8? It's because zero was set to 8 within function. More importantly, function did not declare its own copy of zero, so it simply accesses the one declared in the next "highest" block of code, which exists between ( and ) in this example.

This is why it's important to pay attention to a variable's scope, and to make sure that you declare your variables in the right place. Unexpected and difficult-to-find programming mistakes can occur when you forget to declare a variable, but it is declared elsewhere in your program: you will be allowed to use the variable, but it will be modified unexpectedly. On the other hand, it can be greatly advantageous to be able to access variables declared "outside the local scope" (meaning variables that aren't declared in the same code block in which they're used), but careful thought and planning is required.

Astute readers will notice that it is possible to re-declare the single-letter variable names, allowing you to control their scope. Consider the following program:

(
   var a = 0;
   b =
   {
      var c = 16;
      a = 8;
      a.postln;
      c.postln;
   };
   
   b.value;
   a.postln;
)

This example requires careful examination. What is the scope of a, b, and c? The answers may be surprising.

  • a is declared just after the ( character, so the interpreter destroys it upon reaching the ) character.
  • c is declared just after the { character, so the interpreter destroys it upon reaching the } character.
  • b is not declared in this program, so it refers to the automatically-declared variable with that name. The interpreter does not destroy it until it is restarted or stopped. This means that the Function assigned to b is still available after the program finishes execution. Try it! Execute the program above, and then execute this single-line program alone: b.value;

Object-Oriented SuperCollider

SuperCollider is difficult to describe precisely, because its syntax allows great flexibility. There are many different ways to accomplish the same task. Each one is subtly different, and gives you a different set of possibilities, but there is often no "best solution." One of the advantages to this is that it easily allows three "programming paradigms," although one is used much more often than the others.

Imperative Programming

Imperative programming is easy to understand: it is simply a list of commands, like this:

(
   var a, b, c;

   a = 12;
   b = 25;
   c = a + b;
   a.postln;
)

Declare the variables, set the variables, do a calculation, and print the result of the calculation. This is a simple example, and a simple model, but it is very difficult to escape completely. After all, humans think of large problems in terms of algorithms (the instructions needed to do something). Computers solve large problems, so being able to program them with a series of instructions makes sense.

Functional Programming

Functional programming is also easy to understand, but it can be a little bit more difficult to think about complex tasks. Functional programs use Functions to complete all of their work. This is not strictly possible in SuperCollider: it is more imperative than functional, but the creative use of Functions can easily solve some problems that are difficult to write with an imperative approach.

The following example is an extension of the "Imperative" example. Pretend that the following Functions exist, and do the following tasks:

  • getinput : allows the user to enter a number, and returns that number
  • add : adds together the numbers given as arguments, returning the sum
(
   postln( add( getinput, getinput ) );
)

SuperCollider will always execute the inner-most Functions first. This is how the interpreter executes the single-line program above:

  1. Execute the left call of getinput
  2. Execute the right call of getinput
  3. Execute add with the two numbers returned by getinput
  4. Execute postln with the number returned by add

Both imperative and functional programming have advantages and disadvantages. SuperCollider will allow you to use either approach, or a mix of both, when solving problems.

Object-oriented Programming

Object-oriented programming is more difficult to think about than imperative or functional. When using this paradigm (mode of thought), almost everything in SuperCollider is thought of as an abstract Object. In this way, it allows programmers to make compelling comparisons to the real world, where all tangible things are objects, and where it is not hard to conceive of most intangible things as objects, too. With object-oriented programming, computer science takes a break from mathematics, and is influenced by philosophy.

!!! Include that Classes always start with an upper-case letter !!!

Anything can be represented as an Object - like a bicycle, for instance. Let's pretend that we have an Object called a Bicycle. We don't yet have a particular bicycle - just the abstract class containing everything that is true about all bicycles. If a Bicycle class exists in SuperCollider, you generate a specific instance like this: var bike = Bicycle.new; All SuperCollider Objects can be instantiated in this way: you get a specific Bicycle from the generic class, and you can then modify and work with your own Object as you choose. The specific properties associated with a particular instance of a class are called instance variables.

There are certain things that Bicycles are designed to do: turn the wheels, turn the handlebar, raise and lower the seat, and so on. You can cause these things to happen by providing a certain input to a real-world Bicycle: if you want to turn the wheels, you might push the pedals. In SuperCollider, you cause things to happen by sending a message to the Object that tells it what you want: if you have a Bicycle, you might turn the wheels like this: bike.turnTheWheels; When you do this, you are actually executing the turnTheWheels Function, which is defined by the abstract Bicycle class. Because it doesn't make sense to turn the wheels of all bicycles in existence, you don't call the method (a synonym for "Function") from the Bicycle class itself, but from the particular instance whose wheels you want to turn. The proper way to access instance variables is by using instance methods.

If this kind of programming is new to you, it might seem extremely difficult. It can be intimidating at first, but it is actually not too difficult to understand once you start to use it. In fact, you have already been using it! Remember the postln command that was described earlier as a special kind of Function? It's actually a Function defined by SuperCollider's abstract class Object, which defines a set of messages that can be passed to any SuperCollider Object. Because most things in SuperCollider are Objects, we can send them the postln message, and they will understand that it means to print themselves in the "SuperCollider output" pane.

Why is it that all Objects respond to the postln message? SuperCollider classes are allowed to belong to other SuperCollider classes, of which they are a part. Consider the Bicycle class again. It is a kind of vehicle, and philosophers might say that "things that are members of the bicycle class are also members of the vehicle class." That is, real-world bicycles share certain characteristics with other real-world objects that are classified as "vehicles." The bicycle class is a "sub-class" of the vehicle class, and it inherits certain properties from the vehicles class. SuperCollider allows this behaviour too, and calls it inheritance. In SuperCollider, since all classes define Objects, they are all automatically considered to be a sub-class of the class called Object. All classes therefore inherit certain characteristics from the Object class, like knowing how to respond to the postln message.

equivalent notation: 5.postln versus postln( 5 )

You still don't know how to write new Classes and Objects in SuperCollider, but knowing how to use them is more than enough for now. By the time you need to write your own Classes, you will probably prefer to use the official SuperCollider help files, anyway.

Choosing a Paradigm

At this point you may begin worrying about which programming paradigm you should choose, and when. The answer is unhelpful: "Whichever seems best for the task."

Let's expand on this. If you are primarily a programmer, then you probably already know how to choose the best paradigm and algorithm for the task. If you are a musician, then you probably just want your program to produce the output that you want (in this case, a particular set of sounds). Part of the beauty of SuperCollider's flexibility is that it allows you to produce the same output in different ways. As a musician this means that, as long as your program works as you want it to work, it doesn't matter how you write it. Experience will teach you more and less effective ways of doing things, but there is no need for rules.

Even so, here are some guidelines that will help you to start thinking about music programs:

  1. Programs of all sorts often follow a simple, four-step flow. Not all parts are always present.
    1. Declare variables.
    2. Get input from the user.
    3. Calculate something with the input.
    4. Provide output to the user (i.e. "make noise").
  2. Repetition is the enemy of correctness, so if you're going to execute some code more than once, try writing a Function.
    1. If it's slightly different every time, try using arguments. Arguments with default values are a great way to expand a Function's usefulness.
  3. If it looks too complicated, then it probably is. The more difficult it is to understand something, the greater the chance of making a mistake.
  4. Don't forget the semicolons at the end of every statement.

Sound-Making Functions

It's finally time to start thinking about Functions that produce sound!

This example is discussed below in the following sections. Remember, when running SuperCollider code in GEdit, you can stop the sound by pressing 'Esc' on your keyboard.

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

UGens

"UGen" stands for "unit generator." UGens are special Objects that generate either an audio or a control signal.

The UGen that will be used for most of the experimentation in this Guide, and which was primarily used to generate the "Method One" program that goes with this Guide, is called SinOsc, which generates a sine wave. The Class' name, "SinOsc," means "sine oscillator."

The example at the beginning of this chapter, SinOsc.ar( 440, 0, 0.2 ); produces an "instance" of the SinOsc Class, which continuously outputs a signal, based on the parameters given in parentheses. This instance produces an "audio rate" signal, which means that it is of sufficient quality to eventually become sound.

A slightly modified version of that code will give us a "control rate" signal: SinOsc.kr( 440, 0, 0.2 ); There is only one small difference between the two examples - for us - but for SuperCollider, the difference is huge. A control rate signal will not be of sufficient quality to become sound; it is used to control other UGens that do become sound.

Unlike other Classes, UGen Classes should not be instantiated with the new message. They should always be instantiated as either audio-rate (by passing the ar message), or control-rate (by passing the kr message). Control-rate signals are calculated much less often than audio-rate signals, which allows the SuperCollider interpreter and server to save processing power where it wouldn't be noticed.

The "play" Function

The play Function does exactly what it says: it plays its input. The input must be a Function with an audio-rate signal generator as the return value.

The following two examples produce the same output:

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

The first example is written from an object-oriented perspective. Functions know how to play their return value, when passed the play message. This is true of all Functions whose return value is an audio-rate UGen. The second example is written from a functional perspective. The Function called play will play its input, which must be a Function whose return value is an audio-rate UGen. Whether you should write play in the functional or object-oriented way depends on which makes more sense to you.

Try to re-write the above example so that the play Function operates on a variable-defined Function.

The arguments to SinOsc, whether the audio- or control-rate generator, are these:

  • The first is called freq; it sets the frequency.
  • The second is called add; it is added to all values produced by the UGen.
  • The third is called mul; all values produced by the UGen are multiplied by this.

You now know enough to spend hours with the sine oscillator UGen. Try combining audio- and control-rate UGens, and try to figure out what happens when each of the arguments is adjusted. Be careful that your audio interface's volume isn't set too high! Experiment with this one:

(
   var myFrequency =  SinOsc.kr( freq:1, mul:200, add:400 );
   var sound = { SinOsc.ar( myFrequency, 0, 0.2 ); };
   
   play( sound );
)

WHY DOESN'T THAT WORK?!?!?!?!?!?

Multichannel Audio

By now you must be growing tired of the left-side-only sounds being produced by the examples.

Stereo Array

The easiest way to output multichannel audio in SuperCollider is to use a kind of "Collection" (defined later) called an "Array." SuperCollider will theoretically handle any number of audio output channels, but by default is usually only configured for two-channel stereo audio. Since humans have only two ears, this is sufficient for most tasks! A multichannel array is notated like this: [ LeftChannel.ar( x ), RightChannel.ar( y ) ]

Here is our simple sine oscillator expanded to produce stereo audio:

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

Not much has changed, except that the audio we hear is now being emitted from both the left and right channels. Change the frequency of one of the sine oscillators to 450 and the difference will become much more apparent.

Multichannel arrays can also be combined with each other, like this:

{
   var one = [ x, y, z ];
   var two = [ a, b, c ];
   [ one, two ];
}

If a, b, c, x, y, and z were all audio-rate UGens, this Function could be play'ed. It would produce stereo audio, and each channel would have three independent UGens.

Multichannel Expansion

You can automatically create multiple UGens by providing an Array as one of the parameters. The SuperCollider interpreter will automatically create multichannel UGens as a result.

The following two examples produce equivalent output:

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

The second example can be easier to read, because it is obvious that only the frequency is changing - or in this case, that nothing is changing. This technique is more useful in a situation like the following:

{ SinOsc.ar( [[440, 445, 450, 455, 460, 465],
              [440, 445, 450, 455, 460, 465]],
             0,
             0.2 ); }.play;

That's not exactly easy to read, but it's easier to figure out than the most obvious alternative:

{
   [[ SinOsc.ar( 440, 0, 0.2 ), SinOsc.ar( 445, 0, 0.2 ), SinOsc.ar( 450, 0, 0.2 ), SinOsc.ar( 455, 0, 0.2 ), SinOsc.ar( 460, 0, 0.2 ), SinOsc.ar( 465, 0, 0.2 ) ],
    [ SinOsc.ar( 440, 0, 0.2 ), SinOsc.ar( 445, 0, 0.2 ), SinOsc.ar( 450, 0, 0.2 ), SinOsc.ar( 455, 0, 0.2 ), SinOsc.ar( 460, 0, 0.2 ), SinOsc.ar( 465, 0, 0.2 ) ]];
}.play;

More importantly, multichannel expansion gives us another tool to avoid repetition. Repetition is the enemy of correctness - it's so much more difficult to find a mistake in the second example than in the first!

Method One

Believe it or not, you now know enough to understand a slightly-modified version of the first part of "Method One," a SuperCollider program written and heavily commented specifically for use with this Guide. You should play this example, and experiment with changing the frequencies, volumes, and so on. The fully-commented version provides a full explanation of how the Function works.

{
   // sets up the frequencies of both channels
   var frequencyL = SinOsc.kr( freq:10, mul:200, add:400 ); // oscillating
   var frequencyR = SinOsc.kr( freq:1, mul:50, add:150 ); // oscillating
   var frequencyL_drone = SinOsc.kr( freq:0.03, mul:20, add:100 ); // drone
   var frequencyR_drone = SinOsc.kr( freq:0.01, mul:20, add:210 ); // drone
   
   // changes the volume of the oscillating part in the left channel
   var volumeL = SinOsc.kr( freq:0.5, mul:0.02, add:0.03 );
   
   // left channel
   var left = [ SinOsc.ar( freq:frequencyL, mul:volumeL ), // this is the oscillating part
                SinOsc.ar( freq:[frequencyL_drone,2*frequencyL_drone], mul:0.02 ), // the rest make up the drone
                SinOsc.ar( freq:[5*frequencyL_drone,7*frequencyL_drone], mul:0.005 ),
                SinOsc.ar( freq:[13*frequencyL_drone,28*frequencyL_drone], mul:0.001 ) ];
   
   // right channel
   var right = [ SinOsc.ar( freq:frequencyR, mul:0.1 ), // this is the oscillating part
                 SinOsc.ar( freq:[frequencyR_drone,2*frequencyR_drone], mul:0.02 ), // the rest make up the drone
                 SinOsc.ar( freq:4*frequencyR_drone, mul:0.005 ),
                 SinOsc.ar( freq:[64*frequencyR_drone,128*frequencyR_drone], mul:0.01 ) ]; // high frequencies!
   
   [ left, right ];
}

Collections

A "collection" is just that - a collection of Objects. Collections are simply a means of organizing a large amount of data, without having to assign a variable name for each portion of data. Compared to other programming languages, SuperCollider provides a relatively large number of Collections in the standard library.

We have already seen an example of a Collection as multichannel audio arrays. An Array is a kind of Collection - in object-oriented terminology, the Array Class is a sub-class of the Collection Class, and inherits its behaviours. Conversely, the Collection Class is the super-class of the Array Class. The Collection Class itself is not to be used; it is designed to provide common features so that it is easier to write Classes for collections.

As with all the chapters from this point on, it is not necessary to read this in sequence. If you prefer, you can skip it and return later when you need to manage a large set of data.

Array

!! MAYBE I DON'T NEED THIS SECTION !!

Arrays have been traditionally been very popular with programmers. In SuperCollider, they are capable of storing a large number of Objects, and they provide advanced behaviours that are normally not associated with Arrays. They are not as indespensible as they used to be. Most programming languages now provide (or can easily be extended to add) Lists, Trees, and other kinds of data storage structures, which offer more capabilities, and are easier to use and to think about. Users new to programming might find the various kinds of Lists to be more helpful.

Building an Array

An Array is a Collection with a finite maximum size, determined at declaration time. It is the programmer's responsibility to maintain a meaningful order, and to remember the meaning of the data. Data in an Array is called "elements," each of which is assigned a specific "index number." Index numbers begin at 0. Any mix of Objects can be stored in an Array, including an Array.

This example declares an Array, adds some elements, then prints them out.

(
   var tA = Array.new( 2 ); // "tA" stands for "testArray"
   
   tA = tA.add( 5 );
   tA = tA.add( 3 );
   tA = tA.add( 17 );
   
   tA.postln;
   nil;
)

Notice that Array is a Class, and it must be instantiated before use. Here, the variable tA is assigned an Array with enough space for two objects. Notice that the elements are printed out in the order that you add them to the Array. They are not sorted or shuffled (unless you send a message like scramble). But why did I write tA = tA.add( 17 ); instead of tA.add( 17 );? Shouldn't the second method be sufficient for adding an Object to an Array, thereby making the re-assignment unnecessary? It does, but let's see what happens when we take it away:

(
   var tA = Array.new( 2 ); // "tA" stands for "testArray"
   
   tA.add( 5 );
   tA.add( 3 );
   tA.add( 17 );
   
   tA.postln;
   nil;
)

The 17 is missing - it doesn't get added into the Array! This is because the Array was only declared with two slots, and you can't add three Objects into two slots. So why did this work the first time? SuperCollider was programmed to help us fit additional items into an Array. If an Array has reached its capacity, SuperCollider will automatically make a new, larger Array for us, and returns that from the add method. Therefore, any time you add an element to an Array, you should always re-assign the result, so that you don't have to worry about whether you exceeded the Array's capacity.

Accessing an Array's Elements

There are two ways to access individual elements within an Array. One way is object-oriented, and one way is more traditional, inspired by programming languages such as the wildly popular "C" language. The object-oriented style uses the at and put methods. The traditional style uses square brackets with an index number.

The following examples produce equivalent output. The first uses the object-oriented style, and the second uses the traditional style.

(
   var tA = Array.new( 3 );
   
   tA = tA.add( 5 );
   tA = tA.add( 3 );
   tA = tA.add( 17 );
   
   tA.at( 0 ).postln; // outputs 5
   tA.at( 1 ).postln; // outputs 3
   tA.at( 2 ).postln; // outputs 17
   
   tA.put( 0, 24 ); // assigns 24 to element 0
   
   tA.at( 0 ).postln; // outputs 24
   
   nil;
)
(
   var tA = Array.new( 3 );
   
   tA = tA.add( 5 );
   tA = tA.add( 3 );
   tA = tA.add( 17 );
   
   tA[0].postln; // outputs 5
   tA[1].postln; // outputs 3
   tA[2].postln; // outputs 17
   
   tA[0] = 24 ; // assigns 24 to element 0
   
   tA[0].postln; // outputs 24
   
   nil;
)

Different people prefer different styles of accessing Arrays.

List

An List is a Collection with an infinite maximum size. It is the programmer's responsibility to maintain a meaningful order, and to remember the meaning of the data. Data in a List is called "elements," each of which is assigned a specific "index number." Index numbers begin at 0. Any mix of Objects can be stored in a List, including a List. Lists and Arrays are very similar, but SuperCollider manages some of the dirty work for you, when you use the List Class.

Building a List

There are four methods which instantiate a List. These are all "Class methods," meaning they do not operate on a specific List, but can be used to make any List.

  • List.new creates a List. You can also specify the initial number of elements as an argument, if you choose.
  • List.newClear( x ) creates a List with x number of slots, filled with nil.
  • List.copyInstance( aList ) creates a List which is a copy of aList.
  • List.newUsing( anArray ) creates a List with the same elements as anArray.

Adding to an Existing List

These are "instance methods," meaning that they operate on a specific list.

  • put( index, item ) adds "item" into the List at index number "index".
  • add( item ) adds "item" to the end of a List.
  • addFirst( item ) adds "item" to the beginning of a List.

Accessing an Existing List

These are "instance methods," meaning that they operate on a specific list.

  • at( index ) returns the Object assigned to the "index" index number. If "index" is greater than the last element in the List, returns "nil".
  • clipAt( index ) returns the Object assigned to the "index" index number. If "index" is greater than the last element in the List, returns the last element in the List.
  • wrapAt( index ) returns the Object assigned to the "index" index number. If "index" is greater than the last element in the List, returns an element based on a "wrap-around" index number. For a three-element List, 0 will return element 0, 1 returns 1, 2 returns 2, 3 returns 0, 4 returns 1, 5 returns 2, 6 returns 0, and so on.
  • foldAt( index ) returns the Object assigned to the "index" index number. If "index" is greater than the last element in the List, returns an element based on a "fold-back" index number. Whereas wrapAt() will always continue from the lowest to the highest index number, foldAt() will change every time: low to high, high to low, low to high, and so on.

Removing from a List

One way to remove an element from a List is to re-assign that element's index number the value "nil". These two Functions also remove elements from a List. They are "instance methods," meaning that they operate on a specific list.

  • pop returns the last element in a List, and removes it from the List.
  • removeAt( index ) removes the element assigned to "index" index number, removing it from the List and shrinking the List. This will not leave a "nil" element in the List.

Examples

The following examples

LinkedList

OrderedList

Repetition

The Mixer??

How to Get Help

SynthDefs and Synths

Busses

Groups, Nodes, and Ordering

Buffers??

Scheduling and Clocks

Routines and Tasks

Patterns