Repository: cuthbertLab/music21
Pull request #887 (2021-03-05 00:39:41)

Flatten unnecessary voices in makeTies
Noticed `makeTies` had a comment about leaving unnecessary voices behind, and figured that was easy to clean up with a call to `flattenUnnecessaryVoices()`.

Then promoted the doctest where that was demo'd (along with some version changed annotations) above the OMIT_FROM_DOCS line.


Patch:
diff --git a/.github/scripts/install_ubuntu_deps.sh b/.github/scripts/install_ubuntu_deps.sh
index a62ac93a9..b2447b473 100755
--- a/.github/scripts/install_ubuntu_deps.sh
+++ b/.github/scripts/install_ubuntu_deps.sh
@@ -5,9 +5,9 @@ set -e  # error if anything returns non-zero exit code
 mkdir ~/Desktop
 
 sudo apt-get install -y libpng-dev
-sudo apt-get install -y python-qt4
-wget -q http://lilypond.org/downloads/binaries/linux-64/lilypond-2.19.83-1.linux-64.sh
-sh lilypond-2.19.83-1.linux-64.sh --batch
+# sudo apt-get install -y python-qt4
+wget -q https://lilypond.org/download/binaries/linux-64/lilypond-2.22.0-1.linux-64.sh
+sh lilypond-2.22.0-1.linux-64.sh --batch
 export PATH=/home/runner/bin:$PATH
 pip3 install -r requirements.txt
 pip3 install coveralls
diff --git a/documentation/docbuild/documenters.py b/documentation/docbuild/documenters.py
index a965f2d6d..21e5ef5d3 100644
--- a/documentation/docbuild/documenters.py
+++ b/documentation/docbuild/documenters.py
@@ -1375,11 +1375,10 @@ class ModuleDocumenter(ObjectDocumenter):
         >>> for classDocumenter in modDocumenter.classDocumenters:
         ...     print(classDocumenter.referentPackageSystemPath)
         ...
-        music21.serial.HistoricalTwelveToneRow
         music21.serial.ToneRow
-        music21.serial.TwelveToneMatrix
         music21.serial.TwelveToneRow
-
+        music21.serial.HistoricalTwelveToneRow
+        music21.serial.TwelveToneMatrix
         '''
         result = []
         classDocumenters = {}
diff --git a/documentation/source/about/faq.rst b/documentation/source/about/faq.rst
index 0fcf18ed5..79acecd55 100644
--- a/documentation/source/about/faq.rst
+++ b/documentation/source/about/faq.rst
@@ -95,7 +95,7 @@ Can I synthesize new sounds with `music21`?
 
     Yes, and no.  `Music21` is primarily built for manipulating symbolic 
     musical data not sound waves.  There are lots of great programs for
-    doing that.  But the symbolic data, however, be used as data within 
+    doing that.  But the symbolic data, however, can be used as data within 
     a large variety of synthesis packages. So you could create new
     `music21` objects that control the synthesis package of your choosing.   
     
diff --git a/documentation/source/usersGuide/usersGuide_58_sitesContext.ipynb b/documentation/source/usersGuide/usersGuide_58_sitesContext.ipynb
index ddb7097ff..5c4eca5d6 100644
--- a/documentation/source/usersGuide/usersGuide_58_sitesContext.ipynb
+++ b/documentation/source/usersGuide/usersGuide_58_sitesContext.ipynb
@@ -217,7 +217,7 @@
     }
    ],
    "source": [
-    "v = stream.Voice()\n",
+    "v = stream.Voice(id=1)\n",
     "v.append(es)\n",
     "es.sites.getSiteCount()"
    ]
@@ -291,7 +291,7 @@
     {
      "data": {
       "text/plain": [
-       "[<music21.stream.Voice 0x10d4c7860>]"
+       "[<music21.stream.Voice 1>]"
       ]
      },
      "execution_count": 11,
@@ -385,7 +385,7 @@
      "text": [
       "None\n",
       "<music21.stream.Measure 1 offset=0.0>\n",
-      "<music21.stream.Voice 0x10d4c7860>\n",
+      "<music21.stream.Voice 1>\n",
       "<music21.stream.Measure 10 offset=0.0>\n"
      ]
     }
@@ -412,7 +412,7 @@
      "output_type": "stream",
      "text": [
       "<music21.stream.Measure 10 offset=0.0>\n",
-      "<music21.stream.Voice 0x10d4c7860>\n",
+      "<music21.stream.Voice 1>\n",
       "<music21.stream.Measure 1 offset=0.0>\n"
      ]
     }
@@ -439,7 +439,7 @@
      "output_type": "stream",
      "text": [
       "2.0 <music21.stream.Measure 1 offset=0.0>\n",
-      "0.0 <music21.stream.Voice 0x10d4c7860>\n",
+      "0.0 <music21.stream.Voice 1>\n",
       "20.0 <music21.stream.Measure 10 offset=0.0>\n"
      ]
     }
@@ -472,7 +472,7 @@
        "             (4504830192,\n",
        "              <music21.sites.SiteRef 0/0 to <music21.stream.Measure 1 offset=0.0>>),\n",
        "             (4518082656,\n",
-       "              <music21.sites.SiteRef 1/1 to <music21.stream.Voice 0x10d4c7860>>),\n",
+       "              <music21.sites.SiteRef 1/1 to <music21.stream.Voice 1>>),\n",
        "             (4518134560,\n",
        "              <music21.sites.SiteRef 2/2 to <music21.stream.Measure 10 offset=0.0>>)])"
       ]
diff --git a/music21/_version.py b/music21/_version.py
index 5238d4807..7af8793f9 100644
--- a/music21/_version.py
+++ b/music21/_version.py
@@ -42,7 +42,7 @@ When changing, update the single test case in base.py.
 Changing this number invalidates old pickles -- do it if the old pickles create a problem.
 '''
 
-__version_info__ = (7, 0, 0)  # can be 4-tuple: (7, 0, 5, 'a2')
+__version_info__ = (7, 0, 3)  # can be 4-tuple: (7, 0, 5, 'a2')
 
 v = '.'.join(str(x) for x in __version_info__[0:3])
 if len(__version_info__) > 3 and __version_info__[3]:
diff --git a/music21/analysis/neoRiemannian.py b/music21/analysis/neoRiemannian.py
index a994cc06e..de04da17d 100644
--- a/music21/analysis/neoRiemannian.py
+++ b/music21/analysis/neoRiemannian.py
@@ -187,6 +187,7 @@ def _singlePitchTransform(c, transposeInterval, changingPitch):
     changingPitchCopy = copy.deepcopy(changingPitch)
     newChord = copy.deepcopy(c)
     for i in range(len(newChord.pitches)):
+        newChord.pitches[i].spellingIsInferred = False
         if changingPitchCopy.name == newChord.pitches[i].name:
             newChord.pitches[i].transpose(transposeInterval, inPlace=True)
     return chord.Chord(newChord.pitches)
@@ -353,7 +354,7 @@ def LRP_combinations(c,
     :func:`~music21.analysis.neoRiemannian.completeHexatonic`.
     '''
 
-    if c.forteClassTnI != '3-11':  # First to avoid doing anything else if fail
+    if not c.isMajorTriad() and not c.isMinorTriad():  # First to avoid doing anything else if fail
         if raiseException is True:
             raise LRPException(
                 f'Cannot perform transformations on chord {c}: not a major or minor triad')
@@ -681,6 +682,21 @@ class Test(unittest.TestCase):
         c8_T = LRP_combinations(c8, 'LP')
         self.assertEqual(str(c8_T), '<music21.chord.Chord B-3 E-4 G4>')
 
+        d_sharp_min_misspelled = chord.Chord('B- D# F#')
+        d_sharp_min_transformed = LRP_combinations(d_sharp_min_misspelled, 'LP',
+                                                   raiseException=False)
+        self.assertIs(d_sharp_min_misspelled, d_sharp_min_transformed)
+        with self.assertRaises(LRPException):
+            LRP_combinations(d_sharp_min_misspelled, 'LP', raiseException=True)
+
+        f_min_misspelled = chord.Chord('B# F G#')
+        with self.assertRaises(LRPException):
+            LRP_combinations(f_min_misspelled, 'LP', raiseException=True)
+
+        e_sharp_min = chord.Chord('B# E# G#')
+        e_sharp_min_transformed = LRP_combinations(e_sharp_min, 'LP', raiseException=True)
+        self.assertEqual(chord.Chord('C# E G#').pitches, e_sharp_min_transformed.pitches)
+
     def testIsNeoR(self):
 
         c1 = chord.Chord('C4 E4 G4')
@@ -758,6 +774,30 @@ class Test(unittest.TestCase):
         N2 = N(c11)
         self.assertEqual([x.name for x in N2.pitches], ['G#', 'B', 'E'])
 
+    def testInstantiatingChordPCNumbers(self):
+
+        c_sharp_maj_named_transformed = L(chord.Chord('C# E# G#'))
+        c_sharp_maj_pitch_classes_transformed = L(chord.Chord([1, 5, 8]))
+        self.assertEqual(c_sharp_maj_named_transformed.pitches,
+                         c_sharp_maj_pitch_classes_transformed.pitches)
+        self.assertEqual(c_sharp_maj_pitch_classes_transformed.pitches,
+                         chord.Chord('B# E# G#').pitches)
+
+        b_maj_named = chord.Chord('B D# F#')
+        b_maj_named_transformed = LRP_combinations(b_maj_named, 'LP',
+                                                   simplifyEnharmonics=False)
+        b_maj_pitch_classes = chord.Chord([11, 3, 6])
+        b_maj_pitch_classes_transformed = LRP_combinations(b_maj_pitch_classes, 'LP',
+                                                           simplifyEnharmonics=False)
+        self.assertTrue(b_maj_pitch_classes.pitches[0].spellingIsInferred)
+        for p in b_maj_pitch_classes_transformed.pitches:
+            self.assertFalse(p.spellingIsInferred)
+
+        self.assertEqual(b_maj_named_transformed.pitches,
+                         b_maj_pitch_classes_transformed.pitches)
+        self.assertEqual(b_maj_pitch_classes_transformed.pitches,
+                         chord.Chord('A# D# F##').pitches)
+
 
 # ------------------------------------------------------------------------------
 _DOC_ORDER = [
diff --git a/music21/bar.py b/music21/bar.py
index 912392f00..c59c98072 100644
--- a/music21/bar.py
+++ b/music21/bar.py
@@ -22,7 +22,6 @@ from music21 import expressions
 from music21 import repeat
 
 from music21 import environment
-from music21 import style
 
 _MOD = 'bar'
 environLocal = environment.Environment(_MOD)
@@ -93,7 +92,7 @@ def standardizeBarType(value):
 
 
 # ------------------------------------------------------------------------------
-class Barline(base.Music21Object, style.StyleMixin):
+class Barline(base.Music21Object):
     '''A representation of a barline.
     Barlines are conventionally assigned to Measure objects
     using the leftBarline and rightBarline attributes.
@@ -397,6 +396,18 @@ class Test(unittest.TestCase):
         self.assertEqual(m[0], md)
         self.assertEqual(m[1], b)
 
+    def testFreezeThaw(self):
+        from music21 import converter, stream
+
+        b = Barline()
+        self.assertNotIn('StyleMixin', b.classes)
+        s = stream.Stream([b])
+        data = converter.freezeStr(s, fmt='pickle')
+        s2 = converter.thawStr(data)
+        thawedBarline = s2[0]
+        # Previously, raised AttributeError
+        self.assertEqual(thawedBarline.hasStyleInformation, False)
+
 
 if __name__ == '__main__':
     import music21
diff --git a/music21/base.py b/music21/base.py
index c43979d30..3cb5a73ea 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -28,7 +28,7 @@ available after importing `music21`.
 <class 'music21.base.Music21Object'>
 
 >>> music21.VERSION_STR
-'7.0.0'
+'7.0.3'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
@@ -2055,6 +2055,8 @@ class Music21Object(prebase.ProtoM21Object):
             activeS = self.activeSite  # might be None...
             if activeS is None:
                 return None
+            if className is not None and not common.isListLike(className):
+                className = (className,)
             asTree = activeS.asTree(classList=className, flatten=False)
             prevNode = asTree.getNodeBefore(self.sortTuple())
             if prevNode is None:
diff --git a/music21/braille/segment.py b/music21/braille/segment.py
index f7fcbcda4..f886f54c5 100644
--- a/music21/braille/segment.py
+++ b/music21/braille/segment.py
@@ -1678,8 +1678,8 @@ def prepareSlurredNotes(music21Part, **keywords):
 
     allNotes = music21Part.flat.notes.stream()
     for slur in music21Part.spannerBundle.getByClass(spanner.Slur):
-        firstNote = slur[0]
-        lastNote = slur[1]
+        firstNote = slur.getFirst()
+        lastNote = slur.getLast()
 
         try:
             beginIndex = allNotes.index(firstNote)
diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py
index fb861558f..7edbd2159 100644
--- a/music21/chord/__init__.py
+++ b/music21/chord/__init__.py
@@ -18,7 +18,7 @@ __all__ = ['tables', 'Chord', 'ChordException', 'fromIntervalVector', 'fromForte
 import copy
 import unittest
 import re
-from typing import Union, List, Optional, TypeVar, Set, Tuple
+from typing import Union, List, Optional, TypeVar, Set, Tuple, Dict
 
 from music21 import beam
 from music21 import common
@@ -32,7 +32,7 @@ from music21 import tie
 from music21 import volume
 
 from music21 import environment
-from music21.chord import tables as chordTables
+from music21.chord import tables
 from music21.common.decorators import cacheMethod
 
 _MOD = 'chord'
@@ -908,7 +908,7 @@ class Chord(note.NotRest):
         >>> c4.areZRelations(c3)
         False
         '''
-        zRelationAddress = chordTables.addressToZAddress(self.chordTablesAddress)
+        zRelationAddress = tables.addressToZAddress(self.chordTablesAddress)
         if zRelationAddress is None:
             return False
         if other.chordTablesAddress[0:3] == zRelationAddress[0:3]:
@@ -1270,39 +1270,38 @@ class Chord(note.NotRest):
                     score += 1 / (root_index + 6)
             return score
 
-        orderedChordSteps = (3, 5, 7, 2, 4, 6)
-
         # FIND ROOT FAST -- for cases where one note has perfectly stacked
         # thirds, like E C G; but not C E B-
-        # if one pitch has perfectlyStackedThirds, return it always:
-        stepsFound = set()
-        nonDuplicatingPitches = []
-        for p in self.pitches:
-            if p.step in stepsFound:
-                continue
-            else:
-                stepsFound.add(p.step)
-                nonDuplicatingPitches.append(p)
+        # if one pitch has perfectlyStackedThirds, return it always.
 
+        # we use the music21 unique function since it preserves the order
+        nonDuplicatingPitches = common.misc.unique((n.pitch for n in self._notes),
+                                                   key=lambda pp: pp.step)
         lenPitches = len(nonDuplicatingPitches)
         if not lenPitches:
             raise ChordException(f'no pitches in chord {self!r}')
+
         if lenPitches == 1:
             return self.pitches[0]
-
-        # for C E G C, searchChordSteps will be (3, 5)
-        searchChordSteps = orderedChordSteps[:lenPitches - 1]
-
-        for i, p in enumerate(nonDuplicatingPitches):
-            foundAll = True
-            for chordStepTest in searchChordSteps:
-                if not self.getChordStep(chordStepTest, testRoot=p):
-                    foundAll = False
+        elif lenPitches == 7:  # 13th chord
+            return self.bass()
+
+        stepNumsToPitches: Dict[int, pitch.Pitch] = {pitch.STEP_TO_DNN_OFFSET[p.step]: p
+                                                     for p in nonDuplicatingPitches}
+        stepNums = sorted(stepNumsToPitches)
+        for startIndex in range(lenPitches):
+            all_are_thirds = True
+            this_step_num = stepNums[startIndex]
+            last_step_num = this_step_num
+            for endIndex in range(startIndex + 1, startIndex + lenPitches):
+                endIndexMod = endIndex % lenPitches
+                endStepNum = stepNums[endIndexMod]
+                if endStepNum - last_step_num not in (2, -5):
+                    all_are_thirds = False
                     break
-            if foundAll:
-                # note that for a 13th chord, this will return the bass,
-                # which is great!
-                return p
+                last_step_num = endStepNum
+            if all_are_thirds:
+                return stepNumsToPitches[this_step_num]
 
         # FIND ROOT SLOW
         # no notes (or more than one...) have perfectlyStackedThirds above them.  Return
@@ -1310,11 +1309,13 @@ class Chord(note.NotRest):
         # this is the slowest...
 
         rootnessFunctionScores = []
+        orderedChordSteps = (3, 5, 7, 2, 4, 6)
 
-        for i, p in enumerate(nonDuplicatingPitches):
+        for p in nonDuplicatingPitches:
             currentListOfThirds = []
+            this_step_num = pitch.STEP_TO_DNN_OFFSET[p.step]
             for chordStepTest in orderedChordSteps:
-                if self.getChordStep(chordStepTest, testRoot=p):
+                if (this_step_num + chordStepTest - 1) % 7 in stepNumsToPitches:
                     currentListOfThirds.append(True)
                 else:
                     currentListOfThirds.append(False)
@@ -1664,8 +1665,8 @@ class Chord(note.NotRest):
         '''
         if self.hasZRelation:
             chordTablesAddress = self.chordTablesAddress
-            v = chordTables.addressToIntervalVector(chordTablesAddress)
-            addresses = chordTables.intervalVectorToAddress(v)
+            v = tables.addressToIntervalVector(chordTablesAddress)
+            addresses = tables.intervalVectorToAddress(v)
             # environLocal.printDebug(['addresses', addresses,
             #    'chordTablesAddress', chordTablesAddress])
             # addresses returned here are 2 elements lists
@@ -1674,7 +1675,7 @@ class Chord(note.NotRest):
                 if thisAddress.forteClass != chordTablesAddress.forteClass:
                     other = thisAddress
             # other should always be defined to not None
-            prime = chordTables.addressToTransposedNormalForm(other)
+            prime = tables.addressToTransposedNormalForm(other)
             return Chord(prime)
         return None
         # c2.getZRelation()  # returns a list in non-ET12 space...
@@ -2826,6 +2827,122 @@ class Chord(note.NotRest):
         '''
         return self._checkTriadType((3, 11, 1), 3, 7)
 
+    def isTranspositionallySymmetrical(self, *, requireIntervallicEvenness=False) -> bool:
+        '''
+        Returns True if the Chord is symmetrical under transposition
+        and False otherwise.  A pitch-class-based way of looking at this, is
+        can all the pitch classes be transposed up some number of semitones 1-11
+        and end up with the same pitch-classes.  Like the dyad F-B can have each
+        note transposed up 6 semitones and get another B-F = F-B dyad.
+
+        A tonally-focused way of looking at this would be are we unable
+        to distinguish root position vs. some inversion of the chord by ear alone?
+        For instance, we can see that C-Eb-Gb-Bbb is in root position, while
+        Eb-Gb-Bbb-C is in first inversion.  But only hearing the chord in isolation
+        it would not be possible to tell.
+
+        With either way of looking at it,
+        fourteen set classes of 2-10 pitch classes have this property,
+        including the augmented triad:
+
+        >>> chord.Chord('C E G#').isTranspositionallySymmetrical()
+        True
+
+        ...but not the major triad:
+
+        >>> chord.Chord('C E G').isTranspositionallySymmetrical()
+        False
+
+        The whole-tone scale and the Petrushka chord are both transpositionally symmetrical:
+
+        >>> wholeToneAsChord = chord.Chord('C D E F# G# B- C')
+        >>> wholeToneAsChord.isTranspositionallySymmetrical()
+        True
+
+        >>> petrushka = chord.Chord([0, 1, 3, 6, 7, 9])
+        >>> petrushka.isTranspositionallySymmetrical()
+        True
+
+        If `requireIntervallicEvenness` is True then only chords that also have
+        even spacing / evenly divide the octave are considered transpositionally
+        symmetrical.  The normal cases are the F-B (06) dyad, the augmented triad,
+        the diminished-seventh chord, and the whole-tone scale collection:
+
+        >>> wholeToneAsChord.isTranspositionallySymmetrical(requireIntervallicEvenness=True)
+        True
+
+        >>> petrushka.isTranspositionallySymmetrical(requireIntervallicEvenness=True)
+        False
+
+        Note that complements of these chords (except the whole-tone collection) are
+        not transpositionally symmetrical if `requireIntervallicEvenness` is required:
+
+        >>> chord.Chord([0, 4, 8]).isTranspositionallySymmetrical(requireIntervallicEvenness=True)
+        True
+
+        >>> chord.Chord([1, 2, 3, 5, 6, 7, 9, 10, 11]).isTranspositionallySymmetrical(
+        ...       requireIntervallicEvenness=True)
+        False
+
+        Empty chords and the total aggregate cannot have their inversion determined by ear alone.
+        So they are `True` with or without `requireIntervallicEvenness`.
+
+        >>> chord.Chord().isTranspositionallySymmetrical()
+        True
+
+        >>> chord.Chord(list(range(12))).isTranspositionallySymmetrical()
+        True
+
+        Monads (single-note "chords") cannot be transposed 1-11 semitones to recreate themselves,
+        so they return `False` by default:
+
+        >>> chord.Chord('C').isTranspositionallySymmetrical()
+        False
+
+        But they are the only case where `requireIntervallicEvenness` actually switches from
+        `False` to `True`, because they do evenly divide the octave.
+
+        >>> chord.Chord('C').isTranspositionallySymmetrical(requireIntervallicEvenness=True)
+        True
+
+        11-note chords return `False` in either case:
+
+        >>> chord.Chord(list(range(11))).isTranspositionallySymmetrical()
+        False
+        '''
+        if not self._notes:
+            return True
+
+        address = self.chordTablesAddress
+        if address.cardinality == 1:
+            return requireIntervallicEvenness
+
+        lookup = (address.cardinality, address.forteClass)
+        if lookup in (
+            (2,  6),  # 06 -- omitted by Straus  # noqa: E241
+            (3, 12),  # augmented triad
+            (4, 28),  # diminished seventh chord
+            (6, 35),  # whole-tone scale
+            (12, 1),  # total aggregate.
+        ):
+            return True
+
+        if not requireIntervallicEvenness and lookup in (
+            (4,  9),  # 0167  # noqa: E241
+            (4, 25),  # 0268
+            (6,  7),  # 012678  # noqa: E241
+            (6, 20),  # "Hexatonic scale" 014589
+            (6, 30),  # Petrushka chord 013679
+            (8,  9),  # 01236789  # noqa: E241
+            (8, 25),  # 0124678T
+            (8, 28),  # octatonic scale
+            (9, 12),  # complement to augmented triad
+            (10, 6),  # complement to 06
+        ):
+            return True
+        else:
+            return False
+
     @cacheMethod
     def isSeventh(self):
         '''
@@ -2947,14 +3064,18 @@ class Chord(note.NotRest):
 
         return True
 
+
     @cacheMethod
-    def isTriad(self):
+    def isTriad(self) -> bool:
         '''
-        Returns boolean.
+        Returns True if this Chord is a triad of some sort.  It could even be a rather
+        exotic triad so long as the chord contains at least one Third and one Fifth and
+        all notes have the same name as one of the htree notes.
+
+        Note: only returns True if triad is spelled correctly.
 
-        "Contains vs. Is:" A dominant-seventh chord is NOT a triad.
-        returns True if the chord contains at least one Third and one Fifth and all notes are
-        equivalent to either of those notes. Only returns True if triad is spelled correctly.
+        Note the difference of "containsTriad" vs. "isTriad":
+        A dominant-seventh chord is NOT a triad, but it contains two triads.
 
         >>> cChord = chord.Chord(['C4', 'E4', 'A4'])
         >>> cChord.isTriad()
@@ -3103,6 +3224,7 @@ class Chord(note.NotRest):
              newroot: Union[bool, str, pitch.Pitch, note.Note] = False,
              *,
              find=None):
+        # noinspection PyShadowingNames
         '''
         Returns or sets the Root of the chord. If not set, will find it.
 
@@ -3958,9 +4080,9 @@ class Chord(note.NotRest):
         ChordTableAddress(cardinality=0, forteClass=0, inversion=0, pcOriginal=0)
         '''
         try:
-            return chordTables.seekChordTablesAddress(self)
-        except chordTables.ChordTablesException:
-            return chordTables.ChordTableAddress(0, 0, 0, 0)
+            return tables.seekChordTablesAddress(self)
+        except tables.ChordTablesException:
+            return tables.ChordTableAddress(0, 0, 0, 0)
 
 
     @property
@@ -4093,7 +4215,7 @@ class Chord(note.NotRest):
             else:
                 return 'enharmonic octaves'
 
-        ctn = chordTables.addressToCommonNames(cta)
+        ctn = tables.addressToCommonNames(cta)
         if cta.cardinality == 2:
             pitchNames = {p.name for p in self.pitches}
             pitchPSes = {p.ps for p in self.pitches}
@@ -4258,8 +4380,8 @@ class Chord(note.NotRest):
         '2-2'
         '''
         try:
-            return chordTables.addressToForteName(self.chordTablesAddress, 'tn')
-        except chordTables.ChordTablesException:
+            return tables.addressToForteName(self.chordTablesAddress, 'tn')
+        except tables.ChordTablesException:
             return 'N/A'
 
     @property
@@ -4322,8 +4444,8 @@ class Chord(note.NotRest):
         '2-2'
         '''
         try:
-            return chordTables.addressToForteName(self.chordTablesAddress, 'tni')
-        except chordTables.ChordTablesException:
+            return tables.addressToForteName(self.chordTablesAddress, 'tni')
+        except tables.ChordTablesException:
             return 'N/A'
 
     @property
@@ -4368,8 +4490,8 @@ class Chord(note.NotRest):
         False
         '''
         try:
-            post = chordTables.addressToZAddress(self.chordTablesAddress)
-        except chordTables.ChordTablesException:
+            post = tables.addressToZAddress(self.chordTablesAddress)
+        except tables.ChordTablesException:
             return False  # empty chords have no z-relations
 
         # environLocal.printDebug(['got post', post])
@@ -4400,8 +4522,8 @@ class Chord(note.NotRest):
         [0, 0, 0, 0, 0, 0]
         '''
         try:
-            return list(chordTables.addressToIntervalVector(self.chordTablesAddress))
-        except chordTables.ChordTablesException:
+            return list(tables.addressToIntervalVector(self.chordTablesAddress))
+        except tables.ChordTablesException:
             return [0, 0, 0, 0, 0, 0]
 
     @property
@@ -4580,8 +4702,8 @@ class Chord(note.NotRest):
         '''
         cta = self.chordTablesAddress
         try:
-            transposedNormalForm = chordTables.addressToTransposedNormalForm(cta)
-        except chordTables.ChordTablesException:
+            transposedNormalForm = tables.addressToTransposedNormalForm(cta)
+        except tables.ChordTablesException:
             return []
 
         orderedPCs = self.orderedPitchClasses
@@ -4893,8 +5015,8 @@ class Chord(note.NotRest):
         []
         '''
         try:
-            return list(chordTables.addressToPrimeForm(self.chordTablesAddress))
-        except chordTables.ChordTablesException:
+            return list(tables.addressToPrimeForm(self.chordTablesAddress))
+        except tables.ChordTablesException:
             return []
 
     @property
@@ -5337,7 +5459,7 @@ def fromForteClass(notation):
     else:
         raise ChordException(f'cannot handle specified notation: {notation}')
 
-    prime = chordTables.addressToTransposedNormalForm([card, num, inv])
+    prime = tables.addressToTransposedNormalForm([card, num, inv])
     return Chord(prime)
 
 
@@ -5364,13 +5486,13 @@ def fromIntervalVector(notation, getZRelation=False):
     addressList = None
     if common.isListLike(notation):
         if len(notation) == 6:  # assume its an interval vector
-            addressList = chordTables.intervalVectorToAddress(notation)
+            addressList = tables.intervalVectorToAddress(notation)
     if addressList is None:
         raise ChordException(f'cannot handle specified notation: {notation}')
 
     post = []
     for address in addressList:
-        post.append(Chord(chordTables.addressToTransposedNormalForm(address)))
+        post.append(Chord(tables.addressToTransposedNormalForm(address)))
     # for now, return the first chord
     # z-related chords will have more than one
     if len(post) == 1:
@@ -5754,6 +5876,14 @@ class Test(unittest.TestCase):
         self.assertEqual(unscrambledChord3.pitches[3].name, 'F')
         self.assertEqual(unscrambledChord3.pitches[4].name, 'A-')
 
+    def testEnharmonicSimplification(self):
+        from music21 import chord
+        eFlat = note.Note(63)
+        self.assertEqual(eFlat.pitch.name, 'E-')
+        bMajor = chord.Chord([59, 63, 66])
+        self.assertEqual([p.name for p in bMajor.pitches], ['B', 'D#', 'F#'])
+        self.assertEqual([p.spellingIsInferred for p in bMajor.pitches], [True, True, True])
+
     def testDurations(self):
         from music21 import chord
 
diff --git a/music21/chord/tables.py b/music21/chord/tables.py
index bff82cef8..ce5797b55 100644
--- a/music21/chord/tables.py
+++ b/music21/chord/tables.py
@@ -35,12 +35,12 @@ class ChordTablesException(exceptions21.Music21Exception):
 
 # ------------------------------------------------------------------------------
 # TNI structures are defined
-# 0=pitches, 1=ICV, 2=invariance vector (Robert Morris), 3 = Z-relation)
+# [0]=pitches, [1]=ICV, [2]=invariance vector (Robert Morris), [3] = Z-relation)
 # invariance vector can be used to determine symmetry.
-# the first four entries in the invariance vector shows how many invariance there
+# the first four entries in the invariance vector shows how many transpositions are invariant
 # are under Tn, TnI, TnM, TnMI respectively
 # the entries 5-8 show how many map to their complements under Tn, TnI, TnM, and TnMI respectively
-# at index 1, a value of 1 is symmetrical
+# at index 1, a value of 1 is symmetrical under inversion.
 t1   = ((0,), (0,0,0,0,0,0), (1,1,1,1,11,11,11,11), 0)  # 1-1
 monad = (None, t1)
 
@@ -2811,7 +2811,7 @@ def seekChordTablesAddress(c):
     Table addresses are TN based three character codes:
     cardinality, Forte index number, inversion
 
-    Inversion is either 0 (for symmetrical) or -1/1
+    Inversion is either 0 (for symmetrical under inversion) or -1, 1
 
     NOTE: time consuming, and only should be run when necessary.
 
diff --git a/music21/clef.py b/music21/clef.py
index 4ac9c967e..d53540d35 100644
--- a/music21/clef.py
+++ b/music21/clef.py
@@ -372,6 +372,18 @@ class TabClef(PitchClef):
         self.sign = 'TAB'
         self.line = 5
 
+    def getStemDirectionForPitches(
+        self,
+        pitchList: Union[pitch.Pitch, Iterable[pitch.Pitch]],
+        *,
+        firstLastOnly: bool = True,
+        extremePitchOnly: bool = False,
+    ) -> str:
+        '''
+        Overridden to simply return 'down' for guitar tabs.
+        '''
+        return 'down'
+
 # ------------------------------------------------------------------------------
 
 
@@ -1085,6 +1097,26 @@ class Test(unittest.TestCase):
         self.assertIs(n5.getContextByClass(Clef), tc1)
         self.assertIs(n6.getContextByClass(Clef), tc1)
 
+    def testTabClefBeamDirections(self):
+
+        from music21 import stream
+        from music21 import clef
+        from music21 import meter
+        from music21 import note
+
+        m = stream.Measure()
+
+        n1 = note.Note(64, quarterLength=0.25)
+        n2 = note.Note(67, quarterLength=0.25)
+
+        m.append(clef.TabClef())
+        m.append(meter.TimeSignature('4/4'))
+        m.append(n1)
+        m.append(n2)
+        m.makeBeams(inPlace=True)
+
+        self.assertEqual(m.notes[0].stemDirection, 'down')
+
 
 # ------------------------------------------------------------------------------
 # define presented order in documentation
@@ -1094,4 +1126,3 @@ _DOC_ORDER = [Clef, TrebleClef, BassClef]
 if __name__ == '__main__':
     import music21
     music21.mainTest(Test)
-
diff --git a/music21/common/classTools.py b/music21/common/classTools.py
index 89e0084e6..fe99e5c69 100644
--- a/music21/common/classTools.py
+++ b/music21/common/classTools.py
@@ -103,12 +103,11 @@ def isIterable(usrData: Any) -> bool:
     >>> common.isIterable(range(20))
     True
     '''
-    if hasattr(usrData, "__iter__"):
-        if isinstance(usrData, (str, bytes)):
-            return False
-        return True
-    else:
+    if isinstance(usrData, (str, bytes)):
         return False
+    if hasattr(usrData, '__iter__'):
+        return True
+    return False
 
 
 def classToClassStr(classObj: Type) -> str:
diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py
index 53e9dd2f3..3b6bf21f3 100644
--- a/music21/common/numberTools.py
+++ b/music21/common/numberTools.py
@@ -15,7 +15,10 @@ import unittest
 from typing import List, Tuple, Union, Sequence, Iterable
 
 from fractions import Fraction
+from math import isclose
+
 from music21 import defaults
+from music21.common import deprecated
 
 __all__ = [
     'ordinals', 'musicOrdinals', 'ordinalsToNumbers',
@@ -133,7 +136,7 @@ def numToIntOrFloat(value: Union[int, float]) -> Union[int, float]:
         value = float(value)
         intVal = round(value)
 
-    if almostEquals(intVal, value, 1e-6):
+    if isclose(intVal, value, abs_tol=1e-6):
         return intVal
     else:  # source
         return value
@@ -424,7 +427,7 @@ def roundToHalfInteger(num: Union[float, int]) -> Union[float, int]:
         floatVal = 1
     return intVal + floatVal
 
-
+@deprecated('v.7', 'any time', 'just call math.isclose(x, y, abs_tol=1e-7)')
 def almostEquals(x, y=0.0, grain=1e-7) -> bool:
     # noinspection PyShadowingNames
     '''
@@ -433,31 +436,17 @@ def almostEquals(x, y=0.0, grain=1e-7) -> bool:
 
     Allows comparisons between floats that are normally inconsistent.
 
-    >>> common.almostEquals(1.000000001, 1)
+    DEPRECATED in v.7 -- just call `isclose` with `abs_tol`:
+
+    >>> from math import isclose
+    >>> isclose(1.000000001, 1, abs_tol=1e-7)
     True
-    >>> common.almostEquals(1.001, 1)
+    >>> isclose(1.001, 1, abs_tol=1e-7)
     False
-    >>> common.almostEquals(1.001, 1, grain=0.1)
-    True
-
-    OMIT_FROM_DOCS
-
-    For very small grains, just compare Fractions without converting:
-
-    >>> from fractions import Fraction
-    >>> x = Fraction(1e-10)
-    >>> y = Fraction(1e-10)
-    >>> common.almostEquals(x, y)
+    >>> isclose(1.001, 1, abs_tol=0.1)
     True
     '''
-    # for very small grains, just compare Fractions without converting...
-    if isinstance(x, Fraction) and isinstance(y, Fraction) and grain <= 5e-6:
-        if x == y:
-            return True
-
-    if abs(x - y) < grain:
-        return True
-    return False
+    return isclose(x, y, abs_tol=grain)
 
 
 def addFloatPrecision(x, grain=1e-2) -> Union[float, 'fractions.Fraction']:
@@ -485,7 +474,7 @@ def addFloatPrecision(x, grain=1e-2) -> Union[float, 'fractions.Fraction']:
     values = (1 / 3, 2 / 3,
               1 / 6, 5 / 6)
     for v in values:
-        if almostEquals(x, v, grain=grain):
+        if isclose(x, v, abs_tol=grain):
             return opFrac(v)
     return x
 
@@ -551,13 +540,14 @@ def nearestMultiple(n: float, unit: float) -> Tuple[float, float, float]:
     >>> common.nearestMultiple(0.001, 0.125)[0]
     0.0
 
-    >>> common.almostEquals(common.nearestMultiple(0.25, 1 / 3)[0], 0.33333333)
+    >>> from math import isclose
+    >>> isclose(common.nearestMultiple(0.25, 1 / 3)[0], 0.33333333, abs_tol=1e-7)
     True
-    >>> common.almostEquals(common.nearestMultiple(0.55, 1 / 3)[0], 0.66666666)
+    >>> isclose(common.nearestMultiple(0.55, 1 / 3)[0], 0.66666666, abs_tol=1e-7)
     True
-    >>> common.almostEquals(common.nearestMultiple(234.69, 1 / 3)[0], 234.6666666)
+    >>> isclose(common.nearestMultiple(234.69, 1 / 3)[0], 234.6666666, abs_tol=1e-7)
     True
-    >>> common.almostEquals(common.nearestMultiple(18.123, 1 / 6)[0], 18.16666666)
+    >>> isclose(common.nearestMultiple(18.123, 1 / 6)[0], 18.16666666, abs_tol=1e-7)
     True
 
     >>> common.nearestMultiple(-0.5, 0.125)
@@ -643,7 +633,7 @@ def decimalToTuplet(decNum: float) -> Tuple[int, int]:
         'Utility function.'
         for index in range(1, 1000):
             for j in range(index, index * 2):
-                if almostEquals(inner_working, j / index):
+                if isclose(inner_working, j / index, abs_tol=1e-7):
                     return (int(j), int(index))
         return (0, 0)
 
@@ -805,7 +795,7 @@ def approximateGCD(values: List[Union[int, float]], grain: float = 1e-4) -> floa
         # lowest is already a float
         unused_int, floatingValue = divmod(x / lowest, 1.0)
         # if almost an even division
-        if almostEquals(floatingValue, 0.0, grain=grain):
+        if isclose(floatingValue, 0.0, abs_tol=grain):
             count += 1
     if count == len(values):
         return lowest
@@ -829,7 +819,7 @@ def approximateGCD(values: List[Union[int, float]], grain: float = 1e-4) -> floa
         for coll in divisions:
             for x in coll:
                 # grain here is set low, mostly to catch triplets
-                if almostEquals(x, v, grain=grain):
+                if isclose(x, v, abs_tol=grain):
                     count += 1
                     break  # exit the iteration of coll; only 1 match possible
         # store any division that is found in all values
diff --git a/music21/common/stringTools.py b/music21/common/stringTools.py
index d3a659dab..0b64e91c4 100644
--- a/music21/common/stringTools.py
+++ b/music21/common/stringTools.py
@@ -87,33 +87,34 @@ def getNumFromStr(usrStr: str, numbers: str = '0123456789') -> Tuple[str, str]:
 
 def hyphenToCamelCase(usrStr: str, replacement: str = '-') -> str:
     '''
-    given a hyphen-connected-string, change it to
+    Given a hyphen-connected-string, change it to
     a camelCaseConnectedString.
 
     The replacement can be specified to be something besides a hyphen.
 
-    This code is from:
-
-    http://stackoverflow.com/questions/4303492/
-    how-can-i-simplify-this-conversion-from-underscore-to-camelcase-in-python
-
     >>> common.hyphenToCamelCase('movement-name')
     'movementName'
 
     >>> common.hyphenToCamelCase('movement_name', replacement='_')
     'movementName'
+
+    Safe to call on a string lacking the replacement character:
+
+    >>> common.hyphenToCamelCase('voice')
+    'voice'
+
+    And on "words" beginning with numbers:
+
+    >>> common.hyphenToCamelCase('music-21')
+    'music21'
     '''
-    PATTERN = re.compile(r'''
-    (?<!\A)  # not at the start of the string
-    ''' + replacement + r'''
-    (?=[a-zA-Z])  # followed by a letter
-    ''', re.VERBOSE)  # @UndefinedVariable
-
-    tokens = PATTERN.split(usrStr)
-    response = tokens.pop(0).lower()
-    for remain in tokens:
-        response += remain.capitalize()
-    return response
+    post = ''
+    for i, word in enumerate(usrStr.split(replacement)):
+        if i == 0:
+            post = word
+        else:
+            post += word.capitalize()
+    return post
 
 
 def camelCaseToHyphen(usrStr: str, replacement: str = '-') -> str:
diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py
index a934c1f62..1df7dab69 100644
--- a/music21/converter/__init__.py
+++ b/music21/converter/__init__.py
@@ -47,6 +47,7 @@ import unittest
 import urllib
 import zipfile
 
+from math import isclose
 from typing import Union, Tuple
 
 __all__ = [
@@ -1098,19 +1099,6 @@ def parse(value: Union[bundles.MetadataEntry, bytes, str, pathlib.Path],
     >>> s = converter.parse("2/16 E4 r f# g=lastG trip{b-8 a g} c", format='tinyNotation').flat
     >>> s.getElementsByClass(meter.TimeSignature).first()
     <music21.meter.TimeSignature 2/16>
-
-    .. tip::
-
-        Unlike musicxml, MIDI files are unmeasured. Call :meth:`~music21.stream.Stream.makeNotation`
-        on the parsed result if you wish measures to be created before further manipulation.
-
-        Save the result of `makeNotation`, or use `inPlace=True`::
-
-            unmeasuredStream = converter.parse('/Users/you/Desktop/source.mid')
-            measuredStream = unmeasuredStream.makeNotation()
-
-        This is particularly important when writing formats that do not run `makeNotation` as
-        a convenience during the export (musicxml does; lilypond doesn't.)
     '''
     # environLocal.printDebug(['attempting to parse()', value])
     if 'forceSource' in keywords:
@@ -1692,10 +1680,11 @@ class Test(unittest.TestCase):
         self.assertEqual(len(s.flat.getElementsByClass(note.Note)), 2)
         self.assertEqual(len(s.flat.getElementsByClass(chord.Chord)), 4)
 
-        self.assertEqual(len(s.flat.getElementsByClass(meter.TimeSignature)), 0)
+        # MIDI import makes measures, so we will have one 4/4 time sig
+        self.assertEqual(len(s.flat.getElementsByClass(meter.TimeSignature)), 1)
         self.assertEqual(len(s.flat.getElementsByClass(key.KeySignature)), 0)
 
-        # this sample has eight note triplets
+        # this sample has eighth note triplets
         fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test06.mid'
         s = parseFile(fp)
         # s.show()
@@ -1881,12 +1870,11 @@ class Test(unittest.TestCase):
         Checks quantization when parsing a stream. Here everything snaps to the 8th note.
         '''
         from music21 import omr
-        from music21.common import numberTools
         midiFp = omr.correctors.pathName + os.sep + 'k525short.mid'
         midiStream = parse(midiFp, forceSource=True, storePickle=False, quarterLengthDivisors=[2])
         # midiStream.show()
         for n in midiStream.recurse(classFilter='Note'):
-            self.assertTrue(numberTools.almostEquals(n.quarterLength % 0.5, 0.0))
+            self.assertTrue(isclose(n.quarterLength % 0.5, 0.0, abs_tol=1e-7))
 
     def testParseMidiNoQuantize(self):
         '''
diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py
index ecc445d38..9335a7d8a 100644
--- a/music21/converter/subConverters.py
+++ b/music21/converter/subConverters.py
@@ -643,7 +643,8 @@ class ConverterHumdrum(SubConverter):
 
     def parseData(self, humdrumString, number=None):
         '''
-        Open Humdrum data from a string -- calls humdrum.parseData()
+        Open Humdrum data from a string -- calls
+        :meth:`~music21.humdrum.spineParser.HumdrumDataCollection.parse()`.
 
         >>> humData = ('**kern\\n*M2/4\\n=1\\n24r\\n24g#\\n24f#\\n24e\\n24c#\\n' +
         ...     '24f\\n24r\\n24dn\\n24e-\\n24gn\\n24e-\\n24dn\\n*-')
diff --git a/music21/duration.py b/music21/duration.py
index d63f254ac..8c6318016 100644
--- a/music21/duration.py
+++ b/music21/duration.py
@@ -1476,7 +1476,12 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
     half tied to quintuplet sixteenth note" or simply "quarter note."
 
     A Duration object is made of one or more immutable DurationTuple objects stored on the
-    `components` list.
+    `components` list. A Duration created by setting `quarterLength` sets the attribute
+    `expressionIsInferred` to True, which indicates that consuming functions or applications
+    can express this Duration using another combination of components that sums to the
+    `quarterLength`. Otherwise, `expressionIsInferred` is set to False, indicating that
+    components are not allowed to mutate.
+    (N.B.: `music21` does not yet implement such mutating components.)
 
     Multiple DurationTuples in a single Duration may be used to express tied
     notes, or may be used to split duration across barlines or beam groups.
@@ -1513,11 +1518,16 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
     (DurationTuple(type='eighth', dots=0, quarterLength=0.5),
      DurationTuple(type='32nd', dots=0, quarterLength=0.125))
 
+    >>> d2.expressionIsInferred
+    True
+
     Example 3: A Duration configured by keywords.
 
     >>> d3 = duration.Duration(type='half', dots=2)
     >>> d3.quarterLength
     3.5
+    >>> d3.expressionIsInferred
+    False
     '''
 
     # CLASS VARIABLES #
@@ -1534,7 +1544,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         '_typeNeedsUpdating',
         '_unlinkedType',
         '_dotGroups',
-
+        'expressionIsInferred',
         '_client'
     )
 
@@ -1562,6 +1572,8 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         # defer updating until necessary
         self._quarterLengthNeedsUpdating = False
         self._linked = True
+
+        self.expressionIsInferred = False
         for a in arguments:
             if common.isNum(a) and 'quarterLength' not in keywords:
                 keywords['quarterLength'] = a
@@ -1590,6 +1602,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         # permit as keyword so can be passed from notes
         elif 'quarterLength' in keywords:
             self.quarterLength = keywords['quarterLength']
+            self.expressionIsInferred = True
 
         if 'client' in keywords:
             self.client = keywords['client']
@@ -1715,19 +1728,11 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         '''
         # this update will not be necessary
         self._quarterLengthNeedsUpdating = False
-        if self.linked:
-            try:
-                qlc = quarterConversion(self._qtrLength)
-                self.components = list(qlc.components)
-                if qlc.tuplet is not None:
-                    self.tuplets = (qlc.tuplet,)
-            except DurationException:
-                environLocal.printDebug([
-                    'problem updating components of note with quarterLength ',
-                    self.quarterLength,
-                    'chokes quarterLengthToDurations'
-                ])
-                raise
+        if self.linked and self.expressionIsInferred:
+            qlc = quarterConversion(self._qtrLength)
+            self.components = list(qlc.components)
+            if qlc.tuplet is not None:
+                self.tuplets = (qlc.tuplet,)
         self._componentsNeedUpdating = False
 
     # PUBLIC METHODS #
@@ -1786,10 +1791,10 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
 
         if isinstance(dur, DurationTuple):
             self._components.append(dur)
-        elif isinstance(dur, Duration):  # its a Duration object
+        elif isinstance(dur, Duration):  # it's a Duration object
             for c in dur.components:
                 self._components.append(c)
-        else:  # its a number that may produce more than one component
+        else:  # it's a number that may produce more than one component
             for c in Duration(dur).components:
                 self._components.append(c)
 
@@ -2784,6 +2789,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
             if value == 0.0 and self.linked is True:
                 self.clear()
             self._qtrLength = value
+            self.expressionIsInferred = True
             self._componentsNeedUpdating = True
             self._quarterLengthNeedsUpdating = False
 
diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py
index c48b4962c..cdd885220 100644
--- a/music21/features/jSymbolic.py
+++ b/music21/features/jSymbolic.py
@@ -19,8 +19,8 @@ import math
 import statistics
 import unittest
 from collections import OrderedDict
+from math import isclose
 
-from music21 import common
 from music21 import base
 from music21 import exceptions21
 from music21.features import base as featuresModule
@@ -2332,7 +2332,7 @@ class AverageTimeBetweenAttacksFeature(featuresModule.FeatureExtractor):
             oNext = onsets[i + 1]
             # Don't include simultaneous attacks
             dif = oNext - o
-            if not common.almostEquals(dif, 0.0):
+            if not isclose(dif, 0.0, abs_tol=1e-7):
                 differences.append(dif)
         self.feature.vector[0] = sum(differences) / len(differences)
 
@@ -2373,7 +2373,7 @@ class VariabilityOfTimeBetweenAttacksFeature(featuresModule.FeatureExtractor):
             oNext = onsets[i + 1]
             # Don't include simultaneous attacks
             dif = oNext - o
-            if not common.almostEquals(dif, 0.0):
+            if not isclose(dif, 0.0, abs_tol=1e-7):
                 differences.append(dif)
         self.feature.vector[0] = statistics.pstdev(differences)
 
@@ -2426,7 +2426,7 @@ class AverageTimeBetweenAttacksForEachVoiceFeature(
                 oNext = onsets[i + 1]
                 # Don't include simultaneous attacks
                 dif = oNext - o
-                if not common.almostEquals(dif, 0.0):
+                if not isclose(dif, 0.0, abs_tol=1e-7):
                     differences.append(dif)
             if not differences:
                 raise JSymbolicFeatureException('at least one part lacks notes')
@@ -2486,7 +2486,7 @@ class AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature(
                 oNext = onsets[i + 1]
                 dif = oNext - o
                 # Don't include simultaneous attacks
-                if not common.almostEquals(dif, 0.0):
+                if not isclose(dif, 0.0, abs_tol=1e-7):
                     differences.append(dif)
             if not differences:
                 raise JSymbolicFeatureException('at least one part lacks notes')
diff --git a/music21/figuredBass/checker.py b/music21/figuredBass/checker.py
index 69bf27aa5..96182713d 100644
--- a/music21/figuredBass/checker.py
+++ b/music21/figuredBass/checker.py
@@ -321,9 +321,9 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color="#FF0000
             for partNumber in partNumberTuple:
                 if color is not None:
                     noteA = allParts[partNumber - 1].iter.getElementsByOffset(
-                        initOffsetA, initOffsetA, mustBeginInSpan=False)[0]
+                        initOffsetA, initOffsetA, mustBeginInSpan=False).first()
                     noteB = allParts[partNumber - 1].iter.getElementsByOffset(
-                        initOffsetB, initOffsetB, mustBeginInSpan=False)[0]
+                        initOffsetB, initOffsetB, mustBeginInSpan=False).first()
                     noteA.style.color = color
                     noteB.style.color = color
             if debug is True:
diff --git a/music21/freezeThaw.py b/music21/freezeThaw.py
index 178774151..233ce187a 100644
--- a/music21/freezeThaw.py
+++ b/music21/freezeThaw.py
@@ -1229,7 +1229,7 @@ class Test(unittest.TestCase):
         c = converter.parse(a)
         f = converter.freezeStr(c)
         d = converter.thawStr(f)
-        self.assertEqual(d[1][20].volume._client.__class__.__name__, 'weakref')
+        self.assertEqual(d.parts[1].flat.notes[20].volume._client.__class__.__name__, 'weakref')
 
 
 # -----------------------------------------------------------------------------
diff --git a/music21/instrument.py b/music21/instrument.py
index 779935250..bdd2819d9 100644
--- a/music21/instrument.py
+++ b/music21/instrument.py
@@ -1768,12 +1768,12 @@ def deduplicate(s: Stream, inPlace: bool = False) -> Stream:
     >>> s1 = stream.Stream()
     >>> s1.insert(4, i1)
     >>> s1.insert(4, i2)
-    >>> s1.getInstruments().elements
-    (<music21.instrument.Instrument 'Semi-Hollow Body'>,...
-    <music21.instrument.Instrument 'Electric Guitar: '>)
+    >>> list(s1.getInstruments())
+    [<music21.instrument.Instrument 'Semi-Hollow Body'>,
+        <music21.instrument.Instrument 'Electric Guitar: '>]
     >>> post = instrument.deduplicate(s1)
-    >>> post.getInstruments().elements
-    (<music21.instrument.Instrument 'Electric Guitar: Semi-Hollow Body'>,)
+    >>> list(post.getInstruments())
+    [<music21.instrument.Instrument 'Electric Guitar: Semi-Hollow Body'>]
 
     One `Instrument` instance and one subclass instance, with `inPlace` and parts:
 
@@ -1788,15 +1788,15 @@ def deduplicate(s: Stream, inPlace: bool = False) -> Stream:
     >>> p2.append([instrument.Flute(), instrument.Flute()])
     >>> s2.insert(0, p1)
     >>> s2.insert(0, p2)
-    >>> p1.getInstruments().elements
-    (<music21.instrument.Instrument 'Piccolo: '>, <music21.instrument.Piccolo 'Piccolo'>)
-    >>> p2.getInstruments().elements
-    (<music21.instrument.Flute 'Flute'>, <music21.instrument.Flute 'Flute'>)
+    >>> list(p1.getInstruments())
+    [<music21.instrument.Instrument 'Piccolo: '>, <music21.instrument.Piccolo 'Piccolo'>]
+    >>> list(p2.getInstruments())
+    [<music21.instrument.Flute 'Flute'>, <music21.instrument.Flute 'Flute'>]
     >>> s2 = instrument.deduplicate(s2, inPlace=True)
-    >>> p1.getInstruments().elements
-    (<music21.instrument.Piccolo 'Piccolo: Piccolo'>,)
-    >>> p2.getInstruments().elements
-    (<music21.instrument.Flute 'Flute'>,)
+    >>> list(p1.getInstruments())
+    [<music21.instrument.Piccolo 'Piccolo: Piccolo'>]
+    >>> list(p2.getInstruments())
+    [<music21.instrument.Flute 'Flute'>]
     '''
     if inPlace:
         returnObj = s
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 36b75327c..e3857857b 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -1686,12 +1686,34 @@ def getMetaEvents(events):
 
     return metaEvents
 
+def insertConductorEvents(conductorPart: stream.Part,
+                          target: stream.Part,
+                          *,
+                          isFirst: bool = False,
+                          ):
+    '''
+    Insert a deepcopy of any TimeSignature, KeySignature, or MetronomeMark
+    found in the `conductorPart` into the `target` Part at the same offset.
+
+    Obligatory to do this before making measures. New in v7.
+    '''
+    for e in conductorPart.getElementsByClass(
+            ('TimeSignature', 'KeySignature', 'MetronomeMark')):
+        # create a deepcopy of the element so a flat does not cause
+        # multiple references of the same
+        eventCopy = copy.deepcopy(e)
+        if 'TempoIndication' in eventCopy.classes and not isFirst:
+            eventCopy.style.hideObjectOnPrint = True
+            eventCopy.numberImplicit = True
+        target.insert(conductorPart.elementOffset(e), eventCopy)
 
 def midiTrackToStream(
     mt,
     ticksPerQuarter=None,
     quantizePost=True,
     inputM21=None,
+    conductorPart: Optional[stream.Part] = None,
+    isFirst: bool = False,
     **keywords
 ) -> stream.Part:
     # noinspection PyShadowingNames
@@ -1719,24 +1741,37 @@ def midiTrackToStream(
     >>> p = midi.translate.midiTrackToStream(mt)
     >>> p
     <music21.stream.Part ...>
-    >>> len(p.notesAndRests)
-    11
-    >>> p.notes[0].pitch.midi
+    >>> len(p.flat.notesAndRests)
+    13
+    >>> p.flat.notes[0].pitch.midi
     36
-    >>> p.notes[0].volume.velocity
+    >>> p.flat.notes[0].volume.velocity
     90
 
-    Note that the output Part has not yet had measures made, nor does it have a
-    TimeSignature yet.
+    Changed in v.7 -- Now makes measures
 
     >>> p.show('text')
-    {0.0} <music21.instrument.Instrument ''>
-    {0.0} <music21.note.Note C>
-    {1.0} <music21.note.Rest rest>
-    {2.0} <music21.chord.Chord F3 G#4 C5>
-    {3.0} <music21.note.Rest rest>
-    {4.5} <music21.note.Note B->
-    ...
+    {0.0} <music21.stream.Measure 1 offset=0.0>
+        {0.0} <music21.instrument.Instrument ''>
+        {0.0} <music21.clef.TrebleClef>
+        {0.0} <music21.meter.TimeSignature 4/4>
+        {0.0} <music21.note.Note C>
+        {1.0} <music21.note.Rest rest>
+        {2.0} <music21.chord.Chord F3 G#4 C5>
+        {3.0} <music21.note.Rest rest>
+    {4.0} <music21.stream.Measure 2 offset=4.0>
+        {0.0} <music21.note.Rest rest>
+        {0.5} <music21.note.Note B->
+        {1.5} <music21.note.Rest rest>
+        {3.5} <music21.chord.Chord D2 A4>
+    {8.0} <music21.stream.Measure 3 offset=8.0>
+        {0.0} <music21.note.Rest rest>
+        {0.5} <music21.chord.Chord C#2 B-3 G#6>
+        {1.0} <music21.note.Rest rest>
+        {2.5} <music21.chord.Chord F#3 A4 C#5>
+    {12.0} <music21.stream.Measure 4 offset=12.0>
+        {0.0} <music21.note.Rest rest>
+        {4.0} <music21.bar.Barline type=final>
     '''
     # environLocal.printDebug(['midiTrackToStream(): got midi track: events',
     # len(mt.events), 'ticksPerQuarter', ticksPerQuarter])
@@ -1873,12 +1908,21 @@ def midiTrackToStream(
                    inPlace=True,
                    recurse=False)  # shouldn't be any substreams yet
 
-    if voicesRequired:
-        # this procedure will make the appropriate rests
-        s.makeVoices(inPlace=True, fillGaps=True)
-    else:
+    if not notes:
+        # Conductor track doesn't need measures made
+        # It's an intermediate result only -- not provided to user
+        return s
+
+    if conductorPart is not None:
+        insertConductorEvents(conductorPart, s, isFirst=isFirst)
+
+    # Only make measures if time signatures have been inserted
+    s.makeMeasures(inPlace=True)
+    for m in s.getElementsByClass(stream.Measure):
+        if voicesRequired:
+            m.makeVoices(inPlace=True, fillGaps=True)
         # always need to fill gaps, as rests are not found in any other way
-        s.makeRests(inPlace=True, fillGaps=True)
+        m.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
     return s
 
 
@@ -2320,12 +2364,14 @@ def midiTracksToStreams(
     # conductorPart will store common elements such as time sig, key sig
     # from the conductor track (or any track without notes).
     conductorPart = stream.Part()
-
+    firstTrackWithNotes = None
     for mt in midiTracks:
         # not all tracks have notes defined; only creates parts for those
         # that do
         # environLocal.printDebug(['raw midi tracks', mt])
         if mt.hasNotes():
+            if firstTrackWithNotes is None:
+                firstTrackWithNotes = mt
             streamPart = stream.Part()  # create a part instance for each part
             s.insert(0, streamPart)
         else:
@@ -2335,23 +2381,10 @@ def midiTracksToStreams(
                           ticksPerQuarter,
                           quantizePost,
                           inputM21=streamPart,
+                          conductorPart=conductorPart,
+                          isFirst=(mt is firstTrackWithNotes),
                           **keywords)
 
-    # environLocal.printDebug(['show() conductorTrack elements'])
-    # if we have time sig/key sig/tempo elements, add to each part
-
-    for e in conductorPart.getElementsByClass(
-            ('TimeSignature', 'KeySignature', 'MetronomeMark')):
-        for i, p in enumerate(s.getElementsByClass('Stream')):
-            # create a deepcopy of the element so a flat does not cause
-            # multiple references of the same
-            eventCopy = copy.deepcopy(e)
-            if 'TempoIndication' in eventCopy.classes and i != 0:
-                eventCopy.style.hideObjectOnPrint = True
-                eventCopy.numberImplicit = True
-
-            p.insert(conductorPart.elementOffset(e), eventCopy)
-
     return s
 
 
@@ -2525,14 +2558,19 @@ def midiStringToStream(strData, **keywords):
 
     N.B. -- this has been somewhat problematic, so use at your own risk.
 
-     >>> midiBinStr = (b'MThd\x00\x00\x00\x06\x00\x01\x00\x01\x04\x00'
-     ...               + b'MTrk\x00\x00\x00\x16\x00\xff\x03\x00\x00\xe0\x00@\x00'
-     ...               + b'\x90CZ\x88\x00\x80C\x00\x88\x00\xff/\x00')
-     >>> s = midi.translate.midiStringToStream(midiBinStr)
-     >>> s.show('text')
-     {0.0} <music21.stream.Part ...>
-         {0.0} <music21.note.Note G>
-
+    >>> midiBinStr = (b'MThd\x00\x00\x00\x06\x00\x01\x00\x01\x04\x00'
+    ...               + b'MTrk\x00\x00\x00\x16\x00\xff\x03\x00\x00\xe0\x00@\x00'
+    ...               + b'\x90CZ\x88\x00\x80C\x00\x88\x00\xff/\x00')
+    >>> s = midi.translate.midiStringToStream(midiBinStr)
+    >>> s.show('text')
+    {0.0} <music21.stream.Part 0x108aa94f0>
+        {0.0} <music21.stream.Measure 1 offset=0.0>
+            {0.0} <music21.instrument.Instrument ''>
+            {0.0} <music21.clef.TrebleClef>
+            {0.0} <music21.meter.TimeSignature 4/4>
+            {0.0} <music21.note.Note G>
+            {1.0} <music21.note.Rest rest>
+            {4.0} <music21.bar.Barline type=final>
     '''
     from music21 import midi as midiModule
 
@@ -2576,7 +2614,7 @@ def midiFileToStream(
     >>> s
     <music21.stream.Score ...>
     >>> len(s.flat.notesAndRests)
-    11
+    13
     '''
     # environLocal.printDebug(['got midi file: tracks:', len(mf.tracks)])
     if inputM21 is None:
@@ -3274,18 +3312,28 @@ class Test(unittest.TestCase):
         self.assertEqual(len(s.parts), 3)
         # metronome marks propagate to every staff, but are hidden on subsequent staffs
         self.assertEqual(
-            [mm.numberImplicit for mm in s.parts[0].getElementsByClass('MetronomeMark')],
+            [mm.numberImplicit for mm in s.parts[0].recurse().getElementsByClass('MetronomeMark')],
             [False, False, False, False]
         )
         self.assertEqual(
-            [mm.numberImplicit for mm in s.parts[1].getElementsByClass('MetronomeMark')],
+            [mm.numberImplicit for mm in s.parts[1].recurse().getElementsByClass('MetronomeMark')],
             [True, True, True, True]
         )
         self.assertEqual(
-            [mm.numberImplicit for mm in s.parts[2].getElementsByClass('MetronomeMark')],
+            [mm.numberImplicit for mm in s.parts[2].recurse().getElementsByClass('MetronomeMark')],
             [True, True, True, True]
         )
 
+    def testMidiImportMeter(self):
+        from music21 import converter
+        fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test17.mid'
+        s = converter.parse(fp)
+        for p in s.parts:
+            m = p.getElementsByClass('Measure').first()
+            ts = m.timeSignature
+            self.assertEqual(ts.ratioString, '3/4')
+            self.assertIn(ts, m)
+
     def testMidiExportConductorA(self):
         '''Export conductor data to MIDI conductor track.'''
         from music21 import meter, tempo
@@ -3475,7 +3523,7 @@ class Test(unittest.TestCase):
         s = converter.parse(fp)
         # three chords will be created, as well as two voices
         self.assertEqual(len(s.flat.getElementsByClass('Chord')), 3)
-        self.assertEqual(len(s.parts[0].voices), 2)
+        self.assertEqual(len(s.parts.first().measure(3).voices), 2)
 
     def testImportChordsA(self):
         from music21 import converter
@@ -3552,9 +3600,9 @@ class Test(unittest.TestCase):
         s = converter.parse(testPrimitive.transposing01)
         mf = streamToMidiFile(s)
         out = midiFileToStream(mf)
-        instruments = out.parts[0].getElementsByClass('Instrument')
-        self.assertIsInstance(instruments[0], instrument.Oboe)
-        self.assertEqual(instruments[0].quarterLength, 0)
+        first_instrument = out.parts.first().measure(1).getElementsByClass('Instrument').first()
+        self.assertIsInstance(first_instrument, instrument.Oboe)
+        self.assertEqual(first_instrument.quarterLength, 0)
 
         # Unrecognized instrument 'a'
         dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive'
@@ -3573,8 +3621,8 @@ class Test(unittest.TestCase):
         dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive'
         fp = dirLib / 'test16.mid'
         s = converter.parse(fp)
-        self.assertEqual(len(s.parts[0].voices), 2)
-        els = s.parts[0].flat.getElementsByOffset(0.5)
+        self.assertEqual(len(s.parts.first().measure(1).voices), 2)
+        els = s.parts.first().flat.getElementsByOffset(0.5)
         self.assertSequenceEqual([e.duration.quarterLength for e in els], [0, 1])
 
     def testRepeatsExpanded(self):
@@ -3633,8 +3681,9 @@ class Test(unittest.TestCase):
 
         fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test17.mid'
         inn = converter.parse(fp)
-        numRests = len(inn.parts[1].voices[0].getElementsByClass('Rest'))
-        self.assertEqual(numRests, 2)
+
+        self.assertEqual(
+            len(inn.parts[1].measure(3).voices.last().getElementsByClass('Rest')), 1)
 
 
 # ------------------------------------------------------------------------------
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index f2de9c7b6..04ee237aa 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -762,12 +762,12 @@ class XMLExporterBase:
             stObj = m21Object.style
 
         if not common.isIterable(musicXMLNames):
-            musicXMLNames = [musicXMLNames]
+            musicXMLNames = (musicXMLNames,)
 
         if m21Names is None:
-            m21Names = [common.hyphenToCamelCase(x) for x in musicXMLNames]
+            m21Names = (common.hyphenToCamelCase(x) for x in musicXMLNames)
         elif not common.isIterable(m21Names):
-            m21Names = [m21Names]
+            m21Names = (m21Names,)
 
         for xmlName, m21Name in zip(musicXMLNames, m21Names):
             try:
@@ -1545,7 +1545,7 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
         called by .parse() if the score has individual parts.
 
         Calls makeRests() for the part, then creates a PartExporter for each part,
-        and runs .parse() on that part.  appends the PartExporter to self.partExporterList()
+        and runs .parse() on that part.  appends the PartExporter to self.partExporterList
         '''
         # would like to do something like this but cannot
         # replace object inside of the stream
@@ -1873,8 +1873,6 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
         </appearance>
         '''
         st = self.stream.style
-        if not hasattr(st, 'lineWidths'):
-            return  # TODO: remove in v.5 release after all old data is gone.
 
         if (not st.lineWidths
                 and not st.noteSizes
@@ -2834,8 +2832,13 @@ class MeasureExporter(XMLExporterBase):
         if len(obj.duration.dotGroups) > 1:
             obj.duration.splitDotGroups(inPlace=True)
 
-        # split at durations...
-        if 'GeneralNote' in classes and obj.duration.type == 'complex':
+        # split at durations, if not a full-measure rest (e.g. whole rest in 9/8)
+        if 'GeneralNote' in classes and obj.duration.type == 'complex' and not (
+            'Rest' in classes and (
+                obj.fullMeasure in (True, 'always')
+                or (obj.fullMeasure == 'auto' and obj.duration == self.stream.barDuration)
+            )
+        ):
             objList = obj.splitAtDurations()
         else:
             objList = [obj]
@@ -3182,6 +3185,7 @@ class MeasureExporter(XMLExporterBase):
         >>> n = note.Note('D#5')
         >>> n.quarterLength = 3
         >>> n.volume.velocityScalar = 0.5
+        >>> n.style.color = 'silver'
 
         >>> MEX = musicxml.m21ToXml.MeasureExporter()
         >>> len(MEX.xmlRoot)
@@ -3190,7 +3194,7 @@ class MeasureExporter(XMLExporterBase):
         >>> mxNote
         <Element 'note' at 0x10113cb38>
         >>> MEX.dump(mxNote)
-        <note dynamics="70.56">
+        <note color="#C0C0C0" dynamics="70.56">
           <pitch>
             <step>D</step>
             <alter>1</alter>
@@ -3200,6 +3204,7 @@ class MeasureExporter(XMLExporterBase):
           <type>half</type>
           <dot />
           <accidental>sharp</accidental>
+          <notehead color="#C0C0C0" parentheses="no">normal</notehead>
         </note>
         >>> len(MEX.xmlRoot)
         1
@@ -3239,9 +3244,9 @@ class MeasureExporter(XMLExporterBase):
         >>> n.articulations.append(articulations.Pizzicato())
         >>> mxNote = MEX.noteToXml(n)
         >>> MEX.dump(mxNote)
-        <note dynamics="70.56" pizzicato="yes">
+        <note color="#C0C0C0" dynamics="70.56" pizzicato="yes">
           ...
-          <notehead parentheses="no">diamond</notehead>
+          <notehead color="#C0C0C0" parentheses="no">diamond</notehead>
         </note>
 
         Notes with complex durations need to be simplified before coming here
@@ -3266,15 +3271,17 @@ class MeasureExporter(XMLExporterBase):
         addChordTag = (noteIndexInChord != 0)
         setb = _setAttributeFromAttribute
 
+        mxNote = Element('note')
         chordOrN: note.GeneralNote
         if chordParent is None:
             chordOrN = n
         else:
             chordOrN = chordParent
+            # Ensure color is read from `n`, since only `chordOrN` is handled below
+            self.setColor(mxNote, n)
 
-        mxNote = Element('note')
         # self.setFont(mxNote, chordOrN)
-        self.setPrintStyle(mxNote, chordOrN)
+        self.setPrintStyle(mxNote, chordOrN)  # sets color
         # TODO: attr-group: printout -- replaces print-object, print-spacing below (3.1)
         # TODO: attr: print-leger -- musicxml 3.1
         if (chordOrN.isRest is False
@@ -3287,7 +3294,6 @@ class MeasureExporter(XMLExporterBase):
         # TODO: attr: attack
         # TODO: attr: release
         # TODO: attr: time-only
-        self.setColor(mxNote, n)  # TODO(msc): is this redundant with setColor below?
         _synchronizeIds(mxNote, n)
 
         d = chordOrN.duration
@@ -3315,7 +3321,6 @@ class MeasureExporter(XMLExporterBase):
                 environLocal.warn(f'Duration set as Grace while not being a GraceDuration {d}')
 
         # TODO: cue... / cue-grace
-        self.setColor(mxNote, chordOrN)
 
         self.setPrintObject(mxNote, n)
         if n.hasStyleInformation and n.style.hideObjectOnPrint is True:
@@ -3658,11 +3663,12 @@ class MeasureExporter(XMLExporterBase):
         >>> g = pitch.Pitch('g3')
         >>> h = note.Note('b4')
         >>> h.notehead = 'diamond'
+        >>> h.style.color = 'gold'
         >>> ch2 = chord.Chord([g, h])
         >>> ch2.quarterLength = 2.0
         >>> mxNoteList = MEX.chordToXml(ch2)
         >>> MEX.dump(mxNoteList[1])
-        <note>
+        <note color="#FFD700">
           <chord />
           <pitch>
             <step>B</step>
@@ -3670,7 +3676,7 @@ class MeasureExporter(XMLExporterBase):
           </pitch>
           <duration>20160</duration>
           <type>half</type>
-          <notehead parentheses="no">diamond</notehead>
+          <notehead color="#FFD700" parentheses="no">diamond</notehead>
         </note>
 
         Test articulations of chords with fingerings. Superfluous fingerings will be ignored.
@@ -6147,7 +6153,7 @@ class MeasureExporter(XMLExporterBase):
         if not instSubStream:
             return None
 
-        instSubObj = instSubStream[0]
+        instSubObj = instSubStream.first()
         if instSubObj.transposition is None:
             return None
         self.transpositionInterval = instSubObj.transposition
@@ -6269,8 +6275,9 @@ class Test(unittest.TestCase):
         n = note.Note()
         v1 = stream.Voice([n])
         m = stream.Measure([v1])
+        # Unnecessary voice is removed by makeNotation
         xmlOut = self.getXml(m)
-        self.assertIn('<voice>1</voice>', xmlOut)
+        self.assertNotIn('<voice>1</voice>', xmlOut)
         n2 = note.Note()
         v2 = stream.Voice([n2])
         m.insert(0, v2)
@@ -6446,6 +6453,18 @@ class Test(unittest.TestCase):
         for direction in tree.findall('.//direction'):
             self.assertIsNone(direction.find('offset'))
 
+    def testFullMeasureRest(self):
+        from music21 import converter
+        s = converter.parse('tinynotation: 9/8 r1')
+        r = s.flat.notesAndRests.first()
+        r.quarterLength = 4.5
+        self.assertEqual(r.fullMeasure, 'auto')
+        tree = self.getET(s)
+        # Previously, this 4.5QL rest with a duration.type 'complex'
+        # was split on export into 4.0QL and 0.5QL
+        self.assertEqual(len(tree.findall('.//rest')), 1)
+        rest = tree.find('.//rest')
+        self.assertEqual(rest.get('measure'), 'yes')
 
 
 class TestExternal(unittest.TestCase):  # pragma: no cover
diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py
index c19e57ed7..45b85a1f5 100644
--- a/music21/musicxml/partStaffExporter.py
+++ b/music21/musicxml/partStaffExporter.py
@@ -11,7 +11,7 @@
 # ------------------------------------------------------------------------------
 '''
 A mixin to ScoreExporter that includes the capabilities for producing a single
-MusicXML `<part>` from multiple music21 PartStaff objects.
+MusicXML `<part>` from multiple music21 `PartStaff` objects.
 '''
 from typing import Dict, List, Optional
 import unittest
@@ -28,7 +28,7 @@ from music21.musicxml.xmlObjects import MusicXMLExportException
 def addStaffTags(measure: Element, staffNumber: int, tagList: Optional[List[str]] = None):
     '''
     For a <measure> tag `measure`, add a <staff> grandchild to any instance of
-    a child tag of a type in `tagList`. Raise if a <staff> grandchild already exists.
+    a child tag of a type in `tagList`.
 
     >>> from xml.etree.ElementTree import fromstring as El
     >>> from music21.musicxml.partStaffExporter import addStaffTags
@@ -140,8 +140,9 @@ class PartStaffExporterMixin:
     def joinableGroups(self) -> List[StaffGroup]:
         '''
         Returns a list of :class:`~music21.layout.StaffGroup` objects that
-        represent PartStaff objects that can be joined together into a single
-        MusicXML `<part>`:
+        represent :class:`~music21.stream.PartStaff` objects that can be
+        joined into a single MusicXML `<part>`, so long as there exists a
+        `PartExporter` for it in `ScoreExporter.partExporterList`:
 
         >>> s = stream.Score()
 
@@ -207,6 +208,8 @@ class PartStaffExporterMixin:
         ...     s.insert(0, el)
 
         >>> SX = musicxml.m21ToXml.ScoreExporter(s)
+        >>> SX.scorePreliminaries()
+        >>> SX.parsePartlikeScore()  # populate .partExporterList
         >>> SX.joinableGroups()
         [<music21.layout.StaffGroup <... p1a><... p1b><... p1c>>,
          <music21.layout.StaffGroup <... p2a><... p2b>>,
@@ -223,7 +226,10 @@ class PartStaffExporterMixin:
                 continue
             if not all(p.getElementsByClass('Measure') for p in sg):
                 continue
-            if not all(p in self.stream for p in sg):
+            try:
+                for p in sg:
+                    self.getRootForPartStaff(p)
+            except MusicXMLExportException:
                 continue
             joinableGroups.append(sg)
 
@@ -243,7 +249,7 @@ class PartStaffExporterMixin:
         Create child <staff> tags under each <note>, <direction>, and <forward> element
         in the <part>s being joined.
 
-        Called by :meth:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin.joinPartStaffs`
+        Called by :meth:`joinPartStaffs`.
 
         >>> from music21.musicxml import testPrimitive
         >>> s = converter.parse(testPrimitive.pianoStaff43a)
@@ -312,9 +318,10 @@ class PartStaffExporterMixin:
         For every <part> after the first, find the corresponding measure in the initial
         <part> and merge the contents by inserting all of the contained elements.
 
-        Called by :meth:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin.joinPartStaffs`
+        Called by :meth:`joinPartStaffs`
 
-        StaffGroup must be a valid one from `joinableGroups()`
+        StaffGroup must be a valid one from
+        :meth:`joinableGroups`.
         '''
 
         target = self.getRootForPartStaff(group[0])
@@ -337,8 +344,10 @@ class PartStaffExporterMixin:
         Move elements from subsequent PartStaff's measures into `target`: the <part>
         element representing the initial PartStaff that will soon represent the merged whole.
 
-        Called by movePartStaffMeasureContents(), which is in turn called by
-        :meth:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin.joinPartStaffs`
+        Called by
+        :meth:`movePartStaffMeasureContents`,
+        which is in turn called by
+        :meth:`joinPartStaffs`.
         '''
         DIVIDER_COMMENT = '========================= Measure [NNN] =========================='
         PLACEHOLDER = '[NNN]'
@@ -410,7 +419,7 @@ class PartStaffExporterMixin:
         e.g. RH of piano doesn't appear until m. 40, and earlier music for LH needs
         to be merged first in order to find earliest <attributes>.
 
-        Called by :meth:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin.joinPartStaffs`
+        Called by :meth:`joinPartStaffs`
 
         Multiple keys:
 
@@ -583,14 +592,14 @@ class PartStaffExporterMixin:
     def cleanUpSubsequentPartStaffs(self, group: StaffGroup):
         '''
         Now that the contents of all PartStaffs in `group` have been represented
-        by a single :class:`PartExporter`, remove the obsolete `PartExporter`s from
-        `self.partExporterList` so that they are not included in the export.
+        by a single :class:`~music21.musicxml.m21ToXml.PartExporter`, remove any
+        obsolete `PartExporter` from `self.partExporterList`.
 
         In addition, remove any obsolete `PartStaff` from the `StaffGroup`
         (in the deepcopied stream used for exporting) to ensure <part-group type="stop" />
         is written.
 
-        Called by :meth:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin.joinPartStaffs`
+        Called by :meth:`joinPartStaffs`
 
         >>> from music21.musicxml import testPrimitive
         >>> s = converter.parse(testPrimitive.pianoStaff43a)
@@ -917,10 +926,25 @@ class Test(unittest.TestCase):
 
         SX = musicxml.m21ToXml.ScoreExporter(sch.flat)
         SX.scorePreliminaries()
-        SX.parsePartlikeScore()
+        SX.parseFlatScore()
         # Previously, an exception was raised by getRootForPartStaff()
         SX.joinPartStaffs()
 
+    def testJoinPartStaffsG(self):
+        '''
+        A derived score should still have joinable groups.
+        '''
+        from music21 import corpus
+        from music21 import musicxml
+        s = corpus.parse('demos/two-parts')
+
+        m1 = s.measure(1)
+        self.assertIn('Score', m1.classes)
+        SX = musicxml.m21ToXml.ScoreExporter(m1)
+        SX.scorePreliminaries()
+        SX.parsePartlikeScore()
+        self.assertEqual(len(SX.joinableGroups()), 1)
+
 
 if __name__ == '__main__':
     import music21
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index fe31b8f07..781f39705 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -369,12 +369,12 @@ class XMLParserBase:
             stObj = None
 
         if not common.isIterable(musicXMLNames):
-            musicXMLNames = [musicXMLNames]
+            musicXMLNames = (musicXMLNames,)
 
         if m21Names is None:
-            m21Names = [common.hyphenToCamelCase(x) for x in musicXMLNames]
+            m21Names = (common.hyphenToCamelCase(x) for x in musicXMLNames)
         elif not common.isIterable(m21Names):
-            m21Names = [m21Names]
+            m21Names = (m21Names,)
 
         for xmlName, m21Name in zip(musicXMLNames, m21Names):
             mxValue = mxObject.get(xmlName)
@@ -2674,7 +2674,8 @@ class MeasureParser(XMLParserBase):
 
         # TODO: beams over rests?
         '''
-        n = note.Note()
+        d = self.xmlToDuration(mxNote)
+        n = note.Note(duration=d)
 
         # send whole note since accidental display not in <pitch>
         self.xmlToPitch(mxNote, n.pitch)
@@ -3043,7 +3044,8 @@ class MeasureParser(XMLParserBase):
         Note that we do NOT set `r`'s `.fullMeasure` to True or always, but keep it as
         "auto" so that changes in time signature, etc. will affect output.
         '''
-        r = note.Rest()
+        d = self.xmlToDuration(mxRest)
+        r = note.Rest(duration=d)
         mxRestTag = mxRest.find('rest')
         if mxRestTag is None:
             raise MusicXMLImportException('do not call xmlToRest on a <note> unless it '
@@ -3051,7 +3053,7 @@ class MeasureParser(XMLParserBase):
         isFullMeasure = mxRestTag.get('measure')
         if isFullMeasure == 'yes':
             self.fullMeasureRest = True  # force full measure rest...
-            r.measureRest = True
+            r.fullMeasure = True
             # this attribute is not 100% necessary to get a multi-measure rest spanner
 
         if self.parent:  # will apply if active
@@ -3085,13 +3087,21 @@ class MeasureParser(XMLParserBase):
     def xmlNoteToGeneralNoteHelper(self, n, mxNote, freeSpanners=True):
         '''
         Combined function to work on all <note> tags, where n can be
-        a Note or Rest
+        a Note or Rest.
+
+        >>> from xml.etree.ElementTree import fromstring as EL
+        >>> n = note.Note()
+        >>> mxNote = EL('<note color="silver"></note>')
+        >>> MP = musicxml.xmlToM21.MeasureParser()
+        >>> n = MP.xmlNoteToGeneralNoteHelper(n, mxNote)
+        >>> n.style.color
+        'silver'
         '''
         spannerBundle = self.spannerBundle
         if freeSpanners is True:
             spannerBundle.freePendingSpannedElementAssignment(n)
 
-        # attributes
+        # attributes, including color and position
         self.setPrintStyle(mxNote, n)
         # print object == 'no' and grace notes may have a type but not
         # a duration. they may be filtered out at the level of Stream
@@ -3121,17 +3131,13 @@ class MeasureParser(XMLParserBase):
                 # this technically puts it in the
                 # wrong place in the sequence, but it won't matter
                 ET.SubElement(mxNote, '<type>eighth</type>')
-
-        self.xmlToDuration(mxNote, n.duration)
+            self.xmlToDuration(mxNote, n.duration)
 
         # type styles
         mxType = mxNote.find('type')
         if mxType is not None:
             self.setStyleAttributes(mxType, n, 'size', 'noteSize')
 
-        self.setColor(mxNote, n)
-        self.setPosition(mxNote, n)
-
         if mxNote.find('tie') is not None:
             n.tie = self.xmlToTie(mxNote)
             # provide all because of tied...
@@ -4665,6 +4671,7 @@ class MeasureParser(XMLParserBase):
                                            'placement', 'positionPlacement')
 
                 self.insertCoreAndRef(totalOffset, staffKey, d)
+                self.setPosition(mxDir, d)
                 self.setEditorial(mxDirection, d)
 
         elif tag in ('wedge', 'bracket', 'dashes'):
@@ -4675,6 +4682,7 @@ class MeasureParser(XMLParserBase):
                 spannerList = []
 
             for sp in spannerList:
+                self.setPosition(mxDir, sp)
                 self.setEditorial(mxDirection, sp)
 
         elif tag in ('coda', 'segno'):
@@ -4684,12 +4692,7 @@ class MeasureParser(XMLParserBase):
                 rm = repeat.Coda()
 
             _synchronizeIds(mxDir, rm)
-            dX = mxDir.get('default-x')
-            if dX is not None:
-                rm.style.absoluteX = common.numToIntOrFloat(dX)
-            dY = mxDir.get('default-y')
-            if dY is not None:
-                rm.style.absoluteY = common.numToIntOrFloat(dY)
+            self.setPosition(mxDir, rm)
             self.insertCoreAndRef(totalOffset, staffKey, rm)
             self.setEditorial(mxDirection, rm)
 
@@ -4726,7 +4729,22 @@ class MeasureParser(XMLParserBase):
 
     def xmlToTextExpression(self, mxWords):
         '''
-        Given an mxDirection, create a textExpression
+        Given an `mxWords`, create a :class:`~music21.expression.TextExpression`
+        and set style attributes, fonts, position, etc.
+
+        Calls `setTextFormatting`, which calls `setPrintStyleAlign`.
+
+        >>> from xml.etree.ElementTree import fromstring as EL
+        >>> MP = musicxml.xmlToM21.MeasureParser()
+        >>> m = EL('<words default-y="17" font-family="Courier" ' +
+        ... 'font-style="italic" relative-x="-6">a tempo</words>')
+        >>> te = MP.xmlToTextExpression(m)
+        >>> te.content
+        'a tempo'
+        >>> te.style.relativeX
+        -6
+        >>> te.style.fontFamily
+        ['Courier']
         '''
         # TODO: switch to using the setPrintAlign, etc.
 
@@ -4739,7 +4757,6 @@ class MeasureParser(XMLParserBase):
             wordText = ''
         te = expressions.TextExpression(wordText)
         self.setTextFormatting(mxWords, te)
-        self.setPosition(mxWords, te)
         return te
 
     def xmlToRehearsalMark(self, mxRehearsal):
@@ -4831,6 +4848,7 @@ class MeasureParser(XMLParserBase):
                 mm.parentheses = True
 
         _synchronizeIds(mxMetronome, mm)
+        self.setPosition(mxMetronome, mm)
         return mm
 
     def xmlToOffset(self, mxObj):
@@ -5598,8 +5616,8 @@ class MeasureParser(XMLParserBase):
         True
         >>> len(MP.stream)
         2
-        >>> len(MP.stream.getElementsByClass('Voice'))
-        2
+        >>> list(MP.stream.getElementsByClass('Voice'))
+        [<music21.stream.Voice 1>, <music21.stream.Voice 2>]
         '''
         mxm = self.mxMeasure
         for mxn in mxm.findall('note'):
@@ -6805,6 +6823,48 @@ class Test(unittest.TestCase):
         self.assertEqual(ly3.components[2].syllabic, 'end')
         self.assertEqual(len(s.lyrics(recurse=True)[1][0]), 4)
 
+    def testDirectionPosition(self):
+        from music21 import converter
+        from music21 import corpus
+        from music21.musicxml import testPrimitive, testFiles
+
+        # Dynamic
+        s = converter.parse(testFiles.mozartTrioK581Excerpt)
+        dyn = s.recurse().getElementsByClass('Dynamic').first()
+        self.assertEqual(dyn.style.relativeY, 6)
+
+        # Coda/Segno
+        s = converter.parse(testPrimitive.repeatExpressionsA)
+        seg = s.recurse().getElementsByClass('Segno').first()
+        self.assertEqual(seg.style.relativeX, 10)
+
+        # TextExpression
+        s = converter.parse(testPrimitive.textExpressions)
+        positionedEls = [el for el in s.recurse() if el.hasStyleInformation
+            and el.style.relativeX is not None]
+        self.assertEqual(len(positionedEls), 3)
+        self.assertEqual(
+            list(set(type(el) for el in positionedEls)),
+            [expressions.TextExpression]
+        )
+
+        # Wedge
+        s = corpus.parse('beach')
+        positionedEls = [el for el in s.recurse() if el.hasStyleInformation
+            and el.style.relativeX is not None]
+        self.assertEqual(len(positionedEls), 40)
+        self.assertEqual(
+            sorted(set(type(el) for el in positionedEls), key=repr),
+            [dynamics.Crescendo, dynamics.Diminuendo, dynamics.Dynamic, expressions.TextExpression]
+        )
+        crescendos = [el for el in positionedEls if 'Crescendo' in el.classes]
+        self.assertEqual(crescendos[0].style.relativeX, -6)
+
+        # Metronome
+        s = converter.parse(testFiles.tabTest)
+        metro = s.recurse().getElementsByClass('MetronomeMark').first()
+        self.assertEqual(metro.style.absoluteY, 40)
+
 
 if __name__ == '__main__':
     import music21
diff --git a/music21/note.py b/music21/note.py
index 39ce2dbe4..edbbc106e 100644
--- a/music21/note.py
+++ b/music21/note.py
@@ -1321,14 +1321,11 @@ class Note(NotRest):
         if other is None or not isinstance(other, Note):
             return NotImplemented
 
-        retVal = super().__eq__(other)
-        if retVal is not True:
-            return retVal
-
         # checks pitch.octave, pitch.accidental, uses Pitch.__eq__
         if self.pitch != other.pitch:
             return False
-        return True
+
+        return super().__eq__(other)
 
     def __lt__(self, other):
         '''
@@ -1723,7 +1720,7 @@ class Rest(GeneralNote):
                 # TODO: get it to work.
 
                 "auto" is the default, where if the rest value happens to match the current
-                time signature context, then display it as a whole note, centered, etc.
+                time signature context, then display it as a whole rest, centered, etc.
                 otherwise will display normally.
 
                 See examples in :meth:`music21.musicxml.m21ToXml.MeasureExporter.restToXml`
diff --git a/music21/pitch.py b/music21/pitch.py
index b558f0f39..99738c58e 100644
--- a/music21/pitch.py
+++ b/music21/pitch.py
@@ -547,6 +547,10 @@ def simplifyMultipleEnharmonics(pitches, criterion=_dissonanceScore, keyContext=
     else:
         simplifiedPitches = _greedyEnharmonicsSearch(oldPitches, criterion)
 
+    # Preserve value of spellingIsInferred
+    for oldP, newP in zip(oldPitches, simplifiedPitches):
+        newP.spellingIsInferred = oldP.spellingIsInferred
+
     if remove_first:
         simplifiedPitches = simplifiedPitches[1:]
 
@@ -4325,6 +4329,12 @@ class Pitch(prebase.ProtoM21Object):
         >>> otherPitch.transpose('m-23', inPlace=True)
         >>> print(otherPitch)
         C#-1
+
+        Test an issue with inPlace not setting spellingIsInferred
+
+        >>> pc6.transpose(10, inPlace=True)
+        >>> pc6.spellingIsInferred
+        True
         '''
         # environLocal.printDebug(['Pitch.transpose()', value])
         if 'IntervalBase' in getattr(value, 'classes', []):
@@ -4352,6 +4362,7 @@ class Pitch(prebase.ProtoM21Object):
             self.accidental = p.accidental
             # set fundamental
             self.fundamental = p.fundamental
+            self.spellingIsInferred = p.spellingIsInferred
             return None
 
     # --------------------------------------------------------------------------
@@ -5383,22 +5394,23 @@ class Test(unittest.TestCase):
         bm = converter.parse("tinynotation: 4/4 fn1 fn1 e-8 e'-8 fn4 en4 e'n4").flat
         bm.insert(0, key.KeySignature(1))
         bm.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False)
-        assert(bm.flat.notes[0].pitch.accidental.name == 'natural')     # Fn
-        assert(bm.flat.notes[0].pitch.accidental.displayStatus is True)
-        assert(bm.flat.notes[1].pitch.accidental.name == 'natural')     # Fn
-        assert(bm.flat.notes[1].pitch.accidental.displayStatus is True)
-        assert(bm.flat.notes[2].pitch.accidental.name == 'flat')        # E-4
-        assert(bm.flat.notes[2].pitch.accidental.displayStatus is True)
-        assert(bm.flat.notes[3].pitch.accidental.name == 'flat')        # E-5
-        assert(bm.flat.notes[3].pitch.accidental.displayStatus is True)
-        assert(bm.flat.notes[4].pitch.accidental.name == 'natural')     # En4
-        assert(bm.flat.notes[4].pitch.accidental.displayStatus is True)
-        assert(bm.flat.notes[5].pitch.accidental.name == 'natural')     # En4
-        assert(bm.flat.notes[5].pitch.accidental.displayStatus is True)
-
-        assert(bm.flat.notes[6].pitch.accidental is not None)  # En5
-        assert(bm.flat.notes[6].pitch.accidental.name == 'natural')
-        assert(bm.flat.notes[6].pitch.accidental.displayStatus is True)
+
+        self.assertEqual(bm.flat.notes[0].pitch.accidental.name, 'natural')     # Fn
+        self.assertEqual(bm.flat.notes[0].pitch.accidental.displayStatus, True)
+        self.assertEqual(bm.flat.notes[1].pitch.accidental.name, 'natural')     # Fn
+        self.assertEqual(bm.flat.notes[1].pitch.accidental.displayStatus, True)
+        self.assertEqual(bm.flat.notes[2].pitch.accidental.name, 'flat')        # E-4
+        self.assertEqual(bm.flat.notes[2].pitch.accidental.displayStatus, True)
+        self.assertEqual(bm.flat.notes[3].pitch.accidental.name, 'flat')        # E-5
+        self.assertEqual(bm.flat.notes[3].pitch.accidental.displayStatus, True)
+        self.assertEqual(bm.flat.notes[4].pitch.accidental.name, 'natural')     # En4
+        self.assertEqual(bm.flat.notes[4].pitch.accidental.displayStatus, True)
+        self.assertEqual(bm.flat.notes[5].pitch.accidental.name, 'natural')     # En4
+        self.assertEqual(bm.flat.notes[5].pitch.accidental.displayStatus, True)
+
+        self.assertIsNotNone(bm.flat.notes[6].pitch.accidental)  # En5
+        self.assertEqual(bm.flat.notes[6].pitch.accidental.name, 'natural')
+        self.assertEqual(bm.flat.notes[6].pitch.accidental.displayStatus, True)
 
     def testPitchEquality(self):
         '''
diff --git a/music21/repeat.py b/music21/repeat.py
index 8ec1d56ea..b865eb493 100644
--- a/music21/repeat.py
+++ b/music21/repeat.py
@@ -788,7 +788,7 @@ class Expander:
         for m in srcStream:
             # processes uses the spanner bundle stored on this Stream
             self._repeatBrackets.spannerBundle.replaceSpannedElement(
-                id(m.derivation.origin), m)
+                m.derivation.origin, m)
 
         # srcStream = self._srcMeasureStream
         # post = copy.deepcopy(self._srcMeasureStream)
diff --git a/music21/search/segment.py b/music21/search/segment.py
index 635475e9f..86e93a2f6 100644
--- a/music21/search/segment.py
+++ b/music21/search/segment.py
@@ -18,11 +18,10 @@ Speed notes:
    give you a 3-5x speedup.
 
    If you really want to do lots of comparisons, the `scoreSimilarity` method will
-   use pyLevenshtein if it is installed from http://code.google.com/p/pylevenshtein/ .
-   You will need to compile it by running **sudo python setup.py install** on Mac or
-   Unix (compilation is much more difficult on Windows; sorry). The ratios are very
-   slightly different, but the speedup is between 10 and 100x!
-   (but then PyPy probably won't work)
+   use python-Levenshtein if it is installed, unless forceDifflib is set to True.
+   python-Levenshtein can be installed via **pip install python-Levenshtein**.
+   The ratios are very slightly different, but the speedup is between 10 and 100x!
+   (But then PyPy probably won't work.)
 
 '''
 import copy
diff --git a/music21/serial.py b/music21/serial.py
index a2eaf5e9e..8468ca0e5 100644
--- a/music21/serial.py
+++ b/music21/serial.py
@@ -1452,9 +1452,8 @@ class Test(unittest.TestCase):
 
 # ------------------------------------------------------------------------------
 # define presented order in documentation
-_DOC_ORDER = ['ToneRow', 'TwelveToneRow', 'HistoricalTwelveToneRow', 'ContiguousSegmentOfNotes',
-              'historicalDict',
-              'pcToToneRow', 'TwelveToneMatrix', 'rowToMatrix', 'getHistoricalRowByName',
+_DOC_ORDER = [ToneRow, TwelveToneRow, HistoricalTwelveToneRow,
+              pcToToneRow, TwelveToneMatrix, rowToMatrix, getHistoricalRowByName,
               ]
 
 if __name__ == '__main__':
diff --git a/music21/spanner.py b/music21/spanner.py
index e8d66945f..ba3a7423d 100644
--- a/music21/spanner.py
+++ b/music21/spanner.py
@@ -346,7 +346,9 @@ class Spanner(base.Music21Object):
         return iter(self.spannerStorage)
 
     def __len__(self):
-        return len(self.spannerStorage)
+        # Check _elements to avoid StreamIterator overhead.
+        # Safe, because impossible to put spanned elements at end.
+        return len(self.spannerStorage._elements)
 
     def getSpannedElements(self):
         '''
@@ -370,10 +372,9 @@ class Spanner(base.Music21Object):
         >>> sl.getSpannedElements() == [n1, n2, c1]  # make sure that not sorting
         True
         '''
-        post = []
-        for c in self.spannerStorage.elements:
-            post.append(c)
-        return post
+        # Check _elements to avoid StreamIterator overhead.
+        # Safe, because impossible to put spanned elements at end.
+        return list(self.spannerStorage._elements)
 
     def getSpannedElementsByClass(self, classFilterList):
         '''
@@ -396,8 +397,11 @@ class Spanner(base.Music21Object):
     def getSpannedElementIds(self):
         '''
         Return all id() for all stored objects.
+        Was performance critical, until most uses removed in v.7.
+        Used only as a testing tool now.
+        Spanner.__contains__() was optimized in 839c7e5.
         '''
-        return [id(n) for n in self.spannerStorage]
+        return [id(n) for n in self.spannerStorage._elements]
 
     def addSpannedElements(self,
                            spannedElements: Union['music21.base.Music21Object',
@@ -427,10 +431,10 @@ class Spanner(base.Music21Object):
         # presently, this does not look for redundancies
         if not common.isListLike(spannedElements):
             spannedElements = [spannedElements]
-        else:
+        if arguments:
             spannedElements = spannedElements[:]  # copy
-        # assume all other arguments
-        spannedElements += arguments
+            # assume all other arguments
+            spannedElements += arguments
         # environLocal.printDebug(['addSpannedElements():', spannedElements])
         for c in spannedElements:
             if c is None:
@@ -469,7 +473,12 @@ class Spanner(base.Music21Object):
         return spannedElement in self
 
     def __contains__(self, spannedElement):
-        return spannedElement in self.spannerStorage
+        # Cannot check `in` spannerStorage._elements,
+        # because it would check __eq__, not identity.
+        for x in self.spannerStorage._elements:
+            if x is spannedElement:
+                return True
+        return False
 
     def replaceSpannedElement(self, old, new) -> None:
         '''
@@ -777,7 +786,9 @@ class SpannerBundle(prebase.ProtoM21Object):
         if cacheKey not in self._cache or self._cache[cacheKey] is None:
             post = self.__class__()
             for sp in self._storage:  # storage is a list of spanners
-                if idTarget in sp.getSpannedElementIds():
+                # __contains__() will test for identity, not equality
+                # see Spanner.hasSpannedElement(), which just calls __contains__()
+                if spannedElement in sp:
                     post.append(sp)
             self._cache[cacheKey] = post
         return self._cache[cacheKey]
@@ -789,7 +800,7 @@ class SpannerBundle(prebase.ProtoM21Object):
         with new spannedElements
         for all Spanner objects contained in this bundle.
 
-        The `old` parameter can be either an object or object id.
+        The `old` parameter must be an object, not an object id.
 
         If no replacements are found, no errors are raised.
 
@@ -818,15 +829,17 @@ class SpannerBundle(prebase.ProtoM21Object):
         >>> su2
         <music21.spanner.Glissando <music21.note.Note E><music21.note.Note C>>
 
+        Changed in v.7 -- id() is no longer allowed for `old`.
 
+        >>> sb.replaceSpannedElement(id(n1), n2)
+        Traceback (most recent call last):
+        TypeError: send elements to replaceSpannedElement(), not ids (deprecated)
         '''
         # environLocal.printDebug(['SpannerBundle.replaceSpannedElement()', 'old', old,
         #    'new', new, 'len(self._storage)', len(self._storage)])
-
-        if common.isNum(old):  # assume this is an id
-            idTarget = old
-        else:
-            idTarget = id(old)
+        # TODO: remove in v.8 for speed?
+        if isinstance(old, int):
+            raise TypeError('send elements to replaceSpannedElement(), not ids (deprecated)')
 
         replacedSpanners = []
         # post = self.__class__()  # return a bundle of spanners that had changes
@@ -835,10 +848,10 @@ class SpannerBundle(prebase.ProtoM21Object):
 
         for sp in self._storage:  # Spanners in a list
             # environLocal.printDebug(['looking at spanner', sp, sp.getSpannedElementIds()])
-
-            # must check to see if this id is in this spanner
             sp._cache = {}
-            if idTarget in sp.getSpannedElementIds():
+            # accurate, so long as Spanner.__contains__() checks identity, not equality
+            # see discussion at https://github.com/cuthbertLab/music21/pull/905
+            if old in sp:
                 sp.replaceSpannedElement(old, new)
                 replacedSpanners.append(sp)
                 # post.append(sp)
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 44bdd4493..41c478fcd 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -29,6 +29,7 @@ import unittest
 import sys
 
 from fractions import Fraction
+from math import isclose
 from typing import Union, List, Optional, Set, Tuple, Sequence
 
 from music21 import base
@@ -836,10 +837,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         clefList = self.iter.getElementsByClass('Clef').getElementsByOffset(0)
         # casting to list added 20microseconds...
-        if not clefList:
-            return None
-        else:
-            return clefList[0]
+        return clefList.first()
 
     @clef.setter
     def clef(self, clefObj: Optional['music21.clef.Clef']):
@@ -904,10 +902,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # environLocal.printDebug([
         #    'matched Measure classes of type TimeSignature', tsList, len(tsList)])
         # only return timeSignatures at offset = 0.0
-        if not tsList:
-            return None
-        else:
-            return tsList[0]
+        return tsList.first()
 
     @timeSignature.setter
     def timeSignature(self, tsObj: Optional['music21.meter.TimeSignature']):
@@ -1849,7 +1844,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 raise base.SitesException(
                     f'an entry for this object 0x{id(element):x} is not stored in stream {self}')
 
-        if returnSpecial is False and o in OffsetSpecial:
+        # OffsetSpecial.__contains__() is more expensive, so try to fail fast
+        if isinstance(o, str) and returnSpecial is False and o in OffsetSpecial:
             try:
                 return getattr(self, o)
             except AttributeError:  # pragma: no cover
@@ -2048,7 +2044,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Chords can also be inserted into rests:
 
-        >>> s3.getElementsByOffset(2.0)[0]
+        >>> s3.getElementsByOffset(2.0).first()
         <music21.note.Rest rest>
         >>> s3.insertIntoNoteOrChord(2.0, chord.Chord('C4 E4 G#4'))
         >>> s3.show('text')
@@ -2748,9 +2744,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         `recurse=True` should not be necessary to find elements in streams
         without substreams, such as a loose Voice:
 
-        >>> v = stream.Voice([note.Note(quarterLength=5.5)])
+        >>> v = stream.Voice([note.Note(quarterLength=5.5)], id=1)
         >>> v.splitAtDurations()
-        (<music21.stream.Voice 0x106020430>,)
+        (<music21.stream.Voice 1>,)
         >>> [n.duration for n in v.notes]
         [<music21.duration.Duration 4.0>, <music21.duration.Duration 1.5>]
 
@@ -6011,7 +6007,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 allTimePoints = (0,) + allTimePoints
 
             for offset, endTime in zip(allTimePoints, allTimePoints[1:]):
-                if common.almostEquals(offset, endTime):
+                if isclose(offset, endTime, abs_tol=1e-7):
                     continue
                 vert = timespanTree.getVerticalityAt(offset)
                 quarterLength = endTime - offset
@@ -6269,11 +6265,13 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         refStreamOrTimeRange=None,
         fillGaps=False,
         timeRangeFromBarDuration=False,
-        inPlace=True,
+        inPlace=False,
         hideRests=False,
     ):
         '''
         Calls :py:func:`~music21.stream.makeNotation.makeRests`.
+
+        Changed in v.7, inPlace=False by default.
         '''
         return makeNotation.makeRests(
             self,
@@ -8222,10 +8220,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         mdList = self.getElementsByClass('Metadata')
         # only return metadata that has an offset = 0.0
         mdList = mdList.getElementsByOffset(0)
-        if not mdList:
-            return None
-        else:
-            return mdList[0]
+        return mdList.first()
 
     def _setMetadata(self, metadataObj):
         '''
@@ -8865,7 +8860,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     origin = e.derivation.origin
                     if (origin is not None
                             and e.derivation.method == '__deepcopy__'):
-                        spannerBundle.replaceSpannedElement(id(origin), e)
+                        spannerBundle.replaceSpannedElement(origin, e)
 
         return post
 
@@ -10856,6 +10851,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     # insert shift + offset w/ voice
                     returnObj.coreInsert(shiftOffset + e.getOffsetBySite(v), e)
                 returnObj.remove(v)
+            returnObj.coreElementsChanged()
 
         if not inPlace:
             return returnObj
@@ -11315,6 +11311,9 @@ 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:
@@ -13342,7 +13341,7 @@ class Score(Stream):
             if e.sites.hasSpannerSite():
                 origin = e.derivation.origin
                 if origin is not None and e.derivation.method == '__deepcopy__':
-                    spannerBundle.replaceSpannedElement(id(origin), e)
+                    spannerBundle.replaceSpannedElement(origin, e)
         return post
 
     def measureOffsetMap(self, classFilterList=None):
@@ -13391,7 +13390,7 @@ class Score(Stream):
 
         # find greatest divisor for each measure at a time
         # if no measures this will be zero
-        mStream = returnObj.parts[0].getElementsByClass('Measure')
+        mStream = returnObj.parts.first().getElementsByClass('Measure')
         mCount = len(mStream)
         if mCount == 0:
             mCount = 1  # treat as a single measure
@@ -13593,7 +13592,7 @@ class Score(Stream):
         >>> len(post.flat.notes)
         165
         '''
-        post = self.parts[0].template(fillWithRests=False, retainVoices=False)
+        post = self.parts.first().template(fillWithRests=False, retainVoices=False)
         for i, m in enumerate(post.getElementsByClass('Measure')):
             for p in self.parts:
                 mNew = copy.deepcopy(p.getElementsByClass('Measure')[i]).flat
@@ -13756,7 +13755,7 @@ class Opus(Stream):
         mdNew = metadata.Metadata()
 
         for s in self.scores:
-            p = s.parts[0]  # assuming only one part
+            p = s.parts.first()  # assuming only one part
             sNew.insert(0, p)
 
             md = s.metadata
diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py
index 1aec1ae9b..26cdbf877 100644
--- a/music21/stream/iterator.py
+++ b/music21/stream/iterator.py
@@ -161,14 +161,14 @@ class StreamIterator(prebase.ProtoM21Object):
             else:
                 self.sectionIndex = self.index
 
-            self.index += 1  # increment early in case of an error.
-
             try:
-                e = self.srcStreamElements[self.index - 1]
+                e = self.srcStreamElements[self.index]
             except IndexError:
                 # this may happen if the number of elements has changed
+                self.index += 1
                 continue
 
+            self.index += 1
             if self.matchesFilters(e) is False:
                 continue
 
@@ -1549,14 +1549,15 @@ class RecursiveIterator(StreamIterator):
             else:
                 self.sectionIndex = self.index
 
-            self.index += 1  # increment early in case of an error in the next try.
-
             try:
-                e = self.srcStreamElements[self.index - 1]
+                e = self.srcStreamElements[self.index]
             except IndexError:
-                # this may happen in the number of elements has changed
+                self.index += 1
+                # this may happen if the number of elements has changed
                 continue
 
+            self.index += 1
+
             # in a recursive filter, the stream does not need to match the filter,
             # only the internal elements.
             if e.isStream:
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 2286f82f1..938d63ef6 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -462,12 +462,10 @@ def makeMeasures(
     # del clefList
     clefObj = srcObj.clef or srcObj.getContextByClass('Clef')
     if clefObj is None:
-        clefList = list(srcObj.iter.getElementsByClass('Clef').getElementsByOffset(0))
+        clefObj = srcObj.iter.getElementsByClass('Clef').getElementsByOffset(0).first()
         # only return clefs that have offset = 0.0
-        if not clefList:
+        if not clefObj:
             clefObj = clef.bestClef(srcObj, recurse=True)
-        else:
-            clefObj = clefList[0]
 
     # environLocal.printDebug([
     #    'makeMeasures(): first clef found after copying and flattening',
@@ -683,7 +681,7 @@ def makeRests(
     refStreamOrTimeRange=None,
     fillGaps=False,
     timeRangeFromBarDuration=False,
-    inPlace=True,
+    inPlace=False,
     hideRests=False,
 ):
     '''
@@ -790,10 +788,7 @@ def makeRests(
 
     Changed in v6 -- all but first attribute are keyword only
 
-    Obviously there are problems TODO: fix them
-
-    OMIT_FROM_DOCS
-    TODO: default inPlace=False
+    Changed in v7 -- `inPlace` defaults False.
     '''
     from music21 import stream
 
@@ -971,13 +966,6 @@ def makeTies(
     Notes: uses base.Music21Object.splitAtQuarterLength() once it has figured out
     what to split.
 
-    Changed in v. 4 -- inPlace = False by default.
-
-    OMIT_FROM_DOCS
-    TODO: take a list of classes to act as filter on what elements are tied.
-
-    configure ".previous" and ".next" attributes
-
     Previously a note tied from one voice could not make ties into a note
     in the next measure outside of voices.  Fixed May 2017
 
@@ -1001,7 +989,7 @@ def makeTies(
     >>> p.append([m1, m2])
     >>> p2 = p.makeTies()
 
-    test same thing with needed makeTies...creates a possibly unnecessary voice...
+    test same thing with needed makeTies:
 
     >>> p = stream.Part()
     >>> m1 = stream.Measure(number=1)
@@ -1026,8 +1014,7 @@ def makeTies(
         {0.0} <music21.stream.Voice 2>
             {0.0} <music21.note.Note B>
     {1.0} <music21.stream.Measure 2 offset=1.0>
-        {0.0} <music21.stream.Voice 0x105332ac8>
-            {0.0} <music21.note.Note C>
+        {0.0} <music21.note.Note C>
 
     >>> for n in p2.recurse().notes:
     ...     print(n, n.tie)
@@ -1035,7 +1022,15 @@ def makeTies(
     <music21.note.Note B> None
     <music21.note.Note C> <music21.tie.Tie stop>
 
+
+    Changed in v4 -- inPlace = False by default.
+
     Changed in v6 -- all but first attribute are keyword only
+
+    OMIT_FROM_DOCS
+    TODO: take a list of classes to act as filter on what elements are tied.
+
+    configure ".previous" and ".next" attributes
     '''
     from music21 import stream
 
@@ -1198,6 +1193,8 @@ def makeTies(
                     #    mNext])
                     returnObj.insert(mNext.offset, mNext)
         mCount += 1
+    for measure in measureStream:
+        measure.flattenUnnecessaryVoices(inPlace=True)
     del measureStream  # clean up unused streams
 
     if not inPlace:
diff --git a/music21/style.py b/music21/style.py
index 61ccec58a..dc5804d1c 100644
--- a/music21/style.py
+++ b/music21/style.py
@@ -18,13 +18,14 @@ import unittest
 
 from music21 import common
 from music21 import exceptions21
+from music21.prebase import ProtoM21Object
 
 
 class TextFormatException(exceptions21.Music21Exception):
     pass
 
 
-class Style:
+class Style(ProtoM21Object):
     '''
     A style object is a lightweight object that
     keeps track of information about the look of an object.
@@ -106,18 +107,17 @@ class Style:
     def _setAbsoluteY(self, value):
         if value is None:
             self._absoluteY = None
+        elif value == 'above':
+            self._absoluteY = 10
+        elif value == 'below':
+            self._absoluteY = -70
         else:
-            if value == 'above':
-                value = 10
-            elif value == 'below':
-                value = -70
             try:
-                value = common.numToIntOrFloat(value)
+                self._absoluteY = common.numToIntOrFloat(value)
             except ValueError as ve:
                 raise TextFormatException(
                     f'Not a supported absoluteY position: {value!r}'
                 ) from ve
-            self._absoluteY = value
 
     absoluteY = property(_getAbsoluteY,
                          _setAbsoluteY,
@@ -158,6 +158,10 @@ class NoteStyle(Style):
 class TextStyle(Style):
     '''
     A Style object that also includes text formatting.
+
+    >>> ts = style.TextStyle()
+    >>> ts.classes
+    ('TextStyle', 'Style', 'ProtoM21Object', 'object')
     '''
 
     def __init__(self):
diff --git a/music21/tree/fromStream.py b/music21/tree/fromStream.py
index 0402ee0b2..a00fc2e7c 100644
--- a/music21/tree/fromStream.py
+++ b/music21/tree/fromStream.py
@@ -182,7 +182,7 @@ def asTree(inputStream, flatten=False, classList=None, useTimespans=False, group
     >>> etFlat.getPositionAfter(0.5)
     SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=...)
 
-    >>> etFlatNotes = tree.fromStream.asTree(score, flatten=True, classList=[note.Note])
+    >>> etFlatNotes = tree.fromStream.asTree(score, flatten=True, classList=(note.Note,))
     >>> etFlatNotes
     <ElementTree {12} (0.0 <0.20...> to 8.0) <music21.stream.Score exampleScore>>
 
diff --git a/music21/variant.py b/music21/variant.py
index ce6598d89..8bc9bca7b 100644
--- a/music21/variant.py
+++ b/music21/variant.py
@@ -1899,7 +1899,7 @@ def _getNextElements(s, v, numberOfElements=1):
                                                   mustFinishInSpan=False,
                                                   mustBeginInSpan=True,
                                                   classList=[vClass])
-        returnElement = potentialTargets[0]
+        returnElement = potentialTargets.first()
 
     else:
         replacementDuration = v.replacementDuration
@@ -1910,7 +1910,7 @@ def _getNextElements(s, v, numberOfElements=1):
                                                   mustFinishInSpan=False,
                                                   mustBeginInSpan=True,
                                                   classList=[vClass])
-        returnElement = potentialTargets[0]
+        returnElement = potentialTargets.first()
 
 
     return returnElement
@@ -1982,7 +1982,7 @@ def _getPreviousElement(s, v):
         mustFinishInSpan=False,
         mustBeginInSpan=True,
     ).getElementsByClass(vClass)
-    returnElement = potentialTargets[-1]
+    returnElement = potentialTargets.last()
 
     return returnElement
 


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index 3d5e8b090..fb3c45211 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -12657,7 +12657,7 @@ repeatExpressionsA = '''<?xml version="1.0" encoding="UTF-8"?>
     <measure number="2" width="268">
       <direction placement="above">
         <direction-type>
-          <segno default-x="-2" default-y="18"/>
+          <segno default-x="-2" default-y="18" relative-x="10"/>
         </direction-type>
         <sound divisions="1" segno="2"/>
       </direction>
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 4fb6ed11c..1765e9ca6 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -1337,7 +1337,7 @@ class Test(unittest.TestCase):
 
         attackedTogether = stream1.simultaneousAttacks(stream2)
         self.assertEqual(len(attackedTogether), 3)  # nx1, nx2, nx4
-        thisNote = stream2.getElementsByOffset(attackedTogether[1])[0]
+        thisNote = stream2.getElementsByOffset(attackedTogether[1]).first()
         self.assertIs(thisNote, n22)
 
         playingWhenAttacked = stream1.playingWhenAttacked(n23)
@@ -1880,7 +1880,7 @@ class Test(unittest.TestCase):
             p.transferOffsetToElements()
             self.assertEqual(p.lowestOffset, partOffset)
 
-            p.makeRests()
+            p.makeRests(inPlace=True)
 
             # environLocal.printDebug(['first element', p[0], p[0].duration])
             # by default, initial rest should be made
@@ -1907,8 +1907,8 @@ class Test(unittest.TestCase):
         s.insert(0, m1)
         s.insert(4, m2)
         # must connect Measures to Streams before filling gaps
-        m1.makeRests(fillGaps=True, timeRangeFromBarDuration=True)
-        m2.makeRests(fillGaps=True, timeRangeFromBarDuration=True)
+        m1.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
+        m2.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
         self.assertTrue(m2.isSorted)
         # m2.sort()
 
@@ -3582,7 +3582,7 @@ class Test(unittest.TestCase):
         self.assertEqual(test, match)
 
         self.assertEqual(len(s), 5)
-        s.makeRests(fillGaps=True)
+        s.makeRests(inPlace=True, fillGaps=True)
         self.assertEqual(len(s), 8)
         self.assertEqual(len(s.getElementsByClass(note.Rest)), 3)
 
@@ -4341,7 +4341,7 @@ class Test(unittest.TestCase):
         s.insert(1, n2)  # overlapping, starting after n1 but finishing before
         s.insert(2, n3)
         s.insert(3, n4)  # overlapping, starting after n3 but finishing before
-        # s.makeRests(fillGaps=True)
+        # s.makeRests(inPlace=True, fillGaps=True)
         # this results in two chords; n2 and n4 are effectively shifted
         # to the start of n1 and n3
         sMod = s.makeChords(inPlace=False)
@@ -5380,12 +5380,12 @@ class Test(unittest.TestCase):
         s.insert(0, v3)
 
         sPost = s.makeNotation()
-        # voices are retained for all measures after make notation
+        # voices are retained for all measures after makeNotation, unless unnecessary
         self.assertEqual(len(sPost.getElementsByClass('Measure')), 8)
         self.assertEqual(len(sPost.getElementsByClass('Measure')[0].voices), 3)
         self.assertEqual(len(sPost.getElementsByClass('Measure')[1].voices), 3)
-        self.assertEqual(len(sPost.getElementsByClass('Measure')[5].voices), 3)
-        self.assertEqual(len(sPost.getElementsByClass('Measure')[7].voices), 3)
+        self.assertEqual(len(sPost.getElementsByClass('Measure')[4].voices), 2)
+        self.assertEqual(len(sPost.getElementsByClass('Measure')[5].voices), 0)
 
         # s.show()