Monday, May 31, 2010

Arrays

As you've heard many times, a list surrounded by square brackets is an Array. Specifically, it's a comma delineated list of 0 or more elements. Here are some examples:

[1]
[x, y, z]
["in", "the", "house"]
[]

There is often more than one way to do something in SuperCollider, as in life. We can also create an Array by using a constructor. The constructor takes one argument, the size of the array.

a = Array.new(size);

Arrays can hold any kind of object, including numbers, variables, strings or nothing at all. They can even mix types:

[3, "French hens", 2 "turtledoves", 1 "partridge"]

Arrays can even hold other arrays:

[[1, 2, 3], [4, 5, 6]]

You can also put an expression in an array. The interpreter evaluates it and stores the results. So [ 3 - 1, 4 + 3, 2 * 6] is also a valid array, stored as [2, 7, 12]. [x.foo(2), x.bar(3)] is also an array. It passes those messages to the objects and puts the result into the array. Because commas have extremely low precedence, they get evaluated after the expressions they separate.

A variable can be part of an expression that is put into an array:

(
 var foo, bar;
 ...
 [foo + 1 , bar];
)

In that last case, it holds the value of the expressions (including the variables) at the time they became part of the array. For example:

(
 var foo, arr;
 foo = 2;
 arr = [foo];
 arr.postln;
 foo. postln;

 " ".postln;

 foo = foo + 1;
 arr.postln;
 foo. postln;
)

Outputs:

[ 2 ]
2

[ 2 ]
3
3

This is almost just what we expected. From whence did the second 3 at the bottom come? SuperCollider's interpreter prints out a return value for every code block that it runs. bar was the last object in the code block, so bar gets returned. bar's value is 3.

The variables can go on to change, but the array holds the value that was put in, like a snapshot of when the expression was evaluated.

What if we declare a five element array and want to add something to the array? We use the Array.add message.

(
 var arr, new_arr;
 arr = ["Mary", "had", "a", "little"];
 new_arr = arr.add("lamb");
 arr.postln;
 new_arr.postln;
)

Outputs:

[ Mary, had, a, little ]
[ Mary, had, a, little, lamb ]

Arrays cannot grow in size once they've been created. So, as stated in the helpfile, “the 'add' method may or may not return the same Array object. It will add the argument to the receiver if there is space, otherwise it returns a new Array object with the argument added.” Therefore, when you add something to an array, you need to assign the result to a variable. arr doesn’t change in the example because it is already full.

There are other messages you can send to Arrays, which are detailed in the Array helpfile and the helpfile for its superclass ArrayedCollection. Two of my favorite are scramble and choose.

The helpfile says that scramble "returns a new Array whose elements have been scrambled. The receiver is unchanged."

The results of scramble are different each time you run it, because it scrambles in random order. When it says, "the receiver is unchanged", it means that if we want to save the scrambled Array, we have to assign that output to a new variable. The receiver is the object that receives the message "scramble." In the following example, the receiver is arr, which contains [1, 2, 3, 4, 5, 6].

(
 var arr, scrambled;
 arr = [1, 2, 3, 4, 5, 6];
 scrambled = arr.scramble;
 arr.postln;
 scrambled.postln;
)

For me, this output:

[ 1, 2, 3, 4, 5, 6 ]
[ 4, 1, 2, 3, 6, 5 ]

Of course, the second array is different every time. But arr is always unchanged.

Choose is similar. It picks a random element of the Array and outputs it. The receiver is unchanged.

[1, 2, 3].choose.postln;

Will output 1, 2 or 3 when you run it.

Arrays are lists, but they are not merely lists. They are indexed lists. You can ask what the value of an item in an array is at a particular position.

(
 var arr;
 arr = ["Mary", "had", "a", "little"];
 
 arr.at(1).postln;
 arr.at(3).postln;
)

Outputs:

had
little

Array indexes in SuperCollider start with 0. In the above example, arr.at(0) is "Mary".

You can also put the index number in square brackets. arr[0] is the same as arr.at(0). If you are using square brackets, you can also modify the contents of the array:

(
 var arr;
 arr = ["Mary", "had", "a", "little", "lamb"];
 
 arr[4] = "giraffe";
 arr.postln;
)

Arrays also understand the message do, but treat it a bit differently than an Integer does.

(
 [3, 4, 5].do ({ arg item, index;
  ("index: " ++ index ++ " item: " ++ item).postln;
 });
)

Outputs:

index: 0 item: 3
index: 1 item: 4
index: 2 item: 5

We've called our arguments in this example item and index, but they can have any name we want. It doesn't matter what we call them. The first one always gets the value of the array item that the loop is on and the second one always gets the index.

The ++ means concatenate, by the way. You use it to add something to the end of a string or an array. For example:

"foo " ++ 3 returns the string "foo 3"

With arrays, you can use it to add a single item or another array:

(
 var arr1, arr2, arr3;
 
 arr1 = [1, 2, 3];
 arr2 = arr1 ++ 4;
 arr3 = arr2 ++ [5, 6];
 
 arr1.postln;
 arr2.postln;
 arr3.postln;
)

Outputs

[ 1, 2, 3 ]
[ 1, 2, 3, 4 ]
[ 1, 2, 3, 4, 5, 6 ]

Note that the receiver is unchanged.

Size is a message which gives us the size of the array. This can be useful, for example, if we have a variable that we want to use as an index, but which might get bigger than the array:

arr.at(index % arr.size);

Remember that the modulus (%) gives us a remainder. This means that if the count gets to be greater than the number of elements in the array, using a modulus will cause it to wrap around to zero when it exceeds the size of the array. There is also a message you can send to arrays that does this for you:

arr.wrapAt(index);

It does the modulus for you.

Musical Example

In a previous post, I mentioned Nicole, the ex-grad student working at a SuperCollider startup. Since then, she's gotten another assignment from her boss. She has to write a function that takes as arguments: an array of tuning ratios, a base frequency and a detuning amount. It has to figure out the final pitches by first multiplying the base frequency by the ratio and then adding the detuning amount to the result. It should then play out the resulting scale.

She's created SynthDef:

(
 SynthDef(\example8, {|out = 0, freq, amp, dur, pan = 0|
  
  var pm, modulator, env, panner;
  
  modulator = SinOsc.ar(50, 0, 0.2);
  pm = SinOsc.ar(freq, modulator);
  
  env = EnvGen.kr(Env.perc(0.01, dur, amp), doneAction:2);
  panner = Pan2.ar(pm, pan, env);
  
  Out.ar(out, panner);
 }).store
)

Since she's been working in the field, she's learned that instead of writing "arg" followed by a list or arguments and then a semicolon, she can just put all her arguments between two vertical bars. The two versions are exactly identical.

In the synthdef, one of the SinOscs is modulating the phase of the other SinOsc.

After learning about Arrays, our hero does a bit of research on tuning ratios and comes up with an array of ratios that she will use to test her function. It looks like: [ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ]

That is 1 divided by 1, 3 divided by 2, four divided by 3, etc. Now remember that precedence means that the interpreter evaluates things in a particular order. It looks at / before it looks at commas. So it seems a bunch of /'s and starts dividing. Then it looks at the commas and treats it as an array. The interpreter stores that array as:

[ 1, 1.5, 1.3333333333333, 1.125, 1.7777777777778, 1.25, 1.6 ]

Nicole's function will contain a task which cycles through the ratios, taking each one and multiplying it by the base frequency and adding the detuning amount. Remember that SuperCollider is like a cheap calculator and +, -, *, and / all have the same precedence. Math is evaluated from left to right. So ratio * baseFreq + detune is not equivalent to detune + ratio * baseFreq, like it would be in algebra. However, fortunately, she can use parenthesis like we would in algebra.

She could write out her expression as (ratio * baseFreq) + detune or (detune + (ratio * baseFreq)) or in many other ways. Even though she could get the right answer without using parenthesis at all, it's good programming practice to use them.

Nicole has a formula and she has an Array. She just needs a way to step through it. Fortunately, she knows that the 'do' method also exists for Arrays.

She writes her code as follows:


(

  var func, arr;
  
  func = { |ratio_arr, baseFreq = 440, detune = 10, dur  = 0.2|
   Task({
   var freq;

   ratio_arr.do({ |ratio, index|
    freq =  (ratio * baseFreq) + detune;
    Synth(\example8, [\out, 0, \freq, freq, \amp, 0.2, \dur, dur,
       \pan, 0]);
    dur.wait;
   });
  });
 };
 
 arr = [ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5];
 
 func.value(arr, 440, 10).play;
)

This is pretty cool, but it always plays out in the same order, so she changes her do loop to: ratio_arr.scramble.do({ |ratio, index|

Summary

  • You can declare an array by putting a list in square brackets or by using the constructor Array(size)
  • Arrays can hold any types of objects, all mixed together
  • You can put an expression into an array. It will hold the result of evaluating the expression.
  • You can add items to an array by using the add message
  • The scramble message returns a new array which has the same contents as the receiver, but in a random order
  • The choose message will return a single element of the receiver, chosen at random
  • You can access elements of an array with their index: arr.at(index) or arr[index]
  • You can modify a single element of an array with the index: arr[index] = foo
  • You can loop through an array with do: arr.do({|item, index|
  • ++ can be used to concatenate strings or arrays
  • arr.size returns the size of the array
  • In order to make sure your index is not larger than the size of the array, you can use modulus or wrapAt: arr[index % arr.size] or arr.wrapAt(index)
  • arguments can be listed inside vertical bars instead of after the word arg: |arg1, arg2| is the same as arg arg1, arg2;

Problems

  1. You can navigate arrays of different length using variables for indexes.
  2. (
    
     var arr1, arr2, index;
    
     arr1 = [1, 2, 3];
     arr2 = [3, 4, 5, 6];
     index = 0;
    
     2.do({
      arr1.do({ arg item;
       ("arr1 " ++ item).postln;
       ("\t arr2 " ++ arr2.wrapAt(index)).postln; //"\t" means tab 
       index = index + 1;
      });
     });
    )
    
    Use this idea to create duration, pitch and wait loops of different lengths. Write a task to pay them
  3. In the previous post, there was an example that used if statements to control adding to or subtracting from the frequency. Instead of changing the frequency directly, change an index that will access an array of tuning ratios.

Sunday, May 30, 2010

While

We’ve used Boolean expressions to control the flow of execution of a program with if. Another control structure is while. While is a message passed to a function. The function must return either true or false. It is a test function. There is another function passed in as an argument. test_function.while(loop_function); If the test function is true, the loop function gets run. Then the test function is run again. If it returns true again, the loop function is run again. This continues until the test function returns false. While the condition is true, the loop is executed.

(
 var counter;
 
 counter = 0;
 
 {counter < 6}.while ({
  
  counter = counter + 3.rand;
  counter.postln;
  
 })
)

{counter < 6} is the test function. If it is true, we run the next two lines inside the while loop.

counter = counter + 3.rand; adds the result of 3.rand to the variable counter and then stores the result of that in the variable counter. Recall that in assignment statements (statements where variables get a value), there can only be one thing on the left of the equals sign. Everything to the right is evaluated and then result of that evaluation is assigned to the variable. It's ok to have the same variable on both sides of the equal sign. The value of the variable does not change until everything has already been evaluated.

If instead of counter = 0; we had counter = 7; the test function would have run once, but the loop function would not have run at all.

As with if, functional notation is also common with while. Remember that both notations are entirely equivalent. The following two lines are the same:

test_func.while(loop_func);
while(test_func, loop_func);

Let's have a musical example, where we track elapsed time rather than the number of notes played. First, we need a synthdef:

(
 SynthDef(\example7, {arg out = 0, freq = 440, dur = 1, amp = 0.2, pan = 0;
  
  var env, form, panner;
  
  env = EnvGen.kr(Env.perc(0.01, dur, amp), doneAction:2);
  form = Formant.ar(freq, freq + 100, freq + 245);
  panner = Pan2.ar(form, pan, env);
  
  Out.ar(out, panner);
 }).load(s);
)

Formant is an oscillator that creates formants. You can find out more about it in it's helpfile.

Pan2 is a two channel panner which takes three arguments: the signal, the position and the amplitude. The position can vary from -1 to 1 and 0 is in the center.

Now, our Task:


(
 Task({
 
  var freq, dur, total_dur, count; 
  
  dur = 0.17;
  total_dur = 0;
  count = 0;
  
  {total_dur < 20}.while({
  
   freq = 400;
   
   (count % 3 == 0).if({   
    freq = freq + 200;
   });
   (count % 4 == 0).if({   
    freq = freq - 100;
   });
   (count % 5 == 0).if({   
    freq = freq + 400;
   });
   
   Synth(\example7, [\out, 0, \freq, freq, \dur, dur, \amp, 0.2, \pan, 0.7.rand2]);
   
   count = count + 1;
   total_dur = total_dur + dur;
   dur.wait;
  });
 }).play
)

(This code is translated to SuperCollider from a tutorial for making music with Perl.)

At the start, we declare our variables and then we initialise them. This is an important step, because before we assign them a value, they start out with the value nil. Nil is not meaningful with a <, nor with any other mathematical or boolean operation.

In our test function, we see if the elapsed duration is less than 20 seconds.

Then in our loop, we add and subtract from a given frequency based on the divisors of a counter.

rand2 is a message that can be passed to numbers. It returns a random result between (-1 * this) and this. So 0.7.rand returns a number between -0.7 and 0.7. This will cause our sound to random pan from left of center to right of center.

After we play the synth, we update all our variables. If we forget to add the dur to the total_dur, the test function will always return true and we will never leave the loop. I sometimes forget this, as it's a common bug.

The next example is very similar to one from the previous post.

(
 SynthDef("example7b", {arg out = 0, freq = 440, amp = 0.1,
       dur = 1, pan = 0;

     var blip, env, panner;

     env = EnvGen.kr(Env.triangle(dur, 1), doneAction:2);
     blip = Blip.ar(freq, env * 3);
     panner = Pan2.ar(blip, pan, env * amp);
     Out.ar(out, panner);
  }).load(s);
)


(
  Task({
     var freq, synth_dur, high_freq;
  
     high_freq = 800;
  
     while({high_freq > 0}, {
  
        freq = 200 + high_freq.rand;
        if((freq >= 600), {
          synth_dur = 0.1 + 1.0.rand;
        } , {
          if ((freq <= 400), {
           synth_dur = 1 + 5.0.rand;
          },{
           synth_dur = 0.1+ 2.0.rand;
          });
        });
        Synth(\example7b, [\out, 0, \amp, 0.1, \freq, freq,
            \dur, synth_dur, \pan, 0.8.rand2]);
        
        high_freq = high_freq - 50.rand;
        (0.01 + 0.4.rand).wait;
     });
  }).play;
)

Because we keep subtracting from high_freq, the highest possible frequency is tending downwards. We keep subtracting until it gets to zero. Incidentally, changing allowable ranges of parameters over time is called tendency masking.

There are other Control Structures detailed in a help file called Control-Structures. Highlight "Control-Structures" and press shift-d

Summary

  • While is a control structure that controls a loop
  • The test condition for while is a function which returns a boolean. While is a message passed to that function. It takes a function as an argument.

Problems

  • It is allowable to have two or more tasks going at the same time. Write a programme that has two tasks, one playing notes tending upwards ans the other downwards.
    • Make it such that when one while stops, the other does also. You may find it helpful if there is a variable that they both can access.

Saturday, May 29, 2010

Boolean Expressions

Last time, we learned to randomly pick true or false values, however, it would be more useful if we could test to see what's going on and make decisions according to those tests. Fortunately, we can do that with boolean expressions.

An expression is a bit of code that can be evaluated. 2+3 is an expression, who's value is 5. A boolean expression is an expression that results in a boolean. Boolean expressions are usually tests. For example, we can test for equivalency with ==

(
 a = 3;
 if (a == 3, {
  "true".postln;
 }, {
  "false".postln;
 });
)

Note that is two equal signs next to each other when we're testing for equivalency. If you just use one equal sign, it means assignment. I often accidentally type one equals sign when I mean to type two.

We can test for greater than or less than:

(
 a = 3;
 if (a > 4, {
  "true".postln;
 }, {
  "false".postln;
 });
)
(
 a = 3;
 if (a < 4, {
  "true".postln;
 }, {
  "false".postln;
 });
)

We can also test for greater than or equals to and less than or equals to:

(
 a = 4;
 if (a >= 4, {
  "true".postln;
 }, {
  "false".postln;
 });
)
(
 a = 3;
 if (a <= 4, {
  "true".postln;
 }, {
  "false".postln;
 });
)

We can also do boolean operations. Some of the most important ones are not, and, and or.

The easiest way to illustrate these is with truth tables. A truth table shows you all possible combinations of true and false variables and what the results would be. Any Boolean variable can be either true or false. This is a truth table for not:

not
truefalse
falsetrue

Not is a unary operator. That means it only involves one object. The top of the table shows a true input and a false input. The bottom of the table shows the result. true.not returns false and false.not returns true.

And is a binary operator. Like +, -, *, / and %, it operates on two objects. Lets’ say we have two variables, a and b, and either of them can be true or false. We can put a along the top of the table and b down the left side. In the middle we put the possible results of a and b.

and
truefalse
truetruefalse
falsefalsefalse

Or is also binary:

or
truefalse
truetruetrue
falsetruefalse

So how do we code these? Let’s look again at not. Not can be a message sent to a boolean or boolean expression:

(
 if ((a==4).not , {
  "true".postln;
 }, {
  "false".postln;
 });
)

You can combine not and equivalency with not equals: !=

(
 a = 2;
 
 if (a != 4 , {
  "true".postln;
 }, {
  "false".postln;
 });
)

The last two examples are the same.

And is represented by &&

(
 a = 3;
 b = 4;
 
 if ( (a > 2) && (b < 5), {
  "true".postln;
 }, {
  "false".postln;
 });
)

Both (a > 2) and (b < 5) must be true for this expression to evaluate as true. If one of them is false, the whole thing is false.

Or is represented by || (Those are vertical lines. If you have a Macintosh with an American keyboard, they're over the slash \)

(
 a = 3;
 b = 4;
 
 if ( (a > 2) || (b < 5), {
  "true".postln;
 }, {
  "false".postln;
 });
)

(
 a = 3;
 b = 4;
 
 if ( (a < 2) || (b < 5), {
  "true".postln;
 }, {
  "false".postln;
 });
)

(
 a = 3;
 b = 4;
 
 if ( (a > 2) || (b == 5), {
  "true".postln;
 }, {
  "false".postln;
 });
)

For these expressions to evaluate to true, only one part of it needs to be true. If neither part of an or is true, then the whole thing is false.

Musical Example

Let's have a musical example. We'll again generate random pitches, but low pitches will have long durations and high ones will be short. First, we need a SynthDef:

(
  SynthDef("example6", {arg out = 0, freq = 440, amp = 0.1,
       dur = 1;

     var blip, env_gen, env;

     env = Env.triangle(dur, 1);
     env_gen = EnvGen.kr(env, doneAction:2);
     blip = Blip.ar(freq, env_gen * 3, env_gen * amp);
     Out.ar(out, blip);
  }).load(s);
)

We're trying out a new oscillator called Blip. The helpfile for it says:

Blip.ar(freq, numharm, mul, add)

Band Limited ImPulse generator. All harmonics have equal amplitude. This is the equivalent of 'buzz' in MusicN languages.

WARNING: This waveform in its raw form could be damaging to your ears at high amplitudes or for long periods.

The first argument is the frequency. The second is the number of harmonics, which is to say the number of overtones and the third is amplitude. Loud tones with lots of harmonics can hurt our ears! So let's watch out. I suspect anyhting that's bad for ears is probably also bad for speakers.

We're going to control the number of harmonics with the envelope, so they change over time. We're using the same envelope for amplitude as we are for harmonics, so env_gen * 3 gives us a maximum of 3 harmoics at the peak. If you want more or less, you can change that number. And, of course, env_gen * amp gives us an envelope that goes from 0 to the amp passed in as an argument.

Ok, now for the Task that will play this


(
  Task({
    var freq, synth_dur;
    50.do({
      freq = 200 + 800.rand;
      if((freq >= 600), {
       synth_dur = 0.1 + 1.0.rand;
      } , {
       if ((freq <= 400), {
        synth_dur = 1 + 5.0.rand;
       },{
        synth_dur = 0.1+ 2.0.rand;
       });
      });
      Synth(\example6, [\out, 0, \amp, 0.05, \freq, freq, 
          \dur, synth_dur]);
      (0.01 + 0.4.rand).wait;
    });
  }).play;
)

Ok, in our first if statement, we test for frequencies that are greater than or equal to 600 Hz. If they are that high, then we set our variable synth_dur to 0.1 + 1.0.rand. If the freq is not that high, we go to the false function. In the false function, we find another if statement. In this if statement, we test for frequencies that are less than or equal to 400 Hz. If they are that low, then we set our variable synth_dur to 1 + 5.0.rand. the the freq is not that low, we go to the false condition and set our variable synth_dur to 0.1+ 2.0.rand. We only get to that second false function if both true functions are false. We could re-write those test conditions:

    if((freq >= 600), {
     synth_dur = 0.1 + 1.0.rand;
    });
    if ((freq <= 400), {
     synth_dur = 1 + 5.0.rand;
    });
    if ((freq < 600) && (freq > 400), {
     synth_dur = 0.1+ 2.0.rand;
    });

The result would be the same. But if we do it that way, we have to go through three if statements every time through the loop. If we do it with nested if statements as above, we only have to go through two of them at most. And, if the frequency is high, we only need to go through the first one. So the nested if statements are slightly more efficient, however, it's good to figure out exactly what conditions would have to be to get to a particular code block.

Summary

  • An expression is a snippet of code that has a value or can return a value
  • You can test for equivalency with ==
  • Greater than is >
  • Less than is <
  • Greater than or equal to is >=
  • Less than or equal to is <=
  • Not is boolean.not and causes things to invert
  • Not equals is !=
  • And is &&. Both conditions must be true for and to be true.
  • Or is ||. One or both conditions must be true for or to be true.
  • If statements can be nested

Problems

  • Write if statements using and, or, and not using Boolean values of true and false to illustrate the truth tables, using all possible combinations of true and false. For example:
    (
     if (true.not, {
      "true".postln;
     }, {
      "false".postln;
     });
    )
    
    What do you expect each of them to print? Did the results you got from running them match your expectations?
  • Friday, May 28, 2010

    If

    In our programmes, we need control structures, that is, ways to make decisions. One control structure is if. if has it's own helpfile. Highlight if and press apple-d. If is also explained briefly in the helpfile for Boolean.

    A Boolean is a value that is either true or false. true and false are reserved words in SuperCollider. We can send an if message to Booleans.

    (
     ([true, false].choose).if(
      {
       "true".postln;   
      }, {
       "false".postln;
      }
     );
    )
    

    Remember that a list surrounded by square brackets is an Array. Arrays understand a message called choose. It randomly picks an item in the array and returns it.

    If you run the above code several times, “true” and “false” should print out about the same number of times in a random order, because [true, false].choose ought to be true half the time and false the other half. The result of that expression is a Boolean. We send an if message to the Boolean, which has two arguments, both functions. The first function is evaluated if the Boolean is true. The second function is evaluated if the Boolean is false.

    boolean.if(trueFunction, falseFunction);

    You can omit the false function if you want.

    This syntax that we've been using, object.message(argument1, argument2, . . . argumentN);, is the most commonly used syntax in SuperCollider programs. It's called receiver notation. However, there is more than one correct syntax in SuperCollider. There also exists a syntax called functional notation. It is more commonly used with if messages than receiver notation. When you see if in the helpfiles, the examples almost always use functional notation. Functional notation is:

    message(object, argument1, argument2,  . . . argumentN);

    The two notations are equivalent. You can replace one with the other at any place in any program and it will not change the program. The reason that I bring this up is that people very commonly use functional notation for if:

    if(boolean, trueFunc, falseFunc);

    So our example would change to:

    (
     if([true, false].choose, {
      "true".postln;   
     }, {
      "false".postln;
     });
    )
    

    It works in exactly the same way.

    Why are there multiple correct notations? It's confusing!

    SuperCollider is based on many other programming languages, but the language that it borrows most heavily on is one called Smalltalk. Smalltalk, like SuperCollider, is an object-oriented language. When I took Programming Languages at uni, my teacher said that Smalltalk was the best object oriented language and the only reason it wasn't the most popular was that the syntax was insane.

    So rather than force us to use Smalltalk syntax, James McCartney, the author of SuperCollider, allows us multiple legal syntaxes. Receiver notation is common in several object-oriented languages. Functional notation, however, persists in if, probably because other languages have different ways of thinking about if.

    Let's code an overtone player that has a 50% chance of resting. First, we need a synthdef:

    (
     SynthDef.new("example5", {arg out = 0, freq = 440, amp = 0.2,
         dur = 1;
    
       var saw, env_gen, env;
    
       env = Env.triangle(dur, amp);
       env_gen = EnvGen.kr(env, doneAction:2);
       saw = Saw.ar(freq, env_gen);
       Out.ar(out, saw);
     }).load(s);
    )
    

    This one uses a sawtooth wave oscillator. Check out the helpfile for Saw for more information.

    Then, our player:

    (
     var player;
     
     player = { arg baseFreq, numOvertones, dur = 0.2;
      var freq;
      Task({
        (numOvertones + 1).do({ arg index;
        freq = index * baseFreq;
        if ([true, false].choose, {
         Synth(\example5, [\out, 0, \freq, freq, \dur, dur]);
        });
          dur.wait;
         });
       });
      };
      
      player.value(220, 12).play;
    )
    

    If you run it several times, it should rest in different spots, about half the time.

    If we want to give it a 33.3% chance of resting (a 66% chance of playing), we could change out if to look like [true, true, false].choose and expand our array every time we want to change the probability. But what if we want something to play 99% of the time? We would have to have 99 trues and one false. Fortunately, there is a message you can use that returns a Boolean based on percentage. To play 99% of the time, we would use 0.99.coin

    If you look at the helpfile for float, you learn that coin, "answers a Boolean which is the result of a random test whose probability of success in a range from zero to one is this." Which means that 0.66.coin has a 66% chance of being true and 0.01.coin has a 1% chance of being true.

    The word "this" in that description refers to the number which received the message. If you type 0.66.coin, "this" is 0.66.

    Let's give our an example 75% chance of playing a given note:

    (
     var player;
     
     player = { arg baseFreq, numOvertones, dur = 0.2;
      var freq;
      Task({
        (numOvertones + 1).do({ arg index;
        freq = index * baseFreq;
        if (0.75.coin, {
         Synth(\example5, [\out, 0, \freq, freq, \dur, dur]);
        });
          dur.wait;
         });
       });
      };
      
      player.value(220, 12).play;
    )
    

    Summary

    • Booleans are either true or false
    • if is a way of controlling programme flow and making decisions
    • receiver notation is object.message(argument)
    • functional notation is message(object, argument)
    • receiver notation and functional notation are only stylistic differences and both do exactly the same thing
    • if is often typed as if(boolean, true_function, false_function)
    • The fase function is optional
    • The choose message causes an array to pick a single element at random
    • n.coin returns a boolean value with n probability of being true
    • In helpfiles and other places, "this" refers to the object that received the message

    Problems

    1. Re-write "Hello World".postln in functional notation.
    2. Write a programme that has 66% chance of playing the any given note in an arpeggio and a 33% chance of playing a random note instead. You will need to use a false function.
    3. The functions of ifs can contain other ifs. Write a programme that has a 50% chance of playing any given note in arpeggio, a 25% chance of playing a random note and a 25% chance of resting. Remember that if you write if(0.25.coin . . .), it has a 75% chance of picking false and evaluating the false function (if one is present).

    Thursday, May 27, 2010

    Tasks

    Last time, we learned how to use an envelope to cause synths to stop playing after a set time. Now we're going to look at one way we can control when they start. As with any programming language, there are multiple ways to do this in SuperCollider, each with particular strengths and weaknesses.

    We're going to look at something called a Task:

    (
     r  = Task.new ({
     
      5.do({ arg index;
       index.postln;
       1.wait;
      });
     });
     
     r.play;
    )
    

    That example prints out:

    0
    1
    2
    3
    4
    

    With a one second pause between each number. Try running it.

    Remember that the letters a-z are global variables in SuperCollider. Also, remember that the class SimpleNumber is a superclass of both Float and Integer. It can take a message called wait. That message only works in the context of a task (or it's superclasses, like Routine). If we try wait in a normal function:

    (
     f  = {
     
      5.do({ arg index;
       index.postln;
       1.wait;
      });
     };
     
     f.value;
    )
    

    We get errors:

    • ERROR: yield was called outside of a Routine.
    ERROR: Primitive '_RoutineYield' failed.
    Failed.
    

    Tasks (which are a subclass of Routine) are one powerful way to add timing to your program. Task.new takes a function as an argument. It runs that function when we send it the play message. Hence, r.play; in the Task example. When we play a Task, it will pause for a duration of SimpleNumber.wait. If we code 5.wait, it will pause for five seconds. If we have 0.5.wait, it will pause for half a second.

    Ok, now we know how to control when things happen! Let's try this to play some notes! First we need a SynthDef:

    (
     SynthDef.new("example4", {arg out = 0, freq = 440, amp = 0.2,
        dur = 1;
    
      var square, env_gen, env;
    
      env = Env.triangle(dur, amp);
      env_gen = EnvGen.kr(env, doneAction:2);
      square = Pulse.ar(freq, 0.5, env_gen);
      Out.ar(out, square);
     }).load(s);
    )
    

    This is just like our last SynthDef, except that it uses a square wave instead of a sine wave. To find out more about the Pulse UGen, read the help file on it.

    Ok, let's play some pitches. the range of human hearing is 20Hz - 20,000 Hz, but the range of my laptop speakers is somewhat less than that, so let's pick between 200 - 800 Hz.

    (
     Task({
      var freq;
      10.do({
       freq = 200 + 600.rand;
       Synth(\example4, [\out, 0, \freq, freq, \dur, 0.5]);
       0.5.wait;
      });
     }).play;
    )
    

    When you we that, we get random pitches.

    In the first line, you'll notice that we just have Task({ instead of Task.new({. This is a shortcut that SuperCollider allows. You can just have Class(argument1 , argument2 . . .) in place of Class.new(argument1, argument2 . . .). The .new is implied.

    The next line with something new is freq = 200 + 600.rand;. You can pass the message rand so a SimpleNumber. It will return a random number between 0 and the number that it's passed to. So 600.rand will return a random number between 0 - 600. If we add 200 to that number, then we have a random number between 200 - 800, which is the range we want.

    In the past, we talked a bit about precedence. In SuperCollider, passing messages will be evaluated before math operations like addition, subtraction, etc. So the interpreter will evaluate 600.rand first and then add 200. Therefore, we will get the same results if we type freq = 200 + 600.rand; or if we type freq = 600.rand + 200;

    In the next line, we've got Synth(\example4 instead of Synth.new(\example4. This is the same shortcut as in the first line.

    In that example, we pass 0.5 to the synth for it's duration and wait 0.5 seconds, so there's no overlap between the notes. We don't have to do it that way. We could only wait for 0.25 seconds and let the notes overlap, or we could extend the wait and leave spaces between the notes. Or we could change the Synth duration. We could use random numbers for waits and durations:

    (
     Task({
      var freq;
      10.do({
       freq = 200 + 600.rand;
       Synth(\example4, [\out, 0, \freq, freq, \dur, (0.1+ 2.0.rand)]);
       (0.01 + 0.4.rand).wait;
      });
     }).play;
    )
    

    The two parts that have changed are \dur, (0.1+ 2.0.rand) and (0.01 + 0.4.rand).wait;. The reason that we have 2.0.rand is because Integer.rand returns an Integer and Float.rand returns a float. 2 is an integer, so 2.rand will return either 1 or 0 and no other numbers. 2.0.rand, however, will return real numbers and thus can return things like 0.72889876365662 or 1.6925632953644. Try evaluating 2.rand and 2.0.rand several times and look at the results you get.

    We use parenthesis in (0.01 + 0.4.rand).wait; so that we sent the wait message to the results of 0.01 + 0.4.rand. If we were to skip the parenthesis, we would pick a random number between 0 and 0.4 and then wait that long and then add 0.01 to the results of the wait message, which is not what what we want. We add in that 0.01 to make sure that we never have a wait of 0. This is an aesthetic decision. 0.wait is perfectly acceptable.

    What if we want to modify the number of times we run through our main loop? We can't pass arguments to a Task, but fortunately, because of scope and functions returning things, there is a clever work around:

    (
    var func, task;
    
    func = { arg repeats = 10;
    
     Task({  
      var freq;
      repeats.do({
       freq = 200 + 600.rand;
       Synth(\example4, [\out, 0, \freq, freq, \dur, (0.1+ 2.0.rand)]);
       0.01 + 0.4.rand.wait;
      });
     });
     
    };
    
    task = func.value(15);
    task.play;
    )
    

    First, we write a function that takes an argument. Then, inside the function, we have a Task. Because the Task is inside the function's code block, the Task is within the scope of the argument. And because the Task is the last (and only, aside from the args) thing inside the function, it gets returned when the function gets evaluated. So task gets the Task that is returned by the function. Then we call task.play; to run the Task. It's ok to have a variable called task because SuperCollider is case-sensitive and therefore Task, task, tASK, taSk, etc are all different.

    Remember that functions return their last value. And remember that scope means that inner code blocks can see variables from outer code blocks, but not vice versa. So the Task can see the args. And there's nothing after the Task in the function, so it's the last value, so it gets returned when we send a value message to the function. Then we send a play message to the returned Task. We could abbreviate the last two lines of our program by just having one line:

    func.value(15).play;

    The interpreter, reading from right to left, will pass a value message to the func, with an argument of 15. That returns a Task, which is then passed a message of play.

    Summary

    • Tasks are functions that can pause.
    • In a task, number.wait will pause for that number of seconds.
    • You cannot ask a code block to wait unless it is within a a Routine, which is a superclass of Task.
    • n.rand returns a number between 0 and n. If n is an integer, it returns an integer. If n is a float, it returns a float.
    • You cannot pass arguments to Tasks, but you can put them in functions that take arguments and then return the Task from the function.

    Problems

    1. Write a a task that plays an arpeggio of the first n overtones over a base frequency. Use a wrapper function to pass in both the frequency and the number of overtones. Give the arguments descriptive names. (There is an explanation of overtones in the post on numbers.)
    2. Write a task that produces n separate sound clusters. Pass that number as an argument (using functions). Give the argument a descriptive name.

    Project

    1. Write a short piece that uses tone clusters that are in different frequency ranges. For example, one section might be 100-300 Hz and another might be 600-1200 Hz. You may wish to vary the timbres by using multiple synthdefs. You may wish different sections to have different densities, also.

      You can make a recording of your piece by pressing the "Prepare Record button" on the server window, after you boot it. It will turn red and say "record >". Press it again to start recording. The button will change color again and say "stop []". this means it's now recording. Run your piece. Click the button again when your piece has finished to stop recording. The Recording will go in to your home directory under Music/SuperCollider Recordings. It will be a 32 bit aiff, but you can convert it to a 16bit aiff or to mp3 with Audacity.

      If you do this project, please feel free to share your results in the comments!

    Wednesday, May 26, 2010

    Envelopes

    In the last post, we wrote a function to play overtones, but it played them all at the same time. In order to write a piece of music, we need both to be able to tell notes when to start and for how long they should last. This section will be on the latter part of the problem.

    "Just like human beings, sounds are born, reach their prime and die; but the life of a sound, from creation to evanescence last only a few seconds" Yamaha GX-1 Guide

    We control this process with a type of UGen called an envelope.

    (
     SynthDef.new("example3", {arg out = 0, freq = 440, amp = 0.2,
           dur = 1;
      var sin, env_gen, env;
      
      env = Env.triangle(dur, amp);
      env_gen = EnvGen.kr(env);
      sin = SinOsc.ar(freq, mul: env_gen);
      Out.ar(out, sin);
     }).load(s);
    )
    
    Synth.new("example3");
    

    There are a few things that make that SynthDef different. One is that it uses variables. Variables are a good way to keep SynthDefs more readable. The easier it is to read, the easier it is to understand, to fix and to change.

    The other obvious change is the addition of an envelope. Envelopes have two parts. One is Env. The Env class lets you describe what shape the envelope should have. In this case, we're using a fixed duration envelope shaped like a triangle. When we use a fixed duration envelope, we know the length of the envelope when we create an instance of the Synth. There are other envelopes that we will not know the envelope of until we decide to end the note, for instance, in response to a key up on a keyboard.

    triangle is a constructor. It creates a new instance of an Env. The arguments it takes are the duration and the maximum amplitude.

    The other part of the envelope is the EnvGen. Since EnvGen is a class, kr must be a constructor. The two most common UGen constructors are ar and kr. You may recall that ar stands for “audio rate.” kr stands for “control rate.” Because the EnvGen is not actually creating sound, but is controlling the amplitude of a sound, we want the control rate. Control rate signals change less often than audio rate signals, and so using kr reduces the load on our computer and makes our SynthDef more efficient.

    EnvGen.kr generates the envelope based on the specifications of its first argument, an instance of Env. So, with Env, we define an Envelope. We pass that definition to an Envelop Generator, which plays back the envelope that we defined. We use that envelope to control the amplitude of a SinOsc. We send the output of the SinOsc to an output bus. When the envelope is done, the amplitude of it has returned to zero, so the SinOsc is no longer audible.

    With our previous examples, the Synth never ended. The way to stop sound was to stop execution with apple-. . With our new SynthDef, the sound goes on for exactly the length of dur and then ends with no way to restart it. After we create a Synth and play it, there's nothing that can be done with it.

    Try running Synth.new("example3"); several times. If you look at the localhost server window, you will see the number of UGens and Synths grow every time you run Synth.new. The Avg and Peak CPU may also increase.

    Every time we create a new instance of Synth, we use up more system resources. Eventually we will either run out of memory or the CPU will climb over 100%. We can clear the Synths by hitting apple-., but this still would limit the number of sounds we could play in a row without stopping execution.

    We need an automatic way to remove our synths from the server and deallocate their resources. That means that some new Synths would be able to use the resources the old ones were taking up.

    Fortunately, EnvGens can both silence a Synth forever and lay it to rest so iti s deallocated. EnvGen takes an argument called “doneAction”. Adding a doneAction to your EnvGen would look like:

    env_gen = EnvGen.kr(env, doneAction: 2);

    Try changing adding a doneAction to the SynthDef and running Synth.new("example3"); a few times. Notice that now the Synths don’t accumulate. Now, after a Synth stops making sound, it has ended its useful life and so it gets removed from the server because of the doneAction: 2. However, note that the doneAction removes only instances of the Synth. The SynthDef persists and can be used as many times as we'd like.

    Envelopes don’t have to only control amplitude, but can control (almost) any aspect of any UGen. You can’t set the bus on Out.ar with an Envelope, but you can control almost anything else. For example, we can cause the frequency to warble

    
    (
     SynthDef.new("example3a", {arg out = 0, freq = 440, amp = 0.2,
           dur = 1;
    
      var sin, env_gen, env, freq_env;
      
      env = Env.triangle(dur, amp);
      env_gen = EnvGen.kr(env, doneAction: 2);
      freq_env = EnvGen.kr(Env.sine(dur, 50));
    
      sin = SinOsc.ar(freq + freq_env, mul: env_gen);
      Out.ar(out, sin);
     }).load(s);
    )
    
    Synth.new("example3a");
    

    Note that we only need one of the envelopes to have a doneAction.

    Summary

    • Envelopes can be used to control how the amplitude of a Synth changes over time.
    • Env is used to describe the shape of an enevelope.
    • EnvGen.kr is what actually plays the envelope.
    • Synths can be deallocated by using the doneAction argument of EnvGen.
    • Envelopes can also be used to control frequency and many other parameters of SynthDefs.

    Problems

    1. Try out the different fixed duration envelopes and try them out with different Oscillators. Especially try out the Env.perc envelope with different noise generators.

    Monday, May 24, 2010

    Numbers and Math

    In the previous post, we talked a bit about Integers, which, remember are whole numbers, like -1, 0, 1, 2. And we learned about the do message.

    What are some things you might want to do to a number? Add, subtract, multiply, divide, modulus.

    Remember algebra where a*b + c*d = (a * b) + (c * d). And remember how that one cheap calculator just did what you punched in, in the order you punched it in, without respecting order of operations? SuperCollider is like that cheap calculator. Mathematical expressions are evaluated left to right. Addition and subtraction have the same precedence as multiplication and division. That means that if you want to do things in some order, you've got to use parenthesis.

    Parentheses are evaluated innermost to outermost:
    (a + (b * (c + d)))
    ( 2 + (4 * (3 + 2))) = (2 + (4 *(5))) = (2 + (20)) = 22

    What about order of operations within longer lines of code? If we have func.value(3+4), it evaluates 3+ 4 to 7 before calling the function. We can put anything we want inside those parentheses and it will evaluate what's inside until it gets to the outermost parenthesis and then it will call the function.

    One math operation that you might not have seen before is modulus. It means "remainder" and it's represented by a '%'.
    10 % 3 = 1
    26 % 9 = 8

    Ok, so the output of a modulus between two integers is an integer, by definition. And if you add or subtract two integers, you get an integer. As you do if you multiply two integers. But what is the result of division?
    3 / 2 = 1.5

    1.5 is not an integer. It's a type of number called a Floating point, or in SuperCollider, a Float. A float is a fraction. 1.1, 2.5, -0.2, 5.0 are all Floats. They also can add subtract, etc.

    You can do more with numbers than simple math and do loops. There are many interesting and useful messages one can pass to Integer. It has a help file worth reading. So type in the word Integer, starting with a capital-I and press apple-d to look at the help file. The top of that help file says:

    superclass: SimpleNumber

    superclass is a vocabulary word. It refers to a concept called inheritance. What this superclass designation means is that Integer is a SimpleNumber. When you define classes (recall that a class is the definition for an object), you can define subclasses of any class. A subclass is a special type of the original class. It inherits all the properties of its superclass. So the subclass is the child and the superclass is the parent. We'll come back to this. But what it means for us is that Integer is a SimpleNumber. Which means it understands all the messages that you can pass to a SimpleNumber. So to find out about what these inherited messages are, we should highlight "SimpleNumber" and hit apple-d Float and Integer both inherit from SimpleNumber, so take a look at that help file. Looking at help files and trying stuff out will get you learning the language faster than anything else. They might not make a lot of sense right now, but if you keep looking, you'll get context and be able to figure them out in the future.

    We have one more example that hopefully ties all of this together. Remember our SynthDef from posts past? Here's an updated version of it.

    (
    
     SynthDef.new("example2", {arg freq = 440, amp = 0.2;
      Out.ar(0, SinOsc.ar(freq, mul: amp));
     }).load(s);
     
    )
    

    We’ve added a field called amp, which indicates the amplitude or volume at which the sound should play. In SuperCollider, amplitudes louder than 1 will peak when you listen to them. Therefore output amplitudes should be between 0, which is silent, and 1, which is as loud as possible.

    Now, lets’ write some code to play N overtones of 100Hz. We need to make sure that our overall amplitude doesn’t exceed 1, or else we’ll get peaking. So, we will divide 1 by the number of overtones into 1 to get the amp value to send to each synth.

    
    (
     var func;
     
     func = { arg repeats = 4;
      repeats.do({ arg index;
    
        Synth.new("example2", [\freq, (index + 1) * 100,
            \amp, 1 / repeats]);
       });
     };
     func.value;
    )
    

    Let's hear what happens when we play this. All the sounds play at once! This is because the loop goes through as fast as it can. We'll cover timing issues very soon.

    Summary

    • Whole numbers are Integers
    • Real numbers are Floats
    • You can indicate order of operations with parenthesis
    • Math operations are evaluated innermost to outermost parenthesis and left to right
    • Modulus (%) means remainder
    • Subclasses inherit the properties of their superclasses - an Integer is a SimpleNumber
    • Output amplitudes should be between 0 and 1.

    Problems

    1. Translate the following algebraic expressions into proper supercollider syntax, using parenthesis where needed. Try to make your answers human-readable.
      1. 3 + 5 * 4
      2. 7 * 2 + 1
      3. 6 * (4 + 2)
      4. 2 * 3 + 4 * 5 + 7 / 2 + 1
    2. Get yourself familiar with modulus by working out these problems by hand.
      1. 5 % 4
      2. 163 % 9
      3. 20 % 5
      4. 17 % 6
      5. 23 % 2
      6. 3 % 5
    3. Write a function to print out the first n multiples of 10, starting from 0. Pass the number of multiples into the function as an argument. Set the default number to 7.

    4. Write a function that takes three arguments, numberOfOvertones, pitch and amp. It should play the number of overtones above pitch specified by the argument numberOfOvertones. The amplitude of the overtones should be half the amplitude of the fundamentals and the total amplitude for the fundamental plus the overtones should be amp.

    5. rand is a message that you can send to numbers. y.rand returns a number between 0 and y. Humans are generally able to hear frequencies between 20Hz and 20000Hz (the freq argument for oscillator UGens is in Hz). Write a function that calls your previous function. You want to play n random pitches and the first m overtones of those pitches. Make sure that the random pitches are all in the audible range. Pass the number of random pitches and the number of overtones into your new function as arguments. Set the default number of random pitches to 2 and the default number of overtones to 3. Make certain that your total amplitude will not peak. Finally, give the arguments descriptive names.

      1. Can you figure out a way to make sure that all your pitches, including overtones, are in the audible range?

    Sunday, May 23, 2010

    Functions

    After reading the last chapter on SynthDefs and the one before that on Variables, Nicole feels like she's got the hang of SuperCollider so she's dropped out of grad school to work for the new SuperCollider start-up company SuperSounds.com. On her first day, her boss tells her to write a function that prints "hello world" four times. "No problem," she thinks and goes to look at previous posts. Functions are code blocks encased by curly brackets, { } and hello world is easy enough. So she writes:

    (
    {
    
    var greet;
    greet = "hello world";
    
    greet.postln;
    greet.postln;
    greet.postln;
    greet.postln;
    }
    )
    

    Then she thinks, "I should have asked for a signing bonus." She tries running it, by double clicking to the right of the top parenthesis and hitting enter. In the Untitled output window it says, "a Function"

    What's the problem? She declared a variable called greet. greet gets "hello world". Then she sends a postln message to greet four times. Every line ends with a semicolon . . .

    Then she realizes that she defined a function, but never told the interpreter to run it. The interpreter saw a code block surrounded by curly brackets and thought "a function!" Then it thought, "What do you want me to do with this? Nothing? Ok, I'll throw it away." So Nicole modifies her code:

    (
    var func;
    
    func = {
    
    var greet;
    greet = "hello world";
    
    greet.postln;
    greet.postln;
    greet.postln;
    greet.postln;
    };
    
    func.value;
    )
    

    And then it works great. value is a message you can send to functions. It means, "Run yourself!"

    But then Nicole gets a message from her boss saying, "Sometimes, we need to print out hello world five times and once in a while, three times, and rarely, it needs to print out infinite times." Nicole considers writing a few different versions of the function and calling them func4, func3, etc, but then remembers about arguments to functions.

    (
    var func;
    func = { arg repeats;
    
    var greet;
    greet = "hello world";
    
    repeats.do ({
    greet.postln;
    });
    };
    
    func.value(4);
    )
    

    When she writes her function, she declares to the interpreter that the function takes one argument. An argument is a special type of variable that gets set when the function is called. When she calls the function withfunc.value(4);, she's assigning a value to the argument repeats.

    Then, inside the function, she's written, repeats.do. What is 'do'? It's a message. It takes a function as an argument. 'do' is a message you can send to integers that runs the function passed as an argument the number of times as the integer that it was called on. In this example, repeats is 4, so it runs four times.

    What is an integer? An integer is a whole number. -2, -1, 0, 1, 2, 3, 4, etc. There is a special integer in SuperCollider called inf. It means infinity. If we try calling our above function, with

    func.value(inf);

    hello world will print out forever, or until we stop the program by hitting apple-. .

    Then Nicole's boss sends another email saying that marketing has some changes. Every line needs to start out with the line number, starting with zero. So she makes another change:

    (
    var func;
    func = { arg repeats;
    
    var greet;
    greet = "hello world";
    
    repeats.do ({ arg index;
    index.post;
    " ".post;
    greet.postln;
    });
    };
    
    func.value(4);
    )
    

    The function called by do takes an argument. And that argument is the number of times the loop has been run, starting with 0. So her output from this program is:

    0 hello world
    1 hello world
    2 hello world
    3 hello world
    

    post just means print without a new line at the end.

    Almost every time, she runs this function, the argument is going to be 4. So she can declare a default argument for the function.

    (
    var func;
    func = { arg repeats = 4;
    
    var greet;
    greet = "hello world";
    
    repeats.do ({ arg index;
    index.post;
    " ".post;
    greet.postln;
    });
    };
    
    func.value;
    )
    

    When she calls func.value, if there's no argument, then the interpreter will assign 4 to repeats by default. If she calls it with, func.value(6); then repeats gets 6 instead. What if she accidentally passes in something besides an Integer into her function? That depends. If the object she passes in also understands a do message, then it will use that instead, although the result may differ. Otherwise, she will get an error.

    What if the function took a lot of arguments with default values?

    (
    var func;
    func = { arg foo = 0, bar = 0, baz = 1, repeats = 4;
    
    var greet;
    greet = "hello world";
    
    repeats.do ({ arg index;
    index.post;
    " ".post;
    greet.postln;
    });
    };
    
    func.value;
    )
    

    If she wants to pass in arguments, she does it in the order they're declared in.

    func.value(0 /* foo */, 0 /* bar */, 1 /* baz */, 3 /* repeats */);

    However, if we're happy with all the default values, there's a way to tell the function just to assign to a particular variable, out of order:

    func.value(repeats: 3);

    You can also just pass in the first N arguments and leave the remainder to the default values:

    func.value(3, 1);

    And you can combine approaches:

    func.value(2, repeats: 6);

    Those slash-stars up there are comments. The interpreter ignores everything between a forward slash star and a backslash star. They can span multiple lines. You can also create single line comment by using forward slash forward slash:

    // This is a comment

    Some times it's useful to comment out a line of code while debugging. So you can skip a particular line in order to figure out where your error is.

    The philosophical point of a function is to return a value. Doing things within functions, like printing are technically called side effects. What the function returns is the value of its last line. Let's change that function so it returns the number of times that it printed.

    (
     var func;
     func = { arg repeats = 4;
     
      var greet;
      greet = "hello world";
      
      repeats.do ({ arg index;
       index.post;
       " ".post;
       greet.postln;
      });
      repeats;
     };
     
     func.value;
    )
    

    Now, if we create a new variable called times, we can assign the output of the function to it.

    (
     var func, times;
     func = { arg repeats = 4;
     
      var greet;
      greet = "hello world";
      
      repeats.do ({ arg index;
       index.post;
       " ".post;
       greet.postln;
      });
      repeats;
     };
     
     times = func.value;
     times.postln;
    )
    

    Prints out hello world with the line number like before, and then at the bottom, prints out a 4. Or we could change those last two lines from

     times = func.value;
     times.postln;
    

    to

    func.value.postln;

    and the interpreter will read that statement left to right, first finding the value of the function and then sending that value a postln message.

    Ok, what happens if we take the above function and write some code below it that looks like this:

    (
     var func, times;
     func = { arg repeats = 4;
     
      var greet;
      greet = "hello world";
      
      repeats.do ({ arg index;
       index.post;
       " ".post;
       greet.postln;
      });
      repeats;
     };
     greet.postln;
    )
    

    We get errors.

    • ERROR: Variable 'greet' not defined.
     in file 'selected text'
     line 14 char 6 :
     greet•.postln;
    

    This is because of something called scope. Variables only exist in the code block in which they are declared. Code blocks are zero or more lines of code surrounded by curly braces or highlighted by the user. This means that variables and arguments declared inside a function only exist inside that function. index does not exist outside of its function, which is the function passed as an argument to repeats.do. greet does not exist outside of it's function. None of these variables exist of the text we highlight.

    Variables in outer blocks are accessible within inner blocks. For example,

    (
     var func, times;
     times = 0;
     func = { arg repeats = 4;
     
      var greet;
      greet = "hello world";
      times.postln;
      
      repeats.do ({ arg index;
       index.post;
       " ".post;
       greet.postln;
      });
      repeats;
     };
    )
    

    Is fine because times exists in the outer most block. In the same way, we can use greet inside our repeats.do function.

    There are, however, a few variables that can be used everywhere. The interpreter gives us 26 global variables. Their scope is all of SuperCollider. They are single letter variable names, a, b, c, d, e, f, etc. You don't need to declare them and they keep their value until you change it again, even in different code blocks. This is why the variable 's' refers to the Server. You can change that, but you might not want to.

    If you have any questions about functions, you can look at the Function help file for more information. The most useful message you can send to a function is value, but there are a few others, which you can read about. You may not understand everything you see in the helpfiles, but it’s good to keep reading them.

    Summary

    • Blocks of code surrounded by curly brackets are functions.
    • Functions do not run unless you tell the interpreter to run them, which you can do with the message "value".
    • You can declare arguments to function. That argument can have a default value.
    • An integer is a whole number
    • If you pass the message "do" to an integer with a function as an argument, that function will run as a loop.
    • Functions can have many arguments. You must pass arguments in the correct order or list them by name.
    • Comments are either surrounded by /* and */ or are on a single line, preceded by //
    • All functions return a value, which is the result of the last line of code in the function
    • Variable names are only valid in the block of code in which they were declared, and within blocks of code contained within that block.
    • There are 26 global variables, each a single lowercase letter a-z.

    Problems

    1. Write a function with nested do-loops, so that there is one loop inside the other.
    2. If x and y are intergers and the inner loop is x.do and the outer loop is y.do, how many total times with the inner loop be run?

    Saturday, May 22, 2010

    SynthDefs

    Last time, I promised we would cover sound-making. First, make sure to boot the localhost server. The localhost server is one of the two small gray boxes at the bottom left of your screen. Press the “Boot” button. When it's booted, the window will say "running" in it and the "Boot" button will turn green and change to say "Quit."

    Then, select this code and press enter (not return!). You can select it by double-clicking to the right of the open parenthesis.

     (
    
      var syn, sound;
    
      syn = SynthDef.new("example1", {
       Out.ar(0, SinOsc.ar(440));
      });
     
      syn.load(s);
    
      sound = Synth.new("example1");
     )
    

    When you want to stop the program, press apple-period.

    What's going on here? Our program first declares a variable, and then it defines a SynthDef and loads it on the server, then creates a Synth object that plays the sound. This is a complicated example, but I wanted to get you making sounds as soon as possible. Let's look at the example line by line.

    The first line is var syn, sound; We are declaring two variables. One is called syn and the other is called sound.

    The next line, translated into English, means "I want to create a new SynthDef called 'example1' and store it in the variable syn." Whoa, what's a SynthDef? The SynthDef help file told me that a SynthDef is a "definition of a synth architecture." I got to the help file by highlighting the word SynthDef and pressing apple-d. When I see a term like that, which I don't understand, I can always get help by highlighting it and typing apple-d.

    Recall that SuperCollider is not just one program; it is two. One part of the program is the interpreter, which is what we've been talking about so far. The other part of the program is audio server, which is what actually makes the sound. It does not know anything but what it needs to know. This makes it run much faster and more efficiently. You can tell it to make certain kinds of sounds, but in order to run quickly, it wants to know what kinds of sounds you want before you actually make them. You write a description of what you want the server to do. This description, or definition, is called a SynthDef.

    When we run SynthDef.new, we get back a new SynthDef, which we're stroing in the variable named syn. Inside the parenthesis are some arguments. SynthDef is the class. new is the message. And "example1" and the stuff in between the curly brackets are the two arguments.

    The stuff in the curly brackets is telling the SynthDef what to play. Things between curly brackets {} are called functions. A function is a special type of object that is made up of a code block that you can run. We'll come back to this later.

    Out is a UGen. The help file for UGens says, "A UGen or unit generator is an object for generating or processing audio or control signals." UGens exist in SynthDefs. They are what make up a SynthDef. Out is a UGen that writes a signal to a bus, which, in this case, sends it's output to the left channel. .ar is a type of constructor. So Out.ar creates a new instance of a an Out UGen running at the audio rate. ar stands for "audio rate" and is a common constructor name for UGens.

    Looking at the arguments to Out.ar, 0 means left channel. If that were a 1, it would mean right channel. And the next thing is what gets sent out, which is a sine tone generator. The argument to SinOsc.ar, 440, is the frequency to play. So SinOsc.ar(440) creates a sine wave at 440 Hz. Out.ar takes that sine wave and sends it out to the left channel, which is channel 0.

    The next line, is a closing curly bracket, a close paren and a semicolon. It's the end of the function, the end of the SynthDef and the end of a statement. We've created a new SynthDef that includes in it a function describing what the SynthDef should do when it gets instantiated.

    The next line says, take that SynthDef and load it on the server. We're sending a message to syn, saying load. The argument, s, is the server to send it to. In SuperCollider, a lowercase s by itself refers to the audio server.

    The next line asks to create a new Synth. The interpreter sends a message to the server, saying, "Hey, could you make a synth that plays the SynthDef called 'example1'?" The server looks and says, "oh yeah, I have a SynthDef called that" and makes a new instance of a Synth, which is running the function that we defined.

    We get a new Synth object back from this and store it in "sound."

    Let's say we don't want to play an A. Let's say we want to play an E. We can change 440 to 660.

    
    (
     var sound;
     SynthDef.new("example1a", {
     
      Out.ar(0, SinOsc.ar(660));
     }).load(s);
     
     sound = Synth.new("example1a");
    )
    

    You'll note that we have one fewer variable in that example. Messages are evaluated left to right. So the interpreter first figures out the result of SynthDef.new("example1a" ... ). Then, it takes that new SynthDef and sends it the message load. We can keep adding on messages at the end as long as we'd like:

    object.message1.message2(arg1).message3.message4(arg2)

    Of course, this gets silly and difficult to read after a while, but it's legal.

    When we're writing a piece, we don't want to have to write a new SynthDef for every note that we're going to play. We can create our own argument, which will tell the SynthDef what frequency to play.

    
    (
    
     var sound;
    
     SynthDef.new("example1", { arg freq;
       Out.ar(0, SinOsc.ar(freq));
      }).load(s);
    
      sound = Synth.new("example1", [\freq, 440]);
    )
    

    We call that argument “freq.” An argument is a special kind of variable. You declare then at the top of a code block by using the reserved word “arg.” So the arg freq; part of syn = SynthDef.new("example1", { arg freq;tells SuperCollider that our SynthDef function takes a single argument called freq. We can then use freq like we would any other variable. Here, we’re passing to SinOsc.ar, to use for the frequency.

    Passing variables to Synths is a bit different than normal variable passing. The interpreter has to communicate with the audio server. They use a protocol called OSC. The Synth object will handle the OSC for you, but the server does not know which argument you’re trying to pass a value to unless you specifically tell it. So Synth.new takes an optional second argument, which is an Array. An array is a list separated by commas and surrounded by square brackets. When you are passing arguments to the server via a synth, the array must be made up of pairs. The first item in a pair is a symbol or a string and the second is a value. The symbol or string must match the name of the argument. So for an argument called freq, we use the symbol \freq or the string “freq”. If we had an argument called amp, we would use the symbol \amp or the string "amp". A symbol starts with a forward slash: ‘\’. So to pass a value of 440 to the argument freq, our array contains [\freq, 440]. If we had two arguments, one freq and the other amp, we could pass values to freq and amp with an array that looks like [\freq, 440, \amp,0.2]. The array is made up of pairs, to the 440 is paired with freq and 0.2 is paired with amp. The symbols or strings must match the names of the arguments taken by the SynthDef function.

    Summary

    • You must boot the server in order to play sounds.
    • There is a variable called "s" which always refers to the server
    • The sounds you want to play must be defined on the server before you play them. They are defined in SynthDefs.
    • Get help by highlighting a class name with the mouse and typing apple-d
    • Blocks of code surrounded by curly braces are functions
    • You play SynthDefs by using Synth
    • You can declare arguments to a function with the word arg: { arg agrument1, argument2;
    • Comma-separated lists surrounded by square brackets are Arrays
    • Synths use arrays to pass arguments: Synth.new(name, [\symbol, argument])

    Problems

    When you are writing code, if you want to syntax colorize it, so that reserved words are blue, strings are grey, symbols are green, and so on, you can find that in the Format menu or just type apple-‘ (apple-single quote).

    1. Write your own version of “hello world.”
    2. Write your own SynthDefs, using some of the oscillator UGens. To find a list of them, highlight the word “UGens” and type apple-d. Some oscillators to try are Saw and Pulse.