def addStaffTags(measure: Element,
                 staffNumber: int,
                 tagList: Optional[List[str]] = None):
    '''
    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
    '''
    if measure.tag != 'measure':
        raise MusicXMLExportException(
            'addStaffTags() only accepts <measure> tags')
    for tagName in tagList:
        for tag in measure.findall(tagName):
            if tag.find('staff') is not None:
                e = MusicXMLExportException(
                    'Attempted to create a second <staff> tag')
                e.measureNumber = measure.get('number')
                raise e
            mxStaff = Element('staff')
            mxStaff.text = str(staffNumber)
            helpers.insertBeforeElements(
                tag,
                mxStaff,
                tagList=['beam', 'notations', 'lyric', 'play', 'sound'])
    def moveMeasureContents(measure: Element, otherMeasure: Element,
                            staffNumber: int):
        # noinspection PyShadowingNames
        '''
        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...]
        '''
        if measure.tag != 'measure' or otherMeasure.tag != 'measure':
            raise MusicXMLExportException(
                f'moveMeasureContents() called on {measure} and {otherMeasure} (not measures).'
            )
        maxVoices: int = 0
        otherMeasureLackedVoice: bool = False

        for voice in otherMeasure.findall('*/voice'):
            maxVoices = max(maxVoices, int(voice.text))

        if maxVoices == 0:
            otherMeasureLackedVoice = True
            for elem in otherMeasure.findall('note'):
                voice = Element('voice')
                voice.text = '1'
                helpers.insertBeforeElements(elem,
                                             voice,
                                             tagList=[
                                                 'type',
                                                 'dot',
                                                 'accidental',
                                                 'time-modification',
                                                 'stem',
                                                 'notehead',
                                                 'notehead-text',
                                                 'staff',
                                             ])
            maxVoices = 1

        # Create <backup>
        amountToBackup: int = 0
        for dur in otherMeasure.findall('note/duration'):
            amountToBackup += int(dur.text)
        for dur in otherMeasure.findall('forward/duration'):
            amountToBackup += int(dur.text)
        for backupDur in otherMeasure.findall('backup/duration'):
            amountToBackup -= int(backupDur.text)
        if amountToBackup:
            mxBackup = Element('backup')
            mxDuration = SubElement(mxBackup, 'duration')
            mxDuration.text = str(amountToBackup)
            otherMeasure.append(mxBackup)

        # Move elements
        for elem in measure.findall('*'):
            # Skip elements that already exist in otherMeasure
            if elem.tag == 'print':
                continue
            if elem.tag == 'attributes':
                if elem.findall('divisions'):
                    # This is likely the initial mxAttributes
                    continue
                for midMeasureClef in elem.findall('clef'):
                    midMeasureClef.set('number', str(staffNumber))
            if elem.tag == 'barline':
                # Remove existing <barline>, if any
                for existingBarline in otherMeasure.findall('barline'):
                    otherMeasure.remove(existingBarline)
            if elem.tag == 'note':
                voice = elem.find('voice')
                if voice is not None:
                    if otherMeasureLackedVoice:
                        # otherMeasure assigned voice 1; Bump voice number here
                        voice.text = str(int(voice.text) + 1)
                    else:
                        pass  # No need to alter existing voice numbers
                else:
                    voice = Element('voice')
                    voice.text = str(maxVoices + 1)
                    helpers.insertBeforeElements(elem,
                                                 voice,
                                                 tagList=[
                                                     'type', 'dot',
                                                     'accidental',
                                                     'time-modification',
                                                     'stem', 'notehead',
                                                     'notehead-text', 'staff'
                                                 ])
            # Append to otherMeasure
            otherMeasure.append(elem)
    def setEarliestAttributesAndClefsPartStaff(self, 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 :meth:`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 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.pianoStaffPolymeter)
        >>> SX = musicxml.m21ToXml.ScoreExporter(s)
        >>> root = SX.parse()
        >>> m1 = root.find('part/measure')
        >>> SX.dump(m1)
        <measure 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>F</sign>
                <line>4</line>
            </clef>
            </attributes>
        ...
        </measure>
        '''
        def isMultiAttribute(m21Class, comparison: str = '__eq__') -> bool:
            '''
            Return True if any first instance of m21Class in any subsequent staff
            in this StaffGroup does not compare to the first instance of that class
            in the earliest staff where found (not necessarily the first) using `comparison`.
            '''
            initialM21Instance: Optional[m21Class] = None
            for ps in group:
                if initialM21Instance is None:
                    initialM21Instance = ps.recurse().getElementsByClass(
                        m21Class).first()
                else:
                    firstInstanceSubsequentStaff = ps.recurse(
                    ).getElementsByClass(m21Class).first()
                    if firstInstanceSubsequentStaff is not None:
                        comparisonWrapper = getattr(
                            firstInstanceSubsequentStaff, comparison)
                        if not comparisonWrapper(initialM21Instance):
                            return True
                        # else, keep looking: 3+ staves
                    # else, keep looking: 3+ staves
            return False

        multiKey: bool = isMultiAttribute(KeySignature)
        multiMeter: bool = isMultiAttribute(TimeSignature,
                                            comparison='ratioEqual')

        initialPartStaffRoot: Optional[Element] = None
        mxAttributes: Optional[Element] = None
        for i, ps in enumerate(group):
            staffNumber: int = i + 1  # 1-indexed

            # Initial PartStaff in group: find earliest mxAttributes, set clef #1 and <staves>
            if initialPartStaffRoot is None:
                initialPartStaffRoot = self.getRootForPartStaff(ps)
                mxAttributes: Element = initialPartStaffRoot.find(
                    'measure/attributes')
                clef1: Optional[Element] = mxAttributes.find('clef')
                if clef1 is not None:
                    clef1.set('number', '1')

                mxStaves = Element('staves')
                mxStaves.text = str(len(group))
                helpers.insertBeforeElements(mxAttributes,
                                             mxStaves,
                                             tagList=[
                                                 'part-symbol', 'instruments',
                                                 'clef', 'staff-details',
                                                 'transpose', 'directive',
                                                 'measure-style'
                                             ])

                if multiKey:
                    key1 = mxAttributes.find('key')
                    if key1:
                        key1.set('number', '1')
                if multiMeter:
                    meter1 = mxAttributes.find('time')
                    if meter1:
                        meter1.set('number', '1')

            # Subsequent PartStaffs in group: set additional clefs on mxAttributes
            else:
                thisPartStaffRoot: Element = self.getRootForPartStaff(ps)
                oldClef: Optional[Element] = thisPartStaffRoot.find(
                    'measure/attributes/clef')
                if oldClef is not None and mxAttributes is not None:
                    clefsInMxAttributesAlready = mxAttributes.findall('clef')
                    if len(clefsInMxAttributesAlready) >= staffNumber:
                        e = MusicXMLExportException(
                            'Attempted to add more clefs than staffs')
                        e.partName = ps.partName
                        raise e

                    # Set initial clef for this staff
                    newClef = Element('clef')
                    newClef.set('number', str(staffNumber))
                    newSign = SubElement(newClef, 'sign')
                    newSign.text = oldClef.find('sign').text
                    newLine = SubElement(newClef, 'line')
                    newLine.text = oldClef.find('line').text
                    helpers.insertBeforeElements(mxAttributes,
                                                 newClef,
                                                 tagList=[
                                                     'staff-details',
                                                     'transpose', 'directive',
                                                     'measure-style'
                                                 ])

                if multiMeter:
                    oldMeter: Optional[Element] = thisPartStaffRoot.find(
                        'measure/attributes/time')
                    if oldMeter:
                        oldMeter.set('number', str(staffNumber))
                        helpers.insertBeforeElements(mxAttributes,
                                                     oldMeter,
                                                     tagList=['staves'])
                if multiKey:
                    oldKey: Optional[Element] = thisPartStaffRoot.find(
                        'measure/attributes/key')
                    if oldKey:
                        oldKey.set('number', str(staffNumber))
                        helpers.insertBeforeElements(
                            mxAttributes, oldKey, tagList=['time', 'staves'])
Exemple #4
0
    def setEarliestAttributesAndClefsPartStaff(self, group: StaffGroup):
        '''
        Set the <staff> 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 :meth:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin.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 number="1">
          <attributes>
            <divisions>10080</divisions>
            <key>
              <fifths>0</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>
        '''
        initialPartStaffRoot: Optional[Element] = None
        mxAttributes: Optional[Element] = None
        for i, ps in enumerate(group):
            staffNumber: int = i + 1  # 1-indexed

            # Initial PartStaff in group: find earliest mxAttributes, set clef #1 and <staves>
            if initialPartStaffRoot is None:
                initialPartStaffRoot = self.getRootForPartStaff(ps)
                mxAttributes: Element = initialPartStaffRoot.find(
                    'measure/attributes')
                clef1: Optional[Element] = mxAttributes.find('clef')
                if clef1 is not None:
                    clef1.set('number', '1')

                mxStaves = Element('staves')
                mxStaves.text = str(len(group))
                helpers.insertBeforeElements(mxAttributes,
                                             mxStaves,
                                             tagList=[
                                                 'part-symbol', 'instruments',
                                                 'clef', 'staff-details',
                                                 'transpose', 'directive',
                                                 'measure-style'
                                             ])

            # Subsequent PartStaffs in group: set additional clefs on mxAttributes
            else:
                thisPartStaffRoot: Element = self.getRootForPartStaff(ps)
                oldClef: Optional[Element] = thisPartStaffRoot.find(
                    'measure/attributes/clef')
                if oldClef is not None and mxAttributes is not None:
                    clefsInMxAttributesAlready = mxAttributes.findall('clef')
                    if len(clefsInMxAttributesAlready) >= staffNumber:
                        raise MusicXMLExportException(
                            'Attempted to add more clefs than staffs'
                        )  # pragma: no cover

                    # Set initial clef for this staff
                    newClef = Element('clef')
                    newClef.set('number', str(staffNumber))
                    newSign = SubElement(newClef, 'sign')
                    newSign.text = oldClef.find('sign').text
                    newLine = SubElement(newClef, 'line')
                    newLine.text = oldClef.find('line').text
                    helpers.insertBeforeElements(mxAttributes,
                                                 newClef,
                                                 tagList=[
                                                     'staff-details',
                                                     'transpose', 'directive',
                                                     'measure-style'
                                                 ])
                                    else '')
                    helpers.insertBeforeElements(
                        mxAttributes,
                        newClef,
                        tagList=['staff-details', 'transpose', 'directive', 'measure-style']
                    )

                if multiMeter:
                    oldMeter: t.Optional[Element] = thisPartStaffRoot.find(
                        'measure/attributes/time'
                    )
                    if oldMeter:
                        oldMeter.set('number', str(staffNumber))
                        helpers.insertBeforeElements(
                            mxAttributes,
                            oldMeter,
                            tagList=['staves']
                        )
                if multiKey:
                    oldKey: t.Optional[Element] = thisPartStaffRoot.find('measure/attributes/key')
                    if oldKey:
                        oldKey.set('number', str(staffNumber))
                        helpers.insertBeforeElements(
                            mxAttributes,
                            oldKey,
                            tagList=['time', 'staves']
                        )

    def cleanUpSubsequentPartStaffs(self, group: StaffGroup):
        '''
        Now that the contents of all PartStaffs in `group` have been represented