User’s Guide, Chapter 6: Streams (II): Hierarchies, Recursion, and Flattening

We ended Chapter 4 (Streams (I).) with a Stream that was contained within another Stream object. Let’s recreate that class:

from music21 import *

note1 = note.Note("C4")
note1.duration.type = 'half'
note2 = note.Note("F#4")
note3 = note.Note("B-2")

stream1 = stream.Stream()
stream1.id = 'some notes'
stream1.append(note1)
stream1.append(note2)
stream1.append(note3)

biggerStream = stream.Stream()
note2 = note.Note("D#5")
biggerStream.insert(0, note2)
biggerStream.append(stream1)

The only way to find out what was in the contained Stream that we demonstrated so far was the show() method using the ('text') argument.

biggerStream.show('text')
 {0.0} <music21.note.Note D#>
 {1.0} <music21.stream.Stream some notes>
     {0.0} <music21.note.Note C>
     {2.0} <music21.note.Note F#>
     {3.0} <music21.note.Note B->

As Chapter 4 noted, there’s a way to reach the inner notes such as F# via the biggerStream[1][1] format, but there’s a better way to do that in music21, and for that we need to learn about subclasses of Streams and subclasses in general. (Skip this if you already know about such things from other programming experience)

Classes and Subclasses

An object, such as a note or pitch, is basically a collection of information along with some actions that can be performed on that information. A class is something that can make new objects of a certain type (sometimes this is called a factory). We’ve seen classes such as the note.Note class, where the lowercase note is the “module” that the class Note lives in:

note
 <module 'music21.note' from '/Users/cuthbert/music21/note.py'>
print(note.Note)
 <class 'music21.note.Note'>

We create an object from a class by using the class name with () after it:

n = note.Note()
n
 <music21.note.Note C>

As we’ve seen, we can sometimes put additional information into the (), such as a pitch name in the case of a Note:

d = note.Note('D#5')
d
 <music21.note.Note D#>

The variable d is now a Note object created from the Note class. It’s all a bit confusing, I know. But we’ll get to the point in a second. If you want to find out more about what a Note object can do, the best thing is to read the music21 instruction manual. :-) But for any class in Python, you can use the function help(Class) to find out what it can do:

help(note.Note)
Help on class Note in module music21.note:

class Note(NotRest)
|  One of the most important music21 classes, a Note
|  stores a single note (that is, not a rest or an unpitched element)
|  that can be represented by one or more notational units -- so
|  for instance a C quarter-note and a D# eighth-tied-to-32nd are both
|  a single Note object.
|
|  *** ............ ***
|
|  Method resolution order:
|      Note
|      NotRest
|      GeneralNote
|      music21.base.Music21Object
|      builtins.object

Notice towards the very top there’s the line class Note(NotRest). This says that the Note class is a “subclass” of a class called NotRest which contains all the information for note-like things such as Note, Unpitched percussion, and Chord that have stems, beams, etc. and are, well, not rests. (Chris Ariza and I spent over an hour trying to come up with a better name for these things, but in the end we couldn’t come up with anything better than NotRest, so it’s stuck).

What does it mean for Note to be a subclass of NotRest? It means that everything that NotRest can do, Note can do, and more. For instance, NotRest has a .beams property, so so does Note:

nr = note.NotRest()
n = note.Note()
print(nr.beams, n.beams)
 <music21.beam.Beams> <music21.beam.Beams>

But Rest is not a subclass of NotRest for obvious reasons. So a rest doesn’t know anything about beams:

r = note.Rest()
r.beams
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Input In [9], in <cell line: 2>()
      1 r = note.Rest()
----> 2 r.beams


AttributeError: 'Rest' object has no attribute 'beams'

But Note has properties that NotRest does not, such as .pitch:

print(nr.pitch)

So classes and subclasses are a great way of making sure that things that are mostly similar have many of the same properties, but that they can have their own distinct information (attributes) and actions (methods). Just FYI, here’s how we create a subclass. We can create a Class called Japan and then a subclass called Okinawa (my ancestral home) which has an additional attribute.

class Japan:
    food = "sushi"
    drink = "sake"

class Okinawa(Japan):
    evenBetterFood = "spam potstickers"

The (Japan) in the class definition of Okinawa means that it inherits everything that Japan has and more:

o = Okinawa()
print(o.food, o.drink, o.evenBetterFood)
 sushi sake spam potstickers

But the joy of spam gyoza has not come to the mainland yet:

j = Japan()
print(j.evenBetterFood)
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Input In [12], in <cell line: 2>()
      1 j = Japan()
----> 2 print(j.evenBetterFood)


AttributeError: 'Japan' object has no attribute 'evenBetterFood'

So this is how subclasses work in a nutshell. The first subclasses we will be working with are the three fundamental subclasses of Stream: Score, Part, and Measure.

Accessing Scores, Parts, Measures, and Notes

Streams provide a way to structure and position music21 objects both hierarchically and temporally. A Stream, or a Stream subclass such as Measure, can be placed within another Stream.

A common arrangement of nested Streams is a Score Stream containing one or more Part Streams, each Part Stream in turn containing one or more Measure Streams.

Such an arrangement of Stream objects is the common way musical scores are represented in music21. For example, importing a four-part chorale by J. S. Bach will provide a Score object with four Part Streams, each Part containing multiple Measure objects. Music21 comes with a music21.corpus module that provides access to a large collection of scores, including all the Bach chorales. We can parse the score from the corpus with the parse() function, which we will discuss more in a bit.

sBach = corpus.parse('bach/bwv57.8')

We can access and examine elements at each level of this Score by using standard Python syntax for lists within lists. Thus, we can see the length of each component: first the Score which has six elements: a Metadata object, a StaffGroup object, and four Part objects. Then we find the length of first Part at index three which indicates 14 objects (13 of them are measures). Then within that part we find an object (a Measure) at index 1. All of these subprograms can be accessed from looking within the same score object sBach.

len(sBach)
 9
len(sBach[3])
 14
len(sBach[3][1])
 7

But how did we know that index [3] would be a Part and index [1][1] would be a measure? As writers of the tutorial, we know this piece well enough to know that. But as we noted above, more than just Measures might be stored in a Part object (such as Instrument objects), and more than just Note and Rest objects might be stored in a Measure (such as TimeSignature and KeySignature objects). Therefore, it’s much safer to filter Stream and Stream subclasses by the class we seek.

To repeat the count and select specific classes, we can use the getElementsByClass() method that we discussed in Chapter 4.

Notice how the counts deviate from the examples above.

len(sBach.getElementsByClass(stream.Part))
 4
len(sBach.getElementsByClass(stream.Part)[0].getElementsByClass(stream.Measure))
 13
len(sBach.getElementsByClass(stream.Part)[0].getElementsByClass(
        stream.Measure)[1].getElementsByClass(note.Note))
 3

Recall from Chapter 4 that the getElementsByClass() method can also take a string representation of the last section of the class name. Thus we could’ve rewritten the first call above as:

len(sBach.getElementsByClass('Part'))
 4

This way of doing things is a bit faster to code, but a little less safe. Suppose, for instance there were objects of type stream.Measure and tape.Measure; the latter way of writing the code would get both of them. (But this ambiguity is rare enough that it’s safe enough to use the strings in most code.)

When we introduced .getElementsByClass() we also introduced the convenience properties .notes and .notesAndRests. There is a convenience property for getting parts out as well:

len(sBach.parts)
 4

You might think that there should be a convenience property .measures to get all the measures. But the problem with that is that measure numbers would be quite different from index numbers. For instance, most pieces (that don’t have pickup measures) begin with measure 1, not zero. Sometimes there are measure discontinuities within a piece (e.g., some people number first and second endings with the same measure number). For that reason, gathering Measures is best accomplished not with getElementsByClass(stream.Measure) but instead with either the measures() method (returning a Stream of Parts or Measures) or the measure() method (returning a single Measure).

These are methods, not properties, so they use the () call. Let’s look at how we might use them:

alto = sBach.parts[1]  # parts count from zero, so soprano is 0 and alto is 1
excerpt = alto.measures(1, 4)
excerpt.show()
../_images/usersGuide_06_stream2_47_0.png
measure2 = alto.measure(2)  # measure not measure_s_
measure2.show()
../_images/usersGuide_06_stream2_48_0.png

What is great about .measure() and .measures() is that they can work on a whole score and not just a single part. Sometimes computational musicologists and programmers call a collection of measures across parts a “measureStack”. So let’s get the measure stack consisting of measure numbers 2 and 3 across all parts:

measureStack = sBach.measures(2, 3)
measureStack.show()
../_images/usersGuide_06_stream2_50_0.png

Recursion in Streams

Streams are hierarchical objects where the contained elements can themselves be Streams. In order to get at each lower layer of the stream, a generator method on every stream called recurse() will visit every element in the stream, starting from the beginning, and if any of the subelements are also Streams, they will visit every element in that Stream.

Let’s create a simpler Stream to visualize what .recurse() does.

s = stream.Score(id='mainScore')
p0 = stream.Part(id='part0')
p1 = stream.Part(id='part1')

m01 = stream.Measure(number=1)
m01.append(note.Note('C', type="whole"))
m02 = stream.Measure(number=2)
m02.append(note.Note('D', type="whole"))
p0.append([m01, m02])

m11 = stream.Measure(number=1)
m11.append(note.Note('E', type="whole"))
m12 = stream.Measure(number=2)
m12.append(note.Note('F', type="whole"))
p1.append([m11, m12])

s.insert(0, p0)
s.insert(0, p1)
s.show('text')
 {0.0} <music21.stream.Part part0>
     {0.0} <music21.stream.Measure 1 offset=0.0>
         {0.0} <music21.note.Note C>
     {4.0} <music21.stream.Measure 2 offset=4.0>
         {0.0} <music21.note.Note D>
 {0.0} <music21.stream.Part part1>
     {0.0} <music21.stream.Measure 1 offset=0.0>
         {0.0} <music21.note.Note E>
     {4.0} <music21.stream.Measure 2 offset=4.0>
         {0.0} <music21.note.Note F>
s.show()
../_images/usersGuide_06_stream2_54_0.png

Calling .recurse() on its own isn’t very useful.

recurseScore = s.recurse()
recurseScore
 <music21.stream.iterator.RecursiveIterator for Score:mainScore @:0>

Where it becomes useful is in a for loop:

for el in s.recurse():
    print(el.offset, el, el.activeSite)
 0.0 <music21.stream.Part part0> <music21.stream.Score mainScore>
 0.0 <music21.stream.Measure 1 offset=0.0> <music21.stream.Part part0>
 0.0 <music21.note.Note C> <music21.stream.Measure 1 offset=0.0>
 4.0 <music21.stream.Measure 2 offset=4.0> <music21.stream.Part part0>
 0.0 <music21.note.Note D> <music21.stream.Measure 2 offset=4.0>
 0.0 <music21.stream.Part part1> <music21.stream.Score mainScore>
 0.0 <music21.stream.Measure 1 offset=0.0> <music21.stream.Part part1>
 0.0 <music21.note.Note E> <music21.stream.Measure 1 offset=0.0>
 4.0 <music21.stream.Measure 2 offset=4.0> <music21.stream.Part part1>
 0.0 <music21.note.Note F> <music21.stream.Measure 2 offset=4.0>

This example also introduces the concept of .activeSite, which for now can be thought of as the Stream that the element lives in; though we’ll find that Notes and other elements can be in multiple Streams simultaneously, and this is just the one that they are most recently associated with.

There are a lot of things that we can do with .recurse(), but let’s just introduce one more thing for now. Most “filtering” mechanisms, such as .notes can also be applied between the () of recurse() and the : at the end:

for el in s.recurse().notes:
    print(el.offset, el, el.activeSite)
 0.0 <music21.note.Note C> <music21.stream.Measure 1 offset=0.0>
 0.0 <music21.note.Note D> <music21.stream.Measure 2 offset=4.0>
 0.0 <music21.note.Note E> <music21.stream.Measure 1 offset=0.0>
 0.0 <music21.note.Note F> <music21.stream.Measure 2 offset=4.0>

Note

.recurse() is a generator in music21. Thus, it can only be used in for loops and other things that iterate over each member of a list. To treat the results of .recurse() as a list, you need to wrap it with a list() call like so:

>>> listRecurse = list(sBach.recurse())

In general, .recurse() is the best way to work through all the elements of a Stream, but there is another way that can be handy in some situations, and that is called .flatten().

Flattening a Stream

While nested Streams offer expressive flexibility, it is often useful to be able to flatten all Stream and Stream subclasses into a single Stream containing only the elements that are not Stream subclasses. The flatten() property provides immediate access to such a flat representation of a Stream. For example, doing a similar count of components, such as that show above, we see that we cannot get to all of the Note objects of a complete Score until we flatten its Part and Measure objects by accessing the flat attribute. Note that for historical reasons, .flat is a property, so you do not use () around it.

Let’s look at what .flatten() does to the example score we created.

for el in s.flatten():
    print(el.offset, el, el.activeSite)
 0.0 <music21.note.Note C> <music21.stream.Score mainScore_flat>
 0.0 <music21.note.Note E> <music21.stream.Score mainScore_flat>
 4.0 <music21.note.Note D> <music21.stream.Score mainScore_flat>
 4.0 <music21.note.Note F> <music21.stream.Score mainScore_flat>

A new, temporary Stream with id of “mainScore_flat” has been created, and all of the Note objects are in there. We didn’t filter out non-Notes: .flat automatically removes all Stream objects and in this case there’s nothing else but Notes in there.

All the Note objects are now in the new temporary mainScore_flat object, and their offsets are no longer all 0.0, but are instead measured from the start of the score being flattened. So the whole notes in measure 2 are given offset 4.0

Compare what .flatten() lets you do when looking at a larger score.

There are no Notes in the sBach stream…

len(sBach.getElementsByClass(note.Note))
 0

…they are all inside Measures inside Parts inside the stream. (in a more complex score, they may be in Voices inside Measures inside Parts, etc.). But they are all inside the flat version of the Stream:

len(sBach.flatten().getElementsByClass(note.Note))
 150

Element offsets are always relative to the Stream that contains them. For example, a Measure, when placed in a Stream, might have an offset of 16. This offset describes the position of the Measure in the Stream. Components of this Measure, such as Notes, have offset values relative only to their container, the Measure. The first Note of this Measure, then, has an offset of 0. In the following example we find that the offset of measure eight (using the getOffsetBySite() method) is 21; the offset of the second Note in this Measure (index 1), however, is 1.

m = sBach.parts[0].getElementsByClass('Measure')[7]
m.getOffsetBySite(sBach.parts[0])
 21.0
n = sBach.parts[0].measure(8).notes[1]
n
 <music21.note.Note E->
n.getOffsetBySite(m)
 1.0

Flattening a structure of nested Streams will set new, shifted offsets for each of the elements on the Stream, reflecting their appropriate position in the context of the Stream from which the flat property was accessed. For example, if a flat version of the first part of the Bach chorale is obtained, the note defined above has the appropriate offset of 22 (the Measure offset of 21 plus the Note offset within this Measure of 1).

pFlat = sBach.parts[0].flatten()
indexN = pFlat.index(n)
pFlat[indexN]
 <music21.note.Note E->
pFlat[indexN].offset
 22.0

As an aside, it is important to recognize that the offset of the Note has not been edited; instead, a Note, as all Music21Objects, can store multiple pairs of sites and offsets. Music21Objects retain an offset relative to all Stream or Stream subclasses they are contained within, even if just in passing.

(Note that if you are on a version of music21 before v.7, instead of .flatten() you would write .flat without the parentheses)

There’s still a lot more to learn about Streams, but we can do that later. For now, let’s move on to Chapter 7: Chords.