Repository: cuthbertLab/music21
Pull request #469 (2019-11-05 21:03:02)

Fix beaming issues
Using music21 we've noticed various beaming bugs in the past. We've originally just kept the fixes internally but now as we move to python3 we take this opportunity to contribute the fixes back to the master repo.

As you can see, I've added "TestMakeBeams" class to demonstrate some proper beaming, as well as demonstrate some of the errors we encountered.
The unit tests that originally failed were: "test_1_e_a_after_16th_note" and "test_repeated_1_e_a". Not the best unit test names...I know...but we are working on drummers project here :)

The fix was simple and you can see our changes in the "getBeams" function.
For each failing unit test I've provided the wrong representation today and a proper representation as well that I was aiming for.

I'd be very happy to see these changes come into the product. Please let me know if there is anything I should change and I'd be happy to do so (e.g. remove the notes for example, once you approve them).


Patch:
diff --git a/music21/beam.py b/music21/beam.py
index 8b3b3d464..4d072d3f5 100644
--- a/music21/beam.py
+++ b/music21/beam.py
@@ -222,6 +222,9 @@ class Beams(prebase.ProtoM21Object, EqualSlottedObjectMixin):
     def __len__(self):
         return len(self.beamsList)
 
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.__repr__() == other.__repr__()
+
     def _reprInternal(self):
         msg = []
         for beam in self.beamsList:
diff --git a/music21/meter.py b/music21/meter.py
index 6120e9b8d..02040bc28 100644
--- a/music21/meter.py
+++ b/music21/meter.py
@@ -3687,7 +3687,6 @@ class TimeSignature(base.Music21Object):
         else:
             return []
 
-
         if len(srcList) <= 1:
             return [None for _ in srcList]
 
@@ -3718,7 +3717,6 @@ class TimeSignature(base.Music21Object):
             beamNext = beamsList[i + 1] if not isLast else None
             beamPrevious = beamsList[i - 1] if not isFirst else None
 
-
             # get an archetype of the MeterSequence for this level
             # level is depth, starting at zero
             archetype = self.beamSequence.getLevel(depth)
@@ -3786,25 +3784,19 @@ class TimeSignature(base.Music21Object):
                 else:
                     beamType = 'start'
 
-
-            # last beams was active, last beamNumber was active,
-            # and it was stopped or was a partial-left
             elif (beamPrevious is not None
                   and beamNumber in beamPrevious.getNumbers()
-                  and beamPrevious.getTypeByNumber(beamNumber) in ['stop', 'partial-left']
-                  and beamNext is not None):
-                beamType = 'start'
-
-
-            # last note had beams but stopped, next note cannot be beamed to
-            # was active, last beamNumber was active,
-            # and it was stopped or was a partial-left
-            elif (beamPrevious is not None
-                  and beamNumber in beamPrevious.getNumbers()
-                  and beamPrevious.getTypeByNumber(beamNumber) in ['stop', 'partial-left']
-                  and beamNext is None):
-                beamType = 'partial-left'  # will be deleted later in the script
-
+                  and beamPrevious.getTypeByNumber(beamNumber) in ['stop', 'partial-left']):
+                    # last beams was active, last beamNumber was active,
+                    # and it was stopped or was a partial-left
+                    if beamNext is not None:
+                        beamType = 'start' if beamNumber in beamNext.getNumbers() else 'partial-right'
+
+                    # last note had beams but stopped, next note cannot be beamed to
+                    # was active, last beamNumber was active,
+                    # and it was stopped or was a partial-left
+                    else:
+                        beamType = 'partial-left'  # will be deleted later in the script
 
             # if no beam is defined next (we know this already)
             # then must stop


Test patch:
diff --git a/music21/test/testStream.py b/music21/test/testStream.py
index 07c2a9c36..03e186306 100644
--- a/music21/test/testStream.py
+++ b/music21/test/testStream.py
@@ -14,6 +14,7 @@ import unittest
 import copy
 
 import music21
+from music21.note import GeneralNote
 
 from music21.stream import Stream
 from music21.stream import Voice
@@ -22,6 +23,7 @@ from music21.stream import Score
 from music21.stream import Part
 
 from music21 import bar
+from music21 import beam
 from music21 import chord
 from music21 import clef
 from music21 import common
@@ -8009,6 +8011,161 @@ class Test(unittest.TestCase):
     #     cLast = m10.notes[-1]
     #     self.assertEqual(cLast.expressions, [])
 
+    @staticmethod
+    def get_beams_from_stream(srcList):
+        """Helper function to return beam list for all notes and rests in the stream."""
+        return [n.beams for n in srcList if isinstance(n, GeneralNote)]
+
+    def test_makeBeams__all_quarters(self):
+        """Test that for a measure full of quarters, there are no beams"""
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('4/4')
+        m.repeatAppend(note.Note(quarterLength=1), 4)
+
+        m2 = m.makeBeams()
+        beams = self.get_beams_from_stream(m2)
+
+        self.assertEqual([beam.Beams(), beam.Beams(), beam.Beams(), beam.Beams()], beams)
+
+    def test_makeBeams__all_eighths(self):
+        """Test a full measure full of eighth is grouped by beams into couples"""
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('4/4')
+        m.repeatAppend(note.Note(quarterLength=0.5), 8)
+
+        m2 = m.makeBeams()
+        beams = self.get_beams_from_stream(m2)
+
+        # Prepare the should be beams
+        first_note_beams = beam.Beams()
+        first_note_beams.append('start')
+
+        second_note_beams = beam.Beams()
+        second_note_beams.append('stop')
+
+        # Now test that they are equal
+        self.assertEqual(first_note_beams, beams[0])
+        self.assertEqual(second_note_beams, beams[1])
+
+        self.assertEqual(first_note_beams, beams[2])
+        self.assertEqual(second_note_beams, beams[3])
+
+        self.assertEqual(first_note_beams, beams[4])
+        self.assertEqual(second_note_beams, beams[5])
+
+        self.assertEqual(first_note_beams, beams[6])
+        self.assertEqual(second_note_beams, beams[7])
+
+    def test_makeBeams__eighth_rests_and_eighth(self):
+        """Test a full measure of 8th rest followed by 8th note"""
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('4/4')
+        for i in range(4):
+            m.append(note.Rest(quarterLength=0.5))
+            m.append(note.Note(quarterLength=0.5))
+
+        m2 = m.makeBeams()
+        beams = self.get_beams_from_stream(m2)
+
+        self.assertEqual([beam.Beams(), ] * 8, beams)
+
+    def test_makeBeams__repeated_1_e_a(self):
+        """
+        Test that the pattern of "1 e a" repeated more than once has correct beams.
+
+        Note: proper beams repr: https://share.getcloudapp.com/12uE7eBA
+        """
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('2/4')
+        for i in range(2):
+            m.append(note.Note(quarterLength=0.25))
+            m.append(note.Note(quarterLength=0.50))
+            m.append(note.Note(quarterLength=0.25))
+
+        m2 = m.makeBeams()
+        beams = self.get_beams_from_stream(m2)
+
+        # Prepare the should be beams
+        first_note_beams = beam.Beams()
+        first_note_beams.append('start')
+        first_note_beams.append('partial', 'right')
+
+        second_note_beams = beam.Beams()
+        second_note_beams.append('continue')
+
+        third_note_beams = beam.Beams()
+        third_note_beams.append('stop')
+        third_note_beams.append('partial', 'left')
+
+        # Now test that they are equal
+        self.assertEqual(first_note_beams, beams[0])
+        self.assertEqual(second_note_beams, beams[1])
+        self.assertEqual(third_note_beams, beams[2])
+
+        self.assertEqual(first_note_beams, beams[3])
+        self.assertEqual(second_note_beams, beams[4])
+        self.assertEqual(third_note_beams, beams[5])
+
+    def test_makeBeams__1_e_n_a(self):
+        """Test that 4 16th notes have proper beams across them all."""
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('1/4')
+        m.repeatAppend(note.Note(quarterLength=0.25), 4)
+
+        m2 = m.makeBeams()
+        beams = self.get_beams_from_stream(m2)
+
+        # Prepare the should be beams
+        first_note_beams = beam.Beams()
+        first_note_beams.append('start')
+        first_note_beams.append('start')
+
+        second_note_beams = beam.Beams()
+        second_note_beams.append('continue')
+        second_note_beams.append('continue')
+
+        third_note_beams = beam.Beams()
+        third_note_beams.append('continue')
+        third_note_beams.append('continue')
+
+        fourth_note_beams = beam.Beams()
+        fourth_note_beams.append('stop')
+        fourth_note_beams.append('stop')
+
+        # Now test that they are equal
+        self.assertEqual(first_note_beams, beams[0])
+        self.assertEqual(second_note_beams, beams[1])
+        self.assertEqual(third_note_beams, beams[2])
+        self.assertEqual(fourth_note_beams, beams[3])
+
+    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')
+
+        m.append(note.Note(quarterLength=0.50))
+        m.append(note.Note(quarterLength=0.25))
+        m.append(note.Note(quarterLength=0.25))
+
+        m.append(note.Note(quarterLength=0.25))
+        m.append(note.Note(quarterLength=0.75))
+
+        m2 = m.makeBeams()
+        beams = self.get_beams_from_stream(m2)
+
+        # Prepare the should be beams
+        fourth_note_beams = beam.Beams()
+        fourth_note_beams.append('start')
+        fourth_note_beams.append('partial', 'right')
+
+        # Now test that they are equal
+        self.assertEqual(fourth_note_beams, beams[3])
+
+
 # -----------------------------------------------------------------------------
 
 if __name__ == '__main__':