Showing posts with label if. Show all posts
Showing posts with label if. Show all posts

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