Skip to content

Commit

Permalink
Merge pull request #1242 from jacobtylerwalls/reexpress-tuplets
Browse files Browse the repository at this point in the history
Add makeNotation routines for completing or consolidating tuplets
  • Loading branch information
mscuthbert authored Aug 6, 2022
2 parents 83b7020 + efac0cb commit de1be4b
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 45 deletions.
12 changes: 9 additions & 3 deletions music21/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,11 +1509,11 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
A Duration object is made of one or more immutable DurationTuple objects stored on the
`components` list. A Duration created by setting `quarterLength` sets the attribute
`expressionIsInferred` to True, which indicates that consuming functions or applications
:attr:`expressionIsInferred` to True, which indicates that callers
(such as :meth:`~music21.stream.makeNotation.splitElementsToCompleteTuplets`)
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 @@ -1580,6 +1580,12 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
'_client'
)

_DOC_ATTR = {'expressionIsInferred':
'''
Boolean indicating whether this duration was created from a
number rather than a type and thus can be reexpressed.
'''}

# INITIALIZER #

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

self._unlinkedType: t.Optional[str] = None
self._dotGroups: t.Tuple[int, ...] = (0,)
self._tuplets: t.Union[t.Tuple['Tuplet', ...], t.Tuple] = () # an empty tuple
self._tuplets: t.Tuple['Tuplet', ...] = () # an empty tuple
self._qtrLength: OffsetQL = 0.0

# DurationTuples go here
Expand Down
56 changes: 28 additions & 28 deletions music21/musicxml/m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -1657,16 +1657,6 @@ def setScoreLayouts(self):
self.firstScoreLayout = scoreLayout

def _populatePartExporterList(self):
if self.makeNotation:
# hide any rests created at this late stage, because we are
# merely trying to fill up MusicXML display, not impose things on users
for p in self.parts:
p.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange,
inPlace=True,
hideRests=True,
timeRangeFromBarDuration=True,
)

count = 0
sp = list(self.parts)
for innerStream in sp:
Expand All @@ -1683,9 +1673,7 @@ def _populatePartExporterList(self):
def parsePartlikeScore(self):
'''
Called by .parse() if the score has individual parts.
Calls makeRests() for the part (if `ScoreExporter.makeNotation` is True),
then creates a `PartExporter` for each part, and runs .parse() on that part.
Creates a `PartExporter` for each part, and runs .parse() on that part.
Appends the PartExporter to `self.partExporterList`
and runs .parse() on that part. Appends the PartExporter to self.
Expand Down Expand Up @@ -2652,19 +2640,27 @@ def parse(self):
self.stream.toWrittenPitch(inPlace=True)

# Suppose that everything below this is a measure
if self.makeNotation and not self.stream.getElementsByClass(stream.Measure):
self.fixupNotationFlat()
elif self.makeNotation:
self.fixupNotationMeasured()
if self.makeNotation:
# hide any rests created at this late stage, because we are
# merely trying to fill up MusicXML display, not impose things on users
self.stream.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange,
inPlace=True,
hideRests=True,
timeRangeFromBarDuration=True,
)

# Split complex durations in place (fast if none found)
# Do this after makeRests since makeRests might create complex durations
self.stream = self.stream.splitAtDurations(recurse=True)[0]

if self.stream.getElementsByClass(stream.Measure):
self.fixupNotationMeasured()
else:
self.fixupNotationFlat()
elif not self.stream.getElementsByClass(stream.Measure):
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()

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

# see if accidentals/beams/tuplets should be processed
# see if accidentals/beams should be processed
if not part.streamStatus.haveAccidentalsBeenMade():
part.makeAccidentals(inPlace=True)
if not part.streamStatus.beams:
try:
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)
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 self.spannerBundle:
self.spannerBundle = part.spannerBundle
Expand Down
8 changes: 7 additions & 1 deletion music21/musicxml/partStaffExporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,13 @@ def testJoinPartStaffsF(self):
from music21 import musicxml
sch = corpus.parse('schoenberg/opus19', 2)

SX = musicxml.m21ToXml.ScoreExporter(sch.flatten())
# NB: Using ScoreExporter directly is an advanced use case:
# does not run makeNotation(), so here GeneralObjectExporter is used first
gex = musicxml.m21ToXml.GeneralObjectExporter()
with self.assertWarnsRegex(Warning, 'not well-formed'):
# No part layer. Measure directly under Score.
obj = gex.fromGeneralObject(sch.flatten())
SX = musicxml.m21ToXml.ScoreExporter(obj)
SX.scorePreliminaries()
SX.parseFlatScore()
# Previously, an exception was raised by getRootForPartStaff()
Expand Down
5 changes: 3 additions & 2 deletions music21/musicxml/test_m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,9 @@ def testMultipleInstrumentsPiano(self):
tree = scEx.parse()

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

Expand Down
19 changes: 12 additions & 7 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import pathlib
import unittest
import sys
import warnings

from collections import namedtuple
from fractions import Fraction
Expand Down Expand Up @@ -6723,12 +6724,15 @@ def makeNotation(self: StreamType,

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

# measureStream.makeBeams(inPlace=True)
for m in returnStream.getElementsByClass(Measure):
makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True)
makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True)

if not returnStream.streamStatus.beams:
try:
makeNotation.makeBeams(returnStream, inPlace=True)
except meter.MeterException as me:
environLocal.warn(['skipping makeBeams exception', me])
warnings.warn(str(me))

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

# environLocal.printDebug(['have time signature', m.timeSignature])
if not m.streamStatus.beams:
m.makeBeams(inPlace=True)
if not m.streamStatus.tuplets:
makeNotation.makeTupletBrackets(m, inPlace=True)
makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True)
makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True)

m.makeBeams(inPlace=True)
for m_or_v in [m, *m.voices]:
makeNotation.makeTupletBrackets(m_or_v, inPlace=True)

if not inPlace:
return m
Expand Down
Loading

0 comments on commit de1be4b

Please sign in to comment.