Repository: cuthbertLab/music21 (python)

Issue:
Unexpected mixed stem directions from `setStemDirectionForBeamGroups()`
**music21 version**

dev; likely an issue since 6.7

**Problem summary**
`setStemDirectionOneGroup()`, a helper routine for `setStemDirectionForBeamGroups()` treats a mixture of "up" and "unspecified" stem directions as "consistent" for the purposes of how careful to be about observing the kwarg `overrideConsistentStemDirections`. Then, when unspecified stem directions are encountered during a scan, there is no look-back to fix the earlier directions, leading to mismatch:


**Steps to reproduce**

```
>>> s = stream.Stream()
>>> s.insert(clef.TrebleClef())
>>> n1 = note.Note('B4')
>>> n2 = note.Note('B3')
>>> n1.stemDirection = 'down'
>>> n2.stemDirection
'unspecified'
>>> s.append(n1)
>>> s.append(n2)
>>> stream.makeNotation.setStemDirectionOneGroup([n1, n2])
>>> n1.stemDirection  # expect 'up' to match n2
'down'
>>> n2.stemDirection
'up'
>>> s.show()
```

**Expected vs. actual behavior**
![Screen Shot 2021-07-20 at 7 36 52 PM](https://user-images.githubusercontent.com/38668450/126408341-9931c8c7-9baf-48dc-b9c2-a52f9dbc6c37.png)

**More information**
I suggest treating any group with "unspecified" directions anywhere as NOT a consistent stem direction for the purposes of observing that kwarg.


Pull request description:
Handle "unspecified" direction in middle of a beam group
Fixes #1065, then will add a port for m21j next.

Patch:
diff --git a/music21/_version.py b/music21/_version.py
index 7651c8d0c..b4949ae8b 100644
--- a/music21/_version.py
+++ b/music21/_version.py
@@ -42,7 +42,7 @@ When changing, update the single test case in base.py.
 Changing this number invalidates old pickles -- do it if the old pickles create a problem.
 '''
 
-__version_info__ = (7, 0, 7, 'b1')  # can be 4-tuple: (7, 0, 5, 'a2')
+__version_info__ = (7, 0, 8)  # can be 4-tuple: (7, 0, 5, 'a2')
 
 v = '.'.join(str(x) for x in __version_info__[0:3])
 if len(__version_info__) > 3 and __version_info__[3]:
diff --git a/music21/base.py b/music21/base.py
index 3330d787c..1c930b143 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -28,7 +28,7 @@ available after importing `music21`.
 <class 'music21.base.Music21Object'>
 
 >>> music21.VERSION_STR
-'7.0.7b1'
+'7.0.8'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
diff --git a/music21/meter/base.py b/music21/meter/base.py
index 98d4d016f..ef4ac52c6 100644
--- a/music21/meter/base.py
+++ b/music21/meter/base.py
@@ -1293,6 +1293,14 @@ class TimeSignature(base.Music21Object):
          <music21.beam.Beams <music21.beam.Beam 1/continue>>,
          <music21.beam.Beams <music21.beam.Beam 1/continue>>,
          <music21.beam.Beams <music21.beam.Beam 1/stop>>]
+
+        Fixed in v.7 -- incomplete final measures in 6/8:
+
+        >>> sixEight = meter.TimeSignature('6/8')
+        >>> nList = [note.Note(type='quarter'), note.Note(type='eighth'), note.Note(type='eighth')]
+        >>> beamList = sixEight.getBeams(nList)
+        >>> print(beamList)
+        [None, None, None]
         '''
         from music21 import stream
         if isinstance(srcList, stream.Stream):
@@ -1352,8 +1360,11 @@ class TimeSignature(base.Music21Object):
 
             # watch for a special case where a duration completely fills
             # the archetype; this generally should not be beamed
-            if (start == archetypeSpanStart
-                    and end == archetypeSpanEnd):
+            # same if beamPrevious is None and beamNumber == 1 (quarter-eighth in 6/8)
+            if end == archetypeSpanEnd and (
+                start == archetypeSpanStart
+                or (beamPrevious is None and beamNumber == 1)
+            ):
                 # increment position and continue loop
                 beamsList[i] = None  # replace with None!
                 return
@@ -1368,7 +1379,7 @@ class TimeSignature(base.Music21Object):
 
             elif isLast:  # last is always stop
                 beamType = 'stop'
-                # get a partial beam if we cannot come form a beam
+                # get a partial beam if we cannot form a beam
                 if (beamPrevious is None
                         or beamNumber not in beamPrevious.getNumbers()):
                     # environLocal.warn(['triggering partial left where a stop normally falls'])
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index f84960122..050e8e405 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -1989,8 +1989,8 @@ class PartParser(XMLParserBase):
         Fills an empty measure with a measure of rest (bug in PDFtoMusic and
         other MusicXML writers).
 
-        sets self.lastMeasureWasShort to True or False if it is an incomplete measure
-        that is not a pickup.
+        Sets self.lastMeasureWasShort to True or False if it is an incomplete measure
+        that is not a pickup and sets paddingRight.
 
         >>> m = stream.Measure([meter.TimeSignature('4/4'), harmony.ChordSymbol('C7')])
         >>> m.highestTime
@@ -2002,6 +2002,17 @@ class PartParser(XMLParserBase):
         4.0
         >>> PP.lastMeasureWasShort
         False
+
+        Incomplete final measure:
+
+        >>> m = stream.Measure([meter.TimeSignature('6/8'), note.Note(), note.Note()])
+        >>> m.offset = 24.0
+        >>> PP = musicxml.xmlToM21.PartParser()
+        >>> PP.lastMeasureOffset = 21.0
+        >>> PP.setLastMeasureInfo(m)
+        >>> PP.adjustTimeAttributesFromMeasure(m)
+        >>> m.paddingRight
+        1.0
         '''
         # note: we cannot assume that the time signature properly
         # describes the offsets w/n this bar. need to look at
@@ -2049,6 +2060,9 @@ class PartParser(XMLParserBase):
                         # or something
                         self.lastMeasureWasShort = False
                 else:
+                    # Incomplete measure that is likely NOT an anacrusis, set paddingRight
+                    if m.barDurationProportion() < 1.0:
+                        m.paddingRight = m.barDuration.quarterLength - m.highestTime
                     if mHighestTime < lastTimeSignatureQuarterLength:
                         self.lastMeasureWasShort = True
                     else:
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index f1a71b5e2..ab47aeca8 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -188,6 +188,9 @@ def makeBeams(
             offset = 0.0
             if m.paddingLeft != 0.0:
                 offset = opFrac(m.paddingLeft)
+            elif m.paddingRight != 0.0:
+                pass
+            # Incomplete measure without any padding set: assume paddingLeft
             elif noteStream.highestTime < barQL:
                 offset = barQL - noteStream.highestTime
 
@@ -1727,9 +1730,11 @@ def setStemDirectionOneGroup(
     if not group:  # pragma: no cover
         return  # should not happen
 
-    up_down_stem_directions = set(n.stemDirection for n in group
-                                  if n.stemDirection in ('up', 'down'))
-    if len(up_down_stem_directions) < 2:
+    stem_directions = {n.stemDirection for n in group
+                       if n.stemDirection in ('up', 'down', 'unspecified')}
+    if 'unspecified' in stem_directions:
+        has_consistent_stem_directions = False
+    elif len(stem_directions) < 2:
         has_consistent_stem_directions = True
     else:
         has_consistent_stem_directions = False
@@ -1760,8 +1765,6 @@ def setStemDirectionOneGroup(
 
 
 
-
-
 # -----------------------------------------------------------------------------
 
 class Test(unittest.TestCase):
@@ -1837,6 +1840,31 @@ class Test(unittest.TestCase):
                          + ['down', 'noStem', 'double', 'down']
                          )
 
+    def testSetStemDirectionConsistency(self):
+        """
+        Stems that would all be up starting from scratch,
+        but because of overrideConsistentStemDirections=False,
+        we only change the first group with an "unspecified" direction
+        """
+        from music21 import converter
+        p = converter.parse('tinyNotation: 2/4 b8 f8 a8 b8')
+        p.makeBeams(inPlace=True)
+        self.assertEqual(
+            [n.stemDirection for n in p.flat.notes],
+            ['up', 'up', 'up', 'up']
+        )
+
+        # make manual changes
+        dStems = ['down', 'unspecified', 'down', 'down']
+        for n, stemDir in zip(p.flat.notes, dStems):
+            n.stemDirection = stemDir
+
+        setStemDirectionForBeamGroups(p, setNewStems=True, overrideConsistentStemDirections=False)
+        self.assertEqual(
+            [n.stemDirection for n in p.flat.notes],
+            ['up', 'up', 'down', 'down']
+        )
+
     def testMakeBeamsWithStemDirection(self):
         from music21 import converter
         p = converter.parse(self.allaBreveBeamTest)


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 58eb22af6..5b763f437 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -8303,6 +8303,33 @@ class Test(unittest.TestCase):
         self.assertEqual(beams[3], continue_beam)  # fourth should be continue
         self.assertEqual(beams[4], stop_beam)  # last should be stop
 
+    def test_makeBeams__paddingRight(self):
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('6/8')
+        m.paddingRight = 1.0
+        m.append(note.Note(type='quarter'))
+        m.append(note.Note(type='eighth'))
+        m.append(note.Note(type='eighth'))
+
+        m.makeBeams(inPlace=True)
+        beams = self.get_beams_from_stream(m)
+
+        no_beam = beam.Beams()
+        start_beam = beam.Beams()
+        start_beam.append('start')
+        stop_beam = beam.Beams()
+        stop_beam.append('stop')
+
+        self.assertEqual(beams, [no_beam, no_beam, no_beam])
+
+        m.paddingRight = 0.5
+        m.append(note.Note(type='eighth'))
+
+        m.makeBeams(inPlace=True)
+        beams = self.get_beams_from_stream(m)
+
+        self.assertEqual(beams, [no_beam, no_beam, start_beam, stop_beam])
+
     def testWrite(self):
         import os