Friday, June 25, 2010

Dictionary

Let's take a break from Pbinds and their ilk to talk about a data type. A Dictionary is a data type like an array, except that instead of using integers for indexes, you can use anything that you want.

(
 var id;
 
 id = Dictionary.new;
 id.put(\foo, "bar");
 
 id[\foo].postln;
)

The indexes are called keys and the items associated with them are called values. In the above example, \foo is the key and "bar" is the value. As mentioned above, you can use any kind of object for keys. And, like arrays, any kind of object can be the value.

(
 var id;
 
 id = Dictionary.new;
 id.put(\foo, "bar");
 id.put(15, true);
 id.put("baz", 12.5);
 
 id[15].postln;
)

You can get a list of the keys with the message keys, and then use that to step through a do loop:

(
 var id, keys;
 
 id = Dictionary[
  \foo -> "bar",
  15 -> true,
  "baz" -> 12.5
 ];

 keys = id.keys;
 keys.postln;

 keys.do({|key|
  
  "key is %, value is %".format(key, id.at(key)).postln;
 });
)

In that example, we see another way of declaring a Dictionary, with square brackets and key, value pairs in the format key -> value

We also see a new way of formatting strings. If you want to print a string with some variables in it, you can use % to signify where the variable should go and then pass the variables as arguments to format. You can use as many variables as you would like: "string %, % and %".format(var1, var2, var3)

Notice that the keys are in a random order. If you want them in a particular order, you will have to sort them.

Here's another example:

(
 var triangle_spectrum, freqs;
 
 triangle_spectrum = Dictionary[
  1->1, 
  3->(1/9), 
  5->(1/25), 
  7->(1/49), 
  9->(1/81)
 ];
 
 freqs = triangle_spectrum.keys;
 freqs = freqs.asArray;
 freqs = freqs.sort;
 
 freqs.do({|freq|
 
  "freq %\t amplitude %\n".postf(freq * 440, triangle_spectrum[freq]);
 })
)

This stores the (non-normalised) spectrum of a triangle wave. Note that when we declare the Dictionary, we need to put mathematical expressions inside parentheses.

Then, we get the keys from the dictionary. Then we need to convert that Set to an Array. We can then tell the Array to sort itself.

\t means tab and \n means newline. We use these for formatting, to make our output look nice. postf formats a string and then posts it. It does not include a newline, so we do it ourselves with a \n

Array.sort takes an optional argument, which is a sort function. Here's an example to do a reverse sort:

(
 var triangle_spectrum, freqs;
 
 triangle_spectrum = Dictionary[
  1->1, 
  3->(1/9), 
  5->(1/25), 
  7->(1/49), 
  9->(1/81)
 ];
 
 freqs = triangle_spectrum.keys;
 freqs = freqs.asArray;
 freqs = freqs.sort({|a, b| a > b});
 
 freqs.do({|freq|
 
  "freq %\t amplitude %\n".postf(freq * 440, triangle_spectrum[freq]);
 })
)

The sort function takes two agruments and returns a boolean. In the above example, it compares two items and if the first one is bigger, it returns true. This will order the array so that larger items come first.

We can check to see if a Dictionary includes a particular key:

(
 var triangle_spectrum;
 
 triangle_spectrum = Dictionary[
  1->1, 
  3->(1/9), 
  5->(1/25), 
  7->(1/49), 
  9->(1/81)
 ];
 
 triangle_spectrum.includesKey(4).postln;
)

We can also look for the key of a particular item:

(
 var id;
 
 id = Dictionary[
  \foo -> "bar",
  15 -> true,
  "baz" -> 12.5
 ];

 id.findKeyForValue(12.5).postln;
)

Summary

  • A Dictionary is made up of key, value pairs.
  • You can add items to a Dictionary with a message put(key, value)
  • Dictionaries can be declared with key -> value pairs in square brackets.
  • Dictionary.keys exports a set of the keys, in random order.
  • If you want to sort the keys, you must pass them the message .asArray and then pass the message sort to the result of that.
  • If you want to sort anyhting besides numbers from small ot large you have to write a sort function
  • A sort function takes two arguments and returns a boolean based on a comparison between them.
  • You can test is an Dictionary contains a particular key with includesKey
  • You can look up the key for a value with findKeyForValue

Thursday, June 24, 2010

Streams

There's a very useful helpfile set for Streams Patterns and Events, which is available by looking for the helpfile for Streams.

We know what a pattern is, after looking at Pseq and he other Pthings. But what is a stream? "A stream represents a lazy sequence of values," says the helpfile "Understanding Streams, Patterns and Events - Part 1," which you should go read. Streams respond to the message next. Here's an example:

(
 var stream;
 
 stream = Pseq([1, 2, 3], 1).asStream;
 
 3.do({
  stream.next.postln;
 })
)

Any kind of pattern can be converted to a stream by sending it the message asStream. This can be more useful than you might think. For example, if you have two threads stepping through the same data:

(

 var stream, task1, task2;
 
 stream = Pseq(["Mary", "had", "a", "little", "lamb", "whose", "fleece"], 1).asStream;
 
 task1 = Task({
 
  inf.do({
  
   "task1: ".post;
   stream.next.postln;
   1.wait;
  });
 });
 
 task2 = Task({
 
  inf.do({
  
   "task2: ".post;
   stream.next.postln;
   1.51.wait;
  });
 });
 
 task1.play;
 task2.play
)

The two tasks can strep through a shared list. However, note that the stream starts returning nil when it runs out of other data to return. You have to test for that:

(

 var stream, task;
 
 stream = Pseq(["Mary", "had", "a", "little", "lamb", "whose", "fleece"], 1).asStream;
 
 task = { |id, wait|
  var next;
  Task({
 
   next = stream.next;
   
   {next.notNil}.while({
  
    ("task" ++ id ++ ": " ++ next). postln;
    wait.wait;
    next = stream.next;
   });
  });
 };
 
 task.value(1, 1).play;
 task.value(2, 1.51).play
)

You can also do math on streams:

(
 var stream, squared;
 
 stream = Pseq([1, 2, 3], 1).asStream;
 squared = stream.squared;
 3.do({
  squared.next.postln;
 })
)

And you can do binary math operations on two streams:

(
 var stream1, stream2, sum;

 stream1 = Pseq([1, 2, 3], 1).asStream;
 stream2 = Pseq([4, 5, 6], 1).asStream;
 sum = stream1 + stream2;

 3.do({
  sum.next.postln;
 })
)

You can use streams with Pbinds, but you have to wrap them in Prout or Pfunc:

(
 var notes, pbind;
 
 notes = Pseq([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ].pyramid(1), 2).asStream;
 notes = notes * 440;
 
 pbind = { |dur = 0.5|
 
  Pbind(
   \dur, dur,
   \freq, Pfunc({notes.next})
  )
 };
 
 Ppar([pbind.value(0.5), pbind.value(0.751)]).play;
)

Note that when notes.next returns nil, the Pbind quits playing.

Summary

  • Streams can be create from patterns. The respond to the message next.
  • When streams run out of values, they start to return nil.
  • Multiple tasks can access the same stream.
  • You can do math on streams.
  • Streams can be used in Pbinds, provided a function or a routine calls next

Tuesday, June 08, 2010

Math with Patterns & Prout

Let's say that you want to pick random numbers? You could use Pwhite. But what if you wanted random multiples of 2? You would still use Pwhite:

(
 Pbind(
  \dur,  0.2,
  \note,  Pwhite(0, 6) * 2
 ).play
)

What if you want to square your random numbers?

(
 Pbind(
  \dur,  0.2,
  \note,  Pwhite(0, 5).squared
 ).play
)

You can do any math operation with a pattern as if it was a number.

(
 Pbind(
  \dur,  0.2, 
  \note,  Pseq([1, 2, 3], 15) * Pseq([1, 2, 3, 5, 4], inf)
 ).play
)

If you want to do math with a pattern and a variable that might change over time, you still need to use a Pfunc:

(
 var num;
 num = 5;
 
 Pbind(
  \dur,  0.2,
  \note, Pwhite(0, 5) * Pfunc({num})
 ).play;
 
 Task({
  5.wait;
  num = 2;
 }).play;
)

Let's say you want step through every item in a shuffled array. You could use Pseq:

Pseq([1, 3, 5, 7].scramble, 4)

Or Pshuf does the same thing:

Pshuf([1, 3, 5, 7], 4)

But what if you want the array to reshuffle after every tme through the loop? (As far as I know) there's not a pre-existing pattern for that. But you can write one with Prout.

(
 var arr, rout;
 
 arr = [1, 3, 5, 7];
 
 rout = Prout({ 
  5.do({
   arr.scramble.do({|item|
    item.yield;
  })})
 });

 
 Pbind(
  \dur, 0.2,
  \note, rout
 ).play
)

A Routine is a function that can pause while it's running. It can also return values more than once. Prout is a wrapper for a Routine. Everytime it gets to item.yield, it returns the item and then waits to be asked for it's next return value. Then it starts running again until it finds another yield or runs out of things to run.

Like Pfunc, Prout evaluates variables as it runs. In the above example, if arr had changed, the Prout would have used the new arr:

(
 var arr, rout;
 
 arr = [1, 3, 5, 7];
 
 rout = Prout({ 
  10.do({
   arr.scramble.do({|item|
    item.yield;
  })})
 });

 
 Pbind(
  \dur, 0.2,
  \note, rout
 ).play;
 
 Task({
  2.wait;
  arr = [12, 15, 19, 21];
  2.wait;
  arr = [-12, -13, -11];
 }).play
)

You could do something similar also, this way:

(
 var arr1, arr2, arr3, rout;
 
 arr1 = [1, 3, 5, 7];
 arr2 = [12, 15, 19, 21];
 arr3 = [-12, -13, -11];
 
 rout = Prout({ 
  3.do({
   arr1.scramble.do({|item|
    item.yield;
  })});
  3.do({
   arr2.scramble.do({|item|
    item.yield;
  })});
  3.do({
   arr3.scramble.do({|item|
    item.yield;
  })})
 });

 
 Pbind(
  \dur, 0.2,
  \note, rout
 ).play;
 
)

You can have as many yields as you want in a Routine - or a Prout.

If you want to pass in arguments, you can use a wrapper function:

(
 var rout;
 
 rout = {|arr, repeats = 5|
  Prout({ 
   repeats.do({
    arr.scramble.do({|item|
     item.yield;
   })})
  });
 };

 
 Pbind(
  \dur, 0.2,
  \note, rout.value([0, 2, 4, 6], 6)
 ).play
)

And, as with Pfunc, you can do math:

(
 var rout; 
 
 rout = {|arr, repeats = 5|
  Prout({ 
   repeats.do({
    arr.scramble.do({|item|
     item.yield;
   })})
  });
 };

 
 Pbind(
  \dur, 0.2,
  \note, rout.value([0, 2, 4, 5], 6) * Pseq([1, 2, 3], inf)
 ).play
)

Yielding only works inside Routines. If you try this in a Pfunc, it does not work:

(
 var func; 
 
 func = Pfunc({ 
  5.do({
   [1, 2, 5].scramble.do({|item|
    item.yield; // you can't do this in a Pfunc!!
  })})
 });
 

 
 Pbind(
  \dur, 0.2,
  \note, func
 ).play
)

Note that it does not create an error message, it just behaves in a bizarre way. Silent errors are the hardest to track down, so beware.

Summary

  • Patterns can behave like numbers for math operations.
  • A Routine is a function that can pause
  • A Prout is like a Pfunc, except that it can stop in the midst of things and resume.
  • Routines can return more than one item, at any point in their execution, whenever you yield a value.
  • You can only yield in a Routine or a Prout

Sunday, June 06, 2010

Pfunc

Let's say you want to control some part of your Pattern from, say, a task. For example, we want to execute some block of code, then wait 5 seconds and then casue the amplitude of a Pbind to fade towards zero. We can do this by using some variables:

(
 var db, pbind, task;
 
 db = -12;
 
 task = Task({
 
  // ...
  
  5.wait;
  
  60.do({
  
   db = db - 1;
   0.1.wait;
  });
  
  "done fading".postln;
 });
 
 // ...
)

db will hold our amplitude in decibels. We're using db because it's logarithmic and will sound right if we just use subtraction to change the value.

task will hold our task. That "..." near the top represents whatever we wanted to do before starting the countdown to the fade-out.

After that, we wait for 5 seconds, then we slowly drop 60 db.

Now for the pbind:

 // ...

 pbind = Pbind(
  
  \dur,   Pseq([0.2, 0.4, 0.6], inf),
  \degree,   Pseq([0, 3, 5, 7, 9, 8, 8, 2].pyramid(1), inf),
  \steapsPerOctave, 10,
  \db,    db
 );
 
 // ...
  

In the line for degree, we see that we're sending the message pyramid to the array. It reorders the array in an interesting way. See the helpfile for Array for more information about it.

Pbinds understand the symbol \db and will convert to amp automatically, as needed. There's a message you can pass to numbers called dbamp. It converts from db to amps. -12.dbamp is 0.25118864315096. Or if you want to go the other way, there's a message ampdb. 0.25.ampdb is -12.041199826559. However, the Pbind will do this for you, so you don't need to convert ahead of time.

Now we can try running the task and the pbind simultaneously:

 // ...
 
 pbind.play;
 task.play;

And when you put this all together and try it, "done fading" prints out, without there having been any fade.

This is because the interpretter evaluates the expression db and then passes the result of that expression as an argument to the Pbind contructor. It is only evaluated that one time. In order to get the current value of db, we need a way to tell the pbind to re-evaluate it every time it comes up. We can do that with Pfunc

Pfunc is a wrapper for functions, so they can be used in patterns. It's constructor takes a function as an argument and then it evaluates that function every time it's asked for a value. Let's re-write our pbind:

 // ...

 pbind = Pbind(
  
  \dur,   Pseq([0.2, 0.4, 0.6], inf),
  \degree,   Pseq([0, 3, 5, 7, 9, 8, 8, 2].pyramid(1), inf),
  \steapsPerOctave, 10,
  \db,    Pfunc({db});
 );
 
 // ...

Remember that functions return the value of their last line. Since we only have one line, it returns that. And, indeed, the amplitude fades away to quiet. However, the Pbind keeps running even after we can't hear it. We can use an if statement to tell it to stop:

 pbind = Pbind(
  
  \dur,   Pseq([0.2, 0.4, 0.6], inf),
  \degree,   Pseq([0, 3, 5, 7, 9, 8, 8, 2].pyramid(1), inf),
  \steapsPerOctave, 10,
  \db,    Pfunc({
       if(db > -72, {
        db
       }, {
        nil
       })
      })
 );

Pbinds stop running as soon as any symbol is paired with nil. Therefore, when db gets to -72 and the Pfunc returns nil, the whole Pbind stops. If the Pbind were in a Pseq, it would go on to the next item.

You may have also noticed that we don't have semicolons everywhere they could be. This is because they separate lines of code, so it's ok to skip them on the last (or only) line in a block of code. It makes no difference to the interpretter if there is a semicolon after nil or not. If you find it easier to use them everywhere, then you should keep doing that.

We could also skip the false function for the if, since it will still return nil if we do:

Pfunc({ if(db > -72, { db }) })

The final version of this example, all together is:

(
 var db, pbind, task;
 
 db = -12;
 
 task = Task({
 
  // ...
  
  5.wait;
  
  60.do({
   db = db - 1;
   0.1.wait;
  });
  
  "done fading".postln;
 });
 
 
 pbind = Pbind(
  
  \dur,   Pseq([0.2, 0.4, 0.6], inf),
  \degree,   Pseq([0, 3, 5, 7, 9, 8, 8, 2].pyramid(1), inf),
  \steapsPerOctave, 10,
  \db,    Pfunc({ if(db > -72, { db }) })
 );
 
 
 pbind.play;
 task.play;
 
)

Summary

  • Decibels can be an easier system to use when computing amplitudes
  • Pbinds will convert between db and amp
  • The messages ampdb and dbamp can convert number values from amps to db or db to amps
  • pyramid is an interesting way of shuffling an array
  • You can put a function in a pattern with Pfunc
  • A Pfunc with get re-evaluated for every event
  • A Pbind stops when it gets a nil value

Problem

  1. Write a short piece of music where two separate Pbinds both access the same variable. They can be in the same Ppar or not.
  2. Write a programme such that one Pbind sets a flag that causes another Pbind to change in some way.

Saturday, June 05, 2010

Polyphony and Sequencing

Chords

Now that we can use Pbinds to make sure our sounds come on time, let's look about how to order them and play more than one note at once. The easiest way to do polyphony is with arrays:

Pbind(\note, [1, 3, 5]).play

The Pbind creates a separate synth for each item in the array.

\note determines pitch as a scale degree in an equal tempered scale. It computes the frequency based on the note number and then uses that for the synth's freq argument. \note can be used with any synthdef.

While note is always equally tempered, it need not be 12-tone ET:

(
 Pbind(
  \note,    [1, 4, 7],
  \stepsPerOctave,  10
 ).play
)

By pairing \stepsPerOctave with 10, the example above gives us 10-tone ET. We could do 24 for a quatertone scale, or use whatever number we'd like.

If, for some reason, we wanted to use MIDI note numbers we could do that. We don't need to use whole numbers, but can use Floats to change the tuning.

Pbind(\midinote, 60.05).play

60.05 is middle C, 5 cents sharp.

Rests

If we want to rest for a note, we can do that by pairing the symbol \rest to any of the symbols that control pitch:

(
 Pbind(
  \dur,   0.2,
  \note,    Prand([1, 4, 7, 9, \rest], 15),
  \stepsPerOctave,  10
 ).play
)

We could also pair rest with \freq, \midinote, or \degree - which is the degree in the scale.

Ppar

If we want to play two Pbinds at the same time, just telling one to play right after the other does not guarantee that they will line up perfectly. To make sure they start in sync, we can use Ppar.

Ppar takes two arguments, a list and the number of repeats. Here's an example from the helpfile:

(
 var a, b;
 a = Pbind(\note, Pseq([7, 4, 0], 4), \dur, Pseq([1, 0.5, 1.5], inf));
 b = Pbind(\note, Pseq([5, 10, 12], 4), \dur, 1);
 Ppar([a, b ]).play;
)

a and b play exactly in time with each other.

If we don't want all the patterns to start right away, we can use Ptpar, which is the same except that the list contains pairs of times and patterns. Each time is how long to delay before starting the pattern:

(
 var a, b;
 a = Pbind(\note, Pseq([7, 4, 0], 4), \dur, Pseq([1, 0.5, 1.5], inf));
 b = Pbind(\note, Pseq([5, 10, 12], 4), \dur, 1);
 Ptpar([ 0.0, a, 1.3, b ], 2).play;
)

Pbind a starts immediately, because it's time is 0. Pbind b starts after 1.3 seconds. The whole thing repeats twice, because of the second argument to Ptpar

If, instead of playing at the same time, we wanted to play one after the other, we can put the Pbinds into a Pseq:

(
 var a, b;
 a = Pbind(\note, Pseq([7, 4, 0], 2), \dur, Pseq([1, 0.5, 1.5], inf));
 b = Pbind(\note, Pseq([5, 10, 12], 2), \dur, 1);
 Pseq([a, b ]).play;
)

And Pseqs can be passed to Ppars and Ptpars and vice versa.

Summary

  • Pairing an array with a symbol in a Pbind makes it play chords
  • \note determines pitch as a scale degree in an equal tempered scale and can be use with any synthdef.
  • \degree is the degree in the scale
  • \stepsPerOctave set the number of pitches in the scale used by \note or \degree
  • \midinote is the midinote number - in Floats
  • Pairing \rest with any of pitch-related symbols will cause a rest
  • We can start two or more Pbinds at the same time with Ppar
  • We can start two or more Pbinds with a pre-set delay between them using Ptpar
  • We can start two Pbinds sequentially by putting them in a Pseq
  • We can put Pseqs in Ppars and vice versa.

Problems

  1. Write a short A B A (refrain verse refrain) piece with a higher pitched line and a lower pitched line, with their own rythms. You can use the default synth for Pbind or one (or more) of your own.

Friday, June 04, 2010

Pbinds

If you've been using tasks to control rhythmic sounds, you may have noticed that sometimes the timing can wander a bit. If you have two separate tasks running at the same time, they can also get out of sync. Tasks don't guarantee exact timing. Also, remember that the language interpreter and the audio server are separate programs. There is a small delay between telling the server to play something and when it actually plays it. It's possible to compensate for this, by giving it instructions slightly ahead of time. Pbinds compensate automatically.

p = Pbind.new.play;

That will repeat forever until you stop it.

p.stop;

A Pbind is a sort of a pattern. Patterns use the information that you provide and make assumptions about the information that you don't. In the above example, we didn't specify anything, so it picked all default values. Let's change the frequency:

Pbind(\freq, 660).play

Pbind.new takes an even number of arguments, as many or as few as you would like. They come in pairs, where the first member is a symbol and the second is an expression. For example:

Pbind(\freq, 660, \dur, 0.2 + 0.3.rand).play

Note that the expressions are evaluated when the Pbind's constructor is called. Therefore, whatever value is returned by 0.2 + 0.3.rand will be the duration for every separate event.

Repeating the same note over and over is rather dull, so there are some patterns that can help.

Prand takes two arguments, an array and the number of times it should run. For example:

(
 Pbind(
  \freq, Prand([330, 440, 660], 6)
 ).play
)

When the Pbind is playing, Prand picks a frequency from the array and uses that for the event. The next time, Prand again picks one of the freqs. It does this 6 times. Then the Pbind stops playing.

You can change the number of repeats to any number you would like, including inf. If you have multiple patterns in your Pbind, it will stop with whichever one ends first:

(
 Pbind(
  \freq, Prand([330, 440, 660], 6),
  \dur, Prand([0.2, 0.4, 0.3], 5)
 ).play
)

That only plays five notes because the second Prand only will return a value 5 times.

You can also play through a list with Pseq:

(
 Pbind(
  \freq, Pseq([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ] * 440, 1),
  \dur, Prand([0.2, 0.4, 0.3], inf)
 ).play
)

Pseq takes three arguments: a list and the number of times that it should play through the entire list. The third argument is an optional offset. Note that in the example, we're multiplying the entire array by 440. Every single element is multiplied, which gives us frequencies in the audible range. Note you can do additional math this way also:

(
 Pbind(
  \freq, Pseq(([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ] * 440) + 10, 1),
  \dur, Prand([0.2, 0.4, 0.3], inf)
 ).play
)

The lists passed to patterns can contain other patterns:

(
 Pbind(
  \freq, Pseq([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ] * 440, inf),
  \dur, Pseq([
     Pseq([0.2, 0.4], 2), 
     Pseq([0.3, 0.3, 0.6], 1)
    ], 3)
 ).play
)

You can change what synthdef the Pbind is playing with \instrument. First, let's define a synthdef:

(
 SynthDef(\example9, {|out = 0, freq, amp, dur, pan = 0, mod = 50|
  
  var pm, modulator, env, panner;
  
  modulator = SinOsc.ar(mod, 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
)

Then, specify it:

(
 Pbind(
  \instrument, \example9,
  \freq,  Pseq([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ] * 440, inf),
  \dur,  Pseq([
      Pseq([0.2, 0.4], 2), 
      Pseq([0.3, 0.3, 0.6], 1)
     ], 3)
 ).play
)

We can set values for any of the synthdef's arguments:

(
 Pbind(
  \instrument, \example9,
  \mod,  Pwhite(20, 100, inf),
  \freq,  Pseq([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ] * 440, inf),
  \dur,  Pseq([
      Pseq([0.2, 0.4], 2), 
      Pseq([0.3, 0.3, 0.6], 1)
     ], 3)
 ).play
)

Pwhite takes three arguments, a low number, a high number and the number of times to run. It then picks random numbers between the low and high values. In the example, we use those to control the modulation frequency of our synthdef.

Some argument names for synthdefs are quite common, like: amp, freq, pan, and out. Pbinds expect to see those argument names and will provide reasonable values if you don't specify. If you have a more original name for an argument, it's a good idea to specify values for it in the Pbind.

(
 SynthDef(\example9a, {|out = 0, freq, amp, dur, pan = 0, mod = 50, modulation_depth = 0.2|
  
  var pm, modulator, env, panner;
  
  modulator = SinOsc.ar(mod, 0, modulation_depth);
  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
)


(
 Pbind(
  \instrument, \example9a,
  \modulation_depth,
     0.3,
  \mod,  Pwhite(20, 100, inf),
  \freq,  Pseq([ 1/1, 3/2, 4/3, 9/8, 16/9, 5/4, 8/5 ] * 440, inf),
  \dur,  Pseq([
      Pseq([0.2, 0.4], 2), 
      Pseq([0.3, 0.3, 0.6], 1)
     ], 3)
 ).play
)

Do not assume that your default values from your synthdef will be used. If you want it to always be a certain value, specify that value.

Summary

  • Pbinds are more accurate than Tasks for playing in time.
  • Pbinds assume several default values, which you can specify if you want to change them.
  • Prand(list, n) picks an item from the given list n times.
  • Pseq(list, n) plays the entire list n times.
  • If you do a math operation with a SimpleNumber and an array, it does the operation for each item of the array and stores the result in an array.
  • Pseqs can contain other patterns, including other Pseqs
  • You can specify what synthdef to play with \instrument
  • Pwhite(lo, hi, n) picks a number between low and high n times
  • Symbols in a Pbind can correspond to synthdef argument names
  • If you have an unusual argument name, it's a good idea to specify a value for it in the Pbind

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?