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

Tuesday, August 03, 2010

Event

The most important subclass of Dictionary is Event:

(
 var evt;
 evt = Event.new;
 evt.play;
)

Much like Arrays can be declared just by square brackets, Events can be declared just with parenthesees:

( ).play;

The sonic results of that line of code might sound familiar to you:

Pbind.new.play

This is not a coincidence! Patterns create Events and play them sequentially. Compare:

Pbind(\freq, 660, \pan, -1).play

and

(freq: 660, pan: -1).play

Note that the syntax for the Event is (key: value, key2: value2) . You can have as many key: value pairs as you want. In the example above, freq and pan are both symbols, both in the Pbind and the event.

Events are Dictionaries and they are created with a bunch of default keys and values. Although, you can use any sort of object as the key for a Dictionary, you should only use symbols as keys for an Event.

The Pbind modifies elements of a default Event and then plays the results of that. To find out what interesting thigns can be changed in an Event, (and therefore also in a Pbind), check out it's helpfile, specifically the section called "Useful keys for notes."

In your Pbind, you can get access to the current event:

(
 Pbind(
  \freq, Pseq([550], 1),
  \event, Pfunc({ |evt|
     evt[\freq].postln;
    })
 ).play
)

The current event is passed as an argument to the function in Pfunc and also the one in Prout. Here, we have created a key, \event, that doesn't have meaning to the Synth, nor to the Event. This is allowed. For example:

(
 Pbind(
  \freq, Pseq([550], 1),
  \foo, \bar
 ).play
)

Every time the Pbind makes an event, it goes through the list of keys and values in the order that it recieves them. For example:

(
 Pbind(
  \event, Pfunc({ |evt|
     "Pan before: %".format(evt[\pan]).postln;
    }),
    
  \pan, Pseq([-1], 1),
  
  \event2, Pfunc({ |evt|
     "Pan after: %".format(evt[\pan]).postln;
    })
 ).play
)

The first postln prints out 0, because that's the default value for \pan. The second postln prints out -1. Then it goes back to the first one because the Pbind does not run out the Pseq returns a nil.

Let's make the duration depend on the frequency:

(
 Pbind(
  \freq, Prand([440, 550, 660, 770], 10),
  \dur, Pfunc({ |evt|
   
     var dur;
     
     dur = evt[\freq] / 1000
    })
 ).play
)

Events are one way you can tie together two keys in an event. You can also do this with arrays:

(
 Pbind(
 
  [\freq, \dur],
   Prout({
    var arr, freq, dur;
    
    arr = [440, 550, 660, 770];
    
    10.do({
     
     freq = arr.choose;
     dur = freq / 1000;
     
     [freq, dur].yield;
    })
   })
 ).play
)

Two or more keys can be grouped together in an array. Then, the stream, which can be a Prout, a Pfunc or any other P-thing, must return an array which is the same length as the key array. The first item in the returned array gets paired with the first item in the key array, the second item goes with the second key and so forth.

Summary

  • Event is a subclass of Dictionary.
  • You can declare an event by just using parenthesis.
  • You should only use Symbols as keys for Events.
  • Patterns work by creating events over and over again.
  • You can access events in Pfunc. It's passed to the function as an argument.
  • You can use keys in your Event or Pbind that are not meaningful to the Pbind, the Even or the Synth.
  • Events can be used to tie together two different elements in a Pbind.
  • Arrays can also be used to tie together two different elements in a Pbind.

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

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.