Skip to content

Commit 27469ba

Browse files
Add forceSingleComponent attribute
1 parent 7f26ed3 commit 27469ba

1 file changed

Lines changed: 47 additions & 8 deletions

File tree

music21/duration.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)