from typing import Any, Literal, overload
from .constants.scales import RELATIVE_KEY_DICT
from .parser import parse, parse_scale
from .quality import QualityManager, Quality, scale_notes
from .utils import augment, diminish, transpose_note, note_to_val
[docs]
class Chord:
"""
A chord, made up of two or more notes.
:param chord: Name of the chord, e.g. ``"C"``, ``"Am7"``, ``"F#m7-5/A"``.
"""
def __init__(self, chord: str) -> None:
root, quality, on = parse(chord)
self._chord: str = chord
self._root: str = root
self._quality: Quality = quality
self._on: str = on
def __str__(self) -> str:
return self._chord
def __repr__(self) -> str:
return f"<Chord: {self._chord}>"
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Chord):
raise TypeError(f"Cannot compare Chord object with {type(other)} object")
if note_to_val(self._root) != note_to_val(other.root):
return False
if self._quality != other.quality:
return False
if (
# If one chord has an "on" and not the other, they differ.
bool(self._on)
!= bool(other.on)
) or (
# If both chords have an "on" and they are not enharmonic, they differ.
self._on
and other.on
and note_to_val(self._on) != note_to_val(other.on)
):
return False
return True
[docs]
@classmethod
def from_note_index(
cls,
note: int,
quality: str,
scale: str,
diatonic: bool = False,
chromatic: int = 0,
) -> "Chord":
"""Create a :class:`Chord` from a note index in a scale.
- ``Chord.from_note_index(1, "", "Cmaj")`` returns I of C major => Chord("C")
- ``Chord.from_note_index(3, "m7", "Fmaj")`` returns IIImin of F major => Chord("Am7")
- ``Chord.from_note_index(5, "7", "Amin")`` returns Vmin of A minor => Chord("E7")
- ``Chord.from_note_index(2, "", "Cmaj")`` returns II of C major => Chord("D")
- ``Chord.from_note_index(2, "m", "Cmaj")`` returns IImin of C major => Chord("Dm")
- ``Chord.from_note_index(2, "", "Cmaj", diatonic=True)`` returns IImin of C major => Chord("Dm")
- ``Chord.from_note_index(2, "", "Cmin", chromatic=-1)`` returns bII of C minor => Chord("Db")
:param note: Scale degree of the chord's root, ``1`` to ``7``.
:param quality: Quality of the chord, e.g. ``"m7"``, ``"sus4"``.
:param scale: Base scale, e.g. ``"Cmaj"``, ``"Amin"``, ``"F#maj"``, ``"Ebmin"``.
:param diatonic: If True, chord quality is determined using the base scale (overrides ``quality``).
:param chromatic: Lower or raise the scale degree (and all notes of the chord) by semitone(s).
"""
if not 1 <= note <= 7:
raise ValueError(f"Invalid note {note}")
scale_root, scale_mode = parse_scale(scale)
root = scale_notes(scale_root, scale_mode)[note - 1]
if chromatic != 0:
alter = augment if chromatic > 0 else diminish
for i in range(abs(chromatic)):
root = alter(root)
if diatonic:
scale_degrees = RELATIVE_KEY_DICT[scale_mode]
# construct the chord based on scale degrees, within 1 octave
first = scale_degrees[note - 1]
third = scale_degrees[(note + 1) % 7]
fifth = scale_degrees[(note + 3) % 7]
seventh = scale_degrees[(note + 5) % 7]
# adjust the chord to its root position (as a stack of thirds),
# then set the root to 0
# e.g. (9, 0, 4) -> [0, 3, 7]
def get_diatonic_chord(chord: tuple[int, ...]) -> list[int]:
uninverted: list[int] = []
for note in chord:
if not uninverted:
uninverted.append(note)
elif note > uninverted[-1]:
uninverted.append(note)
else:
uninverted.append(note + 12)
uninverted = [x - uninverted[0] for x in uninverted]
return uninverted
if quality in ["", "-", "maj", "m", "min"]:
triad = (first, third, fifth)
q = get_diatonic_chord(triad)
elif quality in ["7", "M7", "maj7", "m7"]:
seventh_chord = (first, third, fifth, seventh)
q = get_diatonic_chord(seventh_chord)
else:
raise NotImplementedError(
"Only generic chords (triads, sevenths) are supported"
)
# look up QualityManager to determine chord quality
quality_manager = QualityManager()
quality_instance = quality_manager.find_quality_from_components(q)
assert quality_instance is not None
quality = quality_instance.quality
return cls(f"{root}{quality}")
@property
def chord(self) -> str:
"""
The name of the chord, e.g. ``"C"``, ``"Am7"``, ``"F#m7-5/A"``.
"""
return self._chord
@property
def root(self) -> str:
"""
The root note of the chord, e.g. ``"C"``, ``"A"``, ``"F#"``.
"""
return self._root
@property
def quality(self) -> Quality:
"""
The quality of the chord, e.g. ``"maj"``, ``"m7"``, ``"m7-5"``.
"""
return self._quality
@property
def on(self) -> str:
"""
The bass note of a slash chord.
"""
return self._on
[docs]
def info(self) -> str:
"""
Return information of chord to display.
"""
return f"""{self._chord}
root={self._root}
quality={self._quality}
on={self._on}"""
[docs]
def transpose(self, trans: int, scale: str = "C") -> None:
"""
Transpose the chord.
:param trans: The number of semitones.
:param scale: Key scale.
"""
if not isinstance(trans, int):
raise TypeError(f"Expected integers, not {type(trans)}")
self._root = transpose_note(self._root, trans, scale)
if self._on:
self._on = transpose_note(self._on, trans, scale)
self._reconfigure_chord()
@overload
def components(self, visible: Literal[True]) -> list[str]: ...
@overload
def components(self, visible: Literal[False]) -> list[int]: ...
[docs]
def components(self, visible: bool = True) -> list[str] | list[int]:
"""
Return the component notes of the chord.
:param visible: Returns the note names if ``True``, the note pitches otherwise.
"""
if visible:
notes = self._quality.get_components(root=self._root, visible=True)
if self._on:
notes = [n for n in notes if n != self._on]
notes.insert(0, self._on)
return notes
else:
components = self._quality.get_components(root=self._root, visible=False)
if self._on:
on_value = note_to_val(self._on)
components = [c for c in components if c % 12 != on_value % 12]
if on_value > components[0]:
on_value -= 12
components.insert(0, on_value)
return components
[docs]
def components_with_pitch(self, root_pitch: int) -> list[str]:
"""
Return the component notes of chord formatted like ``["C4", "E4", "G4"]``.
:param root_pitch: The pitch of the root note.
"""
components = self.components(visible=False)
notes = self.components(visible=True)
if components[0] < 0:
components = [c + 12 for c in components]
return [f"{n}{root_pitch + c // 12}" for (n, c) in zip(notes, components)]
def _reconfigure_chord(self) -> None:
self._chord = "{}{}{}".format(
self._root,
self._quality.quality,
f"/{self._on}" if self._on else "",
)