Chromatic voice leading in common trichords

…These result from moving downward along the equal-tempered lattice at the center of the chord space. Dmitri Tymoczko, A Geometry of Music, chap 3, page 88, figure 3.8.3 caption.

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
from music21.interval import Interval
from music21.scale import ChromaticScale
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, hasChordSymbolFigure
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 twelve pitches of the chromatic scale. By default, chords are using orbichord chord symbol index, resulting in the space chord defined by a collection pitch-class sets in where each inversion has a different identity. By default also orbichord selects chords that have known symbols within orbichord.

[2]:
scale = ChromaticScale('C')

chord_generator = Generator(
    pitches = scale.getPitches('C','B')
)

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 all the voices or \(L_1\) 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, 1)
)

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, inversion=0)
)

The output of the function is a networkx graph and a dictionary to map node to their counterpart chord object.

Discrete Voice-Leading Lattices

Voice leading neighbors

Make a graph of all the neighbors from a given node.

[5]:
# Create neighbor graph
def neighbor_graph(center):
    neighbors = nx.Graph()
    neighbors.add_node(center)
    for node, edge in graph.adj[center].items():
        neighbors.add_node(node)
        neighbors.add_edge(center, node, distance=edge['distance'])
    nview = hv.Graph.from_networkx(neighbors, nx.layout.spring_layout)
    nview.opts(node_size=30)
    labels = hv.Labels(nview.nodes, ['x', 'y'], 'index')
    return (nview * labels.opts(
        text_font_size='8pt',
        text_color='white',
        bgcolor='white'
    ))

# Map all the nodes to their neighbors
# Each graph is done dynamically therefore
# you need to have jupyter server running with orbichord
#hv.DynamicMap(
#    neighbor_graph,
#    kdims=['chord']
#).redim.values(
#    chord = sorted(graph.nodes)
#)
# Demostration for static html notebooks
neighbor_graph('A')
[5]:

All the shortest Voice Leasing between two dichords

Search for all the possible voice leading for going from F to Cm chord. The the graph reproduces the same voice leading pathways as in Figure 3.8.3b of A Geometry of Music: Harmony and Counterpoint in the Extended Common Practice, by Dmitri Tymoczko.

[6]:
# Generate a graph with all the shortest voice leading between two dichords
def shortest_voice_leadings(source, target):
    paths = nx.DiGraph()
    for path in nx.all_shortest_paths(graph, source=source, target=target):
        prev = None
        for node in path:
            paths.add_node(node)
            if prev:
                paths.add_edge(prev, node)
            prev = node
    pview = hv.Graph.from_networkx(
        paths,
        nx.layout.shell_layout
    )
    pview.opts(directed=True, node_size=30, arrowhead_length=0.05)
    labels = hv.Labels(pview.nodes, ['x', 'y'], 'index')
    return (pview * labels.opts(
        text_font_size='8pt',
        text_color='white', bgcolor='white'
    ))

# Map every pair nodes to all the shortest voice leading
# Each graph is done dynamically therefore
# you need to have jupyter server running with orbichord
#hv.DynamicMap(
#    shortest_voice_leadings,
#    kdims=['source', 'target']
#).redim.values(
#    source = sorted(graph.nodes),
#    target = sorted(graph.nodes)
#)

# Demostration for static html notebooks
shortest_voice_leadings('F', 'Cm')
[6]:

Collection of shortest Voice Leading

Select one of the shortest pathways.

[7]:
shortest_vl_path = None

# Generate a graph with a shortest voice leading between two dichords
def shortest_voice_leading(source, target, index):
    global shortest_vl_path
    paths = list(nx.all_shortest_paths(graph, source=source, target=target))
    if index >= len(paths):
        index = 0
        paths = [[source]]
    path = nx.DiGraph()
    prev = None
    for node in paths[index]:
        path.add_node(node)
        if prev:
            path.add_edge(prev, node)
        prev = node
    shortest_vl_path = path
    pview = hv.Graph.from_networkx(
        path,
        nx.layout.shell_layout
    )
    pview.opts(directed=True, node_size=30, arrowhead_length=0.05)
    labels = hv.Labels(pview.nodes, ['x', 'y'], 'index')
    return (pview * labels.opts(
        text_font_size='8pt',
        text_color='white', bgcolor='white'
    ))

# Map every pair nodes to a collection of shortest voice leading
# Each graph is done dynamically therefore
# you need to have jupyter server running with orbichord
#hv.DynamicMap(
#    shortest_voice_leading,
#    kdims=['source', 'target', 'index']
#).redim.values(
#    source = sorted(graph.nodes),
#    target = sorted(graph.nodes),
#    index = range(80)
#)

# Demostration for static html notebooks
shortest_voice_leading('F', 'Cm', 3)
[7]:

Generate a stream with the selected voice leading

[8]:
stream = Stream()
interval4 = Interval(4*12)
for node in shortest_vl_path.nodes:
    chord = node_to_chord[node].transpose(interval4)
    chord.addLyric(chordSymbolFigure(chord, inversion=0))
    chord.duration.type = 'whole'
    stream.append(chord)
# Render the resulting chord progression
renderWithLily(stream)
[8]:
../_images/user_guide_chromatic_voice_leading_in_common_trichords_18_0.png
[9]:
# Play the chord progression
playAudio(stream)
[9]: