Looping and Branching
- CAL is what's called a "top down" language. This means that execution starts
at the top of the code and flows down to the bottom without any chance of being directed
back up or skipping down or running sub-routines. This lack of sub-routine and jump
functions is annoying at times, but not insurmountable. For the record, there are some
limited ways of branching out of the "top down" format, but very limited. Be
that as it may, these meager branching options are enough to allow the needed flexibility
to generate some very powerful programs. There are two effective looping and two branching
functions. The loops are "forEachEvent" and "while", and the branches
are "if" and "switch" Along with these functions we will discuss the
do-nothing loop function "delay", the nesting control function "do"
and the external program spawning function "include", which is the closest thing
we have to a user friendly "gosub" function. Finely we will touch on the
"DLL" function which is an "include"-type function that operates on
the DLL programs that exist to support Windows and Windows applications. It is NOT user
friendly.
Loops
- Loops are chunks of code that keep being executed over and over until some condition
tells them to stop. We have already touched on both "forEachEvent" and
"while", but now is the time to get knee-deep into them.
-
- (forEachEvent
- Do you notice anything odd about this function - like it has no closing bracket?
Actually it does, but as you may have noticed in the examples so far, it comes after all
of the nested statements that make the loop worth invoking. I will present
"while", "if", "switch" and "do" the same way. The
closing bracket is implied but not shown. This is how it really looks to CAL:
-
- (forEachEvent (function))
-
- or more like you are used to seeing:
-
(forEachEvent
(function)
)
- The "forEachEvent" function can have only one function in its nest. In order
to get things done, that function is often a compound function made up of nested
functions. This compound function is usually a "do" or an "if"
function with lots of nested functions like so:
-
(forEachEvent
(do
(somefunction)
(anotherfunction)
(morefunctions)
)
)
- The "do" tells CAL that even though it would expect only one function for this
loop, there will be several instead. This is because the 3 functions are not nested
together. Each stands alone. We therefore use "do" to create a nest for them all
to share so that the "forEachEvent" loop still sees just one function. The same
is done with an "if" function.
-
(forEachEvent
(if (condition)
(thenfunction)
(elsefunction)
)
)
- Even though the "if" may have "thenfunction" statements and even
"elsefunction" statements, the loop sees only one function in its nest; the
"if". As you may already have gathered by now, this beast is the major workhorse
of CAL. It takes the events in the selected part of the sequence (the area between "From" and "Thru"
on the tracks you have highlighted) and runs each event through it's nested functions, one
event at a time and in sequential order, until all of the events have been scanned. It
then aborts and CAL continues with the next function after the loop's closing bracket. You
can run as many "forEachEvent" loops as you want in a CAL program so long as you
don't try to run one inside of another one. When this loop is running, CAL places the kind
of event currently being serviced in the variable "Event.Kind", the event's starting time in
"Event.Time" and MIDI channel in
"Event.Chan". The event's parameters
are placed in the other event variables as applies. Notes will have their parameters
available in "Note.Key", "Note.Vel" and "Note.Dur" and so on for each type. Also during
the running of this loop, the marker variables "From", "Thru" and
"Now" are no longer responsive to any
changes you might try to make to them. I'm not sure of just what the mechanics of this
loop are as far as memory usage, but while it is running, if you insert a new event at an
"Event.Time" higher than the spot the loop is currently at, CAL will not see the
new event when it comes to the new event's time until the loop is over. This is likely
because such changes are buffered someplace in memory during the running of the loop and
don't actually become integrated into the sequence until the loop ends. If you were to run
a second "forEachEvent" loop, the events inserted by the last loop would be
available to this new loop.
-
- (while (condition)
- The "while" loop is very useful. It can be made to execute a nested function
for as long as "condition" is true. You can run
"while" loops within a "forEachEvent" loop, you can run
"forEachEvent" loops within a "while" loop, you can even run
"while" loops within other "while" loops. The "condition" can be any valid function or group of nested
functions so long as it will resolve to a true or false result. There are so many uses for
the "while" loop that I hardly know where to begin giving examples! We looked at
one example when we demonstrated the "OR" function. Here's another:
-
(while (< looptime stoptime)
(do
(= newa (+ ref (/ (* percent change) 100)))
(insert (+ notetime looptime) chan CONTROL 7 newa))
(+= looptime gap)
(= percent (/ (* looptime 100) stoptime))
)
)
- This may get a bit harry, but here goes. This is a simplified fragment from a program I
wrote that performs automatic crossfades between two tracks at the start of each note so
that part of the note voices from the first track and then switches to the voice on
another track part way through the note. It then goes back to the first track as the note
ends to be ready to do the whole thing again at the start of the next note. I have removed
a lot of the contents of the loop and left just enough to hopefully get across my point.
This "while" loop is within a "forEachEvent" loop so that it runs for
each note in the selected part of the sequence. First we have the condition for the loop.
So long as "looptime" is less than "stoptime", the loop will run. We
use "do" to create a nest for the 4 functions that follow so that
"while" will see only one function in its nest just like
"forEachEvent" as discussed above. The first nested function calculates a value
for "newa" by multiplying "percent" and "change", dividing
the result by 100 then adding it to the offset "ref". This value will be the
value of our volume controller event which we insert in the next line at time
"notetime" plus "looptime". We then add "gap" to looptime to
point to the place we want to insert the next volume event, and then calculate a new value
for percent which is derived by seeing how far along we are in "looptime"
relative to "stoptime". Now we go back and test the condition again to see if we
can run another "while" loop or if the conditions have been met and we can abort
the loop. When "looptime" fails to be smaller than "stoptime", the
loop ends. It is important to keep in mind
that if the "condition"
is false the first time CAL looks at it, the "while" loop will never run at all.
This is useful because there may be times when you will want to run the loop only if
"condition" exists and bypass the loop if "condition" doesn't exist.
By the way, you can use "while" loops to build on-line help systems for your CAL
programs. Check out the section called Implementing On-Line
Help In A CAL Program on the Tips, Techniques and
Work-Arounds page for details.
-
- (delay time)
- This function doesn't do anything except suspend execution of the CAL program by the
number of milliseconds (1000ths of a second) in the value "time".
This is a way of making CAL stop what it's doing long enough for a human to read some
message or react to a condition or perhaps it can be used as some kind of time keeping
function causing some function to run every so often. Personally, I've never had many
occasions to use it, but you might. As an example, let's say we wanted to scan a part of a
sequence and give the user time to make notes of what's going on like this:
-
(forEachEvent
(if (== Event.Kind NOTE)
(do
(message "Current Note Pitch Is " Note.Key)
(delay 2000)
)
)
)
- In this example we call a "forEachEvent" loop and use "if" to make
sure we only look at NOTE events. We display on the status bar at the bottom of the screen
the pitch number of the note we have found, delay for 2 seconds to give the user time to
write this number down, then go on to the next event in the loop.
Branching Functions
- Branching functions allow us to skip over some of the code that we don't need on that
occasion and go directly to some code that we do need. The rules for using the two
branching functions are to be strictly followed or your program can do some strange stuff!
-
- (if (condition)
- Here is our old friend, the "if" function. Thought we had seen enough of him
already? You don't know the half of him yet! Let's look at the entire form of the
"if" function:
-
(if (condition) (thenfunction) (elsefunction))
- This form is just fine if the "thenfunction" and, if used, the optional
"elsefunction" are simple and not made up of nested functions. Otherwise, the
more readable form would be
-
(if (condition)
(thenfunction)
(elsefunction)
)
- Needless to say, the "thenfunction" and "elsefunction" functions can
be compound nested functions as well. Look at this monster "if" function:
1 (if (&& (== Event.Kind NOTE) (> Note.Key lownote))
2 (do
3 (if (!= Note.Key lastnote)
4 (do
5 (if (< Note.Dur length) (= Note.Dur length))
6 (++ count)
7 )
8 (if (>= count limit) (+ lownote offset))
9 )
10 (insert Event.Time Event.Chan NOTE lownote Note.Vel length)
11 )
12 (= count 0)
13 )
- For the sake of this example, I have numbered the lines as well as coloring the
brackets. I must also confess that this code fragment doesn't really do anything useful, I
just made up a bunch of stuff in order to demonstrate the form of a compound
"if" statement. The "if statement on line 1 has a condition made of the
Boolean function "AND" and two relational functions, both of which must be true
for the "if" function to execute. This "if" has a "then"
part that runs from line 2 through line 11, and an "else" part made up of the
assignment function on line 12. The "do" function starting on line 2 and closing
on line 11 nests the two functions, "if" from line 3 to line 9 and the
"insert" function on line 10. As it happens, the "if" on line 3 uses a
"not equal" as its conditional, It nests a "then" part in the form of
the "do" from line 4 to line 7 and an "else" part, the "if"
function on line 8. The "do" on line 4 nests the "if" function on line
5 and the "increment" function on line 6. Both "if" functions on lines
5 and 8 are on one line in that they have only a short conditional statement, a single
"then" function and no "else" part. Do you notice how the functions at
the same indention point within an "if" function make up the "then"
and "else" parts? Notice also that the functions at the same indention point
within a "do" function are all separate functions that share the same nest. Can
you see how the closing brackets of a compound function is always at the same indention
point as the corresponding opening bracket that resides a few lines above? These are
important conventions that make good programming habits and save hours of frustration if
things need to be changed later on. Remember, the "then" part is mandatory, the
"else" part is optional. You cannot technically have an "if" function
with an "else" part and no "then" part. This is of little consequence
in that an "if" statement's conditions can usually be rewritten to make it
unnecessary. If for some reason you have built a condition so complex or wish to keep it
readable from some particular point of view, and the action of the "if" function
as written need only act upon events other than those filtered by the condition (in other
words, the condition selects events that must be bypassed and everything else needs to
have something done to it), then you can make a do-nothing "then" part using the
relatively undocumented "NIL" keyword and
have the "else" part do all of the work. The form might look something like
this:
-
(if (&& (== Event.Kind NOTE) (|| (> Note.Vel 40) (>= Note.Dur 10)))
NIL
(delete)
)
In this
example we set up a rather complicated condition that an event must be a note event and
have either a velocity above 40 or a duration as long or longer than 10 ticks. If these
conditions are true, we do nothing. For all other events and for notes that do not meet
these conditions, we delete them. Notice that "NIL" is not in brackets. I don't
know why this is so, it just works that way. You never need to put "NIL" in
brackets, I guess because as far as CAL is concerned, it doesn't exist! Technically, we
could rewrite the above condition so that the "then" part could hold the
"delete" function like this:
-
- (if (|| (!= Event.Kind NOTE) (&& (<= Note.Vel 40) (< Note.Dur
10))) (delete))
-
- This condition says that if an event is either not a note or is a note that has both a
velocity less than or equal to 40 and a duration less than 10, it will be deleted. Both
statements do the same job, but the second one is simply better form. However, if for
continuity reasons it behooves you to create the "if" condition in the first
form, the "NIL" keyword is available to you.
-
- (switch
- The "switch" function is a very useful device so long as you understand how
and when to use it. This is the full form as you would use it in a program:
-
(switch value
1st_case (function)
2nd_case (function)
.
.
.
last_case (function)
)
- What "switch" does is allow you to select a chunk of code to run based on the
contents of "value". The "case" parts are simply the various possible
values that "value" can have, and the "function" part is what ever you
wish to happen if "value" equals "case". CAL evaluates the
"value" part, which may be a variable or a function or nested functions or
anything you want to use. Whatever the outcome is, even if it's a TRUE or FALSE outcome,
CAL will scan the list of "case" values until it finds a match and then run the
"function" and only that function associated with it. Having done this, CAL
exits the "switch" function (often referred to as a "switch tree") and
continues with whatever follows the "switch" closing bracket. Of course, using a
"switch" tree for a TRUE/FALSE "value" wouldn't make much sense
because there are only two possible outcomes, and an "if" function is all you
need to select a course of action based on two outcomes. Indeed, it makes no sense to use
"switch" at all unless you wish to select from three or more possible outcomes
of "value". As you might suspect, "function" can be a simple function
or a compound function of any size, just so long as CAL sees only one function in each
"case". All you need is a "do" to nest together everything you want to
have run and you can place entire programs in each "case". In fact, that's one
of the best purposes of a switch tree. See the section called Tips,
Techniques And Work-Arounds under the sub-heading Switch
Tree Menus for an example of a menu switch function. Another use for a
"switch" tree is as an access matrix to an array. I suggest you examine the code
of a CAL program I wrote called ANTILAP.CAL to take a look at an example of building and
accessing an array using "switch".
-
- (do
- I know that we have seen this little fellow allot so far, but I think it deserves a
formal introduction. The "do" function is basically a nest builder. (no, it
doesn't chirp and no, it won't poop on your disk drive). What it will do is take multiple
functions and nest them so they will fit into places where CAL expects to see only one
function. This is why we start CAL programs with a "do". A program is simply one
big function! The same is true for the functions that go within "if" functions,
"while" and "forEachEvent" loops, indeed anywhere that has
requirements for a single function but more needs to get done than will fit in one
function. Here is a specific example:
-
(forEachEvent
(do
(somefunction)
(somefunction)
(somefunction)
(anotherfunction)
(soforth)
(andsoforth)
)
)
- See how all of the functions that come after the "do" are grouped into one
nest because the "forEachEvent" function can have but one function as an action.
That one function is "do". Needless to say, any of the functions nested within
the "do" can likewise be "do" functions with their own nested
functions. Here is a place where you might think a "do" is needed but isn't:
-
(forEachEvent
(if (== Event.Kind NOTE)
(while (< Note.Key 64)
(if (> Note.Vel 40)
(+ Note.Key 10)
(+ Note.Key 20)
)
)
(delete)
)
)
- This is another useless code fragment, but proves a point. We don't need any
"do" functions because the "forEachEvent" sees only one function, the
"if". The "if" sees only one function, the "while" loop for
its "then" part. The "while" loop sees only one function, the last
"if". This "if" sees only one function for its "then" part
and only one function for its "else" part. Afterwards, the first "if"
has only one function, the "delete" for its "else" part. This is an
extreme example, but illustrates that you must think about your "do" placements
as each nested function takes up room in CAL and leaves less for calculations. If the
nesting levels become too encumbered, it has been my experience that you can
elicit an
"evaluation stack overflow" error at
some point from CAL. There is one qualifier to the above statements. I have had times when
building multiple "if" functions, that on some rare occasions one of the nested
"if" statements will not execute. I solved the problem just by adding an
otherwise useless "do" and nested the problem "if" statement under it.
I really didn't need it because there was technically only one function there. It seems
that the "do" somehow sharpened CAL's view of the whole compound statement.
Otherwise I can't explain why the "do" was necessary, it just worked! For a more
detailed discussion of this problem, see the section Tips,
Techniques And Work-Arounds under the sub-heading Hidden
Traps In The CAL Editor.
-
- (include "filename.cal")
- The "include" function causes the CAL program named in quotes to be loaded and
run at that point in the program containing the "include". After this spawned
program ends, execution continues with the code that follows the "include"
statement. It is as if the code in the "included" program had been pasted into
the calling program in place of the "include" statement. A good example is:
-
(do
(include "NEED20.CAL")
(declare your variables)
(and continue as usual)
- At the very start of a CAL program, it's a good idea to "include" the small
NEED20.CAL program supplied with Cakewalk that tests the version level of the CAL system
the user is running and makes sure that it is sufficient for running the program. If the
test in NEED20.CAL fails, the entire CAL system aborts and all execution stops. Therefore,
NEED20.CAL doesn't need to return any information back to the calling program. Make sure
you move NEED20.CAL from the Cakewalk directory to whatever directory you keep your CAL
programs in so CAL can find it. Otherwise you will have to put the entire path in the
quotes along with the file name and this makes your programs
less portable.
As a quick aside, some functions such as the new, advanced EDIT MENU functions cannot run
on a Cakewalk version 3 system, and some of them may not run on versions 4 or 5 if they
contain version 6 arguments. Keep this in mind, and if your program requires a more
advanced CAL system than 2.0, you will have to call similar programs to test for
"VERSION" being less than 31, 40 or 60 depending on the point where you wish the
test to fail. Examples of these version testing programs are part of my CAL programs
package (see the link to download the entire package of
my CAL programs). You can also use "include" to add in to all of certain
types of programs some feature they all should have in common such as correcting note
overlaps or removing slurred notes or whatever. See the section Tips, Techniques And Work-Arounds under the sub-heading Building An Include Library for insight into using
included programs.
-
- (DLL "file_name"
"service_name" first_argument second_argument)
The
best thing I can tell you about this function is to stay away from it! Pretend it doesn't
exist. Unless you are an experienced Windows programmer with all of the documentation you
need to use DLL programs, this function is poison. In a nutshell, this function
calls the DLL, or Dynamic Linked Library program "file_name"
listed in quotes and requests execution of the service "service_name"
within that DLL file. Both of these names must be in quotes. After the "service_name", you supply whatever arguments the service
requires in order to carry out its function. The (DLL) function will only accept 2
arguments. If you leave out an argument you are likely to corrupt the system stack and
thus likely crash your session of Windows, or at the very least make CAL and/or Cakewalk
go south. If you supply more than 2, those extra arguments will pass garbage instead of
the data in those extra arguments to the DLL. There is an example listed in the Help
files, but
I won't bother to show it or any other because if you
need an example, you have no business fooling with it and if you are experienced enough to
use it, I won't insult you with some piddly example. Enough said about the "DLL"
function.
- Updated Information: See the Updates page Helpful Tips for
Using the DLL Function for a bit more scoop on using this function.
-