Music Computation Code from September 26, 2017
My approach was to plot the percentage of ascending departures from a pitch as a function of distance from the median. As pitch height increases relative to the median, the percentage of ascending departures should decrease. Multiple plots are generated with differing constraints on the size of the approaching interval. Comparing these different plots can yield insight into the relative effects of regression to the mean and post-skip reversal for skips of various sizes.
from music21 import * from collections import Counter import os
"Matplotlib is a Python 2D plotting library which produces publication quality
figures in a variety of hardcopy formats and interactive environments across
platforms. For simple plotting the
pyplot module provides a MATLAB-like
interface." See https://matplotlib.org/ for more details.
import matplotlib.pyplot as plt
NumPy is the fundamental package for scientific computing with Python. See http://www.numpy.org/ for more details.
import numpy as np
We will also go over some of the basic uses of Matplotlib and NumPy in class.
Class that brings together four pieces of information about every pitch (current): its predecessor (previous), successor (following), and the median pitch (median) of the song in which current occurs.
def __init__(self, previous, current, following, median): self.previous = previous self.current = current self.following = following self.median = median
Basic methods for obtaining intervals from the previous pitch, to the following pitch, and from the median pitch.
def getFromPrevious(self): return self.current - self.previous
def getToNext(self): return self.following - self.current
def getFromMedian(self): return self.current - self.median
Given a flattened Stream of notes, creates an instance
PitchNeighborhood class for each pitch and adds the class
to the list
def update_neighborhoods(neighborhoods, song):
pitches = [n.pitch.ps for n in song]
NumPy has a function for finding the median of list or NumPy array.
med_pitch = np.median(pitches)
Ignore if the median pitch is not an actual pitch. This avoids keeping track of fractional distances from the median. Only 22 out of 704 songs are omitted.
if int(med_pitch) != med_pitch: return
PitchNeighborhood for every pitch (other than the beginning and
ending pitches in each song) and append to the
for i in range(1, len(pitches) - 1): p = PitchNeighborhood(pitches[i-1], pitches[i], pitches[i+1], med_pitch) neighborhoods.append(p)
Function to test regression to the mean versus
neighborhoods is a list of instances of
leaps is a list of numbers
corresponding to plots of the graphs. If a member of
is positive, the corresponding plot includes data only for those
pitches approached by an ascent of at least n. For negative n,
the plot includes data only for those pitches approached by a
descent of at least abs(n). If n is zero, the plot includes data
for all pitches.
def experiment(neighborhoods, leaps, figure):
Counter objects to keep track of frequencies of ascents and all
all non-unison motions. This is to calculate percentages of non-unison
motions that ascend.
ascents = Counter() ascents_descents = Counter()
Details for plotting the figures.
plt.figure(figure) plt.subplot(121) for leap in leaps:
sign is a NumPy function for determining the sign of a number.
sign = np.sign(leap)
Loop to update the
ascents_descents counters with
pitches that are approached by an interval of at least size
for p in neighborhoods:
Filters the data to be included in the plot.
if sign * p.getFromPrevious() >= sign * leap:
Pitches left by an ascent.
if p.getToNext() > 0: ascents.update([p.getFromMedian()])
Pitches not followed by the same pitch.
if p.getToNext() != 0: ascents_descents.update([p.getFromMedian()])
The following loop deletes any keys in the
if there are fewer than 10 instances of the key. Keys in the counters
correspond to distances from the median. Deleting the keys is to
avoid plotting data with a very small sample size.
Note that this
for loop is iterating over a list of keys in the
counter. Since we are potentially altering the counter (by deleting
keys), it would create problem to iterate over the keys directly with
for key in ascents_descents.
for key in list(ascents_descents): if ascents_descents[key] < 10: del ascents_descents[key]
Create values for x- and y-axes of the figure.
xvalues = sorted(ascents_descents) yvalues = [round(float(ascents[x]) / ascents_descents[x], 2) for x in xvalues]
Generate legend for the figure.
if leap == 0: lab = 'All pitches' elif leap > 0: lab = 'Approached by ascent of ' + str(leap) + ' or more' else: lab = 'Approached by descent of ' + str(leap) + ' or more'
Create the plot corresponding to the current value of
plt.plot(xvalues, yvalues, label = lab)
Clear the two counters for the next value of
Label axes, provide title, and place legend in the figure.
plt.xlabel('Distance of pitch from median') plt.ylabel('Percent of motions from pitch that ascend') plt.title('Regression to the mean') plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
Write figure to the hard drive.
plt.savefig('Figure ' + str(figure))
Parse songs in the 'boehme' subdirectory. (Substitute the appropriate file path for your computer.)
dir_name = '/Users/ccallender/Desktop/europa/deutschl/boehme/' filenames = [filename for filename in os.listdir(dir_name)] paths = [dir_name + filename for filename in filenames if filename.endswith('.krn')] songs = [converter.parse(path).flat.notes for path in paths] print("Songs loaded")
Create and update
neighborhoods for each
neighborhoods =  for song in songs: update_neighborhoods(neighborhoods, song)
Figure 1: All pitches regardless of approach. Base plot demonstrating influence of regression to the mean
figure = 1 experiment(neighborhoods, , figure)
Figures 2-5: Base plot plus pitches approached by leaps of at least a minor third, perfect fourth, perfect fifth, and minor sixth, respectively.
for leap in [3, 5, 7, 8]: figure += 1 experiment(neighborhoods, [0, leap, -leap], figure)