In [216]:
from music21 import *
from random import choice as rand_choice

In [379]:
def dfs(row, consonance, subsets, recurse=False, prev=[]):
    # single element
    if len(row) < 2 and consonance[0]:
        if not recurse:
            subsets.append([row[0]])
        return [row[0]]
    # consonant pair
    subset = prev + [row[0]]
    for idx, el in enumerate(row[1:]):
        if consonance[el - row[0]]:
            subset += dfs(row[idx + 1:], consonance, subsets, True, subset)
    subsets.append(subset)
    return []

def find_consonant_subsets(pcs, consonance):
    subsets = []
    for idx in range(len(pcs)):
        row = pcs[idx:]
        dfs(row, consonance, subsets) # dfs fills subsets
    return subsets

def chord_to_intervals(ch):
    intervals = []
    for i in range(len(ch) - 1):
        intervals.append(ch[i + 1] - ch[i])
    return intervals

MAJOR = [0,4,7]
MINOR = [0,3,7]
DOMINANT = [0,4,7,10]

# If it's a major or minor triad, rotate the pcset to root position
def rotatetomode(pcs):
    all_rotations = [pcs[i:] + pcs[:i] for i in range(len(pcs))]
    for rotation in all_rotations:
        rooted = [(x - rotation[0]) % 12 for x in rotation]
        if rooted == MAJOR or rooted == MINOR or rooted == DOMINANT:
            return rotation
    return pcs

# Add non-base pcs as extensions
def addextensions(base, allpcs):
    extensionpcs = list(set(allpcs) - set(base))
    return base + [x + 12 for x in extensionpcs]

# Additional steps for GCT to ensure unique encodings
def optimalchoice(choices):
    # Dyads: select 5th over 4th, 7th over 2nd
    if len(choices[0][1]) == 2:
        for choice in choices:
            if sum(choice[1]) > 6: # 6 semitones
                return choice
    for choice in choices:
        # Select dominant chords with the correct root
        if choice[1] == DOMINANT:
            return choice
    # Prefer chords with intervals larger than major 2nd (i.e. to select minor 7ths or major chords with add 6th)
    for choice in choices:
        intervals = chord_to_intervals(choice[1])
        if all(x > 2 for x in intervals):
            return choice
    return rand_choice(choices)

In [380]:
# GCT algorithm
# t = tonic pitch class
# v = binary consonance vector of length 12. v[i] denotes whether an interval of i semitones is consonant
# c = music21.chord.Chord
def GCT(t, v, c):
    # Create pitch class set and remove duplicate pcs
    pcset = list(set([x.midi % 12 for x in c.pitches])) 
    pcset.sort()
    # Find consonant subsets using DFS
    subsets = find_consonant_subsets(pcset, v)
    # Sort by length and grab the longest ones
    subsets.sort(key = len)
    maxsubsets = [x for x in subsets if len(x) == len(subsets[-1])] 
    rotated = map(rotatetomode, maxsubsets)
    # Special case: fully diminished chords
    for i in range(len(rotated)):
        ch = rotated[i]
        if all(x == 3 for x in chord_to_intervals(ch)):
            rotated += [ch[i:] + ch[:i] for i in range(len(ch))]
    # Add extensions and select root
    rotated = map(list, list(set(map(tuple,rotated))))
    withextensions = [addextensions(x, pcset) for x in rotated]
    withroot = [((x[0] - t) % 12, map(lambda e: (e - x[0]) % 12, x)) for x in withextensions]
    print withroot
    # Return the optimal choice to ensure unique encodings
    return withroot[0] if len(withroot) == 1 else optimalchoice(withroot)

In [381]:
gmajor = 7
cmajor = 0
v = [1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0] # Consonance vector
c1 = chord.Chord([60, 62, 66, 69, 74])
c2 = chord.Chord([50, 60, 62, 65, 69])
c3 = chord.Chord([62, 68, 77, 71])
print GCT(gmajor, v, c1)
print GCT(cmajor, v, c2)
print GCT(cmajor, v, c3)


[(7, [0, 4, 7, 10])]
(7, [0, 4, 7, 10])
[(5, [0, 4, 7, 9]), (2, [0, 3, 7, 10])]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-381-2076065c7350> in <module>()
      6 c3 = chord.Chord([62, 68, 77, 71])
      7 print GCT(gmajor, v, c1)
----> 8 print GCT(cmajor, v, c2)
      9 print GCT(cmajor, v, c3)

<ipython-input-380-9993f8d599be> in GCT(t, v, c)
     24     print withroot
     25     # Return the optimal choice to ensure unique encodings
---> 26     return withroot[0] if len(withroot) == 1 else optimalchoice(withroot)

<ipython-input-379-e0d808c7f386> in optimalchoice(choices)
     63         if all(x > 2 for x in intervals):
     64             return choice
---> 65     return rand_choice(choices)

/Users/hzabriskie/Documents/Music21/anaconda/lib/python2.7/random.pyc in choice(self, seq)
    273     def choice(self, seq):
    274         """Choose a random element from a non-empty sequence."""
--> 275         return seq[int(self.random() * len(seq))]  # raises IndexError if seq is empty
    276 
    277     def shuffle(self, x, random=None):

IndexError: list index out of range

In [376]:
c3 = chord.Chord([50, 60, 62, 65, 69])
print GCT(cmajor, v, c3)


[(5, [0, 4, 7, 9]), (2, [0, 3, 7, 10])]
(2, [0, 3, 7, 10])

In [377]:
dmajor = 2
c4 = chord.Chord(['F#3', 'D4', 'A4'])
c5 = chord.Chord(['E3', 'G3', 'G4', 'C#5'])
c6 = chord.Chord(['D3', 'A3', 'F#4', 'D4'])
c7 = chord.Chord(['G3', 'B3', 'E4', 'E5'])
c8 = chord.Chord(['A3', 'C#4', 'G4', 'E5'])
c9 = chord.Chord(['D3', 'A3', 'F#4', 'D5'])
print GCT(dmajor, v, c4)
print GCT(dmajor, v, c5)
print GCT(dmajor, v, c6)
print GCT(dmajor, v, c7)
print GCT(dmajor, v, c8)
print GCT(dmajor, v, c9)


[(0, [0, 4, 7])]
(0, [0, 4, 7])
[(11, [0, 3, 6]), (5, [0, 6, 9]), (2, [0, 3, 9])]
(11, [0, 3, 6])
[(0, [0, 4, 7])]
(0, [0, 4, 7])
[(2, [0, 3, 7])]
(2, [0, 3, 7])
[(11, [0, 3, 6, 8]), (5, [0, 6, 9, 2]), (7, [0, 4, 7, 10]), (2, [0, 3, 9, 5])]
(7, [0, 4, 7, 10])
[(0, [0, 4, 7])]
(0, [0, 4, 7])

In [378]:
# Tough cases
c10 = chord.Chord(['G3', 'C4', 'G4', 'C5']) # Should choose a fifth
c11 = chord.Chord(['C3', 'D4'])
c12 = chord.Chord(['D3', 'D4'])
c13 = chord.Chord(['C4', 'C#4', 'D4', 'D#4'])
print GCT(cmajor, v, c10) # Should choose a fifth [0,7]
print GCT(cmajor, v, c11) # Should choose a minor seventh [0, 10]
print GCT(cmajor, v, c12) # Should choose unison
print GCT(cmajor, v, c13) # Should choose minor third with strange extensions


[(0, [0, 7])]
(0, [0, 7])
[(2, [0, 10]), (0, [0, 2])]
(2, [0, 10])
[(2, [0])]
(2, [0])
[(3, [0, 9, 10, 11]), (0, [0, 3, 1, 2])]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-378-c80d94a6acb3> in <module>()
      7 print GCT(cmajor, v, c11) # Should choose a minor seventh [0, 10]
      8 print GCT(cmajor, v, c12) # Should choose unison
----> 9 print GCT(cmajor, v, c13) # Should choose minor third with strange extensions

<ipython-input-374-9993f8d599be> in GCT(t, v, c)
     24     print withroot
     25     # Return the optimal choice to ensure unique encodings
---> 26     return withroot[0] if len(withroot) == 1 else optimalchoice(withroot)

<ipython-input-373-6e7a4068ba7c> in optimalchoice(choices)
     54 #     # Somewhat arbitrary but effective
     55     for i in range(len(choices)):
---> 56         if choices[i][1][1] - choices[i][1][0] > 7:
     57             choices.pop(i)
     58     for choice in choices:

IndexError: list index out of range

In [ ]:


In [ ]: