Sunday, September 7, 2008

How I relax...

So if you know me, you know I've been working far too hard the last few weeks. On Friday, I got home and though I had a pile of work to do, I knew I couldn't do anything unless I let myself relax a bit first.

A bit of backstory — one thing I started doing this summer to relax was taking up guitar lessons again, which has been a good thing. One thing I've been working on is learning series of inversions. The thing I like about this exercise is that it allows me to generate a limitless number of new chords, which means I can now help myself get un-bored when I find myself in a guitar rut. Lately, for example, I've been taking some songs I wrote that have very simple progressions (just C and G basically) and playing with different inversions and voicings.

Anyway, I realized what I really wanted to do on Friday was to write a program to generate those series of inversions for me. Here's an example of how it works...
./inverter.py 3x043x
Printing inversions of 3x043x
--x--x--x-
--3--8-12-
--4--7-12-
--0--5--9-
--x--x--x-
--3--7-10-

In that case, there are three inversions generated because it's a three note chord. If I give it a chord with four notes, it will of course generate four inversions:
tom@hydrophax:~/Projects/chord_inverter$ ./inverter.py 3x443x
Printing inversions of 3x443x
--x--x--x--x-
--3--7--8-12-
--4--7-11-12-
--4--5--9-12-
--x--x--x--x-
--3--7-10-14-

I can also make it print out the notes so I can keep track of where the root is:
$ ./inverter.py --include-notes x3201x
Printing inversions of x3201x
--x------x------x-----
--1-(C)--5-(E)--8-(G)-
--0-(G)--5-(C)--9-(E)-
--2-(E)--5-(G)-10-(C)-
--3-(C)--7-(E)-10-(G)-
--x------x------x-----

And of course it can move down as well as up:
$ ./inverter.py --down xxx988
Printing inversions of xxx988
--8--3--0-
--8--5--1-
--9--5--0-
--x--x--x-
--x--x--x-
--x--x--x-

Finally, it handles alternate tunings -- here's the inversions of D in drop-D tuning for example (which, by the way, illustrate that not all inversions are pleasant to finger -- check out the second chord):
$ ./inverter.py --tuning DADGBE 0xx323
Printing inversions of 0xx323
--3--6--9-10-
--2--3--8-11-
--3--6--7-12-
--x--x--x--x-
--x--x--x--x-
--0--5--8-11-

This means it will work for any fretted instrument.

The next step would be to have it name the chords, but of course that's kind of complicated. One of the things that pops out of this sort of exercise are "oh duh" moments like when I generated this series based on Am7 and found that the 2nd chord was already fixed in my muscle memory:
--x--x--x--x-
--5--8-10-13-
--5--9-12-14-
--5--7-10-14-
--x--x--x--x-
--5--8-12-15-

It took me a second until I realized something I already knew: an Am7 has all the same notes as a C6, which is what I'm used to calling 8x798x...

The next step would be to cook up a web interface and make this into a handy tool for everyone. In the mean time, the full source code follows... (by the way, if anyone knows how to attach a generic file to a post or do the equivalent of an lj-clip on blogger, please let me know!)

#!/usr/bin/env python

import types
UP = 1
DOWN = -1
notes = ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B']
synonyms = {'C#':'Db','D#':'Eb','F#':'Gb','G#':'Ab','A#':'Bb'}
x = None

def get_degree (note):
'''Return the numeric degree of the note named note.
'''
return notes.index(synonyms.get(note,note))

def get_name (degree):
'''Return the name of the note from the degree'''
abs_degree = degree % 12
return notes[abs_degree]

def add_to_note (note, steps):
return get_name(get_degree(note)+steps)

def get_interval (a,b):
"""Return the interval in chromatic steps between two notes
"""
a = get_degree(a)
b = get_degree(b)
if a > b:
b += 12
return b - a

def pad (txt, length, pad_char = '-'):
if len(txt) > length:
raise ValueError
elif len(txt) == length:
return txt
else:
pad_with = length - len(txt)
left = pad_with / 2
right = pad_with / 2 + pad_with % 2
return pad_char*left + txt + pad_char*right

class String:

def __init__ (self, root='A'):
self.root = root
self.nroot = get_degree(self.root)

def get_note_for_fret (self, n):
return get_name(self.get_degree_for_fret(n))

def get_degree_for_fret (self, n):
return self.nroot + n

def get_next_fret_in_chord (self, start, chord, direction=UP):
for n in chord:
try:
assert(n in notes or n in synonyms)
except:
raise ValueError('Chord contains invalid note %s'%n)
if start is None: return start
n = start + direction
chord = [synonyms.get(note,note) for note in chord]
while self.get_note_for_fret(n) not in chord:
n += direction
return n

def get_fingering (fingering):
if type(fingering) in types.StringTypes:
fingering_ltrs = fingering[:]; fingering = []
for char in fingering_ltrs:
if char in ('x','X'):
fingering.append(x)
else:
fingering.append(int(char))
return fingering

def fingering_to_txt (fingering):
s = ''
for f in fingering:
if f is None: s += 'x'
else: s += str(f)
s += ' '
return s[:-1]

class FretBoard:

def __init__ (self, strings='EADGBE'):
self.strings = [String(n) for n in strings]


def fingering_to_chord (self, fingering):
fingering = get_fingering(fingering)
chord = []
for fret,string in zip(fingering,self.strings):
if fret is not None:
chord.append(
string.get_note_for_fret(fret)
)
return chord

def invert_chord (self, fingering, direction=UP, chord=None):
fingering = get_fingering(fingering)
if not chord:
chord = self.fingering_to_chord(fingering)
next_fingering = []
for fret,s in zip(fingering,self.strings):
if fret is None:
next_fingering.append(x)
else:
next_fingering.append(
s.get_next_fret_in_chord(fret,
chord,
direction)
)
return next_fingering

def get_inversions (self, fingering, direction=UP):
chord = self.fingering_to_chord(fingering)
n_fingerings = len(set(chord))
fingerings = [get_fingering(fingering)]
for n in range(n_fingerings - 1):
fingering = self.invert_chord(fingering,
direction,
chord)
fingerings.append(get_fingering(fingering))
return fingerings

def print_inversion_series (self, fingering,direction=UP,include_notes=False):
fingerings = self.get_inversions(fingering,direction)
lines = []
for n,s in enumerate(self.strings):
line = '-'
for fing in fingerings:
fret = fing[n]
if fret is not None:
note = '(' + s.get_note_for_fret(int(fret)) + ')'
fret = str(fret)
else:
note = ''; fret='x'
if include_notes:
line += pad(fret,3) + pad(note,4)
else:
line += pad(fret,3)
lines.append(line)
lines.reverse()
for l in lines: print l


if __name__ == '__main__':

# If called from the commandline, treat our argument as a chord
# and print the series of inversions...
import optparse

parser = optparse.OptionParser(option_list=[
optparse.make_option('--tuning',
action='store',
type='string',
dest='tuning',
default=None,
help='A custom tuning'),

optparse.make_option('--down',dest='direction',default=UP,action='store_const',const=DOWN,help='Print the series of inversions moving up the neck (down in pitch). Default is to move down the neck (up in pitch'),

optparse.make_option('--include-notes',default=False,const=True,action='store_const',dest='include_notes')
]
)
(options,args) = parser.parse_args()

if False:
1
if options.tuning:
fb = FretBoard(strings=options.tuning)
else:
fb = FretBoard()
include_notes = options.include_notes
direction = options.direction
for a in args:
try:
print 'Printing inversions of ',a
fb.print_inversion_series(a,
include_notes=include_notes,
direction=direction)
except:
import traceback; traceback.print_exception()