Week 4: L-systems

the Bumbleball!



click here for assignments from this class.

click here to go directly to the L-systems description (and links).



Downloads

You may wish to download the following two packages (gzipped and tar'ed) containing the code and Makefiles discussed below:


This week we began work on modeling the Bumbleball, that amazing off-center-cam toy that bounces around in a rather chaotic manner. One of the first decisions in starting a modeling project (musical or otherwise) is deciding exactly what aspects of the real device will be captured in the model. In the case of the bumbleball, we decided that we were only interested in the ball's motion, especially as it related to a 4-quadrant system where each quadrant represented a particular set of pitches. Depending upon the quadrant where the ball bounced, the pitch would be chosen from the set associated with that quadrant.

Ok, ok -- this is rather silly. The real point of the exercise is to introduce a number or concepts, not the least of which is imbedding a real-time system using RTcmix in the C programming language. Plus it is sort of fun to fool with...

C Programming


[NOTE: for those who are already adept at coding, feel free to skip to here for information on basic RTcmix function calls and TCP/IP setup.]

The first thing we did was to code up a little C program that would simply write out a set of notes for the STRUM instrument. This is not that much different from the exercises we have done using the Minc part of RTcmix. The biggest difference is the extended capabilities to represent data available in C. Here we use an array of pitches (in Minc you need to use a makegen() to do this) and randomly choose a pitch for each note, advancing the start time by 0.2 seconds each time: [NOTE: This program is called "bumble0.c" in the basicbumble download package above. You compile it by saying:

It is NOT included in the
Makefile for the directory.]


In general, I'm not going to go through the programs presented in class line-by-line, opting instead to concentrate on the Important Stuff in the programs that directly relates to what we're trying to do. In this case, however, for those who haven't done a lot of C programming, the following is a line-by-line (or groups of lines!) discussion of the above file.
NOTE: I did a quick search on the web for "C program examples, and I found a pretty decent on-line tutorial for people wanting to learn a bit more about C. Click here to view this tutorial.

At the top of the file are three lines:

You will see many more of these in more complicated programs. Basically, they include what are called "header files" -- files that contain definitions for functions that you use in your program. Beyond a basic functionality of the language, you will need to specify exactly which sets of functions you want in a program. Here we have told the computer to include the stdio.h file, which is called the "standard input-output" header file, the math.h file, which contains definitions for a number of math functions. Because they are standard system files, we don't have to give a full pathname to find the file. Generally these files live in /usr/include, so if you wanted to see exactly what was in math.h you would look in /usr/include/math.h. The last included file is not a standard system file, so we need to give a full pathname to it: /musr/cmix/H/randfuncs.h. This file contains definitions for several random number and statistical functions that are useful for computer music (click here for a short description of these functions).

Because we have specified these header files, we will need to tell the computer to include the appropriate library files when we create the executable command. This is done in the "cc" step described in the NOTE: above -- you will see a reference to /musr/cmix/lib/randfuncs.o for "randfuncs.h" and -lm for the math library. The stdio library is so common that you don't have to tell the system to include the library. When programs get more complicated, the listing of libraries, etc. can become rather tedious. A special file called a Makefile is used to store listings of libraries and other directives for the "cc" compile command.

The next part of the file listing contains:

Every C program listing has to have a main() statement somewhere. This is the point where the computer starts whenever it executes the program. So when you look at a computer program listing (in C), look for this statement to know where to start figuring out what the heck the machine does.

After the open "{" for the main part of the program, the following lines:

declare the variables to be used in a program. In C, you need to tell the computer what type of numbers each variable will be intended to represent. int numbers are integers, i.e. they don't have a fractional part (no decimal point). float numbers have decimals. 1 is an int, 1.0 is a float.

Of special interest is the declaration for p1[7]. This means that p1 is an array of 7 floating point numbers. In this case, we have gone ahead and filled the array with float numbers representing pitches in octave.pc notation. You can assign values to variables when they are declared, but you don't have to.

Next the lines:

sets start to 0.0 and defines a loop that will go around 25 times ("around" meaning between the open "{" and closed "}" a few lines below).

The two lines:

get a random pitch from the array of pitch values p1[7] by first generating a random number between 0.0 and 1.0 (brrand()), multiplying this by 7.0 to get a random number between 0.0 and 7.0, then ignoring the decimal (or fractional) part of that resulting number (pitchindex was declared as an int, which means it cannot handle the decimal part), and finally using that to access a particular value in p1[].

Next is a comment in the code, simply inserted to remind us (me!) of the parameters for the START command in the STRUM instrument:

Note that the beginning of the comment starts with "/*" and is bracketed by "*/" -- the C compiler will ignore any text between those two markers.

At the heart of the loop is the statement:

This does the actually writing-out of the note statement. It should be relatively self-explanatory with two minor strangenesses -- the first is the use of the "%f" symbols in the printed text. printf() has a number of options for formating variable values that it prints. "%f" says to print out a floating point (float) value, which is exactly what the variables start and pitch are. Other common formatting flags in printf() are "%d" to print an integer (int) value, and "%s" to print a string (text) variable. The other minor strangeness is the "\n" character -- this simply says to put in a 'newline' or carriage-return after printing this line. (What would happen without this?)

The variables corresponding with the "%f"'s in the printf() are listed in order at the end of the line.

Finally, we simply add 0.2 seconds to the start time (why?) and close off the various brackets ("{") that are used to mark segments in the C code:

And that's all there is to it!



Writing directly to an RTcmix instrument


Although the above is Really Great Fun, it really doesn't do much more than what we could accomplish in Minc, except add an additional layer of complexity (we would have to save the output of the above program into a scorefile and then run it as a separate step with the STRUM program). What we would prefer to do is be able to send note events directly into an RTcmix instrument, and then have it play the notes we send it immediately. And indeed, we can do this!

RTcmix has the capability to receive note data over a TCP/IP socket, or connection established through a computer network (the network can be totally within a single machine, by the way). These "sockets" are used by most of the common internet programs, like mail, Netscape, telnet, etc.

It's actually not too difficult to take advantage of this capability -- we need to establish a connection to an RTcmix instrument and then replace the printf() with an equivalent statement that will send the note data over the socket connection.

The following program (called "bumble1.c" in class) does this:

See? Easy as pie...

Some comments on the above code:
First of all, we are now including another header file,

which makes our compiling a little more complicated still. At this point, we start to use a Makefile which will direct the creation of an executable instrument. Click here to view the Makefile we are using for these bumbleball simulations. Typing the command "make bumble1" should create the executable command "bumble1", assuming that you have the file "bumble1.c" in the same directory. The Makefile and associated C programs are included in the download packages above.

To set up the RTcmix instrument and establish the socket, we added the following three lines of code:

RTopensocket() starts up the instrument STRUM and sets it to run while listening for data on a particular socket connection. The sleep(3) tells the computer to wait for 3 seconds while this instrument begins executing (why is the wait necessary?).

RTsock() then establishes a connection from our "bumble" program to the STRUM instrument. In this case we told it to look for the STRUM instrument on "localhost", or the same computer that is running our "bumble" program. This name could be any computer on the internet, however. Obviously the computer should be running the STRUM instrument in "socket" mode!

Next, we started to send scorefile commands over to the STRUM instrument. The first command is rtsetparams() to set up the sampling rate, stereo/mono output, etc.:

RTsendsock() takes a variable nuumber of parameters. The first three are always set: the command name (in quotes), the variable representing the socket connection (we get this from RTsock(), and the number of parameters we are sending. The next bunch of parameters are then the values to send over to the instrument. NOTE: These must be floating point numbers!

The sleep(1) following this rtsetparams()/RTsock() command allow the computer time (1 second) to set up an audio connection to play the sound.

In a similar fashion, we replaced the printf() command with RTsendsock(), sending the note data directly over to the STRUM instrument:

The only strange thing here is the sleep(1) immediately following the RTsendsock(). At this point, we no longer need to have the computer wait for an RTcmix instrument to start or to wait for an audio connection to be established... so why can't we just send it the note data?

The reason has to do with how RTcmix interprets the start time for data coming through the socket. Notice that we are always telling it that the start time is "0.0". RTcmix always assumes that "0.0" means now, and will play the note immediately. A start time greater than "0.0" will tell RTcmix to wait that many seconds from now to play the note; i.e. "1.5" as a start time will sound the note 1.5 seconds after receiving the note data on the socket.

This means that if we execute our 25-step loop all at once (which will happen in the above code), the notes will be played all at the same time! By inserting a sleep(1) in the loop, we pause the "bumble" program 1 second after sending each note over the socket.

Although this works, the problem is that sleep() is a rather coarse way to accomplish the delay of notes. For example, it can only take integer values -- this means that "1" is the smallest increment we can step. If we want to send notes over faster than once a second, we're out of luck! (well, there is a way... but you try to figure it out!)

We do have a mechanism to specify precise timing of note events, however. The trick is to isolate the code for generating the note data (and the corresponding RTsendsock() into a separate function, and then call that function at the appropriate time. The following program ("bumble2.c") illustrates how to do this with RTcmix:

NOTE: This uses the same Makefile as the previous "bumble.c" code, except now the build command is "Make bumble1"

In the above code, the 25-step loop is replaced by:

The RTtimeit() line will cause the gobumble function to be 'fired' every 0.2 seconds. The gobumble function now has all of the code that was previously within the 25-step loop, and it issues exactly one RTsendsock() every time it gets called. In this case, we will generate a single note every 0.2 seconds.

The while(1) { . . . sleep(1); . . . } construction is basically to prevent the main() part of the program to stop execution and then terminate. This would also terminate the timing of the gobumble() function, since main() is the "main" part of the program, and ending it will cause the entire program to stop executing. This program will basically run forever, until it gets interrupted by a user typing CTRL-C, or the machine shutting down, or ...


NOTE: Shutting down an RTcmix instrument in this way doesn't work very well. Because of the "socket" nature of how the instrument is working, it (the instrument) will not terminate when the program ("bumble1") stops. You will probably need to stop the instrument 'by hand' by saying "killall STRUM". In future classes we will show how to terminate the whole shebang cleanly.
Two things to notice:

1. The declaration of the variable theSock now occurs above the main() statement. This is so that the gobumble() function can also access this variable. If it were only declared inside the main() function, then gobumble would not be able to use it for RTsendsock().

2. The function gobumble() is declared as:

inside main(). This is because the RTtimeit() function needs that "kind" of function to work. Also note in the definition of gobumble() that it is declared as:

The final step in "modeling" the bouncing bumbleball was to set it up to choose from different pitch sets as it bounced into and out of different quadrants. We had to keep track of the ball's position -- this was done by declaring two variables, xpos and ypos directly after the declaration of theSock (why is it necessary for these two variables to be global?).

NOTE: The following modifications are in the file bumble3.c in the basicbumble download package

We "arbitrarily" decided to bounce our bumbleball on a 500x500 cartesian grid. In the main() function, we set the bumbleball to an initial position (this could anything between 1-499 for both the x and y coordinates): We rewrote the gobumble function as follows: We are using 4 separate pitch arrays, each containing 7 notes (p1[7], p2[7], p3[7] and p4[7]), and we are choosing which array to access in the if clauses right after we randomly select which element in the pitch array to use for the note we generate. The if clauses act like a sieve, choosing first whether the ball has bounced in the left or right half of the "playing field" (decision of x being greater or less than 250), and then deciding if it is in the top or bottom of the selected half (decision of y being greater or less than 250).

We send off the note with RTsendsock() as in our earlier examples, and just before exiting the function we "hop" the ball by choosing new x and y coordinates. We do this by selecting an addition/subtraction between 0 and 25 -- recall that rrand() returns a value between -1.0 and +1.0:

We decided on an upper "hop" value of 25.0 by trying different values in class. We started with 5.0, but the darned ball just wouldn't move!

We need to be careful that the ball doesn't leave the "playing field", so we check that our new x and y values don't exceed the 500x500 boundaries. If it does, we simply set it to fall back from the boundary edge by 5.0 units:


It's not too difficult to take the above code and imbed it in an X-windows application that displays the position of the ball as it generates notes. Typically in this class we won't be going into much progamming detail, but it might not be a bad idea to walk through the outline of the most basic bumbleball simulation we did in class. The output of the simulation displayed like this (with the ball hopping happily around generating wonderful plucked-string sounds, of course):

and the entire code is as follows: Dissecting this code shouldn't be too difficult. The three new #include statements allow us to access the X/motif widgets generally and two specific widgets (a "widget" is a basic object used in X windows -- like a button, or a drawing-area, or a slider, etc.): We declared the pitch arrays outside the main() or gobumble() functions. No good reason for doing this, possibly a small gain in efficiency.

But we did make the following declarations, also outside any function; thus global (accessible by all functions) in the file:

The Widgets are objects that we will use to create X-windows "things" in the main() function, and we will be accessing them as we draw the ball in the gobumble() function. The GC gc and the XtAppContext app declarations are needed to establish a context for the application -- a context here containing things like the colors being used, the width of lines being drawn, where the window is on the computer screen, etc.

Once we get into the main() function, the start-up of the RTcmix instrument and TCP/IP socket should be somewhat familiar. Then there are four function calls to X/motif functions that set up the window, etc. The first one:

creates the appliction an initializes an "X" object in the window manager (the window manager is the thing that keeps track of all the stuff on the computer screen you see).

Next, the function call:

creates a "bulletin board" widget in this new X object, with the specified width (XmNwidth) and height (XmNheight), along with some other parameters about the margin width and height. There are actually quite a few parameters that you can specify for a "bulletin board" widget -- see the X/Motif documentation on-line on the SGI machines or in any of the big X/Motif books down at the CMC.

Inside a "bulletin board" widget, you can place other widgets (and in fact the "bulletin board" widget lies inside the basic topLevel widget -- look closely at the few lines of code above and you will see where this link is made). We are going to use a "drawing area" widget so we can draw our bouncing bumbleball:

Notice that the height and width are set so that it fills the entire bulletin board. We are also specifying a white background (we will be drawing on this widget in black lines).

Just prior to this, we had to create a new "graphics context" for our application:

This is all you have to do -- you can now use the gc variable whenever you need to draw a line, etc.

We did set up the bumbleball a little differently by using random values for the starting point (tsrand() starts the random number generator differently each time the application runs. It uses the system clock to seed the random sequence).

We then told the window manager to "manage" (or build) the widgets we specified, and realize them so they appear on our computer screen: After starting the gobumble() function with an RTtimeit() call (every 0.2 seconds), we called the XtAppMainLoop() function, which basically sits forever and waits for X-events (like mouse clicks, internal drawing updates, window movement, etc.): That's all there is to starting up the X application!

Next we have to deal with drawing into it, based upon where the bumbleball is currently located. In the gobumble() function, the lines:

will draw the ball in the color black, at the xball and yball location on the screen (a 360-degree filled arc is a circle. X is strange).

If we just simply drew the ball over and over, our screen would soon be covered with balls that were drawn in previous calls to gobumble(). Very messy indeed. To keep this from happening, we draw a white version of the ball before we recalculate the new ball coordinates (a "high-tech" white-out...). We do this every time gobumble() is invoked:

Of course, if we do this we will ocassionally blank out part of the axes showing the four quadrants, so we need to redraw them, too: The function call: simply ensures that all our drawing instructions get sent to the computer screen.

The rest of the gobumble() function is identical to our previous bumbleball simulation code.



Next we modified this version slightly to allow a START button, and also threw in a few colors to make the bumbling a bit more exciting. We won't print the entire code listing here (it is available in the download package at the top of this document).



The changes were relatively minor. We included an X/Motif file for a "pushbutton" widget:

as well as code declaring the pushbutton widget object and variables related to color: In the main() function, we built the pushbutton on the bulletin board with the following lines of code: The text variable is declared in the main() function as follows: and is used to put a label ("bumbleize!") on the ball. The XtAddCallback() function is how X associates actions on particular widgets with functions in the code. Whenever the bumbutton gets pushed, the "callback" function GoDraw() gets called. This is what the XtAppMainLoop(app); function does at the end of the definition of main().

GoDraw is declared in main() as:

and it looks like this: All it does is draw the axes and the initial ball position (calculated in the main() function with tsrand() as described above) and start the RTtimeit() process. gobumble() -- the function that actually generates the notes -- is unchanged from our earlier version, and will NOT start going until the "bumbleize!" button gets pushed and the GoDraw() function gets called.

There are several other minor differences between this version of the bumbleball simulation and the previous one. The code in main():

simply sets up some colors that we use when we do our drawing, etc. The "drawing area" widget makes use of one of these colors to set the background color when it gets created in main(): Note also that the original "bulletin board" is set up with a height of 700 pixels (XmNheight, 700,), making it larger than the 500x500 "drawing area" widget we use for displaying the bumbleball. This is so we have space at the bottom of the application window to put the "bumbleize!" button.

Finally, we are using a slightly different version of the STRUM instrument. The START1 scorefile directive invokes a version that has feedback and distortion parameters included. You can hear this occuring in some of the quadrants of the above simulation. The final version of the bumbleball simulation (available in the download) attaches some slider widgets to several of the START1 parameters. Notice how they use global variables to communicate values from the sliders to the gobumble() function when note data is created.

Whew! A lot of code in this one -- as I said at the beginning of the year, we won't be spending hours and hours on trying to learn X code. If you want to try some X/Motif programming, perhaps use this (or some of the other programs we show in class) as a starting point. You will need to consult the on-line X/Motif books (on the SGIs) a lot, or find them on the CMC bookshelves for reference. Have fun!


L-systems


L-systems, created by Dutch biologist Aristid Lindenmayer (hence the clever name), are a way that way can formalize and add some structure to the "bumbleball" simulation. But we're not actually going to do this in the class! We'll discuss how an L-systems approach might be used to extend the model, but mainly we want your imagination to run wild and do amazing things. To assist, here are some of the links to information and fun applications involving L-systems: L-systems are also closely related to cellular automata and fractal techniques in general, which we will be examining in the next class.

yeah!


Assignments:

1. Try attaching more parameters of the STRUM program to features of the simulation -- i.e. perhaps model the "force" of each bounce and tie that to a sonic parameter.

2. Instead of having the bumbleball bounce back from the walls, change them to reflect a toroidal or spherical world.

3. Come up with a scheme to dynamically modify the pitch content of the arrays being used (with perhaps differing numbers of pitches), tied to a new feature of the simulation.