Source code for music21.musicxml.xmlToM21

# ------------------------------------------------------------------------------
# Name:         musicxml/xmlToM21.py
# Purpose:      Conversion from MusicXML to Music21
#
# Authors:      Michael Scott Asato Cuthbert
#               Christopher Ariza
#               Jacob Tyler Walls
#
# Copyright:    Copyright © 2009-2024 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations

import copy
import fractions
import io
import weakref
from math import isclose
import re
import typing as t
import warnings
import xml.etree.ElementTree as ET

from music21 import articulations
from music21 import bar
from music21 import beam
from music21 import chord
from music21 import clef
from music21 import common
from music21 import defaults
from music21 import duration
from music21 import dynamics
from music21.common.enums import OffsetSpecial, OrnamentDelay
from music21.common.numberTools import opFrac, nearestMultiple
from music21 import editorial
from music21 import environment
from music21 import exceptions21
from music21 import expressions
from music21 import harmony  # for chord symbols
from music21 import instrument
from music21 import interval  # for transposing instruments
from music21 import key
from music21 import layout
from music21 import metadata
from music21 import meter
from music21.midi.percussion import MIDIPercussionException, PercussionMapper
from music21 import note
from music21 import percussion
from music21 import pitch
from music21 import repeat
from music21 import spanner
from music21 import stream
from music21 import style
from music21 import tablature
from music21 import tempo
from music21 import text  # for text boxes
from music21 import tie

from music21.musicxml import xmlObjects
from music21.musicxml import helpers
from music21.musicxml import xmlSoundParser
from music21.musicxml.xmlObjects import MusicXMLImportException, MusicXMLWarning

synchronizeIds = helpers.synchronizeIdsToM21
setAttributeFromAttribute = helpers.setM21AttributeFromAttribute

if t.TYPE_CHECKING:
    from music21 import base
    from music21.common.types import OffsetQL

    # what goes in a `.staffReference`
    StaffReferenceType = dict[int, list[base.Music21Object]]

environLocal = environment.Environment('musicxml.xmlToM21')

# const
NO_STAFF_ASSIGNED = 0

# see docstring for isRecognizableMetadataKey for information on
# this list.
_recognizableKeys: list[str] = list(
    metadata.properties.ALL_NAMESPACE_NAMES
    + metadata.properties.ALL_UNIQUE_NAMES
    + metadata.properties.ALL_MUSIC21_WORK_IDS
)


# ------------------------------------------------------------------------------
# Helpers
def _clean(badStr: str|None) -> str|None:
    # need to remove badly-formed strings
    if badStr is None:
        return None
    badStr = badStr.strip()
    goodStr = badStr.replace('\n', ' ')
    return goodStr


[docs] def strippedText(mxObj: ET.Element|None) -> str: ''' Returns the `mxObj.text.strip()` from an Element (or None) taking into account that `.text` might be None, or the Element might be undefined. Replacement for the older textStripValid() >>> from xml.etree.ElementTree import Element >>> e = Element('an-element') >>> musicxml.xmlToM21.strippedText(e) '' >>> e.text = ' ' >>> musicxml.xmlToM21.strippedText(e) '' >>> e.text = ' hello ' >>> musicxml.xmlToM21.strippedText(e) 'hello' >>> musicxml.xmlToM21.strippedText(None) '' >>> musicxml.xmlToM21.strippedText(440.0) '' New in v9. ''' if mxObj is None: return '' try: txt = mxObj.text if txt is None: return '' return txt.strip() except AttributeError: return ''
# Durations
[docs] def musicXMLTypeToType(value: str) -> str: ''' Utility function to convert a MusicXML duration type to a music21 duration type. Changes 'long' to 'longa' and deals with a Guitar Pro 5.2 bug in MusicXML export, that exports a 32nd note with the type '32th'. >>> musicxml.xmlToM21.musicXMLTypeToType('long') 'longa' >>> musicxml.xmlToM21.musicXMLTypeToType('32th') '32nd' >>> musicxml.xmlToM21.musicXMLTypeToType('quarter') 'quarter' >>> musicxml.xmlToM21.musicXMLTypeToType(None) Traceback (most recent call last): music21.musicxml.xmlObjects.MusicXMLImportException: found unknown MusicXML type: None ''' # MusicXML uses long instead of longa if value not in duration.typeToDuration: if value == 'long': return 'longa' elif value == '32th': return '32nd' else: raise MusicXMLImportException(f'found unknown MusicXML type: {value}') else: return value
def _floatOrIntStr(strObj): ''' Convert a string to float or int if possible. >>> _f = musicxml.xmlToM21._floatOrIntStr >>> _f('20.3') 20.3 >>> _f('20.0') 20 >>> _f(None) is None True >>> _f('hi') 'hi' ''' if strObj is None: return None try: val = float(strObj) if val == int(val): val = int(val) return val except ValueError: return strObj def _setAttributeFromTagText(m21El, xmlEl, tag, attributeName=None, *, transform=None): ''' If xmlEl has at least one element of tag==tag with some text. If it does, set the attribute either with the same name (with "foo-bar" changed to "fooBar") or with attributeName to the text contents. Pass a function or lambda function as `transform` to transform the value before setting it >>> from xml.etree.ElementTree import Element, SubElement This is essentially `<accidental><alter>-2</alter></accidental>`: >>> e = Element('accidental') >>> a = SubElement(e, 'alter') >>> a.text = '-2' >>> seta = musicxml.xmlToM21._setAttributeFromTagText >>> acc = pitch.Accidental() Transform the alter text to a float. >>> seta(acc, e, 'alter', transform=float) >>> acc.alter -2.0 >>> e2 = Element('score-partwise') >>> a2 = SubElement(e2, 'movement-title') >>> a2.text = 'Trout' >>> md = metadata.Metadata() >>> seta(md, e2, 'movement-title', 'movementName') >>> md.movementName 'Trout' set a different attribute >>> seta(md, e2, 'movement-title', 'composer') >>> md.composer 'Trout' ''' matchEl = xmlEl.find(tag) # find first if matchEl is None: return value = matchEl.text if value in (None, ''): return if transform is not None: value = transform(value) if attributeName is None: attributeName = common.hyphenToCamelCase(tag) setattr(m21El, attributeName, value) def _addMetadataItemFromTagText(m21md: metadata.Metadata, xmlEl, tag, mdUniqueName): matchEl = xmlEl.find(tag) # find first if matchEl is None: return value = matchEl.text if value in (None, ''): return m21md.add(mdUniqueName, value) # ------------------------------------------------------------------------------
[docs] class XMLParserBase: ''' contains functions that could be called at multiple levels of parsing (Score, Part, Measure). ''' mxAccidentalNameToM21 = {'quarter-sharp': 'half-sharp', 'three-quarters-sharp': 'one-and-a-half-sharp', 'quarter-flat': 'half-flat', 'three-quarters-flat': 'one-and-a-half-flat', 'flat-flat': 'double-flat', 'sharp-sharp': 'double-sharp', } # style attributes
[docs] def setStyleAttributes(self, mxObject, m21Object, musicXMLNames, m21Names=None): # noinspection PyShadowingNames ''' Takes an mxObject, a music21Object, and a list/tuple of musicXML names and a list/tuple of m21Names, and assigns each of the mxObject's attributes that fits this style name to the corresponding style object's m21Name attribute. >>> from xml.etree.ElementTree import fromstring as EL >>> XP = musicxml.xmlToM21.XMLParserBase() >>> mxObj = EL('<a x="20.1" y="10.0" z="yes" />') >>> m21Obj = base.Music21Object() >>> musicXMLNames = ('w', 'x', 'y', 'z') >>> m21Names = ('justify', 'absoluteX', 'absoluteY', 'hideObjectOnPrint') >>> XP.setStyleAttributes(mxObj, m21Obj, musicXMLNames, m21Names) `.justify` requires a TextStyle object. >>> m21Obj.style.justify Traceback (most recent call last): AttributeError: 'Style' object has no attribute 'justify' >>> m21Obj.style.absoluteX 20.1 >>> m21Obj.style.absoluteY 10 >>> m21Obj.style.hideObjectOnPrint True ''' if isinstance(m21Object, style.Style): stObj = m21Object else: stObj = None if not common.isIterable(musicXMLNames): musicXMLNames = (musicXMLNames,) if m21Names is None: m21Names = (common.hyphenToCamelCase(x) for x in musicXMLNames) elif not common.isIterable(m21Names): m21Names = (m21Names,) for xmlName, m21Name in zip(musicXMLNames, m21Names): mxValue = mxObject.get(xmlName) if mxValue is None: continue if m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL: mxValue = xmlObjects.yesNoToBoolean(mxValue) try: if mxValue is not True and mxValue is not False: mxValue = common.numToIntOrFloat(mxValue) except (ValueError, TypeError): pass # only create a style object if we get this far if stObj is None: stObj = m21Object.style setattr(stObj, m21Name, mxValue)
[docs] def setTextFormatting(self, mxObject, m21Object): ''' sets the justification, print-style-align group, and text-decoration, text-rotation, letter-spacing, line-height, lang, text-direction, and enclosure, on an m21Object, which must have style.TextStyle as its Style class, and then calls setPrintStyleAlign conforms to attr-group %text-formatting in the MusicXML DTD ''' musicXMLNames = ('justify', 'text-decoration', 'text-rotation', 'letter-spacing', 'line-height', 'lang', 'text-direction', 'enclosure') m21Names = ('justify', 'textDecoration', 'textRotation', 'letterSpacing', 'lineHeight', 'language', 'textDirection', 'enclosure') # TODO: enclosure should give the style.Enclosure StrEnum self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names) self.setPrintStyleAlign(mxObject, m21Object)
[docs] def setLineStyle(self, mxObject, m21Object): ''' Sets four additional elements for line elements, conforms to entity %line-shape, %line-type, %dashed-formatting (dash-length and space-length) ''' musicXMLNames = ('line-shape', 'line-type', 'dash-length', 'space-length') if hasattr(m21Object, 'lineType'): mxLineType = mxObject.get('line-type') if mxLineType is not None: m21Object.lineType = mxLineType self.setStyleAttributes(mxObject, m21Object, musicXMLNames)
[docs] def setPrintObject(self, mxObject, m21Object): ''' convert 'print-object="no"' to m21Object.style.hideObjectOnPrint = True ''' if mxObject.get('print-object') != 'no': return if hasattr(m21Object, 'style'): m21Object.style.hideObjectOnPrint = True else: try: m21Object.hideObjectOnPrint = True except AttributeError: # slotted object pass
[docs] def setPrintStyleAlign(self, mxObject, m21Object): ''' runs setPrintStyle and then sets horizontalAlign and verticalAlign, on an m21Object, which must have style.TextStyle as its Style class. conforms to attr-group %print-style-align in the MusicXML DTD ''' self.setPrintStyle(mxObject, m21Object) self.setStyleAttributes(mxObject, m21Object, ('valign', 'halign'), ('alignVertical', 'alignHorizontal'))
[docs] def setPrintStyle(self, mxObject, m21Object): ''' get position, font, and color information from the mxObject into the m21Object, which must have style.TextStyle as its Style class. conforms to attr-group %print-style in the MusicXML DTD ''' self.setPosition(mxObject, m21Object) self.setFont(mxObject, m21Object) self.setColor(mxObject, m21Object)
[docs] def setColor(self, mxObject, m21Object): ''' Sets m21Object.style.color to be the same as color. ''' # we repeat 'color' rather than just letting setStyleAttributes # handle it, because otherwise it will run the expensive # hyphenToCamelCase routine on something called on each note. self.setStyleAttributes(mxObject, m21Object, 'color', 'color')
[docs] def setFont(self, mxObject, m21Object): ''' sets font-family, font-style, font-size, and font-weight as fontFamily (list), fontStyle, fontSize and fontWeight from an object into a TextStyle object conforms to attr-group %font in the MusicXML DTD >>> from xml.etree.ElementTree import fromstring as EL >>> XP = musicxml.xmlToM21.XMLParserBase() >>> mxObj = EL('<text font-family="Courier,monospaced" font-style="italic" ' ... 'font-size="24" font-weight="bold" />') >>> te = expressions.TextExpression('hi!') >>> XP.setFont(mxObj, te) >>> te.style.fontFamily ['Courier', 'monospaced'] >>> te.style.fontStyle 'italic' >>> te.style.fontSize 24 >>> te.style.fontWeight 'bold' ''' musicXMLNames = ('font-family', 'font-style', 'font-size', 'font-weight') m21Names = ('fontFamily', 'fontStyle', 'fontSize', 'fontWeight') self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names)
[docs] def setPosition(self, mxObject, m21Object): ''' get positioning information for an object from default-x, default-y, relative-x, relative-y into the .style attribute's absoluteX, relativeX, etc. attributes' conforms to attr-group %position in the MusicXML DTD ''' musicXMLNames = ('default-x', 'default-y', 'relative-x', 'relative-y') m21Names = ('absoluteX', 'absoluteY', 'relativeX', 'relativeY') self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names)
[docs] def setPlacement(self, mxObject, m21Object): ''' Sets the placement for objects that have a .placement attribute (most but not all spanners) and sets the `style.placement` for those that don't. ''' placement = mxObject.get('placement') if placement is None: return if hasattr(m21Object, 'placement'): m21Object.placement = placement else: m21Object.style.placement = placement
[docs] def setEditorial(self, mxObj, m21Obj): # noinspection PyShadowingNames ''' Set editorial information from an mxObj >>> from xml.etree.ElementTree import fromstring as EL >>> XP = musicxml.xmlToM21.XMLParserBase() >>> mxObj = EL('<a/>') >>> n = note.Note('C#4') Most common case: >>> XP.setEditorial(mxObj, n) >>> n.hasEditorialInformation False >>> mxObj = EL('<note><footnote>Sharp is conjectural</footnote>' ... '<level reference="yes">2</level></note>') >>> XP.setEditorial(mxObj, n) >>> n.hasEditorialInformation True >>> len(n.editorial.footnotes) 1 >>> fn = n.editorial.footnotes[0] >>> fn <music21.editorial.Comment 'Sharp is conjectu...'> >>> fn.isFootnote True >>> fn.levelInformation '2' >>> fn.isReference True If no <footnote> tag exists, the editorial information will be found in comments: >>> mxObj = EL('<note><level reference="no">ed</level></note>') >>> n = note.Note('C#4') >>> XP.setEditorial(mxObj, n) >>> len(n.editorial.footnotes) 0 >>> len(n.editorial.comments) 1 >>> com = n.editorial.comments[0] >>> com.isReference False >>> com.text is None True >>> com.levelInformation 'ed' ''' mxFootnote = mxObj.find('footnote') mxLevel = mxObj.find('level') if mxFootnote is None and mxLevel is None: # most common case return c = editorial.Comment() if mxFootnote is not None: c.text = mxFootnote.text c.isFootnote = True self.setTextFormatting(mxFootnote, c) if mxLevel is not None: c.levelInformation = mxLevel.text referenceAttribute = mxLevel.get('reference') if referenceAttribute == 'yes': c.isReference = True # TODO: attr: level-display: bracket, parentheses # TODO: musicxml 4: type=start/stop/single -- does this apply to one note or # start applying from here on until stop is encountered. default: single if c.isFootnote: m21Obj.editorial.footnotes.append(c) else: m21Obj.editorial.comments.append(c)
[docs] def xmlPrintToPageLayout(self, mxPrint, inputM21=None): # noinspection PyShadowingNames ''' Given an mxPrint object, set object data for the print section of a layout.PageLayout object >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxPrint = EL( ... '<print new-page="yes" page-number="5">' ... ' <page-layout><page-height>4000</page-height>' ... ' <page-margins><left-margin>20</left-margin>' ... ' <right-margin>30.25</right-margin></page-margins>' ... '</page-layout></print>' ... ) >>> pl = MP.xmlPrintToPageLayout(mxPrint) >>> pl.isNew True >>> pl.rightMargin 30.25 >>> pl.leftMargin 20 >>> pl.pageNumber 5 >>> pl.pageHeight 4000 ''' if inputM21 is None: pageLayout = layout.PageLayout() else: pageLayout = inputM21 setb = setAttributeFromAttribute setb(pageLayout, mxPrint, 'new-page', 'isNew', transform=xmlObjects.yesNoToBoolean) setb(pageLayout, mxPrint, 'page-number', transform=int) for x in mxPrint: if x.tag == 'page-layout': self.xmlPageLayoutToPageLayout(x, inputM21=pageLayout) break if inputM21 is None: return pageLayout
[docs] def xmlPageLayoutToPageLayout(self, mxPageLayout, inputM21=None): ''' get a PageLayout object from an mxPageLayout Called out from mxPrintToPageLayout because it is also used in the <defaults> tag ''' if inputM21 is None: pageLayout = layout.PageLayout() else: pageLayout = inputM21 seta = _setAttributeFromTagText seta(pageLayout, mxPageLayout, 'page-height', transform=_floatOrIntStr) seta(pageLayout, mxPageLayout, 'page-width', transform=_floatOrIntStr) # TODO -- record even, odd, both margins mxPageMargins = mxPageLayout.find('page-margins') if mxPageMargins is not None: for direction in ('top', 'bottom', 'left', 'right'): seta(pageLayout, mxPageMargins, direction + '-margin', transform=_floatOrIntStr) if inputM21 is None: return pageLayout
[docs] def xmlPrintToSystemLayout(self, mxPrint, inputM21=None): # noinspection PyShadowingNames ''' Given an mxPrint object, set object data >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxPrint = EL( ... '<print new-system="yes">' ... ' <system-layout><system-distance>55</system-distance>' ... ' <system-margins><left-margin>20</left-margin>' ... ' <right-margin>30.25</right-margin></system-margins>' ... '</system-layout></print>' ... ) >>> sl = MP.xmlPrintToSystemLayout(mxPrint) >>> sl.isNew True >>> sl.rightMargin 30.25 >>> sl.leftMargin 20 >>> sl.distance 55 ''' if inputM21 is None: systemLayout = layout.SystemLayout() else: systemLayout = inputM21 setb = setAttributeFromAttribute setb(systemLayout, mxPrint, 'new-system', 'isNew', xmlObjects.yesNoToBoolean) # mxSystemLayout = mxPrint.get('systemLayout') mxSystemLayout = mxPrint.find('system-layout') # blank if mxSystemLayout is not None: self.xmlSystemLayoutToSystemLayout(mxSystemLayout, inputM21=systemLayout) if inputM21 is None: return systemLayout
[docs] def xmlSystemLayoutToSystemLayout(self, mxSystemLayout, inputM21=None): ''' get a SystemLayout object from an <system-layout> element Called out from xmlPrintToSystemLayout because it is also used in the <defaults> tag ''' if inputM21 is None: systemLayout = layout.SystemLayout() else: systemLayout = inputM21 seta = _setAttributeFromTagText # TODO -- record even, odd, both margins mxSystemMargins = mxSystemLayout.find('system-margins') if mxSystemMargins is not None: for direction in ('top', 'bottom', 'left', 'right'): seta(systemLayout, mxSystemMargins, direction + '-margin', transform=_floatOrIntStr) seta(systemLayout, mxSystemLayout, 'system-distance', 'distance', transform=_floatOrIntStr) seta(systemLayout, mxSystemLayout, 'top-system-distance', 'topDistance', transform=_floatOrIntStr) # TODO: system-dividers if inputM21 is None: return systemLayout
[docs] def xmlStaffLayoutToStaffLayout(self, mxStaffLayout, inputM21=None): ''' get a StaffLayout object from an <staff-layout> tag In music21, the <staff-layout> and <staff-details> are intertwined in a StaffLayout object. ''' if inputM21 is None: staffLayout = layout.StaffLayout() else: staffLayout = inputM21 seta = _setAttributeFromTagText seta(staffLayout, mxStaffLayout, 'staff-distance', 'distance', transform=_floatOrIntStr) # ET.dump(mxStaffLayout) staffNumber = mxStaffLayout.get('number') if staffNumber is not None: staffNumber = int(staffNumber) staffLayout.staffNumber = staffNumber if hasattr(self, 'staffLayoutObjects') and hasattr(self, 'offsetMeasureNote'): staffLayoutKey = ((staffNumber or 1), self.offsetMeasureNote) self.staffLayoutObjects[staffLayoutKey] = staffLayout if inputM21 is None: return staffLayout
[docs] class PartGroup: ''' Small helper class for keeping track of part-groups from XML since they are converted to StaffGroup spanners much later. ''' def __init__(self, mxPartGroup): self.mxPartGroup = mxPartGroup self.partGroupIds = [] number = mxPartGroup.get('number') if number is not None: number = int(number) else: number = 1 self.number = number
[docs] def add(self, partGroupId): ''' Add a partGroupId to self.partGroupIds ''' self.partGroupIds.append(partGroupId)
# ------------------------------------------------------------------------------
[docs] class MusicXMLImporter(XMLParserBase): ''' Object for importing .xml, .mxl, .musicxml, MusicXML files into music21. ''' def __init__(self): super().__init__() self.xmlText = None self.xmlFilename = None self.xmlRoot = None self.stream = stream.Score() self.definesExplicitSystemBreaks = False self.definesExplicitPageBreaks = False self.spannerBundle = self.stream.spannerBundle self.mxScorePartDict = {} self.m21PartObjectsById = {} self.partGroupList = [] self.parts = [] self.musicXmlVersion = defaults.musicxmlVersion # Finale (RIP 2025) had a problem with writing extraneous <forward> tags. # if this is True then we will be cautious before interpreting them as # hidden rests. self.applyFinaleWorkarounds = False
[docs] def scoreFromFile(self, filename): ''' main program: opens a file given by filename and returns a complete music21 Score from it. ''' # load filename into text self.readFile(filename) # self.parseXMLText() return self.stream
[docs] def readFile(self, filename): etree = ET.parse(filename) self.xmlRoot = etree.getroot() if self.xmlRoot.tag != 'score-partwise': raise MusicXMLImportException('Cannot parse MusicXML files not in score-partwise. ' + f"Root tag was '{self.xmlRoot.tag}'") self.xmlRootToScore(self.xmlRoot, self.stream)
[docs] def parseXMLText(self): if isinstance(self.xmlText, bytes): self.xmlText = self.xmlText.decode('utf-8') sio = io.StringIO(self.xmlText) try: # StringIO is a SupportsRead[str] type. # noinspection PyTypeChecker etree = ET.parse(sio) self.xmlRoot = etree.getroot() except ET.ParseError: self.xmlRoot = ET.XML(self.xmlText) # might still raise an ET.ParseError if self.xmlRoot.tag != 'score-partwise': raise MusicXMLImportException('Cannot parse MusicXML files not in score-partwise. ' + f"Root tag was '{self.xmlRoot.tag}'") self.xmlRootToScore(self.xmlRoot, self.stream)
[docs] def xmlRootToScore(self, mxScore, inputM21=None): ''' parse an xml file into a Score() object. ''' if inputM21 is None: s = stream.Score() else: s = inputM21 mxVersion = mxScore.get('version') if mxVersion is not None: self.musicXmlVersion = mxVersion md = self.xmlMetadata(mxScore) s.coreInsert(0, md) mxDefaults = mxScore.find('defaults') if mxDefaults is not None: scoreLayout = self.xmlDefaultsToScoreLayout(mxDefaults) s.coreInsert(0, scoreLayout) for mxCredit in mxScore.findall('credit'): credit = self.xmlCreditToTextBox(mxCredit) s.coreInsert(0, credit) self.parsePartList(mxScore) for p in mxScore.findall('part'): partId = p.get('id') if partId is None: # pragma: no cover partId = list(self.mxScorePartDict.keys())[0] # Lilypond Test Suite allows for parsing w/o a part ID for one part try: mxScorePart = self.mxScorePartDict[partId] except KeyError: # pragma: no cover environLocal.printDebug(f'Cannot find info for part with name {partId}' + ', skipping the part') continue part = self.xmlPartToPart(p, mxScorePart) if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part self.partGroups() # Mark all ArpeggioMarkSpanners as complete (now that we've parsed all the Parts) for sp in self.spannerBundle.getByClass(expressions.ArpeggioMarkSpanner): sp.completeStatus = True # copy spanners that are complete into the Score. rm = [] for sp in self.spannerBundle.getByCompleteStatus(True): self.stream.coreInsert(0, sp) rm.append(sp) # remove from original spanner bundle for sp in rm: self.spannerBundle.remove(sp) s.coreElementsChanged() s.definesExplicitSystemBreaks = self.definesExplicitSystemBreaks s.definesExplicitPageBreaks = self.definesExplicitPageBreaks for p in s.parts: p.definesExplicitSystemBreaks = self.definesExplicitSystemBreaks p.definesExplicitPageBreaks = self.definesExplicitPageBreaks s.sort() # do this now so that if the file is cached, we can cache that it's sorted. if inputM21 is None: return s
[docs] def xmlPartToPart(self, mxPart, mxScorePart): ''' Given a <part> object and the <score-part> object, parse a complete part. ''' parser = PartParser(mxPart, mxScorePart=mxScorePart, parent=self) parser.parse() if parser.appendToScoreAfterParse is True: return parser.stream else: return None
[docs] def parsePartList(self, mxScore): ''' Parses the <part-list> tag and adds <score-part> entries into self.mxScorePartDict[partId] and adds them to any open <part-group> entries, stored as PartGroup objects in self.partGroupList ''' mxPartList = mxScore.find('part-list') if mxPartList is None: return openPartGroups = [] for partListElement in mxPartList: if partListElement.tag == 'score-part': partId = partListElement.get('id') self.mxScorePartDict[partId] = partListElement for pg in openPartGroups: pg.add(partId) elif partListElement.tag == 'part-group': if partListElement.get('type') == 'start': pg = PartGroup(partListElement) self.partGroupList.append(pg) openPartGroups.append(pg) elif partListElement.get('type') == 'stop': number = partListElement.get('number') if number is not None: number = int(number) else: number = 1 opgTemp = [] for pg in openPartGroups: if pg.number != number: opgTemp.append(pg) openPartGroups = opgTemp
[docs] def xmlCreditToTextBox(self, mxCredit): # noinspection PyShadowingNames ''' Convert a MusicXML credit to a music21 TextBox >>> from xml.etree.ElementTree import fromstring as EL >>> credit = EL('<credit page="2"><credit-words>Testing</credit-words></credit>') >>> MI = musicxml.xmlToM21.MusicXMLImporter() >>> tb = MI.xmlCreditToTextBox(credit) >>> tb.page 2 >>> tb.content 'Testing' OMIT_FROM_DOCS Capella generates empty credit-words. Make sure they do not crash. >>> credit = EL('<credit><credit-words/></credit>') >>> tb = MI.xmlCreditToTextBox(credit) >>> tb <music21.text.TextBox ''> ''' tb = text.TextBox() # center and middle these are good defaults for new textboxes # but not for musicxml import tb.style.alignHorizontal = None tb.style.alignVertical = None pageNum = mxCredit.get('page') if pageNum is None: pageNum = 1 else: pageNum = int(pageNum) tb.page = pageNum content = [] for cw in mxCredit.findall('credit-words'): if cw.text not in (None, ''): content.append(cw.text) if not content: # no text defined tb.content = '' return tb # capella generates empty credit-words # raise MusicXMLImportException('no credit words defined for a credit tag') tb.content = '\n'.join(content) # join with \n cw1 = mxCredit.find('credit-words') # take formatting from the first, no matter if multiple are defined self.setPrintStyleAlign(cw1, tb) tb.style.justify = cw1.get('justify') # TODO: credit type # TODO: link # TODO: bookmark # TODO: credit-image return tb
[docs] def xmlDefaultsToScoreLayout(self, mxDefaults, inputM21=None): ''' Convert a <defaults> tag to a :class:`~music21.layout.ScoreLayout` object ''' if inputM21 is None: scoreLayout = layout.ScoreLayout() else: scoreLayout = inputM21 seta = _setAttributeFromTagText mxScaling = mxDefaults.find('scaling') if mxScaling is not None: seta(scoreLayout, mxScaling, 'millimeters', 'scalingMillimeters', transform=_floatOrIntStr) seta(scoreLayout, mxScaling, 'tenths', 'scalingTenths', transform=_floatOrIntStr) # TODO: musicxml4: concert-score mxPageLayout = mxDefaults.find('page-layout') if mxPageLayout is not None: scoreLayout.pageLayout = self.xmlPageLayoutToPageLayout(mxPageLayout) mxSystemLayout = mxDefaults.find('system-layout') if mxSystemLayout is not None: scoreLayout.systemLayout = self.xmlSystemLayoutToSystemLayout(mxSystemLayout) for mxStaffLayout in mxDefaults.findall('staff-layout'): staffLayout = self.xmlStaffLayoutToStaffLayout(mxStaffLayout) scoreLayout.staffLayoutList.append(staffLayout) self.styleFromXmlDefaults(mxDefaults) return scoreLayout
[docs] def styleFromXmlDefaults(self, mxDefaults): # noinspection PyShadowingNames ''' Set the appearance and font information from mxDefault <appearance>, <music-font>, <word-font>, <lyric-font> (multiple), and <lyric-language> tags. Here the demo does not include the <appearance> tag since that is documented in `xmlAppearanceToStyle` >>> from xml.etree.ElementTree import fromstring as EL >>> defaults = EL( ... '<defaults>' ... '<music-font font-family="Maestro, Opus" font-weight="bold" />' ... '<word-font font-family="Garamond" font-style="italic" />' ... '<lyric-font name="verse" font-size="12" />' ... '<lyric-font name="chorus" font-size="14" />' ... '<lyric-language name="verse" xml:lang="fr" />' ... '<lyric-language name="chorus" xml:lang="en" />' ... '</defaults>' ... ) >>> MI = musicxml.xmlToM21.MusicXMLImporter() >>> MI.styleFromXmlDefaults(defaults) >>> st = MI.stream.style >>> st.musicFont <music21.style.TextStyle object at 0x10535c0f0> >>> st.musicFont.fontFamily ['Maestro', 'Opus'] >>> st.musicFont.fontWeight 'bold' >>> st.wordFont.fontFamily ['Garamond'] >>> st.wordFont.fontStyle 'italic' >>> len(st.lyricFonts) 2 >>> st.lyricFonts[0] ('verse', <music21.style.TextStyle object at 0x10535d438>) >>> st.lyricFonts[0][1].fontSize 12 >>> st.lyricLanguages [('verse', 'fr'), ('chorus', 'en')] ''' mxAppearance = mxDefaults.find('appearance') if mxAppearance is not None: self.xmlAppearanceToStyle(mxAppearance) mxMusicFont = mxDefaults.find('music-font') if mxMusicFont is not None: st = style.TextStyle() self.setFont(mxMusicFont, st) self.stream.style.musicFont = st mxWordFont = mxDefaults.find('word-font') if mxWordFont is not None: st = style.TextStyle() self.setFont(mxWordFont, st) self.stream.style.wordFont = st for mxLyricFont in mxDefaults.findall('lyric-font'): st = style.TextStyle() self.setFont(mxLyricFont, st) lyricName = mxLyricFont.get('name') styleTuple = (lyricName, st) self.stream.style.lyricFonts.append(styleTuple) for mxLyricLanguage in mxDefaults.findall('lyric-language'): lyricLanguage = 'en' lyricName = mxLyricLanguage.get('name') for aKey, value in mxLyricLanguage.attrib.items(): # {http://www.w3.org/XML/1998/namespace}lang if aKey.endswith('}lang'): lyricLanguage = value break lyricTuple = lyricName, lyricLanguage self.stream.style.lyricLanguages.append(lyricTuple)
[docs] def xmlAppearanceToStyle(self, mxAppearance): # noinspection PyShadowingNames ''' Parse the appearance tag for information about line widths and note sizes >>> from xml.etree.ElementTree import fromstring as EL >>> appear = EL( ... '<appearance>' ... '<line-width type="beam">5</line-width>' ... '<line-width type="ledger">1.5625</line-width>' ... '<note-size type="grace">60</note-size>' ... '<distance type="hyphen">0.5</distance>' ... '<other-appearance type="sharps">dotted</other-appearance>' ... '</appearance>' ... ) >>> MI = musicxml.xmlToM21.MusicXMLImporter() >>> MI.xmlAppearanceToStyle(appear) >>> st = MI.stream.style >>> st.lineWidths [('beam', 5), ('ledger', 1.5625)] >>> st.noteSizes [('grace', 60)] >>> st.distances [('hyphen', 0.5)] >>> st.otherAppearances [('sharps', 'dotted')] ''' for mxLineWidth in mxAppearance.findall('line-width'): lineWidthType = mxLineWidth.get('type') # required lineWidthValue = common.numToIntOrFloat(mxLineWidth.text) lineWidthInfo = (lineWidthType, lineWidthValue) self.stream.style.lineWidths.append(lineWidthInfo) for mxNoteSize in mxAppearance.findall('note-size'): noteSizeType = mxNoteSize.get('type') # required noteSizeValue = common.numToIntOrFloat(mxNoteSize.text) noteSizeInfo = (noteSizeType, noteSizeValue) self.stream.style.noteSizes.append(noteSizeInfo) for mxDistance in mxAppearance.findall('distance'): distanceType = mxDistance.get('type') # required distanceValue = common.numToIntOrFloat(mxDistance.text) distanceInfo = (distanceType, distanceValue) self.stream.style.distances.append(distanceInfo) for mxOther in mxAppearance.findall('other-appearance'): otherType = mxOther.get('type') # required otherValue = mxOther.text # value can be anything otherInfo = (otherType, otherValue) self.stream.style.otherAppearances.append(otherInfo)
[docs] def partGroups(self): ''' set StaffGroup objects from the <part-group> tags. ''' seta = _setAttributeFromTagText for pgObj in self.partGroupList: staffGroup = layout.StaffGroup() for partId in pgObj.partGroupIds: # get music21 part from mxScorePartDictionary try: staffGroup.addSpannedElements(self.m21PartObjectsById[partId]) except KeyError as ke: foundOne = False for partIdTest in sorted(self.m21PartObjectsById): if partIdTest.startswith(partId + '-Staff'): staffGroup.addSpannedElements(self.m21PartObjectsById[partIdTest]) foundOne = True if not foundOne: raise MusicXMLImportException( 'Cannot find part in m21PartObjectsById dictionary by Id:' f' {ke} \n Full Dict:\n {self.m21PartObjectsById!r} ') mxPartGroup = pgObj.mxPartGroup seta(staffGroup, mxPartGroup, 'group-name', 'name') # TODO: group-name-display seta(staffGroup, mxPartGroup, 'group-abbreviation', 'abbreviation') # TODO: group-abbreviation-display mxGroupSymbol = mxPartGroup.find('group-symbol') if mxGroupSymbol is not None: seta(staffGroup, mxPartGroup, 'group-symbol', 'symbol') self.setPosition(mxGroupSymbol, staffGroup) self.setColor(mxGroupSymbol, staffGroup) else: staffGroup.symbol = 'brace' # MusicXML default seta(staffGroup, mxPartGroup, 'group-barline', 'barTogether') # TODO: group-time self.setEditorial(mxPartGroup, staffGroup) staffGroup.completeStatus = True self.spannerBundle.append(staffGroup)
# self.stream.coreInsert(0, staffGroup)
[docs] def xmlMetadata( self, el: ET.Element|None = None, inputM21: metadata.Metadata|None = None ) -> metadata.Metadata|None: ''' Converts part of the root element into a metadata object Supported: work-title, work-number, opus, movement-number, movement-title, identification ''' if el is None: el = self.xmlRoot if inputM21 is None: md = metadata.Metadata() else: md = inputM21 add_m = _addMetadataItemFromTagText # work work = el.find('work') if work is not None: add_m(md, work, 'work-title', 'title') add_m(md, work, 'work-number', 'number') add_m(md, work, 'opus', 'opusNumber') add_m(md, el, 'movement-number', 'movementNumber') add_m(md, el, 'movement-title', 'movementName') # If there is no movementName in the metadata, music21's MusicXML writer will # duplicate the title into the movementName in the written file. Apparently this # is because MusicXML renderers have historically rendered 'movement-title' as # the title at the top of the page, and not the actual work-title. The code # below (which used to live in Metadata.all) notices that md['title'] and # md['movementName'] are the same, and deletes md['title'], undoing that # MusicXML weirdness music21's writer caused. I have moved this code from # Metadata.all to here, since it is clearly MusicXML-specific, and I don't # want to corrupt the actual metadata in other code paths/converters. Perhaps # the world is populated entirely by better MusicXML renderers now, so we can # remove both bits of code from the MusicXML converter?... if md['title'] == md['movementName']: md['title'] = None identification = el.find('identification') if identification is not None: self.addIdentificationToMetadata(identification, md) if inputM21 is None: return md return None
[docs] def addIdentificationToMetadata( self, identification: ET.Element, md: metadata.Metadata, ) -> None: ''' Given an <identification> tag, , containing <creator> tags, <rights> tags, and <miscellaneous> tags etc., add this information to the :class:`~music21.metadata.Metadata` object passed in. Not supported: source, relation Only the first <rights> tag is supported Encoding only parses "supports" and that only has new-system (definesExplicitSystemBreaks) and new-page (definesExplicitPageBreaks) ''' for creator in identification.findall('creator'): c = self.creatorToContributor(creator) if md.isContributorUniqueName(c.role): md.add(c.role, c) else: # custom c.role, store under 'otherContributor' md.add('otherContributor', c) for rights in identification.findall('rights'): c = self.rightsToCopyright(rights) md.add('copyright', c) break encoding = identification.find('encoding') if encoding is not None: self.processEncoding(encoding, md) # TODO: source # TODO: relation miscellaneous = identification.find('miscellaneous') if miscellaneous is not None: for mxMiscField in miscellaneous.findall('miscellaneous-field'): miscFieldName = mxMiscField.get('name') if miscFieldName is None: continue # it is required, so technically can raise an exception miscFieldValue = mxMiscField.text if miscFieldValue is None: miscFieldValue = '' if self.isRecognizableMetadataKey(miscFieldName): md.add(miscFieldName, miscFieldValue) else: # We didn't recognize miscFieldName? Add as custom metadata, # so nothing is lost. md.addCustom(miscFieldName, miscFieldValue)
[docs] @staticmethod def isRecognizableMetadataKey(miscFieldName: str) -> bool: ''' Returns bool on whether `miscFieldName` is a one of the names that is among the list of names we might see in <miscellaneous>, that this parser will interpret as supported metadata keys. Currently, this is all the uniqueName keys (e.g. 'dateCreated'), the 'namespace:name' keys (e.g. 'dcterms:created'), and the pre-v8 music21 workIds (e.g. 'date'). >>> MI = musicxml.xmlToM21.MusicXMLImporter() >>> MI.isRecognizableMetadataKey('dateCreated') True >>> MI.isRecognizableMetadataKey('dcterms:created') True >>> MI.isRecognizableMetadataKey('dateDestroyed') False ''' return miscFieldName in _recognizableKeys
[docs] def processEncoding(self, encoding: ET.Element, md: metadata.Metadata) -> None: ''' Process all information in the <encoding> element and put it into the Metadata object passed in as `md`. Currently only processes 'software' and these `supports` attributes: * new-system = Metadata.definesExplicitSystemBreaks * new-page = Metadata.definesExplicitPageBreaks ''' # TODO: encoder (text + type = role) multiple # TODO: encoding-date either singular or multiple # TODO: encoding-description (string) multiple # If the first software tag contains Finale, then it # is by finale. Otherwise, it is not foundOneSoftwareTag: bool = False finaleIsFirst: bool = False for software in encoding.findall('software'): if softwareText := strippedText(software): if not foundOneSoftwareTag: if 'Finale' in softwareText: finaleIsFirst = True foundOneSoftwareTag = True md.add('software', softwareText) if finaleIsFirst: self.applyFinaleWorkarounds = True for supports in encoding.findall('supports'): # todo: element: required # todo: type: required -- not sure of the difference between this and value # though type is yes-no while value is string attr = supports.get('attribute') value = supports.get('value') if value is None: value = supports.get('type') # found in wild: element=accidental type="no" -- No accidentals are indicated # found in wild: transpose # found in wild: beam # found in wild: stem if (attr, value) == ('new-system', 'yes'): self.definesExplicitSystemBreaks = True elif (attr, value) == ('new-page', 'yes'): self.definesExplicitPageBreaks = True
[docs] def creatorToContributor(self, creator: ET.Element, inputM21: metadata.primitives.Contributor|None = None): # noinspection PyShadowingNames ''' Given a <creator> tag, fill the necessary parameters of a Contributor. >>> from xml.etree.ElementTree import fromstring as EL >>> creator = EL('<creator type="composer">Beethoven, Ludwig van</creator>') >>> MI = musicxml.xmlToM21.MusicXMLImporter() >>> c = MI.creatorToContributor(creator) >>> c <music21.metadata.primitives.Contributor composer:Beethoven, Ludwig van> >>> c.role 'composer' >>> c.name 'Beethoven, Ludwig van' Pass in a Contributor object and set it: >>> c2 = metadata.Contributor() >>> MI.creatorToContributor(creator, c2) >>> c2.role 'composer' ''' if inputM21 is None: c = metadata.Contributor() else: c = inputM21 creatorType = creator.get('type') if creatorType is not None: # We don't check against metadata.Contributor.roleNames here. # Custom roles/creatorTypes are allowed, and will be stored in # the metadata with uniqueName 'otherContributor' (see code in # addIdentificationToMetadata that does this). c.role = creatorType creatorText = creator.text if creatorText is not None: c.name = creatorText.strip() if inputM21 is None: return c
[docs] def rightsToCopyright(self, rights): # noinspection PyShadowingNames ''' Given a <rights> tag, fill the necessary parameters of a :class:`~music21.metadata.primitives.Copyright` object. >>> from xml.etree.ElementTree import fromstring as EL >>> rights = EL('<rights type="owner">CC-SA-BY</rights>') >>> MI = musicxml.xmlToM21.MusicXMLImporter() >>> c = MI.rightsToCopyright(rights) >>> c <music21.metadata.primitives.Copyright CC-SA-BY> >>> c.role 'owner' >>> str(c) 'CC-SA-BY' ''' rt = rights.text if rt is not None: rt = rt.strip() c = metadata.Copyright(rt) copyrightType = rights.get('type') if copyrightType is not None: c.role = copyrightType return c
# -----------------------------------------------------------------------------
[docs] class PartParser(XMLParserBase): ''' parser to work with a single <part> tag. called out for multiprocessing potential in future ''' def __init__(self, mxPart: ET.Element|None = None, mxScorePart: ET.Element|None = None, parent: MusicXMLImporter|None = None): super().__init__() self.mxPart = mxPart self.mxScorePart = mxScorePart if mxPart is not None: self.partId = mxPart.get('id') if self.partId is None and parent is not None: self.partId = list(parent.mxScorePartDict.keys())[0] else: self.partId = '' self.parent = parent if parent is not None else MusicXMLImporter() self.spannerBundle = self.parent.spannerBundle self.stream: stream.Part = stream.Part() if self.mxPart is not None: for mxStaves in self.mxPart.findall('measure/attributes/staves'): stavesText = strippedText(mxStaves) if stavesText and int(stavesText) > 1: self.stream = stream.PartStaff() # PartStaff inherits from Part, so okay. break self.atSoundingPitch = True # a list of StaffReferenceType dicts -- one for each Measure parsed by MeasureParser self.staffReferenceList: list[StaffReferenceType] = [] self.lastTimeSignature: meter.TimeSignature|None = None self.lastMeasureWasShort = False self.lastMeasureOffset = 0.0 # a dict of clefs per staff number -- needed for converting rests w/ steps self.lastClefs: dict[int, clef.Clef|None] = {} self.activeTuplets: list[duration.Tuplet|None] = [None] * 7 self.maxStaves = 1 # will be changed in measure parsing self.lastMeasureNumber = 0 self.lastNumberSuffix: str|None = None self.multiMeasureRestsToCapture = 0 self.activeMultiMeasureRestSpanner: spanner.MultiMeasureRest|None = None self.activeInstrument: instrument.Instrument|None = None self.firstMeasureParsed = False # has the first measure been parsed yet? self.activeAttributes = None # divisions, clef, etc. self.lastDivisions: int = defaults.divisionsPerQuarter # give a default value for testing self.appendToScoreAfterParse = True self.lastMeasureParser: MeasureParser|None = None
[docs] def parse(self) -> None: ''' Run the parser on a single part ''' self.parseXmlScorePart() self.parseMeasures() self.stream.atSoundingPitch = self.atSoundingPitch # TODO: this does not work with voices; there, Spanners # will be copied into the Score # copy spanners that are complete into the part, as this is the # highest level container that needs them. Ottavas are the exception, # they should be put in the PartStaff that contains the first note # in the Ottava. completedSpanners: list[spanner.Spanner] = [] for sp in self.spannerBundle.getByCompleteStatus(True): if not isinstance(sp, spanner.Ottava): # don't insert Ottavas, we'll do that after separateOutPartStaves(). self.stream.coreInsert(0, sp) completedSpanners.append(sp) # remove from original spanner bundle for sp in completedSpanners: self.spannerBundle.remove(sp) # s is the score; adding the part to the score self.stream.coreElementsChanged() partStaves: list[stream.PartStaff] = [] if self.maxStaves > 1: partStaves = self.separateOutPartStaves() elif self.partId is not None: self.stream.addGroupForElements(self.partId) # set group for components (recurse?) self.stream.groups.append(self.partId) # set group for stream itself self._fillAndInsertOttavasInPartStaff(completedSpanners, partStaves)
def _fillAndInsertOttavasInPartStaff( self, spanners: list[spanner.Spanner], partStaves: list[stream.PartStaff] ): # Ottavas should be filled, so that later transpositions can find all the notes that # should be octave-shifted. Ottavas should also be inserted into the partStaff that # contains the Ottava's first note. for sp in spanners: if not isinstance(sp, spanner.Ottava): continue spannerPart: stream.Part|None = None if partStaves: spannerPart = self._findFirstPartStaffContaining(sp.getFirst(), partStaves) else: spannerPart = self.stream if spannerPart is not None: spannerPart.coreInsert(0, sp) spannerPart.coreElementsChanged() sp.fill(spannerPart) def _findFirstPartStaffContaining( self, obj: base.Music21Object|None, partStaves: list[stream.PartStaff] ) -> stream.PartStaff|None: if obj is None: return None for partStaff in partStaves: if partStaff.containerInHierarchy(obj, setActiveSite=False) is not None: # obj is somewhere in the hierarchy of this partStaff return partStaff return None
[docs] def parseXmlScorePart(self): ''' The <score-part> tag contains a lot of information about the Part itself. It was found in the <part-list> in the ScoreParser but was not parsed and instead passed into the PartParser as .mxScorePart. Sets the stream.partName, stream.partAbbreviation, self.activeInstrument, and inserts an instrument at the beginning of the stream. The instrumentObj being configured comes from self.getDefaultInstrument. ''' part = self.stream mxScorePart = self.mxScorePart seta = _setAttributeFromTagText # TODO: musicxml 4: part-link: instrument-link, group-link # put part info into the Part object and retrieve it later seta(part, mxScorePart, 'part-name', transform=_clean) mxPartName = mxScorePart.find('part-name') if mxPartName is not None: printObject = mxPartName.get('print-object') if printObject == 'no': part.style.printPartName = False # This will later be put in the default instrument object also. # TODO: part-name-display seta(part, mxScorePart, 'part-abbreviation', transform=_clean) mxPartAbbreviation = mxScorePart.find('part-abbreviation') if mxPartAbbreviation is not None: printObject = mxPartAbbreviation.get('print-object') if printObject == 'no': part.style.printPartAbbreviation = False # This will later be put in instrument.partAbbreviation also # TODO: part-abbreviation-display # Q: is group covered elsewhere? instrumentObj = self.getDefaultInstrument() # self.firstInstrumentObject = instrumentObj # not used. if instrumentObj.bestName() is not None: part.id = instrumentObj.bestName() self.activeInstrument = instrumentObj part.partName = instrumentObj.partName part.partAbbreviation = instrumentObj.partAbbreviation part.coreInsert(0.0, instrumentObj) # add instrument at zero offset
# TODO: MusicXML 4.0: player tags
[docs] def getDefaultInstrument(self, mxScorePart: ET.Element|None = None) -> instrument.Instrument: # noinspection PyShadowingNames r''' Get a default instrument from the mxScorePart tag. >>> scorePart = ( ... '<score-part id="P4"><part-name>Bass</part-name>' ... '<part-abbreviation>B.</part-abbreviation>' ... '<score-instrument id="P4-I4">' ... ' <instrument-name>Instrument 4</instrument-name>' ... '</score-instrument>' ... '<midi-instrument id="P4-I4">' ... ' <midi-channel>4</midi-channel>' ... '<midi-program>1</midi-program>' ... '</midi-instrument>' ... '</score-part>') >>> from xml.etree.ElementTree import fromstring as EL >>> pp = musicxml.xmlToM21.PartParser() >>> mxScorePart = EL(scorePart) >>> i = pp.getDefaultInstrument(mxScorePart) >>> i <music21.instrument.Instrument ': Instrument 4'> >>> i.instrumentName 'Instrument 4' Non-default transpositions captured as of v7.3: >>> scorePart = ( ... '<score-part id="P5"><part-name>C Trumpet</part-name>' ... '<part-abbreviation>C Tpt.</part-abbreviation>' ... '<score-instrument id="P5-I5">' ... ' <instrument-name>C Trumpet</instrument-name>' ... '</score-instrument>' ... '<midi-instrument id="P5-I5">' ... ' <midi-channel>2</midi-channel>' ... '<midi-program>57</midi-program>' ... '</midi-instrument>' ... '</score-part>') >>> from xml.etree.ElementTree import fromstring as EL >>> pp = musicxml.xmlToM21.PartParser() >>> mxScorePart = EL(scorePart) >>> i = pp.getDefaultInstrument(mxScorePart) >>> i <music21.instrument.Trumpet ': C Trumpet'> >>> i.instrumentName 'C Trumpet' >>> i.transposition <music21.interval.Interval P1> ''' if mxScorePart is None: mxScorePart = self.mxScorePart if mxScorePart is None: raise MusicXMLImportException( 'score-part must be defined before calling this.' ) def _adjustMidiData(mc): adjusted = int(mc) - 1 if adjusted == -1: adjusted = 0 # a lot of zero indexed pianos return adjusted seta = _setAttributeFromTagText # for now, just get first midi instrument # TODO: get all # TODO: midi-device # TODO: midi-name # TODO: midi-bank transform=_adjustMidiData # TODO: midi-volume # TODO: pan # TODO: elevation # TODO: store id attribute somewhere mxMIDIInstrument = mxScorePart.find('midi-instrument') i: instrument.Instrument|None = None if mxMIDIInstrument is not None: mxMidiProgram = mxMIDIInstrument.find('midi-program') mxMidiUnpitched = mxMIDIInstrument.find('midi-unpitched') if midiUnpitchedText := strippedText(mxMidiUnpitched): pm = PercussionMapper() try: i = pm.midiPitchToInstrument(_adjustMidiData(midiUnpitchedText)) except MIDIPercussionException as mpe: # objects not yet existing in m21 such as Cabasa warnings.warn(MusicXMLWarning(mpe), stacklevel=2) i = instrument.UnpitchedPercussion() i.percMapPitch = _adjustMidiData(midiUnpitchedText) elif midiProgramText := strippedText(mxMidiProgram): try: i = instrument.instrumentFromMidiProgram(_adjustMidiData(midiProgramText)) except instrument.InstrumentException as ie: warnings.warn(MusicXMLWarning(ie), stacklevel=2) # Invalid MIDI program, out of range 0-127 i = instrument.Instrument() seta(i, mxMIDIInstrument, 'midi-channel', transform=_adjustMidiData) if i is None: # This catches no mxMIDIInstrument or empty text. i = instrument.Instrument() # for now, just get first instrument # TODO: get all instruments! mxScoreInstrument = mxScorePart.find('score-instrument') if mxScoreInstrument is not None and not isinstance(i, instrument.UnpitchedPercussion): # Retains original midiChannel from `i` inst_from_name = self.reclassifyInstrumentFromName(i, mxScoreInstrument) # Two cases where we use the instrument constructed from the name instead # 1. midiProgram matches (this will catch non-default transpositions in name) # 2. midiProgram is Piano (often this is encoded only as piano for convenience) if inst_from_name.midiProgram == i.midiProgram or isinstance(i, instrument.Piano): i = inst_from_name i.partId = self.partId if self.partId is not None: i.groups.append(self.partId) i.partName = self.stream.partName i.partAbbreviation = self.stream.partAbbreviation # TODO: groups if mxScoreInstrument is not None: seta(i, mxScoreInstrument, 'instrument-name', transform=_clean) seta(i, mxScoreInstrument, 'instrument-abbreviation', transform=_clean) seta(i, mxScoreInstrument, 'instrument-sound') # TODO: solo / ensemble # TODO: virtual-instrument # TODO: store id attribute somewhere return i
[docs] @staticmethod def reclassifyInstrumentFromName( i: instrument.Instrument, mxScoreInstrument: ET.Element, ) -> instrument.Instrument: mxInstrumentName = mxScoreInstrument.find('instrument-name') if instrumentNameText := strippedText(mxInstrumentName): previous_midi_channel = i.midiChannel try: i = instrument.fromString(instrumentNameText) except instrument.InstrumentException: i = instrument.Instrument() i.midiChannel = previous_midi_channel return i
[docs] def parseMeasures(self): ''' Parse each <measure> tag using self.xmlMeasureToMeasure ''' part = self.stream for mxMeasure in self.mxPart.iterfind('measure'): self.xmlMeasureToMeasure(mxMeasure) self.removeFinaleIncorrectEndingForwardRest() part.coreElementsChanged()
[docs] def removeFinaleIncorrectEndingForwardRest(self) -> None: ''' If Finale generated the file AND it ended with an incomplete measure (like 4/4 beginning with a quarter pickup and ending with a 3-beat measure) then the file might have ended with a `<forward>` tag, which Finale used to create hidden rests. If this forward tag is at the end of the piece, then it will create rests that "complete" the measure in an incorrect way If voices are not involved (e.g., NOT bwv66.6) then we should remove this forward tag. * New in v7. ''' lmp = self.lastMeasureParser if lmp is None: # pragma: no cover return # should not happen self.lastMeasureParser = None # clean memory if lmp.lastForwardTagCreatedByFinale is None: return if lmp.useVoices: return endingForwardRest: note.Rest|None = lmp.lastForwardTagCreatedByFinale # important that we find that the last GeneralNote is this Forward tag if (lmp.stream[note.GeneralNote].last() is endingForwardRest and endingForwardRest is not None): lmp.stream.remove(endingForwardRest, recurse=True)
[docs] def separateOutPartStaves(self) -> list[stream.PartStaff]: ''' Take a `Part` with multiple staves and make them a set of `PartStaff` objects. There must be more than one staff to do this. ''' # Elements in these classes appear only on the staff to which they are assigned. # All other classes appear on every staff, except for spanners, which remain on the first. STAFF_SPECIFIC_CLASSES = [ 'Clef', 'Dynamic', 'Expression', 'GeneralNote', 'KeySignature', 'StaffLayout', 'TempoIndication', 'TimeSignature', ] # spanners generally appear only on the first staff. # RepeatBracket spanners, however, need to appear on every staff. EXEMPT_FROM_REMOVE = frozenset( ['RepeatBracket'], ) uniqueStaffKeys: list[int] = self._getUniqueStaffKeys() partStaves: list[stream.PartStaff] = [] appendedElementIds: set[int] = set() # id is id(el) not el.id def copy_into_partStaff(source: stream.Stream, target: stream.Stream, omitTheseElementIds: set[int]) -> None: elementIterator = source.getElementsByClass(STAFF_SPECIFIC_CLASSES) elementIterator.restoreActiveSites = False for sourceElem in elementIterator: idSource = id(sourceElem) if idSource in omitTheseElementIds: continue if idSource in appendedElementIds: targetElem = copy.deepcopy(sourceElem) else: targetElem = sourceElem # do not make a copy if not yet in staff. appendedElementIds.add(idSource) sourceOffset = source.elementOffset(sourceElem, returnSpecial=True) if sourceOffset != OffsetSpecial.AT_END: # the other OffsetSpecials are not returned from elementOffset assert not isinstance(sourceOffset, OffsetSpecial) target.coreInsert(sourceOffset, targetElem) else: target.coreStoreAtEnd(targetElem) target.coreElementsChanged() sourceMeasureIterator = self.stream.getElementsByClass(stream.Measure) for staffIndex, staffKey in enumerate(uniqueStaffKeys): # staffIndex should be staffKey - 1, but you never know removeClasses = STAFF_SPECIFIC_CLASSES[:] if staffIndex != 0: # spanners only on the first staff. removeClasses.append('Spanner') newPartStaff = t.cast( stream.PartStaff, self.stream.template(removeClasses=removeClasses, fillWithRests=False, exemptFromRemove=EXEMPT_FROM_REMOVE) ) partStaffId = f'{self.partId}-Staff{staffKey}' newPartStaff.id = partStaffId # set group for components (recurse?) newPartStaff.addGroupForElements(partStaffId, setActiveSite=False) newPartStaff.groups.append(partStaffId) partStaves.append(newPartStaff) self.parent.m21PartObjectsById[partStaffId] = newPartStaff elementsIdsNotToGoInThisStaff: set[int] = set() # iterate over the StaffReferenceType dicts, one for each measure. for staffReference in self.staffReferenceList: # this is a list of Music21Objects that should not go into the staff # called by staffKey. excludeOneMeasure = self._getStaffExclude( staffReference, staffKey ) for el in excludeOneMeasure: elementsIdsNotToGoInThisStaff.add(id(el)) for sourceMeasure, copyMeasure in zip( sourceMeasureIterator, newPartStaff.getElementsByClass(stream.Measure) ): copy_into_partStaff(sourceMeasure, copyMeasure, elementsIdsNotToGoInThisStaff) for sourceVoice, copyVoice in zip(sourceMeasure.voices, copyMeasure.voices): copy_into_partStaff(sourceVoice, copyVoice, elementsIdsNotToGoInThisStaff) copyMeasure.flattenUnnecessaryVoices(force=False, inPlace=True) score = self.parent.stream staffGroup = layout.StaffGroup(partStaves, name=self.stream.partName, symbol='brace') staffGroup.style.hideObjectOnPrint = True # in truth, hide the name, not the brace score.coreInsert(0, staffGroup) for partStaff in partStaves: score.coreInsert(0, partStaff) score.coreElementsChanged() self.appendToScoreAfterParse = False # ensures that the original stream is not appended. # and thus that these next two lines are not needed: # score.remove(originalPartStaff) # del self.parent.m21PartObjectsById[originalPartStaff.id] return partStaves
def _getStaffExclude( self, staffReference: StaffReferenceType, targetKey: int ) -> list[base.Music21Object]: ''' Given a staff reference dictionary (for a single measure), remove and combine in a list all elements that are NOT part of the given targetKey. Thus, return a list of all entries to remove. It keeps those elements under the staff key None (common to all) and those under given key. This then is the list of all elements that should be deleted. If targetKey is NO_STAFF_ASSIGNED (0) then returns an empty list ''' if targetKey == NO_STAFF_ASSIGNED: return [] post = [] for k in staffReference: if k == NO_STAFF_ASSIGNED: continue elif k == targetKey: continue post += staffReference[k] return post def _getUniqueStaffKeys(self) -> list[int]: ''' Given a list of staffReference dictionaries, collect and return a list of all unique keys except NO_STAFF_ASSIGNED (0) ''' post = [] for staffReference in self.staffReferenceList: for k in staffReference: if k != NO_STAFF_ASSIGNED and k not in post: post.append(k) post.sort() return post
[docs] def xmlMeasureToMeasure(self, mxMeasure: ET.Element) -> stream.Measure: # noinspection PyShadowingNames ''' Convert a measure element to a Measure, using :class:`~music21.musicxml.xmlToM21.MeasureParser` >>> from xml.etree.ElementTree import fromstring as EL Full-measure rests get auto-assigned to match the time signature if they do not have a type, or have a type of "whole". Here is a measure with a rest that lasts 4 beats, but we will put it in a 3/4 context. >>> scoreMeasure = '<measure><note><rest/><duration>40320</duration></note></measure>' >>> mxMeasure = EL(scoreMeasure) >>> pp = musicxml.xmlToM21.PartParser() >>> pp.lastDivisions 10080 >>> 40320 / 10080 4.0 >>> pp.lastTimeSignature = meter.TimeSignature('3/4') >>> m = pp.xmlMeasureToMeasure(mxMeasure) Test that the rest lasts three, not four beats: >>> measureRest = m.notesAndRests[0] >>> measureRest <music21.note.Rest dotted-half> >>> measureRest.duration.type 'half' >>> measureRest.duration.quarterLength 3.0 ''' measureParser = MeasureParser(mxMeasure, parent=self) # noinspection PyBroadException try: measureParser.parse() except MusicXMLImportException as e: e.measureNumber = str(measureParser.measureNumber) e.partName = self.stream.partName raise e except Exception as e: warnings.warn( f'The following exception took place in m. {measureParser.measureNumber} in ' + f'part {self.stream.partName}.', MusicXMLWarning, stacklevel=2 ) raise e self.lastMeasureParser = measureParser self.maxStaves = max(self.maxStaves, measureParser.staves) if measureParser.transposition is not None: self.updateTransposition(measureParser.transposition) self.firstMeasureParsed = True self.staffReferenceList.append(measureParser.staffReference) m = measureParser.stream self.setLastMeasureInfo(m) # TODO: move this into the measure parsing, # because it should happen on a voice level. if measureParser.fullMeasureRest is True: # recurse is necessary because it could be in voices r1 = m[note.Rest].first() if t.TYPE_CHECKING: # fullMeasureRest is True, means Rest will be found assert r1 is not None if self.lastTimeSignature is not None: lastTSQl = self.lastTimeSignature.barDuration.quarterLength else: lastTSQl = 4.0 # sensible default. if (r1.fullMeasure is True # set by xml measure='yes' or (r1.duration.quarterLength != lastTSQl and r1.duration.type in ('whole', 'breve') and r1.duration.dots == 0 and not r1.duration.tuplets)): r1.duration.quarterLength = lastTSQl r1.fullMeasure = True # NB: not coreInsert, because barDurationProportion() # is called in adjustTimeAttributesFromMeasure() self.stream.insert(self.lastMeasureOffset, m) self.adjustTimeAttributesFromMeasure(m) # TODO: musicxml4: listening return m
[docs] def updateTransposition(self, newTransposition: interval.Interval): ''' As one might expect, a measureParser that reveals a change in transposition is going to have an effect on the Part's instrument list. This (totally undocumented) method deals with it. If `measureParser.transposition` is None, does nothing. NOTE: Need to test a change of instrument w/o a change of transposition such as: Bb clarinet to Bb Soprano Sax to Eb clarinet? ''' # STEP 1: determine whether this new transposition # requires creating a new instrument. if self.activeInstrument is not None: if (self.activeInstrument.transposition is None and self.firstMeasureParsed is False): # We already created an instrument (activeInstrument) from the # PartInfo. We haven't done anything with it yet, so # no need for a change of instrument pass # warnings.warn('Put trans on active instrument', MusicXMLWarning) elif self.activeInstrument.transposition != newTransposition: # We have an activeInstrument with a transposition that does # not match, so this change of transposition # requires us to create a new one (think of physical instruments # such as Bb clarinet to A clarinet). newInst = copy.deepcopy(self.activeInstrument) # warnings.warn('Put trans on new instrument', MusicXMLWarning) self.activeInstrument = newInst self.stream.coreInsert(self.lastMeasureOffset, newInst) else: # There is no activeInstrument and we're not at the beginning # of the piece. This shouldn't happen, but let's send a warning # and create a Generic Instrument object rather than dying. warnings.warn( 'Received a transposition tag, but no instrument to put it on!', MusicXMLWarning, stacklevel=2, ) fakeInst = instrument.Instrument() self.activeInstrument = fakeInst self.stream.coreInsert(self.lastMeasureOffset, fakeInst) # STEP 2: # Actually change the transposition of the instrument # and note that the part is definitely NOT all at sounding pitch self.activeInstrument.transposition = newTransposition self.atSoundingPitch = False
[docs] def setLastMeasureInfo(self, m: stream.Measure): # noinspection PyShadowingNames ''' Sets self.lastMeasureNumber and self.lastMeasureSuffix from the measure, which is used in fixing Finale unnumbered measure issues. Also sets self.lastTimeSignature from the timeSignature found in the measure, if any. >>> pp = musicxml.xmlToM21.PartParser() Here are the defaults: >>> pp.lastMeasureNumber 0 >>> pp.lastNumberSuffix is None True >>> pp.lastTimeSignature is None True After setLastMeasureInfo: >>> m = stream.Measure(number=4) >>> m.numberSuffix = 'b' >>> ts38 = meter.TimeSignature('3/8') >>> m.timeSignature = ts38 >>> pp.setLastMeasureInfo(m) >>> pp.lastMeasureNumber 4 >>> pp.lastNumberSuffix 'b' >>> pp.lastTimeSignature <music21.meter.TimeSignature 3/8> >>> pp.lastTimeSignature is ts38 True Note that if there was no timeSignature defined in m, and no lastTimeSignature exists, the PartParser gets a default of 4/4, because after the first measure there's going to be routines that need some sort of time signature: >>> pp2 = musicxml.xmlToM21.PartParser() >>> m2 = stream.Measure(number=2) >>> pp2.setLastMeasureInfo(m2) >>> pp2.lastTimeSignature <music21.meter.TimeSignature 4/4> For obscure reasons relating to how Finale gives suffixes to unnumbered measures, if a measure has the same number as the lastMeasureNumber, the lastNumberSuffix is not updated: >>> pp3 = musicxml.xmlToM21.PartParser() >>> pp3.lastMeasureNumber = 10 >>> pp3.lastNumberSuffix = 'X1' >>> m10 = stream.Measure(number=10) >>> m10.numberSuffix = 'X2' >>> pp3.setLastMeasureInfo(m10) >>> pp3.lastNumberSuffix 'X1' ''' if m.number == self.lastMeasureNumber: pass # we do this check so that we do not compound suffixes, i.e.: # 23, 23.X1, 23.X1X2, 23.X1X2X3 # and instead just do: # 23, 23.X1, 23.X2, etc. else: self.lastMeasureNumber = m.number self.lastNumberSuffix = m.numberSuffix if m.timeSignature is not None: self.lastTimeSignature = m.timeSignature elif self.lastTimeSignature is None: # if no time signature is defined, need to get a default ts = meter.TimeSignature('4/4') self.lastTimeSignature = ts
[docs] def adjustTimeAttributesFromMeasure(self, m: stream.Measure): # noinspection PyShadowingNames ''' Adds padAsAnacrusis to pickup measures and other measures that do not fill the whole tile, if the first measure of the piece, or immediately follows an incomplete measure (such as a repeat sign mid-measure in a piece where each phrase begins with a pickup and ends with an incomplete measure). Fills an empty measure with a measure of rest (bug in PDFtoMusic and other MusicXML writers). Sets self.lastMeasureWasShort to True or False if it is an incomplete measure that is not a pickup and sets paddingRight. >>> m = stream.Measure([meter.TimeSignature('4/4'), harmony.ChordSymbol('C7')]) >>> m.highestTime 0.0 >>> pp = musicxml.xmlToM21.PartParser() >>> pp.setLastMeasureInfo(m) >>> pp.adjustTimeAttributesFromMeasure(m) >>> m.highestTime 4.0 >>> pp.lastMeasureWasShort False Incomplete final measure: >>> m = stream.Measure([meter.TimeSignature('6/8'), note.Note(), note.Note()]) >>> m.offset = 24.0 >>> pp = musicxml.xmlToM21.PartParser() >>> pp.lastMeasureOffset = 21.0 >>> pp.setLastMeasureInfo(m) >>> pp.adjustTimeAttributesFromMeasure(m) >>> m.paddingRight 1.0 ''' # note: we cannot assume that the time signature properly # describes the offsets w/n this bar. need to look at # offsets within measure; if the .highestTime value is greater # use this as the next offset mHighestTime = m.highestTime # warnings.warn(f'{m} {mHighestTime} {self}', MusicXMLWarning) # warnings.warn([self.lastTimeSignature], MusicXMLWarning) # warnings.warn([self.lastTimeSignature.barDuration], MusicXMLWarning) if self.lastTimeSignature is not None: lastTimeSignatureQuarterLength = self.lastTimeSignature.barDuration.quarterLength else: lastTimeSignatureQuarterLength = 4.0 # sensible default. if mHighestTime == lastTimeSignatureQuarterLength: mOffsetShift = mHighestTime elif mHighestTime > lastTimeSignatureQuarterLength: diff = mHighestTime - lastTimeSignatureQuarterLength tol = 1e-6 # If the measure is overfull by a "round" amount, assume that it was intended # otherwise it was likely the result of malformed MusicXML. if (diff > 0.5 or nearestMultiple(diff, 0.0625)[1] < tol or nearestMultiple(diff, 1 / 12)[1] < tol): mOffsetShift = mHighestTime else: mOffsetShift = lastTimeSignatureQuarterLength warnings.warn( f'Warning: measure {m.number} in part {self.stream.partName}' f'is overfull: {mHighestTime} > {lastTimeSignatureQuarterLength},' f'assuming {mOffsetShift} is correct.', MusicXMLWarning, stacklevel=2, ) elif (mHighestTime == 0.0 and not m.recurse().notesAndRests.getElementsNotOfClass('Harmony') ): # this routine fixes a bug in PDFtoMusic and other MusicXML writers # that omit empty rests in a Measure. It is a very quick test if # the measure has any notes. Slower if it does not. r = note.Rest() r.duration.quarterLength = lastTimeSignatureQuarterLength m.insert(0.0, r) mOffsetShift = lastTimeSignatureQuarterLength self.lastMeasureWasShort = False else: # use time signature # for the first measure, this may be a pickup # must detect this when writing, as next measures offsets will be # incorrect if self.lastMeasureOffset == 0.0: # cannot get bar duration proportion if we cannot get a ts if m.barDurationProportion() < 1.0: m.padAsAnacrusis() # environLocal.printDebug(['incompletely filled Measure found on musicxml # import; interpreting as an anacrusis:', 'paddingLeft:', m.paddingLeft]) mOffsetShift = mHighestTime else: mOffsetShift = mHighestTime # lastTimeSignatureQuarterLength if self.lastMeasureWasShort is True: if m.barDurationProportion() < 1.0: m.padAsAnacrusis() # probably a pickup after a repeat or phrase boundary # or something self.lastMeasureWasShort = False else: # Incomplete measure that is likely NOT an anacrusis, set paddingRight if m.barDurationProportion() < 1.0: m.paddingRight = m.barDuration.quarterLength - m.highestTime if mHighestTime < lastTimeSignatureQuarterLength: self.lastMeasureWasShort = True else: self.lastMeasureWasShort = False self.lastMeasureOffset += mOffsetShift
[docs] def applyMultiMeasureRest(self, r: note.Rest): ''' If there is an active MultiMeasureRestSpanner, add the Rest, r, to it: >>> pp = musicxml.xmlToM21.PartParser() >>> mmrSpanner = spanner.MultiMeasureRest() >>> mmrSpanner <music21.spanner.MultiMeasureRest 0 measures> >>> pp.activeMultiMeasureRestSpanner = mmrSpanner >>> pp.multiMeasureRestsToCapture = 2 >>> r1 = note.Rest(type='whole', id='r1') >>> pp.applyMultiMeasureRest(r1) >>> pp.multiMeasureRestsToCapture 1 >>> pp.activeMultiMeasureRestSpanner <music21.spanner.MultiMeasureRest 1 measure> >>> pp.activeMultiMeasureRestSpanner is mmrSpanner True >>> pp.stream.show('text') # Nothing shown! >>> r2 = note.Rest(type='whole', id='r2') >>> pp.applyMultiMeasureRest(r2) >>> pp.multiMeasureRestsToCapture 0 >>> pp.activeMultiMeasureRestSpanner is None True # spanner added to stream >>> pp.stream.show('text') {0.0} <music21.spanner.MultiMeasureRest 2 measures> >>> r3 = note.Rest(type='whole', id='r3') >>> pp.applyMultiMeasureRest(r3) >>> pp.stream.show('text') {0.0} <music21.spanner.MultiMeasureRest 2 measures> ''' if self.activeMultiMeasureRestSpanner is None: return self.activeMultiMeasureRestSpanner.addSpannedElements(r) self.multiMeasureRestsToCapture -= 1 if self.multiMeasureRestsToCapture == 0: self.stream.insert(0, self.activeMultiMeasureRestSpanner) self.activeMultiMeasureRestSpanner = None
# -----------------------------------------------------------------------------
[docs] class MeasureParser(XMLParserBase): ''' parser to work with a single <measure> tag. called out for simplicity. >>> from xml.etree.ElementTree import fromstring as EL >>> scoreMeasure = '<measure><note><rest/><duration>40320</duration></note></measure>' >>> mxMeasure = EL(scoreMeasure) >>> mp = musicxml.xmlToM21.MeasureParser(mxMeasure) >>> mp.parse() >>> mp.restAndNoteCount['rest'] 1 >>> mp.restAndNoteCount['note'] 0 fullMeasureRest indicates that a rest lasts the full measure of the current time signature. >>> mp.fullMeasureRest True ''' attributeTagsToMethods = { 'time': 'handleTimeSignature', 'clef': 'handleClef', 'key': 'handleKeySignature', 'staff-details': 'handleStaffDetails', 'measure-style': 'handleMeasureStyle', } musicDataMethods = { 'note': 'xmlToNote', 'backup': 'xmlBackup', 'forward': 'xmlForward', 'direction': 'xmlDirection', 'attributes': 'parseAttributesTag', 'harmony': 'xmlHarmony', 'figured-bass': None, 'sound': 'xmlSound', 'barline': 'xmlBarline', 'grouping': None, 'link': None, 'bookmark': None, # Note: <print> is handled separately # <sound> and xmlSound are found in xmlSoundParser.py } def __init__(self, mxMeasure: ET.Element|None = None, parent: PartParser|None = None): super().__init__() self.mxMeasure = mxMeasure self.mxMeasureElements: list[ET.Element] = [] self.parent: PartParser = parent if parent is not None else PartParser() self.transposition = None self.spannerBundle = self.parent.spannerBundle self.staffReference: StaffReferenceType = {} self.activeTuplets: list[duration.Tuplet|None] = self.parent.activeTuplets self.useVoices: bool = False self.voicesById: dict[str|int, stream.Voice] = {} self.voiceIndices: set[str|int] = set() self.staves = 1 self.activeAttributes = None self.attributesAreInternal = True self.measureNumber = 0 self.numberSuffix = '' self.divisions = self.parent.lastDivisions # key is a tuple of the # staff number (or None) and offsetMeasureNote, and the value is a # StaffLayout object. self.staffLayoutObjects: dict[tuple[int|None, float], layout.StaffLayout] = {} self.stream = stream.Measure() self.mxNoteList: list[ET.Element] = [] # for accumulating notes in chords self.mxLyricList: list[ET.Element] = [] # for accumulating lyrics assigned to chords self.nLast: note.GeneralNote|None = None # for adding notes to spanners. # Sibelius 7.1 only puts a <voice> tag on the # first note of a chord, and MuseScore doesn't put one # on <forward> elements for hidden rests, so we need to make sure # that we keep track of the last voice. # there is an effort to translate the voice text to an int, but if that fails (unlikely) # we store whatever we find self.lastVoice: str|int|None = None # fullMeasureRest is unreliable because pickup measures # in Finale set <rest measure="yes"> but then define a type like "quarter", # this cannot be trusted to give a whole rest. self.fullMeasureRest = False # for keeping track of full-measureRests. self.restAndNoteCount = {'rest': 0, 'note': 0} self.lastClefs: dict[int, clef.Clef|None] = self.parent.lastClefs self.parseIndex = 0 # what is the offset in the measure of the current note position? self.offsetMeasureNote: OffsetQL = 0.0 # Keep track of the last rest that was added with a forward tag. # Older versions of Finale put a <forward> tag at the end of pieces # which ended with an incomplete measure. Find that last # Forward tag (if created by Finale) and store it. # if later we find that this measure is the last one, # and doesn't have multiple voices, and was created by Finale, # then we'll delete the Rest associated with this forward tag # at the cleanup stage of PartParser. self.lastForwardTagCreatedByFinale: 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()
[docs] def xmlSound(self, mxSound: ET.Element) -> None: ''' Convert a <sound> tag; delegates to :func:`~music21.musicxml.xmlSoundParser.xmlSound`, which is pulled out to keep this file smaller. ''' xmlSoundParser.xmlSound(self, mxSound)
[docs] @staticmethod def getStaffNumber(mxObject: ET.Element|int|None) -> int: ''' gets an int representing a staff number from an mxObject or a number: >>> mp = musicxml.xmlToM21.MeasureParser() >>> from xml.etree.ElementTree import fromstring as EL <note> tags store their staff numbers in a <staff> tag's text: >>> mp.getStaffNumber(EL('<note><staff>3</staff></note>')) 3 If there is no <staff> tag, notes return the special NO_STAFF_ASSIGNED value (which is, for now, just a fancy alias for 0!) >>> el = EL('<note><pitch><step>C</step><octave>4</octave></pitch></note>') >>> NO_STAFF_ASSIGNED = musicxml.xmlToM21.NO_STAFF_ASSIGNED >>> NO_STAFF_ASSIGNED 0 >>> mp.getStaffNumber(el) == NO_STAFF_ASSIGNED True Other objects, such as keys, clefs, and various layouts, however, store their staff numbers in a `number` attribute. >>> mp.getStaffNumber(EL('<key number="2"/>')) 2 If there is no number for most objects, such as keys and time signatures, it means that the object affects all staves (makes sense for a time signature, no?). Hence they are treated as NO_STAFF_ASSIGNED >>> mp.getStaffNumber(EL('<key/>')) == NO_STAFF_ASSIGNED True Clef, staff-layout, and staff-details objects, however, use a missing number to mean staff 1. >>> mp.getStaffNumber(EL('<clef/>')) 1 Passing in None or an int is allowed. >>> mp.getStaffNumber(None) == NO_STAFF_ASSIGNED True >>> mp.getStaffNumber(1) 1 ''' if isinstance(mxObject, int): return mxObject elif mxObject is None: return NO_STAFF_ASSIGNED tag = mxObject.tag # find objects that use a "staff" element # harmony, forward, note, direction if tag in ('harmony', 'forward', 'note', 'direction'): try: staffObject = mxObject.find('staff') if staffObject is not None: staffText = staffObject.text if staffText is not None: k = staffText.strip() return int(k) except (AttributeError, TypeError, ValueError): pass return NO_STAFF_ASSIGNED elif tag in ('staff-layout', 'staff-details', 'measure-style', 'clef', 'key', 'time', 'transpose'): # these objects store staff assignment simply as an attribute called number. try: k_str = mxObject.get('number') # this must be a positive integer as string if k_str is not None: return int(k_str) except (TypeError, ValueError, AttributeError): pass if tag in ('staff-layout', 'staff-details', 'clef'): # for this subset of number'ed tags, no number means staff 1 return 1 return NO_STAFF_ASSIGNED else: return NO_STAFF_ASSIGNED
# TODO: handle part-symbol (attributes: top-staff, bottom-staff) # separately
[docs] def addToStaffReference( self, mxObjectOrNumber: int|ET.Element|None, m21Object: base.Music21Object ) -> None: ''' Utility routine for importing musicXML objects; here, we store a reference to the music21 object in a dictionary, where keys are the staff values. Staff values may be None, 1, 2, etc. >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.addToStaffReference(1, note.Note('C5')) >>> MP.addToStaffReference(2, note.Note('D3')) >>> MP.addToStaffReference(2, note.Note('E3')) >>> len(MP.staffReference) 2 >>> list(sorted(MP.staffReference.keys())) [1, 2] >>> MP.staffReference[1] [<music21.note.Note C>] >>> MP.staffReference[2] [<music21.note.Note D>, <music21.note.Note E>] >>> from xml.etree.ElementTree import fromstring as EL >>> mxNote = EL('<note><staff>1</staff></note>') >>> MP.addToStaffReference(mxNote, note.Note('F5')) >>> MP.staffReference[1] [<music21.note.Note C>, <music21.note.Note F>] No staff reference. >>> mxNote = EL('<note />') >>> MP.addToStaffReference(mxNote, note.Note('G4')) >>> len(MP.staffReference) 3 >>> MP.staffReference[0] [<music21.note.Note G>] ''' staffReference = self.staffReference if isinstance(mxObjectOrNumber, int): staffKey = mxObjectOrNumber else: staffKey = self.getStaffNumber(mxObjectOrNumber) # an int incl. NO_STAFF_ASSIGNED if staffKey not in staffReference: staffReference[staffKey] = [] staffReference[staffKey].append(m21Object)
[docs] def insertCoreAndRef( self, offset, staffSource: int|ET.Element|None, m21Object: base.Music21Object ) -> None: ''' runs addToStaffReference and then insertCore (which will do opFracs, so no need to do so before here) >>> from xml.etree.ElementTree import fromstring as EL >>> mxNote = EL('<note><staff>1</staff></note>') >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.insertCoreAndRef(1.0, mxNote, note.Note('F5')) This routine leaves MP.stream in an unusable state, because it runs insertCore. Thus, before querying the stream we need to run at end: >>> MP.stream.coreElementsChanged() >>> MP.stream.show('text') {1.0} <music21.note.Note F> ''' self.addToStaffReference(staffSource, m21Object) self.stream.coreInsert(offset, m21Object)
[docs] def parse(self): # handle <print> before anything else, because it can affect # attributes! for mxPrint in self.mxMeasure.findall('print'): self.xmlPrint(mxPrint) # these are the attributes of the <measure> tag, not the <attributes> tag self.parseMeasureAttributes() self.updateVoiceInformation() self.mxMeasureElements = list(self.mxMeasure) # for grabbing next note for i, mxObj in enumerate(self.mxMeasureElements): self.parseIndex = i # for grabbing next note if mxObj.tag in self.musicDataMethods: methName = self.musicDataMethods[mxObj.tag] if methName is not None: meth = getattr(self, methName) meth(mxObj) if self.useVoices: for v in self.stream.iter().voices: if v: # do not bother with empty voices # the musicDataMethods use insertCore, thus the voices need to run # coreElementsChanged v.coreElementsChanged() self.stream.coreElementsChanged() if (self.restAndNoteCount['rest'] == 1 and self.restAndNoteCount['note'] == 0): # TODO: do this on a per-voice basis. self.fullMeasureRest = True
# it might already be True because a rest had a "measure='yes'" attribute
[docs] def xmlBackup(self, mxObj: ET.Element): ''' Parse a backup tag by changing :attr:`offsetMeasureNote`. A floor of 0.0 is enforced in case of float rounding issues. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 100 >>> MP.offsetMeasureNote = 1.875 >>> mxBackup = EL('<backup><duration>100</duration></backup>') >>> MP.xmlBackup(mxBackup) >>> MP.offsetMeasureNote 0.875 >>> MP.xmlBackup(mxBackup) >>> MP.offsetMeasureNote 0.0 ''' mxDuration = mxObj.find('duration') if durationText := strippedText(mxDuration): self.offsetMeasureNote = opFrac(self.offsetMeasureNote - float(durationText) / self.divisions) # check for negative offsets produced by # musicxml durations with float rounding issues # https://github.com/cuthbertLab/music21/issues/971 self.offsetMeasureNote = max(self.offsetMeasureNote, 0.0)
[docs] def xmlForward(self, mxObj: ET.Element): ''' Parse a forward tag by changing :attr:`offsetMeasureNote`. ''' mxDuration = mxObj.find('duration') if durationText := strippedText(mxDuration): change = opFrac(float(durationText) / self.divisions) if (self.parent and self.parent.parent and self.parent.parent.applyFinaleWorkarounds): # If the ScoreParser senses the Score was written by Finale # then Forward tags need to create hidden rests (except # at the end of the piece!) So create a hidden rest (spacer) here. r = note.Rest(quarterLength=change) r.style.hideObjectOnPrint = True self.addToStaffReference(mxObj, r) self.insertInMeasureOrVoice(mxObj, r) # old Finale documents close incomplete final measures with <forward> # this will be removed afterward by removeFinaleIncorrectEndingForwardRest() self.lastForwardTagCreatedByFinale = r # Allow overfilled measures for now -- TODO(someday): warn? self.offsetMeasureNote = opFrac(self.offsetMeasureNote + change)
[docs] def xmlPrint(self, mxPrint: ET.Element): ''' <print> handles changes in pages, numbering, layout, etc. so can generate PageLayout, SystemLayout, or StaffLayout objects. Should also be able to set measure attributes on `self.stream` ''' def hasPageLayout(): if mxPrint.get('new-page') not in (None, 'no'): return True if mxPrint.get('page-number') is not None: return True if mxPrint.find('page-layout') is not None: return True return False def hasSystemLayout(): if mxPrint.get('new-system') not in (None, 'no'): return True if mxPrint.find('system-layout') is not None: return True return False addPageLayout = hasPageLayout() addSystemLayout = hasSystemLayout() addStaffLayout = mxPrint.find('staff-layout') is not None # --- now we know what we need to add, add em m = self.stream if addPageLayout is True: pl = self.xmlPrintToPageLayout(mxPrint) m.insert(0.0, pl) # should this be parserOffset? if addSystemLayout is True or addPageLayout is False: sl = self.xmlPrintToSystemLayout(mxPrint) m.insert(0.0, sl) if addStaffLayout is True: # assumes addStaffLayout is there slFunc = self.xmlStaffLayoutToStaffLayout stlList = [slFunc(mx) for mx in mxPrint.iterfind('staff-layout')] # If bugs incorporate Ariza additional checks, but # I think that we don't want to add to an existing staffLayoutObject # so that staff distance can change. for stl in stlList: if stl is None or stl.staffNumber is None: continue # Sibelius likes to give empty staff layouts! self.insertCoreAndRef(0.0, int(stl.staffNumber), stl) self.stream.coreElementsChanged() # TODO: measure-layout -- affect self.stream mxMeasureNumbering = mxPrint.find('measure-numbering') if mxMeasureNumbering is not None: # TODO: musicxml 4: system="yes/no" -- does this apply to whole system? # TODO: musicxml 4: staff attribute. m_style = t.cast(style.StreamStyle, m.style) m_style.measureNumbering = mxMeasureNumbering.text st = style.TextStyle() self.setPrintStyleAlign(mxMeasureNumbering, st) # TODO: musicxml 4: multiple-rest-always, multiple-rest-range m_style.measureNumberStyle = st
# TODO: part-name-display # TODO: part-abbreviation display # TODO: print-attributes: staff-spacing, blank-page; skip deprecated staff-spacing
[docs] def xmlToNote(self, mxNote: ET.Element) -> None: ''' Handles everything for creating a Note or Rest or Chord Does not actually return the note, but sets self.nLast to the note. This routine uses coreInserts for speed, so it can leave either `self.stream` or a `Voice` object within `self.stream` in an unstable state. ''' try: mxObjNext = self.mxMeasureElements[self.parseIndex + 1] if mxObjNext.tag == 'note' and mxObjNext.find('chord') is not None: nextNoteIsChord = True else: nextNoteIsChord = False except IndexError: # last note in measure nextNoteIsChord = False # TODO: Cue notes (no sounding tie) # the first note of a chord is not identified directly; only # by looking at the next note can we tell if we have the first # note of a chord isChord = False isRest = False # TODO: Unpitched offsetIncrement: float|fractions.Fraction = 0.0 if mxNote.find('rest') is not None: # it is a Rest isRest = True if mxNote.find('chord') is not None: isChord = True # do not count extra pitches in chord as note. # it might be the first note of the chord if nextNoteIsChord: isChord = True # first note of chord is not identified. voiceOfChord = mxNote.find('voice') if voiceOfChord is not None: vIndex: str|int|None = voiceOfChord.text if isinstance(vIndex, str): try: vIndex = int(vIndex) except ValueError: pass self.lastVoice = vIndex if isChord is True: # and isRest is False? n = None # for linting self.mxNoteList.append(mxNote) # store lyrics for latter processing for mxLyric in mxNote.findall('lyric'): self.mxLyricList.append(mxLyric) elif isChord is False and isRest is False: # normal note self.restAndNoteCount['note'] += 1 n = self.xmlToSimpleNote(mxNote) else: # it's a rest self.restAndNoteCount['rest'] += 1 n = self.xmlToRest(mxNote) if isChord is False: # normal note or rest generalNote = t.cast(note.GeneralNote, n) self.updateLyricsFromList(generalNote, mxNote.findall('lyric')) self.addToStaffReference(mxNote, generalNote) self.insertInMeasureOrVoice(mxNote, generalNote) offsetIncrement = generalNote.duration.quarterLength self.nLast = generalNote # update # if we have notes in the note list and the next # note either does not exist or is not a chord, we # have a complete chord if self.mxNoteList and nextNoteIsChord is False: c = self.xmlToChord(self.mxNoteList) # add any accumulated lyrics self.updateLyricsFromList(c, self.mxLyricList) self.addToStaffReference(self.mxNoteList[0], c) for thisMxNote in self.mxNoteList: # voice might be in a previous note; in fact, often in first <note> if thisMxNote.find('voice') is not None: self.insertInMeasureOrVoice(thisMxNote, c) break else: self.insertInMeasureOrVoice(mxNote, c) self.mxNoteList = [] # clear for next chord self.mxLyricList = [] offsetIncrement = c.quarterLength self.nLast = c # update # only increment Chords after completion self.offsetMeasureNote = opFrac(self.offsetMeasureNote + offsetIncrement) self.lastForwardTagCreatedByFinale = None
[docs] def xmlToChord(self, mxNoteList: list[ET.Element]) -> chord.ChordBase: # noinspection PyShadowingNames ''' Given a list of mxNotes, fill the necessary parameters >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 10080 >>> qnDuration = r'<duration>7560</duration><type>quarter</type>' >>> a = EL('<note>' ... + '<pitch><step>A</step><octave>3</octave></pitch>' ... + qnDuration ... + '</note>') >>> b = EL('<note><chord/>' ... + '<pitch><step>B</step><octave>3</octave></pitch>' ... + qnDuration ... + '</note>') >>> c = MP.xmlToChord([a, b]) >>> len(c.pitches) 2 >>> c.pitches[0] <music21.pitch.Pitch A3> >>> c.pitches[1] <music21.pitch.Pitch B3> >>> c.duration <music21.duration.Duration unlinked type:quarter quarterLength:0.75> >>> a = EL('<note><pitch><step>A</step><octave>3</octave></pitch>' ... + qnDuration ... + '<notehead>diamond</notehead></note>') >>> c = MP.xmlToChord([a, b]) >>> c[0].notehead 'diamond' >>> a = EL('<note><unpitched><display-step>A</display-step>' ... + '<display-octave>3</display-octave></unpitched>' ... + qnDuration ... + '<notehead>diamond</notehead></note>') >>> MP.xmlToChord([a, b]) <music21.percussion.PercussionChord [unpitched[A3] B3]> ''' notes = [] for mxNote in mxNoteList: notes.append(self.xmlToSimpleNote(mxNote, freeSpanners=False)) c: chord.ChordBase if any(mxNote.find('unpitched') is not None for mxNote in mxNoteList): c = percussion.PercussionChord(notes) else: c = chord.Chord(notes) # type: ignore # they are all Notes. # move beams from first note (TODO: confirm style moved already?) if notes: c.beams = notes[0].beams notes[0].beams = beam.Beams() # move spanners, expressions, articulations from first note to Chord. # See slur in m2 of schoenberg/op19 #2 # but move only one of each class, unless a fingering. # Is there anything else that should be moved??? seenArticulations = set() seenExpressions = set() sortKey = lambda x: x.pitch.ps if hasattr(x, 'pitch') else x.displayPitch().midi for n in sorted(notes, key=sortKey): ss = n.getSpannerSites() # transfer all spanners from the notes to the chord. for sp in ss: sp.replaceSpannedElement(n, c) for art in n.articulations: if type(art) in seenArticulations: continue c.articulations.append(art) if not isinstance(art, (articulations.Fingering, articulations.StringIndication, articulations.FretIndication)): seenArticulations.add(type(art)) for exp in n.expressions: if type(exp) in seenExpressions: continue c.expressions.append(exp) seenExpressions.add(type(exp)) n.articulations = [] n.expressions = [] self.spannerBundle.freePendingSpannedElementAssignment(c) return c
[docs] def xmlToSimpleNote(self, mxNote, freeSpanners=True) -> note.Note|note.Unpitched: # noinspection PyShadowingNames ''' Translate a MusicXML <note> (without <chord/>) to a :class:`~music21.note.Note`. The `spannerBundle` parameter can be a list or a Stream for storing and processing Spanner objects. if freeSpanners is False then pending spanners will not be freed. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 10080 >>> mxNote = EL('<note pizzicato="yes"><pitch><step>D</step>' ... '<alter>-1</alter><octave>6</octave></pitch>' ... '<duration>7560</duration>' ... '<type>eighth</type><dot/></note>') >>> n = MP.xmlToSimpleNote(mxNote) >>> n <music21.note.Note D-> >>> n.octave 6 >>> n.duration <music21.duration.Duration 0.75> >>> n.articulations [<music21.articulations.Pizzicato>] >>> beams = EL('<beam>begin</beam>') >>> mxNote.append(beams) >>> n = MP.xmlToSimpleNote(mxNote) >>> n.beams <music21.beam.Beams <music21.beam.Beam 1/start>> >>> stem = EL('<stem>up</stem>') >>> mxNote.append(stem) >>> n = MP.xmlToSimpleNote(mxNote) >>> n.stemDirection 'up' # TODO: beams over rests? ''' d = self.xmlToDuration(mxNote) n: note.Note|note.Unpitched mxUnpitched = mxNote.find('unpitched') if mxUnpitched is None: # send whole note since accidental display not in <pitch> n = note.Note(duration=d) self.xmlToPitch(mxNote, n.pitch) else: n = note.Unpitched(duration=d) self.xmlToUnpitched(mxUnpitched, n) beamList = mxNote.findall('beam') if beamList: n.beams = self.xmlToBeams(beamList) mxStem = mxNote.find('stem') if mxStem is not None: n.stemDirection = mxStem.text.strip() if mxStem.attrib: stemStyle = style.Style() self.setColor(mxStem, stemStyle) self.setPosition(mxStem, stemStyle) this_note_style = t.cast(style.NoteStyle, n.style) this_note_style.stemStyle = stemStyle # gets the notehead object from the mxNote and sets value of the music21 note # to the value of the notehead object mxNotehead = mxNote.find('notehead') # TODO: notehead-text if mxNotehead is not None: self.xmlNotehead(n, mxNotehead) # after this, use combined function for notes and rests return self.xmlNoteToGeneralNoteHelper(n, mxNote, freeSpanners=freeSpanners)
# beam and beams
[docs] def xmlToBeam(self, mxBeam: ET.Element, inputM21=None): # noinspection PyShadowingNames ''' given an mxBeam object return a :class:`~music21.beam.Beam` object >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxBeam = EL('<beam>begin</beam>') >>> a = MP.xmlToBeam(mxBeam) >>> a.type 'start' >>> mxBeam = EL('<beam>continue</beam>') >>> a = MP.xmlToBeam(mxBeam) >>> a.type 'continue' >>> mxBeam = EL('<beam>end</beam>') >>> a = MP.xmlToBeam(mxBeam) >>> a.type 'stop' >>> mxBeam = EL('<beam>forward hook </beam>') >>> a = MP.xmlToBeam(mxBeam) >>> a.type 'partial' >>> a.direction 'right' >>> mxBeam = EL('<beam>backward hook</beam>') >>> a = MP.xmlToBeam(mxBeam) >>> a.type 'partial' >>> a.direction 'left' >>> mxBeam = EL('<beam>crazy</beam>') >>> a = MP.xmlToBeam(mxBeam) Traceback (most recent call last): music21.musicxml.xmlObjects.MusicXMLImportException: unexpected beam type encountered (crazy) ''' if inputM21 is None: beamOut = beam.Beam() else: beamOut = inputM21 # TODO: get number to preserve # not to-do: repeater; is deprecated. self.setColor(mxBeam, beamOut) self.setStyleAttributes(mxBeam, beamOut, 'fan', 'fan') if isinstance(mxBeam.text, str): mxType = mxBeam.text.strip() else: mxType = 'begin' if mxType == 'begin': beamOut.type = 'start' elif mxType == 'continue': beamOut.type = 'continue' elif mxType == 'end': beamOut.type = 'stop' elif mxType == 'forward hook': beamOut.type = 'partial' beamOut.direction = 'right' elif mxType == 'backward hook': beamOut.type = 'partial' beamOut.direction = 'left' else: raise MusicXMLImportException(f'unexpected beam type encountered ({mxType})') if inputM21 is None: return beamOut
[docs] def xmlToBeams(self, mxBeamList, inputM21=None): # noinspection PyShadowingNames ''' given a list of mxBeam objects, sets the beamsList >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxBeam1 = EL('<beam>begin</beam>') >>> mxBeam2 = EL('<beam>begin</beam>') >>> mxBeamList = [mxBeam1, mxBeam2] >>> b = MP.xmlToBeams(mxBeamList) >>> b <music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>> ''' if inputM21 is None: beamsOut = beam.Beams() else: beamsOut = inputM21 for i, mxBeam in enumerate(mxBeamList): beamObj = self.xmlToBeam(mxBeam) beamObj.number = i + 1 beamsOut.beamsList.append(beamObj) if inputM21 is None: return beamsOut
[docs] def xmlNotehead(self, n, mxNotehead): # noinspection PyShadowingNames ''' Set notehead information from the mxNotehead object >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> n = note.Note() >>> nh = EL('<notehead color="#FF0000" filled="no" parentheses="yes">' ... 'diamond</notehead>') >>> MP.xmlNotehead(n, nh) >>> n.notehead 'diamond' >>> n.noteheadFill False >>> n.noteheadParenthesis True >>> n.style.color '#FF0000' ''' if mxNotehead.text not in ('', None): n.notehead = mxNotehead.text nhf = mxNotehead.get('filled') if nhf is not None: n.noteheadFill = xmlObjects.yesNoToBoolean(nhf) if mxNotehead.get('color') is not None: n.style.color = mxNotehead.get('color') # TODO font nhp = mxNotehead.get('parentheses') if nhp is not None: n.noteheadParenthesis = xmlObjects.yesNoToBoolean(nhp)
[docs] def xmlToPitch(self, mxNote, inputM21=None): ''' Given a MusicXML Note object, set this Pitch object to its values. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> b = EL('<note><pitch><step>E</step><alter>-1</alter>' ... '<octave>3</octave></pitch></note>') >>> a = MP.xmlToPitch(b) >>> print(a) E-3 Conflicting alter and accidental -- alter is still stored, but name is : >>> b = EL('<note><pitch><step>E</step><alter>-1</alter><octave>3</octave></pitch>' ... '<accidental>sharp</accidental></note>') >>> a = MP.xmlToPitch(b) >>> print(a) E#3 >>> a.fullName 'E-sharp in octave 3' >>> a.accidental.alter -1.0 >>> a.accidental.name 'sharp' >>> a.accidental.modifier '#' ''' seta = _setAttributeFromTagText if inputM21 is None: p = pitch.Pitch() else: p = inputM21 if mxNote.tag == 'pitch': mxPitch = mxNote else: mxPitch = mxNote.find('pitch') if mxPitch is None: # whoops!!!! return p seta(p, mxPitch, 'step', 'step') seta(p, mxPitch, 'octave', 'octave', transform=int) mxAlter = mxPitch.find('alter') accAlter = None if alterText := strippedText(mxAlter): accAlter = float(alterText) mxAccidental = mxNote.find('accidental') mxAccidentalName = None if accidentalText := strippedText(mxAccidental): # MuseScore 0.9 made empty accidental tags for notes that did not # need an accidental display. mxAccidentalName = accidentalText if mxAccidentalName is not None: try: accObj = self.xmlToAccidental(mxAccidental) p.accidental = accObj p.accidental.displayStatus = True if accAlter is not None and accAlter != accObj.alter: accObj.setAttributeIndependently('alter', float(accAlter)) except pitch.AccidentalException: # MuseScore 0.9.6 generates Accidentals with empty objects pass elif accAlter is not None: try: p.accidental = pitch.Accidental(accAlter) except pitch.AccidentalException: raise MusicXMLImportException( f'incorrect accidental {accAlter} for pitch {p}') # TODO: check supports for accidentals! p.accidental.displayStatus = False return p
[docs] def xmlToUnpitched( self, mxUnpitched: ET.Element, inputM21: note.Unpitched|None = None, ) -> note.Unpitched: ''' Set `displayStep` and `displayOctave` from `mxUnpitched`. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 10080 >>> mxNote = EL('<note><duration>7560</duration><type>eighth</type></note>') >>> unpitched = EL('<unpitched>' ... '<display-step>E</display-step>' ... '<display-octave>5</display-octave>' ... '</unpitched>') >>> mxNote.append(unpitched) >>> n = MP.xmlToSimpleNote(mxNote) >>> n.displayStep 'E' >>> n.displayOctave 5 >>> n.displayPitch().midi 76 ''' if inputM21 is None: unp = note.Unpitched() else: unp = inputM21 mxDisplayStep = mxUnpitched.find('display-step') mxDisplayOctave = mxUnpitched.find('display-octave') if displayStepText := strippedText(mxDisplayStep): unp.displayStep = displayStepText # type: ignore # str vs literal CDEFGAB if displayOctaveText := strippedText(mxDisplayOctave): unp.displayOctave = int(displayOctaveText) return unp
[docs] def xmlToAccidental( self, mxAccidental: ET.Element, inputM21: pitch.Accidental|None = None, ) -> pitch.Accidental: ''' >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> a = EL('<accidental parentheses="yes">sharp</accidental>') >>> b = MP.xmlToAccidental(a) >>> b.name 'sharp' >>> b.alter 1.0 >>> b.displayStyle 'parentheses' >>> a = EL('<accidental>half-flat</accidental>') >>> b = pitch.Accidental() >>> unused = MP.xmlToAccidental(a, b) >>> b.name 'half-flat' >>> b.alter -0.5 >>> a = EL('<accidental bracket="yes">sharp</accidental>') >>> b = MP.xmlToAccidental(a) >>> b.displayStyle 'bracket' >>> a = EL('<accidental bracket="yes" parentheses="yes">sharp</accidental>') >>> b = MP.xmlToAccidental(a) >>> b.displayStyle 'both' ''' if inputM21 is None: acc = pitch.Accidental() else: acc = inputM21 try: mxName = strippedText(mxAccidental).lower() except AttributeError: return acc if mxName in self.mxAccidentalNameToM21: name = self.mxAccidentalNameToM21[mxName] else: name = mxName # need to use set here to get all attributes up to date acc.set(name, allowNonStandardValue=True) self.setPrintStyle(mxAccidental, acc) # level display parentheses = mxAccidental.get('parentheses') bracket = mxAccidental.get('bracket') if parentheses == 'yes' and bracket == 'yes': acc.displayStyle = 'both' elif parentheses == 'yes': acc.displayStyle = 'parentheses' elif bracket == 'yes': acc.displayStyle = 'bracket' # TODO: attr: size # TODO: attr: cautionary self.setEditorial(mxAccidental, acc) return acc
[docs] def xmlToRest(self, mxRest): # noinspection PyShadowingNames ''' Takes a <note> tag that has been shown to have a <rest> tag in it and return a rest. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 10 >>> mxr = EL('<note><rest/><duration>5</duration><type>eighth</type></note>') >>> r = MP.xmlToRest(mxr) >>> r <music21.note.Rest eighth> >>> r.duration.quarterLength 0.5 Rests that have display-step and display-octave get converted to rests with `stepShift` set. For instance, this rest appears where G4 normally is: >>> mxr_positioned = EL( ... '<note><rest>' ... '<display-step>G</display-step>' ... '<display-octave>4</display-octave>' ... '</rest>' ... '<duration>5</duration><type>eighth</type></note>' ... ) >>> r = MP.xmlToRest(mxr_positioned) >>> r <music21.note.Rest eighth> A rest normally lies at B4 in treble clef, so G4 is shifted down two steps. >>> r.stepShift -2 Note that clef context matters. If the last clef for the staff were BassClef this would be a very high clef. (Rests without a `<staff>` set read the clef from staff number 1, or treble clef if no pitch clef has been assigned). If the last clef for staff 1 were bass clef, this would be a very high rest. >>> MP.lastClefs[1] = clef.BassClef() >>> MP.xmlToRest(mxr_positioned).stepShift 10 Test full measure rest defined with measure="yes" and a duration indicating four quarter notes: >>> mxr = EL('<note><rest measure="yes"/><duration>40</duration></note>') >>> r = MP.xmlToRest(mxr) >>> MP.fullMeasureRest True Note that here set `r`'s `.fullMeasure` to True or always because it has no type. >>> r.fullMeasure True Same goes for rests which define type of whole (or breve), regardless of duration: >>> mxr = EL('<note><rest measure="yes"/><duration>40</duration><type>whole</type></note>') >>> r = MP.xmlToRest(mxr) >>> MP.fullMeasureRest True >>> r.fullMeasure True But a rest that defines `measure="yes"` but has a type other than whole or breve will set MeasureParser to fullMeasureRest but not set fullMeasure = True on the music21 Rest object itself because pickup measures often use measure="yes" in Finale, but display as quarter rests, etc. See https://github.com/w3c-cg/musicxml/issues/478 >>> mxr = EL('<note><rest measure="yes"/><duration>10</duration>' ... '<type>quarter</type></note>') >>> r = MP.xmlToRest(mxr) >>> MP.fullMeasureRest True >>> r.fullMeasure 'auto' OMIT_FROM_DOCS Check that the `<staff>` element for rests is properly read. >>> MP2 = musicxml.xmlToM21.MeasureParser() >>> MP2.divisions = 10 >>> MP2.lastClefs[1] = clef.BassClef() >>> MP2.lastClefs[2] = clef.AltoClef() >>> staff2 = EL('<staff>2</staff>') >>> mxr_positioned.append(staff2) >>> r = MP2.xmlToRest(mxr_positioned) From treble is -2, but +6 for treble to alto clef conversion = 4 >>> r.stepShift 4 ''' d = self.xmlToDuration(mxRest) r = note.Rest(duration=d) mxRestTag = mxRest.find('rest') if mxRestTag is None: raise MusicXMLImportException('do not call xmlToRest on a <note> unless it ' + 'contains a rest tag.') isFullMeasure = mxRestTag.get('measure') if isFullMeasure == 'yes': # fullMeasureRest is now just a counting/debug tool. if not (rType := strippedText(mxRest.find('type'))) or rType in ('whole', 'breve'): # force full measure rest self.fullMeasureRest = True r.fullMeasure = True # this attribute is not 100% necessary to get a multi-measure rest spanner if self.parent: # will apply if active self.parent.applyMultiMeasureRest(r) ds = mxRestTag.find('display-step') if ds_text := strippedText(ds): do = mxRestTag.find('display-octave') if do_text := strippedText(do): ds_text += do_text.strip() tempP = pitch.Pitch(ds_text) # musicxml records rest display as a pitch in the current # clef. Music21 records it as an offset (in steps) from the # middle line. So we need clef context. restStaff = self.getStaffNumber(mxRest) or 1 # 1 = default staff for getting clef try: cc = self.lastClefs[restStaff] if cc is None or not isinstance(cc, clef.PitchClef): ccMidLine = clef.TREBLE_MID_LINE_DNN # assume TrebleClef else: ccMidLine = cc.lowestLine + 4 except KeyError: # assume treble clef ccMidLine = clef.TREBLE_MID_LINE_DNN r.stepShift = tempP.diatonicNoteNum - ccMidLine return self.xmlNoteToGeneralNoteHelper(r, mxRest)
[docs] def xmlNoteToGeneralNoteHelper(self, n, mxNote, freeSpanners=True): # noinspection PyShadowingNames ''' Combined function to work on all <note> tags, where n can be a Note or Rest. >>> from xml.etree.ElementTree import fromstring as EL >>> n = note.Note() >>> mxNote = EL('<note color="silver"></note>') >>> MP = musicxml.xmlToM21.MeasureParser() >>> n = MP.xmlNoteToGeneralNoteHelper(n, mxNote) >>> n.style.color 'silver' ''' spannerBundle = self.spannerBundle if freeSpanners is True: spannerBundle.freePendingSpannedElementAssignment(n) # ATTRIBUTES, including color and position self.setPrintStyle(mxNote, n) # print object == 'no' and grace notes may have a type but not # a duration. they may be filtered out at the level of Stream # processing self.setPrintObject(mxNote, n) # attr dynamics -- MIDI Note On velocity with 90 = 100, but unbounded on the top dynamPercentage = mxNote.get('dynamics') if dynamPercentage is not None and not n.isRest: dynamFloat = float(dynamPercentage) * (90 / 12700) n.volume.velocityScalar = dynamFloat # TODO: attr attack -- alter starting time of the note # TODO: attr end-dynamics -- MIDI Note Off velocity # TODO: attr release -- alter the release time of the note # TODO: attr-group time-only if mxNote.get('pizzicato') == 'yes': n.articulations.append(articulations.Pizzicato()) # SUB-ELEMENTS mxGrace = mxNote.find('grace') isGrace = False if mxGrace is not None: isGrace = True graceType = mxNote.find('type') if graceType is None: # this technically puts it in the # wrong place in the sequence, but it won't matter ET.SubElement(mxNote, '<type>eighth</type>') self.xmlToDuration(mxNote, n.duration) # type styles mxType = mxNote.find('type') if mxType is not None: self.setStyleAttributes(mxType, n, 'size', 'noteSize') if mxNote.find('tie') is not None: n.tie = self.xmlToTie(mxNote) # provide all because of tied # TODO: find tied if tie is not found (cue notes) # translate if necessary, otherwise leaves unchanged if isGrace is True: n = self.xmlGraceToGrace(mxGrace, n) # this must be before notations, to get the slurs, etc. # attached to the grace notes mxNotations = mxNote.findall('notations') for mxN in mxNotations: self.xmlNotations(mxN, n) self.setEditorial(mxNote, n) return n
# TODO: attr: font # TODO: attr: printout # TODO: attr: dynamics # TODO: attr: end-dynamics # TODO: attr: attack # TODO: attr: release # TODO: attr: time-only # TODO: attr: pizzicato # TODO: play # TODO: instrument (musicxml4: multiple instrument objects) # TODO: MusicXML 4.0: listen and sub-elements assess/wait/other-listen # because assess and other-listen have different details based on # the presence of <cue> element, the subroutine will need to pass in # the mxNote object. # (this also applies to Chord notes)
[docs] def xmlToDuration(self, mxNote, inputM21=None): # noinspection PyShadowingNames ''' Translate a `MusicXML` <note> object's <duration>, <type>, <dot>, tuplets, etc. to a music21 :class:`~music21.duration.Duration` object. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 10080 >>> mxNote = EL('<note><pitch><step>D</step>' ... '<alter>-1</alter><octave>6</octave></pitch>' ... '<duration>7560</duration>' ... '<type>eighth</type><dot/></note>') >>> c = duration.Duration() >>> MP.xmlToDuration(mxNote, c) >>> c <music21.duration.Duration 0.75> >>> c.quarterLength 0.75 >>> c.type 'eighth' >>> c.dots 1 If the `<duration>` doesn't match the `<type>` and `<dots>`, an unlinked duration is created so that `.quarterLength` agrees with `<duration>` but the notated types can still be represented. Create a second dot on `mxNote` and parse again, observing the identical `quarterLength`: >>> from xml.etree.ElementTree import SubElement >>> unused = SubElement(mxNote, 'dot') >>> c2 = MP.xmlToDuration(mxNote) >>> c2 <music21.duration.Duration unlinked type:eighth quarterLength:0.75> >>> c2.quarterLength 0.75 >>> c2.type 'eighth' >>> c2.dots 2 Grace note durations will be converted later to GraceDurations: >>> mxDuration = mxNote.find('duration') >>> mxNote.remove(mxDuration) >>> mxGrace = SubElement(mxNote, 'grace') >>> MP.xmlToDuration(mxNote, inputM21=c2) >>> c2 <music21.duration.Duration unlinked type:eighth quarterLength:0.0> >>> gn1 = note.Note(duration=c2) >>> gn2 = MP.xmlGraceToGrace(mxGrace, gn1) >>> gn2.duration <music21.duration.GraceDuration unlinked type:eighth quarterLength:0.0> ''' numDots = 0 tuplets = () if inputM21 is None: d = None else: d = inputM21 divisions = self.divisions mxDuration = mxNote.find('duration') if mxDuration is not None: noteDivisions = float(mxDuration.text.strip()) qLen = opFrac(noteDivisions / divisions) else: qLen = 0.0 mxType = mxNote.find('type') if typeStr := strippedText(mxType): durationType = musicXMLTypeToType(typeStr) forceRaw = False # TODO: dot as print object with print-style and placement. numDots = len(mxNote.findall('dot')) # divide mxNote duration count by divisions to get qL # mxNotations = mxNote.get('notationsObj') mxTimeModification = mxNote.find('time-modification') if mxTimeModification is not None: tuplets = self.xmlToTuplets(mxNote) # get all necessary config from mxNote else: tuplets = () else: # some rests do not define type, and only define duration durationType = None # no type to get, must use raw forceRaw = True # TODO: empty-placement # two ways to create durations, raw (from qLen) and cooked (from type, time-mod, dots) if d is not None: # N.B. music21's parser executes this branch just for grace note corrections durRaw = duration.Duration(quarterLength=qLen) # raw just uses qLen d.components = durRaw.components d.tuplets = durRaw.tuplets else: # N.B. music21's parser executes this branch most of the time d = duration.Duration(quarterLength=qLen) # can't do this unless we have a type, so if not forceRaw if not forceRaw: # a cooked version builds up from pieces dt = duration.durationTupleFromTypeDots(durationType, numDots) if (dt.quarterLength == qLen) and not tuplets: # raw == cooked, so we're done # but this comparison gives false positives if tuplets are involved # don't bother with approximate (is-close); merely trying to short-circuit return d if inputM21 is None else None if d is not None: d.clear() d.tuplets = () d.addDurationTuple(dt) else: d = duration.Duration(durationTuple=dt) for tup in tuplets: d.appendTuplet(tup) # Second check against qLen (raw), now with tuplets # if not almost equal, create unlinked Duration and set raw qLen if not isclose(d.quarterLength, qLen, abs_tol=1e-7): d.linked = False d.quarterLength = qLen if inputM21 is None: return d
[docs] def xmlGraceToGrace(self, mxGrace, noteOrChord): ''' Given a completely formed, non-grace Note or Chord that should become one create and return a m21 grace version of the same. ''' post = noteOrChord.getGrace() if mxGrace.get('slash') in ('yes', None): post.duration.slash = True else: post.duration.slash = False try: post.duration.stealTimePrevious = int(mxGrace.get('steal-time-previous')) / 100 except TypeError: pass try: post.duration.stealTimeFollowing = int(mxGrace.get('steal-time-following')) / 100 except TypeError: pass # TODO: make-time -- maybe; or this is something totally different. return post
[docs] def xmlNotations(self, mxNotations: ET.Element, n: note.GeneralNote): # noinspection PyShadowingNames ''' >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxNotations = EL('<notations>' ... '<fermata type="upright">angled</fermata>' ... '</notations>') >>> n = note.Note() >>> MP.xmlNotations(mxNotations, n) >>> n.expressions [<music21.expressions.Fermata>] >>> n.expressions[0].type 'upright' >>> n.expressions[0].shape 'angled' ''' # attr: print-object -- applies to all printObjectValue = mxNotations.get('print-object') if printObjectValue == 'no': hideObject = True else: hideObject = False def optionalHideObject(obj): if not hideObject: return obj.style.hideObjectOnPrint = True # tied is handled with tie # tuplet is handled with time-modification. # TODO: dynamics # TODO: accidental-mark # TODO: other-notation def flatten(mx, name): findall = mx.findall(name) return common.flattenList(findall) for mxObj in flatten(mxNotations, 'technical'): technicalObj = self.xmlTechnicalToArticulation(mxObj) optionalHideObject(technicalObj) if technicalObj is not None: n.articulations.append(technicalObj) for mxObj in flatten(mxNotations, 'articulations'): articulationObj = self.xmlToArticulation(mxObj) optionalHideObject(articulationObj) if articulationObj is not None: n.articulations.append(articulationObj) # get any fermatas, store on expressions for mxObj in mxNotations.findall('fermata'): fermata = expressions.Fermata() optionalHideObject(fermata) self.setEditorial(mxNotations, fermata) fermataType = mxObj.get('type') if fermataType is not None: fermata.type = fermataType if notationText := strippedText(mxObj): fermata.shape = notationText n.expressions.append(fermata) # get any arpeggios, store in expressions. for tagSearch in ('arpeggiate', 'non-arpeggiate'): # TODO: musicxml 4: arpeggiate 'unbroken' attribute for mxObj in mxNotations.findall(tagSearch): arpeggioType: str = 'normal' if tagSearch == 'non-arpeggiate': arpeggioType = 'non-arpeggio' else: arpeggioType = mxObj.get('direction') or 'normal' idFound: str|None = mxObj.get('number') if idFound is None: arpeggio = expressions.ArpeggioMark(arpeggioType) n.expressions.append(arpeggio) else: sb = self.spannerBundle.getByClassIdLocalComplete( expressions.ArpeggioMarkSpanner, idFound, False) if sb: # if we already have a spanner matching arpeggioSpanner = t.cast(expressions.ArpeggioMarkSpanner, sb[0]) else: arpeggioSpanner = expressions.ArpeggioMarkSpanner(arpeggioType=arpeggioType) arpeggioSpanner.idLocal = idFound 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 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': # TODO: musicxml 4: attr: smufl trillExtObj = self.xmlOneSpanner(mxObj, n, expressions.TrillExtension) optionalHideObject(trillExtObj) self.setEditorial(mxNotations, trillExtObj) elif mxObj.tag == 'tremolo': trem = self.xmlToTremolo(mxObj, n) optionalHideObject(trem) self.setEditorial(mxNotations, trem) # create spanners for rest self.xmlNotationsToSpanners(mxNotations, n)
[docs] def xmlTechnicalToArticulation(self, mxObj): ''' Convert an mxArticulationMark to a music21.articulations.Articulation object or one of its subclasses. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxTech = EL('<down-bow placement="below"/>') >>> a = MP.xmlTechnicalToArticulation(mxTech) >>> a <music21.articulations.DownBow> >>> a.placement 'below' Fingering might have substitution or alternate >>> mxTech = EL('<fingering substitution="yes">5</fingering>') >>> f = MP.xmlTechnicalToArticulation(mxTech) >>> f <music21.articulations.Fingering 5> >>> f.substitution True >>> f.alternate False FingerNumbers get converted to ints if possible >>> f.fingerNumber 5 >>> mxTech = EL('<fingering alternate="yes">4-3</fingering>') >>> f = MP.xmlTechnicalToArticulation(mxTech) >>> <music21.articulations.Fingering 4-3> >>> f.alternate True >>> f.fingerNumber '4-3' ''' tag = mxObj.tag if tag in xmlObjects.TECHNICAL_MARKS: tech = xmlObjects.TECHNICAL_MARKS[tag]() synchronizeIds(mxObj, tech) if tag == 'fingering': self.handleFingering(tech, mxObj) if tag in ('handbell', 'other-technical') and strippedText(mxObj): # The handbell element represents notation for various # techniques used in handbell and handchime music. Valid # values are belltree [v3.1], damp, echo, gyro, hand martellato, # mallet lift, mallet table, martellato, martellato lift, # muted martellato, pluck lift, and swing. tech.displayText = strippedText(mxObj) if tag in ('fret', 'string'): try: tech.number = int(mxObj.text) except (ValueError, TypeError) as unused_err: pass if tag == 'harmonic': self.setHarmonic(mxObj, tech) if tag in ('heel', 'toe'): if mxObj.get('substitution') is not None: tech.substitution = xmlObjects.yesNoToBoolean(mxObj.get('substitution')) if tag == 'bend': self.setBend(mxObj, tech) # TODO: <bend> attr: accelerate, beats, first-beat, last-beat, shape (4.0) # TODO: <bent> sub-elements: bend-alter, pre-bend, with-bar, release # TODO: musicxml 4: release sub-element as offset attribute self.setPlacement(mxObj, tech) return tech else: environLocal.printDebug(f'Cannot translate {tag} in {mxObj}.') return None
[docs] def setBend(self, mxh, bend): ''' Gets the bend amplitude from the bend-alter tag, then optional pre-bend and with-bar tags are processed, as well as release which is converted from divisions to music21 time. Called from xmlTechnicalToArticulation >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxTech = EL('<bend><bend-alter>2</bend-alter></bend>') >>> a = MP.xmlTechnicalToArticulation(mxTech) >>> a <music21.articulations.FretBend 0> >>> a.bendAlter.semitones 2 >>> a.release >>> a.withBar >>> a.preBend False >>> mxTech = EL('<bend><bend-alter>-2</bend-alter><pre-bend/></bend>') >>> a = MP.xmlTechnicalToArticulation(mxTech) >>> a.bendAlter.semitones -2 >>> a.preBend True >>> mxTech = EL('<bend><bend-alter>-2</bend-alter><release offset="1"/></bend>') >>> a = MP.xmlTechnicalToArticulation(mxTech) >>> a.bendAlter.semitones -2 >>> a.release Fraction(1, 10080) >>> mxTech = EL('<bend><bend-alter>-1</bend-alter><with-bar>dip</with-bar></bend>') >>> a = MP.xmlTechnicalToArticulation(mxTech) >>> a.bendAlter.semitones -1 >>> a.withBar 'dip' ''' alter = mxh.find('bend-alter') if alter is not None: if alter.text is not None: bend.bendAlter = interval.Interval(float(alter.text)) if mxh.find('pre-bend') is not None: bend.preBend = True if mxh.find('with-bar') is not None: bend.withBar = mxh.find('with-bar').text if mxh.find('release') is not None: try: divisions = float(mxh.find('release').get('offset')) bend.release = opFrac(divisions / self.divisions) except (ValueError, TypeError) as unused_err: bend.release = 0.0
[docs] @staticmethod def setHarmonic(mxh, harm): ''' From the artificial or natural tag (or no tag) and zero or one of base-pitch, sounding-pitch, touching-pitch, sets .harmonicType and .pitchType on an articulations.Harmonic object Called from xmlTechnicalToArticulation >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxTech = EL('<harmonic><artificial/><sounding-pitch/></harmonic>') >>> a = MP.xmlTechnicalToArticulation(mxTech) >>> a <music21.articulations.StringHarmonic> >>> a.harmonicType 'artificial' >>> a.pitchType 'sounding' ''' if mxh.find('artificial') is not None: harm.harmonicType = 'artificial' elif mxh.find('natural') is not None: harm.harmonicType = 'natural' if mxh.find('base-pitch') is not None: harm.pitchType = 'base' elif mxh.find('sounding-pitch') is not None: harm.pitchType = 'sounding' elif mxh.find('touching-pitch') is not None: harm.pitchType = 'touching'
[docs] def handleFingering(self, tech, mxObj): ''' A few specialized functions for dealing with fingering objects ''' tech.fingerNumber = mxObj.text try: tech.fingerNumber = int(tech.fingerNumber) except (ValueError, TypeError) as unused_err: pass if mxObj.get('substitution') is not None: tech.substitution = xmlObjects.yesNoToBoolean(mxObj.get('substitution')) if mxObj.get('alternate') is not None: tech.alternate = xmlObjects.yesNoToBoolean(mxObj.get('alternate'))
[docs] def xmlToArticulation(self, mxObj): ''' Return an articulation from an mxObj, setting placement >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxArt = EL('<spiccato placement="above"/>') >>> a = MP.xmlToArticulation(mxArt) >>> a <music21.articulations.Spiccato> >>> a.placement 'above' >>> mxArt = EL('<doit dash-length="2" default-x="5" default-y="2" ' ... 'line-shape="curved" line-type="dashed" space-length="1" />') >>> a = MP.xmlToArticulation(mxArt) >>> a <music21.articulations.Doit> >>> a.placement is None True >>> a.style.dashLength 2 >>> a.style.absoluteX 5 >>> a.style.lineShape 'curved' ''' tag = mxObj.tag if tag in xmlObjects.ARTICULATION_MARKS: articulationObj = xmlObjects.ARTICULATION_MARKS[tag]() synchronizeIds(mxObj, articulationObj) self.setPrintStyle(mxObj, articulationObj) self.setPlacement(mxObj, articulationObj) # particular articulations have extra information. if tag == 'strong-accent': pointDirection = mxObj.get('type') if pointDirection is not None: articulationObj.pointDirection = pointDirection elif tag in ('doit', 'falloff', 'plop', 'scoop'): self.setLineStyle(mxObj, articulationObj) elif tag == 'breath-mark' and (breathText := strippedText(mxObj)): articulationObj.symbol = breathText elif tag == 'other-articulation' and (otherText := strippedText(mxObj)): articulationObj.displayText = otherText return articulationObj else: environLocal.printDebug(f'Cannot translate {tag} in {mxObj}.') return None
[docs] 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, 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 >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxOrn = EL('<inverted-turn placement="above" font-size="24"/>') >>> a = MP.xmlOrnamentToExpression(mxOrn) >>> a <music21.expressions.InvertedTurn> >>> a.placement 'above' >>> a.style.fontSize 24 If it can't be converted, return None >>> mxOrn = EL('<crazy-slide placement="above"/>') >>> a = MP.xmlOrnamentToExpression(mxOrn) >>> a is None True 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) elif tag in ('turn', 'inverted-turn'): orn = xmlObjects.ORNAMENT_MARKS[tag](delay=OrnamentDelay.NO_DELAY) else: orn = xmlObjects.ORNAMENT_MARKS[tag]() except KeyError: # should already be checked return None self.setPrintStyle(mxObj, orn) # trill-sound? self.setPlacement(mxObj, orn) return orn
[docs] def xmlDirectionTypeToSpanners( self, mxObj: ET.Element, staffKey: int|None = None, totalOffset: OffsetQL|None = None ): # noinspection PyShadowingNames ''' 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() >>> n1 = note.Note('D4') >>> MP.nLast = n1 >>> len(MP.spannerBundle) 0 >>> mxDirectionType = EL('<wedge type="crescendo" number="2"/>') >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType) >>> retList [<music21.dynamics.Crescendo>] >>> len(MP.spannerBundle) 1 >>> sp = MP.spannerBundle[0] >>> sp <music21.dynamics.Crescendo> >>> mxDirectionType2 = EL('<wedge type="stop" number="2"/>') >>> retList = MP.xmlDirectionTypeToSpanners(mxDirectionType2) retList is empty because nothing new has been added. >>> retList [] >>> len(MP.spannerBundle) 1 >>> 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: type[dynamics.DynamicWedge] = dynamics.Crescendo elif mType == 'diminuendo': spClass = dynamics.Diminuendo elif mType == 'stop': spClass = dynamics.DynamicWedge # parent of Cresc/Dim else: raise MusicXMLImportException(f'Unknown type, {mType}.') if mType != 'stop': sp = self.xmlOneSpanner(mxObj, None, spClass, allowDuplicateIds=True) returnList.append(sp) self.spannerBundle.setPendingSpannedElementAssignment(sp, 'GeneralNote') else: idFound = mxObj.get('number') spb = self.spannerBundle.getByClassIdLocalComplete( 'DynamicWedge', idFound, False) # get first try: sp = spb[0] except IndexError: raise MusicXMLImportException('Error in getting DynamicWedges') sp.completeStatus = True # will only have a target if this follows the note if targetLast is not None: sp.addSpannedElements(targetLast) if mxObj.tag in ('bracket', 'dashes'): mxType = mxObj.get('type') idFound = mxObj.get('number') if mxType == 'start': sp = spanner.Line() sp.idLocal = idFound if mxObj.tag == 'dashes': sp.startTick = 'none' sp.lineType = 'dashed' else: height = mxObj.get('end-length') if height is not None: sp.startHeight = float(height) sp.startTick = mxObj.get('line-end') sp.lineType = mxObj.get('line-type') # redundant with setLineStyle() self.spannerBundle.append(sp) returnList.append(sp) # define this spanner as needing component assignment from # the next general note self.spannerBundle.setPendingSpannedElementAssignment(sp, 'GeneralNote') elif mxType == 'stop': # need to retrieve an existing spanner # try to get base class of both Crescendo and Decrescendo try: sp = self.spannerBundle.getByClassIdLocalComplete( 'Line', idFound, False)[0] # get first except IndexError: warnings.warn( 'Line <' + mxObj.tag + '> stop without start', MusicXMLWarning, stacklevel=2, ) return [] sp.completeStatus = True if mxObj.tag == 'dashes': sp.endTick = 'none' sp.lineType = 'dashed' else: sp.endTick = mxObj.get('line-end') height = mxObj.get('end-length') if height is not None: sp.endHeight = float(height) sp.lineType = mxObj.get('line-type') # will only have a target if this follows the note if targetLast is not None: sp.addSpannedElements(targetLast) else: raise MusicXMLImportException(f'unidentified mxType of mxBracket: {mxType}') if mxObj.tag == 'octave-shift': mxType = mxObj.get('type') mxSize = mxObj.get('size') idFound = mxObj.get('number') if mxType in ('up', 'down'): sp = spanner.Ottava() # MusicXML's pitches are encoded at sounding octaves # Thus, set non-transposing sp.transposing = False if mxType == 'up': # musicxml and m21 have reversed types m21Type = 'down' # Provide default. If encoded, will be overwritten in setPlacement() sp.placement = 'below' else: m21Type = 'up' sp.placement = 'above' sp.idLocal = idFound sp.type = (mxSize or 8, m21Type) self.spannerBundle.append(sp) returnList.append(sp) self.spannerBundle.setPendingSpannedElementAssignment(sp, 'GeneralNote') elif mxType in ('continue', 'stop'): spb = self.spannerBundle.getByClassIdLocalComplete( 'Ottava', idFound, False # get first ) try: sp = spb[0] except IndexError: raise MusicXMLImportException('Error in getting Ottava') if mxType == 'continue': self.spannerBundle.setPendingSpannedElementAssignment(sp, 'GeneralNote') else: # if mxType == 'stop': sp.completeStatus = True if targetLast is not None: sp.addSpannedElements(targetLast) 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
[docs] def xmlNotationsToSpanners(self, mxNotations, n): # TODO: mxNotations attr: print-object for mxObj in mxNotations.findall('slur'): slur = self.xmlOneSpanner(mxObj, n, spanner.Slur) self.setLineStyle(mxObj, slur) self.setPosition(mxObj, slur) self.setPlacement(mxObj, slur) # TODO: attr orientation self.setStyleAttributes(mxObj, slur, ('bezier-offset', 'bezier-offset2', 'bezier-x', 'bezier-y', 'bezier-x2', 'bezier-y2') ) self.setColor(mxObj, slur) for mxObj in mxNotations.findall('technical/hammer-on'): self.xmlOneSpanner(mxObj, n, articulations.HammerOn) for mxObj in mxNotations.findall('technical/pull-off'): self.xmlOneSpanner(mxObj, n, articulations.PullOff) for tagSearch in ('glissando', 'slide'): for mxObj in mxNotations.findall(tagSearch): gliss = self.xmlOneSpanner(mxObj, n, spanner.Glissando) if tagSearch == 'slide': gliss.slideType = 'continuous' self.setLineStyle(mxObj, gliss) if not mxObj.get('line-type') and tagSearch == 'slide': gliss.lineType = 'solid' gliss.style.lineType = 'solid' # TODO: attr bend-sound on <slide> only self.setPrintStyle(mxObj, gliss) synchronizeIds(mxObj, gliss)
[docs] def xmlToTremolo(self, mxTremolo, n): ''' Converts an mxTremolo to either an expression to be added to n.expressions or to a spanner, returning either. ''' # tremolo is tricky -- can be either an # expression or spanner tremoloType = mxTremolo.get('type') isSingle = True if tremoloType in ('start', 'stop'): isSingle = False try: numMarks = int(mxTremolo.text.strip()) except (ValueError, AttributeError): # warnings.warn('could not convert ', dir(mxObj), MusicXMLWarning) numMarks = 3 if isSingle is True: ts = expressions.Tremolo() ts.numberOfMarks = numMarks n.expressions.append(ts) return ts else: tremSpan = self.xmlOneSpanner(mxTremolo, n, expressions.TremoloSpanner) tremSpan.numberOfMarks = numMarks return tremSpan
[docs] def xmlOneSpanner(self, mxObj, target, spannerClass, *, allowDuplicateIds=False): ''' Some spanner types do not have an id necessarily, we allow duplicates of them if allowDuplicateIds is True. Wedges are one. Returns the new spanner created. ''' idFound = mxObj.get('number') # returns a new spanner bundle with just the result of the search sb = self.spannerBundle.getByClassIdLocalComplete(spannerClass, idFound, False) if sb and allowDuplicateIds is False: # if we already have a spanner matching # environLocal.printDebug(['found a match in SpannerBundle']) su = sb[0] # get the first else: # create a new spanner su = spannerClass() su.idLocal = idFound placement = mxObj.get('placement') if placement is not None: # not all spanners have placement su.placement = placement self.spannerBundle.append(su) # add a reference of this note to this spanner if target is not None: su.addSpannedElements(target) # environLocal.printDebug(['adding n', target, id(target), 'su.getSpannedElements', # su.getSpannedElements(), su.getSpannedElementIds()]) if mxObj.get('type') == 'stop': su.completeStatus = True # only add after complete elif mxObj.get('type') == 'start': synchronizeIds(mxObj, su) return su
[docs] def xmlToTie(self, mxNote): # noinspection PyShadowingNames ''' Translate a MusicXML <note> with <tie> SubElements :class:`~music21.tie.Tie` object >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() Create the incomplete part of a Note. >>> mxNote = EL('<note><tie type="start" />' ... '<notations>' ... '<tied line-type="dotted" placement="below" type="start" />' ... '</notations></note>') >>> m21Tie = MP.xmlToTie(mxNote) >>> m21Tie.type 'start' >>> m21Tie.style 'dotted' >>> m21Tie.placement 'below' Same thing but with orientation instead of placement, which both get mapped to placement in Tie objects >>> mxNote = EL('<note><tie type="start" />' ... '<notations>' ... '<tied line-type="dotted" orientation="over" type="start" />' ... '</notations></note>') >>> tieObj = MP.xmlToTie(mxNote) >>> tieObj.placement 'above' ''' tieObj = tie.Tie() allTies = mxNote.findall('tie') if not allTies: return None typesFound = [] for mxTie in allTies: foundType = mxTie.get('type') if foundType is not None: typesFound.append(foundType) else: environLocal.printDebug('found tie element without required type') if len(typesFound) == 1: tieObj.type = typesFound[0] elif 'stop' in typesFound and 'start' in typesFound: tieObj.type = 'continue' else: environLocal.printDebug( ['found unexpected arrangement of multiple tie types when ' + 'importing from musicxml:', typesFound]) # TODO: get everything else from <tied> # besides line-style, placement, and orientation. such as bezier # blocking on redoing tie.Tie to not use "style" mxNotations = mxNote.find('notations') if mxNotations is not None: mxTiedList = mxNotations.findall('tied') if mxTiedList: firstTied = mxTiedList[0] synchronizeIds(firstTied, tieObj) tieStyle = firstTied.get('line-type') if tieStyle is not None and tieStyle != 'wavy': # do not support wavy tieObj.style = tieStyle placement = firstTied.get('placement') if placement is not None: tieObj.placement = placement else: orientation = mxTiedList[0].get('orientation') if orientation == 'over': tieObj.placement = 'above' elif orientation == 'under': tieObj.placement = 'below' return tieObj
[docs] def xmlToTuplets(self, mxNote: ET.Element) -> list[duration.Tuplet]: # noinspection PyShadowingNames ''' Given an mxNote, based on mxTimeModification and mxTuplet objects, return a list of Tuplet objects >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxNote = EL('<note><type>16th</type>' ... '<time-modification><actual-notes>5</actual-notes>' ... '<normal-notes>4</normal-notes></time-modification></note>') >>> tups = MP.xmlToTuplets(mxNote) >>> tups [<music21.duration.Tuplet 5/4/16th>] >>> mxNote = EL('<note><type>eighth</type>' ... '<time-modification><actual-notes>5</actual-notes>' ... '<normal-notes>3</normal-notes>' ... '<normal-type>16th</normal-type><normal-dot /><normal-dot />' ... '</time-modification></note>') >>> tup = MP.xmlToTuplets(mxNote) >>> tup [<music21.duration.Tuplet 5/3/16th>] >>> tup[0].durationNormal DurationTuple(type='16th', dots=2, quarterLength=0.4375) ''' tup = duration.Tuplet() mxTimeModification = mxNote.find('time-modification') if mxTimeModification is None: raise MusicXMLImportException('Note without time-modification in xmlToTuplets') # environLocal.printDebug(['got mxTimeModification', mxTimeModification]) # This should only be a backup in case there are no tuplet definitions # in the tuplet tag. seta = _setAttributeFromTagText seta(tup, mxTimeModification, 'actual-notes', 'numberNotesActual', transform=int) seta(tup, mxTimeModification, 'normal-notes', 'numberNotesNormal', transform=int) mxNormalType = mxTimeModification.find('normal-type') musicXMLNormalType: str if normalTypeText := strippedText(mxNormalType): musicXMLNormalType = normalTypeText else: musicXMLNormalType = strippedText(mxNote.find('type')) durationNormalType = musicXMLTypeToType(musicXMLNormalType) numDots = len(mxTimeModification.findall('normal-dot')) tup.setDurationType(durationNormalType, numDots) mxNotations = mxNote.find('notations') if mxNotations is None: self.activeTuplets[0] = tup # environLocal.printDebug(['got mxNotations', mxNotations]) remainingTupletAmountToAccountFor = tup.tupletMultiplier() timeModTup = tup returnTuplets: list[duration.Tuplet|None] = [None] * 8 removeFromActiveTuplets = set() # a set of tuplets to set to stop tupletsToStop = set() if mxNotations is not None: mxTuplets = mxNotations.findall('tuplet') for mxTuplet in mxTuplets: this_tuplet_type = mxTuplet.get('type') # required tupletNumberStr = mxTuplet.get('number') # str '1' to '6' or None # no tuplet number is equal to 1 tupletIndex = int(tupletNumberStr) if tupletNumberStr is not None else 1 if this_tuplet_type == 'stop': if self.activeTuplets[tupletIndex] is not None: activeT = self.activeTuplets[tupletIndex] if activeT in returnTuplets and activeT is not None: activeT.type = 'startStop' removeFromActiveTuplets.add(tupletIndex) tupletsToStop.add(tupletIndex) continue mxTupletActual = mxTuplet.find('tuplet-actual') mxTupletNormal = mxTuplet.find('tuplet-normal') if mxTupletActual is None or mxTupletNormal is None: # in theory either can be absent, but so far I have only seen both present # or both absent tup = copy.deepcopy(timeModTup) else: tup = duration.Tuplet() seta(tup, mxTupletActual, 'tuplet-number', 'numberNotesActual', transform=int) seta(tup, mxTupletNormal, 'tuplet-number', 'numberNotesNormal', transform=int) mxActualType = mxTupletActual.find('tuplet-type') if (mxActualType is not None and (xmlActualType := mxActualType.text) is not None): xmlActualType = xmlActualType.strip() durType = musicXMLTypeToType(xmlActualType) dots = len(mxActualType.findall('tuplet-dot')) tup.durationActual = duration.durationTupleFromTypeDots(durType, dots) mxNormalType = mxTupletNormal.find('tuplet-type') if (mxNormalType is not None and (mxNormalTypeText := mxNormalType.text) is not None): xmlNormalType = mxNormalTypeText.strip() durType = musicXMLTypeToType(xmlNormalType) dots = len(mxNormalType.findall('tuplet-dot')) tup.durationNormal = duration.durationTupleFromTypeDots(durType, dots) # TODO: combine start + stop into startStop. tup.type = t.cast(t.Literal['start', 'stop', 'startStop', False]|None, this_tuplet_type) bracketMaybe = mxTuplet.get('bracket') if bracketMaybe is not None: tup.bracket = xmlObjects.yesNoToBoolean(bracketMaybe) # environLocal.printDebug(['got bracket', self.bracket]) showNumber = mxTuplet.get('show-number') if showNumber is not None and showNumber == 'none': tup.tupletActualShow = None if bracketMaybe is None: tup.bracket = False elif showNumber is not None and showNumber == 'both': tup.tupletNormalShow = 'number' showType = mxTuplet.get('show-type') if showType is not None and showType == 'actual': tup.tupletActualShow = 'both' if tup.tupletActualShow is not None else 'type' elif showNumber is not None and showNumber == 'both': tup.tupletActualShow = 'both' if tup.tupletActualShow is not None else 'type' tup.tupletNormalShow = 'both' if tup.tupletNormalShow is not None else 'type' lineShape = mxTuplet.get('line-shape') if lineShape is not None and lineShape == 'curved': tup.bracket = 'slur' # TODO: default-x, default-y, relative-x, relative-y tup.placement = t.cast(t.Literal['above', 'below'], mxTuplet.get('placement')) returnTuplets[tupletIndex] = tup remainingTupletAmountToAccountFor /= tup.tupletMultiplier() self.activeTuplets[tupletIndex] = tup # find all activeTuplets that haven't been accounted for. for i in range(1, len(self.activeTuplets)): thisActive = self.activeTuplets[i] if thisActive is None: continue if thisActive in returnTuplets: continue thisActiveCopy = copy.deepcopy(thisActive) if i in tupletsToStop: thisActiveCopy.type = 'stop' else: thisActiveCopy.type = None remainingTupletAmountToAccountFor /= thisActiveCopy.tupletMultiplier() returnTuplets[i] = thisActiveCopy # if there is anything left to if remainingTupletAmountToAccountFor != 1: remainderFraction = fractions.Fraction(remainingTupletAmountToAccountFor) remainderTuplet = duration.Tuplet(remainderFraction.denominator, remainderFraction.numerator) remainderTuplet.durationNormal = timeModTup.durationNormal remainderTuplet.durationActual = timeModTup.durationActual returnTuplets[-1] = remainderTuplet # now we can remove stops for future notes. for tupletIndexToRemove in removeFromActiveTuplets: # set to stop before removing self.activeTuplets[tupletIndexToRemove] = None returnTuplets = [tup for tup in returnTuplets if tup is not None] return returnTuplets
[docs] def updateLyricsFromList(self, n, lyricList): # noinspection PyShadowingNames ''' Takes a list of <lyric> elements and update the note's lyrics from that list. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxLyric1 = EL('<lyric><text>Hi</text><elision/><text>There</text></lyric>') >>> mxLyric2 = EL('<lyric><text>Bye</text></lyric>') >>> n = note.Note() >>> MP.updateLyricsFromList(n, [mxLyric1, mxLyric2]) >>> n.lyrics [<music21.note.Lyric number=1 syllabic=composite text='Hi There'>, <music21.note.Lyric number=2 text='Bye'>] ''' currentLyricNumber = 1 for mxLyric in lyricList: lyricObj = self.xmlToLyric(mxLyric) if lyricObj is None: continue if lyricObj.number == 0: lyricObj.number = currentLyricNumber n.lyrics.append(lyricObj) currentLyricNumber += 1
[docs] def xmlToLyric(self, mxLyric, inputM21=None) -> note.Lyric|None: # noinspection PyShadowingNames ''' Translate a MusicXML <lyric> tag to a music21 :class:`~music21.note.Lyric` object or return None if no Lyric object should be created (empty lyric tags, for instance) If inputM21 is a :class:`~music21.note.Lyric` object, then the values of the mxLyric are transferred there and nothing returned. Otherwise, a new `Lyric` object is created and returned. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxLyric = EL('<lyric number="4" color="red">' ... '<syllabic>single</syllabic>' ... '<text>word</text></lyric>') >>> lyricObj = note.Lyric() >>> MP.xmlToLyric(mxLyric, lyricObj) >>> lyricObj <music21.note.Lyric number=4 syllabic=single text='word'> >>> lyricObj.style.color 'red' Non-numeric MusicXML lyric "number"s are converted to identifiers: >>> mxLyric.set('number', 'part2verse1') >>> l2 = MP.xmlToLyric(mxLyric) >>> l2 <music21.note.Lyric number=0 identifier='part2verse1' syllabic=single text='word'> Multiple texts can be created and result in composite lyrics >>> mxBianco = EL('<lyric>' ... '<syllabic>end</syllabic>' ... '<text>co</text>' ... '<elision>_</elision>' ... '<syllabic>single</syllabic>' ... '<text>e</text>' ... '</lyric>') >>> bianco = MP.xmlToLyric(mxBianco) >>> bianco <music21.note.Lyric number=0 syllabic=composite text='co_e'> >>> bianco.components [<music21.note.Lyric number=1 syllabic=end text='co'>, <music21.note.Lyric number=1 syllabic=single text='e'>] ''' if inputM21 is None: ly = note.Lyric() else: ly = inputM21 # TODO: extend # TODO: humming # TODO: laughing # TODO: end-line # TODO: end-paragraph # TODO: footnote # TODO: level # when lyrics get ids, we should get that ID text_elements = mxLyric.findall('text') syllabic_elements = mxLyric.findall('syllabic') elision_elements = mxLyric.findall('elision') if not text_elements: # sometimes there are empty lyrics if inputM21 is None: return ly return None if len(text_elements) == 1: # standard case -- a lyric has a single text. element_text = text_elements[0].text if element_text is not None: ly.text = element_text.strip() try: ly.syllabic = syllabic_elements[0].text.strip() except (ValueError, IndexError): pass # syllabic is optional. else: # composite lyric, like "co" "e" in "Il bianco_e dolce" ly.components = [] for i, mxText in enumerate(text_elements): component = note.Lyric() ly.components.append(component) if mxText.text is not None: component.text = mxText.text.strip() # there are empty text tags try: # Note that this is not entirely accurate. There # could be omitted syllabic tags in the middle of a text # stream, and this will shift them over. But anyone using # multiple text tags with omitted syllabic tags is asking # for difficulties mxSyllabic = syllabic_elements[i] component.syllabic = mxSyllabic.text.strip() if i >= 1: mxElision = elision_elements[i - 1] # only gets to here if no index error. elision_text = mxElision.text # do not strip -- space is important4 if elision_text is None: elision_text = '' component.elisionBefore = elision_text except (IndexError, ValueError, AttributeError): pass # This is new to account for identifiers number = mxLyric.get('number') try: number = int(number) ly.number = number except (TypeError, ValueError): # If musicXML lyric number is not a number, set it to 0. # This tells the caller of mxToLyric that a new number needs # to be given based on the lyric's context amongst other lyrics. ly.number = 0 if number is not None: ly.identifier = number identifier = mxLyric.get('name') if identifier is not None: ly.identifier = identifier self.setStyleAttributes(mxLyric, ly, ('justify', 'placement', 'print-object'), ('justify', 'placement', 'hideObjectOnPrint')) self.setColor(mxLyric, ly) self.setPosition(mxLyric, ly) if inputM21 is None: return ly
[docs] def insertInMeasureOrVoice(self, mxElement, el): ''' Adds an object to a measure or a voice. Needs a note element (obviously) but also mxNote to get the voice. Uses coreInsert and thus leaves insertStream on the inner voice in an unusable state. ''' insertStream = self.stream if not self.useVoices: insertStream.coreInsert(self.offsetMeasureNote, el) return mxVoice = mxElement.find('voice') # MuseScore doesn't write `<voice>` children on `<forward>` elements, # and Sibelius 7.1 skips it on subsequent chord members, so this might be None # no matter: go on to findM21VoiceFromXmlVoice() thisVoice = self.findM21VoiceFromXmlVoice(mxVoice) if thisVoice is not None: insertStream = thisVoice insertStream.coreInsert(self.offsetMeasureNote, el)
[docs] def findM21VoiceFromXmlVoice( self, mxVoice: ET.Element|None = None, ) -> stream.Voice|None: ''' Find the stream.Voice object from a <voice> tag or None. ''' m = self.stream useVoice: str|int|None if strippedText(mxVoice): useVoice = strippedText(mxVoice) try: self.lastVoice = int(useVoice) except ValueError: self.lastVoice = useVoice else: useVoice = self.lastVoice if useVoice is None: # pragma: no cover warnings.warn('Cannot put in an element with a missing voice tag when ' + 'no previous voice tag was given. Assuming voice 1... ', MusicXMLWarning, stacklevel=2) useVoice = 1 thisVoice: stream.Voice|None = None if useVoice in self.voicesById: thisVoice = self.voicesById[useVoice] elif int(useVoice) in self.voicesById: thisVoice = self.voicesById[int(useVoice)] elif str(useVoice) in self.voicesById: thisVoice = self.voicesById[str(useVoice)] else: warnings.warn( f'Cannot find voice {useVoice!r}; putting outside of voices.', MusicXMLWarning, stacklevel=2) warnings.warn( f'Current voiceIds: {list(self.voicesById)}', MusicXMLWarning, stacklevel=2) warnings.warn( f'Current voices: {list(m.voices)} in m. {m.number}', MusicXMLWarning, stacklevel=2) return thisVoice
[docs] def xmlBarline(self, mxBarline: ET.Element) -> None: ''' Handles everything for putting a barline into a Stream and updating repeat characteristics. ''' m = self.stream mxRepeatObj = mxBarline.find('repeat') if mxRepeatObj is not None: barline = self.xmlToRepeat(mxBarline) else: barline = self.xmlToBarline(mxBarline) # barline objects also store ending objects, that mark begin # and end of repeat bracket designations mxEndingObj = mxBarline.find('ending') if mxEndingObj is not None: # TODO: musicxml 4: system="yes/no" -- does this apply to whole system? # environLocal.printDebug(['found mxEndingObj', mxEndingObj, 'm', m]) # get all incomplete spanners of the appropriate class that are # not complete # TODO: this should also filter by number (in theory.) rbSpanners = self.spannerBundle.getByClass( spanner.RepeatBracket ).getByCompleteStatus(False) # if we have no complete bracket objects, must start a new one if not rbSpanners: # create with this measure as the object rb = spanner.RepeatBracket(m) self.spannerBundle.append(rb) # if we have any incomplete, this must be the end else: # environLocal.printDebug(['matching RepeatBracket spanner', # 'len(rbSpanners)', len(rbSpanners)]) rb = rbSpanners[0] # get RepeatBracket # try to add this measure; may be the same rb.addSpannedElements(m) if mxEndingObj.get('type') == 'start': mxNumber = mxEndingObj.get('number') # RepeatBracket handles comma-separated values, such as "1,2" try: rb.number = mxNumber except spanner.SpannerException: rb.number = 1 # however, if the content is different, use that. # for instance, Finale often uses <ending number="1">2.</ending> for # second endings, since the first ending number has already been closed. endingNumberText = mxEndingObj.text if endingNumberText is not None: rb.overrideDisplay = endingNumberText overrideNumber = re.match(r'^(\d+)\.?$', endingNumberText) # very cautious if overrideNumber: rb.number = int(overrideNumber.group(1)) # there may just be an ending marker, and no start # this implies just one measure if mxEndingObj.get('type') in ('stop', 'discontinue'): rb.completeStatus = True # set number; '' or None is interpreted as 1 if barline.location == 'left': # environLocal.printDebug(['setting left barline', barline]) m.leftBarline = barline elif barline.location == 'right': # environLocal.printDebug(['setting right barline', barline]) m.rightBarline = barline else: # middle barline m.coreElementsChanged() m.append(barline)
[docs] def xmlToRepeat(self, mxBarline, inputM21=None): # noinspection PyShadowingNames ''' Given an mxBarline (not an mxRepeat object) with repeatObj as a parameter, file the necessary parameters and return a bar.Repeat() object >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxBarline = EL('<barline><bar-style>light-heavy</bar-style>' ... '<repeat direction="backward"/></barline>') >>> r = MP.xmlToRepeat(mxBarline) >>> r <music21.bar.Repeat direction=end> Test that the music21 type for a backwards repeat is called "final" (because it resembles a final barline) even though the musicxml style is called light-heavy. >>> r.type 'final' >>> r.direction 'end' Test that a forward repeat with times doesn't raise an exception, and that the resulting Repeat doesn't have times set. >>> mxStartBarline = EL('<barline><bar-style>light-heavy</bar-style>' ... '<repeat direction="forward" times="2"/></barline>') >>> rs = MP.xmlToRepeat(mxStartBarline) >>> rs <music21.bar.Repeat direction=start> ''' if inputM21 is None: r = bar.Repeat() else: r = inputM21 seta = _setAttributeFromTagText seta(r, mxBarline, 'bar-style', 'type') self.setEditorial(mxBarline, r) # TODO: wavy-line # TODO: segno, coda, fermata, # TODO: winged location = mxBarline.get('location') if location is not None: r.location = location else: r.location = 'right' # default in musicxml 3.0 mxRepeat = mxBarline.find('repeat') if mxRepeat is None: raise bar.BarException('attempting to create a Repeat from an MusicXML ' + 'bar that does not define a repeat') # TODO: musicxml 4: mxRepeat attr: after-jump mxDirection = mxRepeat.get('direction') # environLocal.printDebug(['mxRepeat', mxRepeat, mxRepeat._attr]) if mxDirection is None: raise MusicXMLImportException('Repeat sign direction is required') if mxDirection.lower() == 'forward': r.direction = 'start' elif mxDirection.lower() == 'backward': r.direction = 'end' else: raise bar.BarException('cannot handle mx direction format:', mxDirection) if mxRepeat.get('times') is not None: try: # make into a number r.times = int(mxRepeat.get('times')) except bar.BarException: # ignore BarException, just let the set of r.times fail silently pass if inputM21 is None: return r
[docs] def xmlToBarline(self, mxBarline, inputM21=None): # noinspection PyShadowingNames ''' Given an mxBarline, fill the necessary parameters >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxBarline = EL('<barline location="right">' ... '<bar-style>light-light</bar-style>' ... '</barline>') >>> b = MP.xmlToBarline(mxBarline) >>> b <music21.bar.Barline type=double> >>> b.type # music21.type is different from musicxml.style 'double' >>> b.location 'right' ''' if inputM21 is None: b = bar.Barline() else: b = inputM21 seta = _setAttributeFromTagText seta(b, mxBarline, 'bar-style', 'type') location = mxBarline.get('location') if location is not None: b.location = location else: b.location = 'right' # default in musicxml 3.0 if inputM21 is None: return b
[docs] def xmlHarmony(self, mxHarmony): ''' Create a ChordSymbol object and insert it to the core and staff reference. ''' # TODO: musicxml 4: system="yes/no" -- does this apply to whole system? h = self.xmlToChordSymbol(mxHarmony) chordOffset = self.xmlToOffset(mxHarmony) self.insertCoreAndRef(self.offsetMeasureNote + chordOffset, mxHarmony, h)
[docs] def xmlToChordSymbol( self, mxHarmony: ET.Element ) -> harmony.ChordSymbol|harmony.NoChord|tablature.ChordWithFretBoard: # noinspection PyShadowingNames ''' Convert a <harmony> tag to a harmony.ChordSymbol object: >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> elStr = '<harmony><root><root-step>D</root-step><root-alter>-1</root-alter>' >>> elStr += '</root><kind>major-seventh</kind></harmony>' >>> mxHarmony = EL(elStr) >>> cs = MP.xmlToChordSymbol(mxHarmony) >>> cs <music21.harmony.ChordSymbol D-maj7> >>> cs.figure 'D-maj7' >>> cs.pitches (<music21.pitch.Pitch D-3>, <music21.pitch.Pitch F3>, <music21.pitch.Pitch A-3>, <music21.pitch.Pitch C4>) >>> cs.root() <music21.pitch.Pitch D-3> TODO: this is very classically-oriented. Make more Jazz/Rock like possible/default?. >>> mxHarmony.find('kind').text = 'major-sixth' >>> cs = MP.xmlToChordSymbol(mxHarmony) >>> cs <music21.harmony.ChordSymbol D-6> >>> cs.figure 'D-6' >>> cs.pitches (<music21.pitch.Pitch D-3>, <music21.pitch.Pitch F3>, <music21.pitch.Pitch A-3>, <music21.pitch.Pitch B-3>) >>> cs.root() <music21.pitch.Pitch D-3> ''' # TODO: musicxml 4: attr: arrangement -- C/E or C over E etc. # Element offset is covered by xmlHarmony(), which calls this. # Element staff is also covered by insertCoreAndReference in xmlHarmony() b: pitch.Pitch|None = None r: pitch.Pitch|None = None inversion: int|None = None chordKind: str = '' chordKindStr: str = '' mxKind = mxHarmony.find('kind') if mxKindText := strippedText(mxKind): chordKind = mxKindText mxFrame = mxHarmony.find('frame') mxBass: ET.Element | None = mxHarmony.find('bass') if mxBass is not None: # required bassStep = mxBass.find('bass-step') if bassStep is None: raise MusicXMLImportException('bass-step missing') b = pitch.Pitch(bassStep.text) # optional mxBassAlter = mxBass.find('bass-alter') if mxBassAlter is not None and (alterText := mxBassAlter.text) is not None: # can provide integer or float to create accidental on pitch b.accidental = pitch.Accidental(float(alterText)) # TODO: musicxml 4: bass-separator: use something besides slash on output. mxInversion = mxHarmony.find('inversion') if inversionText := strippedText(mxInversion): # TODO: print-style for inversion # TODO: musicxml 4: text attribute overrides display of the inversion. inversion = int(inversionText) # TODO: print-style if chordKind: # two ways of doing it if t.TYPE_CHECKING: assert mxKind is not None # Get m21 chord kind from dict of musicxml aliases ("dominant" -> "dominant-seventh") if chordKind in harmony.CHORD_ALIASES: chordKind = harmony.CHORD_ALIASES[chordKind] mxKindText = mxKind.get('text') or '' # attribute if not (mxKindText == '' and chordKind != 'none'): chordKindStr = mxKindText # TODO: root vs. function; see group "harmony-chord") mxRoot = mxHarmony.find('root') if mxRoot is not None: # choice: <root> or <function> mxRS = mxRoot.find('root-step') if t.TYPE_CHECKING: assert mxRS is not None rootText = mxRS.text if rootText in (None, ''): rootText = mxRS.get('text') # two ways to do it # this should do display even if content is supported. if rootText is not None: r = pitch.Pitch(rootText) mxRootAlter = mxRoot.find('root-alter') if mxRootAlter is not None: # can provide integer or float to create accidental on pitch alterFloat = float(mxRootAlter.text) # type: ignore r.accidental = pitch.Accidental(alterFloat) # TODO: musicxml 4: numeral -- pretty important. cs_class: type[harmony.ChordSymbol|harmony.NoChord|tablature.ChordWithFretBoard] if mxFrame is not None: cs_class = tablature.ChordWithFretBoard elif chordKind == 'none': cs_class = harmony.NoChord else: cs_class = harmony.ChordSymbol cs = cs_class( bass=b, root=r, inversion=inversion, kind=chordKind, kindStr=chordKindStr ) seta = _setAttributeFromTagText if mxRoot is None: # function instead -- deprecated in musicxml 4 seta(cs, mxHarmony, 'function', 'romanNumeral') mxDegrees = mxHarmony.findall('degree') for mxDegree in mxDegrees: # a list of components hd = harmony.ChordStepModification() seta(hd, mxDegree, 'degree-value', 'degree', transform=int) if hd.degree is None: raise MusicXMLImportException('degree-value missing') # TODO: - should allow float, but meaningless to allow microtones in this context. seta(hd, mxDegree, 'degree-alter', 'interval', transform=int) seta(hd, mxDegree, 'degree-type', 'modType') cs.addChordStepModification(hd, updatePitches=True) self.setEditorial(mxHarmony, cs) self.setPrintStyle(mxHarmony, cs) self.setPrintObject(mxHarmony, cs) # TODO: attr: print-frame self.setPlacement(mxHarmony, cs) # TODO: attr: use-symbols # TODO: attr: stack-degrees # TODO: attr: parentheses-degrees # TODO: attr: bracket-degrees # TODO: attrGroup: print-style # TODO: attrGroup: halign # TODO: attrGroup: valign # TODO: frame if mxFrame is not None: pass # TODO: Luke: Uncomment this next line when method is ready # self.xmlFrameToFretBoard(mxFrame, cs) return cs
[docs] def xmlDirection(self, mxDirection): ''' convert a <direction> tag to one or more expressions, metronome marks, etc. and add them to the core and staffReference. ''' # TODO: musicxml 4: system="yes/no" -- does this apply to whole system? # offset is out of order because we need to know it before direction-type offsetDirection = self.xmlToOffset(mxDirection) totalOffset = float(offsetDirection + self.offsetMeasureNote) # out of order: parse <staff> element # staffKey is the staff that this direction applies to. not # found in mxSpecificDirectionTag (inside direction-type) but in mxDirection itself. staffKey = self.getStaffNumber(mxDirection) metronome_added = False # editorial (footnote, level, voice) for the whole <direction> tag is parsed in # setDirectionInDirectionType. -- probably a mistake since they # should all share the same Editorial object and be manipulated together for mxDirType in mxDirection.findall('direction-type'): for mxSpecificDirectionTag in mxDirType: self.setDirectionInDirectionType(mxSpecificDirectionTag, mxDirection, staffKey, totalOffset) if mxSpecificDirectionTag.tag == 'metronome': metronome_added = True # check for sound tag if direction didn't specify a tempo already, # avoiding doubled metronomes. if not metronome_added: for mxSound in mxDirection.findall('sound'): if 'tempo' not in mxSound.attrib: continue # setSound is defined in xmlSoundParser.py xmlSoundParser.setSound(self, mxSound, mxDir=mxDirection, totalOffset=totalOffset) break
# TODO: musicxml 4:listening
[docs] def setDirectionInDirectionType( self, mxDir: ET.Element, mxDirection: ET.Element, staffKey: int, totalOffset: float, ): # TODO: pedal # TODO: harp-pedals # TODO: damp # TODO: damp-all # TODO: eyeglasses # TODO: string-mute # TODO: scordatura # TODO: image # TODO: principal-voice # TODO: accordion-registration # TODO: percussion (including: glass, metal, wood, membrane, effect, timpani, # beater, stick, stick-location, other-percussion) # TODO: other-direction tag = mxDir.tag if tag == 'dynamics': # fp, mf, etc., each as a tag # in rare cases there may be more than one dynamic in the same # direction, so we iterate over them. for mxDyn in mxDir: self.setDynamicsDirection(mxDir, mxDyn, mxDirection, staffKey, totalOffset) elif tag in ('wedge', 'bracket', 'dashes', 'octave-shift', 'pedal'): try: spannerList = self.xmlDirectionTypeToSpanners( mxDir, staffKey, totalOffset ) except MusicXMLImportException as excep: warnings.warn(f'Could not import {tag}: {excep}', MusicXMLWarning, stacklevel=2) spannerList = [] for sp in spannerList: self.setPosition(mxDir, sp) self.setPlacement(mxDir, sp) self.setLineStyle(mxDir, sp) self.setEditorial(mxDirection, sp) elif tag in ('coda', 'segno'): rm: repeat.Segno|repeat.Coda if tag == 'segno': rm = repeat.Segno() else: rm = repeat.Coda() synchronizeIds(mxDir, rm) self.setPosition(mxDir, rm) self.insertCoreAndRef(totalOffset, staffKey, rm) self.setEditorial(mxDirection, rm) elif tag == 'metronome': mm = self.xmlToTempoIndication(mxDir) # SAX was offsetMeasureNote; bug? should be totalOffset??? setAttributeFromAttribute(mm, mxDirection, 'placement', 'placement') self.insertCoreAndRef(totalOffset, staffKey, mm) self.setEditorial(mxDirection, mm) elif tag == 'rehearsal': rm_gen = self.xmlToRehearsalMark(mxDir) self.setStyleAttributes(mxDirection, rm_gen, 'placement') self.insertCoreAndRef(totalOffset, staffKey, rm_gen) self.setEditorial(mxDirection, rm_gen) elif tag == 'words': textExpression = self.xmlToTextExpression(mxDir) # environLocal.printDebug(['got TextExpression object', repr(te)]) # offset here is a combination of the current position # (offsetMeasureNote) and the direction's offset setAttributeFromAttribute(textExpression, mxDirection, 'placement', 'placement') repeatExpression = textExpression.getRepeatExpression() if repeatExpression is not None: # the repeat expression stores a copy of the text # expression within it; replace it here on insertion self.insertCoreAndRef(totalOffset, staffKey, repeatExpression) self.setEditorial(mxDirection, repeatExpression) else: self.insertCoreAndRef(totalOffset, staffKey, textExpression) self.setEditorial(mxDirection, textExpression)
[docs] def setDynamicsDirection( self, mxDir: ET.Element, mxDyn: ET.Element, mxDirection: ET.Element, staffKey: int, totalOffset: float, ): ''' Add a single dynamic element to the core and staffReference. ''' m21DynamicText = mxDyn.tag if m21DynamicText == 'other-dynamic' and mxDyn.text: m21DynamicText = mxDyn.text.strip() d = dynamics.Dynamic(m21DynamicText) synchronizeIds(mxDyn, d) setAttributeFromAttribute(d, mxDirection, 'placement', 'placement') self.insertCoreAndRef(totalOffset, staffKey, d) self.setPosition(mxDir, d) self.setEditorial(mxDirection, d)
[docs] def xmlToTextExpression(self, mxWords): # noinspection PyShadowingNames ''' Given an `mxWords`, create a :class:`~music21.expression.TextExpression` and set style attributes, fonts, position, etc. Calls `setTextFormatting`, which calls `setPrintStyleAlign`. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> m = EL('<words default-y="17" font-family="Courier" ' ... 'font-style="italic" relative-x="-6">a tempo</words>') >>> te = MP.xmlToTextExpression(m) >>> te.content 'a tempo' >>> te.style.relativeX -6 >>> te.style.fontFamily ['Courier'] ''' # TODO: switch to using the setPrintAlign, etc. # environLocal.printDebug(['mxToTextExpression()', mxWords, mxWords.charData]) # content can be passed with creation argument wordText = strippedText(mxWords) te = expressions.TextExpression(wordText) self.setTextFormatting(mxWords, te) return te
[docs] def xmlToRehearsalMark(self, mxRehearsal): ''' Return a rehearsal mark from a rehearsal tag. ''' rehearsalText = strippedText(mxRehearsal) rm = expressions.RehearsalMark(rehearsalText) self.setTextFormatting(mxRehearsal, rm) return rm
[docs] def xmlToTempoIndication(self, mxMetronome, mxWords=None): ''' Given an mxMetronome, convert to either a TempoIndication subclass, either a tempo.MetronomeMark or tempo.MetricModulation. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> m = EL('<metronome><per-minute>125</per-minute>' ... '<beat-unit>half</beat-unit></metronome>') >>> MP.xmlToTempoIndication(m) <music21.tempo.MetronomeMark Half=125> Metric modulation: >>> m = EL('<metronome><beat-unit>long</beat-unit><beat-unit>32nd</beat-unit>' ... '<beat-unit-dot/></metronome>') >>> MP.xmlToTempoIndication(m) <music21.tempo.MetricModulation <music21.tempo.MetronomeMark Imperfect Longa=None>=<music21.tempo.MetronomeMark Dotted 32nd=None>> ''' # get lists of durations and texts durations = [] numbers = [] dActive = None for mxObj in mxMetronome: tag = mxObj.tag if tag == 'beat-unit': durationType = musicXMLTypeToType(mxObj.text) dActive = duration.Duration(type=durationType) durations.append(dActive) elif tag == 'beat-unit-dot': if dActive is None: raise MusicXMLImportException('encountered metronome components out of order') dActive.dots += 1 # add one dot each time these are encountered # should come last elif tag == 'per-minute': # environLocal.printDebug(['found PerMinute', mxObj]) # store as a number perMin = mxObj.text if perMin is not None and perMin.strip() != '': try: numbers.append(common.numToIntOrFloat(float(perMin))) except ValueError: pass # TODO: accept text per minute # TODO: metronome-relation -- specifies how to relate multiple beat units # metronomeRelations = mxMetronome.find('metronome-relation') if len(durations) > 1: # Metric Modulation! mm = tempo.MetricModulation() # environLocal.printDebug(['found metric modulation:', 'durations', durations]) if len(durations) < 2: raise MusicXMLImportException( 'found incompletely specified musicxml metric modulation: ' + 'fewer than two durations defined') # all we have are referents, no values are defined in musicxml # will need to update context after adding to Stream mm.oldReferent = durations[0] mm.newReferent = durations[1] else: # environLocal.printDebug(['found metronome mark:', 'numbers', numbers]) mm = tempo.MetronomeMark() if numbers: mm.number = numbers[0] if durations: mm.referent = durations[0] # TODO: set text if defined in words if mxWords is not None: pass paren = mxMetronome.get('parentheses') if paren is not None: if paren == 'yes': mm.parentheses = True synchronizeIds(mxMetronome, mm) self.setPrintObject(mxMetronome, mm) # new in 4.0 -- do not output until we output 4.0 self.setPosition(mxMetronome, mm) return mm
[docs] def xmlToOffset(self, mxObj: ET.Element) -> float: ''' Finds an <offset> inside the mxObj and returns it as a music21 offset (in quarterLengths) >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.divisions = 40 >>> off = EL('<direction><offset>100</offset></direction>') >>> MP.xmlToOffset(off) 2.5 Returns a float, not fraction, since the inserts will later convert to a Fraction if need be. >>> MP.divisions = 30 >>> off = EL('<direction><offset>10</offset></direction>') >>> MP.xmlToOffset(off) 0.33333... ''' try: offset = float(mxObj.find('offset').text.strip()) # type: ignore except (ValueError, AttributeError): return 0.0 return offset / self.divisions
[docs] def parseMeasureAttributes(self): ''' parses the attributes of the <measure> tag. Not the <attributes> tag inside the measure tag. calls parseMeasureNumbers(), and gets the width from the width tag. # TODO: non-controlling # may need to do a format/unit conversion? ''' implicit = self.mxMeasure.get('implicit') if xmlObjects.yesNoToBoolean(implicit): self.stream.showNumber = stream.enums.ShowNumber.NEVER else: self.stream.showNumber = stream.enums.ShowNumber.DEFAULT self.parseMeasureNumbers() width = self.mxMeasure.get('width') if width is not None: width = _floatOrIntStr(width) self.stream.layoutWidth = width
[docs] def parseAttributesTag(self, mxAttributes): ''' Parses a single attributes tag (mxAttributes) and sets self.attributesAreInternal to False, self.activeAttributes to mxAttributes, self.parent.activeAttributes to mxAttributes and then runs the appropriate attributeTagsToMethods for the attribute. Also sets `self.divisions` for the current divisions (along with self.parent.lastDivisions) and `self.transposition` and to the current transpose. ''' self.attributesAreInternal = False self.activeAttributes = mxAttributes for mxSub in mxAttributes: tag = mxSub.tag # clef, key, measure-style, time, staff-details if tag in self.attributeTagsToMethods: meth = getattr(self, self.attributeTagsToMethods[tag]) meth(mxSub) # NOT to be done: directive -- deprecated since v2. elif tag == 'divisions': self.divisions = opFrac(float(mxSub.text)) # TODO: musicxml4: for-part including part-clef # TODO: instruments -- int if more than one instrument plays most of the time # TODO: part-symbol elif tag == 'staves': self.staves = int(mxSub.text) elif tag == 'transpose': self.transposition = self.xmlTransposeToInterval(mxSub) # warnings.warn(f'Got a transposition of {self.transposition}', MusicXMLWarning) # footnote, level self.setEditorial(mxAttributes, self.stream) if self.parent is not None: self.parent.lastDivisions = self.divisions self.parent.activeAttributes = self.activeAttributes
[docs] def xmlTransposeToInterval(self, mxTranspose): # noinspection PyShadowingNames ''' Convert a MusicXML Transpose object to a music21 Interval object. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> t = EL('<transpose><diatonic>-1</diatonic>' ... '<chromatic>-2</chromatic></transpose>') >>> MP.xmlTransposeToInterval(t) <music21.interval.Interval M-2> >>> t = EL('<transpose><diatonic>-5</diatonic>' ... '<chromatic>-9</chromatic></transpose>') >>> MP.xmlTransposeToInterval(t) <music21.interval.Interval M-6> Not mentioned in MusicXML XSD but supported in (Finale; MuseScore): octave-change refers to both diatonic and chromatic, so we will deal: >>> t = EL('<transpose id="x"><diatonic>-1</diatonic><chromatic>-2</chromatic>' ... '<octave-change>-1</octave-change></transpose>') >>> inv = MP.xmlTransposeToInterval(t) >>> inv <music21.interval.Interval M-9> >>> inv.id 'x' ''' diatonicStep = None mxDiatonic = mxTranspose.find('diatonic') if mxDiatonic is not None: diatonicStep = int(mxDiatonic.text) chromaticStep = None mxChromatic = mxTranspose.find('chromatic') if mxChromatic is not None: chromaticStep = int(mxChromatic.text) octaveChange = 0 mxOctaveChange = mxTranspose.find('octave-change') if mxOctaveChange is not None: octaveChange = int(mxOctaveChange.text) * 12 diatonicStep += 7 * int(mxOctaveChange.text) # TODO: presently not dealing with <double> # doubled one octave down from what is currently written # (as is the case for mixed cello / bass parts in orchestral literature) # environLocal.printDebug(['ds', diatonicStep, 'cs', chromaticStep, 'oc', oc]) # TODO: musicxml 4: double, attr: above if diatonicStep is not None and chromaticStep is not None: # diatonic step can be used as a generic specifier here if # shifted 1 away from zero if diatonicStep < 0: diatonicActual = diatonicStep - 1 else: diatonicActual = diatonicStep + 1 try: post = interval.intervalFromGenericAndChromatic(diatonicActual, chromaticStep + octaveChange) except interval.IntervalException: # some people might use -8 for diatonic for down a 9th, assuming # even if there is an octave change because schema is ambiguous. So try again. if diatonicStep < 0: diatonicActual = (diatonicStep - int(octaveChange * 7 / 12)) - 1 else: diatonicActual = (diatonicStep - int(octaveChange * 7 / 12)) + 1 post = interval.intervalFromGenericAndChromatic(diatonicActual, chromaticStep + octaveChange) elif chromaticStep is not None: post = interval.Interval(chromaticStep + octaveChange) elif diatonicStep is not None: post = interval.GenericInterval(diatonicStep) else: post = interval.Interval('P1') # guaranteed to return an interval object. synchronizeIds(mxTranspose, post) return post
[docs] def handleTimeSignature(self, mxTime): ''' Creates a TimeSignature using xmlToTimeSignature and inserts it into the stream if it is appropriate to do so (now always yes.) ''' ts = self.xmlToTimeSignature(mxTime) if ts is not None: self.insertCoreAndRef(self.offsetMeasureNote, mxTime, ts)
[docs] def xmlToTimeSignature( self, mxTime: ET.Element ) -> meter.TimeSignature|meter.SenzaMisuraTimeSignature: # noinspection PyShadowingNames ''' Returns a TimeSignature or SenzaMisuraTimeSignature (for senza-misura) from a <time> block. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxTime = EL('<time><beats>3</beats><beat-type>8</beat-type></time>') >>> MP.xmlToTimeSignature(mxTime) <music21.meter.TimeSignature 3/8> >>> mxTime = EL('<time symbol="common"><beats>4</beats>' ... '<beat-type>4</beat-type></time>') >>> MP.xmlToTimeSignature(mxTime).symbol 'common' Multiple times give a combined meter >>> mxTime = EL('<time><beats>3</beats><beat-type>8</beat-type>' ... '<beats>4</beats><beat-type>4</beat-type></time>') >>> MP.xmlToTimeSignature(mxTime) <music21.meter.TimeSignature 3/8+4/4> Music21 separates into two parts >>> mxTime = EL('<time><beats>3+2</beats><beat-type>8</beat-type></time>') >>> ts32 = MP.xmlToTimeSignature(mxTime) >>> ts32 <music21.meter.TimeSignature 3/8+2/8> Senza Misura gives a special music21 time signature subclass >>> mxSenza = EL('<time><senza-misura>0</senza-misura></time>') >>> MP.xmlToTimeSignature(mxSenza) <music21.meter.SenzaMisuraTimeSignature 0> Small Duration Time Signatures are supported >>> mxTime = EL('<time><beats>3</beats><beat-type>32</beat-type></time>') >>> MP.xmlToTimeSignature(mxTime) <music21.meter.TimeSignature 3/32> >>> mxTime = EL('<time><beats>3</beats><beat-type>64</beat-type></time>') >>> MP.xmlToTimeSignature(mxTime) <music21.meter.TimeSignature 3/64> >>> mxTime = EL('<time><beats>3</beats><beat-type>128</beat-type></time>') >>> MP.xmlToTimeSignature(mxTime) <music21.meter.TimeSignature 3/128> ''' isSenzaMisura = mxTime.find('senza-misura') if isSenzaMisura is not None: return meter.SenzaMisuraTimeSignature(isSenzaMisura.text) numerators = [] denominators = [] for beatOrType in mxTime: if beatOrType.tag == 'beats': numerators.append(strippedText(beatOrType)) # may be 3+2 elif beatOrType.tag == 'beat-type': denominators.append(strippedText(beatOrType)) elif beatOrType.tag == 'interchangeable': break # interchangeable comes after all beat/beat-type sequences # convert into a string msg = [f'{num}/{denom}' for num, denom in zip(numerators, denominators)] # warnings.warn(f"loading meter string: {'+'.join(msg)}", MusicXMLWarning) if len(msg) == 1: # normal try: ts = meter.TimeSignature(msg[0]) except meter.MeterException: raise MusicXMLImportException( f'Cannot process time signature {msg[0]}') else: ts = meter.TimeSignature() ts.load('+'.join(msg)) # TODO: interchangeable self.setPrintStyleAlign(mxTime, ts) self.setPrintObject(mxTime, ts) # TODO: attr: separator # attr: symbol symbol = mxTime.get('symbol') if symbol is None: pass elif symbol in ('common', 'cut', 'single-number', 'normal'): ts.symbol = symbol elif symbol == 'note': ts.symbolizeDenominator = True elif symbol == 'dotted-note': pass # TODO: support, but not as musicxml style -- reduces by 1/3 the numerator # this should be done by changing the displaySequence directly. return ts
[docs] def handleClef(self, mxClef) -> clef.Clef: # noinspection PyShadowingNames ''' Convert a `<clef>` tag to a music21.clef.Clef object, append it to the core, and set self.lastClefs for the staff number. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.lastClefs {} >>> mxAltoClef = EL('<clef number="2"><sign>C</sign><line>3</line></clef>') >>> altoClef = MP.handleClef(mxAltoClef) >>> altoClef <music21.clef.AltoClef> The MeasureParser (together with its parent PartParser) keeps track of the current clef for each staff in a shared dictionary called `.lastClefs`. >>> MP.lastClefs {2: <music21.clef.AltoClef>} >>> MP.lastClefs[2] is altoClef True The dictionary is shared with the parent PartParser. >>> MP.parent.lastClefs is MP.lastClefs True Per MusicXML standard, a clef without a number is interpreted as being for staff 1 >>> mxClefBC = EL('<clef><sign>F</sign><line>4</line></clef>') >>> bc = MP.handleClef(mxClefBC) >>> bc <music21.clef.BassClef> >>> MP.lastClefs {2: <music21.clef.AltoClef>, 1: <music21.clef.BassClef>} Changed in v10.5 -- Fixed bug where clefs without number were applied to all staves rather than correct MusicXML interpretation of staff 1. Returns the clef created. There is no longer a default treble clef for NO_STAFF_ASSIGNED. ''' clefObj = self.xmlToClef(mxClef) self.insertCoreAndRef(self.offsetMeasureNote, mxClef, clefObj) # Update the list of lastClefs -- needed for rest display. staffNumberStrOrNone = self.getStaffNumber(mxClef) self.lastClefs[staffNumberStrOrNone] = clefObj return clefObj
[docs] def xmlToClef(self, mxClef): # noinspection PyShadowingNames ''' Returns a music21 Clef object from an mxClef element. >>> from xml.etree.ElementTree import fromstring as EL >>> mxClef = EL('<clef><sign>G</sign><line>2</line></clef>') >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.xmlToClef(mxClef) <music21.clef.TrebleClef> >>> mxClef = EL('<clef><sign>G</sign><line>2</line>' ... '<clef-octave-change>-1</clef-octave-change></clef>') >>> MP.xmlToClef(mxClef) <music21.clef.Treble8vbClef> >>> mxClef = EL('<clef><sign>TAB</sign></clef>') >>> MP.xmlToClef(mxClef) <music21.clef.TabClef> ''' sign = mxClef.find('sign').text.strip() if sign.lower() in ('tab', 'percussion', 'none', 'jianpu'): clefObj = clef.clefFromString(sign) else: mxLine = mxClef.find('line') if mxLine is not None: line = mxLine.text.strip() elif sign == 'G': line = '2' else: line = '4' mxOctaveChange = mxClef.find('clef-octave-change') if mxOctaveChange is not None: try: octaveChange = int(mxOctaveChange.text) except ValueError: octaveChange = 0 else: octaveChange = 0 clefObj = clef.clefFromString(sign + line, octaveChange) # number is taken care of by insertCoreAndReference # TODO: additional -- is this clef an additional clef to ignore # TODO: size # TODO: after-barline -- particular style to clef. self.setPrintStyle(mxClef, clefObj) self.setPrintObject(mxClef, clefObj) return clefObj
[docs] def handleKeySignature(self, mxKey): ''' convert mxKey to a Key or KeySignature and run insertCoreAndRef on it ''' keySig = self.xmlToKeySignature(mxKey) self.insertCoreAndRef(self.offsetMeasureNote, mxKey, keySig)
[docs] def xmlToKeySignature(self, mxKey: ET.Element) -> key.KeySignature: # noinspection PyShadowingNames ''' Returns either a KeySignature (traditional or non-traditional) or a Key object based on whether fifths and mode is present. >>> from xml.etree.ElementTree import fromstring as EL >>> mxKey = EL('<key><fifths>-4</fifths></key>') >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.xmlToKeySignature(mxKey) <music21.key.KeySignature of 4 flats> >>> mxKey = EL('<key><fifths>-4</fifths><mode>minor</mode></key>') >>> MP.xmlToKeySignature(mxKey) <music21.key.Key of f minor> Invalid modes get ignored and returned as KeySignature objects, not keys. >>> mxKey = EL('<key><fifths>-4</fifths><mode>crazy</mode></key>') >>> MP.xmlToKeySignature(mxKey) <music21.key.KeySignature of 4 flats> ''' # TODO: cancel if mxKey.find('fifths') is None: ks = self.nonTraditionalKeySignature(mxKey) else: ks = key.KeySignature() seta = _setAttributeFromTagText seta(ks, mxKey, 'fifths', 'sharps', transform=int) mxKeyMode = mxKey.find('mode') if mxKeyMode is not None: modeValue = mxKeyMode.text if modeValue not in (None, ''): try: ks = ks.asKey(modeValue) except exceptions21.Music21Exception: pass # mxKeyMode might not be a valid mode -- in which case ignore self.mxKeyOctaves(mxKey, ks) self.setPrintStyle(mxKey, ks) self.setPrintObject(mxKey, ks) return ks
[docs] def mxKeyOctaves(self, mxKey, ks): # noinspection PyShadowingNames ''' process the <key-octave> tags to potentially change a key signature to a non-standard key signature. >>> from xml.etree.ElementTree import fromstring as EL >>> mxKey = EL('<key><fifths>-4</fifths>' ... '<key-octave number="1">3</key-octave>' ... '<key-octave number="2">4</key-octave>' ... '<key-octave number="4">3</key-octave>' ... '</key>') >>> ks = key.KeySignature(-4) >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.mxKeyOctaves(mxKey, ks) >>> ks.alteredPitches [<music21.pitch.Pitch B-3>, <music21.pitch.Pitch E-4>, <music21.pitch.Pitch A->, <music21.pitch.Pitch D-3>] ''' # key-octave keyOctaves = mxKey.findall('key-octave') if not keyOctaves: return alteredPitches = copy.deepcopy(ks.alteredPitches) for mxKeyOctave in keyOctaves: cancel = mxKeyOctave.get('cancel') # TODO: cancel if cancel == 'yes': continue pitchIndex = mxKeyOctave.get('number') try: alteredPitch = alteredPitches[int(pitchIndex) - 1] except (IndexError, ValueError): continue octaveToSet = int(mxKeyOctave.text) alteredPitch.octave = octaveToSet ks.alteredPitches = alteredPitches
[docs] def nonTraditionalKeySignature(self, mxKey): # noinspection PyShadowingNames ''' Returns a KeySignature object that represents a nonTraditional Key Signature called by xmlToKeySignature if <fifths> is not present. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxKey = EL('<key><key-step>E</key-step><key-alter>-1</key-alter></key>') >>> MP.nonTraditionalKeySignature(mxKey) <music21.key.KeySignature of pitches: [E-]> Should be the same: >>> MP.xmlToKeySignature(mxKey) <music21.key.KeySignature of pitches: [E-]> Works with key-accidental also: >>> mxKey = EL('<key><key-step>G</key-step><key-alter>1</key-alter>' ... '<key-accidental>sharp</key-accidental></key>') >>> MP.nonTraditionalKeySignature(mxKey) <music21.key.KeySignature of pitches: [G#]> ''' children = list(mxKey) lastTag = None steps = [] alters = [] accidentals = [] for c in children: tag = c.tag if lastTag == 'key-alter' and tag == 'key-step': accidentals.append(None) if tag == 'key-step': steps.append(c.text) elif tag == 'key-alter': alters.append(float(c.text)) elif tag == 'key-accidental': accidentals.append(c.text) lastTag = tag if len(accidentals) < len(alters): accidentals.append(None) if len(steps) != len(alters): raise MusicXMLImportException( 'For non traditional signatures each step must have an alter') ks = key.KeySignature(sharps=None) alteredPitches = [] for step, alter, accidental in zip(steps, alters, accidentals): p = pitch.Pitch(step) if accidental is not None: if accidental in self.mxAccidentalNameToM21: accidentalName = self.mxAccidentalNameToM21[accidental] else: accidentalName = accidental p.accidental = pitch.Accidental(accidentalName) p.accidental.alter = alter else: p.accidental = pitch.Accidental(alter) alteredPitches.append(p) ks.alteredPitches = alteredPitches return ks
[docs] def handleStaffDetails(self, mxDetails): ''' StaffDetails (staff-details) handles attributes about the staff itself -- its size, number of lines, tuning, frets, etc. It is different from StaffLayout (staff-layout) which only handles relationship of one staff to another (the distance) Rather than returning a StaffLayout object, it adds it to self.staffLayoutObjects checking to see if there is already an incomplete StaffLayout object for this staff. ''' # staffNumber refers to the staff number for this Part -- i.e., usually None or 1 # except for a piano score, etc. # ET.dump(mxDetails) staffNumber = mxDetails.get('number') if staffNumber is not None: staffNumber = int(staffNumber) else: staffNumber = 1 layoutObjectKey = (staffNumber, self.offsetMeasureNote) existingStaffLayoutObject = self.staffLayoutObjects.get(layoutObjectKey, None) newStaffLayoutObject = self.xmlStaffLayoutFromStaffDetails( mxDetails, m21staffLayout=existingStaffLayoutObject ) if existingStaffLayoutObject is None: self.insertCoreAndRef(self.offsetMeasureNote, mxDetails, newStaffLayoutObject) self.staffLayoutObjects[layoutObjectKey] = newStaffLayoutObject
[docs] def xmlStaffLayoutFromStaffDetails( self, mxDetails, m21staffLayout: layout.StaffLayout|None = None ) -> layout.StaffLayout|None: # noinspection PyShadowingNames ''' Returns a new StaffLayout object from staff-details or sets attributes on an existing one >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> mxDetails = EL('<details number="2" print-object="no">' ... '<staff-size>21.2</staff-size><staff-lines>4</staff-lines>' ... '</details>') >>> stl = MP.xmlStaffLayoutFromStaffDetails(mxDetails) >>> stl.staffSize 21.2 >>> stl.staffLines 4 >>> stl.staffNumber 2 >>> stl.hidden True `staffType` defaults to Regular: >>> stl.staffType <StaffType.REGULAR: 'regular'> >>> mxDetails2 = EL('<details number="2"><staff-type>cue</staff-type></details>') >>> MP.xmlStaffLayoutFromStaffDetails(mxDetails2, m21staffLayout=stl) >>> stl.staffType <StaffType.CUE: 'cue'> ''' seta = _setAttributeFromTagText stl: layout.StaffLayout if not m21staffLayout: stl = layout.StaffLayout() else: stl = m21staffLayout # attributes staffNumber = mxDetails.get('number') if staffNumber is not None: stl.staffNumber = int(staffNumber) staffPrinted = mxDetails.get('print-object') if staffPrinted == 'no' or staffPrinted is False: stl.hidden = True elif staffPrinted == 'yes' or staffPrinted is True: stl.hidden = False # TODO: show-frets # TODO: print-spacing # sub elements seta(stl, mxDetails, 'staff-lines', transform=int) # TODO: musicxml4: line-details mxStaffType = mxDetails.find('staff-type') if mxStaffType is not None: try: xmlText: str = mxStaffType.text.strip() stl.staffType = stream.enums.StaffType(xmlText) except ValueError: warnings.warn( f'Got an incorrect staff-type in details: {mxStaffType}', MusicXMLWarning, stacklevel=2, ) # TODO: staff-tuning* # TODO: capo seta(stl, mxDetails, 'staff-size', transform=_floatOrIntStr) # TODO: musicxml 4: staff-size has a scaling attribute for the notation # on the resized staff. if not m21staffLayout: return stl
[docs] def handleMeasureStyle(self, mxMeasureStyle): ''' measure + multi-measure repeats, slashed repeats, etc. But currently only multiMeasure rests are supported. Each of these applies to the entire measure, so there's no need to insert into the stream. Does not support multiple staves yet. ''' # TODO: attr: number (staff number) # TODO: attr-group color # TODO: beat-repeat # TODO: measure-repeat mxMultiRest = mxMeasureStyle.find('multiple-rest') if mxMultiRest is not None and self.parent is not None: self.parent.multiMeasureRestsToCapture = int(mxMultiRest.text) mmrSpanner = spanner.MultiMeasureRest() useSymbols = mxMultiRest.get('use-symbols') if useSymbols == 'yes': mmrSpanner.useSymbols = True else: # musicxml default is False mmrSpanner.useSymbols = False self.parent.activeMultiMeasureRestSpanner = mmrSpanner self.setFont(mxMultiRest, mmrSpanner)
# TODO: slash
[docs] def parseMeasureNumbers(self, mNumRaw=None): ''' Gets the measure number from the 'number' attribute of the <measure> tag. (Or, for testing, from the mNumRaw argument). Sets MeasureParser.stream.number and possibly MeasureParser.stream.numberSuffix >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.parseMeasureNumbers('5') >>> MP.stream.number 5 Sets not only `stream.number`, but also `MeasureParser.measureNumber` and `MeasureParser.numberSuffix` >>> MP.parseMeasureNumbers('44b') >>> MP.stream.number 44 >>> MP.stream.numberSuffix 'b' >>> MP.measureNumber 44 >>> MP.numberSuffix 'b' >>> MP.parseMeasureNumbers('X1') >>> MP.stream.number 1 >>> MP.stream.numberSuffix 'X' ''' if mNumRaw is None and self.mxMeasure is not None: # this is the default situation mNumRaw = self.mxMeasure.get('number') m = self.stream if self.parent: lastMNum = self.parent.lastMeasureNumber lastMSuffix = self.parent.lastNumberSuffix else: lastMNum = None lastMSuffix = '' if mNumRaw is None: mNum = None mSuffix = None else: mNum, mSuffix = common.getNumFromStr(mNumRaw) # assume that measure numbers are integers if mNum not in (None, ''): m.number = int(mNum) if mSuffix not in (None, ''): m.numberSuffix = mSuffix # fix for Finale which calls unnumbered measures X1, X2, etc. which # we convert to 1.X, 2.X, etc. without this if lastMNum is not None: if m.numberSuffix == 'X' and m.number != lastMNum + 1: newSuffix = m.numberSuffix + str(m.number) if lastMSuffix is not None: newSuffix = lastMSuffix + newSuffix m.number = lastMNum m.numberSuffix = newSuffix self.measureNumber = m.number self.numberSuffix = m.numberSuffix
[docs] def updateVoiceInformation(self): # noinspection PyShadowingNames ''' Finds all the "voice" information in <note> and <forward> tags and updates the set of `.voiceIndices` to be a set of all the voice texts, and if there is more than one voice in the measure, sets `.useVoices` to True and creates a voice for each. >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.mxMeasure = EL('<measure><note><voice>1</voice></note></measure>') >>> MP.updateVoiceInformation() Puts a set object in `.voiceIndices` >>> MP.voiceIndices {'1'} >>> MP.useVoices False >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.mxMeasure = EL('<measure><note><voice>1</voice></note>' ... '<note><voice>2</voice></note></measure>') >>> MP.updateVoiceInformation() >>> sorted(list(MP.voiceIndices)) ['1', '2'] >>> MP.useVoices True >>> len(MP.stream) 2 >>> list(MP.stream.getElementsByClass(stream.Voice)) [<music21.stream.Voice 1>, <music21.stream.Voice 2>] >>> MP = musicxml.xmlToM21.MeasureParser() >>> MP.mxMeasure = EL('<measure><note><voice>1</voice></note>' ... '<forward><voice>2</voice></forward></measure>') >>> MP.updateVoiceInformation() >>> sorted(list(MP.voiceIndices)) ['1', '2'] >>> MP.useVoices True >>> len(MP.stream) 2 >>> list(MP.stream.getElementsByClass(stream.Voice)) [<music21.stream.Voice 1>, <music21.stream.Voice 2>] ''' mxm = self.mxMeasure for tagSearch in ('note', 'forward'): for mxn in mxm.findall(tagSearch): voice = mxn.find('voice') if vIndex := strippedText(voice): self.voiceIndices.add(vIndex) # it is a set, so no need to check if already there # additional time < 1 sec per ten million ops. if len(self.voiceIndices) > 1: for vIndex in sorted(self.voiceIndices): v = stream.Voice() v.id = vIndex # TODO: should use a separate voiceId or something in Voice. self.stream.coreInsert(0.0, v) self.voicesById[v.id] = v self.useVoices = True self.stream.coreElementsChanged()
# ----------------------------------------------------------------------------- # unittests now in test_xmlToM21 if __name__ == '__main__': import music21 music21.mainTest() # doctests only