Introduction to Writing Algorithms.
Christopher Bailey
Hi!! I'm Sammy the Serialist, and I'm going to show you, through a nice example, the basic steps in creating a CMIX algorithm. Along the way, we're going to use Chris Bailey's 6-Step Method for Creating Algorithms.
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."
Generally speaking, in computer music, algorithms do two things:
1) make your life easier: you can use them to do some of the "thinking" for you (but remember, you have to teach the machine "how to think"), creating textures where the details don't matter as much as the overall effect; this is the most common kind of algorithmic composition: using algorithms to create clouds, lines, or whatever: soundfiles that you will insert into a larger piece.
2) for the exploration of things like style modeling, genetics, games, physical laws (gas particles dancing around), artificial intelligence, etc. This gets into "deeper" aspects of algorithmic composition, which we won't get into here (take Brad Garton's advanced class next year for a very interesting introduction to this wonderful world. . .)
What we want to look at here is how to use algorithms to create 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.
Here is
Chris Bailey's Basic 6-Step Process:1) Think of your idea, explore your idea, draw pictures, figure out what's happening in the different musical parameters.
2) Divide job into loops
3) Basic set-up stuff: output files, input files, makegens, etc.
4) Outline each loop (
while or for statements, counters, etc.)5) Action Statements
6) Work outwards from the Action Statements, figuring out each parameter.
7) Initialize variables
8) Run it, test it, etc.
step 1) Think of your idea. Draw a picture. Figure out what's happening in terms of pitch, rhythm (start-times), durations, articulations (amplitude envelopes), loudness, stereo placement, and whatever other factors are involved in the instrument(s) you're using.
Sammy the Serialist Example, cont.: Let's see. . . I have an Evil 12 Tone Row, and, vell, I vant to make a nice leapy melody, where the pitches cycle through my Evil Row. But I vant them to come always in different octaves! The rhythm--vell, 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--vell, of course, it, too must be based on the row! HaHa! And stereo placement too!! Ha ha ha!!
OK, 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. 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
output("/sndgr/sammy/evil.row.music.snd")
makegen(1, 10, 1000, 1, .2, .2, .2, .1)
These are the basic things for our script. "Timbre" stays the same--i.e. makegen type 10. But articulation, makegen 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)
So, now our basic loop looks like yay:
while (x<100)
{
wavetable(start, dur, amp, pitch, stereo)
makegen(2, 24, 2000, bla bla bla bla)
x=x+1
}
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.independent) 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 calculate the non-dependent values, write the note, calculate the Dependent values (for the next note), and then go back to the beginning of the loop (and write 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, (which, yes, should be negative, dont ask why for now), is the 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 waveshape 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.)
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()*11 ) choose a random number between 0 and 11, 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-1) ) The range of random numbers we select has to be between 0, and 1 less than n , the number of items.
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 want to cycle through the row, over and over, as we go through our loop. So we need a counter, c. Every time we need a row pitch, we say
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, there were two choices for envelope, either Sfp, or a <> articulation (a hairpin). These can be chosen randomly. (The type of makegen is non-dependent.) We can generate a random number 'twixt 0 and 1, and if the number is <= (lees 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,9,.3,10,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 last value of start-time, and the last duration used. In other words, it 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)
{
q=random()
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,9,.5,10,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 we have to have our counter, c. At the end of the loop, we have to put
c=c+1
if (c>11) c=0
to 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) + 12) / 4
What this says is: get the value from our row table (table #3) that c points to. Then, add 12, so we don't get any 0 durations (we could have added any positive number, but 12 is just so . . . evil and serialist), then, divide by 4 (again, that's arbitrary) to make the durations shorter. (I like things fast.)
OK, let's check where we are now:
output("evil.rows.snd")
makegen(1, 10 , 2000, 1, .2, .2, .2)
makegen(-3, 2, 12, 0)
0, 2, 8 , 11, 10, 6, 3, 4, 7, 9, 5, 1
while (x<1000)
{
q=random()
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,9,.5,10,0)
if (q>.5) makegen(2, 24, 2000, 0,0,1,1,2,0)
dur=(sampfunc(3, c)+12)/4
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)); 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
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 have to generate a random octave, and then lookup our pc from the table as with the other parameters, 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()*7)+3
gives us a random octave between 3 and 9.
(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:
output("evil.rows.snd")
makegen(1, 10 , 2000, 1, .2, .2, .2)
makegen(-3, 2, 12, 0)
0, 2, 8 , 11, 10, 6, 3, 4, 7, 9, 5, 1
x=0
q=0
dur=0
c=0
amp=0
stereo=0
oct=0
pitch=0
start=0
while (x<100)
{
//* choose articulation (envelope) *//
q=random()
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,9,.5,10,0)
if (q>.5) makegen(2, 24, 2000, 0,0,1,1,2,0)
//* choose duration *//
dur=(sampfunc(3, c)+12)/4
//*choose amplitude and stereo position *//
amp=sampfunc(3,c)+12
stereo=sampfunc(3,c)/1
//*choose pitch*//
oct=trunc(random()*7)+3
pitch=oct + (sampfunc(3, c)/100)
//*Action Statement *//
wavetable(start, dur, amp, pitch, stereo)
//* update dependent variables*//
start=start+dur
//* update the cycling counter *//
c=c+1
if (c>11) c=0
//* update the main loop control counter*//
x=x+1
}
step 8) Run it, test it, fix it, love it!!!
This baby will work, but there are some inherent weaknesses. Namely, a lot of parameters are all based on the exact same row cycling in the exact same way. The only ones that aren't are articulations (the makegen(2, , , ) ) and octave numbers. So, the whole thing's basically going to spit out the same 12 sounds in a row, over and over, with the only differences being in articulation, and in octave transposition. Probably the most tiresome thing would be the rhythm, which will repeat over and over the same 12 durations.
If only we could have some random number that we added to the durations, or the pitches, or both, (or subtracted), and that number would change every 12 notes. (This would be like different transpositions of the row.) The solution (or I should say, a solution, because there are always several solutions) is simple, yet subtle. Try to think of one, before looking below at mine (which starts after initialization statements, to save trees).
add=trunc(random()*12)
while (x<100)
{
q=random()
if (q<=.5) makegen(2, 24, 2000, 0,0,1,1,2,.5,9,.5,10,0)
if (q>.5) makegen(2, 24, 2000, 0,0,1,1,2,0)
dur=(sampfunc(3, c)+12+add) / 4
amp=sampfunc(3,c)+12
stereo=sampfunc(3,c)/1
oct=trunc(random()*7)+3
pitch=oct + ( sampfunc(3, c)+add / 100 )
wavetable(start, dur, amp, pitch, stereo)
start=start+dur
c=c+1
if (c>11)
{c=0
add=trunc(random()*12)
}
x=x+1
}
The solution here is to put in a variable called add, which we add to the value obtained from the sampfunc table. We do this in the statements that figure out the value of dur and of pitch. We want it to change every 12 notes, so we need some kind of signal to tell us when we've done 12 notes. Since that variable c loops back to 0 every 12 notes, we can "latch on" to its "test" statement, the one that tests when c has reached 12. That's why we put the add=trunc(random()*12) where we did: when c gets set back to 0, we also generate a new value for add .
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: 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 stuff is 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. . . .