Skip to content

Commit de1be4b

Browse files
authored
Merge pull request #1242 from jacobtylerwalls/reexpress-tuplets
Add makeNotation routines for completing or consolidating tuplets
2 parents 83b7020 + efac0cb commit de1be4b

6 files changed

Lines changed: 261 additions & 45 deletions

File tree

music21/duration.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,11 +1509,11 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
15091509
15101510
A Duration object is made of one or more immutable DurationTuple objects stored on the
15111511
`components` list. A Duration created by setting `quarterLength` sets the attribute
1512-
`expressionIsInferred` to True, which indicates that consuming functions or applications
1512+
:attr:`expressionIsInferred` to True, which indicates that callers
1513+
(such as :meth:`~music21.stream.makeNotation.splitElementsToCompleteTuplets`)
15131514
can express this Duration using another combination of components that sums to the
15141515
`quarterLength`. Otherwise, `expressionIsInferred` is set to False, indicating that
15151516
components are not allowed to mutate.
1516-
(N.B.: `music21` does not yet implement such mutating components.)
15171517
15181518
Multiple DurationTuples in a single Duration may be used to express tied
15191519
notes, or may be used to split duration across barlines or beam groups.
@@ -1580,6 +1580,12 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
15801580
'_client'
15811581
)
15821582

1583+
_DOC_ATTR = {'expressionIsInferred':
1584+
'''
1585+
Boolean indicating whether this duration was created from a
1586+
number rather than a type and thus can be reexpressed.
1587+
'''}
1588+
15831589
# INITIALIZER #
15841590

15851591
def __init__(self, *arguments, **keywords):
@@ -1595,7 +1601,7 @@ def __init__(self, *arguments, **keywords):
15951601

15961602
self._unlinkedType: t.Optional[str] = None
15971603
self._dotGroups: t.Tuple[int, ...] = (0,)
1598-
self._tuplets: t.Union[t.Tuple['Tuplet', ...], t.Tuple] = () # an empty tuple
1604+
self._tuplets: t.Tuple['Tuplet', ...] = () # an empty tuple
15991605
self._qtrLength: OffsetQL = 0.0
16001606

16011607
# DurationTuples go here

music21/musicxml/m21ToXml.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,16 +1657,6 @@ def setScoreLayouts(self):
16571657
self.firstScoreLayout = scoreLayout
16581658

16591659
def _populatePartExporterList(self):
1660-
if self.makeNotation:
1661-
# hide any rests created at this late stage, because we are
1662-
# merely trying to fill up MusicXML display, not impose things on users
1663-
for p in self.parts:
1664-
p.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange,
1665-
inPlace=True,
1666-
hideRests=True,
1667-
timeRangeFromBarDuration=True,
1668-
)
1669-
16701660
count = 0
16711661
sp = list(self.parts)
16721662
for innerStream in sp:
@@ -1683,9 +1673,7 @@ def _populatePartExporterList(self):
16831673
def parsePartlikeScore(self):
16841674
'''
16851675
Called by .parse() if the score has individual parts.
1686-
1687-
Calls makeRests() for the part (if `ScoreExporter.makeNotation` is True),
1688-
then creates a `PartExporter` for each part, and runs .parse() on that part.
1676+
Creates a `PartExporter` for each part, and runs .parse() on that part.
16891677
Appends the PartExporter to `self.partExporterList`
16901678
and runs .parse() on that part. Appends the PartExporter to self.
16911679
@@ -2652,19 +2640,27 @@ def parse(self):
26522640
self.stream.toWrittenPitch(inPlace=True)
26532641

26542642
# Suppose that everything below this is a measure
2655-
if self.makeNotation and not self.stream.getElementsByClass(stream.Measure):
2656-
self.fixupNotationFlat()
2657-
elif self.makeNotation:
2658-
self.fixupNotationMeasured()
2643+
if self.makeNotation:
2644+
# hide any rests created at this late stage, because we are
2645+
# merely trying to fill up MusicXML display, not impose things on users
2646+
self.stream.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange,
2647+
inPlace=True,
2648+
hideRests=True,
2649+
timeRangeFromBarDuration=True,
2650+
)
2651+
2652+
# Split complex durations in place (fast if none found)
2653+
# Do this after makeRests since makeRests might create complex durations
2654+
self.stream = self.stream.splitAtDurations(recurse=True)[0]
2655+
2656+
if self.stream.getElementsByClass(stream.Measure):
2657+
self.fixupNotationMeasured()
2658+
else:
2659+
self.fixupNotationFlat()
26592660
elif not self.stream.getElementsByClass(stream.Measure):
26602661
raise MusicXMLExportException(
26612662
'Cannot export with makeNotation=False if there are no measures')
26622663

2663-
# Split complex durations in place (fast if none found)
2664-
# must do after fixupNotationFlat(), which may create complex durations
2665-
if self.makeNotation:
2666-
self.stream = self.stream.splitAtDurations(recurse=True)[0]
2667-
26682664
# make sure that all instances of the same class have unique ids
26692665
self.spannerBundle.setIdLocals()
26702666

@@ -2841,7 +2837,7 @@ def fixupNotationMeasured(self):
28412837
them into the first measure if necessary.
28422838
28432839
Checks if makeAccidentals is run, and haveBeamsBeenMade is done, and
2844-
haveTupletBracketsBeenMade is done.
2840+
remake tuplets on the assumption that makeRests() may necessitate changes.
28452841
28462842
Changed in v7 -- no longer accepts `measureStream` argument.
28472843
'''
@@ -2871,16 +2867,20 @@ def fixupNotationMeasured(self):
28712867
if outerTimeSignatures:
28722868
first_measure.timeSignature = outerTimeSignatures.first()
28732869

2874-
# see if accidentals/beams/tuplets should be processed
2870+
# see if accidentals/beams should be processed
28752871
if not part.streamStatus.haveAccidentalsBeenMade():
28762872
part.makeAccidentals(inPlace=True)
28772873
if not part.streamStatus.beams:
28782874
try:
28792875
part.makeBeams(inPlace=True)
2880-
except exceptions21.StreamException: # no measures or no time sig?
2881-
pass
2882-
if part.streamStatus.haveTupletBracketsBeenMade() is False:
2883-
stream.makeNotation.makeTupletBrackets(part, inPlace=True)
2876+
except exceptions21.StreamException as se: # no measures or no time sig?
2877+
warnings.warn(MusicXMLWarning, str(se))
2878+
# tuplets should be processed anyway (affected by earlier makeRests)
2879+
# technically, beams could be affected also, but we don't want to destroy
2880+
# existing beam information (e.g. single-syllable vocal flags)
2881+
for m in measures:
2882+
for m_or_v in [m, *m.voices]:
2883+
stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True)
28842884

28852885
if not self.spannerBundle:
28862886
self.spannerBundle = part.spannerBundle

music21/musicxml/partStaffExporter.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,13 @@ def testJoinPartStaffsF(self):
984984
from music21 import musicxml
985985
sch = corpus.parse('schoenberg/opus19', 2)
986986

987-
SX = musicxml.m21ToXml.ScoreExporter(sch.flatten())
987+
# NB: Using ScoreExporter directly is an advanced use case:
988+
# does not run makeNotation(), so here GeneralObjectExporter is used first
989+
gex = musicxml.m21ToXml.GeneralObjectExporter()
990+
with self.assertWarnsRegex(Warning, 'not well-formed'):
991+
# No part layer. Measure directly under Score.
992+
obj = gex.fromGeneralObject(sch.flatten())
993+
SX = musicxml.m21ToXml.ScoreExporter(obj)
988994
SX.scorePreliminaries()
989995
SX.parseFlatScore()
990996
# Previously, an exception was raised by getRootForPartStaff()

music21/musicxml/test_m21ToXml.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,9 @@ def testMultipleInstrumentsPiano(self):
328328
tree = scEx.parse()
329329

330330
self.assertEqual(
331-
[el.text for el in tree.findall('.//instrument-name')],
332-
['Electric Piano', 'Voice', 'Electric Organ', 'Piano']
331+
# allow for non-deterministic ordering: caused by instrument.deduplicate() (?)
332+
{el.text for el in tree.findall('.//instrument-name')},
333+
{'Electric Piano', 'Voice', 'Electric Organ', 'Piano'}
333334
)
334335
self.assertEqual(len(tree.findall('.//measure/note/instrument')), 6)
335336

music21/stream/base.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import pathlib
3030
import unittest
3131
import sys
32+
import warnings
3233

3334
from collections import namedtuple
3435
from fractions import Fraction
@@ -6723,12 +6724,15 @@ def makeNotation(self: StreamType,
67236724

67246725
makeNotation.makeTies(returnStream, meterStream=meterStream, inPlace=True)
67256726

6726-
# measureStream.makeBeams(inPlace=True)
6727+
for m in returnStream.getElementsByClass(Measure):
6728+
makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True)
6729+
makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True)
6730+
67276731
if not returnStream.streamStatus.beams:
67286732
try:
67296733
makeNotation.makeBeams(returnStream, inPlace=True)
67306734
except meter.MeterException as me:
6731-
environLocal.warn(['skipping makeBeams exception', me])
6735+
warnings.warn(str(me))
67326736

67336737
# note: this needs to be after makeBeams, as placing this before
67346738
# makeBeams was causing the duration's tuplet to lose its type setting
@@ -12801,11 +12805,12 @@ def makeNotation(self,
1280112805
ts = defaultMeters[0]
1280212806
m.timeSignature = ts # a Stream; get the first element
1280312807

12804-
# environLocal.printDebug(['have time signature', m.timeSignature])
12805-
if not m.streamStatus.beams:
12806-
m.makeBeams(inPlace=True)
12807-
if not m.streamStatus.tuplets:
12808-
makeNotation.makeTupletBrackets(m, inPlace=True)
12808+
makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True)
12809+
makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True)
12810+
12811+
m.makeBeams(inPlace=True)
12812+
for m_or_v in [m, *m.voices]:
12813+
makeNotation.makeTupletBrackets(m_or_v, inPlace=True)
1280912814

1281012815
if not inPlace:
1281112816
return m

0 commit comments

Comments
 (0)