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 exmaple (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("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'
  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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
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.

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
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
@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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
@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

_infer_quality

_infer_quality() -> Optional[str]

Helper to try and identify quality from self.pitches, trying each member as root.

Source code in amads/core/chord.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
def _infer_quality(self) -> Optional[str]:
    """
    Helper to try and identify quality from self.pitches, trying each member as root."""
    if not self.pitches:
        return None
    pc_set = frozenset(int(p.key_num) % 12 for p in self.pitches)
    # try root first if known
    candidates = (
        [int(self.root.key_num) % 12] if self.root is not None
        else [int(p.key_num) % 12 for p in self.pitches]
    )
    for r in candidates:
        intervals = frozenset(sorted((pc - r) % 12 for pc in pc_set))
        if intervals in _INTERVAL_SET_TO_QUALITY:
            return _INTERVAL_SET_TO_QUALITY[intervals]
    return None

_infer_root

_infer_root() -> Optional[Pitch]

Helper to find the root that makes self.quality consistent with self.pitches.

Source code in amads/core/chord.py
480
481
482
483
484
485
486
487
488
489
490
491
492
def _infer_root(self) -> Optional[Pitch]:
    """
    Helper to find the root that makes self.quality consistent with self.pitches.
    """
    if not self.pitches or not self.quality:
        return None
    target = frozenset(QUALITIES[self.quality])
    for p in self.pitches:
        r = int(p.key_num) % 12
        intervals = frozenset((int(q.key_num) % 12 - r) % 12 for q in self.pitches)
        if intervals == target:
            return Pitch(r)
    return None

__len__

__len__() -> int

Number of distinct pitch classes.

Source code in amads/core/chord.py
524
525
526
def __len__(self) -> int:
    """Number of distinct pitch classes."""
    return len(self.pitch_class_set) if self.pitch_class_set is not None else 0

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
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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