Previous topic

User’s Guide, Chapter 5: Lists of Lists, Functions, and Recursion

Next topic

User’s Guide, Chapter 7: Chords

Table Of Contents

Table Of Contents

This Page

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

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.

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 five elements, a Metadata object and four parts. Then we find the length of first Part at index one which indicates 19 objects (18 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)
 6
len(sBach[1])
 19
len(sBach[1][1])
 6

But how did we know that index [1] 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). We 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.

Notice how the counts deviate from the examples above.

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

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

There are some convenience properties you should know about. Calling .parts is the same as .getElementsByClass(stream.Part) and calling .notes is the same as .getElementsByClass([note.Note, note.Chord]). Notice that the last example also shows that you can give more than one class to getElementsByClass by passing in a list of classes. Note also that when using .parts or .notes, you do not write the () after the name. Also be aware that .notes does not include rests. For that, we have a method called .notesAndRests.

The index position of a Measure is often not the same as the Measure number. 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). What is great about these methods is that they can work on a whole score and not just a single part.

Recursion in Streams

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 :attr:~music21.stream.Stream.flat 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.

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

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 B->
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].flat
indexN = pFlat.index(n)
pFlat[indexN]
 <music21.note.Note B->
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.