+ self.other_notes: Dict[int, Optional[str]] = {}
+ for semitone_count in other_notes:
+ name = Chord.describe_interval(semitone_count)
+ if name:
+ self.other_notes[semitone_count] = name
+ else:
+ self.other_notes[semitone_count] = None
+ self.scale = list(itertools.islice(self.generate_scale(), 24))
+
+ def generate_scale(self) -> Iterator[str]:
+ starting_note = self.root_note
+ start = 0
+ while Chord.NOTES[start] != starting_note:
+ start += 1
+ while True:
+ if start >= len(Chord.NOTES):
+ start = 0
+ yield Chord.NOTES[start]
+ start += 1
+
+ @staticmethod
+ def describe_interval(semitone_count: int) -> Optional[str]:
+ names: Dict[int, str] = {
+ 0: 'perfect unison',
+ 1: 'minor 2nd',
+ 2: 'major 2nd',
+ 3: 'minor 3rd',
+ 4: 'major 3rd',
+ 5: 'perfect 4th',
+ 6: 'diminished 5th',
+ 7: 'perfect 5th',
+ 8: 'minor 6th',
+ 9: 'major 6th',
+ 10: 'minor 7th',
+ 11: 'major 7th',
+ 12: 'perfect octave',
+ 13: 'minor 9th',
+ 14: 'major 9th',
+ 15: 'minor 10th',
+ 16: 'major 10th',
+ 17: 'perfect 11th',
+ 18: 'diminished 12th',
+ 19: 'perfect 12th',
+ 20: 'minor 13th',
+ 21: 'major 13th',
+ 22: 'minor 14th',
+ 23: 'major 14th',
+ 24: 'double octave',
+ 25: 'augmented 15th',
+ }
+ return names.get(semitone_count, None)
+
+ def degree_of_note(self, target_note: str) -> Optional[Tuple[Optional[str], int]]:
+ target_note = target_note.upper()
+ for degree, note in enumerate(itertools.islice(self.generate_scale(), 24)):
+ if note == target_note:
+ return (Chord.describe_interval(degree), degree)
+ return None
+
+ def note_at_degree(self, semitone_count: int) -> Optional[str]:
+ if semitone_count < len(self.scale):
+ return self.scale[semitone_count]
+ return None
+
+ def add_bass(self, bass_note: str) -> None:
+ name, semitone_count = self.degree_of_note(bass_note)
+ if name:
+ self.other_notes[semitone_count] = f'{name} (bass)'
+ else:
+ self.other_notes[semitone_count] = 'bass note'
+
+ def describe_chord(self) -> str:
+ names: Dict[Tuple[int], str] = {
+ (2, 7): 'sus2',
+ (4, 7): 'major',
+ (5, 7): 'sus4',
+ (3, 7): 'minor',
+ (4, 7, 10): 'dom7',
+ (4, 7, 11): 'maj7',
+ (4, 8): 'aug',
+ (3, 5): 'dim',
+ (4, 7, 10, 17): 'add11',
+ (4, 7, 10, 14): 'add9',
+ (5): 'power chord',
+ }
+ intervals = list(self.other_notes.keys())
+ intervals.sort()
+ intervals_set = set(intervals)
+ for interval in reversed(list(list_utils.powerset(intervals))):
+ name = names.get(tuple(interval), None)
+ if name:
+ for x in interval:
+ intervals_set.remove(x)
+ break
+
+ if not name:
+ name = 'An unknown chord'
+
+ for semitone_count, note_name in self.other_notes.items():
+ if 'bass' in note_name:
+ note = self.note_at_degree(semitone_count)
+ name += f' with a bass {note}'
+ if semitone_count in intervals_set:
+ intervals_set.remove(semitone_count)
+
+ for note in intervals_set:
+ note = self.note_at_degree(note)
+ name += f' add {note}'
+
+ return f'{self.root_note} ({name})'
+
+ def __repr__(self):
+ name = self.describe_chord()
+ ret = f'{name}\n'
+ ret += f'root={self.root_note}\n'
+ for semitone_count, interval_name in self.other_notes.items():
+ note = self.note_at_degree(semitone_count)
+ ret += f'+ {interval_name} ({semitone_count}) => {note}\n'
+ return ret