Repository: cuthbertLab/music21
Pull request #966 (2021-04-19 18:33:11)

Improve Trills and Make sure "methods" are methods
A lot of early documentation did not make a strong distinction between functions and methods.  Went through some places and clarified.

Noticed during review of #962

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

Coverage decreased (-0.004%) to 92.153% when pulling **33526debce816ef697055526bc861734e16f3a85 on docs_on_methods** into **bcfbaf918fbb1cd2a12cd699303c8d0f970660d5 on master**.


Patch:
diff --git a/music21/_version.py b/music21/_version.py
index 7af8793f9..420e32879 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, 3)  # can be 4-tuple: (7, 0, 5, 'a2')
+__version_info__ = (7, 0, 4)  # 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/floatingKey.py b/music21/analysis/floatingKey.py
index 143e8904b..b8970fffa 100644
--- a/music21/analysis/floatingKey.py
+++ b/music21/analysis/floatingKey.py
@@ -59,6 +59,22 @@ class KeyAnalyzer:
      <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>,
      <music21.key.Key of f# minor>, <music21.key.Key of f# minor>,
      <music21.key.Key of f# minor>, <music21.key.Key of f# minor>]
+
+    Fixed in v.7 -- analysis now incorporates final measures in pieces without pickup measures:
+
+    >>> tiny = converter.parse('tinyNotation: c1 e1 g1 c1 d-4 d-4 d-4 d-4')
+    >>> ka = analysis.floatingKey.KeyAnalyzer(tiny)
+    >>> ka.windowSize = 1
+    >>> ka.run()  # This previously only gave four elements: am, CM, CM, CM
+    [<music21.key.Key of a minor>, <music21.key.Key of C major>, <music21.key.Key of C major>,
+     <music21.key.Key of C major>, <music21.key.Key of b- minor>]
+
+    No measures will fail.
+
+    >>> s = stream.Part([note.Note()])
+    >>> ka = analysis.floatingKey.KeyAnalyzer(s)
+    Traceback (most recent call last):
+    music21.analysis.floatingKey.FloatingKeyException: Stream must have Measures inside it
     '''
     def __init__(self, s=None):
         if s is None:
@@ -70,7 +86,7 @@ class KeyAnalyzer:
 
         self.weightAlgorithm = divide
         if s.hasPartLikeStreams():
-            p = s.parts.first()
+            p = s.iter.parts.first()
         else:
             p = s
         self.numMeasures = len(p.getElementsByClass('Measure'))  # could be wrong for endings, etc.
@@ -84,7 +100,8 @@ class KeyAnalyzer:
     def getRawKeyByMeasure(self):
         keyByMeasure = []
         for i in range(self.numMeasures):
-            m = self.stream.measure(i)
+            # now `m` is a measure-slice of the entire stream
+            m = self.stream.measure(i, indicesNotNumbers=True)
             if m is None or not m.recurse().notes:
                 k = None
             else:
@@ -93,15 +110,16 @@ class KeyAnalyzer:
         self.rawKeyByMeasure = keyByMeasure
         return keyByMeasure
 
-    def getInterpretationByMeasure(self, mNumber):
+    def getInterpretationByMeasure(self, mIndex):
         '''
         Returns a dictionary of interpretations for the measure.
+        `mIndex` is 0-indexed.
         '''
-        if mNumber in self._interpretationMeasureDict:
-            return self._interpretationMeasureDict[mNumber]  # CACHE
+        if mIndex in self._interpretationMeasureDict:
+            return self._interpretationMeasureDict[mIndex]  # CACHE
         if not self.rawKeyByMeasure:
             self.getRawKeyByMeasure()
-        mk = self.rawKeyByMeasure[mNumber]
+        mk = self.rawKeyByMeasure[mIndex]
         if mk is None:
             return None
         # noinspection PyDictCreation
@@ -109,7 +127,7 @@ class KeyAnalyzer:
         interpretations[mk.tonicPitchNameWithCase] = mk.correlationCoefficient
         for otherKey in mk.alternateInterpretations:
             interpretations[otherKey.tonicPitchNameWithCase] = otherKey.correlationCoefficient
-        self._interpretationMeasureDict[mNumber] = interpretations
+        self._interpretationMeasureDict[mIndex] = interpretations
         return copy.copy(interpretations)  # for manipulating
 
     def smoothInterpretationByMeasure(self):
diff --git a/music21/base.py b/music21/base.py
index 3cb5a73ea..7e59f8f62 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.3'
+'7.0.4'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
diff --git a/music21/braille/basic.py b/music21/braille/basic.py
index 552f223d1..bd552ccf0 100644
--- a/music21/braille/basic.py
+++ b/music21/braille/basic.py
@@ -1093,7 +1093,7 @@ def showOctaveWithNote(previousNote, currentNote):
 
 
     Of course, these rules cease to apply in quite a few cases, which are not directly reflected
-    in the results of this method:
+    in the results of this function:
 
 
     1) If a braille measure goes to a new line, the first note in the measure carries an
@@ -1109,7 +1109,7 @@ def showOctaveWithNote(previousNote, currentNote):
        those cases needs an octave marking.
 
 
-    If any special case happens, previousNote can be set to None and the method will return
+    If any special case happens, previousNote can be set to None and the function will return
     True.
 
 
@@ -1422,7 +1422,7 @@ def brailleUnicodeToBrailleAscii(brailleUnicode):
     which is the format compatible with most braille embossers.
 
 
-    .. note:: The method works by corresponding braille symbols to ASCII symbols.
+    .. note:: The function works by corresponding braille symbols to ASCII symbols.
         The table which corresponds said values can be found
         `here <http://en.wikipedia.org/wiki/Braille_ASCII#Braille_ASCII_values>`_.
         Because of the way in which the braille symbols translate2, the resulting
@@ -1463,7 +1463,7 @@ def brailleAsciiToBrailleUnicode(brailleAscii):
     can then be displayed on-screen in braille on compatible systems.
 
 
-    .. note:: The method works by corresponding ASCII symbols to braille
+    .. note:: The function works by corresponding ASCII symbols to braille
         symbols in a very direct fashion. It is not a translator from plain
         text to braille, because ASCII symbols may not correspond to their
         equivalents in braille. For example, a literal period is a 4 in
diff --git a/music21/braille/segment.py b/music21/braille/segment.py
index f886f54c5..8107c82ae 100644
--- a/music21/braille/segment.py
+++ b/music21/braille/segment.py
@@ -8,7 +8,7 @@
 # License:      BSD, see license.txt
 # ------------------------------------------------------------------------------
 '''
-Inner classes and methods for transcribing musical segments into braille.
+Inner classes and functions for transcribing musical segments into braille.
 
 This module was made in consultation with the manual "Introduction to Braille
 Music Transcription, Second Edition" by Mary Turner De Garmo, 2005. It is
@@ -1394,10 +1394,10 @@ def findSegments(music21Part, **partKeywords):
 
     Returns a list of :class:`~music21.segment.BrailleSegment` instances.
 
-    Five methods get called in the generation of segments:
+    Five functions or methods get called in the generation of segments:
 
-    * :meth:`~music21.braille.segment.prepareSlurredNotes`
-    * :meth:`~music21.braille.segment.getRawSegments`
+    * :func:`~music21.braille.segment.prepareSlurredNotes`
+    * :func:`~music21.braille.segment.getRawSegments`
     * :meth:`~music21.braille.segment.BrailleSegment.addGroupingAttributes`
     * :meth:`~music21.braille.segment.BrailleSegment.addSegmentAttributes`
     * :meth:`~music21.braille.segment.BrailleSegment.fixArticulations`
@@ -1723,7 +1723,7 @@ def prepareSlurredNotes(music21Part, **keywords):
 def getRawSegments(music21Part, setHand=None):
     '''
     Takes in a :class:`~music21.stream.Part`, divides it up into segments (i.e. instances of
-    :class:`~music21.braille.segment.BrailleSegment`). This method assumes
+    :class:`~music21.braille.segment.BrailleSegment`). This function assumes
     that the Part is already divided up into measures
     (see :class:`~music21.stream.Measure`). An acceptable input is shown below.
 
@@ -1732,10 +1732,10 @@ def getRawSegments(music21Part, setHand=None):
     or :class:`~music21.braille.objects.BrailleOptionalSegmentDivision`
     or after 48 elements if a double bar or repeat sign is encountered.
 
-    Two methods are called on each measure during the creation of segments:
+    Two functions are called for each measure during the creation of segments:
 
-    * :meth:`~music21.braille.segment.prepareBeamedNotes`
-    * :meth:`~music21.braille.segment.extractBrailleElements`
+    * :func:`~music21.braille.segment.prepareBeamedNotes`
+    * :func:`~music21.braille.segment.extractBrailleElements`
 
     >>> tn = converter.parse("tinynotation: 3/4 c4 c c e e e g g g c'2.")
     >>> tn = tn.makeNotation(cautionaryNotImmediateRepeat=False)
@@ -1973,8 +1973,8 @@ def extractBrailleElements(music21Measure):
     {2.0} <music21.bar.Barline type=final>
 
 
-    Spanners are dealt with in :meth:`~music21.braille.segment.prepareSlurredNotes`,
-    so they are not returned by this method, as seen below.
+    Spanners are dealt with in :func:`~music21.braille.segment.prepareSlurredNotes`,
+    so they are not returned by this function, as seen below.
 
     >>> print(segment.extractBrailleElements(measure))
     <music21.meter.TimeSignature 2/4>
@@ -2212,7 +2212,7 @@ def areGroupingsIdentical(noteGroupingA, noteGroupingB):
 
 def splitNoteGrouping(noteGrouping, beatDivisionOffset=0):
     '''
-    Almost identical to :meth:`~music21.braille.segment.splitMeasure`, but
+    Almost identical to :func:`~music21.braille.segment.splitMeasure`, but
     functions on a :class:`~music21.braille.segment.BrailleElementGrouping`
     instead.
 
diff --git a/music21/braille/translate.py b/music21/braille/translate.py
index 41b8288ab..906686e68 100644
--- a/music21/braille/translate.py
+++ b/music21/braille/translate.py
@@ -8,7 +8,7 @@
 # License:      BSD, see license.txt
 # -----------------------------------------------------------------------------
 '''
-Methods for exporting music21 data as braille.
+Functions for exporting music21 data as braille.
 
 This module was made in consultation with the manual "Introduction to Braille
 Music Transcription, Second Edition" by Mary Turner De Garmo, 2005. It is
@@ -16,10 +16,10 @@ available from the Library of Congress `here <http://www.loc.gov/nls/music/>`_,
 and will henceforth be referred to as BMTM.
 
 
-The most important method, and the only one that is needed to translate music
-into braille, is :meth:`~music21.braille.translate.objectToBraille`. This method,
-as well as the others, accept keyword arguments that serve to modify the output.
-If no keyword arguments are needed, then using the method is equivalent to
+The most important function, and the only one that is needed to translate music
+into braille, is :func:`~music21.braille.translate.objectToBraille`. This function,
+as well as the others, accepts keyword arguments that serve to modify the output.
+If no keyword arguments are needed, then using the function is equivalent to
 calling :meth:`~music21.base.Music21Object.show` on the music.
 
 
diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py
index 4864660ed..c5da5e381 100644
--- a/music21/converter/__init__.py
+++ b/music21/converter/__init__.py
@@ -631,7 +631,8 @@ class Converter:
         self.subConverter.keywords = keywords
         self.subConverter.parseData(dataStr, number=number)
 
-    def parseURL(self, url, format=None, number=None, **keywords):  # @ReservedAssignment
+    def parseURL(self, url, *, format=None, number=None,
+                 forceSource=False, **keywords):  # @ReservedAssignment
         '''Given a url, download and parse the file
         into a music21 Stream stored in the `stream`
         property of the converter object.
@@ -639,11 +640,15 @@ class Converter:
         Note that this checks the user Environment
         `autoDownload` setting before downloading.
 
+        Use `forceSource=True` to download every time rather than read from a cached file.
+
         >>> jeanieLightBrownURL = ('https://github.com/cuthbertLab/music21/raw/master' +
         ...        '/music21/corpus/leadSheet/fosterBrownHair.mxl')
         >>> c = converter.Converter()
         >>> #_DOCS_SHOW c.parseURL(jeanieLightBrownURL)
         >>> #_DOCS_SHOW jeanieStream = c.stream
+
+        Changed in v.7 -- made keyword-only and added `forceSource` option.
         '''
         autoDownload = environLocal['autoDownload']
         if autoDownload in ('deny', 'ask'):
@@ -670,7 +675,7 @@ class Converter:
         dst = self._getDownloadFp(directory, ext, url)  # returns pathlib.Path
         urlretrieve = urllib.request.urlretrieve
 
-        if not dst.exists():
+        if forceSource is True or not dst.exists():
             try:
                 environLocal.printDebug(['downloading to:', str(dst)])
                 fp, unused_headers = urlretrieve(url, filename=str(dst))
@@ -1041,14 +1046,17 @@ def parseData(dataStr, number=None, format=None, **keywords):  # @ReservedAssign
 
 # pylint: disable=redefined-builtin
 # noinspection PyShadowingBuiltins
-def parseURL(url, number=None, format=None, forceSource=False, **keywords):  # @ReservedAssignment
+def parseURL(url, *, format=None, number=None,
+             forceSource=False, **keywords):  # @ReservedAssignment
     '''
     Given a URL, attempt to download and parse the file into a Stream. Note:
     URL downloading will not happen automatically unless the user has set their
     Environment "autoDownload" preference to "allow".
+
+    Changed in v.7 -- made keyword-only.
     '''
     v = Converter()
-    v.parseURL(url, format=format, **keywords)
+    v.parseURL(url, format=format, forceSource=forceSource, **keywords)
     return v.stream
 
 
@@ -1678,7 +1686,7 @@ class Test(unittest.TestCase):
         # environLocal.printDebug(['\n' + 'opening fp', fp])
 
         self.assertEqual(len(s.flat.getElementsByClass(note.Note)), 2)
-        self.assertEqual(len(s.flat.getElementsByClass(chord.Chord)), 4)
+        self.assertEqual(len(s.flat.getElementsByClass(chord.Chord)), 5)
 
         # MIDI import makes measures, so we will have one 4/4 time sig
         self.assertEqual(len(s.flat.getElementsByClass(meter.TimeSignature)), 1)
@@ -1939,6 +1947,8 @@ class Test(unittest.TestCase):
             parse('nonexistent_path_ending_in_correct_extension.musicxml')
 
     def testParseURL(self):
+        from music21.humdrum.spineParser import HumdrumException
+
         urlBase = 'http://kern.ccarh.org/cgi-bin/ksdata?l=users/craig/classical/'
         url = urlBase + 'chopin/prelude&file=prelude28-20.krn&format=kern'
 
@@ -1947,6 +1957,18 @@ class Test(unittest.TestCase):
         s = parseURL(url)
         self.assertEqual(len(s.parts), 2)
 
+        # This file should have been written, above
+        destFp = Converter()._getDownloadFp(e.getRootTempDir(), '.krn', url)
+        # Hack garbage into it so that we can test whether or not forceSource works
+        with open(destFp, 'a') as fp:
+            fp.write('all sorts of garbage that Humdrum cannot parse')
+
+        with self.assertRaises(HumdrumException):
+            s = parseURL(url, forceSource=False)
+
+        s = parseURL(url, forceSource=True)
+        self.assertEqual(len(s.parts), 2)
+
 
 # ------------------------------------------------------------------------------
 # define presented order in documentation
diff --git a/music21/corpus/__init__.py b/music21/corpus/__init__.py
index 1a945a5ca..41c664fe0 100644
--- a/music21/corpus/__init__.py
+++ b/music21/corpus/__init__.py
@@ -299,8 +299,8 @@ def parse(workName,
     '''
     The most important method call for corpus.
 
-    Similar to the :meth:`~music21.converter.parse` method of converter (which
-    takes in a filepath on the local hard drive), this method searches the
+    Similar to the :func:`~music21.converter.parse` function of converter (which
+    takes in a filepath on the local hard drive), this function searches the
     corpus (including local corpora) for a work fitting the workName
     description and returns a :class:`music21.stream.Stream`.
 
diff --git a/music21/corpus/manager.py b/music21/corpus/manager.py
index 45f129b3d..4cfb59842 100644
--- a/music21/corpus/manager.py
+++ b/music21/corpus/manager.py
@@ -117,7 +117,7 @@ def getWork(workName,
             fileExtensions=None,
             ):
     '''
-    this parse method is called from `corpus.parse()` and does nothing differently from it.
+    this parse function is called from `corpus.parse()` and does nothing differently from it.
 
     Searches all corpora for a file that matches the name and returns it parsed.
     '''
@@ -218,7 +218,7 @@ def search(query=None, field=None, corpusNames=None, fileExtensions=None, **kwar
     '''
     Search all stored metadata bundles and return a list of file paths.
 
-    This method uses stored metadata and thus, on first usage, will incur a
+    This function uses stored metadata and thus, on first usage, will incur a
     performance penalty during metadata loading.
 
     >>> corpus.search('china')
@@ -244,9 +244,8 @@ def search(query=None, field=None, corpusNames=None, fileExtensions=None, **kwar
     <music21.metadata.bundles.MetadataBundle {368 entries}>
 
 
-
-    This method is implemented in `corpus.manager` but loaded into corpus for
-    ease of use.
+    This function is implemented in `corpus.manager` as a method there but also directly
+    available in the corpus module for ease of use.
 
     The ``corpusNames`` parameter can be used to specify which corpora to search,
     for example:
diff --git a/music21/duration.py b/music21/duration.py
index 6155728c1..051d1f7a3 100644
--- a/music21/duration.py
+++ b/music21/duration.py
@@ -1821,8 +1821,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         True
         '''
         newTuplet.frozen = True
-        self.tuplets = self.tuplets + (newTuplet,)
-        self.informClient()
+        self.tuplets = self._tuplets + (newTuplet,)
 
     def augmentOrDiminish(self, amountToScale, retainComponents=False):
         '''
@@ -2846,9 +2845,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
     def tuplets(self) -> Tuple[Tuplet, ...]:
         '''
         Return a tuple of Tuplet objects.
-        May leave a stream containing objects having this duration
-        in an unusable state, requiring :meth:`~music21.stream.core.coreElementsChanged`
-        to be called. For this reason, prefer using :meth:`appendTuplet` to add tuplets.
+        Setting tuplets will inform the client (Note) that the duration has changed.
         '''
         if self._componentsNeedUpdating:
             self._updateComponents()
@@ -2859,6 +2856,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         # environLocal.printDebug(['assigning tuplets in Duration', tupletTuple])
         self._tuplets = tuple(tupletTuple)
         self._quarterLengthNeedsUpdating = True
+        self.informClient()
 
     def aggregateTupletMultiplier(self) -> OffsetQL:
         '''
@@ -3593,7 +3591,7 @@ class Test(unittest.TestCase):
         self.assertEqual(str(d.components),
                          "(DurationTuple(type='eighth', dots=0, quarterLength=0.5),)")
         self.assertFalse(d._componentsNeedUpdating)
-        self.assertTrue(d._quarterLengthNeedsUpdating)
+        self.assertFalse(d._quarterLengthNeedsUpdating)
         self.assertEqual(repr(d.quarterLength), 'Fraction(1, 3)')
         self.assertEqual(str(unitSpec(d)), "(Fraction(1, 3), 'eighth', 0, 3, 2, 'eighth')")
 
diff --git a/music21/expressions.py b/music21/expressions.py
index 1da73f8a6..f10a66809 100644
--- a/music21/expressions.py
+++ b/music21/expressions.py
@@ -24,7 +24,7 @@ TODO: replace .size with a string representing interval and then
 import copy
 import string
 import unittest
-from typing import List, Optional
+from typing import List, Optional, Tuple
 
 from music21 import base
 from music21 import common
@@ -76,6 +76,7 @@ def realizeOrnaments(srcObject):
                     postExpandList.append(i)
                 if newSrcObject is None:
                     # some ornaments eat up the entire source object. Trills for instance
+                    srcObject = newSrcObject
                     break
                 newSrcObject.expressions = srcObject.expressions[1:]
                 srcObject = newSrcObject
@@ -87,9 +88,11 @@ def realizeOrnaments(srcObject):
                     break
 
         retList = []
+        # TODO: use extend...
         for i in preExpandList:
             retList.append(i)
-        retList.append(srcObject)
+        if srcObject is not None:
+            retList.append(srcObject)
         for i in postExpandList:
             retList.append(i)
         return retList
@@ -507,7 +510,7 @@ class Mordent(GeneralMordent):
     A normal Mordent -- goes downwards and has a line through it.
 
     Note that some computer terminology calls this one an inverted mordent, but this
-    is a modern term.  See Apel, _Harvard Dictionary of Music_, "Mordent"::
+    is a modern term.  See Apel, *Harvard Dictionary of Music*, "Mordent"::
 
         A musical ornament consisting of the alternation of the written note
         with the note immediately below it.
@@ -565,7 +568,7 @@ class InvertedMordent(GeneralMordent):
     An inverted Mordent -- goes upwards and has no line through it.
 
     Note that some computer terminology calls this one a (normal) mordent, but this
-    is a modern term.    See Apel, _Harvard Dictionary of Music_,
+    is a modern term.    See Apel, *Harvard Dictionary of Music*,
     "Inverted Mordent"::
 
         An 18th-century ornament involving alternation of the
@@ -622,23 +625,41 @@ class WholeStepInvertedMordent(InvertedMordent):
 
 # ------------------------------------------------------------------------------
 class Trill(Ornament):
-    '''A basic trill marker.
+    '''
+    A basic trill marker without the trill extension
 
-    >>> m = expressions.Trill()
-    >>> m.placement
+    >>> tr = expressions.Trill()
+    >>> tr.placement
     'above'
-    >>> m.size
-    <music21.interval.Interval M2>
-    '''
+    >>> tr.size
+    <music21.interval.GenericInterval 2>
+
+    Trills have a `.nachschlag` attribute which determines whether there
+    should be extra gracenotes at the end of the trill.
 
+    >>> tr.nachschlag
+    False
+    >>> tr.nachschlag = True
+
+    The Trill also has a "quarterLength" attribute that sets how long
+    each trill note should be.  Defaults to 32nd note:
+
+    >>> tr.quarterLength
+    0.125
+    >>> tr.quarterLength == duration.Duration('32nd').quarterLength
+    True
+
+    Changed in v.7 -- the size should be a generic second.
+    '''
     def __init__(self):
         super().__init__()
-        self.size = interval.Interval('M2')
+        self.size: interval.IntervalBase = interval.GenericInterval(2)
 
         self.placement = 'above'
         self.nachschlag = False  # play little notes at the end of the trill?
         self.tieAttach = 'all'
         self.quarterLength = 0.125
+        self._setAccidentalFromKeySig = True
 
     def splitClient(self, noteList):
         '''
@@ -662,38 +683,110 @@ class Trill(Ornament):
 
         return returnSpanners
 
-    def realize(self, srcObj: 'music21.note.Note'):
+    def realize(
+        self,
+        srcObj: 'music21.note.Note'
+    ) -> Tuple[List['music21.note.Note'], None, List['music21.note.Note']]:
         '''
         realize a trill.
 
-        returns a three-element tuple.
-        The first is a list of the notes that the note was converted to.
-        The second is None because the trill "eats up" the whole note.
-        The third is a list of the notes at the end if nachschlag is True, and empty list if False.
+        Returns a three-element tuple:
 
+        * The first is a list of the notes that the note was converted to.
+        * The second is None because the trill "eats up" the whole note.
+        * The third is a list of the notes at the end if nachschlag is True,
+          and empty list if False.
 
         >>> n1 = note.Note('C4')
-        >>> n1.quarterLength = 0.5
+        >>> n1.duration.type = 'eighth'
         >>> t1 = expressions.Trill()
+        >>> n1.expressions.append(t1)
+        >>> realization = t1.realize(n1)
+        >>> realization
+        ([<music21.note.Note C>,
+          <music21.note.Note D>,
+          <music21.note.Note C>,
+          <music21.note.Note D>], None, [])
+        >>> realization[0][0].quarterLength
+        0.125
+        >>> realization[0][0].pitch.octave
+        4
+
+
+        When inside a stream, the realizations will consult the current key to see
+        if it should be a whole-step or half-step trill:
+
+        >>> m = stream.Measure()
+        >>> k1 = key.Key('D-')
+        >>> m.insert(0, k1)
+        >>> m.append(n1)
+        >>> t1.realize(n1)
+        ([<music21.note.Note C>,
+          <music21.note.Note D->,
+          <music21.note.Note C>,
+          <music21.note.Note D->], None, [])
+
+
+        Note that if the key contradicts the note of the trill, for instance, here
+        having a C-natural rather than a C-sharp, we do not correct the C to C#.
+
+        >>> k2 = key.Key('A')
+        >>> m.replace(k1, k2)
         >>> t1.realize(n1)
         ([<music21.note.Note C>,
           <music21.note.Note D>,
           <music21.note.Note C>,
           <music21.note.Note D>], None, [])
 
+        This can lead to certain unusual circumstances such as augmented second trills
+        which are technically correct, but probably not what a performer exprects.
+
+        >>> k3 = key.Key('E')
+        >>> m.replace(k2, k3)
+        >>> t1.realize(n1)
+        ([<music21.note.Note C>,
+          <music21.note.Note D#>,
+          <music21.note.Note C>,
+          <music21.note.Note D#>], None, [])
+
+
+        To avoid this case, create a :class:`~music21.expressions.HalfStepTrill` or
+        :class:`~music21.expressions.WholeStepTrill`.
+
+
+        If there is a nachschlag, it will appear in the third element of the list.
+
+        >>> n1.duration.type = 'quarter'
+        >>> m.replace(k3, k1)  # back to D-flat major
+        >>> t1.nachschlag = True
+        >>> t1.realize(n1)
+        ([<music21.note.Note C>,
+          <music21.note.Note D->,
+          <music21.note.Note C>,
+          <music21.note.Note D->,
+          <music21.note.Note C>,
+          <music21.note.Note D->], None, [<music21.note.Note C>, <music21.note.Note B->])
+
+        Some notes can be too short to realize.
 
         >>> n2 = note.Note('D4')
-        >>> n2.quarterLength = 0.125
+        >>> n2.duration.type = '32nd'
         >>> t2 = expressions.Trill()
         >>> t2.realize(n2)
         Traceback (most recent call last):
         music21.expressions.ExpressionException: The note is not long enough to realize a trill
 
-        :type srcObj: base.Music21Object
+        A quicker trill makes it possible:
+
+        >>> t2.quarterLength = duration.Duration('64th').quarterLength
+        >>> t2.realize(n2)
+        ([<music21.note.Note D>,
+          <music21.note.Note E>], None, [])
         '''
+        # TODO -- if the trill duration is too short, do not raise an error, simple make the
+        #    quarterLength even shorter.
+
         from music21 import key
-        if self.size == '':
-            raise ExpressionException('Cannot realize a trill if there is no size given')
         if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
         if srcObj.duration.quarterLength < 2 * self.quarterLength:
@@ -704,36 +797,41 @@ class Trill(Ornament):
         transposeInterval = self.size
         transposeIntervalReverse = self.size.reverse()
 
+        numberOfTrillNotes = int(srcObj.duration.quarterLength / self.quarterLength)
         if self.nachschlag:
-            numberOfTrillNotes = int(srcObj.duration.quarterLength / (self.quarterLength - 2))
-        else:
-            numberOfTrillNotes = int(srcObj.duration.quarterLength / self.quarterLength)
+            numberOfTrillNotes -= 2
 
         trillNotes = []
         for unused_counter in range(int(numberOfTrillNotes / 2)):
             self.fillListOfRealizedNotes(srcObj, trillNotes, transposeInterval)
 
-        currentKeySig = srcObj.getContextByClass(key.KeySignature)
-        if currentKeySig is None:
-            currentKeySig = key.KeySignature(0)
+        currentKeySig = None
+        if self._setAccidentalFromKeySig:
+            currentKeySig = srcObj.getContextByClass(key.KeySignature)
+            if currentKeySig is None:
+                currentKeySig = key.KeySignature(0)
 
-        for n in trillNotes:
-            n.pitch.accidental = currentKeySig.accidentalByStep(n.step)
+            for n in trillNotes:
+                if n.pitch.nameWithOctave != srcObj.pitch.nameWithOctave:
+                    # do not correct original note, no matter what.
+                    n.pitch.accidental = currentKeySig.accidentalByStep(n.step)
 
         if self.nachschlag:
             firstNoteNachschlag = copy.deepcopy(srcObj)
-            # TODO: remove expressions
+            firstNoteNachschlag.expressions = []
             firstNoteNachschlag.duration.quarterLength = self.quarterLength
-            firstNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
-                firstNoteNachschlag.step)
 
             secondNoteNachschlag = copy.deepcopy(srcObj)
-            # TODO: remove expressions
+            secondNoteNachschlag.expressions = []
             secondNoteNachschlag.duration.quarterLength = self.quarterLength
             secondNoteNachschlag.transpose(transposeIntervalReverse,
                                            inPlace=True)
-            secondNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
-                secondNoteNachschlag.step)
+
+            if self._setAccidentalFromKeySig:
+                firstNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
+                    firstNoteNachschlag.step)
+                secondNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
+                    secondNoteNachschlag.step)
 
             nachschlag = [firstNoteNachschlag, secondNoteNachschlag]
 
@@ -744,52 +842,89 @@ class Trill(Ornament):
 
 
 class HalfStepTrill(Trill):
-    '''A basic trill marker.
-
+    '''
+    A trill confined to half steps.
 
-    >>> m = expressions.HalfStepTrill()
-    >>> m.placement
+    >>> halfTrill = expressions.HalfStepTrill()
+    >>> halfTrill.placement
     'above'
-    >>> m.size
+    >>> halfTrill.size
     <music21.interval.Interval m2>
+
+    Here the key signature of 2 sharps will not affect the trill:
+
+    >>> n = note.Note('B4', type='eighth')
+    >>> m = stream.Measure()
+    >>> m.insert(0, key.KeySignature(2))
+    >>> m.append(n)
+    >>> halfTrill.realize(n)
+    ([<music21.note.Note B>,
+      <music21.note.Note C>,
+      <music21.note.Note B>,
+      <music21.note.Note C>], None, [])
     '''
 
     def __init__(self):
         super().__init__()
         self.size = interval.Interval('m2')
+        self._setAccidentalFromKeySig = False
 
 
 class WholeStepTrill(Trill):
-    '''A basic trill marker.
-
+    '''
+    A trill that yields whole steps no matter what.
 
-    >>> m = expressions.WholeStepTrill()
-    >>> m.placement
+    >>> wholeTrill = expressions.WholeStepTrill()
+    >>> wholeTrill.placement
     'above'
-    >>> m.size
+    >>> wholeTrill.size
     <music21.interval.Interval M2>
+
+    Here the key signature of one sharp will not affect the trill:
+
+    >>> n = note.Note('B4', type='eighth')
+    >>> m = stream.Measure()
+    >>> m.insert(0, key.KeySignature(1))
+    >>> m.append(n)
+    >>> wholeTrill.realize(n)
+    ([<music21.note.Note B>,
+      <music21.note.Note C#>,
+      <music21.note.Note B>,
+      <music21.note.Note C#>], None, [])
     '''
 
     def __init__(self):
         super().__init__()
         self.size = interval.Interval('M2')
+        self._setAccidentalFromKeySig = False
 
 
 class Shake(Trill):
+    '''
+    A slower trill.
+
+    >>> shake = expressions.Shake()
+    >>> shake.quarterLength
+    0.25
+    '''
     def __init__(self):
         super().__init__()
-        self.size = interval.Interval('M2')
         self.quarterLength = 0.25
 
 
 # ------------------------------------------------------------------------------
 
 # TODO: BaroqueSlide
-# this is a slide or culee
+
 class Schleifer(Ornament):
+    '''
+    A slide or culee
+
+    Changed in v.7 -- size is a Generic second.  removed unused nachschlag component.
+    '''
     def __init__(self):
         super().__init__()
-        self.size = interval.Interval('M2')
+        self.size = interval.GenericInterval(2)
         self.quarterLength = 0.25
 
 
@@ -797,12 +932,13 @@ class Schleifer(Ornament):
 class Turn(Ornament):
     '''
     A turn or Gruppetto.
+
+    Changed in v.7 -- size is a Generic second.  removed unused nachschlag component.
     '''
     def __init__(self):
         super().__init__()
-        self.size = interval.Interval('M2')
+        self.size = interval.GenericInterval(2)
         self.placement = 'above'
-        self.nachschlag = False  # play little notes at the end of the trill?
         self.tieAttach = 'all'
         self.quarterLength = 0.25
 
@@ -828,7 +964,6 @@ class Turn(Ornament):
                                      <music21.note.Note B->,
                                      <music21.note.Note C>])
 
-
         >>> m2 = stream.Measure()
         >>> m2.append(key.KeySignature(5))
         >>> n2 = note.Note('B4')
@@ -840,8 +975,6 @@ class Turn(Ornament):
                                      <music21.note.Note C#>,
                                      <music21.note.Note B>])
 
-
-
         >>> n2 = note.Note('C4')
         >>> n2.quarterLength = 0.125
         >>> t2 = expressions.Turn()
@@ -863,24 +996,22 @@ class Turn(Ornament):
         transposeIntervalDown = self.size.reverse()
         turnNotes = []
 
-        # TODO: if nachschlag...
-
         firstNote = copy.deepcopy(srcObject)
-        # TODO: remove expressions
+        firstNote.expressions = []
         firstNote.duration.quarterLength = self.quarterLength
         firstNote.transpose(transposeIntervalUp, inPlace=True)
 
         secondNote = copy.deepcopy(srcObject)
-        # TODO: remove expressions
+        secondNote.expressions = []
         secondNote.duration.quarterLength = self.quarterLength
 
         thirdNote = copy.deepcopy(srcObject)
-        # TODO: remove expressions
+        thirdNote.expressions = []
         thirdNote.duration.quarterLength = self.quarterLength
         thirdNote.transpose(transposeIntervalDown, inPlace=True)
 
         fourthNote = copy.deepcopy(srcObject)
-        # TODO: remove expressions
+        fourthNote.expressions = []
         fourthNote.duration.quarterLength = self.quarterLength
 
         turnNotes.append(firstNote)
@@ -893,6 +1024,7 @@ class Turn(Ornament):
             currentKeySig = key.KeySignature(0)
 
         for n in turnNotes:
+            # TODO: like in trill, do not affect original note.
             n.pitch.accidental = currentKeySig.accidentalByStep(n.step)
 
         remainderNote = copy.deepcopy(srcObject)
@@ -911,7 +1043,6 @@ class InvertedTurn(Turn):
 class GeneralAppoggiatura(Ornament):
     # up or down -- up means the grace note is below and goes up to the actual note
     direction = ''
-    size = None  # interval.Interval (General, etc.) class
 
     def __init__(self):
         super().__init__()
@@ -1243,7 +1374,7 @@ class TremoloSpanner(spanner.Spanner):
 # ------------------------------------------------------------------------------
 class Test(unittest.TestCase):
 
-    def x_testRealize(self):
+    def testRealize(self):
         from music21 import note
         from music21 import stream
         n1 = note.Note('D4')
@@ -1297,7 +1428,7 @@ class Test(unittest.TestCase):
         re = te.getRepeatExpression()
         self.assertEqual(re.getTextExpression().content, 'd.s. al fine')
 
-    def x_testExpandTurns(self):
+    def testExpandTurns(self):
         from music21 import note, stream, clef, key, meter
         p1 = stream.Part()
         m1 = stream.Measure()
@@ -1306,35 +1437,51 @@ class Test(unittest.TestCase):
         p1.append(key.Key('F', 'major'))
         p1.append(meter.TimeSignature('2/4'))
         n1 = note.Note('C5', type='half')
-        n1.expressions.append(Turn())
-        n2 = note.Note('B4', type='half')
+        turn0 = Turn()
+        n1.expressions.append(turn0)
+        n2 = note.Note('B4', type='quarter')
+        n2.duration.dots = 1
+
         n2.expressions.append(InvertedTurn())
         m1.append(n1)
         m2.append(key.KeySignature(5))
         m2.append(n2)
+        m2.append(note.Rest('eighth'))
         p1.append(m1)
         p1.append(m2)
-        # print(realizeOrnaments(n1))
-        # print(realizeOrnaments(n2))
-
-    def x_testExpandTrills(self):
+        realized1 = realizeOrnaments(n1)
+        realized2 = realizeOrnaments(n2)
+        self.assertEqual('C5 D5 C5 B-4 C5', ' '.join(n.pitch.nameWithOctave for n in realized1))
+        self.assertEqual('B4 A#4 B4 C#5 B4', ' '.join(n.pitch.nameWithOctave for n in realized2))
+        self.assertEqual(realized1[0].quarterLength, 1.0)
+        self.assertEqual(realized1[1].quarterLength, 0.25)
+        self.assertEqual(realized2[0].quarterLength, 0.5)
+        self.assertEqual(realized2[1].quarterLength, 0.25)
+
+        turn0.quarterLength = 0.125
+        realized1b = realizeOrnaments(n1)
+        self.assertEqual(realized1b[0].quarterLength, 1.5)
+        self.assertEqual(realized1b[1].quarterLength, 0.125)
+
+
+    def testExpandTrills(self):
         from music21 import note, stream, clef, key, meter
         p1 = stream.Part()
         m1 = stream.Measure()
         p1.append(clef.TrebleClef())
         p1.append(key.Key('D', 'major'))
-        p1.append(meter.TimeSignature('2/4'))
+        p1.append(meter.TimeSignature('1/4'))
         n1 = note.Note('E4', type='eighth')
         n1.expressions.append(Trill())
         m1.append(n1)
         p1.append(m1)
-        # print(realizeOrnaments(n1))
+        realized = realizeOrnaments(n1)
+        self.assertIsInstance(realized, list)
+        self.assertEqual(len(realized), 4)
+        self.assertIsInstance(realized[0], note.Note)
+        self.assertEqual(realized[0].quarterLength, 0.125)
+        self.assertEqual('E4 F#4 E4 F#4', ' '.join(n.pitch.nameWithOctave for n in realized))
 
-    # def testCPEBachRealizeOrnaments(self):
-    #     from music21 import corpus
-    #     cpe = corpus.parse('cpebach/h186').parts[0].measures(1, 4)
-    #     cpe2 = cpe.realizeOrnaments()
-    #     cpe2.show()
 
     def testTrillExtensionA(self):
         '''Test basic wave line creation and output, as well as passing
@@ -1362,6 +1509,14 @@ class Test(unittest.TestCase):
         self.assertEqual(raw.count(b'wavy-line'), 2)
 
 
+# class TestExternal(unittest.TestCase):
+#     def testCPEBachRealizeOrnaments(self):
+#         from music21 import corpus
+#         cpe = corpus.parse('cpebach/h186').parts[0].measures(1, 4)
+#         cpe2 = cpe.realizeOrnaments()
+#         cpe2.show()
+
+
 # ------------------------------------------------------------------------------
 # define presented order in documentation
 _DOC_ORDER = [TextExpression]
diff --git a/music21/features/base.py b/music21/features/base.py
index d94913202..456fae777 100644
--- a/music21/features/base.py
+++ b/music21/features/base.py
@@ -965,7 +965,7 @@ class DataSet:
 
     def _processNonParallel(self):
         '''
-        The traditional method: run non-parallel
+        The traditional way: run non-parallel
         '''
         # clear features
         self.features = []
diff --git a/music21/graph/plot.py b/music21/graph/plot.py
index 3ed0ad8e1..298a5928b 100644
--- a/music21/graph/plot.py
+++ b/music21/graph/plot.py
@@ -48,7 +48,7 @@ environLocal = environment.Environment(_MOD)
 
 
 def _mergeDicts(a, b):
-    '''utility method to merge two dictionaries'''
+    '''utility function to merge two dictionaries'''
     c = a.copy()
     c.update(b)
     return c
@@ -916,7 +916,7 @@ class WindowedAnalysis(primitives.GraphColorGrid, PlotStreamMixin):
 
     def write(self, fp=None):  # pragma: no cover
         '''
-        Process method here overridden to provide legend.
+        Overrides the normal write method here to add a legend.
         '''
         # call the process routine in the base graph
         super().write(fp)
diff --git a/music21/harmony.py b/music21/harmony.py
index ae2f5ad6a..8899dc5c5 100644
--- a/music21/harmony.py
+++ b/music21/harmony.py
@@ -284,12 +284,12 @@ class Harmony(chord.Chord):
         When you instantiate a harmony object, if you pass in a figure it
         is stored internally and returned when you access the figure
         property. If you don't instantiate the object with a figure, this
-        property calls :meth:`music21.harmony.findFigure` method which
+        property calls :meth:`music21.harmony.Harmony.findFigure` method which
         deduces the figure provided other information about the object,
         especially the chord.
 
         If the pitches of the harmony object have been modified after being
-        instantiated, call :meth:`music21.harmony.findFigure` to deduce the
+        instantiated, call :meth:`music21.harmony.Harmony.findFigure` to deduce the
         new figure.
 
         >>> h = harmony.ChordSymbol('CM')
@@ -1074,7 +1074,7 @@ def chordSymbolFigureFromChord(inChord, includeChordType=False):
        the most thirds above it
        this is not a consistent way to determine the root of 13th chords, for example
     2. a chord vector is extracted from the chord
-        using  :meth:`music21.chord.semitonesFromChordStep`
+        using  :meth:`music21.chord.Chord.semitonesFromChordStep`
         this vector extracts the following degrees: (2, 3, 4, 5, 6, 7, 9, 11, and 13)
     3. this vector is converted to fbNotationString (in the form of chord step,
         and a '-' or '#' to indicate semitone distance)
@@ -1302,7 +1302,7 @@ def chordSymbolFigureFromChord(inChord, includeChordType=False):
 def chordSymbolFromChord(inChord):
     '''
     Get the :class:`~music21.harmony.chordSymbol` object from the chord, using
-    :meth:`music21.harmony.chordSymbolFigureFromChord`
+    :meth:`music21.harmony.Harmony.chordSymbolFigureFromChord`
 
     >>> c = chord.Chord(['D3', 'F3', 'A4', 'B-5'])
     >>> symbol = harmony.chordSymbolFromChord(c)
@@ -1390,7 +1390,7 @@ class ChordSymbol(Harmony):
     To obtain the chord representation of the `ChordSymbol` in the score, change
     :attr:`~music21.harmony.Harmony.writeAsChord` to True. Unless otherwise
     specified, the duration of this chord object will become 1.0. If you have a
-    leadsheet, run :meth:`music21.harmony.realizeChordSymbolDurations` on the
+    leadsheet, run :func:`music21.harmony.realizeChordSymbolDurations` on the
     stream to assign the correct (according to offsets) duration to each
     harmony object.)
 
@@ -2204,7 +2204,7 @@ class ChordSymbol(Harmony):
             'Tristan',
             'augmented-seventh',
             'diminished-seventh',
-            'dominant',
+            'dominant-seventh',
             'half-diminished',
             'major-minor',
             'major-seventh',
@@ -2708,11 +2708,7 @@ class Test(unittest.TestCase):
           </harmony>
           """
         figure = 'A7/G'
-        pitches = ('G2', 'A3', 'C#4', 'E4', 'G4')
-        # TODO: Get rid of the extra G once we do something about ChordSymbol construction,
-        # since currently bass() is called before _updatePitches()
-        # and each of them is creating a G
-        # https://github.com/cuthbertLab/music21/issues/793
+        pitches = ('G2', 'A2', 'C#3', 'E3')
 
         self.runTestOnChord(xmlString, figure, pitches)
 
diff --git a/music21/humdrum/__init__.py b/music21/humdrum/__init__.py
index d5507309b..54679286f 100644
--- a/music21/humdrum/__init__.py
+++ b/music21/humdrum/__init__.py
@@ -37,7 +37,7 @@ extract_      Not needed                                         Use python comm
 fields_       Not needed
 fin2hum_      `music21.converter.parse` (filename)               Enigma Transport Format did not take off and is rarely used. Software to convert Enigma to MusicXML is available from recordare
 freq_         see :meth:`~music21.pitch.Pitch.frequency`
-hint_         see Notes                                          :meth:`~music21.stream.Stream.attachIntervalsBetweenStreams` See alpha.trecento.capua demo to show how it can be done.
+hint_         see Notes                                          :meth:`~music21.stream.Stream.attachIntervalsBetweenStreams` See music21-tools trecento.capua demo to show how it can be done.
 hum2fin_      `.write('musicxml')`                               Writes to musicXML.  A music21 to Enigma converter will not be written (obsolete format)
 humdrum_      Not needed                                         The `spineParser` will report errors when parsing.
 humsed_       Not needed
diff --git a/music21/instrument.py b/music21/instrument.py
index bdd2819d9..b623867f9 100644
--- a/music21/instrument.py
+++ b/music21/instrument.py
@@ -131,8 +131,8 @@ class Instrument(base.Music21Object):
     * instrumentId
     * instrumentName
     * instrumentAbbreviation
-    * midiProgram
-    * midiChannel
+    * midiProgram (0-indexed)
+    * midiChannel (0-indexed)
     * lowestNote (a note object or a string for _written_ pitch)
     * highestNote (a note object or a string for _written_ pitch)
     * transposition (an interval object)
@@ -158,8 +158,8 @@ class Instrument(base.Music21Object):
 
         self.instrumentName = instrumentName
         self.instrumentAbbreviation = None
-        self.midiProgram = None
-        self.midiChannel = None
+        self.midiProgram = None  # 0-indexed
+        self.midiChannel = None  # 0-indexed
         self.instrumentSound = None
 
         self.lowestNote = None
@@ -239,7 +239,7 @@ class Instrument(base.Music21Object):
         assigns the number to self.midiChannel and returns
         it as an int.
 
-        Note that midi channel 10 is special, and
+        Note that midi channel 10 (9 in music21) is special, and
         thus is skipped.
 
         Currently only 16 channels are used.
@@ -248,13 +248,29 @@ class Instrument(base.Music21Object):
         signature is NOT a mistake, but necessary for
         the case where there needs to be a global list.
 
-        >>> used = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
+        >>> used = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11]
         >>> i = instrument.Violin()
         >>> i.autoAssignMidiChannel(used)
         12
         >>> i.midiChannel
         12
 
+        Unpitched percussion will be set to 9, so long as it's not in the filter list:
+
+        >>> used = [0]
+        >>> i = instrument.Maracas()
+        >>> i.autoAssignMidiChannel(used)
+        9
+        >>> i.midiChannel
+        9
+
+        >>> used = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+        >>> i = instrument.Woodblock()
+        >>> i.autoAssignMidiChannel(used)
+        11
+        >>> i.midiChannel
+        11
+
         OMIT_FROM_DOCS
 
         >>> used2 = range(16)
@@ -272,18 +288,21 @@ class Instrument(base.Music21Object):
 
         if not channelFilter:
             self.midiChannel = 0
-            return 0
+            return self.midiChannel
         elif len(channelFilter) >= maxMidi:
             raise InstrumentException('we are out of midi channels! help!')
+        elif 'UnpitchedPercussion' in self.classes and 9 not in usedChannels:
+            self.midiChannel = 9
+            return self.midiChannel
         else:
             for ch in range(maxMidi):
                 if ch in channelFilter:
                     continue
-                elif ch % 16 == 10:
+                elif ch % 16 == 9:
                     continue  # skip 10 / percussion for now
                 else:
                     self.midiChannel = ch
-                    return ch
+                    return self.midiChannel
             return 0
             # raise InstrumentException('we are out of midi channels and this ' +
             #            'was not already detected PROGRAM BUG!')
@@ -1072,6 +1091,7 @@ class UnpitchedPercussion(Percussion):
         super().__init__()
         self._modifier = None
         self._modifierToPercMapPitch = {}
+        self.midiChannel = 9  # 0-indexed, i.e. MIDI channel 10
 
     def _getModifier(self):
         return self._modifier
diff --git a/music21/mei/base.py b/music21/mei/base.py
index 9e2671aa0..eb0a0561c 100644
--- a/music21/mei/base.py
+++ b/music21/mei/base.py
@@ -12,7 +12,7 @@
 These are the public methods for the MEI module by Christopher Antila
 
 To convert a string with MEI markup into music21 objects,
-use :meth:`MeiToM21Converter.convertFromString`.
+use :meth:`~music21.mei.MeiToM21Converter.convertFromString`.
 
 In the future, most of the functions in this module should be moved to a separate, import-only
 module, so that functions for writing music21-to-MEI will fit nicely.
@@ -354,7 +354,9 @@ def safePitch(
     function instead returns a default :class:`~music21.pitch.Pitch` instance.
 
     name: Desired name of the :class:`~music21.pitch.Pitch`.
+
     accidental: (Optional) Symbol for the accidental.
+
     octave: (Optional) Octave number.
 
     Returns A :class:`~music21.pitch.Pitch` with the appropriate properties.
diff --git a/music21/midi/__init__.py b/music21/midi/__init__.py
index 70e0e2b64..7e5c6e6fe 100644
--- a/music21/midi/__init__.py
+++ b/music21/midi/__init__.py
@@ -446,6 +446,12 @@ class MidiEvent(prebase.ProtoM21Object):
     The `data` attribute is used for storing other messages,
     such as SEQUENCE_TRACK_NAME string values.
 
+    .. warning::
+
+        The attributes `.midiProgram` and `.midiChannel` on :class:`~music21.instrument.Instrument`
+        objects are 0-indexed, just as they need to be in the written binary .mid.
+        However, as a convenience, :attr:`MidiEvent.channel` is 1-indexed. No
+        analogous convenience is provided for program change data.
 
     >>> mt = midi.MidiTrack(1)
     >>> me1 = midi.MidiEvent(mt)
@@ -469,7 +475,7 @@ class MidiEvent(prebase.ProtoM21Object):
                  track: Optional['music21.midi.MidiTrack'] = None,
                  type=None,  # @ReservedAssignment
                  time: int = 0,
-                 channel=None):
+                 channel: Optional[int] = None):
         self.track: Optional['music21.midi.MidiTrack'] = track  # a MidiTrack object
         self.type = type
         self.time: int = time
diff --git a/music21/midi/percussion.py b/music21/midi/percussion.py
index b9d91aae5..65e3ef65e 100644
--- a/music21/midi/percussion.py
+++ b/music21/midi/percussion.py
@@ -39,6 +39,10 @@ class PercussionMapper:
 
     But you're better off using the midiPitchToInstrument() method below!
 
+    .. warning::
+
+        Accepts 1-indexed MIDI programs, unlike music21's 0-indexed `.midiProgram`
+        and `.midiChannel` attributes on Instrument instances.
     '''
 
     i = instrument
@@ -96,7 +100,7 @@ class PercussionMapper:
     def midiPitchToInstrument(self, midiPitch):
         '''
         Takes a pitch.Pitch object or int and returns the corresponding
-        instrument in the GM Percussion Map.
+        instrument in the GM Percussion Map, using 1-indexed MIDI programs.
 
 
         >>> pm = midi.percussion.PercussionMapper()
@@ -167,7 +171,7 @@ class PercussionMapper:
     def midiInstrumentToPitch(self, midiInstrument):
         '''
         Takes an instrument.Instrument object and returns a pitch object
-        with the corresponding MIDI note, according to the GM Percussion Map.
+        with the corresponding 1-indexed MIDI note, according to the GM Percussion Map.
 
 
         >>> pm = midi.percussion.PercussionMapper()
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index e3857857b..92b403ab5 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -17,6 +17,7 @@ import unittest
 import math
 import copy
 from typing import Optional, List, Tuple, Dict, Union, Any
+import warnings
 
 from music21 import chord
 from music21 import common
@@ -27,6 +28,7 @@ from music21 import environment
 from music21 import stream
 
 from music21.instrument import Conductor, deduplicate
+from music21.midi import percussion
 
 _MOD = 'midi.translate'
 environLocal = environment.Environment(_MOD)
@@ -36,6 +38,10 @@ environLocal = environment.Environment(_MOD)
 class TranslateException(exceptions21.Music21Exception):
     pass
 
+
+class TranslateWarning(UserWarning):
+    pass
+
 # ------------------------------------------------------------------------------
 # Durations
 
@@ -364,7 +370,7 @@ def midiEventsToNote(eventList, ticksPerQuarter=None, inputM21=None):
     return n
 
 
-def noteToMidiEvents(inputM21, includeDeltaTime=True, channel=1):
+def noteToMidiEvents(inputM21, *, includeDeltaTime=True, channel=1):
     # noinspection PyShadowingNames
     '''
     Translate a music21 Note to a list of four MIDI events --
@@ -401,6 +407,8 @@ def noteToMidiEvents(inputM21, includeDeltaTime=True, channel=1):
     >>> eventList2
     [<music21.midi.MidiEvent NOTE_ON, track=None, channel=9, pitch=61, velocity=90>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=9, pitch=61, velocity=0>]
+
+    Changed in v7 -- made keyword-only.
     '''
     from music21 import midi as midiModule
 
@@ -575,12 +583,14 @@ def midiEventsToChord(eventList, ticksPerQuarter=None, inputM21=None):
     return c
 
 
-def chordToMidiEvents(inputM21, includeDeltaTime=True):
+def chordToMidiEvents(inputM21, *, includeDeltaTime=True, channel=1):
     # noinspection PyShadowingNames
     '''
     Translates a :class:`~music21.chord.Chord` object to a
     list of base.DeltaTime and base.MidiEvents objects.
 
+    The `channel` can be specified, otherwise channel 1 is assumed.
+
     See noteToMidiEvents above for more details.
 
     >>> c = chord.Chord(['c3', 'g#4', 'b5'])
@@ -600,6 +610,8 @@ def chordToMidiEvents(inputM21, includeDeltaTime=True):
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=68, velocity=0>,
      <music21.midi.DeltaTime (empty) track=None, channel=None>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=83, velocity=0>]
+
+    Changed in v7 -- made keyword-only.
     '''
     from music21 import midi as midiModule
     mt = None  # midi track
@@ -662,7 +674,7 @@ def chordToMidiEvents(inputM21, includeDeltaTime=True):
 
         me = midiModule.MidiEvent(track=mt)
         me.type = midiModule.ChannelVoiceMessages.NOTE_OFF
-        me.channel = 1
+        me.channel = channel
         me.pitch = pitchObj.midi
         if not pitchObj.isTwelveTone():
             me.centShift = pitchObj.getCentShiftFromMidi()
@@ -714,6 +726,23 @@ def instrumentToMidiEvents(inputM21,
 def midiEventsToInstrument(eventList):
     '''
     Convert a single MIDI event into a music21 Instrument object.
+
+    >>> me = midi.MidiEvent()
+    >>> me.type = midi.ChannelVoiceMessages.PROGRAM_CHANGE
+    >>> me.data = 53  # MIDI program 54: Voice Oohs
+    >>> midi.translate.midiEventsToInstrument(me)
+    <music21.instrument.Vocalist 'Voice'>
+
+    The percussion map will be used if the channel is 10:
+
+    >>> me.channel = 10
+    >>> i = midi.translate.midiEventsToInstrument(me)
+    >>> i
+    <music21.instrument.Tambourine 'Tambourine'>
+    >>> i.midiChannel  # 0-indexed in music21
+    9
+    >>> i.midiProgram  # 0-indexed in music21
+    53
     '''
     from music21 import midi as midiModule
 
@@ -732,9 +761,21 @@ def midiEventsToInstrument(eventList):
             decoded = decoded.strip()
             i = instrument.fromString(decoded)
         else:
-            i = instrument.instrumentFromMidiProgram(event.data)
+            if event.channel == 10:
+                pm = percussion.PercussionMapper()
+                i = pm.midiPitchToInstrument(event.data + 1)
+            else:
+                i = instrument.instrumentFromMidiProgram(event.data)
+            # Instrument.midiProgram and event.data are both 0-indexed
+            i.midiProgram = event.data
     except (instrument.InstrumentException, UnicodeDecodeError):  # pragma: no cover
         i = instrument.Instrument()
+
+    # Set MIDI channel
+    # Instrument.midiChannel is 0-indexed
+    if event.channel is not None:
+        i.midiChannel = event.channel - 1
+
     # Set partName or instrumentName with literal value from parsing
     if decoded:
         # Except for lousy instrument names
@@ -1110,7 +1151,7 @@ def elementToMidiEventList(
     assuming that dynamics have already been applied, etc.
     Does not include DeltaTime objects.
 
-    Channel is set to the default, 1.
+    Channel (1-indexed) is set to the default, 1.
     Track is not set.
 
     >>> n = note.Note('C4')
@@ -1269,7 +1310,6 @@ def assignPacketsToChannels(
     if initTrackIdToChannelMap is None:
         initTrackIdToChannelMap = {}
 
-    # allChannels = list(range(1, 10)) + list(range(11, 17))  # all but 10
     uniqueChannelEvents = {}  # dict of (start, stop, usedChannel) : channel
     post = []
     usedTracks = []
@@ -1742,7 +1782,7 @@ def midiTrackToStream(
     >>> p
     <music21.stream.Part ...>
     >>> len(p.flat.notesAndRests)
-    13
+    14
     >>> p.flat.notes[0].pitch.midi
     36
     >>> p.flat.notes[0].volume.velocity
@@ -1770,7 +1810,8 @@ def midiTrackToStream(
         {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>
+        {0.0} <music21.chord.Chord F#3 A4 C#5>
+        {2.5} <music21.note.Rest rest>
         {4.0} <music21.bar.Barline type=final>
     '''
     # environLocal.printDebug(['midiTrackToStream(): got midi track: events',
@@ -1921,8 +1962,9 @@ def midiTrackToStream(
     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
-        m.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
+    s.makeTies(inPlace=True)
+    # always need to fill gaps, as rests are not found in any other way
+    s.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
     return s
 
 
@@ -2065,24 +2107,51 @@ def channelInstrumentData(
     Read through Stream `s` and finding instruments in it, return a 2-tuple,
     the first a dictionary mapping MIDI program numbers to channel numbers,
     and the second, a list of unassigned channels that can be used for dynamic
-    allocation.
+    allocation. One channel is always left unassigned for dynamic allocation.
+    If the number of needed channels exceeds the number of available ones,
+    any further MIDI program numbers are assigned to channel 1.
 
     Substreams without notes or rests (e.g. representing a conductor track)
     will not consume a channel.
 
     Only necessarily works if :func:`~music21.midi.translate.prepareStreamForMidi`
     has been run before calling this routine.
+
+    An instrument's `.midiChannel` attribute is observed.
+    `None` is the default `.midiChannel` for all instruments except
+    :class:`~music21.instrument.UnpitchedPercussion`
+    subclasses. Put another way, the priority is:
+
+    - Instrument instance `.midiChannel` (set by user or imported from MIDI)
+    - `UnpitchedPercussion` subclasses receive MIDI Channel 10 (9 in music21)
+    - The channel mappings produced by reading from `acceptableChannelList`,
+      or the default range 1-16. (More precisely, 1-15, since one dynamic channel
+      is always reserved.)
+
+    .. warning::
+
+        The attribute `.midiChannel` on :class:`~music21.instrument.Instrument`
+        is 0-indexed, but `.channel` on :class:`~music21.midi.MidiEvent` is 1-indexed,
+        as are all references to channels in this function.
     '''
     # temporary channel allocation
     if acceptableChannelList is not None:
-        allChannels = acceptableChannelList
+        # copy user input, because we will manipulate it
+        acceptableChannels = acceptableChannelList[:]
     else:
-        allChannels = list(range(1, 10)) + list(range(11, 17))  # all but 10
+        acceptableChannels = list(range(1, 10)) + list(range(11, 17))  # all but 10
 
     # store program numbers
     # tried using set() but does not guarantee proper order.
     allUniqueInstruments = []
 
+    channelByInstrument = {}  # the midiProgram is the key
+    channelsDynamic = []  # remaining channels
+    # create an entry for all unique instruments, assign channels
+    # for each instrument, assign a channel; if we go above 16, that is fine
+    # we just cannot use it and will take modulus later
+    channelsAssigned = set()
+
     # store streams in uniform list
     substreamList = []
     if s.hasPartLikeStreams():
@@ -2102,6 +2171,27 @@ def channelInstrumentData(
         instrumentStream = subs.recurse().getElementsByClass('Instrument')
         setAnInstrument = False
         for inst in instrumentStream:
+            if inst.midiChannel is not None and inst.midiProgram not in channelByInstrument:
+                # Assignment Case 1: read from instrument.midiChannel
+                # .midiChannel is 0-indexed, but MIDI channels are 1-indexed, so convert.
+                thisChannel = inst.midiChannel + 1
+                try:
+                    acceptableChannels.remove(thisChannel)
+                except ValueError:
+                    # Don't warn if 10 is missing, since
+                    # we deliberately made it unavailable above.
+                    if thisChannel != 10:
+                        # If the user wants multiple non-drum programs mapped
+                        # to the same MIDI channel for some reason, solution is to provide an
+                        # acceptableChannelList containing duplicate entries.
+                        warnings.warn(
+                            f'{inst} specified 1-indexed MIDI channel {thisChannel} '
+                            f'but acceptable channels were {acceptableChannels}. '
+                            'Defaulting to channel 1.',
+                            TranslateWarning)
+                        thisChannel = 1
+                channelsAssigned.add(thisChannel)
+                channelByInstrument[inst.midiProgram] = thisChannel
             if inst.midiProgram not in allUniqueInstruments:
                 allUniqueInstruments.append(inst.midiProgram)
             setAnInstrument = True
@@ -2110,24 +2200,21 @@ def channelInstrumentData(
             if None not in allUniqueInstruments:
                 allUniqueInstruments.append(None)
 
-    channelByInstrument = {}  # the instrument is the key
-    channelsDynamic = []  # remaining channels
-    # create an entry for all unique instruments, assign channels
-    # for each instrument, assign a channel; if we go above 16, that is fine
-    # we just cannot use it and will take modulus later
-    channelsAssigned = []
+    programsStillNeeded = [x for x in allUniqueInstruments if x not in channelByInstrument]
 
-    for i, iPgm in enumerate(allUniqueInstruments):
-        # the key is the program number; the values is the start channel
-        if i < len(allChannels) - 1:  # save at least one dynamic channel
-            channelByInstrument[iPgm] = allChannels[i]
-            channelsAssigned.append(allChannels[i])
+    for i, iPgm in enumerate(programsStillNeeded):
+        # the key is the program number; the value is the start channel
+        if i < len(acceptableChannels) - 1:  # save at least one dynamic channel
+            # Assignment Case 2: dynamically assign available channels
+            # if Instrument.midiChannel was None
+            channelByInstrument[iPgm] = acceptableChannels[i]
+            channelsAssigned.add(acceptableChannels[i])
         else:  # just use 1, and deal with the mess: cannot allocate
-            channelByInstrument[iPgm] = allChannels[0]
-            channelsAssigned.append(allChannels[0])
+            channelByInstrument[iPgm] = acceptableChannels[0]
+            channelsAssigned.add(acceptableChannels[0])
 
     # get the dynamic channels, or those not assigned
-    for ch in allChannels:
+    for ch in acceptableChannels:
         if ch not in channelsAssigned:
             channelsDynamic.append(ch)
 
@@ -2252,7 +2339,7 @@ def updatePacketStorageWithChannelInfo(
                 initCh = 1  # fallback, should not happen.
         elif 'Conductor' in instObj.classes:
             initCh = None
-        else:  # use midi program
+        else:  # keys are midi program
             initCh = channelByInstrument[instObj.midiProgram]
         bundle['initChannel'] = initCh  # set for bundle too
 
@@ -2273,6 +2360,12 @@ def streamHierarchyToMidiTracks(
     acceptableChannelList is a list of MIDI Channel numbers that can be used or None.
     If None, then 1-9, 11-16 are used (10 being reserved for percussion).
 
+    In addition, if an :class:`~music21.instrument.Instrument` object in the stream
+    has a `.midiChannel` that is not None, that channel is observed, and
+    also treated as reserved. Only subclasses of :class:`~music21.instrument.UnpitchedPercussion`
+    have a default `.midiChannel`, but users may manipulate this.
+    See :func:`channelInstrumentData` for more, and for documentation on `acceptableChannelList`.
+
     Called by streamToMidiFile()
 
     The process:
@@ -2390,7 +2483,9 @@ def midiTracksToStreams(
 
 def streamToMidiFile(
     inputM21: stream.Stream,
+    *,
     addStartDelay: bool = False,
+    acceptableChannelList: Optional[List[int]] = None,
 ) -> 'music21.midi.MidiFile':
     # noinspection PyShadowingNames
     '''
@@ -2415,11 +2510,16 @@ def streamToMidiFile(
     >>> #_DOCS_SHOW mf.open('/Volumes/disc/_scratch/midi.mid', 'wb')
     >>> #_DOCS_SHOW mf.write()
     >>> #_DOCS_SHOW mf.close()
+
+    See :func:`channelInstrumentData` for documentation on `acceptableChannelList`.
     '''
     from music21 import midi as midiModule
 
     s = inputM21
-    midiTracks = streamHierarchyToMidiTracks(s, addStartDelay=addStartDelay)
+    midiTracks = streamHierarchyToMidiTracks(s,
+                                             addStartDelay=addStartDelay,
+                                             acceptableChannelList=acceptableChannelList,
+                                             )
 
     # may need to update channel information
 
@@ -2614,7 +2714,7 @@ def midiFileToStream(
     >>> s
     <music21.stream.Score ...>
     >>> len(s.flat.notesAndRests)
-    13
+    14
     '''
     # environLocal.printDebug(['got midi file: tracks:', len(mf.tracks)])
     if inputM21 is None:
@@ -2780,12 +2880,15 @@ class Test(unittest.TestCase):
     def testChannelAllocation(self):
         # test instrument assignments
         from music21 import instrument
-        from music21.midi import translate
 
         iList = [instrument.Harpsichord,
                  instrument.Viola,
                  instrument.ElectricGuitar,
-                 instrument.Flute]
+                 instrument.Flute,
+                 instrument.Vibraphone,  # not 10
+                 instrument.BassDrum,  # 10
+                 instrument.HiHatCymbal,  # 10
+                 ]
         iObjs = []
 
         s = stream.Score()
@@ -2797,16 +2900,44 @@ class Test(unittest.TestCase):
             p.append(note.Note('C#'))
             s.insert(0, p)
 
-        channelByInstrument, channelsDynamic = translate.channelInstrumentData(s)
+        channelByInstrument, channelsDynamic = channelInstrumentData(s)
+
+        # Default allocations
+        self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs))
+        self.assertSetEqual(set(channelByInstrument.values()), {1, 2, 3, 4, 5, 10})
+        self.assertListEqual(channelsDynamic, [6, 7, 8, 9, 11, 12, 13, 14, 15, 16])
 
+        # Limit to given acceptable channels
+        acl = list(range(11, 17))
+        channelByInstrument, channelsDynamic = channelInstrumentData(s, acceptableChannelList=acl)
         self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs))
-        self.assertSetEqual(set(channelByInstrument.values()), {1, 2, 3, 4})
-        self.assertListEqual(channelsDynamic, [5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16])
+        self.assertSetEqual(set(channelByInstrument.values()), {10, 11, 12, 13, 14, 15})
+        self.assertListEqual(channelsDynamic, [16])
+
+        # User specification
+        for i, iObj in enumerate(iObjs):
+            iObj.midiChannel = 15 - i
+
+        channelByInstrument, channelsDynamic = channelInstrumentData(s)
+
+        self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs))
+        self.assertSetEqual(set(channelByInstrument.values()), {11, 12, 13, 14, 15, 16})
+        self.assertListEqual(channelsDynamic, [1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+        # User error
+        iObjs[0].midiChannel = 100
+        msg = 'Harpsichord specified 1-indexed MIDI channel 101 but '
+        msg += 'acceptable channels were [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16].'
+        msg += 'Defaulting to channel 1.'
+        with self.assertWarns(TranslateWarning, msg=msg):
+            channelByInstrument, channelsDynamic = channelInstrumentData(s)
+        self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs))
+        self.assertSetEqual(set(channelByInstrument.values()), {1, 11, 12, 13, 14, 15})
+        self.assertListEqual(channelsDynamic, [2, 3, 4, 5, 6, 7, 8, 9, 16])
 
     def testPacketStorage(self):
         # test instrument assignments
         from music21 import instrument
-        from music21.midi import translate
 
         iList = [None,  # conductor track
                  instrument.Harpsichord,
@@ -2827,7 +2958,7 @@ class Test(unittest.TestCase):
                 p.append(note.Note('C#'))
             substreamList.append(p)
 
-        packetStorage = translate.packetStorageFromSubstreamList(substreamList, addStartDelay=False)
+        packetStorage = packetStorageFromSubstreamList(substreamList, addStartDelay=False)
         self.assertIsInstance(packetStorage, dict)
         self.assertEqual(list(packetStorage.keys()), [0, 1, 2, 3, 4, 5])
 
@@ -2848,7 +2979,7 @@ class Test(unittest.TestCase):
             None: 5,
         }
 
-        translate.updatePacketStorageWithChannelInfo(packetStorage, channelInfo)
+        updatePacketStorageWithChannelInfo(packetStorage, channelInfo)
         self.assertSetEqual(set(harpsPacket.keys()),
                             {'rawPackets', 'initInstrument', 'initChannel'})
         self.assertEqual(harpsPacket['initChannel'], 1)
@@ -3534,7 +3665,7 @@ class Test(unittest.TestCase):
         # a simple file created in athenacl
         s = converter.parse(fp)
         # s.show('t')
-        self.assertEqual(len(s.flat.getElementsByClass('Chord')), 4)
+        self.assertEqual(len(s.flat.getElementsByClass('Chord')), 5)
 
     def testMidiEventsImported(self):
         self.maxDiff = None
@@ -3685,6 +3816,28 @@ class Test(unittest.TestCase):
         self.assertEqual(
             len(inn.parts[1].measure(3).voices.last().getElementsByClass('Rest')), 1)
 
+    def testRestsMadeInMeasures(self):
+        from music21 import converter
+
+        fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test17.mid'
+        inn = converter.parse(fp)
+        pianoLH = inn.parts.last()
+        m1 = pianoLH.measure(1)  # quarter note, quarter note, quarter rest
+        m2 = pianoLH.measure(2)
+        self.assertEqual(len(m1.notesAndRests), 3)
+        self.assertEqual(len(m1.notes), 2)
+        self.assertEqual(m1.duration.quarterLength, 3.0)
+        self.assertEqual(pianoLH.elementOffset(m2), 3.0)
+
+        for part in inn.parts:
+            with self.subTest(part=part):
+                self.assertEqual(
+                    sum(m.barDuration.quarterLength for m in part.getElementsByClass(
+                        stream.Measure)
+                        ),
+                    part.duration.quarterLength
+                )
+
 
 # ------------------------------------------------------------------------------
 _DOC_ORDER = [streamToMidiFile, midiFileToStream]
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index 04ee237aa..ea2a14abf 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -1353,6 +1353,8 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
 
         self.partExporterList: List['PartExporter'] = []
 
+        self.joinedGroups: List['StaffGroup'] = []
+
         self.instrumentList = []
         self.midiChannelList = []
 
@@ -1929,6 +1931,20 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
           <score-part id="P4">...
           <part-group number="1" type="stop" />
         </part-list>
+
+        And with a piano staff, to observe that only one <part> tag has both "start" and "stop":
+
+        >>> cpe = corpus.parse('cpebach')
+        >>> SX = musicxml.m21ToXml.ScoreExporter(cpe)
+        >>> SX.scorePreliminaries()
+        >>> SX.parsePartlikeScore()
+
+        >>> mxPartList = SX.setPartList()
+        >>> SX.dump(mxPartList)
+        <part-list>
+          <part-group number="1" type="start">...
+          <part-group number="1" type="stop" />
+        </part-list>
         '''
 
         spannerBundle = self.spannerBundle
@@ -1959,7 +1975,10 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
             # check for last
             activeIndex = None
             for sg in staffGroups:
-                if sg.isLast(p):
+                # Handle last part in the StaffGroup
+                # as well as the first part in the StaffGroup if it was joined to the others
+                # In that latter case, we need the <part-group type="stop" /> on the FIRST m21 part
+                if (sg.isLast(p) or (sg in self.joinedGroups and sg.isFirst(p))):
                     # find the spanner in the dictionary already-assigned
                     for key, value in partGroupIndexRef.items():
                         if value is sg:
@@ -5808,11 +5827,11 @@ class MeasureExporter(XMLExporterBase):
             return mxTime
 
         # always get a flat version to display any subdivisions created
-        fList = [(mt.numerator, mt.denominator) for mt in ts.displaySequence.flat]
+        fList = tuple((mt.numerator, mt.denominator) for mt in ts.displaySequence.flat)
         if ts.summedNumerator:
             # this will try to reduce any common denominators into
             # a common group
-            fList = meter.fractionToSlashMixed(fList)
+            fList = meter.tools.fractionToSlashMixed(fList)
 
         for n, d in fList:
             mxBeats = SubElement(mxTime, 'beats')
@@ -6431,6 +6450,13 @@ class Test(unittest.TestCase):
         endings = x.findall('.//ending')
         self.assertEqual([e.get('number') for e in endings], ['1,2', '1,2', '3', '3'])
 
+        # Check templates also
+        template = s.template()
+        template.makeNotation(inPlace=True)  # not essential, but since getET() skips this
+        x = self.getET(template)
+        endings = x.findall('.//ending')
+        self.assertEqual([e.get('number') for e in endings], ['1,2', '1,2', '3', '3'])
+
         # m21 represents lack of bracket numbers as 0; musicxml uses ''
         s.parts[0].getElementsByClass('RepeatBracket').first().number = 0
         x = self.getET(s)
diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py
index 45b85a1f5..33e0bc05b 100644
--- a/music21/musicxml/partStaffExporter.py
+++ b/music21/musicxml/partStaffExporter.py
@@ -131,7 +131,8 @@ class PartStaffExporterMixin:
         >>> len(staffTags)
         2
         '''
-        for group in self.joinableGroups():
+        self.joinedGroups = self.joinableGroups()
+        for group in self.joinedGroups:
             self.addStaffTagsMultiStaffParts(group)
             self.movePartStaffMeasureContents(group)
             self.setEarliestAttributesAndClefsPartStaff(group)
@@ -283,13 +284,6 @@ class PartStaffExporterMixin:
             <staff>2</staff>
           </note>
         </measure>
-
-        Fails if attempted a second time:
-
-        >>> root = SX.parse()
-        Traceback (most recent call last):
-        music21.musicxml.xmlObjects.MusicXMLExportException:
-            In part (MusicXML Part), measure (1): Attempted to create a second <staff> tag
         '''
         initialPartStaffRoot: Optional[Element] = None
         for i, ps in enumerate(group):
@@ -595,10 +589,6 @@ class PartStaffExporterMixin:
         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:`joinPartStaffs`
 
         >>> from music21.musicxml import testPrimitive
@@ -611,9 +601,6 @@ class PartStaffExporterMixin:
         >>> SX.postPartProcess()
         >>> len(SX.partExporterList)
         1
-        >>> partGroupStop = SX.xmlRoot.findall('.//part-group')[1]
-        >>> SX.dump(partGroupStop)
-        <part-group number="1" type="stop" />
         '''
         for ps in group[1:]:
             partStaffRoot: Element = self.getRootForPartStaff(ps)
@@ -621,8 +608,6 @@ class PartStaffExporterMixin:
             # noinspection PyAttributeOutsideInit
             self.partExporterList = [pex for pex in self.partExporterList
                                         if pex.xmlRoot != partStaffRoot]
-            # Replace PartStaff in StaffGroup -- ensures <part-group number="1" type="stop" />
-            group.replaceSpannedElement(ps, group.getFirst())
 
     @staticmethod
     def moveMeasureContents(measure: Element, otherMeasure: Element, staffNumber: int):
@@ -945,6 +930,18 @@ class Test(unittest.TestCase):
         SX.parsePartlikeScore()
         self.assertEqual(len(SX.joinableGroups()), 1)
 
+    def testJoinPartStaffsAgain(self):
+        '''
+        Regression test for side effects on the stream passed to ScoreExporter
+        preventing it from being written out again.
+        '''
+        from music21 import corpus
+        from music21.musicxml.m21ToXml import ScoreExporter
+        b = corpus.parse('cpebach')
+        SX = ScoreExporter(b)
+        SX.parse()
+        SX.parse()
+
 
 if __name__ == '__main__':
     import music21
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index e786eaf6b..7601297c7 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -3822,7 +3822,7 @@ class MeasureParser(XMLParserBase):
         # add a reference of this note to this spanner
         if target is not None:
             su.addSpannedElements(target)
-        # environLocal.printDebug(['adding n', n, id(n), 'su.getSpannedElements',
+        # environLocal.printDebug(['adding n', target, id(target), 'su.getSpannedElements',
         #     su.getSpannedElements(), su.getSpannedElementIds()])
         if mxObj.get('type') == 'stop':
             su.completeStatus = True
diff --git a/music21/roman.py b/music21/roman.py
index 18f054c99..916b6d76d 100644
--- a/music21/roman.py
+++ b/music21/roman.py
@@ -282,7 +282,7 @@ def postFigureFromChordAndKey(chordObj, keyObj=None):
     ...     )
     'o6#5b3'
 
-    The method substitutes shorthand (e.g., '6' not '63')
+    The function substitutes shorthand (e.g., '6' not '63')
 
     >>> roman.postFigureFromChordAndKey(
     ...     chord.Chord(['E3', 'C4', 'G4']),
@@ -604,7 +604,7 @@ def identifyAsTonicOrDominant(
 def romanInversionName(inChord, inv=None):
     '''
     Extremely similar to Chord's inversionName() method, but returns string
-    values and allows incomplete triads
+    values and allows incomplete triads.
     '''
     if inv is None:
         inv = inChord.inversion()
@@ -3248,7 +3248,7 @@ class RomanNumeral(harmony.Harmony):
     def isNeapolitan(self,
                      require1stInversion: bool = True):
         '''
-        Music21's Chord module offers methods for identifying chords of a particular type,
+        Music21's Chord class contains methods for identifying chords of a particular type,
         such as :meth:`~music21.chord.Chord.isAugmentedSixth`.
 
         Some similar chord types are defined not only by the structure of a chord but
diff --git a/music21/search/serial.py b/music21/search/serial.py
index 8e218a445..fedbed578 100644
--- a/music21/search/serial.py
+++ b/music21/search/serial.py
@@ -1351,8 +1351,8 @@ class TransformedSegmentMatcher(SegmentMatcher):
     The convention for serial
     transformations must be specified to either
     'zero' or 'original', as described in
-    :meth:`~music21.search.serial.zeroCenteredTransformation` and
-    :func:`~music21.search.serial.originalCenteredTransformation` - the default setting
+    :meth:`~music21.search.serial.ToneRow.zeroCenteredTransformation` and
+    :func:`~music21.search.serial.ToneRow.originalCenteredTransformation` - the default setting
     is 'original', as to relate found segments
     directly to the given segments, without first transposing the given segment to
     begin on the pitch class 0.
@@ -1716,7 +1716,7 @@ class TransposedMultisetMatcher(SegmentMatcher):
     Finds all instances of given multisets of pitch classes, with transpositions,
     within a :class:`~music21.stream.Stream`. A multiset
     is a generalization of a set, as described in
-    :meth:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     The inputStream is :class:`~music21.stream.Stream`; as
     in :class:`~music21.search.serial.ContiguousSegmentSearcher`,
@@ -1828,7 +1828,7 @@ class TransposedInvertedMultisetMatcher(TransposedMultisetMatcher):
     Finds all instances of given multisets of pitch classes, with
     transpositions and inversions, within a :class:`~music21.stream.Stream`.
     A multiset is a generalization of a set, as described in
-    :meth:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     The inputStream is :class:`~music21.stream.Stream`; as
     in :class:`~music21.search.serial.ContiguousSegmentSearcher`,
@@ -2214,13 +2214,13 @@ def labelMultisets(inputStream, multisetDict, reps='skipConsecutive', includeCho
     Labels all instances of a given collection of multisets of pitch classes in a
     :class:`~music21.stream.Stream`. A multiset
     is a generalization of a set, as described in
-    :meth:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     The multisetDict is a dictionary whose keys are names of
     the multisets to be searched for, and whose
     values are the segments of pitch classes. The values will be
-    turned in to a segmentList, as in :func:`~music21.search.serial.findMultisets`.
-    All other settings are as in :func:`~music21.search.serial.findMultisets` as well.
+    turned in to a segmentList, as in :class:`~music21.search.serial.MultisetSegmentMatcher`.
+    All other settings are as in :class:`~music21.search.serial.MultisetSegmentMatcher` as well.
 
     Returns a deep copy of the inputStream
     with a :class:`~music21.spanner.Line` connecting the first and last notes
@@ -2281,12 +2281,12 @@ def labelTransposedMultisets(inputStream, multisetDict,
     transpositions, of pitch classes in a :class:`~music21.stream.Stream`.
 
     A multiset is a generalization of a set, as described in
-    :meth:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     The multisetDict is a dictionary whose keys are names of the multisets to
     be searched for, and whose values are the segments of pitch classes. The
     values will be turned in to a segmentList, as in
-    :func:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     All other settings are as in
     :func:`~music21.search.serial.findTransposedMultisets` as well.
@@ -2345,12 +2345,12 @@ def labelTransposedAndInvertedMultisets(inputStream,
     :class:`~music21.stream.Stream`.
 
     A multiset is a generalization of a set, as described in
-    :meth:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     The multisetDict is a dictionary whose keys are names of the multisets to
     be searched for, and whose values are the segments of pitch classes. The
     values will be turned in to a segmentList, as in
-    :func:`~music21.search.serial.findMultisets`.
+    :class:`~music21.search.serial.MultisetSegmentMatcher`.
 
     All other settings are as in
     :func:`~music21.search.serial.findTransposedMultisets` as well.
diff --git a/music21/serial.py b/music21/serial.py
index 8468ca0e5..836c74244 100644
--- a/music21/serial.py
+++ b/music21/serial.py
@@ -49,7 +49,7 @@ class TwelveToneMatrix(stream.Stream):
 
     This object is commonly used by calling the
     :meth:`~music21.stream.TwelveToneRow.matrix` method of
-    :meth:`~music21.stream.TwelveToneRow` (or a subclass).
+    :class:`~music21.stream.TwelveToneRow` (or a subclass).
 
     >>> ttr = serial.TwelveToneRow([0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5])
     >>> aMatrix = ttr.matrix()
@@ -563,7 +563,7 @@ class ToneRow(stream.Stream):
         to another, the second specified in the argument. Each transformation is given as a
         tuple of the transformation type and index.
 
-        See :meth:`~music21.serial.zeroCenteredTransformation` for
+        See :meth:`~music21.serial.ToneRow.zeroCenteredTransformation` for
         an explanation of this convention.
 
 
@@ -766,8 +766,8 @@ class TwelveToneRow(ToneRow):
 
         The convention for serial transformations must also be specified as 'zero' or
         'original', as explained
-        in :meth:`~music21.serial.findZeroCenteredTransformations` and
-        :meth:`~music21.serial.findOriginalCenteredTransformations`.
+        in :meth:`~music21.serial.ToneRow.findZeroCenteredTransformations` and
+        :meth:`~music21.serial.ToneRow.findOriginalCenteredTransformations`.
 
         >>> row = serial.pcToToneRow([5, 9, 11, 3, 6, 7, 4, 10, 0, 8, 2, 1])
         >>> row.findTransformedHistorical('original')
@@ -1045,8 +1045,9 @@ class TwelveToneRow(ToneRow):
         The first and second arguments describe one transformation, while the third and fourth
         describe another. One of the zero-centered or original-centered conventions for tone row
         transformations must be specified in the last argument; see
-        :meth:`~music21.serial.zeroCenteredTransformation` and
-        :meth:`~music21.serial.originalCenteredTransformation` explanations of these conventions.
+        :meth:`~music21.serial.ToneRow.zeroCenteredTransformation` and
+        :meth:`~music21.serial.ToneRow.originalCenteredTransformation` explanations
+        of these conventions.
 
         >>> moses = serial.getHistoricalRowByName('SchoenbergMosesAron')
         >>> moses.pitchClasses()
diff --git a/music21/stream/base.py b/music21/stream/base.py
index c4e39b577..1ea8d8a05 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -2599,7 +2599,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         Given a `target` object, replace it with
         the supplied `replacement` object.
 
-        Does nothing if target cannot be found.
+        Does nothing if target cannot be found. Raises StreamException if replacement
+        is already in the stream.
 
         If `allDerived` is True (as it is by default), all sites (stream) that
         this this stream derives from and also
@@ -2668,6 +2669,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         in stream twice.  recurse and shiftOffsets changed to keywordOnly arguments
 
         Changed in v6 -- recurse works
+
+        Changed in v7 -- raises StreamException if replacement is already in the stream.
         '''
         def replaceDerived(startSite=self):
             if not allDerived:
@@ -2680,6 +2683,14 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                                         recurse=recurse,
                                         allDerived=False)
 
+        try:
+            i = self.index(replacement)
+        except StreamException:
+            # good. now continue.
+            pass
+        else:
+            raise StreamException(f'{replacement} already in {self}')
+
         try:
             i = self.index(target)
         except StreamException:
@@ -4449,6 +4460,15 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 else:
                     out.coreStoreAtEnd(elNew)
 
+        # Replace old measures in spanners with new measures
+        # Example: out is a Part, out.spannerBundle has RepeatBrackets spanning measures
+        # TODO: when dropping support for Py3.9 add strict=True
+        for oldM, newM in zip(
+            self.getElementsByClass('Measure'),
+            out.getElementsByClass('Measure')
+        ):
+            out.spannerBundle.replaceSpannedElement(oldM, newM)
+
         optionalAddRest()
         out.coreElementsChanged()
 
@@ -6296,17 +6316,20 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                  meterStream=None,
                  inPlace=False,
                  displayTiedAccidentals=False,
+                 classFilterList=(note.GeneralNote,),
                  ):
         '''
         Calls :py:func:`~music21.stream.makeNotation.makeTies`.
 
         Changed in v.4., inPlace=False by default.
+        Added in v.7, `classFilterList`.
         '''
         return makeNotation.makeTies(
             self,
             meterStream=meterStream,
             inPlace=inPlace,
             displayTiedAccidentals=displayTiedAccidentals,
+            classFilterList=classFilterList,
         )
 
     def makeBeams(self, *, inPlace=False, setStemDirections=True):
@@ -13910,6 +13933,25 @@ class SpannerStorage(Stream):
     def coreStoreAtEnd(self, element, setActiveSite=True):  # pragma: no cover
         raise StreamException('SpannerStorage cannot store at end.')
 
+    def replace(self,
+                target: base.Music21Object,
+                replacement: base.Music21Object,
+                *,
+                recurse: bool = False,
+                allDerived: bool = True) -> None:
+        '''
+        Overrides :meth:`~music21.stream.Stream.replace` in order to check first
+        whether `replacement` already exists in `self`. If so, delete `target` from
+        `self` and return; otherwise call the superclass method.
+
+        New in v7.
+        '''
+        # Does not perform a recursive search, but shouldn't need to
+        if replacement in self:
+            self.remove(target)
+            return
+        super().replace(target, replacement, recurse=recurse, allDerived=allDerived)
+
 
 class VariantStorage(Stream):
     '''
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index be6ab1ee7..54d1655fc 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -767,27 +767,28 @@ def makeRests(
     4.0
     >>> a.insert(0, meter.TimeSignature('4/4'))
     >>> a.makeMeasures(inPlace=True)
-    >>> a.show('text')
-    {0.0} <music21.stream.Measure 1 offset=0.0>
-        {0.0} <music21.clef.TrebleClef>
-        {0.0} <music21.meter.TimeSignature 4/4>
-    {4.0} <music21.stream.Measure 2 offset=4.0>
-        {0.0} <music21.note.Note C>
-    {8.0} <music21.stream.Measure 3 offset=8.0>
-        {0.0} <music21.note.Note D>
-        {1.0} <music21.bar.Barline type=final>
+    >>> a.show('text', addEndTimes=True)
+    {0.0 - 0.0} <music21.stream.Measure 1 offset=0.0>
+        {0.0 - 0.0} <music21.clef.TrebleClef>
+        {0.0 - 0.0} <music21.meter.TimeSignature 4/4>
+    {4.0 - 5.0} <music21.stream.Measure 2 offset=4.0>
+        {0.0 - 1.0} <music21.note.Note C>
+    {8.0 - 9.0} <music21.stream.Measure 3 offset=8.0>
+        {0.0 - 1.0} <music21.note.Note D>
+        {1.0 - 1.0} <music21.bar.Barline type=final>
     >>> a.makeRests(fillGaps=True, inPlace=True)
-    >>> a.show('text')
-    {0.0} <music21.stream.Measure 1 offset=0.0>
-        {0.0} <music21.clef.TrebleClef>
-        {0.0} <music21.meter.TimeSignature 4/4>
-    {0.0} <music21.note.Rest rest>
-    {4.0} <music21.stream.Measure 2 offset=4.0>
-        {0.0} <music21.note.Note C>
-    {5.0} <music21.note.Rest rest>
-    {8.0} <music21.stream.Measure 3 offset=8.0>
-        {0.0} <music21.note.Note D>
-        {1.0} <music21.bar.Barline type=final>
+    >>> a.show('text', addEndTimes=True)
+    {0.0 - 4.0} <music21.stream.Measure 1 offset=0.0>
+        {0.0 - 0.0} <music21.clef.TrebleClef>
+        {0.0 - 0.0} <music21.meter.TimeSignature 4/4>
+        {0.0 - 4.0} <music21.note.Rest rest>
+    {4.0 - 8.0} <music21.stream.Measure 2 offset=4.0>
+        {0.0 - 1.0} <music21.note.Note C>
+        {1.0 - 4.0} <music21.note.Rest rest>
+    {8.0 - 12.0} <music21.stream.Measure 3 offset=8.0>
+        {0.0 - 1.0} <music21.note.Note D>
+        {1.0 - 4.0} <music21.note.Rest rest>
+        {4.0 - 4.0} <music21.bar.Barline type=final>
 
     Changed in v6 -- all but first attribute are keyword only
 
@@ -811,6 +812,10 @@ def makeRests(
         if timeRangeFromBarDuration and returnObj.isMeasure:
             # NOTE: this will raise an exception if no meter can be found
             oHighTarget = returnObj.barDuration.quarterLength
+        elif timeRangeFromBarDuration and returnObj.hasMeasures():
+            oHighTarget = sum(
+                m.barDuration.quarterLength for m in returnObj.getElementsByClass(stream.Measure)
+            )
         else:
             oHighTarget = returnObj.highestTime
     elif isinstance(refStreamOrTimeRange, stream.Stream):
@@ -828,12 +833,18 @@ def makeRests(
         #    len(refStreamOrTimeRange)])
     if returnObj.hasVoices():
         bundle = list(returnObj.voices)
+    elif returnObj.hasMeasures():
+        bundle = returnObj.getElementsByClass('Measure')
     else:
         bundle = [returnObj]
 
+    # bundle components may be voices, measures, or a flat Stream
     for v in bundle:
         oLow = v.lowestOffset
         oHigh = v.highestTime
+        if returnObj.hasMeasures():
+            # adjust oHigh to not exceed measure
+            oHighTarget = min(v.barDuration.quarterLength, oHighTarget)
 
         # create rest from start to end
         qLen = oLow - oLowTarget
@@ -855,7 +866,6 @@ def makeRests(
             # place at oHigh to reach to oHighTarget
             v.insert(oHigh, r)
 
-
         if fillGaps:
             gapStream = v.findGaps()
             if gapStream is not None:
@@ -866,6 +876,24 @@ def makeRests(
                     v.insert(e.offset, r)
         # environLocal.printDebug(['post makeRests show()', v])
 
+    if returnObj.hasMeasures():
+        # split rests at measure boundaries
+        returnObj.makeTies(classFilterList=(note.Rest,))
+
+        # reposition measures
+        accumulatedTime = 0.0
+        for m in returnObj.getElementsByClass(stream.Measure):
+            returnObj.setElementOffset(m, accumulatedTime)
+            accumulatedTime += m.highestTime
+
+            # process voices
+            for v in m.voices:
+                v.makeRests(inPlace=True,
+                            fillGaps=fillGaps,
+                            hideRests=hideRests,
+                            refStreamOrTimeRange=m,
+                            )
+
     if inPlace is not True:
         return returnObj
 
@@ -875,12 +903,13 @@ def makeTies(
     *,
     meterStream=None,
     inPlace=False,
-    displayTiedAccidentals=False
+    displayTiedAccidentals=False,
+    classFilterList=(note.GeneralNote,),
 ):
     # noinspection PyShadowingNames
     '''
     Given a stream containing measures, examine each element in the
-    Stream. If the elements duration extends beyond the measure's boundary,
+    Stream. If the element's duration extends beyond the measure's boundary,
     create a tied entity, placing the split Note in the next Measure.
 
     Note that this method assumes that there is appropriate space in the
@@ -969,6 +998,40 @@ def makeTies(
     Notes: uses base.Music21Object.splitAtQuarterLength() once it has figured out
     what to split.
 
+    Changed in v. 4 -- inPlace = False by default.
+
+    Changed in v6 -- all but first attribute are keyword only
+
+    Added in v. 7 -- `classFilterList` acts as a filter on what elements will
+    be operated on (i.e. have durations split and/or ties made.)
+    The default `(note.GeneralNote,)` includes Notes, Chords, and Rests.
+
+    Here will we split and make ties only on Notes, leaving the too-long
+    rest in measure 1 alone.
+
+    >>> p = stream.Part()
+    >>> p.append(meter.TimeSignature('2/4'))
+    >>> p.insert(0.0, note.Rest(quarterLength=3.0))
+    >>> p.insert(3.0, note.Note(quarterLength=3.0))
+    >>> p.makeMeasures(inPlace=True)
+    >>> p.makeTies(classFilterList=[note.Note], inPlace=True)
+    >>> p.show('text', addEndTimes=True)
+    {0.0 - 3.0} <music21.stream.Measure 1 offset=0.0>
+        {0.0 - 0.0} <music21.clef.TrebleClef>
+        {0.0 - 0.0} <music21.meter.TimeSignature 2/4>
+        {0.0 - 3.0} <music21.note.Rest rest>
+    {2.0 - 4.0} <music21.stream.Measure 2 offset=2.0>
+        {1.0 - 2.0} <music21.note.Note C>
+    {4.0 - 6.0} <music21.stream.Measure 3 offset=4.0>
+        {0.0 - 2.0} <music21.note.Note C>
+        {2.0 - 2.0} <music21.bar.Barline type=final>
+    >>> p.measure(3).notes[0].tie
+    <music21.tie.Tie stop>
+
+    OMIT_FROM_DOCS
+
+    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
 
@@ -1025,15 +1088,16 @@ def makeTies(
     <music21.note.Note B> None
     <music21.note.Note C> <music21.tie.Tie stop>
 
+    Be helpful and wrap `classFilterList` in a list if need be.
 
-    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
+    >>> m = stream.Measure([note.Note(quarterLength=8.0)])
+    >>> m.insert(0, meter.TimeSignature('4/4'))
+    >>> p = stream.Part([m])
+    >>> p.makeTies(inPlace=True, classFilterList='Note')
+    >>> len(p.getElementsByClass('Measure'))
+    2
+    >>> p.recurse().last().tie
+    <music21.tie.Tie stop>
     '''
     from music21 import stream
 
@@ -1047,6 +1111,9 @@ def makeTies(
     if not returnObj:
         raise stream.StreamException('cannot process an empty stream')
 
+    if not common.isIterable(classFilterList):
+        classFilterList = [classFilterList]
+
     # get measures from this stream
     measureStream = returnObj.getElementsByClass('Measure')
     if not measureStream:
@@ -1134,6 +1201,8 @@ def makeTies(
         # bundle components may be voices, or just a measure
         for v in bundle:
             for e in v:
+                if e.classSet.isdisjoint(classFilterList):
+                    continue
                 vId = v.id
                 # environLocal.printDebug([
                 #    'Stream.makeTies() iterating over elements in measure',


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 3d1165e16..eb18e713d 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -553,14 +553,15 @@ class Test(unittest.TestCase):
         a.append(n)
         self.assertEqual(a.duration.quarterLength, 1.0)
 
-        # Alter the note's duration in a nonstandard way
-        # Normally, one would call Duration.appendTuplet()
         tup = duration.Tuplet()
         n.duration.tuplets = (tup,)
-        # Nonstandard workflow requires coreElementsChanged() to be called
+        self.assertEqual(a.duration.quarterLength, Fraction(2, 3))
+
+        # Also (regression) test clearing the cache
         # https://github.com/cuthbertLab/music21/issues/957
+        n.duration.tuplets = (tup, tup)
         a.coreElementsChanged()
-        self.assertEqual(a.duration.quarterLength, Fraction(2, 3))
+        self.assertEqual(a.duration.quarterLength, Fraction(4, 9))
 
     def testMeasureStream(self):
         '''An approach to setting TimeSignature measures in offsets and durations
@@ -1198,6 +1199,9 @@ class Test(unittest.TestCase):
         stripped = s.stripTies(inPlace=False)
         sn1 = stripped.flat.notes[0]
 
+        self.assertEqual(len(stripped.spanners[0]), 1)
+        self.assertEqual(len(stripped.spanners[1]), 1)
+
         self.assertTrue(stripped.spanners[0].isFirst(sn1))
         self.assertTrue(stripped.spanners[0].isLast(sn1))
         self.assertTrue(stripped.spanners[1].isFirst(sn1))
@@ -1923,9 +1927,7 @@ class Test(unittest.TestCase):
 
         s.insert(0, m1)
         s.insert(4, m2)
-        # must connect Measures to Streams before filling gaps
-        m1.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
-        m2.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
+        s.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
         self.assertTrue(m2.isSorted)
         # m2.sort()
 
@@ -1960,6 +1962,32 @@ class Test(unittest.TestCase):
         # s.show('text')
         # s.show()
 
+    def testMakeRestsInMeasures(self):
+        p = Part()
+        m1 = Measure()
+        m1.timeSignature = meter.TimeSignature('4/4')
+        m1.insert(2, note.Note())
+        m2 = Measure()
+        m2.insert(1, note.Note())
+        p.append(m1)
+        p.append(m2)
+
+        self.assertEqual(m1.duration.quarterLength, 3.0)
+        self.assertEqual(m2.duration.quarterLength, 2.0)
+        self.assertEqual(p.duration.quarterLength, 5.0)
+
+        for m in (m1, m2):
+            m.makeRests(inPlace=True, timeRangeFromBarDuration=True)
+
+        self.assertEqual(m1.duration.quarterLength, 4.0)
+        self.assertEqual(m2.duration.quarterLength, 4.0)
+
+        # m2 was never repositioned in p
+        self.assertEqual(p.duration.quarterLength, 7.0)
+
+        p.makeRests(inPlace=True)
+        self.assertEqual(p.duration.quarterLength, 8.0)
+
     def testMakeMeasuresInPlace(self):
         sScr = Stream()
         sScr.insert(0, clef.TrebleClef())
@@ -2122,6 +2150,10 @@ class Test(unittest.TestCase):
         s.replace(n4, n1)
         self.assertEqual([s[0], s[1]], [n3, n1])
 
+        error_msg = f'{n3} already in {s}'
+        with self.assertRaises(StreamException, msg=error_msg):
+            s.replace(n4, n3)
+
     def testReplaceA1(self):
         sBach = corpus.parse('bach/bwv324.xml')
         partSoprano = sBach.parts.first()