Chapter 11
Topics: Fractals, recursion, Fibonacci numbers, the Golden Ratio, Zipf’s Law, top-down design, Python dictionaries, defining Python classes, Python exceptions, animation, color gradients, Python complex numbers, cymatics and dynamical systems (boids).
We have finally reached the last chapter of the book. Throughout this book we have studied fundamentals of music theory, computer programming, sonification, and algorithmic design. Clearly, we have covered a lot of territory in our journey of learning how to make music with Python. But this journey can and, we hope, will continue. There are many creative possibilities for you to explore. This chapter opens the door to some of them.
In this chapter, we present several advanced computational concepts and algorithmic techniques. For some of them, we will show directly how to use them to make music (as we have done throughout the book so far). For others, we will leave it up to you to add a musical dimension to the sample code. Although we could have easily provided you with our musical examples, we wanted to give you the opportunity to find your own, especially for the algorithms and models presented here that are rich with musical (sonification) possibilities. We didn’t want to bias you toward a single possibility. Therefore, this chapter presents a few opportunities for you to explore and engage your creative side. When reading the sections that follow, start thinking about how you can combine what you have learned so far to create your own music. This will probably involve musical and algorithmic brainstorming, further readings, and experimentation.
Use your imagination. What musical parameters or sonification ideas would you like to explore? Let the concepts inspire you, and use the visualizations to guide your creativity. Your programs will probably involve most of the knowledge of making music with Python you have accumulated thus far. Some of the ideas may be simple. Others may be more involved. Either way, expect your ideas to evolve, as you are finding your own style and your own musical voice.
Let’s begin.
As discussed in Chapter 1, fractals are self-similar objects (or phenomena), that is, objects consisting of multiple parts, with the property that the smaller parts are the same shape as the larger parts, but of a smaller size. The field of fractal geometry was developed by Benoit Mandelbrot to study these types of artifacts (Mandelbrot 1982).
In this section we explore an elegant and powerful programming technique called recursion. Recursion may be used to perform a variety of computation tasks. It mimics the subdivision process found in nature, for example, the branching (starting with a single sprout) that results in a pine tree, or the branching (starting from a single cell) that resulted in the human reading this text. Therefore, among other things, recursion is especially suited for creating fractal artifacts.
This process of repeated branching has been studied extensively. For instance, Leonardo of Pisa, also known as Leonardo Fibonacci (c. 1170–c. 1250) investigated how fast rabbits could breed under ideal conditions. To model this fractal phenomenon, he developed what is now known as the Fibonacci sequence.
Fibonacci numbers appear in many natural objects and processes, including seashells (see Figure 11.1), branching plants, flower petals, flower seeds, leaves, pineapples, and pine cones, among others. They also appear in the formation of tornadoes, hurricanes, and, at the grand scale, of galaxies.
The nautilus shell is a well-known example. As Figure 11.1 shows, its shape can be derived from a geometric process modeled by Fibonacci numbers. This may be seen either as analytical (i.e., holding a shell and seeing how it is made—from large to small), or synthetic (i.e., as a description of the way a nautilus grows its shell—from small to large).
The Fibonacci sequence consists of two numbers, 0 and 1, and a rule on how to generate the next number, namely, “add the previous two numbers.” So the sequence goes like this: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 ... (verify that each number is the sum of the previous two, with the exception, of course, of the two starting numbers, 0 and 1).
Fact: The ratio of two consecutive Fibonacci numbers approximates the golden ratio, φ (phi), which is approximately 0.61803399 … (or, when the larger of the two Fibonacci numbers is used as the numerator, 1.61803399 …).
Fact: Artifacts whose proportions incorporate the golden ratio are usually perceived as aesthetically pleasing by humans.
The golden ratio is found in natural and human-made artifacts (Beer 2008; Calter 2008, pp. 46–57; Hemenway 2005, pp. 91–132; Livio 2002; May 1996; Pickover 1991, pp. 203–205). It is also found in the human body (e.g., the bones of our hands, the cochlea in our ears). It has been shown that the golden ratio appears in musical works by Bach, Beethoven, Mozart, and others (Garland and Kahn, 1995).
The Fibonacci numbers can be easily calculated using recursion (where n denotes the nth number in the sequence):
Definition: A recursive function is a function that calls itself.*
Now let’s translate this to Python:
# fibonacci.py
#
# Find the nth Fibonacci number using recursion.
#
def fib(n):
if n == 0: # simple case 1
result = 0
elif n == 1: # simple case 2
result = 1
else: # recursive case
result = fib(n-2) + fib(n-1)
return result
Notice the two simple cases (or base cases) in the if statement inside the function. If we call fib() with n equal to 0, result is assigned a 0, and the function terminates (after it returns result). Same for n equal to 1.
However, if we call fib() with n equal to 2, it makes two calls to itself with smaller n’s (i.e., n−2 and n−1). In other words, it recursively calls itself with n equal to 0 (i.e., n−2) and with n equal to 1 (i.e., n−1). When the two calls return with values 0 and 1, respectively, it adds the two values (0 + 1) and returns the result. That’s it.†
We can test the function as follows:
# now, let's test it
for i in range(10):
print "fib(" + str(i) + ") =", fib(i)
This outputs:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
Since the function is recursive, we are guaranteed that it will work correctly for larger n’s.
The following program generates a fractal tree, also known as a Golden Tree, since it incorporates golden ratio proportions (see Figure 11.2). The golden tree is constructed by dividing a line into two branches, each rotated by 60 degrees (clockwise and counter-clockwise), with a length reduction factor equal to the golden ratio (0.61803399 …). These smaller lines, again, are each subdivided into two lines following the same procedure. This subdivision may go on indefinitely, but (both in nature and computing) there is a practical limit, where further subdivision does not serve any practical purpose and thus it stops (e.g., the brain cavity has filled up with enough grey matter). Similar patterns appear extensively in nature (as they maximize the amount of matter that can fit in a limited space, because surfaces are touching but not overlapping).
In Figure 11.2 there are 14 levels, or 13 subdivision, starting with the main branch.
The program below demonstrates how to code this using recursion.
Fact: Recursion occurs when a function calls itself.
Of course, this may result in a never-ending execution of a program. In order for a recursive function to eventually end, we have to make sure that
Let’s see how this works:
# goldenTree.py
#
# Demonstrates how to draw a golden tree using recursion.
#
from gui import *
from math import *
# create display
d = Display("Golden Tree", 250, 250)
d.setColor(Color.WHITE)
# calculate phi to the highest accuracy Python allows
phi = (sqrt(5) - 1) / 2 # approx. 0.618033988749895
# recursive drawing parameters
depth = 13 # amount of detail (or branching)
rotation = radians(60) # branch angle is 60 degrees (need radians)
scale = phi # scaling factor of branches
# initial parameters
angle = radians(90) # starting orientation is North
length = d.getHeight() / 3 # length of initial branch (trunk)
startX = d.getWidth() / 2 # start at bottom center
startY = d.getHeight() - 33
# recursive function for drawing tree
def drawTree(x, y, length, angle, depth):
"""
Re cursively draws a tree of depth 'depth' starting at 'x', 'y'.
"""
gl obal d, scale, rotation
# print "depth =", depth, "x =", x, "y =", y, "length =", length,
# print "angle =", degrees(angle)
# draw this line
newX = x + length * cos(angle) # calculate run
newY = y - length * sin(angle) # calculate rise
d.drawLine(int(x), int(y), int(newX), int(newY))
# check if we need more detail
if depth > 1:
# draw left branch - use line with length scaled by phi,
# rotated counter-clockwise
drawTree(newX, newY, length*phi, angle - rotation, depth-1)
# draw right branch - use line with length scaled by phi,
# rotated clockwise
drawTree(newX, newY, length*phi, angle + rotation, depth-1)
# draw complete tree (recursively)
drawTree(startX, startY, length, angle, depth)
First, notice how small this program is. This is usually the case with recursive solutions of problems. They tend to be very succinct and elegant.
Notice the calculation of φ, using a mathematical formula (instead of dividing two Fibonacci numbers), for better accuracy:
# calculate phi to the highest accuracy Python allows
phi = (sqrt(5) - 1) / 2 # approx. 0.618033988749895
Also notice the parameters used to control the recursive process:
# recursive drawing parameters
depth = 14 # amount of detail (or branching)
rotation = radians(60) # branch angle is 60 degrees (need radians)
scale = phi # scaling factor of branches
and the parameters for the initial call to function drawTree():
# initial parameters
angle = radians(90) # starting orientation is North
length = d.getHeight() / 3 # length of initial branch (trunk)
startX = d.getWidth() / 2 # start at bottom center
startY = d.getHeight() - 33
The initial call happens at the end of the program, since we first need to define the function drawTree().‡ This call looks as follows:
# draw complete tree (recursively)
drawTree(startX, startY, length, angle, depth)
To better understand this recursive solution, uncomment the print statements inside drawTree(). Then run the program first with variable depth set to 1, 2, 3, etc.
For example (see Figure 11.3), when depth is 1, the output is
depth = 1 x = 125 y = 217 length = 83 angle = 0.0
When the depth is 2, the output is
depth = 2 x = 125 y = 217 length = 83 angle = 0.0
depth = 1 x = 125.0 y = 134.0 length = 51.30 angle = -60.0
depth = 1 x = 125.0 y = 134.0 length = 51.30 angle = 60.0
Here, notice that the first line (i.e., depth = 2) is generated by the very first call. The other two lines are generated by the two recursive calls (one each). And, finally, for the depth 3:
depth = 3 x = 125 y = 217 length = 83 angle = 0.0
depth = 2 x = 125.0 y = 134.0 length = 51.30 angle = -60.0
depth = 1 x = 80.58 y = 108.35 length = 31.70 angle = -120.0
depth = 1 x = 80.58 y = 108.35 length = 31.70 angle = 0.0
depth = 2 x = 125.0 y = 134.0 length = 51.30 angle = 60.0
depth = 1 x = 169.42 y = 108.35 length = 31.70 angle = 0.0
depth = 1 x = 169.42 y = 108.35 length = 31.70 angle = 120.0
Study this output carefully in conjunction with the code. Actually, the best way to do this is to “play computer,” i.e., take a piece of paper and write down the calls to function drawTree() and the parameter values passed each time as if you were the computer executing this program. To help you understand better, answer the following questions:
Realize that each of these recursive calls executes the same code (i.e., the body of function) as the original call. What changes, for each of these recursive calls, is the value of the input parameters, including depth.
When depth, eventually becomes 1, the corresponding function will draw a line, but will not make any further recursive calls. When that function terminates, Python returns to its parent function (i.e., the function that called it). If that function has more work to do, it does it; otherwise it also returns to whomever called it.
This goes on until we reach the top level of the program the very first call to drawTree(). Then, since that’s the last statement in the program, the program terminates.
With the help of the print output, and some paper and pencil, trace this program carefully.§
In summary, as with the construction of the golden tree, recursion applies a process (actually a function) to a smaller instance of the task being worked on.
Computers have been used extensively in music to aid humans in analysis, composition, and performance. Is it possible to find algorithmic techniques to help explore and identify aspects of musical aesthetics related to balance, pleasantness, and, why not, beauty?
George Kingsley Zipf (1902–1950) was a linguistics professor at Harvard who studied fractal patterns in language. In his seminal book, Human Behavior and the Principle of Least Effort (Zipf 1949), he reports the amazing observation that word proportions in books, as well as notes in musical pieces (among other phenomena) follow the same harmonic proportions first discovered by Pythagoreans on strings (i.e., 1/1, 1/2, 1/3, 1/4, 1/5, etc.).¶ This means that the most common word appears about twice as many times as the second most common word, three times as the third most common word, four times as the fourth most common word, and so on.
Fact: This type of proportion is called Zipf’s law.
Zipf proportions (or distributions) have been observed in a wide range of human and naturally occurring phenomena. These include music, city sizes, incomes, computer function calls, earthquake magnitudes, thickness of sediment depositions, clouds, trees, extinctions of species, traffic jams, and visits to websites (e.g., Zipf 1949, Bak 1996, Schroeder 1991).
Moreover, Zipf showed that if we plot the logarithm of the counts of all events in such a phenomenon against the logarithm of the rank of these events, we get a straight line with a slope of approximately –1.0 (i.e., a 45° orientation). For example, Figure 11.5 demonstrates that the book you are currently reading (this book) has Zipfian proportions. Actually, it has a Zipf slope of –1.23.
The second value, R2, in Figure 11.5 measures how closely the data points fall on the straight line (1.0 means a perfect straight line; 0.0 means data points are scattered around the graph). The data points for this book fall almost perfectly on a straight line (its R2 value is 0.98). This is actually quite common for regular, long books (like this one).**
Calculating both the Zipf slope and R2 values is very useful if you do not wish to generate the graph every time. Having an R2 value near 1.0 (e.g., >0.7) tells you that the data points exhibit harmonic proportions. Then the slope tells you what type of harmonic proportion (or balance) they exhibit. For example, Zipf’s ideal slope, –1.0, corresponds to the harmonic series 1/1, 1/2, 1/3, 1/4, 1/5 …, which can also be represented as:
Sn=11+12+13+14+15+…+1n
A more generalized form of this formula is the Generalized Harmonic Series, which was explored by Zipf:
Sn=11p+12p+13p+14p+15p+…+1np
where p is equal to the absolute value of the Zipf slope.††
For example, a slope near –2.0 generates the series, 1/1, 1/4, 1/9, 1/16, 1/25 and so on.‡‡
This formula is really nice since it allows us to easily generate the harmonic (or, in the general case, hyperharmonic) proportions encountered in any phenomenon with a Zipf slope other than −1.0.
Figure 11.6 shows two examples of measuring the Zipf proportions of pitches in two musical pieces. The left one demonstrates that pitches of Bach’s “Air on a G String” exhibit almost ideal Zipf harmonic proportions—notice the 45° slope of the data points.§§ The slope is –1.078 and the R2 value is 0.81 (actually an R2 value over 0.7 is considered significant). So this piece is almost Zipfian in terms of pitch.
On the other hand (Figure 11.6, right), the random piece has an almost horizontal slope (–0.19) and an R2 of 0.7. This means that pitches have pretty much equal counts, i.e., they have an equal probability of appearing. This is precisely what we expect from using function random() to select pitches, which is how this piece was created using a computer (e.g., see the “Pierre Cage” case study in Chapter 6).
Fact: Zipf’s law allows us to measure the balance and proportions of events in music pieces (e.g., pitch, duration, dynamic, etc.).
In the previous chapter, we saw the types of harmonic shapes that can be generated from simple integer ratios. We also discussed how the Pythagoreans worked on sonifying those same integer ratios, and discovered that some of them sounded better than others. These ratios became the basis of modern musical scales (1:2 is octave, 2:3 is fifth, 3:4 is fourth, 4:5 is minor third, 5:6 is major third, and so on).
But what happens when you put many of these musical intervals together in a song? The answer is that you get a complex artifact consisting of numerous harmonic ratios (musical intervals), some of which overlap (e.g., create chords). We call this complex system of harmonic ratios music, and our ears and minds are built to easily process and appreciate this world of organized sound. Of course, the opposite holds as well—our music has evolved based on our ears and minds, and the types of sound organization we find harmonious or not.
In the case of Zipf’s law, we are looking at phenomena that combine attributes in harmonic ratios 1:1, 1:2, 1:3, 1:4, 1:5, 1:6, 1:7, and so on. For example, think of every appearance of the word “the” in a book as though its appearances were a cycle. In other words, the word “the” oscillates (i.e., appears) in a book every now and then, with a frequency of 1:1. The next most frequent word, say, “to,” oscillates (i.e., appears) in a book half as often, with a frequency of 1:2. And so on.
This approach can be used to understand all phenomena that exhibit Zipf’s law.
Fact: Zipf’s law describes artifacts (music or other) that consist of characteristics that occur according to harmonic ratios which, when taken as a whole, exhibit a specific balance.
Zipf also adapted a Pythagorean approach to explain his observation. He theorized that such harmonic proportions are the result of orthogonal forces. In any environment containing self-adapting agents able to interact with their surroundings, such agents tend to minimize their overall effort associated with this interaction (economy principle). That is, a system of such interacting agents tends to find a global optimum that minimizes overall effort (Bak 1996). This interaction involves some sort of exchange (e.g., information, energy, etc.). We will see this principle at play in the Cymatics section later in this chapter.
Finally, what if we were able to measure music from various music styles (baroque, classical, 20th century, blues, jazz, etc.)? The next section demonstrates how to do this. The technique shown below has been used to help classify music according to composer, style, and pleasantness using computers. Also, it has been used to perform experiments in computer-aided music composition (Manaris et al. 2005, 2007).
Psychologists have shown that people prefer music, and other experiences, that have a balance of predictability and surprise. The following program reads in a sequence of MIDI files and measures how well the note pitches in each song follow Zipf’s law. To do so, it uses functionality from the Zipf library (provided with this book).
It is relatively easy to expand it to measure additional proportions, such as note durations. Even with a single metric, for pitch, we can still get an approximate idea of how a piece may sound in terms of balance and proportion.
Here is the code:
# zipfMetrics.py
#
# Demonstrates how to calculate Zipf metrics from MIDI files for
# comparative analysis. It calculates Zipf slopes and R^2 values
# for pitch.
#
# It also demonstrates how to use Python dictionaries to store
# collections of related items.
#
# Finally, it demonstrates how to implement an algorithm in a top-down
# fashion. First function encountered in the program performs the
# highest-level tasks, and any sub-tasks are relegated to lower-level
# functions.
#
from music import *
from zipf import *
# list of MIDI files to analyze
pieces = ["sonifyBiosignals.mid", "ArvoPart.CantusInMemoriam.mid",
"DeepPurple.SmokeOnTheWater.mid",
"soundscapeLoutrakiSunset.mid",
"Pierre Cage.Structures pour deux chances.mid"]
# define main function
def main(pieces):
""" Calculates and outputs Zipf statistics of all 'pieces'."""
# read MIDI files and count pitches
for piece in pieces:
# read this MIDI file into a score
score = Score() # create an empty score
Read.midi(score, piece) # and read MIDI file into it
# count the score's pitches
histogram = countPitches(score)
# calculate Zipf slope and R^2 value
counts = histogram.values()
slope, r2, yint = byRank(counts)
# output results
print "Zipf slope is", round(slope, 4), ", R^2 is", round(r2, 2),
print "for", piece
# now, all the MIDI files have been read into dictionary
print # output one more newline
def countPitches(score):
""" Returns count of how many times each individual pitch appears in 'score'.
"""
hi stogram = {} # holds each of the pitches found and its count
# iterate through every part, and for every part through every
# phrase, and for every phrase through every note (via nested
# loops)
for part in score.getPartList(): # for every part
fo r phrase in part.getPhraseList(): # for every phrase in this part
fo r note in phrase.getNoteList(): # for every note in this phrase
pitch = note.getPitch() # get this note's pitch
# count this pitch, if not a rest
if (pitch != REST):
# increment this pitch's count (or initialize to 1)
histogram[pitch] = histogram.get(pitch, 0) + 1
# now, all the notes in this phrase have been counted
# now, all the phrases in this part have been counted
# now, all the parts have been counted
# done, so return counts
return histogram
# start the program
main(pieces)
First this program demonstrates a slightly different style of writing Python programs using functions. It demonstrates how to implement an algorithm in a top-down fashion.
As mentioned in Chapter 3, top-down design is a strategy for constructing programs. It starts from the biggest, highest-level task, and subdivides it into smaller tasks. Each of these tasks is implemented using functions.
By specifying the highest-level pieces of our program first, and then dividing them into successively smaller pieces, we gain perspective, and the structure of the program is clearly defined. Then it is easy to go back and fix problems, or update the program to perform slightly different tasks. Top-down design is very beneficial when developing large programs.
The above program first defines a function main() which contains the high-level algorithmic tasks of the program. Then, it defines the function countPitches() which, as is explained below, performs a more specific (lower-level) task of the program. Finally, it calls the function main() to start the program.
By writing programs this way, we can focus first on the high-level tasks and then on the lower-level tasks. Top-down design is very nice and is used extensively by experienced programmers.
The program reads in and analyzes several MIDI pieces we have created in this book. When you run it, it uses Read.midi() to read in MIDI files into scores.
Then it calls the function countPitches() to count every pitch in the score. It is interesting to observe the triple-nested loop in this function:
for part in score.getPartList(): # for every part
for phrase in part.getPhraseList(): # for every phrase in part
for note in phrase.getNoteList(): # for every note in phrase
As the comments indicate, the outer loop traverses all parts in the score. For every part, the middle loop is executed. This loop traverses all phrases inside a part. Then, for every phrase, the inner loop is executed. This traverses all notes in a phrase.
For each note in a phrase, we get its pitch, and if it is not a REST, we count it.
To store this count we use Python dictionaries (see next section).
Finally, the program outputs the results. When you run this program, you will notice that these results are interspersed between output from Read.midi(). Collected together the results read:
Zi pf slope is -2.3118 , R^2 is 0.76 for sonifyBiosignals.mid
Zi pf slope is -0.8641 , R^2 is 0.79 for ArvoPart.CantusInMemoriam.mid
Zi pf slope is -1.2507 , R^2 is 0.94 for DeepPurple.SmokeOnTheWater.mid
Zi pf slope is -0.929 , R^2 is 0.55 for soundscapeLoutrakiSunset.mid
Zi pf slope is -0.4619 , R^2 is 0.71 for Pierre Cage.Structures pour deux chances.mid
Analyzing the results:
In summary, using a single metric (e.g., Zipf proportion of pitch) can give us an idea about a piece, but it is not definitive in any way with regard to our aesthetic appreciation of it. However, calculating many diverse metrics based on Zipf’s law can give us a more accurate idea about the proportions of a piece, as several experiments demonstrate (e.g., Manaris et al. 2005, 2007).
Python has an additional data type, called a dictionary. Dictionaries are similar to Python lists. They are used to store collections of related items. The main difference is that, while lists are indexed using increasing integers (0 to the length of the list minus 1), dictionaries are indexed using arbitrary Python values.
A Python dictionary is similar to a real dictionary (which associates words with their definitions). In the case of Python dictionaries, though, the “word” (or key or index) can be almost any Python value, and the “definition” (or value) can also be any Python value. In other words, Python dictionaries are used to store key-value pairs.
To create an empty dictionary, we use this syntax:
d = {}
Dictionaries have syntax similar to list indexing. For example, given d, the following statements load into it key-value pairs, where the key is a word in English, and the value is the corresponding word in Spanish.
d['hello'] = 'hola'
d['yes'] = 'si'
d['one'] = 'uno'
d['two'] = 'dos'
d['three'] = 'tres'
d['red'] = 'rojo'
d['black'] = 'negro'
d['green'] = 'verde'
d['blue'] = 'azul'
Dictionaries have a rich set of built-in operations:
Function |
Description |
d.keys() |
Returns the list of keys in dictionary d. |
d.values() |
Returns the list of values in dictionary d. This list is ordered to correspond with the list returned by d.keys(), that is, the two lists are parallel. |
d.items() |
Returns the list of key-value pairs (as sublists) in d. |
del d[k] |
Deletes item k from the dictionary d. |
d.has_key(k) |
Returns True if k exists as a key in dictionary d, False otherwise. |
d.get(k, default) |
Returns the value associated with key k in d. If k does not exist, it returns value default (whatever that may be). |
Most Python values will work as indices to dictionaries (e.g., strings, integers, floats, etc.). In the previous case study, we used the pitch MIDI value as the key. Thus we associated a MIDI pitch value with its count. This is done with the statement:
# increment this pitch's count (or initialize to 1)
histogram[pitch] = histogram.get(pitch, 0) + 1
A little more explanation is needed for this. The name of the dictionary is histogram. The expression
histogram.get(pitch, 0) + 1
gets the current value (i.e., count) for the index pitch and adds 1 to it. If index pitch had not been seen before (i.e., it is not a valid index in the dictionary), get(pitch, 0) will create it and return the second parameter, i.e., 0. To this we add 1. Either way, we store the result in histogram[pitch], either incrementing the previous value (count) stored in this dictionary location, or creating a new dictionary location for the new pitch containing the value 1.
As mentioned earlier, functions and classes are containers provided by Python to group related functionality. For example, earlier we saw how to create notes:
n = Note(C4, QN, 127) # create a middle C quarter note
At the time, we focused on the functionality, that is, that variable n gave us an efficient way to store related information about a musical note and eventually play them back through the MIDI synthesizer.
What we left out is that Note is a Python class.
Python classes are a “packaging” mechanism—they allow a programmer to offer functionality, while hiding all the implementation details. This allows others to be more productive and efficient (e.g., to focus on music-making tasks and avoid computational details).
Definition: “Packaging” is done using encapsulation and information hiding that are programming language properties allowing access to functionality through a well-defined interface, while hiding implementation details.
Fact: Python provides two mechanisms for encapsulation and information hiding: functions and classes.
We have already seen how to define functions (Chapter 7). This section focuses on defining classes. As our code is getting more advanced, we need to start hiding things, for simplicity of use. This way, other programmers can efficiently build on our work (the same way that you have been using this book’s music and other libraries).
It is through encapsulation and information hiding (e.g., libraries of Python functions and classes) that a community of creative software developers can collaborate and thrive. It is through encapsulation and information hiding that books can be written on creative programming, while providing useful functionality in well-defined libraries, such as the music, gui, image, midi, and osc libraries.
Throughout the book, we have written code that uses Note objects. In this case study, we see how to define (a simplified version of) this class on our own. This class is used to store related information about musical notes (i.e., pitch, duration, dynamic, and panning). It also provides functions to retrieve (get) and update (set) the information encapsulated in a Note object.
Learning how to define Python classes is very useful. It allows us to package related information (data) and, in essence, create new Python data types.
Fact: Python classes allow us to define new data types.
You will have various opportunities to develop new classes in your own coding practice.
As we recall from Chapter 2, the most basic musical structure in the Python music library is a Note. Python notes have the following attributes:
To create a note, we specify its pitch, duration, dynamic, and panning position, as follows:
Note(pitch, duration, dynamic, panning)
where dynamic and panning are optional. If omitted, dynamic is set to 85 and panning to 0.5 (center).
For example, this Python statement creates a middle C quarter note and stores it in variable n.
n = Note(C4, QN)
Here we create the same note, but as loud as possible (127) and placed to the left side (in the stereo field):
n = Note(C4, QN, 127, 0.0)
Definition: The data values encapsulated within a class are called class attributes or instance variables.
In order to set and retrieve the value of instance variables, we create functions to access them.
Definition: The functions provided to access class attributes are called class functions.
In this simplified case study, Note objects will have the following functions***:
As we saw in Chapter 2, to create a new note, we say:
n = Note(C4, QN)
This creates a Note object and stores it in variable n. Once the note has been created, we can access its data (i.e., data stored inside it) through its class functions:
>>> n = Note(C4, QN)
>>> n.getPitch()
60
>>> n.getDuration()
1.0
>>>
Defining a class is similar to defining a function. A function encapsulates variables and statements. A class, on the other hand, encapsulates both variables and functions.
Here is the code to define the Note class:
# note.py
#
# It demonstrates how to create a class to encapsulate a musical
# note. (This is a simplified version of Note class found in the
# music library.)
#
from music import *
class Note:
def __init__(self, pitch=C4, duration=QN, dynamic=85, panning=0.5):
"""Initializes a Note object."""
self.pitch = pitch # holds note pitch (0-127)
self.duration = duration # holds note duration (QN = 1.0)
self.dynamic = dynamic # holds note dynamic (0-127)
self.panning = panning # holds note panning (0.0-1.0)
def getPitch(self):
"""Returns the note's pitch (0-127)."""
return self.pitch
def setPitch(self, pitch):
"""Sets the note's pitch (0-127)."""
# first ensure data integrity, then update
if 0 <= pitch <= 127: # is pitch in the right range?
self.pitch = pitch # yes, so update value
else: # otherwise let them know
print "TypeError: Note.setPitch(): pitch ranges from",
print "0 to 127 (got " + str(pitch) + ")"
def getDuration(self):
"""Returns the note's duration."""
return self.duration
def setDuration(self, duration):
"""Sets the note's duration (a float)."""
if type(duration) == type(1.0): # is duration a float?
se lf.duration = float(duration) # yes, so update value
else: # otherwise let them know
print "TypeError: Note.setDuration(): duration must be",
print "a float (got " + str(duration) + ")"
First, notice how we import the music library to have access to MIDI constants, such as C4, QN, etc.
The first actual line of the code,
class Note:
is called the class header. It simply begins the class definition and states the class name, that is, Note.†††
Notice how the class name, Note, begins with an uppercase letter. This is a useful convention, so class names can be easily distinguished from function names. (The latter start, by convention, with lowercase letters.)
Good Style: Class names should begin with an uppercase letter.
Below the class header follow the definitions of the class functions. These need to be indented (again, we use 3 spaces). Class Note has five class functions, as seen above.
Of these functions,__init__(), is called the constructor function.
Definition: The constructor function is a special class function which is called automatically every time we create a new instance.
For example, in the following
n = Note(60, 1.0)
the parameters 60 and 1.0 will be passed to the Note constructor function,__init__().
Therefore, when defining the class, we use the constructor to initialize any class attributes or instance variables that need initial values.
Here is the Note constructor function once more:
def __init__(self, pitch=C4, duration=QN, dynamic=85, panning=0.5):
"""Initializes a Note object."""
self.pitch = pitch # holds note pitch (0-127))
self.duration = duration # holds note duration (QN = 1.0)
self.dynamic = dynamic # holds note dynamic (0-127)
self.panning = panning # holds note panning (0.0-1.0)
As seen in the previous section, class Note has four instance variables, pitch, duration, dynamic, and panning. Accordingly, the constructor function creates the four instance variables, self.pitch, self.duration, self.dynamic, and self.panning, and gives them whatever values were passed in as parameters.
Fact: Instance variables in Python classes are always prefixed by the symbol self.
Fact: Instance variables are global to the whole class, that is, every class function can access them.
The remaining class functions utilize these instance variables to get their job done.
Fact: When we define a function within a class, Python requires that we use self as the first parameter.‡‡‡
Fact: When we call a function defined within a class, we do not provide a value for the self parameter. We only provide values for the remaining parameters.
For example, it appears as if function setPitch() has two parameters, namely, self and pitch:
def setPitch(self, pitch):
"""Sets the note's pitch (0-127)."""
...
However, when we call it, we omit self and only provide a value for pitch:
>>> n.setPitch(D4)
This is one of the few awkward moments in learning Python. A class function is called with one less parameter than it is defined. (The value for self is provided automatically by the Python interpreter.)
In summary, classes allow us to encapsulate related variables (instance variables) and functions (class functions). This allows us to define new data types (as needed or desired) for data that we may use often in our programming practice.
Class functions can be used to ensure data integrity. What happens in the Note class if a user (end-programmer, like you in Chapter 2) accidentally tried to set a note pitch to 128?
This would probably cause a significant error when playing the music generated by the program. Most likely, this would generate a cryptic message by your computer’s synthesizer (such as “Ill-formed MIDI sequence” or “MIDI message contains illegal data”). Regardless of the message, tracing this problem back to your code would take forever (especially if your program generated hundreds of notes).§§§
Good Style: Class functions used to set internal class values (class attributes) should check for errors, whenever possible. If an error occurs, they should issue an informative error message (and ignore the provided value).¶¶¶
For example, let’s see function setPitch() once more:
def setPitch(self, pitch):
"""Sets the note's pitch (0-127)."""
# first ensure data integrity, then update
if 0 <= pitch <= 127: # is pitch in the right range?
self.pitch = pitch # yes, so update value
else: # otherwise let them know
print "TypeError: Note.setPitch(): pitch ranges from",
print "0 to 127 (got " + str(pitch) + ")"
Notice how the if statement is used to ensure that the provided pitch is within the acceptable range. Without this error check, anything may happen (that is, the pitch could be set to a float or even a string!).
Notice that the else part outputs an informative error message (which explains what the error is, what the input value was that caused the error, and provides guidance/feedback on how to remedy the error).
Good Style: Error messages should:
For example, the else part above uses these statements:
print "TypeError: Note.setPitch(): pitch ranges from",
print "0 to 127 (got " + str(pitch) + ")"
Python provides exceptions, a very useful mechanism for issuing error messages within programs.
For example, the above error message could be implemented as follows:
raise TypeError("Note.setPitch(): pitch range is 0-127 (got " + str(pitch) + ")")
Notice the difference: instead of using print followed by a string, we use raise followed by the name of an exception, such as TypeError, and the error message in parenthesis.****
Fact: Raising a Python exception stops execution of the code and, in addition to the provided error message, outputs information about where the error occurred during execution.
For example, here is the first error message we ever discussed (see Chapter 2):
>>> note2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'note2' is not defined
This was generated by the Python interpreter raising an exception (to tell us that it does not recognize the variable note2).
Python has a wide variety of predefined exceptions, including ArithmeticError, ZeroDivisionError, NameError, TypeError, and ValueError. All these can be raised, if needed, from inside our programs (using the syntax shown above).
In Chapter 8, we used sliders in conjunction with other controls on GUI displays. Here we define a new class, SliderControl, which can be used to create individual control surfaces (displays). A SliderControl object has a display with a single slider (see Figure 11.7). It can be used to control the value of any program variable interactively. The uses are limitless.§§§§
The SliderControl class has the following attributes:
Here is a SliderControl object whose update function simply outputs the updated slider value in the standard output:
# first, define the update function
def printValue(value):
print value
# create the SliderControl providing the label and update function
s = SliderControl("Print", printValue)
Now for another example, let’s create a SliderControl object to update the value of a program variable. This is a very useful application:
testValue = -1 # the variable to update interactively
# define the update function
def changeAndPrintValue(value):
global testValue
testValue = value
print testValue
# create the SliderControl, providing the update function
s = Slid erControl("Change and Print", changeAndPrintValue, 0, 100, 50, 300, 300)
where 0 is the minValue, 100 is the maxValue, 50 is the startValue, and the control display appears at coordinates 300, 300 of the computer screen.
The SliderControl class works only with integer values. Also, its accuracy depends on mouse accuracy (although it is possible to use arrow keys for finer control).¶¶¶¶ Working with float values is left as an exercise (see below).
Here is the code for the SliderControl class:
# sliderControl.py
#
# It creates a simple slider control surface.
#
from gui import *
class SliderControl:
def __init__(self, title="Slider", updateFunction=None, minValue=10, maxValue=1000, startValue=None, x=0, y=0):
"""Initializes a SliderControl object."""
# holds the title of the control display
self.title = title
# external function to call when slider is updated
self.updateFunction = updateFunction
# determine slider start value
if startValue == None: # if startValue is undefined
# start at middle point between min and max value
startValue = (minValue + maxValue) / 2
# create slider
self.slider = Slider(HORIZONTAL, minValue, maxValue, startValue, self.setValue)
# create control surface display
self.display = Display(self.title, 250, 50, x, y)
# add slider to display
self.display.add(self.slider, 25, 10)
# finally, initialize value and title (using 'startValue')
self.setValue(startValue)
def setValue(self, value):
""" Updates the display title, and calls the external update function with given 'value'.
"""
se lf.display.setTitle(self.title + " = " + str(value))
self.updateFunction(value)
This class definition consists of two functions, the constructor and setValue(). Notice how the class encapsulates a slider and a display. The constructor creates a Slider object and a Display object and adds the former to the latter. Also, it registers the class function setValue() with the Slider object, so that when the slider is interactively changed, this function will be called.
As we create new SliderControl objects, we will see new displays opening up. This is because, whenever a new object is created, Python runs the class __init__() function. In other words, for each SliderControl object created, function __init__() creates a new display window (in addition to all the other instance variables).
Fact: Every object of class has its own copies of instance variables and class functions.
One question is why create a separate class function setValue(), and not register the external updateFunction directly with the Slider object?
The answer is that setValue() also updates the display’s title (as seen above). Afterwards, setValue() calls the external updateFunction and gives it the new slider value.
The rest is straightforward.
def playNote(pitch):
""" Plays a 1-sec note with the provided pitch."""
Play.note(pitch, 0, 1000)
Of course, you will need to import the music library.
We are all familiar with computer animation, as seen in computer-generated cartoons, modern computer games, and CGI (computer-generated imagery) in cinema. Animation is based on the same principle used to make movies; a series of slightly varied still images is replayed in quick succession. This creates the perception of a moving image.
Fact: Animation is produced by showing individual images (frames) in quick succession.
The human eye begins to perceive continuous movement out of individual image sequences at a rate of about 15 frames per second. The standard cinema frame rate is 24 frames per second, although various experiments have been done at higher rates (e.g., 48). The higher the frame rate, the less “choppy” and more natural the experience of continuous movement is.
In order to write an animation program, we need to be able to accurately schedule Python statements (functions) to be executed at a later time, and also in quick (but precisely timed) succession. This is done through a Timer object (which we first used in Chapter 8).
Timer objects require a callback function and a delay (in milliseconds), which specifies when into the future to call this function. Animation is best expressed in terms of frame rate (i.e., changes per second) rather than in milliseconds, so we need to convert from one to the other.
The formula to convert the delay between consecutive events from milliseconds to frame rate is:
frameRate = 1000 / delay # convert delay in milliseconds to frame rate
Similarly, the formula to convert frame rate to delay is, of course:
delay = 1000 / frameRate # convert frame rate to delay in milliseconds
The following case study demonstrates how to create an animation involving a revolving sphere. We model the sphere by using points drawn on a GUI display. As seen on Figure 11.8, we can adjust the animation speed using a slider on a separate control surface (display).***** Additionally, as the points rotate, the code plays musical notes (more on that later).
The points are moved together in a synchronized way (in small increments, one per frame) to create the illusion of a rotating sphere. Using mathematical operations (actually, simple trigonometric functions) we calculate the points to always remain on (or trace) the surface of an imaginary sphere.
Additionally, by changing the colors of points, as they rotate, we create an illusion of depth. We use white to make points appear in front (closer to the viewer). We use darker colors to make points appear further back (that is, as points rotate away, they slowly disappear in the distance). The colors for points slowly change from white, to orange, to black. This is accomplished using a list of RGB colors that define a color gradient.
Definition: A color gradient is a smooth color progression from one color to another which creates the illusion of continuity between the two color extremes.
A color gradient may be easily created using the following function (included in the GUI library):
colorGradient(color1, color2, steps)
This returns a list of RGB colors creating a “smooth” gradient between color1 and color2. The amount of smoothness is determined by steps, which specifies how many intermediate colors to create. The result includes color1, but not color2, to allow for connecting one gradient to another (without duplication of colors). The number of steps equals the number of colors returned.
For example, the following creates a gradient list of 12 colors, from black (that is, [0, 0, 0]) to orange (that is, [251, 147, 14]):
>>> colorGradient([0, 0, 0], [251, 147, 14], 12)
[[0, 0, 0], [20, 12, 1], [41, 24, 2], [62, 36, 3], [83, 49, 4], [104, 61, 5], [125, 73, 7], [146, 85, 8], [167, 98, 9], [188, 110, 10], [209, 122, 11], [230, 134, 12]]
>>>
Notice how the code above excludes the final color (i.e., [251, 147, 14]). This allows us to create composite gradients (without duplication of colors). For example,
black = [0, 0, 0] # RGB values for black
orange = [251, 147, 14] # RGB values for orange
white = [255, 255, 255] # RGB values for white
cg = colorGradient(black, orange, 12) +
colorGradient(orange, white, 12) + [white]
The code above creates a combined list of gradient colors from black to orange, and from orange to white. Notice how the final color, white, has to be included separately (using list concatenation, i.e., “+”). Now cg contains a total of 25 unique gradient colors.
Thia also demonstrates now to use ‘’ to divide a long Python statement into two lines (to improve readability).
The code for the revolving musical sphere is presented below. By encapsulating this code in a Python class, MusicalSphere, we can easily have more than one sphere running at the same time. Each sphere has its own display and speed control surface (see Figure 11.8).
Class MusicalSphere has the following attributes:
As we have seen with other classes already (e.g., Note), creating a new sphere will be as simple as:
sphere = MusicalSphere(radius, density, velocity, frameRate)
where the provided parameters are defined as above.
A sphere is modeled using the spherical coordinate system. The spherical coordinate system is a 3D coordinate system, where the position of a point is specified by three numbers (see Figure 11.9):
Here is the code†††††:
# musicalSphere.py
#
# Demonstrates how to create an animation of a 3D sphere using
# GUI points on a Display. The sphere is modeled using points on
# a spherical coordinate system
# (see http://en.wikipedia.org/wiki/Spherical_coordinate_system).
# We convert from spherical 3D coordinates to cartesian 2D
# coordinates to position the individual points on the display.
# The z axis (3D depth) is mapped to color, using an orange gradient,
# ranging from white (front surface of sphere) to black (back
# surface of sphere). Also, when a point passes the primary meridian
# (the imaginary vertical line closest to the viewer), a note is
# played using the point's latitude for pitch (low to high).
# Also the point turns red momentarily.
#
from gui import *
from music import *
from random import *
from math import *
from sliderControl import *
class MusicalSphere:
"""Creates a revolving sphere that plays music."""
def __init__(self, radius, density, velocity=0.01, frameRate=30):
"""
Construct a revolving sphere with given 'radius', 'density'
number of points (all on the surface), moving with 'velocity'
angular (theta / azimuthal) velocity, at 'frameRate' frames
(or movements) per second. Each point plays a note when
crossing the zero meridian (the sphere's meridian (vertical
line) closest to the viewer).
"""
### musical parameters #######################################
self.instrument = XYLOPHONE
self.scale = PENTATONIC_SCALE
self.lowPitch = C2
self.highPitch = C7
self.noteDuration = 100 # milliseconds
Pl ay.setInstrument(self.instrument, 0) # set the instrument
### visual parameters ########################################
# create display to draw sphere (with black background)
self.display = Display("3D Sphere", radius*3, radius*3)
self.display.setColor(Color.BLACK)
self.radius = radius # how wide sphere is
se lf.numPoints = density # how many points on sphere surface
se lf.velocity = velocity # how far sphere rotates per frame
self.frameRate = frameRate # how many frames to do per second
# place sphere at display's center
self.xCenter = self.display.getWidth() / 2
self.yCenter = self.display.getHeight() / 2
### sphere data structure (parallel lists) ###################
self.points = [] # holds all the points
self.thetaValues = [] # holds point rotation (azimuthal angle)
self.phiValues = [] # holds point latitude (polar angle)
### timer to drive animation #################################
delay = 1000 / frameRate # convert frame rate to delay (ms)
self.timer = Timer(delay, self.movePoints) # create timer
### control surface for animation frame rate #################
xPosition = self.display.getWidth() / 3 # position control
yPosition = self.display.getHeight() + 45
self.control = SliderControl(title="Frame Rate",
updateFunction=self.setFrameRate,
minValue=1, maxValue=120,
startValue=self.frameRate,
x=xPosition, y=yPosition)
### color gradient (used to display depth) ###################
black = [0, 0, 0] # RGB values for black (back)
orange = [251, 147, 14] # RGB values for orange (middle)
white = [255, 255, 255] # RGB values for white (front)
# create list of gradient colors from black to orange, and from
# orange to white (a total of 25 colors)
self.gradientColors = colorGradient(black, orange, 12) +
colorGradient(orange, white, 12) +
[white] # include the final color
self.initSphere() # create the circle
self.start() # and start rotating!
def start(self):
"""Starts sphere animation."""
self.timer.start()
def stop(self):
"""Stops sphere animation."""
self.timer.stop()
def setFrameRate(self, frameRate=30):
""" Controls speed of sphere animation (by setting how many times per second to move points).
"""
# convert from frame rate to delay between each update
delay = 1000 / frameRate # (in milliseconds)
self.timer.setDelay(delay) # and set timer delay
def initSphere(self):
""" Generate a sphere of 'radius' out of points (placed on the surface of the sphere).
"""
for i in range(self.numPoints): # create all the points
# get random spherical coordinates for this point
r = self.radius # placed *on* the surface
theta = mapValue(random(), 0.0, 1.0, 0.0, 2*pi) # rotation
phi = mapValue(random(), 0.0, 1.0, 0.0, pi) # latitude
# remember this point's spherical coordinates by appending
# them to the two parallel lists (since r = self.radius
# for all points, no need to save that)
self.thetaValues.append(theta)
self.phiValues.append(phi)
# project spherical to cartesian 2D coordinates (z is depth)
x, y, z = self.sphericalToCartesian(r, phi, theta)
# convert depth (z) to color
color = self.depthToColor(z, self.radius)
# create point at these x, y coordinates, with this color
# and thickness 1
point = Point(x, y, color, 1)
# remember point by appending it to the third parallel list
self.points.append(point) # this point
# now, display this point
self.display.add(point)
def sphericalToCartesian(self, r, phi, theta):
""" Convert spherical to cartesian coordinates."""
# adjust rotation so that theta is 0 at max z (near viewer)
x = r * sin(phi) * cos(theta + pi/2) # horizontal axis
y = r * cos(phi) # vertical axis
z = r * sin(phi) * sin(theta + pi/2) # depth axis
# move sphere's center to display's center
x = int(x + self.xCenter) # pixel coordinates are integer
y = int(y + self.yCenter)
z = int(z)
return x, y, z
def depthToColor(self, depth, radius):
"""Create color based on depth."""
# map depth to gradient index (farther away less luminosity)
colorIndex = mapValue(depth, -self.radius, self.radius, 0, len(self.gradientColors))
# get corresponding gradient (RBG value), and create the color
colorRGB = self.gradientColors[colorIndex]
color = Color(colorRGB[0], colorRGB[1], colorRGB[2])
return color
def movePoints(self):
"""Rotate points on y axis as specified by angular velocity."""
for i in range(self.numPoints): # for every point
point = self.points[i] # get this point
theta = self.thetaValues[i] # get rotation angle
phi = self.phiValues[i] # get latitude (altitude)
# animate by incrementing angle to simulate rotation
theta = theta + self.velocity
# wrap around at the primary meridian (i.e., 360 degrees
# become 0 degrees) - needed to decide when to play note
theta = theta % (2*pi)
# convert from spherical to cartesian 2D coordinates
x, y, z = self.sphericalToCartesian(self.radius, phi, theta)
# check if point crossed the primary meridian (0 degrees),
# and if so, play musical note, and change its color to red
if self.thetaValues[i] > theta: # did we just wrap around?
# yes, so play note
pitch = mapScale(phi, 0, pi,
self.lowPitch, self.highPitch,
self.scale) # phi is latitude
dynamic = randint(0, 127) # get random dynamic
Play.note(pitch, 0, self.noteDuration, dynamic)
# set point's color to red
color = Color.RED
else: # otherwise, not at the primary meridian, so
# set point's color based on depth, as usual
color = self.depthToColor(z, self.radius)
# now, we have the point's new color and x, y coordinates
self.display.move(point, x, y) # move point to new position
point.setColor(color) # and update its color
# finally, save this point's new rotation angle
self.thetaValues[i] = theta
This program is a little more challenging than what we have seen so far. This is because it combines classes, top-down design, and a new physical model (simulating a sphere through individual, animated points) using trigonometric functions. It also makes music (resembling a music box).
Fact: Good programs are made to be read.‡‡‡‡‡
Advice: When confronted with a new, large program, start slowly from the top, and familiarize yourself in with its components—its class(es), instance variables, and its functions.
For the above program, a good strategy is to focus on the __init__(), initSphere(), and movePoints() functions. You may need to use a paper and pencil to write down some notes or questions. This way you can write important things down for later consideration (e.g., what do the various instance variables hold, or what function does the Timer object call repeatedly), while keeping your mind clear to absorb new things. If you approach things systematically, everything will soon make sense.
The __init__() function (constructor) starts, as usual, setting up the instance variables. First, notice the musical parameters, which define the type of sounds generated by the points, as they rotate through the primary meridian (the imaginary vertical line closest to the viewer).
self.instrument = XYLOPHONE
self.scale = PENTATONIC_SCALE
self.lowPitch = C2
self.highPitch = C7
self.noteDuration = 100 # milliseconds
Of particular interest is the way we model a sphere:
### sphere data structure (parallel lists) ###################
self.points = [] # holds the points
self.thetaValues = [] # holds the points' rotation (azimuthal angle)
self.phiValues = [] # holds the points' latitude (polar angle)
As mentioned earlier, we simulate a sphere using rotating points. For each of these points, we create a GUI point object (to be placed on the display) and its spherical coordinates (see previous section).
Since all points are placed on the sphere’s surface, we only need to store the θ and φ values (as the r value is always equal to the sphere’s radius — see self.radius).
Function initSphere() creates the individual points and updates the three parallel lists. (This will be discussed in more detail below.)
Also, of particular interest are the Timer and the SliderControl objects:
### timer to drive animation #################################
delay = 1000 / frameRate # convert frame rate to delay (ms)
self.timer = Timer(delay, self.movePoints) # create timer
### control surface for animation frame rate #################
xPosition = self.display.getWidth() / 3 # position control
yPosition = self.display.getHeight() + 45
self.control = SliderControl(title="Frame Rate",
updateFunction=self.setFrameRate,
minValue=1, maxValue=120,
startValue=self.frameRate,
x=xPosition, y=yPosition)
First, we convert the frame rate (measured in frames per second, e.g., 30) to a timer delay (that is, how often should the timer call the animation function). The timer delay is measured in milliseconds, hence the formula (delay = 1000 / frameRate).
Then we create the Timer object providing the delay and, most importantly, the animation function, movePoints(). This function is very important and is discussed later.
Notice how, when creating the SliderControl object, we specify its initial parameters (see previous case study). An important parameter is the update function, setFrameRate(). As you can see in the code, this function does one thing—it updates the timer delay. So, by having the slider control call it, when the slider is moved, in essence, we have connected the slider to the timer delay.
Another important set of parameters for the slider control is its initial x and y position. The goal here is to place the control surface below the main sphere display. So we use the main display’s (self.display) width and height to calculate a good initial position for the timer control surface (approximately right below the main display).§§§§§
Function initSphere() uses a for loop (controlled by the number of points needed) to create all the points and store them (and their spherical coordinates) into the three parallel lists described above. Notice how the θ (azimuthal) angle, for each point, is set to a random value between 0 and 2*pi (that is, 360 degrees):
theta = mapValue(random(), 0.0, 1.0, 0.0, 2*pi) # rotation
Similarly, the φ (polar) angle is set to a random value between 0 and pi (that is, 180 degrees):
phi = mapValue(random(), 0.0, 1.0, 0.0, pi) # latitude
We use pi (180 degrees) for φ (as opposed to 2*pi) because φ controls the placement of the point on the meridian (vertical half circle). The other angle, θ, moves this imaginary meridian around 360 degrees, thus covering the complete surface of the sphere. If we had used 360 degrees for φ, we would be covering the sphere surface twice. This would also work, but it is inelegant and wasteful.
Function sphericalToCartesian(), as its name indicates, converts from spherical to cartesian 3D coordinates, x, y, and z. We use the x and y coordinates to place the point on the GUI display. The math behind the conversion guarantees that the x and y coordinates will change proportionally to how the point is placed on the imaginary (simulated) sphere. This bit of math creates a quite stunning effect.¶¶¶¶¶
The third coordinate, z, corresponds to depth. As mentioned earlier, to simulate depth of a point, we use a color gradient (from white, to orange, to black). Function depthToColor() performs the necessary conversion, using the self.gradientColors list. This creates an effective illusion of visual depth.******
Finally, function movePoints() advances the θ angle of each point, as follows:
theta = self.thetaValues[i] # get rotation angle
# animate by incrementing angle to simulate rotation
theta = theta + self.velocity
# wrap around at the primary meridian (i.e., 360 degrees
# become 0 degrees) - needed to decide when to play note
theta = theta % (2*pi)
This adds the velocity (e.g., 0.01) specified by the end-programmer to the current θ value and ensures that when we reach 360° (2*pi), we zero out the θ angle. This is done by using the modulo, %, operator. (Recall that modulo 2*pi is the remainder of the division by 2*pi.) If the value in question is larger than 2*pi, it is replaced by the remainder, that is, it “wraps around” to the corresponding value smaller than 2*pi.††††††
Function movePoints() also checks for when a point crosses the primary meridian. This happens every time the modulo 2*pi statement above returned a value that's smaller than the original θ value:
if self.thetaValues[i] > theta: # did we just wrap around?
# yes, so play note
pitch = mapScale(phi, 0, pi,
self.lowPitch, self.highPitch,
self.scale) # phi is latitude
dynamic = randint(0, 127) # get random dynamic
Play.note(pitch, 0, self.noteDuration, dynamic)
# set point's color to red
color = Color.RED
else: # otherwise, not at the primary meridian, so
# set point's color based on depth, as usual
color = self.depthToColor(z, self.radius)
If so, we temporarily change the color of the point to red (to suggest a “plucking” effect, similar to a musical box). We also play a note with pitch corresponding to the φ angle (low to high)‡‡‡‡‡‡ and random dynamic.
Otherwise, if the point has not crossed the primary meridian, we simply assign it a color based on its depth and the selected color gradient.
Finally, function movePoints() moves the point on the display, using the updated information.
Again, notice how the following line drives the whole simulation:
sphere = MusicalSphere(radius=200, density=200, velocity=0.01, frameRate=30)
This concludes this case study. When you get this program running think of ways that you can map the data to sound and music and then implement those processes to “hear” your animation.
# musical parameters (alternate)
self.instrument = PIANO
self.scale = MAJOR_SCALE
self.lowPitch = C1
self.highPitch = C6
self.noteDuration = 2000 # milliseconds (2 seconds)
Notice how these create a more solemn, meditative mood. This demonstrates the importance of design in music composition, that is, to carefully consider the effect produced by a particular pitch set, register choice, and orchestration (in addition to melody and harmony).
As discussed in Chapter 1, Cymatics (from the Greek κύμα, “wave”) is the study of visible (visualized) sound and vibration in 1-, 2-, and 3-dimensional artifacts. It was influenced significantly by the work of the 18th century physicist and musician Ernst Chladni, who developed a technique to visualize modes of vibration on mechanical surfaces, known as Chladni plates (see Figure 11.10).
In the previous case study, we introduced the idea of simulating a sphere using individual points placed on a GUI display. These points were controlled by the simulation algorithm to move on the surface of an imaginary sphere. The algorithm changed their colors and played sounds when they crossed a certain threshold.
The concept of cymatics takes this idea a step up.
What if, instead of having passive points, the points were actively moving on their “own.” That is, instead of each point being controlled by an external (to it) algorithm, what if each point encapsulated its own set of rules on how to react to its environment? This idea leads to simulations that “behave” more naturally, as they model interactions with various phenomena that surround them.
Since its creation, the universe, as we perceive it, consists of innumerable individual elements, at different scales of size and time. These elements interact, converge, and diverge to create systems of organized behavior for example, an electron, a water molecule, the human brain and its neurons, a sandpile and its sand particles, the solar system and its planets and asteroids, and so on (Bak 1996). Anything we perceive can potentially be described in terms of more atomic elements that somehow interact, and, in some cases, balance out with each other, following the principle of least effort (that is, finding a place of apparent rest or a place of quasi-periodic movement).
The universe is filled with such systems.
This case study demonstrates how to simulate such a system. It is based on “Boids,” a program developed by Craig Reynolds in 1986, modeling the flocking behavior of birds (Reynolds 1987).§§§§§§
Boids are self-adapting entities (agents) which are capable of perceiving their environment and acting upon the environment. This idea is used in developing artificial intelligence applications and simulations. This is a quite powerful concept, full of creative musical potential.
The following program demonstrates the types of behaviors that emerge from self-adapting agents (boids), which sense their surroundings and modify their behavior (movement) accordingly (see Figure 11.11). It is hard to appreciate the liveliness of the resulting animation through a static figure. Imagine each of these boids (points) moving around the display, staying in close proximity to other boids, yet avoiding them, while trying to collectively move in a certain direction. The resulting movement, in some cases, resembles the movement of birds, fish, and other animal collectives.
These boids follow three simple rules:
An additional rule (left as an exercise) is the following:
A boid applies the above rules to decide what to do next. Each of these rules contributes a direction and a distance in which to move. The boid combines these directions and distances to generate a single direction and distance in which to move (in each animation frame). Sound simple? It is.
As discussed in the section on Zipf’s law, such agents tend to minimize their overall effort associated with this interaction (economy principle). That is, a system of such interacting agents tends to find a global optimum that minimizes overall effort (Bak 1996). This interaction models some sort of exchange (e.g., information, energy, etc.).
Given the simulation parameters, the collective movements of each and all boids, over time, can result in three possibilities:
As mentioned above, each of the simple rules (followed by a boid) contributes a direction and a distance to move toward. A boid combines these directions and distances to generate a single direction and distance to move toward.
Each direction and distance is simply modeled by x and y values. The larger the values, the larger the movement. This information is called a vector (see Figure 11.13).
Definition: A vector is a pair of x and y values, which indicate how far to move from a given position (in x and y).*******
Python provides a convenient data structure for storing x and y coordinates, called complex. Python complex numbers consist of two values, the real value (which we will use to store x coordinates) and the imaginary value (which we will use to store y coordinates).†††††††
For example, the following creates a complex number:
>>> c = complex(1, 2)
>>> c
(1+2j)
>>> c.real
1.0
>>> c.imag
2.0
Notice how, after the complex number c is created, we can easily retrieve its real (x) and imaginary (y) parts.
Python complex numbers are great, since they implement addition and multiplication (or division).
The addition of two complex numbers simply adds the two x values together and two y values together. For example:
>>> c = complex(1, 2)
>>> d = complex(4, 6)
>>> e = c + d
>>> e.real
5.0
>>> e.imag
8.0
Multiplication (or division) of a complex number with a regular (single) number simply multiplies each of the two coordinates by the regular number (or divides them by, for division). For example:
>>> c = complex(1, 2)
>>> f = c * 3
>>> f.real
3.0
>>> f.imag
6.0
In the next three sections, we present the code for the Boid Universe simulation. These three pieces of code were placed in the same Python file (“boids.py”). They have been separated here for convenience in explaining their key points.
Here is the code for the boid universe, together with the global simulation parameters:
# boids.py
#
# This program simulates 2D boid behavior.
#
# See http://www.red3d.com/cwr/boids/ and
# http://www.vergenet.net/~conrad/boids/pseudocode.html
#
from gui import *
from math import *
from random import *
# universe parameters
universeWidth = 1000 # how wide the display
universeHeight = 800 # how high the display
# boid generation parameters
numBoids = 200 # from 2 to as much as your CPU can handle
boidRadius = 2 # radius of boids
boidColor = Color.BLUE # color of boids
# boid distance parameters
mi nSeparation = 30 # min comfortable distance between two boids
fl ockThreshold = 100 # boids closer than this are in a local flock
# boid behavior parameters (higher means quicker/stronger)
separationFactor = 0.01 # how quickly to separate
al ignmentFactor = 0.16 # how quickly to align with local flockmates
co hesionFactor = 0.01 # how quickly to converge to attraction point
fr ictionFactor = 1.1 # how hard it is to move (dampening factor)
### define boid universe, a place where boids exist and interact ####
class BoidUniverse:
""" This is the boid universe, where boids exist and interact. It is basically a GUI Display, with boids (moving Circles) added to it. While boids are represented as circles, they have a little more logic to them - they can sense where other boids are, and act accordingly. While individual boids have simple rules for sensing their environment and reacting, very intricate, complex, naturally-looking patterns of behavior emerges, similar to those of birds flying high in the sky (among others). The rules of behavior are the same for all boids, and are defined in the Boid class (a sister class to this one).
"""
def __init__(self, title = "", width = 600, height = 400, frameRate=30):
self.display = Display(title, width, height) # universe display
self.boids = [] # list of boids
# holds attraction point for boids (initially, universe center)
self.attractPoint = complex(width/2, height/2)
# create timer
delay = 1000 / frameRate # convert frame rate to delay (ms)
self.timer = Timer(delay, self.animate) # animation timer
# when mouse is dragged, call this function to set the
# attraction point for boids
self.display.onMouseDrag(self.moveAttractionPoint)
def start(self):
"""Starts animation."""
self.timer.start() # start movement!
def stop(self):
"""Stops animation."""
self.timer.stop() # stop movement!
def add(self, boid):
"""Adds another boid to the system."""
self.boids.append(boid) # remember this boid
self.display.add(boid.circle) # add a circle for this boid
def animate(self):
"""Makes boids come alive."""
### sensing and acting loop for all boids in the universe !!!
for boid in self.boids: # for every boid
# first observe other boids and decide how to adjust movement
boid.sense(self.boids, self.attractPoint)
# and then, make it so (move)!
boid.act(self.display)
def moveAttractionPoint(self, x, y):
"""Update the attraction point for all boids."""
self.attractPoint = complex(x, y)
Notice how the code starts with various parameters that control the boid simulation. These parameters are as follows:
# boid generation parameters
nu mBoids = 200 # from 1 to as much as your CPU can handle
boidRadius = 2 # radius of boids
boidColor = Color.BLUE # color of boids
# boid distance parameters
mi nSeparation = 30 # min comfortable distance between any two boids
fl ockThreshold = 100 # boids closer than this are in a local flock
# boid behavior parameters (higher means quicker/stronger)
separationFactor = 0.01 # how quickly to separate
al ignmentFactor = 0.16 # how quickly to align with local flockmates
co hesionFactor = 0.01 # how quickly to converge to attraction point
fr ictionFactor = 1.1 # how hard it is to move (dampening factor)
These parameters are very important. Modifying them may drastically change the system’s behavior. Again, boids may converge (to a single point), diverge (and disappear), or engage in quasi-periodic oscillations. Run the code and experiment with different settings (also see the exercises at the end of the chapter).
Next we have the class defining the boid universe, called (what else?), BoidUniverse. Notice how its constructor, __init__(), encapsulates the GUI display, a list of boids to be used in the simulation, and the attraction point for all the boids. Also, notice how it creates the timer that runs the animation (probably the most important line of code in the whole program, if there is one):
# create timer
delay = 1000 / frameRate # convert frame rate to delay (ms)
self.timer = Timer(delay, self.animate) # animation timer
This causes function animate() to be called repeatedly by the timer, once every delay milliseconds (e.g., if frameRate is 30, then animate() will be called approx. once every 33 milliseconds).
Finally, the BoidUniverse constructor assigns function moveAttractionPoint() to the display’s mouse-dragging event. This function simply updates the attraction point, when the mouse is dragged. This allows the end-user to control boid flocking behaviors interactively.
The next interesting function is add(). Notice how, when called with a boid, it simply adds it to the BoidUniverse’s list of boids, and also, adds the boid’s internal representation, a circle, to the GUI display. This is necessary, because the GUI display knows nothing about boids—it only knows about GUI widgets, like a circle.
def add(self, boid):
"""Adds another boid to the system."""
self.boids.append(boid) # remember this boid
self.display.add(boid.circle) # add a circle for this boid
Finally, the most important function of the BoidUniverse class is animate(). As its name indicates, this function creates the animation (or makes boids come alive). Notice how it uses a for loop to iterate through the BoidUniverse’s list of boids. Then, for each boid, it calls its sense() and act() functions.
def animate(self):
"""Makes boids come alive."""
### sensing and acting loop for all boids in the universe !!!
for boid in self.boids: # for every boid
# first observe other boids and decide how to adjust movement
boid.sense(self.boids, self.attractPoint)
# and then, make it so (move)!
boid.act(self.display)
Notice how the boid sense() function expects the list of boids, as well as the attraction point. As we will see soon, this is because it will examine the locations of all the other boids, as well as the location of the attraction point. Then, using the boid behavior rules, it will determine a new direction and distance for each boid to travel, in the current animation frame.‡‡‡‡‡‡‡ This movement is accomplished by function act(), which only requires the GUI display, since the rest of the information is maintained in each Boid object’s instance variables.
Next, let’s examine the Boid class.
Here is the code for the individual boids, followed by an explanation of key points:
### define the boids, individual agents who can sense and act #######
class Boid:
""" This a boid. A boid is a simplified bird (or other species) that lives in a flock. A boid is represented as a circle, however, it has a little more logic to it - it can sense where other boids are, and act, simply by adjusting its direction of movement. The new direction is a combination of its reactions from individual rules of thumb (e.g., move towards the center of the universe, avoid collisions with other boids, fly in the same general direction as boids around you (follow the local flock, as you perceive it), and so on. Out of these simple rules intricate, complex behavior emerges, similar to that of real birds (or other species) in nature.
"""
def __init__(self, x, y, radius, color,
initVelocityX=1, initVelocityY=1):
""" Initialize boid's position, size, and initial velocity (x, y).
"""
# a boid is a filled circle
self.circle = Circle(x, y, radius, color, True)
# set boid size, position
self.radius = radius # boid radius
self.coordinates = complex(x, y) # boid coordinates (x, y)
# NOTE: We treat velocity in a simple way, i.e., as the
# x, y displacement to add to the current boid coordinates,
# to find where to move its circle next. This moving is done
# once per animation frame.
# initialize boid velocity (x, y)
self.velocity = complex(initVelocityX, initVelocityY)
def sense(self, boids, center):
"""
Sense other boids' positions, etc., and adjust velocity
(i.e., the displacement of where to move next).
"""
# use individual rules of thumb, to decide where to move next
# 1. Rule of Separation - move away from other flockmates
# to avoid crowding them
self.separation = self.rule1_Separation(boids)
# 2. Rule of Alignment - move towards the average heading
# of other flockmates
self.alignment = self.rule2_Alignment(boids)
# 3. Rule of Cohesion - move toward the center of the universe
self.cohesion = self.rule3_Cohesion(boids, center)
# 4. Rule of Avoidance: move to avoid any obstacles
#self.avoidance = self.rule4_Avoidance(boids)
# create composite behavior
self.velocity = (self.velocity / frictionFactor) +
self.separation + self.alignment +
self.cohesion
def act(self, display):
"""Move boid to a new position using current velocity."""
# Again, we treat velocity in a simple way, i.e., as the
# x, y displacement to add to the current boid coordinates,
# to find where to move its circle next.
# update coordinates
self.coordinates = self.coordinates + self.velocity
# get boid (x, y) coordinates
x = self.coordinates.real # get the x part
y = self.coordinates.imag # get the y part
# act (i.e., move boid to new position)
display.move(self.circle, int(x), int(y))
##### steering behaviors ####################
def rule1_Separation(self, boids):
""" Return proper velocity to keep separate from other boids, i.e., avoid collisions.
"""
newVelocity = complex(0, 0) # holds new velocity
# get distance from every other boid in the flock, and as long
# as we are too close for comfort, calculate direction to
# move away (remember, velocity is just an x, y distance
# to travel in the next animation/movement frame)
for boid in boids: # for each boid
separation = self.distance(boid) # how far are we?
# too close for comfort (excluding ourself)?
if separation < minSeparation and boid != self:
# yes, so let's move away from this boid
newVelocity = newVelocity - (boid.coordinates - self.coordinates)
return newVelocity * separationFactor # return new velocity
def rule2_Alignment(self, boids):
""" Return proper velocity to move in the same general direction as local flockmates.
"""
totalVelocity = complex(0, 0) # holds sum of boid velocities
numLocalFlockmates = 0 # holds count of local flockmates
# iterate through all the boids looking for local flockmates,
# and accumuate all their velocities
for boid in boids:
separation = self.distance(boid) # get boid distance
# if this a local flockmate, record its velocity
if separation < flockThershold and boid != self:
totalVelocity = totalVelocity + boid.velocity
numLocalFlockmates = numLocalFlockmates + 1
# average flock velocity (excluding ourselves)
if numLocalFlockmates > 0:
avgVelocity = totalVelocity / numLocalFlockmates
else:
avgVelocity = totalVelocity
# adjust velocity by how quickly we want to align
newVelocity = avgVelocity - self.velocity
return newVelocity * alignmentFactor # return new velocity
def rule3_Cohesion(self, boids, center):
""" Return proper velocity to bring us closer to center of the universe.
"""
newVelocity = center - self.coordinates
return newVelocity * cohesionFactor # return new velocity
##### helper function ####################
def distance(self, other):
""" Calculate the Euclidean distance between this and another boid.
"""
xDistance = (self.coordinates.real - other.coordinates.real)
yDistance = (self.coordinates.imag - other.coordinates.imag)
return sqrt(xDistance*xDistance + yDistance*yDistance)
The Boid constructor, __init__(), creates the boid’s visual representation (a GUI circle) and initializes the boid’s size (self.radius), position (self.coordinates), and initial velocity (self.velocity). Again, keep in mind that we model velocity in a simple, meaningful way—as the distance and direction a boid needs to travel in one animation frame (that is, a relative x, y displacement from its current position). Thus, moving a boid is as simple as adding self.velocity's x component to self.coordinate's x component. The same is done for y. That’s all!
The Boid class has two main functions, sense() and act().
The sense() function calls a sequence of simple functions that implement the steering (or behavior) rules. Each of these functions contributes a suggested velocity (again, modeled simply as an x and y displacement). We say “suggested” because this velocity is this contribution of one only rule and needs to be combined with the suggested velocities of all other rules. Combining velocities is simply done by adding their x and y components, respectively:
# create composite behavior
self.velocity = (self.velocity / frictionFactor) +
self.separation + self.alignment +
self.cohesion
It is as simple as addition. (If some of the velocities are pointing in opposite directions, the corresponding x and y components simply cancel out—their signs will be opposite. Addition does this.)
All the rules are computed similarly. So here we will study more carefully a representative one, the rule for alignment. As mentioned earlier,
This means that each boid needs to adjust its velocity to match the average velocity of all other boids in its local flock. The local flock is determined by distance. That is, the local flock of a boid consists of boids that are closer to it than a certain distance (flockThreshold).§§§§§§§
This function is called by a single boid (self). It is given access to the list of all boids.
def rule2_Alignment(self, boids):
""" Return proper velocity to move in the same general direction as local flockmates.
"""
Fact: Variable self has the same value as the value returned when we create a new boid by Boid(x, y, boidRadius, boidColor, 1, 1).
That is, inside the boid’s code, we use self to determine who we are (when needed by the algorithm/code).
Inside the function, we first initialize needed variables:
totalVelocity = complex(0, 0) # holds sum of boid velocities
numLocalFlockmates = 0 # holds count of local flockmates
Next we loop through all the boids. For every boid that is close enough, we add up its velocity to the total velocity of other flockmates, so far:
for boid in boids:
separation = self.distance(boid) # get boid distance
# if this a local flockmate, record its velocity
if separation < flockThershold and boid != self:
totalVelocity = totalVelocity + boid.velocity
numLocalFlockmates = numLocalFlockmates + 1
Notice how we use function self.distance() to get the Euclidean distance between the two boid positions. Also, notice how, as we loop through all the boids, we exclude the current boid from the calculation:
if separation < flockThershold and boid != self:
Notice how addition of Python complex numbers takes care of adding the x and y components of the velocities:
totalVelocity = totalVelocity + boid.velocity
And, of course, we keep track of how many boids are in the local flock (to use later, for calculating the average):
numLocalFlockmates = numLocalFlockmates + 1
Then we simply divide totalVelocity by the number of flockmates.
# average flock velocity vector (excluding ourselves)
if numLocalFlockmates > 0:
avgVelocity = totalVelocity / numLocalFlockmates
else:
avgVelocity = totalVelocity
Finally, we return the difference between the flock’s average velocity and our velocity (self.velocity), adjusted by the global parameter, alignmentFactor. This parameter controls the strength of this contribution to the overall steering behavior of this boid (see previous section).
# adjust velocity by how quickly we want to align
newVelocity = avgVelocity - self.velocity
return newVelocity * alignmentFactor # return new velocity
The act() function simply updates the position of the current boid, given the new composite velocity, which was calculated by function sense().
# update coordinates
self.coordinates = self.coordinates + self.velocity
# get boid (x, y) coordinates
x = self.coordinates.real # get the x part
y = self.coordinates.imag # get the y part
Then it updates the boid's visual representation on the GUI display.
# act (i.e., move boid to new position)
display.move(self.circle, int(x), int(y))
This concludes the explanation of the Boid class.
The last section of the code uses the above class definitions to create one BoidUniverse object and several Boid objects:
# start boid simulation universe = BoidUniverse(title="Boid Flocking Behavior",
width=universeWidth, height=universeHeight,
frameRate=30)
# create and place boids
for i in range(0, numBoids):
x = randint(0, universeWidth) # get a random position for this boid
y = randint(0, universeHeight)
# create a boid with random position and velocity
boid = Boid(x, y, boidRadius, boidColor, 1, 1)
universe.add(boid)
# animate boids
universe.start()
Notice how boids are placed randomly across the boid universe. The rest of the code is self-explanatory. The exercises below will help you explore the code better and appreciate its creative musical (and other) potential.
Parameter | Set 1 | Set 2 | Set 3 | Set 4 | Set 5 | Set 6 | Set 7 | Set 8 | Set 9 |
boids | 200 | 200 | 200 | 49 | 19 | 2 | 3 | 4 | 6 |
min separation | 30 | 47 | 47 | 30 | 30 | 70 | 30 | 30 | 47 |
flock threshold | 100 | 267 | 100 | 100 | 100 | 100 | 100 | 100 | 100 |
separation factor | 0.01 | 0.01 | 0.01 | 0.13 | 0.13 | 0.1 | 0.01 | 0.01 | 0.03 |
alignment factor | 0.16 | 0.01 | 0.16 | 0.16 | 0.16 | 1.6 | 1.6 | 1.6 | 0.16 |
coherence factor | 0.01 | 0.15 | 0.01 | 0.01 | 0.01 | 0.001 | 0.001 | 0.001 | 0.01 |
friction factor | 1.1 | 1.1 | 4.3 | 4.2 | 4.2 | 1.1 | 1.1 | 1.1 | 1.1 |
Different people throughout history believed that exploring, studying, classifying, and modeling the types of patterns and structures that emerge in nature holds the key to understanding the mystery of life and the universe we live in. The development of music and mathematics (and, eventually, computer science) was fueled by this human need to explain nature and the observable phenomena that surround us. The mathematical concepts of the golden ratio, Fibonacci numbers, Zipf’s law, cymatics, fractals, and boids are all part of the same theme that interweaves music, nature, and numbers.
In this book, we have underscored the connections among music, number, and nature. We also explored how to represent useful mathematical knowledge and processes in Python. It is interesting to realize that programming is also a representation. What sets programming apart is that it represents not only knowledge (i.e., data) but also ways to act on that knowledge (i.e., process these data with algorithms). Also, and most importantly, this programmed representation is “runnable” on everyday computing devices. To the best of our knowledge, this is the first time in the history of our species that the everyday person (e.g., you and your friends) has access to such powerful (magical?) representations (such as Python)and also access to cheap devices that can execute Python programs.
Our hope is that you have been inspired by this intersection of music, mathematics, and nature to hopefully continue this exploration on your own. And remember, the universe is a mysterious and magical place, which you may explore through music, numbers, and computing. Ars longa, vita brevis ...
* If you are not careful, it is easy to write a recursive function that never terminates. For this reason we make sure we always have a limit (or a simple case), and that we work toward it. When the limit is reached, the function terminates.
† Now, try calling fib() with n equal to 3. You may need to write intermediate results on paper.
‡ In the next case study, we will see a different way to write programs that involve functions, namely, top-down implementation.
§ Teaching you how to write recursive programs is beyond the scope of the book. However, if you understand how this example operates, you are halfway there. And remember, nature is full of recursive processes.
¶ Zipf’s work had considerable influence on Benoit Mandelbrot, who, being inspired by Zipf’s ideas, eventually developed, the field of fractal geometry (Mandelbrot 1982).
** It has been shown that books written in different human languages (including Esperanto) have different Zipf slopes. This is something that Zipf himself expected, but was statistically verified only recently (Manaris et al. 2006).
†† Another name for this formula is Riemann’s Zeta function, which is “probably the most ... mysterious object of modern mathematics, in spite of its utter simplicity” (Watkins 2001, p. 58). Riemann’s Zeta function is connected to prime numbers, which shows us that Zipf’s law is capturing an essential aspect of the universe we live in (a mathematico-philosophical discussion beyond the scope of this chapter). What is relevant to us is that Zipf’s law is connected to music and aesthetics (as discussed below).
‡‡ This corresponds to another very common distribution found in nature (known as Brownian motion).
§§ This means that pitches in this piece have approximately harmonic proportions (i.e., 1/1, 1/2, 1/3, etc.).
¶¶ Ideally, random() should generate a slope closer to 0.0. However, since this piece contains only 100 pitches, with different runs of that program, we may get slightly different results (as it is possible sometimes to get far more heads than tails, when flipping a coin). Also, more notes may be needed for the random behavior to emerge more consistently.
*** Compare these functions to the Note class functions provided in Appendix A.
††† Beware. If you run the above code, the music library’s Note will be superseded by our new Note class definition. This only lasts during the execution of the code. No permanent change is made in the music library.
‡‡‡ Actually, self is not a reserved word. It is possible to use another prefix, but that would go against Python convention.
§§§ Most likely, you would think there is something wrong with your computer or MIDI synthesizer. You would never think to look back through your code. And you shouldn’t have to.
¶¶¶ By providing this defense mechanism, we ensure that class data always remains correct. This is a great guarantee to be able to offer, as it leads to error-free software.
**** This is actually creating an instance of the class TypeError, with the provided string as the parameter to be passed to the TypeError constructor. This is very similar to Note(C4, QN).
†††† This is the smallest integer that can be stored in 32 binary digits (bits) and is clearly outside the valid range for note pitches (0 to 127). A bit is a special memory unit, at the hardware (electronics) level, that can store either a 0 or 1. With its long integers, Python theoretically has no limit as to how large an integer may be. However, other languages, such as Java and C/C++, have such limits (with 32 bits being a common one).
‡‡‡‡ Since Rest is also a Note, Python provides an elegant way to do this, called inheritance. Inheritance is beyond the scope of this chapter.
§§§§ Actually, we will get to use it in the case studies that follow.
¶¶¶¶ For finer control, it is possible to use arrow keys to control a slider. To do so, put the slider in focus (click on the bubble) and then use the arrow keys to increment (or decrement) the slider value by 1.
***** This uses a TimerControl object (as defined above).
††††† Based on code by Uri Wilensky (1998), distributed with NetLogo.
‡‡‡‡‡ Also, further explore Donald Knuth’s concept of Literate Programming (http://www.literateprogramming.com).
§§§§§ Ideally, this calculation should be relative to the main display’s position (here we are assuming that it is always created at the top-left screen corner. To be more correct, we should be using self.display.getPosition(), which returns the display’s x and y coordinates in a list. This is left as an exercise.
¶¶¶¶¶ Similar math is used in modern computer games to project 3D objects onto the 2D computer screen. If you are interested in computer games, this little math demystifies how its all done.
****** Explore using different colors in the gradient. Is white, to orange, to black the best combination? What other colors can you use?
†††††† Wrapping around ever increasing (or decreasing) values, using the modulo operator with the limiting value, is a common technique in computer science. Try it out to see that it works.
‡‡‡‡‡‡ Ideally this should be inverted so that a low-placed point produces a low pitch. But, given the number of points, this subtlety is lost (so we aimed for a more efficient solution – one less operation). Sometimes efficiency (there are hundreds of points to be calculated per animation frame, and many animation frames per second) trumps completeness.
§§§§§§ “Boid” refers to a bird-like object (a bird-oid), and, in a stereotypical New York accent, sounds like “bird.”
¶¶¶¶¶¶ This behavior is quite mesmerizing to watch (and tinker with, by adjusting the program parameters).
******* Actually, this is the definition of a 2D vector (a vector in two dimensions, like a GUI display). For a vector in 3D space, we add one more dimension (z). And so on.
††††††† Do not let the terms “real” and “imaginary” confuse you. For our purposes, “real” means “x coordinate” and “imaginary” means “y coordinate.”
‡‡‡‡‡‡‡ Recall that function animate() is called repeatedly by the BoidUniverse timer, according to the frame rate (e.g., 30 times a second).
§§§§§§§ Recall that flockThreshold is one of the system’s adjustable parameters.
¶¶¶¶¶¶¶ For instance, scaling would be helpful if you implemented three dimensions (x, y, z). In that case, you would map the z coordinate (depth) to Icon scaling/resizing (via the setSize() function).
******** Notice how atan2() provides the inverse calculation of sin() and cos(). In other words, atan2(sin(theta), cos(theta)) == theta.
†††††††† Another possibility is to (also) change the size of the Circle object (but to do this requires creating a new Circle every time a boid changes depth). This is costly, but interesting to try out, especially for few boids. Using an Icon object instead allows you to easily do rescaling via the setSize() function. Explore.
‡‡‡‡‡‡‡‡ It is relatively easy to create a loopable audio sample. Start with a sound you like. Using an audio editor (like Audacity), split the audio sample in half. Switch the two halves (that is, put the second half at the beginning, and the first half at the end), making sure they overlap a little. Using cross-fade mix the two switched halves into one sample. Now the end and the beginning of the new sample match perfectly. An audio loop is born!
§§§§§§§§ This way, you could engage your audience in a musical performance/happening.