Repository: cuthbertLab/music21
Pull request #1486 (2022-11-25 23:25:33)

Ottava transposition fixes


Patch:
diff --git a/music21/_version.py b/music21/_version.py
index 8b418503b..cd27fd16d 100644
--- a/music21/_version.py
+++ b/music21/_version.py
@@ -47,7 +47,7 @@ Changing this number invalidates old pickles -- do it if the old pickles create
 '''
 from __future__ import annotations
 
-__version__ = '9.0.0a6'
+__version__ = '9.0.0a8'
 
 
 def get_version_tuple(vv):
diff --git a/music21/base.py b/music21/base.py
index 9ada4a35c..f8065670c 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -27,7 +27,7 @@ available after importing `music21`.
 <class 'music21.base.Music21Object'>
 
 >>> music21.VERSION_STR
-'9.0.0a6'
+'9.0.0a8'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
diff --git a/music21/dynamics.py b/music21/dynamics.py
index 69aaa4af1..bb7ba8f9a 100644
--- a/music21/dynamics.py
+++ b/music21/dynamics.py
@@ -348,6 +348,9 @@ class DynamicWedge(spanner.Spanner):
     def __init__(self, *spannedElements, **keywords):
         super().__init__(*spannedElements, **keywords)
 
+        # from music21 import note
+        # self.fillElementTypes = [note.GeneralNote]
+
         self.type = None  # crescendo or diminuendo
         self.placement = 'below'  # can above or below, after musicxml
         self.spread = 15  # this unit is in tenths
diff --git a/music21/expressions.py b/music21/expressions.py
index 5f639108d..0a8664524 100644
--- a/music21/expressions.py
+++ b/music21/expressions.py
@@ -1437,6 +1437,10 @@ class TrillExtension(spanner.Spanner):
     # N.B. this extension always includes a trill symbol
     def __init__(self, *spannedElements, **keywords):
         super().__init__(*spannedElements, **keywords)
+
+        # from music21 import note
+        # self.fillElementTypes = [note.NotRest]
+
         self._placement = None  # can above or below or None, after musicxml
 
     def _getPlacement(self):
@@ -1484,6 +1488,7 @@ class TremoloSpanner(spanner.Spanner):
 
     def __init__(self, *spannedElements, **keywords):
         super().__init__(*spannedElements, **keywords)
+
         self.placement = None
         self.measured = True
         self._numberOfMarks = 3
@@ -1565,6 +1570,7 @@ class ArpeggioMarkSpanner(spanner.Spanner):
                  arpeggioType: str = 'normal',
                  **keywords):
         super().__init__(*spannedElements, **keywords)
+
         if arpeggioType not in ('normal', 'up', 'down', 'non-arpeggio'):
             raise ValueError(
                 'Arpeggio type must be "normal", "up", "down", or "non-arpeggio", '
diff --git a/music21/interval.py b/music21/interval.py
index 234f2a2b7..28abe033f 100644
--- a/music21/interval.py
+++ b/music21/interval.py
@@ -3443,6 +3443,12 @@ class Interval(IntervalBase):
         PRIVATE METHOD: Return p even if inPlace is True
         '''
         # NOTE: this is a performance critical method
+
+        inheritAccidentalDisplayStatus: bool = False
+        if self.simpleName == 'P1' and float(self.semitones) == float(int(self.semitones)):
+            # true unison and any multiple of true octave
+            inheritAccidentalDisplayStatus = True
+
         if p.octave is None:
             useImplicitOctave = True
         else:
@@ -3459,6 +3465,7 @@ class Interval(IntervalBase):
         newStep, newOctave = convertDiatonicNumberToStep(newDiatonicNumber)
         pitch2.step = newStep
         pitch2.octave = newOctave
+        oldPitch2Accidental = pitch2.accidental
         pitch2.accidental = None
         # if this is not set to None then terrible things happen
         pitch2.microtone = None  # type: ignore
@@ -3487,13 +3494,35 @@ class Interval(IntervalBase):
                 # pitch2 = pitchAlt
                 pitch2.ps = pitch2.ps + halfStepsToFix
             else:
-                # cannot import Accidental here.
                 pitch2.accidental = halfStepsToFix  # type:ignore
 
-            # inherit accidental display type etc. but not current status
-            if pitch2.accidental is not None and pitch1.accidental is not None:
-                pitch2.accidental.inheritDisplay(pitch1.accidental)
-                pitch2.accidental.displayStatus = None  # set accidental display to None
+            if not inheritAccidentalDisplayStatus:
+                # inherit accidental display type etc. but not current status
+                if pitch2.accidental is not None and pitch1.accidental is not None:
+                    pitch2.accidental.inheritDisplay(pitch1.accidental)
+                    pitch2.accidental.displayStatus = None  # set accidental display to None
+            else:
+                # inherit all accidental display options (including status)
+                if pitch2.accidental is None:
+                    if pitch1.accidental is not None:
+                        pitch2.accidental = 0  # type:ignore
+                        if t.TYPE_CHECKING:
+                            assert pitch2.accidental is not None
+                        pitch2.accidental.inheritDisplay(pitch1.accidental)
+                else:
+                    if pitch1.accidental is not None:
+                        pitch2.accidental.inheritDisplay(pitch1.accidental)
+                    else:
+                        pitch2.accidental.displayStatus = False
+
+        else:
+            # no halfStepsToFix, so pitch2 is fine as is, but...
+            if inheritAccidentalDisplayStatus:
+                # We have set pitch2.accidental to None, so we might have lost some
+                # display options. So we restore oldPitch2Accidental if that makes sense.
+                if (oldPitch2Accidental is not None
+                        and oldPitch2Accidental.name == 'natural'):
+                    pitch2.accidental = oldPitch2Accidental
 
         if useImplicitOctave is True:
             pitch2.octave = None
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index d16968efd..c2302a15b 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -1517,9 +1517,8 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
         if not s:
             return self.emptyObject()
 
-        if s.atSoundingPitch is True:
-            # A copy was already made or elected NOT to be made.
-            s.toWrittenPitch(inPlace=True)
+        # A copy was already made or elected NOT to be made.
+        s.toWrittenPitch(inPlace=True, ottavasToSounding=True)
 
         self.scorePreliminaries()
 
@@ -2730,8 +2729,7 @@ class PartExporter(XMLExporterBase):
         # unless makeNotation=False, but the user
         # should have called toWrittenPitch() first
         # and is explicitly asking for no further copies to be made
-        if self.stream.atSoundingPitch is True:
-            self.stream.toWrittenPitch(inPlace=True)
+        self.stream.toWrittenPitch(inPlace=True, ottavasToSounding=True)
 
         # Suppose that everything below this is a measure
         if self.makeNotation:
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 0fdec8681..b01281578 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -1571,23 +1571,67 @@ class PartParser(XMLParserBase):
         # will be copied into the Score
 
         # copy spanners that are complete into the part, as this is the
-        # highest level container that needs them
-        rm = []
+        # highest level container that needs them. Ottavas are the exception,
+        # they should be put in the PartStaff that contains the first note
+        # in the Ottava.
+        completedSpanners: list[spanner.Spanner] = []
         for sp in self.spannerBundle.getByCompleteStatus(True):
-            self.stream.coreInsert(0, sp)
-            rm.append(sp)
+            if not isinstance(sp, spanner.Ottava):
+                # don't insert Ottavas, we'll do that after separateOutPartStaves().
+                self.stream.coreInsert(0, sp)
+            completedSpanners.append(sp)
         # remove from original spanner bundle
-        for sp in rm:
+        for sp in completedSpanners:
             self.spannerBundle.remove(sp)
         # s is the score; adding the part to the score
         self.stream.coreElementsChanged()
 
+        partStaves: list[stream.PartStaff] = []
         if self.maxStaves > 1:
-            self.separateOutPartStaves()
+            partStaves = self.separateOutPartStaves()
         else:
             self.stream.addGroupForElements(self.partId)  # set group for components (recurse?)
             self.stream.groups.append(self.partId)  # set group for stream itself
 
+        self._fillAndInsertOttavasInPartStaff(completedSpanners, partStaves)
+
+    def _fillAndInsertOttavasInPartStaff(
+        self,
+        spanners: list[spanner.Spanner],
+        partStaves: list[stream.PartStaff]
+    ):
+        # Ottavas should be filled, so that later transpositions can find all the notes that
+        # should be octave-shifted.  Ottavas should also be inserted into the partStaff that
+        # contains the Ottava's first note.
+        for sp in spanners:
+            if not isinstance(sp, spanner.Ottava):
+                continue
+            spannerPart: stream.Part | None = None
+            if partStaves:
+                spannerPart = self._findFirstPartStaffContaining(sp.getFirst(), partStaves)
+            else:
+                spannerPart = self.stream
+
+            if spannerPart is not None:
+                spannerPart.coreInsert(0, sp)
+                spannerPart.coreElementsChanged()
+                sp.fill(spannerPart)
+
+    def _findFirstPartStaffContaining(
+        self,
+        obj: base.Music21Object | None,
+        partStaves: list[stream.PartStaff]
+    ) -> stream.PartStaff | None:
+        if obj is None:
+            return None
+
+        for partStaff in partStaves:
+            if partStaff.containerInHierarchy(obj, setActiveSite=False) is not None:
+                # obj is somewhere in the hierarchy of this partStaff
+                return partStaff
+
+        return None
+
     def parseXmlScorePart(self):
         '''
         The <score-part> tag contains a lot of information about the
@@ -1812,7 +1856,7 @@ class PartParser(XMLParserBase):
         if lmp.stream.recurse().notesAndRests.last() is endedForwardRest:
             lmp.stream.remove(endedForwardRest, recurse=True)
 
-    def separateOutPartStaves(self) -> None:
+    def separateOutPartStaves(self) -> list[stream.PartStaff]:
         '''
         Take a `Part` with multiple staves and make them a set of `PartStaff` objects.
 
@@ -1832,7 +1876,7 @@ class PartParser(XMLParserBase):
         ]
 
         uniqueStaffKeys: list[int] = self._getUniqueStaffKeys()
-        partStaffs: list[stream.PartStaff] = []
+        partStaves: list[stream.PartStaff] = []
         appendedElementIds: set[int] = set()  # id is id(el) not el.id
 
         def copy_into_partStaff(source: stream.Stream,
@@ -1868,7 +1912,7 @@ class PartParser(XMLParserBase):
             # set group for components (recurse?)
             newPartStaff.addGroupForElements(partStaffId, setActiveSite=False)
             newPartStaff.groups.append(partStaffId)
-            partStaffs.append(newPartStaff)
+            partStaves.append(newPartStaff)
             self.parent.m21PartObjectsById[partStaffId] = newPartStaff
             elementsIdsNotToGoInThisStaff: set[int] = set()
             for staffReference in self.staffReferenceList:
@@ -1889,11 +1933,11 @@ class PartParser(XMLParserBase):
                 copyMeasure.flattenUnnecessaryVoices(force=False, inPlace=True)
 
         score = self.parent.stream
-        staffGroup = layout.StaffGroup(partStaffs, name=self.stream.partName, symbol='brace')
+        staffGroup = layout.StaffGroup(partStaves, name=self.stream.partName, symbol='brace')
         staffGroup.style.hideObjectOnPrint = True  # in truth, hide the name, not the brace
         score.coreInsert(0, staffGroup)
 
-        for partStaff in partStaffs:
+        for partStaff in partStaves:
             score.coreInsert(0, partStaff)
         score.coreElementsChanged()
 
@@ -1901,6 +1945,7 @@ class PartParser(XMLParserBase):
         # and thus that these next two lines are not needed:
         # score.remove(originalPartStaff)
         # del self.parent.m21PartObjectsById[originalPartStaff.id]
+        return partStaves
 
     def _getStaffExclude(
         self,
diff --git a/music21/spanner.py b/music21/spanner.py
index 8061e6270..619b46173 100644
--- a/music21/spanner.py
+++ b/music21/spanner.py
@@ -32,6 +32,7 @@ from music21 import defaults
 from music21 import environment
 from music21 import exceptions21
 from music21 import prebase
+from music21 import sites
 from music21 import style
 
 environLocal = environment.Environment('spanner')
@@ -238,11 +239,27 @@ class Spanner(base.Music21Object):
 
         # parameters that spanners need in loading and processing
         # local id is the id for the local area; used by musicxml
-        self.idLocal = None
+        self.idLocal: str | None = None
         # after all spannedElements have been gathered, setting complete
         # will mark that all parts have been gathered.
         self.completeStatus = False
 
+        # data for fill:
+
+        # fillElementTypes is a list of types of object to search for.  This
+        # can be set to something different in the __init__ of a particular
+        # type of Spanner.
+        # Set here to the empty list, so that by default, fill() does nothing.
+        self.fillElementTypes: list[t.Type] = []
+
+        # After a fill operation, filledStatus will be set to True.
+        # Parsers and other clients can also set this to False or
+        # True to mark whether or not a fill operation is needed
+        # (False means fill is needed, True means fill is not
+        # needed, presumably because the fill was done by hand).
+        # Initialized to 'unknown'.
+        self.filledStatus: bool | t.Literal['unknown'] = 'unknown'
+
     def _reprInternal(self):
         msg = []
         for c in self.getSpannedElements():
@@ -529,6 +546,134 @@ class Spanner(base.Music21Object):
         # environLocal.printDebug(['replaceSpannedElement()', 'id(old)', id(old),
         #    'id(new)', id(new)])
 
+    def fill(
+        self,
+        searchStream=None,  # stream.Stream | None, but cannot import stream here
+        *,
+        includeEndBoundary: bool = False,
+        mustFinishInSpan: bool = False,
+        mustBeginInSpan: bool = True,
+        includeElementsThatEndAtStart: bool = False
+    ):
+        '''
+        Fills in the intermediate elements of a spanner, that are found in searchStream between
+        the first element's offset and the last element's offset+duration.  If searchStream
+        is None, the first element's activeSite is used.  If the first element's activeSite
+        is None, a SpannerException is raised.
+
+        Ottava is an example of a Spanner that can be filled. The Ottava does not need
+        to be inserted into the stream in order to be filled.
+
+        >>> m = stream.Measure([note.Note('A'), note.Note('B'), note.Note('C')])
+        >>> ott1 = spanner.Ottava(m.notes[0], m.notes[2])
+        >>> ott1.fill(m)
+        >>> ott1
+        <music21.spanner.Ottava 8va transposing<...Note A><...Note B><...Note C>>
+
+        If the searchStream is not passed in, fill still happens in this case, because
+        the first note's activeSite is used instead.
+
+        >>> ott2 = spanner.Ottava(m.notes[0], m.notes[2])
+        >>> ott2.fill()
+        >>> ott2
+        <music21.spanner.Ottava 8va transposing<...Note A><...Note B><...Note C>>
+
+        If the searchStream is not passed, and the spanner's first element doesn't have
+        an activeSite, a SpannerException is raised.
+
+        >>> ott3 = spanner.Ottava(note.Note('D'), note.Note('E'))
+        >>> ott3.fill()
+        Traceback (most recent call last):
+        music21.spanner.SpannerException: ...requires a searchStream or getFirst().activeSite
+        '''
+
+        if not self.fillElementTypes:
+            # nothing to fill
+            return
+
+        if self.filledStatus is True:
+            # Don't fill twice.  If client wants a refill they can set filledStatus to False.
+            return
+
+        startElement: base.Music21Object | None = self.getFirst()
+        if startElement is None:
+            # no spanned elements?  Nothing to fill.
+            return
+
+        if searchStream is None:
+            searchStream = startElement.activeSite
+            if searchStream is None:
+                raise SpannerException(
+                    'Spanner.fill() requires a searchStream or getFirst().activeSite'
+                )
+
+        if t.TYPE_CHECKING:
+            from music21 import stream
+            assert isinstance(searchStream, stream.Stream)
+
+        endElement: base.Music21Object | None = self.getLast()
+        if endElement is startElement:
+            endElement = None
+
+        if endElement is not None:
+            # Start and end elements are different; we can't just append everything, we need
+            # to save the end element, remove it, add everything, then add the end element
+            # again.  Note that if there are actually more than 2 elements before we start
+            # filling, the new intermediate elements will come after the existing ones,
+            # regardless of offset.  But first and last will still be the same two elements
+            # as before, which is the most important thing.
+            self.spannerStorage.remove(endElement)
+
+        try:
+            startOffsetInHierarchy: OffsetQL = startElement.getOffsetInHierarchy(searchStream)
+        except sites.SitesException:
+            # print('start element not in searchStream')
+            if endElement is not None:
+                self.addSpannedElements(endElement)
+            return
+
+        endOffsetInHierarchy: OffsetQL
+        if endElement is not None:
+            try:
+                endOffsetInHierarchy = (
+                    endElement.getOffsetInHierarchy(searchStream) + endElement.quarterLength
+                )
+            except sites.SitesException:
+                # print('end element not in searchStream')
+                self.addSpannedElements(endElement)
+                return
+        else:
+            endOffsetInHierarchy = (
+                startOffsetInHierarchy + startElement.quarterLength
+            )
+
+        matchIterator = (searchStream
+            .recurse()
+            .getElementsByOffsetInHierarchy(
+                startOffsetInHierarchy,
+                endOffsetInHierarchy,
+                includeEndBoundary=includeEndBoundary,
+                mustFinishInSpan=mustFinishInSpan,
+                mustBeginInSpan=mustBeginInSpan,
+                includeElementsThatEndAtStart=includeElementsThatEndAtStart)
+            .getElementsByClass(self.fillElementTypes)
+        )
+
+        for foundElement in matchIterator:
+            if foundElement is startElement:
+                # it's already in the spanner, skip it
+                continue
+            if foundElement is endElement:
+                # we'll add it below, skip it
+                continue
+            self.addSpannedElements(foundElement)
+
+        if endElement is not None:
+            # add it back in as the end element
+            self.addSpannedElements(endElement)
+
+        self.filledStatus = True
+
     def isFirst(self, spannedElement):
         '''
         Given a spannedElement, is it first?
@@ -1184,6 +1329,8 @@ class Slur(Spanner):
         super().__init__(*spannedElements, **keywords)
         self.placement = None  # can above or below, after musicxml
         self.lineType = None  # can be 'dashed' or None
+        # from music21 import note
+        # self.fillElementTypes = [note.NotRest]
 
     # TODO: add property for placement
 
@@ -1220,6 +1367,9 @@ class MultiMeasureRest(Spanner):
 
     def __init__(self, *spannedElements, **keywords):
         super().__init__(*spannedElements, **keywords)
+
+        # from music21 import note
+        # self.fillElementTypes = [note.Rest]
         self._overriddenNumber = None
         self.useSymbols = keywords.get('useSymbols', defaults.multiMeasureRestUseSymbols)
         self.maxSymbols = keywords.get('maxSymbols', defaults.multiMeasureRestMaxSymbols)
@@ -1356,6 +1506,10 @@ class RepeatBracket(Spanner):
                  overrideDisplay: str | None = None,
                  **keywords):
         super().__init__(*spannedElements, **keywords)
+
+        # from music21 import stream
+        # self.fillElementTypes = [stream.Measure]
+
         # store a range, inclusive of the single number assignment
         self.numberRange: list[int] = []
         self.overrideDisplay = overrideDisplay
@@ -1531,7 +1685,9 @@ class Ottava(Spanner):
                  transposing: bool = True,
                  placement: t.Literal['above', 'below'] = 'above',
                  **keywords):
+        from music21 import note
         super().__init__(*spannedElements, **keywords)
+        self.fillElementTypes = [note.NotRest]
         self._type = None  # can be 8va, 8vb, 15ma, 15mb
         self.type = type
 
@@ -1640,8 +1796,10 @@ class Ottava(Spanner):
     def performTransposition(self):
         '''
         On a transposing spanner, switch to non-transposing,
-        set style.hideObjectOnPrint to True, and transpose all notes and chords
-        in the spanner.  Called by :meth:`~music21.stream.Stream.toSoundingPitch` in Stream
+        and transpose all notes and chords in the spanner.
+        The note/chords will all be transposed to their sounding
+        pitch (at least as far as the ottava is concerned;
+        transposing instruments are handled separately).
 
         >>> ottava = spanner.Ottava(type='8va')
         >>> n1 = note.Note('D#4')
@@ -1660,7 +1818,6 @@ class Ottava(Spanner):
         if not self.transposing:
             return
         self.transposing = False
-        self.style.hideObjectOnPrint = True
 
         myInterval = self.interval()
         for n in self.getSpannedElements():
@@ -1672,10 +1829,10 @@ class Ottava(Spanner):
     def undoTransposition(self):
         '''
         Change a non-transposing spanner to a transposing spanner,
-        making sure it is not hidden and transpose back all the notes
-        and chords in the spanner.
-
-        Called by :meth:`~music21.stream.Stream.toWrittenPitch` in Stream
+        and transpose back all the notes and chords in the spanner.
+        The notes/chords will all be transposed to their written
+        pitch (at least as far as the ottava is concerned;
+        transposing instruments are handled separately).
 
         >>> ottava = spanner.Ottava(type='8va')
         >>> n1 = note.Note('D#4')
@@ -1693,7 +1850,6 @@ class Ottava(Spanner):
         if self.transposing:
             return
         self.transposing = True
-        self.style.hideObjectOnPrint = False
 
         myInterval = self.interval(reverse=True)
         for n in self.getSpannedElements():
@@ -1733,6 +1889,8 @@ class Line(Spanner):
         **keywords
     ):
         super().__init__(*spannedElements, **keywords)
+        # from music21 import note
+        # self.fillElementTypes = [note.GeneralNote]
 
         DEFAULT_TICK = 'down'
         self._endTick = DEFAULT_TICK  # can be up/down/arrow/both/None
@@ -1892,6 +2050,8 @@ class Glissando(Spanner):
                  label: str | None = None,
                  **keywords):
         super().__init__(*spannedElements, **keywords)
+        # from music21 import note
+        # self.fillElementTypes = [note.NotRest]
 
         GLISSANDO_DEFAULT_LINE_TYPE = 'wavy'
         self._lineType = GLISSANDO_DEFAULT_LINE_TYPE
@@ -2004,11 +2164,90 @@ class Test(unittest.TestCase):
 
         self.assertEqual(sl1.getSpannerSites(), [sp])
 
+    def testSpannerAnchorRepr(self):
+        from music21 import stream
+        from music21 import spanner
+
+        # SpannerAnchor with no activeSite
+        sa1 = spanner.SpannerAnchor()
+        self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor unanchored>')
+
+        # SpannerAnchor with activeSite, but no duration
+        m = stream.Measure()
+        m.insert(0.5, sa1)
+        self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor at 0.5>')
+
+        # SpannerAnchor with activeSite and duration
+        sa1.quarterLength = 2.5
+        self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor at 0.5-3.0>')
+
     def testSpannerRepr(self):
         from music21 import spanner
         su1 = spanner.Slur()
         self.assertEqual(repr(su1), '<music21.spanner.Slur>')
 
+    def testSpannerFill(self):
+        from music21 import stream
+        from music21 import note
+        from music21 import spanner
+        theNotes = [note.Note('A'), note.Note('B'), note.Note('C'), note.Note('D')]
+        m = stream.Measure(theNotes)
+
+        # Spanner with no fillElementTypes
+        sp = spanner.Spanner(theNotes[0], theNotes[3])
+        sp.fill(m)
+        # should not have done anything
+        noFillElements = [theNotes[0], theNotes[3]]
+        self.assertEqual(len(sp), 2)
+        for i, el in enumerate(sp.getSpannedElements()):
+            self.assertIs(el, noFillElements[i])
+
+        # Ottava with filledStatus == True
+        ott1 = spanner.Ottava(noFillElements)
+        ott1.filledStatus = True  # pretend it has already been filled
+        ott1.fill(m)
+        # should not have done anything
+        self.assertEqual(len(sp), 2)
+        for i, el in enumerate(sp.getSpannedElements()):
+            self.assertIs(el, noFillElements[i])
+
+        # same Ottava but with filledStatus == False
+        ott1.filledStatus = False
+        ott1.fill(m)
+        # ott1 should have been filled
+        self.assertIs(ott1.filledStatus, True)
+        self.assertEqual(len(ott1), 4)
+        for i, el in enumerate(ott1.getSpannedElements()):
+            self.assertIs(el, theNotes[i])
+
+        # Ottava with no elements
+        ott2 = spanner.Ottava()
+        ott2.fill(m)
+        self.assertEqual(len(ott2), 0)
+
+        # Ottava with only element not in searchStream
+        expectedElements = [note.Note('E')]
+        ott3 = spanner.Ottava(expectedElements)
+        ott3.fill(m)
+        self.assertEqual(len(ott3), 1)
+        self.assertIs(ott3.getFirst(), expectedElements[0])
+
+        # Ottava with start element not in searchStream, end element is
+        expectedElements = [note.Note('F'), m.notes[0]]
+        ott4 = spanner.Ottava(expectedElements)
+        ott4.fill(m)
+        self.assertEqual(len(ott4), 2)
+        for i, el in enumerate(ott4.getSpannedElements()):
+            self.assertIs(el, expectedElements[i])
+
+        # Ottava with endElement not in searchStream, startElement is
+        expectedElements = [m.notes[0], note.Note('G')]
+        ott5 = spanner.Ottava(expectedElements)
+        ott5.fill(m)
+        self.assertEqual(len(ott5), 2)
+        for i, el in enumerate(ott5.getSpannedElements()):
+            self.assertIs(el, expectedElements[i])
+
     def testSpannerBundle(self):
         from music21 import spanner
         from music21 import stream
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 627b123a5..d962975ac 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -55,6 +55,7 @@ from music21 import key
 from music21 import metadata
 from music21 import meter
 from music21 import note
+from music21 import pitch
 from music21 import tie
 from music21 import repeat
 from music21 import sites
@@ -70,7 +71,6 @@ from music21.stream.enums import GivenElementsBehavior, RecursionType, ShowNumbe
 
 
 if t.TYPE_CHECKING:
-    from music21 import pitch
     from music21 import spanner
 
 
@@ -5218,6 +5218,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         *,
         reverse: bool = False,
         transposeKeySignature: bool = True,
+        preserveAccidentalDisplay: bool = False,
         inPlace: t.Literal[True],
     ) -> None:
         pass
@@ -5228,6 +5229,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         *,
         reverse: bool = False,
         transposeKeySignature: bool = True,
+        preserveAccidentalDisplay: bool = False,
         inPlace: t.Literal[False] = False,
     ) -> StreamType:
         pass
@@ -5237,6 +5239,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         *,
         reverse: bool = False,
         transposeKeySignature: bool = True,
+        preserveAccidentalDisplay: bool = False,
         inPlace: bool = False,
     ) -> StreamType | None:
         '''
@@ -5273,6 +5276,8 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         else:
             classFilterList = (note.Note, chord.Chord)
 
+        displayStatusesAreSet: bool = self.haveAccidentalsBeenMade()
+
         for inst in instrument_stream:
             if inst.transposition is None:
                 continue
@@ -5287,9 +5292,12 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
             trans = inst.transposition
             if reverse:
                 trans = trans.reverse()
-            focus.transpose(trans,
-                            inPlace=True,
-                            classFilterList=classFilterList)
+
+            if preserveAccidentalDisplay and displayStatusesAreSet:
+                with makeNotation.saveAccidentalDisplayStatus(focus):
+                    focus.transpose(trans, inPlace=True, classFilterList=classFilterList)
+            else:
+                focus.transpose(trans, inPlace=True, classFilterList=classFilterList)
 
         # restore original durations
         for inst, original_ql in instrument_map.items():
@@ -5326,7 +5334,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                     at_sounding = site.atSoundingPitch
                     break
             else:
-                raise StreamException('atSoundingPitch is unknown: cannot transpose')
+                return 'unknown'
 
         for substream in self.recurse(streamsOnly=True, includeSelf=False):
             if substream.atSoundingPitch == 'unknown':
@@ -5338,7 +5346,12 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         return at_sounding
 
-    def toSoundingPitch(self, *, inPlace=False):
+    def toSoundingPitch(
+        self,
+        *,
+        preserveAccidentalDisplay: bool = False,
+        inPlace=False
+    ):
         # noinspection PyShadowingNames
         '''
         If not at sounding pitch, transpose all Pitch
@@ -5371,23 +5384,26 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         'C3'
 
         If 'atSoundingPitch' is unknown for this Stream and all of its parent Streams
-        then will raise a StreamException:
+        then no transposition will take place, and atSoundingPitch will remain unknown
+        (this used to raise an exception):
 
         >>> s = stream.Score()
         >>> p = stream.Part(id='partEmpty')
-        >>> s.append(p)
+        >>> s.insert(0.0, p)
         >>> p.toSoundingPitch()
-        Traceback (most recent call last):
-        music21.exceptions21.StreamException: atSoundingPitch is unknown: cannot transpose
+        <music21.stream.Part partEmpty>
         >>> s.atSoundingPitch = False
         >>> sp = p.toSoundingPitch()
         >>> sp
         <music21.stream.Part partEmpty>
+        >>> sp.atSoundingPitch
+        'unknown'
         >>> sp.derivation.origin is p
         True
 
         * Changed in v2.0.10: inPlace is False
         * Changed in v5: returns None if inPlace=True
+        * Changed in v9: no transposition instead of exception if atSoundingPitch is 'unknown'
         '''
         from music21 import spanner
 
@@ -5399,7 +5415,10 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         if returnObj.hasPartLikeStreams() or 'Opus' in returnObj.classSet:
             for partLike in returnObj.getElementsByClass(Stream):
                 # call on each part
-                partLike.toSoundingPitch(inPlace=True)
+                partLike.toSoundingPitch(
+                    inPlace=True,
+                    preserveAccidentalDisplay=preserveAccidentalDisplay
+                )
             returnObj.atSoundingPitch = True
             return returnObj if not inPlace else None
 
@@ -5407,7 +5426,11 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         if at_sounding is False:
             # transposition defined on instrument goes from written to sounding
-            returnObj._transposeByInstrument(reverse=False, inPlace=True)
+            returnObj._transposeByInstrument(
+                reverse=False,
+                preserveAccidentalDisplay=preserveAccidentalDisplay,
+                inPlace=True
+            )
             for container in returnObj.recurse(streamsOnly=True, includeSelf=True):
                 container.atSoundingPitch = True
 
@@ -5417,11 +5440,23 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         if not inPlace:
             return returnObj  # the Stream or None
 
-    def toWrittenPitch(self, *, inPlace=False):
+    def toWrittenPitch(
+        self,
+        *,
+        ottavasToSounding: bool = False,
+        preserveAccidentalDisplay: bool = False,
+        inPlace: bool = False
+    ):
         '''
         If not at written pitch, transpose all Pitch elements to
         written pitch. The atSoundingPitch property is used to
-        determine if transposition is necessary.
+        determine if transposition is necessary.  Note that if
+        ottavasToSounding is True, any notes/chords within
+        an Ottava will _then_ be transposed to sounding Pitch (this
+        is useful for the MusicXML writer, since MusicXML likes
+        all pitches to be written pitches, except for those in
+        ottavas, which should be transposed to written (by instrument)
+        and then transposed to sounding (by ottava).
 
         >>> sc = stream.Score()
         >>> p = stream.Part(id='baritoneSax')
@@ -5446,6 +5481,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         * Changed in v3: `inPlace` defaults to `False`
         * Changed in v5 returns `None` if `inPlace=True`
+        * Changed in v9: no transposition instead of exception if atSoundingPitch is 'unknown'
         '''
         from music21 import spanner
 
@@ -5457,20 +5493,32 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         if returnObj.hasPartLikeStreams() or 'Opus' in returnObj.classes:
             for partLike in returnObj.getElementsByClass('Stream'):
                 # call on each part
-                partLike.toWrittenPitch(inPlace=True)
+                if t.TYPE_CHECKING:
+                    assert isinstance(partLike, Stream)
+                partLike.toWrittenPitch(
+                    inPlace=True,
+                    ottavasToSounding=ottavasToSounding,
+                    preserveAccidentalDisplay=preserveAccidentalDisplay
+                )
             returnObj.atSoundingPitch = False
-            return returnObj if not inPlace else None
-
-        at_sounding = returnObj._treatAtSoundingPitch()
-
-        if at_sounding is True:
-            # need to reverse to go to written
-            returnObj._transposeByInstrument(reverse=True, inPlace=True)
-            for container in returnObj.recurse(streamsOnly=True, includeSelf=True):
-                container.atSoundingPitch = False
+        else:
+            at_sounding = returnObj._treatAtSoundingPitch()
+            if at_sounding is True:
+                # need to reverse to go to written
+                returnObj._transposeByInstrument(
+                    reverse=True,
+                    preserveAccidentalDisplay=preserveAccidentalDisplay,
+                    inPlace=True
+                )
+                for container in returnObj.recurse(streamsOnly=True, includeSelf=True):
+                    container.atSoundingPitch = False
 
-        for ottava in returnObj[spanner.Ottava]:
-            ottava.undoTransposition()
+        if ottavasToSounding:
+            for ottava in returnObj[spanner.Ottava]:
+                ottava.performTransposition()
+        else:
+            for ottava in returnObj[spanner.Ottava]:
+                ottava.undoTransposition()
 
         if not inPlace:
             return returnObj
@@ -8968,10 +9016,16 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         else:
             post = self
 
-        intv: interval.IntervalBase
+        intv: interval.Interval | interval.GenericInterval
         if isinstance(value, (int, str)):
             intv = interval.Interval(value)
+        elif isinstance(value, interval.ChromaticInterval):
+            intv = interval.Interval(chromatic=value)
+        elif isinstance(value, interval.DiatonicInterval):
+            intv = interval.Interval(diatonic=value)
         else:
+            if t.TYPE_CHECKING:
+                assert isinstance(value, (interval.GenericInterval, interval.Interval))
             intv = value
 
         # for p in post.pitches:  # includes chords
@@ -9003,7 +9057,8 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                         for p in e.pitches:  # type: ignore
                             intv.transposePitchKeyAware(p, k, inPlace=True)
                 else:
-                    e.transpose(value, inPlace=True)
+                    e.transpose(intv, inPlace=True)
+
         if not inPlace:
             return post
         else:
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 0bf12508e..14a76a2be 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -14,6 +14,7 @@
 from __future__ import annotations
 
 from collections.abc import Iterable, Generator
+import contextlib
 import copy
 import typing as t
 import unittest
@@ -2063,6 +2064,38 @@ def consolidateCompletedTuplets(
                     last_tuplet = None
                     completion_target = None
 
+@contextlib.contextmanager
+def saveAccidentalDisplayStatus(s) -> t.Generator[None, None, None]:
+    '''
+    Restore accidental displayStatus on a Stream after an (inPlace) operation
+    that sets accidental displayStatus (e.g. a transposition).  Note that you
+    should not do this unless you know that the displayStatus values will still
+    be valid after the operation.
+
+        classList = (key.KeySignature, note.Note, chord.Chord)
+        with saveAccidentalDisplayStatus(s):
+            m.transpose(intv, inPlace=True, classFilterList=classList)
+
+    * New in v9.
+    '''
+    displayStatuses: dict[int, bool | None] = {}
+    for p in s.pitches:
+        if p.accidental is not None:
+            displayStatuses[id(p)] = p.accidental.displayStatus
+            continue
+        displayStatuses[id(p)] = False
+
+    try:
+        yield
+    finally:
+        for p in s.pitches:
+            if p.accidental is not None:
+                p.accidental.displayStatus = displayStatuses.get(id(p), None)
+                continue
+            if displayStatuses.get(id(p), False) is True:
+                p.accidental = pitch.Accidental(0)
+                p.accidental.displayStatus = True
+
 
 # -----------------------------------------------------------------------------
 
@@ -2245,6 +2278,28 @@ class Test(unittest.TestCase):
         self.assertEqual(len(pp[stream.Measure][2].notes), 1)
         self.assertEqual(pp[stream.Measure][2].notes.first().duration.quarterLength, 24.0)
 
+    def testSaveAccidentalDisplayStatus(self):
+        from music21 import interval
+        from music21 import stream
+        m = stream.Measure([key.Key('C'), note.Note('C2'), note.Note('D2')])
+        m.notes[0].pitch.accidental = 0
+        m.notes[0].pitch.accidental.displayStatus = True
+        m.notes[1].pitch.accidental = 0
+        m.notes[1].pitch.accidental.displayStatus = False
+        classList = (note.Note, chord.Chord, key.KeySignature)
+        with saveAccidentalDisplayStatus(m):
+            m.transpose(interval.Interval('m2'), inPlace=True, classFilterList=classList)
+            self.assertEqual(m.notes[0].nameWithOctave, 'D-2')
+            self.assertIsNone(m.notes[0].pitch.accidental.displayStatus)
+            self.assertEqual(m.notes[1].nameWithOctave, 'E-2')
+            self.assertIsNone(m.notes[1].pitch.accidental.displayStatus)
+
+        # After exiting the with statement, accidental displayStatus will have been restored
+        self.assertEqual(m.notes[0].nameWithOctave, 'D-2')
+        self.assertIs(m.notes[0].pitch.accidental.displayStatus, True)
+        self.assertEqual(m.notes[1].nameWithOctave, 'E-2')
+        self.assertIs(m.notes[1].pitch.accidental.displayStatus, False)
+
 
 # -----------------------------------------------------------------------------
 


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index 2ad084766..2e1674a60 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -19047,6 +19047,985 @@ multipleFingeringsOnChord = '''
   </score-partwise>
 '''
 
+pianoStaffWithOttava = '''
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE score-partwise  PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="4.0">
+  <work>
+    <work-title>Ottava example from Piano Sonata no. 7 in D major</work-title>
+  </work>
+  <identification>
+    <creator type="composer">Beethoven, Ludwig van</creator>
+    <encoding>
+      <encoding-date>2023-01-31</encoding-date>
+      <software>music21 v.9.0.0a8</software>
+    </encoding>
+  </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+    </scaling>
+  </defaults>
+  <part-list>
+    <score-part id="Pb8c49cf3a7c0a9788ffa21045c504eb5">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno</part-abbreviation>
+      <score-instrument id="Iae4d13d8fe088971bdc57585144cb786">
+        <instrument-name>Piano</instrument-name>
+        <instrument-abbreviation>Pno</instrument-abbreviation>
+      </score-instrument>
+      <midi-instrument id="Iae4d13d8fe088971bdc57585144cb786">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+      </midi-instrument>
+    </score-part>
+  </part-list>
+  <!--=========================== Part 1 ===========================-->
+  <part id="Pb8c49cf3a7c0a9788ffa21045c504eb5">
+    <!--========================= Measure 63 =========================-->
+    <measure implicit="no" number="63">
+      <attributes>
+        <divisions>10080</divisions>
+        <key>
+          <fifths>-1</fifths>
+          <mode>minor</mode>
+        </key>
+        <time>
+          <beats>6</beats>
+          <beat-type>8</beat-type>
+        </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+        </clef>
+        <clef number="2">
+          <sign>G</sign>
+          <line>2</line>
+        </clef>
+      </attributes>
+      <direction placement="above">
+        <direction-type>
+          <words />
+        </direction-type>
+        <staff>1</staff>
+        <sound tempo="40" />
+      </direction>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift number="1" size="8" type="down" />
+        </direction-type>
+        <staff>1</staff>
+      </direction>
+      <note>
+        <pitch>
+          <step>E</step>
+          <alter>-1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>7560</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <dot />
+        <accidental>flat</accidental>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur number="1" placement="below" type="start" />
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>E</step>
+          <alter>-1</alter>
+          <octave>6</octave>
+        </pitch>
+        <duration>7560</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <dot />
+        <accidental>flat</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <slur number="1" type="stop" />
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>D</step>
+          <octave>6</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>C</step>
+          <alter>0</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <accidental>natural</accidental>
+        <staff>1</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        <notations>
+          <articulations>
+            <staccato placement="below" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>C</step>
+          <alter>0</alter>
+          <octave>6</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <accidental>natural</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>E</step>
+          <alter>-1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <articulations>
+            <staccato placement="below" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>E</step>
+          <alter>-1</alter>
+          <octave>6</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>0</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>7560</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <dot />
+        <accidental>natural</accidental>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur number="2" placement="below" type="start" />
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>F</step>
+          <alter>0</alter>
+          <octave>6</octave>
+        </pitch>
+        <duration>7560</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <dot />
+        <accidental>natural</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>E</step>
+          <alter>0</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <accidental>natural</accidental>
+        <staff>1</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <slur number="2" type="stop" />
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>E</step>
+          <alter>0</alter>
+          <octave>6</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <accidental>natural</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        <notations>
+          <articulations>
+            <staccato placement="below" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>D</step>
+          <octave>6</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <articulations>
+            <staccato placement="below" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>F</step>
+          <octave>6</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+      </note>
+      <backup>
+        <duration>30240</duration>
+      </backup>
+      <direction placement="below">
+        <direction-type>
+          <dynamics valign="middle">
+            <other-dynamics>ffp</other-dynamics>
+          </dynamics>
+        </direction-type>
+        <staff>1</staff>
+        <sound dynamics="63" />
+      </direction>
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <dot />
+        <accidental>sharp</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>A</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <dot />
+        <staff>1</staff>
+      </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics valign="middle">
+            <other-dynamics>ffp</other-dynamics>
+          </dynamics>
+        </direction-type>
+        <staff>1</staff>
+        <sound dynamics="63" />
+      </direction>
+      <note>
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <dot />
+        <accidental>sharp</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>B</step>
+          <alter>0</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <dot />
+        <accidental>natural</accidental>
+        <staff>1</staff>
+      </note>
+      <backup>
+        <duration>30240</duration>
+      </backup>
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>3</voice>
+        <type>quarter</type>
+        <dot />
+        <accidental>sharp</accidental>
+        <staff>2</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>3</voice>
+        <type>quarter</type>
+        <dot />
+        <staff>2</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>3</voice>
+        <type>quarter</type>
+        <dot />
+        <accidental>sharp</accidental>
+        <staff>2</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>B</step>
+          <alter>0</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>15120</duration>
+        <voice>3</voice>
+        <type>quarter</type>
+        <dot />
+        <accidental>natural</accidental>
+        <staff>2</staff>
+      </note>
+      <backup>
+        <duration>30240</duration>
+      </backup>
+      <note>
+        <pitch>
+          <step>C</step>
+          <alter>0</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>7560</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <dot />
+        <accidental>natural</accidental>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur number="3" type="start" />
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>4</voice>
+        <type>16th</type>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <slur number="3" type="stop" />
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>E</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>4</voice>
+        <type>16th</type>
+        <accidental>flat</accidental>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        <notations>
+          <articulations>
+            <staccato placement="above" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>A</step>
+          <octave>3</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>4</voice>
+        <type>16th</type>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <articulations>
+            <staccato placement="above" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>7560</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <dot />
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur number="4" type="start" />
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>E</step>
+          <alter>0</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>4</voice>
+        <type>16th</type>
+        <accidental>natural</accidental>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <slur number="4" type="stop" />
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>F</step>
+          <alter>0</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>4</voice>
+        <type>16th</type>
+        <accidental>natural</accidental>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        <notations>
+          <articulations>
+            <staccato placement="above" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>2520</duration>
+        <voice>4</voice>
+        <type>16th</type>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <articulations>
+            <staccato placement="above" />
+          </articulations>
+        </notations>
+      </note>
+      <barline location="right">
+        <bar-style>regular</bar-style>
+      </barline>
+    </measure>
+    <!--========================= Measure 64 =========================-->
+    <measure implicit="no" number="64">
+      <note>
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>6</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <accidental>sharp</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <rest />
+        <duration>5040</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note print-object="no" print-spacing="yes">
+        <rest />
+        <duration>15120</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <dot />
+        <staff>1</staff>
+      </note>
+      <note print-object="no" print-spacing="yes">
+        <rest />
+        <duration>5040</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note print-object="no" print-spacing="yes">
+        <rest />
+        <duration>5040</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note print-object="no" print-spacing="yes">
+        <rest />
+        <duration>5040</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <backup>
+        <duration>40320</duration>
+      </backup>
+      <note>
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>G</step>
+          <alter>0</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <accidental>natural</accidental>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <accidental>flat</accidental>
+        <staff>1</staff>
+      </note>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift number="1" size="8" type="stop" />
+        </direction-type>
+        <staff>1</staff>
+      </direction>
+      <note>
+        <rest />
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics valign="middle">
+            <p />
+          </dynamics>
+        </direction-type>
+        <staff>1</staff>
+        <sound dynamics="44" />
+      </direction>
+      <note>
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur number="5" type="start" />
+          <articulations>
+            <staccato />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>A</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>D</step>
+          <octave>6</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <slur number="5" type="stop" />
+          <articulations>
+            <staccato />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+      </note>
+      <note>
+        <rest />
+        <duration>10080</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+      </note>
+      <backup>
+        <duration>30240</duration>
+      </backup>
+      <note>
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>5</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <accidental>sharp</accidental>
+        <staff>2</staff>
+      </note>
+      <note>
+        <rest />
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur number="6" placement="below" type="start" />
+          <articulations>
+            <staccato placement="below" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <note>
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <slur number="6" type="stop" />
+          <articulations>
+            <staccato placement="below" />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>3</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <note>
+        <rest />
+        <duration>10080</duration>
+        <voice>3</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+      </note>
+      <backup>
+        <duration>30240</duration>
+      </backup>
+      <note>
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+        <notations>
+          <articulations>
+            <staccato />
+          </articulations>
+        </notations>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>G</step>
+          <alter>0</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <accidental>natural</accidental>
+        <staff>2</staff>
+      </note>
+      <note>
+        <chord />
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+        </pitch>
+        <duration>5040</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <accidental>flat</accidental>
+        <staff>2</staff>
+      </note>
+      <note>
+        <rest />
+        <duration>5040</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <note print-object="no" print-spacing="yes">
+        <rest />
+        <duration>15120</duration>
+        <voice>4</voice>
+        <type>quarter</type>
+        <dot />
+        <staff>2</staff>
+      </note>
+      <note print-object="no" print-spacing="yes">
+        <rest />
+        <duration>5040</duration>
+        <voice>4</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+      </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+      </barline>
+    </measure>
+  </part>
+</score-partwise>
+'''
+
 ALL = [
     articulations01, pitches01a, directions31a, lyricsMelisma61d, notations32a,  # 0
     restsDurations02a, rhythmDurations03a, chordsThreeNotesDuration21c,  # 5
@@ -19064,7 +20043,8 @@ ALL = [
     unicodeStrNoNonAscii, unicodeStrWithNonAscii,  # 44
     tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied, pianoStaffPolymeter,  # 46
     arpeggio32d, multiStaffArpeggios, multiMeasureEnding,   # 51
-    pianoStaffPolymeterWithClefOctaveChange, multipleFingeringsOnChord  # 54
+    pianoStaffPolymeterWithClefOctaveChange, multipleFingeringsOnChord,  # 54
+    pianoStaffWithOttava  # 56
 ]
 
 
diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py
index d2a56724e..760cde743 100644
--- a/music21/musicxml/test_xmlToM21.py
+++ b/music21/musicxml/test_xmlToM21.py
@@ -166,6 +166,26 @@ class Test(unittest.TestCase):
         lastOffset = lastMeasure.elementOffset(lastElement, returnSpecial=True)
         self.assertEqual(lastOffset, 'highestTime')
 
+    def testMultipleStavesInPartWithOttava(self):
+        from music21 import converter
+        from music21.musicxml import testPrimitive
+        s = converter.parse(testPrimitive.pianoStaffWithOttava)
+        self.assertEqual(len(s.getElementsByClass(stream.PartStaff)), 2)
+        ps0 = s[stream.PartStaff][0]
+        self.assertEqual(len(ps0.getElementsByClass(spanner.Ottava)), 1)
+        m0 = ps0[stream.Measure][0]
+        self.assertEqual(
+            [p.nameWithOctave for p in m0.pitches],
+            ['E-5', 'E-6', 'D5', 'D6', 'C5', 'C6', 'E-5', 'E-6', 'F5', 'F6', 'E5', 'E6',
+             'D5', 'D6', 'F5', 'F6', 'F#5', 'A5', 'G#5', 'B5']
+        )
+        s.toWrittenPitch(inPlace=True)
+        self.assertEqual(
+            [p.nameWithOctave for p in m0.pitches],
+            ['E-4', 'E-5', 'D4', 'D5', 'C4', 'C5', 'E-4', 'E-5', 'F4', 'F5', 'E4', 'E5',
+             'D4', 'D5', 'F4', 'F5', 'F#4', 'A4', 'G#4', 'B4']
+        )
+
     def testSpannersA(self):
         from music21 import converter
         from music21.musicxml import testPrimitive
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 5916923c4..1d662b80b 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -4570,14 +4570,11 @@ class Test(unittest.TestCase):
                           ('E#', True), ('E#', False), ('F#', False), ('F#', False)]
                          )
 
-        # transposing should reset all transposed accidentals
         mStream.flatten().transpose('p5', inPlace=True)
 
         # mStream.show()
 
-        # after transposition all accidentals are reset
-        # note: last d# is not showing in Finale, but this seems to be a
-        # finale error, as the musicxml is the same in all D# cases
+        self.maxDiff = None
         self.assertEqual(collectAccidentalDisplayStatus(mStream),
                          ['x', ('G#', None), ('G#', None), 'x', 'x', 'x', 'x',
                           ('B#', None), ('B#', None), ('C#', None), ('F#', None), ('G#', None),
@@ -7411,18 +7408,38 @@ class Test(unittest.TestCase):
         self.assertEqual(str(s.parts[1].getElementsByClass(instrument.Instrument)[0].transposition),
                          '<music21.interval.Interval M-6>')
 
+        # Set each part's first note's natural to be visible, to test that it will remain so
+        # after transposition
+        for p in s.parts:
+            firstPitch = p.pitches[0]
+            self.assertIsNone(firstPitch.accidental)
+            firstPitch.accidental = 0
+            firstPitch.accidental.displayStatus = True
+
         self.assertEqual([str(p) for p in s.parts[0].pitches],
                          ['D4', 'E4', 'F#4', 'G4', 'A4', 'B4', 'C#5', 'D5'])
+        self.assertEqual([None if p.accidental is None else p.accidental.displayStatus
+                            for p in s.parts[0].pitches],
+                         [True, None, False, None, None, None, False, None])
         self.assertEqual([str(p) for p in s.parts[1].pitches],
                          ['A4', 'B4', 'C#5', 'D5', 'E5', 'F#5', 'G#5', 'A5'])
+        self.assertEqual([None if p.accidental is None else p.accidental.displayStatus
+                            for p in s.parts[1].pitches],
+                         [True, None, False, None, None, False, False, None])
 
         self.assertEqual(s.atSoundingPitch, 'unknown')
-        s.toSoundingPitch(inPlace=True)
+        s.toSoundingPitch(inPlace=True, preserveAccidentalDisplay=True)
 
         self.assertEqual([str(p) for p in s.parts[0].pitches],
                          ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'])
+        self.assertEqual([None if p.accidental is None else p.accidental.displayStatus
+                            for p in s.parts[0].pitches],
+                         [True, None, None, None, None, None, None, None])
         self.assertEqual([str(p) for p in s.parts[1].pitches],
                          ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'])
+        self.assertEqual([None if p.accidental is None else p.accidental.displayStatus
+                            for p in s.parts[1].pitches],
+                         [True, None, None, None, None, None, None, None])
 
     def testTransposeByPitchC(self):
         p = converter.parse('tinyNotation: c1 d1')