User’s Guide, Chapter 12: Getting Back to Basics: The Music21Object

Almost everything that we’ve been working with so far, Note objects, Chord objects, Stream objects, etc., are subclasses of a object that, for lack of a better name, is called Music21Object.

A Music21Object is something that can go in a Stream, knows where it is in a Stream, and has a Duration at .duration.

Of course, not every object in Python is a Music21Object, but a little surprisingly, not every object in music21 is a “Music21Object”. For instance, the Pitch object is not. If you try to put one in a Stream, you’ll get an error:

from music21 import *

p = pitch.Pitch("A-2")
s = stream.Stream()
s.insert(0, p)
---------------------------------------------------------------------------

StreamException                           Traceback (most recent call last)

/var/folders/mk/qf43gd_s5f30rzzbt7l7l01h0000gn/T/ipykernel_7779/2384846781.py in <module>
      3 p = pitch.Pitch("A-2")
      4 s = stream.Stream()
----> 5 s.insert(0, p)


~/git/music21base/music21/stream/base.py in insert(self, offsetOrItemOrList, itemOrNone, ignoreSort, setActiveSite)
   2092
   2093         # checks if element is self, among other checks
-> 2094         self.coreGuardBeforeAddElement(element)
   2095         # main insert procedure here
   2096


~/git/music21base/music21/stream/core.py in coreGuardBeforeAddElement(self, element, checkRedundancy)
    419                     'cannot insert StreamIterator into a Stream\n'
    420                     "Iterate over it instead (User's Guide chs. 6 and 26)")
--> 421             raise StreamException(
    422                 f'The object you tried to add to the Stream, {element!r}, '
    423                 + 'is not a Music21Object.  Use an ElementWrapper object '


StreamException: The object you tried to add to the Stream, <music21.pitch.Pitch A-2>, is not a Music21Object.  Use an ElementWrapper object if this is what you intend.

Durations are also not Music21Objects:

d = duration.Duration('half')
s.insert(0, d)
---------------------------------------------------------------------------

StreamException                           Traceback (most recent call last)

/var/folders/mk/qf43gd_s5f30rzzbt7l7l01h0000gn/T/ipykernel_7779/777471452.py in <module>
      1 d = duration.Duration('half')
----> 2 s.insert(0, d)


~/git/music21base/music21/stream/base.py in insert(self, offsetOrItemOrList, itemOrNone, ignoreSort, setActiveSite)
   2092
   2093         # checks if element is self, among other checks
-> 2094         self.coreGuardBeforeAddElement(element)
   2095         # main insert procedure here
   2096


~/git/music21base/music21/stream/core.py in coreGuardBeforeAddElement(self, element, checkRedundancy)
    419                     'cannot insert StreamIterator into a Stream\n'
    420                     "Iterate over it instead (User's Guide chs. 6 and 26)")
--> 421             raise StreamException(
    422                 f'The object you tried to add to the Stream, {element!r}, '
    423                 + 'is not a Music21Object.  Use an ElementWrapper object '


StreamException: The object you tried to add to the Stream, <music21.duration.Duration 2.0>, is not a Music21Object.  Use an ElementWrapper object if this is what you intend.

Why don’t we just make everything a Music21Object? There’s an overhead in making a Music21Object, so if we did that, the system would probably run about 10x slower than it does. But there’s no reason to put a Pitch or a Duration in a Stream, when a Note is basically a Pitch plus a Duration. This works much better:

n = note.Note('A-2', type='half')
s.insert(0, n)
s.show('text')
 {0.0} <music21.note.Note A->

How can we tell that a Note is a Music21Object? Well we can read the docs (Note) where it says:

Note bases:

* NotRest
* GeneralNote
* Music21Object

Or we can use the isinstance(obj, class) operator on a given note. The class we are looking for is base.Music21Object. We still have our A♭ as n, so we can do:

isinstance(n, base.Music21Object)
 True

Note that we need to have a Note object first, we can’t test the class itself:

isinstance(note.Note, base.Music21Object)
 False

There’s one other way that you can tell if an object is a Music21Object, that’s to check whether Music21Object appears in the object’s .classes:

'Music21Object' in n.classes
 True

But that’s a bit of a cop-out. Things that are not Music21Objects don’t generally have a .classes property, so that won’t work:

import datetime
dt = datetime.datetime(2015, 9, 27)
dt
 datetime.datetime(2015, 9, 27, 0, 0)
'Music21Object' in dt.classes
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-8-f578ffe0ec17> in <module>
----> 1 'Music21Object' in dt.classes


AttributeError: 'datetime.datetime' object has no attribute 'classes'

But it’s a useful and easy way of checking to see if something you know is a Music21Object is a specific class:

'Chord' in n.classes
 False

In fact, it’s useful enough that we’ve put it in a few objects in music21 that aren’t Music21Objects

'Duration' in d.classes
 True

The class of an object cannot (well, should not) change after it’s been created. Thus it can be thought of as totally stable. Streams have many ways of filtering out Music21Objects (a.k.a. “elements”) according to class. The easiest way is with .getElementsByClass:

s = stream.Stream()
s.append(clef.TrebleClef())
s.append(meter.TimeSignature('3/4'))
s.append(note.Note("A"))
s.append(note.Rest())
s.append(note.Note("B"))

for element in s.getElementsByClass('Note'):
    print(element)
 <music21.note.Note A>
 <music21.note.Note B>

If you wanted to get the Notes and the Rest, you could figure out what their common ancestor class is and filter on that:

note.Rest().classes
 ('Rest', 'GeneralNote', 'Music21Object', 'ProtoM21Object', 'object')
note.Note().classes
 ('Note', 'NotRest', 'GeneralNote', 'Music21Object', 'ProtoM21Object', 'object')

aha! The common ancestor class is GeneralNote so we will filter on this:

for element in s.getElementsByClass('GeneralNote'):
    print(element)
 <music21.note.Note A>
 <music21.note.Rest quarter>
 <music21.note.Note B>

Or you could give a list of relevant classes:

for element in s.getElementsByClass(['Note', 'Rest']):
    print(element)
 <music21.note.Note A>
 <music21.note.Rest quarter>
 <music21.note.Note B>

For those who are familiar with HTML/Javascript/DOM programming, a Python Class is most similar to a DOM Tag (like <img>). So DOM methods such as .getElementsByTagName are similar to music21’s .getElementsByClass.

It’s worth pointing out that there’s also a .classSet (v.2.1+) on every Music21Object that gives the string name of all base classes, the fully-qualified string name for every class, as well as the class object. Since it will return True for basically every form that someone might pass in a class selector, it’s used a lot internally for safety. It returns these objects as a frozenset(), meaning it will return in any order and cannot be manipulated:

r = note.Rest()
r.classSet
 frozenset({music21.base.Music21Object,
            music21.note.GeneralNote,
            music21.note.Rest,
            music21.prebase.ProtoM21Object,
            object,
            'GeneralNote',
            'Music21Object',
            'ProtoM21Object',
            'Rest',
            'builtins.object',
            'music21.base.Music21Object',
            'music21.note.GeneralNote',
            'music21.note.Rest',
            'music21.prebase.ProtoM21Object',
            'object'})
('Rest' in r.classSet, 'music21.note.GeneralNote' in r.classSet, base.Music21Object in r.classSet)
 (True, True, True)

Attributes and Properties

Once you know that something is a music21Object you can exploit the attributes of the object for musical purposes.

id

Each music21Object has a (should be) unique id stored in the .id attribute:

n = note.Note("C#4")
n.id
 4540923128

By default, this .id is the same as the location of the object in memory, which the built-in Python function id() returns:

id(n)
 4540923128

But we can set it manually so that the object is easier to find later:

n.id = 'first_note'
n.id
 'first_note'

We advise .id not to include spaces or special characters, and may enforce it in the future.

This .id is especially useful for Stream objects because it will be displayed in the representation of the Stream and, if there’s no other metadata, can be used as the name of the part:

s = stream.Stream()
s.id = 'empty_stream'
s
 <music21.stream.Stream empty_stream>

Parts can be retrieved from the .parts attribute of a score by id.

bach = corpus.parse('bwv66.6')
sopr = bach.parts['soprano']
sopr
 <music21.stream.Part Soprano>
sopr.id
 'Soprano'

There are some properties such as .getElementById() that retrieve objects by .id:

s.append(n)
x = s.getElementById('first_note')
print(x, x.id)
 <music21.note.Note C#> first_note

If you know HTML/Javascript/DOM programming, the similarity between music21’s .getElementById() and HTML’s .getElementById() is intentional.

Groups

A group is a collection of labels for an object. Think of Groups as being like .id with two differences: (1) each Music21Object can have zero, one, or multiple Groups – but it has exactly one .id and (2) a single group label can belong to multiple Music21Objects.

n.groups
 []

Groups are wrappers around lists that enforce the restriction that the label must be a string. Since they’re otherwise just lists, you can add a group to any object just by appending a string to the group:

n.groups.append('black_key')
n.groups.append('sharped')
n.groups
 ['black_key', 'sharped']

We advise groups not to have spaces in them and will be enforcing this in v.3.

Now we can search through Streams that n is in to find it by searching for the group 'sharped' (or 'black key'). We use Stream.getElementsByGroup(). Note that it is plural Elements while the previous call was getElementById singular. That’s because there should be only one object with each id but there could be many with the same group:

for x in s.getElementsByGroup('sharped'):
    print(x, x.id)
 <music21.note.Note C#> first_note

Groups are the equivalent of the HTML/Javascript/DOM “class”. (Since class means something else in Python, we’ve changed the term). Eventually, Groups will be able to be used in styling objects automatically. For now we can just do it by hand:

for x in s.getElementsByGroup('black_key'):
    x.notehead = 'circle-x'

s.show()
../_images/usersGuide_12_music21object_53_0.png

ActiveSite

A Music21Object that is inside one or more Streams should be able to get its most recently referenced stream via its .activeSite attribute. We’ve put n in s, which is called (now incorrectly) 'empty stream', so n’s .activeSite should be s.

n.activeSite
 <music21.stream.Stream empty_stream>

The activeSite may change over time; obviously if the note is put in another Stream then that Stream will become the activeSite. Let’s put the note in a new stream, four quarter notes from the start:

t = stream.Stream()
t.id = 'new_stream'
t.insert(4.0, n)

n.activeSite
 <music21.stream.Stream new_stream>

We can also change the activeSite…

n.activeSite = s
n.activeSite
 <music21.stream.Stream empty_stream>

As long as it is a Stream that the Element is already a part of:

q = stream.Stream(id='unrelated_stream')
n.activeSite = q
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

~/git/music21base/music21/stream/__init__.py in elementOffset(self, element, stringReturns)
   1639             # 2.3 million times found in TestStream
-> 1640             o = self._offsetDict[id(element)][0]
   1641             # if returnedElement is not element:  # stale reference...


KeyError: 4540923128


During handling of the above exception, another exception occurred:


SitesException                            Traceback (most recent call last)

~/git/music21base/music21/base.py in _setActiveSite(self, site)
   2037             try:
-> 2038                 storedOffset = site.elementOffset(self)
   2039             except SitesException:


~/git/music21base/music21/stream/__init__.py in elementOffset(self, element, stringReturns)
   1650                     'an entry for this object 0x%x is not stored in stream %s' %
-> 1651                     (id(element), self))
   1652


SitesException: an entry for this object 0x10ea8fcf8 is not stored in stream <music21.stream.Stream unrelated_stream>


During handling of the above exception, another exception occurred:


SitesException                            Traceback (most recent call last)

<ipython-input-33-aa8ad4f44014> in <module>
      1 q = stream.Stream(id='unrelated_stream')
----> 2 n.activeSite = q


~/git/music21base/music21/base.py in _setActiveSite(self, site)
   2039             except SitesException:
   2040                 raise SitesException('activeSite cannot be set for '
-> 2041                                      + f'object {self} not in the Stream {site}')
   2042
   2043             self._activeSiteStoredOffset = storedOffset


SitesException: activeSite cannot be set for object <music21.note.Note C#> not in the Stream <music21.stream.Stream unrelated_stream>

Newly created objects have an .activeSite of None

m = note.Rest()
m.activeSite is None
 True

The .activeSite of an object will determine which other objects it is connected to, where it thinks it is, etc. The best way to demonstrate that is with the next attribute…

offset

The .offset of a Music21Object is the number of quarter notes from the start of the Stream it is a part of. The Stream that is referenced is the .activeSite. Remember that n was inserted at offset 0 of s ('empty stream') and offset 4 of t ('new stream'):

n.activeSite = s
n.offset
 0.0
n.activeSite = t
n.offset
 4.0

If we change the offset of the Note it changes it in the Stream, so that if we change the activeSite away and back, the offset is preserved. It’s more easily demonstrated than explained in words:

n.activeSite = s
n.offset = 2.0
n.activeSite = t
n.activeSite = s
n.offset
 2.0

A newly created Music21Object has a great advantage – it can set its offset to anything it wants and then when it is inserted into a Stream with a single argument, it appears at that offset.

n2 = note.Note('G-2')
n2.offset = 20.0
s.insert(n2)
n2.activeSite
 <music21.stream.Stream empty_stream>
n2.offset
 20.0

priority

If you have a Stream with two elements at the same offset, how can you know which one of them should come first? The easiest way to ensure that one comes before the other is to change the .priority of one of them. .priority is any integer, with a default of zero. Let’s create some a new Stream and some notes:

s = stream.Stream()
d = note.Note('D4')
e = note.Note('E4')
s.insert(0.0, d)
s.insert(0.0, e)
s.show('text')
 {0.0} <music21.note.Note D>
 {0.0} <music21.note.Note E>

Both notes are at offset 0, but D was inserted first, so it comes first. But we can move E by making it’s .priority lower than D’s:

d.priority
 0
e.priority = -1
s.show('text')
 {0.0} <music21.note.Note E>
 {0.0} <music21.note.Note D>

Note

Think of priority like a number line. So negative numbers come before positive. Don’t think of a “high priority” like 2000 meaning that you will encounter that object first. It is the opposite.

Prior to music21 v3, changing the priority of an object did not automatically tell its sites that they needed to be sorted again. To get this output in earlier versions, call s.elementsChanged()

If there is enough demand, priority may become a per-site attribute like offset but this is a problem we plan to tackle when need arises.

If we return e’s priority to the default of 0, it will again appear after d:

e.priority = 0
s.show('text')
 {0.0} <music21.note.Note D>
 {0.0} <music21.note.Note E>

classSortOrder

Objects seem to be sorted by offset first, then priority, then when they were inserted. But what about this:

tc = clef.TrebleClef()
s.insert(0.0, tc)
s.show('text')
 {0.0} <music21.clef.TrebleClef>
 {0.0} <music21.note.Note D>
 {0.0} <music21.note.Note E>

How did the Stream (correctly) know that the treble clef should come first? It’s not because of its priority:

(tc.priority, d.priority, e.priority)
 (0, 0, 0)

It’s because there is another property that aids in sorting, and that is called .classSortOrder. We can see that c, our treble clef, has a lower .classSortOrder than d and e by virtue of being of the class Clef:

(tc.classSortOrder, d.classSortOrder, e.classSortOrder)
 (0, 20, 20)

.classSortOrder is like .priority in that lower numbers come first. We’ve arbitrarily placed Clef at 0 and Note at 20 and lots of other Classes in between. .classSortOrder is what is called a Class Attribute, meaning that the class objects have this attribute set:

(clef.TrebleClef.classSortOrder, note.Note.classSortOrder)
 (0, 20)

And any change in .classSortOrder applied to the class changes it for all its members:

clef.TrebleClef.classSortOrder = 25
note.Note.classSortOrder = 10
(tc.classSortOrder, d.classSortOrder, e.classSortOrder)
 (25, 10, 10)

But don’t do that! For one thing, the ordering of existing Streams is not changed, for another, we’ve carefully balanced the classSortOrder so that musically logical order is maintained (have you ever seen a score where the first note comes before the first clef?). So we’ll change it back here.

clef.TrebleClef.classSortOrder = 0
note.Note.classSortOrder = 20
(tc.classSortOrder, d.classSortOrder, e.classSortOrder)
 (0, 20, 20)

If you do need to do something wacky, like have notes before clefs, you can always change the .priority instead.

For more information about how sorting works, jump to Chapter 21: Ordering and Sorting of Stream Elements

That seems like a good place for a break. We’ve got some more to cover, so I’ve split this chapter into two parts, we’ll continue in Chapter 13: More Music21Object Attributes and Methods