Repository: cuthbertLab/music21
Pull request #956 (2021-04-11 15:01:36)

Prevent false positive tie-merging by `stripTies()` when `matchByPitch=True`
**Motivation**
`matchByPitch` defaults True in v7. So it's worth doing some improvements before the release. I noticed a few things that seemed off when retesting the example in #775, which has a set of commands to build this stream:

![continue](https://user-images.githubusercontent.com/38668450/114308680-b404c600-9ab2-11eb-8d17-06f7151ea933.png)

If we do nothing, in v7, `matchByPitch=True` will match all of these notes and replace with a dotted half. In v6, it performs no match.

**Problems**

1. `matchByPitch=True` doesn't reject comparisons between Chords and Notes. So the second onset is connected to the first onset, and then we have a valid match all the way across.  Update: now, matchByPitch=False won't merge these either even if the ties are fixed up. In other words, we will check for equal cardinality in both cases.
2. `matchByPitch=False` does not and should not connect these either, thanks to #586 which made it so that the final chord must have all "stop" ties, not mixed-ties. But I think there is a similar problem with "continue" tie types -- if I adjust the example to put a stop tie on the last F4, we find a valid connection all the way through and replace with a dotted half. I think we should be strict about "continue" also. Essentially, we're giving the user the choice to be strict about tie types (matchByPitch=False), or permissive about tie types, preferring to matchByPitch instead.


**Now**
- `matchByPitch=True` rejects comparisons between Chords and Notes
- `matchByPitch=False` (strict mode) -- a tie type of "continue" on a chord is not taken at face value to be a valid connection, since it might be a mixed-tie-status chord.

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

Coverage increased (+0.004%) to 92.156% when pulling **f26f47ed822b3005014694e5143dc28617033b7d on jacobtylerwalls:stripTies-continue** into **ebc47a1fbc9d65981bc52928593b4b564ae96cd5 on cuthbertLab:master**.

mscuthbert: Give me a minute to think through the last implication -- I'd really like never to have pitches removed during StripTies.  Seems dangerous.  Not sure if the fix should be here, or in another PR.
jacobtylerwalls: Paraphrasing to see if I follow. The idea would be when matching by tie-type (not by pitch), to collect the set of all pitches across the matched span and create them on the replacement rather than merely prolong the first note, whatever its pitches were?

Example cases: stripped version of m.1 would become what you see in m.2?
![Screen Shot 2021-04-19 at 3 11 01 PM](https://user-images.githubusercontent.com/38668450/115290398-7a226800-a121-11eb-8328-a3397d7027c0.png)

![Screen Shot 2021-04-19 at 3 08 37 PM](https://user-images.githubusercontent.com/38668450/115290097-3d567100-a121-11eb-8534-cc15cf53b2c1.png)

mscuthbert: > Paraphrasing to see if I follow. The idea would be when matching by tie-type (not by pitch), to collect the set of all pitches across the matched span and create them on the replacement rather than merely prolong the first note, whatever its pitches were?

I think that if any notes change we shouldn't merge the ties at all.  So in both cases, m1 remains the same after stripTies.  The point of it is to figure out where it's safe to merge two notes/chords together and both of these cases, information is lost.  (Well, not 100% sure about case 2, but definitely in case 1)
jacobtylerwalls: Ah, so this is starting to sound like removing the `matchByPitch` keyword entirely because we no longer want to allow `matchByPitch=False` (which was the default until #818 !)

> The point of it is to figure out where it's safe to merge two notes/chords together and both of these cases, information is lost.

I would possibly gently push back: there could be some use cases for a destructive stripTies operation where it's more important to guarantee that there are no ties than to guarantee that all the pitches were preserved. Maybe I don't care about the pitches at all, maybe it's a drum set part, but I DEFINITELY care that every `NotRest` I iterate over is an onset of a sound, not a continuation, so that my rhythm and beat counting turn out right.

`inPlace` defaults False and `matchByPitch` defaults True, both of which have to be flipped to get irrecoverable pitch destruction, so it's opt-in.

What I remember from working on stripTies before is that it's used in three big places:
 - MIDI output
 - chordify
 - feature extractors

With matchByPitch=True I think those three places are all set. The question being now if we need to take matchByPitch=False away from anybody else wanting a way to guarantee no ties. Well, it doesn't ABSOLUTELY guarantee no ties, that would be more like `force=True`, and we don't have that. We have:
 - `matchByPitch=True` : permissive about tie-types, strict about pitch
 - `matchByPitch=False`: permissive about pitch, strict about tie types

Maybe this needs an enum where the match choices are pitch, type, or force.

In any case, just wanted to clarify I'm not introducing more tie stripping here, this PR as it stands reduces tie stripping by getting stricter in two scenarios; the scenario you were focusing on was status quo :-)
jacobtylerwalls: > I want to enable the use case (drum, things, etc.) that you discuss (though I'd think that a gong-hit with a resonance tie that adds a cymbal crash on beat 2 does sound like two not one onset).

Right, and this use case is starting to sound like the complex-ties-to-voices, i.e. explode() scenario that is a someday-enhancement to get MIDI output to be correct (see #620 and #775).

And thanks, with that bit of history now I see how matchByPitch=False has outlived its usefulness. ☑️ 
jacobtylerwalls: >matchByPitch=False has outlived its usefulness. 

Sorry -- should have said in the case of chords. I see why it was there (and still works) for single notes.
jacobtylerwalls: Opted for a minimal change for now -- like `matchByPitch=True`, have `matchByPitch=False` ALSO check for equal cardinality of notes/chords.

How's this?
jacobtylerwalls: You were on to something that we should be stricter and not just merge things. The failing test in the feature extractors had to do with bridging voices, which is currently mostly undefined in `stripTies`-- we only isolate and processs voices if they are contained in the Part directly (should no longer happen after the MIDI changes in #856). As you know, we don't have a voice-bridging routine at present, so stripTies just takes a .flat of the Part.

8a4bc98 ensured that cardinality was checked everywhere, including a case we hadn't discussed -- matchByPitch=True (default) on the STOP of a tie-span. We took the STOP at face value and checked neither cardinality nor pitch when matchByPitch=True.

In the Schoenberg example, with matchByPitch=True we were merging the D# in left hand measure with the trichord in the righthand measure:

**Source**
![Screen Shot 2021-04-26 at 7 41 23 PM](https://user-images.githubusercontent.com/38668450/116164466-b593d800-a6c7-11eb-89b6-84a59e8d966b.png)


**Stripped with match by pitch -- before bug fixes in 8a4bc98 and 17cda1c**
![Screen Shot 2021-04-26 at 7 43 33 PM](https://user-images.githubusercontent.com/38668450/116164469-b7f63200-a6c7-11eb-919b-e59c4e94984d.png)


So based on that, I added a test for this and will now push a commit changing the feature extractor results which I expect are more correct now.

jacobtylerwalls: > we don't have a voice-bridging routine at present

That's not stated right--we do have `voicesToParts()`. Maybe that could be explored down the road for use here.
mscuthbert: OMG -- voices make this EVEN more complicated.  Will read more on Tuesday and hope to get back to you then on this approach.
jacobtylerwalls: Up to you, but this seems like a candidate for prioritizing for v7 since we've defaulted `matchByPitch=True`, which will generate more traffic into these existing bugs.

If you're short on time, I could cherry-pick the first bullet point under "Now:" to a new PR and at least merge that.

> Now:
> - matchByPitch=True rejects comparisons between Chords and Notes
> - matchByPitch=False (strict mode) -- a tie type of "continue" on a chord is not taken at face value to be a valid connection, since it might be a mixed-tie-status chord.

mscuthbert: > > we don't have a voice-bridging routine at present
> 
> That's not stated right--we do have `voicesToParts()`. Maybe that could be explored down the road for use here.

 Not exactly the same thing as here.
mscuthbert: You're right that we need to get this in before v7, but I'd like the default to be as cautious about removing notes as possible.  If that's where we are, we can merge now and then get other parts in later.  I'd rather have a few too many ties than have any notes removed that shouldn't be.
jacobtylerwalls: > I'd like the default to be as cautious about removing notes as possible. If that's where we are, we can merge now and then get other parts in later. 

Yes, both of the bugfixes on this branch involve making _fewer_ merges/removals. And I have nothing else in mind, so I think we can just merge. Updated the test comments to remark where the failures would occur on current version.
mscuthbert: LGTM -- feel free to merge if all tests pass

Patch:
diff --git a/music21/features/base.py b/music21/features/base.py
index f7e5badb8..8c1bcccd0 100644
--- a/music21/features/base.py
+++ b/music21/features/base.py
@@ -318,9 +318,8 @@ class StreamForms:
 
         Currently: runs stripTies.
         '''
-        # this causes lots of deepcopies, but an inPlace operation loses
-        # accuracy on feature extractors
-        streamObj = streamObj.stripTies()
+        # Let stripTies make a copy so that we don't leave side effects on the input stream
+        streamObj = streamObj.stripTies(inPlace=False)
         return streamObj
 
     def __getitem__(self, key):
diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py
index 77885edb5..03cdf59fc 100644
--- a/music21/features/jSymbolic.py
+++ b/music21/features/jSymbolic.py
@@ -1789,7 +1789,7 @@ class StrengthOfStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor):
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.StrengthOfStrongestRhythmicPulseFeature(sch)
     >>> fe.extract().vector[0]
-    0.853...
+    0.857...
     '''
     id = 'R4'
 
@@ -1816,7 +1816,7 @@ class StrengthOfSecondStrongestRhythmicPulseFeature(
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.StrengthOfSecondStrongestRhythmicPulseFeature(sch)
     >>> fe.extract().vector[0]
-    0.12...
+    0.119...
     '''
     id = 'R5'
 
@@ -1852,7 +1852,7 @@ class StrengthRatioOfTwoStrongestRhythmicPulsesFeature(
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.StrengthRatioOfTwoStrongestRhythmicPulsesFeature(sch)
     >>> fe.extract().vector[0]
-    7.0
+    7.2
 
     '''
     id = 'R6'
@@ -1888,7 +1888,7 @@ class CombinedStrengthOfTwoStrongestRhythmicPulsesFeature(
     ...     p.insert(0, tempo.MetronomeMark('Langsam', 70))
     >>> fe = features.jSymbolic.CombinedStrengthOfTwoStrongestRhythmicPulsesFeature(sch)
     >>> fe.extract().vector[0]
-    0.975...
+    0.976...
     '''
     id = 'R7'
 
diff --git a/music21/features/native.py b/music21/features/native.py
index a0fafa29d..08aff1283 100644
--- a/music21/features/native.py
+++ b/music21/features/native.py
@@ -73,13 +73,13 @@ class QualityFeature(featuresModule.FeatureExtractor):
 
     now we will try it with the last movement of Schoenberg's opus 19 which has
     no mode explicitly encoded in the musicxml but which our analysis routines
-    believe (having very little to go on) fits the profile of F major best.
+    believe (having very little to go on) fits the profile of e minor best.
 
     >>> schoenberg19mvmt6 = corpus.parse('schoenberg/opus19', 6)
     >>> fe2 = features.native.QualityFeature(schoenberg19mvmt6)
     >>> f2 = fe2.extract()
     >>> f2.vector
-    [0]
+    [1]
 
 
     OMIT_FROM_DOCS
@@ -638,7 +638,7 @@ class TriadSimultaneityPrevalence(featuresModule.FeatureExtractor):
     >>> s2 = corpus.parse('schoenberg/opus19', 2)
     >>> fe2 = features.native.TriadSimultaneityPrevalence(s2)
     >>> fe2.extract().vector
-    [0.022727...]
+    [0.021739...]
     '''
     id = 'CS9'
 
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 3fcbc13cf..6eefbfc53 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -6992,10 +6992,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(m.flat.notes)
         1
 
-        In cases where chord members are manipulated after initial tie creation,
-        some chord members might lack ties. Whereas `stripTies` ordinarily only acts
-        on chords if every member has a stop tie, this is not necessary if
-        all the pitches match:
+        In cases where notes are manipulated after initial tie creation,
+        some chord members might lack ties. This will not prevent merging the tied notes
+        if all the pitches match, and `matchByPitch=True` (default):
 
         >>> c1 = chord.Chord('C4 E4')
         >>> c1.tie = tie.Tie('start')
@@ -7016,14 +7015,82 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(strippedPitchMatching.flat.notes)
         1
 
-        This can be prevented with `matchByPitch=False`, in which case every chord member
-        must have a stop tie:
+        This can be prevented with `matchByPitch=False`, in which case every note,
+        including each chord member, must have stop and/or continue tie types,
+        which was not the case above:
 
-        >>> strippedConservative = m.stripTies(matchByPitch=False)
-        >>> len(strippedConservative.flat.notes)
+        >>> strippedMixedTieTypes = m.stripTies(matchByPitch=False)
+        >>> len(strippedMixedTieTypes.flat.notes)
         2
 
-        Changed in v.7 -- `matchByPitch` defaults to True.
+        >>> c2.notes[0].tie = tie.Tie('stop')
+        >>> c2.notes[1].tie = tie.Tie('stop')
+        >>> c2.notes[2].tie = tie.Tie('stop')
+        >>> strippedUniformTieTypes = m.stripTies(matchByPitch=False)
+        >>> len(strippedUniformTieTypes.flat.notes)
+        1
+
+        Notice the matching happens even after altering the pitches:
+
+        >>> c3 = c2.transpose(6)
+        >>> otherM = stream.Measure([c1, c3])
+        >>> strippedTransposed = otherM.stripTies(matchByPitch=False)
+        >>> len(strippedTransposed.flat.notes)
+        1
+
+        Changed in v.7 -- `matchByPitch` defaults True, and the following
+        behavior defined regarding chords with a tie type "continue":
+
+        >>> c1.notes[0].tie = tie.Tie('continue')
+        >>> c1.notes[1].tie = tie.Tie('start')
+        >>> c1.notes[2].tie = tie.Tie('start')
+
+        Continue is accepted here as an ersatz-start:
+
+        >>> stripped1 = m.stripTies(matchByPitch=True)
+        >>> len(stripped1.flat.notes)
+        1
+
+        But prepend an element so that it's considered as a tie continuation:
+
+        >>> c0 = chord.Chord('C4 E4 G4')
+        >>> c0.tie = tie.Tie('start')
+        >>> m2 = stream.Measure()
+        >>> m2.append([c0, c1, c2])
+
+        Now the mixed tie types on c1 will only be connected to c2
+        on the permissive option (`matchByPitch=True`):
+
+        >>> stripped2 = m2.stripTies(matchByPitch=True)
+        >>> stripped2.elements
+        (<music21.chord.Chord C4 E4 G4>,)
+
+        >>> stripped3 = m2.stripTies(matchByPitch=False)
+        >>> stripped3.elements
+        (<music21.chord.Chord C4 E4 G4>,
+         <music21.chord.Chord C4 E4 G4>,
+         <music21.chord.Chord C4 E4 G4>)
+
+        Now correct the tie types on c1 and try the strict option:
+
+        >>> c1.notes[0].tie = tie.Tie('continue')
+        >>> c1.notes[1].tie = tie.Tie('continue')
+        >>> c1.notes[2].tie = tie.Tie('continue')
+        >>> stripped4 = m2.stripTies(matchByPitch=False)
+        >>> stripped4.elements
+        (<music21.chord.Chord C4 E4 G4>,)
+
+        Now replace the first element with just a single C4 note.
+        The following chords will be merged with each other, but not with the single
+        note, even on `matchByPitch=False`.
+        (`matchByPitch=False` is permissive about pitch but strict about cardinality.)
+
+        >>> newC = note.Note('C4')
+        >>> newC.tie = tie.Tie('start')
+        >>> m2.replace(c0, newC)
+        >>> stripped5 = m2.stripTies(matchByPitch=False)
+        >>> stripped5.elements
+        (<music21.note.Note C>, <music21.chord.Chord C4 E4 G4>)
         '''
         # environLocal.printDebug(['calling stripTies'])
         if not inPlace:  # make a copy
@@ -7063,24 +7130,32 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         posConnected = []  # temporary storage for index of tied notes
         posDelete = []  # store deletions to be processed later
 
-        def updateEndMatch(nInner):
+        def updateEndMatch(nInner) -> bool:
             '''
             updateEndMatch based on nList, iLast, matchByPitch, etc.
             '''
-            # ties tell us when they are ended
-            # unless n is a chord, which only tells if SOME member has a tie
+            # 2 cases before matchByPitch=False returns early.
+
+            # Case 1: nInner is not a chord, and it has a stop tie
+            # can't trust chords, which only tell if SOME member has a tie
+            # matchByPitch does not matter here
             # https://github.com/cuthbertLab/music21/issues/502
             if (hasattr(nInner, 'tie')
                     and not isinstance(nInner, chord.Chord)
                     and nInner.tie is not None
                     and nInner.tie.type == 'stop'):
                 return True
-            # but capture case where all chord members have a stop tie
+            # Case 2: matchByPitch=False and all chord members have a stop tie
+            # and checking cardinality passes (don't match chords to single notes)
             elif (hasattr(nInner, 'tie')
+                    and not matchByPitch
                     and isinstance(nInner, chord.Chord)
                     and None not in [inner_p.tie for inner_p in nInner.notes]
-                    and {inner_p.tie.type for inner_p in nInner.notes} == {'stop'}):
+                    and {inner_p.tie.type for inner_p in nInner.notes} == {'stop'}
+                    and nLast is not None and len(nLast.pitches) == len(nInner.pitches)):
                 return True
+
+            # Now, matchByPitch
             # if we cannot find a stop tie, see if last note was connected
             # and this and the last note are the same pitch; this assumes
             # that connected and same pitch value is tied; this is not
@@ -7088,13 +7163,16 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             elif not matchByPitch:
                 return False
 
-            # else...
             # find out if the last index is in position connected
             # if the pitches are the same for each note
             if (nLast is not None
                     and iLast in posConnected
                     and hasattr(nLast, 'pitch')
                     and hasattr(nInner, 'pitch')
+                    # before doing pitch comparison, need to
+                    # make sure we're not comparing a Note to a Chord
+                    and 'Chord' not in nLast.classes
+                    and 'Chord' not in nInner.classes
                     and nLast.pitch == nInner.pitch):
                 return True
             # looking for two chords of equal size
@@ -7115,6 +7193,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
             return False
 
+        def allTiesAreContinue(nr: note.NotRest) -> bool:
+            if nr.tie is None:  # pragma: no cover
+                return False
+            if nr.tie.type != 'continue':
+                return False
+            # check every chord member, since tie type "continue" on a chord
+            # only indicates that SOME member is tie-continue.
+            if 'Chord' in nr.classes:
+                for innerN in nr.notes:
+                    if innerN.tie is None:
+                        return False
+                    if innerN.tie.type != 'continue':
+                        return False
+            return True
+
         for i in range(len(notes)):
             endMatch = None  # can be True, False, or None
             n = notes[i]
@@ -7140,12 +7233,41 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # a connection has been started or continued, so no endMatch
                 endMatch = False
 
+            # a continue may or may not imply a connection
             elif (hasattr(n, 'tie')
                     and n.tie is not None
                     and n.tie.type == 'continue'):
-                # a continue always implies a connection
-                posConnected.append(i)
-                endMatch = False
+                # is this actually a start?
+                if not posConnected:
+                    posConnected.append(i)
+                    endMatch = False
+                elif matchByPitch:
+                    # try to match pitch against nLast
+                    # updateEndMatch() checks for equal cardinality
+                    tempEndMatch = updateEndMatch(n)
+                    if tempEndMatch:
+                        posConnected.append(i)
+                        # ... and keep going.
+                        endMatch = False
+                    else:
+                        # clear list and populate with this element
+                        posConnected = [i]
+                        endMatch = False
+                elif allTiesAreContinue(n):
+                    # uniform-continue suffices if not matchByPitch
+                    # but still need to check cardinality
+                    if nLast and (len(nLast.pitches) != len(n.pitches)):
+                        # different sizes: clear list and populate with this element
+                        # since allTiesAreContinue, it is okay to treat as ersatz-start
+                        posConnected = [i]
+                    else:
+                        posConnected.append(i)
+                    # either way, this was not a stop
+                    endMatch = False
+                else:
+                    # only SOME ties on this chord are "continue": reject
+                    posConnected = []
+                    endMatch = False
 
             # establish end condition
             if endMatch is None:  # not yet set, not a start or continue


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index fa9e6130c..0f024daa7 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -1426,6 +1426,100 @@ class Test(unittest.TestCase):
         self.assertEqual(chordsOut[3].pitches, ch4.pitches)
         self.assertEqual(chordsOut[4].pitches, ch5.pitches)
 
+    def testStripTiesComplexTies(self):
+        '''
+        Make sure tie types of "stop" or "continue" are not taken at face value
+        for Chords if matchByPitch=False; they only represent that SOME
+        chord member has that tie type.
+        '''
+        from music21 import stream, tie
+
+        n0 = note.Note('C4')
+        n0.tie = tie.Tie('start')
+
+        n1 = note.Note('C4')
+        n2 = note.Note('F4')
+        n1.tie = tie.Tie('continue')
+
+        n3 = note.Note('C4')
+        n4 = note.Note('F4')
+        n3.tie = tie.Tie('stop')
+
+        c1 = chord.Chord([n1, n2])
+        c2 = chord.Chord([n3, n4])
+
+        s = stream.Stream()
+        s.append(n0)
+        s.append(c1)
+        s.append(c2)
+
+        stripped1 = s.stripTies(matchByPitch=True)
+        # pitches of c1 don't match pitch of n0,
+        # so disregard the "continue" and treat instead as a start
+        # and match against the pitches of c2
+        self.assertEqual(len(stripped1), 2)  # previously was 1
+
+        stripped2 = s.stripTies(matchByPitch=False)
+        # strict mode: this shouldn't do anything,
+        # because the middle element isn't uniform-continue
+        # and the last element isn't uniform-stop
+        self.assertEqual(len(stripped2), 3)
+
+        n2.tie = tie.Tie('continue')
+
+        stripped3 = s.stripTies(matchByPitch=True)
+        # number of pitches in c1 doesn't match n0
+        # but does match c2, so lack of uniform-tie stop is irrelevant
+        self.assertEqual(len(stripped3), 2)  # previously was 1
+        self.assertEqual(
+            str(stripped3.elements),
+            '(<music21.note.Note C>, <music21.chord.Chord C4 F4>)'
+        )
+
+        stripped4 = s.stripTies(matchByPitch=False)
+        # matchByPitch=False requires last element to be uniform-stop
+        self.assertEqual(len(stripped4), 3)
+
+        n4.tie = tie.Tie('stop')
+
+        stripped5 = s.stripTies(matchByPitch=False)
+        # notice the note STILL isn't merged to the chords -- because different # of notes
+        self.assertEqual(len(stripped5), 2)
+
+        self.assertEqual(
+            str(stripped5.elements),
+            '(<music21.note.Note C>, <music21.chord.Chord C4 F4>)'
+        )
+
+        # replace the first note with a Chord bearing a start tie, and everything can be merged
+        s.replace(n0, chord.Chord('C4 F4'))
+        s.first().tie = tie.Tie('start')
+
+        stripped6 = s.stripTies(matchByPitch=False)
+        self.assertEqual(
+            str(stripped6.elements),
+            '(<music21.chord.Chord C4 F4>,)'
+        )
+
+        # make sure matchByPitch=True is still picky about pitch but merges the rest,
+        # including a continue tie, which becomes ersatz-start
+        s.first().transpose(6, inPlace=True)
+        stripped7 = s.stripTies(matchByPitch=True)
+        self.assertEqual(
+            str(stripped7.elements),
+            '(<music21.chord.Chord F#4 B4>, <music21.chord.Chord C4 F4>)'
+        )  # previously was 1 element
+
+        # also transpose the "continue" chord
+        # to ensure the link from continue -> stop matches pitch
+        s[1].transpose(7, inPlace=True)
+        stripped8 = s.stripTies(matchByPitch=True)
+        self.assertEqual(
+            str(stripped8.elements),
+            '(<music21.chord.Chord F#4 B4>, '
+            + '<music21.chord.Chord G4 C5>, <music21.chord.Chord C4 F4>)'
+        )  # previously was 1 element
+
     def testTwoStreamMethods(self):
         from music21.note import Note