1TONE_NAMES = (
2 "C",
3 "C#",
4 "D",
5 "D#",
6 "E",
7 "F",
8 "F#",
9 "G",
10 "G#",
11 "A",
12 "A#",
13 "B"
14 )
15
16TONES_IN_OCTAVE = 12
17
18INTERVAL_NAMES = (
19 "unison",
20 "minor 2nd",
21 "major 2nd",
22 "minor 3rd",
23 "major 3rd",
24 "perfect 4th",
25 "diminished 5th",
26 "perfect 5th",
27 "minor 6th",
28 "major 6th",
29 "minor 7th",
30 "major 7th"
31 )
32
33
34class Tone:
35 def __init__(self, name):
36 self.name = name
37
38 def __str__(self):
39 return self.name
40
41 def __add__(self, other):
42 if isinstance(other, Tone):
43 return Chord(self, other)
44 elif isinstance(other, Interval):
45 return self._add_interval(other)
46 else:
47 raise TypeError("Invalid operation")
48
49 def _add_interval(self, interval):
50 """Collects a tone and an interval and returns a new tone."""
51
52 tone_index = TONE_NAMES.index(self.name)
53 new_index = (tone_index + interval.halftone) % len(TONE_NAMES)
54 return Tone(TONE_NAMES[new_index])
55
56 def __sub__(self, other):
57 if isinstance(other, Interval):
58 return self._sub_interval(other)
59 if isinstance(other, Tone):
60 return self._sub_tone(other)
61 else:
62 raise TypeError("Invalid operation")
63
64 def _sub_interval(self, interval):
65 """Subtracts an interval from a tone and returns a new tone with a changed pitch."""
66
67 tone_index = TONE_NAMES.index(self.name)
68 new_index = (tone_index - interval.halftone) % len(TONE_NAMES)
69 return Tone(TONE_NAMES[new_index])
70
71 def _sub_tone(self, other_tone):
72 """Subtracts a tone from a tone and returns a space."""
73
74 tone_index = TONE_NAMES.index(self.name)
75 other_tone_index = TONE_NAMES.index(other_tone.name)
76 distance = (tone_index - other_tone_index) % len(TONE_NAMES)
77
78 return Interval(distance)
79
80
81class Interval:
82 def __init__(self, halftone):
83 self.halftone = halftone % TONES_IN_OCTAVE
84
85 def __add__(self, other):
86 if isinstance(other, Interval):
87 new_halftone = (self.halftone + other.halftone) % TONES_IN_OCTAVE
88 return Interval(new_halftone)
89
90 def __str__(self):
91 return INTERVAL_NAMES[self.halftone]
92
93 def __neg__(self):
94 """Returns a new Interval object with a negative semitone value."""
95 return Interval(-self.halftone)
96
97
98class Chord:
99 def __init__(self, base_tone, *other_tones):
100 unique_tones = [base_tone.name]
101
102 for tone in other_tones:
103 if tone.name not in unique_tones:
104 unique_tones.append(tone.name)
105
106 if len(unique_tones) < 2:
107 raise TypeError("Cannot have a chord made of only 1 unique tone")
108
109 self.base_tone = base_tone
110 self.tones = unique_tones
111
112 def get_tone_index(self, tone_name):
113 return TONE_NAMES.index(tone_name)
114
115 def __str__(self):
116 root_index = self.get_tone_index(self.base_tone.name)
117 self.tones.sort(key = lambda current_tone: (TONE_NAMES.index(current_tone) - root_index) % TONES_IN_OCTAVE)
118 return "-".join(self.tones)
119
120 def is_minor(self):
121 MINOR_INTERVAL = 3
122 root_index = self.get_tone_index(self.base_tone.name)
123 for tone_name in self.tones:
124 if (self.get_tone_index(tone_name) - root_index) % TONES_IN_OCTAVE == MINOR_INTERVAL:
125 return True
126 return False
127
128 def is_major(self):
129 MAJOR_INTERVAL = 4
130 root_index = self.get_tone_index(self.base_tone.name)
131 for tone_name in self.tones:
132 if (self.get_tone_index(tone_name) - root_index) % TONES_IN_OCTAVE == MAJOR_INTERVAL:
133 return True
134 return False
135
136 def is_power_chord(self):
137 if self.is_minor() or self.is_major():
138 return False
139 return True
140
141 def __add__(self, other):
142 """Matching a chord with a tone or with another chord."""
143
144 if isinstance(other, Tone):
145 new_tones = self.tones + [other.name]
146 sorted_tones = sorted(set(new_tones), key=lambda t: TONE_NAMES.index(t))
147 return Chord(Tone(sorted_tones[0]), *[Tone(t) for t in sorted_tones[1:]])
148
149 elif isinstance(other, Chord):
150 combined_tones = self.tones + other.tones
151 unique_tones = []
152 for tone in combined_tones:
153 if tone not in unique_tones:
154 unique_tones.append(tone)
155 return Chord(Tone(unique_tones[0]), *[Tone(t) for t in unique_tones[1:]])
156
157 else:
158 raise TypeError("Invalid operation")
159
160 def __sub__(self, tone):
161 if tone.name not in self.tones:
162 raise TypeError(f"Cannot remove tone {tone} from chord {self}")
163
164 new_tones = [t for t in self.tones if t != tone.name]
165 if len(new_tones) < 2:
166 raise TypeError("Cannot have a chord made of only 1 unique tone")
167
168 return Chord(self.base_tone, *[Tone(t) for t in new_tones])
169
170 def transposed(self, interval):
171 """Transposes the chord by the given interval."""
172
173 if not isinstance(interval, Interval):
174 raise TypeError("Expected an Interval object for transposition")
175
176 transposed_tones = []
177 for tone_name in self.tones:
178 tone = Tone(tone_name)
179 if interval.halftone > 0:
180 transposed_tone = tone + interval
181 else:
182 transposed_tone = tone - Interval(abs(interval.halftone))
183 transposed_tones.append(transposed_tone)
184
185 return Chord(*transposed_tones)
..................F..FFF...F.........
======================================================================
FAIL: test_add_interval_to_tone_left_side_error (test.TestOperations.test_add_interval_to_tone_left_side_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 209, in test_add_interval_to_tone_left_side_error
with self.assertRaises(TypeError) as err:
AssertionError: TypeError not raised
======================================================================
FAIL: test_add_tone_to_chord (test.TestOperations.test_add_tone_to_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 254, in test_add_tone_to_chord
self.assertEqual(str(result_chord), "F-A-C-D")
AssertionError: 'C-D-F-A' != 'F-A-C-D'
- C-D-F-A
+ F-A-C-D
======================================================================
FAIL: test_add_tone_to_chord_existing_tone (test.TestOperations.test_add_tone_to_chord_existing_tone)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 270, in test_add_tone_to_chord_existing_tone
self.assertEqual(str(result_chord), "F-G#-C")
AssertionError: 'C-F-G#' != 'F-G#-C'
- C-F-G#
+ F-G#-C
======================================================================
FAIL: test_add_tone_to_chord_order (test.TestOperations.test_add_tone_to_chord_order)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 261, in test_add_tone_to_chord_order
self.assertEqual(str(result_chord), "F-G-A-C")
AssertionError: 'C-F-G-A' != 'F-G-A-C'
- C-F-G-A
? --
+ F-G-A-C
? ++
======================================================================
FAIL: test_subtract_interval_from_tone_left_side_error (test.TestOperations.test_subtract_interval_from_tone_left_side_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 235, in test_subtract_interval_from_tone_left_side_error
self.assertEqual(str(err.exception), INVALID_OPERATION)
AssertionError: "unsupported operand type(s) for -: 'Interval' and 'Tone'" != 'Invalid operation'
- unsupported operand type(s) for -: 'Interval' and 'Tone'
+ Invalid operation
----------------------------------------------------------------------
Ran 37 tests in 0.002s
FAILED (failures=5)
07.11.2024 11:01
07.11.2024 11:02
07.11.2024 11:03
07.11.2024 11:05
07.11.2024 11:06
07.11.2024 11:07
07.11.2024 11:08