In order to examine the construction of CAL programs, let's look at the following example. This is a real CAL program with real CAL functions, arguments and variables. After writing a program like this, you would save it to disk as a .CAL file in the Cakewalk directory or in a directory you may set aside just for these programs. I keep mine in a directory called SUPPORT located as a sub-directory under my Cakewalk directory. To run this program, you would set the "From" and "Thru" markers and highlight the track or tracks you wish to edit just as if you were about to use the TRANSPOSE or QUANTIZE or any other feature in the EDIT menu. Next, you would select "RUN CAL" from the EDIT menu, or from the TOOLS menu in versions 7 and 8, and choose this program from the ones listed.
As with many programming languages, you can add comments to the code by preceding such comments with a semicolon. For clarity in this example, the comment lines are in blue and the brackets are colored to assist in following the program flow.
;This program has more than one function so we start with
"do". All CAL programs
;start with "do".
;Declare an integer (int) called "lownote" and assign it a value of 42.
(int lownote 42)
;Declare a double word number called "newtime" and make it 120 ticks.
(dword newtime 120)
;Ask the user to enter a value between 0 and 127 for "lownote".
(getInt lownote "Enter the lowest note to transpose " 0 127)
;Now starts the loop.
;The loop has more than one function so we use "do".
;Add to the event time of each event the value 120 stored in "newtime".
(+= Event.Time newtime)
;We test to see if the current event being scanned by this pass
of the loop is a note, and if
;this note's pitch is greater than or equal to the lowest note to be considered as per the
;value in "lownote" selected by the user.
(if (&& (== Event.Kind NOTE) (>= Note.Key lownote))
;If all of the above conditions are met, we perform the following
functions as grouped
;by the "do".
;Subtract 12 pitch steps from this note.
(-= Note.Key 12)
;Add 10 to the velocity of this note.
(+= Note.Vel 10)
;Close the "do" function in the "if" function.
;If there had been any contingency for this event having failed
the "if" test, the code
;to handle that contingency would go here. However, we are only interested in events
;that pass the test. Therefore, we close the "if" function without coding any
;Close the "do" function in the "forEachEvent" loop.
;Close the "forEachEvent loop".
;Close the "do" function in the main program body and end the program.
It's time to call attention to a peculiarity of CAL that it shares with other programming languages like LISP. Even the mathematical operations like addition and logical operations such as "greater than" are functions. That is, first you list the function, then you list the arguments or variables the function is to operate on (keep in mind that an argument can be a variable, another function or even a group of functions in brackets). As humans, we say "two plus three" when we add. CAL says "plus two three" instead. Just remember, in CAL you must always put the type of function to be performed first followed by any arguments needed for the operation. Another VERY important point is the difference between single and double equal signs. As you see in the example above, double equal signs "= =" meant "see if argument one equals argument two". The use of the single equal "=" means "set the value of argument one equal to the value of argument two" which is a very different thing altogether. One of the most confusing bugs in a CAL program is when you accidentally use a single equal when you meant to use double equals and instead of making a comparison you end up setting a variable to some obtuse value.
What this sample program does is take all events in the selected range of the sequence and moves them up in time by a quarter note. It then selects all note events that are at or above a selected pitch and lowers them by one octave while increasing their velocities by ten units. Here's how it works. This program declares two variables. The first is an integer that will hold a pitch value we will use to compare against the notes in the sequence. We initialize the variable with a value of 42. The process of giving a variable an initial value in a declaration statement is optional as far as CAL is concerned, but mandatory as far as good programming habits are concerned. Without the initial value provided, the variable would equal whatever stray data happened to be in that pocket of memory set aside for it. It could equal anything! This is of no consequence if the variable is to be set to some known value before it is ever used, but why depend on your memory to assure you that a variable has a valid value when the time comes to use it. Just set it to some value, perhaps zero or as in this case, a default note pitch. It's good programming form to initialize ALL variables at the moment you declare them. Like the shoe slogan says, "Just Do It". The second variable is a double word number that holds the amount of clock ticks we will add to the event times of each event. We initialize it to 120. Later in the program we add this value to the raw times of the events we encounter, thus moving the events up by 120 ticks or one quarter note. There are other variables that you will see in the code. Why aren't we declaring them? Well, as it happens, Cakewalk has a bunch of "internal" variables that it keeps track of all by itself. These are things like the type of event currently being processed by the "forEachEvent" loop which is called "Event.Kind" and there are the various values associated with any given event such as the pitch of a note event called "Note.Key" and others as you can see. In the section titled Functions And Keywords In CAL, we will explore all of these Event Parameter Variables. As you review the rest of that section you will notice many more of these built-in variables that CAL makes available to us. I will also comment on them as we encounter them in the example programs.
After declaring the variables, we ask the user to enter a value for "lownote" using the "getInt" function. What happens is a dialog box opens and the text within the quotes is displayed as a prompt. The initial value of 42, the value we assigned to it when we declared it as a variable, is displayed in the input field as a default value. The user can except this default value by hitting the "ENTER" key or can type in a new value over the default one. Now aren't you glad we initialized this variable when we declared it? It would have made no sense to offer the user a default pitch of something like 23641, now would it? The last two numbers in the function are the input limits. If the user tries to key in a value less than 0 or greater than 127, an error box appears and the user is shown the limits and told to try again. For the record, I like to add a couple of spaces at the end of the prompt text so there is some room between the prompt and the input box on the screen.
Next, the "forEachEvent" loop starts and the first thing we do is add the ticks in "newtime" to each event (note, controller change, patch change, whatever). We perform this addition using the shorthand function "+=" that adds two arguments and automatically assigns the result to the first argument. Now the "if" function goes into action, but only on the events that meet certain conditions. We want to operate only on note events, and what's more, only note events that have a pitch at or above our "lownote" value. The way we do this is to build a condition statement for the "if" function that gives us a go or no-go criteria for every event the "forEachEvent" loop will scan. All of the stuff (arguments) on the line that follows the "if" is the filter. The first part is the "&&" function which denotes a logical AND. This says that there will be two parts to the condition and both must be true for the event to pass the filter. Next is the first of the two conditions. The "= =" function says that the two variable arguments to follow must be equal to each other. The variable "Event.Kind" holds the type of event currently being scanned by the "forEachEvent" loop. "NOTE" is a constant that holds the value of a note event. Thus when the "Event.Kind" variable is compared to the "NOTE" constant, it passes the muster if that event is a note but fails the test if it is a controller event or a pitch wheel event or anything other than a note.
Assuming it was a note, the next part of the condition is imposed on the event. The ">=" function says that the first variable must be greater that or equal to the second variable. Here, the "Note.Key" variable holds the pitch of the current note and is compared to the variable we declared called "lownote" which we will say for the sake of this example the user has elected to keep the default value of 42 or F#3. If the note is F#3 or above, then it passes both tests and will be subjected to the functions that follow. If the event fails to be a note or is a note lower than F#3, then this session ends and the "forEachEvent" loop goes to the next event in the sequence and the process starts over. If, on the other hand, the event passes the tests, then the two nested functions in the "if" function will be performed on that note. Using the "do" after the "if" is very important for more reasons then just telling CAL to expect more than one function. It says that all functions within the "do" are to be treated as one big single function. Without it, the "if" function would think that the first nested function would be executed if the events pass the test and the other nested function would be executed if the events fail the test. As it stands, all notes that have passed will have both nested functions performed on them. The first nested function is a subtraction with assignment. The "-=" says that the first argument is to have the value of the second argument subtracted from it and like the example above, does the subtraction and assigns the result to the first variable all in one step. "Note.Key" holds the pitch of the note event and will have 12 subtracted from it. The next function is addition with assignment, and "+=" says to add to the first argument the value in the second and assign the result to the first like we did for "Event.Time" above. "Note.Vel" holds the velocity value of the note and will have 10 added to it. Therefore, we will lower the notes we find by 12 (one octave) and increase their velocities by 10 points to make them a bit louder. The rest of the code is the all-important closing brackets for our nested functions.
Please do yourself a favor and be aware of what kind of data you will be dealing with and what kinds of variables to declare for them. Note pitch and velocity values can be stored as integers. However, time should be declared as double words even if the value you will be using can fit into a smaller data type. The reason I say this is because it is easy to forget that you may need the extra room as your program executes and stuff starts taking place in memory. It is also good form to keep data types consistent. Controller values also use integers, or "int", and can go both positive and negative. Note duration uses "word" and can go positive only. By the way, "strings" are chunks of text in quotes and are used for things like track names and such. We will go into all of this in the section called Functions And Keywords In CAL.
A quick note about conditions placed on "if" functions and the "while" loop which you will be introduced to shortly. The important thing to know is that when using conditions such as "= =" or "<" and so on as part of an "if" function or a "while" loop function, the test being true will cause the function to be carried out and being false will cause the function and all of it's nested functions to be ignored. To facilitate this process of testing, the Boolean (AND and OR type) Functions allow multiple conditions to be tested at the same time. The "&&" or AND function requires all conditions to be true for the final result to be true and the "||" OR function (this symbol is made by hitting the shifted back slash key twice) allows the result to be true even if only one of the conditions is true. By the way, these functions can be stacked to test for more than two conditions if you need, because any function's argument can also be another function. For example, say you need 4 things to be true before some "if" function is allowed to execute. This is how they would stack (notice the colors of the brackets as a guide to which ones belong to which nesting level and which closing bracket corresponds to which opening one).
(if (&& (&& (&& (== Event.Kind NOTE) (> Note.Vel 80)) (!= Note.Dur 0)) (> Note.Key 64)) (do (whatever) (more whatever) ) )
First the test would be made for the event kind being a note. Then that note would have to have a velocity above 80. Next the note would have to have a duration that was not zero (the "!=" mark means not equal) and finely the note would have to have a pitch above 64. If any of those conditions are not met, the entire "if" function with its nested (whatever) functions is ignored.
This would be a good time to discuss the interactive features of CAL. As you saw in the example above, there is a way to prompt the user for input that can then be applied in a CAL program. All of them work about the same way. You ask for a data type by using the key work getXxxx where Xxxx is the data type of the variable you are inputting. They are "getInt" for an integer and "getWord" for a word. One "get" function that is different is "getTime". This function uses a double word for time just as you would suspect, but the user enters the time in M:B:T format and CAL converts it to raw time when it stores the value. There is a bug in this function for versions 7 and 8 which cause it to misbehave and require the user enter time in ticks instead of M:B:T format. This has the result of causing the function to take on the characteristics of a virtual "getDouble" function. See the section called Assignment Functions for details.
Likewise, there are ways of outputting messages to the user. You can print to the status bar at the bottom of the screen with the "message" function. This function will output text as well as the values of variables. For example, say you want to display the ongoing progress of a loop. You could display a continuously updating message like this:
(forEachEvent (do (message "Now processing event at time " Event.Time) (Etc............
In this example, each time a pass is made through the "forEachEvent" loop, the text in quotes along with the raw time of the event being dealt with would be displayed on the status bar. Of course, this number would be flickering by at a fair clip and may be hard to read, but you get the idea. I'll show you some more useful examples later.
Another output function is the "pause" function. It works the same way as the "message" function except it makes a dialog box appear with the message inside, and the user must click the "OK" box with the mouse or hit the ENTER key for the program to continue. The user also has the option of clicking on the "CANCEL" box and aborting the program (by the way, if the user hits the "ESC" key during the running of a CAL program, execution will stop). The "format" function is for assembling text and variables in readable form with the result being passed on to a naming function such as "TrackName" for the purpose of outputting the resulting string to a screen field. We'll take a look at these functions in the Output Functions section.