# ------------------------------------------------------------------------------
# Name: segment.py
# Purpose: figured bass note and notational realization.
# Authors: Jose Cabal-Ugaz
#
# Copyright: Copyright © 2011 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations
from collections.abc import Callable, Iterable, Iterator
import collections
import copy
import itertools
import typing as t
import unittest
from music21 import chord
from music21 import environment
from music21 import exceptions21
from music21 import note
from music21 import pitch
from music21 import scale
from music21.common.types import OffsetQL
from music21.figuredBass import possibility
from music21.figuredBass.possibility import Possibility
from music21.figuredBass import realizerScale
from music21.figuredBass import resolution
from music21.figuredBass import rules
# used below
_MOD = 'figuredBass.segment'
_defaultRealizerScale: dict[str, realizerScale.FiguredBassScale|None] = {
'scale': None, # singleton
}
[docs]
class Segment:
_DOC_ORDER = ['allSinglePossibilities',
'singlePossibilityRules',
'allCorrectSinglePossibilities',
'consecutivePossibilityRules',
'specialResolutionRules',
'allCorrectConsecutivePossibilities',
'resolveDominantSeventhSegment',
'resolveDiminishedSeventhSegment',
'resolveAugmentedSixthSegment']
_DOC_ATTR: dict[str, str] = {
'bassNote': '''A :class:`~music21.note.Note` whose pitch
forms the bass of each possibility.''',
'numParts': '''The number of parts (including the bass) that possibilities
should contain, which
comes directly from :attr:`~music21.figuredBass.rules.Rules.numParts`
in the Rules object.''',
'pitchNamesInChord': '''A list of allowable pitch names.
This is derived from bassNote.pitch and notationString
using :meth:`~music21.figuredBass.realizerScale.FiguredBassScale.getPitchNames`.''',
'allPitchesAboveBass': '''A list of allowable pitches in the upper parts of a possibility.
This is derived using
:meth:`~music21.figuredBass.segment.getPitches`, providing bassNote.pitch,
:attr:`~music21.figuredBass.rules.Rules.maxPitch`
from the Rules object, and
:attr:`~music21.figuredBass.segment.Segment.pitchNamesInChord` as arguments.''',
'segmentChord': ''':attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`
represented as a :class:`~music21.chord.Chord`.''',
'fbRules': 'A deepcopy of the :class:`~music21.figuredBass.rules.Rules` object provided.',
}
# Attributes set externally (e.g. by figuredBass.realizer) during realization.
# Declared here for typing only; they are not initialized in __init__.
movements: dict[Possibility, list[Possibility]]
correctA: list[Possibility]
quarterLength: OffsetQL
def __init__(self,
bassNote: str|note.Note = 'C3',
notationString: str|None = None,
fbScale: realizerScale.FiguredBassScale|None = None,
fbRules: rules.Rules|None = None,
numParts: int = 4,
maxPitch: str|pitch.Pitch = 'B5',
listOfPitches: Iterable[str]|None = None):
'''
A Segment corresponds to a 1:1 realization of a bassNote and notationString
of a :class:`~music21.figuredBass.realizer.FiguredBassLine`.
It is created by passing six arguments: a
:class:`~music21.figuredBass.realizerScale.FiguredBassScale`, a bassNote, a notationString,
a :class:`~music21.figuredBass.rules.Rules` object, a number of parts and a maximum pitch.
Realizations of a Segment are represented
as possibility tuples (see :mod:`~music21.figuredBass.possibility` for more details).
Methods in Python's `itertools`
module are used extensively. Methods
which generate possibilities or possibility progressions return iterators,
which are turned into lists in the examples
for display purposes only.
if fbScale is None, a realizerScale.FiguredBassScale() is created
if fbRules is None, a rules.Rules() instance is created. Each Segment gets
its own deepcopy of the one given.
Here, a Segment is created using the default values: a FiguredBassScale in C,
a bassNote of C3, an empty notationString, and a default
Rules object.
>>> from music21.figuredBass import segment
>>> s1 = segment.Segment()
>>> s1.bassNote
<music21.note.Note C>
>>> s1.numParts
4
>>> s1.pitchNamesInChord
['C', 'E', 'G']
>>> [str(p) for p in s1.allPitchesAboveBass]
['C3', 'E3', 'G3', 'C4', 'E4', 'G4', 'C5', 'E5', 'G5']
>>> s1.segmentChord
<music21.chord.Chord C3 E3 G3 C4 E4 G4 C5 E5 G5>
'''
if isinstance(bassNote, str):
bassNote = note.Note(bassNote)
if isinstance(maxPitch, str):
maxPitch = pitch.Pitch(maxPitch)
if fbScale is None:
if _defaultRealizerScale['scale'] is None:
_defaultRealizerScale['scale'] = realizerScale.FiguredBassScale()
fbScale = _defaultRealizerScale['scale'] # save the time of making it
assert fbScale is not None # tells mypy that we have it now
if fbRules is None:
self.fbRules = rules.Rules()
else:
self.fbRules = copy.deepcopy(fbRules)
# Empty until the matching allCorrect*/special-resolution method compiles them.
self._specialResolutionRuleChecking: dict[bool, list[tuple[t.Any, ...]]] = {}
self._singlePossibilityRuleChecking: dict[bool, list[tuple[t.Any, ...]]] = {}
self._consecutivePossibilityRuleChecking: dict[bool, list[tuple[t.Any, ...]]] = {}
self.bassNote: note.Note = bassNote
self.numParts: int = numParts
self._maxPitch: pitch.Pitch = maxPitch
# `notationString is None` is the mode sentinel: None means "no figures given,
# take the chord from listOfPitches" (the harmony.ChordSymbol / roman.RomanNumeral
# path); any string (including '') means "figured bass, derive pitches from the
# figures". realizer.py only ever supplies one of the two, so they never collide,
# but if both are given listOfPitches is silently dropped. (This is why
# notationString must NOT be defaulted to '': that would disable the sentinel and
# break every chord-symbol/roman realization.)
if notationString is None and listOfPitches is not None:
self.pitchNamesInChord = listOfPitches
else:
self.pitchNamesInChord = fbScale.getPitchNames(self.bassNote.pitch,
notationString or '')
self.allPitchesAboveBass = getPitches(self.pitchNamesInChord,
self.bassNote.pitch,
self._maxPitch)
self.segmentChord = chord.Chord(self.allPitchesAboveBass,
quarterLength=bassNote.quarterLength)
self._environRules = environment.Environment(_MOD)
# ------------------------------------------------------------------------------
# EXTERNAL METHODS
[docs]
def singlePossibilityRules(
self,
fbRules: rules.Rules|None = None
) -> list[tuple[t.Any, ...]]:
# noinspection PyShadowingNames
'''
A framework for storing single possibility rules and methods to be applied
in :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`.
Takes in a :class:`~music21.figuredBass.rules.Rules` object, fbRules.
If None then a new rules object is created.
Items are added within this method in the following form:
(willRunOnlyIfTrue, methodToRun, keepSolutionsWhichReturn, optionalArgs)
These items are compiled internally when
:meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`
is called on a Segment. Here, the compilation of rules and
methods bases on a default fbRules is shown.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment()
>>> allSingleRules = segmentA.singlePossibilityRules()
>>> segment.printRules(allSingleRules)
Will run: Method: Keep solutions which return: Arguments:
True isIncomplete False ['C', 'E', 'G']
True upperPartsWithinLimit True 12
True voiceCrossing False None
Here, a modified fbRules is provided, which allows for incomplete possibilities.
>>> from music21.figuredBass import rules
>>> fbRules = rules.Rules()
>>> fbRules.forbidIncompletePossibilities = False
>>> allSingleRules = segmentA.singlePossibilityRules(fbRules)
>>> segment.printRules(allSingleRules)
Will run: Method: Keep solutions which return: Arguments:
False isIncomplete False ['C', 'E', 'G']
True upperPartsWithinLimit True 12
True voiceCrossing False None
'''
if fbRules is None:
fbRules = rules.Rules()
singlePossibRules = [
(fbRules.forbidIncompletePossibilities,
possibility.isIncomplete,
False,
[self.pitchNamesInChord]),
(True,
possibility.upperPartsWithinLimit,
True,
[fbRules.upperPartsMaxSemitoneSeparation]),
(fbRules.forbidVoiceCrossing,
possibility.voiceCrossing,
False)
]
return singlePossibRules
[docs]
def consecutivePossibilityRules(
self,
fbRules: rules.Rules|None = None
) -> list[tuple[t.Any, ...]]:
# noinspection PyShadowingNames
'''
A framework for storing consecutive possibility rules and methods to be applied
in :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`.
Takes in a :class:`~music21.figuredBass.rules.Rules` object, fbRules; if None
then a new rules.Rules() object is created.
Items are added within this method in the following form:
(willRunOnlyIfTrue, methodToRun, keepSolutionsWhichReturn, optionalArgs)
These items are compiled internally when
:meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`
is called on a Segment. Here, the compilation of rules and methods
bases on a default fbRules is shown.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment()
>>> allConsecutiveRules = segmentA.consecutivePossibilityRules()
>>> segment.printRules(allConsecutiveRules)
Will run: Method: Keep solutions which return: Arguments:
True partsSame True []
False upperPartsSame True None
True voiceOverlap False None
True partMovementsWithinLimits True []
True parallelFifths False None
True parallelOctaves False None
True hiddenFifths False None
True hiddenOctaves False None
False couldBeItalianA6Resolution True [<music21.pitch.Pitch C3>,
<music21.pitch.Pitch C3>,
<music21.pitch.Pitch E3>,
<music21.pitch.Pitch G3>], True
Now, a modified fbRules is provided, allowing hidden octaves and
voice overlap, and limiting the soprano line to stepwise motion.
>>> from music21.figuredBass import rules
>>> fbRules = rules.Rules()
>>> fbRules.forbidVoiceOverlap = False
>>> fbRules.forbidHiddenOctaves = False
>>> fbRules.partMovementLimits.append((1, 2))
>>> allConsecutiveRules = segmentA.consecutivePossibilityRules(fbRules)
>>> segment.printRules(allConsecutiveRules)
Will run: Method: Keep solutions which return: Arguments:
True partsSame True []
False upperPartsSame True None
False voiceOverlap False None
True partMovementsWithinLimits True [(1, 2)]
True parallelFifths False None
True parallelOctaves False None
True hiddenFifths False None
False hiddenOctaves False None
False couldBeItalianA6Resolution True [<music21.pitch.Pitch C3>,
<music21.pitch.Pitch C3>,
<music21.pitch.Pitch E3>,
<music21.pitch.Pitch G3>], True
'''
if fbRules is None:
fbRules = rules.Rules()
isItalianAugmentedSixth = self.segmentChord.isItalianAugmentedSixth()
consecutivePossibRules = [
(True, possibility.partsSame, True, [fbRules._partsToCheck]),
(fbRules._upperPartsRemainSame, possibility.upperPartsSame, True),
(fbRules.forbidVoiceOverlap, possibility.voiceOverlap, False),
(True, possibility.partMovementsWithinLimits, True, [fbRules.partMovementLimits]),
(fbRules.forbidParallelFifths, possibility.parallelFifths, False),
(fbRules.forbidParallelOctaves, possibility.parallelOctaves, False),
(fbRules.forbidHiddenFifths, possibility.hiddenFifths, False),
(fbRules.forbidHiddenOctaves, possibility.hiddenOctaves, False),
(fbRules.resolveAugmentedSixthProperly and isItalianAugmentedSixth,
possibility.couldBeItalianA6Resolution,
True,
[_unpackTriad(self.segmentChord), fbRules.restrictDoublingsInItalianA6Resolution])
]
return consecutivePossibRules
[docs]
def specialResolutionRules(
self,
fbRules: rules.Rules|None = None
) -> list[tuple[t.Any, ...]]:
'''
A framework for storing methods which perform special resolutions
on Segments. Unlike the methods in
:meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules` and
:meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`,
these methods deal with the Segment itself, and rely on submethods
to resolve the individual possibilities accordingly depending on what
the resolution Segment is.
If fbRules is None, then a new rules.Rules() object is created.
Items are added within this method in the following form:
(willRunOnlyIfTrue, methodToRun, optionalArgs)
These items are compiled internally
when :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`
is called on a Segment. Here, the compilation of rules and methods
based on a default fbRules is shown.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment()
>>> allSpecialResRules = segmentA.specialResolutionRules()
>>> segment.printRules(allSpecialResRules, maxLength=3)
Will run: Method: Arguments:
False resolveDominantSeventhSegment None
False resolveDiminishedSeventhSegment False
False resolveAugmentedSixthSegment None
Dominant Seventh Segment:
>>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='6,5')
>>> allSpecialResRules = segmentA.specialResolutionRules()
>>> segment.printRules(allSpecialResRules, maxLength=3)
Will run: Method: Arguments:
True resolveDominantSeventhSegment None
False resolveDiminishedSeventhSegment False
False resolveAugmentedSixthSegment None
Fully-Diminished Seventh Segment:
>>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='-7')
>>> allSpecialResRules = segmentA.specialResolutionRules()
>>> segment.printRules(allSpecialResRules, maxLength=3)
Will run: Method: Arguments:
False resolveDominantSeventhSegment None
True resolveDiminishedSeventhSegment False
False resolveAugmentedSixthSegment None
Augmented Sixth Segment:
>>> segmentA = segment.Segment(bassNote=note.Note('A-2'), notationString='#6,b5')
>>> allSpecialResRules = segmentA.specialResolutionRules()
>>> segment.printRules(allSpecialResRules, maxLength=3)
Will run: Method: Arguments:
False resolveDominantSeventhSegment None
False resolveDiminishedSeventhSegment False
True resolveAugmentedSixthSegment None
'''
if fbRules is None:
fbRules = rules.Rules()
isDominantSeventh = self.segmentChord.isDominantSeventh()
isDiminishedSeventh = self.segmentChord.isDiminishedSeventh()
isAugmentedSixth = self.segmentChord.isAugmentedSixth()
specialResRules = [
(fbRules.resolveDominantSeventhProperly and isDominantSeventh,
self.resolveDominantSeventhSegment),
(fbRules.resolveDiminishedSeventhProperly and isDiminishedSeventh,
self.resolveDiminishedSeventhSegment,
[fbRules.doubledRootInDim7]),
(fbRules.resolveAugmentedSixthProperly and isAugmentedSixth,
self.resolveAugmentedSixthSegment)
]
return specialResRules
[docs]
def resolveDominantSeventhSegment(
self,
segmentB: Segment
) -> Iterator[tuple[Possibility, Possibility]]:
# noinspection PyShadowingNames
'''
Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord`
spells out a dominant seventh chord. If no applicable method in
:mod:`~music21.figuredBass.resolution` can be used, the Segment is resolved
as an ordinary Segment.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment(bassNote=note.Note('G2'), notationString='7')
>>> allDomPossib = segmentA.allCorrectSinglePossibilities()
>>> allDomPossibList = list(allDomPossib)
>>> len(allDomPossibList)
8
>>> allDomPossibList[2]
(<music21.pitch.Pitch D4>, <music21.pitch.Pitch B3>,
<music21.pitch.Pitch F3>, <music21.pitch.Pitch G2>)
>>> allDomPossibList[5]
(<music21.pitch.Pitch D5>, <music21.pitch.Pitch B4>,
<music21.pitch.Pitch F4>, <music21.pitch.Pitch G2>)
Here, the Soprano pitch of resolution (C6) exceeds default maxPitch of B5, so
it's filtered out.
>>> [p.nameWithOctave for p in allDomPossibList[7]]
['B5', 'F5', 'D5', 'G2']
>>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='')
>>> domResPairs = segmentA.resolveDominantSeventhSegment(segmentB)
>>> domResPairsList = list(domResPairs)
>>> len(domResPairsList)
7
>>> domResPairsList[2]
((<music21.pitch.Pitch D4>, <...B3>, <...F3>, <...G2>),
(<...C4>, <...C4>, <...E3>, <...C3>))
>>> domResPairsList[5]
((<...D5>, <...B4>, <...F4>, <...G2>), (<...C5>, <...C5>, <...E4>, <...C3>))
'''
domChord = self.segmentChord
if not domChord.isDominantSeventh():
# Put here for stand-alone purposes.
raise SegmentException('Dominant seventh resolution: Not a dominant seventh Segment.')
domChordInfo = _unpackSeventhChord(domChord)
dominantScale = t.cast(scale.MajorScale, scale.MajorScale().derive(domChord))
minorScale = dominantScale.getParallelMinor()
# A complete diatonic scale always defines a tonic and every degree 1-7,
# so getTonic() and pitchFromDegree() never return None here; the casts are safe.
tonic = t.cast(pitch.Pitch, dominantScale.getTonic())
subdominant = t.cast(pitch.Pitch, dominantScale.pitchFromDegree(4))
majSubmediant = t.cast(pitch.Pitch, dominantScale.pitchFromDegree(6))
minSubmediant = t.cast(pitch.Pitch, minorScale.pitchFromDegree(6))
resChord = segmentB.segmentChord
domInversion = (domChord.inversion() == 2)
resInversion = (resChord.inversion())
resolveV43toI6 = domInversion and resInversion == 1
if (domChord.inversion() == 0
and resChord.root().name == tonic.name
and (resChord.isMajorTriad() or resChord.isMinorTriad())):
# "V7 to I" resolutions are always incomplete, with a missing fifth.
segmentB.fbRules.forbidIncompletePossibilities = False
dominantResolutionMethods = [
(resChord.root().name == tonic.name and resChord.isMajorTriad(),
resolution.dominantSeventhToMajorTonic,
[resolveV43toI6, domChordInfo]),
(resChord.root().name == tonic.name and resChord.isMinorTriad(),
resolution.dominantSeventhToMinorTonic,
[resolveV43toI6, domChordInfo]),
((resChord.root().name == majSubmediant.name
and resChord.isMinorTriad()
and domInversion == 0),
resolution.dominantSeventhToMinorSubmediant,
[domChordInfo]),
((resChord.root().name == minSubmediant.name
and resChord.isMajorTriad()
and domInversion == 0),
resolution.dominantSeventhToMajorSubmediant,
[domChordInfo]),
((resChord.root().name == subdominant.name
and resChord.isMajorTriad()
and domInversion == 0),
resolution.dominantSeventhToMajorSubdominant,
[domChordInfo]),
((resChord.root().name == subdominant.name
and resChord.isMinorTriad()
and domInversion == 0),
resolution.dominantSeventhToMinorSubdominant,
[domChordInfo])
]
try:
return self._resolveSpecialSegment(segmentB, dominantResolutionMethods)
except SegmentException:
self._environRules.warn(
'Dominant seventh resolution: No proper resolution available. '
+ 'Executing ordinary resolution.')
return self._resolveOrdinarySegment(segmentB)
[docs]
def resolveDiminishedSeventhSegment(
self,
segmentB: Segment,
doubledRoot: bool = False
) -> Iterator[tuple[Possibility, Possibility]]:
# noinspection PyShadowingNames
'''
Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord`
spells out a diminished seventh chord. If no applicable method in
:mod:`~music21.figuredBass.resolution` can be used, the Segment is resolved
as an ordinary Segment.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='b7')
>>> allDimPossib = segmentA.allCorrectSinglePossibilities()
>>> allDimPossibList = list(allDimPossib)
>>> len(allDimPossibList)
7
>>> [p.nameWithOctave for p in allDimPossibList[4]]
['D5', 'A-4', 'F4', 'B2']
>>> [p.nameWithOctave for p in allDimPossibList[6]]
['A-5', 'F5', 'D5', 'B2']
>>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='')
>>> dimResPairs = segmentA.resolveDiminishedSeventhSegment(segmentB)
>>> dimResPairsList = list(dimResPairs)
>>> len(dimResPairsList)
7
>>> dimResPairsList[4]
((<...D5>, <...A-4>, <...F4>, <...B2>), (<...E5>, <...G4>, <...E4>, <...C3>))
>>> dimResPairsList[6]
((<...A-5>, <...F5>, <...D5>, <...B2>), (<...G5>, <...E5>, <...E5>, <...C3>))
'''
dimChord = self.segmentChord
if not dimChord.isDiminishedSeventh():
# Put here for stand-alone purposes.
raise SegmentException(
'Diminished seventh resolution: Not a diminished seventh Segment.')
dimChordInfo = _unpackSeventhChord(dimChord)
dimScale = scale.HarmonicMinorScale().deriveByDegree(7, dimChord.root())
# minorScale = dimScale.getParallelMinor()
# A complete diatonic scale always defines a tonic and every degree 1-7,
# so getTonic() and pitchFromDegree() never return None here; the casts are safe.
tonic = t.cast(pitch.Pitch, dimScale.getTonic())
subdominant = t.cast(pitch.Pitch, dimScale.pitchFromDegree(4))
resChord = segmentB.segmentChord
if dimChord.inversion() == 1: # Doubled root in context
if resChord.inversion() == 0:
doubledRoot = True
elif resChord.inversion() == 1:
doubledRoot = False
diminishedResolutionMethods = [
(resChord.root().name == tonic.name and resChord.isMajorTriad(),
resolution.diminishedSeventhToMajorTonic,
[doubledRoot, dimChordInfo]),
(resChord.root().name == tonic.name and resChord.isMinorTriad(),
resolution.diminishedSeventhToMinorTonic,
[doubledRoot, dimChordInfo]),
(resChord.root().name == subdominant.name and resChord.isMajorTriad(),
resolution.diminishedSeventhToMajorSubdominant,
[dimChordInfo]),
(resChord.root().name == subdominant.name and resChord.isMinorTriad(),
resolution.diminishedSeventhToMinorSubdominant,
[dimChordInfo])
]
try:
return self._resolveSpecialSegment(segmentB, diminishedResolutionMethods)
except SegmentException:
self._environRules.warn(
'Diminished seventh resolution: No proper resolution available. '
+ 'Executing ordinary resolution.')
return self._resolveOrdinarySegment(segmentB)
[docs]
def resolveAugmentedSixthSegment(
self,
segmentB: Segment
) -> Iterator[tuple[Possibility, Possibility]]:
# noinspection PyShadowingNames
'''
Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord`
spells out a
French, German, or Swiss augmented sixth chord. Italian augmented sixth Segments
are solved as an
ordinary Segment using :meth:`~music21.figuredBass.possibility.couldBeItalianA6Resolution`.
If no
applicable method in :mod:`~music21.figuredBass.resolution` can be used, the Segment
is resolved
as an ordinary Segment.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment(bassNote=note.Note('A-2'), notationString='#6,b5,3')
>>> segmentA.pitchNamesInChord # spell out a Gr+6 chord
['A-', 'C', 'E-', 'F#']
>>> allAugSixthPossib = segmentA.allCorrectSinglePossibilities()
>>> allAugSixthPossibList = list(allAugSixthPossib)
>>> len(allAugSixthPossibList)
7
>>> allAugSixthPossibList[1]
(<music21.pitch.Pitch C4>, <music21.pitch.Pitch F#3>, <...E-3>, <...A-2>)
>>> allAugSixthPossibList[4]
(<music21.pitch.Pitch C5>, <music21.pitch.Pitch F#4>, <...E-4>, <...A-2>)
>>> segmentB = segment.Segment(bassNote=note.Note('G2'), notationString='')
>>> allAugResPossibPairs = segmentA.resolveAugmentedSixthSegment(segmentB)
>>> allAugResPossibPairsList = list(allAugResPossibPairs)
>>> len(allAugResPossibPairsList)
7
>>> allAugResPossibPairsList[1]
((<...C4>, <...F#3>, <...E-3>, <...A-2>), (<...B3>, <...G3>, <...D3>, <...G2>))
>>> allAugResPossibPairsList[4]
((<...C5>, <...F#4>, <...E-4>, <...A-2>), (<...B4>, <...G4>, <...D4>, <...G2>))
'''
augSixthChord = self.segmentChord
if not augSixthChord.isAugmentedSixth():
# Put here for stand-alone purposes.
raise SegmentException('Augmented sixth resolution: Not an augmented sixth Segment.')
if augSixthChord.isItalianAugmentedSixth():
return self._resolveOrdinarySegment(segmentB)
elif augSixthChord.isFrenchAugmentedSixth():
augSixthType = 1
elif augSixthChord.isGermanAugmentedSixth():
augSixthType = 2
elif augSixthChord.isSwissAugmentedSixth():
augSixthType = 3
else:
self._environRules.warn(
'Augmented sixth resolution: '
+ 'Augmented sixth type not supported. Executing ordinary resolution.')
return self._resolveOrdinarySegment(segmentB)
tonic = resolution._transpose(augSixthChord.bass(), 'M3')
majorScale = scale.MajorScale(tonic)
# minorScale = scale.MinorScale(tonic)
resChord = segmentB.segmentChord
augSixthChordInfo = _unpackSeventhChord(augSixthChord)
augmentedSixthResolutionMethods = [
((resChord.inversion() == 2
and resChord.root().name == tonic.name
and resChord.isMajorTriad()),
resolution.augmentedSixthToMajorTonic, [augSixthType, augSixthChordInfo]),
((resChord.inversion() == 2
and resChord.root().name == tonic.name
and resChord.isMinorTriad()),
resolution.augmentedSixthToMinorTonic,
[augSixthType, augSixthChordInfo]),
((t.cast(pitch.Pitch, majorScale.pitchFromDegree(5)).name == resChord.bass().name
and resChord.isMajorTriad()),
resolution.augmentedSixthToDominant,
[augSixthType, augSixthChordInfo])
]
try:
return self._resolveSpecialSegment(segmentB, augmentedSixthResolutionMethods)
except SegmentException:
self._environRules.warn(
'Augmented sixth resolution: No proper resolution available. '
+ 'Executing ordinary resolution.')
return self._resolveOrdinarySegment(segmentB)
[docs]
def allSinglePossibilities(self) -> Iterator[Possibility]:
'''
Returns an iterator through a set of naive possibilities for
a Segment, using :attr:`~music21.figuredBass.segment.Segment.numParts`,
the pitch of :attr:`~music21.figuredBass.segment.Segment.bassNote`, and
:attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment()
>>> allPossib = segmentA.allSinglePossibilities()
>>> allPossib.__class__
<class 'itertools.product'>
The number of naive possibilities is always the length of
:attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`
raised to the (:attr:`~music21.figuredBass.segment.Segment.numParts` - 1)
power. The power is 1 less than the number of parts because
the bass pitch is constant.
>>> allPossibList = list(allPossib)
>>> len(segmentA.allPitchesAboveBass)
9
>>> segmentA.numParts
4
>>> len(segmentA.allPitchesAboveBass) ** (segmentA.numParts-1)
729
>>> len(allPossibList)
729
>>> for i in (81, 275, 426):
... [str(p) for p in allPossibList[i]]
['E3', 'C3', 'C3', 'C3']
['C4', 'C4', 'G4', 'C3']
['G4', 'G3', 'C4', 'C3']
'''
iterables = [self.allPitchesAboveBass] * (self.numParts - 1)
iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)])
return itertools.product(*iterables)
[docs]
def allCorrectSinglePossibilities(self) -> list[Possibility]:
'''
Uses :meth:`~music21.figuredBass.segment.Segment.allSinglePossibilities` and
returns an iterator through a set of correct possibilities for
a Segment, all possibilities which pass all filters in
:meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment()
>>> allPossib = segmentA.allSinglePossibilities()
>>> allCorrectPossib = segmentA.allCorrectSinglePossibilities()
Most of the 729 naive possibilities were filtered out using the default rules set,
leaving only 21.
>>> allPossibList = list(allPossib)
>>> len(allPossibList)
729
>>> allCorrectPossibList = list(allCorrectPossib)
>>> len(allCorrectPossibList)
21
>>> for i in (5, 12, 20):
... [str(p) for p in allCorrectPossibList[i]]
['E4', 'G3', 'G3', 'C3']
['C5', 'G4', 'E4', 'C3']
['G5', 'G5', 'E5', 'C3']
'''
self._singlePossibilityRuleChecking = _compileRules(
self.singlePossibilityRules(self.fbRules))
allA = self.allSinglePossibilities()
return [possibA for possibA in allA if self._isCorrectSinglePossibility(possibA)]
[docs]
def allCorrectConsecutivePossibilities(
self,
segmentB: Segment
) -> Iterator[tuple[Possibility, Possibility]]:
# noinspection PyShadowingNames
'''
Returns an iterator through correct (possibA, possibB) pairs.
* If segmentA (self) is a special Segment, meaning that one of the Segment
resolution methods in :meth:`~music21.figuredBass.segment.Segment.specialResolutionRules`
needs to be applied, then this method returns every correct possibility of segmentA
matched up with exactly one resolution possibility.
* If segmentA is an ordinary, non-special Segment, then this method returns every
combination of correct possibilities of segmentA and correct possibilities of segmentB
which passes all filters
in :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`.
Two notes on segmentA being a special Segment:
1. By default, resolution possibilities are not filtered
using :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`
rules of segmentB. Filter by setting
:attr:`~music21.figuredBass.rules.Rules.applySinglePossibRulesToResolution` to True.
2. By default, (possibA, possibB) pairs are not filtered
using :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`
rules of segmentA. Filter by setting
:attr:`~music21.figuredBass.rules.Rules.applyConsecutivePossibRulesToResolution`
to True.
>>> from music21.figuredBass import segment
>>> segmentA = segment.Segment(bassNote=note.Note('C3'), notationString='')
>>> segmentB = segment.Segment(bassNote=note.Note('D3'), notationString='4,3')
Here, an ordinary resolution is being executed, because segmentA is an ordinary Segment.
>>> consecutivePairs1 = segmentA.allCorrectConsecutivePossibilities(segmentB)
>>> consecutivePairsList1 = list(consecutivePairs1)
>>> len(consecutivePairsList1)
31
>>> consecutivePairsList1[29]
((<...G5>, <...G5>, <...E5>, <...C3>), (<...G5>, <...F5>, <...B4>, <...D3>))
Here, a special resolution is being executed, because segmentA below is a
special Segment.
>>> segmentA = segment.Segment(bassNote=note.Note('D3'), notationString='4,3')
>>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='')
>>> consecutivePairs2 = segmentA.allCorrectConsecutivePossibilities(segmentB)
>>> consecutivePairsList2 = list(consecutivePairs2)
>>> len(consecutivePairsList2)
6
>>> consecutivePairsList2[5]
((<...G5>, <...F5>, <...B4>, <...D3>), (<...G5>, <...E5>, <...C5>, <...C3>))
'''
if not (self.numParts == segmentB.numParts):
raise SegmentException('Two segments with unequal numParts cannot be compared.')
if not (self._maxPitch == segmentB._maxPitch):
raise SegmentException('Two segments with unequal maxPitch cannot be compared.')
self._specialResolutionRuleChecking = _compileRules(
self.specialResolutionRules(self.fbRules),
3
)
for (resolutionMethod, args) in self._specialResolutionRuleChecking[True]:
return resolutionMethod(segmentB, *args)
return self._resolveOrdinarySegment(segmentB)
# ------------------------------------------------------------------------------
# INTERNAL METHODS
def _isCorrectSinglePossibility(self, possibA: Possibility) -> bool:
'''
Takes in a possibility (possibA) from a segmentA (self) and returns True
if the possibility is correct given
:meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`
from segmentA.
'''
ruleChecking = self._singlePossibilityRuleChecking
for (method, isCorrect, args) in ruleChecking[True]:
if not (method(possibA, *args) == isCorrect):
return False
return True
def _isCorrectConsecutivePossibility(
self,
possibA: Possibility,
possibB: Possibility
) -> bool:
'''
Takes in a (possibA, possibB) pair from a segmentA (self) and segmentB,
and returns True if the pair is correct given
:meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`
from segmentA.
'''
ruleChecking = self._consecutivePossibilityRuleChecking
for (method, isCorrect, args) in ruleChecking[True]:
if not (method(possibA, possibB, *args) == isCorrect):
return False
return True
def _resolveOrdinarySegment(
self,
segmentB: Segment
) -> Iterator[tuple[Possibility, Possibility]]:
'''
An ordinary segment is defined as a segment which needs no special resolution, where the
segment does not spell out a special chord, for example, a dominant seventh.
Finds iterators through all possibA and possibB by calling
:meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`
on self (segmentA) and segmentB, respectively.
Returns an iterator through (possibA, possibB) pairs for which
:meth:`~music21.figuredBass.segment.Segment._isCorrectConsecutivePossibility` returns True.
>>> from music21.figuredBass import segment
'''
self._consecutivePossibilityRuleChecking = _compileRules(
self.consecutivePossibilityRules(self.fbRules))
correctA = self.allCorrectSinglePossibilities()
correctB = segmentB.allCorrectSinglePossibilities()
correctAB = itertools.product(correctA, correctB)
return filter(lambda possibAB: self._isCorrectConsecutivePossibility(possibA=possibAB[0],
possibB=possibAB[1]),
correctAB)
def _resolveSpecialSegment(
self,
segmentB: Segment,
specialResolutionMethods: list[tuple[t.Any, ...]]
) -> Iterator[tuple[Possibility, Possibility]]:
resolutionMethodExecutor = _compileRules(specialResolutionMethods, 3)
for (resolutionMethod, args) in resolutionMethodExecutor[True]:
iterables = []
for arg in args:
iterables.append(itertools.repeat(arg))
resolutions = map(resolutionMethod, self.allCorrectSinglePossibilities(), *iterables)
correctAB = zip(self.allCorrectSinglePossibilities(), resolutions)
correctAB = filter(lambda possibAB: possibility.pitchesWithinLimit(
possibA=possibAB[1],
maxPitch=segmentB._maxPitch),
correctAB)
if self.fbRules.applyConsecutivePossibRulesToResolution:
correctAB = filter(lambda possibAB: self._isCorrectConsecutivePossibility(
possibA=possibAB[0],
possibB=possibAB[1]),
correctAB)
if self.fbRules.applySinglePossibRulesToResolution:
segmentB._singlePossibilityRuleChecking = _compileRules(
segmentB.singlePossibilityRules(segmentB.fbRules))
correctAB = filter(lambda possibAB: segmentB._isCorrectSinglePossibility(
possibA=possibAB[1]),
correctAB)
return correctAB
raise SegmentException('No standard resolution available.')
[docs]
class OverlaidSegment(Segment):
'''
Class to allow Segments to be overlaid with non-chord notes.
'''
[docs]
def allSinglePossibilities(self) -> Iterator[Possibility]:
iterables = [self.allPitchesAboveBass] * (self.numParts - 1) # Parts 1 -> n-1
iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)]) # Part n
for (partNumber, partPitch) in self.fbRules._partPitchLimits:
iterables[partNumber - 1] = [pitch.Pitch(partPitch.nameWithOctave)]
return itertools.product(*iterables)
# HELPER METHODS
# --------------
[docs]
def getPitches(pitchNames: Iterable[str] = ('C', 'E', 'G'),
bassPitch: str|pitch.Pitch = 'C3',
maxPitch: str|pitch.Pitch = 'C8') -> list[pitch.Pitch]:
'''
Given a list of pitchNames, a bassPitch, and a maxPitch, returns a sorted list of
pitches between the two limits (inclusive) which correspond to items in pitchNames.
>>> from music21.figuredBass import segment
>>> pitches = segment.getPitches()
>>> print(', '.join([p.nameWithOctave for p in pitches]))
C3, E3, G3, C4, E4, G4, C5, E5, G5, C6, E6, G6, C7, E7, G7, C8
>>> pitches = segment.getPitches(['G', 'B', 'D', 'F'], bassPitch=pitch.Pitch('B2'))
>>> print(', '.join([p.nameWithOctave for p in pitches]))
B2, D3, F3, G3, B3, D4, F4, G4, B4, D5, F5, G5, B5, D6, F6, G6, B6, D7, F7, G7, B7
>>> pitches = segment.getPitches(['F##', 'A#', 'C#'], bassPitch=pitch.Pitch('A#3'))
>>> print(', '.join([p.nameWithOctave for p in pitches]))
A#3, C#4, F##4, A#4, C#5, F##5, A#5, C#6, F##6, A#6, C#7, F##7, A#7
The maxPitch must have an octave:
>>> segment.getPitches(maxPitch=pitch.Pitch('E'))
Traceback (most recent call last):
ValueError: maxPitch must be given an octave
'''
if isinstance(bassPitch, str):
bassPitch = pitch.Pitch(bassPitch)
if isinstance(maxPitch, str):
maxPitch = pitch.Pitch(maxPitch)
if maxPitch.octave is None:
raise ValueError('maxPitch must be given an octave')
iter1 = itertools.product(pitchNames, range(maxPitch.octave + 1))
iter2 = map(lambda x: pitch.Pitch(x[0] + str(x[1])), iter1)
iter3 = itertools.filterfalse(lambda samplePitch: bassPitch > samplePitch, iter2)
iter4 = itertools.filterfalse(lambda samplePitch: samplePitch > maxPitch, iter3)
allPitches = list(iter4)
allPitches.sort()
return allPitches
def _unpackSeventhChord(seventhChord: chord.Chord) -> list[pitch.Pitch|None]:
bass = seventhChord.bass()
root = seventhChord.root()
third = seventhChord.getChordStep(3)
fifth = seventhChord.getChordStep(5)
seventh = seventhChord.getChordStep(7)
seventhChordInfo = [bass, root, third, fifth, seventh]
return seventhChordInfo
def _unpackTriad(threePartChord: chord.Chord) -> list[pitch.Pitch|None]:
bass = threePartChord.bass()
root = threePartChord.root()
third = threePartChord.getChordStep(3)
fifth = threePartChord.getChordStep(5)
threePartChordInfo = [bass, root, third, fifth]
return threePartChordInfo
def _compileRules(
rulesList: list[tuple[t.Any, ...]],
maxLength: int = 4
) -> dict[bool, list[tuple[t.Any, ...]]]:
ruleChecking: dict[bool, list[tuple[t.Any, ...]]] = collections.defaultdict(list)
for ruleIndex in range(len(rulesList)):
args = []
if len(rulesList[ruleIndex]) == maxLength:
args = rulesList[ruleIndex][-1]
if maxLength == 4:
(shouldRunMethod, method, isCorrect) = rulesList[ruleIndex][0:3]
ruleChecking[shouldRunMethod].append((method, isCorrect, args))
elif maxLength == 3:
(shouldRunMethod, method) = rulesList[ruleIndex][0:2]
ruleChecking[shouldRunMethod].append((method, args))
return ruleChecking
[docs]
def printRules(rulesList: list[tuple[t.Any, ...]], maxLength: int = 4) -> None:
'''
Method which can print to the console rules inputted into
:meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`,
:meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`, and
:meth:`~music21.figuredBass.segment.Segment.specialResolutionRules`.
For the first two methods, maxLength is 4. For the third method, maxLength is 3.
OMIT_FROM_DOCS
maxLength is the maximum length of a rule, a rule which includes arguments,
because arguments are optional.
'''
MAX_SIZE = 30
for rule in rulesList:
if len(rule[1].__name__) >= MAX_SIZE:
MAX_SIZE = len(rule[1].__name__) + 2
def padMethod(m: Callable[..., t.Any]) -> str:
methodName = m.__name__[0:MAX_SIZE]
if len(methodName) < MAX_SIZE:
methodName += ' ' * (MAX_SIZE - len(methodName))
return methodName
methodStr = 'Method:' + ' ' * (MAX_SIZE - 7)
if maxLength == 4:
print(f'Will run: {methodStr}Keep solutions which return: Arguments:')
elif maxLength == 3:
print(f'Will run: {methodStr}Arguments:')
for ruleIndex in range(len(rulesList)):
ruleToPrint = None
args = []
if len(rulesList[ruleIndex]) == maxLength:
args = rulesList[ruleIndex][-1]
if not args:
argsString = 'None'
else:
argsString = ''
for itemIndex in range(len(args)):
argsString += str(args[itemIndex])
if not itemIndex == len(args) - 1:
argsString += ', '
if maxLength == 4:
(shouldRunMethod, method, isCorrect) = rulesList[ruleIndex][0:3]
method = padMethod(method)
ruleToPrint = f'{str(shouldRunMethod):11}{method}{str(isCorrect):30}{argsString}'
elif maxLength == 3:
(shouldRunMethod, method) = rulesList[ruleIndex][0:2]
method = padMethod(method)
ruleToPrint = f'{str(shouldRunMethod):11}{method}{argsString}'
print(ruleToPrint)
class SegmentException(exceptions21.Music21Exception):
pass
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
pass
if __name__ == '__main__':
import music21
music21.mainTest(Test)