Repository: cuthbertLab/music21
Pull request #789 (2021-01-24 06:51:22)

Fix beaming in pickup measures
First note in a measure does NOT always start partial beams.  Only if measureOffset is 0.0

coveralls: 
[![Coverage Status](https://coveralls.io/builds/36556020/badge)](https://coveralls.io/builds/36556020)

Coverage increased (+0.003%) to 91.305% when pulling **0a37c7d3ede1a982002834542bc21adbd1224e37 on pickup_beams** into **f0491ddacbf49ccce386d11267e03edadcc11c5e on master**.


Patch:
diff --git a/music21/meter.py b/music21/meter.py
index c09ef3c4d..8324ea58a 100644
--- a/music21/meter.py
+++ b/music21/meter.py
@@ -3651,11 +3651,13 @@ class TimeSignature(base.Music21Object):
 
         Pickup measure support included by taking in an additional measureStartOffset argument.
 
-        >>> threeFour = meter.TimeSignature('3/4')
-        >>> nList = [note.Note(type='eighth') for _ in range(3)]
-        >>> beamList = threeFour.getBeams(nList, measureStartOffset=1.5)
+        >>> twoTwo = meter.TimeSignature('2/2')
+        >>> nList = [note.Note(type='eighth') for _ in range(5)]
+        >>> beamList = twoTwo.getBeams(nList, measureStartOffset=1.5)
         >>> print(beamList)
-        [<music21.beam.Beams <music21.beam.Beam 1/start>>,
+        [None,
+         <music21.beam.Beams <music21.beam.Beam 1/start>>,
+         <music21.beam.Beams <music21.beam.Beam 1/continue>>,
          <music21.beam.Beams <music21.beam.Beam 1/continue>>,
          <music21.beam.Beams <music21.beam.Beam 1/stop>>]
         '''
@@ -3674,7 +3676,7 @@ class TimeSignature(base.Music21Object):
         if len(srcList) <= 1:
             return [None for _ in srcList]
 
-        beamsList = beam.Beams.naiveBeams(srcList)  # hold maximum Beams objects
+        beamsList = beam.Beams.naiveBeams(srcList)  # hold maximum Beams objects, all with type None
         beamsList = beam.Beams.removeSandwichedUnbeamables(beamsList)
 
         def fixBeamsOneElementDepth(i, el, depth):
@@ -3724,7 +3726,7 @@ class TimeSignature(base.Music21Object):
                 return
 
             # determine beamType
-            if isFirst:  # if the first, we always start
+            if isFirst and measureStartOffset == 0:  # if the first w/o pickup, we always start
                 beamType = 'start'
                 # get a partial beam if we cannot continue this
                 if (beamNext is None
@@ -3757,6 +3759,9 @@ class TimeSignature(base.Music21Object):
                     # the greater boundary of this span), and we did not have a
                     # beam or beam number in the previous beam
 
+                    # first note in pickup measures might also get 'partial-left'
+                    # here, but this gets fixed in sanitize partial beams
+
                     # may need to check: beamNext is not None and
                     #   beamNumber in beamNext.getNumbers()
                     # note: it is critical that we check archetypeSpan here
@@ -4487,11 +4492,11 @@ class Test(unittest.TestCase):
 
     def testGetBeams(self):
         from music21 import note
-        a = TimeSignature('6/8')
+        ts = TimeSignature('6/8')
         durList = [16, 16, 16, 16, 8, 16, 16, 16, 16, 8]
 
-        b = [note.Note(quarterLength=4 / d) for d in durList]
-        c = a.getBeams(b)
+        notesList = [note.Note(quarterLength=4 / d) for d in durList]
+        beams = ts.getBeams(notesList)
         match = '''[<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>,
         <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>,
         <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>,
@@ -4503,7 +4508,72 @@ class Test(unittest.TestCase):
         <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/stop>>,
         <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
 
-        self.assertTrue(common.whitespaceEqual(str(c), match))
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+    def test_getBeams_offset(self):
+        '''
+        Test getting Beams from a Measure that has an anacrusis that makes the
+        first note not beamed.
+        '''
+        from music21 import note
+        from music21 import stream
+        m = stream.Measure()
+        m.repeatAppend(note.Note(type='eighth'), 5)
+        ts = TimeSignature('2/2')
+
+        beams = ts.getBeams(m, measureStartOffset=1.5)
+        self.assertIsNone(beams[0])
+        for b in beams[1:]:
+            self.assertIsNotNone(b)
+        match = '''[None,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+        m.append(note.Note(type='eighth'))
+        beams = ts.getBeams(m, measureStartOffset=1.0)
+        match = '''[<music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match), str(beams))
+
+        m = stream.Measure()
+        m.repeatAppend(note.Note(type='eighth'), 5)
+        ts = TimeSignature('3/2')
+
+        beams = ts.getBeams(m, measureStartOffset=3.5)
+        match = '''[None,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+
+        m = stream.Measure()
+        m.repeatAppend(note.Note(type='eighth'), 4)
+        ts = TimeSignature('6/8')
+        beams = ts.getBeams(m, measureStartOffset=1.0)
+        match = '''[None,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+        m.append(note.Note(type='eighth'))
+        beams = ts.getBeams(m, measureStartOffset=0.5)
+        match = '''[<music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match), str(beams))
+
 
     def testOffsetToDepth(self):
         # get a maximally divided 4/4 to the level of 1/8
@@ -4936,6 +5006,6 @@ _DOC_ORDER = [TimeSignature]
 
 if __name__ == '__main__':
     import music21
-    music21.mainTest(Test)  # , runTest='testCompoundSameDenominator')
+    music21.mainTest(Test)  # , runTest='test_getBeams_offset')
 
 
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index c8d4309f1..e19f21d01 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -32,6 +32,7 @@ environLocal = environment.Environment(__file__)
 
 
 def makeBeams(s, *, inPlace=False):
+    # noinspection PyShadowingNames
     '''
     Return a new Measure, or Stream of Measures, with beams applied to all
     notes. Measures with Voices will process voices independently.
@@ -845,6 +846,7 @@ def makeTies(
     inPlace=False,
     displayTiedAccidentals=False
 ):
+    # noinspection PyShadowingNames
     '''
     Given a stream containing measures, examine each element in the
     Stream. If the elements duration extends beyond the measure's boundary,
@@ -1174,6 +1176,7 @@ def makeTies(
 
 
 def makeTupletBrackets(s, *, inPlace=False):
+    # noinspection PyShadowingNames
     '''
     Given a Stream of mixed durations, designates the first and last tuplet of any group
     of tuplets as the start or end of the tuplet, respectively.
@@ -1400,6 +1403,7 @@ def moveNotesToVoices(source, classFilterList=('GeneralNote',)):
 
 
 def getTiePitchSet(prior):
+    # noinspection PyShadowingNames
     '''
     helper method for makeAccidentals to get the tie pitch set (or None)
     from the prior


Test patch:
diff --git a/music21/test/testStream.py b/music21/test/testStream.py
index 2866a3278..bf857ce87 100644
--- a/music21/test/testStream.py
+++ b/music21/test/testStream.py
@@ -8120,8 +8120,6 @@ class Test(unittest.TestCase):
     def test_makeBeams__1_e__after_16th_note(self):
         """
         Test that a 16th+8th notes after a 16th notes have proper beams.
-
-        Note: proper beams repr: https://cl.ly/90ce7b
         """
         m = Measure()
         m.timeSignature = meter.TimeSignature('2/4')
@@ -8144,6 +8142,28 @@ class Test(unittest.TestCase):
         # Now test that they are equal
         self.assertEqual(fourth_note_beams, beams[3])
 
+    def test_makeBeams__paddingLeft_2_2(self):
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('2/2')
+        m.paddingLeft = 1.5  # 5-eighth-note pickup
+        m.repeatAppend(note.Note(type='eighth'), 5)
+
+        m.makeBeams(inPlace=True)
+        beams = self.get_beams_from_stream(m)
+
+        start_beam = beam.Beams()
+        start_beam.append('start')
+        continue_beam = beam.Beams()
+        continue_beam.append('continue')
+        stop_beam = beam.Beams()
+        stop_beam.append('stop')
+
+        self.assertEqual(beams[0], beam.Beams())  # first should have no beams
+        self.assertEqual(beams[1], start_beam)  # second should be start
+        self.assertEqual(beams[2], continue_beam)  # third should be continue
+        self.assertEqual(beams[3], continue_beam)  # fourth should be continue
+        self.assertEqual(beams[4], stop_beam)  # last should be stop
+
     def testOpusWrite(self):
         o = Opus()
         s1 = Score()
@@ -8164,5 +8184,5 @@ class Test(unittest.TestCase):
 
 
 if __name__ == '__main__':
-    music21.mainTest(Test, 'verbose')
+    music21.mainTest(Test, 'verbose')  # , runTest='test_makeBeams__paddingLeft_2_2')