Repository: cuthbertLab/music21
Pull request #1762 (2025-03-21 19:47:44)

PedalMark support
Ready for a (second) review, although I'm not going to call it done until I have finished implementing Humdrum read/write and MEI read/write in converter21.


Patch:
diff --git a/music21/_version.py b/music21/_version.py
index 93855a2e7..cabf7fb70 100644
--- a/music21/_version.py
+++ b/music21/_version.py
@@ -50,7 +50,7 @@ so change it if a bug or new feature creates a problem with using old pickles.
 '''
 from __future__ import annotations
 
-__version__ = '9.6.0b2'
+__version__ = '9.6.0b3'
 
 def get_version_tuple(vv):
     v = vv.split('.')
diff --git a/music21/base.py b/music21/base.py
index f44828fb9..449431ee7 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.6.0b2'
+'9.6.0b3'
 
 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 c1adb8dde..2330a73e6 100644
--- a/music21/expressions.py
+++ b/music21/expressions.py
@@ -325,6 +325,98 @@ class RehearsalMark(Expression):
         return RehearsalMark(self.nextContent(), numbering=self.numbering)
 
 
+# ------------------------------------------------------------------------------
+class PedalType(common.StrEnum):
+    Sustain = 'sustain'
+    Sostenuto = 'sostenuto'
+    Soft = 'soft'
+    Silent = 'silent'
+
+class PedalForm(common.StrEnum):
+    Line = 'line'
+    Symbol = 'symbol'
+    SymbolAlt = 'symbolalt'
+    SymbolLine = 'symbolline'
+
+class PedalMark(spanner.Spanner):
+    '''
+    A pedal mark spanner contains a pedaled set of notes.
+    The spanner starts at the moment of down-pedal, and
+    ends at the moment of up-pedal.  There can be any
+    number of pedal "bounces" in the middle of the spanner.
+
+    The visual form of the pedal mark can vary.  The following
+    examples use a pedal mark with one "bounce" in the middle::
+
+        Pedal marks can be lines:            |_______^________|
+        Pedal marks can be normal symbolic:  Ped.    * Ped.   *
+        Pedal marks can be altered symbolic: Ped.    Ped.     *
+        Pedal marks can be symbol and line:  Ped.____^________|
+
+    Pedal marks, whether lines, symbols, or a combination, can
+    represent the sustain pedal (Ped.), the sostenuto pedal
+    (Sost.), the soft (una corda) pedal, or (more rarely) the
+    silent (muting) pedal.
+
+    Pedal marks that are symbolic can be abbreviated: e.g. P. instead
+    of Ped., S. instead of Sost.
+
+    Pedal marks that are lines can have non-printed portions
+    (gaps) in them; these are usually started with "simile",
+    but not necessarily.
+    '''
+    def __init__(
+        self,
+        *spannedElements,
+        **keywords
+    ) -> None:
+        super().__init__(*spannedElements, **keywords)
+        from music21 import note
+        self.fillElementTypes = [note.GeneralNote]
+
+        self.pedalType: PedalType|None = None
+        self.pedalForm: PedalForm|None = None
+        self.abbreviated: bool = False
+        self.placement: str|None = None
+
+
+class PedalObject(base.Music21Object):
+    '''
+    Base class of individual objects that mark various transitions
+    in a PedalMark spanner.
+    '''
+    def __init__(self, **keywords) -> None:
+        super().__init__(**keywords)
+        self.placement: str | None = None
+
+    def _reprInternal(self) -> str:
+        if self.activeSite is None:
+            return 'uninserted'
+        return f'at {self.offset}'
+
+
+class PedalBounce(PedalObject):
+    '''
+    This object, when seen in a PedalMark spanner, represents an up/down bounce
+    of the pedal.
+    '''
+
+
+class PedalGapStart(PedalObject):
+    '''
+    This object, when seen in a PedalMark spanner, represents a disappearance of
+    the pedal line, usually with a TextExpression('simile').  The pedaling should
+    continue (similarly to before), but the line is invisible during this gap.
+    '''
+
+
+class PedalGapEnd(PedalObject):
+    '''
+    This object, when seen in a PedalMark spanner, represents the reappearance of
+    the pedal line.
+    '''
+
+
 # ------------------------------------------------------------------------------
 class TextExpression(Expression):
     '''
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index 171cdc807..ec2441bbb 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -3051,6 +3051,7 @@ class MeasureExporter(XMLExporterBase):
             ('TextExpression', 'textExpressionToXml'),
             ('RepeatExpression', 'textExpressionToXml'),
             ('RehearsalMark', 'rehearsalMarkToXml'),
+            ('PedalObject', 'pedalObjectToXml'),
         ]
     )
     # these need to be wrapped in an attributes tag if not at the beginning of the measure.
@@ -3419,7 +3420,8 @@ class MeasureExporter(XMLExporterBase):
         # this list of spanner classes must match the paramsSet keys in relatedSpanners()
         for thisSpanner in spannerBundle.getByClass((spanner.Ottava,
                                                      dynamics.DynamicWedge,
-                                                     spanner.Line)):
+                                                     spanner.Line,
+                                                     expressions.PedalMark)):
             if thisSpanner.isFirst(obj) or thisSpanner.isLast(obj):
                 return True
 
@@ -3432,8 +3434,8 @@ class MeasureExporter(XMLExporterBase):
         '''
         return two lists or empty tuples:
         (1) spanners related to the object that should appear before the object
-        to the <measure> tag. (2) spanners related to the object that should appear after the
-        object in the measure tag.
+        in the <measure> tag. (2) spanners related to the object that should appear after the
+        object in the <measure> tag.
         '''
         def getProc(su, innerTarget):
             if len(su) == 1:  # have a one element wedge
@@ -3463,8 +3465,9 @@ class MeasureExporter(XMLExporterBase):
                      # TODO: attrGroup: dashed-formatting, print-style
                      'DynamicWedge': ('wedge', ('spread',)),
                      # TODO: niente, attrGroups: line-type, dashed-formatting, position, color
-                     'Line': ('bracket', ('line-end', 'end-length'))
+                     'Line': ('bracket', ('line-end', 'end-length')),
                      # TODO: dashes???
+                     'PedalMark': ('pedal', ('line', 'sign', 'abbreviated'))
                      }
 
         for m21spannerClass, infoTuple in paramsSet.items():
@@ -3508,10 +3511,20 @@ class MeasureExporter(XMLExporterBase):
                     else:
                         postList.append(mxDirection)
 
+                    if m21spannerClass == 'PedalMark' and posSub == 'first':
+                        # check to see if we also need to start a line
+                        if t.TYPE_CHECKING:
+                            assert isinstance(thisSpanner, expressions.PedalMark)
+                        if thisSpanner.pedalForm == expressions.PedalForm.SymbolLine:
+                            mxPedalLine = (
+                                self.makePedalResumeLineXml(thisSpanner)
+                            )
+                            preList.append(mxPedalLine)
+
         return preList, postList
 
     @staticmethod
-    def _spannerStartParameters(spannerClass, sp):
+    def _spannerStartParameters(spannerClass: str, sp: spanner.Spanner) -> dict[str, t.Any]:
         '''
         Return a dict of the parameters for the start of this spanner required by MusicXML output.
 
@@ -3543,14 +3556,20 @@ class MeasureExporter(XMLExporterBase):
         >>> st['spread']
         15
         '''
-        post = {'type': 'start'}
+        post: dict[str, t.Any] = {'type': 'start'}
         if spannerClass == 'Ottava':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, spanner.Ottava)
             post['size'] = sp.shiftMagnitude()
             post['type'] = sp.shiftDirection(reverse=True)  # up or down
         elif spannerClass == 'Line':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, spanner.Line)
             post['line-end'] = sp.startTick
             post['end-length'] = sp.startHeight
         elif spannerClass == 'DynamicWedge':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, dynamics.DynamicWedge)
             post['type'] = sp.type
             if sp.type == 'crescendo':
                 post['spread'] = 0
@@ -3558,11 +3577,28 @@ class MeasureExporter(XMLExporterBase):
                     post['niente'] = 'yes'
             else:
                 post['spread'] = sp.spread
-
+        elif spannerClass == 'PedalMark':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, expressions.PedalMark)
+            if sp.pedalType == expressions.PedalType.Sostenuto:
+                post['type'] = 'sostenuto'
+            else:
+                # non-Sostenuto sp.pedalType might be Sustain, Soft, or Silent.
+                # But MusicXML only has 'start', which implies Sustain,
+                # so that's what we do here, hoping there is a text
+                # direction describing which pedal to use.
+                post['type'] = 'start'
+            if sp.pedalForm == expressions.PedalForm.Line:
+                post['line'] = 'yes'
+            else:
+                # 'symbol', 'altsymbol', and 'symline' all start with a sign
+                post['sign'] = 'yes'
+            if sp.abbreviated:
+                post['abbreviated'] = 'yes'
         return post
 
     @staticmethod
-    def _spannerEndParameters(spannerClass, sp):
+    def _spannerEndParameters(spannerClass: str, sp: spanner.Spanner) -> dict[str, t.Any]:
         '''
         Return a dict of the parameters for the end of this spanner required by MusicXML output.
 
@@ -3573,19 +3609,33 @@ class MeasureExporter(XMLExporterBase):
         >>> en['size']
         8
         '''
-        post = {'type': 'stop'}
+        post: dict[str, t.Any] = {'type': 'stop'}
         if spannerClass == 'Ottava':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, spanner.Ottava)
             post['size'] = sp.shiftMagnitude()
         elif spannerClass == 'Line':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, spanner.Line)
             post['line-end'] = sp.endTick
             post['end-length'] = sp.endHeight
         elif spannerClass == 'DynamicWedge':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, dynamics.DynamicWedge)
             if sp.type == 'crescendo':
                 post['spread'] = sp.spread
             else:
                 post['spread'] = 0
                 if sp.niente:
                     post['niente'] = 'yes'
+        elif spannerClass == 'PedalMark':
+            if t.TYPE_CHECKING:
+                assert isinstance(sp, expressions.PedalMark)
+            if sp.pedalForm in (expressions.PedalForm.Line, expressions.PedalForm.SymbolLine):
+                post['line'] = 'yes'
+            else:
+                # 'symbol', 'altsymbol' both end with a sign
+                post['sign'] = 'yes'
 
         return post
 
@@ -6401,6 +6451,142 @@ class MeasureExporter(XMLExporterBase):
         self.xmlRoot.append(mxDirection)
         return mxDirection
 
+    def pedalObjectToXml(self, po: expressions.PedalObject) -> Element|None:
+        # noinspection PyShadowingNames
+        '''
+        Convert a PedalObject object (i.e. PedalBounce, PedalGapStart,
+        PedalGapEnds) to a MusicXML <direction> tag with a <pedal> tag
+        inside it.  Attributes like pedalForm are stored in the enclosing
+        PedalMark spanner.
+
+        >>> pm = expressions.PedalMark()
+        >>> po = expressions.PedalBounce()
+        >>> pm.addSpannedElements(po)
+        >>> pm.pedalForm = expressions.PedalForm.Line
+        >>> pm.pedalType = expressions.PedalType.Sustain
+        >>> MEX = musicxml.m21ToXml.MeasureExporter()
+        >>> mxPedal = MEX.pedalObjectToXml(po)
+        >>> MEX.dump(mxPedal)
+        <direction>
+          <direction-type>
+            <pedal line="yes" type="change" />
+          </direction-type>
+        </direction>
+
+        >>> pm = expressions.PedalMark()
+        >>> po = expressions.PedalGapStart()
+        >>> pm.addSpannedElements(po)
+        >>> pm.pedalForm = expressions.PedalForm.Line
+        >>> pm.pedalType = expressions.PedalType.Sustain
+        >>> po.placement = 'above'
+        >>> MEX = musicxml.m21ToXml.MeasureExporter()
+        >>> mxPedal = MEX.pedalObjectToXml(po)
+        >>> MEX.dump(mxPedal)
+        <direction placement="above">
+          <direction-type>
+            <pedal line="yes" type="discontinue" />
+          </direction-type>
+        </direction>
+
+        >>> pm = expressions.PedalMark()
+        >>> po = expressions.PedalGapEnd()
+        >>> pm.addSpannedElements(po)
+        >>> pm.pedalForm = expressions.PedalForm.Line
+        >>> pm.pedalType = expressions.PedalType.Sustain
+        >>> po.placement = 'below'
+        >>> MEX = musicxml.m21ToXml.MeasureExporter()
+        >>> mxPedal = MEX.pedalObjectToXml(po)
+        >>> MEX.dump(mxPedal)
+        <direction placement="below">
+          <direction-type>
+            <pedal line="yes" type="resume" />
+          </direction-type>
+        </direction>
+        '''
+        pm: expressions.PedalMark|None = None
+        spanners: list[spanner.Spanner] = po.getSpannerSites()
+        for sp in spanners:
+            if isinstance(sp, expressions.PedalMark):
+                pm = sp
+                break
+
+        if pm is None:
+            # A PedalObject that is not in a PedalMark spanner
+            # doesn't make sense.  Ignore it.
+            return None
+
+        mxPedals: list[Element] = []
+        if isinstance(po, expressions.PedalBounce):
+            if pm.pedalForm in (expressions.PedalForm.Line, expressions.PedalForm.SymbolLine):
+                # Line or SymbolLine bounce is a quick up-down-tick in the line, so this
+                # is a pedal 'change'.
+                mxPedals = [Element('pedal')]
+                mxPedals[0].set('type', 'change')
+            elif pm.pedalForm == expressions.PedalForm.SymbolAlt:
+                # SymbolAlt bounce is just "Ped.", so just a pedal 'start'
+                mxPedals = [Element('pedal')]
+                if pm.pedalType == expressions.PedalType.Sustain:
+                    mxPedals[0].set('type', 'start')
+                elif pm.pedalType == expressions.PedalType.Sostenuto:
+                    mxPedals[0].set('type', 'sostenuto')
+                else:
+                    # not exactly right for Soft or Silent, but
+                    # we can hope that there is a text direction
+                    # somewhere before this that specifies which
+                    # pedal these "Ped." marks refer to.
+                    mxPedals[0].set('type', 'sustain')
+            elif pm.pedalForm == expressions.PedalForm.Symbol:
+                # Symbol bounce is "*Ped.", so a pedal 'stop' followed immediately by pedal 'start'
+                mxPedals = [Element('pedal'), Element('pedal')]
+                mxPedals[0].set('type', 'stop')
+                if pm.pedalType == expressions.PedalType.Sustain:
+                    mxPedals[1].set('type', 'start')
+                elif pm.pedalType == expressions.PedalType.Sostenuto:
+                    mxPedals[1].set('type', 'sostenuto')
+                else:
+                    # not exactly right for Soft or Silent, but
+                    # we can hope that there is a text direction
+                    # somewhere before this that specifies which
+                    # pedal these "Ped." marks refer to.
+                    mxPedals[1].set('type', 'sustain')
+            else:
+                # shouldn't be able to happen
+                return None
+
+        elif isinstance(po, expressions.PedalGapStart):
+            mxPedals = [Element('pedal')]
+            mxPedals[0].set('type', 'discontinue')
+        elif isinstance(po, expressions.PedalGapEnd):
+            mxPedals = [Element('pedal')]
+            mxPedals[0].set('type', 'resume')
+        else:
+            return None
+
+        for mxPedal in mxPedals:
+            if pm.pedalForm in (expressions.PedalForm.Line, expressions.PedalForm.SymbolLine):
+                mxPedal.set('line', 'yes')
+            else:
+                mxPedal.set('sign', 'yes')
+
+            # wrap in <direction><direction-type>
+            mxDirection = self.placeInDirection(mxPedal, po)
+            # placement goes on <direction>
+            self.setStyleAttributes(mxDirection, po, 'placement')
+            self.xmlRoot.append(mxDirection)
+
+        return mxDirection
+
+    def makePedalResumeLineXml(self, pm: expressions.PedalMark) -> Element:
+        # does not append to self.xmlRoot (caller will do that)
+        mxPedal = Element('pedal')
+        mxPedal.set('type', 'resume')
+        mxPedal.set('line', 'yes')
+        # wrap in <direction><direction-type>
+        mxDirection = self.placeInDirection(mxPedal, pm)
+        # placement (from pm spanner) goes on <direction>
+        self.setStyleAttributes(mxDirection, pm, 'placement')
+        return mxDirection
+
     def textExpressionToXml(
         self,
         teOrRe: expressions.TextExpression|repeat.RepeatExpression
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index f29514d4d..18a6a8b68 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -15,6 +15,7 @@ from __future__ import annotations
 import copy
 import fractions
 import io
+import weakref
 from math import isclose
 import re
 import typing as t
@@ -2409,6 +2410,12 @@ class MeasureParser(SoundTagMixin, XMLParserBase):
         # pick this up from the last measure.
         self.endedWithForwardTag: note.Rest|None = None
 
+        # Temporary storage of intended start offset of a PedalMark (we sometimes
+        # need to know this before the PedalMark or its first element have been
+        # inserted into a Stream).
+        # key is PedalMark; value is OffsetQL
+        self.pedalToStartOffset: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
+
     @staticmethod
     def getStaffNumber(mxObjectOrNumber) -> int:
         '''
@@ -4060,11 +4067,20 @@ class MeasureParser(SoundTagMixin, XMLParserBase):
         self.setPlacement(mxObj, orn)
         return orn
 
-    def xmlDirectionTypeToSpanners(self, mxObj):
+    def xmlDirectionTypeToSpanners(
+        self,
+        mxObj: ET.Element,
+        staffKey: int|None = None,
+        totalOffset: OffsetQL|None = None
+    ):
         # noinspection PyShadowingNames
         '''
-        Some spanners, such as MusicXML wedge, bracket, dashes, and ottava
-        are encoded as MusicXML directions.
+        Some spanners, such as MusicXML wedge, bracket, dashes, pedal,
+        and ottava are encoded as MusicXML directions.
+
+        :param mxObj: the specific direction element (e.g. <wedge>).
+        :param staffKey: staff number (required for <pedal>)
+        :param totalOffset: offset in measure of this direction (required for <pedal>)
 
         >>> from xml.etree.ElementTree import fromstring as EL
         >>> MP = musicxml.xmlToM21.MeasureParser()
@@ -4097,14 +4113,62 @@ class MeasureParser(SoundTagMixin, XMLParserBase):
         >>> sp = MP.spannerBundle[0]
         >>> sp
         <music21.dynamics.Crescendo <music21.note.Note D>>
+
+        >>> mxDirection = EL('<direction place="below"/>')
+        >>> mxDirectionType = EL('<pedal type="sostenuto" sign="yes" number="2"/>')
+        >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType, 1, 0.5)
+        >>> retList
+        [<music21.expressions.PedalMark>]
+        >>> pedalMark = retList[0]
+        >>> pedalMark.pedalType
+        <PedalType.Sostenuto>
+        >>> pedalMark.pedalForm
+        <PedalForm.Symbol>
+
+        >>> mxDirectionType1a = EL('<pedal type="resume" line="yes" number="2"/>')
+        >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType1a, 1, 0.5)
+        >>> retList
+        []
+        >>> pedalMark.pedalForm
+        <PedalForm.SymbolLine>
+
+        >>> mxDirectionType2 = EL('<pedal type="change" line="yes" number="2"/>')
+        >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType2, 1, 1.0)
+        >>> retList
+        []
+
+        >>> mxDirectionType3 = EL('<pedal type="discontinue" line="yes" number="2"/>')
+        >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType3, 1, 2.0)
+        >>> retList
+        []
+
+        >>> mxDirectionType4 = EL('<pedal type="resume" line="yes" number="2"/>')
+        >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType4, 1, 3.5)
+        >>> retList
+        []
+
+        >>> mxDirectionType5 = EL('<pedal type="stop" line="yes" number="2"/>')
+        >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType5, 1, 4.0)
+        >>> retList
+        []
+        >>> pedalMark.getFirst()
+        <music21.expressions.PedalBounce at 1.0>
+        >>> pedalMark.getLast() is n1
+        True
+        >>> MP.stream.elements
+        (<music21.expressions.PedalBounce at 1.0>, <music21.expressions.PedalGapStart at 2.0>,
+        <music21.expressions.PedalGapEnd at 3.5>)
         '''
         targetLast = self.nLast
         returnList = []
 
+        if totalOffset is not None:
+            totalOffset = opFrac(totalOffset)
+
         if mxObj.tag == 'wedge':
             mType = mxObj.get('type')
             if mType == 'crescendo':
-                spClass = dynamics.Crescendo
+                spClass: type[dynamics.DynamicWedge] = dynamics.Crescendo
             elif mType == 'diminuendo':
                 spClass = dynamics.Diminuendo
             elif mType == 'stop':
@@ -4217,6 +4281,81 @@ class MeasureParser(SoundTagMixin, XMLParserBase):
             else:
                 raise MusicXMLImportException(f'unidentified mxType of octave-shift: {mxType}')
 
+        if mxObj.tag == 'pedal':
+            mxType = mxObj.get('type')
+            mxAbbreviated = mxObj.get('abbreviated')
+            mxLine = mxObj.get('line')  # 'yes'/'no'
+            mxSign = mxObj.get('sign')  # 'yes'/'no'
+
+            idFound = mxObj.get('number')
+            if mxType in ('start', 'sostenuto'):
+                sp = expressions.PedalMark()
+                sp.idLocal = idFound
+                self.pedalToStartOffset[sp] = totalOffset
+
+                if mxType == 'start':
+                    sp.pedalType = expressions.PedalType.Sustain
+                elif mxType == 'sostenuto':
+                    sp.pedalType = expressions.PedalType.Sostenuto
+
+                if mxLine == 'yes':
+                    sp.pedalForm = expressions.PedalForm.Line
+                elif mxLine == 'no' or mxSign == 'yes':
+                    sp.pedalForm = expressions.PedalForm.Symbol
+
+                if mxAbbreviated == 'yes':
+                    sp.abbreviated = True
+
+                self.spannerBundle.append(sp)
+                returnList.append(sp)
+                self.spannerBundle.setPendingSpannedElementAssignment(sp, 'GeneralNote')
+            elif mxType in ('continue', 'stop', 'discontinue', 'resume', 'change'):
+                spb = self.spannerBundle.getByClassIdLocalComplete(
+                    'PedalMark', idFound, False  # get first
+                )
+                try:
+                    sp = spb[0]
+                except IndexError:
+                    raise MusicXMLImportException('Error in getting PedalMark')
+                if mxType == 'continue':
+                    # pass?  I don't think I need to remember a continue, and if
+                    # someone fills this spanner, we won't know this particular
+                    # GeneralNote is special, anyway.  If continues are truly
+                    # important, they should probably end the spanner and start
+                    # a new one.
+                    pass
+                    # self.spannerBundle.setPendingSpannedElementAssignment(sp, 'GeneralNote')
+                elif mxType == 'discontinue':
+                    # insert a PedalGapStart
+                    pgStart = expressions.PedalGapStart()
+                    self.insertCoreAndRef(totalOffset, staffKey, pgStart)
+                    sp.addSpannedElements(pgStart)
+                elif mxType == 'resume':
+                    # If the current pedalForm is Symbol, and we're still at the start
+                    # offset of the PedalMark, change pedalForm to SymbolLine (because
+                    # we had a symbol, and now we're starting a line without a downtick;
+                    # that is the definition of SymbolLine).
+                    pedalStartOffset: OffsetQL|None = self.pedalToStartOffset.get(sp, None)
+                    if (sp.pedalForm == expressions.PedalForm.Symbol
+                            and pedalStartOffset == totalOffset):
+                        sp.pedalForm = expressions.PedalForm.SymbolLine
+                    else:
+                        # insert a PedalGapEnd
+                        pgEnd = expressions.PedalGapEnd()
+                        self.insertCoreAndRef(totalOffset, staffKey, pgEnd)
+                        sp.addSpannedElements(pgEnd)
+                elif mxType == 'change':
+                    # insert a PedalBounce
+                    pb = expressions.PedalBounce()
+                    self.insertCoreAndRef(totalOffset, staffKey, pb)
+                    sp.addSpannedElements(pb)
+                elif mxType == 'stop':
+                    sp.completeStatus = True
+                    if targetLast is not None:
+                        sp.addSpannedElements(targetLast)
+            else:
+                raise MusicXMLImportException(f'unidentified mxType of pedal: {mxType}')
+
         return returnList
 
     def xmlNotationsToSpanners(self, mxNotations, n):
@@ -5245,9 +5384,11 @@ class MeasureParser(SoundTagMixin, XMLParserBase):
             for mxDyn in mxDir:
                 self.setDynamicsDirection(mxDir, mxDyn, mxDirection, staffKey, totalOffset)
 
-        elif tag in ('wedge', 'bracket', 'dashes', 'octave-shift'):
+        elif tag in ('wedge', 'bracket', 'dashes', 'octave-shift', 'pedal'):
             try:
-                spannerList = self.xmlDirectionTypeToSpanners(mxDir)
+                spannerList = self.xmlDirectionTypeToSpanners(
+                    mxDir, staffKey, totalOffset
+                )
             except MusicXMLImportException as excep:
                 warnings.warn(f'Could not import {tag}: {excep}', MusicXMLWarning)
                 spannerList = []
diff --git a/music21/stream/base.py b/music21/stream/base.py
index bb8b0f53a..6a840b25c 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -4568,7 +4568,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         >>> len(beachExcerpt.spannerBundle)
         8
         >>> len(beachIn.spannerBundle)
-        93
+        94
 
         * Changed in v7: does not create measures automatically.
         * Changed in v7: If `gatherSpanners` is True or GatherSpanners.ALL (default),


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index d6ef8a319..a37258c64 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -1192,6 +1192,565 @@ pitches01a = """<?xml version="1.0" encoding="UTF-8"?>
 </score-partwise>
 """
 
+pedalLines = '''<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN"
+                                "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="2.0">
+  <movement-title>MusicXML pedals (lines)</movement-title>
+  <identification>
+    <miscellaneous>
+      <miscellaneous-field name="description">All &lt;pedal&gt; element contents
+          defined in MusicXML. The lyrics for each note describes the pedal
+          element assigned to that note.</miscellaneous-field>
+    </miscellaneous>
+  </identification>
+  <part-list>
+    <score-part id="P1">
+      <part-name print-object="no">MusicXML Part</part-name>
+    </score-part>
+  </part-list>
+  <!--=========================================================-->
+  <part id="P1">
+    <!--=======================================================-->
+    <measure number="1" width="273">
+      <attributes>
+         <divisions>8</divisions>
+         <key>
+            <fifths>0</fifths>
+            <mode>major</mode>
+         </key>
+         <time>
+            <beats>6</beats>
+            <beat-type>8</beat-type>
+         </time>
+         <clef number="1">
+            <sign>F</sign>
+            <line>4</line>
+         </clef>
+      </attributes>
+      <direction>
+         <direction-type>
+            <pedal line="yes" sign="no" type="start" default-y="-79" relative-x="-12"/>
+         </direction-type>
+      </direction>
+      <note default-x="78">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="110">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <note default-x="143">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="change"/>
+         </direction-type>
+      </direction>
+      <note default-x="175">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="207">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="discontinue"/>
+         </direction-type>
+         <direction-type>
+            <words default-y="-79" font-size="7.2" font-style="italic">simile</words>
+         </direction-type>
+      </direction>
+      <note default-x="240">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+    </measure>
+    <measure number="2" width="208">
+      <note default-x="13">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="46">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <note default-x="78">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+      <note default-x="110">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="143">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <note default-x="175">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+    </measure>
+    <measure number="3" width="208">
+      <note default-x="13">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="46">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="resume" default-y="-79" relative-x="-3"/>
+         </direction-type>
+      </direction>
+      <note default-x="78">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="change"/>
+         </direction-type>
+       </direction>
+      <note default-x="111">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="142">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="stop"/>
+         </direction-type>
+      </direction>
+      <note default-x="175">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+    </measure>
+  </part>
+  <!--=========================================================-->
+</score-partwise>
+'''
+
+pedalSymLines = '''<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN"
+                                "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="2.0">
+  <movement-title>MusicXML pedals (symbol start/lines everywhere)</movement-title>
+  <identification>
+    <miscellaneous>
+      <miscellaneous-field name="description">All &lt;pedal&gt; element contents
+          defined in MusicXML. The lyrics for each note describes the pedal
+          element assigned to that note.</miscellaneous-field>
+    </miscellaneous>
+  </identification>
+  <part-list>
+    <score-part id="P1">
+      <part-name print-object="no">MusicXML Part</part-name>
+    </score-part>
+  </part-list>
+  <!--=========================================================-->
+  <part id="P1">
+    <!--=======================================================-->
+    <measure number="1" width="273">
+      <attributes>
+         <divisions>8</divisions>
+         <key>
+            <fifths>0</fifths>
+            <mode>major</mode>
+         </key>
+         <time>
+            <beats>6</beats>
+            <beat-type>8</beat-type>
+         </time>
+         <clef number="1">
+            <sign>F</sign>
+            <line>4</line>
+         </clef>
+      </attributes>
+      <direction>
+         <direction-type>
+            <pedal line="no" sign="yes" type="start" default-y="-79" relative-x="-12"/>
+         </direction-type>
+      </direction>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="resume"/>
+         </direction-type>
+      </direction>
+      <note default-x="78">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="110">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <note default-x="143">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="change"/>
+         </direction-type>
+      </direction>
+      <note default-x="175">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="207">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="discontinue"/>
+         </direction-type>
+         <direction-type>
+            <words default-y="-79" font-size="7.2" font-style="italic">simile</words>
+         </direction-type>
+      </direction>
+      <note default-x="240">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+    </measure>
+    <measure number="2" width="208">
+      <note default-x="13">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="46">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <note default-x="78">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+      <note default-x="110">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="143">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <note default-x="175">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+    </measure>
+    <measure number="3" width="208">
+      <note default-x="13">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="46">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="resume" default-y="-79" relative-x="-3"/>
+         </direction-type>
+      </direction>
+      <note default-x="78">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="change"/>
+         </direction-type>
+       </direction>
+      <note default-x="111">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">begin</beam>
+      </note>
+      <note default-x="142">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">continue</beam>
+      </note>
+      <direction>
+         <direction-type>
+            <pedal line="yes" type="stop"/>
+         </direction-type>
+      </direction>
+      <note default-x="175">
+         <pitch>
+            <step>D</step>
+            <octave>3</octave>
+         </pitch>
+         <duration>4</duration>
+         <voice>1</voice>
+         <type>eighth</type>
+         <stem default-y="-55">down</stem>
+         <beam number="1">end</beam>
+      </note>
+    </measure>
+  </part>
+  <!--=========================================================-->
+</score-partwise>
+'''
+
 # noinspection SpellCheckingInspection
 directions31a = '''<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN"
@@ -20044,7 +20603,7 @@ ALL = [
     tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied, pianoStaffPolymeter,  # 46
     arpeggio32d, multiStaffArpeggios, multiMeasureEnding,   # 51
     pianoStaffPolymeterWithClefOctaveChange, multipleFingeringsOnChord,  # 54
-    pianoStaffWithOttava  # 56
+    pianoStaffWithOttava, pedalLines, pedalSymLines  # 56
 ]
 
 
diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py
index 0dabca2d7..b9cb461e3 100644
--- a/music21/musicxml/test_m21ToXml.py
+++ b/music21/musicxml/test_m21ToXml.py
@@ -684,6 +684,86 @@ class Test(unittest.TestCase):
             int(tree.findall('.//direction/offset')[0].text),
             defaults.divisionsPerQuarter)
 
+    def testPedals(self):
+        expectedResults1 = (
+            {
+                'type': 'start',
+                'line': 'yes',
+                'number': '1',
+            },
+            {
+                'type': 'change',
+                'line': 'yes',
+            },
+            {
+                'type': 'discontinue',
+                'line': 'yes',
+            },
+            {
+                'type': 'resume',
+                'line': 'yes',
+            },
+            {
+                'type': 'change',
+                'line': 'yes',
+            },
+            {
+                'type': 'stop',
+                'line': 'yes',
+                'number': '1',
+            },
+        )
+        s = converter.parse(testPrimitive.pedalLines)
+        x = self.getET(s)
+        mxPart = x.find('part')
+        for i, mxPedal in enumerate(mxPart.findall('.//pedal')):
+            with self.subTest(pedal_index=i):
+                for k in expectedResults1[i]:
+                    self.assertEqual(mxPedal.get(k, ''), expectedResults1[i][k])
+
+        startIdx = len(expectedResults1)
+
+        expectedResults2 = (
+            {
+                'type': 'start',
+                'sign': 'yes',
+                'number': '1',
+            },
+            {
+                'type': 'resume',
+                'line': 'yes',
+            },
+            {
+                'type': 'change',
+                'line': 'yes',
+            },
+            {
+                'type': 'discontinue',
+                'line': 'yes',
+            },
+            {
+                'type': 'resume',
+                'line': 'yes',
+            },
+            {
+                'type': 'change',
+                'line': 'yes',
+            },
+            {
+                'type': 'stop',
+                'line': 'yes',
+                'number': '1',
+            },
+        )
+
+        s = converter.parse(testPrimitive.pedalSymLines)
+        x = self.getET(s)
+        mxPart = x.find('part')
+        for i, mxPedal in enumerate(mxPart.findall('.//pedal')):
+            with self.subTest(pedal_index=startIdx + i):
+                for k in expectedResults2[i]:
+                    self.assertEqual(mxPedal.get(k, ''), expectedResults2[i][k])
+
     def testArpeggios(self):
         expectedResults = (
             'arpeggiate',
diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py
index dd4450bb0..005172424 100644
--- a/music21/musicxml/test_xmlToM21.py
+++ b/music21/musicxml/test_xmlToM21.py
@@ -1070,6 +1070,81 @@ class Test(unittest.TestCase):
         self.assertEqual(rmIterator[2].content, 'Test')
         self.assertEqual(rmIterator[2].style.enclosure, 'square')
 
+    def testPedalMarks(self):
+        from music21 import converter
+        from music21 import corpus
+        from music21.musicxml import testPrimitive
+
+        s = converter.parse(testPrimitive.directions31a)
+        pedals = list(s[expressions.PedalMark])
+        self.assertEqual(len(pedals), 1)
+        pm = pedals[0]
+        pm.fill(s)
+        self.assertIsNone(pm.pedalForm)
+        self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
+        spElements = pm.getSpannedElements()
+        self.assertEqual(len(spElements), 4)
+        expectedOffsets = [0., 1., 1., 2.]
+        for i, (el, expectedOffset) in enumerate(zip(spElements, expectedOffsets)):
+            if i == 1:
+                self.assertIsInstance(el, expressions.PedalBounce)
+            else:
+                self.assertIsInstance(el, note.Note)
+                self.assertEqual(el.fullName, 'C in octave 4 Quarter Note')
+            self.assertEqual(el.offset, expectedOffset)
+
+        s = converter.parse(testPrimitive.spanners33a)
+        pedals = list(s[expressions.PedalMark])
+        self.assertEqual(len(pedals), 1)
+        pm = pedals[0]
+        pm.fill(s)
+        self.assertIsNone(pm.pedalForm)
+        self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
+        spElements = pm.getSpannedElements()
+        self.assertEqual(len(spElements), 3)
+        expectedOffsets = [0., 1., 1.]
+        for i, (el, expectedOffset) in enumerate(zip(spElements, expectedOffsets)):
+            if i == 1:
+                self.assertIsInstance(el, expressions.PedalBounce)
+            else:
+                self.assertIsInstance(el, note.Note)
+                self.assertEqual(el.fullName, 'B in octave 4 Quarter Note')
+            self.assertEqual(el.offset, expectedOffset)
+
+        s = corpus.parse('beach')
+        pedals = list(s[expressions.PedalMark])
+        self.assertEqual(len(pedals), 1)
+        pm = pedals[0]
+        pm.fill(s.parts[5])
+        self.assertEqual(pm.pedalForm, expressions.PedalForm.Symbol)
+        self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
+        spElements = pm.getSpannedElements()
+        self.assertEqual(len(spElements), 2)
+        self.assertIsInstance(spElements[0], chord.Chord)
+        self.assertEqual(
+            spElements[0].fullName,
+            'Chord {E-flat in octave 2 | B-flat in octave 2} Whole'
+        )
+        self.assertEqual(spElements[0].offset, 0.)
+        self.assertIsInstance(spElements[1], note.Note)
+        self.assertEqual(spElements[1].fullName, 'E-flat in octave 1 Whole Note')
+        self.assertEqual(spElements[1].offset, 0.)
+
+        s = corpus.parse('dichterliebe_no2')
+        pedals = list(s[expressions.PedalMark])
+        self.assertEqual(len(pedals), 1)
+        pm = pedals[0]
+        pm.fill(s.parts[2])
+        self.assertEqual(pm.pedalForm, expressions.PedalForm.Symbol)
+        self.assertEqual(pm.pedalType, expressions.PedalType.Sustain)
+        spElements = pm.getSpannedElements()
+        self.assertEqual(len(spElements), 5)
+        expectedOffsets = [1.5, 1.75, 0., 0.75, 1.0]
+        for i, (el, expectedOffset) in enumerate(zip(spElements, expectedOffsets)):
+            self.assertIsInstance(el, note.Note)
+            self.assertEqual(el.nameWithOctave, 'A3')
+            self.assertEqual(el.offset, expectedOffset)
+
     def testNoChordImport(self):
         from music21 import converter