1class Tone:
2
3 tones = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
4 tone_to_index = {tone: idx for idx, tone in enumerate(tones)}
5
6 def __init__(self, name):
7 self.name = name
8
9 def __str__(self):
10 return self.name
11
12 def __sub__(self, other):
13 if isinstance(other, Tone):
14 difference = (Tone.tone_to_index[self.name] - Tone.tone_to_index[other.name]) % 12
15 return Interval(difference)
16 elif isinstance(other, Interval):
17 new_index = (Tone.tone_to_index[self.name] - other.semitones) % 12
18 return Tone(Tone.tones[new_index])
19
20 def __add__(self, other):
21 if isinstance(other, Tone):
22 return Chord(self, other)
23 elif isinstance(other, Interval):
24 new_tone_index = (Tone.tone_to_index[self.name] + other.semitones) % 12
25 return Tone(Tone.tones[new_tone_index])
26
27class Interval:
28 intervals = {
29 0: "unison", 1: "minor 2nd", 2: "major 2nd", 3: "minor 3rd", 4: "major 3rd",
30 5: "perfect 4th", 6: "tritone", 7: "perfect 5th", 8: "minor 6th", 9: "major 6th",
31 10: "minor 7th", 11: "major 7th"
32 }
33
34 def __init__(self, semitones):
35 self.semitones = semitones % 12
36
37 def __str__(self):
38 return self.intervals[self.semitones]
39
40 def __add__(self, other):
41 if isinstance(other, Interval):
42 return Interval(self.semitones + other.semitones)
43 else:
44 raise TypeError("Invalid operation")
45
46 def __neg__(self):
47 return Interval((12 - self.semitones) % 12)
48
49class Chord:
50
51 def __init__(self, root, *args):
52 self.root = root
53 tones = set(tone for tone in args if isinstance(tone, Tone) and str(tone) != str(root))
54 if not tones:
55 raise TypeError("Cannot have a chord made of only 1 unique tone")
56 self.tones = sorted(tones, key=lambda t: (Tone.tone_to_index[t.name] - Tone.tone_to_index[root.name]) % 12)
57
58 def __str__(self):
59 return str(self.root) + '-' + '-'.join(str(tone) for tone in self.tones)
60
61 def is_minor(self):
62 return any((tone - self.root).semitones == 3 for tone in self.tones)
63
64 def is_major(self):
65 return any((tone - self.root).semitones == 4 for tone in self.tones)
66
67 def is_power_chord(self):
68 return not (self.is_minor() or self.is_major())
69
70 def __add__(self, other):
71 if isinstance(other, Tone):
72 new_tones = set(self.tones) | {other}
73 return Chord(self.root, *new_tones)
74 elif isinstance(other, Chord):
75 new_root = self.root
76 new_tones = [tone for tone in {other.root} | set(other.tones) | set(self.tones)
77 if tone.name != self.root.name]
78 return Chord(new_root, *new_tones)
79
80 def __sub__(self, other):
81 if isinstance(other, Tone):
82 if not any(tone.name == other.name for tone in self.tones) and other.name != self.root.name:
83 raise TypeError(f"Cannot remove tone {other} from chord {self}")
84 if len(self.tones) == 1:
85 raise TypeError("Cannot have a chord made of only 1 unique tone")
86 new_root = self.root
87 new_tones = list(self.tones)
88 if other.name == self.root.name:
89 new_root = new_tones[0]
90 new_tones.pop(0)
91 new_tones = [tone for tone in self.tones if tone.name != other.name]
92 return Chord(new_root, *new_tones)
93
94 def transposed(self, interval):
95 transposed_root = self.root + interval
96 transposed_tones = [tone + interval for tone in self.tones]
97 return Chord(transposed_root, *transposed_tones)
...F.........F..F.....F....F.........
======================================================================
FAIL: test_chord_tone_repetition (test.TestBasicChordFunctionality.test_chord_tone_repetition)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 74, in test_chord_tone_repetition
self.assertEqual(str(a_minor_chord), "A-C-E")
AssertionError: 'A-C-C-C-C-C-C-C-C-C-C-C-C-C-C-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E' != 'A-C-E'
- A-C-C-C-C-C-C-C-C-C-C-C-C-C-C-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E
+ A-C-E
======================================================================
FAIL: test_interval_str (test.TestBasicIntervalFunctionality.test_interval_str)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 42, in test_interval_str
self.assertEqual(str(Interval(index)), interval)
AssertionError: 'tritone' != 'diminished 5th'
- tritone
+ diminished 5th
======================================================================
FAIL: test_add_chords_repeating_notes (test.TestOperations.test_add_chords_repeating_notes)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 305, in test_add_chords_repeating_notes
self.assertEqual(str(result_chord), "C-G")
AssertionError: 'C-G-G' != 'C-G'
- C-G-G
? --
+ C-G
======================================================================
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: 'F-G#-G#-C-C' != 'F-G#-C'
- F-G#-G#-C-C
+ F-G#-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 10:29
07.11.2024 10:30
07.11.2024 10:30
07.11.2024 10:30
07.11.2024 10:31
07.11.2024 10:33
07.11.2024 10:33