#!/usr/bin/env python # coding: utf-8 # usage: python choproTunality.py >song.chopro # TODO # Count "/" as another occurance of last chord encountered. # Go stanza by stanza to look for changing keys. # - elb 2012-03-09 import sys import re aMinorChord = re.compile(r'^([A-G][#b♯♭]?)(m|mi|min|-|dim)$') aMajorChord = re.compile(r'^([A-G][#b♯♭]?)(M|ma|maj|sus|aug|\+|dom)?$') def rootTone(theChord): answer = theChord.strip() m = aMinorChord.match(answer) if (m): answer = m.group(1) + 'm' else: m = aMajorChord.match(answer) if (m): answer = m.group(1) else: print 'This chord is neither minor nor major? ' + theChord return answer.replace('#', '♯').replace('b', '♭') # We only care about the root tone and quality. # This allows funky stuff like rests or dashes or whatever after the tone. hasChord = re.compile(r'^([A-G][#b♯♭]?(m|mi|min|-|dim|M|ma|maj|sus|aug|\+|dom)?)[^\]]*\]') def justChord(theLine): c = 'x' m = hasChord.match(theLine) if (m): c = m.group(1) return c chordsInKey = { # Note: only need cannonical key names. 'C': ['F', 'C', 'G', 'Dm', 'Am', 'Em', 'Bm'], 'Am': ['F', 'C', 'G', 'Dm', 'Am', 'Em', 'Bm'], 'G': ['C', 'G', 'D', 'Am', 'Em', 'Bm', 'F♯m'], 'Em': ['C', 'G', 'D', 'Am', 'Em', 'Bm', 'F♯m'], 'D': ['G', 'D', 'A', 'Em', 'Bm', 'F♯m', 'C♯m'], 'Bm': ['G', 'D', 'A', 'Em', 'Bm', 'F♯m', 'C♯m'], 'A': ['D', 'A', 'E', 'Bm', 'F♯m', 'C♯m', 'G♯m'], 'F♯m': ['D', 'A', 'E', 'Bm', 'F♯m', 'C♯m', 'G♯m'], 'E': ['A', 'E', 'B', 'F♯', 'C♯m', 'G♯m', 'D♯m'], 'C♯m': ['A', 'E', 'B', 'F♯', 'C♯m', 'G♯m', 'D♯m'], 'B': ['E', 'B', 'F♯', 'C♯m', 'G♯m', 'D♯m', 'A♯m'], 'G♯m': ['E', 'B', 'F♯', 'C♯m', 'G♯m', 'D♯m', 'A♯m'], 'F♯': ['B', 'F♯', 'C♯', 'G♯m', 'D♯m', 'A♯m', 'E♯m'], 'D♯m': ['B', 'F♯', 'C♯', 'G♯m', 'D♯m', 'A♯m', 'E♯m'], 'C♯': ['F♯', 'C♯', 'G♯', 'D♯m', 'A♯m', 'E♯m', 'B♯m'], 'A♯m': ['F♯', 'C♯', 'G♯', 'D♯m', 'A♯m', 'E♯m', 'B♯m'], 'C♭': ['F♭', 'C♭', 'G♭', 'D♭m', 'A♭m', 'E♭m', 'B♭m'], 'A♭m': ['F♭', 'C♭', 'G♭', 'D♭m', 'A♭m', 'E♭m', 'B♭m'], 'G♭': ['B', 'G♭', 'D♭', 'A♭m', 'E♭m', 'B♭m' 'F'], 'E♭m': ['B', 'G♭', 'D♭', 'A♭m', 'E♭m', 'B♭m' 'F'], 'D♭': ['G♭', 'D♭', 'A♭', 'E♭m', 'B♭m', 'Fm', 'Cm'], 'B♭m': ['G♭', 'D♭', 'A♭', 'E♭m', 'B♭m', 'Fm', 'Cm'], 'A♭': ['D♭', 'A♭', 'E♭', 'B♭m', 'Fm', 'Cm', 'Gm'], 'Fm': ['D♭', 'A♭', 'E♭', 'B♭m', 'Fm', 'Cm', 'Gm'], 'E♭': ['A♭', 'E♭', 'B♭', 'Fm', 'Cm', 'Gm', 'Dm'], 'Cm': ['A♭', 'E♭', 'B♭', 'Fm', 'Cm', 'Gm', 'Dm'], 'B♭': ['E♭', 'B♭', 'F', 'Cm', 'Gm', 'Dm', 'Am'], 'Gm': ['E♭', 'B♭', 'F', 'Cm', 'Gm', 'Dm', 'Am'], 'F': ['B♭', 'F', 'C', 'Gm', 'Dm', 'Am', 'Em'], 'Dm': ['B♭', 'F', 'C', 'Gm', 'Dm', 'Am', 'Em'] } # count up the occurences of each element of the list. # return a dictionary keyed by the original list members # with a value of how many times that member occured in the list. def chordCount(theChords): chordRoots = {} for c in theChords: cRoot = rootTone(c) if cRoot in chordRoots: chordRoots[cRoot] += 1 else: chordRoots[cRoot] = 1 return chordRoots # Should we return a list of keys because there could be a tie (e.g. between a major and it's relative minor)? # @param chordsFound is a dictionary keyed by chord names with values of how many of that chord found. def tonality(chordsFound): suspectStrength = {} strongestKey = '' keyStrength = 0 for k in chordsInKey: strength = 0 for c in chordsFound: if c in chordsInKey[k]: strength += chordsFound[c] if c == k: strength += chordsFound[c] if keyStrength < strength: keyStrength = strength strongestKey = k return strongestKey def main(argv=None): if argv is None: argv = sys.argv song = sys.stdin.read() byChord = song.split(r'[')[1:]; # First one will not be a chord. sys.stdin.close(); # so we can pipe this output back onto the song. print '{key: ' + tonality(chordCount(map(justChord, byChord))) + '}' print '{comment: key suggested by choproTonality.} # choproTonality' # controversy: # since chopro is ascii should we put out ascii sharp and flat? # since our extension of chopro is unicode, maybe not? if __name__ == '__main__': sys.exit(main())