Chromatic voice leading in common trichords¶
…here the circle is purely contrapuntal, describing minimal voice-leading relationships among chords. But we have just seen that the chain of third-related chords can also be used to model harmonic successions in functionally tonal music. …Harmony and counterpoint here work hand in hand, creating a unified structure in which horizontal and vertical forces are in delicate balance. Dmitri Tymoczko, A Geometry of Music, chap 7, page 231.
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.
Import module¶
[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 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=350, height=320, 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. Then we identify these chords by using orbichord.identify.chordPitchNames that for each chord produces a string following the format pitch class names. For example, a non-inverted C major chord is identified by CEG. Finally, as before, we select only triad chords. It is a collection of python modules
build on top of music21 project.
[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 sum of the absolute number of steps between two chords or \(L_1\) norm. All voice leading between two chords are explored using interscalar matrix. In this case, we do not allow the intercalar matrix to be invariant to any permutation.
[3]:
max_norm_vl = EfficientVoiceLeading(
    scale = scale,
    metric = lambda delta: la.norm(delta, 1),
    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) + ')'
)
# Graph decomposition into its connected components
graph, _ = (graph.subgraph(c) for c in connected_components(graph))
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()
    )
)
[4]:
The output of the function is a networkx graph and a dictionary to map node to their counterpart chord object. We also decompose the graph in its connected components to eliminate the evil permutation. As a result, the resulting graph is made only of chords and their good inversions (for the definition of good and evil permutations, see the guide Voice leading in C major triads and their permutations). Convert the chord networkx graph into a collection of links and nodes. Visualize the chord graph using (ironically) a chord graph.
The circule of thirds with inversions¶
Graph cycle for efficient voice leading graph¶
Find a cycle in the graph that starts from a non-inverted C major. This cycle connects chords by efficient voice leading defined as changing one scale degree at the time. The main point is that the resulting contrapuntal cycle is also a circule-of-thirds chord progression used to model harmonic successions in functionally tonal music.
[5]:
# Generate a graph with all the shortest voice leading between two dichords
cycle = None
def cycle_starting_from(source):
    global cycle
    cycle = nx.Graph()
    for path in nx.find_cycle(graph, source=source):#, target=target):
        prev = None
        for node in path:
            cycle.add_node(node)
            if prev:
                cycle.add_edge(prev, node)
            prev = node
    pview = hv.Graph.from_networkx(
        cycle,
        nx.layout.shell_layout
    )
    pview.opts(directed=True, node_size=60, arrowhead_length=0.01, node_color='black')
    labels = hv.Labels(pview.nodes, ['x', 'y'], 'index')
    return (pview * labels.opts(
        text_font_size='7pt',
        text_color='white', bgcolor='white'
    ))
# Demostration for static html notebooks
cycle_starting_from('C(CEG)')
[5]:
An innovation of this graph is to show that the chord progression with all their inversions.
Listening to contrapuntal and minimal voice-leading relationships among chords¶
Streaming the graph cycle.
[6]:
stream = Stream()
interval4 = Interval(4*12)
for node in cycle.nodes:
    chord = node_to_chord[node].transpose(interval4)
    chord.addLyric(chordSymbolFigure(chord))
    chord.duration.type = 'whole'
    stream.append(chord)
# Render the resulting chord progression
renderWithLily(stream)
[6]:
[7]:
# Play the chord progression
playAudio(stream)
[7]: