Chapter 11

Exploring Powerful Ideas

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).

11.1 Overview

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.

11.2 Fractals and Recursion

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.

11.3 Fibonacci Numbers and the Golden Ratio

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.

Figure 11.1

Image of A nautilus shell and its relationship to Fibonacci numbers

A nautilus shell and its relationship to Fibonacci numbers.

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):

  • fib(0) = 0
  • fib(1) = 1
  • fib(n) = fib(n-2) + fib(n-1) (i.e., to get the next number you add the previous two)

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.

11.3.1 Case Study: The Golden Tree

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).

Figure 11.2

Image of A fractal (golden) tree with depth 14 (13 subdivisions)

A fractal (golden) tree with depth 14 (13 subdivisions).

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

  1. when called recursively, it operates on smaller versions of a problem, and
  2. it contains an if statement, which checks if we have reached the simplest possible (or smallest practical) case, and if so, it then stops by not calling itself any more.

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

Figure 11.3

Image of A fractal (golden) tree with depth 1, 2, and 3, respectively

A fractal (golden) tree with depth 1, 2, and 3, respectively.

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:

  • What are the input parameters, especially depth?
  • What is the value of the condition depth > 1 in the if statement?
  • If True, what are the input parameters to the two recursive calls to drawTree()?

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.

11.3.1.1 Exercises

  1. Explore ways to sonify this fractal process. One way is to map coordinates x, y to pitch and velocity (actually negative y or –y), length to duration, and depth to panning. Try to make the recursive process audible (e.g., depth to panning). Many other possibilities exist, so experiment.
  2. Another fractal shape is the Sierpinski triangle (named after the Polish mathematician who described it in 1915). This fractal triangle consists of three smaller triangles (top, bottom left, bottom right) that have the same shape as the main one (see Figure 11.4). These smaller triangles, again, consist of three even smaller triangles that have the same shape. This repetition or subdivision continues on and on (theoretically) to infinity. Interestingly, similar patterns appear in ancient mosaics, suggesting that self-similarity was a known concept to the ancients. (Hint: Consider using the drawPolygon() function of GUI displays.)
  3. The literature is full of various fractal shapes (Koch curve, Mandelbrot set, etc.). Explore writing recursive programs that generate them.
  4. Explore the numerous resources in the literature and on the web on fractals and fractal music. Can you think of any new ideas?

11.4 Zipf’s Law

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?

Figure 11.4

Image of A Sierpinski fractal triangle with depth 1, 2, 3, and 6, respectively

A Sierpinski fractal triangle with depth 1, 2, 3, and 6, respectively.

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.

Figure 11.5

Image of Zipf plot of word counts in this book

Zipf plot of word counts in this book (slope is −1.23, and R2 is 0.98).

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.

11.4.1 Zipf’s Law and Music

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.

Figure 11.6

Image of Pitch distribution

(Left) Pitch distribution for J. S. Bach’s Orchestral Suite No. 3 in D “Air on a G String” BWV 1068. (Right) Pitch distribution for a piece generated using function random().

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.).

11.4.2 What Does It Mean?

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).

11.4.3 Measuring Zipf Proportions

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
	print
	# 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.

11.4.3.1 Top-Down Design (Revisited)

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:

  • It is interesting to note that Deep Purple’s “Smoke on The Water” has near Zipfian pitch proportions (slope is –1.25, and R2 is 0.94). Perhaps it is not an accident that this guitar riff was very popular among rock guitarists for decades.
  • The “sonifyBiosignals” piece is measured as monotonous in terms of pitch (slope is –2.3). This means that certain notes predominate. Which is true, when you listen to it carefully. It does sound monotonous and repetitive (with some variation, of course).
  • Arvo Pärt’s “Cantus in Memoriam” has near Zipfian pitch proportions (slope is –0.85). Another balanced piece in terms of pitch. Can you hear that?
  • The “soundscape Loutraki Sunset” appears to have near Zipfian pitch proportions (slope is –0.929), but the R2 is 0.5; this means that the data points are kind of scattered, so the slope is not as reliable. Yet, as you listen to it, it does sound well-proportioned in terms of pitch, if not beautiful.
  • Finally, the Zipf pitch proportion for “Pierre Cage.Structures pour deux chances” indicates that the piece is near chaotic (i.e., pitches are approaching randomness), with a slope of –0.46.¶¶ This corresponds with how the piece sounds.

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).

11.4.4 Python Dictionaries

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.

11.4.5 Exercises

  1. Listen to the pieces used in the last case study. Can you hear the pitch proportions in the pieces, as analyzed by the Zipf metric computed by the program cited?
  2. Search for and download MIDI files for Arnold Schoenberg (or other) 12-tone compositions. Listen to them. What type of Zipf pitch slopes would you expect? Verify your prediction using the cited program.
  3. Using the cited program, measure Zipf proportions of popular pieces available in MIDI online. What do you observe?
  4. Add more functions, such as countDurations(), countDynamics(), and countPitchDurations(). The first two are self-explanatory. For the third one, extract both pitch and duration values for each note. Combine them into a single value (for counting purposes) using this statement: pitchDuration = pitch + duration * 0.01 This ensures that, for normal values for pitch and duration, the two values will be safely combined into one, where the pitch forms the integral part of the number (to the left of the decimal point) and duration forms the decimal part of the number. Try some examples to verify this works.
  5. Modify the above code to measure Zipf proportions in a text document. Hint: Instead of reading MIDI pieces, read a text file (as shown in Chapter 7). Using string library functions, separate the text into words. Then use the histogram method used in countPitches() to instead countWords(). Finally, go on project Gutenberg (www.gutenberg.org) and download electronic books to measure. What used to take months to do, by Zipf and his research students, you will be able to do in a few seconds. Enjoy.

11.5 Python Classes

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.

11.6 Case Study: The Note Class

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:

  • pitch—an integer from 0 (low) to 127 (high)
  • duration—a positive real number (quarter note is 1.0)
  • dynamic (or volume)— an integer from 0 (silent) to 127 (loudest)
  • panning—a real number from 0.0 (left) to 1.0 (right)

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***:

  • Note(pitch, duration, dynamic, pan) — create a new note object
  • getPitch() — return the note’s pitch (0−127)
  • setPitch(value) — updates the note’s pitch to value (0−127)
  • getDuration() — returns the note’s duration
  • setDuration(value) — updates the note’s duration to value (a float)

11.6.1 Creating Note Objects

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
>>>

11.6.2 Defining the Class

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.

11.6.2.1 Checking for Data Integrity

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:

  1. state where the error occurred in the code,
  2. describe the error, in a way that can be understood by someone who cannot see your internal code, and
  3. provide guidance on how to fix the error.

For example, the else part above uses these statements:

print "TypeError: Note.setPitch(): pitch ranges from",
print "0 to 127 (got " + str(pitch) + ")"

11.6.3 Python Exceptions

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).

11.6.4 Exercises

  1. Extend the provided Note class with get and set functions for note dynamic and panning. (Hint: Use provided functions as models.) Your get functions should ensure data integrity (that is check for error values). After you define them, run tests to make sure they work as expected.
  2. The music library defines musical rests as notes with pitch equal to −2147483648.†††† Add a function isRest(), which reports if a note is a rest. Hint: You can use an if statement, or (more succinctly) use the following single line in the function body: return (self.pitch == −2147483648) Why does this work?
  3. Define a class, called Rest. This class should have the same instance variables and class functions as class Note. However, since rests only have duration, the Rest constructor should accept only one parameter, namely, duration. The other values should be set as follows: pitch = −2147483648, dynamic = 85, panning = 0.5.‡‡‡‡
  4. Define a class, called Phrase. Just like the one provided by the music library, this class should encapsulate a list of notes. To simplify this exercise, the constructor should accept no parameters (it creates an empty phrase). Also, a Phrase’s start time is always 0.0. The following class functions should be defined: addNote(note), getSize(), getNoteList(), getStartTime(). (See Appendix B for precise descriptions of these functions.)
  5. Extend the Phrase class to include the function getEndTime(). (Hint: Using a loop, iterate through all the notes in the internal list and accumulate (add) their durations. Return the result.)

11.7 Case Study: A Slider Control

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.§§§§

Figure 11.7

Image of The GUI display of a SliderControl

The GUI display of a SliderControl (here used to control a timer delay).

The SliderControl class has the following attributes:

  • title — the control surface (display) title,
  • updateFunction — the function to call when the slider position changes (this function should accept a single parameter, the new value of the slider),
  • minValue — the slider’s min limit,
  • maxValue — the slider’s max limit,
  • startValue — the slider’s starting value,
  • x — the x coordinate of the top-left corner of the control’s display, and
  • y — the y coordinate of the top-left corner of the control’s display.

11.7.1 Creating SliderControl Objects

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).

11.7.2 Defining the Class

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.

11.7.3 Exercises

  1. Create a SliderControl object that calls the following function to play single notes:
	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.

  1. Add one more SliderControl objects to one of your programs, to control variables of interest. Focus on programs that produce interactive music (for instance, using the AudioSample class). This may lead to impromptu programs for quick audio mixing or playing applications. SliderControl objects work only with integer ranges. Use a SliderControl object together with function mapValue() to update a program variable that contains a float value.
  2. Create a SliderFloatControl class that works with float values. To do so, separate the range provided by the user (minValue, maxValue, and startValue) from the range used by the slider (e.g., minSlider, maxSlider, startSlider), and use function mapValue() to provide mapping between the two ranges.
  3. Modify the SliderControl class to work with other GUI widgets of interest, such as Button and DropDownList objects. Explore what types of AudioSample applications you can quickly develop with an arsenal of such independent control surfaces.
  4. Consider combining GUI widgets to create a few common, yet generic control surfaces (such as a button plus a slider surface, or two buttons and a slider). Give these classes appropriate names and consider the type of information (parameters) needed for their constructor. Aim for simplicity and usability.

11.8 Animation

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).

11.8.1 Frame Rate

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

11.8.2 Case Study: A Revolving Musical Sphere

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).

Figure 11.8

Image of A revolving musical sphere

A revolving musical sphere (with speed slider control).

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.

11.8.2.1 Color Gradients

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).

11.8.3 Defining the Class

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:

  • radius — determines the radius of the sphere (in pixels),
  • density — how many points to distribute across the surface of the sphere (e.g., 200),
  • velocity — how many pixels to move each point by per animation frame (e.g., 0.01), and
  • frameRate — how many animation frames per second (e.g., 30).

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.

11.8.3.1 Spherical Coordinate System

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):

Figure 11.9

Image of The spherical coordinate system

The spherical coordinate system (math version). (Note: In the physics version, the meanings of φ and θ are swapped.)

  • the distance, r, of that point from the sphere’s center (also known as radial distance),
  • its polar angle, φ (also known as latitude), and
  • its azimuth angle, θ (also known as longitude).

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.

11.8.4 Exercises

  1. Modify the musical parameters to create a different effect. The original parameters create a happier, more cheerful musical effect (aided by the use of the pentatonic scale and the xylophone sound). Switch to the following:
	# 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).

  1. Explore different musical parameters to create different musical effects. What do you observe?
  2. Experiment with creating more than one musical sphere, each having different musical parameters. Explore creating a set of complementary musical spheres. Explore performance possibilities with adjusting their rotation speeds.
  3. Create SliderControl objects to control musical parameters in real time. Explore the performance possibilities opened by this modification.
  4. (Advanced) Modify the MusicalSphere code to create other rotating geometrical shapes.

11.9 Cymatics

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).

Figure 11.10

Image of Chladni plates, vintage engraving. Old engraved illustration of Chladni plates isolated on a white background

Chladni plates, vintage engraving. Old engraved illustration of Chladni plates isolated on a white background. (From Charton, É. and Cazeaux, E., eds. (1874), Magasin Pittoresque.)

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.

Figure 11.11

Images of Two snapshots of the boid animation

Two snapshots of the boid animation (200 boids, min separation = 47, flock threshold = 267, separation factor = 0.01, alignment factor = 0.01, cohesion factor = 0.15, friction = 1.1). (Note: These parameters will be explained soon.)

These boids follow three simple rules:

  1. Rule of Separation: Each boid keeps separate from other boids; it avoids being at the same place with another boid at the same time.
  2. Rule of Alignment: Each boid moves toward the average heading of its local flock.
  3. Rule of Cohesion (or Attraction): Each boid moves toward a certain point. This may be the center of the universe, the mouse pointer, or the center of a local flock.

An additional rule (left as an exercise) is the following:

  1. Rule of Avoidance: Each boid moves away from a certain point (e.g., an obstacle). This could be a particular point in the universe or the mouse pointer.

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:

  1. Divergence: The boids move away from each other and outside of the universe window (never to be seen again). This divergence resembles a slow explosion—particles moving away from each other.
  2. Convergence: The boids find a place of balance, which, given the initial conditions, may be a single collapsed point (resembling a black hole), or several collapsed points (resembling several black holes), or a static pattern (see Figure 11.12).
  3. Orbital Behavior: The boids oscillate in a quasi-periodic movement that resembles bees flying around a flower, or complex planetary systems revolving around one, or more suns (points of attraction).¶¶¶¶¶¶

11.9.1 Vectors and Python Complex Numbers

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.

Figure 11.12

Image of Boids have converged to a hexagon resembling the molecular structure of benzene, or water molecules in an ice crystal, among others

Boids have converged to a hexagon resembling the molecular structure of benzene, or water molecules in an ice crystal, among others (19 boids, min separation = 30, flock threshold = 100, separation factor = 0.13, alignment factor = 0.16, cohesion factor = 0.01, friction = 4.2).

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).

Figure 11.13

Image of A 2D vector corresponds to x and y displacement

A 2D vector corresponds to x and y displacement (per animation frame).

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.

11.9.2 Defining the Boid Universe

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.

11.9.3 Defining the Boids

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().

11.9.3.1 Boid Sensing

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,

  • Rule of alignment: Each boid moves toward the average heading of its local flock.

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

11.9.3.2 Boid Acting

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.

11.9.4 Creating the Simulation

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.

11.10 Exercises

  1. Experiment with the above code. Try the following sets of simulation parameters and explore the different boid behaviors they generate. Get a feeling about what each parameter contributes to the simulation. Make notes. Realize that you are dealing with a complex chaotic system (like the ones found in nature). Under the right conditions, a small change can have an immense effect. Some of the parameters are interfering/interacting with others. Derive your own combinations.
  2. Add a few SliderControl (or SliderFloatControl) objects to adjust the various boid parameters. Explore the different types of flocking behavior you can create. You are dealing with a chaotic system with various places of balance and behavior. See how many different behaviors you can create. Experiment.

    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

  3. Experiment with the number of boids and their sizes (radius). Notice how, under certain conditions, boids will derive a geometric shape that maximizes balance. This demonstrates how geometric shapes emerge in the universe. Euclidean geometry is then an abstraction of structures and properties that emerge in any universe (or system) given enough freely moving and interacting particles.
  4. Using a prime number (2, 3, 5, 7, 11) forces the boids to keep looking for a place of rest, balance—they never find it.
    1. Explore the different geometric shapes that emerge using 2, 3, 4, 5, 6, 7, 8, 9, and 10 boids. What do you see?
    2. At larger numbers, for example, 250, you see universe-like properties emerging. Some boids are forced to form local systems, being pushed closer than the rule of separation alone would dictate. They are “pushed” by the collective “mass” (or force) of the global system. A real universe (as in cosmos) emerges. Experiment with larger numbers of boids.
  5. Modify the code to make the boids be attracted to the center of the universe, but also avoid the mouse pointer. How can you accomplish this? Hint: Add an avoidance rule. This is very similar (actually, opposite) to the cohesion rule. What types of behaviors can you create now?
  6. Add a rule for boids to be attracted to the center of their local flock. To do so, average the coordinates of all other local flockmates and make that the center of attraction for a given boid. This will create even more natural flocking behaviors, in that different subsystems will emerge with their own local behavior (e.g., direction of movement). This can be very interesting. Experiment.
  7. What other rules can you think of? How about adding control of boids, either local (controlling a single boid) or global (controlling an attraction or avoidance point), using a MIDI instrument or OSC controller (e.g., a smartphone). How about having several boids be controlled by such instruments/controllers. How about using such a system to create a collaborative music performance? How can you incorporate/combine other techniques seen in this book? Brainstorm. Experiment. Create. Replace the boid visual representation (currently, a Circle object) with a Icon object (see Chapter 8 and Appendix C). Pick an interesting icon (a bird or something more abstract/creative). Make use of Icon’s functions for rotation (and scaling/resizing, if needed).¶¶¶¶¶¶¶ Convert each boid’s vector information (x and y displacement, per animation frame) to a direction, or orientation. This can be done easily with Python’s atan2(y, x) trigonometric function. This function returns the angle of a vector with this y displacement (or rise) and this x displacement (or run).******** Use the resultant angle to orient the boid’s visual representation, via Icon’s rotate() function. Notice that atan() returns an angle in radians (in terms of π units), whereas Icon’s rotate() expects the angle in degrees. (Use Python’s radians() function.) All the parts are there. Assemble them.
  8. Add a third dimension to the boid universe. Unfortunately, the Python complex data type works only with two dimensions. Replace it with three individual coordinates (x, y, z), as done in MusicalSphere. (Alternatively, you could create a Coord class, which encapsulates all three coordinates.) Adding 3D coordinates (x, y, z) follows the same approach as adding 2D coordinates. The same holds for multiplication (or division). Map the z coordinate (depth) to a color gradient (as in MusicalSphere).††††††††
  9. And, finally, music making! The various boid systems can drive music generation in amazing ways. This creative space is vast, so we will provide only a few seed ideas. This section invites you to apply everything you have learned so far about music generation/performance in this book (and beyond). Ask yourself what types of musical parameters would you like to drive/control through the position/motion of boids. Here are some ideas:
    1. Perhaps each boid plays a MIDI note, via Play.note(), where the boid’s x coordinate determines the pitch, and the y coordinate determines the volume.
    2. Perhaps each boid is connected with an AudioSample object, where the boid’s velocity controls the sample’s frequency. What are some other AudioSample control possibilities?
    3. Explore playing with timbre, using the AudioSample class to introduce looped recordings of different sounds and of different lengths.‡‡‡‡‡‡‡‡ Clearly, once you open this door, the possibilities are endless. Some great compositions wait to be discovered.
    4. Explore using mapScale() to derive musical outcomes that sound harmonious.
    5. Modify some of the boids to be controlled by input from MIDI instruments and/or OSC controllers (e.g., smartphones).§§§§§§§§ Simply create a MIDIBoid and/or OSCBoid class, which use slightly modified rules of behavior. They may use a single rule, which simply uses MIDI pitch (or some OSC parameter) to set boid’s new velocity (or position). You could still use some of the other rules (e.g., separation) to let the boid system constrain the musical (or OSC) input. This can help guide your musical creativity/performance in natural, unexpected ways.
    6. Brainstorm. Experiment. Create. Perform.

11.11 Summary

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.literate­programming.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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset