music21.musicxml.partStaffExporter

A mixin to ScoreExporter that includes the capabilities for producing a single MusicXML <part> from multiple music21 PartStaff objects.

PartStaffExporterMixin

class music21.musicxml.partStaffExporter.PartStaffExporterMixin

PartStaffExporterMixin methods

PartStaffExporterMixin.addStaffTagsMultiStaffParts(group: StaffGroup)

Create child <staff> tags under each <note>, <direction>, and <forward> element in the <part>s being joined.

Called by joinPartStaffs().

>>> from music21.musicxml import testPrimitive
>>> s = converter.parse(testPrimitive.pianoStaff43a)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> root = SX.parse()
>>> m1 = root.find('part/measure')
>>> SX.dump(m1)
<measure implicit="no" number="1">
...
  <note>
    <pitch>
      <step>F</step>
      <octave>4</octave>
    </pitch>
    <duration>40320</duration>
    <voice>1</voice>
    <type>whole</type>
    <staff>1</staff>
  </note>
  <backup>
    <duration>40320</duration>
  </backup>
  <note>
    <pitch>
      <step>B</step>
      <octave>2</octave>
    </pitch>
    <duration>40320</duration>
    <voice>2</voice>
    <type>whole</type>
    <staff>2</staff>
  </note>
</measure>
PartStaffExporterMixin.cleanUpSubsequentPartStaffs(group: StaffGroup)

Now that the contents of all PartStaffs in group have been represented by a single PartExporter, remove any obsolete PartExporter from self.partExporterList.

Called by joinPartStaffs()

>>> from music21.musicxml import testPrimitive
>>> s = converter.parse(testPrimitive.pianoStaff43a)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> SX.scorePreliminaries()
>>> SX.parsePartlikeScore()
>>> len(SX.partExporterList)
2
>>> SX.postPartProcess()
>>> len(SX.partExporterList)
1
PartStaffExporterMixin.getRootForPartStaff(partStaff: PartStaff) Element

Look up the <part> Element being used to represent the music21 partStaff.

>>> from music21.musicxml import testPrimitive
>>> s = converter.parse(testPrimitive.pianoStaff43a)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> SX.scorePreliminaries()
>>> SX.parsePartlikeScore()
>>> SX.getRootForPartStaff(s.parts[0])
<Element 'part' at 0x...
>>> other = stream.PartStaff()
>>> other.id = 'unrelated'
>>> SX.getRootForPartStaff(other)
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
    <music21.stream.PartStaff unrelated> not found in self.partExporterList
PartStaffExporterMixin.joinPartStaffs()

Collect <part> elements exported from PartStaff objects under a single <part> element using <staff> and <voice> subelements.

Here we load in a simple 2-staff piano piece. Note that they are both elements of the PartStaff Stream subclass.

>>> from music21.musicxml import testPrimitive
>>> s = converter.parse(testPrimitive.pianoStaff43a)
>>> s.show('text')
{0.0} <music21.metadata.Metadata object at 0x107d6a100>
{0.0} <music21.stream.PartStaff P1-Staff1>
    {0.0} <music21.instrument.Instrument 'P1: MusicXML Part: '>
    {0.0} <music21.stream.Measure 1 offset=0.0>
        {0.0} <music21.clef.TrebleClef>
        {0.0} <music21.key.KeySignature of no sharps or flats>
        {0.0} <music21.meter.TimeSignature 4/4>
        {0.0} <music21.note.Note F>
{0.0} <music21.stream.PartStaff P1-Staff2>
    {0.0} <music21.instrument.Instrument 'P1: MusicXML Part: '>
    {0.0} <music21.stream.Measure 1 offset=0.0>
        {0.0} <music21.clef.BassClef>
        {0.0} <music21.key.KeySignature of no sharps or flats>
        {0.0} <music21.meter.TimeSignature 4/4>
        {0.0} <music21.note.Note B>
{0.0} <music21.layout.StaffGroup
         <music21.stream.PartStaff P1-Staff1><music21.stream.PartStaff P1-Staff2>>

Now these get joined into a single part in the parse() process:

>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> root = SX.parse()
>>> parts = root.findall('part')
>>> len(parts)
1
>>> clefs = root.findall('.//clef')
>>> len(clefs)
2

Note that there are exactly two notes (an F and B) in the original score, so there are exactly two staff tags in the output.

>>> staffTags = root.findall('part/measure/note/staff')
>>> len(staffTags)
2
PartStaffExporterMixin.joinableGroups() list[music21.layout.StaffGroup]

Returns a list of StaffGroup objects that represent PartStaff objects that can be joined into a single MusicXML <part>, so long as there exists a PartExporter for it in ScoreExporter.partExporterList.

Sets previousPartStaffInGroup.

>>> s = stream.Score()

Group 1: three staves.

>>> p1a = stream.PartStaff(id='p1a')
>>> p1a.insert(0, stream.Measure())
>>> p1b = stream.PartStaff(id='p1b')
>>> p1b.insert(0, stream.Measure())
>>> p1c = stream.PartStaff(id='p1c')
>>> p1c.insert(0, stream.Measure())
>>> sg1 = layout.StaffGroup([p1a, p1b, p1c])

Group 2: two staves.

>>> p2a = stream.PartStaff(id='p2a')
>>> p2a.insert(0, stream.Measure())
>>> p2b = stream.PartStaff(id='p2b')
>>> p2b.insert(0, stream.Measure())
>>> sg2 = layout.StaffGroup([p2a, p2b])

Group 3: one staff – will not be merged.

>>> p3a = stream.PartStaff(id='p3a')
>>> p3a.insert(0, stream.Measure())
>>> sg3 = layout.StaffGroup([p3a])

Group 4: two staves, but no measures, will not be merged:

>>> p4a = stream.PartStaff(id='p4a')
>>> p4b = stream.PartStaff(id='p4b')
>>> sg4 = layout.StaffGroup([p4a, p4b])

Group 5: two staves, but no staff group

>>> p5a = stream.PartStaff(id='p5a')
>>> p5a.insert(0, stream.Measure())
>>> p5b = stream.PartStaff(id='p5b')
>>> p5b.insert(0, stream.Measure())

Group 6: same as Group 2, just to show that valid groups can come later

>>> p6a = stream.PartStaff(id='p6a')
>>> p6a.insert(0, stream.Measure())
>>> p6b = stream.PartStaff(id='p6b')
>>> p6b.insert(0, stream.Measure())
>>> sg6 = layout.StaffGroup([p6a, p6b])

Group 7: same as Group 6, but with Parts instead of PartStaffs

>>> p7a = stream.Part(id='p7a')
>>> p7a.insert(0, stream.Measure())
>>> p7b = stream.Part(id='p7b')
>>> p7b.insert(0, stream.Measure())
>>> sg7 = layout.StaffGroup([p7a, p7b])

Group 8: encloses same objects as Group 6, just to show it’s gracefully ignored

>>> sg8 = layout.StaffGroup([p6a, p6b])
>>> for el in (p1a, p1b, p1c, sg1, p2a, p2b, sg2, p3a, sg3,
...            p4a, p4b, sg4, p5a, p5b, p6a, p6b, sg6, p7a, p7b, sg7, sg8):
...     s.insert(0, el)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> SX.scorePreliminaries()
>>> SX.parsePartlikeScore()  # populate .partExporterList
>>> SX.joinableGroups()
[<music21.layout.StaffGroup <... p1a><... p1b><... p1c>>,
 <music21.layout.StaffGroup <... p2a><... p2b>>,
 <music21.layout.StaffGroup <... p6a><... p6b>>]
static PartStaffExporterMixin.moveMeasureContents(measure: Element, otherMeasure: Element, staffNumber: int)

Move the child elements of measure into otherMeasure; create voice numbers if needed; bump voice numbers if they conflict; account for <backup> and <forward> tags; skip <print> tags; set “number” on midmeasure clef changes; replace existing <barline> tags.

>>> from xml.etree.ElementTree import fromstring as El
>>> measure = El('<measure><note /></measure>')
>>> otherMeasure = El('<measure><note /></measure>')
>>> SX = musicxml.m21ToXml.ScoreExporter
>>> SX.moveMeasureContents(measure, otherMeasure, 2)
>>> SX().dump(otherMeasure)
<measure>
  <note>
    <voice>1</voice>
  </note>
  <note>
    <voice>2</voice>
  </note>
</measure>
>>> SX.moveMeasureContents(El('<junk />'), otherMeasure, 2)
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
    moveMeasureContents() called on <Element 'junk'...

Only one <barline> should be exported per merged measure:

>>> from music21.musicxml import testPrimitive
>>> s = converter.parse(testPrimitive.mixedVoices1a)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> root = SX.parse()
>>> root.findall('part/measure/barline')
[<Element 'barline' at 0x...]
PartStaffExporterMixin.movePartStaffMeasureContents(group: StaffGroup)

For every <part> after the first, find the corresponding measure in the initial <part> and merge the contents by inserting all contained elements.

Called by joinPartStaffs()

StaffGroup must be a valid one from joinableGroups().

PartStaffExporterMixin.processSubsequentPartStaff(target: Element, source: Element, staffNum: int) dict[int, list[xml.etree.ElementTree.Element]]

Move elements from subsequent PartStaff’s measures into target: the <part> element representing the initial PartStaff that will soon represent the merged whole.

Called by movePartStaffMeasureContents(), which is in turn called by joinPartStaffs().

PartStaffExporterMixin.setEarliestAttributesAndClefsPartStaff(group: StaffGroup)

Set the <staff>, <key>, <time>, and <clef> information on the earliest measure <attributes> tag in the <part> representing the joined PartStaffs.

Need the earliest <attributes> tag, which may not exist in the merged <part> until moved there by movePartStaffMeasureContents() – e.g. RH of piano doesn’t appear until m. 40, and earlier music for LH needs to be merged first in order to find earliest <attributes>.

Called by joinPartStaffs()

Multiple keys:

>>> from music21.musicxml import testPrimitive
>>> xmlDir = common.getSourceFilePath() / 'musicxml' / 'lilypondTestSuite'
>>> s = converter.parse(xmlDir / '43b-MultiStaff-DifferentKeys.xml')
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> root = SX.parse()
>>> m1 = root.find('part/measure')
>>> SX.dump(m1)
<measure implicit="no" number="1">
  <attributes>
    <divisions>10080</divisions>
    <key number="1">
      <fifths>0</fifths>
    </key>
    <key number="2">
      <fifths>2</fifths>
    </key>
    <time>
      <beats>4</beats>
      <beat-type>4</beat-type>
    </time>
    <staves>2</staves>
    <clef number="1">
      <sign>G</sign>
      <line>2</line>
    </clef>
    <clef number="2">
      <sign>F</sign>
      <line>4</line>
    </clef>
  </attributes>
...
</measure>

Multiple meters (not very well supported by MusicXML readers):

>>> from music21.musicxml import testPrimitive
>>> s = converter.parse(testPrimitive.pianoStaffPolymeterWithClefOctaveChange)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> root = SX.parse()
>>> m1 = root.find('part/measure')
>>> SX.dump(m1)
<measure implicit="no" number="1">
    <attributes>
    <divisions>10080</divisions>
    <key>
        <fifths>0</fifths>
    </key>
    <time number="1">
        <beats>4</beats>
        <beat-type>4</beat-type>
    </time>
    <time number="2">
        <beats>2</beats>
        <beat-type>2</beat-type>
    </time>
    <staves>2</staves>
    <clef number="1">
        <sign>G</sign>
        <line>2</line>
    </clef>
    <clef number="2">
        <sign>G</sign>
        <line>2</line>
        <clef-octave-change>-1</clef-octave-change>
    </clef>
    </attributes>
...
</measure>

Functions

music21.musicxml.partStaffExporter.addStaffTags(measure: Element, staffNumber: int, tagList: list[str])

For a <measure> tag measure, add a <staff> grandchild to any instance of a child tag of a type in tagList.

>>> from xml.etree.ElementTree import fromstring as El
>>> from music21.musicxml.partStaffExporter import addStaffTags
>>> from music21.musicxml.helpers import dump
>>> elem = El("""
...     <measure number="1">
...        <note>
...          <rest measure="yes" />
...          <duration>8</duration>
...        </note>
...      </measure>"""
...     )
>>> addStaffTags(elem, 2, tagList=['note', 'forward', 'direction', 'harmony'])
>>> dump(elem)
<measure number="1">
  <note>
    <rest measure="yes" />
    <duration>8</duration>
    <staff>2</staff>
  </note>
</measure>

Raise if a <staff> grandchild is already present:

>>> addStaffTags(elem, 2, tagList=['note', 'forward', 'direction'])
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
    In part (), measure (1): Attempted to create a second <staff> tag

The function doesn’t accept elements other than <measure>:

>>> addStaffTags(elem.find('note'), 2, tagList=['direction'])
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
    addStaffTags() only accepts <measure> tags