mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 19:23:59 +00:00
529 lines
15 KiB
Python
529 lines
15 KiB
Python
|
|
# 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 <bob@mellowood.ca>
|
|
|
|
"""
|
|
|
|
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])<abs(dist):
|
|
dist=x-lastChord[j]
|
|
return dist
|
|
|
|
def sign(x):
|
|
if (x>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 #####
|
|
|
|
|
|
|
|
|