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!

1 comment:

Noiseconformist said...

Charles, I'm thrilled of your efforts! Great! Really!
I'm a true "analogue head" and love my (mostly self-built) hardware,
but I keep looking at SC for a while now starting to sense its vast potential.
The majority of tutorials I found so far are less explanatory.
I think I'll dedicate to it some serious time quite soon!
Thanks a million!