Repository: cuthbertLab/music21
Pull request #1401 (2022-08-27 20:01:47)

Fix (and test) writing of multi-measure RepeatBrackets to MusicXML.
Before this fix, the first and middle measures all had a 'stop' ending on the right barline.


Patch:
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index 949d0252f..9bb6489d1 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -6244,22 +6244,26 @@ class MeasureExporter(XMLExporterBase):
         # TODO: coda
         # TODO: fermata
 
-        if self.rbSpanners:  # and self.rbSpanners[0].isFirst(m)???
-            mxEnding = Element('ending')
-            if position == 'left':
+        if self.rbSpanners:
+            if position == 'left' and self.rbSpanners[0].isFirst(self.stream):
                 endingType = 'start'
-            else:
+            elif position == 'right' and self.rbSpanners[0].isLast(self.stream):
                 endingType = 'stop'
-            numberList = self.rbSpanners[0].getNumberList()
-            numberStr = str(numberList[0])
-            # 0 is not a valid "ending-number"
-            if numberStr == '0':
-                numberStr = ''
-            for num in numberList[1:]:
-                numberStr += ',' + str(num)  # comma-separated ending numbers
-            mxEnding.set('number', numberStr)
-            mxEnding.set('type', endingType)
-            mxBarline.append(mxEnding)  # make sure it is after fermata but before repeat.
+            else:
+                endingType = ''
+
+            if endingType:
+                mxEnding = Element('ending')
+                numberList = self.rbSpanners[0].getNumberList()
+                numberStr = str(numberList[0])
+                # 0 is not a valid "ending-number"
+                if numberStr == '0':
+                    numberStr = ''
+                for num in numberList[1:]:
+                    numberStr += ',' + str(num)  # comma-separated ending numbers
+                mxEnding.set('number', numberStr)
+                mxEnding.set('type', endingType)
+                mxBarline.append(mxEnding)  # make sure it is after fermata but before repeat.
 
         if mxRepeat is not None:
             mxBarline.append(mxRepeat)


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index 34a6ba77c..37938bb65 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -18627,6 +18627,334 @@ pianoStaffPolymeterWithClefOctaveChange = (
 </score-partwise>
 ''')
 
+multiMeasureEnding = (
+    '''<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE score-partwise  PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN"
+ "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>Trio (snippet)</work-title>
+  </work>
+  <movement-number>1</movement-number>
+  <identification>
+    <creator type="composer">Chopin, Fryderyk</creator>
+    <encoding>
+      <encoding-date>2022-08-27</encoding-date>
+      <software>music21 v.8.0.0a12</software>
+    </encoding>
+  </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+    </scaling>
+  </defaults>
+  <part-list>
+    <score-part id="P2c54d8474d928bcfb818261a5e9fa301">
+      <part-name>ALLEGRO | CON | FUOCO.</part-name>
+    </score-part>
+  </part-list>
+  <!--=========================== Part 1 ===========================-->
+  <part id="P2c54d8474d928bcfb818261a5e9fa301">
+    <!--========================= Measure 1 =========================-->
+    <measure number="1">
+      <note>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>10080</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+      </note>
+      <note>
+        <rest />
+        <duration>10080</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+      </note>
+      <note>
+        <rest />
+        <duration>20160</duration>
+        <voice>1</voice>
+        <type>half</type>
+      </note>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+    <!--========================= Measure 2 =========================-->
+    <measure number="2">
+      <barline location="left">
+        <ending number="1" type="start" />
+      </barline>
+      <note>
+        <rest />
+        <duration>20160</duration>
+        <voice>1</voice>
+        <type>half</type>
+      </note>
+      <direction>
+        <direction-type>
+          <dynamics>
+            <p />
+          </dynamics>
+        </direction-type>
+        <sound dynamics="44" />
+      </direction>
+      <direction placement="below">
+        <direction-type>
+          <wedge number="3" spread="0" type="crescendo" />
+        </direction-type>
+      </direction>
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>20160</duration>
+        <tie type="start" />
+        <voice>1</voice>
+        <type>half</type>
+        <accidental>sharp</accidental>
+        <notations>
+          <tied type="start" />
+        </notations>
+      </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge number="3" spread="15" type="stop" />
+        </direction-type>
+      </direction>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+    <!--========================= Measure 3 =========================-->
+    <measure number="3">
+      <direction placement="below">
+        <direction-type>
+          <wedge number="4" spread="15" type="diminuendo" />
+        </direction-type>
+      </direction>
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>40320</duration>
+        <tie type="stop" />
+        <tie type="start" />
+        <voice>1</voice>
+        <type>whole</type>
+        <accidental>sharp</accidental>
+        <notations>
+          <tied type="stop" />
+          <tied type="start" />
+        </notations>
+      </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge number="4" spread="0" type="stop" />
+        </direction-type>
+      </direction>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+    <!--========================= Measure 4 =========================-->
+    <measure number="4">
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>40320</duration>
+        <tie type="stop" />
+        <voice>1</voice>
+        <type>whole</type>
+        <accidental>sharp</accidental>
+        <notations>
+          <tied type="stop" />
+        </notations>
+      </note>
+      <barline location="right">
+        <ending number="1" type="stop" />
+        <repeat direction="backward" />
+      </barline>
+    </measure>
+    <!--========================= Measure 5 =========================-->
+    <measure number="5">
+      <barline location="left">
+        <ending number="2" type="start" />
+      </barline>
+      <note>
+        <rest />
+        <duration>20160</duration>
+        <voice>1</voice>
+        <type>half</type>
+      </note>
+      <direction>
+        <direction-type>
+          <dynamics>
+            <p />
+          </dynamics>
+        </direction-type>
+        <sound dynamics="44" />
+      </direction>
+      <direction placement="below">
+        <direction-type>
+          <wedge number="5" spread="15" type="diminuendo" />
+        </direction-type>
+      </direction>
+      <note>
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>20160</duration>
+        <tie type="start" />
+        <voice>1</voice>
+        <type>half</type>
+        <notations>
+          <tied type="start" />
+        </notations>
+      </note>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+    <!--========================= Measure 6 =========================-->
+    <measure number="6">
+      <note>
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>20160</duration>
+        <tie type="stop" />
+        <voice>1</voice>
+        <type>half</type>
+        <notations>
+          <tied type="stop" />
+        </notations>
+      </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge number="5" spread="0" type="stop" />
+        </direction-type>
+      </direction>
+      <note>
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>20160</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <accidental>sharp</accidental>
+      </note>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+    <!--========================= Measure 7 =========================-->
+    <measure number="7">
+      <note>
+        <pitch>
+          <step>G</step>
+          <alter>0</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>40320</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <accidental>natural</accidental>
+      </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        <ending number="2" type="stop" />
+      </barline>
+    </measure>
+    <!--========================= Measure 8 =========================-->
+    <measure number="8">
+      <direction>
+        <direction-type>
+          <words enclosure="none" font-style="italic" font-weight="bold">Risoluto.</words>
+        </direction-type>
+      </direction>
+      <direction>
+        <direction-type>
+          <dynamics>
+            <f />
+          </dynamics>
+        </direction-type>
+        <sound dynamics="88" />
+      </direction>
+      <note>
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>20160</duration>
+        <voice>1</voice>
+        <type>half</type>
+      </note>
+      <note>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>10080</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <notations>
+          <articulations>
+            <staccato />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <notations>
+          <articulations>
+            <staccato />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <rest />
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+      </note>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+      </note>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+  </part>
+</score-partwise>
+''')
+
 
 ALL = [
     articulations01, pitches01a, directions31a, lyricsMelisma61d, notations32a,  # 0
@@ -18644,7 +18972,8 @@ ALL = [
     colors01, triplets01, textBoxes01, octaveShifts33d,  # 40
     unicodeStrNoNonAscii, unicodeStrWithNonAscii,  # 44
     tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied, pianoStaffPolymeter,  # 46
-    arpeggio32d, multiStaffArpeggios  # 51
+    arpeggio32d, multiStaffArpeggios, multiMeasureEnding,   # 51
+    pianoStaffPolymeterWithClefOctaveChange,  # 54
 ]
 
 
diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py
index 860ad01c7..fe1277bf3 100644
--- a/music21/musicxml/test_m21ToXml.py
+++ b/music21/musicxml/test_m21ToXml.py
@@ -413,6 +413,72 @@ class Test(unittest.TestCase):
         endings = x.findall('.//ending')
         self.assertEqual([e.get('number') for e in endings], ['', '', '3', '3'])
 
+    def testMultiMeasureEndingsWrite(self):
+        # Relevant barlines:
+        # Measure 1, left barline: no ending
+        # Measure 1, right barline: no ending
+        # Measure 2, left barline: <ending number="1" type="start"/>
+        # Measure 2, right barline: no ending
+        # Measure 3, left barline: no ending
+        # Measure 3, right barline: no ending
+        # Measure 4, left barline: no ending
+        # Measure 4, right barline: <ending number="1" type="stop"/>
+        # Measure 5, left barline: <ending number="2" type="start"/>
+        # Measure 5, right barline: no ending
+        # Measure 6, left barline: no ending
+        # Measure 6, right barline: no ending
+        # Measure 7, left barline: no ending
+        # Measure 7, right barline: <ending number="2" type="stop"/>
+        # Measure 8, left barline: no ending
+        # Measure 8, right barline: no ending
+        s = converter.parse(testPrimitive.multiMeasureEnding)
+        x = self.getET(s)
+        endings = x.findall('.//ending')
+        self.assertEqual([e.get('number') for e in endings], ['1', '1', '2', '2'])
+
+        expectedEndings = {
+            # key = measure number, value = list(leftEndingType, rightEndingType)
+            '1': [None, None],     # measure before the endings
+            '2': ['start', None],  # first measure of ending 1
+            '3': [None, None],     # second measure of ending 1
+            '4': [None, 'stop'],   # last measure of ending 1
+            '5': ['start', None],  # first measure of ending 2
+            '6': [None, None],     # second measure of ending 2
+            '7': [None, 'stop'],   # last measure of ending 2
+            '8': [None, None]      # measure after the endings
+        }
+        measures = x.findall('.//measure')
+        self.assertEqual(len(measures), 8)
+        for measure in measures:
+            measNumber = measure.get('number')
+            with self.subTest(measureNumber=measNumber):
+                expectLeftBarline = bool(expectedEndings[measNumber][0] is not None)
+                expectRightBarline = bool(expectedEndings[measNumber][1] is not None)
+
+                gotLeftBarline = False
+                gotRightBarline = False
+                barlines = measure.findall('.//barline')
+                for i, barline in enumerate(barlines):
+                    if barline.get('location') == 'left':
+                        gotLeftBarline = True
+                        leftEndingType = None
+                        leftEnding = barline.find('ending')
+                        if leftEnding is not None:
+                            leftEndingType = leftEnding.get('type')
+                        self.assertEqual(leftEndingType, expectedEndings[measNumber][0])
+                    elif barline.get('location') == 'right':
+                        gotRightBarline = True
+                        rightEndingType = None
+                        rightEnding = barline.find('ending')
+                        if rightEnding is not None:
+                            rightEndingType = rightEnding.get('type')
+                        self.assertEqual(rightEndingType, expectedEndings[measNumber][1])
+
+                if expectLeftBarline:
+                    self.assertTrue(gotLeftBarline)
+                if expectRightBarline:
+                    self.assertTrue(gotRightBarline)
+
     def testTextExpressionOffset(self):
         '''Transfer element offset after calling getTextExpression().'''
         # https://github.com/cuthbertLab/music21/issues/624