In the previous section, we encountered a script that created a short composition consisting of an A section followed by a B section, and then the return of the A section. Here is that code:
''' Music with A and B sections ''' from earsketch import * # initialize Reaper init() setTempo(120) # A section def sectionA(leadGuitar, secondGuitar, drums, bass, startMeasure, endMeasure): # create an A section fitMedia(leadGuitar, 1, startMeasure, endMeasure) # lead fitMedia(drums, 2, startMeasure, endMeasure) # drums # bass beat from startMeasure (inclusive) to endMeasure (exclusive) for measure in range(startMeasure,endMeasure): makeBeat(bass, 3, measure, "0---00--000-0000") # second guitar every other measure from startMeasure (inclusive) to endMeasure+1 (exclusive) for measure in range(startMeasure, endMeasure, 2): fitMedia(secondGuitar, 4, measure, measure+1) setEffect(4, DISTORTION, DISTO_GAIN, 10) # distortion on track 4 # B section def sectionB(guitar, drums, cymbalCrash, startMeasure, endMeasure): fitMedia(drums, 1, startMeasure, endMeasure) fitMedia(guitar, 2, startMeasure, endMeasure) fitMedia(cymbalCrash, 3, startMeasure, startMeasure+1) # set up an ABA musical form through function calls sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, 1, 5) sectionB(Y01_WAH_GUITAR_1, Y01_OPEN_HI_HATS_1, Y01_CRASH_1, 5, 7) sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, 7, 11) # finish finish()
We’ll make an important improvement to this code:
- In longer compositions, we will want to continue the ABA section layout with the same number of measures for each section. But right now, the
sectionA()
andsectionB()
functions take the start measure and end measure as inputs, making it tedious for someone calling the function to simply repeat the ABA fragments. We’ll want to continue the A section for 4 measures, the B section for 2 measures, and then repeat the A section for 4 measures. To make this easier, we’ll modify the functions to return the end measure number, given the start measure number. Then we can use that return value to start the next section where the previous function left off.
To make this change, we’ll modify the sectionA()
function to add music for 4 measures. Similarly, we’ll modify the sectionB()
function to add music for 2 measures. Then we’ll update our top level code (outside the defined functions) to call our updated functions appropriately. Here is the new code:
''' Music with A and B sections ''' from earsketch import * # initialize Reaper init() setTempo(120) # A section def sectionA(leadGuitar, secondGuitar, drums, bass, startMeasure): # create an A section endMeasure = startMeasure + 4 fitMedia(leadGuitar, 1, startMeasure, endMeasure) # lead fitMedia(drums, 2, startMeasure, endMeasure) # drums # bass beat from startMeasure (inclusive) to endMeasure (exclusive) for measure in range(startMeasure,endMeasure): makeBeat(bass, 3, measure, "0---00--000-0000") # second guitar every other measure from startMeasure (inclusive) to endMeasure+1 (exclusive) for measure in range(startMeasure, endMeasure, 2): fitMedia(secondGuitar, 4, measure, measure+1) setEffect(4, DISTORTION, DISTO_GAIN, 10) # distortion on track 4 return endMeasure # B section def sectionB(guitar, drums, cymbalCrash, startMeasure): endMeasure = startMeasure + 2 fitMedia(drums, 1, startMeasure, endMeasure) fitMedia(guitar, 2, startMeasure, endMeasure) fitMedia(cymbalCrash, 3, startMeasure, startMeasure+1) return endMeasure # set up an ABA musical form through function calls endMeasure = sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, 1) endMeasure = sectionB(Y01_WAH_GUITAR_1, Y01_OPEN_HI_HATS_1, Y01_CRASH_1, endMeasure) endMeasure = sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, endMeasure) # finish finish()
First, notice the most important thing. This code produces exactly the same music as the previous code. Second, there are a few lines which are important to discuss, as they may be confusing because they involve two new programming concepts: 1) variable scoping and return values.
The first change is to the function argument lists, shown on lines 11 and 26. We removed the endMeasure
parameter from both the sectionA()
and sectionB()
functions. Because section A is always four measures in length, we define a new variable on line 13 to hold the end measure value, defined as four measures after the startMeasure
input parameter. Similarly, line 27 defines the same variable in the sectionB()
function. But how can the same variable endMeasure
be used in both the sectionA()
and sectionB()
functions? Actually, how can it also be defined and used at lines 34-36, outside of the functions? That this code works demonstrates a simple but necessary programming feature called variable scoping, which refers to the “range” or “view” of a variable definition. Put another way, if a variable is defined in code, it can be used anywhere that the variable is “in scope”. In the code shown above, there are three separate “variable scopes” in use: 1) the top level of the script (lines 34-36), 2) the sectionA()
function variable scope, and 3) the sectionB()
function variable scope. This is because the top level of an EarSketch script always has its own scope (provided by the EarSketch environment). Whenever you call a function (but not when you define it!), a new variable scope is created for the function call. In fact, calling a function is the only way to create a new variable scope in Python. The important effect of variable scoping is that a variable can be used in any scope in which it is defined, or in any scope created from that scope. This is quite technical, but is critically important. On lines 34-36, we call the sectionA()
and sectionB()
functions, which creates a new scope each time a function is called. Let’s walk through lines 34-36 step by step to understand what’s happening.
endMeasure = sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, 1)
Line 34 defines a variable called endMeasure
. The variable is assigned a value that is returned from the sectionA()
function (we will explain this shortly). This endMeasure
variable is at the top level scope of your EarSketch script. Here is a useful diagram that illustrates this top level scope:
When the sectionA()
function is called at line 34, a new scope is created, with its own endMeasure
variable (defined on line 13). This is visualized with the following illustration:
This diagram shows that there are two variable scopes when functionA()
is called: 1) the top level script variable scope and 2) the variable scope created when sectionA()
function is called. The endMeasure
variable defined inside the sectionA()
function is a variable declared inside the new scope, and thus it hides the endMeasure
variable defined at the top level of the script. Thus, the endMeasure
variable used at lines 14, 15, 17, 20, and 23 is the endMeasure
variable declared at line 13 inside the sectionA()
function, and not the endMeasure
variable declared at line 34, which is exactly what we want.
Next, we call functionB()
and assign the return value to the endMeasure
variable:
endMeasure = sectionB(Y01_WAH_GUITAR_1, Y01_OPEN_HI_HATS_1, Y01_CRASH_1, endMeasure)
The endMeasure
variable declared in the sectionB()
function at line 27 works exactly the same way as when we called sectionA()
at line 34, creating another scope just for that function, which is illustrated as follows:
Notice that the sectionA()
function scope and the sectionB()
function scopes are separate, but both are within the top level script scope.
To summarize, there are two important takeaways about the concept of variable scoping:
- A new variable scope is created when a function is called. This ensures that variables declared inside the function (including function parameters) have the expected values when the programmer writes a function and when that function is called.
- A declared variable can be used at the scope at which it is declared, or inside that scope. Thus, when a variable is declared, it can be used inside a function called from that scope. However, if a variable with the same name exists in two scopes (as is the case in the code above), the variable at the innermost scope (“nearest” scope) is always used. Again, this is illustrated in the diagrams above.
Return Values
The second important topic is function return values. This is a much simpler concept than variable scoping. Quite simply, a value can be returned from a function and assigned to a variable, as is done at line 34 in the code above:
endMeasure = sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, 1)
At this line of code, the sectionA()
function is called. The last line of that function, line 23, contains a return
statement, which returns a value from the function to the code that called the function:
return endMeasure
The endMeasure
variable defined inside of the sectionA()
function scope (at line 13) is returned from the function, and assigned to the endMeasure
variable in the top level scope (at line 34 shown above). Similarly, the sectionB()
function also returns the endMeasure
variable defined in the function. Thus, the top level endMeasure
variable is always up to date with the current end measure, and can be passed in to subsequent function calls, and updated using the return values of the functions, shown at lines 35-36:
endMeasure = sectionB(Y01_WAH_GUITAR_1, Y01_OPEN_HI_HATS_1, Y01_CRASH_1, endMeasure) endMeasure = sectionA(Y01_GUITAR_1, Y01_WAH_GUITAR_1, Y01_DRUMS_1, Y01_BASS_1, endMeasure)
Notice that the endMeasure
variable is passed in to the sectionB()
function, and also updated with the return value from the function. The same thing happens at line 36.
Exercise
What do you think will happen if the endMeasure
variable in the sectionA()
function is renamed to finalMeasure
, and similarly changed where it is used in the sectionA()
function? Will the output of the script change? Try it and see. Explain the results.