Repository: cuthbertLab/music21
Pull request #1556 (2023-04-30 14:08:31)

Fix stripTies when accidentals are neutral & none
When a stream contains two notes/chords that contain the same pitch, but one has a "neutral" accidental and the other one doesn't have an accidental at all (None), the tied notes weren't matched even though they should. 
This happens e.g., for quite a lot musicxml files from the ASAP dataset.

It's debatable whether this change should really take place in the pitch.Pitch class, where the `__eq__` method could be adapted to interpret a "neutral" accidental to be equal to a None accidental.  
This seemed like the less intrusive option, but I'm happy to change the PR as well. 
Let me know what you think!



Patch:
diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py
index 84c7a9a3f..af198ad09 100644
--- a/music21/features/jSymbolic.py
+++ b/music21/features/jSymbolic.py
@@ -1828,7 +1828,7 @@ class StrengthOfStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor):
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.StrengthOfStrongestRhythmicPulseFeature(sch)
     >>> fe.extract().vector[0]
-    0.857...
+    0.853...
     '''
     id = 'R4'
 
@@ -1855,7 +1855,7 @@ class StrengthOfSecondStrongestRhythmicPulseFeature(
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.StrengthOfSecondStrongestRhythmicPulseFeature(sch)
     >>> fe.extract().vector[0]
-    0.119...
+    0.121...
     '''
     id = 'R5'
 
@@ -1891,7 +1891,7 @@ class StrengthRatioOfTwoStrongestRhythmicPulsesFeature(
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.StrengthRatioOfTwoStrongestRhythmicPulsesFeature(sch)
     >>> fe.extract().vector[0]
-    7.2
+    7.0
 
     '''
     id = 'R6'
@@ -1927,7 +1927,7 @@ class CombinedStrengthOfTwoStrongestRhythmicPulsesFeature(
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.CombinedStrengthOfTwoStrongestRhythmicPulsesFeature(sch)
     >>> fe.extract().vector[0]
-    0.976...
+    0.975...
     '''
     id = 'R7'
 
diff --git a/music21/features/native.py b/music21/features/native.py
index 5b80a65a4..40fbec733 100644
--- a/music21/features/native.py
+++ b/music21/features/native.py
@@ -480,7 +480,7 @@ class MostCommonSetClassSimultaneityPrevalence(featuresModule.FeatureExtractor):
     >>> s2 = corpus.parse('schoenberg/opus19', 6)
     >>> fe2 = features.native.MostCommonSetClassSimultaneityPrevalence(s2)
     >>> fe2.extract().vector
-    [0.228...]
+    [0.235...]
     '''
     id = 'CS4'
 
@@ -663,7 +663,7 @@ class TriadSimultaneityPrevalence(featuresModule.FeatureExtractor):
     >>> s2 = corpus.parse('schoenberg/opus19', 2)
     >>> fe2 = features.native.TriadSimultaneityPrevalence(s2)
     >>> fe2.extract().vector
-    [0.021739...]
+    [0.02272727...]
     '''
     id = 'CS9'
 
diff --git a/music21/stream/base.py b/music21/stream/base.py
index a82af8db9..8ecaaa28f 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -7342,8 +7342,20 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                     return False
 
                 for pitchIndex in range(len(nLast.pitches)):
-                    if nLast.pitches[pitchIndex] != nInner.pitches[pitchIndex]:
+                    pLast = nLast.pitches[pitchIndex]
+                    pInner = nInner.pitches[pitchIndex]
+                    if (pLast.octave != pInner.octave
+                            or pLast.step != pInner.step
+                            or pLast.microtone != pInner.microtone):
                         return False
+                    # Ugly special case: Treat None and "natural" accidentals as equal
+                    aLast = pLast.accidental
+                    aInner = pInner.accidental
+                    if aLast != aInner:
+                        aLastIsNeutral = aLast is None or aLast.name == 'natural'
+                        aInnerIsNeutral = aInner is None or aInner.name == 'natural'
+                        if not (aLastIsNeutral and aInnerIsNeutral):
+                            return False
                 return True
 
             return False


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 6ef5ea114..a0c19f505 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -1469,6 +1469,19 @@ class Test(unittest.TestCase):
         self.assertEqual(chordsOut[3].pitches, ch4.pitches)
         self.assertEqual(chordsOut[4].pitches, ch5.pitches)
 
+    def testStripTiesChordsAccidentals(self):
+        '''
+        Make sure chords are matched even if some have 'natural' accidentals and some
+        have None accidentals.
+        '''
+        sch = corpus.parse('schoenberg/opus19', 2)
+        self.assertEqual(len(sch.stripTies().flatten().notes), 46)
+        measure = sch.stripTies().parts[0].getElementsByClass('Measure')[6]
+        self.assertEqual(len(measure.notes), 3)
+        self.assertEqual(measure.notes[0].offset, 0.5)
+        self.assertEqual(measure.notes[1].offset, 2.5)
+        self.assertEqual(measure.notes[2].offset, 3.0)
+
     def testStripTiesComplexTies(self):
         '''
         Make sure tie types of "stop" or "continue" are not taken at face value