From 19ef21f2ee9a6fd4b0fa4eb2855557028799db7c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 28 Feb 2022 22:26:15 -0500 Subject: [PATCH 01/16] Apply tuplet to multiple components to express 5/6 or 7/3 QL --- music21/duration.py | 61 +++++++++++++++++++++++++++++++--- music21/musicxml/m21ToXml.py | 8 +++++ music21/stream/makeNotation.py | 4 ++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index cd92f503e2..4a072a811e 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -603,13 +603,29 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: 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: + >>> 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: @@ -667,7 +683,7 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: 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( @@ -681,6 +697,29 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: # remove the largest type out there and keep going. qLenRemainder = opFrac(qLen - typeToDuration[closestSmallerType]) + + # 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), maxToReturn=1) + 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, # 'remainder: ', qLenRemainder, 'components: ', components]) @@ -3741,6 +3780,18 @@ 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 + ) + 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 + ) + def testTinyDuration(self): # e.g. delta from chordify: 1/9 - 1/8 = 1/72 # exercises quarterLengthToNonPowerOf2Tuplet() diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index dd2507a410..62d6457c23 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6979,6 +6979,14 @@ def testTextExpressionOffset(self): mxDirection = tree.find('part/measure/direction') self.assertEqual(mxDirection.get('placement'), 'above') + 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) + def testFullMeasureRest(self): from music21 import converter s = converter.parse('tinynotation: 9/8 r1') diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 05c176605c..6fccd79bf1 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1448,7 +1448,9 @@ def makeTupletBrackets(s: 'music21.stream.Stream', *, 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 7f26ed305192ee239d913c058c5e45ddb8cb41e7 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 1 Mar 2022 20:39:41 -0500 Subject: [PATCH 02/16] Allow dotted types > 1.0QL as "closest larger type" --- music21/duration.py | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 4a072a811e..54add8095d 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -643,6 +643,24 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: dots=0, quarterLength=99.0),), tuplet=None) + + OMIT_FROM_DOCS + + Another > 1.0 QL case, but over 3.0QL to catch "closest smaller type" being dotted: + + >>> duration.quarterConversion(11/3) + 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), + 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=) ''' # this is a performance-critical operation that has been highly optimized for speed # rather than legibility or logic. Most commonly anticipated events appear first @@ -701,24 +719,26 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: # 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), maxToReturn=1) - 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) + # Allow dotted type as "closest larger type" if > 1.0 QL + if qLenRemainder >= 1 and qLenRemainder > opFrac(typeToDuration[closestSmallerType] * 0.5): + components = [durationTupleFromTypeDots(closestSmallerType, 1)] + qLenRemainder = opFrac(qLen - components[0].quarterLength) + + for divisor in range(1, 4): + largestType = components[0] + solutions = quarterLengthToTuplet((qLenRemainder / divisor), maxToReturn=1) + 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, From 27469ba4841866c920a26cca01e9c8b664a4650d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 5 Mar 2022 23:11:41 -0500 Subject: [PATCH 03/16] Add forceSingleComponent attribute --- music21/duration.py | 55 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index 54add8095d..6c11012503 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -497,7 +497,9 @@ def quarterLengthToTuplet( return post -def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: +def quarterConversion( + qLen: OffsetQLIn, *, forceSingleComponent: bool = False +) -> QuarterLengthConversion: ''' Returns a 2-element namedtuple of (components, tuplet) @@ -506,8 +508,12 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: Tuplet is a single :class:`~music21.duration.Tuplet` that adjusts all of the components. - (All quarterLengths can, technically, be notated as a single unit - given a complex enough tuplet, as a last resort will look up to 199 as a tuplet type). + All quarterLengths can, technically, be notated as a single unit + given a complex enough tuplet. (As a last resort will look up to 199 as a tuplet type.) + If this type of solution is *preferred* over a solution involving multiple tied components, + then pass `forceSingleComponent=True` (new in v7.3, and can be set directly on + :class:`Duration` objects via the :attr:`Duration.forceSingleComponent` attribute instead + of calling this function directly). >>> duration.quarterConversion(2) QuarterLengthConversion(components=(DurationTuple(type='half', dots=0, quarterLength=2.0),), @@ -627,6 +633,12 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: DurationTuple(type='16th', dots=0, quarterLength=0.25)), tuplet=) + But with `forceSingleComponent=True`: + + >>> duration.quarterConversion(5/6, forceSingleComponent=True) + QuarterLengthConversion(components=(DurationTuple(type='quarter', dots=0, quarterLength=1.0),), + tuplet=) + This is a very close approximation: >>> duration.quarterConversion(0.18333333333333) @@ -716,10 +728,10 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: qLenRemainder = opFrac(qLen - typeToDuration[closestSmallerType]) - # one opportunity to define a tuplet if remainder can be expressed as one + # but first: 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): - # Allow dotted type as "closest larger type" if > 1.0 QL + if not forceSingleComponent and isinstance(qLenRemainder, fractions.Fraction): + # Allow dotted type as "largest type" if > 1.0 QL if qLenRemainder >= 1 and qLenRemainder > opFrac(typeToDuration[closestSmallerType] * 0.5): components = [durationTupleFromTypeDots(closestSmallerType, 1)] qLenRemainder = opFrac(qLen - components[0].quarterLength) @@ -740,10 +752,15 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion: components = [tup.durationActual for i in range(0, numComponentsTotal)] return QuarterLengthConversion(tuple(components), tup) + # Is it made up of many small types? # cannot recursively call, because tuplets are not possible at this stage. # environLocal.warn(['starting remainder search for qLen:', qLen, # 'remainder: ', qLenRemainder, 'components: ', components]) - for i in range(8): # max 8 iterations. + if forceSingleComponent: + iterations = 0 + else: + iterations = 8 + for _ in range(iterations): # environLocal.warn(['qLenRemainder is:', qLenRemainder]) dots, durType = dottedMatch(qLenRemainder) if durType is not False: # match! @@ -1604,6 +1621,16 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): 3.5 >>> d3.expressionIsInferred False + + Example 4: A Duration that expresses itself using an idiosyncratic + tuplet rather than multiple components: + + >>> d4 = duration.Duration(0.625) # same as example 2 + >>> d4.forceSingleComponent = True + >>> d4.components + (DurationTuple(type='quarter', dots=0, quarterLength=1.0),) + >>> d4.tuplets + (,) ''' # CLASS VARIABLES # @@ -1621,9 +1648,20 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): '_unlinkedType', '_dotGroups', 'expressionIsInferred', + 'forceSingleComponent', '_client' ) + _DOC_ATTR = {'expressionIsInferred': + '''Boolean indicating whether this duration was created from a + number rather than a type and thus can be reexpressed.''', + 'forceSingleComponent': + '''If True, configure a single component (with an idiosyncratic tuplet) + instead of attempting a solution with mulitiple components. If False, + (default) an attempt is made at a multiple-component solution but will + still create an idiosyncratic tuplet if no solution is found.''', + } + # INITIALIZER # def __init__(self, *arguments, **keywords): @@ -1650,6 +1688,7 @@ def __init__(self, *arguments, **keywords): self._linked = True self.expressionIsInferred = False + self.forceSingleComponent = False for a in arguments: if common.isNum(a) and 'quarterLength' not in keywords: keywords['quarterLength'] = a @@ -1805,7 +1844,7 @@ def _updateComponents(self): # this update will not be necessary self._quarterLengthNeedsUpdating = False if self.linked and self.expressionIsInferred: - qlc = quarterConversion(self._qtrLength) + qlc = quarterConversion(self._qtrLength, forceSingleComponent=self.forceSingleComponent) self.components = list(qlc.components) if qlc.tuplet is not None: self.tuplets = (qlc.tuplet,) From d34cf4fe2d9509a54d8e01c76c2301087f0c70ad Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 22 Mar 2022 08:34:05 -0400 Subject: [PATCH 04/16] Typo --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index 6c11012503..b44dae0629 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1657,7 +1657,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): number rather than a type and thus can be reexpressed.''', 'forceSingleComponent': '''If True, configure a single component (with an idiosyncratic tuplet) - instead of attempting a solution with mulitiple components. If False, + instead of attempting a solution with multiple components. If False, (default) an attempt is made at a multiple-component solution but will still create an idiosyncratic tuplet if no solution is found.''', } From c7628ebbd36ee16dee227a91e8aae1b29ded3c01 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 10 Apr 2022 17:15:36 -0400 Subject: [PATCH 05/16] bump version added --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index b44dae0629..1345f1474c 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -511,7 +511,7 @@ def quarterConversion( All quarterLengths can, technically, be notated as a single unit given a complex enough tuplet. (As a last resort will look up to 199 as a tuplet type.) If this type of solution is *preferred* over a solution involving multiple tied components, - then pass `forceSingleComponent=True` (new in v7.3, and can be set directly on + then pass `forceSingleComponent=True` (new in v8, and can be set directly on :class:`Duration` objects via the :attr:`Duration.forceSingleComponent` attribute instead of calling this function directly). From 927c5b4aecbd101bb5015f0ef4397ed82cab2ba1 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 11 Apr 2022 22:08:29 -0400 Subject: [PATCH 06/16] Remake tuplet brackets after splitAtDurations --- music21/_version.py | 2 +- music21/base.py | 2 +- music21/musicxml/m21ToXml.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/music21/_version.py b/music21/_version.py index efb0124c6b..679c4ae6c4 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -42,7 +42,7 @@ Changing this number invalidates old pickles -- do it if the old pickles create a problem. ''' -__version_info__ = (7, 3, 2) # can be 4-tuple: (7, 0, 5, 'a2') +__version_info__ = (7, 3, 3) # can be 4-tuple: (7, 0, 5, 'a2') v = '.'.join(str(x) for x in __version_info__[0:3]) if len(__version_info__) > 3 and __version_info__[3]: # type: ignore diff --git a/music21/base.py b/music21/base.py index 62d4a28691..a2ce8d9d39 100644 --- a/music21/base.py +++ b/music21/base.py @@ -28,7 +28,7 @@ >>> music21.VERSION_STR -'7.3.2' +'7.3.3' Alternatively, after doing a complete import, these classes are available under the module "base": diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 62d6457c23..de8e4a7615 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2558,11 +2558,6 @@ def parse(self): raise MusicXMLExportException( 'Cannot export with makeNotation=False if there are no measures') - # Split complex durations in place (fast if none found) - # must do after fixupNotationFlat(), which may create complex durations - if self.makeNotation: - self.stream = self.stream.splitAtDurations(recurse=True)[0] - # make sure that all instances of the same class have unique ids self.spannerBundle.setIdLocals() @@ -2707,6 +2702,7 @@ def fixupNotationFlat(self): ''' Runs makeNotation on a flatStream, such as one lacking measures. ''' + self.stream = self.stream.splitAtDurations(recurse=True)[0] part = self.stream part.makeMutable() # must mutate # try to add measures if none defined @@ -2742,6 +2738,9 @@ def fixupNotationMeasured(self): Changed in v7 -- no longer accepts `measureStream` argument. ''' + # Split complex durations in place (fast if none found) + self.stream = self.stream.splitAtDurations(recurse=True)[0] + part = self.stream measures = part.getElementsByClass(stream.Measure) first_measure = measures.first() @@ -2776,8 +2775,9 @@ def fixupNotationMeasured(self): part.makeBeams(inPlace=True) except exceptions21.StreamException: # no measures or no time sig? pass - if part.streamStatus.haveTupletBracketsBeenMade() is False: - stream.makeNotation.makeTupletBrackets(part, inPlace=True) + # Always make tuplet brackets, since splitAtDurations() may have created some + for m in measures: + stream.makeNotation.makeTupletBrackets(m, inPlace=True) if not self.spannerBundle: self.spannerBundle = part.spannerBundle From 4b4b2c01407f94234f4f7c6a3a8b57c60354570e Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 12 Apr 2022 08:38:27 -0400 Subject: [PATCH 07/16] Two calls to splitAtDurations() in fixupNotationFlat() --- music21/musicxml/m21ToXml.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index de8e4a7615..465587dc3c 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2702,7 +2702,9 @@ def fixupNotationFlat(self): ''' Runs makeNotation on a flatStream, such as one lacking measures. ''' + # Do this before makeNotation so that measures are filled correctly self.stream = self.stream.splitAtDurations(recurse=True)[0] + part = self.stream part.makeMutable() # must mutate # try to add measures if none defined @@ -2710,8 +2712,9 @@ def fixupNotationFlat(self): part.makeNotation(meterStream=self.meterStream, refStreamOrTimeRange=self.refStreamOrTimeRange, inPlace=True) - # environLocal.printDebug(['fixupNotationFlat: post makeNotation, length', - # len(measureStream)]) + + # Do this again, since makeNotation() might create complex rests + self.stream = self.stream.splitAtDurations(recurse=True)[0] # after calling measuresStream, need to update Spanners, as a deepcopy # has been made From becec131d8cdb6e8a920b4b72fb4f1a062cbd8b2 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 9 Jul 2022 09:41:44 -0400 Subject: [PATCH 08/16] Move test --- music21/musicxml/test_m21ToXml.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 9a9142ab55..f9b350987e 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -387,6 +387,14 @@ def testTextExpressionOffset(self): mxDirection = tree.find('part/measure/direction') self.assertEqual(mxDirection.get('placement'), 'above') + 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) + def testFullMeasureRest(self): s = converter.parse('tinynotation: 9/8 r1') r = s[note.Rest].first() From 61fb0c7826c0352c5316c5eb770ae7472d928e27 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 14 Aug 2022 15:35:12 -0400 Subject: [PATCH 09/16] fix test: getET doesn't run makeNotation --- music21/musicxml/test_m21ToXml.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index a9c861b58a..93f51bfef4 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -393,7 +393,9 @@ def testTextExpressionOffset(self): def testTupletBracketsMadeOnComponents(self): s = stream.Stream() s.insert(0, note.Note(quarterLength=(5 / 6))) - tree = self.getET(s) + # Use GEX to go through wellformed object conversion + gex = GeneralObjectExporter(s) + tree = et_fromstring(gex.parse().decode('utf-8')) # 3 sixteenth-triplets + 2 sixteenth-triplets # tuplet start, tuplet stop, tuplet start, tuplet stop self.assertEqual(len(tree.findall('.//tuplet')), 4) From 375946971f28aecdb5c11e4bfac738c5d490db4a Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 14 Aug 2022 16:03:32 -0400 Subject: [PATCH 10/16] Make splitAtDurations reset measure-level tuplets flag --- music21/musicxml/m21ToXml.py | 5 ++--- music21/stream/base.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 7dd5fe4db7..0731844a64 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2882,12 +2882,11 @@ def fixupNotationMeasured(self): part.makeBeams(inPlace=True) except exceptions21.StreamException as se: # no measures or no time sig? warnings.warn(MusicXMLWarning, str(se)) - # tuplets should be processed anyway (affected by earlier makeRests) - # technically, beams could be affected also, but we don't want to destroy - # existing beam information (e.g. single-syllable vocal flags) for m in measures: for m_or_v in [m, *m.voices]: stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) + if not m.streamStatus.tuplets: + stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) if not self.spannerBundle: self.spannerBundle = part.spannerBundle diff --git a/music21/stream/base.py b/music21/stream/base.py index 865ecfdec4..04e51dc052 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -3090,6 +3090,7 @@ def processContainer(container: Stream): sp.replaceSpannedElement(complexObj, objList[-1]) container.streamStatus.beams = False + container.streamStatus.tuplets = None # Handle "loose" objects in self (usually just Measure or Voice) processContainer(self) From 8996abf099cf7d14d28b6d793bcf204fd7a0c112 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 14 Aug 2022 16:04:18 -0400 Subject: [PATCH 11/16] m_or_v --- 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 0731844a64..6e60f4abb9 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2885,7 +2885,7 @@ def fixupNotationMeasured(self): for m in measures: for m_or_v in [m, *m.voices]: stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) - if not m.streamStatus.tuplets: + if not m_or_v.streamStatus.tuplets: stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) if not self.spannerBundle: From 263dab25d7b65e5467c0653d7237583968afe87b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 9 Sep 2022 07:54:58 -0400 Subject: [PATCH 12/16] Remove duplicative makeTupletBrackets call --- music21/musicxml/m21ToXml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 519e693625..7f3836ae1b 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2943,7 +2943,6 @@ def fixupNotationMeasured(self): warnings.warn(MusicXMLWarning, str(se)) for m in measures: for m_or_v in [m, *m.voices]: - stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) if not m_or_v.streamStatus.tuplets: stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) From 65575cdede0c884370555d234b92d6279d23c430 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 23 Dec 2022 08:09:27 -0500 Subject: [PATCH 13/16] Update version added --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index bee3621c1e..94482ded2b 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -528,7 +528,7 @@ def quarterConversion( All quarterLengths can, technically, be notated as a single unit given a complex enough tuplet. (As a last resort will look up to 199 as a tuplet type.) If this type of solution is *preferred* over a solution involving multiple tied components, - then pass `forceSingleComponent=True` (new in v8, and can be set directly on + then pass `forceSingleComponent=True` (new in v9, and can be set directly on :class:`Duration` objects via the :attr:`Duration.forceSingleComponent` attribute instead of calling this function directly). From 12c8c49b689bdd93f117269a0efdccdcb0b2785d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 23 Dec 2022 08:10:17 -0500 Subject: [PATCH 14/16] Fix faulty merge --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index 94482ded2b..bc20dc007f 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1718,7 +1718,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): '_dotGroups', 'expressionIsInferred', 'forceSingleComponent', - '_client' + 'client' ) _DOC_ATTR = { From 89df531419a3e022c553e3842ccf06bf4a1acaf5 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 23 Dec 2022 08:10:42 -0500 Subject: [PATCH 15/16] Trailing comma --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index bc20dc007f..8106dfc0ad 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1718,7 +1718,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): '_dotGroups', 'expressionIsInferred', 'forceSingleComponent', - 'client' + 'client', ) _DOC_ATTR = { From a825cd291151b47bb7ef79b99fc8a25bb0110ab7 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 1 Jul 2023 17:41:21 -0400 Subject: [PATCH 16/16] Bump version in note --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index 8106dfc0ad..bb2cb8638f 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -528,7 +528,7 @@ def quarterConversion( All quarterLengths can, technically, be notated as a single unit given a complex enough tuplet. (As a last resort will look up to 199 as a tuplet type.) If this type of solution is *preferred* over a solution involving multiple tied components, - then pass `forceSingleComponent=True` (new in v9, and can be set directly on + then pass `forceSingleComponent=True` (new in v9.3, and can be set directly on :class:`Duration` objects via the :attr:`Duration.forceSingleComponent` attribute instead of calling this function directly).