Source code for pychord.chord

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 "", )