Skip to content

Voicing

voicing

Concrete pitch realizations of chords (voicings).

The Voicing namespace finds specific arrangements of chord notes within a given pitch range, drawing from a VoicingDictionary (jazz lefthand voicings, basic triad inversions, or your own).

Three main entry points:

  • search — find every voicing that fits.
  • get — find one good voicing (optionally voice-led from a previous voicing).
  • sequence — chain voicings across a progression with smooth voice leading.

Different defaults for get and search

Voicing.get defaults to the all dictionary (triads + lefthand); Voicing.search defaults to triads only. This matches tonal.js.

Example

from tonal_py import Voicing Voicing.get("Dm7", ["C3", "C5"]) ['F3', 'A3', 'C4', 'E4']

Source parity: @tonaljs/voicing

get

get(chord_name: str, note_range: list[str] | None = None, dictionary: dict[str, list[str]] | None = None, voice_leading: VoiceLeadingFunction | None = None, last_voicing: list[str] | None = None) -> list[str] | None

Get a single voicing of a chord within a range.

If last_voicing is provided, picks the candidate that minimizes voice-leading delta from it (using the supplied or default voice_leading strategy).

Parameters:

Name Type Description Default
chord_name str

A chord name (e.g. "Dm7").

required
note_range list[str] | None

[low, high] note range. Defaults to ["C3", "C5"].

None
dictionary dict[str, list[str]] | None

Voicing dictionary. Defaults to VoicingDictionary.all.

None
voice_leading VoiceLeadingFunction | None

Strategy for picking among candidates. Defaults to top_note_diff.

None
last_voicing list[str] | None

Previous voicing to lead voices smoothly from.

None

Returns:

Type Description
list[str] | None

A list of note names, or None if no voicing fits the range.

Example

from tonal_py import Voicing Voicing.get("Dm7", ["C3", "C5"]) ['F3', 'A3', 'C4', 'E4'] Voicing.get("Cmaj7", ["C3", "C5"]) ['E3', 'G3', 'B3', 'D4']

Source code in src/tonal_py/voicing.py
def get(
    chord_name: str,
    note_range: list[str] | None = None,
    dictionary: dict[str, list[str]] | None = None,
    voice_leading: VoiceLeadingFunction | None = None,
    last_voicing: list[str] | None = None,
) -> list[str] | None:
    """Get a single voicing of a chord within a range.

    If `last_voicing` is provided, picks the candidate that minimizes
    voice-leading delta from it (using the supplied or default
    `voice_leading` strategy).

    Args:
        chord_name: A chord name (e.g. `"Dm7"`).
        note_range: `[low, high]` note range. Defaults to `["C3", "C5"]`.
        dictionary: Voicing dictionary. Defaults to
            `VoicingDictionary.all`.
        voice_leading: Strategy for picking among candidates. Defaults to
            [`top_note_diff`][tonal_py.voice_leading.top_note_diff].
        last_voicing: Previous voicing to lead voices smoothly from.

    Returns:
        A list of note names, or `None` if no voicing fits the range.

    Example:
        >>> from tonal_py import Voicing
        >>> Voicing.get("Dm7", ["C3", "C5"])
        ['F3', 'A3', 'C4', 'E4']
        >>> Voicing.get("Cmaj7", ["C3", "C5"])
        ['E3', 'G3', 'B3', 'D4']
    """
    rng = note_range if note_range is not None else _DEFAULT_RANGE
    dct = dictionary if dictionary is not None else _voicing_dictionary_mod.all
    vl = voice_leading if voice_leading is not None else _voice_leading_mod.top_note_diff
    voicings = search(chord_name, rng, dct)
    if not voicings:
        return None
    if not last_voicing or not len(last_voicing):
        return voicings[0]
    return vl(voicings, last_voicing)

search

search(chord_name: str, note_range: list[str] | None = None, dictionary: dict[str, list[str]] | None = None) -> list[list[str]]

Find every concrete voicing of a chord within a note range.

Returns all dictionary voicings for the chord type (or its aliases) placed at every starting position that fits within the range.

Parameters:

Name Type Description Default
chord_name str

A chord name.

required
note_range list[str] | None

[low, high] note range. Defaults to ["C3", "C5"].

None
dictionary dict[str, list[str]] | None

Voicing dictionary. Defaults to VoicingDictionary.triads — for jazz voicings pass VoicingDictionary.lefthand or .all.

None

Returns:

Type Description
list[list[str]]

A list of voicings (each a list of note names). Empty list if

list[list[str]]

the chord has no entry in the dictionary.

Example

from tonal_py import Voicing

Default search uses triads dictionary; m7 isn't in triads

Voicing.search("Dm7", ["C3", "C5"]) []

Major triad inversions in C3-C5 range

len(Voicing.search("C", ["C3", "C5"])) > 0 True

Source code in src/tonal_py/voicing.py
def search(
    chord_name: str,
    note_range: list[str] | None = None,
    dictionary: dict[str, list[str]] | None = None,
) -> list[list[str]]:
    """Find every concrete voicing of a chord within a note range.

    Returns all dictionary voicings for the chord type (or its aliases)
    placed at every starting position that fits within the range.

    Args:
        chord_name: A chord name.
        note_range: `[low, high]` note range. Defaults to `["C3", "C5"]`.
        dictionary: Voicing dictionary. Defaults to
            `VoicingDictionary.triads` — for jazz voicings pass
            `VoicingDictionary.lefthand` or `.all`.

    Returns:
        A list of voicings (each a list of note names). Empty list if
        the chord has no entry in the dictionary.

    Example:
        >>> from tonal_py import Voicing
        >>> # Default search uses triads dictionary; m7 isn't in triads
        >>> Voicing.search("Dm7", ["C3", "C5"])
        []
        >>> # Major triad inversions in C3-C5 range
        >>> len(Voicing.search("C", ["C3", "C5"])) > 0
        True
    """
    rng = note_range if note_range is not None else _DEFAULT_RANGE
    # Note: JS default for `search` is VoicingDictionary.triads — different
    # from `get`'s default (`all`). Mirror exactly.
    dct = dictionary if dictionary is not None else _voicing_dictionary_mod.triads
    tonic, symbol, _bass = _chord_mod.tokenize(chord_name)
    sets = _voicing_dictionary_mod.lookup(symbol, dct)
    if sets is None:
        return []
    voicings_intervals = [s.split(" ") for s in sets]
    notes_in_range = _range_mod.chromatic(rng)
    out: list[list[str]] = []
    upper_midi = _note_mod.midi(rng[1]) or 0
    for voicing in voicings_intervals:
        # Intervals relative to the bottom note of the voicing
        relative_intervals = [
            _interval_mod.subtract(i, voicing[0]) or "" for i in voicing
        ]
        bottom_pitch_class = _note_mod.transpose(tonic, voicing[0])
        bottom_chroma = _note_mod.chroma(bottom_pitch_class)
        # Find every note in range whose chroma matches the bottom pitch class
        # AND whose top note (after applying the highest relative interval)
        # is still within the upper bound of the range.
        starts: list[str] = []
        for note in notes_in_range:
            if _note_mod.chroma(note) != bottom_chroma:
                continue
            top = _note_mod.transpose(note, relative_intervals[-1])
            top_midi = _note_mod.midi(top) or 0
            if top_midi > upper_midi:
                continue
            starts.append(_note_mod.enharmonic(note, bottom_pitch_class))
        for start in starts:
            out.append([_note_mod.transpose(start, ivl) for ivl in relative_intervals])
    return out

sequence

sequence(chords: list[str], note_range: list[str] | None = None, dictionary: dict[str, list[str]] | None = None, voice_leading: VoiceLeadingFunction | None = None, last_voicing: list[str] | None = None) -> list[list[str]]

Voice an entire chord progression with smooth voice leading.

Each chord's voicing is chosen to minimize movement from the previous voicing (using the supplied or default voice-leading strategy).

Parameters:

Name Type Description Default
chords list[str]

A list of chord names.

required
note_range list[str] | None

[low, high] note range.

None
dictionary dict[str, list[str]] | None

Voicing dictionary.

None
voice_leading VoiceLeadingFunction | None

Strategy for choosing successive voicings.

None
last_voicing list[str] | None

Optional starting voicing to lead from.

None

Returns:

Type Description
list[list[str]]

A list of voicings, one per input chord. Chords with no fit

list[list[str]]

are silently skipped.

Example

from tonal_py import Voicing seq = Voicing.sequence(["Dm7", "G7", "Cmaj7"], ["C3", "C5"]) len(seq) 3 seq[0]['F3', 'A3', 'C4', 'E4']

Source code in src/tonal_py/voicing.py
def sequence(
    chords: list[str],
    note_range: list[str] | None = None,
    dictionary: dict[str, list[str]] | None = None,
    voice_leading: VoiceLeadingFunction | None = None,
    last_voicing: list[str] | None = None,
) -> list[list[str]]:
    """Voice an entire chord progression with smooth voice leading.

    Each chord's voicing is chosen to minimize movement from the previous
    voicing (using the supplied or default voice-leading strategy).

    Args:
        chords: A list of chord names.
        note_range: `[low, high]` note range.
        dictionary: Voicing dictionary.
        voice_leading: Strategy for choosing successive voicings.
        last_voicing: Optional starting voicing to lead from.

    Returns:
        A list of voicings, one per input chord. Chords with no fit
        are silently skipped.

    Example:
        >>> from tonal_py import Voicing
        >>> seq = Voicing.sequence(["Dm7", "G7", "Cmaj7"], ["C3", "C5"])
        >>> len(seq)
        3
        >>> seq[0]
        ['F3', 'A3', 'C4', 'E4']
    """
    voicings: list[list[str]] = []
    last = last_voicing
    for chord in chords:
        voicing = get(chord, note_range, dictionary, voice_leading, last)
        if voicing is None:
            continue
        last = voicing
        voicings.append(voicing)
    return voicings