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...
#include <stdio.h> #include <math.h> #include "/musr/cmix/H/randfuncs.h" main() { int i; int pitchindex; float start; float pitch; float p1[7] = {8.00, 8.02, 8.01, 9.05, 7.10, 9.065, 8.079872}; start = 0.0; for (i=0; i < 25; i++) { pitchindex = brrand()*7.0; pitch = p1[pitchindex]; /* p0 = start; p1 = dur; p2 = pitch (oct.pc); p3 = fundamental decay time p4 = nyquist decay time; p5 = amp, p6 = squish; p7 = stereo spread [optional] */ printf("START(%f, 2.4, %f, 2.4, 1.2, 10000, 1.0)\n",start,pitch); start = start + 0.2; } }
At the top of the file are three lines:
#include <stdio.h> #include <math.h> #include "/musr/cmix/H/randfuncs.h"
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:
main() {
After the open "{" for the main part of the program, the following lines:
int i; int pitchindex; float start; float pitch; float p1[7] = {8.00, 8.02, 8.01, 9.05, 7.10, 9.065, 8.079872};
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:
start = 0.0; for (i=0; i < 25; i++) {
The two lines:
pitchindex = brrand()*7.0; pitch = p1[pitchindex];
Next is a comment in the code, simply inserted to remind us (me!) of the parameters for the START command in the STRUM instrument:
/* p0 = start; p1 = dur; p2 = pitch (oct.pc); p3 = fundamental decay time p4 = nyquist decay time; p5 = amp, p6 = squish; p7 = stereo spread [optional] */
At the heart of the loop is the statement:
printf("START(%f, 2.4, %f, 2.4, 1.2, 10000, 1.0)\n",start,pitch);
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:
start = start + 0.2; } }
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:
/* bumble1.c -- uses the STRUM instrument and sends data * over a TCP/IP socket * * BGG */ #include <stdio.h> #include "/musr/cmix/H/RTsockfuncs.h" #include <math.h> #include "/musr/cmix/H/randfuncs.h" main() { int i; int RTpid; int theSock; int pitchindex; float pitch; float p1[7] = {8.00, 8.02, 8.01, 9.05, 7.10, 9.065, 8.079872}; /* set up the socket connection */ RTpid = RTopensocket(0, "STRUM"); sleep(3); theSock = RTsock("localhost", 0); RTsendsock("rtsetparams", theSock, 2, 44100.0, 1.0); sleep(1); for (i=0; i < 25; i++) { pitchindex = brrand()*7.0; pitch = p1[pitchindex]; /* p0 = start; p1 = dur; p2 = pitch (oct.pc); p3 = fundamental decay time p4 = nyquist decay time; p5 = amp, p6 = squish; p7 = stereo spread [optional] */ RTsendsock("START", theSock, 7, 0.0, 0.3, pitch, 0.3, 0.2, 10000.0, 1.0); sleep(1); } }
Some comments on the above code:
First of all, we are now including another header file,
#include "/musr/cmix/H/RTsockfuncs.h"
To set up the RTcmix instrument and establish the socket, we added the following three lines of code:
RTpid = RTopensocket(0, "STRUM"); sleep(3); theSock = RTsock("localhost", 0);
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("rtsetparams", theSock, 2, 44100.0, 1.0);
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:
RTsendsock("START", theSock, 7, 0.0, 0.3, pitch, 0.3, 0.2, 10000.0, 1.0);
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:
/* bumble2.c -- uses the STRUM instrument, sends data over a TCP/IP * socket and uses the RTtimeit() timing mechanism * * BGG */ #include <stdio.h> #include "/musr/cmix/H/RTsockfuncs.h" #include <math.h> #include "/musr/cmix/H/randfuncs.h" int theSock; main() { int i; int RTpid; void gobumble(); /* set up the socket connection */ RTpid = RTopensocket(0, "STRUM"); sleep(3); theSock = RTsock("localhost", 0); RTsendsock("rtsetparams", theSock, 2, 44100.0, 1.0); sleep(1); /* now DO THE STUFF!!!!!!!! */ RTtimeit(0.2, gobumble); while(1) { sleep(1); } } void gobumble() { int pitchindex; float pitch; float p1[7] = {8.00, 8.02, 8.01, 9.05, 7.10, 9.065, 8.079872}; pitchindex = brrand()*7.0; pitch = p1[pitchindex]; /* p0 = start; p1 = dur; p2 = pitch (oct.pc); p3 = fundamental decay time p4 = nyquist decay time; p5 = amp, p6 = squish; p7 = stereo spread [optional] */ RTsendsock("START", theSock, 7, 0.0, 0.3, pitch, 0.3, 0.2, 10000.0, 1.0); }
In the above code, the 25-step loop is replaced by:
RTtimeit(0.2, gobumble); while(1) { sleep(1); }
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 ...
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:
void gobumble();
void gobumble() {
NOTE: The following modifications are in the file bumble3.c in the basicbumble download package
#include "/musr/cmix/H/randfuncs.h" int theSock; int xpos,ypos; main() {
RTsendsock("rtsetparams", theSock, 2, 44100.0, 1.0); sleep(1); xpos = 50; ypos = 50; /* now DO THE STUFF!!!!!!!! */ RTtimeit(0.2, gobumble);
void gobumble() { int pitchindex; float pitch; float p1[7] = {8.00, 8.02, 8.01, 9.05, 7.10, 9.065, 8.079872}; float p2[7] = {6.00, 6.00, 6.00, 6.00, 6.00, 6.00, 6.00}; float p3[7] = {10.00, 10.01, 10.02, 10.03, 10.04, 10.05, 10.06}; float p4[7] = {6.02, 8.02, 9.02, 10.02, 7.02, 9.0265, 8.0279872}; pitchindex = brrand()*7.0; if (xpos < 250) { if (ypos < 250) { pitch = p1[pitchindex]; } else { pitch = p2[pitchindex]; } } else { if (ypos < 250) { pitch = p3[pitchindex]; } else { pitch = p4[pitchindex]; } } printf("xpos = %d ypos = %d\n",xpos,ypos); /* p0 = start; p1 = dur; p2 = pitch (oct.pc); p3 = fundamental decay time p4 = nyquist decay time; p5 = amp, p6 = squish; p7 = stereo spread [optional] */ RTsendsock("START", theSock, 7, 0.0, 0.3, pitch, 0.3, 0.2, 10000.0, 1.0); xpos = xpos + (int)(rrand() * 25.0); ypos = ypos + (int)(rrand() * 25.0); if (xpos <= 0) xpos = 5; if (xpos >= 500) xpos = 495; if (ypos <= 0) ypos = 5; if (ypos >= 500) ypos = 495; }
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:
xpos = xpos + (int)(rrand() * 25.0); ypos = ypos + (int)(rrand() * 25.0);
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:
if (xpos <= 0) xpos = 5; if (xpos >= 500) xpos = 495; if (ypos <= 0) ypos = 5; if (ypos >= 500) ypos = 495;
/* bumbleshow0.c -- black & white bumbleball! * * BGG */ #include <Xm/Xm.h> #include <Xm/BulletinB.h> #include <Xm/DrawingA.h> #include <stdio.h> #include "/musr/cmix/H/RTsockfuncs.h" #include "/musr/cmix/H/randfuncs.h" int theSock; int RTpid; float pitches1[4] = { 8.00, 8.02, 8.05, 8.07 }; float pitches2[4] = { 6.00, 6.01, 6.02, 6.03 }; float pitches3[4] = { 9.04, 9.07, 9.11, 9.05 }; float pitches4[4] = { 7.09, 8.09, 9.09, 6.09 }; Widget theDraw, topLevel, bboard; int xball,yball; GC gc; XtAppContext app; main(argc,argv) int argc; char *argv[]; { void gobumble(); /* start up the instrument */ RTpid = RTopensocket(0, "CMIX"); sleep(3); /* allow the process time to start */ /* open up the socket */ theSock = RTsock("localhost", 0); /* set up the instrument */ RTsendsock("rtsetparams", theSock, 3, 22050.0, 1.0, 256.0); RTsendsock("load", theSock, 1, "STRUM"); /* now do the window stuff */ topLevel=XtVaAppInitialize(&app,"Bumbleshow", NULL,0,&argc,argv,NULL,NULL); /* set up the main window */ bboard = XtVaCreateManagedWidget("bboard", xmBulletinBoardWidgetClass, topLevel, XmNwidth, 500, XmNheight, 500, XmNmarginWidth, 0, XmNmarginHeight, 0,NULL); /* set up the "graphics context */ gc = XCreateGC(XtDisplay(bboard), RootWindowOfScreen(XtScreen(bboard)), 0, NULL); /* set up the drawing area in the main window */ theDraw = XtVaCreateManagedWidget("theDraw", xmDrawingAreaWidgetClass, bboard, XmNwidth, 500, XmNheight, 500, XmNbackground, WhitePixelOfScreen(XtScreen(bboard)), NULL); tsrand(); /* seeds the random stuff by time-of-day */ xball = brrand() * 500.0; yball = brrand() * 500.0; XtManageChild(bboard); XtManageChild(theDraw); XtRealizeWidget(topLevel); /* get the timer going */ RTtimeit(0.2, gobumble); XtAppMainLoop(app); } void gobumble() { int index; float note; /* erase the ball */ XSetForeground(XtDisplay(theDraw), gc, WhitePixelOfScreen(XtScreen(bboard))); XFillArc(XtDisplay(theDraw), XtWindow(theDraw), gc, xball, yball, 10, 10, 0, (360*64)); xball = (int)(rrand()*50.0) + xball; yball = (int)(rrand()*50.0) + yball; if (xball > 500) xball = 490; if (xball < 0) xball = 10; if (yball > 500) yball = 490; if (yball < 0) yball = 10; /* redraw the axes */ XSetForeground(XtDisplay(theDraw), gc, BlackPixelOfScreen(XtScreen(bboard))); XDrawLine(XtDisplay(theDraw), XtWindow(theDraw), gc, 250, 0, 250, 500); XDrawLine(XtDisplay(theDraw), XtWindow(theDraw), gc, 0, 250, 500, 250); /* redraw the ball */ XSetForeground(XtDisplay(theDraw), gc, BlackPixelOfScreen(XtScreen(bboard))); XFillArc(XtDisplay(theDraw), XtWindow(theDraw), gc, xball, yball, 10, 10, 0, (360*64)); XFlush(XtDisplay(theDraw)); /* ok, decide where the ball is */ if (xball < 250) { if (yball < 250) { index = brrand() * 4.0; note = pitches1[index]; } else { index = brrand() * 4.0; note = pitches2[index]; } } else { if (yball < 250) { index = brrand() * 4.0; note = pitches3[index]; } else { index = brrand() * 4.0; note = pitches4[index]; } } /* p0 = start; p1 = dur; p2 = pitch (oct.pc); p3 = fundamental decay time p4 = nyquist decay time; p5 = amp, p6 = squish; p7 = stereo spread [optional] */ RTsendsock("START", theSock, 7, 0.0, 1.0, note, 1.0, 0.5, 5000.0, 1.0); }
#include <Xm/Xm.h> #include <Xm/BulletinB.h> #include <Xm/DrawingA.h>
But we did make the following declarations, also outside any function; thus global (accessible by all functions) in the file:
Widget theDraw, topLevel, bboard; int xball,yball; GC gc; XtAppContext app;
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:
/* now do the window stuff */ topLevel=XtVaAppInitialize(&app,"Bumbleshow", NULL,0,&argc,argv,NULL,NULL);
Next, the function call:
/* set up the main window */ bboard = XtVaCreateManagedWidget("bboard", xmBulletinBoardWidgetClass, topLevel, XmNwidth, 500, XmNheight, 500, XmNmarginWidth, 0, XmNmarginHeight, 0,NULL);
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:
/* set up the drawing area in the main window */ theDraw = XtVaCreateManagedWidget("theDraw", xmDrawingAreaWidgetClass, bboard, XmNwidth, 500, XmNheight, 500, XmNbackground, WhitePixelOfScreen(XtScreen(bboard)), NULL);
Just prior to this, we had to create a new "graphics context" for our application:
/* set up the "graphics context */ gc = XCreateGC(XtDisplay(bboard), RootWindowOfScreen(XtScreen(bboard)), 0, NULL);
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).
tsrand(); /* seeds the random stuff by time-of-day */ xball = brrand() * 500.0; yball = brrand() * 500.0;
XtManageChild(bboard); XtManageChild(theDraw); XtRealizeWidget(topLevel);
/* get the timer going */ RTtimeit(0.2, gobumble); XtAppMainLoop(app);
Next we have to deal with drawing into it, based upon where the bumbleball is currently located. In the gobumble() function, the lines:
/* redraw the ball */ XSetForeground(XtDisplay(theDraw), gc, BlackPixelOfScreen(XtScreen(bboard))); XFillArc(XtDisplay(theDraw), XtWindow(theDraw), gc, xball, yball, 10, 10, 0, (360*64));
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:
/* erase the ball */ XSetForeground(XtDisplay(theDraw), gc, WhitePixelOfScreen(XtScreen(bboard))); XFillArc(XtDisplay(theDraw), XtWindow(theDraw), gc, xball, yball, 10, 10, 0, (360*64));
/* redraw the axes */ XSetForeground(XtDisplay(theDraw), gc, BlackPixelOfScreen(XtScreen(bboard))); XDrawLine(XtDisplay(theDraw), XtWindow(theDraw), gc, 250, 0, 250, 500); XDrawLine(XtDisplay(theDraw), XtWindow(theDraw), gc, 0, 250, 500, 250);
XFlush(XtDisplay(theDraw));
The rest of the gobumble() function is identical to our previous bumbleball simulation code.
The changes were relatively minor. We included an X/Motif file for a "pushbutton" widget:
#include <Xm/PushB.h>
Widget theDraw, topLevel; Widget bboard, bumbutton; int xball,yball; GC gc; Colormap cmap; XColor bground,ball,lines,unused;
/* set up the bumble button */ text = XmStringCreateSimple("bumbleize!"); bumbutton = XtVaCreateManagedWidget("bumbutton", xmPushButtonWidgetClass, bboard, XmNlabelString, text, XmNx, 100, XmNy, 600, NULL); XtAddCallback(bumbutton, XmNactivateCallback, GoDraw, NULL);
XmString text;
GoDraw is declared in main() as:
void GoDraw();
void GoDraw(w, client, call) Widget w; XtPointer client, call; { void gobumble(); /* draw the axes */ XSetForeground(XtDisplay(theDraw), gc, lines.pixel); XDrawLine(XtDisplay(theDraw), XtWindow(theDraw), gc, 250, 0, 250, 500); XDrawLine(XtDisplay(theDraw), XtWindow(theDraw), gc, 0, 250, 500, 250); /* draw the ball */ XSetForeground(XtDisplay(theDraw), gc, ball.pixel); XFillArc(XtDisplay(theDraw), XtWindow(theDraw), gc, xball, yball, 10, 10, 0, (360*64)); /* get the timer going */ RTtimeit(0.2, gobumble); }
There are several other minor differences between this version of the bumbleball simulation and the previous one. The code in main():
/* set up the colors */ cmap = DefaultColormapOfScreen(XtScreen(bboard)); XAllocNamedColor (XtDisplay(bboard),cmap,"sandybrown",&bground,&unused); XAllocNamedColor (XtDisplay(bboard),cmap,"royalblue",&lines,&unused); XAllocNamedColor (XtDisplay(bboard),cmap,"red1",&ball,&unused); gc = XCreateGC(XtDisplay(bboard), RootWindowOfScreen(XtScreen(bboard)), 0, NULL);
/* set up the drawing area in the main window */ theDraw = XtVaCreateManagedWidget("theDraw", xmDrawingAreaWidgetClass, bboard, XmNwidth, 500, XmNheight, 500, XmNbackground, bground.pixel, NULL);
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!
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.