Repository: cuthbertLab/music21
Pull request #770 (2021-01-12 16:04:45)

Extended duration symbols in ABC chords (marian-schultz)
Extension for the recognition of chords in ABCFormat according to http://abcnotation.com/wiki/abc:standard:v2.1#chords_and_unisons.

* For chords an outer length modifier is supported
Example:
[ceg]2 == [c2e2g2]
[c3e3g3]2 == [ceg]6 == [c6e6g6]

* The length of the chord is determined by its first note
Example:
[c2e4g8] == [c2e2g2]
* Empty chords are discarded
* Additional unittests for variations of abc Chords
* Fix: Missing chord symbols  for chords
* Additional unittests for chord symbols on note & chords


mscuthbert: coveralls had a problem -- closing and reopening to get a fresh test.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/36281620/badge)](https://coveralls.io/builds/36281620)

Coverage increased (+0.001%) to 91.316% when pulling **7551137260a19446d76b33d8680c394b44fa70d3 on marian-schulz:ABCChords** into **29b5db3b6de6e49244aca4b59a45323727ff4899 on cuthbertLab:master**.

mscuthbert: Thanks -- look at the lint errors, etc. -- but it seems to be on the right track!

marian-schulz: I have fixed the PEP8 violations and cleaned up my commit history.
The tests are all green now.

I apologize for not doing the code inspection before.
marian-schulz: The chord symbols before a chord were missing. I have also fixed this and added an additional unittest.
marian-schulz: Optimze the Testcases.
+ Use the ElementsByClass operator instead of an index access to .elements[x].
+ Add sharps, flats, natural and ocatve modificators to notes.

Broken rythm
+ Fixes broken rythm for Chords (i guess i broke that)
+ Add A Testcase to cover brocken rythm between chords and notes
mscuthbert: looks really good -- one little change -- capital letter variable names are only used for classes.  Will merge if everything passes.  Thank you for the great contributions!

Patch:
diff --git a/.gitignore b/.gitignore
index 5571c19ff..6cc846870 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@ __pycache__/
 .idea/other.xml
 .idea/vcs.xml
 .idea/workspace.xml
-
+.idea/markdown-*.xml
 
 # OSX
 .DS_Store
diff --git a/music21/abcFormat/__init__.py b/music21/abcFormat/__init__.py
index d63796501..70f183eb5 100644
--- a/music21/abcFormat/__init__.py
+++ b/music21/abcFormat/__init__.py
@@ -1641,21 +1641,40 @@ class ABCChord(ABCNote):
 
     A subclass of ABCNote.
     '''
-    def __init__(self, src):
+
+    def __init__(self, src: str = ''):
         super().__init__(src)
         # store a list of component objects
         self.subTokens = []
 
     def parse(self, forceKeySignature=None, forceDefaultQuarterLength=None):
+        '''
+        Handles the following types of chords:
+
+        * Chord without length modifier: [ceg]
+
+        * Chords with outer length modifier: [ceg]2, [ceg]/2
+
+        * Chords with inner length modifier: [c2e2g2], [c2eg]
+
+        * Chords with inner and outer length modifier: [c2e2g2]/2, [c/2e/2g/2]2
+        '''
+
         self.chordSymbols, nonChordSymStr = self._splitChordSymbols(self.src)
 
-        tokenStr = nonChordSymStr[1:-1]  # remove outer brackets
-        # environLocal.printDebug(['ABCChord:', nonChordSymStr, 'tokenStr', tokenStr])
+        # position of the closing bracket
+        pos = nonChordSymStr.index(']')
+        # Length modifier string behind the chord brackets
+        outerLengthModifierStr = nonChordSymStr[pos + 1:]
+        # String in the chord brackets
+        tokenStr = nonChordSymStr[1:pos]
 
-        self.quarterLength = self.getQuarterLength(
-            nonChordSymStr,
-            forceDefaultQuarterLength=forceDefaultQuarterLength
-        )
+        # environLocal.printDebug(['ABCChord:', nonChordSymStr, 'tokenStr', tokenStr, '
+        # outerLengthModifierStr', outerLengthModifierStr])
+
+        # Get the outer chord length modifier if present
+        outer_lengthModifier = self.getQuarterLength(outerLengthModifierStr,
+                                                     forceDefaultQuarterLength=1.0)
 
         if forceKeySignature is not None:
             activeKeySignature = forceKeySignature
@@ -1669,7 +1688,7 @@ class ABCChord(ABCNote):
         # may need to supply key?
         ah.tokenize(tokenStr)
 
-        chordDurationPost = None
+        inner_quarterLength = 0
         # tokens contained here are each ABCNote instances
         for t in ah.tokens:
             # environLocal.printDebug(['ABCChord: subTokens', t])
@@ -1678,14 +1697,22 @@ class ABCChord(ABCNote):
                 t.parse(
                     forceDefaultQuarterLength=self.activeDefaultQuarterLength,
                     forceKeySignature=activeKeySignature)
+
+                if t.isRest:
+                    continue
+
                 # get the quarter length from the sub-tokens
-                # note: assuming these are the same
-                chordDurationPost = t.quarterLength
-            if isinstance(t, ABCNote) and not t.isRest:
+                # All the notes within a chord should normally have the same length,
+                # but if not, the chord duration is that of the first note.
+                if not inner_quarterLength:
+                    inner_quarterLength = t.quarterLength
+
                 self.subTokens.append(t)
 
-        if chordDurationPost is not None:
-            self.quarterLength = chordDurationPost
+
+        # When both inside and outside the chord length modifiers are used,
+        # they should be multiplied. Example: [C2E2G2]3 has the same meaning as [CEG]6.
+        self.quarterLength = outer_lengthModifier * inner_quarterLength
 
 
 # ------------------------------------------------------------------------------
@@ -1701,7 +1728,7 @@ class ABCHandler:
     define new phrases.  This is useful for parsing extra information from
     the Essen Folksong repertory
 
-    New in v6.2 -- lineBreaksDefinePhrases -- does not yet do anything
+    New in v6.3 -- lineBreaksDefinePhrases -- does not yet do anything
     '''
     def __init__(self, abcVersion=None, lineBreaksDefinePhrases=False):
         # tokens are ABC objects import n a linear stream
@@ -2193,9 +2220,17 @@ class ABCHandler:
             # get chords
             if c == '[':
                 j = self.pos + 1
+
+                # find closing chord bracket
                 while j < self.srcLen - 1 and self.strSrc[j] != ']':
                     j += 1
+
                 j += 1  # need character that caused break
+
+                # find outer chord length modifier
+                while j < self.srcLen and (self.strSrc[j].isdigit() or self.strSrc[j] in '/'):
+                    j += 1
+
                 # prepend chord symbol
                 if activeChordSymbol != '':
                     self.currentCollectStr = activeChordSymbol + self.strSrc[self.pos:j]
diff --git a/music21/abcFormat/translate.py b/music21/abcFormat/translate.py
index 3fe49f6dd..5af2f8ceb 100644
--- a/music21/abcFormat/translate.py
+++ b/music21/abcFormat/translate.py
@@ -188,6 +188,8 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
                 measureNumber += 1
             p.coreAppend(dst)
 
+    p.coreElementsChanged()
+
     try:
         reBar(p, inPlace=True)
     except (ABCTranslateException, meter.MeterException, ZeroDivisionError):
@@ -198,7 +200,7 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
         if useMeasures:  # assume at start of measures
             p.getElementsByClass('Measure')[0].clef = clef.bestClef(p, recurse=True)
         else:
-            p.coreInsert(0, clef.bestClef(p, recurse=True))
+            p.insert(0, clef.bestClef(p, recurse=True))
 
     if postTransposition != 0:
         p.transpose(postTransposition, inPlace=True)
@@ -263,32 +265,6 @@ def parseTokens(mh, dst, p, useMeasures):
                 mmObj = t.getMetronomeMarkObject()
                 dst.coreAppend(mmObj)
 
-        # as ABCChord is subclass of ABCNote, handle first
-        elif isinstance(t, abcFormat.ABCChord):
-            # may have more than notes?
-            pitchNameList = []
-            accStatusList = []  # accidental display status list
-            for tSub in t.subTokens:
-                # notes are contained as subTokens are already parsed
-                if isinstance(tSub, abcFormat.ABCNote):
-                    pitchNameList.append(tSub.pitchName)
-                    accStatusList.append(tSub.accidentalDisplayStatus)
-            c = chord.Chord(pitchNameList)
-            c.duration.quarterLength = t.quarterLength
-            if t.activeTuplet:
-                thisTuplet = copy.deepcopy(t.activeTuplet)
-                if thisTuplet.durationNormal is None:
-                    thisTuplet.setDurationType(c.duration.type, c.duration.dots)
-                c.duration.appendTuplet(thisTuplet)
-            # adjust accidental display for each contained pitch
-            for pIndex in range(len(c.pitches)):
-                if c.pitches[pIndex].accidental is None:
-                    continue
-                c.pitches[pIndex].accidental.displayStatus = accStatusList[pIndex]
-            dst.coreAppend(c)
-
-            # ql += t.quarterLength
-
         elif isinstance(t, abcFormat.ABCNote):
             # add the attached chord symbol
             if t.chordSymbols:
@@ -305,46 +281,77 @@ def parseTokens(mh, dst, p, useMeasures):
                     dst.coreElementsChanged()
                 except ValueError:
                     pass  # Exclude malformed chord
-            if t.isRest:
-                n = note.Rest()
-            else:
-                n = note.Note(t.pitchName)
-                if n.pitch.accidental is not None:
-                    n.pitch.accidental.displayStatus = t.accidentalDisplayStatus
-
-            n.duration.quarterLength = t.quarterLength
-            if t.activeTuplet:
-                thisTuplet = copy.deepcopy(t.activeTuplet)
-                if thisTuplet.durationNormal is None:
-                    thisTuplet.setDurationType(n.duration.type, n.duration.dots)
-                n.duration.appendTuplet(thisTuplet)
-
-            # start or end a tie at note n
-            if t.tie is not None:
-                if t.tie in ('start', 'continue'):
-                    n.tie = tie.Tie(t.tie)
-                    n.tie.style = 'normal'
-                elif t.tie == 'stop':
-                    n.tie = tie.Tie(t.tie)
-            # Was: Extremely Slow for large Opus files... why?
-            # Answer: some pieces didn't close all their spanners, so
-            # everything was in a Slur/Diminuendo, etc.
-            for span in t.applicableSpanners:
-                span.addSpannedElements(n)
-
-            if t.inGrace:
-                n = n.getGrace()
-
-            n.articulations = []
-            while any(t.articulations):
-                tokenArticulationStr = t.articulations.pop()
-                if tokenArticulationStr not in _abcArticulationsToM21:
+
+            # as ABCChord is subclass of ABCNote, handle first
+            if isinstance(t, abcFormat.ABCChord):
+                # Skip an empty chord
+                if not t.subTokens:
                     continue
-                m21ArticulationClass = _abcArticulationsToM21[tokenArticulationStr]
-                m21ArticulationObj = m21ArticulationClass()
-                n.articulations.append(m21ArticulationObj)
 
-            dst.coreAppend(n, setActiveSite=False)
+                # may have more than notes?
+                pitchNameList = []
+                accStatusList = []  # accidental display status list
+                for tSub in t.subTokens:
+                    # notes are contained as subTokens are already parsed
+                    if isinstance(tSub, abcFormat.ABCNote):
+                        pitchNameList.append(tSub.pitchName)
+                        accStatusList.append(tSub.accidentalDisplayStatus)
+                c = chord.Chord(pitchNameList)
+                c.duration.quarterLength = t.quarterLength
+                if t.activeTuplet:
+                    thisTuplet = copy.deepcopy(t.activeTuplet)
+                    if thisTuplet.durationNormal is None:
+                        thisTuplet.setDurationType(c.duration.type, c.duration.dots)
+                    c.duration.appendTuplet(thisTuplet)
+                # adjust accidental display for each contained pitch
+                for pIndex in range(len(c.pitches)):
+                    if c.pitches[pIndex].accidental is None:
+                        continue
+                    c.pitches[pIndex].accidental.displayStatus = accStatusList[pIndex]
+                dst.coreAppend(c)
+
+                # ql += t.quarterLength
+            else:
+                if t.isRest:
+                    n = note.Rest()
+                else:
+                    n = note.Note(t.pitchName)
+                    if n.pitch.accidental is not None:
+                        n.pitch.accidental.displayStatus = t.accidentalDisplayStatus
+
+                n.duration.quarterLength = t.quarterLength
+                if t.activeTuplet:
+                    thisTuplet = copy.deepcopy(t.activeTuplet)
+                    if thisTuplet.durationNormal is None:
+                        thisTuplet.setDurationType(n.duration.type, n.duration.dots)
+                    n.duration.appendTuplet(thisTuplet)
+
+                # start or end a tie at note n
+                if t.tie is not None:
+                    if t.tie in ('start', 'continue'):
+                        n.tie = tie.Tie(t.tie)
+                        n.tie.style = 'normal'
+                    elif t.tie == 'stop':
+                        n.tie = tie.Tie(t.tie)
+                # Was: Extremely Slow for large Opus files... why?
+                # Answer: some pieces didn't close all their spanners, so
+                # everything was in a Slur/Diminuendo, etc.
+                for span in t.applicableSpanners:
+                    span.addSpannedElements(n)
+
+                if t.inGrace:
+                    n = n.getGrace()
+
+                n.articulations = []
+                while any(t.articulations):
+                    tokenArticulationStr = t.articulations.pop()
+                    if tokenArticulationStr not in _abcArticulationsToM21:
+                        continue
+                    m21ArticulationClass = _abcArticulationsToM21[tokenArticulationStr]
+                    m21ArticulationObj = m21ArticulationClass()
+                    n.articulations.append(m21ArticulationObj)
+
+                dst.coreAppend(n, setActiveSite=False)
 
         elif isinstance(t, abcFormat.ABCSlurStart):
             p.coreAppend(t.slurObj)
diff --git a/music21/freezeThaw.py b/music21/freezeThaw.py
index 7fc0e0c08..13a7f7f53 100644
--- a/music21/freezeThaw.py
+++ b/music21/freezeThaw.py
@@ -777,14 +777,12 @@ class StreamThawer(StreamFreezeThawBase):
                 # works like a whole new hierarchy...  # no need for deepcopy
                 subSF = StreamThawer()
                 subSF.teardownSerializationScaffold(e._stream)
-                e._stream.coreElementsChanged()
                 e._cache = {}
                 # for el in e._stream.flat:
                 #    print(el, el.offset, el.sites.siteDict)
             elif 'Spanner' in eClasses:
                 subSF = StreamThawer()
                 subSF.teardownSerializationScaffold(e.spannerStorage)
-                e.spannerStorage.coreElementsChanged()
                 e._cache = {}
             elif e.isStream:
                 self.restoreStreamStatusClient(e)
@@ -795,7 +793,6 @@ class StreamThawer(StreamFreezeThawBase):
 
         # restore to whatever it was
         streamObj.autoSort = storedAutoSort
-        streamObj.coreElementsChanged()
         _fixId(streamObj)
 
     def restoreElementsFromTuples(self, streamObj):
diff --git a/music21/harmony.py b/music21/harmony.py
index 48658351b..f5e285a34 100644
--- a/music21/harmony.py
+++ b/music21/harmony.py
@@ -2028,7 +2028,7 @@ class ChordSymbol(Harmony):
         if inversionNum not in (0, None):
             for p in pitches[0:inversionNum]:
                 p.octave = p.octave + 1
-                # Repeat if 9th/11th/13th chord in 3rd inversion or greater
+                # Repeat if 9th/11th/13th chord in 4th inversion or greater
                 if inversionNum > 3:
                     p.octave = p.octave + 1
 
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index 560ba25b2..bd3c1b803 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -57,10 +57,6 @@ environLocal = environment.Environment(_MOD)
 
 # ------------------------------------------------------------------------------
 
-class NoteheadException(MusicXMLExportException):
-    pass
-
-
 def typeToMusicXMLType(value):
     '''Convert a music21 type to a MusicXML type.
 
@@ -2354,7 +2350,12 @@ class PartExporter(XMLExporterBase):
             self.addDividerComment('Measure ' + str(m.number))
             measureExporter = MeasureExporter(m, parent=self)
             measureExporter.spannerBundle = self.spannerBundle
-            mxMeasure = measureExporter.parse()
+            try:
+                mxMeasure = measureExporter.parse()
+            except MusicXMLExportException as e:
+                e.measureNumber = str(m.number)
+                e.partName = self.stream.partName
+                raise e
             self.xmlRoot.append(mxMeasure)
 
         return self.xmlRoot
@@ -4576,13 +4577,13 @@ class MeasureExporter(XMLExporterBase):
         >>> nc.write()
         Traceback (most recent call last):
         music21.musicxml.xmlObjects.MusicXMLExportException:
-            NoChord object's chordKindStr must be non-empty
+             In part (None), measure (1): NoChord object's chordKindStr must be non-empty
 
         >>> nc.chordKind = None
         >>> nc.write()
         Traceback (most recent call last):
         music21.musicxml.xmlObjects.MusicXMLExportException:
-            NoChord object's chordKind must be 'none'
+             In part (None), measure (1): NoChord object's chordKind must be 'none'
 
         '''
         if cs.writeAsChord is True:
@@ -6165,6 +6166,19 @@ class Test(unittest.TestCase):
         helpers.indent(mxScore)
         return mxScore
 
+    def testExceptionMessage(self):
+        s = stream.Score()
+        p = stream.Part()
+        p.partName = 'Offstage Trumpet'
+        p.insert(note.Note(quarterLength=(4 / 2048)))
+        s.insert(p)
+
+        msg = 'In part (Offstage Trumpet), measure (1): '
+        msg += 'Cannot convert "2048th" duration to MusicXML (too short).'
+        with self.assertRaises(MusicXMLExportException) as error:
+            s.write()
+        self.assertEqual(str(error.exception), msg)
+
     def testSpannersWrite(self):
         from music21 import converter
         p = converter.parse("tinynotation: 4/4 c4 d e f g a b c' b a g2")
@@ -6468,4 +6482,4 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
 
 if __name__ == '__main__':
     import music21
-    music21.mainTest(Test)  # , runTest='testSpannersWrite')
+    music21.mainTest(Test)  # , runTest='testExceptionMessage')
diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py
index fda43d06a..94dae6748 100644
--- a/music21/musicxml/partStaffExporter.py
+++ b/music21/musicxml/partStaffExporter.py
@@ -54,7 +54,7 @@ def addStaffTags(measure: Element, staffNumber: int, tagList: Optional[List[str]
     >>> addStaffTags(elem, 2, tagList=['note', 'forward', 'direction'])
     Traceback (most recent call last):
     music21.musicxml.xmlObjects.MusicXMLExportException:
-        Attempted to create a second <staff> tag for an element in m. 1
+        In part (), measure (1): Attempted to create a second <staff> tag
 
     The function doesn't accept elements other than <measure>:
 
@@ -68,9 +68,9 @@ def addStaffTags(measure: Element, staffNumber: int, tagList: Optional[List[str]
     for tagName in tagList:
         for tag in measure.findall(tagName):
             if tag.find('staff') is not None:
-                mNum = measure.get('number')
-                raise MusicXMLExportException('Attempted to create a second <staff> tag '
-                                              f'for an element in m. {mNum}')
+                e = MusicXMLExportException('Attempted to create a second <staff> tag')
+                e.measureNumber = measure.get('number')
+                raise e
             mxStaff = Element('staff')
             mxStaff.text = str(staffNumber)
             helpers.insertBeforeElements(tag, mxStaff,
@@ -272,6 +272,13 @@ class PartStaffExporterMixin:
             <staff>2</staff>
           </note>
         </measure>
+
+        Fails if attempted a second time:
+
+        >>> root = SX.parse()
+        Traceback (most recent call last):
+        music21.musicxml.xmlObjects.MusicXMLExportException:
+            In part (MusicXML Part), measure (1): Attempted to create a second <staff> tag
         '''
         initialPartStaffRoot: Optional[Element] = None
         for i, ps in enumerate(group):
@@ -280,11 +287,16 @@ class PartStaffExporterMixin:
 
             # Create <staff> tags under <note>, <direction>, <forward>, <harmony> tags
             for mxMeasure in thisPartStaffRoot.findall('measure'):
-                addStaffTags(
-                    mxMeasure,
-                    staffNumber,
-                    tagList=['note', 'direction', 'forward', 'harmony']
-                )
+                try:
+                    addStaffTags(
+                        mxMeasure,
+                        staffNumber,
+                        tagList=['note', 'direction', 'forward', 'harmony']
+                    )
+                except MusicXMLExportException as e:
+                    e.partName = ps.partName
+                    e.measureNumber = mxMeasure.get('number')
+                    raise e
 
             if initialPartStaffRoot is None:
                 initialPartStaffRoot = thisPartStaffRoot
@@ -330,7 +342,7 @@ class PartStaffExporterMixin:
         sourceMeasure = None  # Set back to None when disposed of
         insertions = {}
 
-        # Walk through <measures> of the target <part>, compare to measure numbers in `ps`
+        # Walk through <measures> of the target <part>, compare measure numbers
         for i, targetMeasure in enumerate(target):
             if targetMeasure.tag != 'measure':
                 continue
@@ -453,8 +465,9 @@ class PartStaffExporterMixin:
                 if oldClef is not None and mxAttributes is not None:
                     clefsInMxAttributesAlready = mxAttributes.findall('clef')
                     if len(clefsInMxAttributesAlready) >= staffNumber:
-                        raise MusicXMLExportException(
-                            'Attempted to add more clefs than staffs')  # pragma: no cover
+                        e = MusicXMLExportException('Attempted to add more clefs than staffs')
+                        e.partName = ps.partName
+                        raise e
 
                     # Set initial clef for this staff
                     newClef = Element('clef')
diff --git a/music21/musicxml/xmlObjects.py b/music21/musicxml/xmlObjects.py
index 4122fa427..0d01c95e9 100644
--- a/music21/musicxml/xmlObjects.py
+++ b/music21/musicxml/xmlObjects.py
@@ -100,7 +100,26 @@ ORNAMENT_MARKS = {'trill-mark': expressions.Trill,
                   # TODO: 'accidental-mark' -- something else...
                   }
 
-class MusicXMLExportException(exceptions21.Music21Exception):
+# ------------------------------------------------------------------------------
+
+class MusicXMLException(exceptions21.Music21Exception):
+    def __init__(self, message):
+        super().__init__(message)
+        self.measureNumber: str = ''
+        self.partName: str = ''
+
+    def __str__(self):
+        msg = super().__str__()
+        if self.measureNumber or self.partName:
+            msg = f'In part ({self.partName}), measure ({self.measureNumber}): ' + msg
+        return msg
+
+
+class MusicXMLExportException(MusicXMLException):
+    pass
+
+
+class MusicXMLImportException(MusicXMLException):
     pass
 
 
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 2e2a9036b..68b27526b 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -15,7 +15,7 @@ import io
 import math
 # import pprint
 import re
-import sys
+# import sys
 # import traceback
 import unittest
 from typing import List, Optional, Dict, Tuple
@@ -25,6 +25,7 @@ import xml.etree.ElementTree as ET
 from music21 import common
 from music21 import exceptions21
 from music21.musicxml import xmlObjects
+from music21.musicxml.xmlObjects import MusicXMLImportException
 
 # modules that import this include converter.py.
 # thus, cannot import these here
@@ -69,15 +70,6 @@ StaffReferenceType = Dict[int, List[base.Music21Object]]
 NO_STAFF_ASSIGNED = 0
 
 
-# ------------------------------------------------------------------------------
-class MusicXMLImportException(exceptions21.Music21Exception):
-    pass
-
-
-class XMLBarException(MusicXMLImportException):
-    pass
-
-
 # ------------------------------------------------------------------------------
 # Helpers...
 def _clean(badStr: Optional[str]) -> Optional[str]:
@@ -151,7 +143,8 @@ def musicXMLTypeToType(value):
     'quarter'
     >>> musicxml.xmlToM21.musicXMLTypeToType(None)
     Traceback (most recent call last):
-    music21.musicxml.xmlToM21.MusicXMLImportException: found unknown MusicXML type: None
+    music21.musicxml.xmlObjects.MusicXMLImportException:
+        found unknown MusicXML type: None
     '''
     # MusicXML uses long instead of longa
     if value not in duration.typeToDuration:
@@ -1243,7 +1236,6 @@ class MusicXMLImporter(XMLParserBase):
             staffGroup.completeStatus = True
             self.spannerBundle.append(staffGroup)
             # self.stream.coreInsert(0, staffGroup)
-        self.stream.coreElementsChanged()
 
     def xmlMetadata(self, el=None, inputM21=None):
         '''
@@ -1766,40 +1758,6 @@ class PartParser(XMLParserBase):
         post.sort()
         return post
 
-    @staticmethod
-    def measureParsingError(mxMeasure, e):  # pragma: no cover
-        '''
-        Raises an exception with more detailed information about the measure number
-        that raised the exception::
-
-            from xml.etree.ElementTree import fromstring as EL
-            measure = EL('<measure number="4"/>')
-            try:
-                raise AttributeError("Cannot find attribute 'asdf'")
-            except AttributeError as error:
-                PP = musicxml.xmlToM21.PartParser
-                PP.measureParsingError(measure, error)
-
-        Returns::
-
-            In measure (4): Cannot find attribute 'asdf'
-
-        '''
-        measureNumber = 'unknown'
-        try:
-            measureNumber = mxMeasure.get('number')
-        except (AttributeError, NameError, ValueError):
-            pass
-        # http://stackoverflow.com/questions/6062576/adding-information-to-a-python-exception
-        execInfoTuple = sys.exc_info()
-        if hasattr(e, 'message'):
-            eMessage = e.message
-        else:
-            eMessage = execInfoTuple[0].__name__ + ' : '  # + execInfoTuple[1].__name__
-        unused_message = 'In measure (' + str(measureNumber) + '): ' + str(eMessage)
-        raise e
-        # raise type(e)(pprint.pformat(traceback.extract_tb(execInfoTuple[2])))
-
     def xmlMeasureToMeasure(self, mxMeasure):
         # noinspection PyShadowingNames
         '''
@@ -1835,8 +1793,10 @@ class PartParser(XMLParserBase):
         measureParser = MeasureParser(mxMeasure, parent=self)
         try:
             measureParser.parse()
-        except Exception as e:  # pylint: disable=broad-except
-            self.measureParsingError(mxMeasure, e)
+        except MusicXMLImportException as e:
+            e.measureNumber = measureParser.measureNumber
+            e.partName = self.stream.partName
+            raise e
         self.lastMeasureParser = measureParser
 
         if measureParser.staves > self.maxStaves:
@@ -2541,12 +2501,8 @@ class MeasureParser(XMLParserBase):
                 self.mxLyricList.append(mxLyric)
         elif isChord is False and isRest is False:  # normal note...
             self.restAndNoteCount['note'] += 1
-            try:
-                n = self.xmlToSimpleNote(mxNote)
-            except MusicXMLImportException as strerror:
-                raise MusicXMLImportException(
-                    f'cannot translate note in measure {self.measureNumber}: {strerror}')
-        else:  # its a rest
+            n = self.xmlToSimpleNote(mxNote)
+        else:  # it's a rest
             self.restAndNoteCount['rest'] += 1
             n = self.xmlToRest(mxNote)
 
@@ -2778,8 +2734,8 @@ class MeasureParser(XMLParserBase):
         >>> mxBeam = EL('<beam>crazy</beam>')
         >>> a = MP.xmlToBeam(mxBeam)
         Traceback (most recent call last):
-        music21.musicxml.xmlToM21.MusicXMLImportException:
-              unexpected beam type encountered (crazy)
+        music21.musicxml.xmlObjects.MusicXMLImportException:
+             unexpected beam type encountered (crazy)
         '''
         if inputM21 is None:
             beamOut = beam.Beam()
@@ -3691,9 +3647,7 @@ class MeasureParser(XMLParserBase):
                 try:
                     sp = spb[0]
                 except IndexError:
-                    raise MusicXMLImportException('Error in getting DynamicWedges...'
-                                                  + 'Measure no. ' + str(self.measureNumber)
-                                                  + ' ' + str(self.parent.partId))
+                    raise MusicXMLImportException('Error in getting DynamicWedges')
                 sp.completeStatus = True
                 # will only have a target if this follows the note
                 if targetLast is not None:
@@ -3746,7 +3700,7 @@ class MeasureParser(XMLParserBase):
                 if targetLast is not None:
                     sp.addSpannedElements(targetLast)
             else:
-                raise MusicXMLImportException('unidentified mxType of mxBracket:', mxType)
+                raise MusicXMLImportException(f'unidentified mxType of mxBracket: {mxType}')
         return returnList
 
     def xmlNotationsToSpanners(self, mxNotations, n):
@@ -4617,7 +4571,7 @@ class MeasureParser(XMLParserBase):
             # only for the purpose of getting _updatePitches() to run
             # then, we will need to get rid of this untruthful override
             # since there may be an inversion!
-            clearBassOverride: bool = True
+            clearBassOverride = True
 
         mxDegrees = mxHarmony.findall('degree')
 
@@ -5622,14 +5576,6 @@ class MeasureParser(XMLParserBase):
         >>> MP.useVoices
         False
 
-        `.updateVoiceInformation` runs `.coreInsert` so we need to call coreElementsChanged:
-
-        >>> MP.stream.coreElementsChanged()
-        >>> len(MP.stream)
-        0
-
-        Now a better test:
-
         >>> MP = musicxml.xmlToM21.MeasureParser()
         >>> MP.mxMeasure = ET.fromstring('<measure><note><voice>1</voice></note>'
         ...                                     + '<note><voice>2</voice></note></measure>')
@@ -5638,7 +5584,6 @@ class MeasureParser(XMLParserBase):
         ['1', '2']
         >>> MP.useVoices
         True
-        >>> MP.stream.coreElementsChanged()
         >>> len(MP.stream)
         2
         >>> len(MP.stream.getElementsByClass('Voice'))
@@ -5661,6 +5606,7 @@ class MeasureParser(XMLParserBase):
                 self.voicesById[v.id] = v
             self.useVoices = True
 
+            self.stream.coreElementsChanged()
 
 # -----------------------------------------------------------------------------
 
@@ -5691,6 +5637,18 @@ class Test(unittest.TestCase):
         out += ']'
         return out
 
+    def testExceptionMessage(self):
+        mxScorePart = self.EL('<score-part><part-name>Elec.</part-name></score-part>')
+        mxPart = self.EL('<part><measure><note><type>thirty-tooth</type></note></measure></part>')
+
+        PP = PartParser(mxPart=mxPart, mxScorePart=mxScorePart)
+        PP.partId = '1'
+
+        msg = 'In part (Elec.), measure (0): found unknown MusicXML type: thirty-tooth'
+        with self.assertRaises(MusicXMLImportException) as error:
+            PP.parse()
+        self.assertEqual(str(error.exception), msg)
+
     def testBarRepeatConversion(self):
         from music21 import corpus
         # a = converter.parse(testPrimitive.simpleRepeat45a)
@@ -6817,4 +6775,4 @@ class Test(unittest.TestCase):
 
 if __name__ == '__main__':
     import music21
-    music21.mainTest(Test)  # , runTest='testLineHeight')
+    music21.mainTest(Test)  # , runTest='testExceptionMessage')
diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py
index 27e01e74b..135f42232 100644
--- a/music21/stream/__init__.py
+++ b/music21/stream/__init__.py
@@ -533,7 +533,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
     @elements.setter
     def elements(self, value: Union['Stream', Sequence[base.Music21Object]]):
         '''
-        Sets this streams elements to the elements in another stream (just give
+        Sets this stream's elements to the elements in another stream (just give
         the stream, not the stream's .elements), or to a list of elements.
 
         Safe:
@@ -558,12 +558,12 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             self._offsetDict = {}
             self._elements = list(value._elements)  # copy list.
             for e in self._elements:
-                self.setElementOffset(e, value.elementOffset(e), addElement=True)
+                self.coreSetElementOffset(e, value.elementOffset(e), addElement=True)
                 e.sites.add(self)
                 self.coreSelfActiveSite(e)
             self._endElements = list(value._endElements)
             for e in self._endElements:
-                self.setElementOffset(e,
+                self.coreSetElementOffset(e,
                                       value.elementOffset(e, stringReturns=True),
                                       addElement=True)
                 e.sites.add(self)
@@ -574,12 +574,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             self._endElements = []
             self._offsetDict = {}
             for e in self._elements:
-                self.setElementOffset(e, e.offset, addElement=True)
+                self.coreSetElementOffset(e, e.offset, addElement=True)
                 e.sites.add(self)
                 self.coreSelfActiveSite(e)
         self.coreElementsChanged()
 
-
     def __setitem__(self, k, value):
         '''
         Insert an item at a currently filled index position,
@@ -608,12 +607,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         # assign in new position
         self._elements[k] = value
-        self.setElementOffset(value, value.offset, addElement=True)
+        self.coreSetElementOffset(value, value.offset, addElement=True)
         self.coreSelfActiveSite(value)
         # must get native offset
 
         value.sites.add(self)
-
         if isinstance(value, Stream):
             # know that this is now not flat
             self.coreElementsChanged(updateIsFlat=False)  # set manually
@@ -793,9 +791,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             # there is no new clef - suppresses the clef of a stream
             return
         self.insert(0.0, clefObj)
-        # for some reason needed to make sure that sorting of Clef happens before TimeSignature
-        # TODO: Test if this can be deleted...
-        self.coreElementsChanged()
 
     @property
     def timeSignature(self):
@@ -1430,7 +1425,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                                                            mustBeginInSpan=True):
 
                         elementOffset = self.elementOffset(e)
-                        self.setElementOffset(e, elementOffset - shiftDur)
+                        self.coreSetElementOffset(e, elementOffset - shiftDur)
             # if renumberMeasures is True and matchedEndElement is False:
             #     pass  # This should maybe just call a function renumberMeasures
         self.coreElementsChanged(clearIsSorted=False)
@@ -1611,6 +1606,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # noinspection PyArgumentList
                 new.coreStoreAtEnd(copy.deepcopy(e, memo))
 
+        new.coreElementsChanged()
+
         return new
 
     def __deepcopy__(self, memo=None):
@@ -1664,12 +1661,14 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         element: base.Music21Object,
         offset: Union[int, float, Fraction, str],
         *,
-        addElement=False,
-        setActiveSite=True
+        addElement=False,  # deprecated
+        setActiveSite=True  # deprecated
     ):
         '''
         Sets the Offset for an element, very quickly.
 
+        TODO: in v.7, remove keyword args `addElement` and `setActiveSite`
+
         >>> s = stream.Stream()
         >>> s.id = 'Stream1'
         >>> n = note.Note('B-4')
@@ -1700,22 +1699,15 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> s.setElementOffset(n2, 30.0, addElement=True)
 
         Changed in v5.5 -- also sets .activeSite for the element unless setActiveSite is False
+        In v6.7 -- also runs coreElementsChanged()
         '''
-        # Note: not documenting 'highestTime' is on purpose, since can only be done for
-        # elements already stored at end.  Infinite loop.
-        try:
-            offset = opFrac(offset)
-        except TypeError:
-            if offset not in core.OFFSET_STRING_VALUES:  # pragma: no cover
-                raise StreamException(f'Cannot set offset to {offset!r} for {element}')
+        self.coreSetElementOffset(element,
+                                  offset,
+                                  addElement=addElement,
+                                  setActiveSite=setActiveSite)
 
-        idEl = id(element)
-        if not addElement and idEl not in self._offsetDict:
-            raise StreamException(
-                f'Cannot set the offset for element {element}, not in Stream {self}.')
-        self._offsetDict[idEl] = (offset, element)  # fast
-        if setActiveSite:
-            self.coreSelfActiveSite(element)
+        # might change sorting, but not flatness.  Maybe other things can be False too.
+        self.coreElementsChanged(updateIsFlat=False)
 
     def elementOffset(self, element, stringReturns=False):
         '''
@@ -1904,7 +1896,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                                   + 'create a music21.ElementWrapper for the item')
         element = item
 
-        # checks of element is self; possibly performs additional checks
+        # checks if element is self; possibly performs additional checks
         self.coreGuardBeforeAddElement(element)
         # main insert procedure here
 
@@ -2214,7 +2206,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             self.coreGuardBeforeAddElement(e)
             # add this Stream as a location for the new elements, with the
             # the offset set to the current highestTime
-            self.setElementOffset(e, highestTime, addElement=True)
+            self.coreSetElementOffset(e, highestTime, addElement=True)
             e.sites.add(self)
             # need to explicitly set the activeSite of the element
             self.coreSelfActiveSite(e)
@@ -2488,12 +2480,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     #                         'o+shiftPos', o+shiftPos])
 
                     # need original offset, shiftDur, plus the distance from the start
-                    self.setElementOffset(e, o + shiftPos)
+                    self.coreSetElementOffset(e, o + shiftPos)
         # after shifting all the necessary elements, append new ones
         # these will not be in order
-        self.insert(offsetOrItemOrList, itemOrNone)
-        # call this is elements are now out of order
         self.coreElementsChanged()
+        self.insert(offsetOrItemOrList, itemOrNone)
 
     # --------------------------------------------------------------------------
     # searching and replacing routines
@@ -2637,14 +2628,14 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             target = self._elements[i]  # target may have been obj id; re-classing
             self._elements[i] = replacement
             # place the replacement at the old objects offset for this site
-            self.setElementOffset(replacement, self.elementOffset(target), addElement=True)
+            self.coreSetElementOffset(replacement, self.elementOffset(target), addElement=True)
             replacement.sites.add(self)
         else:
             # target may have been obj id; reassign
             target = self._endElements[i - eLen]
             self._endElements[i - eLen] = replacement
 
-            self.setElementOffset(replacement, 'highestTime', addElement=True)
+            self.coreSetElementOffset(replacement, 'highestTime', addElement=True)
             replacement.sites.add(self)
 
         target.sites.remove(self)
@@ -3738,7 +3729,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         1
 
 
-
         To get all measures from the beginning, go ahead and always request measure 0 to x,
         there will be no error if there is not a pickup measure.
 
@@ -5067,7 +5057,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             if classFilterList is not None and not e.isClassOrSubclass(classFilterList):
                 continue
 
-            self.setElementOffset(e, opFrac(self.elementOffset(e) + offset))
+            self.coreSetElementOffset(e, opFrac(self.elementOffset(e) + offset))
 
         self.coreElementsChanged()
 
@@ -5092,7 +5082,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         self.shiftElements(self.offset)
         self.offset = 0.0
-        self.coreElementsChanged()
 
     # -------------------------------------------------------------------------
     # utilities for creating large numbers of elements
@@ -6691,7 +6680,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             # Recurse rather than depend on the containers being Measures
             # https://github.com/cuthbertLab/music21/issues/266
             returnObj.remove(nTarget, recurse=True)
-        returnObj.coreElementsChanged()
 
         if retainContainers:
             return returnObj
@@ -6900,7 +6888,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             s._endElements = shallowEndElements
 
             for e in shallowElements + shallowEndElements:
-                s.setElementOffset(e, self.elementOffset(e), addElement=True)
+                s.coreSetElementOffset(e, self.elementOffset(e), addElement=True)
                 e.sites.add(s)
                 # need to explicitly set activeSite
                 s.coreSelfActiveSite(e)
@@ -6913,7 +6901,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
     def _getFlatOrSemiFlat(self, retainContainers=False):
         '''
         A private method that implements the .flat or .semiFlat reduction types by using
-        `retainContainers`=False to get .flat and retainContainers=True to get .semiFlat
+        `retainContainers`=False to get .flat and `retainContainers`=True to get .semiFlat
         '''
         # environLocal.printDebug(['_getFlatOrSemiFlat(): self', self,
         #  'self.activeSite', self.activeSite])
@@ -8297,7 +8285,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
             # environLocal.printDebug(['changing offset', o, scalar, offsetShift])
 
-            returnObj.setElementOffset(e, o)
+            returnObj.coreSetElementOffset(e, o)
             # need to look for embedded Streams, and call this method
             # on them, with inPlace , as already copied if
             # inPlace is != True
@@ -8319,8 +8307,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         To augment or diminish a Stream, see the
         :meth:`~music21.stream.Stream.augmentOrDiminish` method.
 
-        We do not retain durations in any circumstance; if inPlace=True, two deepcopies are done
-        which can be quite slow.
+        We do not retain durations in any circumstance;
+        if inPlace=False, two deepcopies of each duration are done.
         '''
         if not amountToScale > 0:
             raise StreamException('amountToScale must be greater than zero')
@@ -8335,7 +8323,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         for e in returnObj.recurse().getElementsNotOfClass('Stream'):
             e.duration = e.duration.augmentOrDiminish(amountToScale)
 
-        returnObj.coreElementsChanged()
         if inPlace is not True:
             return returnObj
 
@@ -8388,7 +8375,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         returnObj.scaleOffsets(amountToScale=amountToScale, anchorZero='lowest',
                                anchorZeroRecurse=None, inPlace=True)
         returnObj.scaleDurations(amountToScale=amountToScale, inPlace=True)
-        returnObj.coreElementsChanged()
 
         # do not need to call elements changed, as called in sub methods
         return returnObj
@@ -8424,11 +8410,10 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         if `inPlace` is True then the quantization is done on the Stream itself.  If False
         (default) then a new quantized Stream of the same class is returned.
 
-        If `recurse` is True then all substreams are also quantized.  If False (TODO: MAKE default)
+        If `recurse` is True then all substreams are also quantized.
+        If False (TODO: MAKE default in v.7)
         then only the highest level of the Stream is quantized.
 
-
-
         >>> n = note.Note()
         >>> n.quarterLength = 0.49
         >>> s = stream.Stream()
@@ -8443,7 +8428,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> [e.duration.quarterLength for e in s]
         [0.5, 0.5, 0.5, 0.25, 0.25]
 
-
         The error in quantization is set in the editorial attribute for the note in
         two places `.offsetQuantizationError` and `.quarterLengthQuantizationError`
 
@@ -8452,7 +8436,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> [e.editorial.quarterLengthQuantizationError for e in s.notes]
         [-0.01, -0.01, -0.01, 0.01, 0.01]
 
-
         with default quarterLengthDivisors...
 
         >>> s = stream.Stream()
@@ -8468,10 +8451,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         OMIT_FROM_DOCS
 
-
         Test changing defaults, running, and changing back...
 
-
         >>> dd = defaults.quantizationQuarterLengthDivisors
         >>> defaults.quantizationQuarterLengthDivisors = (3,)
 
@@ -8533,7 +8514,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                         sign = -1
                         o = -1 * o
                     unused_error, oNew, signedError = bestMatch(float(o), quarterLengthDivisors)
-                    useStream.setElementOffset(e, oNew * sign)
+                    useStream.coreSetElementOffset(e, oNew * sign)
                     if (hasattr(e, 'editorial')
                             and signedError != 0):
                         e.editorial.offsetQuantizationError = signedError * sign
@@ -8552,6 +8533,10 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                             and signedError != 0):
                         e.editorial.quarterLengthQuantizationError = signedError
 
+            # end for e in ._elements
+            # ran coreSetElementOffset
+            useStream.coreElementsChanged(updateIsFlat=False)
+
         if inPlace is False:
             return returnStream
 
@@ -8704,7 +8689,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             # call on component measures
             for m in returnObj.getElementsByClass('Measure'):
                 m.sliceByGreatestDivisor(addTies=addTies, inPlace=True)
-            returnObj.coreElementsChanged()
             return returnObj  # exit
 
         uniqueQuarterLengths = []
@@ -8721,7 +8705,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         returnObj.sliceByQuarterLengths(quarterLengthList=[divisor],
                                         target=None, addTies=addTies, inPlace=True)
 
-        returnObj.coreElementsChanged()
         if not inPlace:
             return returnObj
 
@@ -10537,6 +10520,17 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
           all voices will be flattened.
 
         Changed in v. 5 -- inPlace is default False and a keyword only arg.
+
+        >>> s = stream.Stream([note.Note(), note.Note(), note.Note()])  # simultaneous
+        >>> s.makeVoices(inPlace=True)
+        >>> len(s.voices)
+        3
+
+        >>> s.remove(s.voices[1].notes[0], recurse=True)
+        >>> s.remove(s.voices[2].notes[0], recurse=True)
+        >>> voicesFlattened = s.flattenUnnecessaryVoices()
+        >>> len(voicesFlattened.voices)
+        0
         '''
         if not self.voices:
             return None  # do not make copy; return immediately
@@ -10567,7 +10561,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     returnObj.coreInsert(shiftOffset + e.getOffsetBySite(v), e)
                 returnObj.remove(v)
 
-        returnObj.coreElementsChanged()
         if not inPlace:
             return returnObj
         else:
@@ -11024,8 +11017,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # Now fix measure numbers given the saved information
         returnObj._fixMeasureNumbers(deletedMeasures, insertedMeasures)
 
-        # have to clear cached variants, as they are no longer the same
-        returnObj.coreElementsChanged()
         if not inPlace:
             return returnObj
         else:
@@ -11537,7 +11528,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                         continue
 
                     elementOffset = e.getOffsetBySite(returnObj)
-                    returnObj.setElementOffset(e, elementOffset - shiftDur)
+                    returnObj.coreSetElementOffset(e, elementOffset - shiftDur)
         else:
             shiftDur = 0.0
             shiftsDict = {}
@@ -11568,11 +11559,14 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
                     if e in exemptObjects:
                         elementOffset = e.getOffsetBySite(returnObj)
-                        returnObj.setElementOffset(e, elementOffset + exemptShift)
+                        returnObj.coreSetElementOffset(e, elementOffset + exemptShift)
                         continue
 
                     elementOffset = e.getOffsetBySite(returnObj)
-                    returnObj.setElementOffset(e, elementOffset + shiftDur)
+                    returnObj.coreSetElementOffset(e, elementOffset + shiftDur)
+
+        # ran coreSetElementOffset
+        returnObj.coreElementsChanged()
 
         if inPlace is True:
             return
@@ -11882,7 +11876,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     r.style.hideObjectOnPrint = True
                     for el in cV._stream:
                         oldOffset = el.getOffsetBySite(cV._stream)
-                        cV._stream.setElementOffset(el, oldOffset + shiftOffset)
+                        cV._stream.coreSetElementOffset(el, oldOffset + shiftOffset)
+                    cV.coreElementsChanged()
                     cV.insert(0.0, r)
                     cV.replacementDuration = oldReplacementDuration
                     self.remove(cV)
@@ -13351,7 +13346,11 @@ class Score(Stream):
                                **subroutineKeywords)
             # note: while the local-streams have updated their caches, the
             # containing score has an out-of-date cache of flat.
-            # this, must call elements changed
+            # thus, must call elements changed
+            # but... since all we have done in this method is call coreGatherMissingSpanners()
+            # and makeNotation(), neither of which are supposed to leave the stream
+            # unusable (with an out-of-date cache), the original issue was likely deeper
+            # no matter, let's just be extra cautious and run this here (Feb 2021 - JTW)
             returnStream.coreElementsChanged()
         else:  # call the base method
             super(Score, returnStream).makeNotation(meterStream=meterStream,
@@ -13366,7 +13365,6 @@ class Score(Stream):
             return returnStream
 
 
-
 class Opus(Stream):
     '''
     A Stream subclass for handling multi-work music encodings.
diff --git a/music21/stream/core.py b/music21/stream/core.py
index 80ad8df5d..873204a5e 100644
--- a/music21/stream/core.py
+++ b/music21/stream/core.py
@@ -27,6 +27,7 @@ from fractions import Fraction
 import unittest
 
 from music21.base import Music21Object
+from music21.common.numberTools import opFrac
 from music21 import spanner
 from music21 import tree
 from music21.exceptions21 import StreamException, ImmutableStreamException
@@ -105,7 +106,7 @@ class StreamCoreMixin:
                         if highestSortTuple < thisSortTuple:
                             storeSorted = True
 
-        self.setElementOffset(
+        self.coreSetElementOffset(
             element,
             float(offset),  # why is this not opFrac?
             addElement=True,
@@ -136,7 +137,7 @@ class StreamCoreMixin:
         # NOTE: this is not called by append, as that is optimized
         # for looping multiple elements
         ht = self.highestTime
-        self.setElementOffset(element, ht, addElement=True)
+        self.coreSetElementOffset(element, ht, addElement=True)
         element.sites.add(self)
         # need to explicitly set the activeSite of the element
         if setActiveSite:
@@ -151,13 +152,54 @@ class StreamCoreMixin:
     # adding and editing Elements and Streams -- all need to call coreElementsChanged
     # most will set isSorted to False
 
+    def coreSetElementOffset(
+        self,
+        element: Music21Object,
+        offset: Union[int, float, Fraction, str],
+        *,
+        addElement=False,
+        setActiveSite=True
+    ):
+        '''
+        Sets the Offset for an element, very quickly.
+        Caller is responsible for calling :meth:`~music21.stream.core.coreElementsChanged`
+        afterward.
+
+        >>> s = stream.Stream()
+        >>> s.id = 'Stream1'
+        >>> n = note.Note('B-4')
+        >>> s.insert(10, n)
+        >>> n.offset
+        10.0
+        >>> s.coreSetElementOffset(n, 20.0)
+        >>> n.offset
+        20.0
+        >>> n.getOffsetBySite(s)
+        20.0
+        '''
+        # Note: not documenting 'highestTime' is on purpose, since can only be done for
+        # elements already stored at end.  Infinite loop.
+        try:
+            offset = opFrac(offset)
+        except TypeError:
+            if offset not in OFFSET_STRING_VALUES:  # pragma: no cover
+                raise StreamException(f'Cannot set offset to {offset!r} for {element}')
+
+        idEl = id(element)
+        if not addElement and idEl not in self._offsetDict:
+            raise StreamException(
+                f'Cannot set the offset for element {element}, not in Stream {self}.')
+        self._offsetDict[idEl] = (offset, element)  # fast
+        if setActiveSite:
+            self.coreSelfActiveSite(element)
+
     def coreElementsChanged(
         self,
         *,
         updateIsFlat=True,
         clearIsSorted=True,
         memo=None,
-        keepIndex=False
+        keepIndex=False,
     ):
         '''
         NB -- a "core" stream method that is not necessary for most users.
@@ -193,6 +235,9 @@ class StreamCoreMixin:
 
         if memo is None:
             memo = []
+
+        if id(self) in memo:
+            return
         memo.append(id(self))
 
         # WHY??? THIS SEEMS OVERKILL, esp. since the first call to .sort() in .flat will
@@ -214,7 +259,7 @@ class StreamCoreMixin:
         # always be a good idea since .flat has changed etc.
         # should not need to do derivation.origin sites.
         for livingSite in self.sites:
-            livingSite.coreElementsChanged()
+            livingSite.coreElementsChanged(memo=memo)
 
         # clear these attributes for setting later
         if clearIsSorted:
@@ -353,7 +398,7 @@ class StreamCoreMixin:
         Core method for adding end elements.
         To be called by other methods.
         '''
-        self.setElementOffset(element, 'highestTime', addElement=True)
+        self.coreSetElementOffset(element, 'highestTime', addElement=True)
         element.sites.add(self)
         # need to explicitly set the activeSite of the element
         if setActiveSite:
diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py
index 1ca9e1f12..85a93cd09 100644
--- a/music21/stream/iterator.py
+++ b/music21/stream/iterator.py
@@ -164,7 +164,7 @@ class StreamIterator(prebase.ProtoM21Object):
             try:
                 e = self.srcStreamElements[self.index - 1]
             except IndexError:
-                # this may happen in the number of elements has changed
+                # this may happen if the number of elements has changed
                 continue
 
             if self.matchesFilters(e) is False:
@@ -250,7 +250,20 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s.notes.asdf
         Traceback (most recent call last):
         AttributeError: 'StreamIterator' object has no attribute 'asdf'
+
+        OMIT_FROM_DOCS
+
+        srcStream is accessible, but not with "__getattr__", which joblib uses
+
+        >>> s.notes.srcStream is s
+        True
+        >>> s.notes.__getattr__('srcStream') is None
+        True
         '''
+        # Prevent infinite loop in feature extractor task serialization
+        if attr == 'srcStream':
+            return None
+
         if not hasattr(self.srcStream, attr):
             # original stream did not have the attribute, so new won't; but raise on iterator.
             raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {attr!r}')
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 0d9fcbea4..67e29af55 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -522,6 +522,8 @@ def makeMeasures(
             v = stream.Voice()
             v.id = voiceIndex  # id is voice index, starting at 0
             m.coreInsert(0, v)
+        if voiceCount:
+            m.coreElementsChanged()
 
         # avoid an infinite loop
         if thisTimeSignature.barDuration.quarterLength == 0:
@@ -535,6 +537,8 @@ def makeMeasures(
         else:
             measureCount += 1
 
+    post.coreElementsChanged()
+
     # cache information about each measure (we used to do this once per element...
     postLen = len(post)
     postMeasureList = []
@@ -619,8 +623,6 @@ def makeMeasures(
     for sp in spannerBundleAccum:
         post.append(sp)
 
-    post.coreElementsChanged()
-
     # clean up temporary streams to avoid extra site accumulation
     del srcObj
 
@@ -1176,8 +1178,6 @@ def makeTies(
         mCount += 1
     del measureStream  # clean up unused streams
 
-    # changes elements
-    returnObj.coreElementsChanged()
     if not inPlace:
         return returnObj
     else:


Test patch:
diff --git a/music21/abcFormat/testFiles.py b/music21/abcFormat/testFiles.py
index c519ac80c..27bc6dbb2 100644
--- a/music21/abcFormat/testFiles.py
+++ b/music21/abcFormat/testFiles.py
@@ -816,10 +816,130 @@ class Test(unittest.TestCase):
         self.assertEqual(notes[8].pitch.midi, 65, 'Natural is ignored')
         self.assertEqual(notes[12].pitch.midi, 72, 'Natural is ignored')
 
+    def testAbc21Chords(self):
+        '''
+        Translation of ABC Chord variations
+        '''
+        from music21 import abcFormat, chord
+        from music21.abcFormat import translate
+
+        af = abcFormat.ABCFile()
+        # default length of this test
+        abc_dl = 'L:1/8\n'
+
+        # Empty Chords should be skipped at all
+        for abc_chord in ['[]', '[z]']:
+            ah = af.readstr(abc_dl + '[]')
+            s = translate.abcToStreamScore(ah)
+            part = s.parts[0]
+            self.assertFalse(part.getElementsByClass(chord.Chord),
+                             'Empty chord "%s" in Score' % abc_chord)
+
+        # list of test abc chords and their quarter lengths at the default length of 1/8
+        # list[tuple(str, int)] = of abc chords and= [( abc_chord: str)]
+        abc_chords = [
+            ('[c_eg]', 0.5, ['C', 'E-', 'G']),
+            ('[ceg]', 0.5, 'CEG'),
+            ('[ceg]2', 1.0, 'CEG'),
+            ('[c2e2^g2]', 1.0, ['C', 'E', 'G#']),
+            ("[c'e2g]", 0.5, 'CEG'),
+            ('[ce^g2]', 0.5, ['C', 'E', 'G#']),
+            ('[c,2e2g2]/2', 0.5, 'CEG'),
+            ("[c/2e'/2=g/2]", 0.25, 'CEG'),
+            ('[c2_e,,/2g/2]/2', 0.5, ['C', 'E-', 'G']),
+            ('[c/2e/2g/2]2', 0.5, 'CEG'),
+            ('[^c/2e/2g/2]/2', 0.125, ['C#', 'E', 'G']),
+            ('[ceg]', 0.5, 'CEG'),
+        ]
+
+        for abc_chord, quarter_length, chord_pitches in abc_chords:
+            ah = af.readstr(abc_dl + abc_chord)
+            s = translate.abcToStreamScore(ah)
+            self.assertEqual(s.duration.quarterLength, quarter_length,
+                             'invalid duration of chord "%s"' % abc_chord)
+
+            notes = s.parts[0].notes
+            chord0 = notes[0]
+            self.assertEqual(len(notes), 1, 'Wrong number of chords found,')
+            self.assertIsInstance(chord0, chord.Chord, 'Not a Chord!')
+            for pitch_name in chord_pitches:
+                self.assertIn(pitch_name, chord0.pitchNames,
+                              'Pitch not in Chord "%s"' % abc_chord)
+
+    def testAbc21ChordSymbol(self):
+        # Test the chord symbol for note and chord
+        from music21 import abcFormat, harmony
+        from music21.abcFormat import translate
+
+        # default length of this test
+        abc_dl = 'L:1/8\n'
+
+        af = abcFormat.ABCFile()
+        for abc_text in ('"C"C', '"C"[ceg]'):
+            ah = af.readstr(abc_dl + abc_text)
+            part = translate.abcToStreamScore(ah).parts[0]
+            chord_symbol = part.getElementsByClass(harmony.ChordSymbol)
+            self.assertTrue(chord_symbol, 'No ChordSymbol found in abc: "%s"' % abc_text)
+            for pitch_name in 'CEG':
+                self.assertIn(pitch_name, chord_symbol[0].pitchNames,
+                              'Pitch not in ChordSymbol of abc: "%s"' % abc_text)
+
+    def testAbc21BrokenRhythm(self):
+        # Test the chord symbol for note and chord
+        from music21 import abcFormat, note
+        from music21.abcFormat import translate
+
+        # default length of this test
+        abc_dl = 'L:1/4\n'
+
+        # test abc strings of broken rhythm between 2 notes and/or chords and their
+        # quarter lengths at the default length of 1/4
+        # list[tuple(abc: str, value1: int, value2: int)]
+        data = [
+            ('[ceg]<f', 0.5, 1.5),
+            ('f<[ceg]', 0.5, 1.5),
+            ('c>g', 1.5, 0.5),
+            ('c<g', 0.5, 1.5),
+            ('c>>=g', 1.75, 0.25),
+            ('c<<g', 0.25, 1.75),
+            ('c>>>g', 1.875, 0.125),
+            ('c<<<_g', 0.125, 1.875),
+            ("[ceg]>^f", 1.5, 0.5),
+            ('[ce^g]>>f', 1.75, 0.25),
+            ("[ceg]<<f", 0.25, 1.75),
+            ('[ceg]>>>f', 1.875, 0.125),
+            ("[ceg]<<<f", 0.125, 1.875),
+            ('f>[ceg]', 1.5, 0.5),
+            ('f>>[_ceg]', 1.75, 0.25),
+            ("f'<<[ceg]", 0.25, 1.75),
+            ('f,>>>[ceg]', 1.875, 0.125),
+            ('f<<<[ce_g]', 0.125, 1.875),
+            ('f<<<[ceg]', 0.125, 1.875),
+            ('f2>[ceg]', 3, 0.5),
+            ('[ceg]>f2', 1.5, 1),
+            ('f>[c_eg]2', 1.5, 1),
+            ('[c^eg]2>f', 3, 0.5),
+            ('f2<[ceg]', 1.0, 1.5),
+            ('[ceg]<f2', 0.5, 3),
+            ('f<[ceg]2', 0.5, 3),
+            ('[ceg]2<f', 1.0, 1.5),
+        ]
+
+        af = abcFormat.ABCFile()
+        for abc, soll_left, soll_right in data:
+            ah = af.readstr(abc_dl + abc)
+            part = translate.abcToStreamScore(ah).parts[0]
+            general_notes = part.getElementsByClass(note.GeneralNote)
+            self.assertEqual(len(general_notes), 2,
+                             f'Wrong numbers of Notes found in abc: {abc}!')
+            ist_left, ist_right = general_notes
+            self.assertEqual(ist_left.duration.quarterLength, soll_left,
+                             f'Invalid left note/chord length of abc broken rhythm: {abc}')
+            self.assertEqual(ist_right.duration.quarterLength, soll_right,
+                             f'Invalid right note/chord length of abc broken rhythm: {abc}')
+
 
 if __name__ == '__main__':
     import music21
     # music21.converter.parse(reelsABC21, format='abc').scores[1].show()
     music21.mainTest(Test)
-
-