Skip to content

Chord

Chord

Chord(
    root: Union[str, int, Pitch, None] = None,
    quality: Optional[str] = None,
    pitches: Optional[Sequence] = None,
    bass: Union[str, int, Pitch, None] = None,
)

Represents a chord as a set of pitch classes, with optional root, quality, and spelling. Properties that cannot be determined are set to None.

Parameters:

  • root (str | int | Pitch | None, default: None ) –

    The root of the chord, as a pitch name ("C"), pitch-class int (0), or Pitch. If None, a rootless chord is created from pitches.

  • quality (str | None, default: None ) –

    Quality string, e.g. "major", "min7", "°" typically as imported from some user provided string, or corpus source. If both root and quality are given, pitches is derived automatically. If only root is given, quality remains None unless it can be inferred from pitches.

  • pitches (sequence of (str | int | Pitch) | None, default: None ) –

    Explicit pitch members. Each element may be a pitch name ("Bb"), a pitch-class int (10), or a Pitch. If given alongside root and quality, these are stored as the chord's spelling.

  • bass (str | int | Pitch | None, default: None ) –

    The bass note if it differs from the root (slash chords, inversions).

Attributes:

  • root (Pitch | None) –
  • quality (str | None) –

    Canonical quality name, e.g. "major", "minor7".

  • pitches (tuple[Pitch, ...] | None) –

    Spelling as pitch-class Pitches (octave == -1), in the order given.

  • bass (Pitch | None) –
  • key (str | None) –

    Set when constructed via :meth:from_roman.

Examples:

These examples demonstrate different ways to build Chord objects, and some of the attributes.

  1. From a root + quality string:
>>> c = Chord("C", "major")
>>> c
Chord(C major)
>>> c == Chord("C", "maj")
True

Here are some derived properties:

>>> c.pitch_class_set
[0, 4, 7]
>>> c.intervals
(0, 4, 7)
>>> c.pitch_class_vector
(1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0)

Here is a different quality example (see QUALTIES for all options):

>>> Chord("G", "dominant7").label
'G7'
  1. From a Roman numeral, given a key.
>>> Chord.from_roman("IV", "C").root.name
'F'
>>> Chord.from_roman("IV", "C").root.pitch_class
5
>>> Chord.from_roman("V7", "G").label
'D7'

Roman numerals are complex and not consistent across standards. This illustrates a case in point: The triad in a minor key on the second degree is diminshed diatonically, and the seventh is a half-diminshed. Parsing routines vary. Here we take "ii7" to imply the diatonic half-diminshed, "iiø7" is equivalent (not needed to specify the half-diminshed) and "iio7" modifies to diminished. See also the from_scale_degree method (also demo'd here).

>>> Chord.from_roman("ii", "d").label
'Eo'
>>> Chord.from_roman("ii7", "d").label
'Eø7'
>>> Chord.from_roman("iiø7", "d").label
'Eø7'
>>> Chord.from_scale_degree(2, "d").label
'Eo'

Explicit suffix takes priority over scale-stacking

>>> Chord.from_roman("iio7", "d").label
'Eo7'

Figured-bass inversion figures are recognised and set the bass note accordingly:

>>> c_e = Chord.from_roman("I6", "C")   # first-inversion tonic triad
>>> c_e.label
'C/E'
>>> c_e.inversion
1
>>> Chord.from_roman("I64", "C").label  # second-inversion tonic triad
'C/G'
>>> Chord.from_roman("V65", "C").label  # first-inversion dominant seventh
'G7/B'
>>> Chord.from_roman("V42", "C").label  # third-inversion dominant seventh
'G7/F'

Some flexibiltiy in the string formatting, e.g., here simplifying to 2 and expanding to the full 642

>>> Chord.from_roman("V642", "C").label  # "
'G7/F'
>>> Chord.from_roman("V2", "C").label    # "
'G7/F'
>>> Chord.from_roman("V642", "C").inversion
3

Only specific, recognised figured-bass syntax is accepted.

>>> Chord.from_roman("I69", "C")
Traceback (most recent call last):
    ...
ValueError: Unrecognised figure '69' in 'I69'. Recognised figures: ...
  1. From pitches directly, either as pitch name or pitch class:
>>> Chord(pitches=["C", "Eb", "G"]).quality
'minor'
>>> Chord(pitches=[0, 3, 7]).quality
'minor'
>>> Chord(pitches=["C", "E", "G", "Bb"]).label
'C7'
Source code in amads/core/chord.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def __init__(
    self,
    root:    Union[str, int, Pitch, None] = None,
    quality: Optional[str]                = None,
    pitches: Optional[Sequence]           = None,
    bass:    Union[str, int, Pitch, None] = None,
) -> None:

    self.key = None
    self.root = _parse_root(root) if root is not None else None
    self.quality = _canonical_quality(quality) if quality is not None else None
    if pitches is not None: # Pitches explicitly supplied:
        self.pitches = tuple(_parse_root(p) for p in pitches)
    elif self.root is not None and self.quality is not None: # Derive pitches from root + quality:
        intervals = QUALITIES[self.quality]
        root_pc   = self.root.key_num % 12
        self.pitches = tuple(
            Pitch((root_pc + i) % 12)
            for i in intervals
        )
    else:
        self.pitches = None

    # Derive quality from pitches:
    if self.quality is None and self.pitches is not None:
        self.quality = self._infer_quality()

    # Derive root from pitches + quality:
    if self.root is None and self.quality is not None and self.pitches is not None:
        self.root = self._infer_root()

    self.bass = _parse_root(bass) if bass is not None else None

Attributes

pitch_class_set property

pitch_class_set: Optional[list[int]]

Sorted list of pitch classes, or None if pitches unknown.

pitch_class_vector property

pitch_class_vector: Optional[tuple[int, ...]]

12-dimensional indicator vector, or None if pitches unknown.

pitch_names property

pitch_names: Optional[list[str]]

Spelled pitch names (e.g. ["C", "Eb", "G"]), or None.

intervals property

intervals: Optional[tuple[int, ...]]

Semitone intervals above the root, or None if root unknown.

inversion property

inversion: Optional[int]

0 = root position, 1 = first inversion, etc.

None if undetermined.

Prefers self.bass when set (e.g., via from_roman's figured-bass parsing, or an explicit slash chord).

label property

label: Optional[str]

Short label like "Cmaj7", "Dmin", "G7". Broadly matches leadsheet notation.

Returns None if either or both of root or quality unknown.

Functions

from_scale_degree classmethod

from_scale_degree(
    degree: int, key: Union[str, int, Pitch], *, seventh: bool = False
) -> Chord

Alternate constructor for making a Chord from a scale degree and key.

Parameters:

  • degree (int) –

    Scale degree. Note that this is 1-indexed (1 = tonic, ..., 7 = leading tone).

  • key (str | int | Pitch) –

    Key, e.g. "C", "d" (lower-case = minor) ...

  • seventh (bool, default: False ) –

    If True, stack the diatonic seventh above the triad.

Returns:

Raises:

  • ValueError

    If degree is outside 1–7.

Examples:

>>> Chord.from_scale_degree(2, "C").label
'Dm'
>>> ii7 = Chord.from_scale_degree(2, "C", seventh=True)
>>> ii7.label
'Dm7'
>>> ii7.intervals
(0, 3, 7, 10)
>>> Chord.from_scale_degree(7, "C").label
'Bo'
>>> Chord.from_scale_degree(2, "d").label
'Eo'
>>> Chord.from_scale_degree(2, "d", seventh=True).label
'Eø7'
Source code in amads/core/chord.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
@classmethod
def from_scale_degree(
        cls,
        degree: int,
        key: Union[str, int, Pitch],
        *,
        seventh: bool = False,
) -> "Chord":
    """
    Alternate constructor for making a `Chord` from a scale degree and key.

    Parameters
    ----------
    degree : int
        Scale degree.
        Note that this is 1-indexed (1 = tonic, ..., 7 = leading tone).
    key : str | int | Pitch
        Key, e.g. `"C"`, `"d"` (lower-case = minor) ...
    seventh : bool
        If True, stack the diatonic seventh above the triad.

    Returns
    -------
    Chord

    Raises
    ------
    ValueError
        If *degree* is outside 1–7.

    Examples
    --------
    >>> Chord.from_scale_degree(2, "C").label
    'Dm'

    >>> ii7 = Chord.from_scale_degree(2, "C", seventh=True)
    >>> ii7.label
    'Dm7'

    >>> ii7.intervals
    (0, 3, 7, 10)

    >>> Chord.from_scale_degree(7, "C").label
    'Bo'

    >>> Chord.from_scale_degree(2, "d").label
    'Eo'

    >>> Chord.from_scale_degree(2, "d", seventh=True).label
    'Eø7'
    """
    if not 1 <= degree <= 7:
        raise ValueError(f"Scale degree must be between 1 and 7, got {degree!r}.")
    numeral = ["I", "II", "III", "IV", "V", "VI", "VII"][degree - 1]
    return cls.from_roman(numeral, key, seventh=seventh)

from_pitch_collection classmethod

from_pitch_collection(collection: PitchCollection) -> Chord

Alternate constructor for making a Chord from a PitchCollection.

Quality is identified by matching the pitch-class set against the quality registry under all rotations (potential roots). If exactly one root yields a known quality, both are set; if multiple match (e.g. augmented triads), root is left None. Spelling is preserved from the collection.

Parameters:

Returns:

Source code in amads/core/chord.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
@classmethod
def from_pitch_collection(cls, collection: PitchCollection) -> "Chord":
    """
    Alternate constructor for making a `Chord` from a PitchCollection.

    Quality is identified by matching the pitch-class set against the
    quality registry under all rotations (potential roots).  If exactly
    one root yields a known quality, both are set; if multiple match
    (e.g. augmented triads), root is left None.  Spelling is preserved
    from the collection.

    Parameters
    ----------
    collection : PitchCollection

    Returns
    -------
    Chord
    """
    pitches = collection.pitches
    pc_pitches: list[Pitch] = [Pitch(p.key_num % 12, p.alt) for p in pitches]
    pc_set = frozenset(p.key_num % 12 for p in pc_pitches)

    # try each member as potential root
    matches: list[tuple[Pitch, str]] = []
    for candidate_root in pc_set:
        intervals = tuple(sorted((pc - candidate_root) % 12 for pc in pc_set))
        key = frozenset(intervals)
        if key in INTERVAL_SET_TO_QUALITY:
            matches.append((Pitch(candidate_root), INTERVAL_SET_TO_QUALITY[key]))

    chord = cls.__new__(cls)
    chord.key  = None
    chord.bass = pc_pitches[0] if pc_pitches else None
    chord.pitches = tuple(pc_pitches)

    if len(matches) == 1:
        chord.root, chord.quality = matches[0]
    elif len(matches) > 1:
        # ambiguous (e.g. augmented): no single root
        chord.root    = None
        chord.quality = matches[0][1]  # at least quality is unambiguous
    else:
        chord.root    = None
        chord.quality = None

    return chord

roman_numeral

roman_numeral(key: Union[str, int, Pitch]) -> Optional[str]

Return the Roman numeral of this chord in the given key, or None.

Source code in amads/core/chord.py
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
def roman_numeral(self, key: Union[str, int, Pitch]) -> Optional[str]:
    """Return the Roman numeral of this chord in the given key, or None."""
    if self.root is None or self.quality is None:
        return None

    scale, tonic_pc = _parse_key(key)
    root_pc  = int(self.root.key_num) % 12
    interval = (root_pc - tonic_pc) % 12

    if interval not in scale:
        return None  # chromatic root — don't attempt diatonic numeral

    degree = scale.index(interval)
    numerals = ["I", "II", "III", "IV", "V", "VI", "VII"]
    numeral  = numerals[degree]

    # lowercase for minor/diminished
    if self.quality in ("minor", "minor7", "diminished", "diminished7", "half-dim7"):
        numeral = numeral.lower()
    if self.quality in ("diminished", "diminished7"):
        numeral += "°"
    if self.quality in ("dominant7", "major7", "minor7",
                        "diminished7", "half-dim7"):
        numeral += "7"

    return numeral