Skip to content

Commit

Permalink
Merge branch 'release-0.0.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
pignacio committed Feb 15, 2015
2 parents ad246c8 + b378794 commit e255299
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 102 deletions.
5 changes: 0 additions & 5 deletions chorddb/chords/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ def variation(self):
def bass(self):
return self._bass

@memoize
def variation_keys(self):
return set([self.key.transpose(interval)
for interval in VARIATIONS_NOTES[self.variation]])

def text(self):
bass = ('' if self._key == self._bass
else "/{}".format(self._bass.text()))
Expand Down
155 changes: 88 additions & 67 deletions chorddb/chords/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,89 +4,110 @@
@author: ignacio
'''

import collections

from .variations import VARIATIONS_NOTES


PositionedNote = collections.namedtuple('PositionedNote', ['position', 'note'])


def find_fingerings(instrument, keys):
for positions in search(instrument, keys):
start = positions.count(None)
fingering = [x.position for x in positions[start:]]
yield Fingering(fingering, instrument, start)


BactrackState = collections.namedtuple('BactrackState', ['keys', 'valid_notes',
'current', 'res'])


def search(instrument, keys):
return _backtrack(BactrackState(
keys=set(keys),
valid_notes=_get_valid_notes(instrument, keys),
current=[],
res=[]
))

class ChordFinder(object):
def __init__(self, instrument, chord):
self._instrument = instrument
self._chord = chord if instrument.has_bass else chord.bassless()
self._keys = chord.variation_keys()
self._keys.add(chord.bass)
self._res = []
self._string_notes = self._get_string_notes()
self._search()

def _search(self):
self._backtrack([])

def _backtrack(self, current):
if set(c[1].key for c in current if c) == self._keys: # is valid
self._res.append(list(current))
if len(current) == len(self._string_notes): # no childs
return

used = sorted([c[0] for c in current if c and c[0]])
maxi = used[-1] if used else 0
mini = used[0] if used else 10000

for pos, note in self._string_notes[len(current)]:
if maxi - pos > 4 or pos - mini > 4: # Stretch basic test
continue
current.append((pos, note))
self._backtrack(current)
current.pop()

# Consider empty:
if all(c is None for c in current):
current.append(None)
self._backtrack(current)
current.pop()

def fingerings(self):
for positions in self._res:
start = positions.count(None)
fingering = [x[0] for x in positions[start:]]
yield Fingering(fingering, self._instrument, start)

def _get_string_notes(self):
string_notes = []
for keyoctave in self._instrument.keyoctaves:
string_notes.append([])
for index in xrange(self._instrument.frets):
key = keyoctave.transpose(index)
if key.key in self._keys:
string_notes[-1].append((index, key))
return string_notes

@classmethod
def find(cls, instrument, chord):
return cls(instrument, chord).fingerings()

def _backtrack(state):
if set(c.note.key for c in state.current if c) == state.keys: # is valid
state.res.append(list(state.current)) # make a copy
if len(state.current) == len(state.valid_notes): # no childs
return

used = sorted([c.position for c in state.current if c and c.position])
maxi = used[-1] if used else 0
mini = used[0] if used else 10000

for posnote in state.valid_notes[len(state.current)]:
if maxi - posnote.position > 4 or posnote.position - mini > 4:
# Stretch basic test
continue
state.current.append(posnote)
_backtrack(state)
state.current.pop()

# Consider empty start:
if all(c is None for c in state.current):
state.current.append(None)
_backtrack(state)
state.current.pop()

return state.res

def _get_valid_notes(instrument, keys):
keys = set(keys)
valid_notes = []
for keyoctave in instrument.keyoctaves:
keyoctaves = (keyoctave.transpose(t) for t in xrange(instrument.frets))
valid_keyoctaves = (PositionedNote(position=i, note=ko)
for i, ko in enumerate(keyoctaves)
if ko.key in keys)
valid_notes.append(list(valid_keyoctaves))
return valid_notes


def get_fingerings(chord, instrument):
return list(_get_fingerings(chord, instrument))


def _get_fingerings(chord, instrument):
if not instrument.has_bass:
chord = chord.bassless() # Remove bass if present
for fingering in ChordFinder.find(instrument, chord):
if instrument.has_bass:
if fingering.bass().key != chord.bass:
continue
if chord.bass not in chord.variation_keys():
bass_count = len([ko for ko in fingering.keyoctaves()
if ko.key == chord.bass])
if bass_count > 1:

variations = instrument.variation_overrides.get(
chord.variation, VARIATIONS_NOTES[chord.variation])
for variation in variations:
keys = set(chord.key.transpose(v) for v in variation)
bass_not_in_variation = False
if instrument.has_bass and not chord.bass in keys:
keys.add(chord.bass)
bass_not_in_variation = True

for fingering in find_fingerings(instrument, keys):
if instrument.has_bass:
if fingering.bass().key != chord.bass:
continue
yield fingering
if bass_not_in_variation:
bass_count = len([ko for ko in fingering.keyoctaves()
if ko.key == chord.bass])
if bass_count > 1:
continue
yield fingering


def _max(fingering):
return max(x[0] for x in fingering if x[0] > 0)
return max(x.position for x in fingering if x.position > 0)


def _min(fingering):
return min(x[0] for x in fingering if x[0] > 0)
return min(x.position for x in fingering if x.position > 0)


class Fingering(object):

def __init__(self, positions, instrument, start=None):
self._instrument = instrument
self._positions = positions
Expand Down
30 changes: 18 additions & 12 deletions chorddb/chords/variations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@
from ..notes import Key


def map_variations_to_intervals(variations):
return[
[Key.parse(note).ord() for note in variation]
for variation in variations]



_VARIATIONS_NOTES = {
None: ["A", "C#", "E"],
"7": ["A", "C#", "E", "G"],
"m": ["A", "C", "E"],
"m7": ["A", "C", "E", "G"],
"maj7": ["A", "C#", "E", "G#"],
"dim": ["A", "C", "Eb"],
"aug": ["A", "C#", "F"],
"sus4": ["A", "D", "E"],
"m7": ["A", "C", "E", "G"],
"m7b5": ["A", "C", "Eb", "G"],
None: [["A", "C#", "E"]],
"7": [["A", "C#", "E", "G"]],
"m": [["A", "C", "E"]],
"m7": [["A", "C", "E", "G"]],
"maj7": [["A", "C#", "E", "G#"]],
"dim": [["A", "C", "Eb"]],
"aug": [["A", "C#", "F"]],
"sus4": [["A", "D", "E"]],
"m7b5": [["A", "C", "Eb", "G"]],
}
VARIATIONS_NOTES = {v: [Key.parse(n).ord() for n in ns]
for (v, ns) in _VARIATIONS_NOTES.items()}
VARIATIONS_NOTES = {v: map_variations_to_intervals(nss)
for (v, nss) in _VARIATIONS_NOTES.items()}

VARIATIONS = sorted((v for v in _VARIATIONS_NOTES if v), reverse=True)
VARIATIONS_RE = "|".join(VARIATIONS)
1 change: 1 addition & 0 deletions chorddb/curses/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def _get_division_blits(vertical_division, chordpad_width, chordpad_height,


def _update_chord_pad(chord_pad, state):
chord_pad.clear()
chord = state.current_indexed_chord().chord
version = state.get_chord_version(chord)
version_count = len(state.chord_versions.get(chord, []))
Expand Down
51 changes: 37 additions & 14 deletions chorddb/instrument.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from .notes import KeyOctave, Key
from .chords.variations import map_variations_to_intervals, VARIATIONS_NOTES


class Instrument(object):
def __init__(self, keyoctaves, frets, has_bass=True):
def __init__(self, name, keyoctaves, frets, has_bass=True,
variation_overrides=None):
self._name = name
self._keyoctaves = keyoctaves
self._frets = frets
self._has_bass = has_bass
self._variation_overrides = variation_overrides or {}

@property
def name(self):
return self._name

@property
def keyoctaves(self):
Expand All @@ -19,8 +27,12 @@ def frets(self):
def has_bass(self):
return self._has_bass

@property
def variation_overrides(self):
return self._variation_overrides

@classmethod
def parse(cls, keys, frets, **kwargs):
def parse(cls, name, keys, frets, **kwargs):
keyoctaves = []
for key in keys:
if not keyoctaves:
Expand All @@ -31,7 +43,7 @@ def parse(cls, keys, frets, **kwargs):
keyoctaves.append(KeyOctave(key, current.octave + 1))
else:
keyoctaves.append(KeyOctave(key, current.octave))
return cls(keyoctaves, frets, **kwargs)
return cls(name, keyoctaves, frets, **kwargs)

@classmethod
def from_name(cls, name, default=None):
Expand All @@ -42,24 +54,35 @@ def from_name(cls, name, default=None):
return default
raise

def __len__(self):
return len(self._keyoctaves)

def capo(self, capo_position):
return Instrument(
"{}(Capo: {})".format(self.name, capo_position),
[ko.transpose(capo_position) for ko in self.keyoctaves],
self.frets - capo_position,
self.has_bass
)

def __str__(self):
return "Instrument: {s.name}".format(s=self)

GUITAR = Instrument.parse([Key(k) for k in list("EADGBE")], 10, has_bass=True)
LOOG = Instrument.parse([Key(k) for k in list("GBE")], 10, has_bass=False)
UKELELE = Instrument([KeyOctave.parse(k) for k in ["G0", "C0", "E0", "A1"]],
10, has_bass=False)
def __len__(self):
return len(self._keyoctaves)

INSTRUMENTS = {
'guitar': GUITAR,
'loog': LOOG,
'ukelele': UKELELE,

LOOG_VARIATION_OVERRIDES = {
'7': map_variations_to_intervals([['G', 'C#', 'E'], ["A", "C#", "G"]]),
'm7': VARIATIONS_NOTES['m'],
}


GUITAR = Instrument.parse('guitar', [Key(k) for k in list("EADGBE")], 10,
has_bass=True)
LOOG = Instrument.parse('loog', [Key(k) for k in list("GBE")], 10,
has_bass=False,
variation_overrides=LOOG_VARIATION_OVERRIDES)
UKELELE = Instrument('ukelele',
[KeyOctave.parse(k) for k in ["G0", "C0", "E0", "A1"]],
10, has_bass=False)


INSTRUMENTS = {i.name: i for i in [GUITAR, LOOG, UKELELE]}
2 changes: 1 addition & 1 deletion chorddb/terminal/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _render_chord_line(line, instrument):
_write_to_buff(buff, chord.chord.text(), color.CYAN,
style=color.STYLE_BRIGHT)
if chord.fingering:
size += _write_to_buff(buff, "({})".format(chord.fingering),
size += _write_to_buff(buff, "({}) ".format(chord.fingering),
color.RED)
return buff.getvalue()

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# http://packaging.python.org/en/latest/tutorial.html#version
version='0.0.2',
version='0.0.3',

description='A tablature parser / chord database',
long_description=long_description,
Expand Down
34 changes: 34 additions & 0 deletions test/test_chords/test_finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

import logging

from nose.tools import ok_


from chorddb.instrument import GUITAR, UKELELE, LOOG
from chorddb.chords.finder import get_fingerings
from chorddb.chords import Chord
from chorddb.notes import Key


logger = logging.getLogger(__name__) # pylint: disable=invalid-name


_TEST_VARIATIONS = [None, "m", "7", "m7"]


def _test_variation_has_fingerings(variation, instrument):
chord = Chord(Key.parse("C"), variation=variation)
ok_(len(get_fingerings(chord, instrument)) > 0,
"Variation has no fingerings! variation={}, instrument={}".format(
variation, instrument))


def test_variation_have_fingerings():
for instrument in [GUITAR, UKELELE, LOOG]:
for variation in _TEST_VARIATIONS:
yield _test_variation_has_fingerings, variation, instrument


4 changes: 2 additions & 2 deletions test/test_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def basic_capo_test():
capo_position = 2
keyoctaves = [KeyOctave.parse(x + "0") for x in "ABCDEFG"]
transposed = [ko.transpose(capo_position) for ko in keyoctaves]
with_bass = Instrument(keyoctaves, 30, True)
without_bass = Instrument(keyoctaves, 30, False)
with_bass = Instrument("test", keyoctaves, 30, True)
without_bass = Instrument("test", keyoctaves, 30, False)
for original in [with_bass, without_bass]:
capoed = original.capo(capo_position)
eq_(capoed.has_bass, original.has_bass)
Expand Down

0 comments on commit e255299

Please sign in to comment.