pychord 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ # An example to create MIDI file with PyChord and pretty_midi
2
+ # Prerequisite: pip install pretty_midi
3
+ # pretty_midi: https://github.com/craffel/pretty-midi
4
+
5
+
6
+ import pretty_midi
7
+
8
+ from pychord import Chord
9
+
10
+
11
+ def create_midi(chords):
12
+ midi_data = pretty_midi.PrettyMIDI()
13
+ piano_program = pretty_midi.instrument_name_to_program('Acoustic Grand Piano')
14
+ piano = pretty_midi.Instrument(program=piano_program)
15
+ length = 1
16
+ for n, chord in enumerate(chords):
17
+ for note_name in chord.components_with_pitch(root_pitch=4):
18
+ note_number = pretty_midi.note_name_to_number(note_name)
19
+ note = pretty_midi.Note(velocity=100, pitch=note_number, start=n * length, end=(n + 1) * length)
20
+ piano.notes.append(note)
21
+ midi_data.instruments.append(piano)
22
+ midi_data.write('chord.mid')
23
+
24
+
25
+ def main():
26
+ chords_str = ["C", "Dm7", "G", "C"]
27
+ chords = [Chord(c) for c in chords_str]
28
+ create_midi(chords)
29
+
30
+
31
+ if __name__ == '__main__':
32
+ main()
pychord/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ # flake8: noqa
2
+ from .analyzer import find_chords_from_notes
3
+ from .chord import Chord
4
+ from .progression import ChordProgression
5
+ from .quality import Quality, QualityManager
pychord/analyzer.py ADDED
@@ -0,0 +1,64 @@
1
+ from typing import List
2
+
3
+ from .chord import Chord
4
+ from .quality import QualityManager
5
+ from .utils import note_to_val
6
+
7
+
8
+ def find_chords_from_notes(notes: List[str]) -> List[Chord]:
9
+ """ Find possible chords consisted from notes
10
+
11
+ :param notes: List of note arranged from lower note. ex) ["C", "Eb", "G"]
12
+ :return: List of chord
13
+ """
14
+ if not notes:
15
+ raise ValueError("Please specify notes which consist a chord.")
16
+ root = notes[0]
17
+ root_and_positions = []
18
+ for rotated_notes in get_all_rotated_notes(notes):
19
+ rotated_root = rotated_notes[0]
20
+ root_and_positions.append((rotated_root, notes_to_positions(rotated_notes, rotated_notes[0])))
21
+ chords = []
22
+ for temp_root, positions in root_and_positions:
23
+ quality = QualityManager().find_quality_from_components(positions)
24
+ if quality is None:
25
+ continue
26
+ if temp_root == root:
27
+ chord = "{}{}".format(root, quality)
28
+ else:
29
+ chord = "{}{}/{}".format(temp_root, quality, root)
30
+ chords.append(Chord(chord))
31
+ return chords
32
+
33
+
34
+ def notes_to_positions(notes: List[str], root: str) -> List[int]:
35
+ """ Get notes positions from the root note
36
+
37
+ >>> notes_to_positions(["C", "E", "G"], "C")
38
+ [0, 4, 7]
39
+
40
+ :param notes: List of notes
41
+ :param root: Root note
42
+ :return: List of note positions
43
+ """
44
+ root_pos = note_to_val(root)
45
+ current_pos = root_pos
46
+ positions = []
47
+ for note in notes:
48
+ note_pos = note_to_val(note)
49
+ if note_pos < current_pos:
50
+ note_pos += 12 * ((current_pos - note_pos) // 12 + 1)
51
+ positions.append(note_pos - root_pos)
52
+ current_pos = note_pos
53
+ return positions
54
+
55
+
56
+ def get_all_rotated_notes(notes: List[str]) -> List[List[str]]:
57
+ """ Get all rotated notes
58
+
59
+ get_all_rotated_notes([A,C,E]) -> [[A,C,E],[C,E,A],[E,A,C]]
60
+ """
61
+ notes_list = []
62
+ for x in range(len(notes)):
63
+ notes_list.append(notes[x:] + notes[:x])
64
+ return notes_list
pychord/chord.py ADDED
@@ -0,0 +1,206 @@
1
+ from typing import List, Union
2
+
3
+ from .constants import NOTE_VAL_DICT, VAL_NOTE_DICT
4
+ from .constants.scales import RELATIVE_KEY_DICT
5
+ from .parser import parse
6
+ from .quality import QualityManager, Quality
7
+ from .utils import transpose_note, display_appended, display_on, note_to_val, val_to_note
8
+
9
+
10
+ class Chord:
11
+ """ Class to handle a chord.
12
+
13
+ Attributes:
14
+ _chord: Name of the chord. (e.g. C, Am7, F#m7-5/A)
15
+ _root: The root note of chord. (e.g. C, A, F#)
16
+ _quality: The quality of chord. (e.g. maj, m7, m7-5)
17
+ _appended: The appended notes on chord.
18
+ _on: The base note of slash chord.
19
+ """
20
+
21
+ def __init__(self, chord: str):
22
+ """ Constructor of Chord instance
23
+
24
+ :param chord: Name of chord (e.g. C, Am7, F#m7-5/A).
25
+ """
26
+ root, quality, appended, on = parse(chord)
27
+ self._chord: str = chord
28
+ self._root: str = root
29
+ self._quality: Quality = quality
30
+ self._appended: List[str] = appended
31
+ self._on: str = on
32
+
33
+ self._append_on_chord()
34
+
35
+ def __unicode__(self):
36
+ return self._chord
37
+
38
+ def __str__(self):
39
+ return self._chord
40
+
41
+ def __repr__(self):
42
+ return f"<Chord: {self._chord}>"
43
+
44
+ def __eq__(self, other):
45
+ if not isinstance(other, Chord):
46
+ raise TypeError(f"Cannot compare Chord object with {type(other)} object")
47
+ if note_to_val(self._root) != note_to_val(other.root):
48
+ return False
49
+ if self._quality != other.quality:
50
+ return False
51
+ if self._appended != other.appended:
52
+ return False
53
+ if self._on and other.on:
54
+ if note_to_val(self._on) != note_to_val(other.on):
55
+ return False
56
+ return True
57
+
58
+ def __ne__(self, other):
59
+ return not self.__eq__(other)
60
+
61
+ @classmethod
62
+ def from_note_index(
63
+ cls,
64
+ note: int,
65
+ quality: str,
66
+ scale: str,
67
+ diatonic: bool = False,
68
+ chromatic: int = 0,
69
+ ) -> "Chord":
70
+ """Create a Chord from note index in a scale
71
+
72
+ Chord.from_note_index(1, "", "Cmaj") returns I of C major => Chord("C")
73
+ Chord.from_note_index(3, "m7", "Fmaj") returns IIImin of F major => Chord("Am7")
74
+ Chord.from_note_index(5, "7", "Amin") returns Vmin of A minor => Chord("E7")
75
+ Chord.from_note_index(2, "", "Cmaj") returns II of C major => Chord("D")
76
+ Chord.from_note_index(2, "m", "Cmaj") returns IImin of C major => Chord("Dm")
77
+ Chord.from_note_index(2, "", "Cmaj", diatonic=True) returns IImin of C major => Chord("Dm")
78
+ Chord.from_note_index(2, "", "Cmin", chromatic=-1) returns bII of C minor => Chord("Db")
79
+
80
+ :param note: Scale degree of the chord's root (1-7)
81
+ :param quality: Quality of the chord (e.g. m7, sus4)
82
+ :param scale: Base scale (e.g. Cmaj, Amin, F#maj, Ebmin)
83
+ :param diatonic: If True, chord quality is determined using the base scale (overrides :param quality)
84
+ :param chromatic: Lower or raise the scale degree (and all notes of the chord) by semitone(s)
85
+ """
86
+ if not 1 <= note <= 8:
87
+ raise ValueError(f"Invalid note {note}")
88
+ relative_key = RELATIVE_KEY_DICT[scale[-3:]][note - 1]
89
+ root_num = NOTE_VAL_DICT[scale[:-3]] + chromatic
90
+ root = VAL_NOTE_DICT[(root_num + relative_key) % 12][0]
91
+
92
+ scale_degrees = RELATIVE_KEY_DICT[scale[-3:]]
93
+
94
+ if diatonic:
95
+ # construct the chord based on scale degrees, within 1 octave
96
+ third = scale_degrees[(note + 1) % 7]
97
+ fifth = scale_degrees[(note + 3) % 7]
98
+ seventh = scale_degrees[(note + 5) % 7]
99
+
100
+ # adjust the chord to its root position (as a stack of thirds),
101
+ # then set the root to 0
102
+ # e.g. (9, 0, 4) -> [0, 3, 7]
103
+ def get_diatonic_chord(chord):
104
+ uninverted = []
105
+ for note in chord:
106
+ if not uninverted:
107
+ uninverted.append(note)
108
+ elif note > uninverted[-1]:
109
+ uninverted.append(note)
110
+ else:
111
+ uninverted.append(note + 12)
112
+ uninverted = [x - uninverted[0] for x in uninverted]
113
+ return uninverted
114
+
115
+ if quality in ["", "-", "maj", "m", "min"]:
116
+ triad = (relative_key, third, fifth)
117
+ q = get_diatonic_chord(triad)
118
+ elif quality in ["7", "M7", "maj7", "m7"]:
119
+ seventh_chord = (relative_key, third, fifth, seventh)
120
+ q = get_diatonic_chord(seventh_chord)
121
+ else:
122
+ raise NotImplementedError("Only generic chords (triads, sevenths) are supported")
123
+
124
+ # look up QualityManager to determine chord quality
125
+ quality_manager = QualityManager()
126
+ quality = quality_manager.find_quality_from_components(q)
127
+ if not quality:
128
+ raise RuntimeError(f"Quality with components {q} not found")
129
+
130
+ return cls(f"{root}{quality}")
131
+
132
+ @property
133
+ def chord(self):
134
+ """ The name of chord """
135
+ return self._chord
136
+
137
+ @property
138
+ def root(self):
139
+ """ The root note of chord """
140
+ return self._root
141
+
142
+ @property
143
+ def quality(self):
144
+ """ The quality of chord """
145
+ return self._quality
146
+
147
+ @property
148
+ def appended(self):
149
+ """ The appended notes on chord """
150
+ return self._appended
151
+
152
+ @property
153
+ def on(self):
154
+ """ The base note of slash chord """
155
+ return self._on
156
+
157
+ def info(self):
158
+ """ Return information of chord to display """
159
+ return f"""{self._chord}
160
+ root={self._root}
161
+ quality={self._quality}
162
+ appended={self._appended}
163
+ on={self._on}"""
164
+
165
+ def transpose(self, trans: int, scale: str = "C") -> None:
166
+ """ Transpose the chord
167
+
168
+ :param trans: Transpose key
169
+ :param scale: key scale
170
+ """
171
+ if not isinstance(trans, int):
172
+ raise TypeError(f"Expected integers, not {type(trans)}")
173
+ self._root = transpose_note(self._root, trans, scale)
174
+ if self._on:
175
+ self._on = transpose_note(self._on, trans, scale)
176
+ self._reconfigure_chord()
177
+
178
+ def components(self, visible: bool = True) -> Union[List[str], List[int]]:
179
+ """ Return the component notes of chord
180
+
181
+ :param visible: returns the name of notes if True else list of int
182
+ :return: component notes of chord
183
+ """
184
+ return self._quality.get_components(root=self._root, visible=visible)
185
+
186
+ def components_with_pitch(self, root_pitch: int) -> List[str]:
187
+ """ Return the component notes of chord formatted like ["C4", "E4", "G4"]
188
+
189
+ :param root_pitch: the pitch of the root note
190
+ :return: component notes of chord
191
+ """
192
+ components = self._quality.get_components(root=self._root)
193
+ if components[0] < 0:
194
+ components = [c + 12 for c in components]
195
+ return [f"{val_to_note(c, scale=self._root)}{root_pitch + c // 12}" for c in components]
196
+
197
+ def _append_on_chord(self):
198
+ if self._on:
199
+ self._quality.append_on_chord(self.on, self.root)
200
+
201
+ def _reconfigure_chord(self):
202
+ # TODO: Use appended
203
+ self._chord = "{}{}{}{}".format(self._root,
204
+ self._quality.quality,
205
+ display_appended(self._appended),
206
+ display_on(self._on))
@@ -0,0 +1,2 @@
1
+ # flake8: noqa
2
+ from .scales import NOTE_VAL_DICT, VAL_NOTE_DICT, SCALE_VAL_DICT
@@ -0,0 +1,115 @@
1
+ # Do not import DEFAULT_QUALITIES directly
2
+ # Use QualityManager instead
3
+ DEFAULT_QUALITIES = [
4
+ # chords consist of 2 notes
5
+ ('5', (0, 7)),
6
+ ('no5', (0, 4)),
7
+ ('omit5', (0, 4)),
8
+ ('m(no5)', (0, 3)),
9
+ ('m(omit5)', (0, 3)),
10
+ # 3 notes
11
+ ('', (0, 4, 7)),
12
+ ('maj', (0, 4, 7)),
13
+ ('m', (0, 3, 7)),
14
+ ('min', (0, 3, 7)),
15
+ ('-', (0, 3, 7)),
16
+ ('dim', (0, 3, 6)),
17
+ # Not to confuse Ab5 with A(b5)
18
+ ('(b5)', (0, 4, 6)),
19
+ ('aug', (0, 4, 8)),
20
+ ('sus2', (0, 2, 7)),
21
+ ('sus4', (0, 5, 7)),
22
+ ('sus', (0, 5, 7)),
23
+ # 4 notes
24
+ ('6', (0, 4, 7, 9)),
25
+ ('6b5', (0, 4, 6, 9)), # https://www.scales-chords.com/chord/piano/C%236b5
26
+ ('6-5', (0, 4, 6, 9)), # https://www.scales-chords.com/chord/piano/C%236b5
27
+ ('7', (0, 4, 7, 10)),
28
+ ('7-5', (0, 4, 6, 10)),
29
+ ('7b5', (0, 4, 6, 10)),
30
+ ('7+5', (0, 4, 8, 10)),
31
+ ('7#5', (0, 4, 8, 10)),
32
+ ('7sus4', (0, 5, 7, 10)),
33
+ ('m6', (0, 3, 7, 9)),
34
+ ('m7', (0, 3, 7, 10)),
35
+ ('m7-5', (0, 3, 6, 10)),
36
+ ('m7b5', (0, 3, 6, 10)),
37
+ ('m7+5', (0, 3, 8, 10)),
38
+ ('m7#5', (0, 3, 8, 10)),
39
+ ('dim6', (0, 3, 6, 8)),
40
+ ('dim7', (0, 3, 6, 9)),
41
+ ('M7', (0, 4, 7, 11)),
42
+ ('maj7', (0, 4, 7, 11)),
43
+ ('maj7+5', (0, 4, 8, 11)),
44
+ ('M7+5', (0, 4, 8, 11)),
45
+ ('mmaj7', (0, 3, 7, 11)),
46
+ ('mM7', (0, 3, 7, 11)),
47
+ ('add4', (0, 4, 5, 7)),
48
+ ('majadd4', (0, 4, 5, 7)),
49
+ ('Madd4', (0, 4, 5, 7)),
50
+ ('madd4', (0, 3, 5, 7)),
51
+ ('add9', (0, 4, 7, 14)),
52
+ ('majadd9', (0, 4, 7, 14)),
53
+ ('Madd9', (0, 4, 7, 14)),
54
+ ('madd9', (0, 3, 7, 14)),
55
+ ('sus4add9', (0, 5, 7, 14)),
56
+ ('sus4add2', (0, 2, 5, 7)),
57
+ ('2', (0, 4, 7, 14)),
58
+ ('add11', (0, 4, 7, 17)),
59
+ ('4', (0, 4, 7, 17)),
60
+ # 5 notes
61
+ ('m69', (0, 3, 7, 9, 14)),
62
+ ('69', (0, 4, 7, 9, 14)),
63
+ ('9', (0, 4, 7, 10, 14)),
64
+ ('m9', (0, 3, 7, 10, 14)),
65
+ ('M9', (0, 4, 7, 11, 14)),
66
+ ('maj9', (0, 4, 7, 11, 14)),
67
+ ('9sus4', (0, 5, 7, 10, 14)),
68
+ ('7-9', (0, 4, 7, 10, 13)),
69
+ ('7b9', (0, 4, 7, 10, 13)),
70
+ ('7(b9)', (0, 4, 7, 10, 13)), # https://www.oolimo.com/guitarchords/Fsharp7(b9)
71
+ ('7+9', (0, 4, 7, 10, 15)),
72
+ ('7#9', (0, 4, 7, 10, 15)),
73
+ ('9-5', (0, 4, 6, 10, 14)),
74
+ ('9b5', (0, 4, 6, 10, 14)),
75
+ ('9+5', (0, 4, 8, 10, 14)),
76
+ ('9#5', (0, 4, 8, 10, 14)),
77
+ ('7#9b5', (0, 4, 6, 10, 15)),
78
+ ('7#9#5', (0, 4, 8, 10, 15)),
79
+ ('m7b9b5', (0, 3, 6, 10, 13)),
80
+ ('7b9b5', (0, 4, 6, 10, 13)),
81
+ ('7b9#5', (0, 4, 8, 10, 13)),
82
+ ('11', (0, 7, 10, 14, 17)),
83
+ ('7+11', (0, 4, 7, 10, 18)),
84
+ ('7#11', (0, 4, 7, 10, 18)),
85
+ ('maj7+11', (0, 4, 7, 11, 18)),
86
+ ('M7+11', (0, 4, 7, 11, 18)),
87
+ ('maj7#11', (0, 4, 7, 11, 18)),
88
+ ('M7#11', (0, 4, 7, 11, 18)),
89
+ ('7b9#9', (0, 4, 7, 10, 13, 15)),
90
+ ('7b9#11', (0, 4, 7, 10, 13, 18)),
91
+ ('7#9#11', (0, 4, 7, 10, 15, 18)),
92
+ ('7-13', (0, 4, 7, 10, 20)),
93
+ ('7b13', (0, 4, 7, 10, 20)),
94
+ ('m7add11', (0, 3, 7, 10, 17)),
95
+ ('maj7add11', (0, 4, 7, 11, 17)),
96
+ ('M7add11', (0, 4, 7, 11, 17)),
97
+ ('mmaj7add11', (0, 3, 7, 11, 17)),
98
+ ('mM7add11', (0, 3, 7, 11, 17)),
99
+ # 6 notes
100
+ ('7b9b13', (0, 4, 7, 10, 13, 17, 20)),
101
+ ('9+11', (0, 4, 7, 10, 14, 18)),
102
+ ('9#11', (0, 4, 7, 10, 14, 18)),
103
+ ('m11', (0, 3, 7, 10, 14, 17)), # https://chord-c.com/guitar-chord/B/minor-eleventh/
104
+ ('13', (0, 4, 7, 10, 14, 21)),
105
+ ('13-9', (0, 4, 7, 10, 13, 21)),
106
+ ('13b9', (0, 4, 7, 10, 13, 21)),
107
+ ('13+9', (0, 4, 7, 10, 15, 21)),
108
+ ('13#9', (0, 4, 7, 10, 15, 21)),
109
+ ('13+11', (0, 4, 7, 10, 18, 21)),
110
+ ('13#11', (0, 4, 7, 10, 18, 21)),
111
+ ('maj13', (0, 4, 7, 11, 14, 21)), # https://chord-c.com/guitar-chord/C/major-thirteenth/
112
+ ('M13', (0, 4, 7, 11, 14, 21)),
113
+ ('maj7add13', (0, 4, 7, 9, 11, 14)),
114
+ ('M7add13', (0, 4, 7, 9, 11, 14)),
115
+ ]
@@ -0,0 +1,80 @@
1
+ NOTE_VAL_DICT = {
2
+ 'Ab': 8,
3
+ 'A': 9,
4
+ 'A#': 10,
5
+ 'Bb': 10,
6
+ 'B': 11,
7
+ 'Cb': 11,
8
+ 'C': 0,
9
+ 'C#': 1,
10
+ 'Db': 1,
11
+ 'D': 2,
12
+ 'D#': 3,
13
+ 'Eb': 3,
14
+ 'E': 4,
15
+ 'F': 5,
16
+ 'F#': 6,
17
+ 'Gb': 6,
18
+ 'G': 7,
19
+ 'G#': 8,
20
+ }
21
+
22
+ VAL_NOTE_DICT = {
23
+ 0: ['C'],
24
+ 1: ['Db', 'C#'],
25
+ 2: ['D'],
26
+ 3: ['Eb', 'D#'],
27
+ 4: ['E'],
28
+ 5: ['F'],
29
+ 6: ['F#', 'Gb'],
30
+ 7: ['G'],
31
+ 8: ['Ab', 'G#'],
32
+ 9: ['A'],
33
+ 10: ['Bb', 'A#'],
34
+ 11: ['B', 'Cb']
35
+ }
36
+
37
+ SHARPED_SCALE = {
38
+ 0: 'C', 1: 'C#', 2: 'D', 3: 'D#',
39
+ 4: 'E', 5: 'F', 6: 'F#', 7: 'G',
40
+ 8: 'G#', 9: 'A', 10: 'A#', 11: 'B'
41
+ }
42
+
43
+ FLATTED_SCALE = {
44
+ 0: 'C', 1: 'Db', 2: 'D', 3: 'Eb',
45
+ 4: 'E', 5: 'F', 6: 'Gb', 7: 'G',
46
+ 8: 'Ab', 9: 'A', 10: 'Bb', 11: 'B'
47
+ }
48
+
49
+ SCALE_VAL_DICT = {
50
+ 'Ab': FLATTED_SCALE,
51
+ 'A': SHARPED_SCALE,
52
+ 'A#': SHARPED_SCALE,
53
+ 'Bb': FLATTED_SCALE,
54
+ 'B': SHARPED_SCALE,
55
+ 'Cb': FLATTED_SCALE,
56
+ 'C': FLATTED_SCALE,
57
+ 'C#': SHARPED_SCALE,
58
+ 'Db': FLATTED_SCALE,
59
+ 'D': SHARPED_SCALE,
60
+ 'D#': SHARPED_SCALE,
61
+ 'Eb': FLATTED_SCALE,
62
+ 'E': SHARPED_SCALE,
63
+ 'F': FLATTED_SCALE,
64
+ 'F#': SHARPED_SCALE,
65
+ 'Gb': FLATTED_SCALE,
66
+ 'G': SHARPED_SCALE,
67
+ 'G#': SHARPED_SCALE,
68
+ }
69
+
70
+ # https://en.wikipedia.org/wiki/Mode_(music)#Modern_modes
71
+ # Ionian -> maj, Aeolian -> min
72
+ RELATIVE_KEY_DICT = {
73
+ 'maj': [0, 2, 4, 5, 7, 9, 11, 12],
74
+ 'Dor': [0, 2, 3, 5, 7, 9, 10, 12],
75
+ 'Phr': [0, 1, 3, 5, 7, 8, 10, 12],
76
+ 'Lyd': [0, 2, 4, 6, 7, 9, 11, 12],
77
+ 'Mix': [0, 2, 4, 5, 7, 9, 10, 12],
78
+ 'min': [0, 2, 3, 5, 7, 8, 10, 12],
79
+ 'Loc': [0, 1, 3, 5, 6, 8, 10, 12],
80
+ }
pychord/parser.py ADDED
@@ -0,0 +1,47 @@
1
+ from typing import Tuple, List
2
+ import re
3
+
4
+ from .quality import QualityManager, Quality
5
+ from .utils import NOTE_VAL_DICT
6
+
7
+ inversion_re = re.compile("/([0-9]+)")
8
+
9
+
10
+ def parse(chord: str) -> Tuple[str, Quality, List[str], str]:
11
+ """Parse a string to get chord component
12
+
13
+ :param chord: str expression of a chord
14
+ :return: (root, quality, appended, on)
15
+ """
16
+
17
+ if len(chord) > 1 and chord[1] in ("b", "#"):
18
+ root = chord[:2]
19
+ rest = chord[2:]
20
+ else:
21
+ root = chord[:1]
22
+ rest = chord[1:]
23
+
24
+ def check_note(note: str):
25
+ """ Raise ValueError if note is invalid """
26
+ if note not in NOTE_VAL_DICT:
27
+ raise ValueError(f"Invalid note {note}")
28
+
29
+ check_note(root)
30
+
31
+ inversion = 0
32
+ inversion_m = inversion_re.search(rest)
33
+ if inversion_m:
34
+ inversion = int(inversion_m.group(1))
35
+ rest = inversion_re.sub("", rest)
36
+
37
+ on_chord_idx = rest.find("/")
38
+ if on_chord_idx >= 0:
39
+ on = rest[on_chord_idx + 1:]
40
+ rest = rest[:on_chord_idx]
41
+ check_note(on)
42
+ else:
43
+ on = ""
44
+ quality = QualityManager().get_quality(rest, inversion)
45
+ # TODO: Implement parser for appended notes
46
+ appended: List[str] = []
47
+ return root, quality, appended, on
pychord/progression.py ADDED
@@ -0,0 +1,112 @@
1
+ from typing import Union, List
2
+
3
+ from .chord import Chord
4
+
5
+
6
+ class ChordProgression:
7
+ """ Class to handle chord progressions.
8
+
9
+ Attributes:
10
+ _chords: component chords of chord progression.
11
+ """
12
+
13
+ def __init__(self, initial_chords: Union[str, Chord, List[Union[str, Chord]]] = []):
14
+ """ Constructor of ChordProgression instance.
15
+
16
+ :param initial_chords: Initial chord or chords of the chord progressions
17
+ """
18
+ if isinstance(initial_chords, Chord):
19
+ chords = [initial_chords]
20
+ elif isinstance(initial_chords, str):
21
+ chords = [self._as_chord(initial_chords)]
22
+ elif isinstance(initial_chords, list):
23
+ chords = [self._as_chord(chord) for chord in initial_chords]
24
+ else:
25
+ raise TypeError(
26
+ f"Cannot initialize ChordProgression with argument of {type(initial_chords)} type")
27
+ self._chords: List[Chord] = chords
28
+
29
+ def __unicode__(self):
30
+ return " | ".join([chord.chord for chord in self._chords])
31
+
32
+ def __str__(self):
33
+ return " | ".join([chord.chord for chord in self._chords])
34
+
35
+ def __repr__(self):
36
+ return f"<ChordProgression: {' | '.join([chord.chord for chord in self._chords])}>"
37
+
38
+ def __add__(self, other):
39
+ self._chords += other.chords
40
+ return self
41
+
42
+ def __len__(self):
43
+ return len(self._chords)
44
+
45
+ def __getitem__(self, item):
46
+ return self._chords[item]
47
+
48
+ def __setitem__(self, key, value):
49
+ self._chords[key] = value
50
+
51
+ def __eq__(self, other):
52
+ if not isinstance(other, ChordProgression):
53
+ raise TypeError(
54
+ f"Cannot compare ChordProgression object with {type(other)} object")
55
+ if len(self) != len(other):
56
+ return False
57
+ for c, o in zip(self, other):
58
+ if c != o:
59
+ return False
60
+ return True
61
+
62
+ def __ne__(self, other):
63
+ return not self.__eq__(other)
64
+
65
+ @property
66
+ def chords(self) -> List[Chord]:
67
+ """ Get component chords of chord progression """
68
+ return self._chords
69
+
70
+ def append(self, chord: Union[str, Chord]) -> None:
71
+ """ Append a chord to chord progressions
72
+
73
+ :param chord: A chord to append
74
+ """
75
+ self._chords.append(self._as_chord(chord))
76
+
77
+ def insert(self, index: int, chord: Union[str, Chord]) -> None:
78
+ """ Insert a chord to chord progressions
79
+
80
+ :param index: Index to insert a chord
81
+ :param chord: A chord to insert
82
+ """
83
+ self._chords.insert(index, self._as_chord(chord))
84
+
85
+ def pop(self, index: int = -1) -> Chord:
86
+ """ Pop a chord from chord progressions
87
+
88
+ :param index: Index of the chord to pop (default: -1)
89
+ """
90
+ return self._chords.pop(index)
91
+
92
+ def transpose(self, trans: int) -> None:
93
+ """ Transpose whole chord progressions
94
+
95
+ :param trans: Transpose key
96
+ """
97
+ for chord in self._chords:
98
+ chord.transpose(trans)
99
+
100
+ @staticmethod
101
+ def _as_chord(chord: Union[str, Chord]) -> Chord:
102
+ """ Convert from str to Chord instance if input is str
103
+
104
+ :param chord: Chord name or Chord instance
105
+ :return: Chord instance
106
+ """
107
+ if isinstance(chord, Chord):
108
+ return chord
109
+ elif isinstance(chord, str):
110
+ return Chord(chord)
111
+ else:
112
+ raise TypeError("input type should be str or Chord instance.")
pychord/quality.py ADDED
@@ -0,0 +1,133 @@
1
+ import copy
2
+ from collections import OrderedDict
3
+ from typing import Tuple, List
4
+
5
+ from .constants.qualities import DEFAULT_QUALITIES
6
+ from .utils import note_to_val, val_to_note
7
+
8
+
9
+ class Quality:
10
+ """ Chord quality """
11
+
12
+ def __init__(self, name: str, components: Tuple[int, ...]):
13
+ """ Constructor of chord quality
14
+
15
+ :param name: name of quality
16
+ :param components: components of quality
17
+ """
18
+ self._quality: str = name
19
+ self.components: Tuple[int, ...] = components
20
+
21
+ def __unicode__(self):
22
+ return self._quality
23
+
24
+ def __str__(self):
25
+ return self._quality
26
+
27
+ def __eq__(self, other):
28
+ if not isinstance(other, Quality):
29
+ raise TypeError(f"Cannot compare Quality object with {type(other)} object")
30
+ return self.components == other.components
31
+
32
+ def __ne__(self, other):
33
+ return not self.__eq__(other)
34
+
35
+ @property
36
+ def quality(self):
37
+ """ Get name of quality """
38
+ return self._quality
39
+
40
+ def get_components(self, root='C', visible=False):
41
+ """ Get components of chord quality
42
+
43
+ :param str root: the root note of the chord
44
+ :param bool visible: returns the name of notes if True
45
+ :rtype: list[str|int]
46
+ :return: components of chord quality
47
+ """
48
+ root_val = note_to_val(root)
49
+ components = [v + root_val for v in self.components]
50
+
51
+ if visible:
52
+ components = [val_to_note(c, scale=root) for c in components]
53
+
54
+ return components
55
+
56
+ def append_on_chord(self, on_chord, root):
57
+ """ Append on chord
58
+
59
+ To create Am7/G
60
+ q = Quality('m7')
61
+ q.append_on_chord('G', root='A')
62
+
63
+ :param str on_chord: bass note of the chord
64
+ :param str root: root note of the chord
65
+ """
66
+ root_val = note_to_val(root)
67
+ on_chord_val = note_to_val(on_chord) - root_val
68
+
69
+ components = list(self.components)
70
+ for idx, val in enumerate(self.components):
71
+ if val % 12 == on_chord_val:
72
+ components.remove(val)
73
+ break
74
+
75
+ if on_chord_val > root_val:
76
+ on_chord_val -= 12
77
+
78
+ if on_chord_val not in components:
79
+ components = [on_chord_val] + [
80
+ v for v in components if v % 12 != on_chord_val % 12
81
+ ]
82
+
83
+ self.components = tuple(components)
84
+
85
+
86
+ class QualityManager:
87
+ """ Singleton class to manage the qualities """
88
+
89
+ def __new__(cls, *args, **kwargs):
90
+ if not hasattr(cls, "_instance"):
91
+ cls._instance = super(QualityManager, cls).__new__(cls)
92
+ cls._instance.load_default_qualities()
93
+ return cls._instance
94
+
95
+ def load_default_qualities(self):
96
+ self._qualities = OrderedDict([
97
+ (q, Quality(q, c)) for q, c in DEFAULT_QUALITIES
98
+ ])
99
+
100
+ def get_quality(self, name: str, inversion: int = 0) -> Quality:
101
+ if name not in self._qualities:
102
+ raise ValueError(f"Unknown quality: {name}")
103
+ # Create a new instance not to affect any existing instances
104
+ q = copy.deepcopy(self._qualities[name])
105
+ # apply requested inversion :
106
+ for i in range(inversion):
107
+ n = q.components[0]
108
+ while n < q.components[-1]:
109
+ n += 12
110
+ q.components = q.components[1:] + (n,)
111
+ return q
112
+
113
+ def get_qualities(self):
114
+ return dict(self._qualities)
115
+
116
+ def set_quality(self, name: str, components: Tuple[int, ...]):
117
+ """ Set a Quality
118
+
119
+ This method will not affect any existing Chord instances.
120
+ :param name: name of quality
121
+ :param components: components of quality
122
+ """
123
+ self._qualities[name] = Quality(name, components)
124
+
125
+ def find_quality_from_components(self, components: List[int]):
126
+ """ Find a quality from components
127
+
128
+ :param components: components of quality
129
+ """
130
+ for q in self._qualities.values():
131
+ if list(q.components) == components:
132
+ return copy.deepcopy(q)
133
+ return None
pychord/utils.py ADDED
@@ -0,0 +1,52 @@
1
+ from typing import List
2
+
3
+ from .constants import NOTE_VAL_DICT, SCALE_VAL_DICT
4
+
5
+
6
+ def note_to_val(note: str) -> int:
7
+ """ Get index value of a note
8
+
9
+ >>> note_to_val("C")
10
+ 0
11
+ >>> note_to_val("B")
12
+ 11
13
+ """
14
+ if note not in NOTE_VAL_DICT:
15
+ raise ValueError(f"Unknown note {note}")
16
+ return NOTE_VAL_DICT[note]
17
+
18
+
19
+ def val_to_note(val: int, scale: str = "C") -> str:
20
+ """ Return note by index in a scale
21
+
22
+ >>> val_to_note(0)
23
+ "C"
24
+ >>> val_to_note(11, "D")
25
+ "D#"
26
+ """
27
+ val %= 12
28
+ return SCALE_VAL_DICT[scale][val]
29
+
30
+
31
+ def transpose_note(note: str, transpose: int, scale: str = "C") -> str:
32
+ """ Transpose a note
33
+
34
+ >>> transpose_note("C", 1)
35
+ "Db"
36
+ >>> transpose_note("D", 4, "A")
37
+ "F#"
38
+ """
39
+ val = note_to_val(note)
40
+ val += transpose
41
+ return val_to_note(val, scale)
42
+
43
+
44
+ def display_appended(appended: List[str]) -> str:
45
+ # TODO: Implement this
46
+ return ""
47
+
48
+
49
+ def display_on(on_note: str) -> str:
50
+ if on_note:
51
+ return f"/{on_note}"
52
+ return ""
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: pychord
3
+ Version: 1.2.3
4
+ Summary: A library to handle musical chords in python.
5
+ Author-email: Yuma Mihira <info@yuma.cloud>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/yuma-m/pychord
8
+ Keywords: music,chord
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Multimedia :: Sound/Audio
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ ![PyChord](./pychord.png)
24
+
25
+ # PyChord ![Build Status](https://github.com/yuma-m/pychord/actions/workflows/build.yml/badge.svg) [![Documentation Status](https://readthedocs.org/projects/pychord/badge/?version=latest)](http://pychord.readthedocs.io/en/latest/?badge=latest)
26
+
27
+ ## Overview
28
+
29
+ PyChord is a python library to handle musical chords.
30
+
31
+ ## Installation
32
+
33
+ ```sh
34
+ $ pip install pychord
35
+ ```
36
+
37
+ ## Basic Usage
38
+
39
+ ### Create a Chord
40
+
41
+ ```python
42
+ >>> from pychord import Chord
43
+ >>> c = Chord("Am7")
44
+ >>> c
45
+ <Chord: Am7>
46
+ >>> c.info()
47
+ """
48
+ Am7
49
+ root=A
50
+ quality=m7
51
+ appended=[]
52
+ on=None
53
+ """
54
+ ```
55
+
56
+ ### Transpose a Chord
57
+
58
+ ```python
59
+ >>> c = Chord("Am7/G")
60
+ >>> c.transpose(3)
61
+ >>> c
62
+ <Chord: Cm7/Bb>
63
+ ```
64
+
65
+ ### Get component notes
66
+
67
+ ```python
68
+ >>> c = Chord("Am7")
69
+ >>> c.components()
70
+ ['A', 'C', 'E', 'G']
71
+ >>> c.components_with_pitch(root_pitch=3)
72
+ ['A3', 'C4', 'E4', 'G4']
73
+ ```
74
+
75
+ ### Compare Chords
76
+
77
+ ```python
78
+ >>> Chord("C") == Chord("D")
79
+ False
80
+ >>> Chord("C#") == Chord("Db")
81
+ True
82
+ >>> c = Chord("C")
83
+ >>> c.transpose(2)
84
+ >>> c == Chord("D")
85
+ True
86
+ ```
87
+
88
+ ### Find Chords from notes
89
+
90
+ ```python
91
+ >>> from pychord import find_chords_from_notes
92
+ >>> find_chords_from_notes(["C", "E", "G"])
93
+ [ <Chord: C>]
94
+ >>> find_chords_from_notes(["F#", "A", "C", "D"])
95
+ [ <Chord: D7/F#>]
96
+ >>> find_chords_from_notes(["F", "G", "C"])
97
+ [ <Chord: Fsus2>, <Chord: Csus4/F>]
98
+ ```
99
+
100
+ ### Create and handle chord progressions
101
+
102
+ ```python
103
+ >>> from pychord import ChordProgression
104
+ >>> cp = ChordProgression(["C", "G/B", "Am"])
105
+ >>> cp
106
+ <ChordProgression: C | G/B | Am>
107
+
108
+ >>> cp.append("Em/G")
109
+ >>> cp
110
+ <ChordProgression: C | G/B | Am | Em/G>
111
+
112
+ >>> cp.transpose(+3)
113
+ >>> cp
114
+ <ChordProgression: Eb | Bb/D | Cm | Gm/Bb>
115
+
116
+ >>> cp[1]
117
+ <Chord: Bb/D>
118
+ ```
119
+
120
+ ## Advanced Usage
121
+
122
+ ### Create a Chord from note index in a scale
123
+
124
+ ```python
125
+ >>> Chord.from_note_index(note=1, quality="", scale="Cmaj")
126
+ <Chord: C> # I of C major
127
+ >>> Chord.from_note_index(note=3, quality="m7", scale="Fmaj")
128
+ <Chord: Am7> # IIIm7 of F major
129
+ >>> Chord.from_note_index(note=5, quality="7", scale="Amin")
130
+ <Chord: E7> # V7 of A minor
131
+ ```
132
+
133
+ ### Overwrite the default Quality components with yours
134
+
135
+ ```python
136
+ >>> from pychord import Chord, QualityManager
137
+ >>> Chord("C11").components()
138
+ ['C', 'G', 'Bb', 'D', 'F']
139
+
140
+ >>> quality_manager = QualityManager()
141
+ >>> quality_manager.set_quality("11", (0, 4, 7, 10, 14, 17))
142
+ >>> Chord("C11").components()
143
+ ['C', 'E', 'G', 'Bb', 'D', 'F']
144
+ ```
145
+
146
+ ### Inversions
147
+
148
+ Chord inversions are created with a forward slash and a number
149
+ indicating the order. This can optionally be combined with an
150
+ additional forward slash to change the bass note:
151
+
152
+ ```python
153
+ >>> Chord("C/1").components() # First inversion of C
154
+ ['E', 'G', 'C']
155
+ >>> Chord("C/2").components() # Second inversion of C
156
+ ['G', 'C', 'E']
157
+
158
+ >>> Chord("Cm7/3/F").components() # Third inversion of Cm7 with an added F bass
159
+ ['F', 'Bb', 'C', 'Eb', 'G']
160
+ ```
161
+
162
+ ## Examples
163
+
164
+ - [pychord-midi.py](./examples/pychord-midi.py) - Create a MIDI file using PyChord and pretty_midi.
165
+
166
+ ## Supported Python Versions
167
+
168
+ - 3.8 and above
169
+
170
+ ## Links
171
+
172
+ - PyPI: https://pypi.python.org/pypi/pychord
173
+ - GitHub: https://github.com/yuma-m/pychord
174
+ - Documentation: http://pychord.readthedocs.io/en/latest/
175
+
176
+ ## Author
177
+
178
+ - [Yuma Mihira](https://yuma.cloud/)
179
+
180
+ ## License
181
+
182
+ - MIT License
183
+
184
+ Icon is made by [Freepik](https://www.flaticon.com/authors/freepik).
@@ -0,0 +1,16 @@
1
+ examples/pychord-midi.py,sha256=nSPFfhJnwYHysADorEXaIIsgTUOm58EzXckpSWReM2E,985
2
+ pychord/__init__.py,sha256=b7x9naaKu7Z2UgG-YXE372xPssAIn2gsMtKa78Tor_k,172
3
+ pychord/analyzer.py,sha256=xt9xoOORLHkMRjD56lFj2w3RVI5Ms9XlgvdnQqTP5lM,1995
4
+ pychord/chord.py,sha256=0hnan-GZVz3-BAKVaSEN3vmnRdBzqPUGLKmw5CQJDX8,7521
5
+ pychord/parser.py,sha256=X_e-pDIZhTp2_8VT39pUN84XsWpk6UUeeVEDrSWojhY,1252
6
+ pychord/progression.py,sha256=fb6w4dMBOHivVj7tIuvYglUHcffgdBnETqC5drFg_Ww,3456
7
+ pychord/quality.py,sha256=2IqBxUX2uoa-rCvDNo11daKevAsvo2U74xoPU0n0lao,4071
8
+ pychord/utils.py,sha256=_d7HK57di37mPe4DnjqGuIjP_b2fZEq3Pq4qCJj3uMk,1027
9
+ pychord/constants/__init__.py,sha256=8eR7MemP8wC9OaxpcwhvyxsrkHJEe2pgjlkJ5mJGusw,80
10
+ pychord/constants/qualities.py,sha256=ui--gDT0iicf_DOfFiWIcRwhudv9Lh1_cxvG5Mf0vEs,3748
11
+ pychord/constants/scales.py,sha256=BQhhGzAhx7xtliBRUdW65W3pDce6GgwTG8zFN3ZZgdo,1605
12
+ pychord-1.2.3.dist-info/licenses/LICENSE,sha256=T84Eir2LETaTDcgSBnUHcsJCuLXxvZSaYFZSL1sCjN0,1066
13
+ pychord-1.2.3.dist-info/METADATA,sha256=EFrX0MG3mMbG0gqLa9gbjW1YMW4xBUDAGgv1lyHyK9M,4060
14
+ pychord-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ pychord-1.2.3.dist-info/top_level.txt,sha256=NOgJx-31h3rXznW6f9whk22j9POizbvhpQ5w2Q-zToM,17
16
+ pychord-1.2.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ examples
2
+ pychord