Chromatic voice leading in common tetrachords¶
The reason I created orbichord is to explore voice leading and chord progression in non-trivial spaces as the tetrachord orbifold. 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.
Import module¶
[1]:
# Import aux modules
import networkx as nx
# 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.identify import chordSymbolIndex
from orbichord.symbol import chordSymbolFigure, hasChordSymbolFigure
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=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(
    dimension = 4,
    pitches = scale.getPitches('C','B')
)
nstantiate 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)
)
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
#hv.DynamicMap(
#    neighbor_graph,
#    kdims=['chord']
#).redim.values(
#    chord = sorted(graph.nodes)
#)
neighbor_graph('A')
[5]:
All the shortest Voice Leasing between two dichords¶
Search for all the possible voice leading for going from A to G chord.
[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('A', 'G')
[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('A', 'G', 0)
[7]:
Generate a stream with the selection 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]:
[9]:
# Play the chord progression
playAudio(stream)
[9]: