Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Apply tuplet to multiple components to express 5/6 or 7/3 QL #763

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f53ba68
Apply tuplet to multiple components to express 5/6 or 7/3 QL
jacobtylerwalls Jan 9, 2021
e3fa8b9
Whitespace
jacobtylerwalls Jan 9, 2021
dfac936
Liberalize choices for 1/n
jacobtylerwalls Jan 9, 2021
0cfdbbb
Add expressionIsInferred
jacobtylerwalls Jan 9, 2021
7d0e21f
Improve implementation and fix tuplet type
jacobtylerwalls Jan 10, 2021
d38df11
whitespace
jacobtylerwalls Jan 10, 2021
67b1d01
splitAtDurations keyword in makeNotation
jacobtylerwalls Jan 10, 2021
59bc88d
Fix test
jacobtylerwalls Jan 11, 2021
b6695dd
Add back clarifying comment
jacobtylerwalls Jan 11, 2021
1a9674d
Permit mixing tuplet types having the same multiplier under a bracket
jacobtylerwalls Jan 29, 2021
74213f6
Merge branch 'master' of https://github.com/cuthbertLab/music21 into …
jacobtylerwalls Feb 6, 2021
a712498
Merge branch 'master' of https://github.com/cuthbertLab/music21 into …
jacobtylerwalls Feb 9, 2021
5247632
Raise if a complex note makes it to the xml exporter
jacobtylerwalls Feb 9, 2021
bc927b0
maxToReturn = 1
jacobtylerwalls Feb 9, 2021
bcba370
Use method variable instead of streamStatus to track re-making tuplet…
jacobtylerwalls Feb 9, 2021
72976c7
Fix lame mistake with iterating and pop
jacobtylerwalls Feb 9, 2021
8331098
-1 could get confused for last
jacobtylerwalls Feb 9, 2021
5f8eabc
Warn about makeBeams failing unless it becomes a problem
jacobtylerwalls Feb 9, 2021
12c46c3
Print something useful
jacobtylerwalls Feb 9, 2021
9fd66a0
prep docs
jacobtylerwalls Feb 9, 2021
8bf27a0
Remove changes to makeNotation
jacobtylerwalls Feb 13, 2021
732f59d
Fixup
jacobtylerwalls Feb 13, 2021
5efc783
more fixup
jacobtylerwalls Feb 13, 2021
5ab5476
more fixup
jacobtylerwalls Feb 13, 2021
cf54067
Merge branch 'components' of https://github.com/jacobtylerwalls/music…
jacobtylerwalls Feb 13, 2021
f2bc696
Merge branch 'master' of https://github.com/cuthbertLab/music21 into …
jacobtylerwalls Feb 13, 2021
d613249
Warn about makeBeams exceptions
jacobtylerwalls Feb 13, 2021
9b676be
fixup
jacobtylerwalls Feb 13, 2021
43157d1
fixup
jacobtylerwalls Feb 13, 2021
c442715
fixup
jacobtylerwalls Feb 13, 2021
065e9c3
Merge branch 'master' of https://github.com/cuthbertLab/music21 into …
jacobtylerwalls Feb 14, 2021
b7632e1
Relocate call to splitAtDurations
jacobtylerwalls Feb 14, 2021
526ed6d
Add TODO for re-making tuplet brackets
jacobtylerwalls Feb 14, 2021
5d67f81
Redraw tuplet brackets when splitting note durations
jacobtylerwalls Feb 14, 2021
583092a
Remove try block in _updateComponents
jacobtylerwalls Feb 15, 2021
0c21c66
Set expressionIsInferred when setting quarterLength directly
jacobtylerwalls Feb 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 78 additions & 25 deletions music21/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,15 +600,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=<music21.duration.Tuplet 12/7/16th>)
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=<music21.duration.Tuplet 3/2/eighth>)

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=<music21.duration.Tuplet 3/2/16th>)

>>> duration.quarterConversion(0.18333333333333)
QuarterLengthConversion(components=(DurationTuple(type='16th', dots=0, quarterLength=0.25),),
Expand All @@ -618,7 +632,6 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
QuarterLengthConversion(components=(DurationTuple(type='zero', dots=0, quarterLength=0.0),),
tuplet=None)


>>> duration.quarterConversion(99.0)
QuarterLengthConversion(components=(DurationTuple(type='inexpressible',
dots=0,
Expand Down Expand Up @@ -662,7 +675,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(
Expand All @@ -674,8 +687,30 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
# 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 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])
Expand Down Expand Up @@ -1476,7 +1511,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.
Expand Down Expand Up @@ -1513,11 +1553,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 #
Expand All @@ -1534,7 +1579,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
'_typeNeedsUpdating',
'_unlinkedType',
'_dotGroups',

'expressionIsInferred',
'_client'
)

Expand Down Expand Up @@ -1562,6 +1607,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
Expand Down Expand Up @@ -1590,6 +1637,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']
Expand Down Expand Up @@ -1715,19 +1763,11 @@ 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
if self.linked and self.expressionIsInferred:
qlc = quarterConversion(self._qtrLength)
self.components = list(qlc.components)
if qlc.tuplet is not None:
self.tuplets = (qlc.tuplet,)
self._componentsNeedUpdating = False

# PUBLIC METHODS #
Expand Down Expand Up @@ -1786,10 +1826,10 @@ def addDurationTuple(self, dur: Union[DurationTuple, 'Duration']):

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)

Expand Down Expand Up @@ -2784,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

Expand Down Expand Up @@ -3642,6 +3683,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
)


# -------------------------------------------------------------------------------
# define presented order in documentation
Expand Down
29 changes: 18 additions & 11 deletions music21/musicxml/m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,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):
Expand Down Expand Up @@ -2834,7 +2835,9 @@ def parseOneElement(self, obj):
if len(obj.duration.dotGroups) > 1:
obj.duration.splitDotGroups(inPlace=True)

# split at durations...
# 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:
Expand Down Expand Up @@ -3250,15 +3253,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)
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>50400</duration>
<type>complex</type>
</note>
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
complex duration encountered:
failure to run myStream.splitAtDurations() first

TODO: Test with spanners...

Expand Down Expand Up @@ -3291,7 +3289,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.splitAtDurations() first')
if d.isGrace is True:
graceElement = SubElement(mxNote, 'grace')
try:
Expand Down Expand Up @@ -6446,6 +6447,12 @@ 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)))
# 3 sixteenth-tuplets, 2 sixteenth-tuplets
# tuplet start, tuplet stop, tuplet start, tuplet stop
self.assertEqual(self.getXml(s).count('<tuplet '), 4)


class TestExternal(unittest.TestCase): # pragma: no cover
Expand Down
7 changes: 7 additions & 0 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion music21/stream/makeNotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1318,7 +1318,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
Expand Down