Repository: cuthbertLab/music21
Pull request #1545 (2023-03-26 19:38:52)

Support ornament accidentals
Fixes issue #1507 


Patch:
diff --git a/music21/_version.py b/music21/_version.py
index bd2fce3cc..53a40cb0b 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.0a9'
+__version__ = '9.0.0a11'
 
 
 def get_version_tuple(vv):
diff --git a/music21/alpha/analysis/ornamentRecognizer.py b/music21/alpha/analysis/ornamentRecognizer.py
index 99a680f0b..40ed0190d 100644
--- a/music21/alpha/analysis/ornamentRecognizer.py
+++ b/music21/alpha/analysis/ornamentRecognizer.py
@@ -131,14 +131,19 @@ class TrillRecognizer(OrnamentRecognizer):
             else:
                 return False
 
-        # set up trill
-        trill = expressions.Trill()
-        trill.quarterLength = self.calculateOrnamentNoteQl(busyNotes, simpleNotes)
-        if isNachschlag:
-            trill.nachschlag = True
-
         if not simpleNotes:
-            trill.size = interval.Interval(noteStart=n1, noteEnd=n2)
+            # set up trill (goes up) or inverted trill (goes down)
+            if n1.pitch.midi <= n2.pitch.midi:
+                trill = expressions.Trill()
+            else:
+                trill = expressions.InvertedTrill()
+            trill.quarterLength = self.calculateOrnamentNoteQl(busyNotes, simpleNotes)
+            if isNachschlag:
+                trill.nachschlag = True
+
+            if n2.pitch.accidental is not None:
+                trill.accidental = n2.pitch.accidental
+            trill.resolveOrnamentalPitches(n1)
             return trill
 
         # currently ignore other notes in simpleNotes
@@ -153,8 +158,19 @@ class TrillRecognizer(OrnamentRecognizer):
         if simpleNote.pitch.midi == n2.pitch.midi:
             endNote = n1
             startNote = n2
-        distance = interval.Interval(noteStart=startNote, noteEnd=endNote)
-        trill.size = distance
+
+        # set up trill (goes up) or inverted trill (goes down)
+        if startNote.pitch.midi <= endNote.pitch.midi:
+            trill = expressions.Trill()
+        else:
+            trill = expressions.InvertedTrill()
+        trill.quarterLength = self.calculateOrnamentNoteQl(busyNotes, simpleNotes)
+        if isNachschlag:
+            trill.nachschlag = True
+
+        if endNote.pitch.accidental is not None:
+            trill.accidental = endNote.pitch.accidental
+        trill.resolveOrnamentalPitches(startNote)
         return trill
 
 class TurnRecognizer(OrnamentRecognizer):
@@ -619,7 +635,14 @@ class Test(unittest.TestCase):
                 # ensure trill is correct
                 self.assertEqual(trill.nachschlag, cond.isNachschlag, cond.name)
                 if cond.ornamentSize:
-                    self.assertEqual(trill.size, cond.ornamentSize, cond.name)
+                    if cond.simpleNotes:
+                        if cond.simpleNotes[0].pitch.midi == cond.busyNotes[1].pitch.midi:
+                            size = trill.getSize(cond.busyNotes[1])
+                        else:
+                            size = trill.getSize(cond.busyNotes[0])
+                    else:
+                        size = trill.getSize(cond.busyNotes[0])
+                    self.assertEqual(size, cond.ornamentSize, cond.name)
             else:
                 self.assertFalse(trill, cond.name)
 
diff --git a/music21/base.py b/music21/base.py
index 2235d1dd7..4a25783f0 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.0a9'
+'9.0.0a11'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
diff --git a/music21/expressions.py b/music21/expressions.py
index 4d97d98eb..f3ad12d53 100644
--- a/music21/expressions.py
+++ b/music21/expressions.py
@@ -20,8 +20,6 @@ A sub-category of Expressions are Ornaments.
 Unlike articulations, expressions can be attached to the Stream itself.
 For instance, TextExpressions.
 '''
-# TODO: replace .size with a string representing interval and then
-#     create interval.Interval objects only when necessary.
 from __future__ import annotations
 
 import copy
@@ -36,6 +34,8 @@ from music21.common.numberTools import opFrac
 from music21.common.types import OffsetQL
 from music21 import exceptions21
 from music21 import interval
+from music21 import key
+from music21 import pitch
 from music21 import spanner
 from music21 import style
 
@@ -44,9 +44,22 @@ if t.TYPE_CHECKING:
     from music21 import note
 
 
-def realizeOrnaments(srcObject):
+def isUnison(intv: interval.IntervalBase) -> bool:
+    if isinstance(intv, interval.Interval):
+        return intv.name == 'P1' and intv.chromatic.semitones == 0
+    elif isinstance(intv, interval.DiatonicInterval):
+        return intv.name == 'P1'
+    elif isinstance(intv, interval.ChromaticInterval):
+        return intv.semitones == 0
+    return False
+
+def realizeOrnaments(
+    srcObj: note.Note | note.Unpitched,
+    *,
+    keySig: key.KeySignature | None = None
+):
     '''
-    given a Music21Object with Ornament expressions,
+    given a Note or Unpitched with Ornament expressions,
     convert them into a list of objects that represents
     the performed version of the object:
 
@@ -60,9 +73,12 @@ def realizeOrnaments(srcObject):
 
     .. image:: images/expressionsMordentRealize.*
          :width: 218
-
-    :type srcObject: base.Music21Object
     '''
+    srcObject: note.Note | note.Unpitched | None = srcObj
+    if t.TYPE_CHECKING:
+        # it comes in as not None
+        assert srcObject is not None
+
     if not hasattr(srcObject, 'expressions'):
         return [srcObject]
     elif not srcObject.expressions:
@@ -73,10 +89,15 @@ def realizeOrnaments(srcObject):
 
         loopBuster = 100
         while loopBuster:
+            if t.TYPE_CHECKING:
+                # if it was set to None, we break out of the loop, so we won't get here
+                assert srcObject is not None
             loopBuster -= 1
             thisExpression = srcObject.expressions[0]
             if hasattr(thisExpression, 'realize'):
-                preExpand, newSrcObject, postExpand = thisExpression.realize(srcObject)
+                preExpand, newSrcObject, postExpand = thisExpression.realize(
+                    srcObject, keySig=keySig
+                )
                 for i in preExpand:
                     preExpandList.append(i)
                 for i in postExpand:
@@ -87,6 +108,9 @@ def realizeOrnaments(srcObject):
                     break
                 newSrcObject.expressions = srcObject.expressions[1:]
                 srcObject = newSrcObject
+                if t.TYPE_CHECKING:
+                    # if newSrcObject/srcObject were None, we would have broken out of the loop
+                    assert srcObject is not None
                 if not srcObject.expressions:
                     break
             else:  # cannot realize this object
@@ -457,21 +481,24 @@ class Ornament(Expression):
     whether to shrink (not currently to expand) the ornament if the
     note it is attached to is too short to realize.
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
         super().__init__(**keywords)
+        # should follow directly on previous; true for most "ornaments".
         self.connectedToPrevious = True
         self.autoScale = True
-        # should follow directly on previous; true for most "ornaments".
+        self._ornamentalPitches: tuple[pitch.Pitch, ...] = ()
 
-    def realize(self,
-                srcObj: note.Note,
-                *,
-                inPlace: bool = False
-                ) -> tuple[list[note.Note],
-                           note.Note | None,
-                           list[note.Note]]:
+    def realize(
+        self,
+        srcObj: note.Note | note.Unpitched,
+        *,
+        keySig: key.KeySignature | None = None,
+        inPlace: bool = False
+    ) -> tuple[list[note.Note | note.Unpitched],
+                note.Note | note.Unpitched | None,
+                list[note.Note | note.Unpitched]]:
         '''
-        subclassable method call that takes a sourceObject
+        subclassable method call that takes a sourceObject and optional keySig
         and returns a three-element tuple of a list of notes before the
         "main note" or the result of the expression if it gobbles up the entire note,
         the "main note" itself (or None) to keep processing for ornaments,
@@ -480,6 +507,8 @@ class Ornament(Expression):
         * New in v8: inPlace boolean; note that some ornaments
           might not return a Note in the second position at all (such as trills)
           so inPlace does nothing.
+        * Changed in v9: Optional keySig can be passed in (useful in cases where there
+          is no keySig in srcObj's context, or where a different keySig is desired).
         '''
         if not inPlace:
             srcObj = copy.deepcopy(srcObj)
@@ -488,8 +517,8 @@ class Ornament(Expression):
 
     def fillListOfRealizedNotes(
         self,
-        srcObj: note.Note,
-        fillObjects: list[note.Note],
+        srcObj: note.Note | note.Unpitched,
+        fillObjects: list[note.Note | note.Unpitched],
         transposeInterval: interval.IntervalBase,
         *,
         useQL: OffsetQL | None = None
@@ -497,7 +526,8 @@ class Ornament(Expression):
         '''
         Used by trills and mordents to fill out their realization.
         '''
-        if not hasattr(srcObj, 'transpose'):
+        isTransposed: bool = not isUnison(transposeInterval)
+        if isTransposed and not hasattr(srcObj, 'transpose'):
             raise TypeError(f'Expected note; got {type(srcObj)}')
 
         if useQL is None:
@@ -512,33 +542,300 @@ class Ornament(Expression):
         secondNote.duration.quarterLength = useQL
         # TODO: remove expressions
         # secondNote.expressions = None
-        secondNote.transpose(transposeInterval, inPlace=True)
+        if isTransposed:
+            if t.TYPE_CHECKING:
+                assert isinstance(secondNote, note.Note)
+            secondNote.transpose(transposeInterval, inPlace=True)
 
         fillObjects.append(firstNote)
         fillObjects.append(secondNote)
 
+    def resolveOrnamentalPitches(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None
+    ):
+        '''
+        Only implemented in Turn, GeneralMordent, and Trill.
+        '''
+        return
+
+    @property
+    def ornamentalPitches(self) -> tuple[pitch.Pitch, ...]:
+        '''
+        Only implemented in Turn, GeneralMordent, and Trill.
+        '''
+        return tuple()
+
+    def updateAccidentalDisplay(
+        self,
+        *,
+        pitchPast: list[pitch.Pitch] | None = None,
+        pitchPastMeasure: list[pitch.Pitch] | None = None,
+        otherSimultaneousPitches: list[pitch.Pitch] | None = None,
+        alteredPitches: list[pitch.Pitch] | None = None,
+        cautionaryPitchClass: bool = True,
+        cautionaryAll: bool = False,
+        overrideStatus: bool = False,
+        cautionaryNotImmediateRepeat: bool = True,
+    ) -> None:
+        '''
+        Only implemented in Turn, GeneralMordent, and Trill.
+        '''
+        return
+
 
 # ------------------------------------------------------------------------------
 class GeneralMordent(Ornament):
     '''
     Base class for all Mordent types.
     '''
-    def __init__(self, **keywords):
+    _direction: str = ''  # up or down
+
+    def __init__(self, *, accidental: pitch.Accidental | None = None, **keywords):
         super().__init__(**keywords)
-        self.direction = ''  # up or down
-        self.size = None  # interval.Interval (General, etc.) class
+        self._accidental: pitch.Accidental | None = accidental
         self.quarterLength = 0.125  # 32nd note default
-        self.size = interval.GenericInterval(2)
         self.placement = 'above'
 
-    def realize(self, srcObj: 'music21.note.Note', *, inPlace=False):
+    @property
+    def name(self) -> str:
+        '''
+        returns the name of the Mordent/InvertedMordent, which is generally
+        the class name lowercased, with spaces where a new capital occurs. The
+        name also will include any accidental, if it exists.
+
+        Subclasses can override this as necessary.
+
+        >>> mordent = expressions.Mordent()
+        >>> mordent.name
+        'mordent'
+
+        >>> sharp = pitch.Accidental('sharp')
+        >>> invertedMordent = expressions.InvertedMordent(accidental=sharp)
+        >>> invertedMordent.name
+        'inverted mordent (sharp)'
+
+        '''
+        theName: str = super().name
+        if self.accidental is not None:
+            theName += ' (' + self.accidental.name + ')'
+        return theName
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        '''
+        This is the GeneralMordent's accidental.
+        '''
+        return self._accidental
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        self._accidental = newAccidental
+
+    @property
+    def direction(self) -> str:
+        '''
+        The direction of the mordent's ornamental pitch from the main note.
+        Can be 'up' or 'down'.
+        '''
+        return self._direction
+
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        '''
+        Returns the size of the mordent's interval, given a source note and
+        an optional key signature.  If the key signature is not specified, the
+        source note's context is searched for the current key signature, and if
+        there is no such key signature, a key signature with no sharps and no flats
+        will be used.  Any `accidental` that has been set on the mordent will also
+        be taken into account.  If no `accidental` has been set, the appropriate
+        accidental from the key signature will be used.
+
+        If keySig is specified, this can be considered to be a theoretical question:
+        "If this particular mordent were to be attached to this note, in this key,
+        what would the size of the mordent interval be?"
+        '''
+        if self._direction not in ('up', 'down'):
+            raise ExpressionException('Cannot compute mordent size if I do not know its direction')
+
+        if not srcObj.pitches:
+            # perfect unison
+            return interval.Interval('P1')
+
+        # Use keySig if passed in, else use keySig from context, else no sharps or flats.
+        keySig = keySig or srcObj.getContextByClass(key.KeySignature) or key.KeySignature(0)
+
+        srcPitch: pitch.Pitch = srcObj.pitches[-1]
+
+        ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        ornamentalPitch.accidental = None
+        if ornamentalPitch.octave is None:
+            ornamentalPitch.octave = ornamentalPitch.implicitOctave
+
+        if self._direction == 'up':
+            ornamentalPitch.transpose(interval.GenericInterval(2), inPlace=True)
+        else:
+            ornamentalPitch.transpose(interval.GenericInterval(-2), inPlace=True)
+
+        if self.accidental:
+            ornamentalPitch.accidental = self.accidental
+        else:
+            # use whatever accidental the key signature says
+            ornamentalPitch.accidental = keySig.accidentalByStep(ornamentalPitch.step)
+
+        return interval.Interval(srcPitch, ornamentalPitch)
+
+    def resolveOrnamentalPitches(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None
+    ):
+        '''
+        Computes and stores the ornamental pitch for a GeneralMordent, given the srcObj
+        (can be any kind of ornamented GeneralNote) and an optional keySig.
+
+        If keySig is None, srcNote's context will be searched for a key signature.
+        If no key signature is found, a key signature with no sharps and no flats
+        will be used.
+
+
+        A mordent on a G in a key with no sharps or flats (ornamental pitch will be F).
+
+        >>> noSharpsOrFlats = key.KeySignature(sharps=0)
+        >>> n1 = note.Note('G4')
+        >>> mordent = expressions.Mordent()
+        >>> mordent.resolveOrnamentalPitches(n1, keySig=noSharpsOrFlats)
+        >>> mordent.ornamentalPitches
+        (<music21.pitch.Pitch F4>,)
+        >>> mordent.ornamentalPitch
+        <music21.pitch.Pitch F4>
+
+        e.g. A mordent on a G in a key with one sharp (ornamental pitch will be F#).
+
+        >>> oneSharp = key.KeySignature(sharps=1)
+        >>> mordent.resolveOrnamentalPitches(n1, keySig=oneSharp)
+        >>> mordent.ornamentalPitches
+        (<music21.pitch.Pitch F#4>,)
+        >>> mordent.ornamentalPitch
+        <music21.pitch.Pitch F#4>
+
+        e.g. A mordent with a natural, on a G, in a key with one sharp
+        (ornamental pitch will be F).
+
+        >>> mordent.accidental = pitch.Accidental('natural')
+        >>> mordent.resolveOrnamentalPitches(n1, keySig=oneSharp)
+        >>> mordent.ornamentalPitches
+        (<music21.pitch.Pitch F4>,)
+        >>> mordent.ornamentalPitch
+        <music21.pitch.Pitch F4>
+        '''
+        if not srcObj.pitches:
+            # There are no ornamental pitches relative to this srcObj
+            return
+
+        srcPitch: pitch.Pitch = srcObj.pitches[-1]
+        transposeInterval: interval.IntervalBase = self.getSize(srcObj, keySig=keySig)
+
+        ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        if ornamentalPitch.octave is None:
+            ornamentalPitch.octave = ornamentalPitch.implicitOctave
+        ornamentalPitch.transpose(transposeInterval, inPlace=True)
+        # if there are microtones, see if they can be converted to quarter tones.
+        if ornamentalPitch.microtone.cents != 0:
+            ornamentalPitch.convertMicrotonesToQuarterTones(inPlace=True)
+
+        if self.accidental is not None:
+            # Note that we don't need to look at what the accidental actually is,
+            # since that has already been incorporated into transposeInterval and the
+            # ornamentalPitch via the call to getSize()/srcPitch.transpose().  But if
+            # accidental is set at all, we need to copy the displayStatus.
+            if ornamentalPitch.accidental is None:
+                ornamentalPitch.accidental = pitch.Accidental(0)
+            ornamentalPitch.accidental.displayStatus = self.accidental.displayStatus
+        self._ornamentalPitches = (ornamentalPitch,)
+
+    @property
+    def ornamentalPitch(self) -> pitch.Pitch | None:
+        '''
+        Returns the mordent's ornamentalPitch.  If resolveOrnamentalPitches
+        has not yet been called, None is returned.
+        '''
+        if self._ornamentalPitches:
+            return self._ornamentalPitches[0]
+        return None
+
+    @property
+    def ornamentalPitches(self) -> tuple[pitch.Pitch, ...]:
+        '''
+        Returns any ornamental pitch that has been resolved (see
+        `resolveOrnamentalPitches`, which must be called first, or an
+        empty tuple will be returned).
+        '''
+        return self._ornamentalPitches
+
+    def updateAccidentalDisplay(
+        self,
+        *,
+        pitchPast: list[pitch.Pitch] | None = None,
+        pitchPastMeasure: list[pitch.Pitch] | None = None,
+        otherSimultaneousPitches: list[pitch.Pitch] | None = None,
+        alteredPitches: list[pitch.Pitch] | None = None,
+        cautionaryPitchClass: bool = True,
+        cautionaryAll: bool = False,
+        overrideStatus: bool = False,
+        cautionaryNotImmediateRepeat: bool = True,
+    ):
+        '''
+        Updates accidental display for a GeneralMordent's ornamental pitch.
+        Defined exactly like Pitch.updateAccidentalDisplay, with two changes:
+        Instead of self being the pitch to update, self is a GeneralMordent whose
+        ornamentalPitch is to be updated; and we pay no attention to ties,
+        since ornamental notes cannot be tied.
+        '''
+        p = self.ornamentalPitch
+        if p is None:
+            return
+
+        if self.accidental is not None and self.accidental.displayStatus is not None:
+            # copy accidental visibility from self.accidental
+            if p.accidental is None:
+                p.accidental = pitch.Accidental(0)
+            p.accidental.displayStatus = self.accidental.displayStatus
+            return
+
+        p.updateAccidentalDisplay(
+            pitchPast=pitchPast,
+            pitchPastMeasure=pitchPastMeasure,
+            alteredPitches=alteredPitches,
+            cautionaryPitchClass=cautionaryPitchClass,
+            cautionaryAll=cautionaryAll,
+            overrideStatus=overrideStatus,
+            cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat,
+            lastNoteWasTied=False)
+
+    def realize(
+        self,
+        srcObj: note.Note | note.Unpitched,
+        *,
+        keySig: key.KeySignature | None = None,
+        inPlace: bool = False
+    ) -> tuple[list[note.Note | note.Unpitched],
+                note.Note | note.Unpitched | None,
+                list[note.Note | note.Unpitched]]:
         '''
         Realize a mordent.
 
         returns a three-element tuple.
         The first is a list of the two notes that the beginning of the note were converted to.
-        The second is the rest of the note
-        The third is an empty list (since there are no notes at the end of a mordent)
+        The second is the rest of the note.
+        The third is an empty list (since there are no notes at the end of a mordent).
 
         >>> n1 = note.Note('C4')
         >>> n1.quarterLength = 0.5
@@ -556,12 +853,8 @@ class GeneralMordent(Ornament):
         music21.expressions.ExpressionException: Cannot realize a mordent if I do not
             know its direction
         '''
-        from music21 import key
-
-        if self.direction not in ('up', 'down'):
+        if self._direction not in ('up', 'down'):
             raise ExpressionException('Cannot realize a mordent if I do not know its direction')
-        if self.size == '':
-            raise ExpressionException('Cannot realize a mordent if there is no size given')
         if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
 
@@ -571,20 +864,28 @@ class GeneralMordent(Ornament):
                 raise ExpressionException('The note is not long enough to realize a mordent')
             use_ql = srcObj.duration.quarterLength / 4
 
+        currentKeySig: key.KeySignature | None = keySig
+        if currentKeySig is None:
+            currentKeySig = srcObj.getContextByClass(key.KeySignature)
+            if currentKeySig is None:
+                currentKeySig = key.KeySignature(0)
+
         remainderQL = srcObj.duration.quarterLength - (2 * use_ql)
-        if self.direction == 'down':
-            transposeInterval = self.size.reverse()
-        else:
-            transposeInterval = self.size
-        mordNotes: list[note.Note] = []
+        transposeInterval = self.getSize(srcObj, keySig=currentKeySig)
+        mordNotes: list[note.Note | note.Unpitched] = []
         self.fillListOfRealizedNotes(srcObj, mordNotes, transposeInterval, useQL=use_ql)
 
-        currentKeySig = srcObj.getContextByClass(key.KeySignature)
-        if currentKeySig is None:
-            currentKeySig = key.KeySignature(0)
+        isTransposed: bool = not isUnison(transposeInterval)
+        if isTransposed:
+            # second (middle) note might need an accidental from the keysig (but
+            # only if it doesn't already have an accidental, from self.accidental)
+            for noteIdx, n in enumerate(mordNotes):
+                if t.TYPE_CHECKING:
+                    assert isinstance(n, note.Note)
+                noteNum: int = noteIdx + 1
+                if n.pitch.accidental is None and noteNum == 2:
+                    n.pitch.accidental = currentKeySig.accidentalByStep(n.pitch.step)
 
-        for n in mordNotes:
-            n.pitch.accidental = currentKeySig.accidentalByStep(n.step)
         inExpressions = -1
         if self in srcObj.expressions:
             inExpressions = srcObj.expressions.index(self)
@@ -610,18 +911,65 @@ class Mordent(GeneralMordent):
         with the note immediately below it.
 
 
+    A mordent has the size of a second, of some form, depending on the note
+    that will have the mordent, the current key signature in that note's context, as
+    well as any accidental on the mordent itself.
+
+    e.g. Mordent without accidentals in default key (no flats or sharps)
+
     >>> m = expressions.Mordent()
     >>> m.direction
     'down'
-    >>> m.size
-    <music21.interval.GenericInterval 2>
+    >>> m.getSize(note.Note('C4'))
+    <music21.interval.Interval m-2>
+    >>> m.getSize(note.Note('B3'))
+    <music21.interval.Interval M-2>
+
+    e.g. Mordent with flat, in default key (no flats or sharps)
+
+    >>> mFlat = expressions.Mordent(accidental=pitch.Accidental('flat'))
+    >>> mFlat.direction
+    'down'
+    >>> mFlat.getSize(note.Note('C4'))
+    <music21.interval.Interval M-2>
+    >>> mFlat.getSize(note.Note('B3'))
+    <music21.interval.Interval A-2>
+
+    e.g. Mordent without accidentals, in key with one flat
+
+    >>> oneFlat = key.KeySignature(-1)
+    >>> mNotFlat = expressions.Mordent()
+    >>> mNotFlat.direction
+    'down'
+    >>> mNotFlat.getSize(note.Note('C4'), keySig=oneFlat)
+    <music21.interval.Interval M-2>
+    >>> mNotFlat.getSize(note.Note('B3'), keySig=oneFlat)
+    <music21.interval.Interval M-2>
+
+    e.g. Mordent without accidentals, with a key from context with one flat (same results)
+
+    >>> noteC4 = note.Note('C4')
+    >>> noteB3 = note.Note('B3')
+    >>> measure = stream.Measure([oneFlat, noteC4, noteB3])
+    >>> mNotFlatWithKeyFromContext = expressions.Mordent()
+    >>> mNotFlatWithKeyFromContext.direction
+    'down'
+    >>> mNotFlatWithKeyFromContext.getSize(noteC4)
+    <music21.interval.Interval M-2>
+    >>> mNotFlatWithKeyFromContext.getSize(noteB3)
+    <music21.interval.Interval M-2>
+
+
 
     * Changed in v7: Mordent sizes are GenericIntervals -- as was originally
       intended but programmed incorrectly.
+    * Changed in v9: Support an accidental on Mordent. This also adds the concept of
+      an ornamental pitch that is processed by makeAccidentals.
+      The size property has been removed and replaced with `.getSize()` (which requires
+      a `srcObj` and optional `keySig` param).  Added optional `keySig` param to
+      `.realize()` as well.
     '''
-    def __init__(self, **keywords):
-        super().__init__(**keywords)
-        self.direction = 'down'  # up or down
+    _direction: str = 'down'  # up or down
 
 
 class HalfStepMordent(Mordent):
@@ -631,12 +979,31 @@ class HalfStepMordent(Mordent):
     >>> m = expressions.HalfStepMordent()
     >>> m.direction
     'down'
-    >>> m.size
-    <music21.interval.Interval m2>
+    >>> m.getSize(note.Note('C4'))
+    <music21.interval.Interval m-2>
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
+        # no accidental supported here, just "HalfStep"
+        if 'accidental' in keywords:
+            raise ExpressionException('Cannot initialize HalfStepMordent with accidental')
         super().__init__(**keywords)
-        self.size = interval.Interval('m2')
+        self._minorSecondDown: interval.IntervalBase = interval.Interval('m-2')
+
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        return self._minorSecondDown
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        return None
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        raise ExpressionException('Cannot set accidental of HalfStepMordent')
 
 
 class WholeStepMordent(Mordent):
@@ -646,12 +1013,31 @@ class WholeStepMordent(Mordent):
     >>> m = expressions.WholeStepMordent()
     >>> m.direction
     'down'
-    >>> m.size
-    <music21.interval.Interval M2>
+    >>> m.getSize(note.Note('C4'))
+    <music21.interval.Interval M-2>
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
+        # no accidental supported here, just "WholeStep"
+        if 'accidental' in keywords:
+            raise ExpressionException('Cannot initialize WholeStepMordent with accidental')
         super().__init__(**keywords)
-        self.size = interval.Interval('M2')
+        self._majorSecondDown: interval.IntervalBase = interval.Interval('M-2')
+
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        return self._majorSecondDown
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        return None
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        raise ExpressionException('Cannot set accidental of WholeStepMordent')
 
 
 # ------------------------------------------------------------------------------
@@ -666,20 +1052,35 @@ class InvertedMordent(GeneralMordent):
         An 18th-century ornament involving alternation of the
         written note with the note immediately above it.
 
-    An inverted mordent has the size of a generic second, of some form.
+    An inverted mordent has the size of a second, of some form, depending on the note
+    that will have the mordent, the current key signature in that note's context, as
+    well as any accidental on the mordent itself.
 
     >>> m = expressions.InvertedMordent()
     >>> m.direction
     'up'
-    >>> m.size
-    <music21.interval.GenericInterval 2>
+    >>> m.getSize(note.Note('C4'))
+    <music21.interval.Interval M2>
+    >>> m.getSize(note.Note('B3'))
+    <music21.interval.Interval m2>
+
+    >>> mSharp = expressions.InvertedMordent(accidental=pitch.Accidental('sharp'))
+    >>> mSharp.direction
+    'up'
+    >>> mSharp.getSize(note.Note('C4'))
+    <music21.interval.Interval A2>
+    >>> mSharp.getSize(note.Note('B3'))
+    <music21.interval.Interval M2>
 
     * Changed in v7: InvertedMordent sizes are GenericIntervals -- as was originally
       intended but programmed incorrectly.
+    * Changed in v9: Support an accidental on InvertedMordent. This also adds the concept of
+      an ornamental pitch that is processed by makeAccidentals.
+      The size property has been removed and replaced with `.getSize()` (which requires
+      a `srcObj` and optional `keySig` param).  Added optional `keySig` param to
+      `.realize()` as well.
     '''
-    def __init__(self, **keywords):
-        super().__init__(**keywords)
-        self.direction = 'up'
+    _direction: str = 'up'
 
 
 class HalfStepInvertedMordent(InvertedMordent):
@@ -689,12 +1090,32 @@ class HalfStepInvertedMordent(InvertedMordent):
     >>> m = expressions.HalfStepInvertedMordent()
     >>> m.direction
     'up'
-    >>> m.size
+    >>> m.getSize(note.Note('C4'))
     <music21.interval.Interval m2>
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
+        # no accidental supported here, just "HalfStep"
+        if 'accidental' in keywords:
+            raise ExpressionException(
+                'Cannot initialize HalfStepInvertedMordent with accidental')
         super().__init__(**keywords)
-        self.size = interval.Interval('m2')
+        self._minorSecondUp: interval.IntervalBase = interval.Interval('m2')
+
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        return self._minorSecondUp
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        return None
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        raise ExpressionException('Cannot set accidental of HalfStepInvertedMordent')
 
 
 class WholeStepInvertedMordent(InvertedMordent):
@@ -704,12 +1125,32 @@ class WholeStepInvertedMordent(InvertedMordent):
     >>> m = expressions.WholeStepInvertedMordent()
     >>> m.direction
     'up'
-    >>> m.size
+    >>> m.getSize(note.Note('C4'))
     <music21.interval.Interval M2>
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
+        # no accidental supported here, just "WholeStep"
+        if 'accidental' in keywords:
+            raise ExpressionException(
+                'Cannot initialize WholeStepInvertedMordent with accidental')
         super().__init__(**keywords)
-        self.size = interval.Interval('M2')
+        self._majorSecondUp: interval.IntervalBase = interval.Interval('M2')
+
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        return self._majorSecondUp
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        return None
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        raise ExpressionException('Cannot set accidental of WholeStepInvertedMordent')
 
 
 # ------------------------------------------------------------------------------
@@ -720,8 +1161,10 @@ class Trill(Ornament):
     >>> tr = expressions.Trill()
     >>> tr.placement
     'above'
-    >>> tr.size
-    <music21.interval.GenericInterval 2>
+    >>> tr.getSize(note.Note('C4'))
+    <music21.interval.Interval M2>
+    >>> tr.getSize(note.Note('B4'))
+    <music21.interval.Interval m2>
 
     Trills have a `.nachschlag` attribute which determines whether there
     should be extra gracenotes at the end of the trill.
@@ -739,17 +1182,67 @@ class Trill(Ornament):
     True
 
     * Changed in v7: the size should be a generic second.
+    * Changed in v9: Support an accidental on trills. This also adds the concept of
+      an ornamental pitch that is processed by makeAccidentals.
+      The size property has been removed and replaced with `.getSize()` (which requires
+      a `srcObj` and optional `keySig` param).  Added optional `keySig` param to
+      `.realize()` as well.
+
     '''
-    def __init__(self, **keywords) -> None:
-        super().__init__(**keywords)
-        self.size: interval.IntervalBase = interval.GenericInterval(2)
+    _direction: str = 'up'
 
+    def __init__(self, *, accidental: pitch.Accidental | None = None, **keywords) -> None:
+        super().__init__(**keywords)
+        self._accidental: pitch.Accidental | None = accidental
         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
 
+    @property
+    def name(self) -> str:
+        '''
+        returns the name of the Trill, which is generally the class name
+        lowercased, with spaces where a new capital occurs. The name also
+        will include the accidental, if it exists.
+
+        Subclasses can override this as necessary.
+
+        >>> trill = expressions.Trill()
+        >>> trill.name
+        'trill'
+
+        >>> doubleSharpedTrill = expressions.Trill(accidental=pitch.Accidental('double-sharp'))
+        >>> doubleSharpedTrill.name
+        'trill (double-sharp)'
+
+        '''
+        theName: str = super().name
+        if self.accidental:
+            theName += ' (' + self.accidental.name + ')'
+        return theName
+
+    @property
+    def direction(self) -> str:
+        '''
+        The direction of the trill's ornamental pitch from the main note.
+        Can be 'up' or 'down'.
+        '''
+        return self._direction
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        '''
+        This is the Trill's accidental.  Whether or not it is visible is dictated by
+        the accidental's displayStatus.
+        '''
+        return self._accidental
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        self._accidental = newAccidental
+
     def splitClient(self, noteList):
         '''
         splitClient is called by base.splitAtQuarterLength() to support splitting trills.
@@ -772,12 +1265,241 @@ class Trill(Ornament):
 
         return returnSpanners
 
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        '''
+        Returns the size of the trill's interval, given a source note and
+        an optional key signature.  If the key signature is not specified, the
+        source note's context is searched for the current key signature, and if
+        there is no such key signature, a key signature with no sharps and no flats
+        will be used.  Any `accidental` that has been set on the trill will also
+        be taken into account.
+
+        If keySig is specified, this can be considered to be a theoretical question:
+        "If this particular trill were to be attached to this note, in this key,
+        what would the size of the trill interval be?"
+        '''
+        if self._direction not in ('up', 'down'):
+            raise ExpressionException('Cannot compute trill size if I do not know its direction')
+
+        if not srcObj.pitches:
+            # perfect unison (e.g. snare drum "trill")
+            return interval.Interval('P1')
+
+        # Use keySig if passed in, else use keySig from context, else no sharps or flats.
+        keySig = keySig or srcObj.getContextByClass(key.KeySignature) or key.KeySignature(0)
+
+        srcPitch: pitch.Pitch = srcObj.pitches[-1]
+
+        ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        ornamentalPitch.accidental = None
+        if ornamentalPitch.octave is None:
+            ornamentalPitch.octave = ornamentalPitch.implicitOctave
+
+        if self._direction == 'up':
+            ornamentalPitch.transpose(interval.GenericInterval(2), inPlace=True)
+        else:
+            ornamentalPitch.transpose(interval.GenericInterval(-2), inPlace=True)
+
+        if self.accidental:
+            ornamentalPitch.accidental = self.accidental
+        else:
+            # use whatever accidental the key signature says
+            ornamentalPitch.accidental = keySig.accidentalByStep(ornamentalPitch.step)
+
+        return interval.Interval(srcPitch, ornamentalPitch)
+
+    def resolveOrnamentalPitches(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None
+    ):
+        '''
+        Computes and stores the ornamental pitch for a Trill, given the srcObj
+        (can be any kind of ornamented GeneralNote) and an optional keySig.
+
+        If keySig is None, srcNote's context will be searched for a key signature.
+        If no key signature is found, a key signature with no sharps and no flats
+        will be used.
+
+        e.g. A trill on a D in a key with no sharps or flats (ornamental pitch will be E).
+
+        >>> noSharpsOrFlats = key.KeySignature(0)
+        >>> n2 = note.Note('D4')
+        >>> trill = expressions.Trill()
+        >>> trill.resolveOrnamentalPitches(n2, keySig=noSharpsOrFlats)
+        >>> trill.ornamentalPitches
+        (<music21.pitch.Pitch E4>,)
+        >>> trill.ornamentalPitch
+        <music21.pitch.Pitch E4>
+        '''
+        if not srcObj.pitches:
+            # There are no ornamental pitches relative to this srcObj
+            return
+
+        srcPitch: pitch.Pitch = srcObj.pitches[-1]
+        transposeInterval: interval.IntervalBase = self.getSize(srcObj, keySig=keySig)
+
+        ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        if ornamentalPitch.octave is None:
+            ornamentalPitch.octave = ornamentalPitch.implicitOctave
+        ornamentalPitch.transpose(transposeInterval, inPlace=True)
+        # if there are microtones, see if they can be converted to quarter tones.
+        if ornamentalPitch.microtone.cents != 0:
+            ornamentalPitch.convertMicrotonesToQuarterTones(inPlace=True)
+
+        if self.accidental is not None:
+            # Note that we don't need to look at what the accidental actually is,
+            # since that has already been incorporated into transposeInterval and the
+            # ornamentalPitch via the call to getSize()/srcPitch.transpose().  But we
+            # do need to copy self.accidental.displayStatus.
+            if ornamentalPitch.accidental is None:
+                ornamentalPitch.accidental = pitch.Accidental(0)
+            ornamentalPitch.accidental.displayStatus = self.accidental.displayStatus
+        self._ornamentalPitches = (ornamentalPitch,)
+
+    @property
+    def ornamentalPitch(self) -> pitch.Pitch | None:
+        '''
+        Returns the trill's ornamentalPitch.  If resolveOrnamentalPitches
+        has not yet been called, None is returned.
+        '''
+        if self._ornamentalPitches:
+            return self._ornamentalPitches[0]
+        return None
+
+    @property
+    def ornamentalPitches(self) -> tuple[pitch.Pitch, ...]:
+        '''
+        Returns any ornamental pitch that has been resolved (see
+        `resolveOrnamentalPitches`, which must be called first, or an
+        empty tuple will be returned).
+        '''
+        return self._ornamentalPitches
+
+    def updateAccidentalDisplay(
+        self,
+        *,
+        pitchPast: list[pitch.Pitch] | None = None,
+        pitchPastMeasure: list[pitch.Pitch] | None = None,
+        otherSimultaneousPitches: list[pitch.Pitch] | None = None,
+        alteredPitches: list[pitch.Pitch] | None = None,
+        cautionaryPitchClass: bool = True,
+        cautionaryAll: bool = False,
+        overrideStatus: bool = False,
+        cautionaryNotImmediateRepeat: bool = True,
+    ):
+        '''
+        Updates accidental display for a Trill's ornamental pitch.
+        Defined exactly like Pitch.updateAccidentalDisplay, with two changes:
+        Instead of self being the pitch to update, self is an Trill whose
+        ornamentalPitch is to be updated; and we pay no attention to ties,
+        since ornamental notes cannot be tied.
+
+        These examples show a Trill whose main note is a G in a key with no sharps or
+        flats, so the trill's ornamental pitch is an A. We show various situations
+        where the A might or might not end up with a natural accidental.
+
+        If updateAccidentalDisplay is called with cautionaryAll, the A gets a (cautionary)
+        natural accidental.
+
+        >>> noSharpsOrFlats = key.KeySignature(0)
+        >>> trill1 = expressions.Trill()
+        >>> trill1.resolveOrnamentalPitches(note.Note('g4'), keySig=noSharpsOrFlats)
+        >>> trill1.ornamentalPitch
+        <music21.pitch.Pitch A4>
+        >>> trill1.ornamentalPitch.accidental is None
+        True
+        >>> past = [pitch.Pitch('a#4'), pitch.Pitch('c#4'), pitch.Pitch('c4')]
+        >>> trill1.updateAccidentalDisplay(pitchPast=past, cautionaryAll=True)
+        >>> trill1.ornamentalPitch.accidental, trill1.ornamentalPitch.accidental.displayStatus
+        (<music21.pitch.Accidental natural>, True)
+
+        If updateAccidentalDisplay is called without cautionaryAll, the A gets a natural
+        accidental, because a previous A had a sharp accidental.
+
+        >>> trill2 = expressions.Trill()
+        >>> trill2.resolveOrnamentalPitches(note.Note('g4'), keySig=noSharpsOrFlats)
+        >>> trill2.ornamentalPitch
+        <music21.pitch.Pitch A4>
+        >>> trill2.ornamentalPitch.accidental is None
+        True
+        >>> past = [pitch.Pitch('a#4'), pitch.Pitch('c#4'), pitch.Pitch('c4')]
+        >>> trill2.updateAccidentalDisplay(pitchPast=past)  # should add a natural
+        >>> trill2.ornamentalPitch.accidental, trill2.ornamentalPitch.accidental.displayStatus
+        (<music21.pitch.Accidental natural>, True)
+
+        If updateAccidentalDisplay is called with cautionaryPitchClass=False, the A does
+        not get a natural accidental because the previous A# was in a different octave.
+
+        >>> trill3 = expressions.Trill()
+        >>> trill3.resolveOrnamentalPitches(note.Note('g4'), keySig=noSharpsOrFlats)
+        >>> trill3.ornamentalPitch
+        <music21.pitch.Pitch A4>
+        >>> trill3.ornamentalPitch.accidental is None
+        True
+        >>> past = [pitch.Pitch('a#3'), pitch.Pitch('c#'), pitch.Pitch('c')]
+        >>> trill3.updateAccidentalDisplay(pitchPast=past, cautionaryPitchClass=False)
+        >>> trill3.ornamentalPitch.accidental is None
+        True
+
+        If we add a natural accidental to the trill (with displayStatus True), and then
+        updateAccidentalDisplay is called with cautionaryPitchClass=False, the A gets a
+        visible natural accidental because of that added natural accidental.
+
+        >>> trill4 = expressions.Trill()
+        >>> natural = pitch.Accidental('natural')
+        >>> natural.displayStatus = True
+        >>> trill4.accidental = natural
+        >>> trill4.resolveOrnamentalPitches(note.Note('g4'), keySig=noSharpsOrFlats)
+        >>> trill4.ornamentalPitch
+        <music21.pitch.Pitch A4>
+        >>> trill4.ornamentalPitch.accidental
+        <music21.pitch.Accidental natural>
+        >>> trill4.ornamentalPitch.accidental.displayStatus
+        True
+        >>> past = [pitch.Pitch('a#3'), pitch.Pitch('c#'), pitch.Pitch('c')]
+        >>> trill4.updateAccidentalDisplay(pitchPast=past, cautionaryPitchClass=False)
+        >>> trill4.ornamentalPitch.accidental
+        <music21.pitch.Accidental natural>
+        >>> trill4.ornamentalPitch.accidental.displayStatus
+        True
+        '''
+        p = self.ornamentalPitch
+        if p is None:
+            return
+
+        if self.accidental:
+            # copy displayStatus from self.accidental
+            if p.accidental is None:
+                p.accidental = pitch.Accidental(0)
+            p.accidental.displayStatus = self.accidental.displayStatus
+            return
+
+        p.updateAccidentalDisplay(
+            pitchPast=pitchPast,
+            pitchPastMeasure=pitchPastMeasure,
+            alteredPitches=alteredPitches,
+            cautionaryPitchClass=cautionaryPitchClass,
+            cautionaryAll=cautionaryAll,
+            overrideStatus=overrideStatus,
+            cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat,
+            lastNoteWasTied=False)
+
     def realize(
         self,
-        srcObj: note.Note,
+        srcObj: note.Note | note.Unpitched,
         *,
+        keySig: key.KeySignature | None = None,
         inPlace: bool = False
-    ) -> tuple[list[note.Note], None, list[note.Note]]:
+    ) -> tuple[list[note.Note | note.Unpitched],
+                note.Note | note.Unpitched | None,
+                list[note.Note | note.Unpitched]]:
         '''
         realize a trill.
 
@@ -874,8 +1596,6 @@ class Trill(Ornament):
 
         inPlace is not used for Trills.
         '''
-        from music21 import key
-
         useQL = self.quarterLength
         if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
@@ -888,28 +1608,38 @@ class Trill(Ornament):
                 raise ExpressionException('The note is not long enough for a nachschlag')
             useQL = srcObj.duration.quarterLength / 4
 
-        transposeInterval = self.size
-        transposeIntervalReverse = self.size.reverse()
+        currentKeySig = keySig
+        if currentKeySig is None:
+            currentKeySig = srcObj.getContextByClass(key.KeySignature)
+            if currentKeySig is None:
+                currentKeySig = key.KeySignature(0)
+
+        transposeInterval = self.getSize(srcObj, keySig=currentKeySig)
+        transposeIntervalReverse = transposeInterval.reverse()
+        isTransposed: bool = not isUnison(transposeInterval)
 
         numberOfTrillNotes = int(srcObj.duration.quarterLength / useQL)
         if self.nachschlag:
             numberOfTrillNotes -= 2
 
-        trillNotes: list[note.Note] = []
+        trillNotes: list[note.Note | note.Unpitched] = []
         for unused_counter in range(int(numberOfTrillNotes / 2)):
             self.fillListOfRealizedNotes(srcObj, trillNotes, transposeInterval, useQL=useQL)
 
-        currentKeySig = None
-        setAccidentalFromKeySig = self._setAccidentalFromKeySig
-        if setAccidentalFromKeySig:
-            currentKeySig = srcObj.getContextByClass(key.KeySignature)
-            if currentKeySig is None:
-                currentKeySig = key.KeySignature(0)
-
-            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 isTransposed:
+            setAccidentalFromKeySig = self._setAccidentalFromKeySig
+            if setAccidentalFromKeySig:
+                for n in trillNotes:
+                    if t.TYPE_CHECKING:
+                        assert isinstance(n, note.Note)
+                        assert isinstance(srcObj, note.Note)
+                    if n.pitch.nameWithOctave != srcObj.pitch.nameWithOctave:
+                        # do not correct original note, no matter what.
+                        if n.pitch.accidental is None:
+                            # correct if there isn't already an accidental (from self.accidental)
+                            n.pitch.accidental = (
+                                currentKeySig.accidentalByStep(n.step)
+                            )
 
         if inPlace and self in srcObj.expressions:
             srcObj.expressions.remove(self)
@@ -922,14 +1652,17 @@ class Trill(Ornament):
             secondNoteNachschlag = copy.deepcopy(srcObj)
             secondNoteNachschlag.expressions = []
             secondNoteNachschlag.duration.quarterLength = useQL
-            secondNoteNachschlag.transpose(transposeIntervalReverse,
-                                           inPlace=True)
-
-            if setAccidentalFromKeySig and currentKeySig:
-                firstNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
-                    firstNoteNachschlag.step)
-                secondNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
-                    secondNoteNachschlag.step)
+            if isTransposed:
+                if t.TYPE_CHECKING:
+                    assert isinstance(secondNoteNachschlag, note.Note)
+                    assert isinstance(firstNoteNachschlag, note.Note)
+                secondNoteNachschlag.transpose(transposeIntervalReverse,
+                                                inPlace=True)
+                if setAccidentalFromKeySig and currentKeySig:
+                    firstNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
+                        firstNoteNachschlag.step)
+                    secondNoteNachschlag.pitch.accidental = currentKeySig.accidentalByStep(
+                        secondNoteNachschlag.step)
 
             nachschlag = [firstNoteNachschlag, secondNoteNachschlag]
 
@@ -939,6 +1672,10 @@ class Trill(Ornament):
             return (trillNotes, None, [])
 
 
+class InvertedTrill(Trill):
+    _direction: str = 'down'
+
+
 class HalfStepTrill(Trill):
     '''
     A trill confined to half steps.
@@ -946,7 +1683,7 @@ class HalfStepTrill(Trill):
     >>> halfTrill = expressions.HalfStepTrill()
     >>> halfTrill.placement
     'above'
-    >>> halfTrill.size
+    >>> halfTrill.getSize(note.Note('C4'))
     <music21.interval.Interval m2>
 
     Here the key signature of 2 sharps will not affect the trill:
@@ -961,11 +1698,31 @@ class HalfStepTrill(Trill):
       <music21.note.Note B>,
       <music21.note.Note C>], None, [])
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
+        # no accidental supported here, just "HalfStep"
+        if 'accidental' in keywords:
+            raise ExpressionException(
+                'Cannot initialize HalfStepTrill with accidental')
         super().__init__(**keywords)
-        self.size = interval.Interval('m2')
+        self._minorSecondUp: interval.IntervalBase = interval.Interval('m2')
         self._setAccidentalFromKeySig = False
 
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        return self._minorSecondUp
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        return None
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        raise ExpressionException('Cannot set accidental of HalfStepTrill')
+
 
 class WholeStepTrill(Trill):
     '''
@@ -974,7 +1731,7 @@ class WholeStepTrill(Trill):
     >>> wholeTrill = expressions.WholeStepTrill()
     >>> wholeTrill.placement
     'above'
-    >>> wholeTrill.size
+    >>> wholeTrill.getSize(note.Note('C4'))
     <music21.interval.Interval M2>
 
     Here the key signature of one sharp will not affect the trill:
@@ -989,11 +1746,31 @@ class WholeStepTrill(Trill):
       <music21.note.Note B>,
       <music21.note.Note C#>], None, [])
     '''
-    def __init__(self, **keywords):
+    def __init__(self, **keywords) -> None:
+        # no accidental supported here, just "WholeStep"
+        if 'accidental' in keywords:
+            raise ExpressionException(
+                'Cannot initialize WholeStepTrill with accidental')
         super().__init__(**keywords)
-        self.size = interval.Interval('M2')
+        self._majorSecondUp: interval.IntervalBase = interval.Interval('M2')
         self._setAccidentalFromKeySig = False
 
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        return self._majorSecondUp
+
+    @property
+    def accidental(self) -> pitch.Accidental | None:
+        return None
+
+    @accidental.setter
+    def accidental(self, newAccidental: pitch.Accidental | None):
+        raise ExpressionException('Cannot set accidental of WholeStepTrill')
+
 
 class Shake(Trill):
     '''
@@ -1031,18 +1808,65 @@ class Turn(Ornament):
 
     * Changed in v7: size is a Generic second.  removed unused nachschlag component.
     * Changed in v9: Added support for delayed vs non-delayed Turn.
+    * Changed in v9: Support upper and lower accidentals on turns. This also adds
+      the concept of ornamental pitches that are processed by makeAccidentals.
+      The size property has been removed and replaced with `.getSize()` (which requires
+      a `srcObj` and optional `keySig` param, as well as which='upper' or which='lower').
+      Added optional `keySig` param to `.realize()` as well.
     '''
-    def __init__(self, *, delay: OrnamentDelay | OffsetQL = OrnamentDelay.NO_DELAY, **keywords):
+    _isInverted: bool = False
+
+    def __init__(
+        self,
+        *,
+        delay: OrnamentDelay | OffsetQL = OrnamentDelay.NO_DELAY,
+        upperAccidental: pitch.Accidental | None = None,
+        lowerAccidental: pitch.Accidental | None = None,
+        **keywords
+    ):
         super().__init__(**keywords)
-        self.size: interval.IntervalBase = interval.GenericInterval(2)
+        self._upperAccidental: pitch.Accidental | None = upperAccidental
+        self._lowerAccidental: pitch.Accidental | None = lowerAccidental
         self.placement: str = 'above'
         self.tieAttach: str = 'all'
         self.quarterLength: OffsetQL = 0.25
         self._delay: OrnamentDelay | OffsetQL = 0.0
         self.delay = delay  # use property setter
 
+    @property
+    def upperAccidental(self) -> pitch.Accidental | None:
+        '''
+        This is the Turn's upperAccidental. Whether or not it is visible is dictated by
+        the upperAccidental's displayStatus.
+        '''
+        return self._upperAccidental
+
+    @upperAccidental.setter
+    def upperAccidental(self, newUpperAccidental: pitch.Accidental | None):
+        self._upperAccidental = newUpperAccidental
+
+    @property
+    def lowerAccidental(self) -> pitch.Accidental | None:
+        '''
+        This is the Turn's lowerAccidental. Whether or not it is visible is dictated by
+        the upperAccidental's displayStatus.
+        '''
+        return self._lowerAccidental
+
+    @lowerAccidental.setter
+    def lowerAccidental(self, newLowerAccidental: pitch.Accidental | None):
+        self._lowerAccidental = newLowerAccidental
+
     @property
     def delay(self) -> OrnamentDelay | OffsetQL:
+        '''
+        If delay is NO_DELAY, the turn is not delayed.
+
+        If delay is anything else (an OffsetQL or DEFAULT_DELAY), the turn is delayed.
+
+        Note that if you set delay to OffsetQL(0), and then get the delay, you will
+        get NO_DELAY, not 0.
+        '''
         return self._delay
 
     @delay.setter
@@ -1054,10 +1878,10 @@ class Turn(Ornament):
 
     @property
     def isDelayed(self) -> bool:
-        # if self.delay is NO_DELAY, the turn is not delayed
-        # if self.delay is anything else (an OffsetQL or DEFAULT_DELAY), the turn is delayed
-        # Note that the implementation of the delay property ensures that if self.delay
-        # is an OffsetQL, it will always be > 0.
+        '''
+        Whether the Turn is delayed (i.e. between a specific note and the following note) or
+        not (i.e. exactly on a specific note).
+        '''
         return self.delay != OrnamentDelay.NO_DELAY
 
     @property
@@ -1066,7 +1890,8 @@ class Turn(Ornament):
         returns the name of the Turn/InvertedTurn, which is generally the class
         name lowercased, with spaces where a new capital occurs, but also with
         a 'delayed' prefix, if the Turn/InvertedTurn is delayed.  If the delay
-        is of a specific duration, the prefix will include that duration.
+        is of a specific duration, the prefix will include that duration. The
+        name also will include upper and lower accidentals, if they exist.
 
         Subclasses can override this as necessary.
 
@@ -1075,23 +1900,263 @@ class Turn(Ornament):
         'turn'
 
         >>> from music21.common.enums import OrnamentDelay
-        >>> delayedInvertedTurn = expressions.InvertedTurn(delay=OrnamentDelay.DEFAULT_DELAY)
+        >>> delayedInvertedTurn = expressions.InvertedTurn(
+        ...     delay=OrnamentDelay.DEFAULT_DELAY,
+        ...     upperAccidental=pitch.Accidental('sharp'),
+        ...     lowerAccidental=pitch.Accidental('natural')
+        ... )
         >>> delayedInvertedTurn.name
-        'delayed inverted turn'
+        'delayed inverted turn (upper=sharp, lower=natural)'
 
-        >>> delayedBy1Turn = expressions.Turn(delay=1.0)
+        >>> delayedBy1Turn = expressions.Turn(
+        ...     delay=1.0, lowerAccidental=pitch.Accidental('double-flat'))
         >>> delayedBy1Turn.name
-        'delayed(delayQL=1.0) turn'
+        'delayed(delayQL=1.0) turn (lower=double-flat)'
 
         '''
-        superName: str = super().name
+        theName: str = super().name
         if self.delay == OrnamentDelay.DEFAULT_DELAY:
-            return 'delayed ' + superName
+            theName = 'delayed ' + theName
         elif isinstance(self.delay, (float, Fraction)):
-            return f'delayed(delayQL={self.delay}) ' + superName
-        return superName
+            theName = f'delayed(delayQL={self.delay}) ' + theName
+
+        if self.upperAccidental is not None or self.lowerAccidental is not None:
+            theName += ' ('
+            if self.upperAccidental is not None:
+                theName += 'upper=' + self.upperAccidental.name
+                if self.lowerAccidental is not None:
+                    theName += ', '
+            if self.lowerAccidental is not None:
+                theName += 'lower=' + self.lowerAccidental.name
+            theName += ')'
+
+        return theName
+
+    def getSize(
+        self,
+        srcObj: note.GeneralNote,
+        which: str,
+        *,
+        keySig: key.KeySignature | None = None,
+    ) -> interval.IntervalBase:
+        '''
+        Returns the size of one of the turn's two intervals (which='upper'
+        or which='lower'), given a source note and an optional key signature.
+        If the key signature is not specified, the source note's context is
+        searched for the current key signature, and if there is no such key
+        signature, a key signature with no sharps and no flats will be used.
+        Any `upperAccidental` or `lowerAccidental` that has been set on the
+        turn will also be taken into account.  If either has not been set,
+        the appropriate accidental from the key signature will be used.
+
+        If keySig is specified, this can be considered to be a theoretical
+        question: "If this particular turn were to be attached to this note,
+        in this key, what would the ('upper' or 'lower') size of the turn
+        interval be?"
+        '''
+        if which not in ('upper', 'lower'):
+            raise ExpressionException(
+                "Turn.getSize requires 'which' parameter be set to 'upper' or 'lower'")
+
+        if not srcObj.pitches:
+            # perfect unison
+            return interval.Interval('P1')
+
+        srcPitch: pitch.Pitch = srcObj.pitches[-1]
+
+        # Use keySig if passed in, else use keySig from context, else no sharps or flats.
+        keySig = keySig or srcObj.getContextByClass(key.KeySignature) or key.KeySignature(0)
+
+        ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        ornamentalPitch.accidental = None
+        if ornamentalPitch.octave is None:
+            ornamentalPitch.octave = ornamentalPitch.implicitOctave
+
+        accidental: pitch.Accidental | None = None
+        if which == 'upper':
+            ornamentalPitch.transpose(interval.GenericInterval(2), inPlace=True)
+            accidental = self.upperAccidental
+        else:
+            ornamentalPitch.transpose(interval.GenericInterval(-2), inPlace=True)
+            accidental = self.lowerAccidental
+
+        if accidental:
+            ornamentalPitch.accidental = accidental
+        else:
+            # use whatever accidental the key signature says
+            ornamentalPitch.accidental = keySig.accidentalByStep(ornamentalPitch.step)
+
+        return interval.Interval(srcPitch, ornamentalPitch)
+
+    def resolveOrnamentalPitches(
+        self,
+        srcObj: note.GeneralNote,
+        *,
+        keySig: key.KeySignature | None = None
+    ):
+        '''
+        Computes and stores the ornamental pitches for a Turn, given the srcObj
+        (can be any kind of ornamented GeneralNote) and an optional keySig.
+
+        If keySig is None, srcNote's context will be searched for a key signature.
+        If no key signature is found, a key signature with no sharps and no flats
+        will be used.
+
+        e.g. A turn on a D in a key with two flats (upper ornamental pitch will be E flat,
+        lower ornamental pitch will be C).
+
+        >>> twoFlats = key.KeySignature(sharps=-2)
+        >>> n1 = note.Note('D4')
+        >>> turn = expressions.Turn()
+        >>> turn.resolveOrnamentalPitches(n1, keySig=twoFlats)
+        >>> turn.ornamentalPitches
+        (<music21.pitch.Pitch E-4>, <music21.pitch.Pitch C4>)
+        >>> turn.upperOrnamentalPitch
+        <music21.pitch.Pitch E-4>
+        >>> turn.lowerOrnamentalPitch
+        <music21.pitch.Pitch C4>
+
+        e.g. A turn with a sharp over it and a flat under it, on a C, in a key with
+        no sharps or flats (upper ornamental pitch will be D#, lower ornamental pitch
+        will be B flat).
+
+        >>> noSharpsOrFlats = key.KeySignature(0)
+        >>> n2 = note.Note('C4')
+        >>> turn = expressions.Turn(
+        ...     upperAccidental=pitch.Accidental('sharp'),
+        ...     lowerAccidental=pitch.Accidental('flat'))
+        >>> turn.resolveOrnamentalPitches(n2, keySig=noSharpsOrFlats)
+        >>> turn.ornamentalPitches
+        (<music21.pitch.Pitch D#4>, <music21.pitch.Pitch B-3>)
+        >>> turn.upperOrnamentalPitch
+        <music21.pitch.Pitch D#4>
+        >>> turn.lowerOrnamentalPitch
+        <music21.pitch.Pitch B-3>
+        '''
+        if not srcObj.pitches:
+            # There are no ornamental pitches relative to this srcObj
+            return
+
+        srcPitch: pitch.Pitch = srcObj.pitches[-1]
+
+        transposeIntervalUp: interval.IntervalBase = self.getSize(
+            srcObj, 'upper', keySig=keySig)
+        transposeIntervalDown: interval.IntervalBase = self.getSize(
+            srcObj, 'lower', keySig=keySig)
+
+        upperPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        if upperPitch.octave is None:
+            upperPitch.octave = upperPitch.implicitOctave
+        upperPitch.transpose(transposeIntervalUp, inPlace=True)
+        # if there are microtones, see if they can be converted to quarter tones.
+        if upperPitch.microtone.cents != 0:
+            upperPitch.convertMicrotonesToQuarterTones(inPlace=True)
+
+        lowerPitch: pitch.Pitch = copy.deepcopy(srcPitch)
+        if lowerPitch.octave is None:
+            lowerPitch.octave = lowerPitch.implicitOctave
+        lowerPitch.transpose(transposeIntervalDown, inPlace=True)
+        # if there are microtones, see if they can be converted to quarter tones.
+        if lowerPitch.microtone.cents != 0:
+            lowerPitch.convertMicrotonesToQuarterTones(inPlace=True)
+
+        # The actual value of upperAccidental (or lowerAccidental) is not
+        # relevant here, since it is already incorporated into the upperPitch
+        # (or lowerPitch) via getSize()/transpose().  We're just looking for
+        # displayStatus.
+        if self.upperAccidental is not None:
+            if upperPitch.accidental is None:
+                upperPitch.accidental = pitch.Accidental(0)
+            upperPitch.accidental.displayStatus = self.upperAccidental.displayStatus
+        if self.lowerAccidental is not None:
+            if lowerPitch.accidental is None:
+                lowerPitch.accidental = pitch.Accidental(0)
+            lowerPitch.accidental.displayStatus = self.lowerAccidental.displayStatus
+
+        # order matters, see upperOrnamentalPitch and lowerOrnamentalPitch properties below
+        self._ornamentalPitches = (upperPitch, lowerPitch)
+
+    @property
+    def upperOrnamentalPitch(self) -> pitch.Pitch | None:
+        '''
+        Returns the turn's upper ornamental pitch.  If resolveOrnamentalPitches
+        has not yet been called, None is returned.
+        '''
+        if len(self._ornamentalPitches) >= 1:
+            return self._ornamentalPitches[0]
+        return None
+
+    @property
+    def lowerOrnamentalPitch(self) -> pitch.Pitch | None:
+        '''
+        Returns the turn's lower ornamental pitch.  If resolveOrnamentalPitches
+        has not yet been called, None is returned.
+        '''
+        if len(self._ornamentalPitches) >= 2:
+            return self._ornamentalPitches[1]
+        return None
+
+    @property
+    def ornamentalPitches(self) -> tuple[pitch.Pitch, ...]:
+        '''
+        Returns any ornamental pitches that have been resolved (see
+        `resolveOrnamentalPitches`, which must be called first, or an
+        empty tuple will be returned).
+        '''
+        return self._ornamentalPitches
+
+    def updateAccidentalDisplay(
+        self,
+        *,
+        pitchPast: list[pitch.Pitch] | None = None,
+        pitchPastMeasure: list[pitch.Pitch] | None = None,
+        otherSimultaneousPitches: list[pitch.Pitch] | None = None,
+        alteredPitches: list[pitch.Pitch] | None = None,
+        cautionaryPitchClass: bool = True,
+        cautionaryAll: bool = False,
+        overrideStatus: bool = False,
+        cautionaryNotImmediateRepeat: bool = True,
+    ):
+        '''
+        Updates accidental display for a Turn's ornamental pitches (upper and lower).
+        Defined exactly like Pitch.updateAccidentalDisplay, with two changes:
+        Instead of self being the pitch to update, self is a Turn whose
+        ornamentalPitches are to be updated; and we pay no attention to ties,
+        since ornamental notes cannot be tied.
+        '''
+        if not self.ornamentalPitches:
+            return
+
+        for p, acc in (
+            (self.upperOrnamentalPitch, self.upperAccidental),
+            (self.lowerOrnamentalPitch, self.lowerAccidental)
+        ):
+            if p is not None:
+                if acc is not None and acc.displayStatus is not None:
+                    # force accidental visibility to be whatever acc's visibility is
+                    if p.accidental is None:
+                        p.accidental = pitch.Accidental(0)
+                    p.accidental.displayStatus = acc.displayStatus
+                else:
+                    p.updateAccidentalDisplay(
+                        pitchPast=pitchPast,
+                        pitchPastMeasure=pitchPastMeasure,
+                        alteredPitches=alteredPitches,
+                        cautionaryPitchClass=cautionaryPitchClass,
+                        cautionaryAll=cautionaryAll,
+                        overrideStatus=overrideStatus,
+                        cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat,
+                        lastNoteWasTied=False)
 
-    def realize(self, srcObj: 'music21.note.Note', *, inPlace=False):
+    def realize(
+        self,
+        srcObj: note.Note | note.Unpitched,
+        *,
+        keySig: key.KeySignature | None = None,
+        inPlace: bool = False
+    ) -> tuple[list[note.Note | note.Unpitched],
+                note.Note | note.Unpitched | None,
+                list[note.Note | note.Unpitched]]:
         # noinspection PyShadowingNames
         '''
         realize a turn.
@@ -1189,13 +2254,17 @@ class Turn(Ornament):
         Traceback (most recent call last):
         music21.expressions.ExpressionException: The note is not long enough to realize a turn
         '''
-        from music21 import key
         useQL = self.quarterLength
-        if self.size is None:
-            raise ExpressionException('Cannot realize a turn if there is no size given')
         if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
 
+        # here we compute transposeIntervals, and invert them if self._isInverted
+        currentKeySig = keySig
+        if currentKeySig is None:
+            currentKeySig = srcObj.getContextByClass(key.KeySignature)
+            if currentKeySig is None:
+                currentKeySig = key.KeySignature(0)
+
         remainderDuration: OffsetQL
         if self.delay == OrnamentDelay.NO_DELAY:
             remainderDuration = 0.0
@@ -1220,15 +2289,24 @@ class Turn(Ornament):
             useQL = self.quarterLength
             fourthNoteQL = opFrac(turnDuration - (3 * useQL))
 
-        transposeIntervalUp = self.size
-        transposeIntervalDown = self.size.reverse()
+        if not self._isInverted:
+            firstTransposeInterval = self.getSize(srcObj, 'upper', keySig=currentKeySig)
+            secondTransposeInterval = self.getSize(srcObj, 'lower', keySig=currentKeySig)
+        else:
+            firstTransposeInterval = self.getSize(srcObj, 'lower', keySig=currentKeySig)
+            secondTransposeInterval = self.getSize(srcObj, 'upper', keySig=currentKeySig)
 
-        turnNotes: list[note.Note] = []
+        # no need to check both intervals, they will both be perfectUnison, or neither will be.
+        isTransposed: bool = not isUnison(firstTransposeInterval)
+        turnNotes: list[note.Note | note.Unpitched] = []
 
         firstNote = copy.deepcopy(srcObj)
         firstNote.expressions = []
         firstNote.duration.quarterLength = useQL
-        firstNote.transpose(transposeIntervalUp, inPlace=True)
+        if isTransposed:
+            if t.TYPE_CHECKING:
+                assert isinstance(firstNote, note.Note)
+            firstNote.transpose(firstTransposeInterval, inPlace=True)
 
         secondNote = copy.deepcopy(srcObj)
         secondNote.expressions = []
@@ -1237,7 +2315,10 @@ class Turn(Ornament):
         thirdNote = copy.deepcopy(srcObj)
         thirdNote.expressions = []
         thirdNote.duration.quarterLength = useQL
-        thirdNote.transpose(transposeIntervalDown, inPlace=True)
+        if isTransposed:
+            if t.TYPE_CHECKING:
+                assert isinstance(thirdNote, note.Note)
+            thirdNote.transpose(secondTransposeInterval, inPlace=True)
 
         fourthNote = copy.deepcopy(srcObj)
         fourthNote.expressions = []
@@ -1251,13 +2332,15 @@ class Turn(Ornament):
         turnNotes.append(thirdNote)
         turnNotes.append(fourthNote)
 
-        currentKeySig = srcObj.getContextByClass(key.KeySignature)
-        if currentKeySig is None:
-            currentKeySig = key.KeySignature(0)
-
-        for n in turnNotes:
-            # TODO: like in trill, do not affect original note.
-            n.pitch.accidental = currentKeySig.accidentalByStep(n.pitch.step)
+        if isTransposed:
+            # first note and third note might need an accidental from the keySig (but
+            # only if they don't already have an accidental from upper/lowerAccidental)
+            for noteIdx, n in enumerate(turnNotes):
+                if t.TYPE_CHECKING:
+                    assert isinstance(n, note.Note)
+                noteNum: int = noteIdx + 1
+                if n.pitch.accidental is None and noteNum in (1, 3):
+                    n.pitch.accidental = currentKeySig.accidentalByStep(n.pitch.step)
 
         inExpressions = -1
         if self in srcObj.expressions:
@@ -1278,9 +2361,7 @@ class Turn(Ornament):
 
 
 class InvertedTurn(Turn):
-    def __init__(self, *, delay: OrnamentDelay | OffsetQL = OrnamentDelay.NO_DELAY, **keywords):
-        super().__init__(delay=delay, **keywords)
-        self.size = self.size.reverse()
+    _isInverted: bool = True
 
 
 # ------------------------------------------------------------------------------
@@ -1292,7 +2373,15 @@ class GeneralAppoggiatura(Ornament):
         super().__init__(**keywords)
         self.size = interval.Interval(2)
 
-    def realize(self, srcObj: 'music21.note.Note', *, inPlace=False):
+    def realize(
+        self,
+        srcObj: note.Note | note.Unpitched,
+        *,
+        keySig: key.KeySignature | None = None,
+        inPlace: bool = False
+    ) -> tuple[list[note.Note | note.Unpitched],
+                note.Note | note.Unpitched | None,
+                list[note.Note | note.Unpitched]]:
         '''
         realize an appoggiatura
 
@@ -1331,7 +2420,11 @@ class GeneralAppoggiatura(Ornament):
 
         appoggiaturaNote = copy.deepcopy(srcObj)
         appoggiaturaNote.duration.quarterLength = newDuration
-        appoggiaturaNote.transpose(transposeInterval, inPlace=True)
+        isTransposed: bool = not isUnison(transposeInterval)
+        if isTransposed:
+            if t.TYPE_CHECKING:
+                assert isinstance(appoggiaturaNote, note.Note)
+            appoggiaturaNote.transpose(transposeInterval, inPlace=True)
 
         inExpressions = -1
         if self in srcObj.expressions:
@@ -1430,7 +2523,15 @@ class Tremolo(Ornament):
                 'Number of marks must be a number from 0 to 8'
             ) from ve
 
-    def realize(self, srcObj: 'music21.note.Note', *, inPlace=False):
+    def realize(
+        self,
+        srcObj: note.Note | note.Unpitched,
+        *,
+        keySig: key.KeySignature | None = None,
+        inPlace: bool = False
+    ) -> tuple[list[note.Note | note.Unpitched],
+                note.Note | note.Unpitched | None,
+                list[note.Note | note.Unpitched]]:
         '''
         Realize the ornament
 
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index c71de9596..afffbb55d 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -1344,10 +1344,9 @@ class XMLExporterBase:
         if mxStaffLayoutIn is None:
             return mxStaffLayout
 
-    def accidentalToMx(self, a):
-        # noinspection PyShadowingNames
+    def accidentalToMx(self, a: pitch.Accidental, *, elName: str = 'accidental') -> Element:
         '''
-        Convert a pitch.Accidental object to a Element of tag accidental
+        Convert a pitch.Accidental object to a Element of tag 'accidental' (or other name)
 
         >>> a = pitch.Accidental()
         >>> a.set('half-sharp')
@@ -1365,8 +1364,8 @@ class XMLExporterBase:
 
 
         >>> a.set('one-and-a-half-sharp')
-        >>> XB.dump(a2m(a))
-        <accidental>three-quarters-sharp</accidental>
+        >>> XB.dump(a2m(a, elName='accidental-mark'))
+        <accidental-mark>three-quarters-sharp</accidental-mark>
 
         >>> a.set('half-flat')
         >>> XB.dump(a2m(a))
@@ -1403,6 +1402,7 @@ class XMLExporterBase:
         >>> XB.dump(a2m(a))
         <accidental>other</accidental>
         '''
+        # noinspection PyShadowingNames
         otherMusicXMLAccidentals = (
             # v. 3.1
             'double-sharp-down', 'double-sharp-up',
@@ -1439,7 +1439,7 @@ class XMLExporterBase:
                     and mxName not in otherMusicXMLAccidentals):
                 mxName = 'other'
 
-        mxAccidental = Element('accidental')
+        mxAccidental = Element(elName)
         # need to remove display in this case and return None
         #         if self.displayStatus == False:
         #             pass
@@ -1453,6 +1453,10 @@ class XMLExporterBase:
         return mxAccidental
 
 
+    def accidentalToMxAccidentalMark(self, a: pitch.Accidental) -> Element:
+        return self.accidentalToMx(a, elName='accidental-mark')
+
+
 # ---------
 
 class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
@@ -4861,6 +4865,39 @@ class MeasureExporter(XMLExporterBase):
             mxExpression = self.expressionToXml(arpeggioMark)
         return mxExpression
 
+    def ornamentToMxAccidentalMarks(self, ornamentObj: expressions.Ornament) -> list[Element]:
+        mxAccidentalMarks: list[Element] = []
+
+        if isinstance(ornamentObj, expressions.Turn):
+            # 0, 1, or 2 accidental marks
+            if (ornamentObj.upperOrnamentalPitch
+                    and ornamentObj.upperOrnamentalPitch.accidental
+                    and ornamentObj.upperOrnamentalPitch.accidental.displayStatus is True):
+                mxAccidentalMarks.append(
+                    self.accidentalToMxAccidentalMark(ornamentObj.upperOrnamentalPitch.accidental)
+                )
+                mxAccidentalMarks[-1].set('placement', 'above')
+            if (ornamentObj.lowerOrnamentalPitch
+                    and ornamentObj.lowerOrnamentalPitch.accidental
+                    and ornamentObj.lowerOrnamentalPitch.accidental.displayStatus is True):
+                mxAccidentalMarks.append(
+                    self.accidentalToMxAccidentalMark(ornamentObj.lowerOrnamentalPitch.accidental)
+                )
+                mxAccidentalMarks[-1].set('placement', 'below')
+        elif isinstance(ornamentObj, (expressions.GeneralMordent, expressions.Trill)):
+            # 0 or 1 accidental marks
+            if (ornamentObj.ornamentalPitch
+                    and ornamentObj.ornamentalPitch.accidental
+                    and ornamentObj.ornamentalPitch.accidental.displayStatus is True):
+                mxAccidentalMarks.append(
+                    self.accidentalToMxAccidentalMark(ornamentObj.ornamentalPitch.accidental)
+                )
+                if ornamentObj.direction == 'down':
+                    mxAccidentalMarks[-1].set('placement', 'below')
+                else:
+                    mxAccidentalMarks[-1].set('placement', 'above')
+
+        return mxAccidentalMarks
 
     def noteToNotations(self, n, noteIndexInChord=0, chordParent=None):
         '''
@@ -4903,6 +4940,12 @@ class MeasureExporter(XMLExporterBase):
                     if mxOrnaments is None:
                         mxOrnaments = Element('ornaments')
                     mxOrnaments.append(mxExpression)
+
+                    mxAccidMarks: list[Element] = (
+                        self.ornamentToMxAccidentalMarks(expObj)
+                    )
+                    for mxAccidMark in mxAccidMarks:
+                        mxOrnaments.append(mxAccidMark)
                     # print(mxExpression)
                 else:
                     notations.append(mxExpression)
@@ -4978,7 +5021,7 @@ class MeasureExporter(XMLExporterBase):
                 notations.append(x)
 
         # TODO: dynamics in notations
-        # TODO: accidental-mark
+        # TODO: accidental-mark (Notations; we've done Ornaments)
         # TODO: other-notation
         return notations
 
@@ -5289,7 +5332,6 @@ class MeasureExporter(XMLExporterBase):
                 ('Mordent', 'mordent'),
                 ('Shake', 'shake'),
                 ('Schleifer', 'schleifer'),
-                # TODO: 'accidental-mark'
                 ('Tremolo', 'tremolo'),  # non-spanner
                 # non-ornaments...
                 ('Fermata', 'fermata'),
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 21bf43531..7253fbad3 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -3788,12 +3788,19 @@ class MeasureParser(XMLParserBase):
                         self.spannerBundle.append(arpeggioSpanner)
                     arpeggioSpanner.addSpannedElements(n)
 
+        mostRecentOrnament: expressions.Ornament | None = None
         for mxObj in flatten(mxNotations, 'ornaments'):
-            if mxObj.tag in xmlObjects.ORNAMENT_MARKS:
-                post = self.xmlOrnamentToExpression(mxObj)
+            if mxObj.tag in xmlObjects.ORNAMENT_MARKS or mxObj.tag == 'accidental-mark':
+                post = self.xmlOrnamentToExpression(
+                    mxObj, mostRecentOrnament=mostRecentOrnament
+                )
+                if mostRecentOrnament is not None and mxObj.tag == 'accidental-mark':
+                    # Resolve any ornamental pitch for that accidental-mark.
+                    mostRecentOrnament.resolveOrnamentalPitches(n)
                 optionalHideObject(post)
                 self.setEditorial(mxNotations, post)
                 if post is not None:
+                    mostRecentOrnament = post
                     n.expressions.append(post)
                 # environLocal.printDebug(['adding to expressions', post])
             elif mxObj.tag == 'wavy-line':
@@ -3986,14 +3993,21 @@ class MeasureParser(XMLParserBase):
             environLocal.printDebug(f'Cannot translate {tag} in {mxObj}.')
             return None
 
-    def xmlOrnamentToExpression(self, mxObj):
+    def xmlOrnamentToExpression(
+        self,
+        mxObj,
+        *,
+        mostRecentOrnament: expressions.Ornament | None = None
+    ):
         '''
         Convert mxOrnament into a music21 ornament.
 
         This only processes non-spanner ornaments.
         Many mxOrnaments are spanners: these are handled elsewhere.
 
-        Returns None if it cannot be converted or is not defined.
+        Returns None if it cannot be converted or is not defined, or if the
+        mxObj is an accidental-mark (in which case the accidental is placed
+        in the mostRecentOrnament instead).
 
         Return an articulation from an mxObj, setting placement
 
@@ -4016,9 +4030,44 @@ class MeasureParser(XMLParserBase):
         >>> a is None
         True
 
-        Not supported currently: 'accidental-mark', 'vertical-turn'
+        If it is 'accidental-mark', add to mostRecentOrnament, and return None
+
+        >>> turn = expressions.Turn()
+        >>> turn.lowerAccidental is None
+        True
+        >>> turn.upperAccidental is None
+        True
+        >>> mxOrn = EL('<accidental-mark placement="below">flat</accidental-mark>')
+        >>> a = MP.xmlOrnamentToExpression(mxOrn, mostRecentOrnament=turn)
+        >>> a is None
+        True
+        >>> turn.lowerAccidental
+        <music21.pitch.Accidental flat>
+        >>> turn.upperAccidental is None
+        True
+
+        Not supported currently: 'vertical-turn'
         '''
         tag = mxObj.tag
+        if tag == 'accidental-mark':
+            if mostRecentOrnament is None:
+                return None
+
+            accid: pitch.Accidental = self.xmlToAccidental(mxObj)
+            accid.displayStatus = True
+
+            if isinstance(mostRecentOrnament, expressions.Turn):
+                # upperAccidentalName or lowerAccidentalName?
+                # Look at placement (default to 'above').
+                placement: str = mxObj.get('placement', 'above')
+                if placement == 'below':
+                    mostRecentOrnament.lowerAccidental = accid
+                else:
+                    mostRecentOrnament.upperAccidental = accid
+            elif isinstance(mostRecentOrnament, (expressions.GeneralMordent, expressions.Trill)):
+                mostRecentOrnament.accidental = accid
+            return None
+
         try:
             if tag in ('delayed-turn', 'delayed-inverted-turn'):
                 orn = xmlObjects.ORNAMENT_MARKS[tag](delay=OrnamentDelay.DEFAULT_DELAY)
diff --git a/music21/pitch.py b/music21/pitch.py
index cb6a07ce2..dfea13742 100644
--- a/music21/pitch.py
+++ b/music21/pitch.py
@@ -105,6 +105,8 @@ accidentalNameToModifier = {
     'one-and-a-half-flat': '-`',
 }
 
+modifierToAccidentalName = {v: k for k, v in accidentalNameToModifier.items()}
+
 unicodeFromModifier = OrderedDict([
     ('####', chr(0x1d12a) + chr(0x1d12a)),
     ('###', '\u266f' + chr(0x1d12a)),
@@ -121,6 +123,33 @@ unicodeFromModifier = OrderedDict([
     ('', '\u266e'),  # natural
 ])
 
+alternateNameToAccidentalName = {
+    'n': 'natural',
+    'is': 'sharp',
+    'isis': 'double-sharp',
+    'isisis': 'triple-sharp',
+    'isisisis': 'quadruple-sharp',
+    'es': 'flat',
+    'b': 'flat',
+    'eses': 'double-flat',
+    'eseses': 'triple-flat',
+    'eseseses': 'quadruple-flat',
+    'quarter-sharp': 'half-sharp',
+    'ih': 'half-sharp',
+    'semisharp': 'half-sharp',
+    'three-quarter-sharp': 'one-and-a-half-sharp',
+    'three-quarters-sharp': 'one-and-a-half-sharp',
+    'isih': 'one-and-a-half-sharp',
+    'sesquisharp': 'one-and-a-half-sharp',
+    'quarter-flat': 'half-flat',
+    'eh': 'half-flat',
+    'semiflat': 'half-flat',
+    'three-quarter-flat': 'one-and-a-half-flat',
+    'three-quarters-flat': 'one-and-a-half-flat',
+    'eseh': 'one-and-a-half-flat',
+    'sesquiflat': 'one-and-a-half-flat',
+}
+
 
 # sort modifiers by length, from longest to shortest
 def _sortModifiers():
@@ -134,6 +163,78 @@ def _sortModifiers():
 
 accidentalModifiersSorted = _sortModifiers()
 
+def isValidAccidentalName(name: str) -> bool:
+    '''
+    Check if name is a valid accidental name string that can
+    be used to initialize an Accidental.
+
+    Standard accidental names are valid:
+
+    >>> pitch.isValidAccidentalName('double-flat')
+    True
+
+    Accidental modifiers are valid:
+
+    >>> pitch.isValidAccidentalName('--')
+    True
+
+    Alternate accidental names are valid:
+
+    >>> pitch.isValidAccidentalName('eses')
+    True
+
+    Anything else is not valid:
+
+    >>> pitch.isValidAccidentalName('two flats')
+    False
+    '''
+    # check against official names
+    if name in accidentalNameToModifier:
+        return True
+
+    # check against official modifiers
+    if name in accidentalNameToModifier.values():
+        return True
+
+    # check against alternate supported names
+    if name in alternateNameToAccidentalName:
+        return True
+
+    return False
+
+def standardizeAccidentalName(name: str) -> str:
+    '''
+    Convert a valid accidental name to the standard accidental name.
+    Raises AccidentalException if name is not a valid accidental name.
+
+    >>> pitch.standardizeAccidentalName('double-flat')
+    'double-flat'
+
+    >>> pitch.standardizeAccidentalName('--')
+    'double-flat'
+
+    >>> pitch.standardizeAccidentalName('eses')
+    'double-flat'
+
+    >>> pitch.standardizeAccidentalName('two flats')
+    Traceback (most recent call last):
+    music21.pitch.AccidentalException: 'two flats' is not a supported accidental type
+    '''
+    if name in accidentalNameToModifier:
+        # it is already standardized, just return it
+        return name
+
+    if name in modifierToAccidentalName:
+        # it is a modifier, look up standardized name
+        return modifierToAccidentalName[name]
+
+    if name in alternateNameToAccidentalName:
+        # it is an alternate name, look up standardized name
+        return alternateNameToAccidentalName[name]
+
+    raise AccidentalException(f"'{name}' is not a supported accidental type")
+
+
 
 # ------------------------------------------------------------------------------
 # utility functions
@@ -4460,6 +4561,18 @@ class Pitch(prebase.ProtoM21Object):
         >>> pc6.transpose(10, inPlace=True)
         >>> pc6.spellingIsInferred
         True
+
+        Test an issue with inPlace not setting microtone.
+
+        >>> flatAndAHalf = pitch.Accidental('one-and-a-half-flat')
+        >>> dFlatAndAHalf = pitch.Pitch('D2')
+        >>> dFlatAndAHalf.accidental = flatAndAHalf
+        >>> dPitch = pitch.Pitch('D2')
+        >>> intv = interval.Interval(dFlatAndAHalf, dPitch)
+        >>> dPitch.transpose(intv, inPlace=True)
+        >>> dPitch
+        <music21.pitch.Pitch D#2(+50c)>
+
         '''
         # environLocal.printDebug(['Pitch.transpose()', value])
         if isinstance(value, interval.IntervalBase):
@@ -4488,6 +4601,9 @@ class Pitch(prebase.ProtoM21Object):
             # set fundamental
             self.fundamental = p.fundamental
             self.spellingIsInferred = p.spellingIsInferred
+            # deepcopy _microtone if present (not thru microtone property to detect None)
+            if p._microtone is not None:
+                self._microtone = copy.deepcopy(p._microtone)
             return None
 
     # --------------------------------------------------------------------------
diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py
index c6b96fa9d..a81195692 100644
--- a/music21/stream/__init__.py
+++ b/music21/stream/__init__.py
@@ -24,4 +24,3 @@ from music21.stream import filters
 from music21.stream import iterator
 from music21.stream import makeNotation
 from music21.stream import streamStatus
-from music21.stream import tools
diff --git a/music21/stream/base.py b/music21/stream/base.py
index d962975ac..75b706a01 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -6830,6 +6830,17 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                 if e.tie is not None and e.tie.type != 'stop':
                     tiePitchSet.add(e.pitch.nameWithOctave)
 
+                # handle this note's ornaments' ornamentalPitches
+                makeNotation.makeOrnamentalAccidentals(
+                    e,
+                    pitchPast=pitchPast,
+                    pitchPastMeasure=pitchPastMeasure,
+                    alteredPitches=alteredPitches,
+                    cautionaryPitchClass=cautionaryPitchClass,
+                    cautionaryAll=cautionaryAll,
+                    overrideStatus=overrideStatus,
+                    cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat)
+
             elif isinstance(e, chord.Chord):
                 # when reading a chord, this will apply an accidental
                 # if pitches in the chord suggest an accidental
@@ -6858,11 +6869,34 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                     if n.tie is not None and n.tie.type != 'stop':
                         seenPitchNames.add(p.nameWithOctave)
 
+                    # handle this note-in-chord's ornaments' ornamentalPitches
+                    makeNotation.makeOrnamentalAccidentals(
+                        n,
+                        pitchPast=pitchPast,
+                        pitchPastMeasure=pitchPastMeasure,
+                        alteredPitches=alteredPitches,
+                        cautionaryPitchClass=cautionaryPitchClass,
+                        cautionaryAll=cautionaryAll,
+                        overrideStatus=overrideStatus,
+                        cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat)
+
                 tiePitchSet.clear()
                 for pName in seenPitchNames:
                     tiePitchSet.add(pName)
 
                 pitchPast += e.pitches
+
+                # handle this chord's ornaments' ornamentalPitches
+                makeNotation.makeOrnamentalAccidentals(
+                    e,
+                    pitchPast=pitchPast,
+                    pitchPastMeasure=pitchPastMeasure,
+                    alteredPitches=alteredPitches,
+                    cautionaryPitchClass=cautionaryPitchClass,
+                    cautionaryAll=cautionaryAll,
+                    overrideStatus=overrideStatus,
+                    cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat)
+
             else:
                 tiePitchSet.clear()
 
@@ -10060,6 +10094,10 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         Streams, and Chords will have their Pitch objects accumulated as
         well. For that reason, a flat representation is not required.
 
+        Note that this does not include any ornamental pitches implied
+        by any ornaments on those Notes and Chords.  To get those, use
+        the makeNotation.ornamentalPitches(s) method.
+
         Pitch objects are returned in a List, not a Stream.  This usage
         differs from the .notes property, but makes sense since Pitch
         objects usually have by default a Duration of zero. This is an important difference
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 14a76a2be..4a2cd6c72 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -26,6 +26,7 @@ from music21 import chord
 from music21 import defaults
 from music21 import duration
 from music21 import environment
+from music21 import expressions
 from music21 import key
 from music21 import meter
 from music21 import note
@@ -1664,14 +1665,17 @@ def makeAccidentalsInMeasureStream(
         # unless this measure has a key signature object
         if i > 0:
             if m.keySignature is None:
-                pitchPastMeasure = measuresOnly[i - 1].pitches
+                pitchPastMeasure = (
+                    measuresOnly[i - 1].pitches + ornamentalPitches(measuresOnly[i - 1])
+                )
             elif ksLast:
                 # If there is any key signature object to the left,
                 # just get the chromatic pitches from previous measure
                 # G-naturals in C major following G-flats in F major need cautionary
                 # G-naturals in C major following G-flats in Db major don't
-                pitchPastMeasure = [p for p in measuresOnly[i - 1].pitches
-                                    if p.name not in ksLastDiatonic]
+                pitchPastMeasure = [p for p in
+                    measuresOnly[i - 1].pitches + ornamentalPitches(measuresOnly[i - 1])
+                    if p.name not in ksLastDiatonic]
             # Get tiePitchSet from previous measure
             try:
                 previousNoteOrChord = measuresOnly[i - 1][note.NotRest][-1]
@@ -1705,6 +1709,68 @@ def makeAccidentalsInMeasureStream(
     if isinstance(s, Stream):
         s.streamStatus.accidentals = True
 
+def ornamentalPitches(s: StreamType) -> list[pitch.Pitch]:
+    '''
+    Returns all ornamental :class:`~music21.pitch.Pitch` objects found in any
+    ornaments in notes/chords in the stream (and substreams) as a Python list.
+
+    Very much like the pitches property, except that instead of returning all
+    the pitches found in notes and chords, it returns the ornamental pitches
+    found in the ornaments on the notes and chords.
+
+    If you want a list of _all_ the pitches in a stream, including the ornamental
+    pitches, you can call s.pitches and makeNotation.ornamentalPitches(s),
+    and then combine the two resulting lists into one big list.
+    '''
+    from music21.stream import Stream
+    post = []
+    for e in s.elements:
+        if isinstance(e, Stream):
+            # recurse
+            post.extend(ornamentalPitches(e))
+        elif hasattr(e, 'expressions'):
+            for orn in e.expressions:
+                if isinstance(orn, expressions.Ornament):
+                    post.extend(orn.ornamentalPitches)
+    return post
+
+def makeOrnamentalAccidentals(
+    noteOrChord: note.Note | chord.Chord,
+    *,
+    pitchPast: list[pitch.Pitch] | None = None,
+    pitchPastMeasure: list[pitch.Pitch] | None = None,
+    otherSimultaneousPitches: list[pitch.Pitch] | None = None,
+    alteredPitches: list[pitch.Pitch] | None = None,
+    cautionaryPitchClass: bool = True,
+    cautionaryAll: bool = False,
+    overrideStatus: bool = False,
+    cautionaryNotImmediateRepeat: bool = True,
+):
+    '''
+        Makes accidentals for the ornamental pitches for any Ornaments on noteOrChord.
+        This is very similar to the processing in pitch.updateAccidentalDisplay, except
+        that there is no tie processing, since ornamental pitches cannot be tied.
+    '''
+    for orn in noteOrChord.expressions:
+        if not isinstance(orn, expressions.Ornament):
+            continue
+
+        orn.resolveOrnamentalPitches(noteOrChord)
+        if not orn.ornamentalPitches:
+            continue
+
+        orn.updateAccidentalDisplay(
+            pitchPast=pitchPast,
+            pitchPastMeasure=pitchPastMeasure,
+            alteredPitches=alteredPitches,
+            cautionaryPitchClass=cautionaryPitchClass,
+            cautionaryAll=cautionaryAll,
+            overrideStatus=overrideStatus,
+            cautionaryNotImmediateRepeat=cautionaryNotImmediateRepeat)
+
+        if pitchPast is not None:
+            pitchPast += orn.ornamentalPitches
+
 def iterateBeamGroups(
     s: StreamType,
     skipNoBeams=True,
diff --git a/music21/stream/tools.py b/music21/stream/tools.py
deleted file mode 100644
index 86f5c2844..000000000
--- a/music21/stream/tools.py
+++ /dev/null
@@ -1,203 +0,0 @@
-# -*- coding: utf-8 -*-
-# ------------------------------------------------------------------------------
-# Name:         stream/tools.py
-# Purpose:      Additional tools for working with streams
-#
-# Authors:      Mark Gotham
-#
-# Copyright:    Copyright © 2022 Michael Scott Asato Cuthbert
-# License:      BSD, see license.txt
-# ------------------------------------------------------------------------------
-
-from __future__ import annotations
-import typing as t
-
-from music21 import clef
-from music21 import environment
-from music21 import key
-from music21 import meter
-from music21 import stream
-
-if t.TYPE_CHECKING:
-    from music21.base import Music21Object
-
-environLocal = environment.Environment('stream.tools')
-
-
-# ------------------------------------------------------------------------------
-
-def removeDuplicates(thisStream: stream.Stream,
-                     classesToRemove: tuple = (meter.TimeSignature, key.KeySignature, clef.Clef),
-                     inPlace: bool = True
-                     ) -> stream.Stream:
-    '''
-    The repetition of some classes like notes is common.
-    By contrast, the explicit repetition of certain other objects like clefs
-    usually indicates an error e.g., resulting from a copy'n'paste.
-    This function serves to remove those that are likely in error and make no change.
-
-    Use the `classesToRemove` argument to specify which music21 classes to check and remove.
-    The classes currently supported are: time signatures, key signatures, and clefs.
-    The `classesToRemove` argument defaults all three.
-    Sometimes there are legitimate reasons to duplicate even these classes.
-    In that case, override the default by specifying the list of which of the three of classes.
-    More classes may be added, but for now they will simply raise a ValueError.
-
-    So let's create an example part with an initial set of
-    time signature, key signature, and clef.
-
-    >>> p = stream.Part()
-    >>> p.append(meter.TimeSignature('3/4'))  # first TS
-    >>> p.append(key.KeySignature(6))  # first KS
-    >>> p.append(clef.TrebleClef())  # first Clef
-
-    Then a few notes, followed by a duplicates of the
-    key signature, and clef.
-
-    >>> p.append(note.Note('C'))
-    >>> p.append(note.Note('C'))
-    >>> p.append(note.Note('D'))
-
-    >>> p.append(meter.TimeSignature('3/4'))  # duplicate
-    >>> p.append(key.KeySignature(6))  # duplicate
-    >>> p.append(clef.TrebleClef())  # duplicate
-
-    Finally, a few more notes, followed by a
-    change of time signature, key signature, and clef.
-
-    >>> p.append(note.Note('E'))
-    >>> p.append(note.Note('F'))
-    >>> p.append(note.Note('G'))
-
-    >>> p.append(meter.TimeSignature('2/4'))
-    >>> p.append(key.KeySignature(-5))
-    >>> p.append(clef.BassClef())
-
-    >>> p.append(note.Note('A'))
-    >>> p.append(note.Note('B'))
-    >>> p.append(note.Note('C5'))
-
-    Now we'll make it into a proper part with measures and see
-    how it looks in its original, unaltered form:
-
-    >>> p.makeMeasures(inPlace=True)
-    >>> p.show('t')
-    {0.0} <music21.stream.Measure 1 offset=0.0>
-        {0.0} <music21.clef.TrebleClef>
-        {0.0} <music21.key.KeySignature of 6 sharps>
-        {0.0} <music21.meter.TimeSignature 3/4>
-        {0.0} <music21.note.Note C>
-        {1.0} <music21.note.Note C>
-        {2.0} <music21.note.Note D>
-    {3.0} <music21.stream.Measure 2 offset=3.0>
-        {0.0} <music21.clef.TrebleClef>
-        {0.0} <music21.key.KeySignature of 6 sharps>
-        {0.0} <music21.meter.TimeSignature 3/4>
-        {0.0} <music21.note.Note E>
-        {1.0} <music21.note.Note F>
-        {2.0} <music21.note.Note G>
-    {6.0} <music21.stream.Measure 3 offset=6.0>
-        {0.0} <music21.clef.BassClef>
-        {0.0} <music21.key.KeySignature of 5 flats>
-        {0.0} <music21.meter.TimeSignature 2/4>
-        {0.0} <music21.note.Note A>
-        {1.0} <music21.note.Note B>
-    {8.0} <music21.stream.Measure 4 offset=8.0>
-        {0.0} <music21.note.Note C>
-        {1.0} <music21.bar.Barline type=final>
-
-    Calling removeDuplicates should remove the duplicates
-    even with those changes now stored within measures,
-    not directly on the part.
-    Specifically, in our example,
-    the duplicates entries are removed from measure 2
-    and the actual changes in measure 3 remain.
-
-    >>> testInPlace = stream.tools.removeDuplicates(p)
-    >>> testInPlace.show('t')
-    {0.0} <music21.stream.Measure 1 offset=0.0>
-        {0.0} <music21.clef.TrebleClef>
-        {0.0} <music21.key.KeySignature of 6 sharps>
-        {0.0} <music21.meter.TimeSignature 3/4>
-        {0.0} <music21.note.Note C>
-        {1.0} <music21.note.Note C>
-        {2.0} <music21.note.Note D>
-    {3.0} <music21.stream.Measure 2 offset=3.0>
-        {0.0} <music21.note.Note E>
-        {1.0} <music21.note.Note F>
-        {2.0} <music21.note.Note G>
-    {6.0} <music21.stream.Measure 3 offset=6.0>
-        {0.0} <music21.clef.BassClef>
-        {0.0} <music21.key.KeySignature of 5 flats>
-        {0.0} <music21.meter.TimeSignature 2/4>
-        {0.0} <music21.note.Note A>
-        {1.0} <music21.note.Note B>
-    {8.0} <music21.stream.Measure 4 offset=8.0>
-        {0.0} <music21.note.Note C>
-        {1.0} <music21.bar.Barline type=final>
-
-    As the example shows, this function defaults to working on a stream inPlace.
-
-    >>> testInPlace == p
-    True
-
-    To avoid this, set inPlace to False.
-
-    >>> testNotInPlace = stream.tools.removeDuplicates(p, inPlace=False)
-    >>> testNotInPlace == p
-    False
-
-    This function is primarily designed and intended for use on a stream.Part object.
-    If called on a steam.Score, it will simply be applied to each of that score's parts in turn.
-
-    >>> s = stream.Score()
-    >>> s.append(p)
-    >>> t = stream.tools.removeDuplicates(s, inPlace=False)
-    >>> s.parts[0] == testInPlace
-    True
-
-    '''
-
-    supportedClasses = (meter.TimeSignature, key.KeySignature, clef.Clef)
-
-    removalDict: dict[stream.Stream, list[Music21Object]] = {}
-
-    if not inPlace:
-        thisStream = thisStream.coreCopyAsDerivation('removeDuplicates')
-
-    if isinstance(thisStream, stream.Score):
-        if len(thisStream.parts) > 0:
-            for p in thisStream.parts:
-                removeDuplicates(p, classesToRemove=classesToRemove, inPlace=True)
-
-    for thisClass in classesToRemove:
-
-        if not any(issubclass(thisClass, supportedClass) for supportedClass in supportedClasses):
-            raise ValueError(f'Invalid class. Only {supportedClasses} are supported.')
-
-        allStates = thisStream.recurse().getElementsByClass(thisClass)
-
-        if len(allStates) < 2:  # Not used, or doesn't change
-            continue
-
-        currentState = allStates[0]  # First to initialize: can't be a duplicate
-        for thisState in allStates[1:]:
-            if thisState == currentState:
-                if thisState.activeSite in removalDict:  # May be several in same (e.g., measure)
-                    removalDict[thisState.activeSite].append(thisState)
-                else:
-                    removalDict[thisState.activeSite] = [thisState]
-            else:
-                currentState = thisState
-
-    for activeSiteKey, valuesToRemove in removalDict.items():
-        activeSiteKey.remove(valuesToRemove, recurse=True)
-
-    return thisStream
-
-
-# -----------------------------------------------------------------------------
-
-if __name__ == '__main__':
-    import music21
-    music21.mainTest()


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index 2e1674a60..fb4e0e14e 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -2724,7 +2724,7 @@ notations32a = '''<?xml version="1.0" encoding="UTF-8"?>
           <ornaments>
             <turn/>
             <accidental-mark placement="above">sharp</accidental-mark>
-            <accidental-mark placement="above">three-quarters-flat</accidental-mark>
+            <accidental-mark placement="below">three-quarters-flat</accidental-mark>
           </ornaments>
         </notations>
         <lyric number="1"><text>turn+acc.(ab.+bel./rel to turn)</text></lyric>
diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py
index 8605dc433..8d368b061 100644
--- a/music21/musicxml/test_m21ToXml.py
+++ b/music21/musicxml/test_m21ToXml.py
@@ -479,6 +479,15 @@ class Test(unittest.TestCase):
         mxMidiInstrument = tree.findall('.//midi-instrument')[0]
         self.assertEqual(mxScoreInstrument.get('id'), mxMidiInstrument.get('id'))
 
+    def testOrnamentAccidentals(self):
+        s = converter.parse(testPrimitive.notations32a)
+        x = self.getET(s)
+        accidentalMarks = x.findall('.//ornaments/accidental-mark')
+        self.assertEqual(
+            [accMark.text for accMark in accidentalMarks],
+            ['natural', 'sharp', 'three-quarters-flat']
+        )
+
     def testMultiDigitEndingsWrite(self):
         # Relevant barlines:
         # Measure 2, left barline: <ending number="1,2" type="start"/>
diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py
index 035e42c52..e9fdd18ad 100644
--- a/music21/musicxml/test_xmlToM21.py
+++ b/music21/musicxml/test_xmlToM21.py
@@ -619,6 +619,18 @@ class Test(unittest.TestCase):
                     count += 1
         self.assertEqual(count, 1)
 
+        upperCount = 0
+        lowerCount = 0
+        for n in s.recurse().notes:
+            for e in n.expressions:
+                if 'Turn' in e.classes:
+                    if e.upperAccidental is not None:
+                        upperCount += 1
+                    if e.lowerAccidental is not None:
+                        lowerCount += 1
+        self.assertEqual(upperCount, 2)
+        self.assertEqual(lowerCount, 1)
+
         count = 0
         for n in s.recurse().notes:
             for e in n.expressions:
diff --git a/music21/test/test_expressions.py b/music21/test/test_expressions.py
index a800d216c..cdcae4dd1 100644
--- a/music21/test/test_expressions.py
+++ b/music21/test/test_expressions.py
@@ -6,10 +6,12 @@ import unittest
 from music21 import chord
 from music21 import clef
 from music21 import expressions
+from music21 import interval
 from music21 import key
 from music21 import meter
 from music21.musicxml import m21ToXml
 from music21 import note
+from music21 import pitch
 from music21 import stream
 
 
@@ -161,11 +163,375 @@ class Test(unittest.TestCase):
         # s.show()
         self.assertEqual(raw.count(b'wavy-line'), 2)
 
-    def testUnpitchedUnsupported(self):
+    def testFixedSizeOrnamentAccidental(self):
+        # Check that accidental works as expected (i.e. not at all) on all the
+        # old-style fixed-size Trill/Mordent types.
+
+        # init with accidental raises ExpressionException
+        flatAccid = pitch.Accidental('flat')
+        with self.assertRaises(expressions.ExpressionException):
+            trill = expressions.HalfStepTrill(accidental=flatAccid)
+        with self.assertRaises(expressions.ExpressionException):
+            trill = expressions.WholeStepTrill(accidental=flatAccid)
+        with self.assertRaises(expressions.ExpressionException):
+            mord = expressions.HalfStepMordent(accidental=flatAccid)
+        with self.assertRaises(expressions.ExpressionException):
+            mord = expressions.WholeStepMordent(accidental=flatAccid)
+        with self.assertRaises(expressions.ExpressionException):
+            mord = expressions.HalfStepInvertedMordent(accidental=flatAccid)
+        with self.assertRaises(expressions.ExpressionException):
+            mord = expressions.WholeStepInvertedMordent(accidental=flatAccid)
+
+        # get accidental returns None, set accidental raises ExpressionException
+        sharpAccid = pitch.Accidental('sharp')
+        trill = expressions.HalfStepTrill()
+        self.assertIsNone(trill.accidental)
+        with self.assertRaises(expressions.ExpressionException):
+            trill.accidental = sharpAccid
+        trill = expressions.WholeStepTrill()
+        self.assertIsNone(trill.accidental)
+        with self.assertRaises(expressions.ExpressionException):
+            trill.accidental = sharpAccid
+        mord = expressions.HalfStepMordent()
+        self.assertIsNone(mord.accidental)
+        with self.assertRaises(expressions.ExpressionException):
+            mord.accidental = sharpAccid
+        mord = expressions.WholeStepMordent()
+        self.assertIsNone(mord.accidental)
+        with self.assertRaises(expressions.ExpressionException):
+            mord.accidental = sharpAccid
+        mord = expressions.HalfStepInvertedMordent()
+        self.assertIsNone(mord.accidental)
+        with self.assertRaises(expressions.ExpressionException):
+            mord.accidental = sharpAccid
+        mord = expressions.WholeStepInvertedMordent()
+        self.assertIsNone(mord.accidental)
+        with self.assertRaises(expressions.ExpressionException):
+            mord.accidental = sharpAccid
+
+    def testResolveOrnamentalPitches(self):
+        # with no accidental, no key, explicit octave
+        trill = expressions.Trill()
+        itrill = expressions.InvertedTrill()
+
+        noteWithExplicitOctave = note.Note('C4')
+        trill.resolveOrnamentalPitches(noteWithExplicitOctave)
+        self.assertEqual(trill.ornamentalPitches, (pitch.Pitch('D4'),))
+        self.assertIsNone(trill.ornamentalPitches[0].accidental)
+        itrill.resolveOrnamentalPitches(noteWithExplicitOctave)
+        self.assertEqual(itrill.ornamentalPitches, (pitch.Pitch('B3'),))
+        self.assertIsNone(itrill.ornamentalPitches[0].accidental)
+
+        # with no accidental, no key, implicit octave
+        noteWithImplicitOctave = note.Note('C')
+        trill.resolveOrnamentalPitches(noteWithImplicitOctave)
+        self.assertEqual(trill.ornamentalPitches, (pitch.Pitch('D4'),))
+        self.assertIsNone(trill.ornamentalPitches[0].accidental)
+        itrill.resolveOrnamentalPitches(noteWithImplicitOctave)
+        self.assertEqual(itrill.ornamentalPitches, (pitch.Pitch('B3'),))
+        self.assertIsNone(itrill.ornamentalPitches[0].accidental)
+
+        # with accidental=naturalAccid, no key, explicit octave
+        naturalAccid = pitch.Accidental(0)
+        naturalAccid.displayStatus = True
+        trill.accidental = naturalAccid
+        itrill.accidental = naturalAccid
+        trill.resolveOrnamentalPitches(noteWithExplicitOctave)
+        expectedPitch = pitch.Pitch('D4')
+        expectedPitch.accidental = pitch.Accidental('natural')
+        self.assertEqual(trill.ornamentalPitches, (expectedPitch,))
+        self.assertEqual(
+            trill.ornamentalPitches[0].accidental.name,
+            'natural'
+        )
+        self.assertEqual(
+            trill.ornamentalPitches[0].accidental.displayStatus,
+            True
+        )
+        itrill.resolveOrnamentalPitches(noteWithImplicitOctave)
+        expectedPitch = pitch.Pitch('B3')
+        expectedPitch.accidental = pitch.Accidental('natural')
+        self.assertEqual(itrill.ornamentalPitches, (expectedPitch,))
+        self.assertEqual(
+            itrill.ornamentalPitches[0].accidental.name,
+            'natural'
+        )
+        self.assertEqual(
+            itrill.ornamentalPitches[0].accidental.displayStatus,
+            True
+        )
+
+        # with accidental=doubleSharpAccid, no key, explicit octave
+        doubleSharpAccid = pitch.Accidental('##')
+        doubleSharpAccid.displayStatus = True
+        trill.accidental = doubleSharpAccid
+        itrill.accidental = doubleSharpAccid
+        trill.resolveOrnamentalPitches(noteWithExplicitOctave)
+        expectedPitch = pitch.Pitch('D4')
+        expectedPitch.accidental = pitch.Accidental('double-sharp')
+        expectedPitch.displayStatus = True
+        self.assertEqual(trill.ornamentalPitches, (expectedPitch,))
+        self.assertEqual(
+            trill.ornamentalPitches[0].accidental.name,
+            'double-sharp'
+        )
+        self.assertEqual(
+            trill.ornamentalPitches[0].accidental.displayStatus,
+            True
+        )
+        itrill.resolveOrnamentalPitches(noteWithImplicitOctave)
+        expectedPitch = pitch.Pitch('B3')
+        expectedPitch.accidental = pitch.Accidental('double-sharp')
+        expectedPitch.displayStatus = True
+        self.assertEqual(itrill.ornamentalPitches, (expectedPitch,))
+        self.assertEqual(
+            itrill.ornamentalPitches[0].accidental.name,
+            'double-sharp'
+        )
+        self.assertEqual(
+            itrill.ornamentalPitches[0].accidental.displayStatus,
+            True
+        )
+
+    def testUpdateAccidentalDisplay(self):
+        # Trill is already tested in header doc, test Mordent and Turn here
+        noSharpsOrFlats = key.KeySignature(0)
+        mord1 = expressions.InvertedMordent()
+        mord1.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        self.assertEqual(mord1.ornamentalPitch, pitch.Pitch('A4'))
+        self.assertIsNone(mord1.ornamentalPitch.accidental, None)
+        past = [pitch.Pitch('A#4'), pitch.Pitch('C#4'), pitch.Pitch('C4')]
+        mord1.updateAccidentalDisplay(pitchPast=past, cautionaryAll=True)
+        self.assertEqual(mord1.ornamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(mord1.ornamentalPitch.accidental.displayStatus)
+
+        mord2 = expressions.InvertedMordent()
+        mord2.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        self.assertEqual(mord2.ornamentalPitch, pitch.Pitch('A4'))
+        self.assertIsNone(mord2.ornamentalPitch.accidental, None)
+        past = [pitch.Pitch('A#4'), pitch.Pitch('C#4'), pitch.Pitch('C4')]
+        mord2.updateAccidentalDisplay(pitchPast=past)  # should add a natural
+        self.assertEqual(mord2.ornamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(mord2.ornamentalPitch.accidental.displayStatus)
+
+        mord3 = expressions.InvertedMordent()
+        mord3.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        self.assertEqual(mord3.ornamentalPitch, pitch.Pitch('A4'))
+        self.assertIsNone(mord3.ornamentalPitch.accidental, None)
+        past = [pitch.Pitch('A#3'), pitch.Pitch('C#'), pitch.Pitch('C')]
+        mord3.updateAccidentalDisplay(pitchPast=past, cautionaryPitchClass=False)
+        self.assertIsNone(mord3.ornamentalPitch.accidental)
+
+        turn1 = expressions.Turn()
+        turn1.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        self.assertEqual(turn1.upperOrnamentalPitch, pitch.Pitch('A4'))
+        self.assertIsNone(turn1.upperOrnamentalPitch.accidental, None)
+        self.assertEqual(turn1.lowerOrnamentalPitch, pitch.Pitch('F4'))
+        self.assertIsNone(turn1.lowerOrnamentalPitch.accidental, None)
+        past = [pitch.Pitch('A#4'), pitch.Pitch('C#4'), pitch.Pitch('C4')]
+        turn1.updateAccidentalDisplay(pitchPast=past, cautionaryAll=True)  # should add naturals
+        self.assertEqual(turn1.upperOrnamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn1.upperOrnamentalPitch.accidental.displayStatus)
+        self.assertEqual(turn1.lowerOrnamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn1.lowerOrnamentalPitch.accidental.displayStatus)
+
+        turn2 = expressions.Turn()
+        turn2.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        self.assertEqual(turn2.upperOrnamentalPitch, pitch.Pitch('A4'))
+        self.assertIsNone(turn2.upperOrnamentalPitch.accidental, None)
+        self.assertEqual(turn2.lowerOrnamentalPitch, pitch.Pitch('F4'))
+        self.assertIsNone(turn2.lowerOrnamentalPitch.accidental, None)
+        past = [pitch.Pitch('A#4'), pitch.Pitch('C#4'), pitch.Pitch('C4')]
+        turn2.updateAccidentalDisplay(pitchPast=past)  # should add upper natural
+        self.assertEqual(turn2.upperOrnamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn2.upperOrnamentalPitch.accidental.displayStatus)
+        self.assertIsNone(turn2.lowerOrnamentalPitch.accidental)
+
+        turn3 = expressions.Turn()
+        turn3.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        self.assertEqual(turn3.upperOrnamentalPitch, pitch.Pitch('A4'))
+        self.assertIsNone(turn3.upperOrnamentalPitch.accidental, None)
+        self.assertEqual(turn3.lowerOrnamentalPitch, pitch.Pitch('F4'))
+        self.assertIsNone(turn3.lowerOrnamentalPitch.accidental, None)
+        past = [pitch.Pitch('A#3'), pitch.Pitch('C#'), pitch.Pitch('C')]
+        turn3.updateAccidentalDisplay(pitchPast=past, cautionaryPitchClass=False)  # no naturals
+        self.assertIsNone(turn3.upperOrnamentalPitch.accidental)
+        self.assertIsNone(turn3.lowerOrnamentalPitch.accidental)
+
+    def testUpdateAccidentalDisplayWithAccidentNameSet(self):
+        # Trill is already tested in header doc, test Mordent and Turn here
+        naturalAccid = pitch.Accidental(0)
+        naturalAccid.displayStatus = True
+        noSharpsOrFlats = key.KeySignature(0)
+        mord = expressions.InvertedMordent()
+        mord.accidental = naturalAccid
+        mord.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        expectedOrnamentalPitch = pitch.Pitch('A4')
+        expectedOrnamentalPitch.accidental = pitch.Accidental('natural')
+        expectedOrnamentalPitch.accidental.displayStatus = True
+        self.assertEqual(mord.ornamentalPitch, expectedOrnamentalPitch)
+        self.assertEqual(mord.ornamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(mord.ornamentalPitch.accidental.displayStatus)
+        past = [pitch.Pitch('a#3'), pitch.Pitch('c#'), pitch.Pitch('c')]
+        mord.updateAccidentalDisplay(pitchPast=past, cautionaryPitchClass=False)
+        self.assertEqual(mord.ornamentalPitch, expectedOrnamentalPitch)
+        self.assertEqual(mord.ornamentalPitch.accidental, pitch.Accidental('natural'))
+        self.assertTrue(mord.ornamentalPitch.accidental.displayStatus)
+
+        turn = expressions.Turn()
+        turn.upperAccidental = naturalAccid
+        turn.lowerAccidental = naturalAccid
+        turn.resolveOrnamentalPitches(note.Note('G4'), keySig=noSharpsOrFlats)
+        expectedOrnamentalPitches = (pitch.Pitch('A4'), pitch.Pitch('F4'))
+        expectedOrnamentalPitches[0].accidental = pitch.Accidental('natural')
+        expectedOrnamentalPitches[0].accidental.displayStatus = True
+        expectedOrnamentalPitches[1].accidental = pitch.Accidental('natural')
+        expectedOrnamentalPitches[1].accidental.displayStatus = True
+        self.assertEqual(turn.ornamentalPitches[0], expectedOrnamentalPitches[0])
+        self.assertEqual(turn.ornamentalPitches[0].accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn.ornamentalPitches[0].accidental.displayStatus)
+        self.assertEqual(turn.ornamentalPitches[1], expectedOrnamentalPitches[1])
+        self.assertEqual(turn.ornamentalPitches[1].accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn.ornamentalPitches[1].accidental.displayStatus)
+        past = [pitch.Pitch('a#3'), pitch.Pitch('c#'), pitch.Pitch('c')]
+        turn.updateAccidentalDisplay(pitchPast=past, cautionaryPitchClass=False)
+        self.assertEqual(turn.ornamentalPitches[0], expectedOrnamentalPitches[0])
+        self.assertEqual(turn.ornamentalPitches[0].accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn.ornamentalPitches[0].accidental.displayStatus)
+        self.assertEqual(turn.ornamentalPitches[1], expectedOrnamentalPitches[1])
+        self.assertEqual(turn.ornamentalPitches[1].accidental, pitch.Accidental('natural'))
+        self.assertTrue(turn.ornamentalPitches[1].accidental.displayStatus)
+
+    def testEdgeCases(self):
+        # Make sure you can call resolveOrnamentalPitches() on non-Trill/Mordent/Turn Ornaments
+        # without raising an exception (or actually doing anything interesting).
+        orn = expressions.Ornament()
+        orn.resolveOrnamentalPitches(note.Note('C4'))
+        self.assertEqual(orn.ornamentalPitches, tuple())
+
+        # Make sure you can call realize() on non-Trill/Mordent/Turn ornaments and
+        # just get an identical note back, with empty before and after lists.
+        n = note.Note('C4')
+        realized = orn.realize(n, inPlace=False)
+        self.assertEqual(realized, ([], n, []))
+        self.assertIsNot(realized[1], n)
+        realized = orn.realize(n, inPlace=True)
+        self.assertEqual(realized, ([], n, []))
+        self.assertIs(realized[1], n)
+
+        # Make sure that fillListOfRealizedNotes raises if srcObj is Unpitched, but
+        # transposeInterval is not unison.
+        filledList = []
+        with self.assertRaises(TypeError):
+            orn.fillListOfRealizedNotes(
+                note.Unpitched(),
+                fillObjects=filledList,
+                transposeInterval=interval.Interval('M2')
+            )
+        self.assertEqual(filledList, [])
+
+        # Make sure various Ornaments have correct direction
+        self.assertEqual(expressions.Trill().direction, 'up')
+        self.assertEqual(expressions.InvertedTrill().direction, 'down')
+        self.assertEqual(expressions.Mordent().direction, 'down')
+        self.assertEqual(expressions.InvertedMordent().direction, 'up')
+
+        # Make sure Turn.getSize raises on bad which value (must be 'upper' or 'lower')
+        with self.assertRaises(expressions.ExpressionException):
+            expressions.Turn().getSize(note.Note('C4'), which='bad: not upper or lower')
+
+    def testUnpitchedOrnaments(self):
         unp = note.Unpitched()
+
         mord = expressions.Mordent()
-        with self.assertRaises(TypeError):
-            mord.realize(unp)  # type: ignore
+        realized = mord.realize(unp)
+        self.assertEqual(len(realized), 3)
+        self.assertEqual(len(realized[0]), 2)
+        self.assertIsInstance(realized[0][0], note.Unpitched)
+        self.assertIsInstance(realized[0][1], note.Unpitched)
+        self.assertIsInstance(realized[1], note.Unpitched)
+        self.assertEqual(len(realized[2]), 0)
+        self.assertEqual(mord.getSize(unp), interval.Interval('P1'))
+        mord.resolveOrnamentalPitches(unp)
+        self.assertEqual(mord.ornamentalPitches, tuple())
+
+        mord = expressions.InvertedMordent()
+        realized = mord.realize(unp)
+        self.assertEqual(len(realized), 3)
+        self.assertEqual(len(realized[0]), 2)
+        self.assertIsInstance(realized[0][0], note.Unpitched)
+        self.assertIsInstance(realized[0][1], note.Unpitched)
+        self.assertIsInstance(realized[1], note.Unpitched)
+        self.assertEqual(len(realized[2]), 0)
+        self.assertEqual(mord.getSize(unp), interval.Interval('P1'))
+        mord.resolveOrnamentalPitches(unp)
+        self.assertEqual(mord.ornamentalPitches, tuple())
+
+        turn = expressions.Turn()
+        realized = turn.realize(unp)
+        self.assertEqual(len(realized), 3)
+        self.assertEqual(len(realized[0]), 0)
+        self.assertIsNone(realized[1])
+        self.assertEqual(len(realized[2]), 4)
+        self.assertIsInstance(realized[2][0], note.Unpitched)
+        self.assertIsInstance(realized[2][1], note.Unpitched)
+        self.assertIsInstance(realized[2][2], note.Unpitched)
+        self.assertIsInstance(realized[2][3], note.Unpitched)
+        self.assertEqual(turn.getSize(unp, which='upper'), interval.Interval('P1'))
+        self.assertEqual(turn.getSize(unp, which='lower'), interval.Interval('P1'))
+        turn.resolveOrnamentalPitches(unp)
+        self.assertEqual(turn.ornamentalPitches, tuple())
+
+        turn = expressions.InvertedTurn()
+        realized = turn.realize(unp)
+        self.assertEqual(len(realized), 3)
+        self.assertEqual(len(realized[0]), 0)
+        self.assertIsNone(realized[1])
+        self.assertEqual(len(realized[2]), 4)
+        self.assertIsInstance(realized[2][0], note.Unpitched)
+        self.assertIsInstance(realized[2][1], note.Unpitched)
+        self.assertIsInstance(realized[2][2], note.Unpitched)
+        self.assertIsInstance(realized[2][3], note.Unpitched)
+        self.assertEqual(turn.getSize(unp, which='upper'), interval.Interval('P1'))
+        self.assertEqual(turn.getSize(unp, which='lower'), interval.Interval('P1'))
+        turn.resolveOrnamentalPitches(unp)
+        self.assertEqual(turn.ornamentalPitches, tuple())
+
+        trill = expressions.Trill()
+        realized = trill.realize(unp)
+        self.assertEqual(len(realized), 3)
+        self.assertEqual(len(realized[0]), 8)
+        self.assertIsInstance(realized[0][0], note.Unpitched)
+        self.assertIsInstance(realized[0][1], note.Unpitched)
+        self.assertIsInstance(realized[0][2], note.Unpitched)
+        self.assertIsInstance(realized[0][3], note.Unpitched)
+        self.assertIsInstance(realized[0][4], note.Unpitched)
+        self.assertIsInstance(realized[0][5], note.Unpitched)
+        self.assertIsInstance(realized[0][6], note.Unpitched)
+        self.assertIsInstance(realized[0][7], note.Unpitched)
+        self.assertIsNone(realized[1])
+        self.assertEqual(len(realized[2]), 0)
+        self.assertEqual(trill.getSize(unp), interval.Interval('P1'))
+        trill.resolveOrnamentalPitches(unp)
+        self.assertEqual(turn.ornamentalPitches, tuple())
+
+        trill = expressions.InvertedTrill()
+        realized = trill.realize(unp)
+        self.assertEqual(len(realized), 3)
+        self.assertEqual(len(realized[0]), 8)
+        self.assertIsInstance(realized[0][0], note.Unpitched)
+        self.assertIsInstance(realized[0][1], note.Unpitched)
+        self.assertIsInstance(realized[0][2], note.Unpitched)
+        self.assertIsInstance(realized[0][3], note.Unpitched)
+        self.assertIsInstance(realized[0][4], note.Unpitched)
+        self.assertIsInstance(realized[0][5], note.Unpitched)
+        self.assertIsInstance(realized[0][6], note.Unpitched)
+        self.assertIsInstance(realized[0][7], note.Unpitched)
+        self.assertIsNone(realized[1])
+        self.assertEqual(len(realized[2]), 0)
+        self.assertEqual(trill.getSize(unp), interval.Interval('P1'))
+        trill.resolveOrnamentalPitches(unp)
+        self.assertEqual(turn.ornamentalPitches, tuple())
 
 
 # class TestExternal(unittest.TestCase):