From f53ba68c9f35d448f8039aee0c3ad3a65eee0c95 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 9 Jan 2021 13:02:42 -0500 Subject: [PATCH 01/31] Apply tuplet to multiple components to express 5/6 or 7/3 QL --- music21/duration.py | 62 ++++++++++++++++++++++++++++++++---- music21/musicxml/m21ToXml.py | 11 +++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index f7d09815f2..2768c7b315 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -589,16 +589,31 @@ def quarterConversion(qLen): tuplet=None) - Since tuplets now apply to the entire Duration, expect some odder tuplets for unusual - values that should probably be split generally... + Since tuplets now apply to the entire Duration, multiple small components may be needed: + + Duration > 1.0 QL: >>> duration.quarterConversion(7/3) - QuarterLengthConversion(components=(DurationTuple(type='whole', dots=0, quarterLength=4.0),), - tuplet=) + QuarterLengthConversion(components=(DurationTuple(type='eighth', dots=0, quarterLength=0.5), + DurationTuple(type='eighth', dots=0, quarterLength=0.5), + DurationTuple(type='eighth', dots=0, quarterLength=0.5), + DurationTuple(type='eighth', dots=0, quarterLength=0.5), + DurationTuple(type='eighth', dots=0, quarterLength=0.5), + DurationTuple(type='eighth', dots=0, quarterLength=0.5), + DurationTuple(type='eighth', dots=0, quarterLength=0.5)), + tuplet=) + Duration < 1.0 QL: - This is a very close approximation: + >>> duration.quarterConversion(5/6) + QuarterLengthConversion(components=(DurationTuple(type='16th', dots=0, quarterLength=0.25), + DurationTuple(type='16th', dots=0, quarterLength=0.25), + DurationTuple(type='16th', dots=0, quarterLength=0.25), + DurationTuple(type='16th', dots=0, quarterLength=0.25), + DurationTuple(type='16th', dots=0, quarterLength=0.25)), + tuplet=) + This is a very close approximation: >>> duration.quarterConversion(0.18333333333333) QuarterLengthConversion(components=(DurationTuple(type='16th', dots=0, quarterLength=0.25),), @@ -608,7 +623,6 @@ def quarterConversion(qLen): QuarterLengthConversion(components=(DurationTuple(type='zero', dots=0, quarterLength=0.0),), tuplet=None) - >>> duration.quarterConversion(99.0) QuarterLengthConversion(components=(DurationTuple(type='inexpressible', dots=0, @@ -664,8 +678,35 @@ def quarterConversion(qLen): # is it built up of many small types? components = [durationTupleFromTypeDots(closestSmallerType, 0)] # remove the largest type out there and keep going. - qLenRemainder = opFrac(qLen - typeToDuration[closestSmallerType]) + + # one opportunity to define a tuplet + # if the remainder can be expressed as 1/n QL and n is reasonably small + # example: 5/6 QL = 1/2 QL largest type + 1/3 QL remainder + # can be expressed as 5 (3 + 2) components of 1/6 QL + for qLenDenominator in defaultTupletNumerators: + hypotheticalTupletQL = opFrac(1 / qLenDenominator) + if qLenRemainder == hypotheticalTupletQL: + if components[0].quarterLength >= 1: + # express largest type in units of remainder + numComponentsA = int(components[0].quarterLength / qLenRemainder) + # remainder is 1 unit + numComponentsB = 1 + newType = quarterLengthToTuplet(hypotheticalTupletQL, 1)[0].durationNormal.type + else: + # express largest type in units of 1 / remainder + numComponentsA = int(1 / qLenRemainder) + # express remainder in units of 1 / largest type + numComponentsB = int(1 / (components[0].quarterLength)) + newType = quarterLengthToClosestType(qLenRemainder)[0] + newComponents = [] + for i in range(0, numComponentsA + numComponentsB): + newComponents.append(durationTupleFromTypeDots(newType)) + return QuarterLengthConversion( + tuple(newComponents), + quarterLengthToTuplet(hypotheticalTupletQL, 1)[0] + ) + # cannot recursively call, because tuplets are not possible at this stage. # environLocal.warn(['starting remainder search for qLen:', qLen, # 'remainder: ', qLenRemainder, 'components: ', components]) @@ -3556,6 +3597,13 @@ def testTupletDurations(self): Duration(fractions.Fraction(6 / 7)).fullName ) + def testDeriveComponentsForTuplet(self): + self.assertEqual( + ('16th Triplet (5/6 QL) tied to ' * 4) + + '16th Triplet (5/6 QL) (5/6 total QL)', + Duration(fractions.Fraction(5 / 6)).fullName + ) + # ------------------------------------------------------------------------------- # define presented order in documentation diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 99e15306b0..c7c61a2865 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2807,6 +2807,10 @@ def parseOneElement(self, obj): # split at durations... if 'GeneralNote' in classes and obj.duration.type == 'complex': objList = obj.splitAtDurations() + tempStream = stream.Stream() + for o in objList: + tempStream.append(o) + stream.makeNotation.makeTupletBrackets(tempStream, inPlace=True) else: objList = [obj] @@ -6316,6 +6320,13 @@ def testTextExpressionOffset(self): for direction in tree.findall('.//direction'): self.assertIsNone(direction.find('offset')) + def testTupletBracketsMadeOnComponents(self): + s = stream.Stream() + s.insert(0, note.Note(quarterLength=(5/6))) + tree = self.getET(s) + # 3 sixteenth-triplets + 2 sixteenth-triplets + # tuplet start, tuplet stop, tuplet start, tuplet stop + self.assertEqual(len(tree.findall('.//tuplet')), 4) class TestExternal(unittest.TestCase): # pragma: no cover From e3fa8b9d17e81a8b9e4a8fefab9c443e0eec3c10 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 9 Jan 2021 13:20:21 -0500 Subject: [PATCH 02/31] Whitespace --- music21/musicxml/m21ToXml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index c7c61a2865..42ee5daad3 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6322,7 +6322,7 @@ def testTextExpressionOffset(self): def testTupletBracketsMadeOnComponents(self): s = stream.Stream() - s.insert(0, note.Note(quarterLength=(5/6))) + s.insert(0, note.Note(quarterLength=(5 / 6))) tree = self.getET(s) # 3 sixteenth-triplets + 2 sixteenth-triplets # tuplet start, tuplet stop, tuplet start, tuplet stop From dfac936297a13efecee4df47fa2bd9ebf189e6cf Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 9 Jan 2021 13:53:37 -0500 Subject: [PATCH 03/31] Liberalize choices for 1/n --- music21/duration.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 2768c7b315..9dcfe6fda2 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -141,7 +141,8 @@ extendedTupletNumerators = (3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199) - +# non-powers of 2 below 2^4 +defaultTupletRemainderDenominators = (3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15) def unitSpec(durationObjectOrObjects): ''' @@ -681,10 +682,10 @@ def quarterConversion(qLen): qLenRemainder = opFrac(qLen - typeToDuration[closestSmallerType]) # one opportunity to define a tuplet - # if the remainder can be expressed as 1/n QL and n is reasonably small + # if the remainder can be expressed as 1/n QL and n is a non-power of 2 under 2^4 # example: 5/6 QL = 1/2 QL largest type + 1/3 QL remainder # can be expressed as 5 (3 + 2) components of 1/6 QL - for qLenDenominator in defaultTupletNumerators: + for qLenDenominator in defaultTupletRemainderDenominators: hypotheticalTupletQL = opFrac(1 / qLenDenominator) if qLenRemainder == hypotheticalTupletQL: if components[0].quarterLength >= 1: From 0cfdbbbb85b883e83753efb46f002f0b40ea44aa Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 9 Jan 2021 18:38:31 -0500 Subject: [PATCH 04/31] Add expressionIsInferred --- music21/duration.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 9dcfe6fda2..2f500eed8a 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1447,7 +1447,12 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): half tied to quintuplet sixteenth note" or simply "quarter note." A Duration object is made of one or more immutable DurationTuple objects stored on the - `components` list. + `components` list. A Duration created by setting `quarterLength` sets the attribute + `expressionIsInferred` to True, which indicates that consuming functions or applications + can express this Duration using another combination of components that sums to the + `quarterLength`. Otherwise, `expressionIsInferred` is set to False, indicating that + components are not allowed to mutate. + (N.B.: `music21` does not yet implement such mutating components.) Multiple DurationTuples in a single Duration may be used to express tied notes, or may be used to split duration across barlines or beam groups. @@ -1484,11 +1489,16 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): (DurationTuple(type='eighth', dots=0, quarterLength=0.5), DurationTuple(type='32nd', dots=0, quarterLength=0.125)) + >>> d2.expressionIsInferred + True + Example 3: A Duration configured by keywords. >>> d3 = duration.Duration(type='half', dots=2) >>> d3.quarterLength 3.5 + >>> d3.expressionIsInferred + False ''' # CLASS VARIABLES # @@ -1505,7 +1515,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): '_typeNeedsUpdating', '_unlinkedType', '_dotGroups', - + 'expressionIsInferred', '_client' ) @@ -1531,6 +1541,8 @@ def __init__(self, *arguments, **keywords): # defer updating until necessary self._quarterLengthNeedsUpdating = False self._linked = True + + self.expressionIsInferred = False for a in arguments: if common.isNum(a) and 'quarterLength' not in keywords: keywords['quarterLength'] = a @@ -1559,6 +1571,7 @@ def __init__(self, *arguments, **keywords): # permit as keyword so can be passed from notes elif 'quarterLength' in keywords: self.quarterLength = keywords['quarterLength'] + self.expressionIsInferred = True if 'client' in keywords: self.client = keywords['client'] @@ -1739,10 +1752,10 @@ def addDurationTuple(self, dur): if isinstance(dur, DurationTuple): self._components.append(dur) - elif isinstance(dur, Duration): # its a Duration object + elif isinstance(dur, Duration): # it's a Duration object for c in dur.components: self._components.append(c) - else: # its a number that may produce more than one component + else: # it's a number that may produce more than one component for c in Duration(dur).components: self._components.append(c) From 7d0e21f323094525d8daf74512c8f9eec65b236d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 10 Jan 2021 18:19:02 -0500 Subject: [PATCH 05/31] Improve implementation and fix tuplet type --- music21/duration.py | 58 +++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 2f500eed8a..7e1839fe39 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -612,7 +612,7 @@ def quarterConversion(qLen): DurationTuple(type='16th', dots=0, quarterLength=0.25), DurationTuple(type='16th', dots=0, quarterLength=0.25), DurationTuple(type='16th', dots=0, quarterLength=0.25)), - tuplet=) + tuplet=) This is a very close approximation: @@ -681,32 +681,27 @@ def quarterConversion(qLen): # remove the largest type out there and keep going. qLenRemainder = opFrac(qLen - typeToDuration[closestSmallerType]) - # one opportunity to define a tuplet - # if the remainder can be expressed as 1/n QL and n is a non-power of 2 under 2^4 - # example: 5/6 QL = 1/2 QL largest type + 1/3 QL remainder - # can be expressed as 5 (3 + 2) components of 1/6 QL - for qLenDenominator in defaultTupletRemainderDenominators: - hypotheticalTupletQL = opFrac(1 / qLenDenominator) - if qLenRemainder == hypotheticalTupletQL: - if components[0].quarterLength >= 1: - # express largest type in units of remainder - numComponentsA = int(components[0].quarterLength / qLenRemainder) - # remainder is 1 unit - numComponentsB = 1 - newType = quarterLengthToTuplet(hypotheticalTupletQL, 1)[0].durationNormal.type - else: - # express largest type in units of 1 / remainder - numComponentsA = int(1 / qLenRemainder) - # express remainder in units of 1 / largest type - numComponentsB = int(1 / (components[0].quarterLength)) - newType = quarterLengthToClosestType(qLenRemainder)[0] - newComponents = [] - for i in range(0, numComponentsA + numComponentsB): - newComponents.append(durationTupleFromTypeDots(newType)) - return QuarterLengthConversion( - tuple(newComponents), - quarterLengthToTuplet(hypotheticalTupletQL, 1)[0] - ) + # one opportunity to define a tuplet if remainder can be expressed as one + # by expressing the largest type (components[0]) in terms of the same tuplet + if isinstance(qLenRemainder, fractions.Fraction): + largestType = components[0] + divisor = 1 + if largestType.quarterLength < 1: + # Subdivide by one level (divide by 2) + divisor = 2 + solutions = quarterLengthToTuplet(qLenRemainder / divisor) + if solutions: + tup = solutions[0] + if largestType.quarterLength % tup.totalTupletLength() == 0: + multiples = int(largestType.quarterLength // tup.totalTupletLength()) + numComponentsLargestType = multiples * tup.numberNotesActual + numComponentsRemainder = int( + (qLenRemainder / tup.totalTupletLength()) + * tup.numberNotesActual + ) + numComponentsTotal = numComponentsLargestType + numComponentsRemainder + components = [tup.durationActual for i in range(0, numComponentsTotal)] + return QuarterLengthConversion(tuple(components), tup) # cannot recursively call, because tuplets are not possible at this stage. # environLocal.warn(['starting remainder search for qLen:', qLen, @@ -2336,6 +2331,9 @@ def updateQuarterLength(self): DEPRECATED -- this is no longer needed except by duration developers and will be removed in v.7 + + OMIT_FROM_DOCS + JTW: still in use Jan 2021 if ._componentsNeedUpdating = True ''' if self.linked is True: self._qtrLength = opFrac(self.quarterLengthNoTuplets * self.aggregateTupletMultiplier()) @@ -3617,7 +3615,11 @@ def testDeriveComponentsForTuplet(self): + '16th Triplet (5/6 QL) (5/6 total QL)', Duration(fractions.Fraction(5 / 6)).fullName ) - + self.assertEqual( + ('32nd Triplet (5/12 QL) tied to ' * 4) + + '32nd Triplet (5/12 QL) (5/12 total QL)', + Duration(fractions.Fraction(5 / 12)).fullName + ) # ------------------------------------------------------------------------------- # define presented order in documentation From d38df115add68bd08f149db200b20feba53ddd0a Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 10 Jan 2021 18:22:13 -0500 Subject: [PATCH 06/31] whitespace --- music21/duration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/music21/duration.py b/music21/duration.py index 7e1839fe39..563bc1e0da 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -3621,6 +3621,7 @@ def testDeriveComponentsForTuplet(self): Duration(fractions.Fraction(5 / 12)).fullName ) + # ------------------------------------------------------------------------------- # define presented order in documentation _DOC_ORDER = [Duration, Tuplet, convertQuarterLengthToType, TupletFixer] From 67b1d0185a82b1ee225ee07989104f189368487f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 10 Jan 2021 18:44:36 -0500 Subject: [PATCH 07/31] splitAtDurations keyword in makeNotation --- music21/duration.py | 3 +-- music21/musicxml/m21ToXml.py | 24 ++--------------------- music21/stream/__init__.py | 37 ++++++++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 563bc1e0da..02d7a8432b 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -141,8 +141,7 @@ extendedTupletNumerators = (3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199) -# non-powers of 2 below 2^4 -defaultTupletRemainderDenominators = (3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15) + def unitSpec(durationObjectOrObjects): ''' diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 42ee5daad3..15255ca764 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2796,30 +2796,11 @@ def parseOneElement(self, obj): if 'GeneralNote' in classes: self.offsetInMeasure += obj.duration.quarterLength - # turn inexpressible durations into complex durations (unless unlinked) - if obj.duration.type == 'inexpressible': - obj.duration.quarterLength = obj.duration.quarterLength - - # make dotGroups into normal notes - if len(obj.duration.dotGroups) > 1: - obj.duration.splitDotGroups(inPlace=True) - - # split at durations... - if 'GeneralNote' in classes and obj.duration.type == 'complex': - objList = obj.splitAtDurations() - tempStream = stream.Stream() - for o in objList: - tempStream.append(o) - stream.makeNotation.makeTupletBrackets(tempStream, inPlace=True) - else: - objList = [obj] - parsedObject = False for className, methName in self.classesToMethods.items(): if className in classes: meth = getattr(self, methName) - for o in objList: - meth(o) + meth(obj) parsedObject = True break @@ -2828,8 +2809,7 @@ def parseOneElement(self, obj): for className, methName in self.wrapAttributeMethodClasses.items(): if className in classes: meth = getattr(self, methName) - for o in objList: - self.wrapObjectInAttributes(o, meth) + self.wrapObjectInAttributes(obj, meth) parsedObject = True break diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index d441e07f0e..54ba4ccd66 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -6243,6 +6243,7 @@ def makeNotation(self, refStreamOrTimeRange=None, inPlace=False, bestClef=False, + splitAtDurations=True, **subroutineKeywords): ''' This method calls a sequence of Stream methods on this Stream to prepare @@ -6265,6 +6266,9 @@ def makeNotation(self, 4 >>> sMeasures.getElementsByClass('Measure')[-1].rightBarline.type 'final' + + Added in v6.7 -- `splitAtDurations` keyword + TODO: doctest ''' # determine what is the object to work on first if inPlace: @@ -6279,6 +6283,31 @@ def makeNotation(self, # retrieve necessary spanners; insert only if making a copy returnStream.coreGatherMissingSpanners(insert=not inPlace) + + if splitAtDurations: + for container in returnStream.recurse(includeSelf=True, classFilter=('Stream')): + for noteObj in container.getElementsByClass('GeneralNote'): + # turn inexpressible durations into complex durations (unless unlinked) + if noteObj.duration.type == 'inexpressible': + noteObj.duration.quarterLength = noteObj.duration.quarterLength + + # make dotGroups into normal notes + if len(noteObj.duration.dotGroups) > 1: + noteObj.duration.splitDotGroups(inPlace=True) + # TODO: QUESTION(JTW): also need to replace/insert these? + + # split at durations + if noteObj.duration.type == 'complex': + container.streamStatus._dirty = True + + insertPoint = noteObj.offset + objList = noteObj.splitAtDurations() + container.replace(noteObj, objList[0]) + insertPoint += objList[0].quarterLength + for subsequent in objList[1:]: + container.insert(insertPoint, subsequent) + insertPoint += subsequent.quarterLength + # only use inPlace arg on first usage if not self.hasMeasures(): # only try to make voices if no Measures are defined @@ -6353,8 +6382,9 @@ def makeNotation(self, # check for tuplet brackets one measure at a time # this means that they will never extend beyond one measure for m in measureStream: - if m.streamStatus.haveTupletBracketsBeenMade() is False: + if m.streamStatus.haveTupletBracketsBeenMade() is False or m.streamStatus._dirty: makeNotation.makeTupletBrackets(m, inPlace=True) + m.streamStatus._dirty = False if not measureStream: raise StreamException( @@ -13321,6 +13351,7 @@ def makeNotation(self, refStreamOrTimeRange=None, inPlace=False, bestClef=False, + splitAtDurations=True, **subroutineKeywords): ''' This method overrides the makeNotation method on Stream, @@ -13345,16 +13376,18 @@ def makeNotation(self, refStreamOrTimeRange=refStreamOrTimeRange, inPlace=True, bestClef=bestClef, + splitAtDurations=splitAtDurations, **subroutineKeywords) # note: while the local-streams have updated their caches, the # containing score has an out-of-date cache of flat. - # this, must call elements changed + # thus, must call elements changed returnStream.coreElementsChanged() else: # call the base method super(Score, returnStream).makeNotation(meterStream=meterStream, refStreamOrTimeRange=refStreamOrTimeRange, inPlace=True, bestClef=bestClef, + splitAtDurations=splitAtDurations, **subroutineKeywords) if inPlace: From 59bc88d794635ce41b67b34d28a4bd475d715da3 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 10 Jan 2021 21:48:22 -0500 Subject: [PATCH 08/31] Fix test --- music21/musicxml/m21ToXml.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 15255ca764..03ed3398b4 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6303,10 +6303,7 @@ def testTextExpressionOffset(self): def testTupletBracketsMadeOnComponents(self): s = stream.Stream() s.insert(0, note.Note(quarterLength=(5 / 6))) - tree = self.getET(s) - # 3 sixteenth-triplets + 2 sixteenth-triplets - # tuplet start, tuplet stop, tuplet start, tuplet stop - self.assertEqual(len(tree.findall('.//tuplet')), 4) + self.assertEqual(self.getXml(s).count(' Date: Sun, 10 Jan 2021 21:51:09 -0500 Subject: [PATCH 09/31] Add back clarifying comment --- music21/musicxml/m21ToXml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 03ed3398b4..47c4b5e6a3 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6303,6 +6303,8 @@ def testTextExpressionOffset(self): def testTupletBracketsMadeOnComponents(self): s = stream.Stream() s.insert(0, note.Note(quarterLength=(5 / 6))) + # 3 sixteenth-tuplets, 2 sixteenth-tuplets + # tuplet start, tuplet stop, tuplet start, tuplet stop self.assertEqual(self.getXml(s).count(' Date: Fri, 29 Jan 2021 07:54:15 -0500 Subject: [PATCH 10/31] Permit mixing tuplet types having the same multiplier under a bracket --- music21/stream/makeNotation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index c8d4309f16..3cd05d1d6a 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1285,7 +1285,9 @@ def makeTupletBrackets(s, *, inPlace=False): # this, below, is optional: # if next normal type is not the same as this one, also stop - elif tupletNext is None or completionCount >= completionTarget: + elif (tupletNext is None + or completionCount == completionTarget + or tupletPrevious.tupletMultiplier() != tupletObj.tupletMultiplier()): tupletObj.type = 'stop' # should be impossible once frozen... completionTarget = None # reset completionCount = 0 # reset From 5247632d7b2a263f16be6e930a4a04eb81ffdf84 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 8 Feb 2021 19:56:13 -0500 Subject: [PATCH 11/31] Raise if a complex note makes it to the xml exporter --- music21/musicxml/m21ToXml.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 88473712ff..f46e4e7376 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3234,15 +3234,10 @@ def noteToXml(self, n: note.GeneralNote, noteIndexInChord=0, chordParent=None): >>> nComplex = note.Note() >>> nComplex.duration.quarterLength = 5.0 >>> mxComplex = MEX.noteToXml(nComplex) - >>> MEX.dump(mxComplex) - - - C - 4 - - 50400 - complex - + Traceback (most recent call last): + music21.musicxml.xmlObjects.MusicXMLExportException: + complex duration encountered: + failure to run myStream.makeNotation(splitAtDurations=True) first TODO: Test with spanners... @@ -3275,7 +3270,10 @@ def noteToXml(self, n: note.GeneralNote, noteIndexInChord=0, chordParent=None): _synchronizeIds(mxNote, n) d = chordOrN.duration - + if d.type == 'complex': + raise MusicXMLExportException( + 'complex duration encountered: ' + 'failure to run myStream.makeNotation(splitAtDurations=True) first') if d.isGrace is True: graceElement = SubElement(mxNote, 'grace') try: From bc927b01150b55c6868f966fa1a1b811677df127 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 8 Feb 2021 22:37:05 -0500 Subject: [PATCH 12/31] maxToReturn = 1 --- music21/duration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 9556d2b72a..321958a1e7 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -666,7 +666,7 @@ def quarterConversion(qLen): dots=0, quarterLength=qLen),), None) - tupleCandidates = quarterLengthToTuplet(qLen, 1) + tupleCandidates = quarterLengthToTuplet(qLen, maxToReturn=1) if tupleCandidates: # assume that the first tuplet candidate, using the smallest type, is best return QuarterLengthConversion( @@ -688,7 +688,7 @@ def quarterConversion(qLen): if largestType.quarterLength < 1: # Subdivide by one level (divide by 2) divisor = 2 - solutions = quarterLengthToTuplet(qLenRemainder / divisor) + solutions = quarterLengthToTuplet((qLenRemainder / divisor), maxToReturn=1) if solutions: tup = solutions[0] if largestType.quarterLength % tup.totalTupletLength() == 0: From bcba3707bf55f8fc827551c6cfe64bc6a6bdf350 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 9 Feb 2021 10:25:22 -0500 Subject: [PATCH 13/31] Use method variable instead of streamStatus to track re-making tuplet brackets --- music21/stream/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index 619a59c613..3e8fcf17bf 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -6364,6 +6364,7 @@ def makeNotation(self, # definitely do NOT put a constrainingSpannerBundle constraint ) + notesWithSplitDurations: List[note.GeneralNote] = [] if splitAtDurations: for container in returnStream.recurse(includeSelf=True, classFilter=('Stream')): for noteObj in container.getElementsByClass('GeneralNote'): @@ -6378,8 +6379,6 @@ def makeNotation(self, # split at durations if noteObj.duration.type == 'complex': - container.streamStatus._dirty = True - insertPoint = noteObj.offset objList = noteObj.splitAtDurations() container.replace(noteObj, objList[0]) @@ -6388,6 +6387,8 @@ def makeNotation(self, container.insert(insertPoint, subsequent) insertPoint += subsequent.quarterLength + notesWithSplitDurations += objList + # only use inPlace arg on first usage if not self.hasMeasures(): # only try to make voices if no Measures are defined @@ -6462,9 +6463,16 @@ def makeNotation(self, # check for tuplet brackets one measure at a time # this means that they will never extend beyond one measure for m in measureStream: - if m.streamStatus.haveTupletBracketsBeenMade() is False or m.streamStatus._dirty: + if m.streamStatus.haveTupletBracketsBeenMade() is False: makeNotation.makeTupletBrackets(m, inPlace=True) - m.streamStatus._dirty = False + else: + # Also remake tuplet brackets if this measure contains any + # notes created by keyword splitAtDurations + for splitNote in notesWithSplitDurations: + if splitNote in m.recurse().notesAndRests: + makeNotation.makeTupletBrackets(m, inPlace=True) + notesWithSplitDurations.pop(splitNote) + break if not measureStream: raise StreamException( From 72976c798a3ef92f40490fc353eba2bb65b7be52 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 9 Feb 2021 10:29:59 -0500 Subject: [PATCH 14/31] Fix lame mistake with iterating and pop --- music21/stream/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index 3e8fcf17bf..412d1412ec 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -6468,11 +6468,14 @@ def makeNotation(self, else: # Also remake tuplet brackets if this measure contains any # notes created by keyword splitAtDurations - for splitNote in notesWithSplitDurations: + iRemove: int = -1 + for i, splitNote in enumerate(notesWithSplitDurations): if splitNote in m.recurse().notesAndRests: makeNotation.makeTupletBrackets(m, inPlace=True) - notesWithSplitDurations.pop(splitNote) + iRemove = i break + if iRemove > -1: + notesWithSplitDurations.pop(iRemove) if not measureStream: raise StreamException( From 8331098eac3d0bade4ac4b813b138922fb77b1cd Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 9 Feb 2021 10:30:41 -0500 Subject: [PATCH 15/31] -1 could get confused for last --- music21/stream/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index 412d1412ec..d44494f768 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -6468,13 +6468,13 @@ def makeNotation(self, else: # Also remake tuplet brackets if this measure contains any # notes created by keyword splitAtDurations - iRemove: int = -1 + iRemove: Optional[int] = None for i, splitNote in enumerate(notesWithSplitDurations): if splitNote in m.recurse().notesAndRests: makeNotation.makeTupletBrackets(m, inPlace=True) iRemove = i break - if iRemove > -1: + if iRemove is not None: notesWithSplitDurations.pop(iRemove) if not measureStream: From 5f8eabc3d6dc7ef2b8224370f9ace14baa25eb97 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 9 Feb 2021 12:12:42 -0500 Subject: [PATCH 16/31] Warn about makeBeams failing unless it becomes a problem --- music21/stream/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index d44494f768..3bb19a585d 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -12301,6 +12301,9 @@ def makeNotation(self, >>> m.notes[1].pitch.accidental ''' + + # TODO: splitAtDurations + # TODO: can this be unified with Stream.makeNotation? # environLocal.printDebug(['Measure.makeNotation']) # TODO: this probably needs to look to see what processes need to be done; # for example, existing beaming may be destroyed. @@ -12349,8 +12352,7 @@ def makeNotation(self, except StreamException: # this is a result of makeMeasures not getting everything # note to measure allocation right - pass - # environLocal.printDebug(['skipping makeBeams exception', StreamException]) + environLocal.warn(['skipping makeBeams exception', StreamException]) if m.streamStatus.haveTupletBracketsBeenMade() is False: makeNotation.makeTupletBrackets(m, inPlace=True) From 12c46c3417ea881a5022510bbea885f20cb003eb Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 9 Feb 2021 14:32:08 -0500 Subject: [PATCH 17/31] Print something useful --- music21/stream/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index 3bb19a585d..7ab7a958ad 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -12349,10 +12349,10 @@ def makeNotation(self, if m.streamStatus.beams is False: try: m.makeBeams(inPlace=True) - except StreamException: + except StreamException as se: # this is a result of makeMeasures not getting everything # note to measure allocation right - environLocal.warn(['skipping makeBeams exception', StreamException]) + environLocal.warn(['skipping makeBeams exception', se]) if m.streamStatus.haveTupletBracketsBeenMade() is False: makeNotation.makeTupletBrackets(m, inPlace=True) From 9fd66a0bbbb71bd5fcbb150b6634c49a9b80d8be Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 9 Feb 2021 16:46:20 -0500 Subject: [PATCH 18/31] prep docs --- music21/stream/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index 7ab7a958ad..fc3a392439 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -6344,7 +6344,7 @@ def makeNotation(self, >>> sMeasures.getElementsByClass('Measure').last().rightBarline.type 'final' - Added in v7 -- `splitAtDurations` keyword + Added in v7 -- `splitAtDurations` keyword runs Stream.splitAtDurations() TODO: doctest ''' # determine what is the object to work on first From 8bf27a023930653ce1549b5f55a71dcfba547870 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:41:41 -0500 Subject: [PATCH 19/31] Remove changes to makeNotation Just call splitAtDurations during xml export prep --- music21/musicxml/m21ToXml.py | 19 ++++++++++++++++++- music21/stream/__init__.py | 30 ------------------------------ 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index f46e4e7376..6276cbceba 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -373,6 +373,8 @@ def parse(self, obj=None): if obj is None: obj = self.generalObj outObj = self.fromGeneralObject(obj) + if 'Stream' in outObj.classes: + unused_tuple = outObj.splitAtDurations(recurse=True) # TODO: set whether to do an additional score copy in submethods. return self.parseWellformedObject(outObj) @@ -2826,11 +2828,26 @@ def parseOneElement(self, obj): if 'GeneralNote' in classes: self.offsetInMeasure += obj.duration.quarterLength + # turn inexpressible durations into complex durations (unless unlinked) + if obj.duration.type == 'inexpressible': + obj.duration.quarterLength = obj.duration.quarterLength + + # make dotGroups into normal notes + if len(obj.duration.dotGroups) > 1: + obj.duration.splitDotGroups(inPlace=True) + + # Last-chance opportunity to split durations (e.g. if not found in a parent stream) + if 'GeneralNote' in classes and obj.duration.type == 'complex': + objList = obj.splitAtDurations() + else: + objList = [obj] + parsedObject = False for className, methName in self.classesToMethods.items(): if className in classes: meth = getattr(self, methName) - meth(obj) + for o in objList: + meth(obj) parsedObject = True break diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index fc3a392439..64d872fe79 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -6320,7 +6320,6 @@ def makeNotation(self, refStreamOrTimeRange=None, inPlace=False, bestClef=False, - splitAtDurations=True, **subroutineKeywords): ''' This method calls a sequence of Stream methods on this Stream to prepare @@ -6333,7 +6332,6 @@ def makeNotation(self, makeAccidentalsKeywords can be a dict specifying additional parameters to send to makeAccidentals - >>> s = stream.Stream() >>> n = note.Note('g') >>> n.quarterLength = 1.5 @@ -6343,9 +6341,6 @@ def makeNotation(self, 4 >>> sMeasures.getElementsByClass('Measure').last().rightBarline.type 'final' - - Added in v7 -- `splitAtDurations` keyword runs Stream.splitAtDurations() - TODO: doctest ''' # determine what is the object to work on first if inPlace: @@ -6364,31 +6359,6 @@ def makeNotation(self, # definitely do NOT put a constrainingSpannerBundle constraint ) - notesWithSplitDurations: List[note.GeneralNote] = [] - if splitAtDurations: - for container in returnStream.recurse(includeSelf=True, classFilter=('Stream')): - for noteObj in container.getElementsByClass('GeneralNote'): - # turn inexpressible durations into complex durations (unless unlinked) - if noteObj.duration.type == 'inexpressible': - noteObj.duration.quarterLength = noteObj.duration.quarterLength - - # make dotGroups into normal notes - if len(noteObj.duration.dotGroups) > 1: - noteObj.duration.splitDotGroups(inPlace=True) - # TODO: QUESTION(JTW): also need to replace/insert these? - - # split at durations - if noteObj.duration.type == 'complex': - insertPoint = noteObj.offset - objList = noteObj.splitAtDurations() - container.replace(noteObj, objList[0]) - insertPoint += objList[0].quarterLength - for subsequent in objList[1:]: - container.insert(insertPoint, subsequent) - insertPoint += subsequent.quarterLength - - notesWithSplitDurations += objList - # only use inPlace arg on first usage if not self.hasMeasures(): # only try to make voices if no Measures are defined From 732f59d037fb3beabbb17423fe17ef6599127bca Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:43:07 -0500 Subject: [PATCH 20/31] Fixup --- music21/musicxml/m21ToXml.py | 4 ++-- music21/stream/__init__.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 6276cbceba..09ec3d77f4 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3254,7 +3254,7 @@ def noteToXml(self, n: note.GeneralNote, noteIndexInChord=0, chordParent=None): Traceback (most recent call last): music21.musicxml.xmlObjects.MusicXMLExportException: complex duration encountered: - failure to run myStream.makeNotation(splitAtDurations=True) first + failure to run myStream.splitAtDurations() first TODO: Test with spanners... @@ -3290,7 +3290,7 @@ def noteToXml(self, n: note.GeneralNote, noteIndexInChord=0, chordParent=None): if d.type == 'complex': raise MusicXMLExportException( 'complex duration encountered: ' - 'failure to run myStream.makeNotation(splitAtDurations=True) first') + 'failure to run myStream.splitAtDurations() first') if d.isGrace is True: graceElement = SubElement(mxNote, 'grace') try: diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index 64d872fe79..f37534e83d 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -13455,7 +13455,6 @@ def makeNotation(self, refStreamOrTimeRange=None, inPlace=False, bestClef=False, - splitAtDurations=True, **subroutineKeywords): ''' This method overrides the makeNotation method on Stream, From 5efc783e50d63e67456b0a28ee9c4ea09fcecc62 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:43:42 -0500 Subject: [PATCH 21/31] more fixup --- music21/stream/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index f37534e83d..9fe3152e15 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -13479,7 +13479,6 @@ def makeNotation(self, refStreamOrTimeRange=refStreamOrTimeRange, inPlace=True, bestClef=bestClef, - splitAtDurations=splitAtDurations, **subroutineKeywords) # note: while the local-streams have updated their caches, the # containing score has an out-of-date cache of flat. @@ -13494,7 +13493,6 @@ def makeNotation(self, refStreamOrTimeRange=refStreamOrTimeRange, inPlace=True, bestClef=bestClef, - splitAtDurations=splitAtDurations, **subroutineKeywords) if inPlace: From 5ab54768b5068ab8a2da91a7c8c25984bb8c003c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:45:35 -0500 Subject: [PATCH 22/31] more fixup --- music21/stream/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index f37534e83d..9fe3152e15 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -13479,7 +13479,6 @@ def makeNotation(self, refStreamOrTimeRange=refStreamOrTimeRange, inPlace=True, bestClef=bestClef, - splitAtDurations=splitAtDurations, **subroutineKeywords) # note: while the local-streams have updated their caches, the # containing score has an out-of-date cache of flat. @@ -13494,7 +13493,6 @@ def makeNotation(self, refStreamOrTimeRange=refStreamOrTimeRange, inPlace=True, bestClef=bestClef, - splitAtDurations=splitAtDurations, **subroutineKeywords) if inPlace: From d61324961a6c19cebf9b8b26d6e819d16eb2a711 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:51:39 -0500 Subject: [PATCH 23/31] Warn about makeBeams exceptions --- music21/stream/base.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index 1fd45561cf..ec4e6d701b 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -6479,10 +6479,10 @@ def makeNotation(self, if not measureStream.streamStatus.beams: try: measureStream.makeBeams(inPlace=True) - except (StreamException, meter.MeterException): + except (StreamException, meter.MeterException) as e: # this is a result of makeMeasures not getting everything # note to measure allocation right - # environLocal.printDebug(['skipping makeBeams exception', StreamException]) + environLocal.warn(['skipping makeBeams exception', e]) pass # note: this needs to be after makeBeams, as placing this before @@ -12358,11 +12358,10 @@ def makeNotation(self, if m.streamStatus.beams is False: try: m.makeBeams(inPlace=True) - except StreamException: + except StreamException as se: # this is a result of makeMeasures not getting everything # note to measure allocation right - pass - # environLocal.printDebug(['skipping makeBeams exception', StreamException]) + environLocal.warn(['skipping makeBeams exception', se]) if m.streamStatus.haveTupletBracketsBeenMade() is False: makeNotation.makeTupletBrackets(m, inPlace=True) From 9b676beaab8f0776aa99999eefee3b8523ab4b6d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:52:21 -0500 Subject: [PATCH 24/31] fixup --- music21/stream/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index ec4e6d701b..edfd365741 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -6483,7 +6483,6 @@ def makeNotation(self, # this is a result of makeMeasures not getting everything # note to measure allocation right environLocal.warn(['skipping makeBeams exception', e]) - pass # note: this needs to be after makeBeams, as placing this before # makeBeams was causing the duration's tuplet to lose its type setting From 43157d14caa4663e10a6deb670a8a14fd7d3becb Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:54:21 -0500 Subject: [PATCH 25/31] fixup --- music21/musicxml/m21ToXml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 0bc45e2504..fb0f422181 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2847,7 +2847,7 @@ def parseOneElement(self, obj): if className in classes: meth = getattr(self, methName) for o in objList: - meth(obj) + meth(o) parsedObject = True break From c442715e818ee60c17d06492ab33c669fbc4e012 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 13 Feb 2021 09:55:01 -0500 Subject: [PATCH 26/31] fixup --- music21/musicxml/m21ToXml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index fb0f422181..8c77460db9 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2856,7 +2856,8 @@ def parseOneElement(self, obj): for className, methName in self.wrapAttributeMethodClasses.items(): if className in classes: meth = getattr(self, methName) - self.wrapObjectInAttributes(obj, meth) + for o in objList: + self.wrapObjectInAttributes(o, meth) parsedObject = True break From b7632e158fdd49f5441753d562a53985019e3a9b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 14 Feb 2021 11:17:37 -0500 Subject: [PATCH 27/31] Relocate call to splitAtDurations --- music21/musicxml/m21ToXml.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 8c77460db9..17d01eea05 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -373,8 +373,6 @@ def parse(self, obj=None): if obj is None: obj = self.generalObj outObj = self.fromGeneralObject(obj) - if 'Stream' in outObj.classes: - unused_tuple = outObj.splitAtDurations(recurse=True) # TODO: set whether to do an additional score copy in submethods. return self.parseWellformedObject(outObj) @@ -417,6 +415,7 @@ def fromGeneralObject(self, obj): 'Cannot translate the object ' + f'{self.generalObj} to a complete musicXML document; put it in a Stream first!' ) + unused_tuple = outObj.splitAtDurations(recurse=True) return outObj def fromScore(self, sc): From 526ed6d6510e44060260db35f138642a3b6c5151 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 14 Feb 2021 11:28:59 -0500 Subject: [PATCH 28/31] Add TODO for re-making tuplet brackets --- music21/musicxml/m21ToXml.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 17d01eea05..53019cd027 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -416,6 +416,9 @@ def fromGeneralObject(self, obj): + f'{self.generalObj} to a complete musicXML document; put it in a Stream first!' ) unused_tuple = outObj.splitAtDurations(recurse=True) + for container in outObj.recurse(includeSelf=True, streamsOnly=True): + # TODO: check if tuplet brackets need to be re-made on the basis of duration splitting + pass return outObj def fromScore(self, sc): @@ -2835,7 +2838,9 @@ def parseOneElement(self, obj): if len(obj.duration.dotGroups) > 1: obj.duration.splitDotGroups(inPlace=True) - # Last-chance opportunity to split durations (e.g. if not found in a parent stream) + # Last-chance opportunity to split durations if complex + # e.g. if just converted here from inexpressible + # Otherwise this is done in fromGeneralObject() if 'GeneralNote' in classes and obj.duration.type == 'complex': objList = obj.splitAtDurations() else: From 5d67f81a83ba0f7546d0657b74c033bb2c5c2920 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 14 Feb 2021 11:57:45 -0500 Subject: [PATCH 29/31] Redraw tuplet brackets when splitting note durations --- music21/musicxml/m21ToXml.py | 3 --- music21/stream/base.py | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 53019cd027..b755da138a 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -416,9 +416,6 @@ def fromGeneralObject(self, obj): + f'{self.generalObj} to a complete musicXML document; put it in a Stream first!' ) unused_tuple = outObj.splitAtDurations(recurse=True) - for container in outObj.recurse(includeSelf=True, streamsOnly=True): - # TODO: check if tuplet brackets need to be re-made on the basis of duration splitting - pass return outObj def fromScore(self, sc): diff --git a/music21/stream/base.py b/music21/stream/base.py index b2d0a7b279..e3fef93f29 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -2783,9 +2783,12 @@ def splitAtDurations(self, *, recurse=False) -> base._SplitTuple: ''' def processContainer(container: Stream): + anyComplexObject: bool = False for complexObj in container.getElementsNotOfClass(['Stream', 'Variant', 'Spanner']): if complexObj.duration.type != 'complex': continue + anyComplexObject = True + insertPoint = complexObj.offset objList = complexObj.splitAtDurations() @@ -2803,6 +2806,10 @@ def processContainer(container: Stream): if sp.getLast() is complexObj: sp.replaceSpannedElement(complexObj, objList[-1]) + # Redraw tuplet brackets + if anyComplexObject: + makeNotation.makeTupletBrackets(container, inPlace=True) + # Handle "loose" objects in self (usually just Measure or Voice) processContainer(self) # Handle inner streams From 583092a141a7a8e0ef7bfcb762414caa07d71830 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 15 Feb 2021 18:21:03 -0500 Subject: [PATCH 30/31] Remove try block in _updateComponents --- music21/duration.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index c2dfc38e89..8edded5720 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1764,18 +1764,10 @@ def _updateComponents(self): # this update will not be necessary self._quarterLengthNeedsUpdating = False if self.linked: - try: - qlc = quarterConversion(self._qtrLength) - self.components = list(qlc.components) - if qlc.tuplet is not None: - self.tuplets = (qlc.tuplet,) - except DurationException: - environLocal.printDebug([ - 'problem updating components of note with quarterLength ', - self.quarterLength, - 'chokes quarterLengthToDurations' - ]) - raise + qlc = quarterConversion(self._qtrLength) + self.components = list(qlc.components) + if qlc.tuplet is not None: + self.tuplets = (qlc.tuplet,) self._componentsNeedUpdating = False # PUBLIC METHODS # From 0c21c66734cde47b17e0a90e588dfb90c7308f45 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 15 Feb 2021 18:30:20 -0500 Subject: [PATCH 31/31] Set expressionIsInferred when setting quarterLength directly --- music21/duration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index 8edded5720..d8ec88a58c 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1763,7 +1763,7 @@ def _updateComponents(self): ''' # this update will not be necessary self._quarterLengthNeedsUpdating = False - if self.linked: + if self.linked and self.expressionIsInferred: qlc = quarterConversion(self._qtrLength) self.components = list(qlc.components) if qlc.tuplet is not None: @@ -2824,6 +2824,7 @@ def _setQuarterLength(self, value: OffsetQLIn): if value == 0.0 and self.linked is True: self.clear() self._qtrLength = value + self.expressionIsInferred = True self._componentsNeedUpdating = True self._quarterLengthNeedsUpdating = False