Functions: making your own tools - Introduction to programming in ChucK - Programming for Musicians and Digital Artists: Creating music with ChucK (2015)

Programming for Musicians and Digital Artists: Creating music with ChucK (2015)

Part 1. Introduction to programming in ChucK

Chapter 5. Functions: making your own tools

This chapter covers

· Writing and using functions in ChucK

· Defining and naming functions

· Function arguments and return types

· Functions that can call themselves

· Using functions for sound and music composition

Now that you’ve had fun building a drum machine using the SndBuf UGen, it’s time to learn how to write and use functions. You’ve already used a number of functions, which we’ve also called methods, ranging from changing the .freq and .gain of oscillators, to using Std.mtof() for converting MIDI note numbers into frequencies, to generating random numbers using Math.random2() and Math.random2f(). All of these are examples of functions.

Often in writing programs you need to do the same types of things multiple times and in multiple places. So far you’ve retyped or copied and pasted blocks of code, possibly changing them just a little. You’ve seen how using ChucK’s built-in Standard and Math library functions help you get lots of work done by calling them with an argument or two. What if you could create and call your own functions? That’s what you’ll learn how to do in this chapter.

Adding functions to your programs will allow you to break up common tasks into individual units. Functions let you make little modules of code that you can use over and over. Modularity helps with reusability and with collaboration with other programmers and artists. It also makes your code more readable.

In this chapter you’ll learn how write functions, define and name them, pass values into them, and specify what they return. We’ll also look at functions that can call themselves, called recursive functions. We’ll always have an eye toward using functions to make music and make your compositions even better.

5.1. Creating and using functions in your programs

Initially, you’ll be creating new functions inside a single main file, looking conceptually like figure 5.1. You’ll have a file, saved as myProgram.ck, for example, that contains the main program that executes your code, and from that main program you’ll call your functions, boxes labeled Function 1 and Function 2, which are defined in the same myProgram.ck file. Because they’re defined in the same file, you can use them by name as needed for your program and composition. Once you’ve defined and tested your functions, you usually move them to the end of your program file, but you can define your functions anywhere.

Figure 5.1. For now, one program file will contain all of your code, including the functions that the main program uses.

Note

It’s possible to define functions and save them in their own files, and you’ll be doing that later, but for now you’ll keep everything in the same file.

5.1.1. Declaring functions

Functions are like variables, so you have to declare them in order to use them, just as you did with ints, floats, SinOscs, and the like. We’ll begin by discussing how to declare a function. There are four main parts to function declaration, as shown in figure 5.2. You begin the line with the wordfunction, or shortened as fun. Next, you declare the return type, or what type of data the function will give back after it finishes running. For example, this function will return an integer when it’s complete; this is like Std.ftoi(), which returns an integer from a float.

Figure 5.2. Parts of function code labeled

Note

Using a function is also sometimes called calling or invoking a function.

Next, you give the function a name. Just like with variables, you can name functions whatever you choose, although it’s helpful for those names to be meaningful and imply the utility of the function. Finally, you list the input arguments, which are optional. These are variables that you pass into the function for it to use while executing. There can be many different input arguments, all of varying types, based on your design choices as a programmer.

5.1.2. Your first musical function

Now let’s look at a musically useful function, one that returns the interval between two MIDI note numbers:

function int interval(int note1, int note2)

{

note1 - note2 => int result;

return result;

}

Yes, this is fairly trivial, because you could just subtract the two note numbers, but doing it in a function serves two purposes; you can use it over and again and you can name it something that reminds you why you’re doing it in the first place, in this case, interval.

As you can see from figure 5.3, you can view a function like a module of code with input values and an output value. This one has two integer input variables named note1 and note2. The function uses these values and creates a result as an integer value that’s output back to the calling program. For example, if you passed in (72,60) for the arguments, the interval function would return 12 (72-60), the number of steps in an octave.

Figure 5.3. A function is a block of code that takes arguments and returns a result.

To better understand this process, let’s start with an even simpler example, a function that adds an octave to any MIDI note number you pass to it, as shown in listing 5.1. You first create a function named addOctave with an input argument of an integer you name note and declare a return type of int. Then you create an integer named result , which you use to store your final answer. You add 12 (one octave) to your variable note and store this new value in your variable result. The result is returned to the main program .

When the program begins, it always starts in the main program and doesn’t run the function until it’s called. The program starts where addOctave() is called . Notice that addOctave() has a set of parentheses, and here you can enter any input you want (but it must be an integer). In this case you start by passing the number 60, and the function is executed, using 60 as the argument and running the function. At the end of the function, the result is returned, sending 72 as the answer.

Listing 5.1. Defining and testing a function that adds an octave to any MIDI note number

answer prints this out in the console:

72 :(int)

Calling addOctave with an argument of 72 would return 84, 90 would yield 102, and so on. Defining a function such as addOctave lets you use it over and again, and because it has a meaningful name, you know what it’s doing each time you use it or see it in code.

Two ways to call functions in ChucK

Functions with one argument can be called in two ways:

addOctave(60)

or

60 => addOctave;

These two ways of invoking a function work exactly the same way.

Let’s do a simple sonic/musical test of the addOctave function and also test two ways of calling functions, by adding these lines to listing 5.1. In listing 5.2 you play a sine tone on middle C (MIDI note 60, as assigned to an integer variable called note ). You first use the parentheses form of calling the Std.mtof and then use the “ChucK-to” form of the addOctave function and the Std.mtof function . As you can hear, either works fine.

Listing 5.2. Testing the addOctave function with sound

Now let’s go back and test the interval function. You pass two input arguments to the function, note1 and note2, as shown in the following listing. The function is executed twice, with two different pairs of numbers, which then computes the result, twice, returning it to the main program to be printed out, yielding this in the console:

12 -7

Listing 5.3. Defining and testing a MIDI interval function

// Function definition

fun int interval( int note1, int note2)

{

note2 - note1 => int result;

return result;

}

// Main program, test and print

interval(60,72) => int int1;

interval(67,60) => int int2;

<<< int1, int2 >>>;

5.1.3. Local vs. global variables

In the modified version of the program shown in listing 5.4, it’s important to understand that global variables can be accessed everywhere, including inside the functions or any structure with a set of { }; the local variables from the function, in this case, result, can’t be called outside that scope.

Every variable has a scope based on where it’s defined, called the locality. In every function, there is a set of curly brackets { }. This defines the area of the local scope. So in the programs of listings 5.4 and 5.5, the interval function has local scope variables note1, note2, and result. The main program, as shown in listing 5.4, has global scope variables int1, int2, glob, and howdy.

The program in the following listing will give an error, “line 16: undefined variable result.” But if you delete that last line, or comment it out by inserting // at the beginning, the program will run just fine.

Listing 5.4. Local vs. global scope of variables

// Define some global variables

"HOWDY!!" => string howdy;

100.0 => float glob;

int int1, int2;

// Function definition

fun int interval( int note1, int note2)

{

int result;

note2 - note1 => result;

<<< howdy, glob >>>;

return result;

}

// Main program, test and print

interval(60,72) => int1;

interval(67,60) => int2;

<<< int1, int2 >>>;

<<< result >>>; // This line will cause an error

Thus far you’ve made and used some simple functions that operate on integers interpreted as MIDI note numbers. Now you’ll make and use some new float functions for gain and frequency.

5.2. Some functions to compute gain and frequency

Now that you’ve seen the basics of functions and used them to solve a few simple problems, let’s put them into action to help control sound. You’ll define functions that operate on floats, interpreted as gains (0.0 to 1.0) and frequencies, and then use those to make your programs more musically expressive. You’ll set oscillator gains and frequencies using your new functions. You’ll then look at defining and using a function to gradually ramp gain up and down, creating a smooth amplitude envelope for each note you play. You’ll then go back to using a SndBuf to play a sound file, but with the addition of a function to chop up that file randomly, or granularize it, as it plays.

Let’s begin with a simple program, shown in listing 5.5, that has a function called halfGain() , which takes a floating point input value named originalGain and yields a floating point output. As you can see, this function is extremely simple, just dividing the input in half before it’s returned to the main program. To use this, you make a SinOsc s connected to the dac . Next you jump to the main program; execution skips the function definition until the function is explicitly called. You then print the current value of s.gain() . Notice that when you call s.gain()with an empty set of parentheses, a method built into SinOsc returns the current value of the .gain of s. You wait for one second, letting the sine play at this initial volume. Then you call the function p with the input s.gain() . The function executes, dividing originalGain in half and returning the new value of 0.5 to set s.gain. Again you wait for 1 second while object s is played at the new lower volume.

Listing 5.5. Function to cut gain (or any float) in half

5.2.1. Making real music with functions

Next, you’re going to build a real musical example that uses three different square wave oscillators: s, t, and u. This time you want to use functions to help set frequencies for all oscillators. First, you define two functions, octave() and fifth():

// functions for octave and fifth

fun float octave( float originalFreq )

{

return 2.0*originalFreq;

}

fun float fifth( float originalFreq )

{

return 1.5*originalFreq;

}

Notice that both of these functions have the same name, originalFreq, for the input argument. Because the scope of both variables is local only to each of the functions, it’s okay! The originalFreq in function octave() can be seen only within its own local scope, just like theoriginalFreq in function fifth() can be seen only locally to that function. But to avoid confusion, you probably wouldn’t want to label any global variable originalFreq.

If you dig a bit deeper, you’ll see that the new octave() function is different from the previous one (which accepted a MIDI integer note number). This one takes the input variable and multiplies it by 2.0. The function expects a frequency value in Hertz. Acoustical theory says that an octave leap up occurs for every doubling of frequency. The fifth() function multiplies by 1.5 giving a musical interval called a “just fifth” (named because it’s the fifth note in a standard musical scale) above any frequency argument.

Let’s now look at a complete program that uses those two functions, to create a very rich-sounding ascending frequency sweep, as shown in listing 5.6. You first make three oscillators and connect them to the left, center, and right dac channels . Then you set the gain of all oscillators , so that when the sounds add together, the total won’t exceed 1.0, which could cause the dac/speakers to overload and sound bad. Note that you take advantage of a feature of ChucK to set all three oscillators to the same value ; setting a parameter or variable value also returns that same value. The main program revolves around a for loop that increases from 100 to 500 by increments of 0.5 . Each time around, the value of the for loop variable freq is used to set the frequency of SqrOsc s . Then you use the return value of the octave() function to set the frequency of t , and you use the fifth() function to set the frequency of u .

Listing 5.6. Using functions to set oscillator frequencies

You’ve seen two examples of how functions can be used to help manipulate .gain and .freq methods of your oscillator unit generators. But you’ve been controlling .freq and .gain so far without using your own functions, right? It’s nice, however, to have meaningfully named functions, even if they only do something that you could do otherwise, because anyone reading your code (including you in the future) can guess what the octave and fifth functions are doing.

5.2.2. Using a function to gradually change sonic parameters

In this next example we’ll show you how to do something that would be much harder and less flexible to do without using a function. We’re going to define a function to gradually change the volume of an oscillator, ramping it up and down at an arbitrary rate. If you look at the program inlisting 5.7 you’ll see our new swell() function . Notice that it has four arguments: a UGen called osc and three floating-point inputs: begin, end, and step. The latter three variables are used to control two loops, one to increase and then another to decrease the volume of the oscillator osc.

Note

The swell function has a return type of void, which means it doesn’t return a value at all, because it doesn’t need to. Remember in chapter 1 when we introduced data types, and we promised you that we’d use void later on? Well, here we are. You can also make functions with void arguments, like you used for s.gain() in listing 5.6, because they don’t need data input to do their thing. Some functions like this return a value, others do useful work without either needing or yielding data. These might work on global variables, advance time, or do other things.

Listing 5.7. Using a swell function to ramp oscillator volume up and down

Note

When we defined our swell() function, we specified the first argument to be a UGen, which means that you can pass absolutely any type of unit generator into the function in that place. This takes advantage of a property called inheritance, which you’ll learn more about in a later chapter. For now, you can exploit it to make your functions extremely flexible and much more reusable. You’ll also learn about many more kinds of UGens, beginning in the next chapter.

If you look at our main program, shown in listing 5.8, which uses swell, you’ll see an oscillator => dac sound chain and an array you’ll use to play a melody . Then you enter a loop to play all of the notes in the array . Each time, the swell() function is called . Note thatswell() has time advancing inside it. It’s important to understand that the main program jumps into the function at and procedurally executes every line of code. Time passes in this function as the volume changes, and when the function is complete, the function returns to the main program, which executes the next trip around the loop.

Listing 5.8. Main program uses swell to expressively play a melody

As you can see, this is a highly expressive function that can be used to turn a simple oscillator into a musical instrument with smooth-sounding individual note beginnings and endings.

5.2.3. Granularize: an audio blender function for SndBuf

In a final example for this section, you’ll use concepts you learned in chapter 4 (about samples and sound files) but now using a function. The program in listing 5.9 loads and plays a SndBuf but constantly chops it up, playing random pieces using the .pos() method. This is a form of sound synthesis and manipulation called granular synthesis, which has been around for a long time; even before digital, people cut and spliced pieces of audio tape to do granular synthesis.

In listing 5.9, you first make a SndBuf2 named click, connect it to the dac , and load a nice stereo sound file . Remember that the audio directory containing your sound files must be in the same place as this program. To exploit functions, you make a new one called granularize() , which takes an argument called myWav (any SndBuf) and an integer called steps. This function uses steps to make random grains (little clips) of sound from myWav. To do that, it takes the total number of samples in the sound file and divides it by the steps variable to get a grain size . You then select a random play position within the sound file . You then advance time by grain and return to the main loop, which will be executed again forever.

Listing 5.9. Creating and using a cool granularize() function to chop up a sound file

Aside from the awesome sounds your granularize function makes, the wonderful thing about this function is that it can take any sound file and chop it up, so it can be reused over and over again. All you have to do is load a different sound file.

Try This

Load different sound files into the click SndBuf2 object in listing 5.9. There are other stereo and longer files in the audio directory you’ve been using, so try them out. Try changing the steps argument and hear the difference. Also, find other sound files (.wav, .aiff) on your computer, copy them into the audio directory, load them into click, and hear what they sound like when they’re granularized.

5.3. Functions to make compositional forms

You’ve begun to see how you can use functions to give you lots of expressive control and in ways that you can use repeatedly. In this section you’ll look at how functions can be used to help create new compositional forms. You’ll first create a melody pattern, then learn how you can use functions to operate on arrays, both reading and modifying the internal elements. Finally, you’ll make a super-flexible drum machine using functions and arrays.

5.3.1. Playing a scale with functions and global variables

Let’s explore how functions and global variables can work together by writing a program that walks up and down a little scale pattern, rising in pitch forever. In listing 5.10, you’ll use a new sound-making UGen, a Mandolin, and connect it to the dac . The Mandolin is a complete “instrument” that you can play with simple commands like .freq (like SndBuf) and .noteOn (which essentially plucks the Mandolin). We’ll talk more about Mandolin and many other UGens starting in the next chapter. Continuing with listing 5.10, you also define a global variable called note and initialize it to 60(middle C) . Both mand and note are global, because you declare them at the top of your program, outside any braces. Next, you define two functions: noteUp() and noteDown() , which have no input arguments, because they operate only on global variables. Also notice that the output types are set to void (denoting no return value). If you now look more closely at noteUp() , you’ll see that it updates the global variable note, by adding one , and prints out the new note value . noteDown() has similar functionality but this time subtracts 1 from note You define one more function called play() , which sets the pitch of your mand , plucks it with noteOn , and advances time by 1 second . The two functions noteUp() and noteDown() call the play() function to cause the mandolin to play.

Listing 5.10. Void functions on global variables, for scalar musical fun

Now let’s look at listing 5.11, which is the main infinite-loop program that uses our noteUp() and noteDown() functions. Notice that the loop doesn’t have any advancement of time in it. In many cases, this means the program wouldn’t work and ChucK would hang! But as you saw in the previous swell() and granularize() examples, time can be advanced inside functions. In this case, remember that when noteUp() is called , after it does its work, it calls the play() function ( in the previous listing), which does its work and advances time by 1 second. After this is complete, play returns to noteUp(), which immediately returns to the main program, where noteDown() is called . noteDown() decrements note, prints it out, calls play(), and then returns. This process continues for the remaining calls to noteUp() and noteDown().

Listing 5.11. Using the noteUp and noteDown functions in a main loop

This process keeps repeating, and this program plays the notes and prints out the following:

61 :(int)

60 :(int)

61 :(int)

62 :(int)

61 :(int)

62 :(int)

61 :(int)

62 :(int)

63 :(int)

62 :(int)

63 :(int)

... etc ...

5.3.2. Changing scale pitches by using a function on an array

You’ll now learn how you can use arrays with functions. Just as you passed UGens as arguments to functions, arrays are valid arguments as well. For example, you can define a function called arrayAdder that modifies one member (index) of an array (temp), to have a new value (add one to it). You’ll use this shortly to change the pitches of a note array called scale.

fun void arrayAdder( int temp[], int index )

{

1 +=> temp[index];

}

The following listing tests this new function, declaring a global array and our arrayAdder() function and then testing it out a couple of times, modifying two elements of the array.

Listing 5.12. Functions on arrays

The console prints out

60 62 63 65

60 62 64 65

scale[6] = 70

scale[6] = 71

showing that the elements of the global array were indeed modified; array[2] went from 63 to 64, and array[6] went from 70 to 71.

More on scope: temporary copies of int and float arguments inside arrays

When you pass an int or float into an array, the variable inside the function is a copy of the value passed in, local only to that function. So this program

// global integer variable

60 => int glob;

// function adds one to argument

fun void addOne(int loc)

{

1 +=> loc;

<<< "Local copy of loc =", loc >>>;

}

// call the function

addOne(glob);

// nothing happens to global glob!!

<<< "Global version of glob =", glob >>>;

prints out in the console

Local copy of loc = 61

Global version of glob = 60

clearly demonstrating that even though the local variable loc gets modified, the global variable passed in as the argument is not modified. For computer science techies, this is called passing by value, where the value of glob is copied into a locally declared variable, loc. Arrays are different, because they’re passed into functions by reference, and the local array variable name is just a reference to the exact same array that was passed in.

In other words, when you pass most data types into a function, you need to explicitly use the return keyword to get a result. But when you pass an array to a function, the contents of the array are modified directly.

Now let’s put our arrayAdder() function to work for a musical purpose. Listing 5.13 makes a Mandolin on which to play a scale , then uses the arrayAdder() function to convert the original scale array to a different scale. Using a new playScale() function you create to play the notes of any integer array passed in as an argument, you play the scale as it’s originally defined , then move the second and sixth elements up by one , and then play the scale again . Note that you call the arrayAdder() function twice and could call it any number of times with different arguments. This is the powerful aspect of functions, because they can be used over and over.

Note

For music theory types, we converted a Dorian minor mode scale into a standard major scale.

Listing 5.13. Using the arrayAdder() function to convert a scale from minor to major

5.3.3. Building a drum machine with functions and arrays

Now that you know how to use both global variables and arrays with functions, let’s start looking at how you can make a program that builds form into your compositions. In listing 5.14, you start out by setting up bass drum and snare drum sound buffers . Then you read in the sound files and set their pointers to the end (so they don’t make any sound initially). Then you define arrays that you’ll use to control your drum sample playback sequences . In the function playSection() , wherever you place a 1 in these arrays, you’ll hear a sound, and a 0 will be silence. The main loop calls playSection() with different pattern arrays to make a drum loop.

Listing 5.14. Drum machine using patterns stored in arrays

Exercise

Customize your drum machine! Change the 1s and 0s in the arrays, change the contents of the arrays used in the calls to playSection(), and change the tempo by changing the last argument in the calls to playSection(). Add more drums—you’re the composer and the programmer!

So far in this chapter, using functions and arrays, you’ve greatly expanded your expressiveness and also made your code more flexible and readable. But you can go farther, of course.

5.4. Recursion (functions that call themselves)

You’ve learned the basics of functions and how they can transform your music and program architecture. Now it’s time to learn advanced techniques that can result in very interesting sonic and structural materials. You’ve seen how functions can call other functions (like when noteUp() andnoteDown() called play() in listing 5.10). But can functions call themselves? And why would you want to do that? There are many musical structures (like scales) and figures (like trills and drum rolls) that are really repeated events or transformations. Many of these might benefit from a function that can call itself.

Here we introduce the concept of recursion, which in programming means that useful things can happen because a function can call itself. Because functions let you get lots of useful work done, sometimes repeatedly, then a function calling itself might multiply its power dramatically. The classic example of recursion taught in nearly every programming book is that of the mathematical function factorial, but because we’re artists, we’ll put that off and jump right into a musical example. Listing 5.15 shows a mandolin that gets played by a function recurScale() . After setting frequency , plucking with noteOn , and advancing time , the function calls itself, with a lower note (subtract 1) and a shorter duration (90%) . But it does this only if the argument is above some lower value (stops at note 40) . Otherwise the function would keep calling itself forever.

Listing 5.15. Recursive (function that calls itself) scale-playing function

So for a very small amount of code, you’re able to play lots and lots of structured (non-random) notes, by exploiting the power of recursion. You could, of course, accomplish this same thing using a for loop or by explicitly coding each and every note, but recursion gives you yet another new and powerful technique for controlling your sound and music.

Note

We should warn you that, while extremely powerful, programming recursions can be a bit dangerous, because you always have to build the stopping conditions (if note > 40) into your recursions. Otherwise, they’ll wind into themselves infinitely and never end. You always have the Remove Last Shred and Clear VM buttons on the miniAudicle to kill off any undead zombie processes, though.

5.4.1. Computing factorial by recursion

Let’s go back to the mathematical factorial function, but very shortly you’ll add a musical twist. The factorial function (written in math as N!, but we’ll write it here as a function, factorial(N)) computes the product of an integer with every other integer less than it, down to integer 1. For example, factorial(3) is 3 * 2 * 1 = 6, and factorial(4) is 4 * 3 * 2 * 1 = 24. Factorial has real-world applications in statistics, counting things, and other areas. The number of four-letter combinations (permutations) of the letters ABCD (like ABCD, ACBD,....) is 24 (factorial(4)). Note that factorial(4) = 4 * factorial(3), which is also 4 * 3 * factorial(2), and so on. This allows you to use recursion to compute all factorials, just by writing one function, as shown in listing 5.16. The factorial() function calls itself unless the argument is less than 1, in which case it returns 1. So factorial(4) returns 4 * factorial(3), which returns 3 * factorial(2), which returns 2 * factorial(1), which returns 1. All that returns the final value, 24.

Listing 5.16. Computing factorial by using recursion

fun int factorial( int x)

{

if ( x <= 1 )

{

// when we reach here, function ends

return 1;

}

else

{

// recursive function calls itself

return (x*factorial(x-1));

}

}

// Main Program, call factorial

<<< factorial(4) >>>;

5.4.2. Sonifying the recursive factorial function

For musical fun, let’s sonify (turn data or process information into sound) the recursive factorial function, as shown in listing 5.17. Here you’ll use a SinOsc but add a line inside the factorial function to sonify() the current value. That function adds half that number to 60(middle C) and plays the associated frequency on your SinOsc . In the main program you sonify the calls to factorial , so what you actually hear is a number of low descending pitches. You hear one note for each (recursive) call to factorial, followed by the final higher pitch of the result. Listen carefully; you may hear that factorial(2) is equal to 2 (same note at beginning and end) and that factorial(5) yields a result that’s almost too high to hear. This demonstrates, through sound, that the factorial function grows quickly in value for increasing argument value. This is one of the cool things about sonification and about ChucK.

Listing 5.17. Sonifying the factorial() function

5.4.3. Using recursion to make rhythmic structures

Let’s look at one more example in listing 5.18, similar to our factorial examples, this time to make a drumroll pattern but using only an Impulse unit generator . The Impulse UGen generates a click each time you tell it to. In the impRoll() function , you use the index argument as your counter to determine how many times the function will call itself recursively , and you also use the index as your delay between impulses for advancing time . The result is an ever accelerating roll, whose starting tempo and total length are determined by the argument to the call to theimpRoll() function from the main program .

Listing 5.18. Recursive drum roll using only an Impulse UGen

So you’ve seen that although they are powerful, functions that can call themselves can be super powerful. You can play many notes or sounds and compute complex mathematical results by exploiting recursion. But you also need to be careful in designing recursive functions, to make sure that they have a guaranteed stopping condition.

5.5. Example: making chords using functions

To wrap up this chapter, we want to give you one more musical example using functions, giving you the ability to play different types of chords (multiple simultaneous notes in harmony). To start, we’re going to show you an advanced way to declare your UGen sound network, which is to declare an array of unit generators. Any data type or object can be declared and put into an array, so you’ll use this to declare an array of SinOsc UGens named chord[] in the first line of listing 5.19 . You want your chord to have three notes. You use a for loop to ChucK each element in your chord[] array to dac , and then you set the .gain of each SinOsc so that they add up to 1.0 .

The function playChord() takes in a root MIDI note number as an integer value, a chord quality (major or minor), and a duration of time that the chord will play, named howLong. Those of you who are musicians will know that major and minor chords consist of three notes: a root note , a note a third above, and another note a fifth above. In both major and minor chords, the root and the fifth are the same. The fifth is seven MIDI notes higher than the root . The difference comes with the third. A major third is four MIDI notes above the root , sounding brighter; a minor third is three MIDI notes above the root , sounding darker. You control all of this with simple if/else logic. You also do one more test to make sure the user has specified one of the legal chord qualities, major or minor .

Listing 5.19. Playing chords on an array of SinOsc UGens, using a function

The main program, shown in the following listing, is an infinite loop that calls the playChord() function , generating a random number for the root value and playing a minor chord based on that note. Then you call playChord with two fixed chords, a C minor chord and a G major chord .

Listing 5.20. Using playChord

Exercise

Change the parameters to the calls to playChord, including root, quality, and duration. Add more calls to playChord() in the while loop. If you’re really musically ambitious, try coding up a whole set of chord changes for a song. Hint, “Twinkle” might start like this:

playChord(60,"major", second/2);

playChord(60,"major", second/2);

playChord(72,"major", second/2);

playChord(60,"major", second/2);

playChord(65,"major", second/2);

playChord(65,"major", second/2);

playChord(60,"major", second/2);

5.6. Summary

In this chapter you learned how to write and use your own functions, including the following facts.

· Functions allow you to better organize and document your code.

· Functions are declared by name (unique, like variables), return type (int, float, UGen, any type, even void), and arguments (values passed in for the function to operate upon).

· Well-designed functions can be used over and over again between programs.

· Functions can call themselves. This is called recursion.

· Variables have scope, which is local to a function or curly brace context or global, visible to all code.

You made lots of interesting sounds and musical structures with your new skills and knowledge of functions. Going through all these examples on functions should give you a strong grasp on how to organize your code into modules and, in essence, make your programs much more expressive, readable, and reusable.

In the next chapter, we’ll open the doors to creating new sounds in ChucK, looking at a variety of unit generators, which are the building blocks of sound synthesis and processing.