Introduction to Writing Algorithms.
(In MINC)
Christopher Bailey
What are algorithms: Basically, the word "algorithm" usually refers simply to a way or method of doing a task, with the task usually being of a repetitive nature, or being something that can be described easily in an "overall" way. In computer music, examples of such tasks might be "play a chromatic scale" or "play 200 random notes" or "make a very cloudy tremolating sound." As a tool for composers, in other words, algorithms are most often used to generate textures.
Generally speaking, in computer music, algorithms do two things:
What we want to look at here is how to use algorithms to create simple textures and other sound materials for use in your music. The idea/example illustrated here is rather assinine, but it does cover all the concepts you will need to know for basic algorithmic design.
To make this process easier, I came up with simple way of thinking things through before and while writing code to accomplish a given musical task.
Here it is:
1) Think of and explore your idea, draw pictures, diagrams, and etc. to figure out what's happening in the different musical parameters. How is pitch changing? How is start-time happening? How is duration working? Remember that you don't necessarily have to write all of the notes in their temporal order: some algorithms may splatter notes in time randomly (not in time-order); others may write notes (or other sounds) in rhythm, from first-to-last, time-wise. All sorts of possibilities can happen, so try to open your mind to different ways of solving whatever musical problem you have.
2) Divide job into loops. (Sometimes the task you have can be accomplished with one loop that writes all of the sound involved, other times you may want to have a number of loops write the sounds for different sections of a segment of music, for example.)
3) Basic set-up stuff: Here is where you write statements at the top of the score for things that won't be changing during the algorithmic process. Thus commands like rtsetparams() , opening input and output files with rtinput() and rtoutput() , certain makegen()s, (if they're not going to be changing throughout the score), and so on.
4) Outline each loop. Type your while or for statements, and then add brackets below, into which you will eventually insert your loop(s).
5) Action Statements. Write out the statements that actually make the sound. (typically the instrument name(s), as in WAVETABLE or the like.) Then fill in their parameters with variables.
6) Work outwards from the Action Statements, figuring out how each parameter will be determined each time the loop goes around---pitch,duration,start, etc.
7) Initialize all variables before each loop starts.
8) Run it, test it, fix it, modify it, love it.
Sammy the Serialist Example, cont.: Let's see. . . I have an Evil 12 Tone Row, and I want to make a nice leapy melody, with the pitch-classes cycling through my Evil Row. Of course, they must always come in different octaves. The rhythm--well, I'm an evil serialist, so of course I want it to be based on my row. The duration of any note will be simply to sound until the next note. I want the articulations to be either an Sfp attack, or else a hairpin (<>) kind of articulation. Loudness--well, of course, it, too must be based on the row! HaHa! And stereo placement too!! Ha ha ha!!
So, now you've thought of how your musical excerpt is going to work. Now, go on to the next step--
step 2) Divide your job into as many separate simple-as-possible loops as you can. In general, a single loop should create either a single line, or a uniform or uniformly changing texture.
Sammy Example, cont.: I think I will only need one loop, since I am creating one line of pitches. Later on, for counterpoint, I can use more loops.
step 3) Begin by defining basic stuff, like your output file, your input files, any makegens that stay the same for the entire thing, etc.
Sammy Example, cont.:
rtsetparams(44100, 2)
rtoutput("evil.rows.aiff")
makegen(1, 10,1000, 1, .2, .2, .2, .1)
These are the basic setup items for our script. "Timbre" stays the same--(which, for WAVETABLE is makegen slot 1). But articulation, (i.e.--amplitude envelope, which for WAVETABLE is makegen slot 2, (and usually type 24)) changes, so that will be "in" the loop--well get to it later on.
step 4)Write the outlines of your loop(s). This means, basically, control (while or for) statements, plus (if necessary) a statement at the end of the loop that increments your counter.
while: As long as the expression in () is True, then the stuff below, the statements in {} will execute. After the stuff in {} is done executing, the () expression is tested again, if still True, then the statements in {} are executed again, and so on, until the () expression is False, then the {} stuff is skipped and the program goes on.
Example:
while (x<100)
{
statements, commands, 'n' stuff
x=x+1
}
Here the {} stuff will execute over and over until x=99. Then the {} stuff will execute one more time, then x=x+1=100, and "x<100" will now be a False statement, so the the program will skip the {} stuff and go on.
For : Similar to while, but here, in the initial statement, you tell it the initial value of a counter variable, followed by a semicolon, followed by an expression that has to be True for the {} stuff to be executed (usually that the counter is less than some upper limit, as with while ), followed by another semicolon, followed by a command (usually a change in the counter variable) that you execute each time you go through the loop.
for (x=0; x<9; x=x+1)
{
commands & stuff
}
if: This is another control statement which you may want to use at some point. If the expression is True then the {} stuff will be executed. There can also be an optional else part. If the expression following the if is False then the {} stuff following the else will be executed. If there is no else part, then the program just goes on, skipping the {} stuff after the if statement.
if (x<.5)
{
commands & stuff
}
else
{
commands & stuff
}
Example, cont.:Hi, Sammy here again. . . . Im going to use While type loops in my example.
step 5) Now begin to write the loop. Remember, you DON'T, repeat, DON'T start at the beginning and work your way down. You start at the middle, and work your way back to the beginning. More precisely, you start at the Action Statement(s), and work backwards. The Action Statement(s) is/are usually your instrument statement(s), such as WAVETABLE, or COMBIT, or whatever, or maybe a makegen for envelope (articulation), timbre, etc. the Action Statements usually take a set of Parameters, like pitch, duration, etc., which change each time you go through the loop.
Example, cont.: We are going to have two Action Statements. One, of course, makes the notes:
WAVETABLE(start, dur, amp, pitch, stereo)
The other makes the "articulation" with the appropriate amplitude envelope:
makegen(2, 24, 2000, bla bla bla (we'll fill this in eventually))
So, now our basic loop looks like yay:
while (x<100)
WAVETABLE(start, dur, amp, pitch, stereo)
x=x+1
makegen(2, 24, 2000, bla bla bla bla)
}
step 6)Now, work backwards from your Action Statement(s), considering each Parameter in turn, and figuring out how it is calculated. Generally, there are three types of Parameters:
CONSTANT: This means that the parameter is the same for every note created by the loop. Constants are easy: you just fill them in with the appropriate number. If the loudness is always the same in a loop, for example, you would just fill in the third slot of the WAVETABLE command with the constant loudness. For example:
WAVETABLE(start, dur, 1000, pitch, stereo)
NON-DEPENDENT:This means that every time the loop goes around, the parameter is calculated afresh. If in a loop, the statement dur=random()*2, this means that every time the loop goes around, dur is recalculated, regardless (i.e.in- or non-dependent) of its previous value.
random()= May as well introduce this here: the random() function produces a random value from 0 to 1. Hence x=random() will assign to the variable x, a random number from 0 to 1.
DEPENDENT:This is when a variable's value depends on the value it had last time. A typical standard example is when the start time of the next note you're going to create equals the current start time plus the dur of the current note. (In other words, "start the next note when this one ends.")
As a rule of thumb, dependent statements, like our example (start=start+dur) usually go after the Action Statements. Non-dependent values go before the Action Statements. In other words, you, or rather, the loop, calculates the non-dependent values, then, writes the note, then calculate the Dependent values (for the next note), and then goes back to the beginning of the loop (to do the same for the next note.)
About choosing values for things: There are, in general, 3 ways of choosing values for things. Let's call them RANGE, SELECT and CYCLIC:
RANGE: This is just if you want any random value between x and y, then you just write random()*(y-x)+x, where x < y. Remember that equation--youll learn to love it. It gives you floating-point numbers, (i.e.--with a decimal point and lots of stuff after it); if you need integers, you can use the trunc() function, which just removes the decimal stuff. (trunc(x.y)=x) (It doesnt round, it just removes the nastiness beyond the decimal point.) Because of the truncation, you have to add 1 in the equation to get your range of numbers, so it becomes: trunc(random()*(y-x+1)+x) (Remember, this one is for integers from x to y .)
SELECT: but let's say you want to select from 5 specific values, such as 1, 3, 4, 5, and 6. It would be nice to have these in a table, and then be able to say, "choose a random value from the table." Well, we can do this with sampfunc, and the data , or type 2, makegen. First, we make our table of values that we will select from. Heres an example:
makegen(a, 2, n, 0)
1, 3, 4, 5, 6
a is the ["slot"] number of the table: this is the same in all makegens. If you use more than one table, they should obviously be numbered differently. Also, you shouldn't use the numbers 1, 2, 3, or even 4, because these are often used already by your CMIX instrument; (remember, for example, that WAVETABLE uses slots 1 and 2 for timbre and amplitude envelope , respectively.)
n is the number of values you are going to store.
2 specifies that this is a "data" type makegen (as opposed to a "make a harmonic wave" type (10) or a "make an envelope" (24) type, or any of the other types available (see Luke's makegen page.))
The second line contains the actual table, with values separated by commas.
Sammy example, cont.: Let's make a table for our Evil 12-Tone Row. Heh, heh.
makegen(-3, 2, 12, 0)
0, 2, 8 , 11, 10, 6, 3, 4, 7, 9, 5, 1
OK, now, how do we later choose a random value from this table? The answer is:
sampfunc(t, i)
where t is the table# (referring to the first of the the makegen parameters) and i is the # of the piece of data. (However, note that the data gets numbered from 0, not 1, so the last value in our table is #11, not #12.)
Let's pretend we made that table above, and now, what will happen when we call sampfunc ?
sampfunc(3, 1)=2
sampfunc(3, 0)=0
sampfunc(3, 6)=3
sampfunc(4, 6)=error, because we didn't make a table numbered 4 yet.
sampfunc(3, random()*12 ) choose a random number between 0 and 11.99999, and select that # value. Thus if (random()*11) gave us 4.42533, (automatically truncated to 4) then the sampfunc would give us 10 (item#4=10).
sampfunc(3, random()*n ) The range of random numbers we select is between 0, and n , the number of items. (Note that if you are selecting from, say, 6 items, they are numbered 0-1-2-3-4-5, but you type random()*6 because the highest possible number, 5.99999, would be truncated automatically (by the sampfunc command) to give you 5. Thus you will never get an actual "6" as a result of the randomizing.)
CYCLIC: OK, but let's say we don't want to select randomly from our table, but to cycle through it as we cycle through our loop. If our table had, say, six elements, we could use a counter variable, say c, and each time we went through the loop, at the end, we would say: c=c+1; except we would have to check if c > number-of-elements in the table, and if it was, we would reset it to 0, and the cycling would begin again.
Our example, cont.: So, we are going to want to cycle through the row, over and over, as we go through our loop. So we need a counter, c. Every time we're going to need a row pitch, (or pitch-class, since we'll determine the octave later), we say
pitchclass=sampfunc(3, c)
Later, we say
c=c+1
if (c>11) c=0
That makes sure c keeps : climbing to 11, skipping back to 0, then climbing to 11, skipping back to 0, then climbing to 11, skipping back to 0 . . . . etc.
So now let's go ahead and consider each parameter of our Action Statements, one by one, gradually filling in our algorithm.
First, let's look at what we have so far:
while (x<100)
{
makegen(2, 24, bla bla bla)
WAVETABLE(start, dur, amp, pitch, stereo)
x=x+1
Well start with the first thingy, the envelope makegen. Remember, we wanted to have two choices for envelope, either Sfp, or a <> articulation (a hairpin). These can be chosen randomly. (I.e.--this is a non-dependent decision--what was decided the last time we went through the loop doesn't matter to us.) We can generate a random number 'twixt 0 and 1, and if the number is <= (less than or equal to) .5, then do an Sfp, if it's >.5, then do a <> articulation. In other words:
q=random()
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,19,.5,30,0)
else makegen(2, 24, 2000, 0,0,1,1,2,0)
The first, if you inspect it, is an Sfp envelope, the second, a <> envelope.
Oh-kee, now for start-time. Start-time is dependent. It depends on the value that start-time had the last time we went through the loop, and the last duration used. In other words, the start-time of the current note depends on when and how long the last note was. Since it's dependent, we put it after the Action Statement(s). In our case, it's simply going to equal start+dur. So here's our growing algorithm now:
while (x<1000)
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,19,.5,30,0)
if (q>.5) makegen(2, 24, 2000,0,0,1,1,2,0)
WAVETABLE(start, dur, amp, pitch,stereo)
start=start+dur
x=x+1
Now we consider dur. This baby is non-dependent , it doesn't care what the last dur was, but it is CYCLIC. We want to cycle through the row to get durations. So here's where we use our counter, c. To reiterate what we developed a while back, the statements
c=c+1
if (c>11) c=0
will keep our counter going round and round as we go round the loop.
Then, before our Action Statements, we put an equation giving us the value of dur:
dur = (sampfunc(3, c) + 2) / 20
Remember, what this says is: get the value from our row table (table #3) that c points to. Then, add 2, so we don't get any 0 durations (we could have added any positive number), then, divide by 20 (again, that's arbitrary) to make the durations shorter (turning up the tempo (I like things fast (I do live in New York, after all.)))
OK, let's check where we are now:
rtsetparams(44100, 2)
rtoutput("evil.rows.aiff")
makegen(1, 10 , 2000, 1, .2, .2, .2)
makegen(3, 2, 12, 0)
while (x<1000)
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,19,.5,30,0)
if (q>.5) makegen(2, 24, 2000, 0,0,1,1,2,0)
dur=(sampfunc(3, c)+2)/20
WAVETABLE(start, dur, amp, pitch, stereo)
start=start+dur
c=c+1
if (c>11) c=0
x=x+1
Amp and stereo follow similar procedures. For amp, we have to add a number (12 again, I guess) so we don't get an amp of 0 once every 12 notes, (which would be pointless (but then again, this whole example is rather pointless)), then multiply it all by 1000 to bring the amplitude values into an audible range (remember, RTcmix amplitudes run from 0 to 32768); for stereo, which as you remember, must be from 0 to 1 (left to right), we must "shrink" the range from 0 - 11 to 0 - 1, by dividing by 11.
Hence:
amp=(sampfunc(3,c)+12)*1000
stereo=sampfunc(3,c) / 11
Pitch is not too much more complicated. Remember that octave-pitch-class notation has 2 parts, an octave argument, followed by a . , then a pitch-class , or pc argument. So, we will generate a random octave, and then look up our pitch-class from the table as we discussed earlier, but divide it by 100, so that it will come after a decimal point. (4 divided by one hundred will be .04, or 10/100 will be .10) Then, we just add them, and
taadaa!! instant pitch. (In other words, pc 2 will become .02, if we add that to octave 8, we get 8.02, middle d.)Hence:
oct=trunc(random()*5)+6
gives us a random octave between 6 and 11.
(since random() gives us numbers with lots of decimal stuff, and we just want integers for the octave, no decimal stuff, we use trunc(). So if random()*7 gives us 6.632453645, then trunc(6.632453645) gives us 6)
The following statement gives us our complete pitch argument:
pitch=oct + (sampfunc(3, c)/100)
step 7) Initialize all variables. Write all the variables you've used, at the top of your score, and set them equal to 0 (or another initial value, if necessary).
Let's check the whole example out now:
step 8) Run it, test it, fix it, love it!!!
Whelp, that's about it.
(fun
with fonts)Let's just review the 8 steps:
1) Explore your idea, what's happening in the different musical parameters . .
2) Divide job into loops
3) Basic stuff: rtsetparams, output files, input files, makegens, etc.
repeat 4-7 for each loop:4) Outlines of loop (while or for statements, counters, etc.)
5) Action Statements
6) Work outwards from
Action Statements , figuring out each parameter.7) Initialize variables.
8) Run it, test it, etc.
Note: This tutorial was just to get you started using algorithmic compositional tools. There are an infinite number of approaches to algorithmic composition, and we encourage anyone to strike out in different directions. Every new approach will bring new musical results, and ways of thinking about music, and thats always exciting. . . .