# ------------------------------------------------------------------------------
# Name: musicxml/m21ToXml.py
# Purpose: Translate from music21 objects to musicxml representation
#
# Authors: Michael Scott Asato Cuthbert
# Christopher Ariza
# Jacob Tyler Walls
#
# Copyright: Copyright © 2010-22 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Converters for music21 objects to musicxml using ElementTree
'''
from __future__ import annotations
from collections import OrderedDict
import copy
import datetime
import fractions
import io
import math
import typing as t
import warnings
from xml.etree.ElementTree import (
Element, SubElement, Comment
)
# external dependencies
import webcolors # type: ignore # no typing in module
# Imported in music21.converter. Thus, cannot import it here
from music21 import articulations
from music21 import base
from music21 import bar
from music21 import clef
from music21 import chord
from music21 import common
from music21.common.enums import AppendSpanners
from music21.common.numberTools import opFrac
from music21 import defaults
from music21 import duration
from music21 import dynamics
from music21 import environment
from music21 import exceptions21
from music21 import expressions
from music21 import harmony
from music21 import instrument
from music21 import key
from music21 import layout
from music21 import metadata
from music21 import note
from music21 import meter
from music21 import pitch
from music21 import prebase
from music21 import repeat
from music21 import spanner
from music21 import stream
from music21.stream.iterator import OffsetIterator
from music21 import style
from music21 import tempo
from music21 import text
from music21 import tie
from music21.musicxml import helpers
from music21.musicxml.partStaffExporter import PartStaffExporterMixin
from music21.musicxml import xmlObjects
from music21.musicxml.xmlObjects import MusicXMLExportException
from music21.musicxml.xmlObjects import MusicXMLWarning
environLocal = environment.Environment('musicxml.m21ToXml')
synchronizeIds = helpers.synchronizeIdsToXML
setAttributeFromAttribute = helpers.setXMLAttributeFromAttribute
if t.TYPE_CHECKING:
from music21.common.types import OffsetQL
from music21 import roman
from music21 import tablature
# ------------------------------------------------------------------------------
[docs]
def typeToMusicXMLType(value: str) -> str:
'''
Convert a music21 type to a MusicXML type or raise a MusicXMLExportException
>>> musicxml.m21ToXml.typeToMusicXMLType('longa')
'long'
>>> musicxml.m21ToXml.typeToMusicXMLType('quarter')
'quarter'
>>> musicxml.m21ToXml.typeToMusicXMLType('duplex-maxima')
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
Cannot convert "duplex-maxima" duration to MusicXML (too long).
>>> musicxml.m21ToXml.typeToMusicXMLType('inexpressible')
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
Cannot convert inexpressible durations to MusicXML.
>>> musicxml.m21ToXml.typeToMusicXMLType('zero')
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
Cannot convert durations without types to MusicXML.
'''
# MusicXML uses long instead of longa
if value == 'longa':
return 'long'
elif value == '2048th':
raise MusicXMLExportException('Cannot convert "2048th" duration to MusicXML (too short).')
elif value == 'duplex-maxima':
raise MusicXMLExportException(
'Cannot convert "duplex-maxima" duration to MusicXML (too long).')
elif value == 'inexpressible':
raise MusicXMLExportException('Cannot convert inexpressible durations to MusicXML.')
elif value == 'complex':
raise MusicXMLExportException(
'Cannot convert complex durations to MusicXML. '
+ 'Try exporting with makeNotation=True or manually running splitAtDurations()')
elif value == 'zero':
raise MusicXMLExportException('Cannot convert durations without types to MusicXML.')
else:
return value
[docs]
def normalizeColor(color: str) -> str:
'''
Normalize a css3 name to hex or leave it alone.
>>> musicxml.m21ToXml.normalizeColor('')
''
>>> musicxml.m21ToXml.normalizeColor('red')
'#FF0000'
>>> musicxml.m21ToXml.normalizeColor('#00ff00')
'#00FF00'
'''
if not color:
return ''
if '#' not in color:
return webcolors.name_to_hex(color).upper()
else:
return color.upper()
[docs]
def getMetadataFromContext(s: stream.Stream) -> metadata.Metadata|None:
# noinspection PyShadowingNames
'''
Get metadata from site or context, so that a Part
can be shown and have the rich metadata of its Score
>>> s = stream.Score()
>>> s2 = s.transpose(4)
>>> md = metadata.Metadata()
>>> md.title = 'emptiness'
>>> s.metadata = md
>>> s2.metadata is None
True
>>> musicxml.m21ToXml.getMetadataFromContext(s2).title
'emptiness'
>>> musicxml.m21ToXml.getMetadataFromContext(s).title
'emptiness'
>>> p = stream.Part()
>>> s2.insert(0, p)
>>> musicxml.m21ToXml.getMetadataFromContext(p).title
'emptiness'
'''
# get metadata from context.
md = s.metadata
if md is not None:
return md
for contextSite in s.contextSites():
if contextSite.site.metadata is not None:
return contextSite.site.metadata
return None
def _setTagTextFromAttribute(
m21El: t.Any,
xmlEl: Element,
tag: str,
attributeName: str|None = None,
*,
transform: t.Callable[[t.Any], t.Any]|None = None,
forceEmpty: bool = False
) -> Element|None:
'''
If m21El has an attribute called attributeName, create a new SubElement
for xmlEl and set its text to the value of the m21El attribute.
Pass a function or lambda function as transform to transform the
value before setting it. String transformation is assumed.
Returns the SubElement
Will not create an empty element unless forceEmpty is True
>>> from xml.etree.ElementTree import Element
>>> e = Element('accidental')
>>> seta = musicxml.m21ToXml._setTagTextFromAttribute
>>> acc = pitch.Accidental()
>>> acc.alter = -2.0
>>> subEl = seta(acc, e, 'alter')
>>> subEl.text
'-2.0'
>>> subEl in e
True
>>> XB = musicxml.m21ToXml.XMLExporterBase()
>>> XB.dump(e)
<accidental>
<alter>-2.0</alter>
</accidental>
add a transform
>>> subEl = seta(acc, e, 'alter', transform=int)
>>> subEl.text
'-2'
'''
if attributeName is None:
attributeName = common.hyphenToCamelCase(tag)
try:
value = getattr(m21El, attributeName)
except AttributeError:
return None
if transform is not None:
value = transform(value)
if value in (None, '') and forceEmpty is not True:
return None
subElement = SubElement(xmlEl, tag)
if value is not None:
subElement.text = str(value)
return subElement
[docs]
class GeneralObjectExporter:
'''
Packs any `Music21Object` into a well-formed score and exports
a bytes object MusicXML representation.
'''
classMapping = OrderedDict([
('Score', 'fromScore'),
('Part', 'fromPart'),
('Measure', 'fromMeasure'),
('Voice', 'fromVoice'),
('Stream', 'fromStream'),
# ## individual parts
('GeneralNote', 'fromGeneralNote'),
('Pitch', 'fromPitch'),
('Duration', 'fromDuration'), # not an m21 object
('Dynamic', 'fromDynamic'),
('DiatonicScale', 'fromDiatonicScale'),
('Scale', 'fromScale'),
('Music21Object', 'fromMusic21Object'),
])
def __init__(self, obj: prebase.ProtoM21Object|None = None):
self.generalObj = obj
self.makeNotation: bool = True
[docs]
def parse(self, obj: prebase.ProtoM21Object|None = None) -> bytes:
r'''
Return a bytes object representation of anything from a
Score to a single pitch.
When :attr:`makeNotation` is True (default), wraps `obj` in a well-formed
`Score`, makes a copy, and runs :meth:`~music21.stream.base.Stream.makeNotation`
on each of the parts. To skip copying and making notation, set `.makeNotation`
on this instance to False.
>>> p = pitch.Pitch('D#4')
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter(p)
>>> out = GEX.parse() # out is bytes
>>> outStr = out.decode('utf-8') # now is string
>>> print(outStr.strip())
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE score-partwise
PUBLIC "-//Recordare//DTD MusicXML ... Partwise//EN"
"http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="...">
<movement-title>Music21 Fragment</movement-title>
<identification>
<creator type="composer">Music21</creator>
<encoding>
<encoding-date>...</encoding-date>
<software>music21 v...</software>
<supports element="beam" type="yes" />
<supports element="stem" type="yes" />
<supports element="accidental" type="yes" />
</encoding>
</identification>
<defaults>
<scaling>
<millimeters>7</millimeters>
<tenths>40</tenths>
</scaling>
</defaults>
<part-list>
<score-part id="...">
<part-name />
</score-part>
</part-list>
<!--=========================== Part 1 ===========================-->
<part id="...">
<!--========================= Measure 1 ==========================-->
<measure implicit="no" number="1">
<attributes>
<divisions>10080</divisions>
<time>
<beats>1</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>G</sign>
<line>2</line>
</clef>
</attributes>
<note>
<pitch>
<step>D</step>
<alter>1</alter>
<octave>4</octave>
</pitch>
<duration>10080</duration>
<type>quarter</type>
<accidental>sharp</accidental>
</note>
</measure>
</part>
</score-partwise>
'''
if obj is None:
obj = self.generalObj
if obj is None:
raise MusicXMLExportException('Must have an object to export')
if self.makeNotation:
outObj = self.fromGeneralObject(obj)
return self.parseWellformedObject(outObj)
else:
if not isinstance(obj, stream.Score):
raise MusicXMLExportException('Can only export Scores with makeNotation=False')
return self.parseWellformedObject(obj)
[docs]
def fromGeneralObject(self, obj: prebase.ProtoM21Object):
'''
Converts any Music21Object (or a duration or a pitch) to something that
can be passed to :class:`ScoreExporter`.
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter()
>>> s = GEX.fromGeneralObject(duration.Duration(3.0))
>>> s
<music21.stream.Score 0x...>
>>> s.show('t')
{0.0} <music21.stream.Part 0x...>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 6/8>
{0.0} <music21.note.Note C>
{3.0} <music21.bar.Barline type=final>
>>> s[note.NotRest].first().duration
<music21.duration.Duration 3.0>
* Changed in v8: fills gaps with rests before calling makeNotation
to avoid duplicating effort with :meth:`PartExporter.fixupNotationMeasured`.
>>> v = stream.Voice(note.Note())
>>> m = stream.Measure([meter.TimeSignature(), v])
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter(m)
>>> out = GEX.parse() # out is bytes
>>> outStr = out.decode('utf-8') # now is string
>>> '<note print-object="no" print-spacing="yes">' in outStr
True
>>> len(v[note.Rest]) # original stream unchanged
0
'''
classes = obj.classes
outObj = None
if isinstance(obj, stream.Stream) and self.makeNotation:
# This is where the first (and hopefully only) copy is made.
obj = obj.makeRests(refStreamOrTimeRange=[0.0, obj.highestTime],
fillGaps=True,
inPlace=False,
hideRests=True, # just to fill up MusicXML display
timeRangeFromBarDuration=True,
)
for cM, methName in self.classMapping.items():
if cM in classes:
meth = getattr(self, methName)
outObj = meth(obj)
break
if outObj is None:
raise MusicXMLExportException(
'Cannot translate the object '
f'{self.generalObj} to a complete musicXML document; put it in a Stream first!'
)
return outObj
[docs]
def fromScore(self, sc):
'''
Runs :meth:`~music21.stream.Score.makeNotation` on the copy.
'''
if self.makeNotation:
sc.makeNotation(inPlace=True)
if not sc.isWellFormedNotation():
warnings.warn(
f'{sc} is not well-formed; see isWellFormedNotation()',
category=MusicXMLWarning,
stacklevel=2,
)
# sc.makeImmutable()
return sc
[docs]
def fromPart(self, p):
'''
From a part, put it in a new score.
'''
if p.isFlat:
p.makeMeasures(inPlace=True)
# p.makeImmutable() # impossible, we haven't made notation yet.
sc = stream.Score()
sc.insert(0, p)
sc.metadata = copy.deepcopy(getMetadataFromContext(p))
return self.fromScore(sc)
[docs]
def fromMeasure(self, m):
'''
Translate a music21 Measure into a
complete MusicXML string representation.
Note: this method is called for complete MusicXML
representation of a Measure, not for partial
solutions in Part or Stream production.
'''
m.coreGatherMissingSpanners()
m.makeNotation(inPlace=True)
if m.style.measureNumbering is None:
# Provide a default
m.style.measureNumbering = 'measure'
clef_from_measure_start_or_context = m.getContextByClass(
clef.Clef,
getElementMethod=common.enums.ElementSearch.AT_OR_BEFORE_OFFSET
)
if clef_from_measure_start_or_context is None:
m.clef = clef.bestClef(m, recurse=True)
else:
m.clef = clef_from_measure_start_or_context
p = stream.Part()
p.append(m)
p.metadata = copy.deepcopy(getMetadataFromContext(m))
context_part = m.getContextByClass(stream.Part)
if context_part is not None:
p.partName = context_part.partName
p.partAbbreviation = context_part.partAbbreviation
return self.fromPart(p)
[docs]
def fromVoice(self, v):
m = stream.Measure(number=1)
m.insert(0, v)
return self.fromMeasure(m)
[docs]
def fromStream(self, st):
if st.isFlat:
st2 = stream.Part()
st2.mergeAttributes(st)
st2.elements = copy.deepcopy(st)
if not st.getElementsByClass(clef.Clef).getElementsByOffset(0.0):
st2.clef = clef.bestClef(st2)
st2.makeNotation(inPlace=True)
st2.metadata = copy.deepcopy(getMetadataFromContext(st))
return self.fromPart(st2)
elif st.hasPartLikeStreams():
st2 = stream.Score()
st2.mergeAttributes(st)
st2.elements = copy.deepcopy(st)
st2.makeNotation(inPlace=True)
st2.metadata = copy.deepcopy(getMetadataFromContext(st))
return self.fromScore(st2)
elif st.getElementsByClass(stream.Stream).first().isFlat: # like a part w/ measures
st2 = stream.Part()
st2.mergeAttributes(st)
st2.elements = copy.deepcopy(st)
if not st.getElementsByClass(clef.Clef).getElementsByOffset(0.0):
bestClef = True
else:
bestClef = False
st2.makeNotation(inPlace=True, bestClef=bestClef)
st2.metadata = copy.deepcopy(getMetadataFromContext(st))
return self.fromPart(st2)
else:
# probably a problem? or a voice
if not st.getElementsByClass(clef.Clef).getElementsByOffset(0.0):
bestClef = True
else:
bestClef = False
st2 = st.makeNotation(inPlace=False, bestClef=bestClef)
return self.fromScore(st2)
[docs]
def fromDuration(self, d):
'''
Translate a music21 :class:`~music21.duration.Duration` into
a complete MusicXML representation.
Rarely, rarely used. Only if you call .show() on a duration object
'''
# make a copy, as we this process will change tuple types
# not needed, since fromGeneralNote does it too. but so
# rarely used, it doesn't matter, and the extra safety is nice.
dCopy = copy.deepcopy(d)
n = note.Note()
n.duration = dCopy
# call the musicxml property on Stream
return self.fromGeneralNote(n)
[docs]
def fromDynamic(self, dynamicObject):
'''
Provide a complete MusicXML string from a single dynamic by
putting it into a Stream first.
'''
dCopy = copy.deepcopy(dynamicObject)
out = stream.Stream()
out.append(dCopy)
# call the musicxml property on Stream
return self.fromStream(out)
[docs]
def fromScale(self, scaleObject):
# noinspection PyShadowingNames
'''
Generate the pitches from this scale
and put it into a stream.Measure, then call
fromMeasure on it.
>>> cMaj = scale.MajorScale('C')
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter()
>>> m = GEX.fromScale(cMaj)
>>> m
<music21.stream.Score 0x11d4f17b8>
>>> m.show('text')
{0.0} <music21.stream.Part 0x116a04b38>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 10/4>
{0.0} <music21.note.Note C>
{4.0} <music21.note.Note D>
{5.0} <music21.note.Note E>
{6.0} <music21.note.Note F>
{7.0} <music21.note.Note G>
{8.0} <music21.note.Note A>
{9.0} <music21.note.Note B>
'''
m = stream.Measure(number=1)
for i in range(1, scaleObject.abstract.getDegreeMaxUnique() + 1):
p = scaleObject.pitchFromDegree(i)
n = note.Note()
n.pitch = p
if i == 1:
n.addLyric(scaleObject.name)
if p.name == scaleObject.getTonic().name:
n.quarterLength = 4 # set longer
else:
n.quarterLength = 1
m.append(n)
m.timeSignature = m.bestTimeSignature()
return self.fromMeasure(m)
[docs]
def fromDiatonicScale(self, diatonicScaleObject):
# noinspection PyShadowingNames
'''
Return a complete musicxml of the DiatonicScale
Overrides the general scale behavior to highlight
the tonic and dominant.
>>> cMaj = scale.MajorScale('C')
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter()
>>> m = GEX.fromDiatonicScale(cMaj)
>>> m
<music21.stream.Score 0x11d4f17b8>
>>> m.show('text')
{0.0} <music21.stream.Part 0x116a04b38>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 11/4>
{0.0} <music21.note.Note C>
{4.0} <music21.note.Note D>
{5.0} <music21.note.Note E>
{6.0} <music21.note.Note F>
{7.0} <music21.note.Note G>
{9.0} <music21.note.Note A>
{10.0} <music21.note.Note B>
'''
m = stream.Measure(number=1)
for i in range(1, diatonicScaleObject.abstract.getDegreeMaxUnique() + 1):
p = diatonicScaleObject.pitchFromDegree(i)
n = note.Note()
n.pitch = p
if i == 1:
n.addLyric(diatonicScaleObject.name)
if p.name == diatonicScaleObject.getTonic().name:
n.quarterLength = 4 # set longer
elif p.name == diatonicScaleObject.getDominant().name:
n.quarterLength = 2 # set longer
else:
n.quarterLength = 1
m.append(n)
m.timeSignature = m.bestTimeSignature()
return self.fromMeasure(m)
[docs]
def fromMusic21Object(self, obj):
'''
return a single TimeSignature as a musicxml document
'''
# return a complete musicxml representation
objCopy = copy.deepcopy(obj)
# m = stream.Measure()
# m.timeSignature = tsCopy
# m.append(note.Rest())
out = stream.Measure(number=1)
out.append(objCopy)
return self.fromMeasure(out)
[docs]
def fromGeneralNote(self, n):
# noinspection PyShadowingNames
'''
Translate a music21 :class:`~music21.note.Note` into an object
ready to be parsed.
An attempt is made to find the best TimeSignature for quarterLengths
<= 6:
>>> n = note.Note('c3')
>>> n.quarterLength = 3
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter()
>>> sc = GEX.fromGeneralNote(n)
>>> sc.show('t')
{0.0} <music21.stream.Part 0x1046afa90>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.BassClef>
{0.0} <music21.meter.TimeSignature 6/8>
{0.0} <music21.note.Note C>
{3.0} <music21.bar.Barline type=final>
But longer notes will be broken into tied components placed in
4/4 measures:
>>> long_note = note.Note('e5', quarterLength=40)
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter()
>>> sc = GEX.fromGeneralNote(long_note)
>>> sc[meter.TimeSignature].first()
<music21.meter.TimeSignature 4/4>
>>> len(sc[stream.Measure])
10
'''
# make a copy, as this process will change tuple types
# this method is called infrequently (only displaying a single note)
nCopy = copy.deepcopy(n)
new_part = stream.Part(nCopy)
if 0 < n.quarterLength <= 6.0:
new_part.insert(0, meter.bestTimeSignature(new_part))
stream.makeNotation.makeMeasures(
new_part,
inPlace=True,
refStreamOrTimeRange=[0, nCopy.quarterLength]
)
stream.makeNotation.makeTupletBrackets(new_part, inPlace=True)
return self.fromPart(new_part)
[docs]
def fromPitch(self, p: pitch.Pitch):
# noinspection PyShadowingNames
'''
Translate a music21 :class:`~music21.pitch.Pitch` into an object
ready to be parsed.
>>> p = pitch.Pitch('c#3')
>>> GEX = musicxml.m21ToXml.GeneralObjectExporter()
>>> sc = GEX.fromPitch(p)
>>> sc.show('t')
{0.0} <music21.stream.Part 0x1046afa90>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.BassClef>
{0.0} <music21.meter.TimeSignature 1/4>
{0.0} <music21.note.Note C#>
'''
n = note.Note()
n.pitch = copy.deepcopy(p)
out = stream.Measure(number=1)
out.append(n)
# call the musicxml property on Stream
return self.fromMeasure(out)
[docs]
class XMLExporterBase:
'''
contains functions that could be called
at multiple levels of exporting (Score, Part, Measure).
'''
def __init__(self) -> None:
self.xmlRoot = Element('override-me-in-subclasses')
self.stream: stream.Stream = stream.Stream()
[docs]
def asBytes(self, noCopy=True) -> bytes:
'''
returns the xmlRoot as a bytes object. If noCopy is True
(default), modifies the file for pretty-printing in place. Otherwise,
make a copy.
'''
sio = io.BytesIO()
sio.write(self.xmlHeader())
rootObj = self.xmlRoot
rootObj_string = helpers.dumpString(rootObj, noCopy=noCopy)
sio.write(rootObj_string.encode('utf-8'))
v = sio.getvalue()
sio.close()
return v
# ------------------------------------------------------------------------------
[docs]
@staticmethod
def dump(obj):
return helpers.dump(obj)
# style attributes
[docs]
def setStyleAttributes(self, mxObject, m21Object, musicXMLNames, m21Names=None):
'''
Sets any attribute from .style, doing some conversions.
m21Object can also be a style.Style object itself.
'''
if isinstance(m21Object, style.Style):
stObj = m21Object
elif m21Object.hasStyleInformation is False:
return
else:
stObj = m21Object.style
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):
try:
m21Value = getattr(stObj, m21Name)
except AttributeError:
continue
if m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL:
m21Value = xmlObjects.booleanToYesNo(m21Value)
if m21Value is None:
continue
try:
m21Value = str(m21Value)
except ValueError:
continue
mxObject.set(xmlName, m21Value)
[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')
self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names)
self.setPrintStyleAlign(mxObject, m21Object)
[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 setPrintObject(self, mxObject, m21Object):
'''
sets print-object to 'no' if m21Object.style.hideObjectOnPrint is True
or if m21Object is a StyleObject and has .hideObjectOnPrint set to True.
'''
if isinstance(m21Object, style.Style):
st = m21Object
elif m21Object.hasStyleInformation:
st = m21Object.style
else:
return
if st.hideObjectOnPrint:
mxObject.set('print-object', 'no')
[docs]
def setColor(self, mxObject, m21Object):
'''
Sets mxObject['color'] to a normalized version of m21Object.style.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')
if 'color' in mxObject.attrib: # set
mxObject.attrib['color'] = normalizeColor(mxObject.attrib['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
>>> XB = musicxml.m21ToXml.XMLExporterBase()
>>> mxObj = EL('<text>hi</text>')
>>> te = expressions.TextExpression('hi!')
>>> te.style.fontFamily = ['Courier', 'monospaced']
>>> te.style.fontStyle = 'italic'
>>> te.style.fontSize = 24.0
>>> XB.setFont(mxObj, te)
>>> XB.dump(mxObj)
<text font-family="Courier,monospaced" font-size="24" font-style="italic">hi</text>
>>> XB = musicxml.m21ToXml.XMLExporterBase()
>>> mxObj = EL('<text>hi</text>')
>>> te = expressions.TextExpression('hi!')
>>> te.style.fontStyle = 'bold'
>>> XB.setFont(mxObj, te)
>>> XB.dump(mxObj)
<text font-weight="bold">hi</text>
>>> te.style.fontStyle = 'bolditalic'
>>> XB.setFont(mxObj, te)
>>> XB.dump(mxObj)
<text font-style="italic" font-weight="bold">hi</text>
'''
musicXMLNames = ('font-style', 'font-size', 'font-weight')
m21Names = ('fontStyle', 'fontSize', 'fontWeight')
self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names)
if isinstance(m21Object, style.Style):
st = m21Object
elif m21Object.hasStyleInformation:
st = m21Object.style
else:
return
if hasattr(st, 'fontStyle'):
# mxml does not support bold or bolditalic as font-style value
if st.fontStyle == 'bold':
mxObject.set('font-weight', 'bold')
mxObject.attrib.pop('font-style', None)
elif st.fontStyle == 'bolditalic':
mxObject.set('font-weight', 'bold')
mxObject.set('font-style', 'italic')
if hasattr(st, 'fontFamily') and st.fontFamily:
if common.isIterable(st.fontFamily):
mxObject.set('font-family', ','.join(st.fontFamily))
else:
mxObject.set('font-family', st.fontFamily)
[docs]
def setPosition(self, mxObject, m21Object):
'''
set positioning information for an mxObject from
default-x, default-y, relative-x, relative-y from
the .style attribute's absoluteX, relativeX, etc. attributes.
'''
musicXMLNames = ('default-x', 'default-y', 'relative-x', 'relative-y')
m21Names = ('absoluteX', 'absoluteY', 'relativeX', 'relativeY')
self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names)
[docs]
def setEditorial(self, mxObject, m21Object):
'''
>>> from xml.etree.ElementTree import fromstring as EL
>>> XB = musicxml.m21ToXml.XMLExporterBase()
>>> mxObj = EL('<note />')
>>> n = note.Note('C-5')
Most common case: does nothing
>>> XB.setEditorial(mxObj, n)
>>> XB.dump(mxObj)
<note />
>>> fn = editorial.Comment('flat is obvious error for sharp')
>>> fn.levelInformation = 2
>>> fn.isFootnote = True
>>> n.editorial.footnotes.append(fn)
>>> XB.setEditorial(mxObj, n)
>>> XB.dump(mxObj)
<note>
<footnote>flat is obvious error for sharp</footnote>
<level reference="no">2</level>
</note>
Placing information in `.editorial.comments` only puts out the level:
>>> mxObj = EL('<note />')
>>> n = note.Note('C-5')
>>> com = editorial.Comment('flat is obvious error for sharp')
>>> com.levelInformation = 'hello'
>>> com.isReference = True
>>> n.editorial.comments.append(com)
>>> XB.setEditorial(mxObj, n)
>>> XB.dump(mxObj)
<note>
<level reference="yes">hello</level>
</note>
'''
if m21Object.hasEditorialInformation is False:
return
# MusicXML allows only one footnote or level, so we take the first.
e = m21Object.editorial
if 'footnotes' not in e and 'comments' not in e:
return
makeFootnote = False
if e.footnotes:
c = e.footnotes[0]
makeFootnote = True
elif e.comments:
c = e.comments[0]
else:
return
if makeFootnote:
mxFn = SubElement(mxObject, 'footnote')
self.setTextFormatting(mxFn, c)
if c.text is not None:
mxFn.text = c.text
if c.levelInformation is not None:
mxLevel = SubElement(mxObject, 'level')
mxLevel.text = str(c.levelInformation)
mxLevel.set('reference', xmlObjects.booleanToYesNo(c.isReference))
# TODO: attr: parentheses
# TODO: attr: bracket
# TODO: attr: size
###################
[docs]
def pageLayoutToXmlPrint(self, pageLayout, mxPrintIn=None):
# noinspection PyShadowingNames
'''
Given a PageLayout object, set object data for <print>
>>> pl = layout.PageLayout()
>>> pl.pageHeight = 4000
>>> pl.isNew = True
>>> pl.rightMargin = 30.25
>>> pl.leftMargin = 20
>>> pl.pageNumber = 5
>>> XPBase = musicxml.m21ToXml.XMLExporterBase()
>>> mxPrint = XPBase.pageLayoutToXmlPrint(pl)
>>> XPBase.dump(mxPrint)
<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>
>>> MP = musicxml.xmlToM21.MeasureParser()
>>> pl2 = MP.xmlPrintToPageLayout(mxPrint)
>>> pl2.isNew
True
>>> pl2.rightMargin
30.25
>>> pl2.leftMargin
20
>>> pl2.pageNumber
5
>>> pl2.pageHeight
4000
'''
if mxPrintIn is None:
mxPrint = Element('print')
else:
mxPrint = mxPrintIn
setb = setAttributeFromAttribute
setb(pageLayout, mxPrint, 'new-page', 'isNew', transform=xmlObjects.booleanToYesNo)
setb(pageLayout, mxPrint, 'page-number')
mxPageLayout = self.pageLayoutToXmlPageLayout(pageLayout)
if len(mxPageLayout):
mxPrint.append(mxPageLayout)
if mxPrintIn is None:
return mxPrint
[docs]
def pageLayoutToXmlPageLayout(self, pageLayout, mxPageLayoutIn=None):
'''
get a <page-layout> element from a PageLayout
Called out from pageLayoutToXmlPrint because it
is also used in the <defaults> tag
'''
if mxPageLayoutIn is None:
mxPageLayout = Element('page-layout')
else:
mxPageLayout = mxPageLayoutIn
seta = _setTagTextFromAttribute
seta(pageLayout, mxPageLayout, 'page-height')
seta(pageLayout, mxPageLayout, 'page-width')
# TODO -- record even, odd, both margins
mxPageMargins = Element('page-margins')
for direction in ('left', 'right', 'top', 'bottom'):
seta(pageLayout, mxPageMargins, direction + '-margin')
if len(mxPageMargins):
mxPageLayout.append(mxPageMargins)
if mxPageLayoutIn is None:
return mxPageLayout
[docs]
def systemLayoutToXmlPrint(self, systemLayout, mxPrintIn=None):
# noinspection PyShadowingNames
'''
Given a SystemLayout tag, set a <print> tag
>>> sl = layout.SystemLayout()
>>> sl.distance = 55
>>> sl.isNew = True
>>> sl.rightMargin = 30.25
>>> sl.leftMargin = 20
>>> XPBase = musicxml.m21ToXml.XMLExporterBase()
>>> mxPrint = XPBase.systemLayoutToXmlPrint(sl)
>>> XPBase.dump(mxPrint)
<print new-system="yes">
<system-layout>
<system-margins>
<left-margin>20</left-margin>
<right-margin>30.25</right-margin>
</system-margins>
<system-distance>55</system-distance>
</system-layout>
</print>
Test return conversion
>>> MP = musicxml.xmlToM21.MeasureParser()
>>> sl2 = MP.xmlPrintToSystemLayout(mxPrint)
>>> sl2.isNew
True
>>> sl2.rightMargin
30.25
>>> sl2.leftMargin
20
>>> sl2.distance
55
'''
if mxPrintIn is None:
mxPrint = Element('print')
else:
mxPrint = mxPrintIn
setb = setAttributeFromAttribute
setb(systemLayout, mxPrint, 'new-system', 'isNew', transform=xmlObjects.booleanToYesNo)
mxSystemLayout = Element('system-layout')
self.systemLayoutToXmlSystemLayout(systemLayout, mxSystemLayout)
if len(mxSystemLayout):
mxPrint.append(mxSystemLayout)
if mxPrintIn is None:
return mxPrint
[docs]
def systemLayoutToXmlSystemLayout(self, systemLayout, mxSystemLayoutIn=None):
'''
get given a SystemLayout object configure <system-layout> or <print>
Called out from xmlPrintToSystemLayout because it
is also used in the <defaults> tag
>>> sl = layout.SystemLayout()
>>> sl.distance = 40.0
>>> sl.topDistance = 70.0
>>> XPBase = musicxml.m21ToXml.XMLExporterBase()
>>> mxSl = XPBase.systemLayoutToXmlSystemLayout(sl)
>>> XPBase.dump(mxSl)
<system-layout>
<system-distance>40.0</system-distance>
<top-system-distance>70.0</top-system-distance>
</system-layout>
>>> sl = layout.SystemLayout()
>>> sl.leftMargin = 30.0
>>> mxSl = XPBase.systemLayoutToXmlSystemLayout(sl)
>>> XPBase.dump(mxSl)
<system-layout>
<system-margins>
<left-margin>30.0</left-margin>
</system-margins>
</system-layout>
'''
if mxSystemLayoutIn is None:
mxSystemLayout = Element('system-layout')
else:
mxSystemLayout = mxSystemLayoutIn
seta = _setTagTextFromAttribute
# TODO -- record even, odd, both margins
mxSystemMargins = Element('system-margins')
for direction in ('top', 'bottom', 'left', 'right'):
seta(systemLayout, mxSystemMargins, direction + '-margin')
if len(mxSystemMargins):
mxSystemLayout.append(mxSystemMargins)
seta(systemLayout, mxSystemLayout, 'system-distance', 'distance')
seta(systemLayout, mxSystemLayout, 'top-system-distance', 'topDistance')
# TODO: system-dividers
if mxSystemLayoutIn is None:
return mxSystemLayout
[docs]
def staffLayoutToXmlStaffLayout(self, staffLayout, mxStaffLayoutIn=None):
'''
get a <staff-layout> tag from a StaffLayout object
In music21, the <staff-layout> and <staff-details> are
intertwined in a StaffLayout object.
>>> sl = layout.StaffLayout()
>>> sl.distance = 40.0
>>> sl.staffNumber = 1
>>> XPBase = musicxml.m21ToXml.XMLExporterBase()
>>> mxSl = XPBase.staffLayoutToXmlStaffLayout(sl)
>>> XPBase.dump(mxSl)
<staff-layout number="1">
<staff-distance>40.0</staff-distance>
</staff-layout>
'''
if mxStaffLayoutIn is None:
mxStaffLayout = Element('staff-layout')
else:
mxStaffLayout = mxStaffLayoutIn
seta = _setTagTextFromAttribute
setb = setAttributeFromAttribute
seta(staffLayout, mxStaffLayout, 'staff-distance', 'distance')
# ET.dump(mxStaffLayout)
setb(staffLayout, mxStaffLayout, 'number', 'staffNumber')
if mxStaffLayoutIn is None:
return mxStaffLayout
[docs]
def accidentalToMx(self, a: pitch.Accidental, *, elName: str = 'accidental') -> Element:
'''
Convert a pitch.Accidental object to a Element of tag 'accidental' (or other name)
>>> acc = pitch.Accidental()
>>> acc.set('half-sharp')
>>> acc.alter == 0.5
True
>>> XB = musicxml.m21ToXml.XMLExporterBase()
>>> a2m = XB.accidentalToMx
>>> XB.dump(a2m(acc))
<accidental>quarter-sharp</accidental>
>>> acc.set('double-flat')
>>> XB.dump(a2m(acc))
<accidental>flat-flat</accidental>
>>> acc.set('one-and-a-half-sharp')
>>> XB.dump(a2m(acc, elName='accidental-mark'))
<accidental-mark>three-quarters-sharp</accidental-mark>
>>> acc.set('half-flat')
>>> XB.dump(a2m(acc))
<accidental>quarter-flat</accidental>
>>> acc.set('one-and-a-half-flat')
>>> XB.dump(a2m(acc))
<accidental>three-quarters-flat</accidental>
>>> acc.set('sharp')
>>> acc.displayStyle = 'parentheses'
>>> XB.dump(a2m(acc))
<accidental parentheses="yes">sharp</accidental>
>>> acc.displayStyle = 'bracket'
>>> XB.dump(a2m(acc))
<accidental bracket="yes">sharp</accidental>
>>> acc.displayStyle = 'both'
>>> XB.dump(a2m(acc))
<accidental bracket="yes" parentheses="yes">sharp</accidental>
>>> acc = pitch.Accidental('flat')
>>> acc.style.relativeX = -2
>>> XB.dump(a2m(acc))
<accidental relative-x="-2">flat</accidental>
>>> acc = pitch.Accidental()
>>> acc.name = 'double-sharp-down' # musicxml 3.1
>>> XB.dump(a2m(acc))
<accidental>double-sharp-down</accidental>
>>> acc.name = 'funnyAccidental' # unknown
>>> XB.dump(a2m(acc))
<accidental>other</accidental>
'''
# noinspection PyShadowingNames
otherMusicXMLAccidentals = (
# v. 3.1
'double-sharp-down', 'double-sharp-up',
'flat-flat-down', 'flat-flat-up',
'arrow-down', 'arrow-up',
'other',
# v. 3.0
'sharp-down', 'sharp-up',
'natural-down', 'natural-up',
'flat-down', 'flat-up',
'slash-quarter-sharp', 'slash-sharp',
'slash-flat', 'double-slash-flat',
'sharp-1', 'sharp-2',
'sharp-3', 'sharp-5',
'flat-1', 'flat-2',
'flat-3', 'flat-4',
'sori', 'koron',
)
if a.name == 'half-sharp':
mxName = 'quarter-sharp'
elif a.name == 'one-and-a-half-sharp':
mxName = 'three-quarters-sharp'
elif a.name == 'half-flat':
mxName = 'quarter-flat'
elif a.name == 'one-and-a-half-flat':
mxName = 'three-quarters-flat'
elif a.name == 'double-flat':
mxName = 'flat-flat'
else: # all others are the same
mxName = a.name
if (mxName not in pitch.accidentalNameToModifier
and mxName not in otherMusicXMLAccidentals):
mxName = 'other'
mxAccidental = Element(elName)
# need to remove display in this case and return None
# if self.displayStatus == False:
# pass
mxAccidental.text = mxName
if a.displayStyle in ('parentheses', 'both'):
mxAccidental.set('parentheses', 'yes')
if a.displayStyle in ('bracket', 'both'):
mxAccidental.set('bracket', 'yes')
self.setPrintStyle(mxAccidental, a)
return mxAccidental
[docs]
def accidentalToMxAccidentalMark(self, a: pitch.Accidental) -> Element:
return self.accidentalToMx(a, elName='accidental-mark')
# ---------
[docs]
class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
'''
Convert a Score (or outer stream with .parts) into
a musicxml Element.
'''
def __init__(self, score: stream.Score|None = None, makeNotation: bool = True):
super().__init__()
if score is None:
# should not be done this way.
self.stream = stream.Score()
else:
self.stream = score
self.xmlRoot = Element('score-partwise', version=defaults.musicxmlVersion)
self.mxIdentification: Element|None = None
self.scoreMetadata: metadata.Metadata|None = None
self.spannerBundle: spanner.SpannerBundle|None = None
self.meterStream: stream.Stream[meter.TimeSignatureBase]|None = None
self.scoreLayouts: list[layout.ScoreLayout] = []
self.firstScoreLayout: layout.ScoreLayout|None = None
self.textBoxes: list[text.TextBox] = []
self.highestTime = 0.0
self.refStreamOrTimeRange = [0.0, self.highestTime]
self.partExporterList: list[PartExporter] = []
self.groupsToJoin: list[layout.StaffGroup] = []
# key = id(stream) (NB: not stream.id); value = .instrumentStream
self.instrumentsByStream: dict[int, stream.Stream] = {}
self.instrumentList: list[instrument.Instrument] = []
self.instrumentIdList: list[str|None] = []
self.midiChannelList: list[int|None] = [] # TODO: just list[int]?
self.parts: list[stream.Part] = []
self.makeNotation: bool = makeNotation
[docs]
def parse(self) -> Element:
'''
the main function to call.
If self.stream is empty, call self.emptyObject(). Otherwise,
convert sounding to written pitch,
set scorePreliminaries(), call parsePartlikeScore or parseFlatScore, then postPartProcess(),
clean up circular references for garbage collection, and returns the <score-partwise>
object.
>>> b = corpus.parse('bwv66.6')
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
>>> mxScore = SX.parse()
>>> SX.dump(mxScore)
<score-partwise version="...">...</score-partwise>
'''
s = self.stream
if not s:
return self.emptyObject()
# A copy was already made or elected NOT to be made.
s.toWrittenPitch(inPlace=True, ottavasToSounding=True)
self.scorePreliminaries()
if s.hasPartLikeStreams():
# Pre-populate partExporterList so that joinable groups can be identified
# before attempting to identify and count instruments
self._populatePartExporterList()
self.groupsToJoin = self.joinableGroups()
self.setPartExporterStaffGroups()
self.renumberVoicesWithinStaffGroups()
self.parsePartlikeScore()
else:
self.parseFlatScore()
self.postPartProcess()
# clean up for circular references.
self.partExporterList.clear()
return self.xmlRoot
[docs]
def emptyObject(self) -> Element:
'''
Creates a cheeky "This Page Intentionally Left Blank" for a blank score
>>> emptySX = musicxml.m21ToXml.ScoreExporter()
>>> mxScore = emptySX.parse() # will call emptyObject
>>> emptySX.dump(mxScore)
<score-partwise version="...">
<work>
<work-title>This Page Intentionally Left Blank</work-title>
</work>
...
<note>
<rest />
<duration>40320</duration>
<type>whole</type>
</note>
</measure>
</part>
</score-partwise>
'''
out = stream.Score()
p = stream.Part()
m = stream.Measure()
r = note.Rest(quarterLength=4)
m.append(r)
p.append(m)
out.append(p)
# return the processing of this Stream
md = metadata.Metadata(title='This Page Intentionally Left Blank')
out.insert(0, md)
# recursive call to this non-empty stream
self.stream = out
return self.parse()
[docs]
def scorePreliminaries(self) -> None:
'''
Populate the exporter object with
`meterStream`, `scoreLayouts`, `spannerBundle`, and `textBoxes`
>>> emptySX = musicxml.m21ToXml.ScoreExporter()
>>> emptySX.scorePreliminaries() # will call emptyObject
>>> len(emptySX.textBoxes)
0
>>> emptySX.spannerBundle
<music21.spanner.SpannerBundle of size 0>
'''
self.setScoreLayouts()
self.setMeterStream()
self.setPartsAndRefStream()
# get all text boxes
self.textBoxes = list(self.stream[text.TextBox])
# we need independent sub-stream elements to shift in presentation
self.highestTime = 0.0 # redundant, but set here.
if self.spannerBundle is None:
self.spannerBundle = self.stream.spannerBundle
[docs]
def setPartsAndRefStream(self) -> None:
'''
Transfers the offset of the inner stream to elements and sets self.highestTime
>>> b = corpus.parse('bwv66.6')
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
>>> SX.highestTime
0.0
>>> SX.setPartsAndRefStream()
>>> SX.highestTime
36.0
>>> SX.refStreamOrTimeRange
[0.0, 36.0]
>>> len(SX.parts)
4
>>> isinstance(SX.parts[0], stream.Part)
True
>>> b.insert(stream.Score())
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
>>> SX.setPartsAndRefStream()
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
Exporting scores nested inside scores is not supported
'''
s = t.cast(stream.Score, self.stream)
# environLocal.printDebug('streamToMx(): interpreting multipart')
streamOfStreams = s.getElementsByClass(stream.Stream)
for innerStream in streamOfStreams:
# may need to copy element here
# apply this stream's offset to elements
# but retain offsets if inner stream is a Measure
# https://github.com/cuthbertLab/music21/issues/580
if isinstance(innerStream, stream.Measure):
ht = innerStream.offset + innerStream.highestTime
elif isinstance(innerStream, (stream.Score, stream.Opus)):
raise MusicXMLExportException(
'Exporting scores nested inside scores is not supported')
else:
innerStream.transferOffsetToElements()
ht = innerStream.highestTime
self.highestTime = max(self.highestTime, ht)
self.refStreamOrTimeRange = [0.0, self.highestTime]
self.parts = list(s.parts)
[docs]
def setMeterStream(self) -> None:
'''
sets `self.meterStream` or uses a default.
Used in makeNotation in Part later.
>>> b = corpus.parse('bwv66.6')
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
>>> SX.setMeterStream()
>>> SX.meterStream
<music21.stream.Score bach/bwv66.6.mxl>
>>> len(SX.meterStream)
4
>>> SX.meterStream[0]
<music21.meter.TimeSignature 4/4>
'''
s = self.stream
meterStream = s.getTimeSignatures(searchContext=False,
sortByCreationTime=False,
returnDefault=True)
self.meterStream = meterStream
[docs]
def setScoreLayouts(self) -> None:
'''
sets `self.scoreLayouts` and `self.firstScoreLayout`
>>> b = corpus.parse('schoenberg/opus19', 2)
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
>>> SX.setScoreLayouts()
>>> SX.scoreLayouts
[<music21.layout.ScoreLayout>]
>>> len(SX.scoreLayouts)
1
>>> SX.firstScoreLayout
<music21.layout.ScoreLayout>
'''
s = self.stream
scoreLayouts = s.getElementsByClass(layout.ScoreLayout)
self.firstScoreLayout = scoreLayouts.first()
self.scoreLayouts = list(scoreLayouts)
def _populatePartExporterList(self) -> None:
count = 0
sp = list(self.parts)
for innerStream in sp:
count += 1
# This guards against making an error in a future refactor
# Raises if editing while iterating instead of casting to list above
if count > len(sp): # pragma: no cover
raise MusicXMLExportException('infinite stream encountered')
pp = PartExporter(innerStream, parent=self)
pp.spannerBundle = self.spannerBundle
self.partExporterList.append(pp)
[docs]
def parsePartlikeScore(self) -> None:
'''
Called by .parse() if the score has individual parts.
Creates a `PartExporter` for each part, and runs .parse() on that part.
Appends the PartExporter to `self.partExporterList`
and runs .parse() on that part. Appends the PartExporter to self.
'''
if not self.partExporterList:
self._populatePartExporterList()
for part_ex in self.partExporterList:
part_ex.parse()
[docs]
def parseFlatScore(self) -> None:
'''
creates a single PartExporter for this Stream and parses it.
Note that the Score does not need to be totally flat, it just cannot have Parts inside it;
measures are fine.
>>> c = converter.parse('tinyNotation: 3/4 c2. d e')
>>> SX = musicxml.m21ToXml.ScoreExporter(c)
>>> SX.parseFlatScore()
>>> len(SX.partExporterList)
1
>>> SX.partExporterList[0]
<music21.musicxml.m21ToXml.PartExporter object at 0x...>
>>> SX.dump(SX.partExporterList[0].xmlRoot)
<part id="...">
<!--========================= Measure 1 ==========================-->
<measure implicit="no" number="1">...</measure>
</part>
>>> del SX.partExporterList[:] # for garbage collection
'''
s = self.stream
p = stream.Part()
for el in s:
p.coreInsert(el.offset, el)
p.coreElementsChanged()
pp = PartExporter(p, parent=self)
pp.parse()
self.partExporterList.append(pp)
[docs]
def setPartExporterStaffGroups(self) -> None:
'''
Figures out the containing StaffGroup for every PartExporter that has one.
Called automatically by .parse()
'''
for partExp in self.partExporterList:
joinableGroup = None
for sg in self.groupsToJoin:
if partExp.stream in sg:
joinableGroup = sg
break
partExp.staffGroup = joinableGroup
[docs]
def renumberVoicesWithinStaffGroups(self) -> None:
'''
Renumbers voices (as appropriate) in each StaffGroup, so that
voices have unique numbers across the entire group.
Called automatically by .parse()
'''
staffGroupsProcessed: list = []
for partExp in self.partExporterList:
if partExp.staffGroup is None:
# no staffGroup to process
continue
if partExp.staffGroup in staffGroupsProcessed:
# we already processed this one
continue
# renumber the voices in this StaffGroup
staffGroupScore = stream.Score(partExp.staffGroup.getSpannedElements())
measuresStream: stream.Stream[stream.Measure] = (
staffGroupScore.recurse().getElementsByClass(stream.Measure).stream()
)
offsetIterator: OffsetIterator[stream.Measure] = OffsetIterator(measuresStream)
measureStack: t.Sequence[stream.Stream[stream.Measure]]
for measureStack in offsetIterator:
nextVoiceId: int = 1
for m in measureStack:
for v in m[stream.Voice]:
if not isinstance(v.id, int):
# it's not an integer, leave it as is, and don't move nextVoiceId
continue
elif v.id < defaults.minIdNumberToConsiderMemoryLocation:
# it's a low integer, leave it as is, and jump nextVoiceId to v.id + 1
nextVoiceId = v.id + 1
else:
# it's a memory location, set v.id to nextVoiceId and increment
v.id = nextVoiceId
nextVoiceId += 1
# remember we did this one, so we don't do it again
staffGroupsProcessed.append(partExp.staffGroup)
[docs]
def postPartProcess(self) -> None:
'''
calls .joinPartStaffs() from the
:class:`~music21.musicxml.partStaffExporter.PartStaffExporterMixin`,
then calls .setScoreHeader(),
then appends each PartExporter's xmlRoot from
self.partExporterList to self.xmlRoot.
Called automatically by .parse().
'''
self.joinPartStaffs()
self.setScoreHeader()
for i, pex in enumerate(self.partExporterList):
self.addDividerComment('Part ' + str(i + 1))
self.xmlRoot.append(pex.xmlRoot)
[docs]
def textBoxToXmlCredit(self, textBox: text.TextBox) -> Element:
# noinspection PyShadowingNames
r'''
Convert a music21 TextBox to a MusicXML Credit.
>>> tb = text.TextBox('testing')
>>> tb.style.absoluteY = 400
>>> tb.style.absoluteX = 300
>>> tb.page = 3
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> mxCredit = SX.textBoxToXmlCredit(tb)
>>> SX.dump(mxCredit)
<credit page="3">
<credit-words default-x="300" default-y="400"
halign="center" valign="top">testing</credit-words>
</credit>
Default of page 1:
>>> tb = text.TextBox('testing')
>>> tb.page
1
>>> mxCredit = SX.textBoxToXmlCredit(tb)
>>> SX.dump(mxCredit)
<credit page="1">...</credit>
* Changed in v8: Multi-line text now exports as one `<credit-words>`
element (preserving newlines).
>>> tb = text.TextBox('Snare\nCymbals')
>>> mxCredit = SX.textBoxToXmlCredit(tb)
>>> SX.dump(mxCredit)
<credit page="1">
<credit-words default-x="500" default-y="500" halign="center" valign="top"
xml:space="preserve">Snare
Cymbals</credit-words>
</credit>
'''
mxCredit = Element('credit')
# TODO: credit-type
# TODO: link
# TODO: bookmark
# TODO: credit-image
if textBox.page is not None:
mxCredit.set('page', str(textBox.page))
else:
mxCredit.set('page', '1')
mxCreditWords = Element('credit-words')
if '\n' in textBox.content:
mxCreditWords.set('xml:space', 'preserve')
mxCreditWords.text = textBox.content
# TODO: link/bookmark in credit-words
self.setPrintStyleAlign(mxCreditWords, textBox)
if textBox.hasStyleInformation:
sty = t.cast(style.TextStyle, textBox.style)
if sty.justify is not None:
mxCreditWords.set('justify', sty.justify)
mxCredit.append(mxCreditWords)
return mxCredit
[docs]
def setDefaults(self):
# noinspection PyShadowingNames
'''
Returns a default object from self.firstScoreLayout or a very simple one if none exists.
Simple:
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> mxDefaults = SX.setDefaults()
>>> SX.dump(mxDefaults)
<defaults>
<scaling>
<millimeters>7</millimeters>
<tenths>40</tenths>
</scaling>
</defaults>
These numbers come from the `defaults` module:
>>> defaults.scalingMillimeters
7
>>> defaults.scalingTenths
40
More complex:
>>> s = corpus.parse('schoenberg/opus19', 2)
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> SX.setScoreLayouts() # necessary to call before .setDefaults()
>>> mxDefaults = SX.setDefaults()
>>> mxDefaults.tag
'defaults'
>>> mxScaling = mxDefaults.find('scaling')
>>> SX.dump(mxScaling)
<scaling>
<millimeters>6.1472</millimeters>
<tenths>40</tenths>
</scaling>
>>> mxPageLayout = mxDefaults.find('page-layout')
>>> SX.dump(mxPageLayout)
<page-layout>
<page-height>1818</page-height>
<page-width>1405</page-width>
<page-margins>
<left-margin>83</left-margin>
<right-margin>83</right-margin>
<top-margin>103</top-margin>
<bottom-margin>103</bottom-margin>
</page-margins>
</page-layout>
>>> mxSystemLayout = mxDefaults.find('system-layout')
>>> SX.dump(mxSystemLayout)
<system-layout>
<system-margins>
<left-margin>0</left-margin>
<right-margin>0</right-margin>
</system-margins>
<system-distance>121</system-distance>
<top-system-distance>70</top-system-distance>
</system-layout>
>>> mxStaffLayoutList = mxDefaults.findall('staff-layout')
>>> len(mxStaffLayoutList)
1
>>> SX.dump(mxStaffLayoutList[0])
<staff-layout>
<staff-distance>98</staff-distance>
</staff-layout>
'''
# get score defaults if any:
if self.firstScoreLayout is None:
scoreLayout = layout.ScoreLayout()
scoreLayout.scalingMillimeters = defaults.scalingMillimeters
scoreLayout.scalingTenths = defaults.scalingTenths
else:
scoreLayout = self.firstScoreLayout
mxDefaults = SubElement(self.xmlRoot, 'defaults')
if scoreLayout.scalingMillimeters is not None or scoreLayout.scalingTenths is not None:
mxScaling = SubElement(mxDefaults, 'scaling')
mxMillimeters = SubElement(mxScaling, 'millimeters')
mxMillimeters.text = str(scoreLayout.scalingMillimeters)
mxTenths = SubElement(mxScaling, 'tenths')
mxTenths.text = str(scoreLayout.scalingTenths)
if scoreLayout.pageLayout is not None:
mxPageLayout = self.pageLayoutToXmlPageLayout(scoreLayout.pageLayout)
mxDefaults.append(mxPageLayout)
if scoreLayout.systemLayout is not None:
mxSystemLayout = self.systemLayoutToXmlSystemLayout(scoreLayout.systemLayout)
mxDefaults.append(mxSystemLayout)
for staffLayout in scoreLayout.staffLayoutList:
mxStaffLayout = self.staffLayoutToXmlStaffLayout(staffLayout)
mxDefaults.append(mxStaffLayout)
self.addStyleToXmlDefaults(mxDefaults)
return mxDefaults # mostly for testing
[docs]
def addStyleToXmlDefaults(self, mxDefaults):
# noinspection PyShadowingNames
'''
Optionally add an <appearance> tag (using `styleToXmlAppearance`)
and <music-font>, <word-font>, zero or more <lyric-font> tags,
and zero or more <lyric-language> tags to mxDefaults
Demonstrating round tripping:
>>> import xml.etree.ElementTree as ET
>>> from xml.etree.ElementTree import fromstring as EL
>>> defaults = ET.fromstring(
... '<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)
>>> SX = musicxml.m21ToXml.ScoreExporter(MI.stream)
>>> mxDefaults = ET.Element('defaults')
>>> SX.addStyleToXmlDefaults(mxDefaults)
>>> SX.dump(mxDefaults)
<defaults>
<music-font font-family="Maestro,Opus" font-weight="bold" />
<word-font font-family="Garamond" font-style="italic" />
<lyric-font font-size="12" name="verse" />
<lyric-font font-size="14" name="chorus" />
<lyric-language name="verse" xml:lang="fr" />
<lyric-language name="chorus" xml:lang="en" />
</defaults>
'''
if not self.stream.hasStyleInformation:
return
mxAppearance = self.styleToXmlAppearance()
if mxAppearance is not None:
mxDefaults.append(mxAppearance)
st = self.stream.style
if st.musicFont is not None:
mxMusicFont = SubElement(mxDefaults, 'music-font')
self.setFont(mxMusicFont, st.musicFont)
if st.wordFont is not None:
mxWordFont = SubElement(mxDefaults, 'word-font')
self.setFont(mxWordFont, st.wordFont)
for lyricName, lyricFont in st.lyricFonts:
mxLyricFont = SubElement(mxDefaults, 'lyric-font')
if lyricFont is not None:
self.setFont(mxLyricFont, lyricFont)
if lyricName is not None:
mxLyricFont.set('name', lyricName)
for lyricType, lyricLang in st.lyricLanguages:
mxLyricLanguage = SubElement(mxDefaults, 'lyric-language')
if lyricType is not None:
mxLyricLanguage.set('name', lyricType)
if lyricLang is not None:
mxLyricLanguage.set('xml:lang', lyricLang)
[docs]
def styleToXmlAppearance(self):
# noinspection PyShadowingNames
'''
Populates the <appearance> tag of the <defaults> with
information from the stream's .style information.
>>> s = stream.Score()
>>> s.style.lineWidths.append(('beam', 5.0))
>>> s.style.noteSizes.append(('cue', 75))
>>> s.style.distances.append(('hyphen', 0.1))
>>> s.style.otherAppearances.append(('flags', 'wavy'))
>>> SX = musicxml.m21ToXml.ScoreExporter(s)
>>> mxAppearance = SX.styleToXmlAppearance()
>>> SX.dump(mxAppearance)
<appearance>
<line-width type="beam">5.0</line-width>
<note-size type="cue">75</note-size>
<distance type="hyphen">0.1</distance>
<other-appearance type="flags">wavy</other-appearance>
</appearance>
'''
st = self.stream.style
if (not st.lineWidths
and not st.noteSizes
and not st.distances
and not st.otherAppearances):
return None # appearance tag cannot be empty
mxAppearance = Element('appearance')
for thisProperty, tag in [('lineWidths', 'line-width'),
('noteSizes', 'note-size'),
('distances', 'distance'),
('otherAppearances', 'other-appearance')]:
propertyList = getattr(st, thisProperty)
for propertyType, propertyValue in propertyList:
mxProperty = SubElement(mxAppearance, tag)
mxProperty.set('type', propertyType)
mxProperty.text = str(propertyValue)
return mxAppearance
[docs]
def setPartList(self):
# noinspection PyShadowingNames
'''
Returns a <part-list> and appends it to self.xmlRoot.
This is harder than it looks because MusicXML and music21's idea of where to store
staff-groups are quite different.
We find each stream in self.partExporterList, then look at the StaffGroup spanners in
self.spannerBundle. If the part is the first element in a StaffGroup then we add a
<staff-group> object with 'start' as the starting point (and same for multiple StaffGroups)
this is in `staffGroupToXmlPartGroup(sg)`.
then we add the <score-part> descriptor of the part and its instruments, etc. (currently
just one!), then we iterate again through all StaffGroups and if this part is the last
element in a StaffGroup we add a <staff-group> descriptor with type="stop".
This Bach example has four parts and one staff-group bracket linking them:
>>> b = corpus.parse('bwv66.6')
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
Needs some strange setup to make this work in a demo. `.parse()` takes care of all this.
>>> SX.scorePreliminaries()
>>> SX.parsePartlikeScore()
>>> mxPartList = SX.setPartList()
>>> SX.dump(mxPartList)
<part-list>
<part-group number="1" type="start">...
<score-part id="P1">...
<score-part id="P2">...
<score-part id="P3">...
<score-part id="P4">...
<part-group number="1" type="stop" />
</part-list>
Multi-staff parts (such as piano staves), should NOT receive `<part-group>` tags,
since they are joined by `<staff>` tags:
>>> cpe = corpus.parse('cpebach')
>>> SX = musicxml.m21ToXml.ScoreExporter(cpe)
>>> SX.scorePreliminaries()
>>> SX.parsePartlikeScore()
>>> SX.joinPartStaffs()
>>> mxPartList = SX.setPartList()
>>> SX.dump(mxPartList)
<part-list>
<score-part id="P1">...
</score-part>
</part-list>
'''
spannerBundle = self.spannerBundle
mxPartList = SubElement(self.xmlRoot, 'part-list')
# mxComponents is just a list
# returns a spanner bundle
staffGroups = spannerBundle.getByClass(layout.StaffGroup)
# environLocal.printDebug(['got staff groups', staffGroups])
# first, find which parts are start/end of partGroups
partGroupIndexRef = {} # have id be key
partGroupIndex = 1 # start by 1 by convention
for pex in self.partExporterList:
p = pex.stream
# check for first
for sg in staffGroups:
if sg in self.groupsToJoin:
continue
if sg.isFirst(p):
mxPartGroup = self.staffGroupToXmlPartGroup(sg)
mxPartGroup.set('number', str(partGroupIndex))
# assign the spanner in the dictionary
partGroupIndexRef[partGroupIndex] = sg
partGroupIndex += 1 # increment for next usage
mxPartList.append(mxPartGroup)
# add score part
mxScorePart = pex.getXmlScorePart()
mxPartList.append(mxScorePart)
# check for last
activeIndex = None
for sg in staffGroups:
if sg in self.groupsToJoin:
continue
# Handle last part in the StaffGroup
if sg.isLast(p):
# find the spanner in the dictionary already-assigned
for k, value in partGroupIndexRef.items():
if value is sg:
activeIndex = k
break
mxPartGroup = Element('part-group')
mxPartGroup.set('type', 'stop')
if activeIndex is not None:
mxPartGroup.set('number', str(activeIndex))
mxPartList.append(mxPartGroup)
return mxPartList
[docs]
def staffGroupToXmlPartGroup(self, staffGroup):
# noinspection PyShadowingNames
'''
Create and configure an mxPartGroup object for the 'start' tag
from a staff group spanner. Note that this object
is not completely formed by this procedure (number isn't done).
>>> b = corpus.parse('bwv66.6')
>>> SX = musicxml.m21ToXml.ScoreExporter(b)
>>> firstStaffGroup = b.spannerBundle.getByClass(layout.StaffGroup)[0]
>>> mxPartGroup = SX.staffGroupToXmlPartGroup(firstStaffGroup)
>>> SX.dump(mxPartGroup)
<part-group type="start">
<group-symbol>bracket</group-symbol>
<group-barline>yes</group-barline>
</part-group>
At this point, you should set the number of the mxPartGroup, since it is required:
>>> mxPartGroup.set('number', str(1))
What can we do with it?
>>> firstStaffGroup.name = 'Voices'
>>> firstStaffGroup.abbreviation = 'Ch.'
>>> firstStaffGroup.symbol = 'brace' # 'none', 'brace', 'line', 'bracket', 'square'
>>> firstStaffGroup.barTogether = False # True, False, or 'Mensurstrich'
>>> mxPartGroup = SX.staffGroupToXmlPartGroup(firstStaffGroup)
>>> SX.dump(mxPartGroup)
<part-group type="start">
<group-name>Voices</group-name>
<group-abbreviation>Ch.</group-abbreviation>
<group-symbol>brace</group-symbol>
<group-barline>no</group-barline>
</part-group>
Now we avoid printing the name of the group:
>>> firstStaffGroup.style.hideObjectOnPrint = True
>>> mxPartGroup = SX.staffGroupToXmlPartGroup(firstStaffGroup)
>>> SX.dump(mxPartGroup)
<part-group type="start">
<group-name>Voices</group-name>
<group-name-display print-object="no" />
<group-abbreviation>Ch.</group-abbreviation>
<group-symbol>brace</group-symbol>
<group-barline>no</group-barline>
</part-group>
'''
mxPartGroup = Element('part-group')
mxPartGroup.set('type', 'start')
seta = _setTagTextFromAttribute
seta(staffGroup, mxPartGroup, 'group-name', 'name')
if staffGroup.style.hideObjectOnPrint:
mxGroupNameDisplay = SubElement(mxPartGroup, 'group-name-display')
mxGroupNameDisplay.set('print-object', 'no')
seta(staffGroup, mxPartGroup, 'group-abbreviation', 'abbreviation')
mxGroupSymbol = seta(staffGroup, mxPartGroup, 'group-symbol', 'symbol')
if mxGroupSymbol is not None:
self.setColor(mxGroupSymbol, staffGroup)
self.setPosition(mxGroupSymbol, staffGroup)
mxGroupBarline = SubElement(mxPartGroup, 'group-barline')
if staffGroup.barTogether is True:
mxGroupBarline.text = 'yes'
elif staffGroup.barTogether is False:
mxGroupBarline.text = 'no'
elif staffGroup.barTogether == 'Mensurstrich':
mxGroupBarline.text = 'Mensurstrich'
# TODO: group-time
self.setEditorial(mxPartGroup, staffGroup)
# environLocal.printDebug(['configureMxPartGroupFromStaffGroup: mxPartGroup', mxPartGroup])
return mxPartGroup
[docs]
def setIdentification(self) -> Element:
# noinspection SpellCheckingInspection, PyShadowingNames
'''
Returns an identification object from self.scoreMetadata and appends it to the score.
For defaults:
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> mxIdentification = SX.setIdentification()
>>> SX.dump(mxIdentification)
<identification>
<creator type="composer">Music21</creator>
<encoding>
<encoding-date>20...-...-...</encoding-date>
<software>music21 v...</software>
<supports element="beam" type="yes" />
<supports element="stem" type="yes" />
<supports element="accidental" type="yes" />
</encoding>
</identification>
More realistic:
>>> md = metadata.Metadata()
>>> md.composer = 'Francesca Caccini'
>>> c = metadata.Contributor(role='arranger', name='Aliyah Shanti')
>>> md.addContributor(c)
Need a fresh ScoreExporter, or, it will otherwise append to the existing mxIdentification
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> SX.scoreMetadata = md
>>> mxIdentification = SX.setIdentification()
>>> SX.dump(mxIdentification)
<identification>
<creator type="composer">Francesca Caccini</creator>
<creator type="arranger">Aliyah Shanti</creator>
<encoding>
<encoding-date>...</encoding-date>
<software>music21 v...</software>
<supports element="beam" type="yes" />
<supports element="stem" type="yes" />
<supports element="accidental" type="yes" />
</encoding>
</identification>
Overriding the default:
>>> defaults.author = "Batch Conversion March 2022"
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> mxIdentification = SX.setIdentification()
>>> SX.dump(mxIdentification)
<identification>
<creator type="composer">Batch Conversion March 2022</creator>
<encoding>
<encoding-date>20...-...-...</encoding-date>
<software>music21 v...</software>
<supports element="beam" type="yes" />
<supports element="stem" type="yes" />
<supports element="accidental" type="yes" />
</encoding>
</identification>
'''
if self.mxIdentification is not None:
mxId = self.mxIdentification
else:
mxId = SubElement(self.xmlRoot, 'identification')
self.mxIdentification = mxId
# creators
foundOne = False
if self.scoreMetadata is not None:
# We ignore the name ('namespace:name') here, and use
# c.role instead so we can represent non-standard roles.
# If c.role is non-standard, the name will be very boring:
# 'marcrel:CTB', which means 'otherContributor'.
for _, c in self.scoreMetadata.all(
skipNonContributors=True, # we only want contributors
returnPrimitives=True, # we want Contributor values
returnSorted=False):
mxCreator = self.contributorToXmlCreator(c)
mxId.append(mxCreator)
foundOne = True
if foundOne is False and defaults.author:
mxCreator = SubElement(mxId, 'creator')
mxCreator.set('type', 'composer')
mxCreator.text = defaults.author
if self.scoreMetadata is not None:
copyrights: tuple[metadata.Copyright, ...] = self.scoreMetadata['copyright']
for c in copyrights:
mxRights = SubElement(mxId, 'rights')
if c.role is not None:
mxRights.set('type', c.role)
mxRights.text = str(c)
# Encoding does its own append.
self.setEncoding()
# TODO: source
# TODO: relation
self.metadataToMiscellaneous()
return mxId
[docs]
def setEncoding(self):
# noinspection PyShadowingNames
'''
Returns an encoding object that might have information about <supports> also.
and appends to mxIdentification (if any)
Will use the date of generation as encoding-date.
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> mxEncoding = SX.setEncoding()
>>> SX.dump(mxEncoding)
<encoding>
<encoding-date>20...-...-...</encoding-date>
<software>music21 v...</software>
<supports element="beam" type="yes" />
<supports element="stem" type="yes" />
<supports element="accidental" type="yes" />
</encoding>
Encoding-date is in YYYY-MM-DD format.
'''
if self.mxIdentification is not None:
mxEncoding = SubElement(self.mxIdentification, 'encoding')
else:
mxEncoding = Element('encoding')
mxEncodingDate = SubElement(mxEncoding, 'encoding-date')
mxEncodingDate.text = str(datetime.date.today()) # right format
# TODO: encoder
if self.scoreMetadata is not None:
found_m21_already = False
for software in self.scoreMetadata.software:
if 'music21 v.' in software:
if found_m21_already:
# only write out one copy of the music21 software
# tag. First one should be current version.
continue
else:
found_m21_already = True
mxSoftware = SubElement(mxEncoding, 'software')
mxSoftware.text = software
else:
# there will not be a music21 software tag if there was no scoreMetadata
# if not for these lines.
mxSoftware = SubElement(mxEncoding, 'software')
mxSoftware.text = defaults.software
# TODO: encoding-description
mxSupportsList = self.getSupports()
for mxSupports in mxSupportsList:
mxEncoding.append(mxSupports)
return mxEncoding # for testing
[docs]
def getSupports(self) -> list[Element]:
'''
return a list of <supports> tags for what this supports. Does not append to
mxEncoding itself.
Currently just supports beam, stem, accidental, and
print's new-system if s.definesExplicitSystemBreaks is True
and print's new-page if s.definesExplicitPageBreaks is True.
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> SX.getSupports()
[<Element 'supports' at 0x...>,
<Element 'supports' at 0x...>,
<Element 'supports' at 0x...>]
>>> for support in SX.getSupports():
... SX.dump(support)
<supports element="beam" type="yes" />
<supports element="stem" type="yes" />
<supports element="accidental" type="yes" />
>>> SX.stream.definesExplicitSystemBreaks = True
>>> len(SX.getSupports())
4
>>> SX.dump(SX.getSupports()[-1])
<supports attribute="new-system" element="print" type="yes" value="yes" />
>>> SX.stream.definesExplicitPageBreaks = True
>>> SX.dump(SX.getSupports()[-1])
<supports attribute="new-page" element="print" type="yes" value="yes" />
'''
def getSupport(element: str, supports_type: bool,
attribute: str|None = None, value: str|None = None):
su = Element('supports')
su.set('element', element)
su.set('type', xmlObjects.booleanToYesNo(supports_type))
if attribute:
su.set('attribute', attribute)
if value is not None:
su.set('value', xmlObjects.booleanToYesNo(value))
return su
supportsList: list[Element] = [
getSupport('beam', True),
getSupport('stem', True),
getSupport('accidental', True),
]
s = self.stream
if s.definesExplicitSystemBreaks:
supportsList.append(getSupport('print', True, 'new-system', 'yes'))
if s.definesExplicitPageBreaks:
supportsList.append(getSupport('print', True, 'new-page', 'yes'))
return supportsList
[docs]
def setTitles(self) -> None:
'''
puts work (with work-title), movement-number, movement-title into the self.xmlRoot
'''
mdObj = self.scoreMetadata
if self.scoreMetadata is None:
mdObj = metadata.Metadata()
if t.TYPE_CHECKING:
assert mdObj is not None
mxScoreHeader = self.xmlRoot
mxWork = Element('work')
# TODO: work-number
firstTitleFound: metadata.Text|None = None
titles: tuple[metadata.Text, ...] = mdObj['title']
if titles:
if firstTitleFound is None:
firstTitleFound = titles[0]
mxWorkTitle = SubElement(mxWork, 'work-title')
mxWorkTitle.text = str(titles[0])
if len(mxWork):
mxScoreHeader.append(mxWork)
movementNumbers: tuple[metadata.Text, ...] = mdObj['movementNumber']
if movementNumbers:
mxMovementNumber = SubElement(mxScoreHeader, 'movement-number')
mxMovementNumber.text = str(movementNumbers[0])
# musicxml often defaults to show only movement title
# if no movementName is found in mdObj, set movement title to
# first title of the mdObj instead. Fall back to defaults.title if
# necessary (and if possible).
movement_title: str = ''
movementNames: tuple[metadata.Text, ...] = mdObj['movementName']
if movementNames:
movement_title = str(movementNames[0])
else: # there are no movementNames
if firstTitleFound is not None:
movement_title = str(firstTitleFound)
elif defaults.title:
movement_title = defaults.title
else:
return
mxMovementTitle = SubElement(mxScoreHeader, 'movement-title')
mxMovementTitle.text = movement_title
[docs]
def contributorToXmlCreator(self, c):
# noinspection SpellCheckingInspection, PyShadowingNames
'''
Return a <creator> tag from a :class:`~music21.metadata.Contributor` object.
>>> md = metadata.Metadata()
>>> md.composer = 'Oliveros, Pauline'
>>> contrib = md.contributors[0]
>>> contrib
<music21.metadata.primitives.Contributor composer:Oliveros, Pauline>
>>> SX = musicxml.m21ToXml.ScoreExporter()
>>> mxCreator = SX.contributorToXmlCreator(contrib)
>>> SX.dump(mxCreator)
<creator type="composer">Oliveros, Pauline</creator>
'''
mxCreator = Element('creator')
if c.role is not None:
mxCreator.set('type', str(c.role))
if c.name is not None:
mxCreator.text = c.name
return mxCreator
# ------------------------------------------------------------------------------
[docs]
class PartExporter(XMLExporterBase):
'''
Object to convert one Part stream to a <part> tag on .parse()
'''
_DOC_ATTR: dict[str, str] = {
'previousPartStaffInGroup': '''
If the part being exported is a :class:`~music21.stream.base.PartStaff`,
this attribute will be used to store the immediately previous `PartStaff`
in the :class:`~music21.layout.StaffGroup`, if any. (E.g. if this is
the left hand, store a reference to the right hand.)''',
}
midiChannelList: list[int|None] # TODO: just list[int]?
meterStream: stream.Stream[meter.TimeSignatureBase]
def __init__(self,
partObj: stream.Part|stream.Score|None = None,
parent: ScoreExporter|None = None):
super().__init__()
# partObj can be a Score IF it has no parts within it. But better to
# always have it be a Part
if partObj is None:
partObj = stream.Part()
self.stream: stream.Part|stream.Score = partObj
self.parent = parent # ScoreExporter
self.xmlRoot = Element('part')
if parent is None:
self.meterStream = stream.Stream[meter.TimeSignatureBase]()
self.refStreamOrTimeRange = [0.0, 0.0]
self.midiChannelList = []
self.makeNotation = True
else:
# else should not be executed.
self.meterStream = (parent.meterStream
if parent.meterStream is not None
else stream.Stream[meter.TimeSignatureBase]())
self.refStreamOrTimeRange = parent.refStreamOrTimeRange
self.midiChannelList = parent.midiChannelList # shared list
self.makeNotation = parent.makeNotation
self.previousPartStaffInGroup: stream.PartStaff|None = None
self.instrumentStream: stream.Stream[instrument.Instrument]|None = None
self.firstInstrumentObject: instrument.Instrument|None = None
# keep track of this so that we only put out new attributes when something
# has changed
self.lastDivisions = None
# The staffGroup to which this part belongs (if it belongs to one)
self.staffGroup: layout.StaffGroup|None = None
self.spannerBundle = partObj.spannerBundle
[docs]
def parse(self):
'''
Set up instruments, convert sounding pitch to written pitch,
create a partId (if no good one exists) and set it on
<part>, fixes up the notation
(:meth:`fixupNotationFlat` or :meth:`fixupNotationMeasured`),
setIdLocals() on spanner bundle. Run parse() on each measure's MeasureExporter and
append the output to the <part> object.
In other words, one-stop shopping.
:attr:`makeNotation` when False, will avoid running
:meth:`~music21.stream.base.Stream.makeNotation`
on the Part. Generally this attribute is set on `GeneralObjectExporter`
or `ScoreExporter` and read from there. Running with `makeNotation`
as False will raise `MusicXMLExportException` if no measures are present.
If `makeNotation` is False, the transposition to written pitch is still
performed and thus will be done in place.
>>> from music21.musicxml.m21ToXml import PartExporter
>>> noMeasures = stream.Part(note.Note())
>>> pex = PartExporter(noMeasures)
>>> pex.makeNotation = False
>>> pex.parse()
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
Cannot export with makeNotation=False if there are no measures
'''
# A copy has already been made
# unless makeNotation=False, but the user
# should have called toWrittenPitch() first
# and is explicitly asking for no further copies to be made
self.stream.toWrittenPitch(inPlace=True, ottavasToSounding=True)
# Suppose that everything below this is a measure
if self.makeNotation:
self.stream = self.stream.splitAtDurations(recurse=True)[0]
if self.stream.getElementsByClass(stream.Measure):
self.fixupNotationMeasured()
else:
self.fixupNotationFlat()
elif not self.stream.getElementsByClass(stream.Measure):
raise MusicXMLExportException(
'Cannot export with makeNotation=False if there are no measures')
# make sure that all instances of the same class have unique ids
self.spannerBundle.setIdLocals()
# must do after fixupNotation, since instrument instances may be created anew!
self.instrumentSetup()
self.xmlRoot.set('id', str(self.firstInstrumentObject.partId))
for m in self.stream.getElementsByClass(stream.Measure):
self.addDividerComment('Measure ' + str(m.number))
measureExporter = MeasureExporter(m, parent=self)
measureExporter.spannerBundle = self.spannerBundle
try:
mxMeasure = measureExporter.parse()
except MusicXMLExportException as e:
e.measureNumber = str(m.number)
if isinstance(self.stream, stream.Part):
e.partName = self.stream.partName
# else: could be a Score without parts (flat)
raise e
self.xmlRoot.append(mxMeasure)
return self.xmlRoot
[docs]
def instrumentSetup(self):
'''
Sets self.instrumentStream and self.firstInstrumentObject for the stream,
checks for a unique midiChannel and then blocks it off from future use.
>>> p = converter.parse('tinyNotation: 4/4 c1 d1 e1')
>>> p.getElementsByClass(stream.Measure)[0].insert(0, instrument.Clarinet())
>>> p.getElementsByClass(stream.Measure)[1].insert(0, instrument.BassClarinet())
>>> PEX = musicxml.m21ToXml.PartExporter(p)
>>> PEX.instrumentStream is None
True
>>> PEX.firstInstrumentObject is None
True
>>> PEX.instrumentSetup()
>>> PEX.instrumentStream
<music21.stream.Part 0x10ae02780>
The "P" signifies that it is the main instrument associated with a Part.
>>> PEX.instrumentStream.show('text')
{0.0} <music21.instrument.Clarinet 'P...: Clarinet'>
{4.0} <music21.instrument.BassClarinet 'Bass clarinet'>
'''
# Collect instruments
if self.parent is not None and id(self.stream) in self.parent.instrumentsByStream:
# this condition will be satisfied if this is the second or subsequent
# PartStaff in a StaffGroup
self.instrumentStream = self.parent.instrumentsByStream[id(self.stream)]
else:
# get a default instrument if not assigned
self.instrumentStream = self.stream.getInstruments(returnDefault=True, recurse=True)
# store first, as handled differently
firstInstrumentObject = t.cast(instrument.Instrument, self.instrumentStream[0])
self.firstInstrumentObject = firstInstrumentObject
if self.parent is not None:
instIdList = [x.partId for x in self.parent.instrumentList]
else:
instIdList = [self.stream.id]
firstInstId = firstInstrumentObject.partId
if firstInstId in instIdList or firstInstId is None: # must have unique ids
firstInstrumentObject.partIdRandomize() # set new random id
should_short_circuit = self.mergeInstrumentStreamPartStaffAware()
if should_short_circuit:
return
seen_instrument_classes = set()
for thisInstrument in self.instrumentStream:
# fragile against two or more instruments with the same
# Instrument subclass but different MIDI numbers or channels.
if type(thisInstrument) in seen_instrument_classes:
continue
seen_instrument_classes.add(type(thisInstrument))
if (thisInstrument.midiChannel is None
or thisInstrument.midiChannel in self.midiChannelList):
try:
thisInstrument.autoAssignMidiChannel(usedChannels=self.midiChannelList)
except exceptions21.InstrumentException as e:
environLocal.warn(str(e))
# this is shared among all PartExporters, so long as they are created by a
# ScoreExporter
# TODO: skip if .midiChannel is None?
self.midiChannelList.append(thisInstrument.midiChannel)
# environLocal.printDebug(['midiChannel list', self.midiChannelList])
# Enforce uniqueness
if (thisInstrument.instrumentId is None
or (self.parent
and thisInstrument.instrumentId in self.parent.instrumentIdList)):
thisInstrument.instrumentIdRandomize()
# add to the lists for checking on next part
if self.parent is not None:
self.parent.instrumentIdList.append(thisInstrument.instrumentId)
if thisInstrument is firstInstrumentObject:
self.parent.instrumentList.append(thisInstrument)
[docs]
def mergeInstrumentStreamPartStaffAware(self) -> bool:
'''
Merges instrument streams from subsequent parts in a PartStaff group.
Does nothing in the normal case of single staves.
Returns whether instrument processing should short circuit,
which is False for the general case and True for subsequent
PartStaff objects after the first in a group.
'''
if self.parent is None:
return False
# This is a list of StaffGroups, not PartStaffs, so check if any
if not self.parent.groupsToJoin:
return False
for joined_group in self.parent.groupsToJoin:
if joined_group.isFirst(self.stream):
# need to insert instruments from subsequent staffs
for subsequent_staff in joined_group[1:]:
other_instruments = subsequent_staff.getInstruments(
returnDefault=True, recurse=True)
self.instrumentStream += other_instruments
if self.instrumentStream is not None:
instrument.deduplicate(self.instrumentStream, inPlace=True)
# Place a reference to this instrument stream in a place
# where subsequent staffs entering this method will find and use it
if self.parent and self.instrumentStream is not None:
next_id = id(subsequent_staff)
self.parent.instrumentsByStream[next_id] = self.instrumentStream
elif self.stream in joined_group:
# This stream was already (or will be) processed
# UNLESS there is a spaghetti case where
# this stream is second in groupB, but first in groupA,
# but PartStaffExporterMixin guarantees that won't happen
return True
return False
[docs]
def fixupNotationFlat(self):
'''
Runs makeNotation on a flatStream, such as one lacking measures.
'''
part = self.stream
part.makeMutable() # must mutate
# try to add measures if none defined
# returns a new stream w/ new Measures but the same objects
part.makeNotation(meterStream=self.meterStream,
refStreamOrTimeRange=self.refStreamOrTimeRange,
inPlace=True)
# environLocal.printDebug(['fixupNotationFlat: post makeNotation, length',
# len(measureStream)])
# after calling measuresStream, need to update Spanners, as a deepcopy
# has been made
# might need to getAll b/c might need spanners
# from a higher level container
# allContexts = []
# spannerContext = measureStream.flatten().getContextByClass('Spanner')
# while spannerContext:
# allContexts.append(spannerContext)
# spannerContext = spannerContext.getContextByClass('Spanner')
#
# spannerBundle = spanner.SpannerBundle(allContexts)
# only getting spanners at this level
# spannerBundle = spanner.SpannerBundle(measureStream.flatten())
self.spannerBundle = part.spannerBundle
[docs]
def fixupNotationMeasured(self):
'''
Checks to see if there are any attributes in the part stream and moves
them into the first measure if necessary.
Checks if makeAccidentals is run, and haveBeamsBeenMade is done, and
tuplets have been made.
* Changed in v7: no longer accepts `measureStream` argument.
'''
part = self.stream
measures = part.getElementsByClass(stream.Measure)
first_measure = measures.first()
if not first_measure:
return
# check that first measure has any attributes in outer Stream
# this is for non-standard Stream formations (some kern imports)
# that place key/clef information in the containing stream
if hasattr(first_measure, 'clef') and first_measure.clef is None:
first_measure.makeMutable() # must mutate
outerClefs = part.getElementsByClass(clef.Clef)
if outerClefs:
first_measure.clef = outerClefs.first()
if hasattr(first_measure, 'keySignature') and first_measure.keySignature is None:
first_measure.makeMutable() # must mutate
outerKeySignatures = part.getElementsByClass(key.KeySignature)
if outerKeySignatures:
first_measure.keySignature = outerKeySignatures.first()
if hasattr(first_measure, 'timeSignature') and first_measure.timeSignature is None:
first_measure.makeMutable() # must mutate
outerTimeSignatures = part.getElementsByClass(meter.TimeSignature)
if outerTimeSignatures:
first_measure.timeSignature = outerTimeSignatures.first()
# see if accidentals/beams/tuplets should be processed
if not part.streamStatus.haveAccidentalsBeenMade():
part.makeAccidentals(inPlace=True)
if not part.streamStatus.beams:
try:
part.makeBeams(inPlace=True)
except exceptions21.StreamException as se: # no measures or no time sig?
warnings.warn(MusicXMLWarning(se), stacklevel=2)
if not part.streamStatus.tuplets:
for m in measures:
for m_or_v in [m, *m.voices]:
stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True)
if not self.spannerBundle:
self.spannerBundle = part.spannerBundle
[docs]
def getXmlScorePart(self):
'''
make a <score-part> from a music21 Part object and a parsed mxPart (<part>) element.
contains details about instruments, etc.
called directly by the ScoreExporter as a late part of the processing.
'''
part = self.stream
mxScorePart = Element('score-part')
# TODO: identification -- specific metadata could go here
mxScorePart.set('id', self.xmlRoot.get('id'))
mxPartName = SubElement(mxScorePart, 'part-name')
if hasattr(part, 'partName') and part.partName is not None:
mxPartName.text = part.partName
else:
mxPartName.text = defaults.partName
if part.hasStyleInformation and not part.style.printPartName:
mxPartName.set('print-object', 'no')
# TODO: part-name-display
if hasattr(self.stream, 'partAbbreviation') and part.partAbbreviation is not None:
mxPartAbbreviation = SubElement(mxScorePart, 'part-abbreviation')
mxPartAbbreviation.text = part.partAbbreviation
if part.hasStyleInformation and not part.style.printPartAbbreviation:
mxPartAbbreviation.set('print-object', 'no')
# TODO: part-abbreviation-display
# TODO: group
seen_instrument_classes = set()
# The first instrument of each Class appears as a <score-instrument>
for inst in self.instrumentStream:
# only use the first instance of this class
if type(inst) in seen_instrument_classes:
continue
if (inst.instrumentName is not None
or inst.instrumentAbbreviation is not None
or inst.midiProgram is not None):
mxScorePart.append(self.instrumentToXmlScoreInstrument(inst))
seen_instrument_classes.add(type(inst))
seen_instrument_classes = set()
# now iterate again to write <midi-instrument> tags for those
# same instruments, tags which must follow all <score-instrument> tags.
for inst in self.instrumentStream:
# TODO: disambiguate instrument instance with different midi programs?
if type(inst) in seen_instrument_classes:
continue
# TODO: midi-device
if inst.midiProgram is not None or isinstance(inst, instrument.UnpitchedPercussion):
mxScorePart.append(self.instrumentToXmlMidiInstrument(inst))
seen_instrument_classes.add(type(inst))
return mxScorePart
[docs]
def instrumentToXmlScoreInstrument(self, i):
# noinspection SpellCheckingInspection, PyShadowingNames
'''
Convert an :class:`~music21.instrument.Instrument` object to a
<score-instrument> element and return it.
>>> i = instrument.Clarinet()
>>> i.instrumentId = 'clarinet1'
>>> i.midiChannel = 4
>>> PEX = musicxml.m21ToXml.PartExporter()
>>> mxScoreInstrument = PEX.instrumentToXmlScoreInstrument(i)
>>> PEX.dump(mxScoreInstrument)
<score-instrument id="clarinet1">
<instrument-name>Clarinet</instrument-name>
<instrument-abbreviation>Cl</instrument-abbreviation>
</score-instrument>
>>> i.instrumentName = "Klarinette 1."
>>> i.instrumentAbbreviation = 'Kl.1'
>>> mxScoreInstrument = PEX.instrumentToXmlScoreInstrument(i)
>>> PEX.dump(mxScoreInstrument)
<score-instrument id="clarinet1">
<instrument-name>Klarinette 1.</instrument-name>
<instrument-abbreviation>Kl.1</instrument-abbreviation>
</score-instrument>
'''
mxScoreInstrument = Element('score-instrument')
mxScoreInstrument.set('id', str(i.instrumentId))
mxInstrumentName = SubElement(mxScoreInstrument, 'instrument-name')
mxInstrumentName.text = str(i.instrumentName)
if i.instrumentAbbreviation is not None:
mxInstrumentAbbreviation = SubElement(mxScoreInstrument, 'instrument-abbreviation')
mxInstrumentAbbreviation.text = str(i.instrumentAbbreviation)
# TODO: instrument-sound
# TODO: solo / ensemble
# TODO: virtual-instrument
return mxScoreInstrument
[docs]
def instrumentToXmlMidiInstrument(self, i):
# noinspection PyShadowingNames
'''
Convert an instrument object to a <midi-instrument> tag and return the element
>>> i = instrument.Clarinet()
>>> i.instrumentId = 'clarinet1'
>>> i.midiChannel = 4
>>> PEX = musicxml.m21ToXml.PartExporter()
>>> mxMidiInstrument = PEX.instrumentToXmlMidiInstrument(i)
>>> PEX.dump(mxMidiInstrument)
<midi-instrument id="clarinet1">
<midi-channel>5</midi-channel>
<midi-program>72</midi-program>
</midi-instrument>
>>> m = instrument.Maracas()
>>> m.instrumentId = 'my maracas'
>>> m.midiChannel # 0-indexed
9
>>> m.percMapPitch
70
>>> PEX = musicxml.m21ToXml.PartExporter()
>>> mxMidiInstrument = PEX.instrumentToXmlMidiInstrument(m)
>>> PEX.dump(mxMidiInstrument) # 1-indexed in MusicXML
<midi-instrument id="my maracas">
<midi-channel>10</midi-channel>
<midi-unpitched>71</midi-unpitched>
</midi-instrument>
'''
mxMidiInstrument = Element('midi-instrument')
mxMidiInstrument.set('id', str(i.instrumentId))
if i.midiChannel is None:
try:
i.autoAssignMidiChannel(self.midiChannelList)
except exceptions21.InstrumentException:
# warning will bubble up.
i.midiChannel = 0
mxMidiChannel = SubElement(mxMidiInstrument, 'midi-channel')
mxMidiChannel.text = str(i.midiChannel + 1)
# TODO: midi-name
# TODO: midi-bank
if i.midiProgram is not None:
mxMidiProgram = SubElement(mxMidiInstrument, 'midi-program')
mxMidiProgram.text = str(i.midiProgram + 1)
if isinstance(i, instrument.UnpitchedPercussion) and i.percMapPitch is not None:
mxMidiUnpitched = SubElement(mxMidiInstrument, 'midi-unpitched')
mxMidiUnpitched.text = str(i.percMapPitch + 1)
# TODO: volume
# TODO: pan
# TODO: elevation
return mxMidiInstrument
# ------------------------------------------------------------------------------
[docs]
class MeasureExporter(XMLExporterBase):
classesToMethods = OrderedDict(
[
('Note', 'noteToXml'),
('NoChord', 'noChordToXml'),
('ChordWithFretBoard', 'chordWithFretBoardToXml'), # these three
('ChordSymbol', 'chordSymbolToXml'), # must come before
('RomanNumeral', 'romanNumeralToXml'), # ChordBase
('ChordBase', 'chordToXml'),
('Unpitched', 'unpitchedToXml'),
('Rest', 'restToXml'),
('Dynamic', 'dynamicToXml'),
('Segno', 'segnoToXml'),
('Coda', 'codaToXml'),
('TempoText', 'tempoIndicationToXml'),
('MetronomeMark', 'tempoIndicationToXml'),
('MetricModulation', 'tempoIndicationToXml'),
('TextExpression', 'textExpressionToXml'),
('RepeatExpression', 'textExpressionToXml'),
('RehearsalMark', 'rehearsalMarkToXml'),
('PedalObject', 'pedalObjectToXml'),
]
)
# these need to be wrapped in an attributes tag if not at the beginning of the measure.
wrapAttributeMethodClasses = OrderedDict(
[('Clef', 'clefToXml'),
('KeySignature', 'keySignatureToXml'),
('TimeSignature', 'timeSignatureToXml'),
])
ignoreOnParseClasses = {'LayoutBase', 'Barline'}
def __init__(self,
measureObj: stream.Measure|None = None,
parent: PartExporter|None = None):
super().__init__()
if measureObj is None: # no point, but give an empty one anyhow.
self.stream = stream.Measure()
else:
self.stream = measureObj
self.parent = parent
self.xmlRoot = Element('measure')
self.currentDivisions = defaults.divisionsPerQuarter
# Adding ids to measure in the MusicXML export
synchronizeIds(self.xmlRoot, self.stream)
# TODO: allow for mid-measure transposition changes.
self.transpositionInterval = None
self.mxTranspose = None
self.measureOffsetStart = 0.0
self.offsetInMeasure = 0.0
self.currentVoiceId: int|str|None = None
self.nextFreeVoiceNumber: int = 1
self.nextArpeggioNumber: int = 1
self.arpeggioNumbers: dict[expressions.ArpeggioMarkSpanner, int] = {}
self.rbSpanners: list[spanner.RepeatBracket] = [] # repeatBracket spanners
if parent is None:
self.spannerBundle = spanner.SpannerBundle()
else:
self.spannerBundle = parent.spannerBundle
self.objectSpannerBundle = self.spannerBundle # will change for each element.
[docs]
def parse(self):
'''
main parse call.
deals with transposing, repeat brackets, setting measureNumber and width,
the first mxPrint, the first <attributes> tag, the left barline, parsing all internal
elements, setting the right barline, then returns the root <measure> tag.
'''
self.setTranspose()
self.setRbSpanners()
self.setMxAttributes()
self.setMxPrint()
self.setMxAttributesObjectForStartOfMeasure()
self.setLeftBarline()
# BIG ONE
self.mainElementsParse()
# continue
self.setRightBarline()
return self.xmlRoot
[docs]
def mainElementsParse(self):
'''
deals with parsing all the elements in a stream, whether it has voices or not.
'''
m = self.stream
# need to handle objects in order when creating musicxml
# we thus assume that objects are sorted here
if not m.hasVoices():
self.parseFlatElements(m, backupAfterwards=False)
return
nonVoiceMeasureItems = m.getElementsNotOfClass(stream.Voice).stream()
self.parseFlatElements(nonVoiceMeasureItems, backupAfterwards=True)
allVoices = list(m.voices)
for i, v in enumerate(allVoices):
if i == len(allVoices) - 1:
backupAfterwards = False
else:
backupAfterwards = True
# Assumes voices are flat.
self.parseFlatElements(v, backupAfterwards=backupAfterwards)
[docs]
def moveForward(self, byOffset: OffsetQL):
'''
Moves self.offsetInMeasure forward by an OffsetQL, appending the appropriate
<forward> tag (expressed in divisions) to self.xmlRoot.
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> len(MEX.xmlRoot)
0
>>> MEX.moveForward(1)
>>> MEX.dump(MEX.xmlRoot)
<measure>
<forward>
<duration>10080</duration>
</forward>
</measure>
>>> len(MEX.xmlRoot)
1
'''
amountToMoveForward: int = int(round(byOffset * self.currentDivisions))
if amountToMoveForward:
mxForward = Element('forward')
mxDuration = SubElement(mxForward, 'duration')
mxDuration.text = str(amountToMoveForward)
self.xmlRoot.append(mxForward)
self.offsetInMeasure += byOffset
[docs]
def moveBackward(self, byOffset: OffsetQL):
'''
Moves self.offsetInMeasure backward by an OffsetQL, appending the appropriate
<backup> tag (expressed in divisions) to self.xmlRoot.
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> len(MEX.xmlRoot)
0
>>> MEX.moveBackward(2)
>>> MEX.dump(MEX.xmlRoot)
<measure>
<backup>
<duration>20160</duration>
</backup>
</measure>
>>> len(MEX.xmlRoot)
1
'''
amountToBackup: int = int(round(byOffset * self.currentDivisions))
if amountToBackup:
mxBackup = Element('backup')
mxDuration = SubElement(mxBackup, 'duration')
mxDuration.text = str(amountToBackup)
self.xmlRoot.append(mxBackup)
self.offsetInMeasure -= byOffset
[docs]
def parseFlatElements(
self,
m: stream.Measure|stream.Voice,
*,
backupAfterwards: bool = False
) -> None:
'''
Deals with parsing all the elements in .elements, assuming that .elements is flat.
`m` here can be a Measure or Voice, but it must be flat.
If m is a 'Voice' class, we use the .id element to set self.currentVoiceId and then
send a backup tag to go back to the beginning of the measure.
Note that if the .id is high enough to be an id(x) memory location, then a small
voice number is used instead.
'''
self.offsetInMeasure = 0.0
voiceId: int|str|None
if isinstance(m, stream.Voice):
if isinstance(m.id, int) and m.id < defaults.minIdNumberToConsiderMemoryLocation:
voiceId = m.id
self.nextFreeVoiceNumber = voiceId + 1
elif isinstance(m.id, int):
# This voice id is actually a memory location, so we need to change it
# to a low number so it can be used in MusicXML.
voiceId = self.nextFreeVoiceNumber
self.nextFreeVoiceNumber += 1
else:
voiceId = m.id
else:
voiceId = None
self.currentVoiceId = voiceId
# We order things differently if there are any SpannerAnchors in the flat stream.
# If there are SpannerAnchors, we do two passes. The first pass skips all
# relatedSpanners processing, and then the second pass goes back to the
# beginning and does all the relatedSpanners processing.
# If there are no SpannerAnchors, we do one pass, doing any relatedSpanners
# processing as we process each element.
hasSpannerAnchors: bool = bool(m.getElementsByClass(spanner.SpannerAnchor))
# First (and possibly only) pass:
# Group all objects by offsets and then do a different order than normal sort.
# That way chord symbols and other 0-width objects appear before notes as much as
# possible.
objIterator: OffsetIterator[base.Music21Object] = OffsetIterator(m)
for objGroup in objIterator:
groupOffset = m.elementOffset(objGroup[0])
offsetToMoveForward = groupOffset - self.offsetInMeasure
# PyCharm typechecker does not properly read __next__ from OffsetIterator
# noinspection PyTypeChecker
if offsetToMoveForward > 0 and any(
isinstance(obj, (note.GeneralNote, clef.Clef)) for obj in objGroup):
# gap in stream between GeneralNote/Clef objects: create <forward>
self.moveForward(offsetToMoveForward)
notesForLater = []
# noinspection PyTypeChecker
for obj in objGroup:
# we do all non-note elements (including ChordSymbols not written as chord)
# first before note elements, in musicxml
if isinstance(obj, spanner.SpannerAnchor):
# nothing to do with this (only relatedSpanners, and
# we will do that below)
continue
if isinstance(obj, note.GeneralNote) and (
not (isHarm := isinstance(obj, harmony.Harmony))
or (isHarm and obj.writeAsChord)
):
notesForLater.append(obj)
else:
if hasSpannerAnchors:
self.parseOneElement(obj, AppendSpanners.NONE)
else:
self.parseOneElement(obj, AppendSpanners.NORMAL)
for n in notesForLater:
if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible':
# Prefer a gap in stream, to be filled with a <forward> tag by
# fill_gap_with_forward_tag() rather than raising exceptions
continue
if hasSpannerAnchors:
self.parseOneElement(n, AppendSpanners.NONE)
else:
self.parseOneElement(n, AppendSpanners.NORMAL)
# Second pass: If this flat stream (measure/voice) contains any SpannerAnchors, we
# will perform a second pass, emitting any pre/postList for any object that is
# functioning as a spanner anchor (the SpannerAnchors, as well as any GeneralNotes
# that start or end spanners).
# Before second pass, stash off self.offsetInMeasure in case we need to jump forward
# to it after the second pass.
firstPassEndOffsetInMeasure: OffsetQL = self.offsetInMeasure
if hasSpannerAnchors:
# return to the beginning of the measure, to emit any SpannerAnchors.
if self.offsetInMeasure:
self.moveBackward(self.offsetInMeasure)
objIterator = OffsetIterator(m)
for objGroup in objIterator:
# noinspection PyTypeChecker
if not any(self._hasRelatedSpanners(obj) for obj in objGroup):
continue
groupOffset = m.elementOffset(objGroup[0])
offsetToMoveForward = groupOffset - self.offsetInMeasure
if offsetToMoveForward > 0:
# gap in stream before spanner start/stop: create <forward>
self.moveForward(offsetToMoveForward)
# noinspection PyTypeChecker
for obj in objGroup:
self.parseOneElement(obj, AppendSpanners.RELATED_ONLY)
if backupAfterwards:
# return to the beginning of the measure.
if self.offsetInMeasure:
self.moveBackward(self.offsetInMeasure)
else:
# if necessary, jump to end of the measure.
if self.offsetInMeasure < firstPassEndOffsetInMeasure:
self.moveForward(opFrac(firstPassEndOffsetInMeasure - self.offsetInMeasure))
self.currentVoiceId = None
[docs]
def parseOneElement(
self,
obj,
appendSpanners: AppendSpanners = AppendSpanners.NORMAL
):
'''
parse one element completely and add it to xmlRoot, updating
offsetInMeasure, etc.
'''
root = self.xmlRoot
self.objectSpannerBundle = self.spannerBundle.getBySpannedElement(obj)
preList, postList = self.relatedSpanners(obj)
# pre-spanners (if appropriate)
if appendSpanners != AppendSpanners.NONE:
# emit the related spanners that precede the element
for sp in preList:
root.append(sp)
# element itself (if appropriate)
if appendSpanners != AppendSpanners.RELATED_ONLY:
# emit the object
classes = obj.classes
# Ignore Harmony objects having writeAsChord = False
if 'GeneralNote' in classes and getattr(obj, 'writeAsChord', True):
self.offsetInMeasure += obj.duration.quarterLength
# turn inexpressible durations into complex durations (unless unlinked)
if obj.duration.type == 'inexpressible':
obj.duration.quarterLength = obj.duration.quarterLength
objList = obj.splitAtDurations()
# make dotGroups into normal notes
elif len(obj.duration.dotGroups) > 1:
obj.duration.splitDotGroups(inPlace=True)
objList = obj.splitAtDurations()
# otherwise, splitAtDurations() was already called by parse(), no need to repeat
else:
objList = [obj]
parsedObject = False
for className, methName in self.classesToMethods.items():
if className in classes:
meth = getattr(self, methName)
for o in objList:
meth(o)
parsedObject = True
break
# these are classes that need to be wrapped in an attribute tag if
# not at the beginning of a measure
for className, methName in self.wrapAttributeMethodClasses.items():
if className in classes:
meth = getattr(self, methName)
for o in objList:
self.wrapObjectInAttributes(o, meth)
parsedObject = True
break
if parsedObject is False:
for className in classes:
if className in self.ignoreOnParseClasses:
parsedObject = True
break
if parsedObject is False:
environLocal.printDebug(['did not convert object', obj])
else: # appendSpanners == AppendSpanners.RELATED_ONLY
# skip the element itself, just move forward by element duration
if postList and obj.quarterLength > 0:
# gap in stream before spanner stop: create <forward>
# Ignore Harmony objects having writeAsChord = False
if 'GeneralNote' in obj.classes and getattr(obj, 'writeAsChord', True):
self.moveForward(obj.duration.quarterLength)
# post-spanners (if appropriate)
if appendSpanners != AppendSpanners.NONE:
# emit the related spanners that follow the element
for sp in postList:
root.append(sp)
def _hasRelatedSpanners(self, obj) -> bool:
'''
returns True if and only if:
(1) there are spanners related to the object that should appear before the object
to the <measure> tag, or (2) there are spanners related to the object that should
appear after the object in the measure tag.
'''
spannerBundle = self.spannerBundle.getBySpannedElement(obj)
if not spannerBundle:
return False
# this list of spanner classes must match the paramsSet keys in relatedSpanners()
for thisSpanner in spannerBundle.getByClass((spanner.Ottava,
dynamics.DynamicWedge,
spanner.Line,
expressions.PedalMark)):
if thisSpanner.isFirst(obj) or thisSpanner.isLast(obj):
return True
return False
@staticmethod
def _spannerStartParameters(spannerClass: str, sp: spanner.Spanner) -> dict[str, t.Any]:
'''
Return a dict of the parameters for the start of this spanner required by MusicXML output.
>>> ssp = musicxml.m21ToXml.MeasureExporter._spannerStartParameters
>>> ottava = spanner.Ottava(type='8va')
>>> st = ssp('Ottava', ottava)
Note that an ottava up in MusicXML is type "down", perhaps the opposite of what
you might expect. It means that the music should be engraved "down" from
where the sounding pitches would normally be engraved.
>>> st['type']
'down'
>>> st['size']
8
>>> cresc = dynamics.Crescendo()
>>> st = ssp('DynamicWedge', cresc)
>>> st['type']
'crescendo'
>>> st['spread']
0
>>> diminuendo = dynamics.Diminuendo()
>>> st = ssp('DynamicWedge', diminuendo)
>>> st['type']
'diminuendo'
>>> st['spread']
15
'''
post: dict[str, t.Any] = {'type': 'start'}
if spannerClass == 'Ottava':
ottava = t.cast(spanner.Ottava, sp)
post['size'] = ottava.shiftMagnitude()
post['type'] = ottava.shiftDirection(reverse=True) # up or down
elif spannerClass == 'Line':
line = t.cast(spanner.Line, sp)
post['line-end'] = line.startTick
post['end-length'] = line.startHeight
elif spannerClass == 'DynamicWedge':
wedge = t.cast(dynamics.DynamicWedge, sp)
post['type'] = wedge.type
if wedge.type == 'crescendo':
post['spread'] = 0
if wedge.niente:
post['niente'] = 'yes'
else:
post['spread'] = wedge.spread
elif spannerClass == 'PedalMark':
pedalMark = t.cast(expressions.PedalMark, sp)
if pedalMark.pedalType == expressions.PedalType.Sostenuto:
post['type'] = 'sostenuto'
else:
# non-Sostenuto pedalMark.pedalType might be Sustain, Soft, or Silent.
# But MusicXML only has 'start', which implies Sustain,
# so that's what we do here, hoping there is a text
# direction describing which pedal to use.
post['type'] = 'start'
if pedalMark.pedalForm == expressions.PedalForm.Line:
post['line'] = 'yes'
else:
# 'symbol', 'altsymbol', and 'symline' all start with a sign
post['sign'] = 'yes'
if pedalMark.abbreviated:
post['abbreviated'] = 'yes'
return post
@staticmethod
def _spannerEndParameters(spannerClass: str, sp: spanner.Spanner) -> dict[str, t.Any]:
'''
Return a dict of the parameters for the end of this spanner required by MusicXML output.
>>> ottava = spanner.Ottava(type='8va')
>>> en = musicxml.m21ToXml.MeasureExporter._spannerEndParameters('Ottava', ottava)
>>> en['type']
'stop'
>>> en['size']
8
'''
post: dict[str, t.Any] = {'type': 'stop'}
if spannerClass == 'Ottava':
ottava = t.cast(spanner.Ottava, sp)
post['size'] = ottava.shiftMagnitude()
elif spannerClass == 'Line':
line = t.cast(spanner.Line, sp)
post['line-end'] = line.endTick
post['end-length'] = line.endHeight
elif spannerClass == 'DynamicWedge':
wedge = t.cast(dynamics.DynamicWedge, sp)
if wedge.type == 'crescendo':
post['spread'] = wedge.spread
else:
post['spread'] = 0
if wedge.niente:
post['niente'] = 'yes'
elif spannerClass == 'PedalMark':
pedalMark = t.cast(expressions.PedalMark, sp)
if pedalMark.pedalForm in (
expressions.PedalForm.Line, expressions.PedalForm.SymbolLine
):
post['line'] = 'yes'
else:
# 'symbol', 'altsymbol' both end with a sign
post['sign'] = 'yes'
return post
[docs]
def getArpeggioNumber(self, arp: expressions.ArpeggioMarkSpanner) -> int:
arpeggioNumber: int = self.arpeggioNumbers.get(arp, -1)
if arpeggioNumber == -1:
arpeggioNumber = self.nextArpeggioNumber
self.nextArpeggioNumber += 1
if self.nextArpeggioNumber > 16:
self.nextArpeggioNumber = 1
self.arpeggioNumbers[arp] = arpeggioNumber
return arpeggioNumber
[docs]
def objectAttachedSpannersToNotations(
self,
obj: base.Music21Object,
noteIndexInChord: int = 0,
objectSpannerBundle: spanner.SpannerBundle|None = None
) -> list[Element]:
'''
return a list of <notations> from spanners related to the object that should appear
in the notations tag (slurs, slides, etc.)
>>> n0 = note.Note('C')
>>> n1 = note.Note('D')
>>> trem = expressions.TremoloSpanner([n0, n1])
>>> m = stream.Measure()
>>> m.insert(0, trem)
>>> m.append(n0)
>>> m.append(n1)
>>> mex = musicxml.m21ToXml.MeasureExporter(m)
>>> out = mex.objectAttachedSpannersToNotations(n0, objectSpannerBundle=m.spannerBundle)
>>> out
[<Element 'ornaments' at 0x1114d9408>]
>>> mex.dump(out[0])
<ornaments>
<tremolo type="start">3</tremolo>
</ornaments>
>>> out = mex.objectAttachedSpannersToNotations(n1, objectSpannerBundle=m.spannerBundle)
>>> mex.dump(out[0])
<ornaments>
<tremolo type="stop">3</tremolo>
</ornaments>
'''
notations: list[Element] = []
if objectSpannerBundle is not None:
sb = objectSpannerBundle
else:
sb = self.objectSpannerBundle
if not sb:
return notations
self.appendArpeggioMarkSpannersToNotations(obj, noteIndexInChord, notations, sb)
isSingleNoteOrFirstInChord = (noteIndexInChord == 0)
# Everything below this point is only for single note or first note in chord
if not isSingleNoteOrFirstInChord:
return notations
ornaments = []
for su in sb.getByClass('Slur'):
mxSlur = Element('slur')
if su.isFirst(obj):
mxSlur.set('type', 'start')
self.setLineStyle(mxSlur, su)
self.setPosition(mxSlur, su)
self.setStyleAttributes(mxSlur,
su,
('bezier-offset', 'bezier-offset2',
'bezier-x', 'bezier-y',
'bezier-x2', 'bezier-y2'),
)
if su.placement is not None:
mxSlur.set('placement', str(su.placement))
elif su.isLast(obj):
mxSlur.set('type', 'stop')
else:
continue # do not put a notation on mid-slur notes.
mxSlur.set('number', str(su.idLocal))
notations.append(mxSlur)
for su in sb.getByClass('Glissando'):
if su.slideType == 'continuous':
mxTag = 'slide'
else:
mxTag = 'glissando'
mxGlissando = Element(mxTag)
mxGlissando.set('number', str(su.idLocal))
if su.lineType is not None:
mxGlissando.set('line-type', str(su.lineType))
# is this note first in this spanner?
if su.isFirst(obj):
if su.label is not None:
mxGlissando.text = str(su.label)
mxGlissando.set('type', 'start')
elif su.isLast(obj):
mxGlissando.set('type', 'stop')
else:
continue # do not put a notation on mid-gliss notes.
synchronizeIds(mxGlissando, su)
# placement???
notations.append(mxGlissando)
# These add to Ornaments
for su in sb.getByClass('TremoloSpanner'):
mxTrem = Element('tremolo')
mxTrem.text = str(su.numberOfMarks)
if su.isFirst(obj):
mxTrem.set('type', 'start')
if su.placement is not None:
mxTrem.set('placement', str(su.placement))
elif su.isLast(obj):
mxTrem.set('type', 'stop')
else:
# this is always an error for tremolos
environLocal.printDebug(
['spanner w/ a component that is neither a start nor an end.', su, obj])
# Tremolos get in a separate ornaments tag.
ornaments.append(mxTrem)
for su in sb.getByClass('TrillExtension'):
mxWavyLine = Element('wavy-line')
mxWavyLine.set('number', str(su.idLocal))
isFirstOrLast = False
isFirstANDLast = False
# is this note first in this spanner?
if su.isFirst(obj):
mxWavyLine.set('type', 'start')
# print('Trill is first')
isFirstOrLast = True
if su.placement is not None:
mxWavyLine.set('placement', su.placement)
# a Trill on a single Note can be both first and last!
if su.isLast(obj):
if isFirstOrLast is True:
isFirstANDLast = True
else:
mxWavyLine.set('type', 'stop')
isFirstOrLast = True
# print('Trill is last')
if isFirstOrLast is False:
continue # do not put a wavy-line tag on mid-trill notes
ornaments.append(mxWavyLine)
if isFirstANDLast is True:
# make another one:
mxWavyLine = Element('wavy-line')
mxWavyLine.set('number', str(su.idLocal))
mxWavyLine.set('type', 'stop')
ornaments.append(mxWavyLine)
if ornaments:
mxOrnGroup = Element('ornaments')
for mxOrn in ornaments:
mxOrnGroup.append(mxOrn)
notations.append(mxOrnGroup)
return notations
[docs]
def objectAttachedSpannersToTechnicals(self, obj, objectSpannerBundle=None):
'''
return a list of <technical> from spanners related to the object that should appear
in the technical tag of the notations tag (hammer-on, pull-off etc.)
>>> n0 = note.Note('C')
>>> n1 = note.Note('D')
>>> n2 = note.Note('F')
>>> n3 = note.Note('E')
>>> hammerOn01 = articulations.HammerOn([n0, n1])
>>> pullOff23 = articulations.PullOff([n2, n3])
>>> m = stream.Measure()
>>> m.insert(0, hammerOn01)
>>> m.insert(0, pullOff23)
>>> m.append(n0)
>>> m.append(n1)
>>> m.append(n2)
>>> m.append(n3)
>>> mex = musicxml.m21ToXml.MeasureExporter(m)
>>> out = mex.objectAttachedSpannersToTechnicals(n0, m.spannerBundle)
>>> out
[<Element 'hammer-on' at 0x102857f40>]
>>> mex.dump(out[0])
<hammer-on number="1" type="start">H</hammer-on>
>>> out = mex.objectAttachedSpannersToTechnicals(n3, m.spannerBundle)
>>> mex.dump(out[0])
<pull-off number="1" type="stop" />
OMIT_FROM_DOCS
The other permutations of class and first/last:
>>> out = mex.objectAttachedSpannersToTechnicals(n1, m.spannerBundle)
>>> mex.dump(out[0])
<hammer-on number="1" type="stop" />
>>> out = mex.objectAttachedSpannersToTechnicals(n2, m.spannerBundle)
>>> mex.dump(out[0])
<pull-off number="1" type="start">P</pull-off>
'''
technicals = []
if objectSpannerBundle is not None:
sb = objectSpannerBundle
else:
sb = self.objectSpannerBundle
if not sb:
return technicals
for su in sb.getByClass(articulations.HammerOn):
mxHammerOn = Element('hammer-on')
if su.isFirst(obj):
mxHammerOn.set('type', 'start')
mxHammerOn.text = 'H'
elif su.isLast(obj):
mxHammerOn.set('type', 'stop')
else:
continue
mxHammerOn.set('number', '1')
technicals.append(mxHammerOn)
for su in sb.getByClass(articulations.PullOff):
mxPullOff = Element('pull-off')
if su.isFirst(obj):
mxPullOff.set('type', 'start')
mxPullOff.text = 'P'
elif su.isLast(obj):
mxPullOff.set('type', 'stop')
else:
continue
mxPullOff.set('number', '1')
technicals.append(mxPullOff)
return technicals
[docs]
def appendArpeggioMarkSpannersToNotations(
self,
obj: base.Music21Object,
noteIndexInChord: int,
notations: list[Element],
sb: spanner.SpannerBundle,
) -> None:
for ams in sb.getByClass(expressions.ArpeggioMarkSpanner):
if not ams.hasSpannedElement(obj):
continue
# putting this check inside the loop rather than outside,
# because it'll run rarely.
sub_obj = obj
if isinstance(obj, chord.Chord):
sub_obj = obj[noteIndexInChord]
mxArpeggio: Element|None = None
if ams.type == 'non-arpeggio':
min_note, max_note = ams.noteExtremes()
# <non-arpeggiate> goes only on top and bottom note in chord
if sub_obj is min_note:
mxArpeggio = Element('non-arpeggiate')
mxArpeggio.set('type', 'bottom')
elif sub_obj is max_note:
mxArpeggio = Element('non-arpeggiate')
mxArpeggio.set('type', 'top')
else:
mxArpeggio = Element('arpeggiate')
if ams.type != 'normal':
mxArpeggio.set('direction', ams.type)
if mxArpeggio is not None and (len(ams) > 1 or (len(ams) == 1 and len(ams[0]) > 1)):
# There is more than one GeneralNote in the arpeggio, so we must
# add a number attribute that will be the same for all GeneralNotes
# in this spanner. In MusicXML this number must be between
# 1 and 16. We just cycle through that range.
arpeggioNumber: int = self.getArpeggioNumber(ams)
mxArpeggio.set('number', str(arpeggioNumber))
if mxArpeggio is not None:
notations.append(mxArpeggio)
[docs]
def noteToXml(self, n: note.GeneralNote, noteIndexInChord=0, chordParent=None):
# noinspection PyShadowingNames
'''
Translate a music21 :class:`~music21.note.Note` or a Rest into a
ElementTree, note element.
Note that, some note-attached spanners, such
as octave shifts, produce direction (and direction types)
in this method.
>>> n = note.Note('D#5')
>>> n.quarterLength = 3
>>> n.volume.velocityScalar = 0.5
>>> n.style.color = 'silver'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> len(MEX.xmlRoot)
0
>>> mxNote = MEX.noteToXml(n)
>>> mxNote
<Element 'note' at 0x10113cb38>
>>> MEX.dump(mxNote)
<note color="#C0C0C0" dynamics="70.56">
<pitch>
<step>D</step>
<alter>1</alter>
<octave>5</octave>
</pitch>
<duration>30240</duration>
<type>half</type>
<dot />
<accidental>sharp</accidental>
<notehead color="#C0C0C0" parentheses="no">normal</notehead>
</note>
>>> len(MEX.xmlRoot)
1
>>> r = note.Rest()
>>> r.quarterLength = 1/3
>>> r.duration.tuplets[0].type = 'start'
>>> mxRest = MEX.noteToXml(r)
>>> MEX.dump(mxRest)
<note>
<rest />
<duration>3360</duration>
<type>eighth</type>
<time-modification>
<actual-notes>3</actual-notes>
<normal-notes>2</normal-notes>
<normal-type>eighth</normal-type>
</time-modification>
<notations>
<tuplet bracket="yes" number="1" placement="above" type="start">
<tuplet-actual>
<tuplet-number>3</tuplet-number>
<tuplet-type>eighth</tuplet-type>
</tuplet-actual>
<tuplet-normal>
<tuplet-number>2</tuplet-number>
<tuplet-type>eighth</tuplet-type>
</tuplet-normal>
</tuplet>
</notations>
</note>
>>> len(MEX.xmlRoot)
2
>>> n.notehead = 'diamond'
>>> n.articulations.append(articulations.Pizzicato())
>>> mxNote = MEX.noteToXml(n)
>>> MEX.dump(mxNote)
<note color="#C0C0C0" dynamics="70.56" pizzicato="yes">
...
<notehead color="#C0C0C0" parentheses="no">diamond</notehead>
</note>
Notes with complex durations need to be simplified before coming here
otherwise they raise :class:`MusicXMLExportException`:
>>> nComplex = note.Note()
>>> nComplex.duration.quarterLength = 5.0
>>> mxComplex = MEX.noteToXml(nComplex)
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
Cannot convert complex durations to MusicXML.
Try exporting with makeNotation=True or manually running splitAtDurations()
TODO: Test with spanners
'''
addChordTag = (noteIndexInChord != 0)
setb = setAttributeFromAttribute
mxNote = Element('note')
chordOrN: note.GeneralNote
if chordParent is None:
chordOrN = n
else:
chordOrN = chordParent
# Ensure style is read from `n` before reading from `chordOrN`
self.setPrintStyle(mxNote, n) # sets color
# self.setFont(mxNote, chordOrN)
self.setPrintStyle(mxNote, chordOrN) # sets color
# TODO: attr-group: printout -- replaces print-object, print-spacing below (3.1)
# TODO: attr: print-leger -- musicxml 3.1
if (isinstance(chordOrN, note.NotRest)
and chordOrN.hasVolumeInformation()
and chordOrN.volume.velocityScalar is not None):
vel = chordOrN.volume.velocityScalar * 100 * (127 / 90)
mxNote.set('dynamics', f'{vel:.2f}')
# TODO: attr: end-dynamics
# TODO: attr: attack
# TODO: attr: release
# TODO: attr: time-only
synchronizeIds(mxNote, n)
d = chordOrN.duration
if isinstance(d, duration.GraceDuration):
graceElement = SubElement(mxNote, 'grace')
try:
if d.slash in (True, False):
setb(d, graceElement, 'slash', transform=xmlObjects.booleanToYesNo)
if d.stealTimePrevious is not None:
setb(d,
graceElement,
'steal-time-previous',
transform=xmlObjects.fractionToPercent)
if d.stealTimeFollowing is not None:
setb(d,
graceElement,
'steal-time-following',
transform=xmlObjects.fractionToPercent)
# TODO: make-time -- specifically not implemented for now.
except AttributeError:
environLocal.warn(f'Duration set as Grace while not being a GraceDuration {d}')
# TODO: cue / cue-grace
self.setPrintObject(mxNote, n)
if n.hasStyleInformation and n.style.hideObjectOnPrint is True:
mxNote.set('print-spacing', 'yes')
for art in chordOrN.articulations:
if 'Pizzicato' in art.classes:
mxNote.set('pizzicato', 'yes')
if addChordTag:
SubElement(mxNote, 'chord')
if hasattr(n, 'pitch'):
n = t.cast(note.Note, n)
mxPitch = self.pitchToXml(n.pitch)
mxNote.append(mxPitch)
elif n.isRest:
SubElement(mxNote, 'rest')
if d.isGrace is not True:
mxDuration = self.durationXml(d)
mxNote.append(mxDuration)
# divisions only
# TODO: skip if cue:
if n.tie is not None:
mxTieList = self.tieToXmlTie(n.tie)
for mxTie in mxTieList:
mxNote.append(mxTie)
if isinstance(n, note.NotRest):
self.setNoteInstrument(n, mxNote, chordParent)
self.setEditorial(mxNote, n)
if self.currentVoiceId is not None:
mxVoice = SubElement(mxNote, 'voice')
mxVoice.text = str(self.currentVoiceId)
mxType = Element('type')
if d.isGrace is True and d.type == 'zero':
# Default type-less grace durations to eighths
mxType.text = 'eighth'
else:
try:
mxType.text = typeToMusicXMLType(d.type)
except MusicXMLExportException:
if isinstance(n, note.Rest) and helpers.isFullMeasureRest(n):
# type will be removed in xmlToRest()
pass
else:
raise
self.setStyleAttributes(mxType, n, 'size', 'noteSize')
mxNote.append(mxType)
for unused_dotCounter in range(d.dots):
SubElement(mxNote, 'dot')
# TODO: dot placement
if (isinstance(n, note.Note)
and n.pitch.accidental is not None
and n.pitch.accidental.displayStatus in (True, None)):
mxAccidental = self.accidentalToMx(n.pitch.accidental)
mxNote.append(mxAccidental)
if len(d.tuplets) == 1:
mxTimeModification = self.tupletToTimeModification(d.tuplets[0])
mxNote.append(mxTimeModification)
elif len(d.tuplets) > 1:
# create a composite tuplet to use as a timeModification guide
tupletFraction = fractions.Fraction(d.aggregateTupletMultiplier()
).limit_denominator(1000)
tempTuplet = duration.Tuplet(tupletFraction.denominator,
tupletFraction.numerator)
# don't set durationType until this can be done properly.
# tempTuplet.setDurationType(d.tuplets[0].durationNormal.type,
# d.tuplets[0].durationNormal.dots)
mxTimeModification = self.tupletToTimeModification(tempTuplet)
mxNote.append(mxTimeModification)
# stem:
stemDirection = None
# if we are not in a chord, or we are the first note of a chord, get stem
# direction from the chordOrNote object
if (addChordTag is False
and isinstance(chordOrN, note.NotRest)
and chordOrN.stemDirection != 'unspecified'):
chordOrN = t.cast(note.NotRest, chordOrN)
stemDirection = chordOrN.stemDirection
# or if we are in a chord, but the sub-note has its own stem direction,
# record that.
elif (chordOrN is not n
and isinstance(n, note.NotRest)
and n.stemDirection != 'unspecified'):
n = t.cast(note.NotRest, n)
stemDirection = n.stemDirection
stem_style = None
if (chordOrN.hasStyleInformation
and isinstance(chordOrN.style, style.NoteStyle)
and chordOrN.style.stemStyle is not None):
stem_style = chordOrN.style.stemStyle
if stemDirection is not None or stem_style:
mxStem = SubElement(mxNote, 'stem')
if stemDirection is None:
if closest_clef := chordOrN.getContextByClass(clef.Clef):
sdText = closest_clef.getStemDirectionForPitches(chordOrN.pitches)
else:
sdText = 'up'
else:
sdText = stemDirection
if sdText == 'noStem':
sdText = 'none'
mxStem.text = sdText
if stem_style:
self.setColor(mxStem, stem_style)
self.setPosition(mxStem, stem_style)
# end Stem
# notehead
self.dealWithNotehead(mxNote, n, chordParent)
# TODO: notehead-text
# beam
if addChordTag is False:
if isinstance(chordOrN, note.NotRest) and chordOrN.beams is not None:
nBeamsList = self.beamsToXml(chordOrN.beams)
for mxB in nBeamsList:
mxNote.append(mxB)
mxNotationsList = self.noteToNotations(n, noteIndexInChord, chordParent)
# add tuplets if it's a note or the first <note> of a chord.
if addChordTag is False:
for i, tup in enumerate(d.tuplets):
tupTagList = self.tupletToXmlTuplet(tup, i + 1)
mxNotationsList.extend(tupTagList)
if mxNotationsList:
mxNotations = SubElement(mxNote, 'notations')
for mxN in mxNotationsList:
mxNotations.append(mxN)
# lyric
if addChordTag is False:
for lyricObj in chordOrN.lyrics:
if lyricObj.text is None:
continue # happens sometimes!
mxLyric = self.lyricToXml(lyricObj)
mxNote.append(mxLyric)
# TODO: play
self.xmlRoot.append(mxNote)
return mxNote
[docs]
def setNoteInstrument(self,
n: note.NotRest,
mxNote: Element,
chordParent: chord.Chord|None):
'''
Insert <instrument> tags if necessary, that is, when there is more than one
instrument anywhere in the same musicxml <part>.
'''
if self.parent is None or self.parent.instrumentStream is None:
return
if len(self.parent.instrumentStream) <= 1:
return
if n.isRest:
return
searchingObject: note.NotRest|chord.Chord = chordParent if chordParent else n
closest_inst_or_none = searchingObject.getInstrument()
if closest_inst_or_none is None:
return # no instrument, so no need to add anything
closest_inst: instrument.Instrument = closest_inst_or_none
instance_to_use = None
inst: instrument.Instrument
for inst in self.parent.instrumentStream:
if inst.classSet == closest_inst.classSet:
instance_to_use = inst
break
if instance_to_use is None:
# exempt coverage, because this is only for safety/unreachable
raise MusicXMLExportException(
f'Instrument instance {closest_inst} for note {n} not found in instrumentStream'
) # pragma: no cover
mxInstrument = SubElement(mxNote, 'instrument')
if instance_to_use.instrumentId is not None:
mxInstrument.set('id', instance_to_use.instrumentId)
[docs]
def restToXml(self, r: note.Rest):
# noinspection PyShadowingNames
'''
Convert a Rest object to a <note> with a <rest> tag underneath it.
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> r = note.Rest(quarterLength=2.0)
Give the rest some context:
>>> m = stream.Measure()
>>> m.timeSignature = meter.TimeSignature('4/4')
>>> m.append(r)
>>> mxNoteRest = MEX.restToXml(r)
>>> MEX.dump(mxNoteRest)
<note>
<rest />
<duration>20160</duration>
<type>half</type>
</note>
Now it is a full measure:
>>> m.timeSignature = meter.TimeSignature('2/4')
>>> mxNoteRest = MEX.restToXml(r)
>>> MEX.dump(mxNoteRest)
<note>
<rest measure="yes" />
<duration>20160</duration>
</note>
Unless we specify that it should not be converted to a full measure:
>>> r.fullMeasure = False
>>> mxNoteRest = MEX.restToXml(r)
>>> MEX.dump(mxNoteRest)
<note>
<rest />
<duration>20160</duration>
<type>half</type>
</note>
With True or "always" it will be converted to full measure even if
it does not match:
>>> m.timeSignature = meter.TimeSignature('4/4')
>>> r.duration.dots = 1
>>> r.fullMeasure = True
>>> mxNoteRest = MEX.restToXml(r)
>>> MEX.dump(mxNoteRest)
<note>
<rest measure="yes" />
<duration>30240</duration>
</note>
Note that if a measure has paddingLeft/paddingRight (such as a pickup)
then a fullMeasure duration might not match the TimeSignature duration.
The display-step and display-octave are set from a Rest's stepShift:
>>> r = note.Rest()
>>> r.stepShift = 1
>>> mxNoteRest = MEX.restToXml(r)
>>> MEX.dump(mxNoteRest)
<note>
<rest>
<display-step>C</display-step>
<display-octave>5</display-octave>
</rest>
<duration>10080</duration>
<type>quarter</type>
</note>
Clef context matters:
>>> m = stream.Measure()
>>> m.clef = clef.BassClef()
>>> m.append(r)
>>> mxNoteRest = MEX.restToXml(r)
>>> MEX.dump(mxNoteRest)
<note>
<rest>
<display-step>E</display-step>
<display-octave>3</display-octave>
</rest>
<duration>10080</duration>
<type>quarter</type>
</note>
'''
mxNote = self.noteToXml(r)
mxRestTag = mxNote.find('rest')
if mxRestTag is None:
raise MusicXMLExportException('Something went wrong -- converted rest w/o rest tag')
if helpers.isFullMeasureRest(r):
mxRestTag.set('measure', 'yes')
mxType = mxNote.find('type')
if mxType is not None:
mxNote.remove(mxType)
mxDots = mxNote.findall('dot')
for mxDot in mxDots:
mxNote.remove(mxDot)
# should tuplet, etc. be removed? hard to think of a full measure with one.
if r.stepShift != 0:
mxDisplayStep = SubElement(mxRestTag, 'display-step')
mxDisplayOctave = SubElement(mxRestTag, 'display-octave')
currentClef: clef.PitchClef|None = r.getContextByClass(clef.PitchClef)
if currentClef is None or not hasattr(currentClef, 'lowestLine'):
currentClef = clef.TrebleClef()
# this should not be common enough to
# worry about the overhead
currentClef = t.cast(clef.PitchClef, currentClef)
midLineDNN = currentClef.lowestLine + 4
restObjectPseudoDNN = midLineDNN + r.stepShift
tempPitch = pitch.Pitch()
tempPitch.diatonicNoteNum = restObjectPseudoDNN
mxDisplayStep.text = tempPitch.step
mxDisplayOctave.text = str(tempPitch.octave)
return mxNote
[docs]
def chordToXml(self, c: chord.ChordBase) -> list[Element]:
# noinspection PyShadowingNames
'''
Returns a list of <note> tags, all but the first with a <chord/> tag on them.
And appends them to self.xmlRoot
Attributes of notes are merged from different locations: first from the
duration objects, then from the pitch objects. Finally, GeneralNote
attributes are added.
>>> ch = chord.Chord()
>>> ch.quarterLength = 2
>>> b = pitch.Pitch('A-2')
>>> c = pitch.Pitch('D3')
>>> d = pitch.Pitch('E4')
>>> e = [b, c, d]
>>> ch.pitches = e
>>> len(ch.pitches)
3
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> len(MEX.xmlRoot)
0
>>> mxNoteList = MEX.chordToXml(ch)
>>> len(mxNoteList) # get three mxNotes
3
>>> len(MEX.xmlRoot)
3
>>> MEX.dump(mxNoteList[0])
<note>
<pitch>
<step>A</step>
<alter>-1</alter>
<octave>2</octave>
</pitch>
<duration>20160</duration>
<type>half</type>
<accidental>flat</accidental>
</note>
>>> MEX.dump(mxNoteList[1])
<note>
<chord />
<pitch>
<step>D</step>
<octave>3</octave>
</pitch>
<duration>20160</duration>
<type>half</type>
</note>
>>> MEX.dump(mxNoteList[2])
<note>
<chord />
<pitch>
<step>E</step>
<octave>4</octave>
</pitch>
<duration>20160</duration>
<type>half</type>
</note>
Test that notehead and style translation works:
>>> g = pitch.Pitch('g3')
>>> h = note.Note('b4')
>>> h.notehead = 'diamond'
>>> h.style.color = 'gold'
>>> h.style.absoluteX = 176
>>> ch2 = chord.Chord([g, h])
>>> ch2.quarterLength = 2.0
>>> mxNoteList = MEX.chordToXml(ch2)
>>> MEX.dump(mxNoteList[1])
<note color="#FFD700" default-x="176">
<chord />
<pitch>
<step>B</step>
<octave>4</octave>
</pitch>
<duration>20160</duration>
<type>half</type>
<notehead color="#FFD700" parentheses="no">diamond</notehead>
</note>
And unpitched chord members:
>>> perc = percussion.PercussionChord([note.Unpitched(), note.Unpitched()])
>>> for n in MEX.chordToXml(perc):
... MEX.dump(n)
<note>
<unpitched>
<display-step>B</display-step>
<display-octave>4</display-octave>
</unpitched>
<duration>10080</duration>
<type>quarter</type>
</note>
<note>
<chord />
<unpitched>
<display-step>B</display-step>
<display-octave>4</display-octave>
</unpitched>
<duration>10080</duration>
<type>quarter</type>
</note>
Test articulations of chords with fingerings. Superfluous fingerings will be ignored.
>>> testChord = chord.Chord('E4 C5')
>>> testChord.articulations = [articulations.Fingering(1),
... articulations.Accent(), articulations.Fingering(5), articulations.Fingering(3)]
>>> for n in MEX.chordToXml(testChord):
... MEX.dump(n)
<note>
...
<notations>
<articulations>
<accent />
</articulations>
<technical>
<fingering alternate="no" substitution="no">1</fingering>
</technical>
</notations>
</note>
<note>
<chord />
...
<notations>
<technical>
<fingering alternate="no" substitution="no">5</fingering>
</technical>
</notations>
</note>
'''
mxNoteList = []
if isinstance(c, chord.Chord):
c.sortAscending()
for i, n in enumerate(c):
if 'Unpitched' in n.classSet:
mxNoteList.append(self.unpitchedToXml(n, noteIndexInChord=i, chordParent=c))
else:
mxNoteList.append(self.noteToXml(n, noteIndexInChord=i, chordParent=c))
return mxNoteList
[docs]
def durationXml(self, dur: duration.Duration):
# noinspection PyShadowingNames
'''
Convert a duration.Duration object to a <duration> tag using self.currentDivisions
>>> d = duration.Duration(1.5)
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEX.currentDivisions = 10
>>> mxDuration = MEX.durationXml(d)
>>> MEX.dump(mxDuration)
<duration>15</duration>
'''
mxDuration = Element('duration')
mxDuration.text = str(int(round(self.currentDivisions * dur.quarterLength)))
return mxDuration
[docs]
def pitchToXml(self, p: pitch.Pitch):
# noinspection PyShadowingNames
'''
Convert a :class:`~music21.pitch.Pitch` to xml.
Does not create the <accidental> tag.
>>> p = pitch.Pitch('D#5')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxPitch = MEX.pitchToXml(p)
>>> MEX.dump(mxPitch)
<pitch>
<step>D</step>
<alter>1</alter>
<octave>5</octave>
</pitch>
'''
mxPitch = Element('pitch')
_setTagTextFromAttribute(p, mxPitch, 'step', 'step')
if p.accidental is not None:
mxAlter = SubElement(mxPitch, 'alter')
mxAlter.text = str(common.numToIntOrFloat(p.accidental.alter))
_setTagTextFromAttribute(p, mxPitch, 'octave', 'implicitOctave')
return mxPitch
[docs]
def unpitchedToXml(self,
up: note.Unpitched,
noteIndexInChord: int = 0,
chordParent: chord.ChordBase|None = None) -> Element:
# noinspection PyShadowingNames
'''
Convert an :class:`~music21.note.Unpitched` to a <note>
with an <unpitched> subelement.
>>> up = note.Unpitched(displayName='D5')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxUnpitched = MEX.unpitchedToXml(up)
>>> MEX.dump(mxUnpitched)
<note>
<unpitched>
<display-step>D</display-step>
<display-octave>5</display-octave>
</unpitched>
<duration>10080</duration>
<type>quarter</type>
</note>
>>> graceUp = up.getGrace()
>>> mxUnpitched = MEX.unpitchedToXml(graceUp)
>>> MEX.dump(mxUnpitched)
<note>
<grace slash="yes" />
<unpitched>
<display-step>D</display-step>
<display-octave>5</display-octave>
</unpitched>
<type>quarter</type>
</note>
'''
mxNote = self.noteToXml(up, noteIndexInChord=noteIndexInChord, chordParent=chordParent)
mxUnpitched = Element('unpitched')
_setTagTextFromAttribute(up, mxUnpitched, 'display-step')
_setTagTextFromAttribute(up, mxUnpitched, 'display-octave')
helpers.insertBeforeElements(mxNote, mxUnpitched, tagList=['duration', 'type'])
return mxNote
[docs]
def fretNoteToXml(self, fretNote) -> Element:
'''
Converts a FretNote Object to MusicXML readable format.
Note that, although music21 is referring to FretNotes as FretNotes,
musicxml refers to them as frame notes. To convert between the two formats,
each 'Fret-Note' must be converted to 'Frame-Note'.
>>> fn = tablature.FretNote(string=3, fret=1, fingering=2)
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEXFretNote = MEX.fretNoteToXml(fn)
>>> MEX.dump(MEXFretNote)
<frame-note>
<string>3</string>
<fret>1</fret>
<fingering>2</fingering>
</frame-note>
Without fingering!
>>> fn2 = tablature.FretNote(string=5, fret=2)
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEXOtherFretNote = MEX.fretNoteToXml(fn2)
>>> MEX.dump(MEXOtherFretNote)
<frame-note>
<string>5</string>
<fret>2</fret>
</frame-note>
'''
mxFrameNote = Element('frame-note')
_setTagTextFromAttribute(fretNote, mxFrameNote, 'string', 'string')
_setTagTextFromAttribute(fretNote, mxFrameNote, 'fret', 'fret')
if fretNote.fingering is not None:
_setTagTextFromAttribute(fretNote, mxFrameNote, 'fingering', 'fingering')
return mxFrameNote
[docs]
def fretBoardToXml(self, fretBoard) -> Element|None:
'''
The ChordWithFretBoard Object combines chord symbols with FretNote objects.
>>> myFretNote1 = tablature.FretNote(1, 2, 2)
>>> myFretNote2 = tablature.FretNote(2, 3, 3)
>>> myFretNote3 = tablature.FretNote(3, 2, 1)
>>> guitarChord = tablature.ChordWithFretBoard('DM', numStrings=6,
... fretNotes=[myFretNote1, myFretNote2, myFretNote3])
>>> guitarChord.tuning = tablature.GuitarFretBoard().tuning
>>> guitarChord.getPitches()
[None, None, None,
<music21.pitch.Pitch A3>, <music21.pitch.Pitch D4>, <music21.pitch.Pitch F#4>]
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEXChordWithFret = MEX.fretBoardToXml(guitarChord)
>>> MEX.dump(MEXChordWithFret)
<frame>
<frame-strings>6</frame-strings>
<frame-frets>4</frame-frets>
<frame-note>
<string>3</string>
<fret>2</fret>
<fingering>1</fingering>
</frame-note>
<frame-note>
<string>2</string>
<fret>3</fret>
<fingering>3</fingering>
</frame-note>
<frame-note>
<string>1</string>
<fret>2</fret>
<fingering>2</fingering>
</frame-note>
</frame>
'''
if not fretBoard.fretNotes:
return None
# why isn't this the same as the function above? This seems a good deal simpler!
mxFrame = Element('frame')
mxFrameStrings = SubElement(mxFrame, 'frame-strings')
mxFrameStrings.text = str(fretBoard.numStrings)
mxFrameFrets = SubElement(mxFrame, 'frame-frets')
mxFrameFrets.text = str(fretBoard.displayFrets)
for thisFretNote in fretBoard.fretNotesLowestFirst():
mxFretNote = self.fretNoteToXml(thisFretNote)
mxFrame.append(mxFretNote)
return mxFrame
[docs]
def chordWithFretBoardToXml(self, cwf: tablature.ChordWithFretBoard) -> Element:
'''
Deals with both chords and frets.
Generate harmony and append xml to it.
'''
mxHarmony = self.chordSymbolToXml(cwf)
if t.TYPE_CHECKING:
# should not write out lists of Elements.
assert isinstance(mxHarmony, Element)
mxFrame = self.fretBoardToXml(cwf)
if mxFrame is not None:
mxHarmony.append(mxFrame)
return mxHarmony
[docs]
def tupletToTimeModification(self, tup) -> Element:
# noinspection PyShadowingNames
'''
>>> tup = duration.Tuplet(11, 8)
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxTimeMod = MEX.tupletToTimeModification(tup)
>>> MEX.dump(mxTimeMod)
<time-modification>
<actual-notes>11</actual-notes>
<normal-notes>8</normal-notes>
</time-modification>
>>> tup.setDurationType('eighth', dots=2)
>>> mxTimeMod = MEX.tupletToTimeModification(tup)
>>> MEX.dump(mxTimeMod)
<time-modification>
<actual-notes>11</actual-notes>
<normal-notes>8</normal-notes>
<normal-type>eighth</normal-type>
<normal-dot />
<normal-dot />
</time-modification>
'''
mxTimeModification = Element('time-modification')
_setTagTextFromAttribute(tup, mxTimeModification, 'actual-notes', 'numberNotesActual')
_setTagTextFromAttribute(tup, mxTimeModification, 'normal-notes', 'numberNotesNormal')
if tup.durationNormal is not None:
mxNormalType = SubElement(mxTimeModification, 'normal-type')
mxNormalType.text = typeToMusicXMLType(tup.durationNormal.type)
if tup.durationNormal.dots > 0:
for i in range(tup.durationNormal.dots):
SubElement(mxTimeModification, 'normal-dot')
return mxTimeModification
[docs]
def dealWithNotehead(
self,
mxNote: Element,
n: note.GeneralNote,
chordParent: chord.Chord|None = None
) -> None:
'''
Determine if an <notehead> element needs to be added to this <note>
element (mxNote) and if it does then get the <notehead> element from
noteheadToXml and add it to mxNote.
Complicated because the chordParent might have notehead
set, which would affect every note along the way.
Returns nothing. The mxNote is modified in place.
'''
foundANotehead = False
if (isinstance(n, note.NotRest)
and (n.notehead != 'normal'
or n.noteheadParenthesis
or n.noteheadFill is not None
or (n.hasStyleInformation and n.style.color not in (None, '')))):
n = t.cast(note.NotRest, n)
foundANotehead = True
mxNotehead = self.noteheadToXml(n)
mxNote.append(mxNotehead)
if foundANotehead is False and chordParent is not None:
if (hasattr(chordParent, 'notehead')
and (chordParent.notehead != 'normal'
or chordParent.noteheadParenthesis
or chordParent.noteheadFill is not None
or (chordParent.hasStyleInformation
and chordParent.style.color not in (None, '')))):
mxNotehead = self.noteheadToXml(chordParent)
mxNote.append(mxNotehead)
[docs]
def noteheadToXml(self, n: note.NotRest) -> Element:
# noinspection PyShadowingNames
'''
Translate a music21 :class:`~music21.note.NotRest` object
such as a Note, or Chord into a `<notehead>` tag.
>>> n = note.Note('C#4')
>>> n.notehead = 'diamond'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxN = MEX.noteheadToXml(n)
>>> MEX.dump(mxN)
<notehead parentheses="no">diamond</notehead>
>>> n1 = note.Note('c3')
>>> n1.style.color = 'red'
>>> n1.notehead = 'diamond'
>>> n1.noteheadParenthesis = True
>>> n1.noteheadFill = False
>>> mxN = MEX.noteheadToXml(n1)
>>> MEX.dump(mxN)
<notehead color="#FF0000" filled="no" parentheses="yes">diamond</notehead>
>>> n1 = note.Note('c3')
>>> n1.style.color = 'red'
>>> n1.notehead = 'diamond'
>>> n1.noteheadParenthesis = True
>>> n1.noteheadFill = False
>>> mxN = MEX.noteheadToXml(n1)
>>> MEX.dump(mxN)
<notehead color="#FF0000" filled="no" parentheses="yes">diamond</notehead>
'''
mxNotehead = Element('notehead')
nh = n.notehead
if nh is None:
nh = 'none'
mxNotehead.text = nh
setb = setAttributeFromAttribute
setb(n, mxNotehead, 'filled', 'noteheadFill', transform=xmlObjects.booleanToYesNo)
setb(n, mxNotehead, 'parentheses', 'noteheadParenthesis',
transform=xmlObjects.booleanToYesNo)
# TODO: font
if n.hasStyleInformation and n.style.color:
color = normalizeColor(n.style.color)
mxNotehead.set('color', color)
return mxNotehead
[docs]
def arpeggioMarkToMxExpression(self, arpeggioMark, chordOrNote, noteIndexInChord):
mxExpression = None
if arpeggioMark.type == 'non-arpeggio':
# <non-arpeggiate> goes on top and bottom note in chord
if noteIndexInChord == 0:
mxExpression = self.expressionToXml(arpeggioMark)
mxExpression.set('type', 'bottom')
elif noteIndexInChord == len(chordOrNote.notes) - 1:
mxExpression = self.expressionToXml(arpeggioMark)
mxExpression.set('type', 'top')
else:
# <arpeggiate> goes on every note in the chord
mxExpression = self.expressionToXml(arpeggioMark)
return mxExpression
[docs]
def ornamentToMxAccidentalMarks(self, ornamentObj: expressions.Ornament) -> list[Element]:
mxAccidentalMarks: list[Element] = []
if isinstance(ornamentObj, expressions.Turn):
# 0, 1, or 2 accidental marks
if (ornamentObj.upperOrnamentalPitch
and ornamentObj.upperOrnamentalPitch.accidental
and ornamentObj.upperOrnamentalPitch.accidental.displayStatus is True):
mxAccidentalMarks.append(
self.accidentalToMxAccidentalMark(ornamentObj.upperOrnamentalPitch.accidental)
)
mxAccidentalMarks[-1].set('placement', 'above')
if (ornamentObj.lowerOrnamentalPitch
and ornamentObj.lowerOrnamentalPitch.accidental
and ornamentObj.lowerOrnamentalPitch.accidental.displayStatus is True):
mxAccidentalMarks.append(
self.accidentalToMxAccidentalMark(ornamentObj.lowerOrnamentalPitch.accidental)
)
mxAccidentalMarks[-1].set('placement', 'below')
elif isinstance(ornamentObj, (expressions.GeneralMordent, expressions.Trill)):
# 0 or 1 accidental marks
if (ornamentObj.ornamentalPitch
and ornamentObj.ornamentalPitch.accidental
and ornamentObj.ornamentalPitch.accidental.displayStatus is True):
mxAccidentalMarks.append(
self.accidentalToMxAccidentalMark(ornamentObj.ornamentalPitch.accidental)
)
if ornamentObj.direction == 'down':
mxAccidentalMarks[-1].set('placement', 'below')
else:
mxAccidentalMarks[-1].set('placement', 'above')
return mxAccidentalMarks
[docs]
def noteToNotations(
self,
n: note.GeneralNote,
noteIndexInChord: int = 0,
chordParent: chord.Chord|None = None
) -> list[Element]:
'''
Take information from .expressions,
.articulations, and spanners to
make the <notations> tag for a note.
'''
mxArticulations = None
mxTechnicalMark = None
mxOrnaments = None
isSingleNoteOrFirstInChord = (noteIndexInChord == 0)
notations: list[Element] = []
chordOrNote = n
if chordParent is not None:
# get expressions from first note of chord
chordOrNote = chordParent
# apply all expressions apart from arpeggios only to the first note of a chord.
for expObj in chordOrNote.expressions:
if isinstance(expObj, expressions.ArpeggioMark):
mxExpression = self.arpeggioMarkToMxExpression(
expObj, chordOrNote, noteIndexInChord
)
if mxExpression is None:
# the ArpeggioMark is not applicable on this note.
continue
notations.append(mxExpression)
elif isSingleNoteOrFirstInChord:
mxExpression = self.expressionToXml(expObj)
if mxExpression is None:
# print('Could not convert expression: ', mxExpression)
# TODO: should not!
continue
if isinstance(expObj, expressions.Ornament):
if mxOrnaments is None:
mxOrnaments = Element('ornaments')
mxOrnaments.append(mxExpression)
mxAccidMarks: list[Element] = (
self.ornamentToMxAccidentalMarks(expObj)
)
for mxAccidMark in mxAccidMarks:
mxOrnaments.append(mxAccidMark)
# print(mxExpression)
else:
notations.append(mxExpression)
# apply all articulations apart from fingerings and hammer-on/pull-off
# only to first note of chord
applicableArticulations: list[articulations.Articulation] = []
fingeringNumber = 0
for aObj in chordOrNote.articulations:
if isinstance(aObj, articulations.Fingering):
if fingeringNumber == noteIndexInChord:
applicableArticulations.append(aObj)
fingeringNumber += 1
elif isSingleNoteOrFirstInChord:
# Ignore hammer-on/pull-off:
# are written from their Spanner representation instead.
# It's an anti-pattern to find spanners in the articulations
# array, and the musicxml importer doesn't put them here,
# but it's a potential point of user confusion, so we guard
# against it here to avoid writing out superfluous <other-technical>
if not isinstance(aObj, (articulations.HammerOn, articulations.PullOff)):
applicableArticulations.append(aObj)
for aObj in applicableArticulations:
if isinstance(aObj, articulations.Pizzicato):
continue
if isinstance(aObj, articulations.StringIndication) and aObj.number < 1:
continue
if isinstance(aObj, articulations.TechnicalIndication):
if mxTechnicalMark is None:
mxTechnicalMark = Element('technical')
mxTechnicalMark.append(self.articulationToXmlTechnical(aObj))
else:
if mxArticulations is None:
mxArticulations = Element('articulations')
mxArticulations.append(self.articulationToXmlArticulation(aObj))
# TODO: attrGroup: print-object (for individual notations)
# TODO: editorial (will be hard! -- requires parsing again in order.)
# <tied>
# for ties get for each note of chord too
if n.tie is not None:
tiedList = self.tieToXmlTied(n.tie)
notations.extend(tiedList)
# <tuplet> handled elsewhere, because it's on the overall duration on chord
if chordParent is not None:
notations.extend(self.objectAttachedSpannersToNotations(chordParent, noteIndexInChord))
else:
notations.extend(self.objectAttachedSpannersToNotations(n, noteIndexInChord))
# TODO: slur
# TODO: glissando
# TODO: slide
if isSingleNoteOrFirstInChord and chordParent is not None:
spannerTechnicals = self.objectAttachedSpannersToTechnicals(chordParent)
elif chordParent is not None:
spannerTechnicals = None
else:
spannerTechnicals = self.objectAttachedSpannersToTechnicals(n)
if spannerTechnicals:
if mxTechnicalMark is None:
mxTechnicalMark = Element('technical')
mxTechnicalMark.extend(spannerTechnicals)
for x in (mxArticulations,
mxTechnicalMark,
mxOrnaments):
if x is not None:
notations.append(x)
# TODO: dynamics in notations
# TODO: accidental-mark (Notations; we've done Ornaments)
# TODO: other-notation
return notations
[docs]
def tieToXmlTie(self, m21Tie: tie.Tie):
# noinspection PyShadowingNames
'''
returns a list of ties from a Tie object.
A 'continue' tie requires two <tie> tags to represent.
>>> tieObj = tie.Tie('continue')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> tieList = MEX.tieToXmlTie(tieObj)
>>> for mxT in tieList:
... MEX.dump(mxT)
<tie type="stop" />
<tie type="start" />
'''
mxTieList = []
if m21Tie.type == 'continue':
musicxmlTieType = 'stop'
else:
musicxmlTieType = m21Tie.type
mxTie = Element('tie')
mxTie.set('type', musicxmlTieType)
mxTieList.append(mxTie)
if m21Tie.type == 'continue':
mxTie = Element('tie')
mxTie.set('type', 'start')
mxTieList.append(mxTie)
return mxTieList
[docs]
def tieToXmlTied(self, m21Tie: tie.Tie):
# noinspection PyShadowingNames
'''
In musicxml, a tie is represented in sound
by the tie tag (near the pitch object), and
the <tied> tag in notations. This
creates the <tied> tag.
Returns a list since a music21 "continue" tie type needs two tags
in musicxml. List may be empty if tie.style == "hidden"
>>> tieObj = tie.Tie('continue')
>>> tieObj.id = 'tied1'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> tiedList = MEX.tieToXmlTied(tieObj)
>>> for mxT in tiedList:
... MEX.dump(mxT)
<tied id="tied1" type="stop" />
<tied type="start" />
>>> tieObj.style = 'hidden'
>>> tiedList = MEX.tieToXmlTied(tieObj)
>>> len(tiedList)
0
'''
if m21Tie.style == 'hidden':
return []
mxTiedList = []
if m21Tie.type == 'continue':
musicxmlTieType = 'stop'
else:
musicxmlTieType = m21Tie.type
mxTied = Element('tied')
synchronizeIds(mxTied, m21Tie)
mxTied.set('type', musicxmlTieType)
mxTiedList.append(mxTied)
if m21Tie.type == 'continue':
mxTied = Element('tied')
mxTied.set('type', 'start')
mxTiedList.append(mxTied)
# TODO: attr: number (distinguishing ties on enharmonics)
if m21Tie.style != 'normal' and m21Tie.type != 'stop':
mxTied.set('line-type', m21Tie.style)
# wavy is not supported as a tie type.
# Tie style needs to be dealt with after changes to Tie object
# TODO: attrGroup: dashed-formatting
# TODO: attrGroup: position
if m21Tie.placement is not None:
mxTied.set('placement', m21Tie.placement)
orientation = None
if m21Tie.placement == 'above':
orientation = 'over'
elif m21Tie.placement == 'below':
orientation = 'under'
else:
orientation = ''
# MuseScore requires 'orientation' not placement
# should be no need for separate orientation
# https://forums.makemusic.com/viewtopic.php?f=12&t=2179&start=0
if orientation:
mxTied.set('orientation', orientation)
# TODO: attrGroup: bezier
# TODO: attrGroup: color
return mxTiedList
[docs]
def tupletToXmlTuplet(self, tuplet, tupletIndex=1):
'''
In musicxml, a tuplet is represented by
a timeModification and visually by the
<notations><tuplet> tag. This method
creates the latter.
Returns a list of them because a
startStop type tuplet needs two tuplet
brackets.
TODO: make sure something happens if
makeTupletBrackets is not set.
>>> tup = duration.Tuplet(11, 8)
>>> tup.type = 'start'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxTup = MEX.tupletToXmlTuplet(tup)
>>> len(mxTup)
1
>>> MEX.dump(mxTup[0])
<tuplet bracket="yes" number="1" placement="above" type="start">
<tuplet-actual>
<tuplet-number>11</tuplet-number>
</tuplet-actual>
<tuplet-normal>
<tuplet-number>8</tuplet-number>
</tuplet-normal>
</tuplet>
>>> tup.tupletActualShow = 'both'
>>> tup.tupletNormalShow = 'type'
>>> mxTup = MEX.tupletToXmlTuplet(tup)
>>> MEX.dump(mxTup[0])
<tuplet bracket="yes" number="1" placement="above"
show-number="actual" show-type="both" type="start">...</tuplet>
'''
if tuplet.type in (None, False, ''):
return []
if tuplet.type not in ('start', 'stop', 'startStop'):
raise MusicXMLExportException(
'Cannot create music XML from a tuplet of type ' + tuplet.type)
if tuplet.type == 'startStop': # need two musicxml
localType = ['start', 'stop']
else:
localType = [tuplet.type] # place in list
retList = []
for tupletType in localType:
# might be multiple in case of startStop
mxTuplet = Element('tuplet')
mxTuplet.set('type', tupletType)
mxTuplet.set('number', str(tupletIndex))
# only provide other parameters if this tuplet is a start
if tupletType == 'start':
tBracket = tuplet.bracket
if tBracket == 'slur':
tBracket = True
mxTuplet.set('bracket',
xmlObjects.booleanToYesNo(tBracket))
if tuplet.placement is not None:
mxTuplet.set('placement', tuplet.placement)
tas = tuplet.tupletActualShow
tns = tuplet.tupletNormalShow
if tas is None:
mxTuplet.set('show-number', 'none')
# cannot show normal without actual
elif tas in ('both', 'number') and tns in ('both', 'number'):
mxTuplet.set('show-number', 'both')
elif tas in ('both', 'actual'):
mxTuplet.set('show-number', 'actual')
if tas in ('both', 'type') and tns in ('both', 'type'):
mxTuplet.set('show-type', 'both')
elif tas in ('both', 'type'):
mxTuplet.set('show-type', 'actual')
if tuplet.bracket == 'slur':
mxTuplet.set('line-shape', 'curved')
# TODO: attrGroup: position
mxTupletActual = SubElement(mxTuplet, 'tuplet-actual')
mxTupletNumber = SubElement(mxTupletActual, 'tuplet-number')
mxTupletNumber.text = str(tuplet.numberNotesActual)
if tuplet.durationActual is not None:
actualType = typeToMusicXMLType(tuplet.durationActual.type)
if actualType:
mxTupletType = SubElement(mxTupletActual, 'tuplet-type')
mxTupletType.text = actualType
for unused_counter in range(tuplet.durationActual.dots):
SubElement(mxTupletActual, 'tuplet-dot')
mxTupletNormal = SubElement(mxTuplet, 'tuplet-normal')
mxTupletNumber = SubElement(mxTupletNormal, 'tuplet-number')
mxTupletNumber.text = str(tuplet.numberNotesNormal)
if tuplet.durationNormal is not None:
normalType = typeToMusicXMLType(tuplet.durationNormal.type)
if normalType:
mxTupletType = SubElement(mxTupletNormal, 'tuplet-type')
mxTupletType.text = normalType
for unused_counter in range(tuplet.durationNormal.dots):
SubElement(mxTupletNormal, 'tuplet-dot')
retList.append(mxTuplet)
return retList
[docs]
def expressionToXml(self, expression: expressions.Expression) -> Element:
'''
Convert a music21 Expression (expression or ornament)
to a musicxml tag;
return None if no conversion is possible.
Expressions apply only to the first note of chord.
>>> invTurn = expressions.InvertedTurn()
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxExpression = MEX.expressionToXml(invTurn)
>>> MEX.dump(mxExpression)
<inverted-turn placement="above" />
>>> invDelayedTurn = expressions.InvertedTurn(delay=1.0)
>>> invDelayedTurn.placement = 'below'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxExpression = MEX.expressionToXml(invDelayedTurn)
>>> MEX.dump(mxExpression)
<delayed-inverted-turn placement="below" />
Some special types:
>>> f = expressions.Fermata()
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxExpression = MEX.expressionToXml(f)
>>> MEX.dump(mxExpression)
<fermata type="inverted" />
>>> f.shape = 'angled'
>>> mxExpression = MEX.expressionToXml(f)
>>> MEX.dump(mxExpression)
<fermata type="inverted">angled</fermata>
>>> trem = expressions.Tremolo()
>>> trem.numberOfMarks = 4
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxExpression = MEX.expressionToXml(trem)
>>> MEX.dump(mxExpression)
<tremolo type="single">4</tremolo>
>>> arp = expressions.ArpeggioMark()
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxExpression = MEX.expressionToXml(arp)
>>> MEX.dump(mxExpression)
<arpeggiate />
>>> arp.type = 'down'
>>> mxExpression = MEX.expressionToXml(arp)
>>> MEX.dump(mxExpression)
<arpeggiate direction="down" />
>>> nonArp = expressions.ArpeggioMark('non-arpeggio')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxExpression = MEX.expressionToXml(nonArp)
>>> MEX.dump(mxExpression)
<non-arpeggiate />
'''
mx = None
classes = expression.classes
# ArpeggioMark maps to two different elements
if isinstance(expression, expressions.ArpeggioMark):
if expression.type == 'non-arpeggio':
mx = Element('non-arpeggiate')
else:
mx = Element('arpeggiate')
if expression.type != 'normal':
mx.set('direction', expression.type)
# InvertedTurn/Turn map to two different elements each
if isinstance(expression, expressions.Turn):
if isinstance(expression, expressions.InvertedTurn):
if expression.isDelayed:
mx = Element('delayed-inverted-turn')
else:
mx = Element('inverted-turn')
else:
if expression.isDelayed:
mx = Element('delayed-turn')
else:
mx = Element('turn')
if mx is None:
mapping = OrderedDict([
('Trill', 'trill-mark'),
# TODO: vertical-turn
('InvertedMordent', 'inverted-mordent'),
('Mordent', 'mordent'),
('Shake', 'shake'),
('Schleifer', 'schleifer'),
('Tremolo', 'tremolo'), # non-spanner
# non-ornaments
('Fermata', 'fermata'),
# keep last
('Ornament', 'other-ornament'),
])
for k, v in mapping.items():
if k in classes:
mx = Element(v)
break
if mx is None:
environLocal.printDebug(['no musicxml conversion for:', expression])
return Element('other-ornament')
self.setPrintStyle(mx, expression)
# TODO: trill-sound
if hasattr(expression, 'placement') and expression.placement is not None:
mx.set('placement', expression.placement)
if isinstance(expression, expressions.Fermata):
mx.set('type', str(expression.type))
if expression.shape in ('angled', 'square'): # only valid shapes
mx.text = expression.shape
synchronizeIds(mx, expression)
if isinstance(expression, expressions.Tremolo):
mx.set('type', 'single')
mx.text = str(expression.numberOfMarks)
return mx
[docs]
def articulationToXmlArticulation(
self,
articulationMark: articulations.Articulation
) -> Element:
# noinspection PyShadowingNames
'''
Returns a class (mxArticulationMark) that represents the
MusicXML structure of an articulation mark.
>>> a = articulations.Accent()
>>> a.placement = 'below'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxArticulationMark = MEX.articulationToXmlArticulation(a)
>>> MEX.dump(mxArticulationMark)
<accent placement="below" />
>>> a = articulations.Staccatissimo()
>>> a.placement = 'below'
>>> mxArticulationMark = MEX.articulationToXmlArticulation(a)
>>> MEX.dump(mxArticulationMark)
<staccatissimo placement="below" />
Articulations can have style attached to them:
>>> a = articulations.Doit()
>>> a.style.lineShape = 'curved'
>>> a.style.lineType = 'dashed'
>>> a.style.dashLength = 2
>>> a.style.spaceLength = 1
>>> a.style.absoluteX = 5
>>> a.style.absoluteY = 2
>>> mxArticulationMark = MEX.articulationToXmlArticulation(a)
>>> MEX.dump(mxArticulationMark)
<doit dash-length="2" default-x="5" default-y="2"
line-shape="curved" line-type="dashed" space-length="1" />
'''
# these articulations have extra information
musicXMLArticulationName = None
for c in xmlObjects.ARTICULATION_MARKS_REV:
if isinstance(articulationMark, c):
musicXMLArticulationName = xmlObjects.ARTICULATION_MARKS_REV[c]
break
if musicXMLArticulationName is None:
musicXMLArticulationName = 'other-articulation'
# raise MusicXMLExportException(f'Cannot translate {articulationMark} to musicxml')
mxArticulationMark = Element(musicXMLArticulationName)
if articulationMark.placement is not None:
mxArticulationMark.set('placement', articulationMark.placement)
self.setPrintStyle(mxArticulationMark, articulationMark)
if musicXMLArticulationName == 'strong-accent':
strongAccent = t.cast(articulations.StrongAccent, articulationMark)
mxArticulationMark.set('type', strongAccent.pointDirection)
if musicXMLArticulationName in ('doit', 'falloff', 'plop', 'scoop'):
self.setLineStyle(mxArticulationMark, articulationMark)
if musicXMLArticulationName == 'breath-mark':
breathMark = t.cast(articulations.BreathMark, articulationMark)
if breathMark.symbol is not None:
mxArticulationMark.text = breathMark.symbol
if (musicXMLArticulationName == 'other-articulation'
and articulationMark.displayText is not None):
mxArticulationMark.text = articulationMark.displayText
# mxArticulations.append(mxArticulationMark)
return mxArticulationMark
[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')
m21Names = ('lineShape', 'lineType', 'dashLength', 'spaceLength')
self.setStyleAttributes(mxObject, m21Object, musicXMLNames, m21Names)
# def fretboardToXmlFrame(self, fretboardMark):
# '''
# >>> MEX = musicxml.m21ToXml.MeasureExporter()
# >>> fb = instruments.fretted.FretBoard(1, 2, 3)
#
# configure it here
#
# >>> mxFret = MEX.fretboardToXmlTechnical(fb)
# >>> MEX.dump(mxFret)
# <frame>lots of stuff here</frame>
# '''
# pass
[docs]
def articulationToXmlTechnical(self, articulationMark: articulations.Articulation):
# noinspection PyShadowingNames, SpellCheckingInspection
'''
Returns a tag that represents the
MusicXML structure of an articulation mark that is primarily a TechnicalIndication.
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> a = articulations.UpBow()
>>> a.placement = 'below'
>>> mxTechnicalMark = MEX.articulationToXmlTechnical(a)
>>> MEX.dump(mxTechnicalMark)
<up-bow placement="below" />
>>> f = articulations.Fingering(4)
>>> f.substitution = True
>>> mxFingering = MEX.articulationToXmlTechnical(f)
>>> MEX.dump(mxFingering)
<fingering alternate="no" substitution="yes">4</fingering>
Technical marks too specific to express in musicxml just get other-technical.
>>> g = articulations.OrganIndication()
>>> g.displayText = 'unda maris'
>>> mxOther = MEX.articulationToXmlTechnical(g)
>>> MEX.dump(mxOther)
<other-technical>unda maris</other-technical>
Same with technical marks not yet supported.
TODO: support HammerOn, PullOff, Hole, Arrow.
>>> h = articulations.HammerOn()
>>> mxOther = MEX.articulationToXmlTechnical(h)
>>> MEX.dump(mxOther)
<other-technical />
'''
# these technical have extra information
# TODO: hammer-on
# TODO: pull-off
# TODO: hole
# TODO: arrow
musicXMLTechnicalName = None
for c in xmlObjects.TECHNICAL_MARKS_REV:
if isinstance(articulationMark, c):
musicXMLTechnicalName = xmlObjects.TECHNICAL_MARKS_REV[c]
break
if musicXMLTechnicalName is None:
musicXMLTechnicalName = 'other-technical'
# TODO: support additional technical marks listed above
if musicXMLTechnicalName in ('hole', 'arrow'):
musicXMLTechnicalName = 'other-technical'
mxTechnicalMark = Element(musicXMLTechnicalName)
if articulationMark.placement is not None:
mxTechnicalMark.set('placement', articulationMark.placement)
if musicXMLTechnicalName == 'fingering':
fingering = t.cast(articulations.Fingering, articulationMark)
mxTechnicalMark.text = str(fingering.fingerNumber)
mxTechnicalMark.set('alternate',
xmlObjects.booleanToYesNo(fingering.alternate))
if (musicXMLTechnicalName in ('handbell', 'other-technical')
and articulationMark.displayText is not None):
# The handbell element represents notation for various
# techniques used in handbell and handchime music. Valid
# values are belltree [v 3.1], damp, echo, gyro, hand martellato,
# mallet lift, mallet table, martellato, martellato lift,
# muted martellato, pluck lift, and swing.
mxTechnicalMark.text = articulationMark.displayText
if musicXMLTechnicalName in ('heel', 'toe', 'fingering'):
if t.TYPE_CHECKING:
assert isinstance(articulationMark, (articulations.Fingering,
articulations.OrganIndication))
mxTechnicalMark.set('substitution',
xmlObjects.booleanToYesNo(articulationMark.substitution))
if musicXMLTechnicalName == 'string':
stringIndication = t.cast(articulations.StringIndication, articulationMark)
mxTechnicalMark.text = str(stringIndication.number)
if musicXMLTechnicalName == 'fret':
fretIndication = t.cast(articulations.FretIndication, articulationMark)
mxTechnicalMark.text = str(fretIndication.number)
if musicXMLTechnicalName == 'bend':
fretBend = t.cast(articulations.FretBend, articulationMark)
self.setBend(mxTechnicalMark, fretBend)
# harmonic needs to check for whether it is artificial or natural, and
# whether it is base-pitch, sounding-pitch, or touching-pitch
if musicXMLTechnicalName == 'harmonic':
stringHarmonic = t.cast(articulations.StringHarmonic, articulationMark)
self.setHarmonic(mxTechnicalMark, stringHarmonic)
if (musicXMLTechnicalName == 'other-technical'
and articulationMark.displayText is not None):
mxTechnicalMark.text = articulationMark.displayText
self.setPrintStyle(mxTechnicalMark, articulationMark)
# mxArticulations.append(mxArticulationMark)
return mxTechnicalMark
[docs]
@staticmethod
def setBend(mxh: Element, bend: articulations.FretBend) -> None:
'''
Sets the bend-alter SubElement and the pre-bend,
release and with-bar SubElements when present.
Called from articulationToXmlTechnical
>>> from xml.etree.ElementTree import Element
>>> from fractions import Fraction
>>> MEXclass = musicxml.m21ToXml.MeasureExporter
>>> a = articulations.FretBend(bendAlter=interval.Interval(2))
>>> mxh = Element('bend')
>>> MEXclass.setBend(mxh, a)
>>> MEXclass.dump(mxh)
<bend>
<bend-alter>2</bend-alter>
</bend>
>>> mxh = Element('bend')
>>> a = articulations.FretBend(bendAlter=interval.Interval(2))
>>> a.preBend = True
>>> MEXclass.setBend(mxh, a)
>>> MEXclass.dump(mxh)
<bend>
<bend-alter>2</bend-alter>
<pre-bend />
</bend>
>>> mxh = Element('bend')
>>> a = articulations.FretBend(bendAlter=interval.Interval(-2), release=Fraction(1, 10080))
>>> MEXclass.setBend(mxh, a)
>>> MEXclass.dump(mxh)
<bend>
<bend-alter>-2</bend-alter>
<release offset="1" />
</bend>
>>> mxh = Element('bend')
>>> a = articulations.FretBend(bendAlter=interval.Interval(-2), withBar='scoop')
>>> MEXclass.setBend(mxh, a)
>>> MEXclass.dump(mxh)
<bend>
<bend-alter>-2</bend-alter>
<with-bar>scoop</with-bar>
</bend>
'''
bendAlterSubElement = SubElement(mxh, 'bend-alter')
alter = bend.bendAlter
if alter is not None:
bendAlterSubElement.text = str(alter.semitones)
if bend.preBend:
SubElement(mxh, 'pre-bend')
if bend.release is not None:
releaseSubElement = SubElement(mxh, 'release')
quarterLengthValue = bend.release
divisionsValue = int(defaults.divisionsPerQuarter * quarterLengthValue)
releaseSubElement.set('offset', str(divisionsValue))
if bend.withBar is not None:
withBarSubElement = SubElement(mxh, 'with-bar')
withBarSubElement.text = str(bend.withBar)
[docs]
@staticmethod
def setHarmonic(mxh: Element, harm: articulations.StringHarmonic) -> None:
# noinspection PyShadowingNames
'''
Sets the artificial or natural tag (or no tag) and
zero or one of base-pitch, sounding-pitch, touching-pitch
Called from articulationToXmlTechnical
>>> MEXClass = musicxml.m21ToXml.MeasureExporter
>>> a = articulations.StringHarmonic()
>>> a.harmonicType = 'artificial'
>>> a.pitchType = 'sounding'
>>> from xml.etree.ElementTree import Element
>>> mxh = Element('harmonic')
>>> MEXClass.setHarmonic(mxh, a)
>>> MEXClass.dump(mxh)
<harmonic>
<artificial />
<sounding-pitch />
</harmonic>
:class:`~music21.articulations.Harmonic` is probably too general for most uses,
but if you use it, you get a harmonic tag with no details:
>>> b = articulations.Harmonic()
>>> mxh2 = Element('harmonic')
>>> MEXClass.setHarmonic(mxh2, b)
>>> MEXClass.dump(mxh2)
<harmonic />
'''
if not hasattr(harm, 'harmonicType'):
return
if harm.harmonicType == 'artificial':
SubElement(mxh, 'artificial')
elif harm.harmonicType == 'natural':
SubElement(mxh, 'natural')
if harm.pitchType == 'base':
SubElement(mxh, 'base-pitch')
elif harm.pitchType == 'sounding':
SubElement(mxh, 'sounding-pitch')
elif harm.pitchType == 'touching':
SubElement(mxh, 'touching-pitch')
[docs]
def noChordToXml(self, cs: harmony.NoChord) -> Element:
'''
Convert a NoChord object to an mxHarmony object (or a rest
if .writeAsChord = True).
Expected attributes of the NoChord object:
>>> nc = harmony.NoChord()
>>> nc.chordKind
'none'
>>> nc.chordKindStr
'N.C.'
Other values may not export:
>>> nc.chordKindStr = ''
>>> nc.write()
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
In part (None), measure (1): NoChord object's chordKindStr must be non-empty
>>> nc.chordKind = None
>>> nc.write()
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException:
In part (None), measure (1): NoChord object's chordKind must be 'none'
To realize the NoChord as a rest:
>>> nc2 = harmony.NoChord()
>>> nc2.writeAsChord = True
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxNote = MEX.noChordToXml(nc2)
>>> MEX.dump(mxNote)
<note>
<rest />
<duration>10080</duration>
<type>quarter</type>
</note>
'''
if cs.writeAsChord is True:
r = note.Rest(duration=cs.duration)
return self.restToXml(r)
mxHarmony = Element('harmony')
synchronizeIds(mxHarmony, cs)
self.setPrintObject(mxHarmony, cs)
self.setPrintStyle(mxHarmony, cs)
mxRoot = SubElement(mxHarmony, 'root')
mxStep = SubElement(mxRoot, 'root-step')
mxStep.text = 'C'
mxStep.set('text', '')
mxKind = SubElement(mxHarmony, 'kind')
cKind = cs.chordKind
if cs.chordKind != 'none':
raise MusicXMLExportException("NoChord object's chordKind must be 'none'")
mxKind.text = str(cKind)
if cs.chordKindStr in (None, ''):
raise MusicXMLExportException("NoChord object's chordKindStr must be non-empty")
mxKind.set('text', cs.chordKindStr)
self.setOffsetOptional(cs, mxHarmony)
self.setEditorial(mxHarmony, cs)
self.xmlRoot.append(mxHarmony)
return mxHarmony
[docs]
def romanNumeralToXml(self, rn: roman.RomanNumeral) -> Element|list[Element]:
'''
Convert a RomanNumeral object to either a chord (if .writeAsChord is True)
or a Harmony XML Element.
>>> rnI = roman.RomanNumeral('I', 'C')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEX.currentDivisions = 10
>>> listMxChords = MEX.romanNumeralToXml(rnI)
>>> len(listMxChords)
3
>>> MEX.dump(listMxChords[1])
<note>
<chord />
<pitch>
<step>E</step>
<octave>4</octave>
</pitch>
<duration>10</duration>
<type>quarter</type>
</note>
If writeAsChord is False, we create a MusicXML 4.0 <numeral> tag.
This does not work in the current version of MuseScore (which only
supports MusicXML 3.1) but outputs decently well in Finale.
>>> rnI.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(rnI)
>>> MEX.dump(mxHarmonyFromRN)
<harmony>
<numeral>
<numeral-root text="I">1</numeral-root>
<numeral-key print-object="no">
<numeral-fifths>0</numeral-fifths>
<numeral-mode>major</numeral-mode>
</numeral-key>
</numeral>
<kind>major</kind>
</harmony>
>>> complexRn = roman.RomanNumeral('#iio65', 'e-')
>>> complexRn.followsKeyChange = True
>>> complexRn.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(complexRn)
>>> MEX.dump(mxHarmonyFromRN)
<harmony>
<numeral>
<numeral-root text="#iio65">2</numeral-root>
<numeral-alter location="left">1.0</numeral-alter>
<numeral-key>
<numeral-fifths>-6</numeral-fifths>
<numeral-mode>minor</numeral-mode>
</numeral-key>
</numeral>
<kind>diminished-seventh</kind>
<inversion>1</inversion>
</harmony>
>>> maj6 = roman.RomanNumeral('VI7', 'd')
>>> maj6.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(maj6)
>>> MEX.dump(mxHarmonyFromRN)
<harmony>
<numeral>
<numeral-root text="VI7">6</numeral-root>
<numeral-key print-object="no">
<numeral-fifths>-1</numeral-fifths>
<numeral-mode>natural minor</numeral-mode>
</numeral-key>
</numeral>
<kind>major-seventh</kind>
</harmony>
>>> min6 = roman.RomanNumeral('vi', 'd')
>>> min6.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(min6)
>>> mxHarmonyFromRN.find('.//numeral-mode').text
'melodic minor'
>>> dim7 = roman.RomanNumeral('viiø65', 'd')
>>> dim7.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(dim7)
>>> mxHarmonyFromRN.find('.//numeral-mode').text
'harmonic minor'
>>> mxHarmonyFromRN.find('kind').text
'half-diminished'
>>> maj7 = roman.RomanNumeral('VII64', 'd')
>>> maj7.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(maj7)
>>> mxHarmonyFromRN.find('.//numeral-mode').text
'natural minor'
'''
if rn.writeAsChord is True:
return self.chordToXml(rn)
# because parsing "kind" is very hard, it's easier to
# create a new chordSymbol in order to get the musicxml "kind"
# a little slower than needs to be.
cs = harmony.chordSymbolFromChord(rn)
cs.offset = rn.offset # needed for not getting an extra offset tag w/ forward.
mxHarmony = self.chordSymbolToXml(cs, append=False)
if t.TYPE_CHECKING:
# will not get a list of Elements because writeAsChord is False
assert isinstance(mxHarmony, Element)
mxRoot = mxHarmony.find('root')
if mxRoot is not None:
# should never be None, but for type checking
mxHarmony.remove(mxRoot)
mxBass = mxHarmony.find('bass')
if mxBass is not None:
mxHarmony.remove(mxBass)
# use v4 RomanNumerals
mxNumeral = Element('numeral')
mxNumeralRoot = SubElement(mxNumeral, 'numeral-root')
mxNumeralRoot.set('text', rn.primaryFigure)
mxNumeralRoot.text = str(rn.scaleDegree)
if rn.frontAlterationAccidental:
mxNumeralAlter = SubElement(mxNumeral, 'numeral-alter')
# float is allowed
mxNumeralAlter.text = str(rn.frontAlterationAccidental.alter)
mxNumeralAlter.set('location', 'left')
if rn.key:
mxNumeralKey = SubElement(mxNumeral, 'numeral-key')
if not rn.followsKeyChange:
mxNumeralKey.set('print-object', 'no')
mxNumeralFifths = SubElement(mxNumeralKey, 'numeral-fifths')
mxNumeralFifths.text = str(rn.key.sharps)
mxNumeralMode = SubElement(mxNumeralKey, 'numeral-mode')
modeText = ''
if rn.key.mode == 'major':
modeText = 'major'
elif rn.scaleDegree not in (6, 7):
modeText = 'minor'
elif rn.scaleDegree == 6:
# simplest way to figure this out.
if (rn.root().pitchClass - rn.key.tonic.pitchClass) % 12 == 8:
modeText = 'natural minor'
else:
modeText = 'melodic minor'
else:
if (rn.root().pitchClass - rn.key.tonic.pitchClass) % 12 == 10:
modeText = 'natural minor'
else:
modeText = 'harmonic minor'
mxNumeralMode.text = modeText
mxHarmony.insert(0, mxNumeral)
self.xmlRoot.append(mxHarmony)
return mxHarmony
[docs]
def chordSymbolToXml(
self,
cs: harmony.ChordSymbol,
*,
append: bool = True
) -> Element|list[Element]:
# noinspection PyShadowingNames
'''
Convert a ChordSymbol object to either a chord (if .writeAsChord is True)
or a Harmony XML Element.
>>> cs = harmony.ChordSymbol()
>>> cs.root('E-')
>>> cs.bass('B-', allow_add=True)
>>> cs.inversion(2, transposeOnSet=False)
>>> cs.chordKind = 'major'
>>> cs.chordKindStr = 'M'
>>> cs
<music21.harmony.ChordSymbol E-/B->
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEX.currentDivisions = 10
>>> mxHarmony = MEX.chordSymbolToXml(cs)
>>> MEX.dump(mxHarmony)
<harmony>
<root>
<root-step>E</root-step>
<root-alter>-1</root-alter>
</root>
<kind text="M">major</kind>
<inversion>2</inversion>
<bass>
<bass-step>B</bass-step>
<bass-alter>-1</bass-alter>
</bass>
</harmony>
Now give function:
>>> cs.romanNumeral = 'I64'
>>> mxHarmony = MEX.chordSymbolToXml(cs)
>>> MEX.dump(mxHarmony)
<harmony>
<function>I64</function>
<kind text="M">major</kind>
<inversion>2</inversion>
<bass>
<bass-step>B</bass-step>
<bass-alter>-1</bass-alter>
</bass>
</harmony>
>>> hd = harmony.ChordStepModification()
>>> hd.modType = 'alter'
>>> hd.interval = -1
>>> hd.degree = 3
>>> cs.addChordStepModification(hd)
>>> mxHarmony = MEX.chordSymbolToXml(cs)
>>> MEX.dump(mxHarmony)
<harmony>
<function>I64</function>
<kind text="M">major</kind>
<inversion>2</inversion>
<bass>
<bass-step>B</bass-step>
<bass-alter>-1</bass-alter>
</bass>
<degree>
<degree-value>3</degree-value>
<degree-alter>-1</degree-alter>
<degree-type>alter</degree-type>
</degree>
</harmony>
Test altered chords:
>>> f = harmony.ChordSymbol('F sus add 9')
>>> f
<music21.harmony.ChordSymbol F sus add 9>
>>> mxHarmony = MEX.chordSymbolToXml(f)
>>> MEX.dump(mxHarmony)
<harmony>
<root>
<root-step>F</root-step>
</root>
<kind>suspended-fourth</kind>
<degree>
<degree-value>9</degree-value>
<degree-alter>0</degree-alter>
<degree-type>add</degree-type>
</degree>
</harmony>
MusicXML uses "dominant" for "dominant-seventh" so check aliases back...
>>> dom7 = harmony.ChordSymbol('C7')
>>> dom7.chordKind
'dominant-seventh'
>>> mxHarmony = MEX.chordSymbolToXml(dom7)
>>> MEX.dump(mxHarmony)
<harmony>
<root>
<root-step>C</root-step>
</root>
<kind>dominant</kind>
</harmony>
set writeAsChord to not get a symbol, but the notes. Will return a list of notes.
>>> dom7.writeAsChord = True
>>> harmonyList = MEX.chordSymbolToXml(dom7)
>>> len(harmonyList)
4
>>> MEX.dump(harmonyList[0])
<note>
<pitch>
<step>C</step>
<octave>3</octave>
</pitch>
<duration>10</duration>
<type>quarter</type>
</note>
'''
if cs.writeAsChord is True:
return self.chordToXml(cs)
mxHarmony = Element('harmony')
synchronizeIds(mxHarmony, cs)
self.setPrintObject(mxHarmony, cs)
# TODO: attr: print-frame
if cs.placement is not None:
mxHarmony.set('placement', cs.placement)
self.setPrintStyle(mxHarmony, cs)
csRoot = cs.root()
# TODO: do not look at ._attributes
if cs._roman is not None:
mxFunction = SubElement(mxHarmony, 'function')
mxFunction.text = cs.romanNumeral.figure
elif csRoot is not None:
mxRoot = SubElement(mxHarmony, 'root')
mxStep = SubElement(mxRoot, 'root-step')
mxStep.text = str(csRoot.step)
# not a todo, text attribute; use element.
# TODO: attrGroup: print-style
if csRoot.accidental is not None:
mxAlter = SubElement(mxRoot, 'root-alter')
mxAlter.text = str(common.numToIntOrFloat(csRoot.accidental.alter))
# TODO: attrGroup: print-object (why here)??
# TODO: attrGroup: print-style
# TODO: attr: location (left, right)
else:
environLocal.printDebug(['need either a root or a _roman to show'])
return mxHarmony
mxKind = SubElement(mxHarmony, 'kind')
cKind = cs.chordKind or 'none'
for xmlAlias in harmony.CHORD_ALIASES:
if harmony.CHORD_ALIASES[xmlAlias] == cKind:
cKind = xmlAlias
mxKind.text = str(cKind)
if cs.chordKindStr not in (None, ''):
mxKind.set('text', cs.chordKindStr)
# 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
csInv = cs.inversion()
if csInv not in (None, 0):
mxInversion = SubElement(mxHarmony, 'inversion')
mxInversion.text = str(csInv)
# first -- go with the already defined bass, from overrides, etc.
# but if that is not defined, then find the bass itself.
csBass = cs.bass(find=False) or cs.bass(find=True)
if csBass is not None and (csRoot is None or csRoot.name != csBass.name):
# TODO.. reuse above from Root
mxBass = SubElement(mxHarmony, 'bass')
mxStep = SubElement(mxBass, 'bass-step')
mxStep.text = str(csBass.step)
# not a todo, text attribute; use element.
# TODO: attrGroup: print-style
if csBass.accidental is not None:
mxAlter = SubElement(mxBass, 'bass-alter')
mxAlter.text = str(common.numToIntOrFloat(csBass.accidental.alter))
# TODO: attrGroup: print-object (why here)??
# TODO: attrGroup: print-style
# TODO: attr: location (left, right)
csm = cs.getChordStepModifications()
for hd in csm:
mxDegree = SubElement(mxHarmony, 'degree')
# types should be compatible
# TODO: print-object
mxDegreeValue = SubElement(mxDegree, 'degree-value')
mxDegreeValue.text = str(hd.degree)
mxDegreeAlter = SubElement(mxDegree, 'degree-alter')
# will return -1 for '-a1'
mxDegreeAlter.text = str(hd.interval.chromatic.directed) if hd.interval else '0'
# TODO: attrGroup: print-style
# TODO: attr: plus-minus (yes, no)
mxDegreeType = SubElement(mxDegree, 'degree-type')
mxDegreeType.text = str(hd.modType)
# TODO: attr: text -- alternate display
# TODO: attrGroup: print-style
# TODO: frame -- fretboard
self.setOffsetOptional(cs, mxHarmony)
self.setEditorial(mxHarmony, cs)
# staff: see joinPartStaffs()
if append:
self.xmlRoot.append(mxHarmony)
return mxHarmony
[docs]
def setOffsetOptional(self,
m21Obj: base.Music21Object,
mxObj: Element|None = None,
*,
setSound: bool = True
) -> Element|None:
'''
If this object has an offset different from self.offsetInMeasure,
then create and return an offset Element.
If mxObj is not None then the offset element will be appended to it.
'''
if m21Obj.offset == self.offsetInMeasure:
return None
offsetDifferenceInQl = m21Obj.offset - self.offsetInMeasure
offsetDifferenceInDivisions = int(offsetDifferenceInQl * self.currentDivisions)
if mxObj is not None:
mxOffset = SubElement(mxObj, 'offset')
else:
mxOffset = Element('offset')
mxOffset.text = str(offsetDifferenceInDivisions)
if setSound:
mxOffset.set('sound', 'yes') # always affects sound at location in measure.
return mxOffset
[docs]
def placeInDirection(self,
mxObj: Element,
m21Obj: base.Music21Object|None = None,
*,
setSound: bool = True
) -> Element:
'''
Places the mxObj <element> inside <direction><direction-type>
and sets <offset> if needed.
* Changed in v8: added `setSound` keyword (see :meth:`setOffsetOptional`).
'''
mxDirection = Element('direction')
mxDirectionType = SubElement(mxDirection, 'direction-type')
mxDirectionType.append(mxObj)
if m21Obj is not None:
if hasattr(m21Obj, 'placement') and m21Obj.placement is not None:
mxDirection.set('placement', m21Obj.placement)
self.setOffsetOptional(m21Obj, mxDirection, setSound=setSound)
return mxDirection
[docs]
def dynamicToXml(self, d: dynamics.Dynamic) -> Element:
# noinspection PyShadowingNames
'''
return a nested tag:
<direction><direction-type><dynamic><ff>
or whatever:
>>> ppp = dynamics.Dynamic('ppp')
>>> print(f'{ppp.volumeScalar:.2f}')
0.15
>>> ppp.style.relativeY = -10
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxDirection = MEX.dynamicToXml(ppp)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<dynamics default-x="-36" default-y="-80" relative-y="-10">
<ppp />
</dynamics>
</direction-type>
<sound dynamics="19" />
</direction>
appends to score.
Now with offset not zero.
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> ppp.offset = 1.0
>>> mxDirection = MEX.dynamicToXml(ppp)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<dynamics default-x="-36" default-y="-80" relative-y="-10">
<ppp />
</dynamics>
</direction-type>
<offset sound="yes">10080</offset>
<sound dynamics="19" />
</direction>
'''
mxDynamics = Element('dynamics')
synchronizeIds(mxDynamics, d)
if d.value in xmlObjects.DYNAMIC_MARKS:
mxThisDynamic = SubElement(mxDynamics, d.value)
else:
mxThisDynamic = SubElement(mxDynamics, 'other-dynamics')
mxThisDynamic.text = str(d.value)
# TODO: smufl
self.setPrintStyleAlign(mxDynamics, d)
# TODO: attrGroup: placement (but done for direction, so okay)
# TODO: attrGroup: text-decoration
# TODO: attrGroup: enclosure
mxDirection = self.placeInDirection(mxDynamics, d) # also handles offset
# direction todos
self.setEditorial(mxDirection, d)
# TODO: voice
# staff: see joinPartStaffs()
# sound
vS = d.volumeScalar
if vS is not None:
mxSound = SubElement(mxDirection, 'sound')
# do not set id, since the element is the same in music21.
dynamicVolume = int(vS * 127)
mxSound.set('dynamics', str(dynamicVolume))
self.xmlRoot.append(mxDirection)
return mxDirection
[docs]
def segnoToXml(self, segno: repeat.Segno) -> Element:
'''
returns a segno inside a direction-type inside a direction.
appends to score
>>> s = repeat.Segno()
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxSegnoDir = MEX.segnoToXml(s)
>>> MEX.dump(mxSegnoDir)
<direction>
<direction-type>
<segno default-y="20" />
</direction-type>
</direction>
>>> s.id = 'segno0'
>>> s.style.alignHorizontal = 'left'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxSegnoDir = MEX.segnoToXml(s)
>>> MEX.dump(mxSegnoDir)
<direction>
<direction-type>
<segno default-y="20" halign="left" id="segno0" />
</direction-type>
</direction>
'''
mxSegno = Element('segno')
synchronizeIds(mxSegno, segno)
self.setPrintStyleAlign(mxSegno, segno)
mxDirection = self.placeInDirection(mxSegno, segno)
self.xmlRoot.append(mxDirection)
return mxDirection
[docs]
def codaToXml(self, coda: repeat.Coda) -> Element:
'''
returns a coda inside a direction-type inside a direction IF coda.useSymbol is
True; otherwise returns a TextExpression and appends to score:
>>> c = repeat.Coda()
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxCodaDir = MEX.codaToXml(c)
>>> MEX.dump(mxCodaDir)
<direction>
<direction-type>
<coda default-y="20" />
</direction-type>
</direction>
turn coda.useSymbol to `False` to get a text expression instead,
and set enclosure to NO_ENCLOSURE to explicitly generate
enclosure="none".
>>> c.useSymbol = False
>>> c.style.enclosure = style.Enclosure.NO_ENCLOSURE
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxCodaText = MEX.codaToXml(c)
>>> MEX.dump(mxCodaText)
<direction>
<direction-type>
<words default-y="20" enclosure="none"
justify="center">Coda</words>
</direction-type>
</direction>
'''
if coda.useSymbol:
mxCoda = Element('coda')
synchronizeIds(mxCoda, coda)
self.setPrintStyleAlign(mxCoda, coda)
mxDirection = self.placeInDirection(mxCoda, coda)
self.xmlRoot.append(mxDirection)
return mxDirection
else:
codaTe = coda.getTextExpression()
codaTe.offset = coda.offset
return self.textExpressionToXml(codaTe)
[docs]
def tempoIndicationToXml(self, ti: tempo.TempoIndication) -> Element:
# noinspection PyShadowingNames
'''
returns a <direction> tag for a single tempo indication.
note that **two** direction tags may be added to xmlroot, the second one
as a textExpression, but only the first will be returned.
>>> mm = tempo.MetronomeMark('slow', 40, note.Note(type='half'))
>>> mm.style.justify = 'left'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxDirection = MEX.tempoIndicationToXml(mm)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<metronome parentheses="no">
<beat-unit>half</beat-unit>
<per-minute>40</per-minute>
</metronome>
</direction-type>
<sound tempo="80" />
</direction>
In this case, two directions were added to xmlRoot. Here is the other one:
>>> MEX.dump(MEX.xmlRoot.findall('direction')[1])
<direction>
<direction-type>
<words default-y="45" font-weight="bold"
justify="left">slow</words>
</direction-type>
</direction>
>>> mm = tempo.MetronomeMark('slow', 40, duration.Duration(quarterLength=1.5))
>>> mxDirection = MEX.tempoIndicationToXml(mm)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<metronome parentheses="no">
<beat-unit>quarter</beat-unit>
<beat-unit-dot />
<per-minute>40</per-minute>
</metronome>
</direction-type>
<sound tempo="60" />
</direction>
>>> mmod1 = tempo.MetricModulation()
>>> mmod1.oldReferent = 0.75 # quarterLength
>>> mmod1.newReferent = 'quarter' # type
>>> mxDirection = MEX.tempoIndicationToXml(mmod1)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<metronome parentheses="no">
<beat-unit>eighth</beat-unit>
<beat-unit-dot />
<beat-unit>quarter</beat-unit>
</metronome>
</direction-type>
</direction>
>>> mmod1.newReferent = 'longa' # music21 type w/ different musicxml name
>>> mxDirection = MEX.tempoIndicationToXml(mmod1)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<metronome parentheses="no">
<beat-unit>eighth</beat-unit>
<beat-unit-dot />
<beat-unit>long</beat-unit>
</metronome>
</direction-type>
</direction>
This is the case where only a sound tag is added and no metronome mark
>>> mm = tempo.MetronomeMark()
>>> mm.numberSounding = 60
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxDirection = MEX.tempoIndicationToXml(mm)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<words />
</direction-type>
<sound tempo="60" />
</direction>
This is the case of a TempoText.
>>> tt = tempo.TempoText('Andante')
>>> mxDirection = MEX.tempoIndicationToXml(tt)
>>> MEX.dump(mxDirection)
<direction>
<direction-type>
<words default-y="45" font-weight="bold">Andante</words>
</direction-type>
</direction>
'''
# if writing just a sound tag, place an empty words tag in a
# direction type and then follow with sound declaration
# storing lists to accommodate metric modulations
durs = [] # duration objects
numbers = [] # tempi
hideNumericalMetro = False # if numbers implicit, hide metronome numbers
hideNumber = [] # hide the number after equal, e.g., quarter=120, hide 120
# store the last value necessary as a sounding tag in bpm
soundingQuarterBPM = 0.0 # use 0.0 as False/None/etc. since unplayable.
# handle TempoText simply by exporting its textExpression.
if isinstance(ti, tempo.TempoText):
te = ti.getTextExpression()
te.offset = ti.offset
return self.textExpressionToXml(te)
if isinstance(ti, tempo.MetronomeMark):
# will not show a number of implicit
if ti.numberImplicit or ti.number is None:
# environLocal.printDebug(['found numberImplicit', ti.numberImplicit])
hideNumericalMetro = True
else:
durs.append(ti.referent)
numbers.append(ti.number)
hideNumber.append(False)
# determine number sounding; first, get from numberSounding, then
# number (if implicit, that is fine); get in terms of quarter bpm
soundingQuarterBPM = ti.getQuarterBPM() or 0.0
elif isinstance(ti, tempo.MetricModulation):
# may need to reverse order if classical style or otherwise
# may want to show first number
hideNumericalMetro = False # must show for metric modulation
for sub in (ti.oldMetronome, ti.newMetronome):
hideNumber.append(True) # cannot show numbers in a metric modulation
durs.append(sub.referent)
numbers.append(sub.number)
# soundingQuarterBPM should be obtained from the last MetronomeMark
soundingQuarterBPM = ti.newMetronome.getQuarterBPM() or 0.0
# environLocal.printDebug(['found metric modulation', ti, durs, numbers])
mxMetro = Element('metronome')
synchronizeIds(mxMetro, ti)
for i, d in enumerate(durs):
# charData of BeatUnit is the type string
mxSub = Element('beat-unit')
mxSub.text = typeToMusicXMLType(d.type)
mxMetro.append(mxSub)
for unused_dotCounter in range(d.dots):
mxMetro.append(Element('beat-unit-dot'))
if numbers and not hideNumber[i]:
mxPerMinute = SubElement(mxMetro, 'per-minute') # TODO: font.
mxPerMinute.text = str(common.numToIntOrFloat(numbers[0]))
if hasattr(ti, 'parentheses') and ti.parentheses:
mxMetro.set('parentheses', 'yes') # only attribute
else:
mxMetro.set('parentheses', 'no') # only attribute
# if writing just a sound tag, place an empty words tag in a
# direction type and then follow with sound declaration
if durs:
mxDirection = self.placeInDirection(mxMetro, ti)
else:
mxWords = Element('words')
synchronizeIds(mxWords, ti)
mxDirection = self.placeInDirection(mxWords, ti)
if soundingQuarterBPM:
mxSound = SubElement(mxDirection, 'sound')
mxSound.set('tempo', str(common.numToIntOrFloat(soundingQuarterBPM)))
if hideNumericalMetro is not None:
self.xmlRoot.append(mxDirection)
if isinstance(ti, tempo.MetronomeMark):
if ti.getTextExpression(returnImplicit=False) is not None:
te = ti.getTextExpression(returnImplicit=False)
te.offset = ti.offset
unused_mxDirectionText = self.textExpressionToXml(te)
return mxDirection
[docs]
def rehearsalMarkToXml(self, rm: expressions.RehearsalMark) -> Element:
# noinspection PyShadowingNames
'''
Convert a RehearsalMark object to a MusicXML <direction> tag with a <rehearsal> tag
inside it.
>>> rm = expressions.RehearsalMark('II')
>>> rm.style.enclosure = 'square'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxRehearsal = MEX.rehearsalMarkToXml(rm)
>>> MEX.dump(mxRehearsal)
<direction>
<direction-type>
<rehearsal enclosure="square" halign="center" valign="middle">II</rehearsal>
</direction-type>
</direction>
'''
mxRehearsal = Element('rehearsal')
self.setTextFormatting(mxRehearsal, rm)
mxRehearsal.text = str(rm.content)
mxDirection = self.placeInDirection(mxRehearsal, rm)
self.setStyleAttributes(mxDirection, rm, 'placement')
self.xmlRoot.append(mxDirection)
return mxDirection
[docs]
def pedalObjectToXml(self, po: expressions.PedalObject) -> Element|None:
# noinspection PyShadowingNames
'''
Convert a PedalObject object (i.e. PedalBounce, PedalGapStart,
PedalGapEnds) to a MusicXML <direction> tag with a <pedal> tag
inside it. Attributes like pedalForm are stored in the enclosing
PedalMark spanner.
>>> pm = expressions.PedalMark()
>>> po = expressions.PedalBounce()
>>> pm.addSpannedElements(po)
>>> pm.pedalForm = expressions.PedalForm.Line
>>> pm.pedalType = expressions.PedalType.Sustain
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxPedal = MEX.pedalObjectToXml(po)
>>> MEX.dump(mxPedal)
<direction>
<direction-type>
<pedal line="yes" type="change" />
</direction-type>
</direction>
>>> pm = expressions.PedalMark()
>>> po = expressions.PedalGapStart()
>>> pm.addSpannedElements(po)
>>> pm.pedalForm = expressions.PedalForm.Line
>>> pm.pedalType = expressions.PedalType.Sustain
>>> po.placement = 'above'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxPedal = MEX.pedalObjectToXml(po)
>>> MEX.dump(mxPedal)
<direction placement="above">
<direction-type>
<pedal line="yes" type="discontinue" />
</direction-type>
</direction>
>>> pm = expressions.PedalMark()
>>> po = expressions.PedalGapEnd()
>>> pm.addSpannedElements(po)
>>> pm.pedalForm = expressions.PedalForm.Line
>>> pm.pedalType = expressions.PedalType.Sustain
>>> po.placement = 'below'
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxPedal = MEX.pedalObjectToXml(po)
>>> MEX.dump(mxPedal)
<direction placement="below">
<direction-type>
<pedal line="yes" type="resume" />
</direction-type>
</direction>
'''
pm: expressions.PedalMark|None = None
spanners: list[spanner.Spanner] = po.getSpannerSites()
for sp in spanners:
if isinstance(sp, expressions.PedalMark):
pm = sp
break
if pm is None:
# A PedalObject that is not in a PedalMark spanner
# doesn't make sense. Ignore it.
return None
mxPedals: list[Element] = []
if isinstance(po, expressions.PedalBounce):
if pm.pedalForm in (expressions.PedalForm.Line, expressions.PedalForm.SymbolLine):
# Line or SymbolLine bounce is a quick up-down-tick in the line, so this
# is a pedal 'change'.
mxPedals = [Element('pedal')]
mxPedals[0].set('type', 'change')
elif pm.pedalForm == expressions.PedalForm.SymbolAlt:
# SymbolAlt bounce is just "Ped.", so just a pedal 'start'
mxPedals = [Element('pedal')]
if pm.pedalType == expressions.PedalType.Sustain:
mxPedals[0].set('type', 'start')
elif pm.pedalType == expressions.PedalType.Sostenuto:
mxPedals[0].set('type', 'sostenuto')
else:
# not exactly right for Soft or Silent, but
# we can hope that there is a text direction
# somewhere before this that specifies which
# pedal these "Ped." marks refer to.
mxPedals[0].set('type', 'sustain')
elif pm.pedalForm == expressions.PedalForm.Symbol:
# Symbol bounce is "*Ped.", so a pedal 'stop' followed immediately by pedal 'start'
mxPedals = [Element('pedal'), Element('pedal')]
mxPedals[0].set('type', 'stop')
if pm.pedalType == expressions.PedalType.Sustain:
mxPedals[1].set('type', 'start')
elif pm.pedalType == expressions.PedalType.Sostenuto:
mxPedals[1].set('type', 'sostenuto')
else:
# not exactly right for Soft or Silent, but
# we can hope that there is a text direction
# somewhere before this that specifies which
# pedal these "Ped." marks refer to.
mxPedals[1].set('type', 'sustain')
else:
# shouldn't be able to happen
return None
elif isinstance(po, expressions.PedalGapStart):
mxPedals = [Element('pedal')]
mxPedals[0].set('type', 'discontinue')
elif isinstance(po, expressions.PedalGapEnd):
mxPedals = [Element('pedal')]
mxPedals[0].set('type', 'resume')
else:
return None
for mxPedal in mxPedals:
if pm.pedalForm in (expressions.PedalForm.Line, expressions.PedalForm.SymbolLine):
mxPedal.set('line', 'yes')
else:
mxPedal.set('sign', 'yes')
# wrap in <direction><direction-type>
mxDirection = self.placeInDirection(mxPedal, po)
# placement goes on <direction>
self.setStyleAttributes(mxDirection, po, 'placement')
self.xmlRoot.append(mxDirection)
return mxDirection
[docs]
def makePedalResumeLineXml(self, pm: expressions.PedalMark) -> Element:
# does not append to self.xmlRoot (caller will do that)
mxPedal = Element('pedal')
mxPedal.set('type', 'resume')
mxPedal.set('line', 'yes')
# wrap in <direction><direction-type>
mxDirection = self.placeInDirection(mxPedal, pm)
# placement (from pm spanner) goes on <direction>
self.setStyleAttributes(mxDirection, pm, 'placement')
return mxDirection
[docs]
def textExpressionToXml(
self,
teOrRe: expressions.TextExpression|repeat.RepeatExpression
) -> Element:
'''
Convert a TextExpression or RepeatExpression to a MusicXML mxDirection type.
returns a musicxml.mxObjects.Direction object
'''
# TODO: if expression is empty do not set an empty <words/> tag.
mxWords = Element('words')
te: expressions.TextExpression
if isinstance(teOrRe, expressions.TextExpression): # TextExpression
te = teOrRe
mxWords.text = str(te.content)
elif isinstance(teOrRe, repeat.RepeatExpression): # RepeatExpression
te = teOrRe.getTextExpression()
te.offset = teOrRe.offset
mxWords.text = str(te.content)
else:
raise MusicXMLExportException('teOrRe must be a TextExpression or RepeatExpression')
self.setTextFormatting(mxWords, te)
mxDirection = self.placeInDirection(mxWords, te, setSound=False) # handles offset
self.xmlRoot.append(mxDirection)
return mxDirection
[docs]
def wrapObjectInAttributes(
self,
objectToWrap: base.Music21Object,
methodToMx: t.Callable[[t.Any], Element],
) -> Element|None:
# noinspection PyShadowingNames
'''
given a Clef, KeySignature, or TimeSignature which is in .elements and not m.clef,
etc. insert it in self.xmlRoot as
part of the current mxAttributes using methodToMx as a wrapper function.
(or insert into a new mxAttributes if Clef/KeySignature/etc. is not at the beginning
of the measure and not at the same point as an existing mxAttributes)
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEX.offsetInMeasure = 3.0
>>> cl = clef.BassClef()
>>> methodToMx = MEX.clefToXml
>>> mxAttributes = MEX.wrapObjectInAttributes(cl, methodToMx)
>>> MEX.dump(mxAttributes)
<attributes>
<clef>
<sign>F</sign>
<line>4</line>
</clef>
</attributes>
Also puts it in MEX.xmlRoot.
If offsetInMeasure is 0.0 then nothing is done or returned:
>>> MEX.offsetInMeasure = 0.0
>>> nothing = MEX.wrapObjectInAttributes(cl, methodToMx)
>>> nothing is None
True
'''
if self.offsetInMeasure == 0.0:
return None
mxAttributes = Element('attributes')
mxObj = methodToMx(objectToWrap)
mxAttributes.append(mxObj)
self.xmlRoot.append(mxAttributes)
return mxAttributes
def _matchesPreviousPartStaffInGroup(
self,
obj: base.Music21Object,
attr: str = 'keySignature',
comparison: str = '__eq__'
) -> bool:
'''
If this measure is part of a subsequent PartStaff in a joinable staff
group (e.g. the left hand of a keyboard part), then look up the
corresponding measure by number in the previous PartStaff, retrieve
the `attr` value there, and compare it to `obj` by `comparison`.
'''
if self.parent is None: # pragma: no cover
return False
if self.parent.previousPartStaffInGroup is None:
return False
# Not a foolproof measure lookup: see more robust measure
# matching algorithm in PartStaffExporterMixin.processSubsequentPartStaff().
# getElementsByOffset() would not be a perfect solution either,
# see https://groups.google.com/g/music21list/c/ObNOanMQjJU/m/2LMPz5NAAwAJ
if obj.measureNumber is None: # pragma: no cover
return False
maybe_measure = self.parent.previousPartStaffInGroup.measure(obj.measureNumber)
if maybe_measure is None:
return False
comparison_wrapper = getattr(obj, comparison)
return comparison_wrapper(getattr(maybe_measure, attr))
# -----------------------------
# note helpers
[docs]
def lyricToXml(self, ly: note.Lyric):
'''
Translate a music21 :class:`~music21.note.Lyric` object
to a <lyric> tag.
Lyrics have attribute list %justify, %position, %placement, %color, %print-object
'''
mxLyric = Element('lyric')
if not ly.isComposite:
_setTagTextFromAttribute(ly, mxLyric, 'syllabic', 'syllabic')
_setTagTextFromAttribute(ly, mxLyric, 'text', 'text', forceEmpty=True)
else:
# composite must have at least one component
if t.TYPE_CHECKING:
assert ly.components is not None
for i, component in enumerate(ly.components):
if component.syllabic == 'composite':
# skip doubly nested lyrics -- why, oh, why would you do that!
continue
if i >= 1:
mxElision = SubElement(mxLyric, 'elision')
if component.elisionBefore:
mxElision.text = component.elisionBefore
_setTagTextFromAttribute(component, mxLyric, 'syllabic', 'syllabic')
_setTagTextFromAttribute(component, mxLyric, 'text', 'text', forceEmpty=True)
# TODO: extend
# TODO: laughing
# TODO: humming
# TODO: end-line
# TODO: end-paragraph
# TODO: editorial
if ly.identifier is not None:
mxLyric.set('name', str(ly.identifier))
if ly.number is not None:
mxLyric.set('number', str(ly.number))
elif ly.identifier is not None:
mxLyric.set('number', str(ly.identifier))
self.setStyleAttributes(mxLyric, ly,
('justify', 'placement'),
('justify', 'placement'))
self.setPrintObject(mxLyric, ly)
self.setColor(mxLyric, ly)
self.setPosition(mxLyric, ly)
return mxLyric
[docs]
def beamsToXml(self, beams):
# noinspection PyShadowingNames
'''
Returns a list of <beam> tags
from a :class:`~music21.beam.Beams` object
>>> a = beam.Beams()
>>> a.fill(2, type='start')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxBeamList = MEX.beamsToXml(a)
>>> len(mxBeamList)
2
>>> for b in mxBeamList:
... MEX.dump(b)
<beam number="1">begin</beam>
<beam number="2">begin</beam>
'''
mxBeamList = []
for beamObj in beams.beamsList:
mxBeamList.append(self.beamToXml(beamObj))
return mxBeamList
[docs]
def beamToXml(self, beamObject):
'''
Returns an ElementTree Element from a :class:`~music21.beam.Beam` object
>>> a = beam.Beam()
>>> a.type = 'start'
>>> a.number = 1
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> b = MEX.beamToXml(a)
>>> b
<Element 'beam' at 0x104f3a728>
>>> MEX.dump(b)
<beam number="1">begin</beam>
>>> a.type = 'continue'
>>> b = MEX.beamToXml(a)
>>> MEX.dump(b)
<beam number="1">continue</beam>
>>> a.type = 'stop'
>>> b = MEX.beamToXml(a)
>>> MEX.dump(b)
<beam number="1">end</beam>
>>> a.type = 'partial'
>>> a.direction = 'left'
>>> b = MEX.beamToXml(a)
>>> MEX.dump(b)
<beam number="1">backward hook</beam>
>>> a.direction = 'right'
>>> a.id = 'beam1'
>>> b = MEX.beamToXml(a)
>>> MEX.dump(b)
<beam id="beam1" number="1">forward hook</beam>
>>> a.direction = None
>>> b = MEX.beamToXml(a)
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException: partial beam defined
without a proper direction set (set to None)
>>> a.type = 'crazy'
>>> b = MEX.beamToXml(a)
Traceback (most recent call last):
music21.musicxml.xmlObjects.MusicXMLExportException: unexpected beam type
encountered (crazy)
'''
mxBeam = Element('beam')
synchronizeIds(mxBeam, beamObject)
beamToType = {'start': 'begin',
'continue': 'continue',
'stop': 'end',
}
if beamObject.type in beamToType:
mxBeam.text = beamToType[beamObject.type]
elif beamObject.type == 'partial':
if beamObject.direction == 'left':
mxBeam.text = 'backward hook'
elif beamObject.direction == 'right':
mxBeam.text = 'forward hook'
else:
raise MusicXMLExportException(
'partial beam defined without a proper direction set '
f'(set to {beamObject.direction})'
)
else:
raise MusicXMLExportException(
f'unexpected beam type encountered ({beamObject.type})'
)
mxBeam.set('number', str(beamObject.number))
# BeamObject has no .id -- fix?
# synchronizeIds(mxBeam, beamObject)
# not to be done: repeater (deprecated)
self.setColor(mxBeam, beamObject)
# again, we pass the name 'fan' twice, so we don't have to run
# hyphenToCamelCase on it.
self.setStyleAttributes(mxBeam, beamObject, 'fan', 'fan')
return mxBeam
[docs]
def setRightBarline(self):
'''
Calls self.setBarline for the right side if the measure has a .rightBarline set.
'''
m = self.stream
if not hasattr(m, 'rightBarline'):
return
# rb = repeatBracket
rbSpanners = self.rbSpanners
rightBarline = self.stream.rightBarline
if (rightBarline is None
and (not rbSpanners or not rbSpanners[0].isLast(m))):
return
else:
# rightBarline may be None
self.setBarline(rightBarline, 'right')
[docs]
def setLeftBarline(self):
'''
Calls self.setBarline for the left side if the measure has a .leftBarline set.
'''
m = self.stream
if not hasattr(m, 'leftBarline'):
return
# rb = repeatBracket
rbSpanners = self.rbSpanners
leftBarline = m.leftBarline
if (leftBarline is None
and (not rbSpanners or not rbSpanners[0].isFirst(m))):
return
else:
# leftBarline may be None. that's okay
self.setBarline(leftBarline, 'left')
[docs]
def setBarline(self, barline, position):
'''
sets either a left or right barline from a
bar.Barline() object or bar.Repeat() object
'''
mxRepeat = None
if barline is None:
mxBarline = Element('barline')
else:
if isinstance(barline, bar.Repeat):
mxBarline = Element('barline')
mxRepeat = self.repeatToXml(barline)
else:
mxBarline = self.barlineToXml(barline)
synchronizeIds(mxBarline, barline)
mxBarline.set('location', position)
# TODO: editorial
# TODO: wavy-line
# TODO: segno
# TODO: coda
# TODO: fermata
if self.rbSpanners:
if position == 'left' and self.rbSpanners[0].isFirst(self.stream):
endingType = 'start'
elif position == 'right' and self.rbSpanners[0].isLast(self.stream):
endingType = 'stop'
else:
endingType = ''
if endingType:
mxEnding = Element('ending')
numberList = self.rbSpanners[0].numberRange
numberStr = str(numberList[0])
# 0 is not a valid "ending-number"
if numberStr == '0':
numberStr = ''
for num in numberList[1:]:
numberStr += ',' + str(num) # comma-separated ending numbers
mxEnding.set('number', numberStr)
mxEnding.set('type', endingType)
mxBarline.append(mxEnding) # make sure it is after fermata but before repeat.
if mxRepeat is not None:
mxBarline.append(mxRepeat)
# TODO: attr: segno
# TODO: attr: coda
# TODO: attr: divisions
self.xmlRoot.append(mxBarline)
return mxBarline
[docs]
def barlineToXml(self, barObject):
# noinspection PyShadowingNames
'''
Translate a music21 bar.Bar object to an mxBar
while making two substitutions: double -> light-light
and final -> light-heavy as shown below.
>>> b = bar.Barline('final')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxBarline = MEX.barlineToXml(b)
>>> MEX.dump(mxBarline)
<barline>
<bar-style>light-heavy</bar-style>
</barline>
>>> b.location = 'right'
>>> mxBarline = MEX.barlineToXml(b)
>>> MEX.dump(mxBarline)
<barline location="right">
<bar-style>light-heavy</bar-style>
</barline>
'''
mxBarline = Element('barline')
mxBarStyle = SubElement(mxBarline, 'bar-style')
mxBarStyle.text = barObject.musicXMLBarStyle()
# TODO: mxBarStyle attr: color
if barObject.location is not None:
mxBarline.set('location', barObject.location)
return mxBarline
[docs]
def repeatToXml(self, r):
# noinspection PyShadowingNames
'''
returns a <repeat> tag from a barline object.
>>> b = bar.Repeat(direction='end')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxRepeat = MEX.repeatToXml(b)
>>> MEX.dump(mxRepeat)
<repeat direction="backward" />
>>> b.times = 3
>>> mxRepeat = MEX.repeatToXml(b)
>>> MEX.dump(mxRepeat)
<repeat direction="backward" times="3" />
'''
mxRepeat = Element('repeat')
if r.direction == 'start':
mxRepeat.set('direction', 'forward')
elif r.direction == 'end':
mxRepeat.set('direction', 'backward')
# elif self.direction == 'bidirectional':
# environLocal.printDebug(['skipping bi-directional repeat'])
else:
raise bar.BarException('cannot handle direction format:', r.direction)
if r.times is not None:
mxRepeat.set('times', str(r.times))
# TODO: attr: winged
return mxRepeat
[docs]
def setMxAttributesObjectForStartOfMeasure(self):
'''
creates an <attributes> tag at the start of the measure.
We create one for each measure unless it is identical to self.parent.mxAttributes
'''
m = self.stream
mxAttributes = Element('attributes')
# TODO: footnote
# TODO: level
# TODO: Do something more intelligent with this
self.currentDivisions = defaults.divisionsPerQuarter
if self.parent is None or self.currentDivisions != self.parent.lastDivisions:
mxDivisions = SubElement(mxAttributes, 'divisions')
mxDivisions.text = str(self.currentDivisions)
self.parent.lastDivisions = self.currentDivisions
if (m.keySignature is not None
and not self._matchesPreviousPartStaffInGroup(
m.keySignature, 'keySignature'
)):
mxAttributes.append(self.keySignatureToXml(m.keySignature))
if (m.timeSignature is not None
and not self._matchesPreviousPartStaffInGroup(
m.timeSignature, 'timeSignature', comparison='ratioEqual'
)):
mxAttributes.append(self.timeSignatureToXml(m.timeSignature))
smts = list(m.getElementsByClass(meter.SenzaMisuraTimeSignature))
if smts:
mxAttributes.append(self.timeSignatureToXml(smts[0]))
# For staves, see joinPartStaffs()
# TODO: part-symbol
# TODO: instruments
if m.clef is not None:
mxAttributes.append(self.clefToXml(m.clef))
found = m.getElementsByClass(layout.StaffLayout)
if found:
sl = found[0] # assume only one per measure
mxAttributes.append(self.staffLayoutToXmlStaffDetails(sl))
if self.transpositionInterval is not None:
mxAttributes.append(self.intervalToXmlTranspose(self.transpositionInterval))
# directive goes here, but is deprecated, do not support
# measureStyle
mxMeasureStyle = self.measureStyle()
if mxMeasureStyle is not None:
mxAttributes.append(mxMeasureStyle)
if len(mxAttributes) > 0 or mxAttributes.attrib:
self.xmlRoot.append(mxAttributes)
return mxAttributes
[docs]
def measureStyle(self):
'''
return a <measure-style> Element or None according to the contents of the Stream.
Currently, only multiple-rest is supported.
'''
m = self.stream
mxMeasureStyle = None
mxMultipleRest = None
rests = m.getElementsByClass(note.Rest)
if rests:
hasMMR = rests[0].getSpannerSites('MultiMeasureRest')
if hasMMR:
firstRestMMR = hasMMR[0]
if firstRestMMR.isFirst(rests[0]):
mxMultipleRest = Element('multiple-rest')
if firstRestMMR.useSymbols:
mxMultipleRest.set('use-symbols', 'yes')
else:
mxMultipleRest.set('use-symbols', 'no')
mxMultipleRest.text = str(firstRestMMR.numRests)
if mxMultipleRest is not None:
mxMeasureStyle = Element('measure-style')
# Skip: id: has no corresponding element.
# TODO or skip: font, color, number.
mxMeasureStyle.append(mxMultipleRest)
return mxMeasureStyle
[docs]
def staffLayoutToXmlStaffDetails(self, staffLayout):
'''
Convert a :class:`~music21.layout.StaffLayout` object to a
<staff-details> element.
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> sl = layout.StaffLayout()
>>> sl.staffLines = 3 # tenor drums?
>>> sl.staffType = stream.enums.StaffType.CUE
>>> sl.hidden = True
>>> mxDetails = MEX.staffLayoutToXmlStaffDetails(sl)
>>> MEX.dump(mxDetails)
<staff-details print-object="no">
<staff-type>cue</staff-type>
<staff-lines>3</staff-lines>
</staff-details>
'''
# TODO: number lines from the bottom and hide others as necessary
# see: https://github.com/w3c-cg/musicxml/issues/351
# TODO: number (bigger issue)
# TODO: show-frets
# TODO: print-spacing
mxStaffDetails = Element('staff-details')
if staffLayout.hidden is True:
mxStaffDetails.set('print-object', 'no')
else:
mxStaffDetails.set('print-object', 'yes')
StaffType = stream.enums.StaffType
if staffLayout.staffType not in (StaffType.REGULAR, StaffType.OTHER):
mxStaffType = SubElement(mxStaffDetails, 'staff-type')
mxStaffType.text = staffLayout.staffType.value
if staffLayout.staffLines is not None:
mxStaffLines = SubElement(mxStaffDetails, 'staff-lines')
mxStaffLines.text = str(staffLayout.staffLines)
# TODO: staff-tuning
# TODO: capo
# TODO: staff-size
return mxStaffDetails
[docs]
def timeSignatureToXml(self, ts: meter.TimeSignature|meter.SenzaMisuraTimeSignature):
'''
Returns a single <time> tag from a meter.TimeSignature object.
Compound meters are represented as multiple pairs of beat
and beat-type elements
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> a = meter.TimeSignature('3/4')
>>> b = MEX.timeSignatureToXml(a)
>>> MEX.dump(b)
<time>
<beats>3</beats>
<beat-type>4</beat-type>
</time>
>>> a = meter.TimeSignature('3/4+2/4')
>>> b = MEX.timeSignatureToXml(a)
>>> MEX.dump(b)
<time>
<beats>3</beats>
<beat-type>4</beat-type>
<beats>2</beats>
<beat-type>4</beat-type>
</time>
>>> a.setDisplay('5/4')
>>> b = MEX.timeSignatureToXml(a)
>>> MEX.dump(b)
<time>
<beats>5</beats>
<beat-type>4</beat-type>
</time>
>>> a = meter.TimeSignature('4/4')
>>> a.symbol = 'common'
>>> b = MEX.timeSignatureToXml(a)
>>> MEX.dump(b)
<time symbol="common">
<beats>4</beats>
<beat-type>4</beat-type>
</time>
>>> a.symbol = ''
>>> a.symbolizeDenominator = True
>>> b = MEX.timeSignatureToXml(a)
>>> MEX.dump(b)
<time symbol="note">
<beats>4</beats>
<beat-type>4</beat-type>
</time>
>>> sm = meter.SenzaMisuraTimeSignature('free')
>>> b = MEX.timeSignatureToXml(sm)
>>> MEX.dump(b)
<time>
<senza-misura>free</senza-misura>
</time>
'''
# mxTimeList = []
mxTime = Element('time')
synchronizeIds(mxTime, ts)
if isinstance(ts, meter.SenzaMisuraTimeSignature):
mxSenzaMisura = SubElement(mxTime, 'senza-misura')
if ts.text is not None:
mxSenzaMisura.text = ts.text
return mxTime
# always get a flat version to display any subdivisions created
fList = tuple((mt.numerator, mt.denominator) for mt in ts.displaySequence.flatten())
if ts.summedNumerator:
# this will try to reduce any common denominators into
# a common group
fList = meter.tools.fractionToSlashMixed(fList)
for n, d in fList:
mxBeats = SubElement(mxTime, 'beats')
mxBeats.text = str(n)
mxBeatType = SubElement(mxTime, 'beat-type')
mxBeatType.text = str(d)
# TODO: interchangeable
# TODO: choice -- senza-misura
# TODO: attr: interchangeable
# attr: symbol
if ts.symbolizeDenominator:
mxTime.set('symbol', 'note')
elif ts.symbol != '':
mxTime.set('symbol', ts.symbol)
# symbol: dotted-note not supported
# TODO: attr: separator
self.setPrintStyleAlign(mxTime, ts)
self.setPrintObject(mxTime, ts)
return mxTime
[docs]
def keySignatureToXml(self, keyOrKeySignature):
# noinspection PyShadowingNames
'''
returns a key tag from a music21
key.KeySignature or key.Key object
>>> ks = key.KeySignature(-3)
>>> ks
<music21.key.KeySignature of 3 flats>
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxKey = MEX.keySignatureToXml(ks)
>>> MEX.dump(mxKey)
<key>
<fifths>-3</fifths>
</key>
>>> ks.mode = 'major'
>>> mxKey = MEX.keySignatureToXml(ks)
>>> MEX.dump(mxKey)
<key>
<fifths>-3</fifths>
<mode>major</mode>
</key>
>>> ksNonTrad = key.KeySignature(sharps=None)
>>> ksNonTrad.alteredPitches = ['C#', 'E-4']
>>> ksNonTrad
<music21.key.KeySignature of pitches: [C#, E-4]>
>>> mxKeyNonTrad = MEX.keySignatureToXml(ksNonTrad)
>>> MEX.dump(mxKeyNonTrad)
<key>
<key-step>C</key-step>
<key-alter>1</key-alter>
<key-step>E</key-step>
<key-alter>-1</key-alter>
<key-octave number="2">4</key-octave>
</key>
'''
seta = _setTagTextFromAttribute
mxKey = Element('key')
synchronizeIds(mxKey, keyOrKeySignature)
# TODO: attr: number
self.setPrintStyle(mxKey, keyOrKeySignature)
# TODO: attr: print-object
if not keyOrKeySignature.isNonTraditional:
# Choice traditional-key
# TODO: cancel
seta(keyOrKeySignature, mxKey, 'fifths', 'sharps')
if hasattr(keyOrKeySignature, 'mode') and keyOrKeySignature.mode is not None:
seta(keyOrKeySignature, mxKey, 'mode', 'mode')
else:
# choice: non-traditional-key
for p in keyOrKeySignature.alteredPitches:
seta(p, mxKey, 'key-step', 'step')
a = p.accidental
if a is None: # can't imagine why it would be
a = pitch.Accidental(0)
mxAlter = SubElement(mxKey, 'key-alter')
mxAlter.text = str(common.numToIntOrFloat(a.alter))
# TODO: key-accidental
for i, p in enumerate(keyOrKeySignature.alteredPitches):
if p.octave is not None:
mxKeyOctave = SubElement(mxKey, 'key-octave')
mxKeyOctave.text = str(p.octave)
mxKeyOctave.set('number', str(i + 1))
return mxKey
[docs]
def clefToXml(self, clefObj):
'''
Given a music21 Clef object, return a MusicXML clef
tag.
>>> gc = clef.GClef()
>>> gc
<music21.clef.GClef>
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> mxc = MEX.clefToXml(gc)
>>> MEX.dump(mxc)
<clef>
<sign>G</sign>
</clef>
>>> b = clef.Treble8vbClef()
>>> b.octaveChange
-1
>>> mxc2 = MEX.clefToXml(b)
>>> MEX.dump(mxc2)
<clef>
<sign>G</sign>
<line>2</line>
<clef-octave-change>-1</clef-octave-change>
</clef>
>>> pc = clef.PercussionClef()
>>> mxc3 = MEX.clefToXml(pc)
>>> MEX.dump(mxc3)
<clef>
<sign>percussion</sign>
</clef>
Clefs without signs get exported as G clefs with a warning
>>> generic = clef.Clef()
>>> mxc4 = MEX.clefToXml(generic)
Clef with no .sign exported; setting as a G clef
>>> MEX.dump(mxc4)
<clef>
<sign>G</sign>
</clef>
'''
mxClef = Element('clef')
synchronizeIds(mxClef, clefObj)
self.setPrintStyle(mxClef, clefObj)
# TODO: attr: print-object
# For attr: number, see joinPartStaffs()
# TODO: attr: additional
# TODO: attr: size
# TODO: attr: after-barline
sign = clefObj.sign
if sign is None:
print('Clef with no .sign exported; setting as a G clef')
sign = 'G'
mxSign = SubElement(mxClef, 'sign')
mxSign.text = sign
_setTagTextFromAttribute(clefObj, mxClef, 'line', 'line')
if clefObj.octaveChange not in (0, None):
_setTagTextFromAttribute(clefObj, mxClef, 'clef-octave-change', 'octaveChange')
return mxClef
[docs]
def intervalToXmlTranspose(self, i=None):
# noinspection PyShadowingNames
'''
>>> ME = musicxml.m21ToXml.MeasureExporter()
>>> i = interval.Interval('P5')
>>> mxTranspose = ME.intervalToXmlTranspose(i)
>>> ME.dump(mxTranspose)
<transpose>
<diatonic>4</diatonic>
<chromatic>7</chromatic>
</transpose>
>>> i = interval.Interval('A13')
>>> mxTranspose = ME.intervalToXmlTranspose(i)
>>> ME.dump(mxTranspose)
<transpose>
<diatonic>5</diatonic>
<chromatic>10</chromatic>
<octave-change>1</octave-change>
</transpose>
>>> i = interval.Interval('-M6')
>>> mxTranspose = ME.intervalToXmlTranspose(i)
>>> ME.dump(mxTranspose)
<transpose>
<diatonic>-5</diatonic>
<chromatic>-9</chromatic>
</transpose>
>>> i = interval.Interval('-M9')
>>> mxTranspose = ME.intervalToXmlTranspose(i)
>>> ME.dump(mxTranspose)
<transpose>
<diatonic>-1</diatonic>
<chromatic>-2</chromatic>
<octave-change>-1</octave-change>
</transpose>
'''
# TODO: number attribute (staff number)
# TODO: double empty attribute
if i is None:
i = self.transpositionInterval
genericSteps = i.diatonic.generic.directed
musicxmlOctaveShift, musicxmlDiatonic = divmod(abs(genericSteps) - 1, 7)
musicxmlChromatic = abs(i.chromatic.semitones) % 12
if genericSteps < 0:
musicxmlDiatonic *= -1
musicxmlOctaveShift *= -1
musicxmlChromatic *= -1
mxTranspose = Element('transpose')
synchronizeIds(mxTranspose, i)
mxDiatonic = SubElement(mxTranspose, 'diatonic')
mxDiatonic.text = str(musicxmlDiatonic)
mxChromatic = SubElement(mxTranspose, 'chromatic')
mxChromatic.text = str(musicxmlChromatic)
if musicxmlOctaveShift != 0:
mxOctaveChange = SubElement(mxTranspose, 'octave-change')
mxOctaveChange.text = str(musicxmlOctaveShift)
return mxTranspose
[docs]
def setMxPrint(self):
'''
Creates a <print> element and appends it to root, if one is needed.
'''
m = self.stream
# print objects come before attributes
# note: this class match is a problem in cases where the object
# is created in the module itself, as in a test.
# do a quick search for any layout objects before searching individually
foundAny = m.getElementsByClass(layout.LayoutBase)
if not foundAny:
return
mxPrint = None
found = foundAny.getElementsByClass(layout.PageLayout)
if found:
pl = found[0] # assume only one per measure
mxPrint = self.pageLayoutToXmlPrint(pl)
found = foundAny.getElementsByClass(layout.SystemLayout)
if found:
sl = found[0] # assume only one per measure
if mxPrint is None:
mxPrint = self.systemLayoutToXmlPrint(sl)
else:
self.systemLayoutToXmlPrint(sl, mxPrint)
found = foundAny.getElementsByClass(layout.StaffLayout)
if found:
sl = found[0] # assume only one per measure
if mxPrint is None:
mxPrint = self.staffLayoutToXmlPrint(sl)
else:
self.staffLayoutToXmlPrint(sl, mxPrint)
# TODO: measure-layout
if m.hasStyleInformation and m.style.measureNumbering is not None:
if mxPrint is None:
mxPrint = Element('print')
mxMeasureNumbering = SubElement(mxPrint, 'measure-numbering')
mxMeasureNumbering.text = m.style.measureNumbering
mnStyle = m.style.measureNumberStyle
if mnStyle is not None:
self.setPrintStyleAlign(mxMeasureNumbering, mnStyle)
# TODO: part-name-display
# TODO: part-abbreviation-display
# TODO: attr: blank-page
# TODO: attr: page-number
if mxPrint is not None:
self.xmlRoot.append(mxPrint)
[docs]
def staffLayoutToXmlPrint(self, staffLayout, mxPrint=None):
if mxPrint is None:
mxPrint = Element('print')
synchronizeIds(mxPrint, staffLayout)
mxStaffLayout = self.staffLayoutToXmlStaffLayout(staffLayout)
mxPrint.append(mxStaffLayout)
return mxPrint
[docs]
def setMxAttributes(self):
'''
sets the attributes (x=y) for a measure,
that is, number, implicit, and layoutWidth
Does not create the <attributes> tag. That's elsewhere.
'''
m = self.stream
if hasattr(m, 'measureNumberWithSuffix'):
self.xmlRoot.set('number', m.measureNumberWithSuffix())
setAttributeFromAttribute(
m, self.xmlRoot, 'implicit', 'showNumber',
lambda showNum: xmlObjects.booleanToYesNo(showNum is stream.enums.ShowNumber.NEVER))
# TODO: attr: non-controlling
if hasattr(m, 'layoutWidth') and m.layoutWidth is not None:
setAttributeFromAttribute(m, self.xmlRoot, 'width', 'layoutWidth')
[docs]
def setRbSpanners(self):
'''
Makes a set of spanners from repeat brackets
'''
spannersOnStream = self.spannerBundle.getBySpannedElement(self.stream)
self.rbSpanners = spannersOnStream.getByClass('RepeatBracket')
[docs]
def setTranspose(self):
'''
Set the transposition interval based on whether the active
instrument for this period has a transposition object.
Stores in self.transpositionInterval. Returns None
'''
if self.parent is None:
return None
if self.parent.stream is None:
return None
m = self.stream
self.measureOffsetStart = m.getOffsetBySite(self.parent.stream)
instSubStream = self.parent.instrumentStream.getElementsByOffset(
self.measureOffsetStart,
self.measureOffsetStart + m.duration.quarterLength,
includeEndBoundary=False)
if not instSubStream:
return None
instSubObj = instSubStream.first()
if instSubObj.transposition is None:
return None
self.transpositionInterval = instSubObj.transposition
# do here???
# self.mxTranspose = self.intervalToMXTranspose()
return None
# unittests moved to test_m21ToXml
if __name__ == '__main__':
import music21
music21.mainTest() # doctests only