One of the goals of programming is to produce code that is reusable, modular, and legible. Reusability means that once you solve a particular problem with a particular solution, there’s no need to re-invent the wheel: the solution is already out there (or in your files) and can be accessed and implemented whenever you need it. Code is kept modular in order to separate logical ideas and solutions for particular problems into chunks that can be applied in a number of situations. Modularity helps to make code reusable because you can see how abstract solutions can be useful in a variety of contexts. It also makes solutions extractable and shareable on the EarSketch social network, making remixing projects easier. Finally, perhaps the most important goal is to make code that is legible--understandable and easy to read. We use comments to add some in-line notes to help readers (or even the same programmer later!) understand the goals of particular pieces of code. Sometimes, though, this isn’t enough to get a good understanding of what’s going on in a particular piece of software. Promoting legibility in code and keeping code organized, logical, and efficient is essential to keep things understandable.
The techniques that we’re going to cover today serve to create manageable code, and along the way we’re going to be rewriting the code from Day 1’s projects. This revision process is called refactoring. Refactoring involves rewriting code to create a program that is more efficient, cleaner, and understandable than it was before. These tools and techniques aren’t solely useful for refactoring though: as they become more and more natural, you’ll be writing clear, well-organized scripts from the beginning.
There are many programming situations where you may wish to have some section of code execute several times. For instance, let’s say that we want to insert a single, four measure long clip of music onto a particular track three times. The beginning of one clip should be four measures from the beginning of the previous clip. In other words, we want to place the clips so that, as soon as one clip ends, another clip begins. At first, you might approach this situation this way:
# Method 1 path = HIP_HOP_DRUM_FOLDER randomFile = selectRandomFile(path) fitMedia(randomFile, 1, 1, 5) randomFile = selectRandomFile(path) fitMedia(randomFile, 1, 5, 9) randomFile = selectRandomFile(path) fitMedia(randomFile, 1, 9, 13)
This will yield the desired results. However, there’s a sign that there is a better way of doing things here--the code is very repetitive and difficult to read. Because programmers are always looking for simpler and clearer ways of representing common situations, there are techniques to accomplish the same thing while typing less. In this case, it would be much simpler to use a loop. The loop structure makes the code appear much more readable and clearly indicates the logic. The code above can be rewritten using a loop like so:
# Method 2 path = HIP_HOP_DRUM_FOLDER for count in range(1, 13, 4): randomFile = selectRandomFile(path) fitMedia(randomFile, 1, count, count + 4)
Yesterday, we covered the basics of loops, but let's delve into more detail with this example. The variable
count is the index variable for the loop. As the loop iterates, or executes the statements inside the loop,
count will be assigned numbers from 1 through 12. As with the loop examples from yesterday, when measure reaches 13, the loop stops execution. In this case, the range is extending from measure 1 to measure 12 (because as with Day 1's loop, the code stops being executed on the 13th iteration, leaving 12 complete cycles).
range() function in this example has three parameters, not two as in yesterday's example. The third parameter tells the loop how much to increment the index variable every iteration. If there is no third parameter, the index variable will increase by one every time. After each iteration, the value of
count will be increased by four. In this example,
count equals 1, then 5, then 9, and finally 13, when the loop ends. As
count increases, so does the measure value within the
fitMedia() function that places your clip into Reaper. Since
count is being incremented by 4, so is the measure value also controlled by
endMeasure parameter of
fitMedia() can be similarly defined by
count + 4, offering a much more compact way to implement the same logic as the first example.
Questions and Exercises #1
1. How could this code be made more manageable?
fitMedia(HIP_HOP_BASS12_4, 1, 1, 2) fitMedia(HIP_HOP_BASS12_4, 1, 2, 3) fitMedia(HIP_HOP_BASS12_4, 1, 3, 4) fitMedia(HIP_HOP_BASS12_4, 1, 4, 5) fitMedia(HIP_HOP_BASS12_4, 1, 5, 6) fitMedia(HIP_HOP_BASS12_4, 1, 6, 7) fitMedia(HIP_HOP_BASS12_4, 1, 7, 8) fitMedia(HIP_HOP_BASS12_4, 1, 8, 9) fitMedia(HIP_HOP_BASS12_4, 1, 9, 10) fitMedia(HIP_HOP_BASS12_4, 1, 10, 11)
range() are all examples of functions. Functions allow a user to execute some preexisting section of code without having to write it over again. For instance, without the
setTempo() function, you would have to write all of the code that works to set the tempo of a project within Reaper for every single project. That would be a lot of wasted energy. Instead, if you want the tempo of your project to be 120 beats per minute, you only need to call the function by typing:
Functions also make code flow in a clearer way. They reduce duplication of lines and small details that get in the way of a clear understanding of a script’s goals. Instead of a mass of undifferentiated functions, the backbone of a script becomes a list of function calls that build towards a clear purpose. Functions provide a script with structure, making it more readable.
Take the following example from Day 1's exercises:
''' Day 1 Sample Project 3 Music with A and B sections ''' from earsketch import * # initialize Reaper init() setTempo(120) # Create an A section just like usual insertMedia(DRUM_AND_BASS_MASTERBASS1_4M, 1, 1) #Lead insertMedia(DRUM_N_BASS_DRUMS1_4M, 2, 1) #Drums # fit a 2 measure B section between measures 5 and 7 fitMedia(DRUM_N_BASS_DRUMS3_1M, 1, 5, 7) #Drums breakout # Then back to section A at measure 7 insertMedia(DRUM_AND_BASS_MASTERBASS1_4M, 1, 7) #Lead insertMedia(DRUM_N_BASS_DRUMS1_4M, 2, 7) #Drums # Set the project cursor to be at the beginning of the file finish()
This code creates a Reaper project with an A and B section. The code essentially uses two different clips within the same track to create music that has two different sections. This particular structure instructs for one (A) section to play, then another (B) section to play, then the first (A) section to play again. To make this code more modular, we could split the various components of the script into functions.
We’ll start by splitting the code into functions that create track A and track B:
# Day 2 Sample Project 1 from earsketch import * # initialize Reaper init() setTempo(120) # make a function for section A def insertSectionA(file1, file2, measure): insertMedia(file1, 1, measure) insertMedia(file2, 2, measure) # make a function for section B def fitSectionB(file1, startMeasure, endMeasure): fitMedia(file1, 1, startMeasure, endMeasure) # Call the functions that play the sections: insertSectionA(DRUM_AND_BASS_MASTERBASS1_4M, DRUM_N_BASS_DRUMS1_4M, 1) fitSectionB(DRUM_N_BASS_DRUMS3_1M, 5, 7) insertSectionA(DRUM_AND_BASS_MASTERBASS1_4M, DRUM_N_BASS_DRUMS1_4M, 7) # Set the project cursor to be at the beginning of the file finish()
Let’s take a quick look at how a function is defined:
# make a function for section A def insertSectionA(file1, file2, measure): insertMedia(file1, 1, measure) insertMedia(file2, 2, measure)
When we define a function, we use
def to let python know that there is a new function being defined. Then we name the function, and define what kinds of parameters it expects. These parameters, defined through variables, allow the function to accept arguments. In this case,
insertSectionA() is taking
measure so that they can be passed on into the code that the function executes, indented inside the function, just like a loop. The code inside the function uses the same variables that are defined in that function’s parameters, letting the system know how the parameters should be passed along to the code that is executed when the function gets called.
This code makes use of two new functions:
insertSectionB(). The first takes in two file names as parameters as well as a measure number. When
insertSectionA() is called, the files are inserted into the first two tracks to be played simultaneously, just like the original code. It also takes a measure, meaning that you can pass it a number to make it play at particular times. This is why we call the function twice in the script above: the first time to pass it a measure value of 1, and the second to play the section at measure 7.
# make a function for section B def fitSectionB(file1, startMeasure, endMeasure): fitMedia(file1, 1, startMeasure, endMeasure)
fitSectionB() takes in one file, a starting measure variable, and an ending measure variable. It requests a single file, in this case the keyboard clip
DRUM_N_BASS_DRUMS3_1M, and plays it by itself beginning on
startMeasure, automatically looping the clip (or truncating it) to end on
As you can see, it is now possible for a user to choose any clip he or she might like and to produce a specific style of song that has an A and B section, or AABB sections, or any other permutations just by manipulating the function calls and parameters. Additionally, one nice thing that functions do is let you make code more generic, meaning that it can be used in situations that go far beyond their original intended uses. At this point, it seems like it may be a good idea to rename
fitOneClip() and just use the parameters like files and measure number to create unique sections.
Remember: defining the function itself does not ask the computer to execute any code. Code does not execute until the function is called with the correct parameters inserted.
Questions and Exercises #2
1. How would you clean up this code?
from earsketch import * init() setTempo(120) fitMedia(AFRO_LATIN_DRUMS1_2M, 1, 1, 5) fitMedia(AFRO_LATIN_BASS1_2M, 2, 1, 5) randomFile = selectRandomFile(AFRO_LATIN_FOLDER) fitMedia(randomFile, 3, 1, 9) fitMedia(ELEKTRO_HOUSE_DRUMS1_2M, 1, 4, 9) fitMedia(ELEKTRO_HOUSE_BASS1_2M , 2, 4, 9) randomFile = selectRandomFile(ELEKTRO_FOLDER) fitMedia(randomFile, 3, 4, 9) fitMedia(SOUL_DRUMS1_2M, 1, 8, 13) fitMedia(SOUL_BASS1_2M, 2, 8, 13) randomFile = selectRandomFile(SOUL_MUSIC_FOLDER) fitMedia(randomFile, 3, 8, 13) fitMedia(RAW_POWER_DRUMS1_2M, 1, 12, 17) fitMedia(RAW_POWER_BASS1_2M, 2, 12, 17) randomFile = selectRandomFile(RAW_POWER_FOLDER) fitMedia(randomFile, 3, 12, 17) finish()
2. Write a function called
placeFile() that insert a random clip from any sound folder at any measure on any track and fit them to any measure length.
So far, we've discussed how functions improve the readability and versatility of our code. A third reason to use functions is to help in the process of finding errors within code.
As a programmer, you will make many mistakes that will cause your code to work incorrectly. This is common, and it happens to everybody! The process of finding and fixing these mistakes, or errors, is called debugging. There are two different types of errors that you will encounter while programming: syntax errors and runtime errors.
Syntax errors occur because something within the code is not typed according to the syntactical rules of python. If we remember, python has a specidic syntax, which is like its very own grammar. If the specific rules of this grammar are not followed properly, syntax errors occur. In Komodo, syntax errors are easy to find. They get highlighted with a red, wavy underline that points you to where the error is.
Runtime errors are errors that do not occur until after the program has begun to execute. Komodo cannot find and highlight runtime errors and they are only detected when a program begins to run and then crashes or shuts down. For this reason, it can be difficult to fix runtime errors because it is often unclear as to which portion of code is causing the problem.
By refactoring your code and splitting it into easy to read functions, the job of finding the source of a runtime error is made significantly less daunting. This is done by selectively calling functions and commenting out parts of your code in order to see if certain parts of the code work correctly. For instance, if your code worked perfectly while only one function is being called, you can deduce that the error is not contained within that function. Once you’ve determined that one section of code works properly, you can move on to another section, thereby continuing to narrow your search. Once the program begins to throw exceptions, a term used to describe what happens when a runtime error is encountered, you have a much more narrow field of vision and can easily point out the section of code that needs to be examined.
Once you have found the function that contains the error, closer examination is necessary. Another way to aid in debugging is to add print statements within the functions. If the code is run and the print statement is not printing anything to Reaper's console window, then the section of the code that contains the print statement is not being reached. Therefore, that section of code should be further inspected.
The example below illustrates the debugging process to find and fix an error in a multi-function script.
from earsketch import * from random import randint #setup init() setTempo(120) #adds a music clip to track 1 def music(soundFile, length): fitMedia(soundFile, 1, 1, length) #switches between two bass sounds every two measures in track 2 def switchBass(soundFile1, soundFile2, length): for measure in range(1, length - 1, 2): fitMedia(soundFile1, 2, measure, measure+1) fitMedia(soundFile2, 2, measure +1, measure+2) #defines a beat for track 3 def beat(soundFile, startMeasure, endMeasure): for measure in range(startMeasure, endMeasure): fitMedia(3, soundFile, measure, measure + 1) #random scratch sounds on track 4 def randomScratch(startMeasure, length): for measure in range(startMeasure, startMeasure + length): randomScratch = selectRandomFile(HIP_HOP_SCRATCH_FOLDER) beatLength = randint(2,3) # insert media in track 4, in the currect measure at the # first beat to the beat + beatLength fitMedia(randomScratch, 4, (measure, 1, 0), (measure, beatLength)) #function calls music(HIP_HOP_STRINGMELODY_4M, 12) switchBass(HIP_HOP_BEATBOX1_2M, LATIN_BASS1_2M, 12) beat( AFRO_LATIN_DRUMS5_2M, 2, 12) randomScratch(3,9) #finish finish()
Debugging Step #1: Find the function that is causing the error
In order to complete the first step of our debugging process, attention should be directed to the bottom of the script where all of the function calls are placed. By selectively commenting out the different function calls, we can determine where the error is based on whether the program runs smoothly or not with different combinations of functions being called. Let’s first comment out the
#function calls #music(HIP_HOP_STRINGMELODY_4M, 12) switchBass(HIP_HOP_BEATBOX1_2M, LATIN_BASS1_2M, 12) beat( AFRO_LATIN_DRUMS5_2M, 2, 12) randomScratch(3,9)
Upon running the script again, we notice that the error still occurs. We can then assume that the problem is not with the
music() function. Next we decide to comment out the
#function calls music(HIP_HOP_STRINGMELODY_4M, 12) switchBass(HIP_HOP_BEATBOX1_2M, LATIN_BASS1_2M, 12) #beat( AFRO_LATIN_DRUMS5_2M, 2, 12) randomScratch(3,9)
This time, when the script is run, no error is thrown. We can assume that the error is contained within the
Debugging Step #2: print statements
Since we have discovered the function that contains the error, we can examine that function more closely. As the programmer, you can decide to continue to debug by commenting out code within the problematic function itself, or you can use print statements to locate the error. By placing print statements within a function, it is simple to determine which parts of code are being reached and which are not. If a print statement actually prints something to the Reaper console window, that portion of the code was reached. If nothing is printed, you can deduce that the area of code was never reached and the error must have occurred before the print statement. Let’s begin by placing a print statement within the function’s for loop to see if the for loop is being entered as the script runs.
#defines a beat for track 3 def beat(soundFile, startMeasure, endMeasure): for measure in range(startMeasure, endMeasure): println(“I’m in the for loop”) fitMedia(3, soundFile, measure, measure + 1)
Upon running the script, we see our message printed to the Reaper console, which looks like this:
It should be noted that, even though the statement was printed, it was only printed once. This is a problem because, as we know, a for loop executes some block of code some number of times. We also know that this particular for loop should be executed more than once. Because the print statement is only being printed once, we know the for loop is not running correctly and that some code within it must be causing the error. Now, let’s put a new print statement at the end of the for loop to see if the loop is, in fact, being run one full time.
#defines a beat for track 3 def beat(soundFile, startMeasure, endMeasure): for measure in range(startMeasure, endMeasure): fitMedia(3, soundFile, measure, measure + 1) println(“The for loop is completed”)
When the script is run this time, the print statement is not printed at all. We now know that the loop did not run to completion even once and that the error must be within our
fitMedia() function call.
Upon closer examination, we realize that the parameters within our function call are not entered in the right order. The beatString variable and the track number are in the wrong spaces and should be switched. We have now successfully found and fixed a runtime error in our script by finding the problematic function and then by finding the line of code containing the error. As you gain more experience as a programmer, your code will become more and more complex and having a debugging strategy such as this one will be a very valuable asset.
Questions and Exercises #3
For the following complete scripts, find the bug(s).
init() setTempo(120) insertMedia(DRUM_N_BASS_DRUM_1, 1) finish()
from earsketch import * from random import * init() setTempo(120) fitMedia(DRUM_N_BASS_MASTER_1, 1, 1, 5) #Lead fitMedia(DRUM_N_BASS_DRUM_1, 2, 1, 5) #Drums fitMidia(DRUM_N_BASS_MASTER_5, 1, 5, 7) #Drums breakout fitMedia(DRUM_N_BASS_MASTER_1, 1, 7, 11) #Lead fitMedia(DRUM_N_BASS_DRUM_1, 2, 7, 11) #Drums for measure in range(1,11): startBeat = randint(1,3) beatLength = randint(2,3) randomFile = selectRandomFile(HIP_HOP_SCRATCH_FOLDER) fitMedia(randomFile, 3, measure + startBeat, measure + startBeat + beatLength) finish()
Custom beat patterns and strings
We’ve just been taking the samples we have and placing them on the timeline as they are, so far. The only manipulation we’ve had access to is whether or not these samples have been truncated or looped, and we’ve had to do a lot of that work by hand. Wouldn't it also be interesting to use custom beats? In this case, a custom beat is your own rhythm, and you are defining each musical note that plays instead of using a longer audio file that has its own rhythm already defined. While it's possible to do this sort of work using
fitMedia(), It would also be incredibly tedious to create such rhythms with the GUI, as you would have to perfectly position each note in the rhythm on the track. One way to make a custom beat pattern like this in EarSketch without as much effort is to use a string to define a pattern. Strings are what programmers call any text or sequence of characters that a program uses that don't have special meaning within the programming language. In order to make it clear that particular text is a string and not a command, strings are placed within quotation marks. Like anything else, strings can be referred to via variables:
truck = "2001 Ford F-150" blerg = "adsfset@8#$REQ#DSFfjd" story = "The little boy fell down the well."
Beat patterns in EarSketch use strings to refer to subsections of a measure in order to place clips at specific places in the measure as well as define the clip’s play length in one go. Here’s an example of a beat pattern using a string:
beat = "0-00-00-0+++0+0+"
Every character stands for one sixteenth of a measure. Minus signs are rests, meaning that there’s nothing being played, and plus signs extend the playing of the sample into the next sixteenth. This string is telling Reaper that it should:
|0||play the clip for one sixteenth of a measure|
|-||rest for one sixteenth|
|0||play for one sixteenth|
|0||play for one sixteenth again|
|-||rest for one sixteenth|
|0||play for one sixteenth|
|0||play for one sixteenth again|
|-||rest for one sixteenth|
|0+++||play the clip for four sixteenths (or one quarter)|
|0+||play for two sixteenths (or one eighth)|
|0+||play for one eighth again|
Using strings, it’s possible to make complex beats that would be very annoying to generate by hand using functions like
fitMedia() or by hand in the GUI.
In order to generate this beat from the string, we use the function
makeBeat(), which takes a sound file, a track number, a measure number, and a string pattern as its parameters:
from earsketch import * init() setTempo(120) beat = "0-00-00-0+++0+0+" drum = DRUM_N_BASS_DRUMS1_4M makeBeat(drum, 1, 1, beat) finish()
This code makes a simple one-measure beat. To have the beat repeat, it’s a simple matter to put it in a loop:
from earsketch import * init() setTempo(120) beat = "0-00-00-0+++0+0+" drum = DRUM_N_BASS_DRUMS1_4M for counter in range(1,11): makeBeat(drum, 1, counter, beat) finish()
Now we have 10 measures of a drum beat ready for other samples to be placed over it.
Here’s an example of how it’s possible to use
makeBeat() to create complex beats across multiple tracks:
from earsketch import * init() setTempo(120) # Track 1 beat = "0-00-00-0+++0+0+" drum = DRUM_N_BASS_DRUMS1_4M for measure in range(1,9): makeBeat(drum, 1, measure, beat) # Track 2 beat2 = "00--0+-0+0+0+000" drum2 = HIP_HOP_DRUMLOOP2_2M for measure in range(1,9): makeBeat(drum2, 2, measure, beat2) # Track 3 beat3 = "0+0+0+0+0-000000" drum3 = HIP_HOP_PROGDRUMS8_2M for measure in range(1,9,4): makeBeat(drum3, 3, measure+3, beat3) # Track 4 beat4 = "0+----0+0+------" drum4 = ELEKTRO_HOUSE_DRUMS6_2M for measure in range(1,9): makeBeat(drum4, 4, measure, beat4) finish()
Questions and Exercises #4
1. Refactor the following code using a function and a for loop:
from earsketch import * init() setTempo(120) Drum1 = TECHNO_CRUNCHYPERCDRUMS_2M Drum2 = TECHNO_RULEROFTHEDEEPDRUMS_2M Beat1 = "0+++0+++0-000+0+" Beat2 = "0+0+0+0+-0--00-0" makeBeat(Drum1, 1, 1, Beat1) makeBeat(Drum1, 1, 2, Beat1) makeBeat(Drum1, 1, 3, Beat1) makeBeat(Drum1, 1, 4, Beat1) makeBeat(Drum2, 2, 1, Beat2) makeBeat(Drum2, 2, 2, Beat2) makeBeat(Drum2, 2, 3, Beat2) makeBeat(Drum2, 2, 4, Beat2) finish()
Answers to Questions and Exercises
Questions and Exercises #1
for count in range (1, 11): fitMedia(HIP_HOP_BASS12_4, 1, count, count+1,)
Questions and Exercises #2
Possible answer to exercise #1:
from earsketch import * init() setTempo(120) def makeSection(file1, file2, folder, startMeasure, endMeasure): fitMedia(file1, 1, startMeasure, endMeasure) fitMedia(file2, 2, startMeasure, endMeasure) fitMedia(selectRandomFile(folder), 3, startMeasure, endMeasure) makeSection(AFRO_LATIN_DRUMS1_2M, AFRO_LATIN_BASS1_2M, AFRO_LATIN_FOLDER, 1, 4) makeSection(ELEKTRO_HOUSE_DRUMS1_2M, ELEKTRO_HOUSE_BASS1_2M, ELEKTRO_FOLDER, 4, 8) makeSection(SOUL_DRUMS1_2M, SOUL_BASS1_2M, SOUL_MUSIC_FOLDER, 4, 8) makeSection(RAW_POWER_DRUMS1_2M, RAW_POWER_BASS1_2M, RAW_POWER_FOLDER, 12, 16) finish()
Possible answer to exercise #2:
def placeFile(folder, track, startMeasure, endMeasure): file = selectRandomFile(folder) fitMedia(file, track, startMeasure, endMeasure)
Questions and Exercises #3
Example 1. missing
from earsketch import *
Example 2. improper indentation in the for loop
Questions and Exercises #4
Answer to Question 1:
from earsketch import * init() setTempo(120) Drum1 = TECHNO_CRUNCHYPERCDRUMS_2M Drum2 = TECHNO_RULEROFTHEDEEPDRUMS_2M Beat1 = "0+++0+++0-000+0+" Beat2 = "0+0+0+0+-0--00-0" def myFunctionBeats(start, end): for measure in range(start, end): makeBeat(Drum1, 1, measure, Beat1) makeBeat(Drum2, 2, measure, Beat2) myFunctionBeats(1, 5) finish()