1class Tone:
2 """Describe the concept of musical tone."""
3
4 ORDER = ('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B')
5 ALL_TONES = len(ORDER)
6
7 def __init__(self, name):
8 self.name = name
9
10 def __str__(self):
11 return self.name
12
13 def index(self):
14 """Return the index of the name in tone order"""
15 return Tone.ORDER.index(self.name)
16
17 def __add__(self, other):
18 if isinstance(other, Tone):
19 return Chord(self, other)
20 elif isinstance(other, Interval):
21 semitones = Tone.ORDER.index(self.name) + other.number_of_semitones
22 while semitones >= self.ALL_TONES:
23 semitones -= self.ALL_TONES
24 return Tone(self.ORDER[semitones])
25
26 def __sub__(self, other):
27 if isinstance(other, Tone):
28 return Interval.from_tones(self, other)
29 elif isinstance(other, Interval):
30 semitones = Tone.ORDER.index(self.name) - other.number_of_semitones
31 while semitones < 0:
32 semitones += self.ALL_TONES
33 return Tone(self.ORDER[semitones])
34
35
36class Interval:
37 """Describe the concept of an interval"""
38
39 @staticmethod
40 def interval_name(number_of_semitones):
41 """Return the name relative to the number of semitones in the interval"""
42 names_of_intervals = ('unison', 'minor 2nd', 'major 2nd', 'minor 3rd', 'major 3rd', 'perfect 4th',
43 'diminished 5th', 'perfect 5th', 'minor 6th', 'major 6th', 'minor 7th', 'major 7th')
44 return names_of_intervals[number_of_semitones]
45
46 def __init__(self, number_of_semitones):
47 while number_of_semitones >= Tone.ALL_TONES:
48 number_of_semitones -= Tone.ALL_TONES
49 while number_of_semitones < 0:
50 number_of_semitones += Tone.ALL_TONES
51 self.name = Interval.interval_name(number_of_semitones)
52 self.number_of_semitones = number_of_semitones
53 self.operator = True #True for +, False for -, used for add and substract
54
55 def __str__(self):
56 return self.name
57
58 def __neg__(self):
59 self.operator = False
60 return self
61
62 @classmethod
63 def from_tones(cls, first, second):
64 """Return a new interval by two given tones"""
65 if first.index() >= second.index():
66 return Interval(first.index() - second.index())
67 else:
68 return Interval(second.index() - first.index())
69
70 def __add__(self, other):
71 if isinstance(other, Tone):
72 raise TypeError("Invalid operation")
73 elif isinstance(other, Interval):
74 semitones = self.number_of_semitones + other.number_of_semitones
75 return Interval(semitones)
76
77 def __sub__(self, other):
78 if isinstance(other, Tone):
79 raise TypeError("Invalid operation")
80
81
82class Chord:
83 """Describe the concept of a chord"""
84
85 @staticmethod
86 def validate_chord(tones):
87 """Check if a chord is valid - long enough to be a chord"""
88 if len(set(tones)) < 2:
89 raise TypeError("Cannot have a chord made of only 1 unique tone")
90
91 @staticmethod
92 def ordered_name(root, tones):
93 """Return the sorted name of the chord by the tones associated with - in between"""
94 start_index = Tone.ORDER.index(root)
95 custom_order = Tone.ORDER[start_index:] + Tone.ORDER[:start_index]
96 unique_tones = list(set(tones))
97 if root in unique_tones:
98 unique_tones.remove(root)
99 ordered_tones = sorted(unique_tones, key=lambda tone: custom_order.index(tone))
100 chord_name = root + '-' + '-'.join(tone for tone in ordered_tones)
101 return chord_name
102
103 def __init__(self, root, *args):
104 self.root = root
105 self.other_tones = list(args)
106 self.other_tones_names = [str(tone) for tone in args]
107 Chord.validate_chord([str(self.root)] + self.other_tones_names)
108 self.chord_name = Chord.ordered_name(str(self.root), self.other_tones_names)
109
110 def __str__(self):
111 return self.chord_name
112
113 def is_minor(self):
114 """Check if the chord is minor"""
115 for second_tone in self.other_tones:
116 if str(Interval.from_tones(self.root, second_tone)) == 'minor 3rd':
117 return True
118 return False
119
120 def is_major(self):
121 """Check if the chord is major"""
122 for second_tone in self.other_tones:
123 if str(Interval.from_tones(self.root, second_tone)) == 'major 3rd':
124 return True
125 return False
126
127 def is_power_chord(self):
128 """Check if the chord is power chord"""
129 return not self.is_minor() and not self.is_major()
130
131 def __add__(self, other):
132 if isinstance(other, Tone):
133 tones = self.other_tones
134 tones.append(other)
135 return Chord(self.root, *tones)
136 elif isinstance(other, Chord):
137 new_root = self.root
138 new_tones = self.other_tones + [other.root] + other.other_tones
139 return Chord(new_root, *new_tones)
140
141 def __sub__(self, other):
142 if other.name != self.root.name and other.name not in self.other_tones_names:
143 raise TypeError(f"Cannot remove tone {str(other)} from chord {str(self)}")
144 elif len(self.other_tones) < 2:
145 raise TypeError("Cannot have a chord made of only 1 unique tone")
146 elif other.name is self.root.name:
147 new_root = self.other_tones_names[1]
148 new_tones = [tone for tone in self.other_tones if tone.name != new_root.name]
149 return Chord(new_root, *new_tones)
150 else:
151 new_tones = [tone for tone in self.other_tones if tone.name != other.name]
152 return Chord(self.root, *new_tones)
153
154 def transposed(self, interval):
155 """Transpose a chord - shift each of its tones by the same interval"""
156 new_root = self.root
157 new_tones = []
158 if interval.operator:
159 new_root += interval
160 for tone in self.other_tones:
161 new_tones.append(tone+interval)
162 else:
163 new_root -= interval
164 for tone in self.other_tones:
165 new_tones.append(tone-interval)
166 return Chord(new_root, *new_tones)
....FFF....F.......................F.
======================================================================
FAIL: test_is_major (test.TestBasicChordFunctionality.test_is_major)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 107, in test_is_major
self.assertTrue(a_major_chord.is_major())
AssertionError: False is not true
======================================================================
FAIL: test_is_minor (test.TestBasicChordFunctionality.test_is_minor)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 90, in test_is_minor
self.assertTrue(a_minor_chord.is_minor())
AssertionError: False is not true
======================================================================
FAIL: test_is_power_chord (test.TestBasicChordFunctionality.test_is_power_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 116, in test_is_power_chord
self.assertFalse(a_minor_chord.is_power_chord())
AssertionError: True is not false
======================================================================
FAIL: test_interval_negative (test.TestBasicIntervalFunctionality.test_interval_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 51, in test_interval_negative
self.assertEqual(str(minor_2nd), "minor 2nd")
AssertionError: 'major 7th' != 'minor 2nd'
- major 7th
+ minor 2nd
======================================================================
FAIL: test_tone_subtraction_inverse (test.TestOperations.test_tone_subtraction_inverse)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 178, in test_tone_subtraction_inverse
self.assertEqual(str(perfect_4th), "perfect 4th")
AssertionError: 'perfect 5th' != 'perfect 4th'
- perfect 5th
? ^
+ perfect 4th
? ^
----------------------------------------------------------------------
Ran 37 tests in 0.002s
FAILED (failures=5)
f | 1 | class Tone: | f | 1 | class Tone: |
2 | """Describe the concept of musical tone.""" | 2 | """Describe the concept of musical tone.""" | ||
n | 3 | ALL_TONES = 12 | n | 3 | |
4 | ORDER = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] | 4 | ORDER = ('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B') | ||
5 | ALL_TONES = len(ORDER) | ||||
6 | |||||
5 | def __init__(self, name): | 7 | def __init__(self, name): | ||
6 | self.name = name | 8 | self.name = name | ||
7 | 9 | ||||
8 | def __str__(self): | 10 | def __str__(self): | ||
9 | return self.name | 11 | return self.name | ||
10 | 12 | ||||
11 | def index(self): | 13 | def index(self): | ||
12 | """Return the index of the name in tone order""" | 14 | """Return the index of the name in tone order""" | ||
13 | return Tone.ORDER.index(self.name) | 15 | return Tone.ORDER.index(self.name) | ||
14 | 16 | ||||
15 | def __add__(self, other): | 17 | def __add__(self, other): | ||
16 | if isinstance(other, Tone): | 18 | if isinstance(other, Tone): | ||
17 | return Chord(self, other) | 19 | return Chord(self, other) | ||
18 | elif isinstance(other, Interval): | 20 | elif isinstance(other, Interval): | ||
19 | semitones = Tone.ORDER.index(self.name) + other.number_of_semitones | 21 | semitones = Tone.ORDER.index(self.name) + other.number_of_semitones | ||
20 | while semitones >= self.ALL_TONES: | 22 | while semitones >= self.ALL_TONES: | ||
21 | semitones -= self.ALL_TONES | 23 | semitones -= self.ALL_TONES | ||
22 | return Tone(self.ORDER[semitones]) | 24 | return Tone(self.ORDER[semitones]) | ||
23 | 25 | ||||
24 | def __sub__(self, other): | 26 | def __sub__(self, other): | ||
25 | if isinstance(other, Tone): | 27 | if isinstance(other, Tone): | ||
n | 26 | return Interval.fromTones(self, other) | n | 28 | return Interval.from_tones(self, other) |
27 | elif isinstance(other, Interval): | 29 | elif isinstance(other, Interval): | ||
28 | semitones = Tone.ORDER.index(self.name) - other.number_of_semitones | 30 | semitones = Tone.ORDER.index(self.name) - other.number_of_semitones | ||
29 | while semitones < 0: | 31 | while semitones < 0: | ||
30 | semitones += self.ALL_TONES | 32 | semitones += self.ALL_TONES | ||
31 | return Tone(self.ORDER[semitones]) | 33 | return Tone(self.ORDER[semitones]) | ||
32 | 34 | ||||
33 | 35 | ||||
34 | class Interval: | 36 | class Interval: | ||
35 | """Describe the concept of an interval""" | 37 | """Describe the concept of an interval""" | ||
n | n | 38 | |||
36 | @staticmethod | 39 | @staticmethod | ||
37 | def interval_name(number_of_semitones): | 40 | def interval_name(number_of_semitones): | ||
38 | """Return the name relative to the number of semitones in the interval""" | 41 | """Return the name relative to the number of semitones in the interval""" | ||
n | 39 | if number_of_semitones == 0: | n | 42 | names_of_intervals = ('unison', 'minor 2nd', 'major 2nd', 'minor 3rd', 'major 3rd', 'perfect 4th', |
40 | return 'unison' | 43 | 'diminished 5th', 'perfect 5th', 'minor 6th', 'major 6th', 'minor 7th', 'major 7th') | ||
41 | elif number_of_semitones == 1: | 44 | return names_of_intervals[number_of_semitones] | ||
42 | return 'minor 2nd' | 45 | |||
43 | elif number_of_semitones == 2: | ||||
44 | return 'major 2nd' | ||||
45 | elif number_of_semitones == 3: | ||||
46 | return 'minor 3rd' | ||||
47 | elif number_of_semitones == 4: | ||||
48 | return 'major 3rd' | ||||
49 | elif number_of_semitones == 5: | ||||
50 | return 'perfect 4th' | ||||
51 | elif number_of_semitones == 6: | ||||
52 | return 'diminished 5th' | ||||
53 | elif number_of_semitones == 7: | ||||
54 | return 'perfect 5th' | ||||
55 | elif number_of_semitones == 8: | ||||
56 | return 'minor 6th' | ||||
57 | elif number_of_semitones == 9: | ||||
58 | return 'major 6th' | ||||
59 | elif number_of_semitones == 10: | ||||
60 | return 'minor 7th' | ||||
61 | elif number_of_semitones == 11: | ||||
62 | return 'major 7th' | ||||
63 | |||||
64 | def __init__(self, number_of_semitones): | 46 | def __init__(self, number_of_semitones): | ||
65 | while number_of_semitones >= Tone.ALL_TONES: | 47 | while number_of_semitones >= Tone.ALL_TONES: | ||
66 | number_of_semitones -= Tone.ALL_TONES | 48 | number_of_semitones -= Tone.ALL_TONES | ||
67 | while number_of_semitones < 0: | 49 | while number_of_semitones < 0: | ||
68 | number_of_semitones += Tone.ALL_TONES | 50 | number_of_semitones += Tone.ALL_TONES | ||
69 | self.name = Interval.interval_name(number_of_semitones) | 51 | self.name = Interval.interval_name(number_of_semitones) | ||
70 | self.number_of_semitones = number_of_semitones | 52 | self.number_of_semitones = number_of_semitones | ||
71 | self.operator = True #True for +, False for -, used for add and substract | 53 | self.operator = True #True for +, False for -, used for add and substract | ||
72 | 54 | ||||
73 | def __str__(self): | 55 | def __str__(self): | ||
74 | return self.name | 56 | return self.name | ||
75 | 57 | ||||
76 | def __neg__(self): | 58 | def __neg__(self): | ||
77 | self.operator = False | 59 | self.operator = False | ||
78 | return self | 60 | return self | ||
79 | 61 | ||||
80 | @classmethod | 62 | @classmethod | ||
n | 81 | def fromTones(cls, first, second): | n | 63 | def from_tones(cls, first, second): |
82 | """Return a new interval by two given tones""" | 64 | """Return a new interval by two given tones""" | ||
83 | if first.index() >= second.index(): | 65 | if first.index() >= second.index(): | ||
84 | return Interval(first.index() - second.index()) | 66 | return Interval(first.index() - second.index()) | ||
85 | else: | 67 | else: | ||
86 | return Interval(second.index() - first.index()) | 68 | return Interval(second.index() - first.index()) | ||
87 | 69 | ||||
88 | def __add__(self, other): | 70 | def __add__(self, other): | ||
89 | if isinstance(other, Tone): | 71 | if isinstance(other, Tone): | ||
90 | raise TypeError("Invalid operation") | 72 | raise TypeError("Invalid operation") | ||
91 | elif isinstance(other, Interval): | 73 | elif isinstance(other, Interval): | ||
92 | semitones = self.number_of_semitones + other.number_of_semitones | 74 | semitones = self.number_of_semitones + other.number_of_semitones | ||
93 | return Interval(semitones) | 75 | return Interval(semitones) | ||
94 | 76 | ||||
95 | def __sub__(self, other): | 77 | def __sub__(self, other): | ||
96 | if isinstance(other, Tone): | 78 | if isinstance(other, Tone): | ||
97 | raise TypeError("Invalid operation") | 79 | raise TypeError("Invalid operation") | ||
98 | 80 | ||||
99 | 81 | ||||
100 | class Chord: | 82 | class Chord: | ||
101 | """Describe the concept of a chord""" | 83 | """Describe the concept of a chord""" | ||
n | n | 84 | |||
102 | @staticmethod | 85 | @staticmethod | ||
n | 103 | def check_valid_chord(tones): | n | 86 | def validate_chord(tones): |
104 | """Check if a chord is valid - long enough to be a chord""" | 87 | """Check if a chord is valid - long enough to be a chord""" | ||
105 | if len(set(tones)) < 2: | 88 | if len(set(tones)) < 2: | ||
106 | raise TypeError("Cannot have a chord made of only 1 unique tone") | 89 | raise TypeError("Cannot have a chord made of only 1 unique tone") | ||
107 | 90 | ||||
108 | @staticmethod | 91 | @staticmethod | ||
109 | def ordered_name(root, tones): | 92 | def ordered_name(root, tones): | ||
110 | """Return the sorted name of the chord by the tones associated with - in between""" | 93 | """Return the sorted name of the chord by the tones associated with - in between""" | ||
111 | start_index = Tone.ORDER.index(root) | 94 | start_index = Tone.ORDER.index(root) | ||
112 | custom_order = Tone.ORDER[start_index:] + Tone.ORDER[:start_index] | 95 | custom_order = Tone.ORDER[start_index:] + Tone.ORDER[:start_index] | ||
113 | unique_tones = list(set(tones)) | 96 | unique_tones = list(set(tones)) | ||
114 | if root in unique_tones: | 97 | if root in unique_tones: | ||
115 | unique_tones.remove(root) | 98 | unique_tones.remove(root) | ||
116 | ordered_tones = sorted(unique_tones, key=lambda tone: custom_order.index(tone)) | 99 | ordered_tones = sorted(unique_tones, key=lambda tone: custom_order.index(tone)) | ||
117 | chord_name = root + '-' + '-'.join(tone for tone in ordered_tones) | 100 | chord_name = root + '-' + '-'.join(tone for tone in ordered_tones) | ||
118 | return chord_name | 101 | return chord_name | ||
119 | 102 | ||||
120 | def __init__(self, root, *args): | 103 | def __init__(self, root, *args): | ||
121 | self.root = root | 104 | self.root = root | ||
122 | self.other_tones = list(args) | 105 | self.other_tones = list(args) | ||
123 | self.other_tones_names = [str(tone) for tone in args] | 106 | self.other_tones_names = [str(tone) for tone in args] | ||
n | 124 | Chord.check_valid_chord([str(self.root)] + self.other_tones_names) | n | 107 | Chord.validate_chord([str(self.root)] + self.other_tones_names) |
125 | self.chord_name = Chord.ordered_name(str(self.root), self.other_tones_names) | 108 | self.chord_name = Chord.ordered_name(str(self.root), self.other_tones_names) | ||
126 | 109 | ||||
127 | def __str__(self): | 110 | def __str__(self): | ||
128 | return self.chord_name | 111 | return self.chord_name | ||
129 | 112 | ||||
130 | def is_minor(self): | 113 | def is_minor(self): | ||
131 | """Check if the chord is minor""" | 114 | """Check if the chord is minor""" | ||
132 | for second_tone in self.other_tones: | 115 | for second_tone in self.other_tones: | ||
n | 133 | if str(Interval.fromTones(self.root, second_tone)) == 'minor 3rd': | n | 116 | if str(Interval.from_tones(self.root, second_tone)) == 'minor 3rd': |
134 | return True | 117 | return True | ||
135 | return False | 118 | return False | ||
136 | 119 | ||||
137 | def is_major(self): | 120 | def is_major(self): | ||
138 | """Check if the chord is major""" | 121 | """Check if the chord is major""" | ||
139 | for second_tone in self.other_tones: | 122 | for second_tone in self.other_tones: | ||
n | 140 | if str(Interval.fromTones(self.root, second_tone)) == 'major 3rd': | n | 123 | if str(Interval.from_tones(self.root, second_tone)) == 'major 3rd': |
141 | return True | 124 | return True | ||
142 | return False | 125 | return False | ||
143 | 126 | ||||
144 | def is_power_chord(self): | 127 | def is_power_chord(self): | ||
145 | """Check if the chord is power chord""" | 128 | """Check if the chord is power chord""" | ||
n | 146 | if not self.is_minor() and not self.is_major(): | n | 129 | return not self.is_minor() and not self.is_major() |
147 | return True | ||||
148 | return False | ||||
149 | 130 | ||||
150 | def __add__(self, other): | 131 | def __add__(self, other): | ||
151 | if isinstance(other, Tone): | 132 | if isinstance(other, Tone): | ||
152 | tones = self.other_tones | 133 | tones = self.other_tones | ||
153 | tones.append(other) | 134 | tones.append(other) | ||
154 | return Chord(self.root, *tones) | 135 | return Chord(self.root, *tones) | ||
155 | elif isinstance(other, Chord): | 136 | elif isinstance(other, Chord): | ||
156 | new_root = self.root | 137 | new_root = self.root | ||
157 | new_tones = self.other_tones + [other.root] + other.other_tones | 138 | new_tones = self.other_tones + [other.root] + other.other_tones | ||
158 | return Chord(new_root, *new_tones) | 139 | return Chord(new_root, *new_tones) | ||
159 | 140 | ||||
160 | def __sub__(self, other): | 141 | def __sub__(self, other): | ||
161 | if other.name != self.root.name and other.name not in self.other_tones_names: | 142 | if other.name != self.root.name and other.name not in self.other_tones_names: | ||
162 | raise TypeError(f"Cannot remove tone {str(other)} from chord {str(self)}") | 143 | raise TypeError(f"Cannot remove tone {str(other)} from chord {str(self)}") | ||
163 | elif len(self.other_tones) < 2: | 144 | elif len(self.other_tones) < 2: | ||
164 | raise TypeError("Cannot have a chord made of only 1 unique tone") | 145 | raise TypeError("Cannot have a chord made of only 1 unique tone") | ||
165 | elif other.name is self.root.name: | 146 | elif other.name is self.root.name: | ||
166 | new_root = self.other_tones_names[1] | 147 | new_root = self.other_tones_names[1] | ||
167 | new_tones = [tone for tone in self.other_tones if tone.name != new_root.name] | 148 | new_tones = [tone for tone in self.other_tones if tone.name != new_root.name] | ||
168 | return Chord(new_root, *new_tones) | 149 | return Chord(new_root, *new_tones) | ||
169 | else: | 150 | else: | ||
170 | new_tones = [tone for tone in self.other_tones if tone.name != other.name] | 151 | new_tones = [tone for tone in self.other_tones if tone.name != other.name] | ||
171 | return Chord(self.root, *new_tones) | 152 | return Chord(self.root, *new_tones) | ||
172 | 153 | ||||
173 | def transposed(self, interval): | 154 | def transposed(self, interval): | ||
174 | """Transpose a chord - shift each of its tones by the same interval""" | 155 | """Transpose a chord - shift each of its tones by the same interval""" | ||
175 | new_root = self.root | 156 | new_root = self.root | ||
t | 176 | new_tones = list() | t | 157 | new_tones = [] |
177 | if interval.operator: | 158 | if interval.operator: | ||
178 | new_root += interval | 159 | new_root += interval | ||
179 | for tone in self.other_tones: | 160 | for tone in self.other_tones: | ||
180 | new_tones.append(tone+interval) | 161 | new_tones.append(tone+interval) | ||
181 | else: | 162 | else: | ||
182 | new_root -= interval | 163 | new_root -= interval | ||
183 | for tone in self.other_tones: | 164 | for tone in self.other_tones: | ||
184 | new_tones.append(tone-interval) | 165 | new_tones.append(tone-interval) | ||
185 | return Chord(new_root, *new_tones) | 166 | return Chord(new_root, *new_tones) |
Legends | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
|