Skip to content

Commit

Permalink
Merge pull request #505 from cuthbertLab/deriveByDegree
Browse files Browse the repository at this point in the history
key.Key.deriveByDegree overloads on Scale
  • Loading branch information
mscuthbert authored Dec 27, 2019
2 parents cf0a98d + 7a513ff commit 1fb6cc4
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 16 deletions.
44 changes: 44 additions & 0 deletions music21/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,50 @@ def tonicPitchNameWithCase(self):
tonic = tonic.lower()
return tonic

def deriveByDegree(self, degree, pitchRef):
'''
Given a degree and pitchReference derive a new
Key object that has the same mode but a different tonic
Example: What minor key has scale degree 3 as B-flat?
>>> minorKey = key.Key(mode='minor')
>>> newMinor = minorKey.deriveByDegree(3, 'B-')
>>> newMinor
<music21.key.Key of g minor>
Note that in minor, the natural form is used:
>>> minorKey.deriveByDegree(7, 'E')
<music21.key.Key of f# minor>
>>> minorKey.deriveByDegree(6, 'G')
<music21.key.Key of b minor>
To use the harmonic form, change `.abstract` on the key to
another abstract scale:
>>> minorKey.abstract = scale.AbstractHarmonicMinorScale()
>>> minorKey.deriveByDegree(7, 'E')
<music21.key.Key of f minor>
>>> minorKey.deriveByDegree(6, 'G')
<music21.key.Key of b minor>
Currently because of a limitation in bidirectional scale
searching, melodic minor scales cannot be used as abstracts
for deriving by degree.
New in v.6 -- preserve mode in key.Key.deriveByDegree
'''
ret = super().deriveByDegree(degree, pitchRef)
ret.mode = self.mode

# clear these since they no longer apply.
ret.correlationCoefficient = None
ret.alternateInterpretations = []

return ret


def _tonalCertaintyCorrelationCoefficient(self, *args, **keywords):
# possible measures:
if not self.alternateInterpretations:
Expand Down
81 changes: 73 additions & 8 deletions music21/scale/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,8 +612,21 @@ def show(self, fmt=None, app=None, direction=DIRECTION_ASCENDING):


class AbstractDiatonicScale(AbstractScale):
'''
An abstract representation of a Diatonic scale w/ or without mode.
>>> as1 = scale.AbstractDiatonicScale('major')
>>> as1.type
'Abstract diatonic'
>>> as1.mode
'major'
>>> as1.octaveDuplicating
True
'''
def __init__(self, mode: Optional[str] = None):
super().__init__()
self.mode = mode
self.type = 'Abstract diatonic'
self.tonicDegree = None # step of tonic
self.dominantDegree = None # step of dominant
Expand All @@ -625,12 +638,22 @@ def __init__(self, mode: Optional[str] = None):

def __eq__(self, other):
'''
Two AbstractDiatonicScale objects are equal if
their tonicDegrees, their dominantDegrees, and
their networks are the same.
>>> as1 = scale.AbstractDiatonicScale('major')
>>> as2 = scale.AbstractDiatonicScale('lydian')
>>> as3 = scale.AbstractDiatonicScale('ionian')
>>> as1 == as2
False
Note that their modes do not need to be the same.
For instance for the case of major and Ionian which have
the same networks:
>>> as3 = scale.AbstractDiatonicScale('ionian')
>>> (as1.mode, as3.mode)
('major', 'ionian')
>>> as1 == as3
True
'''
Expand All @@ -653,26 +676,30 @@ def buildNetwork(self, mode=None):
Given sub-class dependent parameters, build and assign the IntervalNetwork.
>>> sc = scale.AbstractDiatonicScale()
>>> sc.buildNetwork('lydian')
>>> sc.buildNetwork('Lydian') # N.B. case insensitive
>>> [str(p) for p in sc.getRealization('f4', 1, 'f2', 'f6')]
['F2', 'G2', 'A2', 'B2', 'C3', 'D3', 'E3',
'F3', 'G3', 'A3', 'B3', 'C4', 'D4', 'E4',
'F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5',
'F5', 'G5', 'A5', 'B5', 'C6', 'D6', 'E6', 'F6']
Unknown modes raise an exception:
>>> sc.buildNetwork('blues-like')
Traceback (most recent call last):
music21.scale.ScaleException: Cannot create a scale of the following mode: 'blues-like'
Changed in v.6 -- case insensitive modes
'''
# reference: http://cnx.org/content/m11633/latest/
# most diatonic scales will start with this collection
srcList = ('M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2')
self.tonicDegree = 1
self.dominantDegree = 5

if isinstance(mode, str):
mode = mode.lower()

if mode in (None, 'major', 'ionian'): # c to C
intervalList = srcList
self.relativeMajorDegree = 1
Expand Down Expand Up @@ -1350,10 +1377,31 @@ def abstract(self):
False
>>> sc1.abstract == sc2.abstract
True
Abstract scales can also be set afterwards:
>>> scVague = scale.ConcreteScale()
>>> scVague.abstract = scale.AbstractDiatonicScale('major')
>>> scVague.tonic = pitch.Pitch('D')
>>> [p.name for p in scVague.getPitches()]
['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
>>> scVague.abstract = scale.AbstractOctatonicScale()
>>> [p.name for p in scVague.getPitches()]
['D', 'E', 'F', 'G', 'A-', 'B-', 'C-', 'D-', 'D']
New and beta in v.6 -- changing `.abstract` is now allowed.
'''
# copy before returning? (too slow)
# copy before returning? (No... too slow)
return self._abstract

@abstract.setter
def abstract(self, newAbstract: AbstractScale):
if not isinstance(newAbstract, AbstractScale):
raise TypeError(f'abstract must be an AbstractScale, not {type(newAbstract)}')
self._abstract = newAbstract


def getDegreeMaxUnique(self):
'''
Convenience routine to get this from the AbstractScale.
Expand Down Expand Up @@ -2314,7 +2362,17 @@ def deriveByDegree(self, degree, pitchRef):
TODO: Does not yet work for directional scales
'''
p = self._abstract.getNewTonicPitch(pitchReference=pitchRef, nodeName=degree)
p = self._abstract.getNewTonicPitch(
pitchReference=pitchRef,
nodeName=degree,
)
# except intervalNetwork.IntervalNetworkException:
# p = self._abstract.getNewTonicPitch(
# pitchReference=pitchRef,
# nodeName=degree,
# direction=DIRECTION_DESCENDING,
# )

if p is None:
raise ScaleException('cannot derive new tonic')

Expand Down Expand Up @@ -3338,7 +3396,7 @@ def testCyclicalScales(self):
direction=DIRECTION_ASCENDING), 1)

def testDeriveByDegree(self):
from music21 import scale
from music21 import scale # to get correct reprs
sc1 = scale.MajorScale()
self.assertEqual(str(sc1.deriveByDegree(7, 'G#')),
'<music21.scale.MajorScale A major>')
Expand All @@ -3350,7 +3408,14 @@ def testDeriveByDegree(self):
self.assertEqual(str(sc1.deriveByDegree(2, 'E')),
'<music21.scale.HarmonicMinorScale D harmonic minor>')

# add serial rows as scales
# TODO(CA): add serial rows as scales

# # This test does not yet work.
# def testDeriveByDegreeBiDirectional(self):
# sc1 = MelodicMinorScale()
# sc1.deriveByDegree(6, 'G')



def testMelodicMinorA(self):
mm = MelodicMinorScale('a')
Expand Down Expand Up @@ -3952,7 +4017,7 @@ def testDerivedScaleAbsurdOctaves(self):
if __name__ == '__main__':
# sys.arg test options will be used in mainTest()
import music21
music21.mainTest(Test)
music21.mainTest(Test) # , runTest='testDeriveByDegreeBiDirectional')

# store implicit tonic or Not
# if not set, then comparisons fall to abstract
18 changes: 10 additions & 8 deletions music21/scale/intervalNetwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -1941,8 +1941,9 @@ def realizeIntervals(self,
return iList

def realizeTermini(self, pitchReference, nodeId=None, alteredDegrees=None):
'''Realize the pitches of the 'natural' terminus of a network. This (presently) m
ust be done by ascending, and assumes only one valid terminus for both extremes.
'''
Realize the pitches of the 'natural' terminus of a network. This (presently)
must be done by ascending, and assumes only one valid terminus for both extremes.
This suggests that in practice termini should not be affected by directionality.
Expand Down Expand Up @@ -2550,9 +2551,9 @@ def getPitchFromNodeDegree(self,
>>> net.getPitchFromNodeDegree('c', 1, 6, 'descending')
<music21.pitch.Pitch A-4>
'''
# this is the reference node
# TODO: takes the first, need to add probabilistic selection
nodeId = self.nodeNameToNodes(nodeName)[0] # get the first
# these are the reference node -- generally one except for bidirectional
# scales.
nodeListForNames = self.nodeNameToNodes(nodeName)
# environLocal.printDebug(['getPitchFromNodeDegree()', 'node reference',
# nodeId, 'node degree', nodeId.degree,
# 'pitchReference', pitchReference, 'alteredDegrees', alteredDegrees])
Expand Down Expand Up @@ -2596,14 +2597,15 @@ def getPitchFromNodeDegree(self,
# pass direction as well when getting realization

# TODO: need a way to force that we get a realization that
# may goes through a particular node; we could start at that node?
# brut force approach might make multiple attempts to realize
# may goes through a particular node; we could start at that node?
# brute force approach might make multiple attempts to realize
# TODO: BUG: Does not work with bidirectional scales.

# TODO: possibly cache results
for unused_counter in range(10):
realizedPitch, realizedNode = self.realize(
pitchReference=pitchReference,
nodeId=nodeId,
nodeId=nodeListForNames[0],
minPitch=minPitch,
maxPitch=maxPitch,
direction=direction,
Expand Down

0 comments on commit 1fb6cc4

Please sign in to comment.