Introduction
When designing Csound instruments, there are times when, depending a certain condition, we would rather have one part of the instrument code run or another. At other times, depending on a certain condition, we'd like to have one section of code run multiple times with slightly changing properties. In most programming languages, these situations come up often and are dealt with by using control flow constructs built into the language, and Csound has these possibilities too.This article will discuss methods of controlling the flow of operation in Csound instruments, how to use the opcodes for implementing the various techniques, and demonstrate some possible ways to use these techniques for musical purposes.
I. Branching: goto-label, if-goto, if-then
goto and labels
To begin we'll start off with discussing goto's and label's. In
the normal flow of evaluating instrument code, Csound wil run line by
line, opcode to opcode. In the following:
ivalue = 10
ivalue = ivalue + 2
ivalue = ivalue + 10
print ivalue
Csound will first assign ivalue to the number 10, then increase the value of ivalue by 2, then increase the value of ivalue by 10, and finally print the value of ivalue, which will equal 22. Now if we add a label and a goto:
ivalue = 10
ivalue = ivalue + 2
igoto skipSection
;this code will not be evaluated
ivalue = ivalue + 10
skipSection:
print ivalue
The value of ivalue, when printed, will be 12. Why? Because then Csound evaluates the igoto statement, it reads "From here, jump to the label marked as skipSection". So the Csound first assigns ivalue to the number 10, then increases the value of ivalue by 2, then jumps to "skipSection", and finally prints ivalue.
- A goto statement is one that, when that line is evaluated, jumps to a label.
- A label is a point of code that is labelled for the purpose of jumping to from another point of code
if-goto
Gotos and labels by themselves are not so useful as they
will always jump from one place to another when evaluated, but together
with conditional branching, the use of gotos and labels will enable us to express certain ideas.
A conditional branch is one where, depending on a certain condition, the evaluation of code will proceed or not down a certain branch. The number of branches can be one or many, depending on what is necessary to express what you want. When coding up instruments, we sometimes come to a point where we want to express an idea in the form of "if this condition is true, do this". That may continue on with other criteria: "If this condition is true, do this, otherwise if this other condition is true, do this, and if no condition passes then do this".
In Csound, the syntax for implementing a condition goto is:
if (test) goto label
where test is the conditional statement. Let's say we create an instrument where we want to print the value of an i-rate variable only if p5 is equal to 1, we can do so with the following code:
<CsoundSynthesizer> <CsInstruments> instr 1 ipch = cpspch(p4) iprint = p5 ; print p3 so we know which note instance this is print p3 if (iprint != 1) igoto skipPrint print ipch skipPrint: endin </CsInstruments> <CsScore> sr = 44100 ksmps = 1 nchnls = 2 i1 0 .5 8.00 0 i1 0 .5 8.01 1 ; this note will print it's ipch value i1 0 .5 8.02 2 </CsScore> </CsoundSynthesizer>
The code in instrument 1 says "if the value of p5 is not equal to 1, goto the label", which results in anything but one will skip to the label and if a 1 is given, the statements between the if and the label will be executed.
Running this you should have received output like the following:
new alloc for instr 1: instr 1: p2 = 0.000 B 0.000 .. 0.500 T 0.500 TT 0.500 M: 0.0 instr 1: p2 = 0.500 instr 1: ipch = 277.167 B 0.500 .. 1.000 T 1.000 TT 1.000 M: 0.0 instr 1: p2 = 1.000 B 1.000 .. 1.500 T 1.500 TT 1.500 M: 0.0
Notice the line in bold and compare it to the score in the CSD. Only the second note has a p5 value equal to 1, and only that note printed out its ipch, as expressed in the code.
NOTE: For Csound versions prior to 5.01, one thing to be careful about in using the if statement is to make sure there is a space after the if and before the parenthesis. Programmers used to writing code like:
if(test){
will find that Csound will give an error and one that might not be easy to understand that the problem is a missing space.
For the types of tests one can do inside the parenthesis, please see the manual entries for if or Conditional Values.
A Musical Example
Let's look at musical situation for using ifs, gotos, and labels. In my own instrument design, I use a pfield to designate what kind of articulation the instrument should use for it's amplitude envelope. Depending on what number is given, a number of different envelope types can be used. The following is a simplified example of this idea:
<CsoundSynthesizer> <CsInstruments> instr 1 ipch = cpspch(p4) iamp = ampdb(p5) ienv = p6 if (ienv == 0) kgoto envelope0 if (ienv == 1) kgoto envelope1 if (ienv == 2) kgoto envelope2 envelope0: ;ADSR kenv adsr 0.05, 0.05, .95, .05 kgoto endEnvelope envelope1: ;Linear Triangular Envelope kenv linseg 0, p3 * .5, 1, p3 * .5, 0 kgoto endEnvelope envelope2: ;Ramp Up kenv linseg 0, p3 - .01, 1, .01, 0 kgoto endEnvelope endEnvelope: aout vco2 1, ipch, 10 aout moogvcf aout, ipch + (kenv * 4 * ipch) , .05 aout = aout * iamp * kenv outc aout, aout endin </CsInstruments> <CsScore> sr = 44100 ksmps = 1 nchnls = 2 ;Triangle Ostinato i1 0 1.333 7.03 80 1 i1 + . 7.04 i1 + . 7.02 i1 + . 7.03 i1 + . 7.04 i1 + . 7.02 i1 + . 7.03 i1 + . 7.04 i1 + . 7.02 i1 + . 7.03 i1 + . 7.04 i1 + . 7.02 ;ADSR Chords i1 0 2 8.00 76 0 i1 . . 8.01 i1 . . 8.02 i1 5 2 8.00 74 0 i1 . . 8.01 i1 . . 8.02 i1 10 2 8.00 72 0 i1 . . 8.01 i1 . . 8.02 ;Ramp Lines i1 2 4 9.00 70 2 i1 7 4 9.01 70 2 i1 12 4 8.11 70 2 </CsScore> </CsoundSynthesizer>
The section of code above marked in bold
shows how the goto's and label's are being used. Notice the use of kgoto, as the code within each labelled group needs to operate at k-rate. Making sure rates match can be tricky and is dependent on the code within the code branches. As a general rule, you'll want to make sure that the code in the different labelled code branches run at the same rate, and that the type goto you use after the if(test) matches.
if-then-elseif-else
Using the if-goto-label form of conditional branching can be tedious with a large number of branches and it can sometimes be awkward to express the intention of the code, as in the example for the if-goto section, where one has to almost say the code backwards compared to what one is trying to express.
Another way to express conditional branching beyond using if-goto's and
labels is to use the if-then form in Csound. This was introduced
much later in Csound's history than if-goto, so it is common to see
if-goto in older Csound ORC/SCO's while it is more common to find
if-then in newer Csound files.
The syntax of if-then style conditional code looks like the following:
if (test) then
...do something...
endif
If there are multiple branches to the condition, you can append to the code using elseif-then:
if (test) then
...do something...
elseif (test) then
...do something else...
endif
You can add as many elseif branches as necessary to express what you are interested in expressing. Finally, to add default condition, something that happens if none of the other conditions for the other branches pass, you can do so using else:
if (test) then ...do something... elseif (test) then ...do something else... else ...do this if none of the other tests pass... endif
Rewrting the code from the previous example, the instrument would become
instr 1 ipch = cpspch(p4) iamp = ampdb(p5) ienv = p6 if (ienv == 0) kthen ;ADSR kenv adsr 0.05, 0.05, .95, .05 elseif (ienv == 1) kthen ;Linear Triangular Envelope kenv linseg 0, p3 * .5, 1, p3 * .5, 0 elseif (ienv == 2) kthen ;Ramp Up kenv linseg 0, p3 - .01, 1, .01, 0 endif aout vco2 1, ipch, 10 aout moogvcf aout, ipch + (kenv * 4 * ipch) , .05 aout = aout * iamp * kenv outc aout, aout endin
The text in bold shows the previous code rewritten using the if-then form of conditional branching. The same code idea has been expressed in much fewer lines of code and is generally easier to read and edit in my opinion. I highly recommend using this form of writing conditional code for current Csound practice.
Ternary Expressions
A common shorthand found in many programming languages is
the ternary expression. It is used in situations where one wants
to express "if this is true, do this, otherwise do that", and where
what is to be done is a simple expression, usually an assignment.
A popular ternary expression used in Csound is the following:
ifreq = (p4 < 15 ? cpspch(p4) : p4)
What this bit of code says is "If p4 is less than the value of 15, assign ifreq to cpspch(p4), otherwise set ifreq to p4". For ternary expressions, the syntax is:
(expression ? value1 : value2
)The above code is useful for designing instruments that can take in p4 values that are either in Csound's PCH format or as frequency in hertz. The assumption above is that values for Csound PCH do not go above 15 for the octave part of the PCH, and values given in hertz do not go below 15 as it is below the limits of human hearing.
The above could be rewritten with as the following if-then statement:
if (p4 < 15) then
ifreq = cpspch(p4)
else
ifreq = p4
endif
but it would be much more verbose. One might prefer to use this syntax if one finds the ternary syntax awkward, but the ternary syntax is more concise and can be read quickly once one knows its syntax. Ultimately when and where to use the ternary statement in one's code becomes a matter of taste and experience with when it makes sense ot use and when it doesn't.
II. Looping: loop opcodes
General Information
Looping(often called iteration in more technical settings) is a technique use to repeatedly execute a section of code
until a condition is met. To express an idea like "I want this
section of code to repeat n times" or "I want this section of code to
repeat for every position in the ftable", we could either write the
code n number of times for each time we'd like to do that part of code,
or we could write a loop.
First let's take a look at what would be required to invert every
position in an ftable about the zero axis. For an 8 point table,
the code required would be:
f1 0 8 -2 0 1 2 3 4 5 6 7
ival tablei 0, 1
ival = ival * -1
tableiw ival, 0, 1
ival tablei 1, 1
ival = ival * -1
tableiw ival, 1, 1
ival tablei 2, 1
ival = ival * -1
tableiw ival, 2, 1
ival tablei 3, 1
ival = ival * -1
tableiw ival, 3, 1
ival tablei 4, 1
ival = ival * -1
tableiw ival, 4, 1
ival tablei 5, 1
ival = ival * -1
tableiw ival, 5, 1
ival tablei 6, 1
ival = ival * -1
tableiw ival, 6, 1
ival tablei 7, 1
ival = ival * -1
tableiw ival, 7, 1
The above code works, but there's many limitations to coding like this:
- Fixed Size: Only works for the above fixed size ftable; if use wants to use bigger tables, they will have to add more and more lines of code
- Verbose: Coding the above is very long and would require many lines of code if there were many items in the ftable
Creating a Loop
If we take a closer look at the above code, it's mostly the same set of things
happening over and over, but with just one difference of where in the ftable
it is reading. The code below shows what is the same about each fragment of
code and what is different
ival tablei i_index, 1
ival = ival * -1
tableiw ival, i_index, 1
In the original code, the only difference between each fragment of code was the index into the ftable (what position of the ftable) used to read from, invert, and write back to.
To code more clearly and concisely and also in a manner that can scale to any size ftable, instead doing the operation each time by hand coding for each index, let's use a loop of code. A loop is where a single fragment of code is evaluated line by line, and then at the end of the line returns to the beginning of the fragment to evaluate once again. Now, if this was just it, we'd easily find ourselves in an infinite loop, a bug that would cause Csound to hang. Loops are often used with a conditional expression to break out of the loop.
So, let's rewrite the initial code, but this time using an if-goto statement to create a loop:
i_index = 0 loopStart: ival tablei i_index, 1 ival = ival * -1 tableiw ival, i_index, 1 i_index = i_index + 1 if (i_index < 8) igoto loopStart
Let's take a look at the above code. We start off setting a variable, i_index, equal to 0, which is the first position in the ftable we're going to operate on. Next we have a label called "loopStart", the place we'll want to return to for looping through the following code. After that we have the code we saw earlier that does the reading from the ftable location, the inversion of the value, and the rewriting of the value into the ftable.
In the next line we increment the value of i_index by 1, then we evaluate an if statement, "If the value of the index is less than 8 (the size of the ftable), go back to the start of the loop and run again". Now if you read through the code line by line, you'll see that i_index starts at 0, runs through the inversion code, increments to 1, then loops over, again and again, until i_index is equal to 8. When i_index is equal to 8, the condition to go to the loop start no longer passes so the if statement doesn't jump back to the beginning of the loop and then breaks out to contnue beyond that point in the code.
To make this bit of code a little more generic and easier to reuse with other ftables, let's modify it so we're not directly referencing the ftable's number but instead referencing an i-rate variable that holds the table number (itable). Also, instead of having to know the size of the table before hand, let's inspect the table itself to get the table size by using ftlen:
itable = 1 itablesize = ftlen(itable) i_index = 0 loopStart: ival tablei i_index, itable ival = ival * -1 tableiw ival, i_index, itable i_index = i_index + 1 if (i_index < itablesize) igoto loopStart
With this final version of this bit of code, we can now change the itable to any number and the rest of the code will work, no matter how large the the table size.
NOTE: With loops, it's very easy when is not used to writing them to get into a state where the loop never ends. If the above code did not have:
i_index = i_index + 1
Then the value for the test in the loop would always pass and igoto would always jump to loopStart:, and Csound would not on its own stop until you killed the process manually. Be very careful when creating loops!
Loop Opcodes
In Csound5, Istvan Varga introduced the loop family of opcodes which makes coding loops a litle more concise. The type of loop opcode used is dependent on the type of test one wants to make, but all of the more used cases are covered. The following shows the previous example using the loop opcodes:
itable = 1 itablesize = ftlen(itable) i_index = 0 loopStart: ival tablei i_index, itable ival = ival * -1 tableiw ival, i_index, itable loop_lt i_index, 1, itablesize, loopStart
By using the loop opcode, the line of code that involves incrementing the index is automatically done as part of the opcode, and in my opinion seeing the word "loop" helps to quickly see what that section of code is doing. In my opinion, the loop opcodes help to clarify code, especially when one uses nested loops (loops within loops).
Conclusion - Part I
When building instruments, often times cases arise where using conditional branching and looping can help to express ideas more concisely and/or ideas which can not be expressed in any other way. These techniques take time to learn not only how to use them but also when to use them, but learning to use these techniques can help to open up new expressive possibilities with your instrument design.
In Part II of this article, we'll take a look at the programming techniques of subroutines and recursion by using Csound's User-Defined Opcode system, which will further open up ways to design and reuse your instrument code.