1MAX_COUNT_TONES = 12
2
3
4class Tone:
5 """Represent a musical tone."""
6 TONE_NAMES = ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
7
8 def __init__(self, name):
9 """Initialize a Tone instance with name."""
10 self.name = name
11
12 @property
13 def name(self):
14 """Return the name of the tone."""
15 return self.__name
16
17 @name.setter
18 def name(self, value):
19 """Validate tone and set the tone name."""
20 if value not in self.TONE_NAMES:
21 raise ValueError("Invalid tone name")
22 self.__name = value
23
24 def __str__(self):
25 """Return string representation of the tone."""
26 return self.name
27
28 def __add__(self, other):
29 """Add an interval or a tone to the current tone."""
30 if isinstance(other, Tone):
31 return Chord(self, other)
32 if isinstance(other, Interval):
33 idx = (self.TONE_NAMES.index(self.name) + other.steps) % MAX_COUNT_TONES
34 return Tone(self.TONE_NAMES[idx])
35 raise TypeError("Invalid operation")
36
37 def __sub__(self, other):
38 """Subtract an interval or a tone from the current tone."""
39 if isinstance(other, Tone):
40 steps = (Tone.TONE_NAMES.index(self.name) - Tone.TONE_NAMES.index(other.name)) % MAX_COUNT_TONES
41 return Interval(steps)
42 if isinstance(other, Interval):
43 idx = (self.TONE_NAMES.index(self.name) - other.steps) % MAX_COUNT_TONES
44 return Tone(self.TONE_NAMES[idx])
45 raise TypeError("Invalid operation")
46
47 def __eq__(self, other):
48 """Check if two tones are equal."""
49 return isinstance(other, Tone) and self.name == other.name
50
51
52class Interval:
53 """Represent a musical interval between two tones."""
54 _INTERVAL_NAMES = ("unison", "minor 2nd", "major 2nd", "minor 3rd", "major 3rd",
55 "perfect 4th", "diminished 5th", "perfect 5th", "minor 6th",
56 "major 6th", "minor 7th", "major 7th")
57
58 def __init__(self, steps):
59 """Initialize a Interval instance with steps."""
60 self.steps = steps
61
62 @property
63 def steps(self):
64 """Return the number of the steps for the interval."""
65 return self.__steps
66
67 @steps.setter
68 def steps(self, value):
69 """Validate steps and set the number of steps for the interval."""
70 if not isinstance(value, int):
71 raise ValueError("Invalid steps")
72 self.__steps = value % MAX_COUNT_TONES
73
74 def __str__(self):
75 """Return string representation of the interval."""
76 return self._INTERVAL_NAMES[self.steps]
77
78 def __add__(self, other):
79 """Add an interval to the current tone."""
80 if isinstance(other, Interval):
81 return Interval(self.steps + other.steps)
82 raise TypeError("Invalid operation")
83
84 def __neg__(self):
85 """Negates the interval"""
86 return Interval(-self.steps)
87
88
89class Chord:
90 """Represent a musical chord consisting of a root tone and additional tones."""
91 __MIN_POSSIBLE_TONES = 2
92
93 def __init__(self, root, *tones):
94 """Initialize a Chord instance with a root tone and other tones."""
95 self.root = root
96 self.tones = tones
97
98 @property
99 def root(self):
100 """Return the root tone."""
101 return self.__root
102
103 @root.setter
104 def root(self, value):
105 """Validate root and set the root tone of the chord."""
106 if not isinstance(value, Tone):
107 raise ValueError("Invalid root")
108 self.__root = value
109
110 @property
111 def tones(self):
112 """Return the list of tones in the chord."""
113 return self.__tones
114
115 @tones.setter
116 def tones(self, value):
117 """Validate list of tones and set the list of tones of the chord."""
118 unique_tones = [self.root]
119
120 for tone in value:
121 if not isinstance(tone, Tone):
122 raise ValueError("Invalid tone")
123 if tone not in unique_tones:
124 unique_tones.append(tone)
125
126 if len(unique_tones) < self.__MIN_POSSIBLE_TONES:
127 raise TypeError("Cannot have a chord made of only 1 unique tone")
128
129 root_idx = Tone.TONE_NAMES.index(self.root.name)
130 self.__tones = sorted(unique_tones, key=lambda x: (Tone.TONE_NAMES.index(x.name) - root_idx) % MAX_COUNT_TONES)
131
132 def __str__(self):
133 """Return string representation of the chord."""
134 return "-".join(str(tone) for tone in self.__tones)
135
136 def _check_interval(self, steps):
137 """Helper method to check if the chord contains a tone with a specific interval from the root."""
138 root_idx = Tone.TONE_NAMES.index(self.root.name)
139
140 for t in self.__tones:
141 if t != self.root:
142 tone_index = Tone.TONE_NAMES.index(t.name)
143 interval = (tone_index - root_idx) % MAX_COUNT_TONES
144 if interval == steps:
145 return True
146 return False
147
148 def is_minor(self):
149 """Check if the chord is a minor chord."""
150 return self._check_interval(3)
151
152 def is_major(self):
153 """Check if the chord is a major chord."""
154 return self._check_interval(4)
155
156 def is_power_chord(self):
157 """Check if the chord is a power chord."""
158 return not self.is_minor() and not self.is_major()
159
160 def __add__(self, other):
161 """Add a tone or a chord to the current chord."""
162 if isinstance(other, Tone):
163 if other in self.tones:
164 return self
165 return Chord(self.root, *self.__tones, other)
166 if isinstance(other, Chord):
167 combined_tones = list(self.__tones) + list(other.tones)
168 return Chord(self.root, *combined_tones)
169 raise TypeError("Invalid operation")
170
171 def __sub__(self, other):
172 """Remove a tone from the current chord."""
173 if isinstance(other, Tone):
174 if other not in self.tones:
175 raise TypeError(f"Cannot remove tone {other} from chord {self}")
176 new_tones = [t for t in self.tones if t != other]
177 if len(new_tones) < self.__MIN_POSSIBLE_TONES:
178 raise TypeError("Cannot have a chord made of only 1 unique tone")
179 new_root = new_tones[0] if other == self.root else self.root
180 return Chord(new_root, *new_tones)
181 raise TypeError("Invalid operation")
182
183 def transposed(self, interval):
184 """Transpose the chord by given interval."""
185 if not isinstance(interval, Interval):
186 raise TypeError("Invalid interval")
187
188 transposed_tones = [t + interval for t in self.tones]
189 return Chord(*transposed_tones)
...........................F.........
======================================================================
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=1)
07.11.2024 10:06
07.11.2024 10:12
07.11.2024 10:09
07.11.2024 10:10
07.11.2024 10:12