diff --git a/music21/figuredBass/checker.py b/music21/figuredBass/checker.py index cf6cf36f0..f92061df5 100644 --- a/music21/figuredBass/checker.py +++ b/music21/figuredBass/checker.py @@ -10,20 +10,37 @@ import collections import copy +import typing as t import unittest +from music21 import note from music21 import pitch from music21 import stream from music21 import voiceLeading from music21.common.numberTools import opFrac -from music21.figuredBass import possibility +from music21.common.types import OffsetQL +from music21.figuredBass.possibility import PitchQuartetToBool from music21.exceptions21 import Music21Exception +if t.TYPE_CHECKING: + from collections.abc import Callable + +# A vertical sonority observed from a score for voice-leading checking: one +# element per part, each a Pitch or the rest placeholder 'RT' returned by +# generalNoteToPitch. Distinct from possibility.Possibility, which is pitch-only +# and represents a candidate realization rather than an observed sonority. +type PossibilityWithRests = tuple[pitch.Pitch | t.Literal['RT'], ...] +# (offset, endTime) delimiting a voice-leading moment. +type OffsetEndTime = tuple[OffsetQL, OffsetQL] +# (partNumberA, partNumberB) one-indexed voice pair forming a violation. +type PartPair = tuple[int, int] + + # ------------------------------------------------------------------------------ # Parsing scores into voice leading moments (a.k.a. harmonies) -def getVoiceLeadingMoments(music21Stream): +def getVoiceLeadingMoments(music21Stream: stream.Stream) -> stream.Score: ''' Takes in a :class:`~music21.stream.Stream` and returns a :class:`~music21.stream.Score` of the :class:`~music21.stream.Stream` broken up into its voice leading moments. @@ -43,10 +60,14 @@ def getVoiceLeadingMoments(music21Stream): :width: 700 ''' allHarmonies = extractHarmonies(music21Stream) - allParts = music21Stream.getElementsByClass(stream.Part).stream() - newParts = [allParts[i].flatten().getElementsNotOfClass('GeneralNote').stream() - for i in range(len(allParts))] - paddingLeft = allParts[0].getElementsByClass(stream.Measure).first().paddingLeft + allParts: stream.Stream[stream.Part] = music21Stream.getElementsByClass(stream.Part).stream() + newParts: list[stream.Stream] = [ + allParts[i].flatten().getElementsNotOfClass('GeneralNote').stream() + for i in range(len(allParts)) + ] + firstMeasure = t.cast('stream.Measure', + allParts[0].getElementsByClass(stream.Measure).first()) + paddingLeft = firstMeasure.paddingLeft for (offsets, notes) in sorted(allHarmonies.items()): (initOffset, endTime) = offsets for genNoteIndex in range(len(notes)): @@ -65,7 +86,9 @@ def getVoiceLeadingMoments(music21Stream): return newScore -def extractHarmonies(music21Stream): +def extractHarmonies( + music21Stream: stream.Stream +) -> dict[OffsetEndTime, list[note.GeneralNote]]: # noinspection PyShadowingNames ''' Takes in a :class:`~music21.stream.Stream` and returns a dictionary whose values @@ -112,7 +135,9 @@ def extractHarmonies(music21Stream): return allHarmonies -def createOffsetMapping(music21Part): +def createOffsetMapping( + music21Part: stream.Part +) -> dict[OffsetEndTime, list[note.GeneralNote]]: ''' Creates an initial offset mapping of a :class:`~music21.stream.Part`. @@ -135,7 +160,7 @@ def createOffsetMapping(music21Part): (9.0, 11.0) [] (11.0, 12.0) [ ] ''' - currentMapping = collections.defaultdict(list) + currentMapping: dict[OffsetEndTime, list[note.GeneralNote]] = collections.defaultdict(list) for music21GeneralNote in music21Part.flatten().notesAndRests: initOffset = music21GeneralNote.offset endTime = initOffset + music21GeneralNote.quarterLength @@ -143,7 +168,10 @@ def createOffsetMapping(music21Part): return currentMapping -def correlateHarmonies(currentMapping, music21Part): +def correlateHarmonies( + currentMapping: dict[OffsetEndTime, list[note.GeneralNote]], + music21Part: stream.Part +) -> dict[OffsetEndTime, list[note.GeneralNote]]: # noinspection PyShadowingNames ''' Adds a new :class:`~music21.stream.Part` to an existing offset mapping. @@ -173,7 +201,7 @@ def correlateHarmonies(currentMapping, music21Part): (10.5, 11.0) [ ] (11.0, 12.0) [ ] ''' - newMapping = {} + newMapping: dict[OffsetEndTime, list[note.GeneralNote]] = {} for offsets in sorted(currentMapping.keys()): (initOffset, endTime) = offsets @@ -199,7 +227,12 @@ def correlateHarmonies(currentMapping, music21Part): # Generic functions for checking for composition rule violations in streams -def checkSinglePossibilities(music21Stream, functionToApply, color='#FF0000', debug=False): +def checkSinglePossibilities( + music21Stream: stream.Score, + functionToApply: Callable[[PossibilityWithRests], list[PartPair]], + color: str | None = '#FF0000', + debug: bool = False +) -> None: # noinspection PyShadowingNames ''' Takes in a :class:`~music21.stream.Score` and a functionToApply which takes in a possibility @@ -231,7 +264,7 @@ def checkSinglePossibilities(music21Stream, functionToApply, color='#FF0000', de .. image:: images/figuredBass/corelli_voiceCrossing.* :width: 700 ''' - debugInfo = [] + debugInfo: list[str] = [] if debug is True: debugInfo.append('Function To Apply: ' + functionToApply.__name__) debugInfo.append(f"{'(Offset, End Time):'!s:25}Part Numbers:") @@ -239,7 +272,7 @@ def checkSinglePossibilities(music21Stream, functionToApply, color='#FF0000', de allHarmonies = sorted(list(extractHarmonies(music21Stream).items())) allParts = [p.flatten() for p in music21Stream.getElementsByClass(stream.Part)] for (offsets, notes) in allHarmonies: - vlm = [generalNoteToPitch(n) for n in notes] + vlm = tuple(generalNoteToPitch(n) for n in notes) vlm_violations = functionToApply(vlm) initOffset = offsets[0] for partNumberTuple in vlm_violations: @@ -260,7 +293,12 @@ def checkSinglePossibilities(music21Stream, functionToApply, color='#FF0000', de print(lineInfo) -def checkConsecutivePossibilities(music21Stream, functionToApply, color='#FF0000', debug=False): +def checkConsecutivePossibilities( + music21Stream: stream.Score, + functionToApply: Callable[[PossibilityWithRests, PossibilityWithRests], list[PartPair]], + color: str | None = '#FF0000', + debug: bool = False +) -> None: # noinspection PyShadowingNames ''' Takes in a :class:`~music21.stream.Score` and a functionToApply which takes in two consecutive @@ -294,7 +332,7 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color='#FF0000 .. image:: images/figuredBass/checker_parallelOctaves.* :width: 700 ''' - debugInfo = [] + debugInfo: list[str] = [] if debug is True: debugInfo.append('Function To Apply: ' + functionToApply.__name__) debugInfo.append('(Offset A, End Time A): (Offset B, End Time B): Part Numbers:') @@ -302,11 +340,11 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color='#FF0000 allHarmonies = sorted(extractHarmonies(music21Stream).items()) allParts = [p.flatten() for p in music21Stream.getElementsByClass(stream.Part)] (previousOffsets, previousNotes) = allHarmonies[0] - vlmA = [generalNoteToPitch(n) for n in previousNotes] + vlmA = tuple(generalNoteToPitch(n) for n in previousNotes) initOffsetA = previousOffsets[0] for (offsets, notes) in allHarmonies[1:]: - vlmB = [generalNoteToPitch(n) for n in notes] + vlmB = tuple(generalNoteToPitch(n) for n in notes) initOffsetB = offsets[0] vlm_violations = functionToApply(vlmA, vlmB) for partNumberTuple in vlm_violations: @@ -316,6 +354,9 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color='#FF0000 initOffsetA, initOffsetA, mustBeginInSpan=False).first() noteB = allParts[partNumber - 1].getElementsByOffset( initOffsetB, initOffsetB, mustBeginInSpan=False).first() + if noteA is None or noteB is None: + raise Music21Exception( + 'Expected notes to color at the violation offset, but found none') noteA.style.color = color noteB.style.color = color if debug is True: @@ -338,7 +379,7 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color='#FF0000 # represent two voices which form a voice crossing. -def voiceCrossing(possibA): +def voiceCrossing(possibA: PossibilityWithRests) -> list[PartPair]: ''' Returns a list of (partNumberA, partNumberB) pairs, each representing two voices which form a voice crossing. The parts from the lowest part to @@ -359,14 +400,14 @@ def voiceCrossing(possibA): >>> checker.voiceCrossing(possibA2) [] ''' - partViolations = [] + partViolations: list[PartPair] = [] for part1Index in range(len(possibA)): higherPitch = possibA[part1Index] - if not hasattr(higherPitch, 'ps'): + if not isinstance(higherPitch, pitch.Pitch): continue for part2Index in range(part1Index + 1, len(possibA)): lowerPitch = possibA[part2Index] - if not hasattr(lowerPitch, 'ps'): + if not isinstance(lowerPitch, pitch.Pitch): continue if higherPitch < lowerPitch: partViolations.append((part1Index + 1, part2Index + 1)) @@ -376,17 +417,13 @@ def voiceCrossing(possibA): # Consecutive Possibility Rule-Checking Methods -type PITCH_QUARTET_TO_BOOL_TYPE = dict[ - tuple[pitch.Pitch, pitch.Pitch, pitch.Pitch, pitch.Pitch], - bool -] -parallelFifthsTable: PITCH_QUARTET_TO_BOOL_TYPE = {} -parallelOctavesTable: PITCH_QUARTET_TO_BOOL_TYPE = {} -hiddenFifthsTable: PITCH_QUARTET_TO_BOOL_TYPE = {} -hiddenOctavesTable: PITCH_QUARTET_TO_BOOL_TYPE = {} +parallelFifthsTable: PitchQuartetToBool = {} +parallelOctavesTable: PitchQuartetToBool = {} +hiddenFifthsTable: PitchQuartetToBool = {} +hiddenOctavesTable: PitchQuartetToBool = {} -def parallelFifths(possibA, possibB): +def parallelFifths(possibA: PossibilityWithRests, possibB: PossibilityWithRests) -> list[PartPair]: ''' Returns a list of (partNumberA, partNumberB) pairs, each representing two voices which form parallel fifths. @@ -428,19 +465,24 @@ def parallelFifths(possibA, possibB): >>> checker.parallelFifths(possibA2, possibB2) [] ''' - pairsList = possibility.partPairs(possibA, possibB) - partViolations = [] + partViolations: list[PartPair] = [] + pairsList = list(zip(possibA, possibB)) for pair1Index in range(len(pairsList)): (higherPitchA, higherPitchB) = pairsList[pair1Index] + if not isinstance(higherPitchA, pitch.Pitch): + continue + if not isinstance(higherPitchB, pitch.Pitch): + continue for pair2Index in range(pair1Index + 1, len(pairsList)): (lowerPitchA, lowerPitchB) = pairsList[pair2Index] - try: - if not abs(higherPitchA.ps - lowerPitchA.ps) % 12 == 7: - continue - if not abs(higherPitchB.ps - lowerPitchB.ps) % 12 == 7: - continue - except AttributeError: + if not isinstance(lowerPitchA, pitch.Pitch): + continue + if not isinstance(lowerPitchB, pitch.Pitch): + continue + if not abs(higherPitchA.ps - lowerPitchA.ps) % 12 == 7: + continue + if not abs(higherPitchB.ps - lowerPitchB.ps) % 12 == 7: continue # Very high probability of ||5, but still not certain. pitchQuartet = (lowerPitchA, lowerPitchB, higherPitchA, higherPitchB) @@ -457,7 +499,7 @@ def parallelFifths(possibA, possibB): return partViolations -def hiddenFifth(possibA, possibB): +def hiddenFifth(possibA: PossibilityWithRests, possibB: PossibilityWithRests) -> list[PartPair]: ''' Returns a list with a (highestPart, lowestPart) pair which represents a hidden fifth between shared outer parts of possibA and possibB. The @@ -505,33 +547,38 @@ def hiddenFifth(possibA, possibB): >>> checker.hiddenFifth(possibA3, possibB3) [] ''' - partViolations = [] - pairsList = possibility.partPairs(possibA, possibB) + partViolations: list[PartPair] = [] + pairsList = list(zip(possibA, possibB)) (highestPitchA, highestPitchB) = pairsList[0] (lowestPitchA, lowestPitchB) = pairsList[-1] - - try: - if abs(highestPitchB.ps - lowestPitchB.ps) % 12 == 7: - # Very high probability of hidden fifth, but still not certain. - pitchQuartet = (lowestPitchA, lowestPitchB, highestPitchA, highestPitchB) - if pitchQuartet in hiddenFifthsTable: - hasHiddenFifth = hiddenFifthsTable[pitchQuartet] - if hasHiddenFifth: - partViolations.append((1, len(possibB))) - return partViolations - vlq = voiceLeading.VoiceLeadingQuartet(*pitchQuartet) - if vlq.hiddenFifth(): + if not isinstance(highestPitchA, pitch.Pitch): + return partViolations + if not isinstance(highestPitchB, pitch.Pitch): + return partViolations + if not isinstance(lowestPitchA, pitch.Pitch): + return partViolations + if not isinstance(lowestPitchB, pitch.Pitch): + return partViolations + + if abs(highestPitchB.ps - lowestPitchB.ps) % 12 == 7: + # Very high probability of hidden fifth, but still not certain. + pitchQuartet = (lowestPitchA, lowestPitchB, highestPitchA, highestPitchB) + if pitchQuartet in hiddenFifthsTable: + hasHiddenFifth = hiddenFifthsTable[pitchQuartet] + if hasHiddenFifth: partViolations.append((1, len(possibB))) - hiddenFifthsTable[pitchQuartet] = True - hiddenFifthsTable[pitchQuartet] = False return partViolations - except AttributeError: - pass + vlq = voiceLeading.VoiceLeadingQuartet(*pitchQuartet) + if vlq.hiddenFifth(): + partViolations.append((1, len(possibB))) + hiddenFifthsTable[pitchQuartet] = True + hiddenFifthsTable[pitchQuartet] = False + return partViolations return partViolations -def parallelOctaves(possibA, possibB): +def parallelOctaves(possibA: PossibilityWithRests, possibB: PossibilityWithRests) -> list[PartPair]: ''' Returns a list of (partNumberA, partNumberB) pairs, each representing two voices which form parallel octaves. @@ -574,19 +621,24 @@ def parallelOctaves(possibA, possibB): >>> checker.parallelOctaves(possibA2, possibB2) [] ''' - pairsList = possibility.partPairs(possibA, possibB) - partViolations = [] + partViolations: list[PartPair] = [] + pairsList = list(zip(possibA, possibB)) for pair1Index in range(len(pairsList)): (higherPitchA, higherPitchB) = pairsList[pair1Index] + if not isinstance(higherPitchA, pitch.Pitch): + continue + if not isinstance(higherPitchB, pitch.Pitch): + continue for pair2Index in range(pair1Index + 1, len(pairsList)): (lowerPitchA, lowerPitchB) = pairsList[pair2Index] - try: - if not abs(higherPitchA.ps - lowerPitchA.ps) % 12 == 0: - continue - if not abs(higherPitchB.ps - lowerPitchB.ps) % 12 == 0: - continue - except AttributeError: + if not isinstance(lowerPitchA, pitch.Pitch): + continue + if not isinstance(lowerPitchB, pitch.Pitch): + continue + if not abs(higherPitchA.ps - lowerPitchA.ps) % 12 == 0: + continue + if not abs(higherPitchB.ps - lowerPitchB.ps) % 12 == 0: continue # Very high probability of ||8, but still not certain. pitchQuartet = (lowerPitchA, lowerPitchB, higherPitchA, higherPitchB) @@ -603,7 +655,7 @@ def parallelOctaves(possibA, possibB): return partViolations -def hiddenOctave(possibA, possibB): +def hiddenOctave(possibA: PossibilityWithRests, possibB: PossibilityWithRests) -> list[PartPair]: ''' Returns a list with a (highestPart, lowestPart) pair which represents a hidden octave between shared outer parts of possibA and possibB. The @@ -641,28 +693,33 @@ def hiddenOctave(possibA, possibB): >>> checker.hiddenOctave(possibA2, possibB2) [] ''' - partViolations = [] - pairsList = possibility.partPairs(possibA, possibB) + partViolations: list[PartPair] = [] + pairsList = list(zip(possibA, possibB)) (highestPitchA, highestPitchB) = pairsList[0] (lowestPitchA, lowestPitchB) = pairsList[-1] - - try: - if abs(highestPitchB.ps - lowestPitchB.ps) % 12 == 0: - # Very high probability of hidden octave, but still not certain. - pitchQuartet = (lowestPitchA, lowestPitchB, highestPitchA, highestPitchB) - if pitchQuartet in hiddenOctavesTable: - hasHiddenOctave = hiddenOctavesTable[pitchQuartet] - if hasHiddenOctave: - partViolations.append((1, len(possibB))) - return partViolations - vlq = voiceLeading.VoiceLeadingQuartet(*pitchQuartet) - if vlq.hiddenOctave(): + if not isinstance(highestPitchA, pitch.Pitch): + return partViolations + if not isinstance(highestPitchB, pitch.Pitch): + return partViolations + if not isinstance(lowestPitchA, pitch.Pitch): + return partViolations + if not isinstance(lowestPitchB, pitch.Pitch): + return partViolations + + if abs(highestPitchB.ps - lowestPitchB.ps) % 12 == 0: + # Very high probability of hidden octave, but still not certain. + pitchQuartet = (lowestPitchA, lowestPitchB, highestPitchA, highestPitchB) + if pitchQuartet in hiddenOctavesTable: + hasHiddenOctave = hiddenOctavesTable[pitchQuartet] + if hasHiddenOctave: partViolations.append((1, len(possibB))) - hiddenOctavesTable[pitchQuartet] = True - hiddenOctavesTable[pitchQuartet] = False return partViolations - except AttributeError: - pass + vlq = voiceLeading.VoiceLeadingQuartet(*pitchQuartet) + if vlq.hiddenOctave(): + partViolations.append((1, len(possibB))) + hiddenOctavesTable[pitchQuartet] = True + hiddenOctavesTable[pitchQuartet] = False + return partViolations return partViolations @@ -670,7 +727,7 @@ def hiddenOctave(possibA, possibB): # Helper Methods -def generalNoteToPitch(music21GeneralNote): +def generalNoteToPitch(music21GeneralNote: note.GeneralNote) -> pitch.Pitch | t.Literal['RT']: ''' Takes a :class:`~music21.note.GeneralNote`. If it is a :class:`~music21.note.Note`, returns its pitch. Otherwise, returns the string "RT", a rest placeholder. @@ -683,7 +740,7 @@ def generalNoteToPitch(music21GeneralNote): >>> figuredBass.checker.generalNoteToPitch(c1) 'RT' ''' - if music21GeneralNote.isNote: + if isinstance(music21GeneralNote, note.Note): return music21GeneralNote.pitch else: return 'RT' diff --git a/music21/figuredBass/examples.py b/music21/figuredBass/examples.py index 77ae133d1..f75684d46 100644 --- a/music21/figuredBass/examples.py +++ b/music21/figuredBass/examples.py @@ -21,15 +21,19 @@ from __future__ import annotations import copy +import typing as t import unittest from music21.figuredBass import realizer from music21.figuredBass import rules +if t.TYPE_CHECKING: + from music21 import stream + # ------------------------------------------------------------------------------ -def exampleA(): +def exampleA() -> realizer.FiguredBassLine: ''' This example was a homework assignment for 21M.302: Harmony & Counterpoint II at MIT in the fall of 2010, taught by Charles Shadle of the MIT Music Program. @@ -76,7 +80,7 @@ def exampleA(): return realizer.figuredBassFromStream(s) -def exampleD(): +def exampleD() -> realizer.FiguredBassLine: ''' This example was a homework assignment for 21M.302: Harmony & Counterpoint II at MIT in the fall of 2010, taught by Charles Shadle of the MIT Music Program. @@ -139,7 +143,7 @@ def exampleD(): return realizer.figuredBassFromStream(s) -def exampleB(): +def exampleB() -> realizer.FiguredBassLine: ''' This example was retrieved from page 114 of *The Music Theory Handbook* by Marjorie Merryman. @@ -182,7 +186,7 @@ def exampleB(): return realizer.figuredBassFromStream(s) -def exampleC(): +def exampleC() -> realizer.FiguredBassLine: ''' This example was retrieved from page 114 of *The Music Theory Handbook* by Marjorie Merryman. @@ -227,7 +231,7 @@ def exampleC(): return realizer.figuredBassFromStream(s) -def V43ResolutionExample(): +def V43ResolutionExample() -> realizer.FiguredBassLine: ''' The dominant 4,3 can resolve to either the tonic 5,3 or tonic 6,3. The proper resolution is dependent on the bass note of the tonic, and is determined in context, as shown in the @@ -248,7 +252,7 @@ def V43ResolutionExample(): return realizer.figuredBassFromStream(s) -def viio65ResolutionExample(): +def viio65ResolutionExample() -> realizer.FiguredBassLine: ''' For a fully diminished seventh chord resolving to the tonic, the resolution chord can contain either a doubled third (standard resolution) or a doubled tonic (alternate @@ -278,7 +282,7 @@ def viio65ResolutionExample(): return realizer.figuredBassFromStream(s) -def augmentedSixthResolutionExample(): +def augmentedSixthResolutionExample() -> realizer.FiguredBassLine: ''' This example was retrieved from page 61 of *The Music Theory Handbook* by Marjorie Merryman. @@ -308,7 +312,7 @@ def augmentedSixthResolutionExample(): return realizer.figuredBassFromStream(s) -def italianA6ResolutionExample(): +def italianA6ResolutionExample() -> realizer.FiguredBassLine: ''' The Italian augmented sixth chord (It+6) is the only augmented sixth chord to consist of only three @@ -353,7 +357,7 @@ def italianA6ResolutionExample(): return realizer.figuredBassFromStream(s) -def twelveBarBlues(): +def twelveBarBlues() -> realizer.FiguredBassLine: ''' This is a progression in Bb major based on the twelve bar blues. The progression used is: @@ -392,7 +396,10 @@ def twelveBarBlues(): # Functions that generate Boogie/Blues vamps. -def generateBoogieVamp(blRealization=None, numRepeats=5): +def generateBoogieVamp( + blRealization: realizer.Realization | None = None, + numRepeats: int = 5, +) -> stream.Score: ''' Turns whole notes in twelve bar blues bass line to blues boogie woogie bass line. Takes in numRepeats, which is the number of times to repeat the bass line. Also, takes in a @@ -440,7 +447,10 @@ def generateBoogieVamp(blRealization=None, numRepeats=5): return newScore -def generateTripletBlues(blRealization=None, numRepeats=5): # 12/8 +def generateTripletBlues( + blRealization: realizer.Realization | None = None, + numRepeats: int = 5, +) -> stream.Score: # 12/8 ''' Turns whole notes in twelve bar blues bass line to triplet blues bass line. Takes in numRepeats, which is the number of times to repeat the bass line. Also, takes in a diff --git a/music21/figuredBass/harmony.py b/music21/figuredBass/harmony.py index 652fb3578..00015721f 100644 --- a/music21/figuredBass/harmony.py +++ b/music21/figuredBass/harmony.py @@ -66,7 +66,7 @@ def __init__(self, figureString: str = '', *, figureStrings: Iterable[str] = (), - **keywords): + **keywords) -> None: super().__init__(**keywords) self._figs: str = '' @@ -100,7 +100,7 @@ def notation(self) -> notation.Notation: return self._figNotation @notation.setter - def notation(self, figureNotation: notation.Notation): + def notation(self, figureNotation: notation.Notation) -> None: self._figNotation = figureNotation self._figs = figureNotation.notationColumn @@ -126,7 +126,7 @@ def figureString(self) -> str: return self._figs @figureString.setter - def figureString(self, figureString: str): + def figureString(self, figureString: str) -> None: self._figs = figureString self.notation = notation.Notation(self._figs) @@ -153,7 +153,7 @@ def figureStrings(self) -> list[str]: def figureStrings(self, figureStrings: Iterable[str]) -> None: self.figureString = ','.join(figureStrings) - def _reprInternal(self): + def _reprInternal(self) -> str: return self.notation.notationColumn diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 6164e5ed7..972a84ce4 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -10,13 +10,15 @@ import copy import re +import typing as t import unittest from music21 import exceptions21 from music21 import pitch from music21 import prebase -shorthandNotation = {(None,): (5, 3), +shorthandNotation: dict[tuple[int|None, ...], tuple[int, ...]] = { + (None,): (5, 3), (5,): (5, 3), (6,): (6, 3), (7,): (7, 5, 3), @@ -208,9 +210,9 @@ def __init__(self, notationColumn: str = '') -> None: # Parse notation string self.notationColumn: str = notationColumn or '' self.figureStrings: list[str] = [] - self.origNumbers: tuple[int|None, ...] = () + self.origNumbers: tuple[int|str|None, ...] = () self.origModStrings: tuple[str|None, ...] = () - self.numbers: list[int] = [] + self.numbers: tuple[int|str|None, ...] = () self.modifierStrings: tuple[str|None, ...] = () self.extenders: list[bool] = [] self.hasExtenders: bool = False @@ -224,10 +226,10 @@ def __init__(self, notationColumn: str = '') -> None: self._getModifiers() self._getFigures() - def _reprInternal(self): + def _reprInternal(self) -> str: return str(self.notationColumn) - def _parseNotationColumn(self): + def _parseNotationColumn(self) -> None: ''' Given a notation column below a pitch, defines both self.numbers and self.modifierStrings, which provide the intervals above the @@ -282,9 +284,9 @@ def _parseNotationColumn(self): figures = re.split(delimiter, self.notationColumn) patternA1 = '([0-9_]*)' patternA2 = '([^0-9_]*)' - numbers = [] - modifierStrings = [] - figureStrings = [] + numbers: list[int|str|None] = [] + modifierStrings: list[str|None] = [] + figureStrings: list[str] = [] for figure in figures: figure = figure.strip() @@ -298,8 +300,8 @@ def _parseNotationColumn(self): if not (len(m1) <= 1 or len(m2) <= 1): raise NotationException('Invalid Notation: ' + figure) - number = None - modifierString = None + number: int|str|None = None + modifierString: str|None = None extender = False if m1: # if no number is there and only an extender is found. @@ -322,16 +324,16 @@ def _parseNotationColumn(self): modifierStrings.append(modifierString) self.extenders.append(extender) - numbers = tuple(numbers) - modifierStrings = tuple(modifierStrings) + numbersTuple = tuple(numbers) + modifierStringsTuple = tuple(modifierStrings) - self.origNumbers = numbers # Keep original numbers - self.numbers = numbers # Will be converted to longhand - self.origModStrings = modifierStrings # Keep original modifier strings - self.modifierStrings = modifierStrings # Will be converted to longhand + self.origNumbers = numbersTuple # Keep original numbers + self.numbers = numbersTuple # Will be converted to longhand + self.origModStrings = modifierStringsTuple # Keep original modifier strings + self.modifierStrings = modifierStringsTuple # Will be converted to longhand self.figureStrings = figureStrings - def _translateToLonghand(self): + def _translateToLonghand(self) -> None: ''' Provided the numbers and modifierStrings of a parsed notation column, translates it to longhand. @@ -349,47 +351,36 @@ def _translateToLonghand(self): ('-', '-') ''' oldNumbers = self.numbers - newNumbers = oldNumbers + newNumbers: tuple[int|str|None, ...] = oldNumbers oldModifierStrings = self.modifierStrings - newModifierStrings = oldModifierStrings + newModifierStrings: tuple[str|None, ...] = oldModifierStrings try: - newNumbers = shorthandNotation[oldNumbers] - newModifierStrings = [] - - oldNumbers = list(oldNumbers) - temp = [] - for number in oldNumbers: - if number is None: - temp.append(3) - else: - temp.append(number) + shorthandKey = t.cast('tuple[int|None, ...]', oldNumbers) + newNumbers = shorthandNotation[shorthandKey] + modStrings: list[str|None] = [] - oldNumbers = tuple(temp) + expandedOldNumbers = tuple( + 3 if number is None else number for number in oldNumbers + ) for number in newNumbers: - newModifierString = None - if number in oldNumbers: - modifierStringIndex = oldNumbers.index(number) + newModifierString: str|None = None + if number in expandedOldNumbers: + modifierStringIndex = expandedOldNumbers.index(number) newModifierString = oldModifierStrings[modifierStringIndex] - newModifierStrings.append(newModifierString) + modStrings.append(newModifierString) - newModifierStrings = tuple(newModifierStrings) + newModifierStrings = tuple(modStrings) except KeyError: - newNumbers = list(newNumbers) - temp = [] - for number in newNumbers: - if number is None: - temp.append(3) - else: - temp.append(number) - - newNumbers = tuple(temp) + newNumbers = tuple( + 3 if number is None else number for number in newNumbers + ) self.numbers = newNumbers self.modifierStrings = newModifierStrings - def _getModifiers(self): + def _getModifiers(self) -> None: ''' Turns the modifier strings into Modifier objects. A modifier object keeps track of both the modifier string @@ -406,7 +397,7 @@ def _getModifiers(self): >>> notation1.modifiers[2] ''' - modifiers = [] + modifiers: list[Modifier] = [] for i in range(len(self.numbers)): modifierString = self.modifierStrings[i] @@ -510,12 +501,12 @@ class Figure(prebase.ProtoM21Object): def __init__( self, - number: int|None = 1, + number: int|str|None = 1, modifierString: str|None = '', *, extender: bool = False - ): - self.number: int|None = number + ) -> None: + self.number: int|str|None = number self.modifierString: str|None = modifierString self.modifier: Modifier = Modifier(modifierString) # look for extender's underscore @@ -542,7 +533,7 @@ def isPureExtender(self) -> bool: ''' return self.number == 1 and self.hasExtender - def _reprInternal(self): + def _reprInternal(self) -> str: if self.isPureExtender: num = 'pure-extender' ext = '' @@ -629,18 +620,18 @@ class Modifier(prebase.ProtoM21Object): ''', } - def __init__(self, modifierString=None): - self.modifierString = modifierString - self.accidental = self._toAccidental() + def __init__(self, modifierString: str|None = None) -> None: + self.modifierString: str|None = modifierString + self.accidental: pitch.Accidental|None = self._toAccidental() - def _reprInternal(self): + def _reprInternal(self) -> str: if self.accidental is not None: acc = self.accidental.name else: acc = None return f'{self.modifierString} {acc}' - def _toAccidental(self): + def _toAccidental(self) -> pitch.Accidental|None: ''' >>> from music21.figuredBass import notation as n @@ -677,7 +668,7 @@ def _toAccidental(self): return a - def modifyPitchName(self, pitchNameToAlter): + def modifyPitchName(self, pitchNameToAlter: str) -> str: ''' Given a pitch name, modify its accidental given the Modifier's :attr:`~music21.figuredBass.notation.Modifier.accidental`. @@ -697,7 +688,12 @@ def modifyPitchName(self, pitchNameToAlter): self.modifyPitch(pitchToAlter, inPlace=True) return pitchToAlter.name - def modifyPitch(self, pitchToAlter, *, inPlace=False): + def modifyPitch( + self, + pitchToAlter: pitch.Pitch, + *, + inPlace: bool = False + ) -> pitch.Pitch|None: ''' Given a :class:`~music21.pitch.Pitch`, modify its :attr:`~music21.pitch.Pitch.accidental` given the Modifier's :attr:`~music21.figuredBass.notation.Modifier.accidental`. @@ -751,7 +747,7 @@ class ModifierException(exceptions21.Music21Exception): # Helper Methods -def convertToPitch(pitchString): +def convertToPitch(pitchString: str|pitch.Pitch) -> pitch.Pitch: ''' Converts a pitchString to a :class:`~music21.pitch.Pitch`, only if necessary. diff --git a/music21/figuredBass/possibility.py b/music21/figuredBass/possibility.py index a570a2bf3..a6c1c7d33 100644 --- a/music21/figuredBass/possibility.py +++ b/music21/figuredBass/possibility.py @@ -56,6 +56,7 @@ ''' from __future__ import annotations +import typing as t import unittest from music21 import chord @@ -64,10 +65,14 @@ from music21 import pitch from music21 import voiceLeading +# A possibility is a tuple of pitches, one per part, ordered from the highest +# part to the lowest part (the last element is the bass). +type Possibility = tuple[pitch.Pitch, ...] + # SINGLE POSSIBILITY RULE-CHECKING METHODS # ---------------------------------------- -def voiceCrossing(possibA): +def voiceCrossing(possibA: Possibility) -> bool: ''' Returns True if there is voice crossing present between any two parts in possibA. The parts from the lowest part to the highest part (right to left) @@ -100,7 +105,7 @@ def voiceCrossing(possibA): return hasVoiceCrossing -def isIncomplete(possibA, pitchNamesToContain): +def isIncomplete(possibA: Possibility, pitchNamesToContain: list[str]) -> bool: ''' Returns True if possibA is incomplete, if it doesn't contain at least one of every pitch name in pitchNamesToContain. @@ -126,7 +131,7 @@ def isIncomplete(possibA, pitchNamesToContain): False ''' isIncompleteV = False - pitchNamesContained = [] + pitchNamesContained: list[str] = [] for givenPitch in possibA: if givenPitch.name not in pitchNamesContained: pitchNamesContained.append(givenPitch.name) @@ -141,7 +146,7 @@ def isIncomplete(possibA, pitchNamesToContain): return isIncompleteV -def upperPartsWithinLimit(possibA, maxSemitoneSeparation=12): +def upperPartsWithinLimit(possibA: Possibility, maxSemitoneSeparation: int|None = 12) -> bool: ''' Returns True if the pitches in the upper parts of possibA are found within maxSemitoneSeparation of each other. The @@ -188,7 +193,7 @@ def upperPartsWithinLimit(possibA, maxSemitoneSeparation=12): DEFAULT_MAX_PITCH = pitch.Pitch('B5') -def pitchesWithinLimit(possibA, maxPitch=DEFAULT_MAX_PITCH): +def pitchesWithinLimit(possibA: Possibility, maxPitch: pitch.Pitch = DEFAULT_MAX_PITCH) -> bool: ''' Returns True if all pitches in possibA are less than or equal to the maxPitch provided. Comparisons between pitches are done using pitch @@ -224,7 +229,10 @@ def pitchesWithinLimit(possibA, maxPitch=DEFAULT_MAX_PITCH): return True -def limitPartToPitch(possibA, partPitchLimits=None): +def limitPartToPitch( + possibA: Possibility, + partPitchLimits: dict[int, pitch.Pitch]|None = None +) -> bool: ''' Takes in a dict, partPitchLimits containing (partNumber, partPitch) pairs, each of which limits a part in possibA to a certain :class:`~music21.pitch.Pitch`. @@ -256,17 +264,21 @@ def limitPartToPitch(possibA, partPitchLimits=None): # CONSECUTIVE POSSIBILITY RULE-CHECKING METHODS # --------------------------------------------- # Speedup tables -type PITCH_QUARTET_TO_BOOL_TYPE = dict[ +# TODO: reinstitute PitchQuartetToBool caching with proper pitch hash checking +# (keying on Pitch objects relies on identity, so these caches rarely hit; +# rework to key on nameWithOctave strings). Same applies to the copies in +# figuredBass.checker. +type PitchQuartetToBool = dict[ tuple[pitch.Pitch, pitch.Pitch, pitch.Pitch, pitch.Pitch], bool ] -parallelFifthsTable: PITCH_QUARTET_TO_BOOL_TYPE = {} -parallelOctavesTable: PITCH_QUARTET_TO_BOOL_TYPE = {} -hiddenFifthsTable: PITCH_QUARTET_TO_BOOL_TYPE = {} -hiddenOctavesTable: PITCH_QUARTET_TO_BOOL_TYPE = {} +parallelFifthsTable: PitchQuartetToBool = {} +parallelOctavesTable: PitchQuartetToBool = {} +hiddenFifthsTable: PitchQuartetToBool = {} +hiddenOctavesTable: PitchQuartetToBool = {} -def parallelFifths(possibA, possibB): +def parallelFifths(possibA: Possibility, possibB: Possibility) -> bool: ''' Returns True if there are parallel fifths between any two shared parts of possibA and possibB. @@ -339,7 +351,7 @@ def parallelFifths(possibA, possibB): return hasParallelFifths -def parallelOctaves(possibA, possibB): +def parallelOctaves(possibA: Possibility, possibB: Possibility) -> bool: ''' Returns True if there are parallel octaves between any two shared parts of possibA and possibB. @@ -413,7 +425,7 @@ def parallelOctaves(possibA, possibB): return hasParallelOctaves -def hiddenFifth(possibA, possibB): +def hiddenFifth(possibA: Possibility, possibB: Possibility) -> bool: ''' Returns True if there is a hidden fifth between shared outer parts of possibA and possibB. The outer parts here are the first and last @@ -480,7 +492,7 @@ def hiddenFifth(possibA, possibB): return hasHiddenFifth -def hiddenOctave(possibA, possibB): +def hiddenOctave(possibA: Possibility, possibB: Possibility) -> bool: ''' Returns True if there is a hidden octave between shared outer parts of possibA and possibB. The outer parts here are the first and last @@ -537,7 +549,7 @@ def hiddenOctave(possibA, possibB): return hasHiddenOctave -def voiceOverlap(possibA, possibB): +def voiceOverlap(possibA: Possibility, possibB: Possibility) -> bool: ''' Returns True if there is voice overlap between any two shared parts of possibA and possibB. @@ -603,7 +615,11 @@ def voiceOverlap(possibA, possibB): return hasVoiceOverlap -def partMovementsWithinLimits(possibA, possibB, partMovementLimits=None): +def partMovementsWithinLimits( + possibA: Possibility, + possibB: Possibility, + partMovementLimits: list[tuple[int, int]]|None = None +) -> bool: # noinspection PyShadowingNames ''' Returns True if all movements between shared parts of possibA and possibB @@ -654,7 +670,7 @@ def partMovementsWithinLimits(possibA, possibB, partMovementLimits=None): return withinLimits -def upperPartsSame(possibA, possibB): +def upperPartsSame(possibA: Possibility, possibB: Possibility) -> bool: ''' Returns True if the upper parts are the same. False otherwise. @@ -684,7 +700,11 @@ def upperPartsSame(possibA, possibB): return True -def partsSame(possibA, possibB, partsToCheck=None): +def partsSame( + possibA: Possibility, + possibB: Possibility, + partsToCheck: list[int]|None = None +) -> bool: ''' Takes in partsToCheck, a list of part numbers. Checks if pitches at those part numbers of possibA and possibB are equal, determined by pitch space. @@ -713,7 +733,12 @@ def partsSame(possibA, possibB, partsToCheck=None): return True -def couldBeItalianA6Resolution(possibA, possibB, threePartChordInfo=None, restrictDoublings=True): +def couldBeItalianA6Resolution( + possibA: Possibility, + possibB: Possibility, + threePartChordInfo: list[pitch.Pitch]|None = None, + restrictDoublings: bool = True +) -> bool: ''' Speed-enhanced but designed to stand alone. Returns True if possibA is an Italian A6 chord @@ -784,8 +809,8 @@ def couldBeItalianA6Resolution(possibA, possibB, threePartChordInfo=None, restri raise PossibilityException('possibA does not spell out an It+6 chord.') bass = augSixthChord.bass() root = augSixthChord.root() - third = augSixthChord.getChordStep(3) - fifth = augSixthChord.getChordStep(5) + third = t.cast(pitch.Pitch, augSixthChord.getChordStep(3)) + fifth = t.cast(pitch.Pitch, augSixthChord.getChordStep(5)) threePartChordInfo = [bass, root, third, fifth] allowedIntervalNames = ['M3', 'm3', 'M2', 'm-2'] @@ -876,7 +901,10 @@ def couldBeItalianA6Resolution(possibA, possibB, threePartChordInfo=None, restri # -------------- -def partPairs(possibA, possibB): +def partPairs( + possibA: Possibility, + possibB: Possibility +) -> list[tuple[pitch.Pitch, pitch.Pitch]]: ''' Groups together pitches of possibA and possibB which correspond to the same part, constituting a shared part. diff --git a/music21/figuredBass/realizer.py b/music21/figuredBass/realizer.py index d1686e8ce..1588460d1 100644 --- a/music21/figuredBass/realizer.py +++ b/music21/figuredBass/realizer.py @@ -53,11 +53,17 @@ from music21 import note from music21 import pitch 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 + +if t.TYPE_CHECKING: + from music21 import harmony + from music21 import roman def figuredBassFromStream(streamPart: stream.Stream) -> FiguredBassLine: @@ -158,7 +164,7 @@ def updateAnnotationString(annotationString: str, inputText: str) -> str: return fb -def addLyricsToBassNote(bassNote, notationString=None): +def addLyricsToBassNote(bassNote: note.Note, notationString: str|None = None) -> None: ''' Takes in a bassNote and a corresponding notationString as arguments. Adds the parsed notationString as lyrics to the bassNote, which is @@ -177,7 +183,7 @@ def addLyricsToBassNote(bassNote, notationString=None): :width: 100 ''' bassNote.lyrics = [] - n = notation.Notation(notationString) + n = notation.Notation(notationString or '') if not n.figureStrings: return maxLength = max([len(fs) for fs in n.figureStrings]) @@ -220,7 +226,9 @@ class FiguredBassLine: ''', } - def __init__(self, inKey=None, inTime=None): + 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: @@ -228,12 +236,19 @@ def __init__(self, inKey=None, inTime=None): self.inKey = inKey self.inTime = inTime - self._paddingLeft = 0.0 + self._paddingLeft: OffsetQL = 0.0 self._overlaidParts = stream.Part() - self._fbScale = realizerScale.FiguredBassScale(inKey.pitchFromDegree(1), inKey.mode) - self._fbList = [] - - def addElement(self, bassObject: note.Note, notationString=None): + 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 + ] = [] + + 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. @@ -261,18 +276,20 @@ def addElement(self, bassObject: note.Note, notationString=None): bassObject.editorial.notationString = notationString c = bassObject.classes if 'Note' in c: - self._fbList.append((bassObject, notationString)) # a bass note, and a notationString - addLyricsToBassNote(bassObject, notationString) + bassNote = t.cast(note.Note, bassObject) + self._fbList.append((bassNote, notationString)) # a bass note, and a notationString + addLyricsToBassNote(bassNote, notationString) # ---------- Added to accommodate harmony.ChordSymbol and roman.RomanNumeral objects --- elif 'RomanNumeral' in c or 'ChordSymbol' in c: - self._fbList.append(bassObject) # a roman Numeral object + harmonyObject = t.cast('harmony.ChordSymbol | roman.RomanNumeral', bassObject) + self._fbList.append(harmonyObject) # a roman Numeral object else: raise FiguredBassLineException( 'Not a valid bassObject (only note.Note, ' f'harmony.ChordSymbol, and roman.RomanNumeral supported) was {bassObject!r}' ) - def generateBassLine(self): + def generateBassLine(self) -> stream.Part: ''' Generates the bass line as a :class:`~music21.stream.Score`. @@ -312,7 +329,9 @@ def generateBassLine(self): r = note.Rest(quarterLength=self._paddingLeft) bassLine.append(r) - for (bassNote, unused_notationString) in self._fbList: + # 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) @@ -322,7 +341,12 @@ def generateBassLine(self): m0.padAsAnacrusis() return bl2 - def retrieveSegments(self, fbRules=None, numParts=4, maxPitch=None): + 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 @@ -334,7 +358,7 @@ def retrieveSegments(self, fbRules=None, numParts=4, maxPitch=None): fbRules = rules.Rules() if maxPitch is None: maxPitch = pitch.Pitch('B5') - segmentList = [] + segmentList: list[segment.Segment] = [] bassLine = self.generateBassLine() if len(self._overlaidParts) >= 1: self._overlaidParts.append(bassLine) @@ -342,10 +366,10 @@ def retrieveSegments(self, fbRules=None, numParts=4, maxPitch=None): else: currentMapping = checker.createOffsetMapping(bassLine) allKeys = sorted(currentMapping.keys()) - bassLine = bassLine.flatten().notes + bassNotes = bassLine.flatten().notes bassNoteIndex = 0 - previousBassNote = bassLine[bassNoteIndex] - bassNote = currentMapping[allKeys[0]][-1] + previousBassNote = bassNotes[bassNoteIndex] + bassNote = t.cast(note.Note, currentMapping[allKeys[0]][-1]) previousSegment = segment.OverlaidSegment(bassNote, bassNote.editorial.notationString, self._fbScale, fbRules, numParts, maxPitch) @@ -353,16 +377,16 @@ def retrieveSegments(self, fbRules=None, numParts=4, maxPitch=None): segmentList.append(previousSegment) for k in allKeys[1:]: (startTime, unused_endTime) = k - bassNote = currentMapping[k][-1] + 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 = currentMapping[k][partNumber - 1] + upperPitch = t.cast(note.Note, currentMapping[k][partNumber - 1]) currentSegment.fbRules._partPitchLimits.append((partNumber, upperPitch)) if startTime == previousBassNote.offset + previousBassNote.quarterLength: bassNoteIndex += 1 - previousBassNote = bassLine[bassNoteIndex] + previousBassNote = bassNotes[bassNoteIndex] currentSegment.quarterLength = previousBassNote.quarterLength else: for partNumber in range(len(currentMapping[k]), numParts + 1): @@ -374,10 +398,15 @@ def retrieveSegments(self, fbRules=None, numParts=4, maxPitch=None): previousSegment = currentSegment return segmentList - def overlayPart(self, music21Part): + def overlayPart(self, music21Part: stream.Part) -> None: self._overlaidParts.append(music21Part) - def realize(self, fbRules=None, numParts=4, maxPitch=None): + 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` @@ -438,14 +467,14 @@ def realize(self, fbRules=None, numParts=4, maxPitch=None): if maxPitch is None: maxPitch = pitch.Pitch('B5') - segmentList = [] + segmentList: list[segment.Segment] = [] listOfHarmonyObjects = False for item in self._fbList: - try: - c = item.classes - except AttributeError: + if isinstance(item, tuple): + # a (bassNote, notationString) pair, not a harmony object continue + c = item.classes if 'Note' in c: break # Added to accommodate harmony.ChordSymbol and roman.RomanNumeral objects @@ -454,7 +483,9 @@ def realize(self, fbRules=None, numParts=4, maxPitch=None): break if listOfHarmonyObjects: - for harmonyObject in self._fbList: + harmonyObjects = t.cast( + 'list[harmony.ChordSymbol | roman.RomanNumeral]', self._fbList) + for harmonyObject in harmonyObjects: listOfPitchesJustNames = [] for thisPitch in harmonyObject.pitches: listOfPitchesJustNames.append(thisPitch.name) @@ -464,7 +495,7 @@ def realize(self, fbRules=None, numParts=4, maxPitch=None): d[x] = x outputList = d.values() - def g(y): + def g(y: float) -> float: return y if y != 0.0 else 1.0 passedNote = note.Note(harmonyObject.bass().nameWithOctave, @@ -501,7 +532,7 @@ def g(y): inTime=self.inTime, overlaidParts=self._overlaidParts[0:-1], paddingLeft=self._paddingLeft) - def _trimAllMovements(self, segmentList): + 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 @@ -517,7 +548,7 @@ def _trimAllMovements(self, segmentList): elif len(segmentList) >= 3: segmentList.reverse() # gets this wrong # pylint: disable=cell-var-from-loop - movementsAB = None + movementsAB: dict[Possibility, list[Possibility]]|None = None for segmentIndex in range(1, len(segmentList) - 1): movementsAB = segmentList[segmentIndex + 1].movements movementsBC = segmentList[segmentIndex].movements @@ -529,9 +560,10 @@ def _trimAllMovements(self, segmentList): movementsAB[possibA] = list( filter(lambda possibBB: (possibBB in movementsBC), possibBList)) - for (possibA, possibBList) in list(movementsAB.items()): + movementsABFinal = t.cast('dict[Possibility, list[Possibility]]', movementsAB) + for (possibA, possibBList) in list(movementsABFinal.items()): if not possibBList: - del movementsAB[possibA] + del movementsABFinal[possibA] segmentList.reverse() return True @@ -561,7 +593,7 @@ class Realization: where n is the number of parts. SATB if n = 4.''', } - def __init__(self, **fbLineOutputs): + 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'] @@ -576,7 +608,7 @@ def __init__(self, **fbLineOutputs): self._paddingLeft = fbLineOutputs['paddingLeft'] self.keyboardStyleOutput = True - def getNumSolutions(self): + 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` @@ -597,10 +629,10 @@ def getNumSolutions(self): return len(self._segmentList[0].correctA) # What if there's only one (bassNote, notationString)? self._segmentList.reverse() - pathList = {} + pathList: dict[Possibility, int] = {} for segmentIndex in range(1, len(self._segmentList)): segmentA = self._segmentList[segmentIndex] - newPathList = {} + newPathList: dict[Possibility, int] = {} if not pathList: for possibA in segmentA.movements: newPathList[possibA] = len(segmentA.movements[possibA]) @@ -618,7 +650,7 @@ def getNumSolutions(self): self._segmentList.reverse() return numSolutions - def getAllPossibilityProgressions(self): + def getAllPossibilityProgressions(self) -> list[list[Possibility]]: ''' Compiles each unique possibility progression, adding it to a master list. Returns the master list. @@ -650,11 +682,11 @@ def getAllPossibilityProgressions(self): return progressions - def getRandomPossibilityProgression(self): + def getRandomPossibilityProgression(self) -> list[Possibility]: ''' Returns a random unique possibility progression. ''' - progression = [] + progression: list[Possibility] = [] if len(self._segmentList) == 1: possibA = random.sample(self._segmentList[0].correctA, 1)[0] progression.append(possibA) @@ -674,7 +706,10 @@ def getRandomPossibilityProgression(self): return progression - def generateRealizationFromPossibilityProgression(self, possibilityProgression): + def generateRealizationFromPossibilityProgression( + self, + possibilityProgression: list[Possibility], + ) -> stream.Score: ''' Generates a realization as a :class:`~music21.stream.Score` given a possibility progression. ''' @@ -710,7 +745,7 @@ def generateRealizationFromPossibilityProgression(self, possibilityProgression): rightHand[0].padAsAnacrusis() else: # Chorale-style output - upperParts = [] + upperParts: list[stream.Part] = [] for _partNumber in range(len(possibilityProgression[0]) - 1): fbPart = stream.Part() sol.insert(0.0, fbPart) @@ -745,7 +780,7 @@ def generateRealizationFromPossibilityProgression(self, possibilityProgression): sol.insert(0.0, bassLine) return sol - def generateAllRealizations(self): + def generateAllRealizations(self) -> stream.Score: ''' Generates all unique realizations as a :class:`~music21.stream.Score`. @@ -769,14 +804,14 @@ def generateAllRealizations(self): return allSols - def generateRandomRealization(self): + def generateRandomRealization(self) -> stream.Score: ''' Generates a random unique realization as a :class:`~music21.stream.Score`. ''' possibilityProgression = self.getRandomPossibilityProgression() return self.generateRealizationFromPossibilityProgression(possibilityProgression) - def generateRandomRealizations(self, amountToGenerate=20): + def generateRandomRealizations(self, amountToGenerate: int = 20) -> stream.Score: ''' Generates *amountToGenerate* unique realizations as a :class:`~music21.stream.Score`. @@ -811,7 +846,7 @@ class FiguredBassLineException(exceptions21.Music21Exception): class Test(unittest.TestCase): - def testMultipleFiguresInLyric(self): + def testMultipleFiguresInLyric(self) -> None: from music21 import converter s = converter.parse('tinynotation: 4/4 C4 F4 G4_64 G4 C1', makeNotation=False) diff --git a/music21/figuredBass/realizerScale.py b/music21/figuredBass/realizerScale.py index bf964da98..0ba1b8cd9 100644 --- a/music21/figuredBass/realizerScale.py +++ b/music21/figuredBass/realizerScale.py @@ -10,6 +10,7 @@ import copy import itertools +import typing as t import unittest from music21 import exceptions21 @@ -61,15 +62,20 @@ class FiguredBassScale: ''', } - def __init__(self, scaleValue='C', scaleMode='major'): + def __init__(self, + scaleValue: str | pitch.Pitch | note.Note = 'C', + scaleMode: str = 'major') -> None: try: scaleClass = scaleModes[scaleMode] - self.realizerScale = scaleClass(scaleValue) - self.keySig = key.KeySignature(key.pitchToSharps(scaleValue, scaleMode)) + self.realizerScale: scale.ConcreteScale = scaleClass(scaleValue) + self.keySig: key.KeySignature = key.KeySignature( + key.pitchToSharps(scaleValue, scaleMode)) except KeyError: raise FiguredBassScaleException('Unsupported scale type-> ' + scaleMode) - def getPitchNames(self, bassPitch, notationString=None): + def getPitchNames(self, + bassPitch: str | pitch.Pitch, + notationString: str | None = None) -> list[str]: ''' Takes a bassPitch and notationString and returns a list of corresponding pitch names based on the scale value and mode above and inclusive of the @@ -88,7 +94,7 @@ def getPitchNames(self, bassPitch, notationString=None): ''' bassPitch = convertToPitch(bassPitch) # Convert string to pitch (if necessary) bassSD = self.realizerScale.getScaleDegreeFromPitch(bassPitch) - nt = notation.Notation(notationString) + nt = notation.Notation(notationString or '') if bassSD is None: bassPitchCopy = copy.deepcopy(bassPitch) @@ -98,10 +104,12 @@ def getPitchNames(self, bassPitch, notationString=None): bassNote.pitch.accidental = self.keySig.accidentalByStep(bassNote.pitch.step) bassSD = self.realizerScale.getScaleDegreeFromPitch(bassNote.pitch) - pitchNames = [] + bassScaleDegree = t.cast(int, bassSD) + pitchNames: list[str] = [] for i in range(len(nt.numbers)): - pitchSD = (bassSD + nt.numbers[i] - 1) % 7 - samplePitch = self.realizerScale.pitchFromDegree(pitchSD) + number = t.cast(int, nt.numbers[i]) + pitchSD = (bassScaleDegree + number - 1) % 7 + samplePitch = t.cast(pitch.Pitch, self.realizerScale.pitchFromDegree(pitchSD)) pitchName = nt.modifiers[i].modifyPitchName(samplePitch.name) pitchNames.append(pitchName) @@ -109,7 +117,9 @@ def getPitchNames(self, bassPitch, notationString=None): pitchNames.reverse() return pitchNames - def getSamplePitches(self, bassPitch, notationString=None): + def getSamplePitches(self, + bassPitch: str | pitch.Pitch, + notationString: str | None = None) -> list[pitch.Pitch]: ''' Returns all pitches for a bassPitch and notationString within an octave of the bassPitch, inclusive of the bassPitch but @@ -154,7 +164,10 @@ def getSamplePitches(self, bassPitch, notationString=None): samplePitches = self.getPitches(bassPitch, notationString, maxPitch) return samplePitches - def getPitches(self, bassPitch, notationString=None, maxPitch=None): + def getPitches(self, + bassPitch: str | pitch.Pitch, + notationString: str | None = None, + maxPitch: str | pitch.Pitch | None = None) -> list[pitch.Pitch]: ''' Takes in a bassPitch, a notationString, and a maxPitch representing the highest possible pitch that can be returned. Returns a sorted list of pitches which @@ -188,7 +201,8 @@ def getPitches(self, bassPitch, notationString=None, maxPitch=None): bassPitch = convertToPitch(bassPitch) maxPitch = convertToPitch(maxPitch) pitchNames = self.getPitchNames(bassPitch, notationString) - iter1 = itertools.product(pitchNames, range(maxPitch.octave + 1)) + maxOctave = t.cast(int, maxPitch.octave) + iter1 = itertools.product(pitchNames, range(maxOctave + 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) @@ -196,7 +210,7 @@ def getPitches(self, bassPitch, notationString=None, maxPitch=None): allPitches.sort() return allPitches - def _reprInternal(self): + def _reprInternal(self) -> str: return f'{self.realizerScale!r}' diff --git a/music21/figuredBass/resolution.py b/music21/figuredBass/resolution.py index ab116477d..a46d9e000 100644 --- a/music21/figuredBass/resolution.py +++ b/music21/figuredBass/resolution.py @@ -31,13 +31,19 @@ from music21 import note from music21 import pitch from music21 import stream +from music21.figuredBass.possibility import Possibility + +# unpacked seventh-chord info: [bass, root, third, fifth, seventh], any of which may be None. +type SeventhChordInfo = list[pitch.Pitch | None] +# a (predicate, intervalString) pair used to decide how to transpose each pitch. +type ResolveRule = tuple[t.Callable[[pitch.Pitch], t.Any], str] def augmentedSixthToDominant( - augSixthPossib, + augSixthPossib: Possibility, augSixthType: int | None = None, - augSixthChordInfo: list[pitch.Pitch | None] | None = None -) -> tuple[pitch.Pitch, ...]: + augSixthChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) augmented sixth chords to the root position dominant triad. @@ -111,20 +117,21 @@ def augmentedSixthToDominant( fifth = t.cast(pitch.Pitch, fifth) other = t.cast(pitch.Pitch, other) - howToResolve = [(lambda p: p and bass and p.name == bass.name, '-m2'), - (lambda p: p and root and p.name == root.name, 'm2'), - (lambda p: p and fifth and p.name == fifth.name, '-m2'), - (lambda p: p and other and p.name == other.name and augSixthType == 3, 'd1'), - (lambda p: p and other and p.name == other.name and augSixthType == 2, '-m2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p and bass and p.name == bass.name, '-m2'), + (lambda p: p and root and p.name == root.name, 'm2'), + (lambda p: p and fifth and p.name == fifth.name, '-m2'), + (lambda p: p and other and p.name == other.name and augSixthType == 3, 'd1'), + (lambda p: p and other and p.name == other.name and augSixthType == 2, '-m2')] return _resolvePitches(augSixthPossib, howToResolve) def augmentedSixthToMajorTonic( - augSixthPossib, + augSixthPossib: Possibility, augSixthType: int | None = None, - augSixthChordInfo: list[pitch.Pitch | None] | None = None -) -> tuple[pitch.Pitch, ...]: + augSixthChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) augmented sixth chords to the major tonic 6,4. @@ -193,21 +200,22 @@ def augmentedSixthToMajorTonic( fifth = t.cast(pitch.Pitch, fifth) other = t.cast(pitch.Pitch, other) - howToResolve = [(lambda p: p and bass and p.name == bass.name, '-m2'), - (lambda p: p and root and p.name == root.name, 'm2'), - (lambda p: p and fifth and p.name == fifth.name, 'P1'), - (lambda p: p and other and p.name == other.name and augSixthType == 1, 'M2'), - (lambda p: p and other and p.name == other.name and augSixthType == 2, 'A1'), - (lambda p: p and other and p.name == other.name and augSixthType == 3, 'm2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p and bass and p.name == bass.name, '-m2'), + (lambda p: p and root and p.name == root.name, 'm2'), + (lambda p: p and fifth and p.name == fifth.name, 'P1'), + (lambda p: p and other and p.name == other.name and augSixthType == 1, 'M2'), + (lambda p: p and other and p.name == other.name and augSixthType == 2, 'A1'), + (lambda p: p and other and p.name == other.name and augSixthType == 3, 'm2')] return _resolvePitches(augSixthPossib, howToResolve) def augmentedSixthToMinorTonic( - augSixthPossib, + augSixthPossib: Possibility, augSixthType: int | None = None, - augSixthChordInfo: list[pitch.Pitch | None] | None = None -) -> tuple[pitch.Pitch, ...]: + augSixthChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) augmented sixth chords to the minor tonic 6,4. @@ -276,16 +284,21 @@ def augmentedSixthToMinorTonic( fifth = t.cast(pitch.Pitch, fifth) other = t.cast(pitch.Pitch, other) - howToResolve = [(lambda p: p and bass and p.name == bass.name, '-m2'), - (lambda p: p and root and p.name == root.name, 'm2'), - (lambda p: p and fifth and p.name == fifth.name, 'P1'), - (lambda p: p and other and p.name == other.name and augSixthType == 1, 'm2'), - (lambda p: p and other and p.name == other.name and augSixthType == 3, 'd2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p and bass and p.name == bass.name, '-m2'), + (lambda p: p and root and p.name == root.name, 'm2'), + (lambda p: p and fifth and p.name == fifth.name, 'P1'), + (lambda p: p and other and p.name == other.name and augSixthType == 1, 'm2'), + (lambda p: p and other and p.name == other.name and augSixthType == 3, 'd2')] return _resolvePitches(augSixthPossib, howToResolve) -def dominantSeventhToMajorTonic(domPossib, resolveV43toI6=False, domChordInfo=None): +def dominantSeventhToMajorTonic( + domPossib: Possibility, + resolveV43toI6: bool = False, + domChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a dominant seventh chord in root position or any of its inversions to the major tonic, in root position or first inversion. @@ -346,19 +359,25 @@ def dominantSeventhToMajorTonic(domPossib, resolveV43toI6=False, domChordInfo=No if not domChord.isDominantSeventh(): raise ResolutionException('Possibility is not a dominant seventh chord.') domChordInfo = _unpackSeventhChord(chord.Chord(domPossib)) - [bass, root, third, fifth, seventh] = domChordInfo + [bass, root, third, fifth, seventh] = t.cast( + list[pitch.Pitch], domChordInfo) - howToResolve = [(lambda p: p.name == root.name and p == bass, 'P4'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name and resolveV43toI6, 'M2'), - (lambda p: p.name == fifth.name, '-M2'), - (lambda p: p.name == seventh.name and resolveV43toI6, 'M2'), - (lambda p: p.name == seventh.name, '-m2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name and p == bass, 'P4'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name and resolveV43toI6, 'M2'), + (lambda p: p.name == fifth.name, '-M2'), + (lambda p: p.name == seventh.name and resolveV43toI6, 'M2'), + (lambda p: p.name == seventh.name, '-m2')] return _resolvePitches(domPossib, howToResolve) -def dominantSeventhToMinorTonic(domPossib, resolveV43toi6=False, domChordInfo=None): +def dominantSeventhToMinorTonic( + domPossib: Possibility, + resolveV43toi6: bool = False, + domChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a dominant seventh chord in root position or any of its inversions to the minor tonic, in root position or first inversion, @@ -419,19 +438,24 @@ def dominantSeventhToMinorTonic(domPossib, resolveV43toi6=False, domChordInfo=No if not domChord.isDominantSeventh(): raise ResolutionException('Possibility is not a dominant seventh chord.') domChordInfo = _unpackSeventhChord(chord.Chord(domPossib)) - [bass, root, third, fifth, seventh] = domChordInfo + [bass, root, third, fifth, seventh] = t.cast( + list[pitch.Pitch], domChordInfo) - howToResolve = [(lambda p: p.name == root.name and p == bass, 'P4'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name and resolveV43toi6, 'm2'), - (lambda p: p.name == fifth.name, '-M2'), - (lambda p: p.name == seventh.name and resolveV43toi6, 'M2'), - (lambda p: p.name == seventh.name, '-M2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name and p == bass, 'P4'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name and resolveV43toi6, 'm2'), + (lambda p: p.name == fifth.name, '-M2'), + (lambda p: p.name == seventh.name and resolveV43toi6, 'M2'), + (lambda p: p.name == seventh.name, '-M2')] return _resolvePitches(domPossib, howToResolve) -def dominantSeventhToMajorSubmediant(domPossib, domChordInfo=None): +def dominantSeventhToMajorSubmediant( + domPossib: Possibility, + domChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a dominant seventh chord in root position to the major submediant (VI) in root position. @@ -457,17 +481,22 @@ def dominantSeventhToMajorSubmediant(domPossib, domChordInfo=None): domChordInfo = _unpackSeventhChord(chord.Chord(domPossib)) if not domChord.inversion() == 0: raise ResolutionException('Possibility must be in root position.') - [unused_bass, root, third, fifth, seventh] = domChordInfo + [unused_bass, root, third, fifth, seventh] = t.cast( + list[pitch.Pitch], domChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'm2'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name, '-M2'), - (lambda p: p.name == seventh.name, '-M2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'm2'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name, '-M2'), + (lambda p: p.name == seventh.name, '-M2')] return _resolvePitches(domPossib, howToResolve) -def dominantSeventhToMinorSubmediant(domPossib, domChordInfo=None): +def dominantSeventhToMinorSubmediant( + domPossib: Possibility, + domChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a dominant seventh chord in root position to the minor submediant (vi) in root position. @@ -493,17 +522,22 @@ def dominantSeventhToMinorSubmediant(domPossib, domChordInfo=None): domChordInfo = _unpackSeventhChord(chord.Chord(domPossib)) if not domChord.inversion() == 0: raise ResolutionException('Possibility must be in root position.') - [unused_bass, root, third, fifth, seventh] = domChordInfo + [unused_bass, root, third, fifth, seventh] = t.cast( + list[pitch.Pitch], domChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'M2'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name, '-M2'), - (lambda p: p.name == seventh.name, '-m2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'M2'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name, '-M2'), + (lambda p: p.name == seventh.name, '-m2')] return _resolvePitches(domPossib, howToResolve) -def dominantSeventhToMajorSubdominant(domPossib, domChordInfo=None): +def dominantSeventhToMajorSubdominant( + domPossib: Possibility, + domChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a dominant seventh chord in root position to the major subdominant (IV) in first inversion. @@ -529,16 +563,21 @@ def dominantSeventhToMajorSubdominant(domPossib, domChordInfo=None): domChordInfo = _unpackSeventhChord(chord.Chord(domPossib)) if not domChord.inversion() == 0: raise ResolutionException('Possibility must be in root position.') - [unused_bass, root, third, fifth, unused_seventh] = domChordInfo + [unused_bass, root, third, fifth, unused_seventh] = t.cast( + list[pitch.Pitch], domChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'M2'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name, '-M2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'M2'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name, '-M2')] return _resolvePitches(domPossib, howToResolve) -def dominantSeventhToMinorSubdominant(domPossib, domChordInfo=None): +def dominantSeventhToMinorSubdominant( + domPossib: Possibility, + domChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a dominant seventh chord in root position to the minor subdominant (iv) in first inversion. @@ -564,16 +603,22 @@ def dominantSeventhToMinorSubdominant(domPossib, domChordInfo=None): domChordInfo = _unpackSeventhChord(chord.Chord(domPossib)) if not domChord.inversion() == 0: raise ResolutionException('Possibility must be in root position.') - [unused_bass, root, third, fifth, unused_seventh] = domChordInfo + [unused_bass, root, third, fifth, unused_seventh] = t.cast( + list[pitch.Pitch], domChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'm2'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name, '-M2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'm2'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name, '-M2')] return _resolvePitches(domPossib, howToResolve) -def diminishedSeventhToMajorTonic(dimPossib, doubledRoot=False, dimChordInfo=None): +def diminishedSeventhToMajorTonic( + dimPossib: Possibility, + doubledRoot: bool = False, + dimChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a fully diminished seventh chord to the major tonic, in root position or either inversion. @@ -607,18 +652,24 @@ def diminishedSeventhToMajorTonic(dimPossib, doubledRoot=False, dimChordInfo=Non if not dimChord.isDiminishedSeventh(): raise ResolutionException('Possibility is not a fully diminished seventh chord.') dimChordInfo = _unpackSeventhChord(chord.Chord(dimPossib)) - [unused_bass, root, third, fifth, seventh] = dimChordInfo + [unused_bass, root, third, fifth, seventh] = t.cast( + list[pitch.Pitch], dimChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'm2'), - (lambda p: p.name == third.name and doubledRoot, '-M2'), - (lambda p: p.name == third.name, 'M2'), - (lambda p: p.name == fifth.name, '-m2'), - (lambda p: p.name == seventh.name, '-m2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'm2'), + (lambda p: p.name == third.name and doubledRoot, '-M2'), + (lambda p: p.name == third.name, 'M2'), + (lambda p: p.name == fifth.name, '-m2'), + (lambda p: p.name == seventh.name, '-m2')] return _resolvePitches(dimPossib, howToResolve) -def diminishedSeventhToMinorTonic(dimPossib, doubledRoot=False, dimChordInfo=None): +def diminishedSeventhToMinorTonic( + dimPossib: Possibility, + doubledRoot: bool = False, + dimChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a fully diminished seventh chord to the minor tonic, in root position or either inversion. @@ -652,18 +703,23 @@ def diminishedSeventhToMinorTonic(dimPossib, doubledRoot=False, dimChordInfo=Non if not dimChord.isDiminishedSeventh(): raise ResolutionException('Possibility is not a fully diminished seventh chord.') dimChordInfo = _unpackSeventhChord(chord.Chord(dimPossib)) - [unused_bass, root, third, fifth, seventh] = dimChordInfo + [unused_bass, root, third, fifth, seventh] = t.cast( + list[pitch.Pitch], dimChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'm2'), - (lambda p: p.name == third.name and doubledRoot, '-M2'), - (lambda p: p.name == third.name, 'm2'), - (lambda p: p.name == fifth.name, '-M2'), - (lambda p: p.name == seventh.name, '-m2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'm2'), + (lambda p: p.name == third.name and doubledRoot, '-M2'), + (lambda p: p.name == third.name, 'm2'), + (lambda p: p.name == fifth.name, '-M2'), + (lambda p: p.name == seventh.name, '-m2')] return _resolvePitches(dimPossib, howToResolve) -def diminishedSeventhToMajorSubdominant(dimPossib, dimChordInfo=None): +def diminishedSeventhToMajorSubdominant( + dimPossib: Possibility, + dimChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a fully diminished seventh chord to the major subdominant (IV). @@ -687,16 +743,21 @@ def diminishedSeventhToMajorSubdominant(dimPossib, dimChordInfo=None): if not dimChord.isDiminishedSeventh(): raise ResolutionException('Possibility is not a fully diminished seventh chord.') dimChordInfo = _unpackSeventhChord(chord.Chord(dimPossib)) - [unused_bass, root, third, unused_fifth, seventh] = dimChordInfo + [unused_bass, root, third, unused_fifth, seventh] = t.cast( + list[pitch.Pitch], dimChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'm2'), - (lambda p: p.name == third.name, '-M2'), - (lambda p: p.name == seventh.name, 'A1')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'm2'), + (lambda p: p.name == third.name, '-M2'), + (lambda p: p.name == seventh.name, 'A1')] return _resolvePitches(dimPossib, howToResolve) -def diminishedSeventhToMinorSubdominant(dimPossib, dimChordInfo=None): +def diminishedSeventhToMinorSubdominant( + dimPossib: Possibility, + dimChordInfo: SeventhChordInfo | None = None +) -> Possibility: ''' Resolves a fully diminished seventh chord to the minor subdominant (iv). @@ -720,15 +781,17 @@ def diminishedSeventhToMinorSubdominant(dimPossib, dimChordInfo=None): if not dimChord.isDiminishedSeventh(): raise ResolutionException('Possibility is not a fully diminished seventh chord.') dimChordInfo = _unpackSeventhChord(chord.Chord(dimPossib)) - [unused_bass, root, third, unused_fifth, unused_seventh] = dimChordInfo + [unused_bass, root, third, unused_fifth, unused_seventh] = t.cast( + list[pitch.Pitch], dimChordInfo) - howToResolve = [(lambda p: p.name == root.name, 'm2'), - (lambda p: p.name == third.name, '-M2')] + howToResolve: list[ResolveRule] = [ + (lambda p: p.name == root.name, 'm2'), + (lambda p: p.name == third.name, '-M2')] return _resolvePitches(dimPossib, howToResolve) -def showResolutions(*allPossib): +def showResolutions(*allPossib: Possibility) -> None: ''' Takes in possibilities as arguments and adds them in order to a :class:`~music21.stream.Score` which is then displayed @@ -752,11 +815,14 @@ def showResolutions(*allPossib): # INTERNAL METHODS -def _transpose(samplePitch, intervalString): +def _transpose(samplePitch: pitch.Pitch, intervalString: str) -> pitch.Pitch: return samplePitch.transpose(intervalString) -def _resolvePitches(possibToResolve, howToResolve) -> tuple[pitch.Pitch, ...]: +def _resolvePitches( + possibToResolve: Possibility, + howToResolve: list[ResolveRule] +) -> Possibility: ''' Takes in a possibility to resolve and a list of (lambda function, intervalString) pairs and transposes each pitch by the intervalString corresponding to the lambda @@ -775,10 +841,10 @@ def _resolvePitches(possibToResolve, howToResolve) -> tuple[pitch.Pitch, ...]: def _unpackSeventhChord( seventhChord: chord.Chord -) -> list[pitch.Pitch|None]: +) -> SeventhChordInfo: ''' Takes in a Chord and returns a list of Pitches (or Nones) corresponding - to the bass, root, fifth, seventh. + to the bass, root, third, fifth, seventh. ''' bass = seventhChord.bass() root = seventhChord.root() diff --git a/music21/figuredBass/rules.py b/music21/figuredBass/rules.py index 0a4a58048..4a2183c7c 100644 --- a/music21/figuredBass/rules.py +++ b/music21/figuredBass/rules.py @@ -9,6 +9,7 @@ from __future__ import annotations import unittest +from music21 import note from music21 import prebase doc_forbidIncompletePossibilities = '''True by default. If True, @@ -143,34 +144,34 @@ class Rules(prebase.ProtoM21Object): + specialResDoc ) - def __init__(self): + def __init__(self) -> None: # Single Possibility rules - self.forbidIncompletePossibilities = True - self.upperPartsMaxSemitoneSeparation = 12 - self.forbidVoiceCrossing = True + self.forbidIncompletePossibilities: bool = True + self.upperPartsMaxSemitoneSeparation: int | None = 12 + self.forbidVoiceCrossing: bool = True # Consecutive Possibility rules - self.forbidParallelFifths = True - self.forbidParallelOctaves = True - self.forbidHiddenFifths = True - self.forbidHiddenOctaves = True - self.forbidVoiceOverlap = True - self.partMovementLimits = [] + self.forbidParallelFifths: bool = True + self.forbidParallelOctaves: bool = True + self.forbidHiddenFifths: bool = True + self.forbidHiddenOctaves: bool = True + self.forbidVoiceOverlap: bool = True + self.partMovementLimits: list[tuple[int, int]] = [] # Special resolution rules - self.resolveDominantSeventhProperly = True - self.resolveDiminishedSeventhProperly = True - self.resolveAugmentedSixthProperly = True - self.doubledRootInDim7 = False - self.applySinglePossibRulesToResolution = False - self.applyConsecutivePossibRulesToResolution = False - self.restrictDoublingsInItalianA6Resolution = True - - self._upperPartsRemainSame = False - self._partPitchLimits = [] - self._partsToCheck = [] - - def _reprInternal(self): + self.resolveDominantSeventhProperly: bool = True + self.resolveDiminishedSeventhProperly: bool = True + self.resolveAugmentedSixthProperly: bool = True + self.doubledRootInDim7: bool = False + self.applySinglePossibRulesToResolution: bool = False + self.applyConsecutivePossibRulesToResolution: bool = False + self.restrictDoublingsInItalianA6Resolution: bool = True + + self._upperPartsRemainSame: bool = False + self._partPitchLimits: list[tuple[int, note.Note]] = [] + self._partsToCheck: list[int] = [] + + def _reprInternal(self) -> str: return '' diff --git a/music21/figuredBass/segment.py b/music21/figuredBass/segment.py index 7e13aae77..362b350cf 100644 --- a/music21/figuredBass/segment.py +++ b/music21/figuredBass/segment.py @@ -8,9 +8,11 @@ # ------------------------------------------------------------------------------ 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 @@ -19,7 +21,9 @@ 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 @@ -63,14 +67,20 @@ class Segment: '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=4, + numParts: int = 4, maxPitch: str|pitch.Pitch = 'B5', - listOfPitches=None): + 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`. @@ -124,13 +134,13 @@ def __init__(self, else: self.fbRules = copy.deepcopy(fbRules) - self._specialResolutionRuleChecking = None - self._singlePossibilityRuleChecking = None - self._consecutivePossibilityRuleChecking = None + self._specialResolutionRuleChecking: dict[bool, list[tuple[t.Any, ...]]]|None = None + self._singlePossibilityRuleChecking: dict[bool, list[tuple[t.Any, ...]]]|None = None + self._consecutivePossibilityRuleChecking: dict[bool, list[tuple[t.Any, ...]]]|None = None - self.bassNote = bassNote - self.numParts = numParts - self._maxPitch = maxPitch + self.bassNote: note.Note = bassNote + self.numParts: int = numParts + self._maxPitch: pitch.Pitch = maxPitch if notationString is None and listOfPitches is not None: # must be a chord symbol or roman num. self.pitchNamesInChord = listOfPitches @@ -148,7 +158,10 @@ def __init__(self, # ------------------------------------------------------------------------------ # EXTERNAL METHODS - def singlePossibilityRules(self, fbRules=None): + 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 @@ -205,7 +218,10 @@ def singlePossibilityRules(self, fbRules=None): return singlePossibRules - def consecutivePossibilityRules(self, fbRules=None): + 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 @@ -287,7 +303,10 @@ def consecutivePossibilityRules(self, fbRules=None): return consecutivePossibRules - def specialResolutionRules(self, fbRules=None): + 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 @@ -366,7 +385,10 @@ def specialResolutionRules(self, fbRules=None): return specialResRules - def resolveDominantSeventhSegment(self, segmentB): + def resolveDominantSeventhSegment( + self, + segmentB: Segment + ) -> Iterator[tuple[Possibility, Possibility]]: # noinspection PyShadowingNames ''' Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord` @@ -409,13 +431,13 @@ def resolveDominantSeventhSegment(self, segmentB): # Put here for stand-alone purposes. raise SegmentException('Dominant seventh resolution: Not a dominant seventh Segment.') domChordInfo = _unpackSeventhChord(domChord) - dominantScale = scale.MajorScale().derive(domChord) + dominantScale = t.cast(scale.MajorScale, scale.MajorScale().derive(domChord)) minorScale = dominantScale.getParallelMinor() - tonic = dominantScale.getTonic() - subdominant = dominantScale.pitchFromDegree(4) - majSubmediant = dominantScale.pitchFromDegree(6) - minSubmediant = minorScale.pitchFromDegree(6) + 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) @@ -465,7 +487,11 @@ def resolveDominantSeventhSegment(self, segmentB): + 'Executing ordinary resolution.') return self._resolveOrdinarySegment(segmentB) - def resolveDiminishedSeventhSegment(self, segmentB, doubledRoot=False): + 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` @@ -503,8 +529,8 @@ def resolveDiminishedSeventhSegment(self, segmentB, doubledRoot=False): dimScale = scale.HarmonicMinorScale().deriveByDegree(7, dimChord.root()) # minorScale = dimScale.getParallelMinor() - tonic = dimScale.getTonic() - subdominant = dimScale.pitchFromDegree(4) + 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 @@ -536,7 +562,10 @@ def resolveDiminishedSeventhSegment(self, segmentB, doubledRoot=False): + 'Executing ordinary resolution.') return self._resolveOrdinarySegment(segmentB) - def resolveAugmentedSixthSegment(self, segmentB): + def resolveAugmentedSixthSegment( + self, + segmentB: Segment + ) -> Iterator[tuple[Possibility, Possibility]]: # noinspection PyShadowingNames ''' Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord` @@ -607,7 +636,7 @@ def resolveAugmentedSixthSegment(self, segmentB): and resChord.isMinorTriad()), resolution.augmentedSixthToMinorTonic, [augSixthType, augSixthChordInfo]), - ((majorScale.pitchFromDegree(5).name == resChord.bass().name + ((t.cast(pitch.Pitch, majorScale.pitchFromDegree(5)).name == resChord.bass().name and resChord.isMajorTriad()), resolution.augmentedSixthToDominant, [augSixthType, augSixthChordInfo]) @@ -621,7 +650,7 @@ def resolveAugmentedSixthSegment(self, segmentB): + 'Executing ordinary resolution.') return self._resolveOrdinarySegment(segmentB) - def allSinglePossibilities(self): + def allSinglePossibilities(self) -> Iterator[Possibility]: ''' Returns an iterator through a set of naive possibilities for a Segment, using :attr:`~music21.figuredBass.segment.Segment.numParts`, @@ -660,7 +689,7 @@ def allSinglePossibilities(self): iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)]) return itertools.product(*iterables) - def allCorrectSinglePossibilities(self): + def allCorrectSinglePossibilities(self) -> list[Possibility]: ''' Uses :meth:`~music21.figuredBass.segment.Segment.allSinglePossibilities` and returns an iterator through a set of correct possibilities for @@ -693,7 +722,10 @@ def allCorrectSinglePossibilities(self): allA = self.allSinglePossibilities() return [possibA for possibA in allA if self._isCorrectSinglePossibility(possibA)] - def allCorrectConsecutivePossibilities(self, segmentB): + def allCorrectConsecutivePossibilities( + self, + segmentB: Segment + ) -> Iterator[tuple[Possibility, Possibility]]: # noinspection PyShadowingNames ''' Returns an iterator through correct (possibA, possibB) pairs. @@ -761,31 +793,42 @@ def allCorrectConsecutivePossibilities(self, segmentB): # ------------------------------------------------------------------------------ # INTERNAL METHODS - def _isCorrectSinglePossibility(self, possibA): + 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. ''' - for (method, isCorrect, args) in self._singlePossibilityRuleChecking[True]: + ruleChecking = self._singlePossibilityRuleChecking + assert ruleChecking is not None + for (method, isCorrect, args) in ruleChecking[True]: if not (method(possibA, *args) == isCorrect): return False return True - def _isCorrectConsecutivePossibility(self, possibA, possibB): + 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. ''' - for (method, isCorrect, args) in self._consecutivePossibilityRuleChecking[True]: + ruleChecking = self._consecutivePossibilityRuleChecking + assert ruleChecking is not None + for (method, isCorrect, args) in ruleChecking[True]: if not (method(possibA, possibB, *args) == isCorrect): return False return True - def _resolveOrdinarySegment(self, segmentB): + 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. @@ -807,7 +850,11 @@ def _resolveOrdinarySegment(self, segmentB): possibB=possibAB[1]), correctAB) - def _resolveSpecialSegment(self, segmentB, specialResolutionMethods): + 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 = [] @@ -840,7 +887,7 @@ class OverlaidSegment(Segment): Class to allow Segments to be overlaid with non-chord notes. ''' - def allSinglePossibilities(self): + 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: @@ -850,9 +897,9 @@ def allSinglePossibilities(self): # HELPER METHODS # -------------- -def getPitches(pitchNames=('C', 'E', 'G'), +def getPitches(pitchNames: Iterable[str] = ('C', 'E', 'G'), bassPitch: str|pitch.Pitch = 'C3', - maxPitch: str|pitch.Pitch = 'C8'): + 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. @@ -892,7 +939,7 @@ def getPitches(pitchNames=('C', 'E', 'G'), return allPitches -def _unpackSeventhChord(seventhChord): +def _unpackSeventhChord(seventhChord: chord.Chord) -> list[pitch.Pitch|None]: bass = seventhChord.bass() root = seventhChord.root() third = seventhChord.getChordStep(3) @@ -902,7 +949,7 @@ def _unpackSeventhChord(seventhChord): return seventhChordInfo -def _unpackTriad(threePartChord): +def _unpackTriad(threePartChord: chord.Chord) -> list[pitch.Pitch|None]: bass = threePartChord.bass() root = threePartChord.root() third = threePartChord.getChordStep(3) @@ -911,8 +958,11 @@ def _unpackTriad(threePartChord): return threePartChordInfo -def _compileRules(rulesList, maxLength=4): - ruleChecking = collections.defaultdict(list) +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: @@ -927,7 +977,7 @@ def _compileRules(rulesList, maxLength=4): return ruleChecking -def printRules(rulesList, maxLength=4): +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`, @@ -944,7 +994,7 @@ def printRules(rulesList, maxLength=4): if len(rule[1].__name__) >= MAX_SIZE: MAX_SIZE = len(rule[1].__name__) + 2 - def padMethod(m): + def padMethod(m: Callable[..., t.Any]) -> str: methodName = m.__name__[0:MAX_SIZE] if len(methodName) < MAX_SIZE: methodName += ' ' * (MAX_SIZE - len(methodName)) diff --git a/music21/roman.py b/music21/roman.py index 125c20816..b456cf231 100644 --- a/music21/roman.py +++ b/music21/roman.py @@ -3219,7 +3219,7 @@ def _updatePitches(self) -> None: for j in range(numberNotes): i = numberNotes - j - 1 thisScaleDegree = (bassScaleDegree - + self.figuresNotationObj.numbers[i] + + t.cast(int, self.figuresNotationObj.numbers[i]) - 1) newPitch = useScale.pitchFromDegree(thisScaleDegree, direction=scale.Direction.ASCENDING) @@ -3603,7 +3603,7 @@ def bassScaleDegreeFromNotation( if notationObject.numbers not in FIGURES_IMPLYING_ROOT: return self.scaleDegree for i in notationObject.numbers: - distanceToMove = i - 1 + distanceToMove = t.cast(int, i) - 1 newDiatonicNumber = (cDNN + distanceToMove) newStep, newOctave = interval.convertDiatonicNumberToStep(