
# 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
from MMA.keysig import keySig # needed for voicing mode keycenter()

####################################################
# 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))

    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))

    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 "%s: %s %s %s" % (c, chordlist[c][0], chordlist[c][1], chordlist[c][2])
        except KeyError:
            error("Chord '%s' is unknown. Use only the chord type (m, M, M7, ...)." % 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
        polychord = None

        if name == 'z':
            self.tonic = self.chordType = None
            self.noteListLen = 0
            self.notesList = self.bnoteList = []
            return

        if '|' in name:
            if name.count('|') > 1:
                error("Polychord marker '|' can only be used once.")
            
            name, polychord = name.split('|')

        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 KeyError:
            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 polychord:
            ctable = ChordNotes(polychord)
            self.noteList.extend(ctable.noteList)
            self.noteList = list(set(self.noteList))
            self.noteList.sort()
            if len(self.noteList) > 8:
                i = len(self.noteList)
                self.noteList=self.noteList[:8]
                warning("Polychord %s|%s has been truncated from %d to 8 notes." % \
                            (name, polychord, i))
            self.noteListLen = len(self.noteList)
            self.bnoteList = tuple(self.noteList)


        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 keycenter(self):
        """ Rotate the chord notes until the tonic is close to
            the keysig value. Note that keysig is set C=0, D=2, etc
            which matches the note values in the noteList.
        """

        key = keySig.keyNoteValue
        notes = self.noteList  # just a shorter variable name

        nbelow=0
        nabove=0

        for n in notes:
            if n<key: nbelow+=1
            elif n>key: nabove+=1

        if nbelow>nabove:
            for a in range(nbelow-nabove-1): # this is 0,1 ... start of list
                notes[a] += 12

        elif nbelow<nabove:
            for a in range(1,nabove-nbelow):   # this should give 1,2 
                notes[-a] -= 12                # and the - means end of list

        return None

    def key2center(self):
        """ This is the same as 'key', but the 9th, 11th, etc. are not modified.
            We do this by changing the note list so that 9th, etc. are deleted,
            the 'key' function is called and the deleted notes are restored.
        """
        
        orig = self.noteList[:]

        saved=[]
        self.noteList = []
        root = orig[0]
        for n in orig: 
            if n >= root+12:
                saved.append(n)
            else:
                self.noteList.append(n)
        self.keycenter()
        self.noteList.extend(saved)

        return None

    def rootkey(self):
        """ Modify the chord so that the notes are all close to, but above, the 
            root note (from the key sig).
        """
        
        key = keySig.keyNoteValue
        new = []

        for n in self.noteList: 
            while n<0:
                n+=12
            while n>12:
                n-=12

            if n-12 >= key:
                new.append(n-12)
            else:
                new.append(n)

        self.noteList = new

    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 #####




