@@ -497,7 +497,9 @@ def quarterLengthToTuplet(
497497 return post
498498
499499
500- def quarterConversion (qLen : OffsetQLIn ) -> QuarterLengthConversion :
500+ def quarterConversion (
501+ qLen : OffsetQLIn , * , forceSingleComponent : bool = False
502+ ) -> QuarterLengthConversion :
501503 '''
502504 Returns a 2-element namedtuple of (components, tuplet)
503505
@@ -506,8 +508,12 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
506508
507509 Tuplet is a single :class:`~music21.duration.Tuplet` that adjusts all of the components.
508510
509- (All quarterLengths can, technically, be notated as a single unit
510- given a complex enough tuplet, as a last resort will look up to 199 as a tuplet type).
511+ All quarterLengths can, technically, be notated as a single unit
512+ given a complex enough tuplet. (As a last resort will look up to 199 as a tuplet type.)
513+ If this type of solution is *preferred* over a solution involving multiple tied components,
514+ then pass `forceSingleComponent=True` (new in v7.3, and can be set directly on
515+ :class:`Duration` objects via the :attr:`Duration.forceSingleComponent` attribute instead
516+ of calling this function directly).
511517
512518 >>> duration.quarterConversion(2)
513519 QuarterLengthConversion(components=(DurationTuple(type='half', dots=0, quarterLength=2.0),),
@@ -627,6 +633,12 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
627633 DurationTuple(type='16th', dots=0, quarterLength=0.25)),
628634 tuplet=<music21.duration.Tuplet 3/2/16th>)
629635
636+ But with `forceSingleComponent=True`:
637+
638+ >>> duration.quarterConversion(5/6, forceSingleComponent=True)
639+ QuarterLengthConversion(components=(DurationTuple(type='quarter', dots=0, quarterLength=1.0),),
640+ tuplet=<music21.duration.Tuplet 6/5/32nd>)
641+
630642 This is a very close approximation:
631643
632644 >>> duration.quarterConversion(0.18333333333333)
@@ -716,10 +728,10 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
716728
717729 qLenRemainder = opFrac (qLen - typeToDuration [closestSmallerType ])
718730
719- # one opportunity to define a tuplet if remainder can be expressed as one
731+ # but first: one opportunity to define a tuplet if remainder can be expressed as one
720732 # by expressing the largest type (components[0]) in terms of the same tuplet
721- if isinstance (qLenRemainder , fractions .Fraction ):
722- # Allow dotted type as "closest larger type" if > 1.0 QL
733+ if not forceSingleComponent and isinstance (qLenRemainder , fractions .Fraction ):
734+ # Allow dotted type as "largest type" if > 1.0 QL
723735 if qLenRemainder >= 1 and qLenRemainder > opFrac (typeToDuration [closestSmallerType ] * 0.5 ):
724736 components = [durationTupleFromTypeDots (closestSmallerType , 1 )]
725737 qLenRemainder = opFrac (qLen - components [0 ].quarterLength )
@@ -740,10 +752,15 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
740752 components = [tup .durationActual for i in range (0 , numComponentsTotal )]
741753 return QuarterLengthConversion (tuple (components ), tup )
742754
755+ # Is it made up of many small types?
743756 # cannot recursively call, because tuplets are not possible at this stage.
744757 # environLocal.warn(['starting remainder search for qLen:', qLen,
745758 # 'remainder: ', qLenRemainder, 'components: ', components])
746- for i in range (8 ): # max 8 iterations.
759+ if forceSingleComponent :
760+ iterations = 0
761+ else :
762+ iterations = 8
763+ for _ in range (iterations ):
747764 # environLocal.warn(['qLenRemainder is:', qLenRemainder])
748765 dots , durType = dottedMatch (qLenRemainder )
749766 if durType is not False : # match!
@@ -1604,6 +1621,16 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
16041621 3.5
16051622 >>> d3.expressionIsInferred
16061623 False
1624+
1625+ Example 4: A Duration that expresses itself using an idiosyncratic
1626+ tuplet rather than multiple components:
1627+
1628+ >>> d4 = duration.Duration(0.625) # same as example 2
1629+ >>> d4.forceSingleComponent = True
1630+ >>> d4.components
1631+ (DurationTuple(type='quarter', dots=0, quarterLength=1.0),)
1632+ >>> d4.tuplets
1633+ (<music21.duration.Tuplet 8/5/32nd>,)
16071634 '''
16081635
16091636 # CLASS VARIABLES #
@@ -1621,9 +1648,20 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
16211648 '_unlinkedType' ,
16221649 '_dotGroups' ,
16231650 'expressionIsInferred' ,
1651+ 'forceSingleComponent' ,
16241652 '_client'
16251653 )
16261654
1655+ _DOC_ATTR = {'expressionIsInferred' :
1656+ '''Boolean indicating whether this duration was created from a
1657+ number rather than a type and thus can be reexpressed.''' ,
1658+ 'forceSingleComponent' :
1659+ '''If True, configure a single component (with an idiosyncratic tuplet)
1660+ instead of attempting a solution with mulitiple components. If False,
1661+ (default) an attempt is made at a multiple-component solution but will
1662+ still create an idiosyncratic tuplet if no solution is found.''' ,
1663+ }
1664+
16271665 # INITIALIZER #
16281666
16291667 def __init__ (self , * arguments , ** keywords ):
@@ -1650,6 +1688,7 @@ def __init__(self, *arguments, **keywords):
16501688 self ._linked = True
16511689
16521690 self .expressionIsInferred = False
1691+ self .forceSingleComponent = False
16531692 for a in arguments :
16541693 if common .isNum (a ) and 'quarterLength' not in keywords :
16551694 keywords ['quarterLength' ] = a
@@ -1805,7 +1844,7 @@ def _updateComponents(self):
18051844 # this update will not be necessary
18061845 self ._quarterLengthNeedsUpdating = False
18071846 if self .linked and self .expressionIsInferred :
1808- qlc = quarterConversion (self ._qtrLength )
1847+ qlc = quarterConversion (self ._qtrLength , forceSingleComponent = self . forceSingleComponent )
18091848 self .components = list (qlc .components )
18101849 if qlc .tuplet is not None :
18111850 self .tuplets = (qlc .tuplet ,)
0 commit comments