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