Showing posts with label control structures. Show all posts
Showing posts with label control structures. Show all posts

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.

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).