Source code for music21.figuredBass.realizer
# ------------------------------------------------------------------------------
# Name: realizer.py
# Purpose: figured bass lines, consisting of notes
# and figures in a given key.
# Authors: Jose Cabal-Ugaz
#
# Copyright: Copyright © 2011 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module, the heart of fbRealizer, is all about realizing
a bass line of (bassNote, notationString)
pairs. All it takes to create well-formed realizations of a
bass line is a few lines of music21 code,
from start to finish. See :class:`~music21.figuredBass.realizer.FiguredBassLine` for more details.
>>> from music21.figuredBass import realizer
>>> fbLine = realizer.FiguredBassLine()
>>> fbLine.addElement(note.Note('C3'))
>>> fbLine.addElement(note.Note('D3'), '4,3')
>>> fbLine.addElement(note.Note('C3', quarterLength = 2.0))
>>> allSols = fbLine.realize()
>>> allSols.getNumSolutions()
30
>>> #_DOCS_SHOW allSols.generateRandomRealizations(14).show()
.. image:: images/figuredBass/fbRealizer_intro.*
:width: 500
The same can be accomplished by taking the notes and notations
from a :class:`~music21.stream.Stream`.
See :meth:`~music21.figuredBass.realizer.figuredBassFromStream` for more details.
>>> s = converter.parse('tinynotation: C4 D4_4,3 C2', makeNotation=False)
>>> fbLine = realizer.figuredBassFromStream(s)
>>> allSols2 = fbLine.realize()
>>> allSols2.getNumSolutions()
30
'''
from __future__ import annotations
import collections
import copy
import random
import typing as t
import unittest
from music21 import base
from music21 import chord
from music21 import clef
from music21 import exceptions21
from music21 import harmony
from music21 import key
from music21 import meter
from music21 import note
from music21 import pitch
from music21 import roman
from music21 import stream
from music21.common.types import OffsetQL
from music21.figuredBass import checker
from music21.figuredBass import notation
from music21.figuredBass import realizerScale
from music21.figuredBass import rules
from music21.figuredBass import segment
from music21.figuredBass.possibility import Possibility
[docs]
def figuredBassFromStream(streamPart: stream.Stream) -> FiguredBassLine:
# noinspection PyShadowingNames
'''
Takes a :class:`~music21.stream.Part` (or another :class:`~music21.stream.Stream` subclass)
and returns a :class:`~music21.figuredBass.realizer.FiguredBassLine` object whose bass notes
have notations taken from the lyrics in the source stream. This method along with the
:meth:`~music21.figuredBass.realizer.FiguredBassLine.realize` method provide the easiest
way of converting from a notated version of a figured bass (such as in a MusicXML file) to
a realized version of the same line.
>>> s = converter.parse('tinynotation: 4/4 C4 D8_6 E8_6 F4 G4_7 c1', makeNotation=False)
>>> fb = figuredBass.realizer.figuredBassFromStream(s)
>>> fb
<music21.figuredBass.realizer.FiguredBassLine object at 0x...>
>>> fbRules = figuredBass.rules.Rules()
>>> fbRules.partMovementLimits = [(1, 2), (2, 12), (3, 12)]
>>> fbRealization = fb.realize(fbRules)
>>> fbRealization.getNumSolutions()
13
>>> #_DOCS_SHOW fbRealization.generateRandomRealizations(8).show()
.. image:: images/figuredBass/fbRealizer_fbStreamPart.*
:width: 500
* Changed in v7.3: multiple figures in same lyric (e.g. '64') now supported.
'''
sf = streamPart.flatten()
sfn = sf.getElementsByClass(note.Note)
myKey: key.Key
if firstKey := sf[key.Key].first():
myKey = firstKey
elif firstKeySignature := sf[key.KeySignature].first():
myKey = firstKeySignature.asKey('major')
else:
myKey = key.Key('C')
ts: meter.TimeSignature
if first_ts := sf[meter.TimeSignature].first():
ts = first_ts
else:
ts = meter.TimeSignature('4/4')
fb = FiguredBassLine(myKey, ts)
if streamPart.hasMeasures():
m_first = streamPart.measure(0, indicesNotNumbers=True)
if t.TYPE_CHECKING:
assert m_first is not None
paddingLeft = m_first.paddingLeft
if paddingLeft != 0.0:
fb._paddingLeft = paddingLeft
# noinspection PyShadowingNames
def updateAnnotationString(annotationString: str, inputText: str) -> str:
'''
Continue building the working `annotationString` based on some `inputText`
that has yet to be processed. Called recursively until `inputText` is exhausted
or contains unexpected characters.
'''
# "64" and "#6#42" but not necessarily "4-3" or "sus4"
stop_index_exclusive: int = 0
if inputText[0] in '+#bn' and len(inputText) > 1 and inputText[1].isnumeric():
stop_index_exclusive = 2
elif inputText[0].isnumeric():
stop_index_exclusive = 1
else:
# quit
stop_index_exclusive = 1000
annotationString += inputText[:stop_index_exclusive]
# Is there more?
if inputText[stop_index_exclusive:]:
annotationString += ', '
annotationString = updateAnnotationString(
annotationString, inputText[stop_index_exclusive:])
return annotationString
for n in sfn:
if n.lyrics:
annotationString: str = ''
for i, lyric_line in enumerate(n.lyrics):
if lyric_line.text in (None, ''):
continue
if ',' in lyric_line.text:
# presence of comma suggests we already have a separated
# sequence of figures, e.g. "#6, 4, 2"
annotationString = lyric_line.text
else:
# parse it more carefully
annotationString = updateAnnotationString(annotationString, lyric_line.text)
if i + 1 < len(n.lyrics):
annotationString += ', '
fb.addElement(n, annotationString)
else:
fb.addElement(n)
return fb
[docs]
def addLyricsToBassNote(bassNote: note.Note, notationString: str = '') -> None:
'''
Takes in a bassNote and a corresponding notationString as arguments.
Adds the parsed notationString as lyrics to the bassNote, which is
useful when displaying the figured bass in external software.
>>> from music21.figuredBass import realizer
>>> n1 = note.Note('G3')
>>> realizer.addLyricsToBassNote(n1, '6,4')
>>> n1.lyrics[0].text
'6'
>>> n1.lyrics[1].text
'4'
>>> #_DOCS_SHOW n1.show()
.. image:: images/figuredBass/fbRealizer_lyrics.*
:width: 100
'''
bassNote.lyrics = []
n = notation.Notation(notationString)
if not n.figureStrings:
return
maxLength = max([len(fs) for fs in n.figureStrings])
for fs in n.figureStrings:
spacesInFront = ''
for i in range(maxLength - len(fs)):
spacesInFront += ' '
bassNote.addLyric(spacesInFront + fs, applyRaw=True)
[docs]
class FiguredBassLine:
'''
A FiguredBassLine is an interface for realization of a line of (bassNote, notationString) pairs.
Currently, only 1:1 realization is supported, meaning that every bassNote is realized and the
:attr:`~music21.note.GeneralNote.quarterLength` or duration of a realization above a bassNote
is identical to that of the bassNote.
`inKey` defaults to C major.
`inTime` defaults to 4/4.
>>> from music21.figuredBass import realizer
>>> fbLine = realizer.FiguredBassLine(key.Key('B'), meter.TimeSignature('3/4'))
>>> fbLine.inKey
<music21.key.Key of B major>
>>> fbLine.inTime
<music21.meter.TimeSignature 3/4>
'''
_DOC_ORDER = ['addElement', 'generateBassLine', 'realize']
_DOC_ATTR: dict[str, str] = {
'inKey': '''
A :class:`~music21.key.Key` which implies a scale value,
scale mode, and key signature for a
:class:`~music21.figuredBass.realizerScale.FiguredBassScale`.
''',
'inTime': '''
A :class:`~music21.meter.TimeSignature` which specifies the
time signature of realizations outputted to a
:class:`~music21.stream.Score`.
''',
}
def __init__(self,
inKey: key.Key|None = None,
inTime: meter.TimeSignature|None = None) -> None:
if inKey is None:
inKey = key.Key('C')
if inTime is None:
inTime = meter.TimeSignature('4/4')
self.inKey = inKey
self.inTime = inTime
self._paddingLeft: OffsetQL = 0.0
self._overlaidParts = stream.Part()
tonic = t.cast(pitch.Pitch, inKey.pitchFromDegree(1))
self._fbScale = realizerScale.FiguredBassScale(tonic, inKey.mode)
self._fbList: list[
tuple[note.Note, str|None] | harmony.ChordSymbol | roman.RomanNumeral
] = []
[docs]
def addElement(
self,
bassObject: note.Note|harmony.ChordSymbol|roman.RomanNumeral,
notationString: str|None = None,
) -> None:
'''
Use this method to add (bassNote, notationString) pairs to the bass line. Elements
are realized in the order they are added.
>>> from music21.figuredBass import realizer
>>> fbLine = realizer.FiguredBassLine(key.Key('B'), meter.TimeSignature('3/4'))
>>> fbLine.addElement(note.Note('B2'))
>>> fbLine.addElement(note.Note('C#3'), '6')
>>> fbLine.addElement(note.Note('D#3'), '6')
>>> #_DOCS_SHOW fbLine.generateBassLine().show()
.. image:: images/figuredBass/fbRealizer_bassLine.*
:width: 200
OMIT_FROM_DOCS
>>> fbLine = realizer.FiguredBassLine(key.Key('C'), meter.TimeSignature('4/4'))
>>> fbLine.addElement(harmony.ChordSymbol('C'))
>>> fbLine.addElement(harmony.ChordSymbol('G'))
>>> fbLine = realizer.FiguredBassLine(key.Key('C'), meter.TimeSignature('4/4'))
>>> fbLine.addElement(roman.RomanNumeral('I'))
>>> fbLine.addElement(roman.RomanNumeral('V'))
'''
bassObject.editorial.notationString = notationString
if isinstance(bassObject, note.Note):
self._fbList.append((bassObject, notationString)) # a bass note, and a notationString
addLyricsToBassNote(bassObject, notationString or '')
# ---------- Added to accommodate harmony.ChordSymbol and roman.RomanNumeral objects ---
elif isinstance(bassObject, (roman.RomanNumeral, harmony.ChordSymbol)):
self._fbList.append(bassObject) # a roman Numeral object
else:
raise FiguredBassLineException(
'Not a valid bassObject (only note.Note, '
f'harmony.ChordSymbol, and roman.RomanNumeral supported) was {bassObject!r}'
)
[docs]
def generateBassLine(self) -> stream.Part:
'''
Generates the bass line as a :class:`~music21.stream.Score`.
>>> from music21.figuredBass import realizer
>>> fbLine = realizer.FiguredBassLine(key.Key('B'), meter.TimeSignature('3/4'))
>>> fbLine.addElement(note.Note('B2'))
>>> fbLine.addElement(note.Note('C#3'), '6')
>>> fbLine.addElement(note.Note('D#3'), '6')
>>> #_DOCS_SHOW fbLine.generateBassLine().show()
.. image:: images/figuredBass/fbRealizer_bassLine.*
:width: 200
>>> sBach = corpus.parse('bach/bwv307')
>>> sBach.parts.last().measure(0).show('text')
{0.0} ...
{0.0} <music21.clef.BassClef>
{0.0} <music21.key.Key of B- major>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.note.Note B->
{0.5} <music21.note.Note C>
>>> fbLine = realizer.figuredBassFromStream(sBach.parts.last())
>>> fbLine.generateBassLine().measure(1).show('text')
{0.0} <music21.clef.BassClef>
{0.0} <music21.key.KeySignature of 2 flats>
{0.0} <music21.meter.TimeSignature 4/4>
{3.0} <music21.note.Note B->
{3.5} <music21.note.Note C>
'''
bassLine = stream.Part()
bassLine.append(clef.BassClef())
bassLine.append(key.KeySignature(self.inKey.sharps))
bassLine.append(copy.deepcopy(self.inTime))
r = None
if self._paddingLeft != 0.0:
r = note.Rest(quarterLength=self._paddingLeft)
bassLine.append(r)
# generateBassLine is only used for note/notationString pairs, not harmony objects
fbPairs = t.cast('list[tuple[note.Note, str|None]]', self._fbList)
for (bassNote, unused_notationString) in fbPairs:
bassLine.append(bassNote)
bl2 = bassLine.makeNotation(inPlace=False, cautionaryNotImmediateRepeat=False)
if r is not None:
m0 = bl2.getElementsByClass(stream.Measure).first()
m0.remove(m0.getElementsByClass(note.Rest).first())
m0.padAsAnacrusis()
return bl2
[docs]
def retrieveSegments(
self,
fbRules: rules.Rules|None = None,
numParts: int = 4,
maxPitch: pitch.Pitch|None = None,
) -> list[segment.Segment]:
'''
generates the segmentList from an fbList, including any overlaid Segments
if fbRules is None, creates a new rules.Rules() object
if maxPitch is None, uses pitch.Pitch('B5')
'''
if fbRules is None:
fbRules = rules.Rules()
if maxPitch is None:
maxPitch = pitch.Pitch('B5')
segmentList: list[segment.Segment] = []
bassLine = self.generateBassLine()
if len(self._overlaidParts) >= 1:
self._overlaidParts.append(bassLine)
currentMapping = checker.extractHarmonies(self._overlaidParts)
else:
currentMapping = checker.createOffsetMapping(bassLine)
allKeys = sorted(currentMapping.keys())
bassNotes = bassLine.flatten().notes
bassNoteIndex = 0
previousBassNote = bassNotes[bassNoteIndex]
bassNote = t.cast(note.Note, currentMapping[allKeys[0]][-1])
previousSegment = segment.OverlaidSegment(bassNote, bassNote.editorial.notationString,
self._fbScale,
fbRules, numParts, maxPitch)
previousSegment.quarterLength = previousBassNote.quarterLength
segmentList.append(previousSegment)
for k in allKeys[1:]:
(startTime, unused_endTime) = k
bassNote = t.cast(note.Note, currentMapping[k][-1])
currentSegment = segment.OverlaidSegment(bassNote, bassNote.editorial.notationString,
self._fbScale,
fbRules, numParts, maxPitch)
for partNumber in range(1, len(currentMapping[k])):
upperPitch = t.cast(note.Note, currentMapping[k][partNumber - 1])
currentSegment.fbRules._partPitchLimits.append((partNumber, upperPitch))
if startTime == previousBassNote.offset + previousBassNote.quarterLength:
bassNoteIndex += 1
previousBassNote = bassNotes[bassNoteIndex]
currentSegment.quarterLength = previousBassNote.quarterLength
else:
for partNumber in range(len(currentMapping[k]), numParts + 1):
previousSegment.fbRules._partsToCheck.append(partNumber)
# Fictitious, representative only for harmonies preserved
# with addition of melody or melodies
currentSegment.quarterLength = 0.0
segmentList.append(currentSegment)
previousSegment = currentSegment
return segmentList
[docs]
def overlayPart(self, music21Part: stream.Part) -> None:
self._overlaidParts.append(music21Part)
[docs]
def realize(
self,
fbRules: rules.Rules|None = None,
numParts: int = 4,
maxPitch: pitch.Pitch|None = None,
) -> Realization:
# noinspection PyShadowingNames
'''
Creates a :class:`~music21.figuredBass.segment.Segment`
for each (bassNote, notationString) pair
added using :meth:`~music21.figuredBass.realizer.FiguredBassLine.addElement`.
Each Segment is associated
with the :class:`~music21.figuredBass.rules.Rules` object provided, meaning that rules are
universally applied across all Segments. The number of parts in a realization
(including the bass) can be controlled through numParts, and the maximum pitch can
likewise be controlled through maxPitch.
Returns a :class:`~music21.figuredBass.realizer.Realization`.
If this method is called without having provided any (bassNote, notationString) pairs,
a FiguredBassLineException is raised. If only one pair is provided, the Realization will
contain :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`
for the one note.
if `fbRules` is None, creates a new rules.Rules() object
if `maxPitch` is None, uses pitch.Pitch('B5')
>>> from music21.figuredBass import realizer
>>> from music21.figuredBass import rules
>>> fbLine = realizer.FiguredBassLine(key.Key('B'), meter.TimeSignature('3/4'))
>>> fbLine.addElement(note.Note('B2'))
>>> fbLine.addElement(note.Note('C#3'), '6')
>>> fbLine.addElement(note.Note('D#3'), '6')
>>> fbRules = rules.Rules()
>>> r1 = fbLine.realize(fbRules)
>>> r1.getNumSolutions()
208
>>> fbRules.forbidVoiceOverlap = False
>>> r2 = fbLine.realize(fbRules)
>>> r2.getNumSolutions()
7908
OMIT_FROM_DOCS
>>> fbLine3 = realizer.FiguredBassLine(key.Key('C'), meter.TimeSignature('2/4'))
>>> h1 = harmony.ChordSymbol('C')
>>> h1.bass().octave = 4
>>> fbLine3.addElement(h1)
>>> h2 = harmony.ChordSymbol('G')
>>> h2.bass().octave = 4
>>> fbLine3.addElement(h2)
>>> r3 = fbLine3.realize()
>>> r3.getNumSolutions()
13
>>> fbLine4 = realizer.FiguredBassLine(key.Key('C'), meter.TimeSignature('2/4'))
>>> fbLine4.addElement(roman.RomanNumeral('I'))
>>> fbLine4.addElement(roman.RomanNumeral('IV'))
>>> r4 = fbLine4.realize()
>>> r4.getNumSolutions()
13
'''
if fbRules is None:
fbRules = rules.Rules()
if maxPitch is None:
maxPitch = pitch.Pitch('B5')
segmentList: list[segment.Segment] = []
listOfHarmonyObjects = False
for item in self._fbList:
if isinstance(item, tuple):
# a (bassNote, notationString) pair, not a harmony object
continue
# Added to accommodate harmony.ChordSymbol and roman.RomanNumeral objects
if isinstance(item, (roman.RomanNumeral, harmony.ChordSymbol)):
listOfHarmonyObjects = True
break
if listOfHarmonyObjects:
harmonyObjects = t.cast(
'list[harmony.ChordSymbol | roman.RomanNumeral]', self._fbList)
for harmonyObject in harmonyObjects:
listOfPitchesJustNames = []
for thisPitch in harmonyObject.pitches:
listOfPitchesJustNames.append(thisPitch.name)
# remove duplicates just in case
d = {}
for x in listOfPitchesJustNames:
d[x] = x
outputList = d.values()
def g(y: float) -> float:
return y if y != 0.0 else 1.0
passedNote = note.Note(harmonyObject.bass().nameWithOctave,
quarterLength=g(harmonyObject.duration.quarterLength))
correspondingSegment = segment.Segment(bassNote=passedNote,
fbScale=self._fbScale,
fbRules=fbRules,
numParts=numParts,
maxPitch=maxPitch,
listOfPitches=outputList)
correspondingSegment.quarterLength = g(harmonyObject.duration.quarterLength)
segmentList.append(correspondingSegment)
# ---------- Original code - Accommodates a tuple (figured bass) --------
else:
segmentList = self.retrieveSegments(fbRules, numParts, maxPitch)
if len(segmentList) >= 2:
for segmentIndex in range(len(segmentList) - 1):
segmentA = segmentList[segmentIndex]
segmentB = segmentList[segmentIndex + 1]
correctAB = segmentA.allCorrectConsecutivePossibilities(segmentB)
segmentA.movements = collections.defaultdict(list)
listAB = list(correctAB)
for (possibA, possibB) in listAB:
segmentA.movements[possibA].append(possibB)
self._trimAllMovements(segmentList)
elif len(segmentList) == 1:
segmentA = segmentList[0]
segmentA.correctA = list(segmentA.allCorrectSinglePossibilities())
elif not segmentList:
raise FiguredBassLineException('No (bassNote, notationString) pairs to realize.')
return Realization(realizedSegmentList=segmentList, inKey=self.inKey,
inTime=self.inTime, overlaidParts=self._overlaidParts[0:-1],
paddingLeft=self._paddingLeft)
def _trimAllMovements(self, segmentList: list[segment.Segment]) -> bool|None:
'''
Each :class:`~music21.figuredBass.segment.Segment` which resolves to another
defines a list of movements, nextMovements. Keys for nextMovements are correct
single possibilities of the current Segment. For a given key, a value is a list
of correct single possibilities in the subsequent Segment representing acceptable
movements between the two. There may be movements in a string of Segments which
directly or indirectly lead nowhere. This method is designed to be called on
a list of Segments **after** movements are found, as happens in
:meth:`~music21.figuredBass.realizer.FiguredBassLine.realize`.
'''
if len(segmentList) == 1 or len(segmentList) == 2:
return True
elif len(segmentList) >= 3:
segmentList.reverse()
# gets this wrong # pylint: disable=cell-var-from-loop
movementsAB: dict[Possibility, list[Possibility]] = {}
for segmentIndex in range(1, len(segmentList) - 1):
movementsAB = segmentList[segmentIndex + 1].movements
movementsBC = segmentList[segmentIndex].movements
# eliminated = []
for (possibB, possibCList) in list(movementsBC.items()):
if not possibCList:
del movementsBC[possibB]
for (possibA, possibBList) in list(movementsAB.items()):
movementsAB[possibA] = list(
filter(lambda possibBB: (possibBB in movementsBC), possibBList))
for (possibA, possibBList) in list(movementsAB.items()):
if not possibBList:
del movementsAB[possibA]
segmentList.reverse()
return True
[docs]
class Realization:
'''
Returned by :class:`~music21.figuredBass.realizer.FiguredBassLine` after calling
:meth:`~music21.figuredBass.realizer.FiguredBassLine.realize`. Allows for the
generation of realizations as a :class:`~music21.stream.Score`.
* See the :mod:`~music21.figuredBass.examples` module for examples on the generation
of realizations.
* A possibility progression is a valid progression through a string of
:class:`~music21.figuredBass.segment.Segment` instances.
See :mod:`~music21.figuredBass.possibility` for more details on possibilities.
'''
_DOC_ORDER = ['getNumSolutions', 'generateRandomRealization',
'generateRandomRealizations', 'generateAllRealizations',
'getAllPossibilityProgressions', 'getRandomPossibilityProgression',
'generateRealizationFromPossibilityProgression']
_DOC_ATTR: dict[str, str] = {
'keyboardStyleOutput': '''
True by default. If True, generated realizations
are represented in keyboard style, with two staves. If False,
realizations are represented in chorale style with n staves,
where n is the number of parts. SATB if n = 4.''',
}
# Populated in __init__ from FiguredBassLine.realize()'s keyword outputs.
# Declared here for typing; **fbLineOutputs stays untyped for Sphinx's sake.
_segmentList: list[segment.Segment]
_inKey: key.Key
_keySig: key.KeySignature
_inTime: meter.TimeSignature
_overlaidParts: list[base.Music21Object]
_paddingLeft: OffsetQL
def __init__(self, **fbLineOutputs: t.Any) -> None:
# fbLineOutputs always will have three elements, checks are for sphinx documentation only.
if 'realizedSegmentList' in fbLineOutputs:
self._segmentList = fbLineOutputs['realizedSegmentList']
if 'inKey' in fbLineOutputs:
self._inKey = fbLineOutputs['inKey']
self._keySig = key.KeySignature(self._inKey.sharps)
if 'inTime' in fbLineOutputs:
self._inTime = fbLineOutputs['inTime']
if 'overlaidParts' in fbLineOutputs:
self._overlaidParts = fbLineOutputs['overlaidParts']
if 'paddingLeft' in fbLineOutputs:
self._paddingLeft = fbLineOutputs['paddingLeft']
self.keyboardStyleOutput = True
[docs]
def getNumSolutions(self) -> int:
'''
Returns the number of solutions (unique realizations) to a Realization by calculating
the total number of paths through a string of :class:`~music21.figuredBass.segment.Segment`
movements. This is faster and more efficient than compiling each unique realization into a
list, adding it to a master list, and then taking the length of the master list.
>>> from music21.figuredBass import examples
>>> fbLine = examples.exampleB()
>>> fbRealization = fbLine.realize()
>>> fbRealization.getNumSolutions()
422
>>> fbLine2 = examples.exampleC()
>>> fbRealization2 = fbLine2.realize()
>>> fbRealization2.getNumSolutions()
833
'''
if len(self._segmentList) == 1:
return len(self._segmentList[0].correctA)
# What if there's only one (bassNote, notationString)?
self._segmentList.reverse()
pathList: dict[Possibility, int] = {}
for segmentIndex in range(1, len(self._segmentList)):
segmentA = self._segmentList[segmentIndex]
newPathList: dict[Possibility, int] = {}
if not pathList:
for possibA in segmentA.movements:
newPathList[possibA] = len(segmentA.movements[possibA])
else:
for possibA in segmentA.movements:
prevValue = 0
for possibB in segmentA.movements[possibA]:
prevValue += pathList[possibB]
newPathList[possibA] = prevValue
pathList = newPathList
numSolutions = 0
for possibA in pathList:
numSolutions += pathList[possibA]
self._segmentList.reverse()
return numSolutions
[docs]
def getAllPossibilityProgressions(self) -> list[list[Possibility]]:
'''
Compiles each unique possibility progression, adding
it to a master list. Returns the master list.
.. warning:: This method is unoptimized, and may take a prohibitive amount
of time for a Realization which has more than 200,000 solutions.
'''
progressions = []
if len(self._segmentList) == 1:
for possibA in self._segmentList[0].correctA:
progressions.append([possibA])
return progressions
currMovements = self._segmentList[0].movements
for possibA in currMovements:
possibBList = currMovements[possibA]
for possibB in possibBList:
progressions.append([possibA, possibB])
for segmentIndex in range(1, len(self._segmentList) - 1):
currMovements = self._segmentList[segmentIndex].movements
for unused_progressionIndex in range(len(progressions)):
progression = progressions.pop(0)
possibB = progression[-1]
for possibC in currMovements[possibB]:
newProgression = copy.copy(progression)
newProgression.append(possibC)
progressions.append(newProgression)
return progressions
[docs]
def getRandomPossibilityProgression(self) -> list[Possibility]:
'''
Returns a random unique possibility progression.
'''
progression: list[Possibility] = []
if len(self._segmentList) == 1:
possibA = random.sample(self._segmentList[0].correctA, 1)[0]
progression.append(possibA)
return progression
currMovements = self._segmentList[0].movements
if self.getNumSolutions() == 0:
raise FiguredBassLineException('Zero solutions')
prevPossib = random.sample(list(currMovements.keys()), 1)[0]
progression.append(prevPossib)
for segmentIndex in range(len(self._segmentList) - 1):
currMovements = self._segmentList[segmentIndex].movements
nextPossib = random.sample(currMovements[prevPossib], 1)[0]
progression.append(nextPossib)
prevPossib = nextPossib
return progression
[docs]
def generateRealizationFromPossibilityProgression(
self,
possibilityProgression: list[Possibility],
) -> stream.Score:
'''
Generates a realization as a :class:`~music21.stream.Score` given a possibility progression.
'''
sol = stream.Score()
bassLine = stream.Part()
bassLine.append([copy.deepcopy(self._keySig), copy.deepcopy(self._inTime)])
r = None
if self._paddingLeft != 0.0:
r = note.Rest(quarterLength=self._paddingLeft)
bassLine.append(copy.deepcopy(r))
if self.keyboardStyleOutput:
rightHand = stream.Part()
sol.insert(0.0, rightHand)
rightHand.append([copy.deepcopy(self._keySig), copy.deepcopy(self._inTime)])
if r is not None:
rightHand.append(copy.deepcopy(r))
for segmentIndex in range(len(self._segmentList)):
possibA = possibilityProgression[segmentIndex]
bassNote = self._segmentList[segmentIndex].bassNote
bassLine.append(copy.deepcopy(bassNote))
rhPitches = possibA[0:-1]
rhChord = chord.Chord(rhPitches)
rhChord.quarterLength = self._segmentList[segmentIndex].quarterLength
rightHand.append(rhChord)
rightHand.insert(0.0, clef.TrebleClef())
rightHand.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False)
if r is not None:
rightHand[0].pop(3)
rightHand[0].padAsAnacrusis()
else: # Chorale-style output
upperParts: list[stream.Part] = []
for _partNumber in range(len(possibilityProgression[0]) - 1):
fbPart = stream.Part()
sol.insert(0.0, fbPart)
fbPart.append([copy.deepcopy(self._keySig), copy.deepcopy(self._inTime)])
if r is not None:
fbPart.append(copy.deepcopy(r))
upperParts.append(fbPart)
for segmentIndex in range(len(self._segmentList)):
possibA = possibilityProgression[segmentIndex]
bassNote = self._segmentList[segmentIndex].bassNote
bassLine.append(copy.deepcopy(bassNote))
for partNumber in range(len(possibA) - 1):
n1 = note.Note(possibA[partNumber])
n1.quarterLength = self._segmentList[segmentIndex].quarterLength
upperParts[partNumber].append(n1)
for upperPart in upperParts:
c = clef.bestClef(upperPart, allowTreble8vb=True, recurse=True)
upperPart.insert(0.0, c)
upperPart.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False)
if r is not None:
upperPart[0].pop(3)
upperPart[0].padAsAnacrusis()
bassLine.insert(0.0, clef.BassClef())
bassLine.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False)
if r is not None:
bassLine[0].pop(3)
bassLine[0].padAsAnacrusis()
sol.insert(0.0, bassLine)
return sol
[docs]
def generateAllRealizations(self) -> stream.Score:
'''
Generates all unique realizations as a :class:`~music21.stream.Score`.
.. warning:: This method is unoptimized, and may take a prohibitive amount
of time for a Realization which has more than 100 solutions.
'''
allSols = stream.Score()
possibilityProgressions = self.getAllPossibilityProgressions()
if not possibilityProgressions:
raise FiguredBassLineException('Zero solutions')
sol0 = self.generateRealizationFromPossibilityProgression(possibilityProgressions[0])
for music21Part in sol0:
allSols.append(music21Part)
for possibIndex in range(1, len(possibilityProgressions)):
solX = self.generateRealizationFromPossibilityProgression(
possibilityProgressions[possibIndex])
for partIndex in range(len(solX)):
for music21Measure in solX[partIndex]:
allSols[partIndex].append(music21Measure)
return allSols
[docs]
def generateRandomRealization(self) -> stream.Score:
'''
Generates a random unique realization as a :class:`~music21.stream.Score`.
'''
possibilityProgression = self.getRandomPossibilityProgression()
return self.generateRealizationFromPossibilityProgression(possibilityProgression)
[docs]
def generateRandomRealizations(self, amountToGenerate: int = 20) -> stream.Score:
'''
Generates *amountToGenerate* unique realizations as a :class:`~music21.stream.Score`.
.. warning:: This method is unoptimized, and may take a prohibitive amount
of time if amountToGenerate is more than 100.
'''
if amountToGenerate > self.getNumSolutions():
return self.generateAllRealizations()
allSols = stream.Score()
sol0 = self.generateRandomRealization()
for music21Part in sol0:
allSols.append(music21Part)
for unused_counter_solution in range(1, amountToGenerate):
solX = self.generateRandomRealization()
for partIndex in range(len(solX)):
for music21Measure in solX[partIndex]:
allSols[partIndex].append(music21Measure)
return allSols
_DOC_ORDER = [figuredBassFromStream, addLyricsToBassNote,
FiguredBassLine, Realization]
class FiguredBassLineException(exceptions21.Music21Exception):
pass
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testMultipleFiguresInLyric(self) -> None:
from music21 import converter
s = converter.parse('tinynotation: 4/4 C4 F4 G4_64 G4 C1', makeNotation=False)
third_note = s[note.Note][2]
self.assertEqual(third_note.lyric, '64')
unused_fb = figuredBassFromStream(s)
self.assertEqual(third_note.editorial.notationString, '6, 4')
third_note.lyric = '#6#42'
unused_fb = figuredBassFromStream(s)
self.assertEqual(third_note.editorial.notationString, '#6, #4, 2')
third_note.lyric = '#64#2'
unused_fb = figuredBassFromStream(s)
self.assertEqual(third_note.editorial.notationString, '#6, 4, #2')
# original case
third_note.lyric = '6\n4'
unused_fb = figuredBassFromStream(s)
self.assertEqual(third_note.editorial.notationString, '6, 4')
# single accidental
for single_symbol in '+#bn':
with self.subTest(single_symbol=single_symbol):
third_note.lyric = single_symbol
unused_fb = figuredBassFromStream(s)
self.assertEqual(third_note.editorial.notationString, single_symbol)
if __name__ == '__main__':
import music21
music21.mainTest(Test)