Skip to content

VoiceLeading

voice_leading

Voice-leading strategies for Voicing.sequence.

A voice-leading strategy picks one voicing from a list of candidates given the previous voicing. The default strategy is top_note_diff, which minimizes movement of the highest note.

Custom strategies should match the type alias VoiceLeadingFunction = Callable[[list[list[str]], list[str]], list[str]].

Example

from tonal_py import VoiceLeading chosen = VoiceLeading.top_note_diff( ... [["C3", "E3", "G3"], ["C4", "E4", "G4"]], ... ["B3", "D4", "F4"], ... ) chosen ['C4', 'E4', 'G4']

Source parity: @tonaljs/voice-leading

top_note_diff

top_note_diff(voicings: list[list[str]], last_voicing: list[str]) -> list[str]

Pick the voicing whose top note is closest to the previous top note.

Default voice-leading strategy used by Voicing.get and Voicing.sequence. Compares the last note of each candidate to the last note of last_voicing.

Parameters:

Name Type Description Default
voicings list[list[str]]

Candidate voicings (each a list of note names).

required
last_voicing list[str]

Previous voicing. If empty, returns the first candidate unchanged.

required

Returns:

Type Description
list[str]

The chosen voicing (a list of note names).

Example

from tonal_py import VoiceLeading

Last voicing's top note F4 (MIDI 65) is closer to G4 (67) than G3 (55)

VoiceLeading.top_note_diff( ... [["C3", "E3", "G3"], ["C4", "E4", "G4"]], ... ["B3", "D4", "F4"], ... ) ['C4', 'E4', 'G4']

No previous voicing -> returns first

VoiceLeading.top_note_diff( ... [["C3", "E3", "G3"], ["C4", "E4", "G4"]], ... [], ... ) ['C3', 'E3', 'G3']

Source code in src/tonal_py/voice_leading.py
def top_note_diff(voicings: list[list[str]], last_voicing: list[str]) -> list[str]:
    """Pick the voicing whose top note is closest to the previous top note.

    Default voice-leading strategy used by [`Voicing.get`][tonal_py.voicing.get]
    and [`Voicing.sequence`][tonal_py.voicing.sequence]. Compares the last
    note of each candidate to the last note of `last_voicing`.

    Args:
        voicings: Candidate voicings (each a list of note names).
        last_voicing: Previous voicing. If empty, returns the first
            candidate unchanged.

    Returns:
        The chosen voicing (a list of note names).

    Example:
        >>> from tonal_py import VoiceLeading
        >>> # Last voicing's top note F4 (MIDI 65) is closer to G4 (67) than G3 (55)
        >>> VoiceLeading.top_note_diff(
        ...     [["C3", "E3", "G3"], ["C4", "E4", "G4"]],
        ...     ["B3", "D4", "F4"],
        ... )
        ['C4', 'E4', 'G4']
        >>> # No previous voicing -> returns first
        >>> VoiceLeading.top_note_diff(
        ...     [["C3", "E3", "G3"], ["C4", "E4", "G4"]],
        ...     [],
        ... )
        ['C3', 'E3', 'G3']
    """
    if not last_voicing or not len(last_voicing):
        return voicings[0]

    def top_midi(voicing: list[str]) -> int:
        return _note_mod.midi(voicing[-1]) or 0

    def diff(voicing: list[str]) -> int:
        return abs(top_midi(last_voicing) - top_midi(voicing))

    return sorted(voicings, key=diff)[0]