diff --git a/.gitignore b/.gitignore index 04aa7dd0e..e9db9c422 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ __pycache__/ .idea/vcs.xml .idea/workspace.xml .idea/markdown-*.xml +.idea/copilot # OSX .DS_Store diff --git a/music21/analysis/reduceChords.py b/music21/analysis/reduceChords.py index 18121cddc..6aaf44e5f 100644 --- a/music21/analysis/reduceChords.py +++ b/music21/analysis/reduceChords.py @@ -384,8 +384,7 @@ def procedure(timespan): ) print(msg) # raise ChordReducerException(msg) - if offset < previousTimespan.endTime: - offset = previousTimespan.endTime + offset = max(offset, previousTimespan.endTime) scoreTree.removeTimespan(group[0]) subtree.removeTimespan(group[0]) newTimespan = group[0].new(offset=offset) @@ -542,8 +541,7 @@ def reduceMeasureToNChords( measureObject.flatten().notes, weightAlgorithm, ) - if maximumNumberOfChords > len(chordWeights): - maximumNumberOfChords = len(chordWeights) + maximumNumberOfChords = min(maximumNumberOfChords, len(chordWeights)) sortedChordWeights = sorted( chordWeights, key=chordWeights.get, diff --git a/music21/analysis/reduceChordsOld.py b/music21/analysis/reduceChordsOld.py index 8a76ee862..886c2180d 100644 --- a/music21/analysis/reduceChordsOld.py +++ b/music21/analysis/reduceChordsOld.py @@ -86,8 +86,7 @@ def reduceMeasureToNChords(self, chordWeights = self.computeMeasureChordWeights(mObj, weightAlgorithm) - if numChords > len(chordWeights): - numChords = len(chordWeights) + numChords = min(numChords, len(chordWeights)) maxNChords = sorted(chordWeights, key=chordWeights.get, reverse=True)[:numChords] if not maxNChords: diff --git a/music21/analysis/windowed.py b/music21/analysis/windowed.py index 58236939b..1b1740bae 100644 --- a/music21/analysis/windowed.py +++ b/music21/analysis/windowed.py @@ -193,10 +193,8 @@ def analyze(self, windowSize, windowType='overlap'): elif windowType == 'noOverlap': start = 0 end = start + windowSize - i = 0 - while True: - if end >= len(self._windowedStream): - end = len(self._windowedStream) + for i in range(windowCount): + end = min(end, len(self._windowedStream)) current = stream.Stream() for j in range(start, end): @@ -210,9 +208,6 @@ def analyze(self, windowSize, windowType='overlap'): start = end end = start + windowSize - i += 1 - if i >= windowCount: - break elif windowType == 'adjacentAverage': # first get overlapping windows diff --git a/music21/base.py b/music21/base.py index ccca277de..a474d78f4 100644 --- a/music21/base.py +++ b/music21/base.py @@ -899,7 +899,7 @@ def getOffsetBySite( returnSpecial: bool = False, ) -> OffsetQL|OffsetSpecial: return 0.0 # dummy until Astroid #1015 is fixed. Replace with ... - # using bool instead of t.Literal[True] because of + # using bool instead of t.Literal[True] because of other errors def getOffsetBySite( self, diff --git a/music21/braille/text.py b/music21/braille/text.py index c71d2398d..20d2d39cc 100644 --- a/music21/braille/text.py +++ b/music21/braille/text.py @@ -336,8 +336,7 @@ def recenterHeadings(self): if self.allLines[i].isHeading: break lineLength = self.allLines[i].textLocation - if lineLength > maxLineLength: - maxLineLength = lineLength + maxLineLength = max(maxLineLength, lineLength) for j in range(indexStart, indexFinal): brailleTextLine = self.allLines[j] lineStrToCenter = str(brailleTextLine) @@ -565,12 +564,10 @@ def insert(self, textLocation, text): ''' if not self.canInsert(textLocation, text): raise BrailleTextException('Text cannot be inserted at specified location.') - self.textLocation = textLocation - for char in list(text): - self.allChars[self.textLocation] = char - self.textLocation += 1 - if self.textLocation > self.highestUsedLocation: - self.highestUsedLocation = self.textLocation + for i, char in enumerate(text, start=textLocation): + self.allChars[i] = char + self.textLocation = textLocation + len(text) + self.highestUsedLocation = max(self.highestUsedLocation, self.textLocation) def canAppend(self, text, addSpace=True): ''' @@ -600,10 +597,7 @@ def canAppend(self, text, addSpace=True): >>> btl.canAppend('1234', addSpace=False) False ''' - if self.highestUsedLocation > self.textLocation: - searchLocation = self.highestUsedLocation - else: - searchLocation = self.textLocation + searchLocation = max(self.highestUsedLocation, self.textLocation) addSpaceAmount = 1 if addSpace else 0 if (searchLocation + len(text) + addSpaceAmount) > self.lineLength: return False diff --git a/music21/common/objects.py b/music21/common/objects.py index 0816639f1..69fd7c7ae 100644 --- a/music21/common/objects.py +++ b/music21/common/objects.py @@ -67,8 +67,7 @@ class RelativeCounter(collections.Counter): def __iter__(self): sortedKeys = sorted(super().__iter__(), key=lambda x: self[x], reverse=True) - for k in sortedKeys: - yield k + yield from sortedKeys def items(self): for k in self: diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py index 28746b69c..37c666dee 100644 --- a/music21/features/jSymbolic.py +++ b/music21/features/jSymbolic.py @@ -3016,8 +3016,7 @@ def process(self): for p in c.pitches: for gSub in p.groups: g.append(gSub) # add to temporary group; will act as a set - if len(g) > found: - found = len(g) + found = max(found, len(g)) self.feature.vector[0] = found diff --git a/music21/figuredBass/realizer.py b/music21/figuredBass/realizer.py index bd7d7fef1..ad606f133 100644 --- a/music21/figuredBass/realizer.py +++ b/music21/figuredBass/realizer.py @@ -186,10 +186,7 @@ def addLyricsToBassNote(bassNote, notationString=None): n = notation.Notation(notationString) if not n.figureStrings: return - maxLength = 0 - for fs in n.figureStrings: - if len(fs) > maxLength: - maxLength = len(fs) + maxLength = max([len(fs) for fs in n.figureStrings]) for fs in n.figureStrings: spacesInFront = '' for i in range(maxLength - len(fs)): diff --git a/music21/harmony.py b/music21/harmony.py index b3ea94eff..ab8611636 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -1890,7 +1890,7 @@ def _getKindFromShortHand(self, sH): for charString in getAbbreviationListGivenChordType(chordKind): if sH == charString: self.chordKind = chordKind - return originalsH.replace(charString, '') + return originalsH[len(sH):] return originalsH def _hasPitchAboveC4(self, pitches): @@ -3162,6 +3162,12 @@ def testExpressSusUsingAlterations(self): self.assertEqual(ch1.pitches, ch2.pitches) + def testDoubledCharacters(self): + ch1 = ChordSymbol('Co omit5') + ch2 = ChordSymbol('Cdim omit5') + + self.assertEqual(ch1.pitches, ch2.pitches) + def x_testPower(self): ''' power chords should not have inversions diff --git a/music21/humdrum/spineParser.py b/music21/humdrum/spineParser.py index ca33c5122..272ad8dd0 100644 --- a/music21/humdrum/spineParser.py +++ b/music21/humdrum/spineParser.py @@ -401,8 +401,7 @@ def parseEventListFromDataStream(self, dataStream=None): self.eventList.append(GlobalCommentLine(self.parsePositionInStream, line)) else: thisLine = SpineLine(self.parsePositionInStream, line) - if thisLine.numSpines > self.maxSpines: - self.maxSpines = thisLine.numSpines + self.maxSpines = max(self.maxSpines, thisLine.numSpines) self.eventList.append(thisLine) self.parsePositionInStream += 1 self.fileLength = self.parsePositionInStream diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index d6321e801..ed24b107c 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -1551,8 +1551,7 @@ def setPartsAndRefStream(self) -> None: else: innerStream.transferOffsetToElements() ht = innerStream.highestTime - if ht > self.highestTime: - self.highestTime = ht + self.highestTime = max(self.highestTime, ht) self.refStreamOrTimeRange = [0.0, self.highestTime] self.parts = list(s.parts) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index bc2d7098d..9c1ea7017 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -1967,8 +1967,7 @@ def xmlMeasureToMeasure(self, mxMeasure: ET.Element) -> stream.Measure: self.lastMeasureParser = measureParser - if measureParser.staves > self.maxStaves: - self.maxStaves = measureParser.staves + self.maxStaves = max(self.maxStaves, measureParser.staves) if measureParser.transposition is not None: self.updateTransposition(measureParser.transposition) diff --git a/music21/note.py b/music21/note.py index 11946ab51..c38b000b0 100644 --- a/music21/note.py +++ b/music21/note.py @@ -871,7 +871,7 @@ def fullName(self) -> str: @property def pitches(self) -> tuple[Pitch, ...]: ''' - Returns an empty tuple. (Useful for iterating over NotRests since they + Returns an empty tuple. (Useful for iterating over GeneralNotes since they include Notes and Chords.) ''' return () diff --git a/music21/scale/intervalNetwork.py b/music21/scale/intervalNetwork.py index a42b5fac5..1ecff02bb 100644 --- a/music21/scale/intervalNetwork.py +++ b/music21/scale/intervalNetwork.py @@ -828,8 +828,8 @@ def degreeMin(self): for n in self.nodes.values(): if x is None: x = n.degree - if n.degree < x: - x = n.degree + else: + x = min(x, n.degree) return x @property @@ -847,8 +847,8 @@ def degreeMax(self): for n in self.nodes.values(): if x is None: x = n.degree - if n.degree > x: - x = n.degree + else: + x = max(x, n.degree) return x @property @@ -870,8 +870,8 @@ def degreeMaxUnique(self): continue if x is None: x = n.degree - if n.degree > x: - x = n.degree + else: + x = max(x, n.degree) return x @property diff --git a/music21/stream/base.py b/music21/stream/base.py index 4dc799a1b..e407600f6 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -2828,8 +2828,7 @@ def insertAndShift(self, offsetOrItemOrList, itemOrNone=None): o = insertList[i] e = insertList[i + 1] qL = e.duration.quarterLength - if o + qL > highestTimeInsert: - highestTimeInsert = o + qL + highestTimeInsert = max(highestTimeInsert, o + qL) if lowestOffsetInsert is None or o < lowestOffsetInsert: lowestOffsetInsert = o i += 2 @@ -8425,8 +8424,7 @@ def highestTime(self): for e in self._elements: candidateOffset = (self.elementOffset(e) + e.duration.quarterLength) - if candidateOffset > highestTimeSoFar: - highestTimeSoFar = candidateOffset + highestTimeSoFar = max(highestTimeSoFar, candidateOffset) self._cache['HighestTime'] = opFrac(highestTimeSoFar) return self._cache['HighestTime'] @@ -11183,10 +11181,7 @@ def makeVoices(self, *, inPlace=False, fillGaps=True): # environLocal.printDebug(['makeVoices(): olDict', olDict]) # find the max necessary voices by finding the max number # of elements in each group; these may not all be necessary - maxVoiceCount = 1 - for group in olDict.values(): - if len(group) > maxVoiceCount: - maxVoiceCount = len(group) + maxVoiceCount = max([len(group) for group in olDict.values()] + [1]) if maxVoiceCount == 1: # nothing to do here if not inPlace: return returnObj diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py index 5c9bf2c40..3991982fe 100644 --- a/music21/stream/iterator.py +++ b/music21/stream/iterator.py @@ -508,8 +508,7 @@ def __contains__(self, item): def __reversed__(self): me = self.matchingElements() me.reverse() - for item in me: - yield item + yield from me def clone(self: StreamIteratorType) -> StreamIteratorType: ''' diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index f7f60b3df..f9dc2a4ac 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -523,8 +523,7 @@ def makeMeasures( refStreamHighestTime = refStreamOrTimeRange.highestTime else: # assume it's a list refStreamHighestTime = max(refStreamOrTimeRange) - if refStreamHighestTime > oMax: - oMax = refStreamHighestTime + oMax = max(oMax, refStreamHighestTime) # create a stream of measures to contain the offsets range defined # create as many measures as needed to fit in oMax diff --git a/music21/test/treeYield.py b/music21/test/treeYield.py index 89b2a76b0..eb8875165 100644 --- a/music21/test/treeYield.py +++ b/music21/test/treeYield.py @@ -66,16 +66,14 @@ def run(self, obj, memo=None): dictTuple = ('dict', keyX) self.stackVals.append(dictTuple) x = obj[keyX] - for z in self.run(x, memo=memo): - yield z + yield from self.run(x, memo=memo) self.stackVals.pop() elif tObj in [list, tuple]: for i, x in enumerate(obj): listTuple = ('listLike', i) self.stackVals.append(listTuple) - for z in self.run(x, memo=memo): - yield z + yield from self.run(x, memo=memo) self.stackVals.pop() else: # objects or uncaught types... @@ -95,8 +93,7 @@ def run(self, obj, memo=None): objTuple = ('getattr', x) self.stackVals.append(objTuple) try: - for z in self.run(gotValue, memo=memo): - yield z + yield from self.run(gotValue, memo=memo) except RuntimeError: raise ValueError(f'Maximum recursion on:\n{self.currentLevel()}') self.stackVals.pop() diff --git a/music21/tree/core.py b/music21/tree/core.py index 530785d1b..b1f12bf77 100644 --- a/music21/tree/core.py +++ b/music21/tree/core.py @@ -519,12 +519,10 @@ def __iter__(self): def recurse(node): if node is not None: if node.leftChild is not None: - for n in recurse(node.leftChild): - yield n + yield from recurse(node.leftChild) yield node if node.rightChild is not None: - for n in recurse(node.rightChild): - yield n + yield from recurse(node.rightChild) return recurse(self.rootNode) def populateFromSortedList(self, listOfTuples): diff --git a/music21/tree/node.py b/music21/tree/node.py index 2b7965e5c..191543c4b 100644 --- a/music21/tree/node.py +++ b/music21/tree/node.py @@ -273,18 +273,14 @@ def updateEndTimes(self): leftChild = self.leftChild if leftChild: leftChild.updateEndTimes() - if leftChild.endTimeLow < endTimeLow: - endTimeLow = leftChild.endTimeLow - if endTimeHigh < leftChild.endTimeHigh: - endTimeHigh = leftChild.endTimeHigh + endTimeLow = min(endTimeLow, leftChild.endTimeLow) + endTimeHigh = max(endTimeHigh, leftChild.endTimeHigh) rightChild = self.rightChild if rightChild: rightChild.updateEndTimes() - if rightChild.endTimeLow < endTimeLow: - endTimeLow = rightChild.endTimeLow - if endTimeHigh < rightChild.endTimeHigh: - endTimeHigh = rightChild.endTimeHigh + endTimeLow = min(endTimeLow, rightChild.endTimeLow) + endTimeHigh = max(endTimeHigh, rightChild.endTimeHigh) self.endTimeLow = endTimeLow self.endTimeHigh = endTimeHigh @@ -515,18 +511,14 @@ def updateEndTimes(self): leftChild = self.leftChild if leftChild: leftChild.updateEndTimes() - if leftChild.endTimeLow < endTimeLow: - endTimeLow = leftChild.endTimeLow - if endTimeHigh < leftChild.endTimeHigh: - endTimeHigh = leftChild.endTimeHigh + endTimeLow = min(endTimeLow, leftChild.endTimeLow) + endTimeHigh = max(endTimeHigh, leftChild.endTimeHigh) rightChild = self.rightChild if rightChild: rightChild.updateEndTimes() - if rightChild.endTimeLow < endTimeLow: - endTimeLow = rightChild.endTimeLow - if endTimeHigh < rightChild.endTimeHigh: - endTimeHigh = rightChild.endTimeHigh + endTimeLow = min(endTimeLow, rightChild.endTimeLow) + endTimeHigh = max(endTimeHigh, rightChild.endTimeHigh) self.endTimeLow = endTimeLow self.endTimeHigh = endTimeHigh diff --git a/music21/tree/timespanTree.py b/music21/tree/timespanTree.py index 5ad401330..fabe7db21 100644 --- a/music21/tree/timespanTree.py +++ b/music21/tree/timespanTree.py @@ -34,7 +34,7 @@ if t.TYPE_CHECKING: - from music21.tree.verticality import VerticalitySequence + from music21.tree.verticality import Verticality, VerticalitySequence environLocal = environment.Environment('tree.timespanTree') @@ -244,11 +244,15 @@ def removeTimespan(self, elements, offsets=None, runUpdate=True): ''' self.removeElements(elements, offsets, runUpdate) - def findNextPitchedTimespanInSameStreamByClass(self, pitchedTimespan, classList=None): + def findNextPitchedTimespanInSameStreamByClass( + self, + pitchedTimespan: spans.PitchedTimespan, + classList=None + ) -> spans.PitchedTimespan | None: r''' Finds next element timespan in the same stream class as `PitchedTimespan`. - Default classList is (stream.Part, ) + Default classList is (stream.Part,) >>> score = corpus.parse('bwv66.6') >>> scoreTree = score.asTimespans(classList=(note.Note,)) @@ -289,7 +293,11 @@ def findNextPitchedTimespanInSameStreamByClass(self, pitchedTimespan, classList= pitchedTimespan.getParentageByClass(classList)): return nextPitchedTimespan - def findPreviousPitchedTimespanInSameStreamByClass(self, pitchedTimespan, classList=None): + def findPreviousPitchedTimespanInSameStreamByClass( + self, + pitchedTimespan: spans.PitchedTimespan, + classList=None + ) -> spans.PitchedTimespan | None: r''' Finds next element timespan in the same Part/Measure, etc. (specify in classList) as the `pitchedTimespan`. @@ -445,8 +453,8 @@ def iterateConsonanceBoundedVerticalities(self): def iterateVerticalities( self, - reverse=False, - ): + reverse: bool = False, + ) -> Generator[Verticality, None, None]: r''' Iterates all vertical moments in this TimespanTree, represented as :class:`~music21.tree.verticality.Verticality` objects. diff --git a/music21/tree/verticality.py b/music21/tree/verticality.py index ee091e791..ebe8043cc 100644 --- a/music21/tree/verticality.py +++ b/music21/tree/verticality.py @@ -20,6 +20,7 @@ import copy import itertools import typing as t +from typing import overload import unittest from music21 import chord @@ -32,11 +33,19 @@ # from music21 import key # from music21 import pitch from music21.common.types import OffsetQL, OffsetQLIn - from music21.tree import spans +if t.TYPE_CHECKING: + from music21.tree.trees import OffsetTree + from music21.voiceLeading import VoiceLeadingQuartet + environLocal = environment.Environment('tree.verticality') +PitchedTimespanQuartet = tuple[ + tuple[spans.PitchedTimespan, spans.PitchedTimespan], + tuple[spans.PitchedTimespan, spans.PitchedTimespan], +] + class VerticalityException(exceptions21.TreeException): pass @@ -123,6 +132,7 @@ class Verticality(prebase.ProtoM21Object): # CLASS VARIABLES # __slots__ = ( + 'offsetTree', 'timespanTree', 'overlapTimespans', 'startTimespans', @@ -131,8 +141,11 @@ class Verticality(prebase.ProtoM21Object): ) _DOC_ATTR: dict[str, str] = { + 'offsetTree': r''' + Returns the tree initially set else None + ''', 'timespanTree': r''' - Returns the timespanTree initially set. + Returns the tree initially set if it was a TimespanTree, else None ''', 'overlapTimespans': r''' Gets timespans overlapping the start offset of a verticality. @@ -202,32 +215,33 @@ class Verticality(prebase.ProtoM21Object): def __init__( self, - offset=None, - overlapTimespans=(), - startTimespans=(), - stopTimespans=(), + offset: OffsetQL = 0.0, + overlapTimespans: tuple[spans.ElementTimespan, ...] = (), + startTimespans: tuple[spans.ElementTimespan, ...] = (), + stopTimespans: tuple[spans.ElementTimespan, ...] = (), timespanTree=None, ): - from music21.tree import trees - if timespanTree is not None and not isinstance(timespanTree, trees.OffsetTree): - raise VerticalityException( - f'timespanTree {timespanTree!r} is not a OffsetTree or None') + from music21.tree.timespanTree import TimespanTree + self.offsetTree: OffsetTree | None = timespanTree + self.timespanTree: TimespanTree | None = None + if isinstance(timespanTree, TimespanTree): + self.timespanTree = timespanTree - self.timespanTree = timespanTree - self.offset = offset + self.offset: OffsetQL = offset if not isinstance(startTimespans, tuple): - raise VerticalityException(f'startTimespans must be a tuple, not {startTimespans!r}') - if not isinstance(stopTimespans, (tuple, type(None))): raise VerticalityException( - f'stopTimespans must be a tuple or None, not {stopTimespans!r}') - if not isinstance(overlapTimespans, (tuple, type(None))): + f'startTimespans must be a tuple of ElementTimespans, not {startTimespans!r}') + if not isinstance(stopTimespans, tuple): + raise VerticalityException( + f'stopTimespans must be a tuple of ElementTimespans, not {stopTimespans!r}') + if not isinstance(overlapTimespans, tuple): raise VerticalityException( - f'overlapTimespans must be a tuple or None, not {overlapTimespans!r}') + f'overlapTimespans must be a tuple of ElementTimespans, not {overlapTimespans!r}') - self.startTimespans = startTimespans - self.stopTimespans = stopTimespans - self.overlapTimespans = overlapTimespans + self.startTimespans: tuple[spans.ElementTimespan, ...] = startTimespans + self.stopTimespans: tuple[spans.ElementTimespan, ...] = stopTimespans + self.overlapTimespans: tuple[spans.ElementTimespan, ...] = overlapTimespans # SPECIAL METHODS # @@ -350,7 +364,7 @@ def nextStartOffset(self) -> float|None: If a verticality has no tree attached, then it will return None ''' - tree = self.timespanTree + tree = self.offsetTree if tree is None: return None offset = tree.getPositionAfter(self.offset) @@ -382,7 +396,7 @@ def nextVerticality(self): >>> verticality.nextVerticality ''' - tree = self.timespanTree + tree = self.offsetTree if tree is None: return None offset = tree.getPositionAfter(self.offset) @@ -496,7 +510,7 @@ def previousVerticality(self): >>> verticality.previousVerticality ''' - tree = self.timespanTree + tree = self.offsetTree if tree is None: return None offset = tree.getPositionBefore(self.offset) @@ -777,6 +791,9 @@ def newNote(ts, n: note.Note) -> note.Note: return nNew offsetDifference = common.opFrac(self.offset - ts.offset) + if t.TYPE_CHECKING: + assert quarterLength is not None + endTimeDifference = common.opFrac(ts.endTime - (self.offset + quarterLength)) if t.TYPE_CHECKING: assert endTimeDifference is not None @@ -933,16 +950,43 @@ def conditionalAdd(ts, n: note.Note) -> None: return c # Analysis type things... + @overload def getAllVoiceLeadingQuartets( self, *, includeRests=True, includeOblique=True, includeNoMotion=False, - returnObjects=True, - partPairNumbers=None - ): - # noinspection PyShadowingNames + returnObjects: t.Literal[False], + partPairNumbers: list[tuple[int, int]] | None = None + ) -> list[PitchedTimespanQuartet]: + # dummy until Astroid #1015 is fixed. Replace with ... + return [] + + @overload + def getAllVoiceLeadingQuartets( + self, + *, + includeRests=True, + includeOblique=True, + includeNoMotion=False, + returnObjects: t.Literal[True] = True, + partPairNumbers: list[tuple[int, int]] | None = None + ) -> list[VoiceLeadingQuartet]: + # dummy until Astroid #1015 is fixed. Replace with ... + return [] + + + def getAllVoiceLeadingQuartets( + self, + *, + includeRests=True, + includeOblique=True, + includeNoMotion=False, + returnObjects: bool = True, + partPairNumbers: list[tuple[int, int]] | None = None + ) -> list[VoiceLeadingQuartet] | list[PitchedTimespanQuartet]: + # noinspection PyShadowingNames,PyCallingNonCallable ''' >>> c = corpus.parse('luca/gloria').measures(1, 8) >>> tsCol = tree.fromStream.asTimespans(c, flatten=True, @@ -971,7 +1015,7 @@ def getAllVoiceLeadingQuartets( [] - Raw output + Raw output, returns a 2-element tuple of 2-element tuples of PitchedTimespans >>> for vlqRaw in verticality22.getAllVoiceLeadingQuartets(returnObjects=False): ... pp(vlqRaw) @@ -1002,28 +1046,37 @@ def getAllVoiceLeadingQuartets( * Changed in v8: all parameters are keyword only. ''' + if not self.timespanTree: + raise VerticalityException('Cannot iterate without .timespanTree defined') + from music21.voiceLeading import VoiceLeadingQuartet - pairedMotionList = self.getPairedMotion(includeRests=includeRests, - includeOblique=includeOblique) - allQuartets = itertools.combinations(pairedMotionList, 2) - filteredList = [] + pairedMotionList: list[ + tuple[spans.PitchedTimespan, spans.PitchedTimespan] + ] = self.getPairedMotion( + includeRests=includeRests, + includeOblique=includeOblique + ) + allPairedMotion = itertools.combinations(pairedMotionList, 2) + filteredList: list[PitchedTimespanQuartet] = [] + filteredQuartet: list[VoiceLeadingQuartet] = [] verticalityStreamParts = self.timespanTree.source.parts - for thisQuartet in allQuartets: - if not hasattr(thisQuartet[0][0], 'pitches'): + pairedMotion: PitchedTimespanQuartet + for pairedMotion in allPairedMotion: + if not hasattr(pairedMotion[0][0], 'pitches'): continue # not a PitchedTimespan if includeNoMotion is False: - if (thisQuartet[0][0].pitches == thisQuartet[0][1].pitches - and thisQuartet[1][0].pitches == thisQuartet[1][1].pitches): + if (pairedMotion[0][0].pitches == pairedMotion[0][1].pitches + and pairedMotion[1][0].pitches == pairedMotion[1][1].pitches): continue if partPairNumbers is not None: isAppropriate = False for pp in partPairNumbers: - thisQuartetTopPart = thisQuartet[0][0].part - thisQuartetBottomPart = thisQuartet[1][0].part + thisQuartetTopPart = pairedMotion[0][0].part + thisQuartetBottomPart = pairedMotion[1][0].part if ((verticalityStreamParts[pp[0]] == thisQuartetTopPart or verticalityStreamParts[pp[0]] == thisQuartetBottomPart) and (verticalityStreamParts[pp[1]] == thisQuartetTopPart @@ -1034,23 +1087,31 @@ def getAllVoiceLeadingQuartets( continue if returnObjects is False: - filteredList.append(thisQuartet) + filteredList.append(pairedMotion) else: - n11 = thisQuartet[0][0].element - n12 = thisQuartet[0][1].element - n21 = thisQuartet[1][0].element - n22 = thisQuartet[1][1].element - - if (n11 is not None - and n12 is not None - and n21 is not None - and n22 is not None): + n11 = pairedMotion[0][0].element + n12 = pairedMotion[0][1].element + n21 = pairedMotion[1][0].element + n22 = pairedMotion[1][1].element + + # fail on Chords for now. + if (isinstance(n11, note.Note) + and isinstance(n12, note.Note) + and isinstance(n21, note.Note) + and isinstance(n22, note.Note)): vlq = VoiceLeadingQuartet(n11, n12, n21, n22) - filteredList.append(vlq) + filteredQuartet.append(vlq) + if returnObjects: + return filteredQuartet return filteredList - def getPairedMotion(self, includeRests=True, includeOblique=True): + def getPairedMotion( + self, + *, + includeRests: bool = True, + includeOblique: bool = True + ) -> list[tuple[spans.PitchedTimespan, spans.PitchedTimespan]]: ''' Get a list of two-element tuples that are in the same part [TODO: or containing stream??] @@ -1097,16 +1158,23 @@ def getPairedMotion(self, includeRests=True, includeOblique=True): ... print(pm) (>, >) + + Changed in v9.3 -- arguments are keyword only ''' + if not self.timespanTree: + return [] + stopTss = self.stopTimespans startTss = self.startTimespans overlapTss = self.overlapTimespans - allPairedMotions = [] + allPairedMotions: list[tuple[spans.PitchedTimespan, spans.PitchedTimespan]] = [] for startingTs in startTss: + if not isinstance(startingTs, spans.PitchedTimespan): + continue previousTs = self.timespanTree.findPreviousPitchedTimespanInSameStreamByClass( startingTs) - if previousTs is None: + if previousTs is None or not isinstance(previousTs, spans.PitchedTimespan): continue # first not in piece in this part... if includeRests is False: @@ -1119,6 +1187,8 @@ def getPairedMotion(self, includeRests=True, includeOblique=True): if includeOblique is True: for overlapTs in overlapTss: + if not isinstance(overlapTs, spans.PitchedTimespan): + continue tsTuple = (overlapTs, overlapTs) allPairedMotions.append(tsTuple) diff --git a/music21/voiceLeading.py b/music21/voiceLeading.py index b722b136a..ad107fdc0 100644 --- a/music21/voiceLeading.py +++ b/music21/voiceLeading.py @@ -19,7 +19,7 @@ The list of objects included here are: * :class:`~music21.voiceLeading.VoiceLeadingQuartet` : two by two matrix of notes - +* :func:`~music21.voiceLeading.iterateAllVoiceLeadingQuartets` : yields each VLQ in a piece. * :class:`~music21.voiceLeading.Verticality` : vertical context in a score, composed of any music21 objects * :class:`~music21.voiceLeading.VerticalityNTuplet` : group of three @@ -35,6 +35,7 @@ ''' from __future__ import annotations +from collections.abc import Generator import enum import typing as t import unittest @@ -51,6 +52,8 @@ from music21 import pitch from music21 import scale +if t.TYPE_CHECKING: + from music21 import stream # from music21 import harmony can't do this either # from music21 import roman Can't import roman because of circular @@ -1554,11 +1557,7 @@ def getShortestDuration(self): >>> vs1.getShortestDuration() 1.0 ''' - leastQuarterLength = self.objects[0].quarterLength - for obj in self.objects: - if obj.quarterLength < leastQuarterLength: - leastQuarterLength = obj.quarterLength - return leastQuarterLength + return min([obj.quarterLength for obj in self.objects]) def getLongestDuration(self): ''' @@ -1574,11 +1573,7 @@ def getLongestDuration(self): >>> vs1.getLongestDuration() 4.0 ''' - longestQuarterLength = self.objects[0].quarterLength - for obj in self.objects: - if obj.quarterLength > longestQuarterLength: - longestQuarterLength = obj.quarterLength - return longestQuarterLength + return max([obj.quarterLength for obj in self.objects]) def changeDurationOfAllObjects(self, newQuarterLength): ''' @@ -2414,6 +2409,42 @@ def bassInterval(self): return interval.notesToChromatic(self.chordList[0].bass(), self.chordList[1].bass()) +def iterateAllVoiceLeadingQuartets( + s: stream.Stream, + *, + includeRests: bool = True, + includeOblique: bool = True, + includeNoMotion: bool = False, + reverse: bool = False, +) -> Generator[VoiceLeadingQuartet, None, None]: + ''' + Iterate through all VoiceLeading quartets in a Stream (generally a Score), + yielding a generator of VoiceLeadingQuartets. N.B. does not yet support Streams with + Chords in them. + + >>> b = corpus.parse('bwv66.6') + >>> for vlq in voiceLeading.iterateAllVoiceLeadingQuartets(b): + ... print(vlq.v1n1.measureNumber, + ... vlq.v1n1.getContextByClass(stream.Part).id, + ... vlq.v2n1.getContextByClass(stream.Part).id, + ... vlq) + 0 Soprano Tenor + 0 Soprano Bass + 0 Tenor Bass + 1 Soprano Alto + 1 Soprano Tenor + 1 Soprano Bass + 1 Alto Tenor + 1 Alto Bass + ... + ''' + for v in s.asTimespans().iterateVerticalities(reverse=reverse): + yield from v.getAllVoiceLeadingQuartets( + includeRests=includeRests, + includeOblique=includeOblique, + includeNoMotion=includeNoMotion + ) + # ------------------------------------------------------------------------------ class Test(unittest.TestCase):