[Top: John S. Allen's Home Page]
[Up: CAL home page]
[Previous: Writing and debugging]
[Next: Edit Menu Cross-Reference]

[contact John S. Allen by e-mail]

Site Logo, Track bike (2 KB GIF)Bikexprt.com Web Site


Speeding up CAL programs

The Cakewalk Application Language, CAL, is no speed demon. CAL uses interpreted code -- meaning that every time you run a CAL program, Cakewalk reads the program code line by line and converts it into instructions for your computer. That makes CAL slow. I have found that a CAL (forEachEvent ) or (while ) loop will typically process 20 to 200 events per second on a 75 MHz Pentium.

The tradeoff is that interpreted code makes a programming language easier to use -- you can test your programming changes immediately. In CAL, you can use the "Run" button in the CAL View. With compiled code, the program would run faster, but you would have to run a compiler program on it before you could test your changes.

CAL's tradeoffs in speed are appropriate for its purpose. And there are many things you can do to make CAL run faster. Here are some hints:

Manage the History list

You can make a major improvement t CAL program performance at run time. The performance increase works for all CAL programs, whether or not you wrote them yourself -- and also works for many other Cakewalk functions.

Recent version of Cakewalk have a multi-level Undo function. By default, the History List (where the Undo steps are stored) is set to 128 steps. After you have performed 128 operations on a .wrk file, the oldest Undo steps are bumped off the bottom of the list. This takes time! In my experience, a CAL program runs about twice as fast if the History list is not full.

Every time the CAL program calls a function which modifies your .wrk file, a new item is added to the History list. A (forEachEvent ) loop only uses a single step -- but some CAL programs may have two or three (forEachEvent ) loops for every track! A complicated CAL program may use many steps in the History list.

If you have done a lot of editing on a .wrk file before running a big CAL program, close and reopen the .wrk file to purge the History list.You can also lengthen the History list to let you do more editing be before the list fills up. But be aware that a longer History list uses more of your computer's memory. If you only have 16 MB of RAM in your computer, a long History list can start to swap data out to the hard drive, and that is really slow. So, for the best CAL performance, install more RAM in your computer and increase the number of steps in the History list. I have 80 MB of RAM and a 1024 step History list, and I have never had a glitch with it. I only get slowdowns after running a very complicated CAL program several times.

A complicated CAL program doesn't completely exhaust the History list, backtracking through dozens of Undo steps to undo a CAL program is tedious. It would be nice to have a DisableUndo command in CAL, so you could make a program use only one Undo step, except when debugging....but we have to make do with Undo as it is, for now.

There's a neat trick you can use to undo the changes a CAL program makes if all of them are in a single (forEachEvent ) loop. If you cancel the program inside the (forEachEvent ), your file automatically returns to the way it was before you ran that loop. You can easily put the option to cancel inside the loop. A preliminary (forEachEvent ) loop which puts the event count of the selection into a variable lets you trigger a (pause ) message after you the main loop has processed the last event. The (pause) message can report on the changes the program would make, and allow you to backtrack out of the program in two steps -- one for the main loop, and the other for the earlier event count loop. The event count loop actually didn't make any changes, but if you want to undo earlier changes, you do have to backtrack past it...

Know when to call Cakewalk menu functions in CAL

When you use a (forEachEvent) loop, CAL operates on events one at a time. If you write a (forEachEvent) loop that duplicates a Cakewalk menu function (for example, changing Event.Time for each event to duplicate the Slide command), your program will run much more slowly than a CAL program that just calls the Cakewalk function.

Actually, the Cakewalk menu function operates on one event at a time just as the (forEachEvent) loop does, but Cakewalk's compiled program code makes more efficient use of your computer's processing power than CAL does. When a CAL program calls a Cakewalk menu function, CAL itself then does relatively little work. All CAL has to do is point to the Cakewalk function, saying in effect "OK, now you go move those 5000 events."

Calling on menu functions works fine as long as the CAL program only calls each menu function once or a few times. Avoid using Cakewalk menu functions inside a loop that repeats hundreds or thousands of times. Each single Cakewalk menu function slows down CAL much more than other typical single operations inside a (forEachEvent ) or (while ) loop. So it is generally faster to use CAL functions if you are processing only one event or a few events at a time. Use menu functions to work on many events at a time.

The (EditCut40 ) command is especially slow, in my experience. This is probably because it deletes events and stores data to the Undo queue and to a buffer each time it is invoked.

Event Filter or (if (Event.Kind )...?

Here's another way you can call on a menu command to save time. Inside a (forEachEvent ) loop, you could, for example, choose to perform operations only on notes, using the (if (Event.Kind NOTE ) conditional. Or, you could use then Event Filter once before the loop, to select notes.

Using the Event Filter once is faster than testing each event separately, so use the Event Filter when you can. You can't always, though. Some conditions can't be described by the Event Filter. And, once you use the Event Filter, you have changed the selection that existed before you ran the CAL program. Suppose you want the loop to move controller messages, but to move notes AND change their duration. Clearly, you will have to use an (if (Event.Kind NOTE ) conditional inside the loop to initiate the extra operation performed on each note.

The Event Filter can tear down...or build up

When you invoke the Event filter from the Cakewalk Control Bar, it always starts out with all types of events selected. Then you deselect events until you have the selection you want.

If you run the CAL recorder to translate this process into CAL, you will generate a list of commands starting with

(ResetFilter 0 1)

Every type of event you want to deselect now will require another CAL statement. If you want to select only notes, the CAL Recorder will generate 16 more statements like

(SetFilterKind 0 CONTROL 0)
(SetFilterKind 0 KEYAFT 0)

and so forth for every kind of events except notes, until only notes remain selected. But you can also start with

(ResetFilter 0 0)

Then you can build up. If, for example, you want to select only notes, you add only one more line,

(SetFilterKind 0 NOTE 1)

So, tear down or build up, whichever uses the smaller number of statements. But beware of some serious BUGS in the CAL Event Filter commands under CPA 6.01 and probably under earlier versions. If you use the "build up" approach, the selection extends by one tick on each end. For more details, check out the demonstration file selchnge.zip on this site.These bugs have been corrected in CPA 7.01.

A zero-length selection (which nonetheless includes all of the events on its one tick, including the full duration of notes on that tick) will expand to the full length of the file when you run a (forEachEvent ) loop...even in v. 7. Cakewalk technical support claims that this is intentional! Suppose you actually only want to select the events on one tick? You can work around this "feature" by making the selection one tick long and then using (if (== Event Time dFirstTick) to eliminate the events on the second tick of the selection..

Here is a CAL file demonstrating some characteristics of the Event Filter when building up:

(do
; start with nothing selected.
(TrackSelect 0 -1)
(= From 0)
(= Thru End)
; Select track 1
(TrackSelect 1 0)
; Start with nothing in the filter, to build up.
(ResetFilter 0 0)
; The next line must be present, or no notes will be selected.
(SetFilterKind 0 NOTE 1)
; Restrict the range of selected notes to keys 106 through 108.
(SetFilterRange 0 0 1 106 108)
; The (SetFilterKind ) command remains in effect as long as not all of the notes are deleted.
(EditSlide40 10 1 1 0) ; deletion of some but not all selected notes would also work here.
; The next line overwrites the previous SetFilterRange command.
(SetFilterRange 0 0 1 100 103)
; Now, only notes in the key range of 100 to 103 are selected.
(SetFilterKind 0 CONTROL 1)
; Next, restrict the range of selected controllers to controllers 1 through 3.
(SetFilterRange 0 5 1 1 3)
; Next, restrict the range of selected controllers to those with values 64 through 96.
(SetFilterRange 0 6 1 64 96)
; The following SetFilterRange command overwrites the one two lines back.
; The order of SetFilterRange commands is unimportant, but they must be preceded
; by the appropriate SetFilterKind command.
(SetFilterRange 0 5 1 7 11)
; Now, controllers 7 through 11 with values 64 through 96, and notes 100 through 103 are selected.
)

When this program has run, notes with key numbers 100 through 103 are selected, as well as controller messages 7 through 11 whose values are 64 through 96. When using the Event filter to build up, a (SetFilterKind n KIND 1) command must come before all (SetFilterRange ) commands for that kind of event, or no events of that kind will be selected. Also, the latest SetFilterRange command for each parameter overwrites an earlier one for that parameter. It is not possible, therefore, to select or deselect more than one range at a time for any parameter. In the example above, selecting the range of notes from 100 to 103 deselects the range of notes from 106 to 108. You must run a loop to process one range and then select another range. If that routine does not delete the entire selection for that type of event, the (SetFilterKind n Kind 1) command remains in effect and a new (SetFilterRange ) command may be issued alone.

Manage included modules

Included modules are very helpful to avoid your having to rewrite code. You can string together a complicated program from a series of modules.

But avoid using included modules inside loops which execute repeatedly. Calling an included module and returning to the main program take time. I had a (while ) loop go from 14 milliseconds to 40 milliseconds per iteration when I placed its contents in an included module.

An included module in CAL will recognize any variable which has been defined previously. This allows the included module to process data which is passed to it. Parameter passing is a very powerful feature, allowing you to use the same included module in many different programs or at different places in the same program.

Parameter passing also may cause program bugs due to reused variable names. You may use the "undef" command after an included module to release variable names -- but defining and undefining variables does take time. Don't define and undefine repeatedly unless you have to. In particular, don't define and undefine inside a (forEachEvent ) or (while ) loop which executes over and over. Defining and undefining 18 variables takes about 7 milliseconds on a 75 MHz Pentium. I tested this by placing a (while ) loop inside and then outside the definitions.

Alternative calculation procedures save time - if...

Arithmetic division simplifies if the divisor is 1, so you might want to use a routine like this:

(if (== dDivisor 1)
(= dQuotient dDividend)
(= dQuotient (/ dDividend dDivisor) ;Else
)

I have tried such alternative procedures in CAL -- even in situations where they let me skip over many lines of code -- and they didn't save much time. For example, I have written a CAL program which changes note times and optionally, also changes durations. This program runs no faster if I use an impossible (if ) expression to make sure that the duration option is never called. The program does run faster when the unused expressions that change durations are deleted. Apparently, CAL spends most of its time chugging through code looking for a matching parenthesis. The actual calculations take relatively little time, by comparison.

You can save time by placing different options in separate, complete, shorter loops and having the program choose among them. The branch to the alternative procedure is then made only once, rather than for every pass through the loop. The lines that would have to be skipped over are in a different branch of the program where they don't have to be read repeatedly.

You may even place alternate procedures in included modules to avoid making your CAL program too long. As long as the CAL engine has to open the included module only once for an entire, repeated loop, the speed penalty will be small.

Managing screen messages, comments and blank lines

Sending a message to the Cakewalk status line is extremely time-consuming in CAL. I have a very long (forEachEvent ) loop that went from 30 milliseconds to 20 per iteration when I commented out its one "message" line. A useful compromise is to make the message change only once every 100 events, with code like this at the end of the loop:

(++ dEventsProcessed)
(if (% dEventsProcessed 100) ; false if divisible by 100)
NIL ; do nothing if true
(message dEventsProcessed "events processed.") ; else post message
)

The display will update every couple of seconds, allowing time to read it -- and the program will run much faster.

Comments and blank space do not noticeably slow down a CAL program. CAL evidently discards them when it reads the file into memory. However, there is a limit to the length of a CAL file, so you may have to remove comments and blank lines from a long program.

Skip over empty tracks, channels, key numbers...

I have a CAL program that must separately check each note on each MIDI key number of each channel of each track, channel by channel and track by track, nested in that order from inside to outside.

When I first got the program running, it took 30 minutes to process a 5-minute MIDI file with 10 active tracks. Most of that time, it was searching for notes where there weren't any.

I set up an outer loop to make the program skip empty tracks. If the note count for a track is zero, the program moves on to the next track without checking each channel in the empty track

Inside the outer loop, I set up another loop to look for empty channels. If the channel contains no notes, the program moves on to the next channel.

As a final touch, I made the program set variables for the highest and lowest note in the channel it is processing. When it finally goes to process notes in that channel, key by key, it skips all of the key numbers below the lowest one in use and above the highest one in use. It still, unavoidably, looks for notes on some key numbers which have no notes, but only within the range of notes actually used in the track.

The program now completes in 20 seconds. It is a hundred times faster than it was. Look for opportunities like the ones this program gave me, and use them.

Get a newer Cakewalk version?

I have found that certain CAL commands run much faster in newer versions of Cakewalk. Another reason to upgrade.

Get a faster computer?

A Pentium III 500 computer would make your CAL programs and everything else run faster. I didn't have to tell you this, now did I? On the other hand, you can do enough to speed up your CAL programs that you can probably avoid buying a new computer till next year, when they'll be twice as fast at the same price.


[Top: John S. Allen's Home Page]
[Up: CAL home page]
[Previous: Writing and debugging]
[Next: Edit Menu Cross-Reference]

[contact John S. Allen by e-mail]

Contents © 1998 John S. Allen

Last revised 1 March 1999