# Owen Campbell
# MAT 240E Homework 2
#
# For the purposes of this assignment, key modulations will be defined (rather naively) as follows:
# 'parallel minor' - The first chord and the last chord are of the same scale degree, but change from major to
# major to minor
# 'parallel major' - The first chord and the last chord are of the same scale degree, but change from major to
# minor to major
# 'relative minor' - The root of the last chord is 9 semitones above the root of the first chord and
# there is a shift from major to minor tonality
# 'relative major' - The root of the last chord is 3 semitones above the root of the first chord and
# there is a shift from minor to major tonality
# 'submediant' - The root of the last chord is 9 semitones above the root of the first chord but
# there is no shift in tonality
# 'mediant' - The root of the last chord is 3 semitones above the root of the first chord but
# there is no shift in tonality
# 'subdominant' - The root of the last chord is 5 semitones above the root of the first chord
# 'dominant' - The root of the last chord is 7 semitones above the root of the first chord
# 'unknown' - The first chord in the work differs from the last chord but the modulation does
# not fall into any of the above categories
# 'none' - The first chord in the work does not differ from the last chord
# Hypothesis: Since these are classical works, it is very likely that the majority of the pieces will
# involve a fairly predictable key modulation; specifically, one to a parallel, relative,
# dominant or subdominant key. I originally intended to process all the works in the corpus,
# but due to time constraints I am only using the Bach Chorales
from music21 import *
import ntpath
import string
# this function attempts to determine the interval, in semitones (+/-) between two pitches
def degree_dif(start, end):
if start == end:
return 0
if end - start < 0:
return end - start + 12
else:
return end - start
core = corpus.CoreCorpus()
modulations = {'parallel minor': 0, 'parallel major': 0, 'relative minor': 0, 'relative major': 0,
'submediant': 0, 'mediant': 0, 'subdominant': 0, 'dominant': 0, 'unknown': 0, 'none': 0}
works = core.getBachChorales()
#print ntpath.basename(works[0])
for work in works:
#name = ntpath.basename(work)
work = corpus.parse(work)
excerpt = work.measures(0,5)
reduction = work.chordify().stripTies(retainContainers=True)
chords = reduction.flat.getElementsByClass('Chord')
nchords = len(chords)
try:
# attempt to get pitch class of first real chord
first = harmony.chordSymbolFigureFromChord(chords[0], True)
if first[0] == 'Chord Symbol Cannot Be Identified':
idx = 2
while first == 'Chord Symbol Cannot Be Identified':
first = harmony.chordSymbolFigureFromChord(chords[nchords + idx], True)
idx += 1
fc_name = first[0].split('/', 1)[0]
fc_color = first[1]
fc_num = pitch.Pitch(str(fc_name).translate(None, string.ascii_lowercase)).pitchClass
ft = first[1]
# attempt to get pitch class of last chord
last = harmony.chordSymbolFigureFromChord(chords[nchords - 1], True)
if last[0] == 'Chord Symbol Cannot Be Identified':
idx = 2
while last[0] == 'Chord Symbol Cannot Be Identified':
last = harmony.chordSymbolFigureFromChord(chords[nchords - idx], True)
idx += 1
lc_name = last[0].split('/', 1)[0]
lc_color = last[1]
lc_num = pitch.Pitch(str(lc_name).translate(None, string.ascii_lowercase)).pitchClass
lt = last[1]
distance = degree_dif(fc_num, lc_num)
#print '~~~~~'
#print 'first: {} ({})'.format(fc_num, fc_name)
#print 'last: {} ({})'.format(lc_num, lc_name)
#print 'dist: {}'.format(distance)
if distance == 0:
if fc_color == lc_color:
modulations['none'] += 1
#print 'conclusion: none'
elif fc_color == 'minor' and lc_color == 'major':
modulations['parallel major'] += 1
#print 'conclusion: parallel major'
elif fc_color == 'major' and lc_color == 'minor':
modulations['parallel minor'] += 1
#print 'conclusion: parallel minor'
else: # this will happen with less common chord labels like 'Gpedal' or 'Gsus'
modulations['none'] += 1
#print 'conclusion: none'
elif distance == 9:
if fc_color == 'major' and lc_color == 'minor':
modulations['relative minor'] += 1
#print 'conclusion: relative minor'
else: # modulating to MAJOR 6
modulations['submediant'] += 1
#print 'conclusion: submediant'
elif distance == 3:
if fc_color == 'minor' and lc_color == 'major':
modulations['relative major'] += 1
#print 'conclusion: relative major'
else: # modulating to MINOR 3
modulations['mediant'] += 1
#print 'conclusion: mediant'
elif distance == 5:
modulations['subdominant'] += 1
#print 'conclusion: subdominant'
elif distance == 7:
modulations['dominant'] += 1
#print 'conclusion: dominant'
else:
modulations['unknown'] += 1
#print 'conclusion: unknown'
#print
except:
continue
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (16, 4)
pyplot.bar(range(len(modulations)), modulations.values(), align='center')
pyplot.xticks(range(len(modulations)), modulations.keys(), rotation='vertical')
pyplot.title('Modulations in Bach Chorales')
pyplot.show()
# Conclusion:
# Though modulation to the parallel major was very common, the vast majority of the Bach Chorales ended
# in the same key they began in, undermining my hypothesis that modulations would be most common. Perhaps
# if the same analysis was performed on other Bach works or works by another composer we would see
# different results. Furthermore, this analysis method has obvious limitations, and I would suspect that
# many of the pieces which returned to the original key included some sort of modulation part way through
# the work.