User’s Guide, Chapter 58: Understanding Sites and Contexts¶
Music21
contains a powerful context and hierarchy system that lets
users find analytical information in a single place that actually
requires looking at many pieces of information scattered throughout a
score. Take for instance, a fragment of code that we use a lot such as:
>>> n.beat
4
It’s great when it works, but then there are times when it doesn’t and it’s just frustrating. Avoiding those times is what this chapter is about. And to do so, we’ll need to start asking some “how” questions.
How does a note know what beat it is on? It might help to think about
when we read a printed score, how do we know what beat a note is on? We
have to look at the note, then look up the score to find the most recent
time signature, then find the note again and look at the measure it is
in, count everything preceeding it in the measure, and then calculate
the beat. At least three different musical objects need to be consulted:
the note itself, the surrounding measure, and the time signature that
provides an interpretation of how note durations translate to beats.
Music21
needs to do the same search, and it does all that just on a
little call to the .beat
property.
(In the early days of ``music21``, I did not know about the convention that properties should be fast and easily computable so that the user does not even realize it is something more complex than an attribute lookup. The property ``.beat`` is none of the above. If I were starting over, it would be a method, ``.beat()``, but it is too late to change now.)
To understand how this lookup works, we will need to understand better
how Sites
and Contexts
work. Advanced users and beginners alike
(and occasionally even the music21
developers) are frequently
confused by music21
’s context and hierarchy system. When it works,
it works great, it’s just magic. But, when it doesn’t, it appears to be
a random bug, and it is probably the most common type of bug mention on
the music21
GitHub tracker that gets “not a bug” as a response.
Magic is fickle that way.
Let’s start by looking at a simple example. We will create a measure and add a single E-flat to it:
from music21 import *
m = stream.Measure(number=1)
es = note.Note('E-4')
m.insert(2, es)
m.show('text')
{2.0} <music21.note.Note E->
At this point, there’s obviously a connection made between the Measure and the Note.
m[0]
<music21.note.Note E->
es.activeSite
<music21.stream.Measure 1 offset=0.0>
es.offset
2.0
m.elementOffset(es)
2.0
But we need to know, what is the nature of this connection? It has
changed several times as music21
has developed but has been stable
since at least v.4, and it looks to stay that way. The measure (or any
Stream
) contains an ordered list of the elements in it, and it also
contains a mapping of each element in it to its offset.
The element in a stream (such as a Note) does not however, have a direct
list of what stream or streams it is in. Instead elements have a
property .sites
that is itself a rather complex object called
Sites
that keeps track of this information for
it:
es.sites
<music21.sites.Sites at 0x10c806488>
Working with Sites¶
The Sites
object keeps track of how many and which streams an
element has been placed in:
es.sites.getSiteCount()
1
v = stream.Voice(id=1)
v.append(es)
es.sites.getSiteCount()
2
If we need to figure out a particular attribute based on context, the
Sites
object comes in very handy. For instance, the
.measureNumber
on a Note or other element, finds a container that is
a Measure and has a .number
attribute:
es.measureNumber
1
We can do the same sort of thing, by calling the Sites
object
directly with the getAttrByName
method:
es.sites.getAttrByName('number')
1
Or we can just get a list of sites that match a certain class:
es.sites.getSitesByClass(stream.Voice)
[<music21.stream.Voice 1>]
Or with a string:
es.sites.getSitesByClass('Measure')
[<music21.stream.Measure 1 offset=0.0>]
Notice that what is returned is a list, because an element can appear in
multiple sites, even multiple sites of the same class, so long as those
sites don’t belong to the same hierarchy (that is, those streams are not
both in the same stream or have a common stream somewhere in their
Sites). Let’s put the note in another Measure
object:
m10 = stream.Measure(number=10)
m10.insert(20, es)
es.sites.getSitesByClass('Measure')
[<music21.stream.Measure 1 offset=0.0>, <music21.stream.Measure 10 offset=0.0>]
Users can iterate through all sites in a Stream using .yieldSites()
:
for site in es.sites.yieldSites():
print(site)
None
<music21.stream.Measure 1 offset=0.0>
<music21.stream.Voice 1>
<music21.stream.Measure 10 offset=0.0>
Note two things: (1) each element has a special site called “None” that stores information about the element when it is not in any Stream (it used to be used much more, but is not used as much anymore, and is not counted in the number of sites an element is in), and (2) the sites are yielded from earliest added to latest. We can reverse it and eliminate None, with a few parameters:
for site in es.sites.yieldSites(excludeNone=True, sortByCreationTime=True):
print(site)
<music21.stream.Measure 10 offset=0.0>
<music21.stream.Voice 1>
<music21.stream.Measure 1 offset=0.0>
So effectively a Note
or other Music21Object
can always get its
position in the streams that it is in via .sites
:
for site in es.sites.yieldSites(excludeNone=True):
print(site.elementOffset(es), site)
2.0 <music21.stream.Measure 1 offset=0.0>
0.0 <music21.stream.Voice 1>
20.0 <music21.stream.Measure 10 offset=0.0>
The Sites
object keeps track of the order of insertion through an
attribute called .siteDict
es.sites.siteDict
OrderedDict([(None, <music21.sites.SiteRef Global None Index>),
(4504830192,
<music21.sites.SiteRef 0/0 to <music21.stream.Measure 1 offset=0.0>>),
(4518082656,
<music21.sites.SiteRef 1/1 to <music21.stream.Voice 1>>),
(4518134560,
<music21.sites.SiteRef 2/2 to <music21.stream.Measure 10 offset=0.0>>)])
Each element has as its index the memory location of the site and a
lightweight wrapper object around the site (i.e., stream) called a
SiteRef
.
(all sites except “None” are currently “Stream” objects – it was our intention at the beginning to have other types of site contexts, such as interpretative contexts (“baroque”, “meantone tuning”) and we might still someday add those, but for now, a site is a Stream)
Let’s look at the last one:
lastSiteRef = list(es.sites.siteDict.items())[-1][1]
lastSiteRef
<music21.sites.SiteRef 2/2 to <music21.stream.Measure 10 offset=0.0>>
lastSiteRef.siteIndex
2
lastSiteRef.siteIndex
2
lastSiteRef.globalSiteIndex
2
This item allows music21
to find sites by class without needing to
unwrap the site
lastSiteRef.classString
'Measure'
lastSiteRef.site
<music21.stream.Measure 10 offset=0.0>
From weakness I get strength of memory¶
From what has been shown so far, it appears that effectively the relationship between a stream and its containing element is a mirror or two-way: streams know what notes they contain and notes know what streams they are contained in. But it is a bit more complicated and it comes from the type of reference each holds to each other.
Streams hold elements with a standard or “strong” reference. As long as the Stream exists in the computer’s memory, all notes contained in it will also continue to exist. You never need to worry about streams losing notes.
The .sites
object, does not, however, contain strong references to
Streams. Instead it contains what are called “weak references” to
streams. A weak reference allows notes and their Sites
object to get
access to the stream, as long as it is still in use somewhere else,
but once the stream is no longer in use it is allowed to disappear
anytime.
As a demonstration, let’s delete that pesky voice:
del v
Now whenever Python determines that it needs some extra space, the Note
object will no longer have reference to the voice in its sites. Note
that, this might not happen immediately. The removal of dead weak
references, part of Python’s garbage collection, takes place at odd
times, dependent on the amount of memory currently used and many other
factors. So one cannot predict whether the Voice
object would still
be in the note’s sites or not by the time you finish reading this
paragraph. Music21
uses weak references in a number of other
situations, such as
derivation objects, though we are
reducing the number of places where they are used as the version of
Python supported by music21
gets a smarter and smarter garbage
collector.
You might be thinking that it’s been years since the last time you
called del
on a variable, so this doesn’t really apply to you. But
look at this code, which represents pretty typical music21 usage:
p = stream.Part()
m = stream.Measure()
p.insert(0, m)
m.insert(1, note.Note('D'))
firstNote = p.recurse().notes.first()
m.insert(0, note.Note('C'))
newFirstNote = p.recurse().notes.first()
(firstNote, newFirstNote)
(<music21.note.Note D>, <music21.note.Note C>)
for n in p.flatten():
if n is firstNote:
print('Yup, there is a D in the part')
Yup, there is a D in the part
Did you see where we created an extra stream only to immediately discard
it? The expression p.flatten()
creates a new stream that exists just
for long enough to get the first note from it. (We actually store it in
a cache on p
so that it’s faster the next time we need it, but once
we add another note to m
the cache is invalidated). The creation of
another stream is one reason to generally prefer .recurse()
over
.flatten()
.
Prior to music21
v.3, the .notes
call would have created yet
another Stream, but we’ve optimized this out.
The reason why music21
uses weak references instead of normal
(strong) references to sites is to save some memory because how
frequently objects, such as notes and streams, are copied. When you run
certain analytical or manipulation routines such as
toSoundingPitch()
or stripTies()
or even common operations such
as .flatten()
and show()
, copies of streams need to be made,
often only to be discarded in a single line of code. If every one of
those streams, with all of their contents, persisted forever just
because a single note from that stream stayed in memory, then the memory
usage of music21
would be much higher.
Also note that the Sites
object cleans up “dead” sites from time to
time, and certain context-dependent calls, such as .next(note.Rest)
or .getContextByClass('Measure')
need to search every living site.
Over time, these calls would get slower and slower if otherwise
long-forgotten streams created as byproducts of .show()
or
.flatten()
stuck around.
Here’s an example adapted from actual code that caused a problem. The user was trying to figure out the beat for each note that was not the continuation of a tie, and he wrote:
bach = corpus.parse('bach/bwv66.6')
allNotes = list(bach.stripTies().recurse().notes)
firstNote = allNotes[0]
Problems quickly arose though when he tried to figure out the note’s
beat via firstNote.beat
– it said that it was on beat 1, even though
this piece in 4/4 began with a one-beat pickup, and should be on beat 4.
What happened? Again, it’s a problem with confusions from disappearing
streams and sites. The .stripTies()
method creates a new score
hierarchy, where each note in the score hierarchy is a copy derived from
the previous one:
firstNoteOriginal = bach.recurse().notes[0]
firstNoteStripped = bach.stripTies().recurse().notes[0]
(firstNoteOriginal, firstNoteStripped)
(<music21.note.Note C#>, <music21.note.Note C#>)
firstNoteOriginal is firstNoteStripped
False
firstNoteStripped.derivation
<Derivation of <music21.note.Note C#> from <music21.note.Note C#> via 'stripTies'>
firstNoteStripped.derivation.origin is firstNoteOriginal
True
So the Note object obtained from .stripTies()
is not to be found in
the original bach
score:
bach.containerInHierarchy(firstNoteStripped) is None
True
bach.containerInHierarchy(firstNoteOriginal)
<music21.stream.Measure 0 offset=0.0>
in fact, firstNoteStripped’s entire containerHierarchy is generally
empty if garbage collection has run. Why? Because firstNoteStripped only
directly belongs to the hierarchy created by the unnamed and unsaved
stream created by stripTies()
. So how to solve this? In code that
needs access to the hierarchy, make sure that it is preserved by saving
it to a variable. Here we will break up the code calling stripTies()
and save it as a variable, st_bach
. Not only does this solve our
problems, but it makes the code more readable:
bach = corpus.parse('bach/bwv66.6')
st_bach = bach.stripTies()
allNotes = list(st_bach.recurse().notes)
firstNote = allNotes[0]
firstNote.beat
4.0
Doing this also fixes another thing that looked like a bug, but is
expected behavior – that getContextByClass('Measure')
was needing to
follow the derivation chain to find a measure that firstNote
was not
in. Here it works as expected:
firstNote.getContextByClass('Measure').elementOffset(firstNote)
0.0
This was a pretty dense chapter, I know, and it barely scratches the
surface of the complexities of the Context system, so we’ll move to
something lighter if even more distant, and look at Medieval and
Renaissance extensions in music21
– as soon as that chapter is
completed. Until then, jump ahead to
Chapter 61: TimespanTrees and Verticalities.