PIANOPAN.CAL

The text of this program is different than you would use in a real CAL program because of the addition of "nesting indention level" numbers at the start of each line of code. The reason they are there is because:

  1. HTML has limited ability to translate indention in normal fonts and so in order to demonstrate indention, that empty space has to be filled with something.
  2. With the addition of the red nesting level numbers, it is easy to see the nesting level of each line of code, see how the THEN and ELSE parts of an "if" function line up and how the closing brackets line up with the corresponding opening ones.

As far as the comment lines, they all follow the rules for comments in code because each comment line is preceded with a semi-colon. The colors serve to distinguish the nature of the comments. The blue comments explain what the code is doing in that section. The green ones denote the specific action being taken by the next line of code as a way of keeping strait the steps being taken within a larger blue-commented section.


Program Text for PIANOPAN.CAL


;pianopan.cal version 1.0 - A pan event insertion tool for Cakewalk (c) 1997 by D. Glen Cardenas

;

;PURPOSE

;The idea here is to simulate the stereo effect obtained by double miking an acoustical grand

;piano or other such instrument which has a physical range that corresponds to the pitch range.

;If you listen to the way such a piano is miked on stage for example, the lower notes come

;from the left and higher notes are projected progressively to the right as pitch increases. The

;trouble with most electronic instruments is that this stereo effect is not available, thus subtracting

;even more from the attempt to emulate an acoustical instrument. This program adds this measure

;of realism to the piano track or any other track to which you wish to create the effect of panoramic

;placement following the pitch. Vibes, marimbas and bell arrays also benefit from this effect.

;

;INSTRUCTIONS

;The first step in creating a stereo piano track is to split the left and right hands onto two

;separate tracks. It is recommended that you start with a "range" edit that cuts all notes below

;a certain pitch and then paste them to a new track. Supplement this edit by hand-selecting

;any notes for the left hand that were not cut and adding them to the new track and moving

;back any right hand notes that were inadvertently cut out. If you are doing a monophonic

;track or a polyphonic track that has only a few notes that tend to be grouped together, then

;this step is unnecessary. The reason for the split is that this program "looks ahead" in the

;sequence and groups notes that are struck at close to the same time calling them a chord and

;calculating one pan event for the entire group. If notes played in drastically different parts

;of the pitch range occur close together, than this program will be forced to group them anyway

;and give them all one pan location, that of the highest note. This runes the effect. Splitting the

;track into multiple tracks specific to a "range" will add to the realism of this effect. Just run the

;program on all of the tracks using the same option settings, then mix them with attention to

;smooth volume and EQ integration, and you have an absolutely believable stereo miking effect.

;When running the program, you can use the presets to give a good stereo mix for a piano

;track under various staging conditions. If you wish to hand-enter the parameters, the strength

;variable controls how wide the panoramic spread will be. The center variable determines which

;note will act as the middle of the pan. Middle C is note number 60 and is the default. If you enter

;negative 1, then the range of notes will be scanned and the note half way between the highest

;pitch and the lowest pitch will be calculated and used as the center of pan. Finely, you

;must give a gap setting for the program to use in grouping closely played notes as chords that

;will, as a group, receive only one pan value. The default value of 5 says that if there is a group

;of notes and they are no more than 5 ticks of each other, then they are a chord. A new pan position

;will be calculated only for the highest note in the group.

; ---------------END OF DOCUMENTATION-----------------

;

;

;----------------START OF CODE BLOCK-------------------

;As always, we begin this program with an opening "do" statement.

;This tells CAL that the program will be made up of multiple statements.

 0 (do

 ;Next we "include" the program "need20.cal" to be sure the user is running

;CAL version 2 or later. If not, the "need20.cal" program will end all CAL execution.

0 1 (include "need20.cal")

;Because this program depends on the order of the notes on the track being in the

;same order as they will voice, we include this program that tests for the condition

;I refer to as a track being split into "sub-tracks". This is a condition where after a

;group of edits on a track, Cakewalk is no longer storing all of the events as one

;continuous track but has sub-divided it. This is only a problem to CAL and is otherwise

;unnoticeable to the user. The condition will cause programs like this one to malfunction.

;If this condition exists, the included program will issue an error message and halt CAL.

0 1 (include "timerror.cal")

;Now we declare our variables.

;This is the storage for the note we will be setting the pan for. We also use it to calculate the

;center of the note span if requested, so it is initialized for that calculation just in case.

0 1 (int note 128)

;This is the number of ticks in which a chord will be grouped. The default is 5 ticks.

0 1 (int step 5)

;Here we have the factor that determines the strength of the effect. The default is 100 percent or one-to-one.

0 1 (int factor 100)

;Now we declare the variable that will hold the pan amount as the program runs.

0 1 (int pan 0)

;This next variable prevents us from inserting duplicate pan events. If the last pan event inserted

;is, say, 35 and the next one would also be 35, the program will not insert the second 35 event

;because it knows that 35 was the last pan inserted and the pan setting is already at 35. Just in

;case our very first pan event will be 0, we cannot initialize this variable to 0 otherwise the

;program will think that this first event is a duplicate and refuse to insert it. Therefore, we

;initialize it to -1.

;This storage variable holds the last pan value inserted into the sequence so we don't send duplicates.

0 1 (int last -1)

;Here is the note number that will be at center pan. The default is Middle C. A value of -1 will request

;the program calculate the center pitch in the span of notes.

0 1 (int center 60)

;The preset allows us to quick-select a setup for the program. A -1 is the default for the help boxes.

0 1 (int preset -1)

;This double word holds the event time of the note being processed.

0 1 (dword time 0)

;This double word holds the event time of the first note in a chord grouping.

0 1 (dword first 0)

;We will now run a "while" loop that drives the preset selection menu system. We can select any of several

;preset assignments for the variables and so quickly run the program using parameters that will give a

;predictable result. We can also elect to run all of the input functions and custom set the program's

;parameters or if we need a reminder of what the presets do, we can call up some simple help boxes.

;For so long as "preset" equals -1, run the following loop.

0 1 (while (== preset -1)

0 1 2 (do

;Ask the user to enter a preset number or request the help boxes.

0 1 2 3 (getInt preset "Select Preset Mode or Hit ENTER To Review List Of Presets " -1 5)

;For so long as "preset" equals -1…

0 1 2 3 (if (== preset -1)

;Do the following.

0 1 2 3 4 (do

;Display one or more "pause" boxes with user hints in the text fields.

0 1 2 3 4 5 (pause "Preset List : 0=Set All Variables By Hand 1=Narrow Stage Piano 2=Concert Hall Recording")

0 1 2 3 4 5 (pause "3=Wide Stage Piano 4=Panoramic Special Effects")

;Close the "do" nesting the "pause" statements.

0 1 2 3 4 )

;Close the "if" checking the value of "preset".

0 1 2 3 )

;Close the "do" nesting the contents of the "while" loop.

0 1 2 )

;Close the "while" loop.

0 1 )

;Now that we have a value for "preset", we use a "switch" tree to locate the preset values for our

;parameters. If the user enters 0 or a number not set up with a set of values, the tree defaults to the

;section that allows the user to enter all of the parameter values by hand for a custom setup.

;select based on value of "preset".

0 1 (switch preset

;If "preset" is 1…

0 1 1

;do the following.

0 1 2 (do

;Set variables for selection #1.

0 1 2 3 (= factor 30)

0 1 2 3 (= center 32)

;close the "do" for #1.

0 1 2 )

;If "preset" is 2…

0 1 2

;do the following.

0 1 2 (do

;Set variables for selection #2.

0 1 2 3 (= factor 50)

0 1 2 3 (= center 60)

;Close the "do" for #2.

0 1 2 )

;If "preset" is 3…

0 1 3

;do the following.

0 1 2 (do

;Set variables for selection #3.

0 1 2 3 (= factor 100)

0 1 2 3 (= center 60)

;Close the "do" for #3.

0 1 2 )

;If "preset" is 4…

0 1 4

;do the following.

0 1 2 (do

;Set variables for selection #4.

0 1 2 3 (= factor 200)

0 1 2 3 (= center -1)

;Close the "do" for #4.

0 1 2 )

;If "preset" equals "preset" (if "preset" is any number other than those tested for so far)…

0 1 preset

;then do the following.

0 1 2 (do

;Ask the user to enter values for variables.

0 1 2 3 (getInt factor "Enter the strength of the pan effect from 5 to 500 percent " 5 500)

0 1 2 3 (getInt center "Enter the note number for center pan " -1 127)

;Close the "do" for the custom input section.

0 1 2 )

;Close the "switch" function.

0 1 )

;There is one parameter left to be set, and the user is asked to enter the amount of ticks to allow

;for grouping notes into a chord.

0 1 (getInt step "Set minimum gap between chord notes " 1 20)

;Now we test "center" to see of it equals -1. If it does, we are being asked to find the center

;pitch in this range of notes. To do so, we compare the note pitches one at a time to our "hi"

;and "low" reference values in "center" and "note". The reason "center" is allowed to stay

;at -1 and "note" is initialized to 128 is that both of these numbers are outside the limits for

;note values. Therefore, no matter what the value of the first note, it will have to be greater

;than -1 and less than 128. As we keep testing notes, we keep comparing the pitch value to

;the values stored in "center" and "note" and thereby continue to drive the values in "center"

;higher and "note" lower. At the end of the loop, the lowest note we encountered will have

;its value captured in "note" and the highest note's value captured in "center".

;If requested to find the center pitch in this note range…

0 1 (if (== center -1)

;do the following.

0 1 2 (do

;Scan all of the events in the selected range.

0 1 2 3 (forEachEvent

;We are looking only for notes.

0 1 2 3 4 (if (== Event.Kind NOTE)

;Whenever we find a note, do the following.

0 1 2 3 4 5 (do

;If the note's pitch is lower than the lowest note so far, make this the new lowest note.

0 1 2 3 4 5 6 (if (< Note.Key note) (= note Note.Key))

;If the note's pitch is higher than the highest note so far, make this the new highest note.

0 1 2 3 4 5 6 (if (> Note.Key center) (= center Note.Key))

;Close the "do" nesting the compare equations.

0 1 2 3 4 5 )

;Close the "if" looking for notes.

0 1 2 3 4 )

;Run the loop until we run out of events, then close it.

0 1 2 3 )

;Here we perform a test to see if there were any notes found. If the value of 128 initialized into "note"

;is unchanged, then there were no notes in the selected part of the sequence. We issue a message

;stating this to the user and then abort the program.

;If no notes were found…

0 1 2 3 (if (== note 128)

;do the following.

0 1 2 3 4 (do

;Let the user know if the problem.

0 1 2 3 4 5 (pause "There Are No Notes In This Range! - Program Aborting.")

;Abort the program.

0 1 2 3 4 5 (exit)

;Close the "do".

0 1 2 3 4 )

;Close the "if".

0 1 2 3 )

;Sense we know we have notes, we calculate the center pitch among them. To do this, we first

;subtract the lowest note pitch from the highest. We divide the result by 2 and then add back

;the value if the lowest note to give us the proper reference point. This result is now placed in

;the "center" variable.

0 1 2 3 (= center (+ (/ (- center note) 2) note))

;Close the "do" nesting the center value calculation code.

0 1 2 )

;Close the "if" testing for "center" equaling -1.

0 1 )

;OK, so much for the set-up. Now we clear "preset" so we can use it for something else. We will use it

;as a marker to let us know if we are on our first pass of the loop to follow.

0 1 (= preset 0)

;This is the main program loop.

;For so long as there are events yet to be scanned…

0 1 (forEachEvent

;and if the current event being scanned is a NOTE…

0 1 2 (if (== Event.Kind NOTE)

;If this is the first pass through the loop and we are encountering our very first note, we must store the

;note and take no further action. There must be a note in storage at all times for this program to run.

;We will be using a somewhat complex process of testing for a valid stored note because of the part

;of the program that processes chords. This kink in the note storage update process prevents us from

;using the kind of straight-forward storage algorithm demonstrated in, for example, MONO.CAL.

;then test to see if we have a note is storage. If this is the very first pass through the loop…

0 1 2 3 (if (== preset 0)

;then do the following.

0 1 2 3 4 (do

;Store this current note pitch in "note" to become the next target note.

0 1 2 3 4 5 (= note Note.Key)

;Store the note's time in "time" as well.

0 1 2 3 4 5 (= time Event.Time)

;In case we encounter a chord, make sure "first" is updated with the value of this first note's time.

0 1 2 3 4 5 (= first time)

;Now set "preset" to 1 so we never run this part of the program again.

0 1 2 3 4 5 (= preset 1)

;Close the "do" that nested all of this code for storing the first note of the loop.

0 1 2 3 4 )

;If this is not the first pass, do the ELSE part of the "if" statement. Here is where the work is done. Now

;we use the target note in memory and see how close it is to the next note which is the note now being

;scanned. If the distance between the target note and the current note is shorter in ticks than the low

;limit selected by the user, then this is part of a chord and we must wait for the last note of the chord to

;calculate the pan for the whole chord. Notice how we use "time" to compare to the current note's

;event time and in no way alter the time stored in "first". This is because we will eventually use the value

;stored in "first" to insert the pan event. We want to insert the event at the start of the chord, so "first"

;always holds the time of the first note in the chord and "time" is used to do our gap calculations.

;Else, if this in not the first pass (in other words "preset" is not 0)…

;and if the current note and the note in storage (the target note) are closer than "step" to each other…

0 1 2 3 4 (if (< (- Event.Time time) step)

;Then do the following.

0 1 2 3 4 5 (do

;we must find the highest pitch in the chord, so we compare the target note to the current note and

;if the current note is higher in pitch, we make it the new target note. To do so, we update the target

;note's pitch in "note". Regardless of which note is higher, we update the note time in "time" to the most

;recent note so we can test this time against the next note to see if this note was the last note in a chord.

;If the current note is higher in pitch than the stored target note, make this note the new target.

0 1 2 3 4 5 6 (if (> Note.Key note) (= note Note.Key))

;Either way, store the current note's time in "time" for use in testing the next note.

0 1 2 3 4 5 6 (= time Event.Time)

;Close the "do" that nested the chord update functions that made up the THEN part of the "if".

0 1 2 3 4 5 )

;This is the ELSE part of the "if" that tested the distance between the notes.

;if there is more than "step" ticks between the current note's event time and the value stored in "time"…

;do the following.

0 1 2 3 4 5 (do

;We have a valid note in storage by virtue of having gotten to this part of the program. Also, the span in

;ticks between that note in storage (the target note or the note that is about to receive a pan event) and the

;note currently being scanned by this pass of the loop is at least "step" number of ticks apart by virtue of

;having failed the conditions of the "if" that tests for this minimum distance. All of the prerequisites having

;been satisfied, we can now generate a pan event based on the pitch if the note stored in "note". To start

;the process, we must convert the note's pitch range into a pan range. The first step is to adjust the note's

;position to the center of pan as entered by the user. The unaltered center of pan would be the same as the

;center of pitch, or 64. However, the user may have requested a different pitch to serve as the center of pan

;and so we adjust the pitch to offset it by the difference between the normal center and the user's selected

;center. Now we have a number that is positive if it was above 64 and negative if it was below 64. We

;use this new center value to calculate the note's pan position. This is accomplished all in one equation.

;Add to "note" the difference between normal center and user's requested center.

0 1 2 3 4 5 6 (-= note (- center 64))

;Now the scaling part. We have a value for absolute pan but the user may have asked the program to

;scale the result so that there is no longer a one-to-one relationship between the note number and the

;pan number. If the scale amount in the variable "factor" is 100%, then there is no scaling. A 1 number

;note shift yields a 1 number shift in pan. If "factor" is 50%, then every 3 numbers of shift in pitch yields

;2 numbers of shift in pan. If "factor" is 200%, then every 1 number shift in pitch generates 2 numbers

;of shift in pan, and so on. This then controls the "wideness" of the panoramic effect. To do this, we

;first take the adjusted note number and subtract 64 to move the center point of the number to 0. The

;note is now high or low relative to 0 instead of relative to 64. Now we do the scaling by multiplying the

;result by "factor" and then dividing by 100. Finely we add back the 64 we took away to return the

;center point to the middle of pan. The resulting value is the pan value for that note.

0 1 2 3 4 5 6 (= pan (+ (/ (* (- note 64) factor) 100) 64))

;If result fell below 0, correct to 0.

0 1 2 3 4 5 6 (if (< pan 0) (= pan 0))

;If the result went above 127, correct to 127.

0 1 2 3 4 5 6 (if (> pan 127) (= pan 127))

;This last error trap is obscure but important. If we are about to assign a pan event to a note that starts

;at the very beginning of a sequence and thus has an event time equal to 0, we must account for this

;because as a normal point of operation, this program inserts the pan event one tick before the starting

;time of the note. However, if the note has a starting time of 0, this is impossible, so we "fool" the

;program into thinking the note starts at event time =1 so it can successfully insert the pan event one

;tick sooner at event time 0. This isn't too important that the note and pan event start at the same tick, but

;if it does cause audible problems, you might want to think about changing the note's event time to 1 for

;real.

;If by chance the note starts at event time =0, correct to event time =1

0 1 2 3 4 5 6 (if (== first 0) (= first 1))

;Inform user of insertion of pan event.

0 1 2 3 4 5 6 (message "Installing PAN Event At Time Index " (- first 1))

;The above message may be wrong if by chance the pan event we are about to insert is the same value

;as the last pan event we inserted. This is because the next function tests for a duplicate pan event

;before doing the "insert" and refuses to do so if the pan event about to be inserted is the same value

;as the last one. Keep in mind that we use "first" as the source of our note time. If the stored note was

;not part of a chord, then this value is the same as that in "time" because when storing non-chord notes

;we always make sure the two are in sync. If we are finishing up a chord, then "first" will hold the time of

;the first note in the chord regardless of its pitch and "note" holds the highest note in the chord regardless

;of its time. These same two variables are used to insert the pan events for normal notes and for chords

;even though their origins may differ.

;If this pan event is not a duplicate of the last one, insert it at one tick before the start of the note.

0 1 2 3 4 5 6 (if (!= pan last) (insert (- first 1) Event.Chan CONTROL 10 pan))

;Update "last" to reflect the value of this most recent pan event for testing during the next insertion.

0 1 2 3 4 5 6 (= last pan)

;Now that we have used the current note to test our note in storage, and we have inserted a pan event

;for that note in storage, we no longer need it. The current note is placed into storage to become the

;target note during the next pass of the loop.

;Update "note" with pitch of current note.

0 1 2 3 4 5 6 (= note Note.Key)

;Update "time" with event time of current note.

0 1 2 3 4 5 6 (= time Event.Time)

;Make sure "first" agrees with "time" in case we start evaluation of a chord during the next loop pass.

0 1 2 3 4 5 6 (= first time)

;Close the "do" nesting the ELSE part of the "if" testing for the distance between notes.

0 1 2 3 4 5 )

;Close the "if" testing for the distance between notes.

0 1 2 3 4 )

;Close the "if" testing for a "preset" of 0.

0 1 2 3 )

;Close the "if" testing for event's being notes.

0 1 2 )

;Run the "forEachEvent" loop until we run out of events, then close it.

0 1 )

;If after running the loop, "preset" still equals 0, then there were no notes selected and the program did

;nothing. We therefore call the "pause" function inform the user that nothing happened and then we

;call the "exit" function to abort CAL.

;If there were no notes in the loop…

0 1 (if (== preset 0)

;do the following.

0 1 2 (do

;Inform the user of the error.

0 1 2 3 (pause "There were no notes selected - Program aborting!")

;Abort CAL.

0 1 2 3 (exit)

;Close the "do".

0 1 2 )

;Close the "if".

0 1 )

;If the above error trap was not sprung, then we found at least one note in the sequence and therefore

;there is a need to generate a pan event for the last note scanned by the loop. Remember, the note that

;received the last pan event was the last note in storage. Therefore, the last note scanned never had

;the chance to go into storage and have a pan event made for it. We will now do that here. Also, as an

;aside, if there had been only one note in the loop, then even though it went into storage there was

;never a "next" pass of the loop to insert a pan event for it. Luckily, this note in storage is the same as

;the last note scanned, so this will take care of him as well. The only change from the equations above

;is that we are not using the storage locations for our calculations, but the current note parameters.

;Add to "Note.Key" the difference between normal center and user's requested center.

0 1 (-= Note.Key (- center 64))

;Calculate pan amount.

0 1 (= pan (+ (/ (* (- Note.Key 64) factor) 100) 64))

;If result is less than 0, correct to 0.

0 1 (if (< pan 0) (= pan 0))

;If result is greater than 127, correct to 127.

0 1 (if (> pan 127) (= pan 127))

;If "Event.Time" is 0, correct to 1.

0 1 (if (== Event.Time 0) (= Event.Time 1))

;If current pan event is not a duplicate of the last one, insert it.

0 1 (if (!= pan last) (insert (- Event.Time 1) Event.Chan CONTROL 10 pan))

;Close the "do" nesting the program.

0 )

;-------------------- END OF PROGRAM ---------------------


Demonstrations of Running PIANOPAN.CAL


The following three graphics show a fragment of a sequence before and after running PIANOPAN.CAL on a right-hand piano track. The event list shows how the pan events were added in one tick ahead of the note. It also shows how notes occurring within five ticks of each other were grouped as chords and the pan event was inserted one tick before the first chord note but calculated from the pitch of the highest note in the chord. The last graphic shows the relationship between the pitch and the pan position.

 

1) This is a screen shot of the EVENT VIEW in Cakewalk before running PIANOPAN.CAL on a right-hand piano track.

 

2) Here is the same segment of the sequence after running the PIANOPAN.CAL program. Note how the pan events start one tick ahead of the note and how the chords are clustered and assigned a single pan event for the entire chord.

 

3) This screen shot shows the PIANO ROLL VIEW with PAN events selected for the CONTROLLER display under the note display. This shows graphically how the pan events rise and fall in concert with the pitch of the notes they are associated with.