# -*- coding: utf-8 -*-
from .constants import NOTE_VAL_DICT, VAL_NOTE_DICT
from .constants.scales import RELATIVE_KEY_DICT
from .parser import parse
from .quality import QualityManager
from .utils import transpose_note, display_appended, display_on, note_to_val, val_to_note
[docs]class Chord(object):
""" Class to handle a chord.
:param str _chord: Name of the chord. (e.g. C, Am7, F#m7-5/A)
:param str _root: The root note of chord.
:param pychord.Quality _quality: The quality of chord. (e.g. m7, 6, M9, ...)
:param list[str] _appended: The appended notes on chord.
:param str _on: The base note of slash chord.
"""
def __init__(self, chord):
""" Constructor of Chord instance
:param str chord: Name of chord.
"""
self._chord = chord
self._root, self._quality, self._appended, self._on = "", "", "", ""
self._parse(chord)
def __unicode__(self):
return self._chord
def __str__(self):
return self._chord
def __repr__(self):
return "<Chord: {}>".format(self._chord)
def __eq__(self, other):
if not isinstance(other, Chord):
raise TypeError("Cannot compare Chord object with {} object".format(type(other)))
if note_to_val(self._root) != note_to_val(other.root):
return False
if self._quality != other.quality:
return False
if self._appended != other.appended:
return False
if self._on and other.on:
if note_to_val(self._on) != note_to_val(other.on):
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
[docs] @classmethod
def from_note_index(cls, note, quality, scale, diatonic=False):
""" Create a Chord from note index in a scale
Chord.from_note_index(1, "", "Cmaj") returns I of C major => Chord("C")
Chord.from_note_index(3, "m7", "Fmaj") returns IIImin of F major => Chord("Am7")
Chord.from_note_index(5, "7", "Amin") returns Vmin of A minor => Chord("E7")
:param int note: Note index in a Scale I, II, ..., VIII
:param str quality: Quality of a chord (m7, sus4, ...)
:param str scale: Base scale (Cmaj, Amin, F#maj, Ebmin, ...)
:rtype: Chord
"""
if not 1 <= note <= 8:
raise ValueError("Invalid note {}".format(note))
relative_key = RELATIVE_KEY_DICT[scale[-3:]][note - 1]
root_num = NOTE_VAL_DICT[scale[:-3]]
root = VAL_NOTE_DICT[(root_num + relative_key) % 12][0]
scale_degrees = RELATIVE_KEY_DICT[scale[-3:]]
if diatonic:
# construct the chord based on scale degrees, within 1 octave
third = scale_degrees[(note + 1) % 7]
fifth = scale_degrees[(note + 3) % 7]
seventh = scale_degrees[(note + 5) % 7]
# adjust the chord to its root position (as a stack of thirds),
# then set the root to 0
def get_diatonic_chord(chord):
uninverted = []
for note in chord:
if not uninverted:
uninverted.append(note)
elif note > uninverted[-1]:
uninverted.append(note)
else:
uninverted.append(note + 12)
uninverted = [x - uninverted[0] for x in uninverted]
return uninverted
if quality in ["", "-", "maj", "m", "min"]:
triad = (relative_key, third, fifth)
q = get_diatonic_chord(triad)
elif quality in ["7", "M7", "maj7", "m7"]:
seventh_chord = (relative_key, third, fifth, seventh)
q = get_diatonic_chord(seventh_chord)
else:
raise NotImplementedError("Only generic chords (triads, sevenths) are supported")
# look up QualityManager to determine chord quality
quality_manager = QualityManager()
quality = quality_manager.find_quality_from_components(q)
if not quality:
raise RuntimeError("Quality with components {} not found".format(q))
return cls("{}{}".format(root, quality))
@property
def chord(self):
""" The name of chord """
return self._chord
@property
def root(self):
""" The root note of chord """
return self._root
@property
def quality(self):
""" The quality of chord """
return self._quality
@property
def appended(self):
""" The appended notes on chord """
return self._appended
@property
def on(self):
""" The base note of slash chord """
return self._on
[docs] def info(self):
""" Return information of chord to display """
return """{}
root={}
quality={}
appended={}
on={}""".format(self._chord, self._root, self._quality, self._appended, self._on)
[docs] def transpose(self, trans, scale="C"):
""" Transpose the chord
:param int trans: Transpose key
:param str scale: key scale
:return:
"""
if not isinstance(trans, int):
raise TypeError("Expected integers, not {}".format(type(trans)))
self._root = transpose_note(self._root, trans, scale)
if self._on:
self._on = transpose_note(self._on, trans, scale)
self._reconfigure_chord()
[docs] def components(self, visible=True):
""" Return the component notes of chord
:param bool visible: returns the name of notes if True else list of int
:rtype: list[(str or int)]
:return: component notes of chord
"""
if self._on:
self._quality.append_on_chord(self.on, self.root)
return self._quality.get_components(root=self._root, visible=visible)
[docs] def components_with_pitch(self, root_pitch):
""" Return the component notes of chord formatted like ["C4", "E4", "G4"]
:param int root_pitch: the pitch of the root note
:rtype: list[str]
:return: component notes of chord
"""
if self._on:
self._quality.append_on_chord(self.on, self.root)
components = self._quality.get_components(root=self._root)
if components[0] < 0:
components = [c + 12 for c in components]
return ["{}{}".format(val_to_note(c, scale=self._root), root_pitch + c // 12) for c in components]
def _parse(self, chord):
""" parse a chord
:param str chord: Name of chord.
"""
root, quality, appended, on = parse(chord)
self._root = root
self._quality = quality
self._appended = appended
self._on = on
def _reconfigure_chord(self):
# TODO: Use appended
self._chord = "{}{}{}{}".format(self._root,
self._quality._quality,
display_appended(self._appended),
display_on(self._on))
[docs]def as_chord(chord):
""" convert from str to Chord instance if input is str
:type chord: str|pychord.Chord
:param chord: Chord name or Chord instance
:rtype: pychord.Chord
:return: Chord instance
"""
if isinstance(chord, Chord):
return chord
elif isinstance(chord, str):
return Chord(chord)
else:
raise TypeError("input type should be str or Chord instance.")