Voice leading in C major triads and their permutations¶
Understanding efficient leading voices between triad chords is only possible if you account for chord permutation. Victor E. Bazterra
Introduction¶
This notebook is a demonstration of Orbichord, a project to explore topologically non-trivial space of music chords that are global-quotient orbifolds, see references:
Project page: https://orbichord.github.io
Project Github: https://github.com/orbichord/orbichord
Tymoczko, Dmitri. “The geometry of musical chords.” Science 313.5783 (2006): 72-74.
Callender, Clifton, Ian Quinn, and Dmitri Tymoczko. “Generalized voice-leading spaces.” Science 320.5874 (2008): 346-348.
Dmitri Tymoczko, A Geometry of Music: Harmony and Counterpoint in the Extended Common Practice, Oxford University Press, 2011.
Orbichord comes from combining the words orbifold and chord. It is a collection of python modules build on top of music21 project.
Importing modules¶
Import music and graphite modules
[1]:
# Import music modules
import itertools
from music21.interval import Interval
from music21.scale import MajorScale
from music21.stream import Stream
from networkx import connected_components
from numpy import inf
from numpy import linalg as la
from orbichord.chordinate import EfficientVoiceLeading, Permutation
from orbichord.graph import createGraph, convertGraphToData
from orbichord.generator import Generator
from orbichord.identify import chordPitchNames
from orbichord.symbol import chordSymbolFigure
from orbichord.utils import renderWithLily, playAudio
# Import graphic modules
import pandas as pd
import holoviews as hv
from holoviews import opts, dim
from bokeh.sampledata.les_mis import data
hv.extension('bokeh')
hv.output(size=180)
defaults = dict(width=280, height=280, padding=0.1)
hv.opts.defaults(
    opts.EdgePaths(**defaults), opts.Graph(**defaults), opts.Nodes(**defaults))
Configure an orbichord generator¶
Create a chord generator using seven pitches of the C major scale. By default, chords are identified by the popular name with no inversion, resulting in the space chord defined by a collection pitch-class sets. In this case, however, we would like to account for all the chord permutations. For this, we define the generator with an overridden combinator function that is a wrapper to
intertools.product. This generator will generate all possible combinations of three pitches from the C major scale. We identify these chords by using orbichord.identify.chordSymbolFigureWithPitchName that for each chord produces a string following the format {Popular chord name}(ordered pitch classes). For example, a non-inverted C major chord is identified with C (CEG) label. Finally, as before, we select only
triad chords.
[2]:
def combinator(iterable, dimension):
    return itertools.product(iterable, repeat = dimension)
scale = MajorScale('C')
chord_generator = Generator(
    pitches = scale.getPitches('C','B'),
    combinator = combinator,
    identify = chordPitchNames,
    select = lambda chord: chord.isTriad()
)
Instantiate an efficient voice leading object using the C major scale to define voice leading steps. Define the metric as the maximum of the absolute number of steps between all the voices or \(L_{\infty}\) norm. All non-crossing voice leading between two chords are explored using interscalar matrix computed in polynomial time by algorithm descrived in Dmitri Tymoczko, A Geometry of Music, Appendix D, page 420.
[3]:
max_norm_vl = EfficientVoiceLeading(
    scale = scale,
    metric = lambda delta: la.norm(delta, inf),
    permutation = Permutation.NONE
)
Use a chord graph to explore how chords are connected. For this, you need to pass a generator and voice leading objects, as well as a tolerance function. The tolerance function provides the criteria to select how close chords can be to be considered an efficient voice leading.
[4]:
graph, node_to_chord = createGraph(
    generator = chord_generator,
    voice_leading = max_norm_vl,
    tolerance = lambda x: x == 1.0,
    label = lambda chord: chordSymbolFigure(chord) +\
            ' (' + chordPitchNames(chord) + ')'
)
The output of the function is a networkx graph and a dictionary to map node to their counterpart chord object.
Visualizing chord graph with holoview¶
Convert the chord networkx graph into a collection of links and nodes. Visualize the chord graph using (ironically) a chord graph.
[5]:
edges, vertices = convertGraphToData(graph)
links = pd.DataFrame(edges)
nodes = hv.Dataset(pd.DataFrame(vertices), 'index')
chord = hv.Chord((links, nodes))
chord.opts(
    opts.Chord(
        cmap='Category20',
        edge_cmap='Category20',
        edge_color=dim('source').str(),
        labels='name', node_color=dim('index').str()
    )
)
[5]:
This rather complicated looking graph shows the efficient voice leading connection between chords and their permutations.
Graph decomposition into connected components¶
Hidden in the previous graph is the fact that it is made of two connected components. Using networkx module, we can find out and plot those components. We will call these components the good and evil twins.
[6]:
# Graph decomposition into its connected components
good_twin, evil_twin = (graph.subgraph(c) for c in connected_components(graph))
# Preparing data for the good
good_edges, good_vertices = convertGraphToData(good_twin)
good_links = pd.DataFrame(good_edges)
good_nodes = hv.Dataset(pd.DataFrame(good_vertices), 'index')
good_chord = hv.Chord((good_links, good_nodes))
# Plot both components side by side
good_chord.opts(
    opts.Chord(
        cmap='Category20',
        edge_cmap='Category20',
        edge_color=dim('source').str(),
        labels='name', node_color=dim('index').str()
    )
)
[6]:
[7]:
# Preparing data for the evil
evil_edges, evil_vertices = convertGraphToData(evil_twin)
evil_links = pd.DataFrame(evil_edges)
evil_nodes = hv.Dataset(pd.DataFrame(evil_vertices), 'index')
evil_chord = hv.Chord((evil_links, evil_nodes))
evil_chord.opts(
    opts.Chord(
        cmap='Category20',
        edge_cmap='Category20',
        edge_color=dim('source').str(),
        labels='name', node_color=dim('index').str()
    )
)
[7]:
The good twin contains all the C major triad chords and its inversions (three cyclic permutations). The same for evil twin except each inversion has the last two pitches permutated.
Analyzing the good twin¶
We can analyze all the C major neighbor chords from the good-twin graph.
[8]:
# Set a step to bring all pitchs to a treble clef
interval3 = Interval(3*12)
interval4 = Interval(4*12)
# Set the central node
node = 'C (CEG)'
# Collect its neigbors
neighbors = [node]
for neighbor, edge in good_twin.adj[node].items():
    neighbors.append(neighbor)
# Sort chords based on their named pitch
neighbors.sort(key = lambda name: name[0])
size = len(neighbors)
# Cyclic permutations to start for C major cord
neighbors = [neighbors[(i+2)%size] for i in range(size)]
# Define a stream of chords
stream = Stream()
counter = 0
for neighbor in neighbors:
    if counter in (2, 4, 6):
        chord = node_to_chord[neighbor].transpose(interval3)
    else:
        chord = node_to_chord[neighbor].transpose(interval4)
    chord.addLyric(chordSymbolFigure(chord))
    chord.duration.type = 'whole'
    stream.append(chord)
    counter += 1
# Render the resulting chord progression
renderWithLily(stream)
[8]:
The neighbor chords are some inversion of major triads from the C major scale. Transposing them to the right octave, we can verify they are only one scale step away from each other. However, the sound different than the usual no inverted progression.
[9]:
# Play the chord progression
playAudio(stream)
[9]:
Analyzing the bad twin¶
We can analyze all the C major neighbor chords from the bad-twin graph.
[10]:
# Set a step to bring all pitchs to a treble clef
interval3 = Interval(3*12)
interval4 = Interval(4*12)
# Set the central node
node = 'C (CGE)'
# Collect its neigbors
neighbors = [node]
for neighbor, edge in evil_twin.adj[node].items():
    neighbors.append(neighbor)
# Sort chords based on their named pitch
neighbors.sort(key = lambda name: name[0])
size = len(neighbors)
# Cyclic permutations to start for C major cord
neighbors = [neighbors[(i+2)%size] for i in range(size)]
# Define a stream of chords
stream = Stream()
counter = 0
for neighbor in neighbors:
    if counter in (2, 4, 6):
        chord = node_to_chord[neighbor].transpose(interval3)
    else:
        chord = node_to_chord[neighbor].transpose(interval4)
    chord.addLyric(chordSymbolFigure(chord))
    chord.duration.type = 'whole'
    stream.append(chord)
    counter += 1
# Render the resulting chord progression
renderWithLily(stream)
[10]:
The neighbor chords are permutations of all inversion of major triads from the C major scale. Transposing them to the right octave, we can verify they are only one scale step away from each other.
The main difference with good and evil chords is the fact evil chords tend to spread beyond one octave, making the much harder to play. This could answer the old question of why talk about triad three inversions and say nothing about its six possible permutations.
[11]:
# Play the chord progression
playAudio(stream)
[11]:
Music modes, voice leading and chord inversions¶
Exploring the neighbors of the G major chord is equivalent to derive the chord progression in G Mixolydian mode.
[12]:
# Set a step to bring all pitchs to a treble clef
interval4 = Interval(4*12)
# Set the central node
node = 'G (GBD)'
# Collect its neigbors
neighbors = [node]
for neighbor, edge in good_twin.adj[node].items():
    neighbors.append(neighbor)
# Sort chords based on their named pitch
neighbors.sort(key = lambda name: name[0])
size = len(neighbors)
# Cyclic permutations to start for C major cord
neighbors = [neighbors[(i-1)%size] for i in range(size)]
# Define a stream of chords
stream = Stream()
for neighbor in neighbors:
    chord = node_to_chord[neighbor].transpose(interval4)
    chord.addLyric(chordSymbolFigure(chord))
    chord.duration.type = 'whole'
    stream.append(chord)
# Render the resulting chord progression
renderWithLily(stream)
[12]:
The resulting chord progression is a cyclic permutation of chords relative to the neighbors of C major chord. However, these chords are not present in the same inversions as C major neighbors. Therefore, efficient voice leading in different music mode implies more than a different starting point to the chord progression, it also means that those chords to be present in different inversions relative to Ionian mode.
[13]:
# Play the chord progression
playAudio(stream)
[13]:
Ascending chord progression¶
Chord progression from C major triad generated by our definition of effective voice leading has the problem that does not sound as ascending progression like the original inversion free. In this section, we will try to create ascending chord progression using the neighbors of C major.
[14]:
# Set a step to bring all pitchs to a treble clef
interval3 = Interval(3*12)
interval4 = Interval(4*12)
# Set the central node
node = 'C (CEG)'
# Collect its neigbors
neighbors = [node]
for neighbor, edge in good_twin.adj[node].items():
    neighbors.append(neighbor)
# Transpose octaves to get chords as close as possible
counter = 0
chords = []
for neighbor in neighbors:
    if counter > 3:
        chord = node_to_chord[neighbor].transpose(interval3)
    else:
        chord = node_to_chord[neighbor].transpose(interval4)
    counter += 1
    chords.append(chord)
# Define a comparison function between chords
def chordComp(chordA, chordB):
    A = chordA.pitches
    B = chordB.pitches
    if A[0] < B[0]:
        return -1
    elif A[0] > B[0]:
        return +1
    if A[1] < B[1]:
        return -1
    elif A[1] > B[1]:
        return +1
    if A[2] < B[2]:
        return -1
    elif A[2] > B[2]:
        return +1
    return 0
# Sort chords based on their named pitch
from functools import cmp_to_key
chords.sort(key = cmp_to_key(chordComp))
# Define a stream of chords
stream = Stream()
for chord in chords:
    chord.addLyric(chordSymbolFigure(chord))
    chord.duration.type = 'whole'
    stream.append(chord)
# Render the resulting chord progression
renderWithLily(stream)
[14]:
The resulting chord progression starts from the Bdim chord and then round-robin on each pitch and transposing them one scale step up.
[15]:
# Play the chord progression
playAudio(stream)
[15]:
Conclusion¶
Major scale triads can be connected by efficient voice leading as define in this notebook. In this sense, triads as very close to each other. The only caveat is that this tight cluster of chords is made of some inversions of the typical triads. These different inversions allow keeping all the chords of the progression just one scale step away from each other in \(L_{\infty}\) norm.
We can quickly generate this chord progression for any significant scale. Start with vii chord with its bass note below the bass note of the starting I chord for a given scale. Then round robin on each pitch of the initial triad and transpose them one scale up at the time. The final chord of this sequence is the ii in where all the pitches are transposed by one scale step up from I.