From ba295d8f6b25275392fd517b0736a0488be35560 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 11 Mar 2021 10:30:32 -0500 Subject: [PATCH 01/12] Add `makeNotation=False` option to musicxml export --- music21/base.py | 9 +++ music21/converter/subConverters.py | 68 +++++++++++++++++- music21/musicxml/m21ToXml.py | 106 ++++++++++++++++++++--------- music21/musicxml/xmlObjects.py | 4 ++ music21/note.py | 4 +- music21/stream/base.py | 2 +- music21/stream/tests.py | 5 ++ 7 files changed, 160 insertions(+), 38 deletions(-) diff --git a/music21/base.py b/music21/base.py index 3a5f133514..d128c7167d 100644 --- a/music21/base.py +++ b/music21/base.py @@ -2558,6 +2558,10 @@ def write(self, fmt=None, fp=None, **keywords): # pragma: no cover be used. For most people that is musicxml. Returns the full path to the file. + + Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed + score if necessary, and run :meth:`~music21.stream.Score.makeNotation`. To + avoid this when writing .musicxml, use `makeNotation=False`. ''' if fmt is None: # get setting in environment fmt = environLocal['writeFormat'] @@ -2605,6 +2609,7 @@ def show(self, fmt=None, app=None, **keywords): # pragma: no cover fmt argument or, if not provided, the format set in the user's Environment Valid formats include (but are not limited to):: + musicxml text midi @@ -2618,6 +2623,10 @@ def show(self, fmt=None, app=None, **keywords): # pragma: no cover N.B. score.write('lily') returns a bare lilypond file, score.show('lily') runs it through lilypond and displays it as a png. + + Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed + score if necessary, and run :meth:`~music21.stream.Score.makeNotation`. To + avoid this when showing .musicxml, use `makeNotation=False`. ''' # note that all formats here must be defined in # common.VALID_SHOW_FORMATS diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 1dce94d110..d9d5e85232 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -952,10 +952,23 @@ def writeDataStream(self, fp, dataBytes: bytes): # pragma: no cover return fp - def write(self, obj, fmt, fp=None, subformats=None, - compress=False, **keywords): # pragma: no cover + def write(self, + obj, + fmt, + *, + fp=None, + subformats=None, + makeNotation=True, + compress=False, + **keywords): # pragma: no cover ''' Write to a .xml file. + + Set `makeNotation=False` to prevent fixing up the notation, and where possible, + to prevent making additional deepcopies. (If `obj` is not a + :class:`~music21.stream.Score`, `obj` or its elements might still be copied. + See :meth:`~music21.musicxml.m21ToXml.GeneralObjectExporter.fromGeneralObject`.) + Set `compress=True` to immediately compress the output to a .mxl file. ''' from music21.musicxml import archiveTools, m21ToXml @@ -969,8 +982,13 @@ def write(self, obj, fmt, fp=None, subformats=None, defaults.title = '' defaults.author = '' + dataBytes: bytes = b'' generalExporter = m21ToXml.GeneralObjectExporter(obj) - dataBytes: bytes = generalExporter.parse() + if makeNotation is False and 'Score' in obj.classes: + # Assume well-formed and bypass deepcopy in GeneralObjectExporter.fromScore() + dataBytes = generalExporter.parseWellformedObject(obj, makeNotation=False) + else: + dataBytes = generalExporter.parse(makeNotation=makeNotation) writeDataStreamFp = fp if fp is not None and subformats is not None: @@ -1451,6 +1469,50 @@ def testWriteMXL(self): self.assertTrue(str(mxlPath).endswith('.mxl')) os.remove(mxlPath) + def testWriteMusicXMLMakeNotation(self): + import os + from music21 import converter + from music21 import note + + m1 = stream.Measure(note.Note(quarterLength=5.0)) + m2 = stream.Measure() + p = stream.Part([m1, m2]) + s = stream.Score(p) + + self.assertEqual(len(m1.notes), 1) + self.assertEqual(len(m2.notes), 0) + + out1 = s.write(makeNotation=True) + # 4/4 will be assumed; quarter note will be moved to measure 2 + roundtrip_back = converter.parse(out1) + self.assertEqual( + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[0].notes), 1) + self.assertEqual( + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 1) + + out2 = s.write(makeNotation=False) + roundtrip_back = converter.parse(out2) + # 4/4 will not be assumed; quarter note will still be split out from 5.0QL + # but it will remain in measure 1 + # and there will be no rests in measure 2 + self.assertEqual( + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[0].notes), 2) + self.assertEqual( + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) + + # makeNotation=False on a non-Score still prevents some copying, + # but packing into Score inevitably does some making of notation + out3 = p.write(makeNotation=False) + roundtrip_back = converter.parse(out3) + # 4/4 will be assumed; quarter note will be moved to measure 2 + self.assertEqual( + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[0].notes), 1) + self.assertEqual( + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 1) + + for out in (out1, out2, out3): + os.remove(out) + class TestExternal(unittest.TestCase): # pragma: no cover diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index eb264f75b3..cdb937cbbf 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -19,6 +19,7 @@ import io import math import unittest +import warnings import xml.etree.ElementTree as ET from xml.etree.ElementTree import Element, SubElement, ElementTree from typing import List, Optional, Union @@ -50,6 +51,7 @@ from music21.musicxml.partStaffExporter import PartStaffExporterMixin from music21.musicxml import xmlObjects from music21.musicxml.xmlObjects import MusicXMLExportException +from music21.musicxml.xmlObjects import MusicXMLWarning from music21 import environment _MOD = "musicxml.m21ToXml" @@ -319,11 +321,16 @@ class GeneralObjectExporter: def __init__(self, obj=None): self.generalObj = obj - def parse(self, obj=None): + def parse(self, obj=None, *, makeNotation: bool = True): r''' - return a bytes object representation of anything from a + Return a bytes object representation of anything from a Score to a single pitch. + Wrap `obj` in a well-formed stream (using + :meth:`parseWellformedObject`), making a copy and making notation + along the way. To skip making notation, pass `makeNotation=False`. + To skip making a copy, call `parseWellformedObject()` directly. + >>> p = pitch.Pitch('D#4') >>> GEX = musicxml.m21ToXml.GeneralObjectExporter(p) >>> out = GEX.parse() # out is bytes @@ -385,16 +392,28 @@ def parse(self, obj=None): if obj is None: obj = self.generalObj outObj = self.fromGeneralObject(obj) - # TODO: set whether to do an additional score copy in submethods. - return self.parseWellformedObject(outObj) + return self.parseWellformedObject(outObj, makeNotation=makeNotation) - def parseWellformedObject(self, sc): + def parseWellformedObject(self, sc, *, makeNotation: bool = True) -> bytes: ''' - parse an object that has already gone through the - `.fromGeneralObject` conversion. Returns bytes. + Parse an object that has usually already gone through the + `.fromGeneralObject` conversion, which has produced a copy with + :meth:`~music21.stream.Score.makeNotation` run on it. + + `makeNotation=True` (default) runs :meth:`~music21.stream.makeNotation` + on each of the parts, this time without making a copy, possibly leaving + side effects on `sc`. + + Set `makeNotation=False` to avoid making notation. + + Returns bytes. ''' + if not sc.isWellFormedNotation(): + warnings.warn(f'{sc} is not well-formed; see isWellFormedNotation()', + category=MusicXMLWarning) + scoreExporter = ScoreExporter(sc) - scoreExporter.parse() + scoreExporter.parse(makeNotation=makeNotation) return scoreExporter.asBytes() def fromGeneralObject(self, obj): @@ -431,7 +450,7 @@ def fromGeneralObject(self, obj): def fromScore(self, sc): ''' - the best one of all -- a perfectly made Score (or something like that) + Copies the input score and runs :meth:`~music21.stream.Score.makeNotation` on the copy. ''' scOut = sc.makeNotation(inPlace=False) # scOut.makeImmutable() @@ -439,7 +458,7 @@ def fromScore(self, sc): def fromPart(self, p): ''' - from a part, put it in a score... + From a part, put it in a new score. ''' if p.isFlat: p = p.makeMeasures() @@ -1370,7 +1389,7 @@ def __init__(self, score: Optional[stream.Score] = None): self.parts = [] - def parse(self): + def parse(self, makeNotation: bool = True): ''' the main function to call. @@ -1393,9 +1412,9 @@ def parse(self): self.scorePreliminaries() if s.hasPartLikeStreams(): - self.parsePartlikeScore() + self.parsePartlikeScore(makeNotation=makeNotation) else: - self.parseFlatScore() + self.parseFlatScore(makeNotation=makeNotation) self.postPartProcess() @@ -1552,12 +1571,14 @@ def setScoreLayouts(self): self.scoreLayouts = scoreLayouts self.firstScoreLayout = scoreLayout - def parsePartlikeScore(self): + def parsePartlikeScore(self, makeNotation: bool = True): ''' - called by .parse() if the score has individual parts. + Called by .parse() if the score has individual parts. - Calls makeRests() for the part, then creates a PartExporter for each part, - and runs .parse() on that part. appends the PartExporter to self. + Calls makeRests() for the part (if `makeNotation=True`), + then 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. Hide rests created at this late stage. @@ -1569,14 +1590,15 @@ def parsePartlikeScore(self): >>> '' in outStr True ''' - # self.parts is a stream of parts - # hide any rests created at this late stage, because we are - # merely trying to fill up MusicXML display, not impose things on users - self.parts.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange, - inPlace=True, - hideRests=True, - timeRangeFromBarDuration=True, - ) + if makeNotation: + # self.parts is a stream of parts + # hide any rests created at this late stage, because we are + # merely trying to fill up MusicXML display, not impose things on users + self.parts.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange, + inPlace=True, + hideRests=True, + timeRangeFromBarDuration=True, + ) count = 0 sp = list(self.parts) @@ -1592,7 +1614,7 @@ def parsePartlikeScore(self): pp.parse() self.partExporterList.append(pp) - def parseFlatScore(self): + def parseFlatScore(self, makeNotation: bool = True): ''' creates a single PartExporter for this Stream and parses it. @@ -1615,7 +1637,7 @@ def parseFlatScore(self): ''' s = self.stream pp = PartExporter(s, parent=self) - pp.parse() + pp.parse(makeNotation=makeNotation) self.partExporterList.append(pp) def postPartProcess(self): @@ -2389,26 +2411,43 @@ def __init__(self, partObj: Union[stream.Part, stream.Score, None] = None, paren self.spannerBundle = partObj.spannerBundle - def parse(self): + def parse(self, makeNotation: bool = True): ''' Set up instruments, create a partId (if no good one exists) and sets it on - , fixes up the notation (`fixupNotationFlat()` or `fixupNotationMeasured()`) + , fixes up the notation (:meth:`fixupNotationFlat` or :meth:`fixupNotationMeasured`, + if `makeNotation` is True), setsIdLocals on spanner bundle. runs parse() on each measure's MeasureExporter and appends the output to the object. In other words, one-stop shopping. + + `makeNotation=False` is used to avoid running `makeNotation` on the Part. + This keyword is passed down from `show()`, `write()`, and + :meth:`~music21.musicxml.m21ToXml.ScoreExporter.parse`. It + will raise if no measures are present in the part. + + >>> from music21.musicxml.m21ToXml import PartExporter + >>> noMeasures = stream.Part(note.Note()) + >>> pex = PartExporter(noMeasures) + >>> pex.parse(makeNotation=False) + Traceback (most recent call last): + music21.musicxml.xmlObjects.MusicXMLExportException: + Cannot export with makeNotation=False if there are no measures ''' self.instrumentSetup() self.xmlRoot.set('id', str(self.firstInstrumentObject.partId)) # Suppose that everything below this is a measure measureStream = self.stream.getElementsByClass('Stream').stream() - if not measureStream: + if makeNotation and not measureStream: self.fixupNotationFlat() # Now we have measures measureStream = self.stream.getElementsByClass('Stream').stream() - else: + elif makeNotation: self.fixupNotationMeasured(measureStream) + elif not measureStream: + raise MusicXMLExportException( + 'Cannot export with makeNotation=False if there are no measures') # make sure that all instances of the same class have unique ids self.spannerBundle.setIdLocals() for m in measureStream: @@ -6427,7 +6466,10 @@ def testFromScoreNoParts(self): s.append(note.Note()) s.append(note.Note()) gex = GeneralObjectExporter(s) - tree = ET.fromstring(gex.parse().decode('utf-8')) + + warn_msg = f'{s} is not well-formed; see isWellFormedNotation()' + with self.assertWarns(MusicXMLWarning, msg=warn_msg): + tree = ET.fromstring(gex.parse().decode('utf-8')) # Assert no gaps in stream self.assertSequenceEqual(tree.findall('.//forward'), []) diff --git a/music21/musicxml/xmlObjects.py b/music21/musicxml/xmlObjects.py index 0d01c95e93..848d9d033f 100644 --- a/music21/musicxml/xmlObjects.py +++ b/music21/musicxml/xmlObjects.py @@ -123,6 +123,10 @@ class MusicXMLImportException(MusicXMLException): pass +class MusicXMLWarning(UserWarning): + pass + + # ------------------------------------------------------------------------------ # helpers STYLE_ATTRIBUTES_YES_NO_TO_BOOL = ('hideObjectOnPrint', ) diff --git a/music21/note.py b/music21/note.py index 628a2c82b7..5b99a62d4c 100644 --- a/music21/note.py +++ b/music21/note.py @@ -878,7 +878,7 @@ class NotRest(GeneralNote): ''' Parent class for Note-like objects that are not rests; that is to say they have a stem, can be tied, and volume is important. - Basically, that's a `Note` or + Basically, that's a `Note` or `Chord` or `Unpitched` object for now. ''' # unspecified means that there may be a stem, but its orientation @@ -1130,7 +1130,7 @@ def hasVolumeInformation(self) -> bool: def pitches(self) -> Tuple[pitch.Pitch]: ''' Returns an empty tuple. (Useful for iterating over NotRests since they - include Notes and Chords + include Notes and Chords.) ''' return () diff --git a/music21/stream/base.py b/music21/stream/base.py index 044d7e6a13..98210275a5 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -9389,7 +9389,7 @@ def isTwelveTone(self): return False return True - def isWellFormedNotation(self): + def isWellFormedNotation(self) -> bool: # noinspection PyShadowingNames ''' Return True if, given the context of this Stream or Stream subclass, diff --git a/music21/stream/tests.py b/music21/stream/tests.py index a2254a571c..c357a67b7d 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -8179,10 +8179,15 @@ def test_makeBeams__paddingLeft_2_2(self): def testOpusWrite(self): import os + from music21 import converter o = Opus() s1 = Score() s2 = Score() + p1 = converter.parse('tinyNotation: 4/4 e1') + p2 = converter.parse('tinyNotation: 4/4 f1') + s1.append(p1) + s2.append(p2) o.append([s1, s2]) out = o.write() From 383f6b4c2477ef51e3ce0ad9d3bc6862ec812714 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 2 May 2021 10:59:42 -0400 Subject: [PATCH 02/12] Remove redundant import --- music21/converter/subConverters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index d9d5e85232..03f2231dc7 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -786,7 +786,6 @@ def parseFile(self, fp, number=None): ''' Open Noteworthy data (as nwctxt) from a file path. - >>> import os #_DOCS_HIDE >>> nwcTranslatePath = common.getSourceFilePath() / 'noteworthy' #_DOCS_HIDE >>> filePath = nwcTranslatePath / 'Part_OWeisheit.nwctxt' #_DOCS_HIDE >>> #_DOCS_SHOW paertPath = converter.parse('d:/desktop/arvo_part_o_weisheit.nwctxt') @@ -1470,7 +1469,6 @@ def testWriteMXL(self): os.remove(mxlPath) def testWriteMusicXMLMakeNotation(self): - import os from music21 import converter from music21 import note From 727a63cf812a8f4c9880cbd7d4a702428cd8be7d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 22 May 2021 21:18:27 -0400 Subject: [PATCH 03/12] Rewrite test now that splitAtDurations() moved (good!) --- music21/converter/subConverters.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 3e01b0017f..7a73262ee3 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1541,15 +1541,13 @@ def testWriteMusicXMLMakeNotation(self): self.assertEqual( len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) - # makeNotation=False on a non-Score still prevents some copying, - # but packing into Score inevitably does some making of notation out3 = p.write(makeNotation=False) roundtrip_back = converter.parse(out3) - # 4/4 will be assumed; quarter note will be moved to measure 2 + # ensure 5.0QL note is not broken up self.assertEqual( - len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[0].notes), 1) + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[0].notes), 2) self.assertEqual( - len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 1) + len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) for out in (out1, out2, out3): os.remove(out) From a4302fd25c93c0d0d23eeb7f0e33b76e57461ec1 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 19 Jun 2021 17:33:33 -0400 Subject: [PATCH 04/12] Fix unit test of warning message --- music21/musicxml/m21ToXml.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 9865ce50f6..e2741c81ab 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6471,15 +6471,23 @@ def testSetPartsAndRefStreamMeasure(self): self.assertSequenceEqual(measuresAtOffsetZero, p.elements[:1]) def testFromScoreNoParts(self): + ''' + Badly nested streams should warn but output no gaps. + ''' s = stream.Score() s.append(meter.TimeSignature('1/4')) s.append(note.Note()) s.append(note.Note()) gex = GeneralObjectExporter(s) - warn_msg = f'{s} is not well-formed; see isWellFormedNotation()' - with self.assertWarns(MusicXMLWarning, msg=warn_msg): + with self.assertWarns(MusicXMLWarning) as cm: tree = ET.fromstring(gex.parse().decode('utf-8')) + self.assertIn(repr(s).split(' 0x')[0], str(cm.warning)) + self.assertIn(' is not well-formed; see isWellFormedNotation()', str(cm.warning)) + # The original score with its original address should not + # be found in the message because makeNotation=True makes a copy + self.assertNotIn(str(s), str(cm.warning)) + # Assert no gaps in stream self.assertSequenceEqual(tree.findall('.//forward'), []) From 07a23311f73dd8dd2bf7c6541027e660b0a8c144 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 19 Jun 2021 17:54:56 -0400 Subject: [PATCH 05/12] No reason to mix str() and repr() --- music21/musicxml/m21ToXml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index e2741c81ab..c2d93a9390 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6486,7 +6486,7 @@ def testFromScoreNoParts(self): self.assertIn(' is not well-formed; see isWellFormedNotation()', str(cm.warning)) # The original score with its original address should not # be found in the message because makeNotation=True makes a copy - self.assertNotIn(str(s), str(cm.warning)) + self.assertNotIn(repr(s), str(cm.warning)) # Assert no gaps in stream self.assertSequenceEqual(tree.findall('.//forward'), []) From d9b54d7ac898c24b432ddc343c0e9f1119da8840 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 25 Jun 2021 19:54:40 -0400 Subject: [PATCH 06/12] Use instance attribute rather than keyword --- music21/converter/subConverters.py | 23 +++++------ music21/musicxml/m21ToXml.py | 64 ++++++++++++++++++------------ 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 7a73262ee3..b37e1c4e3a 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1033,11 +1033,14 @@ def write(self, dataBytes: bytes = b'' generalExporter = m21ToXml.GeneralObjectExporter(obj) - if makeNotation is False and 'Score' in obj.classes: - # Assume well-formed and bypass deepcopy in GeneralObjectExporter.fromScore() - dataBytes = generalExporter.parseWellformedObject(obj, makeNotation=False) + generalExporter.makeNotation = makeNotation + if makeNotation is False: + if 'Score' not in obj.classes: + raise SubConverterException('Can only export Scores with makeNotation=False') + # bypass deepcopy in GeneralObjectExporter.fromScore() + dataBytes = generalExporter.parseWellformedObject(obj) else: - dataBytes = generalExporter.parse(makeNotation=makeNotation) + dataBytes = generalExporter.parse() writeDataStreamFp = fp if fp is not None and subformats: # could be empty list @@ -1541,15 +1544,11 @@ def testWriteMusicXMLMakeNotation(self): self.assertEqual( len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) - out3 = p.write(makeNotation=False) - roundtrip_back = converter.parse(out3) - # ensure 5.0QL note is not broken up - self.assertEqual( - len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[0].notes), 2) - self.assertEqual( - len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) + # makeNotation = False cannot be used on non-scores + with self.assertRaises(SubConverterException): + p.write(makeNotation=False) - for out in (out1, out2, out3): + for out in (out1, out2): os.remove(out) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index c2d93a9390..4e9f8bb215 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -302,6 +302,10 @@ def _synchronizeIds(element: Element, m21Object: Optional[base.Music21Object]) - class GeneralObjectExporter: + ''' + Packs any `Music21Object` into a well-formed score and exports + a bytes object MusicXML representation. + ''' classMapping = OrderedDict([ ('Score', 'fromScore'), ('Part', 'fromPart'), @@ -320,15 +324,17 @@ class GeneralObjectExporter: def __init__(self, obj=None): self.generalObj = obj + self.makeNotation: bool = True - def parse(self, obj=None, *, makeNotation: bool = True): + def parse(self, obj=None): r''' Return a bytes object representation of anything from a Score to a single pitch. Wrap `obj` in a well-formed stream (using :meth:`parseWellformedObject`), making a copy and making notation - along the way. To skip making notation, pass `makeNotation=False`. + along the way. To skip making notation, set `.makeNotation` on this + `GeneralObjectExporter` instance to False. To skip making a copy, call `parseWellformedObject()` directly. >>> p = pitch.Pitch('D#4') @@ -392,19 +398,20 @@ def parse(self, obj=None, *, makeNotation: bool = True): if obj is None: obj = self.generalObj outObj = self.fromGeneralObject(obj) - return self.parseWellformedObject(outObj, makeNotation=makeNotation) + return self.parseWellformedObject(outObj) - def parseWellformedObject(self, sc, *, makeNotation: bool = True) -> bytes: + def parseWellformedObject(self, sc) -> bytes: ''' Parse an object that has usually already gone through the `.fromGeneralObject` conversion, which has produced a copy with :meth:`~music21.stream.Score.makeNotation` run on it. - `makeNotation=True` (default) runs :meth:`~music21.stream.makeNotation` + When :attr:`makeNotation` is True (default), runs + :meth:`~music21.stream.makeNotation` on each of the parts, this time without making a copy, possibly leaving side effects on `sc`. - Set `makeNotation=False` to avoid making notation. + Set `.makeNotation` to False to avoid making notation. Returns bytes. ''' @@ -412,8 +419,8 @@ def parseWellformedObject(self, sc, *, makeNotation: bool = True) -> bytes: warnings.warn(f'{sc} is not well-formed; see isWellFormedNotation()', category=MusicXMLWarning) - scoreExporter = ScoreExporter(sc) - scoreExporter.parse(makeNotation=makeNotation) + scoreExporter = ScoreExporter(sc, makeNotation=self.makeNotation) + scoreExporter.parse() return scoreExporter.asBytes() def fromGeneralObject(self, obj): @@ -1358,7 +1365,7 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin): a musicxml Element. ''' - def __init__(self, score: Optional[stream.Score] = None): + def __init__(self, score: Optional[stream.Score] = None, makeNotation: bool = True): super().__init__() if score is None: # should not be done this way. @@ -1389,7 +1396,9 @@ def __init__(self, score: Optional[stream.Score] = None): self.parts = [] - def parse(self, makeNotation: bool = True): + self.makeNotation: bool = makeNotation + + def parse(self): ''' the main function to call. @@ -1412,9 +1421,9 @@ def parse(self, makeNotation: bool = True): self.scorePreliminaries() if s.hasPartLikeStreams(): - self.parsePartlikeScore(makeNotation=makeNotation) + self.parsePartlikeScore() else: - self.parseFlatScore(makeNotation=makeNotation) + self.parseFlatScore() self.postPartProcess() @@ -1581,11 +1590,11 @@ def setScoreLayouts(self): self.scoreLayouts = scoreLayouts self.firstScoreLayout = scoreLayout - def parsePartlikeScore(self, makeNotation: bool = True): + def parsePartlikeScore(self): ''' Called by .parse() if the score has individual parts. - Calls makeRests() for the part (if `makeNotation=True`), + Calls makeRests() for the part (if `ScoreExporter.makeNotation` is True), then 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. @@ -1600,7 +1609,7 @@ def parsePartlikeScore(self, makeNotation: bool = True): >>> '' in outStr True ''' - if makeNotation: + if self.makeNotation: # self.parts is a stream of parts # hide any rests created at this late stage, because we are # merely trying to fill up MusicXML display, not impose things on users @@ -1624,7 +1633,7 @@ def parsePartlikeScore(self, makeNotation: bool = True): pp.parse() self.partExporterList.append(pp) - def parseFlatScore(self, makeNotation: bool = True): + def parseFlatScore(self): ''' creates a single PartExporter for this Stream and parses it. @@ -1647,7 +1656,7 @@ def parseFlatScore(self, makeNotation: bool = True): ''' s = self.stream pp = PartExporter(s, parent=self) - pp.parse(makeNotation=makeNotation) + pp.parse() self.partExporterList.append(pp) def postPartProcess(self): @@ -2406,10 +2415,12 @@ def __init__(self, partObj: Union[stream.Part, stream.Score, None] = None, paren self.meterStream = stream.Stream() self.refStreamOrTimeRange = [0.0, 0.0] self.midiChannelList = [] + self.makeNotation = True else: self.meterStream = parent.meterStream self.refStreamOrTimeRange = parent.refStreamOrTimeRange self.midiChannelList = parent.midiChannelList # shared list + self.makeNotation = parent.makeNotation self.instrumentStream = None self.firstInstrumentObject = None @@ -2420,7 +2431,7 @@ def __init__(self, partObj: Union[stream.Part, stream.Score, None] = None, paren self.spannerBundle = partObj.spannerBundle - def parse(self, makeNotation: bool = True): + def parse(self): ''' Set up instruments, create a partId (if no good one exists) and sets it on , fixes up the notation (:meth:`fixupNotationFlat` or :meth:`fixupNotationMeasured`, @@ -2430,15 +2441,16 @@ def parse(self, makeNotation: bool = True): In other words, one-stop shopping. - `makeNotation=False` is used to avoid running `makeNotation` on the Part. - This keyword is passed down from `show()`, `write()`, and - :meth:`~music21.musicxml.m21ToXml.ScoreExporter.parse`. It - will raise if no measures are present in the part. + :attr:`makeNotation` when False, will avoid running `makeNotation` on the Part. + Generally this attribute is set on `GeneralObjectExporter` or `ScoreExporter` + and read from there. Running with `makeNotation` as False will raise + `MusicXMLExportException` if no measures are present in the part. >>> from music21.musicxml.m21ToXml import PartExporter >>> noMeasures = stream.Part(note.Note()) >>> pex = PartExporter(noMeasures) - >>> pex.parse(makeNotation=False) + >>> pex.makeNotation = False + >>> pex.parse() Traceback (most recent call last): music21.musicxml.xmlObjects.MusicXMLExportException: Cannot export with makeNotation=False if there are no measures @@ -2451,11 +2463,11 @@ def parse(self, makeNotation: bool = True): self.stream = self.stream.splitAtDurations(recurse=True)[0] # Suppose that everything below this is a measure - if makeNotation and not self.stream.getElementsByClass(stream.Measure): + if self.makeNotation and not self.stream.getElementsByClass(stream.Measure): self.fixupNotationFlat() - elif makeNotation: + elif self.makeNotation: self.fixupNotationMeasured() - else: + elif not self.stream.getElementsByClass(stream.Measure): raise MusicXMLExportException( 'Cannot export with makeNotation=False if there are no measures') # make sure that all instances of the same class have unique ids From 7a5de6c43cc2adc53fd640e254b2b2ac9205a8e3 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 25 Jun 2021 20:05:59 -0400 Subject: [PATCH 07/12] Import the class defined earlier in this submodule? Why this didn't fail with a NameError strikes me as odd. --- music21/converter/subConverters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index b37e1c4e3a..57f02faa9b 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1517,6 +1517,7 @@ def testWriteMXL(self): def testWriteMusicXMLMakeNotation(self): from music21 import converter from music21 import note + from .subConverters import SubConverterException as subEx m1 = stream.Measure(note.Note(quarterLength=5.0)) m2 = stream.Measure() @@ -1545,7 +1546,7 @@ def testWriteMusicXMLMakeNotation(self): len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) # makeNotation = False cannot be used on non-scores - with self.assertRaises(SubConverterException): + with self.assertRaises(subEx): p.write(makeNotation=False) for out in (out1, out2): From f00496289546456f7a2cb485b76882a18549d175 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Jun 2021 09:52:56 -0400 Subject: [PATCH 08/12] Correct exception class name --- music21/converter/subConverters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 57f02faa9b..2f00a8ffec 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1517,7 +1517,6 @@ def testWriteMXL(self): def testWriteMusicXMLMakeNotation(self): from music21 import converter from music21 import note - from .subConverters import SubConverterException as subEx m1 = stream.Measure(note.Note(quarterLength=5.0)) m2 = stream.Measure() @@ -1546,7 +1545,7 @@ def testWriteMusicXMLMakeNotation(self): len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) # makeNotation = False cannot be used on non-scores - with self.assertRaises(subEx): + with self.assertRaises(converter.subConverters.SubConverterException): p.write(makeNotation=False) for out in (out1, out2): From f6b68bd23b9e1788b5bbce3a71c0f14374a13e48 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Jun 2021 10:05:34 -0400 Subject: [PATCH 09/12] Add caveats --- music21/base.py | 6 ++++-- music21/converter/subConverters.py | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/music21/base.py b/music21/base.py index 7b834c1993..1c06dc336c 100644 --- a/music21/base.py +++ b/music21/base.py @@ -2561,7 +2561,8 @@ def write(self, fmt=None, fp=None, **keywords): # pragma: no cover Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed score if necessary, and run :meth:`~music21.stream.Score.makeNotation`. To - avoid this when writing .musicxml, use `makeNotation=False`. + avoid this when writing .musicxml, use `makeNotation=False`, an advanced option + that prioritizes speed but may not guarantee satisfactory notation. ''' if fmt is None: # get setting in environment fmt = environLocal['writeFormat'] @@ -2626,7 +2627,8 @@ def show(self, fmt=None, app=None, **keywords): # pragma: no cover Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed score if necessary, and run :meth:`~music21.stream.Score.makeNotation`. To - avoid this when showing .musicxml, use `makeNotation=False`. + avoid this when showing .musicxml, use `makeNotation=False`, an advanced option + that prioritizes speed but may not guarantee satisfactory notation. ''' # note that all formats here must be defined in # common.VALID_SHOW_FORMATS diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 2f00a8ffec..432ce62842 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1014,9 +1014,10 @@ def write(self, Write to a .musicxml file. Set `makeNotation=False` to prevent fixing up the notation, and where possible, - to prevent making additional deepcopies. (If `obj` is not a - :class:`~music21.stream.Score`, `obj` or its elements might still be copied. - See :meth:`~music21.musicxml.m21ToXml.GeneralObjectExporter.fromGeneralObject`.) + to prevent making additional deepcopies. (This option cannot be used if `obj` is not a + :class:`~music21.stream.Score`). `makeNotation=True` generally solves common notation + issues, whereas makeNotation=False is intended for advanced users facing + special cases where speed is a priority or making notation reverses user choices. Set `compress=True` to immediately compress the output to a .mxl file. ''' From 0e8ce5fa09664ea34d8f6e114e9891ab8bb2da12 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Jun 2021 10:19:49 -0400 Subject: [PATCH 10/12] Refactor --- music21/converter/subConverters.py | 15 +++++-------- music21/musicxml/m21ToXml.py | 35 ++++++++++++++---------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 432ce62842..dcf6c1a1b5 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1015,8 +1015,8 @@ def write(self, Set `makeNotation=False` to prevent fixing up the notation, and where possible, to prevent making additional deepcopies. (This option cannot be used if `obj` is not a - :class:`~music21.stream.Score`). `makeNotation=True` generally solves common notation - issues, whereas makeNotation=False is intended for advanced users facing + :class:`~music21.stream.Score`.) `makeNotation=True` generally solves common notation + issues, whereas `makeNotation=False` is intended for advanced users facing special cases where speed is a priority or making notation reverses user choices. Set `compress=True` to immediately compress the output to a .mxl file. @@ -1035,13 +1035,7 @@ def write(self, dataBytes: bytes = b'' generalExporter = m21ToXml.GeneralObjectExporter(obj) generalExporter.makeNotation = makeNotation - if makeNotation is False: - if 'Score' not in obj.classes: - raise SubConverterException('Can only export Scores with makeNotation=False') - # bypass deepcopy in GeneralObjectExporter.fromScore() - dataBytes = generalExporter.parseWellformedObject(obj) - else: - dataBytes = generalExporter.parse() + dataBytes = generalExporter.parse() writeDataStreamFp = fp if fp is not None and subformats: # could be empty list @@ -1518,6 +1512,7 @@ def testWriteMXL(self): def testWriteMusicXMLMakeNotation(self): from music21 import converter from music21 import note + from music21.musicxml.xmlObjects import MusicXMLExportException m1 = stream.Measure(note.Note(quarterLength=5.0)) m2 = stream.Measure() @@ -1546,7 +1541,7 @@ def testWriteMusicXMLMakeNotation(self): len(roundtrip_back.parts.first().getElementsByClass(stream.Measure)[1].notes), 0) # makeNotation = False cannot be used on non-scores - with self.assertRaises(converter.subConverters.SubConverterException): + with self.assertRaises(MusicXMLExportException): p.write(makeNotation=False) for out in (out1, out2): diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 4e9f8bb215..57584484bb 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -331,11 +331,10 @@ def parse(self, obj=None): Return a bytes object representation of anything from a Score to a single pitch. - Wrap `obj` in a well-formed stream (using - :meth:`parseWellformedObject`), making a copy and making notation - along the way. To skip making notation, set `.makeNotation` on this - `GeneralObjectExporter` instance to False. - To skip making a copy, call `parseWellformedObject()` directly. + When :attr:`makeNotation` is True (default), wraps `obj` in a well-formed + `Score`, makes a copy, and runs + :meth:`~music21.stream.makeNotation` on each of the parts. To skip copying + and making notation, set `.makeNotation` on this instance to False. >>> p = pitch.Pitch('D#4') >>> GEX = musicxml.m21ToXml.GeneralObjectExporter(p) @@ -397,8 +396,13 @@ def parse(self, obj=None): ''' if obj is None: obj = self.generalObj - outObj = self.fromGeneralObject(obj) - return self.parseWellformedObject(outObj) + if self.makeNotation: + outObj = self.fromGeneralObject(obj) + return self.parseWellformedObject(outObj) + else: + if 'Score' not in obj.classes: + raise MusicXMLExportException('Can only export Scores with makeNotation=False') + return self.parseWellformedObject(obj) def parseWellformedObject(self, sc) -> bytes: ''' @@ -406,13 +410,6 @@ def parseWellformedObject(self, sc) -> bytes: `.fromGeneralObject` conversion, which has produced a copy with :meth:`~music21.stream.Score.makeNotation` run on it. - When :attr:`makeNotation` is True (default), runs - :meth:`~music21.stream.makeNotation` - on each of the parts, this time without making a copy, possibly leaving - side effects on `sc`. - - Set `.makeNotation` to False to avoid making notation. - Returns bytes. ''' if not sc.isWellFormedNotation(): @@ -2435,16 +2432,16 @@ def parse(self): ''' Set up instruments, create a partId (if no good one exists) and sets it on , fixes up the notation (:meth:`fixupNotationFlat` or :meth:`fixupNotationMeasured`, - if `makeNotation` is True), + if :attr:`makeNotation` is True), setsIdLocals on spanner bundle. runs parse() on each measure's MeasureExporter and appends the output to the object. In other words, one-stop shopping. - :attr:`makeNotation` when False, will avoid running `makeNotation` on the Part. - Generally this attribute is set on `GeneralObjectExporter` or `ScoreExporter` - and read from there. Running with `makeNotation` as False will raise - `MusicXMLExportException` if no measures are present in the part. + :attr:`makeNotation` when False, will avoid running :meth:`~music21.stream.makeNotation` + on the Part. Generally this attribute is set on `GeneralObjectExporter` + or `ScoreExporter` and read from there. Running with `makeNotation` + as False will raise `MusicXMLExportException` if no measures are present. >>> from music21.musicxml.m21ToXml import PartExporter >>> noMeasures = stream.Part(note.Note()) From 6d5c7b7787b29d3aedda4d64b847f3fbdd9e8ef6 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Jun 2021 10:30:01 -0400 Subject: [PATCH 11/12] Move warning to fromScore() --- music21/musicxml/m21ToXml.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 57584484bb..e65bef1983 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -406,16 +406,13 @@ def parse(self, obj=None): def parseWellformedObject(self, sc) -> bytes: ''' - Parse an object that has usually already gone through the + Parse an object that has already gone through the `.fromGeneralObject` conversion, which has produced a copy with - :meth:`~music21.stream.Score.makeNotation` run on it. + :meth:`~music21.stream.Score.makeNotation` run on it + (unless :attr:`makeNotation` is False). Returns bytes. ''' - if not sc.isWellFormedNotation(): - warnings.warn(f'{sc} is not well-formed; see isWellFormedNotation()', - category=MusicXMLWarning) - scoreExporter = ScoreExporter(sc, makeNotation=self.makeNotation) scoreExporter.parse() return scoreExporter.asBytes() @@ -457,6 +454,9 @@ def fromScore(self, sc): Copies the input score and runs :meth:`~music21.stream.Score.makeNotation` on the copy. ''' scOut = sc.makeNotation(inPlace=False) + if not scOut.isWellFormedNotation(): + warnings.warn(f'{scOut} is not well-formed; see isWellFormedNotation()', + category=MusicXMLWarning) # scOut.makeImmutable() return scOut From f6a5a44197612c959d6e24c2c46865b042cbe205 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Jun 2021 10:48:53 -0400 Subject: [PATCH 12/12] Fix Sphinx links to makeNotation() here and elsewhere --- .../source/usersGuide/usersGuide_14_timeSignatures.ipynb | 2 +- .../source/usersGuide/usersGuide_31_clefs.ipynb | 2 +- music21/audioSearch/__init__.py | 2 +- music21/braille/translate.py | 4 ++-- music21/musicxml/m21ToXml.py | 9 +++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/documentation/source/usersGuide/usersGuide_14_timeSignatures.ipynb b/documentation/source/usersGuide/usersGuide_14_timeSignatures.ipynb index d5085a5390..a259e54f63 100644 --- a/documentation/source/usersGuide/usersGuide_14_timeSignatures.ipynb +++ b/documentation/source/usersGuide/usersGuide_14_timeSignatures.ipynb @@ -982,7 +982,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Whoops! The last F of measure 2 is actually too long, and the next measure's first F begins half a beat later. Let's run the powerful command :meth:`~music21.stream.Stream.makeNotation` first before showing:" + "Whoops! The last F of measure 2 is actually too long, and the next measure's first F begins half a beat later. Let's run the powerful command :meth:`~music21.stream.base.Stream.makeNotation` first before showing:" ] }, { diff --git a/documentation/source/usersGuide/usersGuide_31_clefs.ipynb b/documentation/source/usersGuide/usersGuide_31_clefs.ipynb index a4322ca8e0..8526752b74 100644 --- a/documentation/source/usersGuide/usersGuide_31_clefs.ipynb +++ b/documentation/source/usersGuide/usersGuide_31_clefs.ipynb @@ -2226,7 +2226,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This is accomplished because before showing the Stream, `music21` runs the powerful method :meth:`~music21.stream.Stream.makeNotation` on the stream. This calls a function in :ref:`moduleStreamMakeNotation` module called :func:`~music21.stream.makeNotation.makeBeams` that does the real work. That function checks the stream to see if any beams exist on it:" + "This is accomplished because before showing the Stream, `music21` runs the powerful method :meth:`~music21.stream.base.Stream.makeNotation` on the stream. This calls a function in :ref:`moduleStreamMakeNotation` module called :func:`~music21.stream.makeNotation.makeBeams` that does the real work. That function checks the stream to see if any beams exist on it:" ] }, { diff --git a/music21/audioSearch/__init__.py b/music21/audioSearch/__init__.py index 1cc74a8c31..9de265b936 100644 --- a/music21/audioSearch/__init__.py +++ b/music21/audioSearch/__init__.py @@ -820,7 +820,7 @@ def notesAndDurationsToStream( returns a :class:`~music21.stream.Score` object, containing a metadata object and a single :class:`~music21.stream.Part` object, which in turn - contains the notes, etc. Does not run :meth:`~music21.stream.Stream.makeNotation` + contains the notes, etc. Does not run :meth:`~music21.stream.base.Stream.makeNotation` on the Score. diff --git a/music21/braille/translate.py b/music21/braille/translate.py index 906686e686..124683fc19 100644 --- a/music21/braille/translate.py +++ b/music21/braille/translate.py @@ -26,12 +26,12 @@ Keywords: -* **inPlace** (False): If False, then :meth:`~music21.stream.Stream.makeNotation` is called +* **inPlace** (False): If False, then :meth:`~music21.stream.base.Stream.makeNotation` is called on all :class:`~music21.stream.Measure`, :class:`~music21.stream.Part`, and :class:`~music21.stream.PartStaff` instances. Copies of those objects are then used to transcribe the music. If True, the transcription is done "as is." This is useful for strict transcription because - sometimes :meth:`~music21.stream.Stream.makeNotation` + sometimes :meth:`~music21.stream.base.Stream.makeNotation` introduces some unwanted artifacts in the music. However, the music needs to be organized into measures for transcription to work. * **debug** (False): If True, a braille-english representation of the music is returned. Useful diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index e65bef1983..53a036381f 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -332,9 +332,9 @@ def parse(self, obj=None): Score to a single pitch. When :attr:`makeNotation` is True (default), wraps `obj` in a well-formed - `Score`, makes a copy, and runs - :meth:`~music21.stream.makeNotation` on each of the parts. To skip copying - and making notation, set `.makeNotation` on this instance to False. + `Score`, makes a copy, and runs :meth:`~music21.stream.base.Stream.makeNotation` + on each of the parts. To skip copying and making notation, set `.makeNotation` + on this instance to False. >>> p = pitch.Pitch('D#4') >>> GEX = musicxml.m21ToXml.GeneralObjectExporter(p) @@ -2438,7 +2438,8 @@ def parse(self): In other words, one-stop shopping. - :attr:`makeNotation` when False, will avoid running :meth:`~music21.stream.makeNotation` + :attr:`makeNotation` when False, will avoid running + :meth:`~music21.stream.base.Stream.makeNotation` on the Part. Generally this attribute is set on `GeneralObjectExporter` or `ScoreExporter` and read from there. Running with `makeNotation` as False will raise `MusicXMLExportException` if no measures are present.