Source code for orbichord.chordinate

"""Implement tools to compute chordinates and distances in a given scale."""

from enum import Enum
import itertools
import math
from music21.chord import Chord
from music21.pitch import Pitch
from music21.scale import ConcreteScale
from numpy import array
from typing import Callable


[docs]def scalePoint( chord: Chord, scale: ConcreteScale ) -> list: """Compute chord coordinates using the degree for a given scale Parameters ---------- chrod : Chrod Chord to estimate normal order scale : ConcreteScale Scale use as metric step Return ------ list List with scalar normal order Examples -------- >>> from music21.chord import Chord >>> from music21.scale import MajorScale >>> from orbichord.chordinate import scalePoint >>> scale = MajorScale('C') >>> chord = Chord('C E G') >>> scalePoint(chord, scale) [0, 2, 4] """ # Chordinates point = [] # Loop over pitches and extract scale for pitch in chord.pitches: point.append( scale.getScaleDegreeFromPitch( pitch, comparisonAttribute='pitchClass' ) - 1 ) return point
[docs]def standardSimplex( chord: Chord, scale: ConcreteScale, normalize: bool = True ) -> list: """Compute chord scale point in the standard simplex Parameters ---------- chrod : Chrod Chord to estimate normal order scale : ConcreteScale Scale use as metric step normalize : int, optional Normalize coordinates by the number of scale degrees Return ------ list List with scalar point within standard simplex Examples -------- >>> from music21.chord import Chord >>> from music21.scale import ChromaticScale >>> from orbichord.chordinate import standardSimplex >>> scale = ChromaticScale('C') >>> chord = Chord('C E G') >>> standardSimplex(chord, scale) [0.9166666666666666, 0.3333333333333333, 0.25] >>> standardSimplex(chord, scale, normalize=False) [11, 4, 3] """ # Get scale max degree and compute scalar point max_scale_degree = scale.getDegreeMaxUnique() point = scalePoint(chord, scale) # Reduce to the standard simplex dimension = len(point) sumchord = sum(point) point.sort() while sumchord >= max_scale_degree: last = point[-1] for index in range(1, dimension): point[dimension-index] = point[dimension-index-1] point[0] = last - max_scale_degree sumchord = sum(point) # Apply affine transformation previous = point[0] for index in range(1, dimension): interval = point[index] - previous previous = point[index] point[index] = interval point[0] = sumchord if normalize: for index in range(len(point)): point[index] /= max_scale_degree return point
[docs]def mod(x, y, d): """Implement a modify module to provide shortest possible voice leading. """ positive = (x - y) % d negative = (y - x) % d if positive > negative: return -negative return positive
[docs]class Permutation(Enum): """Define type permutation used interscalar matrix.""" NONE = 1 CYCLIC = 2 ANY = 3
[docs]def interscalarMatrix( chordA: Chord, chordB: Chord, scale: ConcreteScale, cardinality: bool = True, permutation: Permutation = Permutation.ANY ) -> list: """Compute the interscalar matrix between two chords Parameters ---------- chrodA : Chrod Voice leading start chord. chrodA : Chrod Voice leading end chord scale : ConcreteScale Scale use a metric. cardinality: bool, optional If true chord cardinality is invariant. permutation : Permutation, optional Permutation invariance in the interscalar matrix. Return ------ list List of voice leading scalar steps Examples -------- >>> from music21.chord import Chord >>> from music21.scale import MajorScale >>> from orbichord.chordinate import interscalarMatrix, Permutation >>> scale = MajorScale('C') >>> chordA = Chord('C E G') >>> chordB = Chord('A C E') >>> matrix = interscalarMatrix( ... chordA, chordB, scale >>> ) >>> print(matrix) [[0, 0, 1], [2, 3, 3], [-2, -2, -2]] """ if chordA.multisetCardinality != chordB.multisetCardinality: raise ValueError('Chords are not of the same dimension!') if cardinality: tmpA = chordA.removeRedundantPitchClasses(inPlace=False) tmpB = chordB.removeRedundantPitchClasses(inPlace=False) if tmpA.multisetCardinality == tmpB.multisetCardinality: chordA = tmpA chordB = tmpB pointA = scalePoint(chordA, scale) pointB = scalePoint(chordB, scale) if permutation == Permutation.ANY: pointA.sort(); pointB.sort() dimension = len(pointA) voice_leadings = [] max_scale_degree = scale.getDegreeMaxUnique() while pointB[0] < max_scale_degree: delta = [0]*dimension for i in range(dimension): delta[i] = mod(pointB[i], pointA[i], max_scale_degree) voice_leadings.append(delta) if permutation == Permutation.NONE: break tmp = pointB[0] pointB = pointB[1:] + [tmp + max_scale_degree] return voice_leadings
[docs]class EfficientVoiceLeading: """ Compute efficient voice leading between two chords. Parameters ---------- scale : ConcreteScale Scale use to define voice leading steps metric : Callable[[list], float] Metric function permutation : Permutation, optional Permutation invariance in the voice leading. Examples -------- >>> from music21.chord import Chord >>> from music21.scale import MajorScale >>> from numpy import inf >>> from numpy import linalg as la >>> from orbichord.chordinate import EfficientVoiceLeading >>> scale = MajorScale('C') >>> C = Chord('C E G') >>> G = Chord('G B D') >>> voice_leading = EfficientVoiceLeading( ... scale = scale, ... metric = lambda delta: la.norm(delta, inf) >>> ) >>> vl, dist = voice_leading(C, G) >>> print(vl, dist) [-1, -1, 0] 1.0 """ def __init__(self, scale: ConcreteScale, metric: Callable[[list], float], permutation: Permutation = Permutation.ANY ): """Constructor.""" self._scale = scale self._metric = metric self._permutation = permutation @property def scale(self): """Returns voice leaging scale.""" return self._scale @property def metric(self): """Returns voice leaging metric.""" return self._metric @property def permutation(self): """Returns voice leaging metric.""" return self._permutation
[docs] def __call__(self, chordA: Chord, chordB: Chord, ) -> tuple: """Return the efficient voice leading and its distance Parameters ---------- chrodA : Chrod Voice leading start chord chrodA : Chrod Voice leading end chord Return ------ tuple Efficient voice leading scalar steps and its distance """ voice_leading_distance = None voice_leading_index = None matrix = interscalarMatrix( chordA, chordB, self._scale, permutation=self._permutation ) for index in range(len(matrix)): voice_leading = matrix[index] distance = self._metric(voice_leading) if voice_leading_distance is None: voice_leading_distance = distance voice_leading_index = index continue if distance < voice_leading_distance: voice_leading_distance = distance voice_leading_index = index return matrix[voice_leading_index], voice_leading_distance