Source code for music21.analysis.enharmonics
# ------------------------------------------------------------------------------
# Name: enharmonics.py
# Purpose: Tools for returning best enharmonics
#
# Authors: Mark Gotham
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2017 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations
import itertools
from math import inf
import unittest
from music21 import environment
from music21 import pitch
from music21 import musedata
environLocal = environment.Environment('analysis.enharmonics')
[docs]
class EnharmonicScoreRules:
def __init__(self):
self.sameStaffLine = False
self.alterationPenalty = 4
self.augDimPenalty = 2
self.mixSharpsFlatsPenalty = False
[docs]
class ChordEnharmonicScoreRules(EnharmonicScoreRules):
def __init__(self):
super().__init__()
self.mixSharpsFlatsPenalty = 2
[docs]
class EnharmonicSimplifier:
'''
Takes any pitch list input and returns the best enharmonic respelling according to the input
criteria and rule weightings.
Those criteria and rule weightings are currently fixed, but in future the user should be able
to select their own combination and weighting of rules according to preferences,
with predefined defaults for melodic and harmonic norms.
Note: EnharmonicSimplifier itself returns nothing.
'''
def __init__(self, pitchList, ruleClass=EnharmonicScoreRules):
if isinstance(pitchList[0], str):
pitchList = [pitch.Pitch(p) for p in pitchList]
self.pitchList = pitchList
self.ruleObject = ruleClass()
self.allPossibleSpellings = None
self.allSpellings = []
self.getRepresentations()
[docs]
def getRepresentations(self):
'''
Takes a list of pitches or pitch names and retrieves all enharmonic spellings.
Note: getRepresentations itself returns nothing.
'''
allSpellings = []
for p in self.pitchList:
spellings = [p] + p.getAllCommonEnharmonics(1)
allSpellings.append(spellings)
self.allSpellings = allSpellings
[docs]
def getProduct(self):
self.allPossibleSpellings = list(itertools.product(*self.allSpellings))
return self.allPossibleSpellings
[docs]
def bestPitches(self):
'''
Returns a list of pitches in the best enharmonic
spelling according to the input criteria.
>>> pList1 = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
>>> es = analysis.enharmonics.EnharmonicSimplifier(pList1)
>>> es.bestPitches()
(<music21.pitch.Pitch C>, <music21.pitch.Pitch D>, <music21.pitch.Pitch E>)
>>> pList2 = ['D--', 'E', 'F##']
>>> es = analysis.enharmonics.EnharmonicSimplifier(pList2)
>>> es.bestPitches()
(<music21.pitch.Pitch C>, <music21.pitch.Pitch E>, <music21.pitch.Pitch G>)
'''
self.getProduct()
bestPitches = []
minScore = inf
for possibility in self.allPossibleSpellings:
thisAugDimScore = self.getAugDimScore(possibility)
thisAlterationScore = self.getAlterationScore(possibility)
thisMixSharpsFlatScore = self.getMixSharpFlatsScore(possibility)
thisScore = thisAugDimScore + thisAlterationScore + thisMixSharpsFlatScore
if thisScore < minScore:
minScore = thisScore
bestPitches = possibility
return bestPitches
[docs]
def getAlterationScore(self, possibility):
'''
Returns a score according to the number of sharps and flats in a possible spelling.
The score is the sum of the flats and sharps + 1, multiplied by the alterationPenalty.
'''
if self.ruleObject.alterationPenalty is False:
return 1
joinedPossibility = ''.join([p.name for p in possibility])
flatCount = joinedPossibility.count('-')
sharpCount = joinedPossibility.count('#')
score = (flatCount + sharpCount + 1) * self.ruleObject.alterationPenalty
return score
[docs]
def getMixSharpFlatsScore(self, possibility):
'''
Returns a score based on the mixture of sharps and flats in a possible spelling:
the score is given by the number of the lesser used accidental (sharps or flats)
multiplied by the mixSharpsFlatsPenalty.
'''
if self.ruleObject.mixSharpsFlatsPenalty is False:
return 1
joinedPossibility = ''.join([p.name for p in possibility])
flatCount = joinedPossibility.count('-')
sharpCount = joinedPossibility.count('#')
score = min([flatCount, sharpCount]) * self.ruleObject.mixSharpsFlatsPenalty
return score
[docs]
def getAugDimScore(self, possibility):
'''
Returns a score based on the number of augmented and diminished intervals between
successive pitches in the given spelling.
'''
if self.ruleObject.augDimPenalty is False:
return 1
intervalStr = ''
for i in range(len(possibility) - 1):
p0 = musedata.base40.base40Representation[possibility[i].name]
p1 = musedata.base40.base40Representation[possibility[i + 1].name]
base40diff = (p1 - p0) % 40
intervalStr += musedata.base40.base40IntervalTable.get(base40diff, 'ddd')
dimCount = intervalStr.count('A')
augCount = intervalStr.count('d')
score = (dimCount + augCount + 1) * self.ruleObject.augDimPenalty
return score
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testBestPitches(self):
pList = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
es = EnharmonicSimplifier(pList)
bestPitchList = es.bestPitches()
self.assertEqual(len(pList), 3)
self.assertEqual(len(bestPitchList), 3)
self.assertIsInstance(bestPitchList[0], pitch.Pitch)
def testGetAlterationScore(self):
pList = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
es = EnharmonicSimplifier(pList)
poss = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
testAltScore = es.getAlterationScore(poss)
self.assertEqual(len(pList), 3)
self.assertIsInstance(testAltScore, int)
def testGetMixSharpFlatsScore(self):
pList = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
es = EnharmonicSimplifier(pList)
poss = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
testMixScore = es.getMixSharpFlatsScore(poss)
self.assertEqual(len(pList), 3)
self.assertIsInstance(testMixScore, int)
def testGetAugDimScore(self):
pList = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
es = EnharmonicSimplifier(pList)
poss = [pitch.Pitch('C'), pitch.Pitch('D'), pitch.Pitch('E')]
testAugDimScore = es.getAugDimScore(poss)
self.assertEqual(len(pList), 3)
self.assertIsInstance(testAugDimScore, int)
# -----------------------------------------------------------------------------
if __name__ == '__main__':
import music21
music21.mainTest(Test)