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.
- examples/pychord-midi.py +32 -0
- pychord/__init__.py +5 -0
- pychord/analyzer.py +64 -0
- pychord/chord.py +206 -0
- pychord/constants/__init__.py +2 -0
- pychord/constants/qualities.py +115 -0
- pychord/constants/scales.py +80 -0
- pychord/parser.py +47 -0
- pychord/progression.py +112 -0
- pychord/quality.py +133 -0
- pychord/utils.py +52 -0
- pychord-1.2.3.dist-info/METADATA +184 -0
- pychord-1.2.3.dist-info/RECORD +16 -0
- pychord-1.2.3.dist-info/WHEEL +5 -0
- pychord-1.2.3.dist-info/licenses/LICENSE +21 -0
- pychord-1.2.3.dist-info/top_level.txt +2 -0
examples/pychord-midi.py
ADDED
|
@@ -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
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,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
|
+

|
|
24
|
+
|
|
25
|
+
# PyChord  [](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,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.
|