diff --git a/pychord/chord.py b/pychord/chord.py index 90becc0..468923e 100644 --- a/pychord/chord.py +++ b/pychord/chord.py @@ -2,6 +2,7 @@ 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 @@ -50,7 +51,7 @@ def __ne__(self, other): return not self.__eq__(other) @classmethod - def from_note_index(cls, note, quality, scale): + 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") @@ -67,6 +68,44 @@ def from_note_index(cls, note, quality, scale): 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 diff --git a/test/test_chord.py b/test/test_chord.py index 16c3075..0c2b52c 100644 --- a/test/test_chord.py +++ b/test/test_chord.py @@ -123,6 +123,26 @@ def test_note_9(self): with self.assertRaises(ValueError): Chord.from_note_index(note=9, quality="", scale="Fmaj") + def test_diatonic_note_1(self): + chord = Chord.from_note_index(note=1, quality="", diatonic=True, scale="Dmaj") + self.assertEqual(chord, Chord("D")) + + def test_diatonic_note_2_mode(self): + chord = Chord.from_note_index(note=2, quality="7", diatonic=True, scale="BLoc") + self.assertEqual(chord, Chord("Cmaj7")) + + def test_diatonic_note_3_mode(self): + chord = Chord.from_note_index(note=3, quality="m", diatonic=True, scale="G#Mix") + self.assertEqual(chord, Chord("Cdim")) + + def test_diatonic_note_4_mode(self): + chord = Chord.from_note_index(note=4, quality="-", diatonic=True, scale="AbDor") + self.assertEqual(chord, Chord("C#")) + + def test_diatonic_note_nongeneric(self): + with self.assertRaises(NotImplementedError): + Chord.from_note_index(note=5, quality="sus", diatonic=True, scale="Fmaj") + if __name__ == '__main__': unittest.main()