Overview
See this code in action here!
One of the common problems in musical set theory involves determining how to pitch class sets (PC sets) are related. One way to determine how sets of the same cardinality (size) are related is to compute the prime form of each set.
For example, the prime form of a C major scale is 7-35 ([0,1,3,5,6,8,10]). The prime form of D major scale is also 7-35. Also, the prime form of the A minor scale is also 7-35. All major and minor scales have 7-35 as their prime form. This relation between different scales is not immediately apparent, making the prime form extremely useful for finding relations among pitch class sets.
The name, 7-35, comes from the catalog that Allen Forte published in his Structure of Atonal Music. 7 denotes that it is a 7-note scale, and 35 denotes that it is the 35th in the list when the list is sorted. The algorithm used here comes from The Structure of Atonal Music, p. 4, as well as from Joseph Straus’ Introduction to Post Tonal Theory, p. 50.
I will soon convert this code to a PC set calculator that lives somewhere on this site. This code is released under Creative Commons Attribution-Noncommercial-Share Alike 3.0 license. It is provided as is with no warranty. If you find any errors, please let me know!
Implementation
For this implementation, I decided to use Python for its functional programming features and because it is already installed on this webserver. I had tried previously to implement this using a procedural language, but did not get very far.
I looked at two other online PC Set calculators (and to verify that my own code worked properly):
Both were implemented in Javascript. The PC Set Calculator used about 3000 lines of code. Granted, there was a lot of whitespace, as well as the PC set catalog and a lot of UI-specific code. Still, the code was considerably larger than SetFinder, which used about 400 lines of code. My Python implementation, which does not include the PC set catalog or any UI-code, comes in at 100 lines.
Programming Notes
I tried to use an associative array to associate the pitch class with its outer interval, but I could not because I implemented the pitch class set itself as a list. Python says that lists are not hashable, and therefore cannot be used as keys in associative arrays.
Instead, I used a tuple format: (a, b), where a is the pitch class set as a list, and b is the outer interval. Here is an example: ([0,1,3], 3)
I made extensive use of list comprehensions, often to create a new list by applying a transform to an existing list. An example of this is the inversion function, which returns a new list by subtracting 12 from each element of the input list.
One last thing, notice how the order of the ternary operator in Python is different from how it is in C/C++.
def rangeDiff(pcs):
# Return the difference between the first and last items of a list of pitch class sets
diff = (pcs[len(pcs) - 1] - pcs[0])
if diff < 0: diff += 12 return diff def cycleLeft(pcs): # Return a single cyclic left permutation of a pitch class set # Ex: [a,b,c] -> [b, c, a]
cycle = pcs[1:]
cycle.append(pcs[0])
return cycle
def cycleDiff(pcs):
# Given a pitch class set as a list
# Return a duple (2-tuple) of the form ([pcs], diff)
# Example:
# Input: [0,1,2,3]
# Output: ([0,1,2,3], 3)
return (pcs, rangeDiff(pcs))
def removeLast(list):
# Given a list, remove the last element
# Unlike pop(), which returns the value of the
# last element this function returns the
# modified list instead
return list[0:len(list) - 1]
def selectSmallest(pcsDupleList):
# From a list of PCS duples (2-tuples),
# select the N sets with the smallest
# outside intervals
# If N = 1, return
# Else, recurse until N = 1
# Use a lambda to sort by the difference
# the smallest difference will be first
pcsDupleList.sort(key = lambda x: x[1])
# Get the value of the smallest interval
smallestDiff = pcsDupleList[0][1]
# filter for the N with smallest outer intervals
pcsDupleList = filter(lambda x: x[1] == smallestDiff, pcsDupleList)
if len(pcsDupleList) == 1:
return pcsDupleList
else :
subsets = [cycleDiff(removeLast(x[0])) for x in pcsDupleList]
return selectSmallest(subsets)
def normalForm(pcs):
# Return the normal form of the input pitch class set
cycle = pcs
cycleCount = 0
cycles = []
while cycleCount < len(cycle) :
cycle = cycleLeft(cycle)
cycleCount += 1
# Insert into a quasi-associative array
# cannot use an actual associative array
# because lists are not hashable
# Format [([pcs], diff), ... ]
cycles.append(cycleDiff(cycle))
if cycle == pcs:
break
smallestSets = selectSmallest(cycles)
if len(smallestSets[0][0]) < len(cycles[0][0]) : lengthDiff = len(cycles[0][0]) - len(smallestSets[0][0]) # create a list that only has subsets from the cycle list # that start with the same number element filterList = [x[0][:len(x[0]) - lengthDiff] for x in cycles] # look for its position in the list index = filterList.index(smallestSets[0][0]) # because the lists are sorted, the indexes match smallestSets[0] = cycles[index] return smallestSets[0][0] def inversion(pcs): # Determine the non-transposed inversion of the # pitch class set by subtracting every element from 12 return [12 - x for x in pcs] def zeroNormal(pcs): # Convert the normal form so that the first element is 0 # By subtracting the first element from every element # Add 12 if the result is less than 0 normal = normalForm(pcs) zeroNormal = [x - normal[0] % 12 for x in normal] # Note the unusual order for the ternary operator return [x if x >= 0 else x + 12 for x in zeroNormal]
def primeForm(pcs):
# To determine the prime form, compute the zeroNormal of the set
# as well as the zeroNormal of its inversion
# Select whichever one is more packed to the left (smaller intervals first)
zeroNorm = zeroNormal(pcs)
zeroInv = sorted(zeroNormal(inversion(pcs)))
primeForm = zeroNorm
# Luckily, we can use Python's built-in list comparison
# to determine which set is more packed to the left
if zeroInv < zeroNorm :
primeForm = zeroInv
return primeForm