Showing posts with label task. Show all posts
Showing posts with label task. Show all posts

Wednesday, October 13, 2010

More Buffers

Waiting

Last time, we learned that the language does not wait for the server to finish loading buffers before it carries on with the programme, which could lead to a situation where we instruct the server to start playing a buffer that hasn't yet loaded. This will fail to play correctly. Fortunately, there are a few ways to make sure this doesn't happen.

If we use a Task (or a Routine), we can tell it to pause until the server has caught up.

s.boot;
 
(
 var buf;
 
 SynthDef(\playBufMono, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).add;

 buf = Buffer.read(s, "sounds/a11wlk01.wav");

 Task.new({
  
  s.sync; // wait for the server
  Synth(\playBufMono, [\out, 0, \bufnum, buf.bufnum, \rate, 1])
  
 }).play
)

The s.sync makes the Task wait for the server to get caught up with the language. The SynthDef, like the buffer, is also asynchronous, so the sync gives everything a chance to get caught up.

We can also give the buffer an action. This is a function that gets evaluated when the buffer has finished loading. This might be a good idea when we are going to load several buffers, but don't need to use all of them right away. We could use the action, for example, to set a flag so our programme knows it's ok to start using the buffer:

s.boot;
 
(
 var buf, bufloaded;
 
 SynthDef(\playBuf, {| out = 0, bufnum = 0, rate = 1, 
       dur = 0.2, amp = 0.2, startPos = 0 |
  var scaledRate, player, env;
  
  env = EnvGen.kr(Env.sine(dur, amp), doneAction: 2);
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, 
       startPos: startPos, loop:1);
  Out.ar(out, player * env)
 }).add;

 bufloaded = false;
 
 buf = Buffer.read(s, "sounds/a11wlk01.wav", 
  action: { bufloaded = nil});


 Pseq([
  Pbind( // play this Pbind first
   \scale,  Scale.gong,
   \dur,   Pwhite(1, 3, inf) * 0.001,
   \degree,  Prand([0, 2, 4, \rest], inf),
   \amp,  0.2,
   \test,  Pfunc({ bufloaded }) // when this is nil, this Pbind ends
  ),
  Pbind( // next play this one
   \instrument, \playBuf,
   \bufnum,  Pfunc({buf.bufnum}), // use the buffer, now that we know it's ready
   \dur,  Pwhite(0.01, 0.3, 20),
   \startFrame, Pfunc({buf.numFrames.rand}),
   \amp,  1
  )
 ], 1). play

)


Recall that when a Pbind gets a nil, it stops playing and the pattern goes on to the next section, so setting the flag to nil, in this case, advances the piece. Because the second buffer has not yet loaded when the interpreter first evaluates the second Pbind, buf.numFrames will still be nil. Therefore, we put that into a Pfunc so that it gets evaluated when the Pbind plays, instead of when the interpretter first looks at the code.

In that example, we don't always start playing back at the start of the Buffer, but instead offset a random number of samples (called "Frames" here). Coupled with the short duration, this can be an interesting effect.

When you are writing your own actions, you can also do a more typical true / false flag or do anything else, as it's a function. For example, it's possible to combine the two approaches:

s.boot;
 
(
 var buf1, syn, pink;
 
 SynthDef(\playBuf1, {| out = 0, bufnum = 0, rate = 1, loop = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, loop: loop,
       doneAction:2);
  Out.ar(out, player)
 }).add;

 SynthDef(\playBufST, {| out = 0, bufnum = 0, rate = 1, loop = 1,
       dur = 1, amp = 0.2 |
  var scaledRate, player, env;
  
  env = EnvGen.kr(Env.sine(dur, amp), doneAction: 2);
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(2, bufnum, scaledRate, loop:loop);
  Out.ar(out, player * env)
 }).add;

 buf1 = Buffer.read(s, "sounds/a11wlk01.wav");

 Task.new({
  
  s.sync; // wait for buf1
  syn = Synth(\playBuf1, [\out, 0, \bufnum, buf1.bufnum, 
       \rate, 1, \loop, 1]);  // play buf1

  pink =  Buffer.read(s, "sounds/SinedPink.aiff",
   action: {  // run this when pink loads
    syn.set("loop", 0);
    syn = Synth(\playBufST, [\out, 0, \bufnum, pink.bufnum,
        \rate, 1, \loop, 1, \dur, 10]); // play pink
   });
    
  
 }).play
)

In the above example, we first wait for the allwlk01 buffer to load and then start playing it. While it's playing, we tell the SinedPink buffer to load also and give it in action. When it has loaded, the action will be evaluated. The action tells the first synth to stop looping and then starts playing a new Synth with the new buffer.

If we wanted to, we could use the action to set a flag or do any number of other things.

Recording

We don't need to just play Buffers, we can also record to them. Let's start by allocating a new Buffer:

b = Buffer.alloc(s, s.sampleRate * 3, 1)

The first argument is the server. The second is the number of frames. I've allocated a 3 second long Buffer. The third argument is the number of channels.

Now, let's make a synth to record into it:

 SynthDef(\recBuf, {| in = 1, bufnum, loop = 1|
 
  var input;
  input = AudioIn.ar(in);
  RecordBuf.ar(input, bufnum, recLevel: 0.7, preLevel: 0.4, loop:loop, doneAction: 2);
 }).add;

AudioIn.ar reads from our microphone or line in. Channels start at 1, for this UGen.

RecordBuf takes an input array, a bufnum and several other arguments. In this example, we're scaling the input by 0.7 and keeping previous data in the buffer, scaled by 0.4. We'll keep looping until instructed to stop.

We can mix this with a stuttering playback from earlier:

s.boot;
 
(
 var buf, syn, pb;
 
 SynthDef(\playBuf2, {| out = 0, bufnum = 0, rate = 1, 
       dur = 5, amp = 1, startPos = 0 |
  var scaledRate, player, env;
  
  env = EnvGen.kr(Env.sine(dur, amp), doneAction: 2);
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, 
       startPos: startPos, loop:1);
  Out.ar(out, player * env)
 }).add;
 
 SynthDef(\recBuf, {| in = 1, bufnum, loop = 1|
 
  var input;
  input = AudioIn.ar(in);
  RecordBuf.ar(input, bufnum, recLevel: 0.7, preLevel: 0.4, loop:loop, doneAction: 2);
 }).add;
 
 buf = Buffer.alloc(s, s.sampleRate * 3, 1);
 
 Task({
  
  s.sync; // wait for the buffer
  
  syn = Synth(\recBuf, [\in, 1, \bufnum, buf.bufnum, \loop, 1]); // start recording
  
  2.wait; // let some stuff get recorded;
  
  
  pb = Pbind( // play from the same buffer
   \instrument, \playBuf2,
   \bufnum,  buf.bufnum,
   \dur,  Pwhite(0.01, 0.5, 2000),
   \startFrame, Pwhite(0, buf.numFrames.rand, inf),
   \amp,  1
  ).play;
  
  5.wait;
  
  syn.set("loop", 0);  // stop looping the recorder
  
  3.wait;
  pb.stop; // stop playing back
 }).play
)

Summary

  • Buffers load asynchronously and we can't count on them to be ready unless we wait for them.
  • One way to wait is to call s.ync inside a Task or a Routine.
  • We can start playing back a Buffer from any sample with the startFrame argument.
  • Buffer.read has an action argument, to which we can pass a function that will be evaluated after the Buffer has been read on the server.
  • We can allocate empty Buffers on the server with Buffer.alloc.
  • AudioIn.ar starts with channel 1, even though most other input busses start at 0.
  • RecordBuf.ar records to a Buffer.

Problems

  1. Write a programme that opens and plays several Buffers. Make sure that every Buffer is ready before you play it. Spend the least possible amount of time waiting.
  2. Write a programme that reads a Buffer, starts playing it back in some way and the starts recording to the same Buffer. You can use AudioIn.ar or check out InFeedback.ar. Note that, as the name implies, you may get feedback, especially if you are playing back in a linear manner at a rate of 1. Try other rates or ways of playing back to avoid this.
  3. Find a short audio file that contains some beat-driven audio. If you know the number of beats in the file, could you make an array of startFrames? Write a Pbind that does something interesting with your file and your array.

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!