Programming in C

Introduction

This chapter is not about how to write in C Major, nor is it about how to realize a piece by Terry Riley with a computer. Rather, it is about a programming language called ``C''. Specifically, this chapter is about how to use a subset of the C language to make music with CMT. I hope to expand this chapter in the future, but for now, it's going to look more like lecture notes than a complete guide to programming.

Compiling a Program

We will start with a simple program that has already been written. The purpose of this exercise is to learn how to run a program once it is written. We will start with a working program to minimize the number of possible problems.

The directory app/template has a simple program named template.c. Before experimenting with this program, back up the template directory so the original files can be recovered. Template is designed to serve as a template that you add to in order to build a program that does something.

Another example program is app/test/prog.c. This one actually plays some MIDI, so if you have trouble with app/template/template.c, try getting prog.c to run.

To hear the results of a program, you have to compile your program, then link it, and then execute it. The C compiler and linker are run by typing make. (With Amiga Lattice C, type lmk; for Microsoft C, type nmake /F template.mak). The resulting file, named template (or template.exe under DOS) can then be executed, meaning that the machine instructions in the file are loaded into working memory, and the computer then carries them out.

On the Macintosh with Lightspeed C, the procedure is different. You should consult your Lightspeed C manual for instructions, run the project called template, and proceed to Section ``Writing a Program''. Under DOS, you may prefer to use a more integrated programming environment, for example the bc command of Borland C, or pwb in Microsoft C. Consult your compiler documentation for details.

If your program has errors, the line number where the error was discovered will be printed along with a brief description of the error. You must fix these errors by editing your program and then compiling again. (template.c does not have any errors, so it should compile and link without errors.)

To run your program (except for Macintosh systems), type

template
The template program prompts you to type RETURN, but does nothing. To run your program again, you need not recompile unless you change your program with the editor.

On the Macintosh, there is a Run menu item in Think C, or you can build an application, save it, exit Think C, and double click on the application to run it. As with other systems, you must recompile and save the application after you edit the program.

Stopping a Program

Sometimes you need to stop programs in progress before they finish on their own. You can type CTRL-C (hold down the CTRL key and type C or CTRL-BREAK to stop your program in most cases. It is possible to write programs that cannot be stopped except by turning off the computer, but these are unusual (Footnote 4)

Writing a Program

Let's say you want to modify template to play a couple of notes. In template.c, find the word "mainscore". You should see a section of the file that looks like this:
void mainscore()
{
    while (askbool("Type RETURN to play, or N RETURN to quit",
                   true)) {
        /* PUT YOUR SCORE HERE */
    }
}
Now, add some new lines as shown below:
void mainscore()
{
    while (askbool("Type RETURN to play, or N RETURN to quit",
                   true)) {
        note(60, 100);
        note(62, 50);
        note(64, 50);
        note(65, 100);
    }
}
This program will play 4 notes in sequence by ``calling'' note 4 times. You indicate what kind of note you want by giving two parameters in parentheses after the word note. The parameters for note specify pitch and duration. The pitch parameter is 60 for middle C (C4), and goes up by one for each semitone. Thus, the pitches in this program are C4, D4, E4, and F4. The second parameter is the duration parameter, expressed in hundredths of a second. Thus, the first note will be 1 second long, the next two will be 0.5 seconds, and the last will be 1 second.

A few more observations: notice that the ``score'' consists of calls to note, and is bracketed by braces: { and }. Also notice that calls to note end with semicolons (;). All of these details are essential, and C is not very forgiving of even trivial mistakes.

On the first line of the file and in various other places, you will see what is called a comment. Any text between /* and */ is ignored by C. You will find it useful to use comments to remind yourself what your program does, when it was written, and so on.

Unlike Adagio, C is a case sensitive language. This means that mainscore, MainScore, MAINSCORE, and MaInScOrE are all distinct identifiers. Because of this, I suggest that you never use upper case letters in C programs, except in strings and comments.

Your program will have a number of additional lines copied from template.c. Do not delete or change these for now. Use make or the equivalent to recompile and relink template, and run it by typing template. You should hear a 4-note sequence.

If you have problems, try typing

template -trace
to get a printout of the outgoing MIDI messages. If you see messages but do not hear anything, then check your MIDI and audio connections.

Writing a Procedure

Procedures allow you to give a name to a musical phrase. The phrase can then be called from several places. This saves your having to retype all of the calls to note every time you want to hear the phrase. In the following example, there are two phrases, or procedures, up and down, which are called from mainscore:
-- initial part of template.c omitted from this listing --

/* these declarations are necessary to keep most compilers from complaining: */ void up(); void down();

void mainscore() { while (askbool("Type RETURN to play, or N RETURN to quit", true)) { up(); down(); up(); note(60, 100); } }

void up() { note(60, 20); note(62, 20); note(64, 20); note(65, 20); }

void down() { note(67, 20); note(65, 20); note(64, 20); note(62, 20); }

-- remainder of template.c omitted from this listing --

Notice that up and down obey the same rules as mainscore: procedure names are preceded by void and followed by a pair of parentheses and an open brace. Then there is a list of calls to note or other procedures terminated by semicolons. While note has two parameters, up and down each have zero parameters. You indicate the absence of parameters when you call up or down by not putting anything between the open and close parentheses. However, you must always type the parentheses after the name of any procedure you call. Finally, the sequence of calls is ended by a close brace.

Notice the appearance of up and down near the beginning of the file. These tell the compiler that up and down are procedures that will be defined later. Notice the semicolons in these lines.

When this program is run, the computer will do what mainscore says to do. The first call is to up, so the computer finds the definition of up and does what up says to do. In this case, up plays four notes (C, D, E, F). Now that up is finished, mainscore continues by calling down, which in turn plays (G, F, E, D). When down returns, mainscore continues with another call to up, and again up will play C, D, E, F. Finally, up returns and mainscore plays a C. At this point the mainscore program is finished.

Repeats

Repetition is important in both programming and music. To repeat a phrase, a special ``repeat'' construct is provided. Consider the following example. (In the remaining examples, only the relevant changes to template.c are shown. You will need the entire file, with changes as shown, in order to compile and run the program:
void mainscore()
{
    repeat(i, 5)
	note(60, 30);
	note(72, 30);
    endrep
}
Look at the two calls to note above. By themselves, these calls would play the phrase C4 C5. The repeat construct consists of the form
repeat(counter, howmany) phrase endrep
where counter is just the name you want the computer to give to a variable number that keeps track of which repeat is being played (more about this later), howmany is the number of repeats to take, and phrase is a sequence of calls to note or any other procedure. The example above will play the phrase C4 C5 C4 C5 C4 C5 C4 C5 C4 C5 because we wrote 5 for the number of repeats.

The phrase could just as well have been calls to other procedures. The next example uses repeat to play C4 D4 E4 F4 three times, using the up procedure used earlier.

void up();

void mainscore() { repeat(i, 3) up(); endrep }

void up() { note(60, 20); note(62, 20); note(64, 20); note(65, 20); }

Conditions

Computer programs are made more flexible by the use of conditionals, that is, the ability to test a condition and act upon the result of the test. For example, suppose you want to repeat a phrase, but you want the phrase to have a first and second ending. In other words, the first time through you want the end of the phrase to be played one way, and the second time through, you want the ending to be played another way. How would you do this using just repeats and procedures? The following program uses a new construct (the if construct) that makes this programming task easy:
void mainscore()
{
    repeat(i, 2)
	note(72, 30);
	note(71, 15);
	note(69, 15);
	note(67, 15);
	note(65, 15);
	note(64, 15);
	if (i == 1) {
	    note(65, 15);
	    note(67, 120);
	} else {
	    note(62, 15);
	    note(60, 120);
	}
    endrep
}
This is a program with a repeat construct, but notice that the last part of the repeated phrase is of the form:
if (condition) { phrase1 } else { phrase2 }
The computer interprets this construct as follows: whenever an if is encountered, the computer evaluates the following condition. In this case the condition is i == 1, which is true when the repeat counter i is equal to one (the first time through) and false when the repeat counter is not equal to one
(Footnote 5) . If the condition is true (which happens the first time through in this example), the phrase following the first open brace ({) is performed up to ``} else {'', and the second phrase (after else) is skipped. If the condition is false, the first phrase is skipped, and the phrase in braces after else is performed. As usual, these phrases can be calls to other procedures like up and down.

The if construct is used to select between two alternatives where the selection is based on a condition. Useful conditions are:

a == b  true if a is equal to b
a > b   true if a is greater than b
a < b   true if a is less than b
a >= b  true if a is greater than or equal to b
a <= b  true if a is less than or equal to b
a != b  true if a is not equal to b
Here, a and b stand for repeat counters (like i) or numbers (like 0, 1, etc.). Like the repeat construct, the if construct can be used anywhere in a phrase. The following example plays a phrase three times. On the second time through, the middle of the phrase is extended. The procedures p1, p2, and p3 are not shown.
mainscore()
{
    repeat(i, 3)
	p1();
	if (i == 2) {
	    p2();
	} else {
	}
	p3();
    endrep
}
In this example, the phrase after else is empty (has no statments). That means that on the first and third times through, the complete phrase will be equivalent to p1(); p3();. On the second time through, the phrase will be equivalent to p1(); p2(); p3();. The empty else part can be omitted, resulting in:
mainscore()
{
    repeat(i, 3)
	p1();
	if (i == 2) {
	    p2();
	}
	p3();
    endrep
}

Parameters

It's time to confess: there is really no difference between mainscore, note, up, and down. They are all procedures, and every procedure has a list of parameters, which can serve to modify its behavior. For example, the parameters to note tell the note procedure what pitch and duration to use. You can easily write your own procedures that have parameters. Study the newup procedure below, which plays C4 D4 E4 F4. The procedure has one parameter, called dur, which determines the duration of each note:
void newup(int dur);

void mainscore() { newup(25); newup(50); }

void newup(int dur) { note(60, dur); note(62, dur); note(64, dur); note(65, dur); }

When mainscore is played, it first calls newup with the parameter 25. The computer will then find the definition of newup and notice that the parameter is to be named dur. Now, whenever the computer finds the name dur within newup, it will substitute dur's value (25). Thus, the duration specified for all of the calls to note will be 25, or one quarter second.

After the fourth note is played, the computer returns to mainscore, where the next thing in the sequence is another call to newup. But this time, the parameter is 50. The computer goes through the same steps as before: it finds the definition of newup, associates the value of 50 with dur, and substitutes 50 for dur throughout the definition of newup. The result is that the four notes (C4 D4 E4 F4) are now played with durations of 50, or one half second.

Parameters may be named with any string of letters, just like procedures. It is a good idea to use mnemonic names, that is, names that remind you of their purpose. It makes no difference to the computer, but when you start writing large programs, you will find that it is important to make programs readable and understandable.

Notice that parameters in the procedure definition are preceded by the word int. This declares the type of the parameter to be an integer value. For now, we will use nothing but integers, so all parameter declarations should be of the form ``int parametername.''

Important: When you use parameters, the number of parameters in the definition must match the number of parameters in the call. The order of parameters in the call determines which parameter gets which value. If you use a procedure (e.g. in mainscore) before defining it, you must declare the procedure as illustrated in the top line of this example. The declaration is similar to the definition, but the entire body of the definition delineated by braces is replaced by a semicolon. The parameters in the declaration must match the parameters in the full definition.

Producing Chords

The C language is called a sequential language because programs are executed in sequence, performing only one operation at a time. This is a big limitation for music, and the next chapter will present some solutions to the problems of using C. In the meantime, there is a fairly simple way to get two notes sounding at once. The procedure pnote is just like note except that pnote does not wait for the specified duration. Instead, it schedules the end of the note to happen in the future but returns immediately without waiting. The following procedure plays a minor chord with the specified tonic and duration:
minor(int tonic, int duration)
{
    pnote(tonic, duration);
    pnote(tonic + 3, duration);
    note(tonic + 7, duration);
}
The first two notes of the chord are played using pnote. Since pnote returns immediately, all three notes start very close to the same time. The third note uses the note routine, which will delay for duration before returning. Thus the minor procedure will also take duration before returning.

Low-level Procedures

Up until now, we have concentrated on writing programs that control the high level structure of your music. By now, you are beginning to enjoy some of the power available to you as a computer programmer. Now, it is time to learn about the lower levels of control, which concern direct control over the synthesizer. Recall that note is just a procedure. Here it is:
void note(int pitch, int duration)
{
	midi 
note(1, pitch, 100);
	rest(duration);
	midi 
note(1, pitch, 0);
}
The note procedure first uses the procedure midi note to start a note. The parameters are the MIDI channel number (1), the pitch, and the key velocity (100).

The rest procedure stops the program for the length of time specified by duration. This sustains the note.

The third procedure call is another call to midi note. The key velocity of zero (the third parameter) indicates to turn the note off.

Incidentally, the Adagio program also uses the same midi note procedure. You can find the C programs that make up Adagio in the directories app/adagio and lib.

Other Procedures and Functions

A complete list of music functions and procedures are listed in the file midifns.c and in Appendix ``The MIDI Interface''. A summary of some useful ones, as well as some useful C procedures are given below. Italicized parameters stand for values that you supply when you call the function.
gprintf(TRANS, "this is a string\n");
writes this is a string on the screen when called. The two characters \n should be included after the text you want written. This tells the computer to ``write'' a newline after writing the line of text. gprintf is portable among versions of CMU Midi Toolkit. It is similar to fprintf which is described fully in any C programming manual.

rest(duration);
does nothing for duration (expressed in hundredths of seconds).

getkey(waitflag);
gets a keyboard event. Returns a the key number (numbered starting at 0) when a key is pressed, and the key number plus 128 when a key is released. If waitflag is true, getkey will wait for a key to be pressed. If waitflag is false, getkey may return -1 to indicate no key has been pressed. Example return values and their meaning are -1: no key was pressed, 60: middle C was pressed, 188: middle C was released (188 = 128 + 60).

midi notenote
(channel, pitch, velocity);sends a MIDI note-on command. The parameters are the MIDI channel (from 1 to 16), the key number (from 0 to 127), and the key velocity (from 0 to 127). If the velocity is 0, then the note is turned off.

midi programprogram


Previous Section | Next Section | Table of Contents | Index | Title Page