VocalEasel/mma/MMA/chords.py

529 lines
15 KiB
Python
Raw Permalink Normal View History

2006-11-10 08:07:56 +00:00
# chords.py
"""
This module is an integeral part of the program
MMA - Musical Midi Accompaniment.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2007-04-29 06:47:40 +00:00
Bob van der Poel <bob@mellowood.ca>
2006-11-10 08:07:56 +00:00
"""
import copy
from MMA.common import *
2009-05-17 22:34:44 +00:00
from MMA.chordtable import chordlist
2011-07-26 22:49:39 +00:00
import MMA.roman
####################################################
# Convert a roman numeral chord to standard notation
2006-11-10 08:07:56 +00:00
def defChord(ln):
2007-04-29 06:47:40 +00:00
""" Add a new chord type to the chords{} dict. """
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
emsg="DefChord needs NAME (NOTES) (SCALE)"
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# At this point ln is a list. The first item should be
# the new chord type name.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if not len(ln):
error(emsg)
name = ln.pop(0)
2009-05-17 22:34:44 +00:00
if name in chordlist.keys():
2007-04-29 06:47:40 +00:00
warning("Redefining chordtype '%s'" % name)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if '/' in name:
error("A slash in not permitted in chord type name")
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if '>' in name:
error("A '>' in not permitted in chord type name")
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
ln=pextract(''.join(ln), '(', ')')
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if ln[0] or len(ln[1])!=2:
error(emsg)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
notes=ln[1][0].split(',')
if len(notes) < 2 or len(notes)>8:
error("There must be 2..8 notes in a chord, not '%s'" % len(note))
notes.sort()
for i,v in enumerate(notes):
v=stoi(v, "Note offsets in chord must be integers, not '%s'" % v)
if v<0 or v>24:
error("Note offsets in chord must be 0..24, not '%s'" % v)
notes[i]=v
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
scale=ln[1][1].split(',')
if len(scale) != 7:
error("There must be 7 offsets in chord scale, not '%s'" % len(scale))
scale.sort()
for i,v in enumerate(scale):
v=stoi(v, "Scale offsets in chord must be integers, not '%s'" % v)
if v<0 or v>24:
error("Scale offsets in chord must be 0..24, not '%s'" % v)
scale[i]=v
2006-11-10 08:07:56 +00:00
2009-05-17 22:34:44 +00:00
chordlist[name] = ( notes, scale, "User Defined")
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if gbl.debug:
2009-05-17 22:34:44 +00:00
print "ChordType '%s', %s" % (name, chordlist[name])
2006-11-10 08:07:56 +00:00
def printChord(ln):
2007-04-29 06:47:40 +00:00
""" Display the note/scale/def for chord(s). """
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
for c in ln:
2009-05-17 22:34:44 +00:00
try:
print c, ':', chordlist[c][0], chordlist[c][1], chordlist[c][2]
except:
2007-04-29 06:47:40 +00:00
error("Chord '%s' is unknown" % c)
2009-05-17 22:34:44 +00:00
2006-11-10 08:07:56 +00:00
2009-05-17 22:34:44 +00:00
""" Table of chord adjustment factors. Since the initial chord is based
on a C scale, we need to shift the chord for different degrees. Note,
that with C as a midpoint we shift left for G/A/B and right for D/E/F.
2006-11-10 08:07:56 +00:00
2009-05-17 22:34:44 +00:00
Should the shifts take in account the current key signature?
2006-11-10 08:07:56 +00:00
"""
2007-04-29 06:47:40 +00:00
cdAdjust = {
'Gb':-6,
'G' :-5,
'G#':-4, 'Ab':-4,
'A' :-3,
'A#':-2, 'Bb':-2,
'B' :-1, 'Cb':-1,
'B#': 0, 'C' : 0,
'C#': 1, 'Db': 1,
'D' : 2,
'D#': 3, 'Eb': 3,
'E' : 4, 'Fb': 4,
'E#': 5, 'F' : 5,
'F#': 6 }
2006-11-10 08:07:56 +00:00
def chordAdjust(ln):
2007-04-29 06:47:40 +00:00
""" Adjust the chord point up/down one octave. """
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
notpair, ln = opt2pair(ln)
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
if not ln:
error("ChordAdjust: Needs at least one argument.")
if notpair:
error("ChordAdjust: All args have to be in the format opt=value.")
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
for pitch, octave in ln:
2007-04-29 06:47:40 +00:00
if pitch not in cdAdjust:
error("ChordAdjust: '%s' is not a valid pitch" % pitch)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
octave = stoi(octave, "ChordAdjust: expecting integer, not '%s'" % octave)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
p=cdAdjust[pitch]
if octave == 0:
if p < -6:
cdAdjust[pitch] += 12
elif p > 6:
cdAdjust[pitch]-=12
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
elif octave == -1 and p <= 6 and p >= -6:
cdAdjust[pitch] -= 12
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
elif octave == 1 and p <= 6 and p >= -6:
cdAdjust[pitch] += 12
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
else:
error("ChordAdjust: '%s' is not a valid octave. Use 1, 0 or -1" % octave)
2006-11-10 08:07:56 +00:00
###############################
# Chord creation/manipulation #
###############################
class ChordNotes:
2007-04-29 06:47:40 +00:00
""" The Chord class creates and manipulates chords for MMA. The
class is initialized with a call with the chord name. Eg:
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
ch = ChordNotes("Am")
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
The following methods and variables are defined:
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
noteList - the notes in the chord as a list. The "Am"
would be [9, 12, 16].
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
noteListLen - length of noteList.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
tonic - the tonic of the chord ("Am" would be "A").
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
chordType - the type of chord ("Am" would be "m").
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
rootNote - the root note of the chord ("Am" would be a 9).
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
bnoteList - the original chord notes, bypassing any
invert(), etc. mangling.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
scaleList - a 7 note list representing a scale similar to
the chord.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
reset() - resets noteList to the original chord notes.
This is useful to restore the original after
chord note mangling by invert(), etc. without having to
create a new chord object.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
invert(n) - Inverts a chord by 'n'. This is done inplace and
returns None. 'n' can have any integer value, but -1 and 1
are most common. The order of the notes is not changed. Eg:
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
ch=Chord('Am')
ch.noteList == [9, 12, 16]
ch.invert(1)
ch.noteList = [21, 12, 16]
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
compress() - Compresses the range of a chord to a single octave. This is
done inplace and return None. Eg:
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
ch=Chord("A13")
ch.noteList == [1, 5, 8, 11, 21]
ch.compress()
ch.noteList == [1, 5, 8, 11, 10 ]
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
limit(n) - Limits the range of the chord 'n' notes. Done inplace
and returns None. Eg:
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
ch=Chord("CM711")
ch.noteList == [0, 4, 7, 11, 15, 18]
ch.limit(4)
ch.noteList == [0, 4, 7, 11]
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
"""
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
#################
### Functions ###
#################
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
def __init__(self, name):
2007-04-29 06:47:40 +00:00
""" Create a chord object. Pass the chord name as the only arg.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
NOTE: Chord names ARE case-sensitive!
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
The chord NAME at this point is something like 'Cm' or 'A#7'.
Split off the tonic and the type.
If the 2nd char is '#' or 'b' we have a 2 char tonic,
otherwise, it's the first char only.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
A chord can start with a single '+' or '-'. This moves
the entire chord and scale up/down an octave.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
Note pythonic trick: By using ranges like [1:2] we
avoid runtime errors on too-short strings. If a 1 char
string, name[1] is an error; name[1:2] just returns None.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
Further note: I have tried to enable caching of the generated
chords, but found no speed difference. So, to make life simpler
I've decided to generate a new object each time.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
"""
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
slash = None
2011-07-26 22:49:39 +00:00
wmessage = '' # slash warning msg, builder needed for gbl.rmShow
2007-04-29 06:47:40 +00:00
octave = 0
inversion = 0
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if name == 'z':
self.tonic = self.chordType = None
self.noteListLen = 0
self.notesList = self.bnoteList = []
return
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if '/' in name and '>' in name:
error("You cannot use both an inversion and a slash in the same chord")
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
if ':' in name:
name, barre = name.split(':', 1)
barre = stoi(barre, "Expecting integer after ':'")
if barre < -20 or barre > 20:
error("Chord barres limited to -20 to 20 (more is silly)")
else:
barre = 0
2007-04-29 06:47:40 +00:00
if '>' in name:
name, inversion = name.split('>', 1)
2011-07-26 22:49:39 +00:00
inversion = stoi(inversion, "Expecting integer after '>'")
2007-04-29 06:47:40 +00:00
if inversion < -5 or inversion > 5:
error("Chord inversions limited to -5 to 5 (more seems silly)")
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if name.startswith('-'):
name = name[1:]
octave = -12
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if name.startswith('+'):
name = name[1:]
octave = 12
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
# we have just the name part. Save 'origname' for debug print
origName = name = name.replace('&', 'b')
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# Strip off the slash part of the chord. Use later
# to do proper inversion.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if name.find('/') > 0:
name, slash = name.split('/')
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
if name[0] in ("I", "V", "i", "v"):
n=name
name = MMA.roman.convert(name)
2007-04-29 06:47:40 +00:00
if name[1:2] in ( '#b' ):
tonic = name[0:2]
ctype = name[2:]
else:
tonic = name[0:1]
ctype = name[1:]
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if not ctype: # If no type, make it a Major
ctype='M'
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
try:
2009-05-17 22:34:44 +00:00
notes = chordlist[ctype][0]
2007-04-29 06:47:40 +00:00
adj = cdAdjust[tonic] + octave
except:
error( "Illegal/Unknown chord name: '%s'" % name )
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
self.noteList = [ x + adj for x in notes ]
self.bnoteList = tuple(self.noteList)
2009-05-17 22:34:44 +00:00
self.scaleList = tuple([ x + adj for x in chordlist[ctype][1] ])
2007-04-29 06:47:40 +00:00
self.chordType = ctype
self.tonic = tonic
self.rootNote = self.noteList[0]
2011-07-26 22:49:39 +00:00
self.barre = barre
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
self.noteListLen = len(self.noteList)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# Inversion
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if inversion:
self.invert(inversion)
self.bnoteList = tuple(self.noteList)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# Do inversions if there is a valid slash notation.
2006-11-10 08:07:56 +00:00
2011-07-26 22:49:39 +00:00
if slash: # convert Roman or Arabic to name of note from chord scale
if slash[0] in ('I', 'i', 'V', 'v') or slash[0].isdigit():
n = MMA.roman.rvalue(slash)
n = self.scaleList[n] # midi value
while n >=12:
n-=12
while n<0:
n+=12
slash = ('C', 'C#', 'D', 'D#', 'E', 'F',
'F#', 'G', 'G#', 'A', 'A#', 'B')[n]
2009-05-17 22:34:44 +00:00
try:
r=cdAdjust[slash] # r = -6 to 6
except KeyError:
2007-04-29 06:47:40 +00:00
error("The note '%s' in the slash chord is unknown" % slash)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# If the slash note is in the chord we invert
# the chord so the slash note is in root position.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
c_roted = 0
s=self.noteList
for octave in [0, 12, 24]:
if r+octave in s:
rot=s.index(r+octave)
for i in range(rot):
s.append(s.pop(0)+12)
if s[0] >= 12:
for i,v in enumerate(s):
s[i] = v-12
self.noteList = s
self.bnoteList = tuple(s)
self.rootNote = self.noteList[0]
c_roted = 1
break
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
s_roted = 0
s=list(self.scaleList)
for octave in [0, 12, 24]:
if r+octave in s:
rot=s.index(r+octave)
for i in range(rot):
s.append(s.pop(0)+12)
if s[0] > 12:
for i,v in enumerate(s):
s[i] = v-12
self.scaleList=tuple(s)
s_roted = 1
break
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if not c_roted and not s_roted:
2011-07-26 22:49:39 +00:00
wmessage = "The slash chord note '%s' not in chord or scale" % slash
if not gbl.rmShow:
warning(wmessage)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
elif not c_roted:
2011-07-26 22:49:39 +00:00
wmessage = "The slash chord note '%s' not in chord '%s'" % (slash, name)
if not gbl.rmShow:
warning(wmessage)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
elif not s_roted: # Probably will never happen :)
2011-07-26 22:49:39 +00:00
wmessage = "The slash chord note '%s' not in scale for the chord '%s'" \
% (slash, name)
if not gbl.rmShow:
warning(wmessage)
if gbl.rmShow:
if slash:
a = '/'+slash
else:
a = ''
if wmessage:
a+=' ' + wmessage
print " %03s] %-09s -> %s%s" % (gbl.lineno, origName, name, a)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def reset(self):
""" Restores notes array to original, undoes mangling. """
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
self.noteList = list(self.bnoteList[:])
self.noteListLen = len(self.noteList)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def invert(self, n):
""" Apply an inversion to a chord.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
This does not reorder any notes, which means that the root note of
the chord reminds in postion 0. We just find that highest/lowest
notes in the chord and adjust their octave.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
NOTE: Done on the existing list of notes. Returns None.
"""
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if n:
c=self.noteList[:]
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
while n>0: # Rotate up by adding 12 to lowest note
n -= 1
c[c.index(min(c))]+=12
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
while n<0: # Rotate down, subtract 12 from highest note
n += 1
c[c.index(max(c))]-=12
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
self.noteList = c
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
return None
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def compress(self):
""" Compress a chord to one ocatve.
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
Get max permitted value. This is the lowest note
plus 12. Note: use the unmodifed value bnoteList!
"""
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
mx = self.bnoteList[0] + 12
c=[]
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
for i, n in enumerate(self.noteList):
if n > mx:
n -= 12
c.append(n)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
self.noteList = c
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
return None
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def limit(self, n):
""" Limit the number of notes in a chord. """
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if n < self.noteListLen:
self.noteList = self.noteList[:n]
self.noteListLen = len(self.noteList)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
return None
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def center1(self, lastChord):
""" Descriptive comment needed here!!!! """
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def minDistToLast(x, lastChord):
dist=99
for j in range(len(lastChord)):
if abs(x-lastChord[j])<abs(dist):
dist=x-lastChord[j]
return dist
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def sign(x):
if (x>0):
return 1
elif (x<0):
return -1
else:
return 0
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# Only change what needs to be changed compared to the last chord
# (leave notes where they are if they are in the new chord as well).
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
if lastChord:
ch=self.noteList
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
for i in range(len(ch)):
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
# minimize distance to last chord
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
oldDist = minDistToLast(ch[i], lastChord)
while abs(minDistToLast(ch[i] - sign(oldDist)*12,
lastChord)) < abs(oldDist):
ch[i] -= 12* sign(oldDist)
oldDist = minDistToLast(ch[i], lastChord)
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
return None
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
def center2(self, centerNote, noteRange):
""" Need COMMENT """
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
ch=self.noteList
for i,v in enumerate(ch):
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
dist = v - centerNote
if dist < -noteRange:
ch[i] = v + 12 * ( abs(dist) / 12+1 )
if dist > noteRange:
ch[i] = v - 12 * ( abs(dist) / 12+1 )
2006-11-10 08:07:56 +00:00
2007-04-29 06:47:40 +00:00
return None
2006-11-10 08:07:56 +00:00
######## End of Chord class #####