1# -*- coding: utf-8 -*-
2"""
3Created on Sun Nov 3 17:03:13 2024
4
5@author: User
6 """
7intervals = [
8 "unison", "minor 2nd", "major 2nd", "minor 3rd", "major 3rd",
9 "perfect 4th", "diminished 5th", "perfect 5th", "minor 6th",
10 "major 6th", "minor 7th", "major 7th", "octave"
11 ]
12
13scale = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
14
15
16def sort_tones(root, unique_elements):
17 l = []
18 if root.name in scale:
19 ind = scale.index(root.name)
20
21 for i in range(ind, 11):
22 if scale[i] in [ul.name for ul in unique_elements]:
23 l.append(scale[i]) # Process the element as needed
24
25 # Then, wrap around and iterate from the beginning to start_index - 1
26 for i in range(0, ind):
27 if scale[i] in [ul.name for ul in unique_elements]:
28 l.append(scale[i]) # Process the element as needed
29 return l
30
31 else:
32 return []
33
34
35class Tone:
36 def __init__(self, name):
37 if name in scale:
38 self.name = name
39 else:
40 raise ValueError("Wrong tone!")
41
42 def __str__(self):
43 return self.name
44
45 def __add__(self, other):
46 if isinstance(other, Tone):
47 return Chord(self, other) # Връща нов обект Chord
48 elif isinstance(other, Interval):
49 start_index = scale.index(self.name)
50 new_index = (start_index + other.semitones) % 12
51 return Tone(scale[new_index])
52 raise TypeError("Invalid operation")
53
54 def __sub__(self, other):
55 if isinstance(other, Tone):
56 index_self = scale.index(self.name)
57 index_other = scale.index(other.name)
58 interval_semitones = abs(index_other - index_self) % 12
59 return Interval(interval_semitones) # Връща нов обект Interval
60 elif isinstance(other, Interval):
61 start_index = scale.index(self.name)
62 new_index = (start_index - other.semitones) % 12
63 return Tone(scale[new_index])
64 raise TypeError("Operand must be an instance of Tone")
65
66 def transposed(self, interval):
67 start_index = scale.index(self.name)
68 new_index = (start_index + interval.semitones) % 12
69 return Tone(scale[new_index])
70
71
72class Interval:
73 def __init__(self, semitones):
74 self.semitones = semitones % 12
75
76 def __str__(self):
77 return intervals[self.semitones]
78
79 def __add__(self, other):
80 if isinstance(other, Interval):
81 combined_semitones = (self.semitones + other.semitones) % 12
82 return Interval(combined_semitones)
83 raise TypeError("Can only add Interval to Interval")
84
85 def __neg__(self):
86 return Interval(-self.semitones)
87
88
89class Chord:
90 def __init__(self, root, *tones):
91 other_tones = [tone.name for tone in tones]
92 unique_tones = {root, *tones}
93 other_tones.append(root.name)
94 if len(other_tones) < 2:
95 raise TypeError("Cannot have a chord made of only 1 unique tone")
96
97 self.root = root
98 # self.tones = sorted(unique_tones, key=lambda tone: scale.index(tone.name))
99 self.tones = sort_tones(root, unique_tones)
100
101 def __add__(self, other):
102 if isinstance(other, Tone):
103 if other.name in self.tones:
104 return self
105
106 return Chord( *(Tone(tone.name) for tone in self.tones), other)
107 # else:
108 # raise TypeError("Can only add a Tone to a Chord")
109 if isinstance(other, Chord):
110 combined_tones = set([tone.name for tone in self.tones]) | set([tone.name for tone in other.tones])
111 return Chord(*(Tone(tone) for tone in combined_tones))
112 # else:
113 # raise TypeError("Can only add a Chord to another Chord")
114
115
116 def __sub__(self, other):
117 if isinstance(other, Tone):
118 if other.name not in [tone.name for tone in self.tones]:
119 raise TypeError(f"Cannot remove tone {other} from chord {self}")
120 remaining_tones = [tone.name for tone in self.tones if tone.name != other.name]
121 if len(remaining_tones) < 2:
122 raise TypeError("Cannot have a chord made of only 1 unique tone")
123 return Chord( *(Tone(tone) for tone in remaining_tones))
124 raise TypeError("Can only subtract a Tone from a Chord")
125
126 def is_minor(self):
127 for tone in self.tones:
128 if tone != self.root:
129 interval = (scale.index(tone.name) - scale.index(self.root.name)) % 12
130 if interval == 3: # Minor 3rd
131 return True
132 return False
133
134 def is_major(self):
135 for tone in self.tones:
136 if tone != self.root:
137 interval = (scale.index(tone.name) - scale.index(self.root.name)) % 12
138 if interval == 4: # Major 3rd
139 return True
140 return False
141
142 def is_power_chord(self):
143 return not self.is_minor() and not self.is_major()
144
145 def __str__(self):
146 return "-".join(str(tone) for tone in self.tones)
147
148 def transposed(self, interval):
149 if not isinstance(interval, Interval):
150 raise TypeError("Interval expected")
151
152 transposed_tones = [Tone(tone.name).transposed(interval) for tone in self.tones]
153
154 return Chord(transposed_tones[0], *transposed_tones[1:])
F...EEEEEEE....EE.F..E.E...F..EE.F.F.
======================================================================
ERROR: test_is_major (test.TestBasicChordFunctionality.test_is_major)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 103, in test_is_major
self.assertFalse(a_minor_chord.is_major())
^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 137, in is_major
interval = (scale.index(tone.name) - scale.index(self.root.name)) % 12
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: 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())
^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 129, in is_minor
interval = (scale.index(tone.name) - scale.index(self.root.name)) % 12
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: 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())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 143, in is_power_chord
return not self.is_minor() and not self.is_major()
^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 129, in is_minor
interval = (scale.index(tone.name) - scale.index(self.root.name)) % 12
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_transposed (test.TestBasicChordFunctionality.test_transposed)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 129, in test_transposed
result_chord = d_major_chord.transposed(Interval(2))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 152, in transposed
transposed_tones = [Tone(tone.name).transposed(interval) for tone in self.tones]
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_transposed_negative (test.TestBasicChordFunctionality.test_transposed_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 136, in test_transposed_negative
result_chord = e_minor_chord.transposed(-Interval(2))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 152, in transposed
transposed_tones = [Tone(tone.name).transposed(interval) for tone in self.tones]
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_transposed_negative_overflow (test.TestBasicChordFunctionality.test_transposed_negative_overflow)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 150, in test_transposed_negative_overflow
result_chord = e_minor_chord.transposed(-Interval(8))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 152, in transposed
transposed_tones = [Tone(tone.name).transposed(interval) for tone in self.tones]
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_transposed_overflow (test.TestBasicChordFunctionality.test_transposed_overflow)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 143, in test_transposed_overflow
result_chord = d_major_chord.transposed(Interval(8))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 152, in transposed
transposed_tones = [Tone(tone.name).transposed(interval) for tone in self.tones]
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_add_chords (test.TestOperations.test_add_chords)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 298, in test_add_chords
result_chord = c5_chord + this_other_chord
~~~~~~~~~^~~~~~~~~~~~~~~~~~
File "/tmp/solution.py", line 110, in __add__
combined_tones = set([tone.name for tone in self.tones]) | set([tone.name for tone in other.tones])
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_add_chords_repeating_notes (test.TestOperations.test_add_chords_repeating_notes)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 304, in test_add_chords_repeating_notes
result_chord = c5_chord + inverted_c5_chord
~~~~~~~~~^~~~~~~~~~~~~~~~~~~
File "/tmp/solution.py", line 110, in __add__
combined_tones = set([tone.name for tone in self.tones]) | set([tone.name for tone in other.tones])
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_add_tone_to_chord (test.TestOperations.test_add_tone_to_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 252, in test_add_tone_to_chord
result_chord = f_major_chord + Tone("D")
~~~~~~~~~~~~~~^~~~~~~~~~~
File "/tmp/solution.py", line 106, in __add__
return Chord( *(Tone(tone.name) for tone in self.tones), other)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 106, in <genexpr>
return Chord( *(Tone(tone.name) for tone in self.tones), other)
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_add_tone_to_chord_order (test.TestOperations.test_add_tone_to_chord_order)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 259, in test_add_tone_to_chord_order
result_chord = f_major_chord + Tone("G")
~~~~~~~~~~~~~~^~~~~~~~~~~
File "/tmp/solution.py", line 106, in __add__
return Chord( *(Tone(tone.name) for tone in self.tones), other)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 106, in <genexpr>
return Chord( *(Tone(tone.name) for tone in self.tones), other)
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_subtract_tone_from_chord (test.TestOperations.test_subtract_tone_from_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 275, in test_subtract_tone_from_chord
result_chord = f_minor_chord - Tone("C")
~~~~~~~~~~~~~~^~~~~~~~~~~
File "/tmp/solution.py", line 118, in __sub__
if other.name not in [tone.name for tone in self.tones]:
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
ERROR: test_subtract_tone_from_chord_error (test.TestOperations.test_subtract_tone_from_chord_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 292, in test_subtract_tone_from_chord_error
c5_chord - a
~~~~~~~~~^~~
File "/tmp/solution.py", line 118, in __sub__
if other.name not in [tone.name for tone in self.tones]:
^^^^^^^^^
AttributeError: 'str' object has no attribute 'name'
======================================================================
FAIL: test_chord_not_enough_tones (test.TestBasicChordFunctionality.test_chord_not_enough_tones)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 83, in test_chord_not_enough_tones
with self.assertRaises(TypeError) as err:
AssertionError: TypeError not raised
======================================================================
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 211, in test_add_interval_to_tone_left_side_error
self.assertEqual(str(err.exception), INVALID_OPERATION)
AssertionError: 'Can only add Interval to Interval' != 'Invalid operation'
- Can only add Interval to Interval
+ Invalid operation
======================================================================
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
======================================================================
FAIL: test_tone_addition_same_tone (test.TestOperations.test_tone_addition_same_tone)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 166, in test_tone_addition_same_tone
with self.assertRaises(TypeError) as err:
AssertionError: TypeError not raised
======================================================================
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.006s
FAILED (failures=5, errors=13)
07.11.2024 09:34
07.11.2024 09:35
07.11.2024 09:39
07.11.2024 09:35
07.11.2024 09:40
07.11.2024 09:41
07.11.2024 09:43
07.11.2024 09:44