This module builds on the previous Skips and Steps and Style module.
In the previous module, we counted the skips and steps in Maple Leaf Rag, a piano composition by Scott Joplin from the late nineteenth century.
Here’s a summary of our work so far (see the previous module for the code for the count_skipstep function):
joplin = corpus.parse('joplin/maple_leaf_rag.mxl')
top_staff = joplin.parts[0]
count_skipstep(top_line)
> Steps: 51
> Skips: 305
But if we look closely at the score, we’ll see that each staff often contains more than one note at a time. This is common in piano music. But the presence of multiple intervals between each pair of notes has implications for our methodology, which was originally built assuming one note at a time.
When faced with ambiguous analytical situations, we often have to make assumptions based on our musical knowledge in order to proceed. (This is true whether using computational methods or not!) The trick is to make sure you acknowledge those assumptions in your research.
In ragtime music, the right hand (top staff) often carries the melody. More generally, in many styles of music we interpret the highest pitches we can hear as the melody. Therefore, we will measure the intervals between the highest notes in the top staff as a way of assessing the prevalence of skips and steps in what we hear as the melody.
In order to pick the highest note at each moment–in a way that includes moments where there’s just one note–we have to treat every note as a group of multiple notes, called a chord. We do this in music21 by using a function called chordify:
top_chords = top_staff.chordify()
If we view top_chords with show(), it will look identical to top_staff. But now we can iterate over the “chords” in the piece, and pull out the highest note of each. We’ll a couple of new tools for this.
The first is the recurse() function. Recurse lets us access all levels of a stream (recall that streams are how music21 stores musical information). When we’re looking for notes in a chord, for example, we need to search through multiple hierarchical levels within the stream, which contains not only sequences of chords, but also sequences of notes, measures, parts, etc.
So if we want to pull up the first chord in our stream, for example, we’ll use index number zero with recurse() and a second new (reasonably self-explanatory) tool, getElementsByClass(), as follows:
top_chords.recurse().getElementsByClass('Chord')[0]
> <music21.chord.Chord A-4>
The getElementsByClass() function does exactly what it says: you specify the class (and index number), and it gets them! We see the first “chord” in our melody, which is actually just a single note (A-flat). So far, so good. As for the next chord:
top_chords.recurse().getElementsByClass('Chord')[1]
> <music21.chord.Chord E-4 E-5>
Our second chord is two E-flats one octave apart. We can verify the accuracy of our results by checking back with the score.
The highest note in each chord is always last in the list. In Python, we can use the index value -1 to access the last value in any list (no matter the list length).
In music21, we have to specify that we’re working with pitches as well. In this example, which gets us the last (highest) pitch in the first chord, the first index number gives us the chord, and the second gives us the pitch:
top_chords.recurse().getElementsByClass('Chord')[0].pitches[-1]
> <music21.pitch.Pitch E-5>
Now we can generate a melody of single notes only by incorporating this into a list comprehension. We’ll also switch over to MIDI note numbers to calculate the intervals in a different way than in the previous example:
joplin_melody = [chord.pitches[-1].midi for chord in top_chords.recurse().getElementsByClass('Chord')]
Now we have the entire melody simplified to a list of integers (MIDI note numbers). Since intervals are just differences between pitch values, we can use Python’s zip() function to subtract consecutive pairs of pitches. In the code below, we take the absolute value of the difference of i and j, where i and j represent two consecutive integers in our list:
interval_list = [abs(j - i) for i, j in zip(joplin_melody, joplin_melody[1:])]
(Note the use of the index “1:” to indicate the next position in the sequence from the current. This is called “slice notation”.)
Now we can use a simplified version of our previous function to compare skips and steps:
def skipstep_from_MIDI(MIDI_intervals):
skips = 0
steps = 0
for i in MIDI_intervals:
if i > 2:
skips = skips + 1
elif (i > 0) and (i < 3):
steps = steps + 1
print("Steps:", steps)
print("Skips:", skips)
skipstep_from_MIDI(interval_list)
> Steps: 91
> Skips: 302
Indeed, our numbers have changed slightly with this new methodology. However, the basic result is the same: skips are way more prevalent than steps! It’s important to recognize how methodological changes–and the assumptions that underpin them–can have a big impact on the results.
Extensions
- What other methods could be used to account for melodies that are a combination of single notes and chords?
- How can we simplify the steps leading up to calling the function? In other words, which of these steps can be integrated into the function?