Voice leading in C major triads

The overdetermination of Western chords and scales leads … composers … [that are] interested in harmonic consistency, acoustic consonance, or scalar transposition … necessarily … toward the same familiar musical objects. Dmitri Tymoczko, A Geometry of Music, chap 4, page 123.

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

[15]:
# Import music modules
from music21.interval import Interval
from music21.scale import MajorScale
from music21.stream import Stream
from numpy import inf
from numpy import linalg as la
from orbichord.chordinate import EfficientVoiceLeading
from orbichord.graph import createGraph, convertGraphToData
from orbichord.generator import Generator
from orbichord.symbol import chordSymbolFigure
from orbichord.utils import renderWithLily, playAudio

import networkx as nx

# 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=300, height=300, 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 an ordered pitch-class string, resulting in the space chord defined by a collection pitch-class sets. We also select those triad chords, resulting in a generator for C major triad chords.

[16]:
scale = MajorScale('C')

chord_generator = Generator(
    pitches = scale.getPitches('C','B'),
    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.

[17]:
max_norm_vl = EfficientVoiceLeading(
    scale = scale,
    metric = lambda delta: la.norm(delta, inf)
)

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.

[18]:
graph, node_to_chord = createGraph(
    generator = chord_generator,
    voice_leading = max_norm_vl,
    tolerance = lambda x: x <= 1.0,
    label = lambda chord: chordSymbolFigure(chord, inversion=0)
)

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.

[19]:
edges, vertices = convertGraphToData(graph)
links = pd.DataFrame(edges)
nodes = hv.Dataset(pd.DataFrame(vertices), 'index')
print(links.head())
nodes.data.head()
   source  target  value
0       0       1    1.0
1       0       2    1.0
2       0       3    1.0
3       0       4    1.0
4       0       5    1.0
[19]:
index name group
0 0 C 1
1 1 Am 1
2 2 F 1
3 3 Dm 1
4 4 Bdim 1

Visualize chord graph using (ironically) a chord graph.

[20]:
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()
    )
)

Data type cannot be displayed:

[20]:

We can also visualized the graph directly.

[21]:
gview = hv.Graph.from_networkx(graph, nx.layout.circular_layout)
gview.opts(node_size=40)
labels = hv.Labels(gview.nodes, ['x', 'y'], 'index')
(gview * labels.opts(text_font_size='10pt', text_color='white', bgcolor='white'))

Data type cannot be displayed:

[21]:

Conclusion

All the triad chords in the space of pitch-class sets can be connected by an efficient voice leading. This voice leading can change multiple pitches in the chord but not more that one scale steps on each pitch. However, naively, we find that, for example, that G major seems to be transposed five scale steps from C major.

[22]:
# Set a step to bring all pitchs to a treble clef
interval4 = Interval(4*12)
# Set the central node
node = 'C'

# Collect its neigbors
neighbors = [node]
for neighbor, edge in graph.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()
for neighbor in neighbors:
    chord = node_to_chord[neighbor].transpose(interval4)
    chord.inversion(0)
    chord.addLyric(chordSymbolFigure(chord))
    chord.duration.type = 'whole'
    stream.append(chord)
# Render the resulting chord progression
renderWithLily(stream)
[22]:
../_images/user_guide_C_triad_chord_graph_16_0.png
[24]:
# Play the chord progression
playAudio(stream)
[24]:

But then, in what sense C and G major chords are only one step of the major scale?

To understand this, you need to realize that for each chord, there are six possible permutations (three inversion x two permutations for each inversion). In the space of pitch-class sets, all these permutations are identified as the same musical object. Therefore, a better understanding of distances between triad requires accounting for chord permutation as detailed in another notebook.