# 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 Bob van der Poel """ import copy from MMA.common import * from MMA.chordtable import chordlist import MMA.roman #################################################### # Convert a roman numeral chord to standard notation def defChord(ln): """ Add a new chord type to the chords{} dict. """ emsg="DefChord needs NAME (NOTES) (SCALE)" # At this point ln is a list. The first item should be # the new chord type name. if not len(ln): error(emsg) name = ln.pop(0) if name in chordlist.keys(): warning("Redefining chordtype '%s'" % name) if '/' in name: error("A slash in not permitted in chord type name") if '>' in name: error("A '>' in not permitted in chord type name") ln=pextract(''.join(ln), '(', ')') if ln[0] or len(ln[1])!=2: error(emsg) 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 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 chordlist[name] = ( notes, scale, "User Defined") if gbl.debug: print "ChordType '%s', %s" % (name, chordlist[name]) def printChord(ln): """ Display the note/scale/def for chord(s). """ for c in ln: try: print c, ':', chordlist[c][0], chordlist[c][1], chordlist[c][2] except: error("Chord '%s' is unknown" % c) """ 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. Should the shifts take in account the current key signature? """ 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 } def chordAdjust(ln): """ Adjust the chord point up/down one octave. """ notpair, ln = opt2pair(ln) if not ln: error("ChordAdjust: Needs at least one argument.") if notpair: error("ChordAdjust: All args have to be in the format opt=value.") for pitch, octave in ln: if pitch not in cdAdjust: error("ChordAdjust: '%s' is not a valid pitch" % pitch) octave = stoi(octave, "ChordAdjust: expecting integer, not '%s'" % octave) p=cdAdjust[pitch] if octave == 0: if p < -6: cdAdjust[pitch] += 12 elif p > 6: cdAdjust[pitch]-=12 elif octave == -1 and p <= 6 and p >= -6: cdAdjust[pitch] -= 12 elif octave == 1 and p <= 6 and p >= -6: cdAdjust[pitch] += 12 else: error("ChordAdjust: '%s' is not a valid octave. Use 1, 0 or -1" % octave) ############################### # Chord creation/manipulation # ############################### class ChordNotes: """ The Chord class creates and manipulates chords for MMA. The class is initialized with a call with the chord name. Eg: ch = ChordNotes("Am") The following methods and variables are defined: noteList - the notes in the chord as a list. The "Am" would be [9, 12, 16]. noteListLen - length of noteList. tonic - the tonic of the chord ("Am" would be "A"). chordType - the type of chord ("Am" would be "m"). rootNote - the root note of the chord ("Am" would be a 9). bnoteList - the original chord notes, bypassing any invert(), etc. mangling. scaleList - a 7 note list representing a scale similar to the chord. 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. 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: ch=Chord('Am') ch.noteList == [9, 12, 16] ch.invert(1) ch.noteList = [21, 12, 16] compress() - Compresses the range of a chord to a single octave. This is done inplace and return None. Eg: ch=Chord("A13") ch.noteList == [1, 5, 8, 11, 21] ch.compress() ch.noteList == [1, 5, 8, 11, 10 ] limit(n) - Limits the range of the chord 'n' notes. Done inplace and returns None. Eg: ch=Chord("CM711") ch.noteList == [0, 4, 7, 11, 15, 18] ch.limit(4) ch.noteList == [0, 4, 7, 11] """ ################# ### Functions ### ################# def __init__(self, name): """ Create a chord object. Pass the chord name as the only arg. NOTE: Chord names ARE case-sensitive! 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. A chord can start with a single '+' or '-'. This moves the entire chord and scale up/down an octave. 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. 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. """ slash = None wmessage = '' # slash warning msg, builder needed for gbl.rmShow octave = 0 inversion = 0 if name == 'z': self.tonic = self.chordType = None self.noteListLen = 0 self.notesList = self.bnoteList = [] return if '/' in name and '>' in name: error("You cannot use both an inversion and a slash in the same chord") 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 if '>' in name: name, inversion = name.split('>', 1) inversion = stoi(inversion, "Expecting integer after '>'") if inversion < -5 or inversion > 5: error("Chord inversions limited to -5 to 5 (more seems silly)") if name.startswith('-'): name = name[1:] octave = -12 if name.startswith('+'): name = name[1:] octave = 12 # we have just the name part. Save 'origname' for debug print origName = name = name.replace('&', 'b') # Strip off the slash part of the chord. Use later # to do proper inversion. if name.find('/') > 0: name, slash = name.split('/') if name[0] in ("I", "V", "i", "v"): n=name name = MMA.roman.convert(name) if name[1:2] in ( '#b' ): tonic = name[0:2] ctype = name[2:] else: tonic = name[0:1] ctype = name[1:] if not ctype: # If no type, make it a Major ctype='M' try: notes = chordlist[ctype][0] adj = cdAdjust[tonic] + octave except: error( "Illegal/Unknown chord name: '%s'" % name ) self.noteList = [ x + adj for x in notes ] self.bnoteList = tuple(self.noteList) self.scaleList = tuple([ x + adj for x in chordlist[ctype][1] ]) self.chordType = ctype self.tonic = tonic self.rootNote = self.noteList[0] self.barre = barre self.noteListLen = len(self.noteList) # Inversion if inversion: self.invert(inversion) self.bnoteList = tuple(self.noteList) # Do inversions if there is a valid slash notation. 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] try: r=cdAdjust[slash] # r = -6 to 6 except KeyError: error("The note '%s' in the slash chord is unknown" % slash) # If the slash note is in the chord we invert # the chord so the slash note is in root position. 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 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 if not c_roted and not s_roted: wmessage = "The slash chord note '%s' not in chord or scale" % slash if not gbl.rmShow: warning(wmessage) elif not c_roted: wmessage = "The slash chord note '%s' not in chord '%s'" % (slash, name) if not gbl.rmShow: warning(wmessage) elif not s_roted: # Probably will never happen :) 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) def reset(self): """ Restores notes array to original, undoes mangling. """ self.noteList = list(self.bnoteList[:]) self.noteListLen = len(self.noteList) def invert(self, n): """ Apply an inversion to a chord. 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. NOTE: Done on the existing list of notes. Returns None. """ if n: c=self.noteList[:] while n>0: # Rotate up by adding 12 to lowest note n -= 1 c[c.index(min(c))]+=12 while n<0: # Rotate down, subtract 12 from highest note n += 1 c[c.index(max(c))]-=12 self.noteList = c return None def compress(self): """ Compress a chord to one ocatve. Get max permitted value. This is the lowest note plus 12. Note: use the unmodifed value bnoteList! """ mx = self.bnoteList[0] + 12 c=[] for i, n in enumerate(self.noteList): if n > mx: n -= 12 c.append(n) self.noteList = c return None def limit(self, n): """ Limit the number of notes in a chord. """ if n < self.noteListLen: self.noteList = self.noteList[:n] self.noteListLen = len(self.noteList) return None def center1(self, lastChord): """ Descriptive comment needed here!!!! """ def minDistToLast(x, lastChord): dist=99 for j in range(len(lastChord)): if abs(x-lastChord[j])0): return 1 elif (x<0): return -1 else: return 0 # 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). if lastChord: ch=self.noteList for i in range(len(ch)): # minimize distance to last chord 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) return None def center2(self, centerNote, noteRange): """ Need COMMENT """ ch=self.noteList for i,v in enumerate(ch): 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 ) return None ######## End of Chord class #####