Week 2: Equations/Probabilities/Markov Chains



click here for some additional things to try based on this class.




Downloads

If you would just like to grab the code we wrote in class (or the MAX/MSP patches) without having to go through the rest of this web page the following package (gziped and tar'ed) contains the source code and RTcmix scorefiles:


Equations

We started with a very basic idea -- take an equation for a simple parabola (y = x^2) and use the output to control a "musical parameter"; in this instance we chose to have the equation control the frequency of a series of generated notes. The most important aspect of this simple exercise is to understand scaling and mapping (shifting) of the equation to the range of values we desire for the particular parameter. For this example, we wanted to achieve a rising pitch followed by a decreasing pitch that would track an inverted parabola.

The basic parabolic equation y = x^2 creates a graph that looks like this:

there will be a graph here

There are several problems with the basic equation. First of all, it goes in the opposite direction from the sonic effect we want. Secondly, the output numbers are wrong for our purpose -- frequencies between 0 Hz 4 Hz, while surely fun to have around, won't really create the rising/falling pitch that we'd like.

There are two approaches to dealing with these problems. The first is to change the equation to output the numerical values we want directly. The second is to take the numbers resulting from the equation and then operate on them to move them to the range we prefer. In this case, we'll do both.

The first thing is to change the direction of the equation. This is easy -- just change the sign of the equation to generate negative values instead of positive values:

there will be a graph here

Next we'll shift the equation up to a partly positive range to give us a span of values that we can scale up to appropriate frequencies. This is done by adding a constant value to the equation:

there will be a graph here

Now we have a nice, "well-behaved" equation that will give us values between 0.0 and +4.0 as we move from x = -2.0 to x = +2.0. At this point, it is trivial to translate this into a scorefile to generate the notes. We do need to make some decisions about how the sound will be generated -- we'll create 40 separate notes using the WAVETABLE instrument (see the documentation for WAVETABLE for information describing how the instrument works), and we'll let each one last for 1 second. We'll also generate one note every 0.1 seconds to create a snazzy overlapping effect that will surely shake the foundations of serious music. Finally, we will have our notes start with a base frequency of 100 Hz and reach a height of 500 Hz at the top of the parabolic trajectory:

Easy as pie, eh?

A couple of notes -- the line

is the translation of our modified parabolic equation into RTcmix scorefile 'notation'. Also the line accomplishes the scaling/shifting of the <0.0 -- 4.0> output of the equation to the <100.0 -- 500.0> Hz frequency range. And of course moves the starting time ahead by 0.1 seconds, and causes x to change from -2.0 to 2.0 over the course of 40 steps.

Next, we decided to control several parameters of a slightly more complex synthesis technique (FM) using an algorithmic/equation approach. Basic FM requires that you specify the frequency of the carrier, the frequency of the modulator and an index (or depth of FM) for each note (see the documentation for FMINST for more information.) One way to accomplish this would be to define two independent equations that would generate values for the three parameters. This is a "one-to-many" mapping, where each equation would essentially be a function of time (NOTE: the above simple parabola example is also a function of time -- x varies in direct proportion to the start time of each note).

A more interesting technique (perhaps) would be to link the parameters together, where the values of carrier/modulator/index would be somewhat dependent on each other. This would define a "surface" of possible values; the shape of the surface would be constrained by the equation we used to link the parameters together. We worked from a modified version of the simple parabolic equation, linking the frequency of the carrier to the frequency of the modulator to produce the index: z = (x^2) - (y^2):

there will be a graph here

"x" became the carrier frequency, "y" the modulator, and "z" the resultant index. The surface above decribes the set of possible combinations of these three parameters. By changing how we vary x and y over time, we can 'explore' parts of this surface, discovering bold new combinations of carrier/modulator/index never before heard by human ears!

The following scorefile demonstrates this:

An interesting inversion of the above procedure is to vary z instead of x and y, or to use a cursor to specify where on the surface to generate parameters for the instrument. This approach is quite popular among contemporary "hard-core" algorithmic music types. Michael Gogins' article Iterated Function Systems Music in the Spring, 1991 (v. 15) issue of the Computer Music Journal is a good example of this. (NOTE: the Computer Music Journal is available at the Music Library in Dodge Hall).


Probabilities


As fascinating as these equation-driven sounds can be, many composers are after a sound that is a little less "computer-music-like". The direct mapping of abstract mathematical constructions onto sonic parameters can yield a rather 'perfect' sound -- and music in the Real World is often much less than perfect.

The quick and dirty way to change this is to simply inject a bit of randomness (or actually pseudo-randomness on contemporary digital machines, but random enough to fool my ears!) into the generated parameter values. The following RTcmix score does exactly this. It creates a "cloud" of notes that follow the simple parabolic frequency trajectory used in the first scorefile above:

The score creates four discrete notes for each step of start and x. Each note uses the base value of freq that is generated by the y = x^2 equation, but adds a random amount between 0 and 200 to it. The result is a wobbling set of pitch trajectories that follow the basic parabolic curve:

there will be a graph here


[NOTE: Although the above examples are created using RTcmix, they can also be realized quite easily in other computer music languages (such as MAX/MSP). The algopatches.sit stuffit archive contains two MAX/MSP patches doing the simple parabola tracking (algo1) and the "cloud" of random trajectories (algo2) developed above.]

As powerful as this technique of "windowing" a random value can be, it is possible to get much more subtle in our use of randomness. The use of different types of randomness allows us to control things like 'tendencies' towards particular values, or to generate random numbers with greater or lesser likelihoods in certain ranges. This is done by employing different probability distributions. The good news is that the generating equations for a wide variety of probability distributions already exist (an early Computer Music Journal titled A Panoply of Stochastic Cannons (Spring, 1979, vol. 3 -- also available through the Music Library) by Denis Lorraine has a good description/listing of a number of musically useful probability distribution equations).

In class, we used one of the simplest and most common probability distributions, the Gaussian or "normal" distribution (also known as the infamous "Bell Curve"):

there will be a graph here

The appropriately-scaled equation for this distribution will generate "random" numbers between 0 and 1, but the probabilty of getting a number close to the 0.5 is much greater than a number near 0 or 1.


[NOTE: The program gauss.c in the eq-prob-markovprogs.tar.gz package of programs demonstrates the output of the gauss() function we will be using.]

The first thing we tried with the Gaussian probability distribution is a simple mapping of generated numbers onto one octave of a chromatic scale. We were hoping to achieve a "pitchiness" centered around F# due to the higher probability of generating a random number in the middle of the octave (0.5). The following C program (diationic1.c) prints an RTcmix scorefile for the STRUM instrument that does this:

Note the use of a "sieve" approach to map the values returned by gauss() onto the chromatic octave. Starting with the low end of the probability number range (< 0.1) we just go up the scale until we hit a matching number: The results weren't quite what we expected. Although the generated notes did indeed cluster around F#, our sense of a single "pitchiness" was undermined by the chromatic nature of the surrounding, but still highly probable, notes (F-F#-G).

A better approach would be to use what we know of the tonal system to make pitches in the scale more probable that would reinforce a specific tonality. The following program (diatonic2.c) attempts to do just that. It writes an RTcmix scorefile for the WAVESHAPE instrument, placing notes that reinforce a tonality of F (F, Bb, C, etc.) at the center of the Gaussian distribution:

Instead of using a "sieve" approach to map the Gaussian numbers onto specific pitches, we used an array of pitches constructed so that the pitches we wanted with a higher probability were located close to array index value 5. Then we multiplied the numbers from gauss() by 10, moving the, from the range 0-1 to 0-10, and we used the integer part of the generated number as an index into our prioritized array. Pretty sneaky, eh?

Our final use of the Gaussian distribution was to generate values for time points instead of pitches. Our goal was to produce a semi-convincing simulation of a group of people clapping hands in unison, sort of like what you might experience at a sporting event. Using the first part of the Gaussian curve as a guide, it is possible to create the 'thwump' of a crowd attempting to clap a repeated rhythm together.

The method used to do this is very straighfoward -- just take a repeated time-step and add to it a scaled-down version of the values coming from the first half of the Gaussian curve. The following program does this, using as input a file of individual handclaps as a source soundfile:

The numbers used in claparray[] were generated by using a soundfile editor to fine the starting time of the individual handclaps in the source soundfile. For each "clap" generated in the 30 handclaps for every time-step (beat), we chose randomly one of these source handclaps.

Note that the following section of code (used to produce the "start point offset" from each time-step starting point) makes use of only the 'rising' portion of the Gaussian distribution:

We take the number from the portion of the distribution, scale it appropriately, add it to the base time, and viola! -- we get the 'thuwmp' of a bunch of imperfect humans trying to work their hands together. What fun!


Markov Chains


In one sense, the above use of probabilities is relatively straightforward. The probability distribution is mapped directly onto a musical parameter for a single note or event -- every other event is independent from the others. Markov chains are probabilities that depend on earlier probabilistic decisions. This endows them with the ability to represent change over a span of time in a way that simple probabilities cannot. Earlier events can influence the unfolding of later events; a very powerful tool for producing musical passages.

The basic idea behind Markov chains is quite simple. Given the occurence of some event E1, then the next events E2, E3, etc. have a certain probability of subsequently happening. Each of these events can lead to another probabilistic branching. The following graph shows how this works:

there will be a graph here

To see this idea in action, we took a set of simple garage-band blues guitar riffs (the set of riffs can be generated using the "blues" program in the eq-prob-markovprogs.tar.gz package) -- seven total -- and created a set of Markov dependencies according to the following graph:

there will be a graph here

Note the recursion, or return back to the "base" riff, that keeps the music happening until we decide we've had enough.

The following code realizes the Markov graph for the blues guitar riffs (markovblues.c):

The Markov graph is realized as a set of "if-then-elseif" constructions, with each decision being governed by a call to the brrand() random number generator and a comparison with the particular probability associated with each branch.

The striking thing about this example is that the probabilities were working at a fairly 'high' level in the musical construction -- all of the basic work of note-creation was encapsulted in the various riff*() functions that were being selected. We generally took a lower-level approach in most of the class examples, using algorithmic and probabilistic processes to set the values of parameters for individual notes. These same processes can indeed be applied to greater spans of musical data, to the point where a single mathematical process can be used to unfold an entire piece of music.

See, composition doesn't have to be hard!


Things to Try:

1. Try different equations to generate single or multiple note parameters (my personal favorite from the Apple Graphing Calculator demos is the following:)

there will be a graph here

2. Model a varying drum pattern using Markov techniques. Try altering the probabilities of each branch as time passes.

3. Apply a probabilistic approach to the construction of timbre.