Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
music21/music21/mei/base.py /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
3540 lines (2798 sloc)
137 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # -*- coding: utf-8 -*- | |
| # ----------------------------------------------------------------------------- | |
| # Name: mei/base.py | |
| # Purpose: Public interfaces for the MEI module | |
| # | |
| # Authors: Christopher Antila | |
| # | |
| # Copyright: Copyright © 2014 Michael Scott Asato Cuthbert | |
| # License: BSD, see license.txt | |
| # ----------------------------------------------------------------------------- | |
| ''' | |
| These are the public interfaces for the MEI module by Christopher Antila | |
| To convert a string with MEI markup into music21 objects, | |
| use :meth:`~music21.mei.MeiToM21Converter.convertFromString`. | |
| In the future, most of the functions in this module should be moved to a separate, import-only | |
| module, so that functions for writing music21-to-MEI will fit nicely. | |
| **Simple "How-To"** | |
| Use :class:`MeiToM21Converter` to convert a string to a set of music21 objects. In the future, the | |
| :class:`M21ToMeiConverter` class will convert a set of music21 objects into a string with an MEI | |
| document. | |
| >>> meiString = """<?xml version="1.0" encoding="UTF-8"?> | |
| ... <mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="2013"> | |
| ... <music> | |
| ... <score> | |
| ... <scoreDef meter.count="6" meter.unit="8"> | |
| ... <staffGrp> | |
| ... <staffDef n="1" clef.shape="F" clef.line="4"/> | |
| ... </staffGrp> | |
| ... </scoreDef> | |
| ... <section> | |
| ... <scoreDef key.sig="1f" key.mode="major"/> | |
| ... <measure n="1"> | |
| ... <staff n="1"> | |
| ... <layer n="1"> | |
| ... <beam> | |
| ... <note pname="E" oct="3" dur="8" artic="stacc"/> | |
| ... <note pname="E" oct="3" dur="8"/> | |
| ... <note pname="E" oct="3" dur="8"/> | |
| ... </beam> | |
| ... <chord dur="4" dots="1"> | |
| ... <note pname="F" oct="2"/> | |
| ... <note pname="A" oct="2" accid="f"/> | |
| ... </chord> | |
| ... </layer> | |
| ... </staff> | |
| ... </measure> | |
| ... </section> | |
| ... </score> | |
| ... </music> | |
| ... </mei> | |
| ... """ | |
| >>> conv = mei.MeiToM21Converter(meiString) | |
| >>> result = conv.run() | |
| >>> result | |
| <music21.stream.Score 0x10ee474f0> | |
| **Terminology** | |
| This module's documentation adheres to the following terminology regarding XML documents, using | |
| this snippet, ``<note pname="C"/>`` as an example: | |
| - the entire snippet is an *element*. | |
| - the word ``note`` is the *tag*. | |
| - the word ``pname`` is an *attribute*. | |
| - the letter ``C`` is a *value*. | |
| Because Python also uses "attributes," an XML attribute is always preceded by an "at sign," as in | |
| @pname, whereas a Python attribute is set as :attr:`pname`. | |
| **Ignored Elements** | |
| The following elements are not yet imported, though you might expect they would be: | |
| * <sb>: a system break, since this is not usually semantically significant | |
| * <lb>: a line break, since this is not usually semantically significant | |
| * <pb>: a page break, since this is not usually semantically significant | |
| **Where Elements Are Processed** | |
| Most elements are processed in functions called :func:`tagFromElement`, where "tag" is replaced by | |
| the element's tag name (e.g., :func:`staffDefFromElement` for <staffDef> elements). These functions | |
| convert from a Python :class:`xml.etree.ElementTree.Element` | |
| object to the appropriate music21 object. | |
| However, certain elements are processed primarily in | |
| another way, by "private" functions that are not | |
| documented in this API. Rather than converting an :class:`Element` object into a music21 object, | |
| these functions modify the MEI document tree by adding instructions for the :func:`tagFromElement` | |
| functions. The elements processed by private functions include: | |
| * <slur> | |
| * <tie> | |
| * <beamSpan> | |
| * <tupletSpan> | |
| Whereas you can expect functions like :func:`clefFromElement` | |
| to convert a <clef> into a :class:`Clef` | |
| with no loss of information. Because we cannot provide a simple one-to-one conversion for slurs, | |
| ties, and tuplets, we have kept their conversion functions "private," | |
| to emphasize the fact that you | |
| must use the :class:`MeiToM21Converter` to process them properly. | |
| **Guidelines for Encoders** | |
| While we aim for the best possible compatibility, the MEI | |
| specification is very large. The following | |
| guidelines will help you produce a file that this MEI-to-music21 module will import correctly and | |
| in the most efficient way. These should not necessarily be considered recommendations when using | |
| MEI in any other context. | |
| * Tuplets indicated only in a @tuplet attribute do not work. | |
| * For elements that allow @startid, @endid, and @plist attributes, | |
| use all three for faster importing. | |
| * For a <tupletSpan> that does not specify a @plist attribute, a tuplet spanning more than two | |
| measures will always and unavoidably be imported incorrectly. | |
| * For any tuplet, specify at least @num and @numbase. The module refuses to import a tuplet that | |
| does not have the @numbase attribute. | |
| * Retain consistent @n values for the same layer, staff, and instrument throughout the score. | |
| * Always indicate the duration of <mRest> and <mSpace> elements. | |
| * Avoid using the <barLine> element if you require well-formatted output from music21, since (as of | |
| January 2015) the music21-to-something converters will only output a :class:`Barline` that is | |
| part of a :class:`Measure`. | |
| **List of Supported Elements** | |
| Alphabetical list of the elements currently supported by this module: | |
| * :func:`accidFromElement` | |
| * :func:`articFromElement` | |
| * :func:`barLineFromElement` | |
| * :func:`beamFromElement` | |
| * :func:`chordFromElement` | |
| * :func:`clefFromElement` | |
| * :func:`dotFromElement` | |
| * :func:`instrDefFromElement` | |
| * :func:`layerFromElement` | |
| * :func:`measureFromElement` | |
| * :func:`noteFromElement` | |
| * :func:`restFromElement` | |
| * :func:`mRestFromElement` | |
| * :func:`spaceFromElement` | |
| * :func:`mSpaceFromElement` | |
| * :func:`scoreFromElement` | |
| * :func:`scoreDefFromElement` | |
| * :func:`sectionFromElement` | |
| * :func:`staffFromElement` | |
| * :func:`staffDefFromElement` | |
| * :func:`staffGrpFromElement` | |
| * :func:`sylFromElement` | |
| * :func:`tupletFromElement` | |
| * :func:`verseFromElement` | |
| To know which MEI attributes are known to import correctly, read the documentation for the relevant | |
| element. For example, to know whether the @color attribute on a <note> element is supported, read | |
| the "Attributes/Elements Implemented" section of the :func:`noteFromElement` documentation. | |
| **List of Ignored Elements** | |
| The following elements are (silently) ignored by the MEI-to-music21 converter because they primarily | |
| affect the layout and typesetting of a musical score. We may choose to implement these elements in | |
| the future, but they are a lower priority because music21 is not primarily a layout or typesetting | |
| tool. | |
| * <multiRest>: a multi-measure rest (these will be "converted" to single-measure rests) | |
| * <pb>: a page break | |
| * <lb>: a line break | |
| * <sb>: a system break | |
| ''' | |
| from __future__ import annotations | |
| from collections import defaultdict | |
| from copy import deepcopy | |
| import typing as t | |
| from uuid import uuid4 | |
| from xml.etree.ElementTree import Element, ParseError, fromstring, ElementTree | |
| # music21 | |
| from music21 import articulations | |
| from music21 import bar | |
| from music21 import chord | |
| from music21 import clef | |
| from music21 import duration | |
| from music21 import environment | |
| from music21 import exceptions21 | |
| from music21 import instrument | |
| from music21 import interval | |
| from music21 import key | |
| from music21 import metadata | |
| from music21 import meter | |
| from music21 import note | |
| from music21 import pitch | |
| from music21 import stream | |
| from music21 import spanner | |
| from music21 import tie | |
| if t.TYPE_CHECKING: | |
| from fractions import Fraction | |
| environLocal = environment.Environment('mei.base') | |
| # Module-Level Constants | |
| # ----------------------------------------------------------------------------- | |
| _XMLID = '{http://www.w3.org/XML/1998/namespace}id' | |
| MEI_NS = '{http://www.music-encoding.org/ns/mei}' | |
| # when these tags aren't processed, we won't worry about them (at least for now) | |
| _IGNORE_UNPROCESSED = ( | |
| f'{MEI_NS}sb', # system break | |
| f'{MEI_NS}lb', # line break | |
| f'{MEI_NS}pb', # page break | |
| f'{MEI_NS}slur', # slurs; handled in convertFromString() | |
| f'{MEI_NS}tie', # ties; handled in convertFromString() | |
| f'{MEI_NS}tupletSpan', # tuplets; handled in convertFromString() | |
| f'{MEI_NS}beamSpan', # beams; handled in convertFromString() | |
| f'{MEI_NS}instrDef', # instrument; handled separately by staffDefFromElement() | |
| ) | |
| # Exceptions | |
| # ----------------------------------------------------------------------------- | |
| class MeiValidityError(exceptions21.Music21Exception): | |
| ''' | |
| When there is an otherwise-unspecified validity error that prevents parsing. | |
| ''' | |
| pass | |
| class MeiValueError(exceptions21.Music21Exception): | |
| ''' | |
| When an attribute has an invalid value. | |
| ''' | |
| pass | |
| class MeiAttributeError(exceptions21.Music21Exception): | |
| ''' | |
| When an element has an invalid attribute. | |
| ''' | |
| pass | |
| class MeiElementError(exceptions21.Music21Exception): | |
| ''' | |
| When an element itself is invalid. | |
| ''' | |
| pass | |
| # Text Strings for Error Conditions | |
| # ----------------------------------------------------------------------------- | |
| # NOTE: these are all collected handily at the top for two reasons: help you find the easier, and | |
| # help you translate them easier | |
| _TEST_FAILS = 'MEI module had {} failures and {} errors; run music21/mei/base.py to find out more.' | |
| _INVALID_XML_DOC = 'MEI document is not valid XML.' | |
| _WRONG_ROOT_ELEMENT = 'Root element should be <mei> in the MEI namespace, not <{}>.' | |
| _UNKNOWN_TAG = 'Found unexpected tag while parsing MEI: <{}>.' | |
| _UNEXPECTED_ATTR_VALUE = 'Unexpected value for "{}" attribute: {}' | |
| _SEEMINGLY_NO_PARTS = 'There appear to be no <staffDef> tags in this score.' | |
| _MISSING_VOICE_ID = 'Found a <layer> without @n attribute and no override.' | |
| _CANNOT_FIND_XMLID = 'Could not find the @{} so we could not create the {}.' | |
| _MISSING_TUPLET_DATA = 'Both @num and @numbase attributes are required on <tuplet> tags.' | |
| _UNIMPLEMENTED_IMPORT = 'Importing {} without {} is not yet supported.' | |
| _UNPROCESSED_SUBELEMENT = 'Found an unprocessed <{}> element in a <{}>.' | |
| _MISSED_DATE = 'Unable to decipher the composition date "{}"' | |
| _BAD_VERSE_NUMBER = 'Verse number must be an int (got "{}")' | |
| # Module-level Functions | |
| # ----------------------------------------------------------------------------- | |
| class MeiToM21Converter: | |
| ''' | |
| A :class:`MeiToM21Converter` instance manages the conversion of an MEI document into music21 | |
| objects. | |
| If ``theDocument`` does not have <mei> as the root element, the class raises an | |
| :class:`MeiElementError`. If ``theDocument`` is not a valid XML file, the class raises an | |
| :class:`MeiValidityError`. | |
| :param str theDocument: A string containing an MEI document. | |
| :raises: :exc:`MeiElementError` when the root element is not <mei> | |
| :raises: :exc:`MeiValidityError` when the MEI file is not valid XML. | |
| ''' | |
| def __init__(self, theDocument=None): | |
| # The __init__() documentation doesn't isn't processed by Sphinx, | |
| # so I put it at class level. | |
| environLocal.printDebug('*** initializing MeiToM21Converter') | |
| if theDocument is None: | |
| # Without this, the class can't be pickled. | |
| self.documentRoot = Element(f'{MEI_NS}mei') | |
| else: | |
| try: | |
| self.documentRoot = fromstring(theDocument) | |
| except ParseError as parseErr: | |
| environLocal.printDebug( | |
| '\n\nERROR: Parsing the MEI document with ElementTree failed.') | |
| environLocal.printDebug(f'We got the following error:\n{parseErr}') | |
| raise MeiValidityError(_INVALID_XML_DOC) | |
| if isinstance(self.documentRoot, ElementTree): | |
| # pylint warns that :class:`Element` doesn't have a getroot() method, which is | |
| # true enough, but... | |
| self.documentRoot = self.documentRoot.getroot() # pylint: disable=maybe-no-member | |
| if f'{MEI_NS}mei' != self.documentRoot.tag: | |
| raise MeiElementError(_WRONG_ROOT_ELEMENT.format(self.documentRoot.tag)) | |
| # This defaultdict stores extra, music21-specific attributes that we add to elements to help | |
| # importing. The key is an element's @xml:id, and the value is a regular dict with keys | |
| # corresponding to attributes we'll add and values | |
| # corresponding to those attributes' values. | |
| self.m21Attr = defaultdict(lambda: {}) | |
| # This SpannerBundle holds the slurs that will be created by _ppSlurs() and used while | |
| # importing whatever note, rest, chord, or other object. | |
| self.slurBundle = spanner.SpannerBundle() | |
| def run(self) -> stream.Stream: | |
| ''' | |
| Run conversion of the internal MEI document to produce a music21 object. | |
| Returns a :class:`~music21.stream.Stream` subclass, depending on the MEI document. | |
| ''' | |
| environLocal.printDebug('*** pre-processing spanning elements') | |
| _ppSlurs(self) | |
| _ppTies(self) | |
| _ppBeams(self) | |
| _ppTuplets(self) | |
| _ppConclude(self) | |
| environLocal.printDebug('*** processing <score> elements') | |
| theScore = scoreFromElement( | |
| self.documentRoot.find(f'.//{MEI_NS}music//{MEI_NS}score'), | |
| self.slurBundle) | |
| environLocal.printDebug('*** preparing metadata') | |
| theScore.metadata = makeMetadata(self.documentRoot) | |
| return theScore | |
| # Module-level Functions | |
| # ----------------------------------------------------------------------------- | |
| def safePitch( | |
| name: str, | |
| accidental: str | None = None, | |
| octave: str | int = '' | |
| ) -> pitch.Pitch: | |
| ''' | |
| Safely build a :class:`~music21.pitch.Pitch` from a string. | |
| When :meth:`~music21.pitch.Pitch.__init__` is given an empty string, | |
| it raises a :exc:`~music21.pitch.PitchException`. This | |
| function instead returns a default :class:`~music21.pitch.Pitch` instance. | |
| name: Desired name of the :class:`~music21.pitch.Pitch`. | |
| accidental: (Optional) Symbol for the accidental. | |
| octave: (Optional) Octave number. | |
| Returns A :class:`~music21.pitch.Pitch` with the appropriate properties. | |
| >>> from music21.mei.base import safePitch # OMIT_FROM_DOCS | |
| >>> safePitch('D#6') | |
| <music21.pitch.Pitch D#6> | |
| >>> safePitch('D', '#', '6') | |
| <music21.pitch.Pitch D#6> | |
| >>> safePitch('D', '#') | |
| <music21.pitch.Pitch D#> | |
| ''' | |
| if not name: | |
| return pitch.Pitch() | |
| if octave and accidental is not None: | |
| return pitch.Pitch(name, octave=int(octave), accidental=accidental) | |
| if octave: | |
| return pitch.Pitch(name, octave=int(octave)) | |
| if accidental is not None: | |
| return pitch.Pitch(name, accidental=accidental) | |
| else: | |
| return pitch.Pitch(name) | |
| def makeDuration( | |
| base: float | int | Fraction = 0.0, | |
| dots: int = 0 | |
| ) -> duration.Duration: | |
| ''' | |
| Given a ``base`` duration and a number of ``dots``, create a :class:`~music21.duration.Duration` | |
| instance with the | |
| appropriate ``quarterLength`` value. | |
| Returns a :class:`Duration` corresponding to the fully-augmented value. | |
| **Examples** | |
| >>> from fractions import Fraction | |
| >>> mei.base.makeDuration(base=2.0, dots=0).quarterLength # half note, no dots | |
| 2.0 | |
| >>> mei.base.makeDuration(base=2.0, dots=1).quarterLength # half note, one dot | |
| 3.0 | |
| >>> mei.base.makeDuration(base=2, dots=2).quarterLength # 'base' can be an int or float | |
| 3.5 | |
| >>> mei.base.makeDuration(2.0, 10).quarterLength # you want ridiculous dots? Sure... | |
| 3.998046875 | |
| >>> mei.base.makeDuration(0.33333333333333333333, 0).quarterLength # works with fractions too | |
| Fraction(1, 3) | |
| >>> mei.base.makeDuration(Fraction(1, 3), 1).quarterLength | |
| 0.5 | |
| ''' | |
| returnDuration = duration.Duration(base) | |
| returnDuration.dots = dots # pylint: disable=assigning-non-slot | |
| return returnDuration | |
| def allPartsPresent(scoreElem) -> tuple[str, ...]: | |
| # noinspection PyShadowingNames | |
| ''' | |
| Find the @n values for all <staffDef> elements in a <score> element. This assumes that every | |
| MEI <staff> corresponds to a music21 :class:`~music21.stream.Part`. | |
| scoreElem is the <score> `Element` in which to find the part names. | |
| Returns all the unique @n values associated with a part in the <score>. | |
| **Example** | |
| >>> meiDoc = """<?xml version="1.0" encoding="UTF-8"?> | |
| ... <score xmlns="http://www.music-encoding.org/ns/mei"> | |
| ... <scoreDef> | |
| ... <staffGrp> | |
| ... <staffDef n="1" clef.shape="G" clef.line="2"/> | |
| ... <staffDef n="2" clef.shape="F" clef.line="4"/> | |
| ... </staffGrp> | |
| ... </scoreDef> | |
| ... <section> | |
| ... <!-- ... some music ... --> | |
| ... <staffDef n="2" clef.shape="C" clef.line="4"/> | |
| ... <!-- ... some music ... --> | |
| ... </section> | |
| ... </score>""" | |
| >>> import xml.etree.ElementTree as ETree | |
| >>> meiDoc = ETree.fromstring(meiDoc) | |
| >>> mei.base.allPartsPresent(meiDoc) | |
| ('1', '2') | |
| Even though there are three <staffDef> elements in the document, there are only two unique @n | |
| attributes. The second appearance of <staffDef> with @n="2" signals a change of clef on that | |
| same staff---not that there is a new staff. | |
| ''' | |
| # xpathQuery = f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}staffDef' | |
| xpathQuery = f'.//{MEI_NS}staffDef' | |
| partNs = [] # hold the @n attribute for all the parts | |
| for staffDef in scoreElem.findall(xpathQuery): | |
| if staffDef.get('n') not in partNs: | |
| partNs.append(staffDef.get('n')) | |
| if not partNs: | |
| raise MeiValidityError(_SEEMINGLY_NO_PARTS) | |
| return tuple(partNs) | |
| # Constants for One-to-One Translation | |
| # ----------------------------------------------------------------------------- | |
| # for _accidentalFromAttr() | |
| # None is for when @accid is omitted | |
| _ACCID_ATTR_DICT = {'s': '#', 'f': '-', 'ss': '##', 'x': '##', 'ff': '--', 'xs': '###', | |
| 'ts': '###', 'tf': '---', 'n': 'n', 'nf': '-', 'ns': '#', 'su': '#~', | |
| 'sd': '~', 'fu': '`', 'fd': '-`', 'nu': '~', 'nd': '`', None: None} | |
| # for _accidGesFromAttr() | |
| # None is for when @accid is omitted | |
| _ACCID_GES_ATTR_DICT = {'s': '#', 'f': '-', 'ss': '##', 'ff': '--', 'n': 'n', 'su': '#~', | |
| 'sd': '~', 'fu': '`', 'fd': '-`', None: None} | |
| # for _qlDurationFromAttr() | |
| # None is for when @dur is omitted; it's silly so it can be identified | |
| _DUR_ATTR_DICT = {'long': 16.0, 'breve': 8.0, '1': 4.0, '2': 2.0, '4': 1.0, '8': 0.5, '16': 0.25, | |
| '32': 0.125, '64': 0.0625, '128': 0.03125, '256': 0.015625, '512': 0.0078125, | |
| '1024': 0.00390625, '2048': 0.001953125, None: 0.00390625} | |
| # for _articulationFromAttr() | |
| # NOTE: 'marc-stacc' and 'ten-stacc' require multiple music21 events, so they are handled | |
| # separately in _articulationFromAttr(). | |
| _ARTIC_ATTR_DICT = {'acc': articulations.Accent, | |
| 'stacc': articulations.Staccato, | |
| 'ten': articulations.Tenuto, | |
| 'stacciss': articulations.Staccatissimo, | |
| 'marc': articulations.StrongAccent, | |
| 'spicc': articulations.Spiccato, | |
| 'doit': articulations.Doit, | |
| 'plop': articulations.Plop, | |
| 'fall': articulations.Falloff, | |
| 'dnbow': articulations.DownBow, | |
| 'upbow': articulations.UpBow, | |
| 'harm': articulations.Harmonic, | |
| 'snap': articulations.SnapPizzicato, | |
| 'stop': articulations.Stopped, | |
| 'open': articulations.OpenString, # this may also mean "no mute?" | |
| 'dbltongue': articulations.DoubleTongue, | |
| 'toe': articulations.OrganToe, | |
| 'trpltongue': articulations.TripleTongue, | |
| 'heel': articulations.OrganHeel, | |
| # TODO: these aren't implemented in music21, so I'll make new ones | |
| 'tap': articulations.Articulation, | |
| 'lhpizz': articulations.Articulation, | |
| 'dot': articulations.Articulation, | |
| 'stroke': articulations.Articulation, | |
| 'rip': articulations.Articulation, | |
| 'bend': articulations.Articulation, | |
| 'flip': articulations.Articulation, | |
| 'smear': articulations.Articulation, | |
| 'fingernail': articulations.Articulation, # (u1D1B3) | |
| 'damp': articulations.Articulation, | |
| 'dampall': articulations.Articulation, | |
| } | |
| # for _barlineFromAttr() | |
| # TODO: make new music21 Barline styles for 'dbldashed' and 'dbldotted' | |
| _BAR_ATTR_DICT = {'dashed': 'dashed', | |
| 'dotted': 'dotted', | |
| 'dbl': 'double', | |
| 'end': 'final', | |
| 'invis': 'none', | |
| 'single': 'regular', | |
| } | |
| # One-to-One Translator Functions | |
| # ----------------------------------------------------------------------------- | |
| def _attrTranslator(attr, name, mapping): | |
| ''' | |
| Helper function for other functions that need to translate the value of an attribute to another | |
| known value. :func:`_attrTranslator` tries to return the value of ``attr`` in ``mapping`` and, | |
| if ``attr`` isn't in ``mapping``, an exception is raised. | |
| :param str attr: The value of the attribute to look up in ``mapping``. | |
| :param str name: Name of the attribute, used when raising an exception (read below). | |
| :param mapping: A mapping type (nominally a dict) with relevant key-value pairs. | |
| :raises: :exc:`MeiValueError` when ``attr`` is not found in ``mapping``. The error message will | |
| be of this format: 'Unexpected value for "name" attribute: attr'. | |
| Examples: | |
| >>> from music21.mei.base import _attrTranslator, _ACCID_ATTR_DICT, _DUR_ATTR_DICT | |
| >>> _attrTranslator('s', 'accid', _ACCID_ATTR_DICT) | |
| '#' | |
| >>> _attrTranslator('9', 'dur', _DUR_ATTR_DICT) | |
| Traceback (most recent call last): | |
| music21.mei.base.MeiValueError: Unexpected value for "dur" attribute: 9 | |
| ''' | |
| try: | |
| return mapping[attr] | |
| except KeyError: | |
| raise MeiValueError(_UNEXPECTED_ATTR_VALUE.format(name, attr)) | |
| def _accidentalFromAttr(attr): | |
| ''' | |
| Use :func:`_attrTranslator` to convert the value of an "accid" attribute to its music21 string. | |
| >>> mei.base._accidentalFromAttr('s') | |
| '#' | |
| ''' | |
| return _attrTranslator(attr, 'accid', _ACCID_ATTR_DICT) | |
| def _accidGesFromAttr(attr): | |
| ''' | |
| Use :func:`_attrTranslator` to convert the value of an @accid.ges | |
| attribute to its music21 string. | |
| >>> mei.base._accidGesFromAttr('s') | |
| '#' | |
| ''' | |
| return _attrTranslator(attr, 'accid.ges', _ACCID_GES_ATTR_DICT) | |
| def _qlDurationFromAttr(attr): | |
| ''' | |
| Use :func:`_attrTranslator` to convert an MEI "dur" attribute to a music21 quarterLength. | |
| >>> mei.base._qlDurationFromAttr('4') | |
| 1.0 | |
| .. note:: This function only handles data.DURATION.cmn, not data.DURATION.mensural. | |
| ''' | |
| return _attrTranslator(attr, 'dur', _DUR_ATTR_DICT) | |
| def _articulationFromAttr(attr): | |
| ''' | |
| Use :func:`_attrTranslator` to convert an MEI "artic" attribute to a | |
| :class:`music21.articulations.Articulation` subclass. | |
| :returns: A **tuple** of one or two :class:`Articulation` subclasses. | |
| .. note:: This function returns a singleton tuple *unless* ``attr`` is ``'marc-stacc'`` or | |
| ``'ten-stacc'``. These return ``(StrongAccent, Staccato)`` and ``(Tenuto, Staccato)``, | |
| respectively. | |
| ''' | |
| if 'marc-stacc' == attr: | |
| return (articulations.StrongAccent(), articulations.Staccato()) | |
| elif 'ten-stacc' == attr: | |
| return (articulations.Tenuto(), articulations.Staccato()) | |
| else: | |
| return (_attrTranslator(attr, 'artic', _ARTIC_ATTR_DICT)(),) | |
| def _makeArticList(attr): | |
| ''' | |
| Use :func:`_articulationFromAttr` to convert the actual value of an MEI "artic" attribute | |
| (including multiple items) into a list suitable for :attr:`GeneralNote.articulations`. | |
| ''' | |
| articList = [] | |
| for eachArtic in attr.split(' '): | |
| articList.extend(_articulationFromAttr(eachArtic)) | |
| return articList | |
| def _getOctaveShift(dis: t.Literal['8', '15', '22'] | None, | |
| disPlace: str) -> int: | |
| ''' | |
| Use :func:`_getOctaveShift` to calculate the :attr:`octaveShift` attribute for a | |
| :class:`~music21.clef.Clef` subclass. Any of the arguments may be ``None``. | |
| :param str dis: The "dis" attribute from the <clef> tag. | |
| :param str disPlace: The "dis.place" attribute from the <clef> tag. | |
| :returns: The octave displacement compared to the clef's normal position. This may be 0. | |
| :rtype: integer | |
| ''' | |
| # NB: dis: 8, 15, or 22 (or "ottava" clefs) | |
| # NB: dis.place: "above" or "below" depending on whether the ottava clef is Xva or Xvb | |
| octavesDict = {None: 0, '8': 1, '15': 2, '22': 3} | |
| if 'below' == disPlace: | |
| return -1 * octavesDict[dis] | |
| else: | |
| return octavesDict[dis] | |
| def _sharpsFromAttr(signature): | |
| ''' | |
| Use :func:`_sharpsFromAttr` to convert MEI's ``data.KEYSIGNATURE`` datatype to an integer | |
| representing the number of sharps, for use with music21's :class:`~music21.key.KeySignature`. | |
| :param str signature: The @key.sig attribute. | |
| :returns: The number of sharps. | |
| :rtype: int | |
| >>> from music21.mei.base import _sharpsFromAttr | |
| >>> _sharpsFromAttr('3s') | |
| 3 | |
| >>> _sharpsFromAttr('3f') | |
| -3 | |
| >>> _sharpsFromAttr('0') | |
| 0 | |
| ''' | |
| if signature.startswith('0'): | |
| return 0 | |
| elif signature.endswith('s'): | |
| return int(signature[0]) | |
| else: | |
| return -1 * int(signature[0]) | |
| # "Preprocessing" and "Postprocessing" Functions for convertFromString() | |
| # ----------------------------------------------------------------------------- | |
| def _ppSlurs(theConverter): | |
| # noinspection PyShadowingNames | |
| ''' | |
| Pre-processing helper for :func:`convertFromString` that handles slurs specified in <slur> | |
| elements. The input is a :class:`MeiToM21Converter` with data about the file currently being | |
| processed. This function reads from ``theConverter.documentRoot`` and writes into | |
| ``theConverter.m21Attr`` and ``theConverter.slurBundle``. | |
| :param theConverter: The object responsible for storing data about this import. | |
| :type theConverter: :class:`MeiToM21Converter`. | |
| **This Preprocessor** | |
| The slur preprocessor adds @m21SlurStart and @m21SlurEnd attributes to elements that are at the | |
| beginning or end of a slur. The value of these attributes is the ``idLocal`` of a :class:`Slur` | |
| in the :attr:`slurBundle` attribute of ``theConverter``. This attribute is not part of the MEI | |
| specification, and must therefore be handled specially. | |
| If :func:`noteFromElement` encounters an element like ``<note m21SlurStart="82f87cd7"/>``, the | |
| resulting :class:`music21.note.Note` should be set as the starting point of the slur with an | |
| ``idLocal`` of ``'82f87cd7'``. | |
| **Example of Changes to ``m21Attr``** | |
| The ``theConverter.m21Attr`` attribute must be a defaultdict that returns an empty (regular) | |
| dict for non-existant keys. The defaultdict stores the @xml:id attribute of an element; the | |
| dict holds attribute names and their values that should be added to the element with the | |
| given @xml:id. | |
| For example, if the value of ``m21Attr['fe93129e']['tie']`` is ``'i'``, then this means the | |
| element with an @xml:id of ``'fe93129e'`` should have the @tie attribute set to ``'i'``. | |
| **Example** | |
| Consider the following example. | |
| >>> meiDoc = """<?xml version="1.0" encoding="UTF-8"?> | |
| ... <mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="2013"> | |
| ... <music><score> | |
| ... <section> | |
| ... <note xml:id="1234"/> | |
| ... <note xml:id="2345"/> | |
| ... <slur startid="#1234" endid="#2345"/> | |
| ... </section> | |
| ... </score></music> | |
| ... </mei>""" | |
| >>> theConverter = mei.base.MeiToM21Converter(meiDoc) | |
| >>> | |
| >>> mei.base._ppSlurs(theConverter) | |
| >>> 'm21SlurStart' in theConverter.m21Attr['1234'] | |
| True | |
| >>> 'm21SlurEnd' in theConverter.m21Attr['2345'] | |
| True | |
| >>> theConverter.slurBundle | |
| <music21.spanner.SpannerBundle of size 1> | |
| >>> firstSpanner = list(theConverter.slurBundle)[0] | |
| >>> (theConverter.m21Attr['1234']['m21SlurStart'] == | |
| ... theConverter.m21Attr['2345']['m21SlurEnd'] == | |
| ... firstSpanner.idLocal) | |
| True | |
| This example is a little artificial because of the limitations of a doctest, where we need to | |
| know all values in advance. The point here is that the values of 'm21SlurStart' and 'm21SlurEnd' | |
| of a particular slur-attached object will match the 'idLocal' of a slur in :attr:`slurBundle`. | |
| The "id" is a UUID determined at runtime, which looks something like | |
| ``'d3731f89-8a2f-4b82-ad02-f0bc6f5f8b04'``. | |
| ''' | |
| environLocal.printDebug('*** pre-processing slurs') | |
| # for readability, we use a single-letter variable | |
| c = theConverter # pylint: disable=invalid-name | |
| # pre-processing for <slur> tags | |
| for eachSlur in c.documentRoot.iterfind( | |
| f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}slur' | |
| ): | |
| if eachSlur.get('startid') is not None and eachSlur.get('endid') is not None: | |
| thisIdLocal = str(uuid4()) | |
| thisSlur = spanner.Slur() | |
| thisSlur.idLocal = thisIdLocal | |
| c.slurBundle.append(thisSlur) | |
| c.m21Attr[removeOctothorpe(eachSlur.get('startid'))]['m21SlurStart'] = thisIdLocal | |
| c.m21Attr[removeOctothorpe(eachSlur.get('endid'))]['m21SlurEnd'] = thisIdLocal | |
| else: | |
| environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<slur>', '@startid and @endid')) | |
| def _ppTies(theConverter): | |
| ''' | |
| Pre-processing helper for :func:`convertFromString` that handles ties specified in <tie> | |
| elements. The input is a :class:`MeiToM21Converter` with data about the file currently being | |
| processed. This function reads from ``theConverter.documentRoot`` and writes into | |
| ``theConverter.m21Attr``. | |
| :param theConverter: The object responsible for storing data about this import. | |
| :type theConverter: :class:`MeiToM21Converter`. | |
| **This Preprocessor** | |
| The tie preprocessor works similarly to the slur preprocessor, adding @tie attributes. The | |
| value of these attributes conforms to the MEI Guidelines, so no special action is required. | |
| **Example of ``m21Attr``** | |
| The ``theConverter.m21Attr`` attribute must be a defaultdict that returns an empty (regular) | |
| dict for non-existent keys. The defaultdict stores the @xml:id attribute of an element; the | |
| dict holds attribute names and their values that should be added to the element with the | |
| given @xml:id. | |
| For example, if the value of ``m21Attr['fe93129e']['tie']`` is ``'i'``, then this means the | |
| element with an @xml:id of ``'fe93129e'`` should have the @tie attribute set to ``'i'``. | |
| ''' | |
| environLocal.printDebug('*** pre-processing ties') | |
| # for readability, we use a single-letter variable | |
| c = theConverter # pylint: disable=invalid-name | |
| for eachTie in c.documentRoot.iterfind( | |
| f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}tie'): | |
| if eachTie.get('startid') is not None and eachTie.get('endid') is not None: | |
| c.m21Attr[removeOctothorpe(eachTie.get('startid'))]['tie'] = 'i' | |
| c.m21Attr[removeOctothorpe(eachTie.get('endid'))]['tie'] = 't' | |
| else: | |
| environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<tie>', '@startid and @endid')) | |
| def _ppBeams(theConverter): | |
| ''' | |
| Pre-processing helper for :func:`convertFromString` that handles beams specified in <beamSpan> | |
| elements. The input is a :class:`MeiToM21Converter` with data about the file currently being | |
| processed. This function reads from ``theConverter.documentRoot`` and writes into | |
| ``theConverter.m21Attr``. | |
| :param theConverter: The object responsible for storing data about this import. | |
| :type theConverter: :class:`MeiToM21Converter`. | |
| **This Preprocessor** | |
| The beam preprocessor works similarly to the slur preprocessor, adding the @m21Beam attribute. | |
| The value of this attribute is either ``'start'``, ``'continue'``, or ``'stop'``, indicating | |
| the music21 ``type`` of the primary beam attached to this element. This attribute is not | |
| part of the MEI specification, and must therefore be handled specially. | |
| **Example of ``m21Attr``** | |
| The ``theConverter.m21Attr`` argument must be a defaultdict that returns an empty (regular) | |
| dict for non-existent keys. The defaultdict stores the @xml:id attribute of an element; the | |
| dict holds attribute names and their values that should be added to the element with the | |
| given @xml:id. | |
| For example, if the value of ``m21Attr['fe93129e']['tie']`` is ``'i'``, then this means the | |
| element with an @xml:id of ``'fe93129e'`` should have the @tie attribute set to ``'i'``. | |
| ''' | |
| environLocal.printDebug('*** pre-processing beams') | |
| # for readability, we use a single-letter variable | |
| c = theConverter # pylint: disable=invalid-name | |
| # pre-processing for <beamSpan> elements | |
| for eachBeam in c.documentRoot.iterfind( | |
| f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}beamSpan'): | |
| if eachBeam.get('startid') is None or eachBeam.get('endid') is None: | |
| environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<beamSpan>', '@startid and @endid')) | |
| continue | |
| c.m21Attr[removeOctothorpe(eachBeam.get('startid'))]['m21Beam'] = 'start' | |
| c.m21Attr[removeOctothorpe(eachBeam.get('endid'))]['m21Beam'] = 'stop' | |
| # iterate things in the @plist attribute | |
| for eachXmlid in eachBeam.get('plist', '').split(' '): | |
| eachXmlid = removeOctothorpe(eachXmlid) | |
| # if not eachXmlid: | |
| # # this is either @plist not set or extra spaces around the contained xml:id values | |
| # pass | |
| if 'm21Beam' not in c.m21Attr[eachXmlid]: | |
| # only set to 'continue' if it wasn't previously set to 'start' or 'stop' | |
| c.m21Attr[eachXmlid]['m21Beam'] = 'continue' | |
| def _ppTuplets(theConverter): | |
| ''' | |
| Pre-processing helper for :func:`convertFromString` that handles tuplets specified in | |
| <tupletSpan> elements. The input is a :class:`MeiToM21Converter` with data about the file | |
| currently being processed. This function reads from ``theConverter.documentRoot`` and writes | |
| into ``theConverter.m21Attr``. | |
| :param theConverter: The object responsible for storing data about this import. | |
| :type theConverter: :class:`MeiToM21Converter`. | |
| **This Preprocessor** | |
| The slur preprocessor works similarly to the slur preprocessor, adding @m21TupletNum and | |
| @m21TupletNumbase attributes. The value of these attributes corresponds to the @num and | |
| @numbase attributes found on a <tuplet> element. This preprocessor also performs a significant | |
| amount of guesswork to try to handle <tupletSpan> elements that do not include a @plist | |
| attribute. This attribute is not part of the MEI specification, and must therefore be handled | |
| specially. | |
| **Example of ``m21Attr``** | |
| The ``theConverter.m21Attr`` attribute must be a defaultdict that returns an empty (regular) | |
| dict for non-existent keys. The defaultdict stores the @xml:id attribute of an element; the | |
| dict holds attribute names and their values that should be added to the element with the | |
| given @xml:id. | |
| For example, if the value of ``m21Attr['fe93129e']['tie']`` is ``'i'``, then this means the | |
| element with an @xml:id of ``'fe93129e'`` should have the @tie attribute set to ``'i'``. | |
| ''' | |
| environLocal.printDebug('*** pre-processing tuplets') | |
| # for readability, we use a single-letter variable | |
| c = theConverter # pylint: disable=invalid-name | |
| # pre-processing <tupletSpan> tags | |
| for eachTuplet in c.documentRoot.iterfind( | |
| f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}tupletSpan'): | |
| if ((eachTuplet.get('startid') is None or eachTuplet.get('endid') is None) | |
| and eachTuplet.get('plist') is None): | |
| environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<tupletSpan>', | |
| '@startid and @endid or @plist')) | |
| elif eachTuplet.get('plist') is not None: | |
| # Ideally (for us) <tupletSpan> elements will have a @plist that enumerates the | |
| # @xml:id of every affected element. In this case, tupletSpanFromElement() can use the | |
| # @plist to add our custom @m21TupletNum and @m21TupletNumbase attributes. | |
| for eachXmlid in eachTuplet.get('plist', '').split(' '): | |
| eachXmlid = removeOctothorpe(eachXmlid) | |
| if eachXmlid: | |
| # protect against extra spaces around the contained xml:id values | |
| c.m21Attr[eachXmlid]['m21TupletNum'] = eachTuplet.get('num') | |
| c.m21Attr[eachXmlid]['m21TupletNumbase'] = eachTuplet.get('numbase') | |
| else: | |
| # For <tupletSpan> elements that don't give a @plist attribute, we have to do some | |
| # guesswork and hope we find all the related elements. Right here, we're only setting | |
| # the "flags" that this guesswork must be done later. | |
| startid = removeOctothorpe(eachTuplet.get('startid')) | |
| endid = removeOctothorpe(eachTuplet.get('endid')) | |
| c.m21Attr[startid]['m21TupletSearch'] = 'start' | |
| c.m21Attr[startid]['m21TupletNum'] = eachTuplet.get('num') | |
| c.m21Attr[startid]['m21TupletNumbase'] = eachTuplet.get('numbase') | |
| c.m21Attr[endid]['m21TupletSearch'] = 'end' | |
| c.m21Attr[endid]['m21TupletNum'] = eachTuplet.get('num') | |
| c.m21Attr[endid]['m21TupletNumbase'] = eachTuplet.get('numbase') | |
| def _ppConclude(theConverter): | |
| ''' | |
| Pre-processing helper for :func:`convertFromString` that adds attributes from ``m21Attr`` to the | |
| appropriate elements in ``documentRoot``. The input is a :class:`MeiToM21Converter` with data | |
| about the file currently being processed. This function reads from ``theConverter.m21Attr`` and | |
| writes into ``theConverter.documentRoot``. | |
| :param theConverter: The object responsible for storing data about this import. | |
| :type theConverter: :class:`MeiToM21Converter`. | |
| **Example of ``m21Attr``** | |
| The ``m21Attr`` argument must be a defaultdict that returns an empty (regular) dict for | |
| non-existent keys. The defaultdict stores the @xml:id attribute of an element; the dict holds | |
| attribute names and their values that should be added to the element with the given @xml:id. | |
| For example, if the value of ``m21Attr['fe93129e']['tie']`` is ``'i'``, then this means the | |
| element with an @xml:id of ``'fe93129e'`` should have the @tie attribute set to ``'i'``. | |
| **This Preprocessor** | |
| The slur preprocessor adds all attributes from the ``m21Attr`` to the appropriate element in | |
| ``documentRoot``. In effect, it finds the element corresponding to each key in ``m21Attr``, | |
| then iterates the keys in its dict, *appending* the ``m21Attr``-specified value to any existing | |
| value. | |
| ''' | |
| environLocal.printDebug('*** concluding pre-processing') | |
| # for readability, we use a single-letter variable | |
| c = theConverter # pylint: disable=invalid-name | |
| # conclude pre-processing by adding music21-specific attributes to their respective elements | |
| for eachObject in c.documentRoot.iterfind('*//*'): | |
| # we have a defaultdict, so this "if" isn't strictly necessary; but without it, every single | |
| # element with an @xml:id creates a new, empty dict, which would consume a lot of memory | |
| if eachObject.get(_XMLID) in c.m21Attr: | |
| for eachAttr in c.m21Attr[eachObject.get(_XMLID)]: | |
| eachObject.set(eachAttr, (eachObject.get(eachAttr, '') | |
| + c.m21Attr[eachObject.get(_XMLID)][eachAttr])) | |
| # Helper Functions | |
| # ----------------------------------------------------------------------------- | |
| def _processEmbeddedElements( | |
| elements: list[Element], | |
| mapping, | |
| callerTag=None, | |
| slurBundle=None | |
| ): | |
| # noinspection PyShadowingNames | |
| ''' | |
| From an iterable of MEI ``elements``, use functions in the ``mapping`` to convert each element | |
| to its music21 object. This function was designed for use with elements that may contain other | |
| elements; the contained elements will be converted as appropriate. | |
| If an element itself has embedded elements (i.e., its converter function in ``mapping`` returns | |
| a sequence), those elements will appear in the returned sequence in order---there are no | |
| hierarchic lists. | |
| :param elements: A list of :class:`Element` objects to convert to music21 objects. | |
| :type elements: iterable of :class:`~xml.etree.ElementTree.Element` | |
| :param mapping: A dictionary where keys are the :attr:`Element.tag` attribute and values are | |
| the function to call to convert that :class:`Element` to a music21 object. | |
| :type mapping: mapping of str to function | |
| :param str callerTag: The tag of the element on behalf of which this function is processing | |
| sub-elements (e.g., 'note' or 'staffDef'). Do not include < and >. This is used in a | |
| warning message on finding an unprocessed element. | |
| :param slurBundle: A slur bundle, as used by the other :func:`*fromElements` functions. | |
| :type slurBundle: :class:`music21.spanner.SlurBundle` | |
| :returns: A list of the music21 objects returned by the converter functions, or an empty list | |
| if no objects were returned. | |
| :rtype: sequence of :class:`~music21.base.Music21Object` | |
| **Examples:** | |
| Because there is no ``'rest'`` key in the ``mapping``, that :class:`Element` is ignored. | |
| >>> from xml.etree.ElementTree import Element | |
| >>> elements = [Element('note'), Element('rest'), Element('note')] | |
| >>> mapping = {'note': lambda x, y: note.Note('D2')} | |
| >>> mei.base._processEmbeddedElements(elements, mapping, 'doctest') | |
| [<music21.note.Note D>, <music21.note.Note D>] | |
| If debugging is enabled for the previous example, this warning would be displayed: | |
| ``mei.base: Found an unprocessed <rest> element in a <doctest>. | |
| The "beam" element holds "note" elements. All elements appear in a single level of the list: | |
| >>> elements = [Element('note'), Element('beam'), Element('note')] | |
| >>> mapping = {'note': lambda x, y: note.Note('D2'), | |
| ... 'beam': lambda x, y: [note.Note('E2') for _ in range(2)]} | |
| >>> mei.base._processEmbeddedElements(elements, mapping) | |
| [<music21.note.Note D>, <music21.note.Note E>, <music21.note.Note E>, <music21.note.Note D>] | |
| ''' | |
| processed = [] | |
| for eachElem in elements: | |
| if eachElem.tag in mapping: | |
| result = mapping[eachElem.tag](eachElem, slurBundle) | |
| if isinstance(result, (tuple, list)): | |
| for eachObject in result: | |
| processed.append(eachObject) | |
| else: | |
| processed.append(result) | |
| elif eachElem.tag not in _IGNORE_UNPROCESSED: | |
| environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, callerTag)) | |
| return processed | |
| def _timeSigFromAttrs(elem): | |
| ''' | |
| From any tag with @meter.count and @meter.unit attributes, make a :class:`TimeSignature`. | |
| :param :class:`~xml.etree.ElementTree.Element` elem: An :class:`Element` with @meter.count and | |
| @meter.unit attributes. | |
| :returns: The corresponding time signature. | |
| :rtype: :class:`~music21.meter.TimeSignature` | |
| ''' | |
| return meter.TimeSignature(f"{elem.get('meter.count')!s}/{elem.get('meter.unit')!s}") | |
| def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: | |
| ''' | |
| From any tag with (at minimum) either @key.pname or @key.sig attributes, make a | |
| :class:`KeySignature` or :class:`Key`, as possible. | |
| elem is an :class:`Element` with either the @key.pname or @key.sig attribute. | |
| Returns the key or key signature. | |
| ''' | |
| if elem.get('key.pname') is not None: | |
| # @key.accid, @key.mode, @key.pname | |
| # noinspection PyTypeChecker | |
| mode = elem.get('key.mode', '') | |
| step = elem.get('key.pname') | |
| if step is None: # pragma: no cover | |
| raise MeiValidityError('Key missing step') | |
| accidental = _accidentalFromAttr(elem.get('key.accid')) | |
| if accidental is None: | |
| tonic = step | |
| else: | |
| tonic = step + accidental | |
| return key.Key(tonic=tonic, mode=mode) | |
| else: | |
| # @key.sig, @key.mode | |
| # If @key.mode is null, assume it is a 'major' key (default for ks.asKey) | |
| ks = key.KeySignature(sharps=_sharpsFromAttr(elem.get('key.sig'))) | |
| # noinspection PyTypeChecker | |
| return ks.asKey(mode=elem.get('key.mode', 'major')) | |
| def _transpositionFromAttrs(elem): | |
| ''' | |
| From any element with the @trans.diat and @trans.semi attributes, make an :class:`Interval` that | |
| represents the interval of transposition from written to concert pitch. | |
| :param :class:`~xml.etree.ElementTree.Element` elem: An :class:`Element` with the @trans.diat | |
| and @trans.semi attributes. | |
| :returns: The interval of transposition from written to concert pitch. | |
| :rtype: :class:`music21.interval.Interval` | |
| ''' | |
| # noinspection PyTypeChecker | |
| transDiat = int(elem.get('trans.diat', 0)) | |
| # noinspection PyTypeChecker | |
| transSemi = int(elem.get('trans.semi', 0)) | |
| # If the difference between transSemi and transDiat is greater than five per octave... | |
| # noinspection SpellCheckingInspection | |
| if abs(transSemi - transDiat) > 5 * (abs(transSemi) // 12 + 1): | |
| # ... we need to add octaves to transDiat so it's the proper size. Otherwise, | |
| # intervalFromGenericAndChromatic() tries to create things like AAAAAAAAA5. Except it | |
| # actually just fails. | |
| # NB: we test this against transSemi because transDiat could be 0 when transSemi is a | |
| # multiple of 12 *either* greater or less than 0. | |
| if transSemi < 0: | |
| transDiat -= 7 * (abs(transSemi) // 12) | |
| elif transSemi > 0: | |
| transDiat += 7 * (abs(transSemi) // 12) | |
| # NB: MEI uses zero-based unison rather than 1-based unison, so for music21 we must make every | |
| # diatonic interval one greater than it was. E.g., '@trans.diat="2"' in MEI means to | |
| # "transpose up two diatonic steps," which music21 would rephrase as "transpose up by a | |
| # diatonic third." | |
| if transDiat < 0: | |
| transDiat -= 1 | |
| elif transDiat > 0: | |
| transDiat += 1 | |
| return interval.intervalFromGenericAndChromatic(interval.GenericInterval(transDiat), | |
| interval.ChromaticInterval(transSemi)) | |
| # noinspection SpellCheckingInspection | |
| def _barlineFromAttr(attr): | |
| ''' | |
| Use :func:`_attrTranslator` to convert the value of a "left" or "right" attribute to a | |
| :class:`Barline` or :class:`Repeat` or occasionally a list of :class:`Repeat`. The only time a | |
| list is returned is when "attr" is ``'rptboth'``, in which case the end and start barlines are | |
| both returned. | |
| :param str attr: The MEI @left or @right attribute to convert to a barline. | |
| :returns: The barline. | |
| :rtype: :class:`music21.bar.Barline` or :class:`~music21.bar.Repeat` or list of them | |
| ''' | |
| # NB: the MEI Specification says @left is used only for legacy-format conversions, so we'll | |
| # just assume it's a @right attribute. Not a huge deal if we get this wrong (I hope). | |
| if attr.startswith('rpt'): | |
| if 'rptboth' == attr: | |
| return _barlineFromAttr('rptend'), _barlineFromAttr('rptstart') | |
| elif 'rptend' == attr: | |
| return bar.Repeat('end', times=2) | |
| else: | |
| return bar.Repeat('start') | |
| else: | |
| return bar.Barline(_attrTranslator(attr, 'right', _BAR_ATTR_DICT)) | |
| def _tieFromAttr(attr): | |
| ''' | |
| Convert a @tie attribute to the required :class:`Tie` object. | |
| :param str attr: The MEI @tie attribute to convert. | |
| :return: The relevant :class:`Tie` object. | |
| :rtype: :class:`music21.tie.Tie` | |
| ''' | |
| if 'm' in attr or ('t' in attr and 'i' in attr): | |
| return tie.Tie('continue') | |
| elif 'i' in attr: | |
| return tie.Tie('start') | |
| else: | |
| return tie.Tie('stop') | |
| def addSlurs(elem, obj, slurBundle): | |
| ''' | |
| If relevant, add a slur to an ``obj`` (object) that was created from an ``elem`` (element). | |
| :param elem: The :class:`Element` that caused creation of the ``obj``. | |
| :type elem: :class:`xml.etree.ElementTree.Element` | |
| :param obj: The musical object (:class:`Note`, :class:`Chord`, etc.) created from ``elem``, to | |
| which a slur might be attached. | |
| :type obj: :class:`music21.base.Music21Object` | |
| :param slurBundle: The :class:`Slur`-holding :class:`SpannerBundle` associated with the | |
| :class:`Stream` that holds ``obj``. | |
| :type slurBundle: :class:`music21.spanner.SpannerBundle` | |
| :returns: Whether at least one slur was added. | |
| :rtype: bool | |
| **A Note about Importing Slurs** | |
| Because of how the MEI format specifies slurs, the strategy required for proper import to | |
| music21 is not obvious. There are two ways to specify a slur: | |
| #. With a ``@slur`` attribute, in which case :func:`addSlurs` reads the attribute and manages | |
| creating a :class:`Slur` object, adding the affected objects to it, and storing the | |
| :class:`Slur` in the ``slurBundle``. | |
| #. With a ``<slur>`` element, which requires pre-processing. In this case, :class:`Slur` objects | |
| must already exist in the ``slurBundle``, and special attributes must be added to the | |
| affected elements (``@m21SlurStart`` to the element at the start of the slur and | |
| ``@m21SlurEnd`` to the element at the end). These attributes hold the ``id`` of a | |
| :class:`Slur` in the ``slurBundle``, allowing :func:`addSlurs` to find the slur and add | |
| ``obj`` to it. | |
| .. caution:: If an ``elem`` has an @m21SlurStart or @m21SlurEnd attribute that refer to an | |
| object not found in the ``slurBundle``, the slur is silently dropped. | |
| ''' | |
| addedSlur = False | |
| def wrapGetByIdLocal(theId): | |
| ''' | |
| Avoid crashing when getByIdLocl() doesn't find the slur | |
| ''' | |
| try: | |
| slurBundle.getByIdLocal(theId)[0].addSpannedElements(obj) | |
| return True | |
| except IndexError: | |
| # when getByIdLocal() couldn't find the Slur | |
| return False | |
| if elem.get('m21SlurStart') is not None: | |
| addedSlur = wrapGetByIdLocal(elem.get('m21SlurStart')) | |
| if elem.get('m21SlurEnd') is not None: | |
| addedSlur = wrapGetByIdLocal(elem.get('m21SlurEnd')) | |
| if elem.get('slur') is not None: | |
| theseSlurs = elem.get('slur').split(' ') | |
| for eachSlur in theseSlurs: | |
| slurNum, slurType = eachSlur | |
| if 'i' == slurType: | |
| newSlur = spanner.Slur() | |
| newSlur.idLocal = slurNum | |
| slurBundle.append(newSlur) | |
| newSlur.addSpannedElements(obj) | |
| addedSlur = True | |
| elif 't' == slurType: | |
| addedSlur = wrapGetByIdLocal(slurNum) | |
| # 'm' is currently ignored; we may need it for cross-staff slurs | |
| return addedSlur | |
| def beamTogether(someThings): | |
| ''' | |
| Beam some things together. The function beams every object that has a :attr:`beams` attribute, | |
| leaving the other objects unmodified. | |
| :param someThings: An iterable of things to beam together. | |
| :type someThings: iterable of :class:`~music21.base.Music21Object` | |
| :returns: ``someThings`` with relevant objects beamed together. | |
| :rtype: same as ``someThings`` | |
| ''' | |
| # Index of the most recent beamedNote/Chord in someThings. Not all Note/Chord objects will | |
| # necessarily be beamed (especially when this is called from tupletFromElement()), so we have | |
| # to make that distinction. | |
| iLastBeamedNote = -1 | |
| for i, thing in enumerate(someThings): | |
| if hasattr(thing, 'beams'): | |
| if iLastBeamedNote == -1: | |
| beamType = 'start' | |
| else: | |
| beamType = 'continue' | |
| # checking for len(thing.beams) avoids clobbering beams that were set with a nested | |
| # <beam> element, like a grace note | |
| if duration.convertTypeToNumber(thing.duration.type) > 4 and not thing.beams: | |
| thing.beams.fill(thing.duration.type, beamType) | |
| iLastBeamedNote = i | |
| someThings[iLastBeamedNote].beams.setAll('stop') | |
| return someThings | |
| def removeOctothorpe(xmlid): | |
| ''' | |
| Given a string with an @xml:id to search for, remove a leading octothorpe, if present. | |
| >>> from music21.mei.base import removeOctothorpe | |
| >>> removeOctothorpe('110a923d-a13a-4a2e-b85c-e1d438e4c5d6') | |
| '110a923d-a13a-4a2e-b85c-e1d438e4c5d6' | |
| >>> removeOctothorpe('#e46cbe82-95fc-4522-9f7a-700e41a40c8e') | |
| 'e46cbe82-95fc-4522-9f7a-700e41a40c8e' | |
| ''' | |
| if xmlid.startswith('#'): | |
| return xmlid[1:] | |
| else: | |
| return xmlid | |
| def makeMetadata(documentRoot): | |
| ''' | |
| Produce metadata objects for all the metadata stored in the MEI header. | |
| :param documentRoot: The MEI document's root element. | |
| :type documentRoot: :class:`~xml.etree.ElementTree.Element` | |
| :returns: A :class:`Metadata` object with some of the metadata stored in the MEI document. | |
| :rtype: :class:`music21.metadata.Metadata` | |
| ''' | |
| meta = metadata.Metadata() | |
| work = documentRoot.find(f'.//{MEI_NS}work') | |
| if work is not None: | |
| # title, subtitle, and movement name | |
| meta = metaSetTitle(work, meta) | |
| # composer | |
| meta = metaSetComposer(work, meta) | |
| # date | |
| meta = metaSetDate(work, meta) | |
| return meta | |
| def metaSetTitle(work, meta): | |
| ''' | |
| From a <work> element, find the title, subtitle, and movement name (<tempo> element) and store | |
| the values in a :class:`Metadata` object. | |
| :param work: A <work> :class:`~xml.etree.ElementTree.Element` with metadata you want to find. | |
| :param meta: The :class:`~music21.metadata.Metadata` object in which to store the metadata. | |
| :return: The ``meta`` argument, having relevant metadata added. | |
| ''' | |
| # title, subtitle, and movement name | |
| subtitle = None | |
| for title in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}title'): | |
| if title.get('type', '') == 'subtitle': # or 'subordinate', right? | |
| subtitle = title.text | |
| elif meta.title is None: | |
| meta.title = title.text | |
| if subtitle: | |
| # Since m21.Metadata doesn't actually have a "subtitle" attribute, we'll put the subtitle | |
| # in the title | |
| meta.title = f'{meta.title} ({subtitle})' | |
| tempo = work.find(f'./{MEI_NS}tempo') | |
| if tempo is not None: | |
| meta.movementName = tempo.text | |
| return meta | |
| def metaSetComposer(work, meta): | |
| ''' | |
| From a <work> element, find the composer(s) and store the values in a :class:`Metadata` object. | |
| :param work: A <work> :class:`~xml.etree.ElementTree.Element` with metadata you want to find. | |
| :param meta: The :class:`~music21.metadata.Metadata` object in which to store the metadata. | |
| :return: The ``meta`` argument, having relevant metadata added. | |
| ''' | |
| composers = [] | |
| for persName in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}respStmt/{MEI_NS}persName'): | |
| if persName.get('role') == 'composer' and persName.text: | |
| composers.append(persName.text) | |
| for composer in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}composer'): | |
| if composer.text: | |
| composers.append(composer.text) | |
| else: | |
| persName = composer.find(f'./{MEI_NS}persName') | |
| if persName.text: | |
| composers.append(persName.text) | |
| if len(composers) == 1: | |
| meta.composer = composers[0] | |
| elif len(composers) > 1: | |
| meta.composers = composers | |
| return meta | |
| def metaSetDate(work, meta): | |
| ''' | |
| From a <work> element, find the date (range) of composition and store the values in a | |
| :class:`Metadata` object. | |
| :param work: A <work> :class:`~xml.etree.ElementTree.Element` with metadata you want to find. | |
| :param meta: The :class:`~music21.metadata.Metadata` object in which to store the metadata. | |
| :return: The ``meta`` argument, having relevant metadata added. | |
| ''' | |
| date = work.find(f'./{MEI_NS}history/{MEI_NS}creation/{MEI_NS}date') | |
| if date is not None: # must use explicit "is not None" for an Element | |
| if date.text or date.get('isodate'): | |
| dateStr = date.get('isodate') if date.get('isodate') else date.text | |
| theDate = metadata.Date() | |
| try: | |
| theDate.loadStr(dateStr.replace('-', '/')) | |
| except ValueError: | |
| environLocal.warn(_MISSED_DATE.format(dateStr)) | |
| else: | |
| meta.dateCreated = theDate | |
| else: | |
| dateStart = date.get('notbefore') if date.get('notbefore') else date.get('startdate') | |
| dateEnd = date.get('notafter') if date.get('notafter') else date.get('enddate') | |
| if dateStart and dateEnd: | |
| meta.dateCreated = metadata.DateBetween((dateStart, dateEnd)) | |
| return meta | |
| def getVoiceId(fromThese): | |
| ''' | |
| From a list of objects with mixed type, find the "id" of the :class:`music21.stream.Voice` | |
| instance. | |
| :param list fromThese: A list of objects of any type, at least one of which must be a | |
| :class:`~music21.stream.Voice` instance. | |
| :returns: The ``id`` of the :class:`Voice` instance. | |
| :raises: :exc:`RuntimeError` if zero or many :class:`Voice` objects are found. | |
| ''' | |
| fromThese = [item for item in fromThese if isinstance(item, stream.Voice)] | |
| if len(fromThese) == 1: | |
| return fromThese[0].id | |
| else: | |
| raise RuntimeError('getVoiceId: found too few or too many Voice objects') | |
| # noinspection PyTypeChecker | |
| def scaleToTuplet(objs, elem): | |
| ''' | |
| Scale the duration of some objects by a ratio indicated by a tuplet. The ``elem`` must have the | |
| @m21TupletNum and @m21TupletNumbase attributes set, and optionally the @m21TupletSearch or | |
| @m21TupletType attributes. | |
| The @m21TupletNum and @m21TupletNumbase attributes should be equal to the @num and @numbase | |
| values of the <tuplet> or <tupletSpan> that indicates this tuplet. | |
| The @m21TupletSearch attribute, whose value must either be ``'start'`` or ``'end'``, is required | |
| when a <tupletSpan> does not include a @plist attribute. It indicates that the importer must | |
| "search" for a tuplet near the end of the import process, which involves scaling the durations | |
| of all objects discovered between those with the "start" and "end" search values. | |
| The @m21TupletType attribute is set directly as the :attr:`type` attribute of the music21 | |
| object's :class:`Tuplet` object. If @m21TupletType is not set, the @tuplet attribute will be | |
| consulted. Note that this attribute is ignored if the @m21TupletSearch attribute is present, | |
| since the ``type`` will be set later by the tuplet-finding algorithm. | |
| .. note:: Objects without a :attr:`duration` attribute will be skipped silently, unless they | |
| will be given the @m21TupletSearch attribute. | |
| :param objs: The object(s) whose durations will be scaled. | |
| You may provide either a single object | |
| or an iterable; the return type corresponds to the input type. | |
| :type objs: (list of) :class:`~music21.base.Music21Object` | |
| :param elem: An :class:`Element` with the appropriate attributes (as specified above). | |
| :type elem: :class:`xml.etree.ElementTree.Element` | |
| :returns: ``objs`` with scaled durations. | |
| :rtype: (list of) :class:`~music21.base.Music21Object` | |
| ''' | |
| if not isinstance(objs, (list, set, tuple)): | |
| objs = [objs] | |
| wasList = False | |
| else: | |
| wasList = True | |
| for obj in objs: | |
| if not isinstance(obj, (note.Note, note.Rest, chord.Chord)): | |
| # silently skip objects that don't have a duration | |
| continue | |
| elif elem.get('m21TupletSearch') is not None: | |
| obj.m21TupletSearch = elem.get('m21TupletSearch') | |
| obj.m21TupletNum = elem.get('m21TupletNum') | |
| obj.m21TupletNumbase = elem.get('m21TupletNumbase') | |
| else: | |
| obj.duration.appendTuplet(duration.Tuplet( | |
| numberNotesActual=int(elem.get('m21TupletNum')), | |
| numberNotesNormal=int(elem.get('m21TupletNumbase')), | |
| durationNormal=obj.duration.type, | |
| durationActual=obj.duration.type)) | |
| if elem.get('m21TupletType') is not None: | |
| obj.duration.tuplets[0].type = elem.get('m21TupletType') | |
| elif elem.get('tuplet', '').startswith('i'): | |
| obj.duration.tuplets[0].type = 'start' | |
| elif elem.get('tuplet', '').startswith('t'): | |
| obj.duration.tuplets[0].type = 'stop' | |
| if wasList: | |
| return objs | |
| else: | |
| return objs[0] | |
| def _guessTuplets(theLayer): | |
| # TODO: nested tuplets don't work when they're both specified with <tupletSpan> | |
| # TODO: adjust this to work with cross-measure tuplets (i.e., where only the "start" or "end" | |
| # is found in theLayer) | |
| ''' | |
| Given a list of music21 objects, possibly containing :attr:`m21TupletSearch`, | |
| :attr:`m21TupletNum`, and :attr:`m21TupletNumbase` attributes, adjust the durations of the | |
| objects as specified by those "m21Tuplet" attributes, then remove the attributes. | |
| This function finishes processing for tuplets encoded as a <tupletSpan> where @startid and | |
| @endid are indicated, but not @plist. Knowing the starting and ending object in the tuplet, we | |
| can guess that all the Note, Rest, and Chord objects between the starting and ending objects | |
| in that <layer> are part of the tuplet. (Grace notes retain a 0.0 duration). | |
| .. note:: At the moment, this will likely only work for simple tuplets---not nested tuplets. | |
| :param theLayer: Objects from the <layer> in which to search for objects that have the | |
| :attr:`m21TupletSearch` attribute. | |
| :type theScore: list | |
| :returns: The same list, with durations adjusted to account for tuplets. | |
| ''' | |
| # NB: this is a hidden function because it uses the "m21TupletSearch" attribute, which are only | |
| # supposed to be used within the MEI import module | |
| inATuplet = False # we hit m21TupletSearch=='start' but not 'end' yet | |
| tupletNum = None | |
| tupletNumbase = None | |
| for eachNote in theLayer: | |
| # we'll skip objects that don't have a duration | |
| if not isinstance(eachNote, (note.Note, note.Rest, chord.Chord)): | |
| continue | |
| if hasattr(eachNote, 'm21TupletSearch') and eachNote.m21TupletSearch == 'start': | |
| inATuplet = True | |
| tupletNum = int(eachNote.m21TupletNum) | |
| tupletNumbase = int(eachNote.m21TupletNumbase) | |
| del eachNote.m21TupletSearch | |
| del eachNote.m21TupletNum | |
| del eachNote.m21TupletNumbase | |
| if inATuplet: | |
| scaleToTuplet(eachNote, Element('', | |
| m21TupletNum=str(tupletNum), | |
| m21TupletNumbase=str(tupletNumbase))) | |
| if hasattr(eachNote, 'm21TupletSearch') and eachNote.m21TupletSearch == 'end': | |
| # we've reached the end of the tuplet! | |
| eachNote.duration.tuplets[0].type = 'stop' | |
| del eachNote.m21TupletSearch | |
| del eachNote.m21TupletNum | |
| del eachNote.m21TupletNumbase | |
| # reset the tuplet-tracking variables | |
| inATuplet = False | |
| return theLayer | |
| # Element-Based Converter Functions | |
| # ----------------------------------------------------------------------------- | |
| def scoreDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <scoreDef> Container for score meta-information. | |
| In MEI 2013: pg.431 (445 in PDF) (MEI.shared module) | |
| This function returns a dictionary with objects that may relate to the entire score, to all | |
| parts at a particular moment, or only to a specific part at a particular moment. The dictionary | |
| keys determine the object's scope. If the key is... | |
| * ``'whole-score objects'``, it applies to the entire score (e.g., page size); | |
| * ``'all-part objects'``, it applies to all parts at the moment this <scoreDef> appears; | |
| * the @n attribute of a part, it applies only to | |
| that part at the moment this <scoreDef> appears. | |
| While the multi-part objects will be held in a list, the single-part objects will be in a dict | |
| like that returned by :func:`staffDefFromElement`. | |
| Note that it is the caller's responsibility to determine the right action if there are | |
| conflicting objects in the returned dictionary. | |
| For example: | |
| >>> meiDoc = """<?xml version="1.0" encoding="UTF-8"?> | |
| ... <scoreDef meter.count="3" meter.unit="4" xmlns="http://www.music-encoding.org/ns/mei"> | |
| ... <staffGrp> | |
| ... <staffDef n="1" label="Clarinet"/> | |
| ... <staffGrp> | |
| ... <staffDef n="2" label="Flute"/> | |
| ... <staffDef n="3" label="Violin"/> | |
| ... </staffGrp> | |
| ... </staffGrp> | |
| ... </scoreDef> | |
| ... """ | |
| >>> from xml.etree import ElementTree as ET | |
| >>> scoreDef = ET.fromstring(meiDoc) | |
| >>> result = mei.base.scoreDefFromElement(scoreDef) | |
| >>> len(result) | |
| 5 | |
| >>> result['1'] | |
| {'instrument': <music21.instrument.Clarinet '1: Clarinet: Clarinet'>} | |
| >>> result['3'] | |
| {'instrument': <music21.instrument.Violin '3: Violin: Violin'>} | |
| >>> result['all-part objects'] | |
| [<music21.meter.TimeSignature 3/4>] | |
| >>> result['whole-score objects'] | |
| [] | |
| :param elem: The ``<scoreDef>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :returns: Objects from the ``<scoreDef>``, as described above. | |
| :rtype: dict | |
| **Attributes/Elements Implemented:** | |
| - (att.meterSigDefault.log (@meter.count, @meter.unit)) | |
| - (att.keySigDefault.log (@key.accid, @key.mode, @key.pname, @key.sig)) | |
| - contained <staffGrp> | |
| **Attributes/Elements in Testing:** None | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.scoreDef.log | |
| - (att.cleffing.log (@clef.shape, @clef.line, @clef.dis, @clef.dis.place)) | |
| - (att.duration.default (@dur.default, @num.default, @numbase.default)) | |
| - (att.keySigDefault.log (@key.sig.mixed)) | |
| - (att.octavedefault (@octave.default)) | |
| - (att.transposition (@trans.diat, @trans.semi)) | |
| - (att.scoreDef.log.cmn (att.beaming.log (@beam.group, @beam.rests))) | |
| - (att.scoreDef.log.mensural | |
| - (att.mensural.log (@mensur.dot, @mensur.sign, | |
| @mensur.slash, @proport.num, @proport.numbase) | |
| - (att.mensural.shared (@modusmaior, @modusminor, @prolatio, @tempus)))) | |
| - att.scoreDef.vis (all) | |
| - att.scoreDef.ges (all) | |
| - att.scoreDef.anl (none exist) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: meterSig meterSigGrp | |
| - MEI.harmony: chordTable | |
| - MEI.linkalign: timeline | |
| - MEI.midi: instrGrp | |
| - MEI.shared: keySig pgFoot pgFoot2 pgHead pgHead2 | |
| - MEI.usersymbols: symbolTable | |
| ''' | |
| # make the dict | |
| allParts = 'all-part objects' | |
| wholeScore = 'whole-score objects' | |
| post = {allParts: [], wholeScore: []} | |
| # 1.) process all-part attributes | |
| # --> time signature | |
| if elem.get('meter.count') is not None: | |
| post[allParts].append(_timeSigFromAttrs(elem)) | |
| # --> key signature | |
| if elem.get('key.pname') is not None or elem.get('key.sig') is not None: | |
| post[allParts].append(_keySigFromAttrs(elem)) | |
| # 2.) staff-specific things (from contained <staffGrp> >> <staffDef>) | |
| for eachGrp in elem.iterfind(f'{MEI_NS}staffGrp'): | |
| post.update(staffGrpFromElement(eachGrp, slurBundle)) | |
| return post | |
| def staffGrpFromElement(elem, slurBundle=None, staffDefDict=None): | |
| ''' | |
| <staffGrp> A group of bracketed or braced staves. | |
| In MEI 2013: pg.448 (462 in PDF) (MEI.shared module) | |
| For now, this function is merely a container-processor for <staffDef> elements contained | |
| in this <staffGrp> element given as the "elem" argument. That is, the function does not yet | |
| create the brackets/braces and labels expected of a staff group. | |
| Note however that all <staffDef> | |
| elements will be processed, even if they're contained within several layers of <staffGrp>. | |
| :param elem: The ``<staffGrp>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :returns: Dictionary where keys are the @n attribute on a contained <staffDef>, and values are | |
| the result of calling :func:`staffDefFromElement` with that <staffDef>. | |
| **Attributes/Elements Implemented:** | |
| - contained <staffDef> | |
| - contained <staffGrp> | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.declaring (@decls) | |
| - att.facsimile (@facs) | |
| - att.staffGrp.vis (@barthru) | |
| - (att.labels.addl (@label.abbr)) | |
| - (att.staffgroupingsym (@symbol)) | |
| - (att.visibility (@visible)) | |
| - att.staffGrp.ges (att.instrumentident (@instr)) | |
| **Contained Elements not Implemented:** | |
| - MEI.midi: instrDef | |
| - MEI.shared: grpSym label | |
| ''' | |
| staffDefTag = f'{MEI_NS}staffDef' | |
| staffGroupTag = f'{MEI_NS}staffGrp' | |
| staffDefDict = staffDefDict if staffDefDict is not None else {} | |
| for el in elem.findall('*'): | |
| # return all staff defs in this staff group | |
| if el.tag == staffDefTag: | |
| staffDefDict[el.get('n')] = staffDefFromElement(el, slurBundle) | |
| # recurse if there are more groups, append to the working staffDefDict | |
| elif el.tag == staffGroupTag: | |
| staffGrpFromElement(el, slurBundle, staffDefDict) | |
| return staffDefDict | |
| def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <staffDef> Container for staff meta-information. | |
| In MEI 2013: pg.445 (459 in PDF) (MEI.shared module) | |
| :returns: A dict with various types of metadata information, depending on what is specified in | |
| this <staffDef> element. Read below for more information. | |
| :rtype: dict | |
| **Possible Return Values** | |
| The contents of the returned dictionary depend on the contents of the <staffDef> element. The | |
| dictionary keys correspond to types of information. Possible keys include: | |
| - ``'instrument'``: for a :class:`music21.instrument.Instrument` subclass | |
| - ``'clef'``: for a :class:`music21.clef.Clef` subclass | |
| - ``'key'``: for a :class:`music21.key.Key` or :class:`~music21.key.KeySignature` subclass | |
| - ``'meter'``: for a :class:`music21.meter.TimeSignature` | |
| **Examples** | |
| This <staffDef> only returns a single item. | |
| >>> meiDoc = """<?xml version="1.0" encoding="UTF-8"?> | |
| ... <staffDef n="1" label="Clarinet" xmlns="http://www.music-encoding.org/ns/mei"/> | |
| ... """ | |
| >>> from xml.etree import ElementTree as ET | |
| >>> staffDef = ET.fromstring(meiDoc) | |
| >>> result = mei.base.staffDefFromElement(staffDef) | |
| >>> len(result) | |
| 1 | |
| >>> result | |
| {'instrument': <music21.instrument.Clarinet '1: Clarinet: Clarinet'>} | |
| >>> result['instrument'].partId | |
| '1' | |
| >>> result['instrument'].partName | |
| 'Clarinet' | |
| This <staffDef> returns many objects. | |
| >>> meiDoc = """<?xml version="1.0" encoding="UTF-8"?> | |
| ... <staffDef n="2" label="Tuba" key.pname="B" key.accid="f" key.mode="major" | |
| ... xmlns="http://www.music-encoding.org/ns/mei"> | |
| ... <clef shape="F" line="4"/> | |
| ... </staffDef> | |
| ... """ | |
| >>> from xml.etree import ElementTree as ET | |
| >>> staffDef = ET.fromstring(meiDoc) | |
| >>> result = mei.base.staffDefFromElement(staffDef) | |
| >>> len(result) | |
| 3 | |
| >>> result['instrument'] | |
| <music21.instrument.Tuba '2: Tuba: Tuba'> | |
| >>> result['clef'] | |
| <music21.clef.BassClef> | |
| >>> result['key'] | |
| <music21.key.Key of B- major> | |
| **Attributes/Elements Implemented:** | |
| - @label (att.common) as Instrument.partName | |
| - @label.abbr (att.labels.addl) as Instrument.partAbbreviation | |
| - @n (att.common) as Instrument.partId | |
| - (att.keySigDefault.log (@key.accid, @key.mode, @key.pname, @key.sig)) | |
| - (att.meterSigDefault.log (@meter.count, @meter.unit)) | |
| - (att.cleffing.log (@clef.shape, @clef.line, @clef.dis, @clef.dis.place)) | |
| (via :func:`clefFromElement`) | |
| - @trans.diat and @trans.demi (att.transposition) | |
| - <instrDef> held within | |
| - <clef> held within | |
| **Attributes/Elements Ignored:** | |
| - @key.sig.mixed (from att.keySigDefault.log) | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@n, @xml:base) (att.id (@xml:id)) | |
| - att.declaring (@decls) | |
| - att.staffDef.log | |
| - (att.duration.default (@dur.default, @num.default, @numbase.default)) | |
| - (att.octavedefault (@octave.default)) | |
| - (att.staffDef.log.cmn (att.beaming.log (@beam.group, @beam.rests))) | |
| - (att.staffDef.log.mensural | |
| - (att.mensural.log (@mensur.dot, @mensur.sign, @mensur.slash, | |
| @proport.num, @proport.numbase) | |
| - (att.mensural.shared (@modusmaior, @modusminor, @prolatio, @tempus)))) | |
| - att.staffDef.vis (all) | |
| - att.staffDef.ges (all) | |
| - att.staffDef.anl (none exist) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: meterSig meterSigGrp | |
| - MEI.mensural: mensural support | |
| - MEI.shared: clefGrp keySig label layerDef | |
| ''' | |
| # mapping from tag name to our converter function | |
| tagToFunction = {f'{MEI_NS}clef': clefFromElement} | |
| # first make the Instrument | |
| post = elem.find(f'{MEI_NS}instrDef') | |
| if post is not None: | |
| post = {'instrument': instrDefFromElement(post)} | |
| else: | |
| try: | |
| post = {'instrument': instrument.fromString(elem.get('label', ''))} | |
| except instrument.InstrumentException: | |
| post = {} | |
| if 'instrument' in post: | |
| post['instrument'].partName = elem.get('label') | |
| post['instrument'].partAbbreviation = elem.get('label.abbr') | |
| post['instrument'].partId = elem.get('n') | |
| # --> transposition | |
| if elem.get('trans.semi') is not None: | |
| if 'instrument' not in post: | |
| post['instrument'] = instrument.Instrument() | |
| post['instrument'].transposition = _transpositionFromAttrs(elem) | |
| # process other part-specific information | |
| # --> time signature | |
| if elem.get('meter.count') is not None: | |
| post['meter'] = _timeSigFromAttrs(elem) | |
| # --> key signature | |
| if elem.get('key.pname') is not None or elem.get('key.sig') is not None: | |
| post['key'] = _keySigFromAttrs(elem) | |
| # --> clef | |
| if elem.get('clef.shape') is not None: | |
| el = Element( | |
| 'clef', { | |
| 'shape': elem.get('clef.shape'), | |
| 'line': elem.get('clef.line'), | |
| 'dis': elem.get('clef.dis'), | |
| 'dis.place': elem.get('clef.dis.place') | |
| } | |
| ) | |
| post['clef'] = clefFromElement(el) | |
| embeddedItems = _processEmbeddedElements(elem.findall('*'), tagToFunction, elem.tag, slurBundle) | |
| for eachItem in embeddedItems: | |
| if isinstance(eachItem, clef.Clef): | |
| post['clef'] = eachItem | |
| return post | |
| def dotFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| Returns ``1`` no matter what is passed in. | |
| <dot> Dot of augmentation or division. | |
| In MEI 2013: pg.304 (318 in PDF) (MEI.shared module) | |
| :returns: 1 | |
| :rtype: int | |
| **Attributes/Elements Implemented:** none | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.dot.log (all) | |
| - att.dot.vis (all) | |
| - att.dot.gesatt.dot.anl (all) | |
| **Elements not Implemented:** none | |
| ''' | |
| return 1 | |
| def articFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <artic> An indication of how to play a note or chord. | |
| In MEI 2013: pg.259 (273 in PDF) (MEI.shared module) | |
| :returns: A list of :class:`~music21.articulations.Articulation` objects. | |
| **Examples** | |
| This function is normally called by, for example, :func:`noteFromElement`, to determine the | |
| :class:`Articulation` objects that will be assigned to the | |
| :attr:`~music21.note.GeneralNote.articulations` attribute. | |
| >>> from xml.etree import ElementTree as ET | |
| >>> meiSnippet = '<artic artic="acc" xmlns="http://www.music-encoding.org/ns/mei"/>' | |
| >>> meiSnippet = ET.fromstring(meiSnippet) | |
| >>> mei.base.articFromElement(meiSnippet) | |
| [<music21.articulations.Accent>] | |
| A single <artic> element may indicate many :class:`Articulation` objects. | |
| >>> meiSnippet = '<artic artic="acc ten" xmlns="http://www.music-encoding.org/ns/mei"/>' | |
| >>> meiSnippet = ET.fromstring(meiSnippet) | |
| >>> mei.base.articFromElement(meiSnippet) | |
| [<music21.articulations.Accent>, <music21.articulations.Tenuto>] | |
| **Attributes Implemented:** | |
| - @artic | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.typography (@fontfam, @fontname, @fontsize, @fontstyle, @fontweight) | |
| - att.artic.log | |
| - (att.controlevent | |
| - (att.plist (@plist, @evaluate)) | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer))) | |
| - att.artic.vis (all) | |
| - att.artic.gesatt.artic.anl (all) | |
| **Contained Elements not Implemented:** none | |
| ''' | |
| articElement = elem.get('artic') | |
| if articElement is not None: | |
| return _makeArticList(articElement) | |
| else: | |
| return [] | |
| def accidFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <accid> Records a temporary alteration to the pitch of a note. | |
| In MEI 2013: pg.248 (262 in PDF) (MEI.shared module) | |
| :returns: A string indicating the music21 representation of this accidental. | |
| **Examples** | |
| Unlike most of the ___FromElement() functions, this does not return any music21 object---just | |
| a string. Accidentals up to triple-sharp and triple-flat are supported. | |
| >>> from xml.etree import ElementTree as ET | |
| >>> meiSnippet = '<accid accid="s" xmlns="http://www.music-encoding.org/ns/mei"/>' | |
| >>> meiSnippet = ET.fromstring(meiSnippet) | |
| >>> mei.base.accidFromElement(meiSnippet) | |
| '#' | |
| >>> meiSnippet = '<accid accid="tf" xmlns="http://www.music-encoding.org/ns/mei"/>' | |
| >>> meiSnippet = ET.fromstring(meiSnippet) | |
| >>> mei.base.accidFromElement(meiSnippet) | |
| '---' | |
| **Attributes/Elements Implemented:** | |
| - @accid (from att.accid.log) | |
| - @accid.ges (from att.accid.ges) | |
| .. note:: If set, the @accid.ges attribute is always imported as the music21 :class:`Accidental` | |
| for this note. We assume it corresponds to the accidental implied by a key signature. | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.typography (@fontfam, @fontname, @fontsize, @fontstyle, @fontweight) | |
| - att.accid.log (@func) | |
| - (att.controlevent | |
| - (att.plist (@plist, @evaluate)) | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) (att.layerident (@layer))) | |
| - att.accid.vis (all) | |
| - att.accid.anl (all) | |
| **Contained Elements not Implemented:** none | |
| ''' | |
| if elem.get('accid.ges') is not None: | |
| return _accidGesFromAttr(elem.get('accid.ges', '')) | |
| else: | |
| return _accidentalFromAttr(elem.get('accid')) | |
| def sylFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <syl> Individual lyric syllable. | |
| In MEI 2013: pg.454 (468 in PDF) (MEI.shared module) | |
| :returns: An appropriately-configured :class:`music21.note.Lyric`. | |
| **Attributes/Elements Implemented:** | |
| - @con and @wordpos (from att.syl.log) | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.syl.vis (att.typography (@fontfam, @fontname, @fontsize, @fontstyle, @fontweight)) | |
| - (att.visualoffset (att.visualoffset.ho (@ho)) | |
| - (att.visualoffset.to (@to)) | |
| - (att.visualoffset.vo (@vo))) | |
| - (att.xy (@x, @y)) | |
| - (att.horizontalalign (@halign)) | |
| - att.syl.anl (att.common.anl (@copyof, @corresp, @next, @prev, @sameas, @synch) | |
| - (att.alignment (@when))) | |
| **Contained Elements not Implemented:** | |
| - MEI.edittrans: (all) | |
| - MEI.figtable: fig | |
| - MEI.namesdates: corpName geogName periodName persName styleName | |
| - MEI.ptrref: ptr ref | |
| - MEI.shared: address bibl date identifier lb name num rend repository stack title | |
| ''' | |
| wordPos = elem.get('wordpos') | |
| wordPosDict = {'i': 'begin', 'm': 'middle', 't': 'end', None: None} | |
| conDict = {'s': ' ', 'd': '-', 't': '~', 'u': '_', None: '-'} | |
| if 'i' == wordPos: | |
| text = elem.text + conDict[elem.get('con')] | |
| elif 'm' == wordPos: | |
| text = conDict[elem.get('con')] + elem.text + conDict[elem.get('con')] | |
| elif 't' == wordPos: | |
| text = conDict[elem.get('con')] + elem.text | |
| else: | |
| text = elem.text | |
| syllabic = wordPosDict[wordPos] | |
| if syllabic: | |
| return note.Lyric(text=text, syllabic=syllabic, applyRaw=True) | |
| else: | |
| return note.Lyric(text=text) | |
| def verseFromElement(elem, backupN=None, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <verse> Lyric verse. | |
| In MEI 2013: pg.480 (494 in PDF) (MEI.lyrics module) | |
| :param int backupN: The backup verse number to use if no @n attribute exists on ``elem``. | |
| :returns: The appropriately-configured :class:`Lyric` objects. | |
| :rtype: list of :class:`music21.note.Lyric` | |
| **Attributes/Elements Implemented:** | |
| - @n and <syl> | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.lang (@xml:lang) | |
| - att.verse.log (@refrain, @rhythm) | |
| - att.verse.vis (att.typography (@fontfam, @fontname, @fontsize, @fontstyle, @fontweight)) | |
| - (att.visualoffset.to (@to)) | |
| - ((att.visualoffset.vo (@vo)) | |
| - (att.xy (@x, @y)) | |
| - att.verse.anl (att.common.anl (@copyof, @corresp, @next, @prev, @sameas, @synch) | |
| - (att.alignment (@when))) | |
| **Contained Elements not Implemented:** | |
| - MEI.shared: dir dynam lb space tempo | |
| ''' | |
| syllables = [sylFromElement(s) for s in elem.findall(f'./{MEI_NS}syl')] | |
| for eachSyl in syllables: | |
| try: | |
| eachSyl.number = int(elem.get('n', backupN)) | |
| except (TypeError, ValueError): | |
| environLocal.warn(_BAD_VERSE_NUMBER.format(elem.get('n', backupN))) | |
| return syllables | |
| def noteFromElement(elem, slurBundle=None): | |
| # NOTE: this function should stay in sync with chordFromElement() where sensible | |
| ''' | |
| <note> is a single pitched event. | |
| In MEI 2013: pg.382 (396 in PDF) (MEI.shared module) | |
| .. note:: If set, the @accid.ges attribute is always imported as the music21 :class:`Accidental` | |
| for this note. We assume it corresponds to the accidental implied by a key signature. | |
| .. note:: If ``elem`` contains both <syl> and <verse> elements as immediate children, the lyrics | |
| indicated with <verse> element(s) will always obliterate those given indicated with <syl> | |
| elements. | |
| **Attributes/Elements Implemented:** | |
| - @accid and <accid> | |
| - @accid.ges for key signatures | |
| - @pname, from att.pitch: [a--g] | |
| - @oct, from att.octave: [0..9] | |
| - @dur, from att.duration.musical: (via _qlDurationFromAttr()) | |
| - @dots: [0..4], and <dot> contained within | |
| - @xml:id (or id), an XML id (submitted as the Music21Object "id") | |
| - @artic and <artic> | |
| - @tie, (many of "[i|m|t]") | |
| - @slur, (many of "[i|m|t][1-6]") | |
| - @grace, from att.note.ges.cmn: partial implementation (notes marked as grace, but the | |
| duration is 0 because we ignore the question of which neighbouring note to borrow time from) | |
| - <syl> and <verse> | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.facsimile (@facs) | |
| - att.note.log | |
| - (att.event | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer))) | |
| - (att.fermatapresent (@fermata)) | |
| - (att.syltext (@syl)) | |
| - (att.note.log.cmn | |
| - (att.tupletpresent (@tuplet)) | |
| - (att.beamed (@beam)) | |
| - (att.lvpresent (@lv)) | |
| - (att.ornam (@ornam))) | |
| - (att.note.log.mensural (@lig)) | |
| - att.note.vis (all) | |
| - att.note.ges | |
| - (@oct.ges, @pname.ges, @pnum) | |
| - att.articulation.performed (@artic.ges)) | |
| - (att.duration.performed (@dur.ges)) | |
| - (att.instrumentident (@instr)) | |
| - (att.note.ges.cmn (@gliss) | |
| - (att.graced (@grace, @grace.time))) <-- partially implemented | |
| - (att.note.ges.mensural (att.duration.ratio (@num, @numbase))) | |
| - (att.note.ges.tablature (@tab.fret, @tab.string)) | |
| - att.note.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.critapp: app | |
| - MEI.edittrans: (all) | |
| ''' | |
| tagToFunction = {f'{MEI_NS}dot': dotFromElement, | |
| f'{MEI_NS}artic': articFromElement, | |
| f'{MEI_NS}accid': accidFromElement, | |
| f'{MEI_NS}syl': sylFromElement} | |
| # start with a Note with Pitch | |
| theNote = _accidentalFromAttr(elem.get('accid')) | |
| theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) | |
| theNote = note.Note(theNote) | |
| # set the Note's duration | |
| theDuration = _qlDurationFromAttr(elem.get('dur')) | |
| theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) | |
| theNote.duration = theDuration | |
| # iterate all immediate children | |
| dotElements = 0 # count the number of <dot> elements | |
| for subElement in _processEmbeddedElements(elem.findall('*'), | |
| tagToFunction, | |
| elem.tag, | |
| slurBundle): | |
| if isinstance(subElement, int): | |
| dotElements += subElement | |
| elif isinstance(subElement, articulations.Articulation): | |
| theNote.articulations.append(subElement) | |
| elif isinstance(subElement, str): | |
| theNote.pitch.accidental = pitch.Accidental(subElement) | |
| elif isinstance(subElement, note.Lyric): | |
| theNote.lyrics = [subElement] | |
| # adjust for @accid.ges if present | |
| if elem.get('accid.ges') is not None: | |
| theNote.pitch.accidental = pitch.Accidental(_accidGesFromAttr(elem.get('accid.ges', ''))) | |
| # we can only process slurs if we got a SpannerBundle as the "slurBundle" argument | |
| if slurBundle is not None: | |
| addSlurs(elem, theNote, slurBundle) | |
| # id in the @xml:id attribute | |
| if elem.get(_XMLID) is not None: | |
| theNote.id = elem.get(_XMLID) | |
| # articulations in the @artic attribute | |
| if elem.get('artic') is not None: | |
| theNote.articulations.extend(_makeArticList(elem.get('artic'))) | |
| # ties in the @tie attribute | |
| if elem.get('tie') is not None: | |
| theNote.tie = _tieFromAttr(elem.get('tie')) | |
| # dots from inner <dot> elements | |
| if dotElements > 0: | |
| theNote.duration = makeDuration(_qlDurationFromAttr(elem.get('dur')), dotElements) | |
| # grace note (only mark as grace note---don't worry about "time-stealing") | |
| if elem.get('grace') is not None: | |
| theNote.duration = duration.GraceDuration(theNote.duration.quarterLength) | |
| # beams indicated by a <beamSpan> held elsewhere | |
| if elem.get('m21Beam') is not None: | |
| if duration.convertTypeToNumber(theNote.duration.type) > 4: | |
| theNote.beams.fill(theNote.duration.type, elem.get('m21Beam')) | |
| # tuplets | |
| if elem.get('m21TupletNum') is not None: | |
| theNote = scaleToTuplet(theNote, elem) | |
| # lyrics indicated with <verse> | |
| if elem.find(f'./{MEI_NS}verse') is not None: | |
| tempLyrics = [] | |
| for i, eachVerse in enumerate(elem.findall(f'./{MEI_NS}verse')): | |
| tempLyrics.extend(verseFromElement(eachVerse, backupN=i + 1)) | |
| theNote.lyrics = tempLyrics | |
| return theNote | |
| def restFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <rest/> is a non-sounding event found in the source being transcribed | |
| In MEI 2013: pg.424 (438 in PDF) (MEI.shared module) | |
| **Attributes/Elements Implemented:** | |
| - xml:id (or id), an XML id (submitted as the Music21Object "id") | |
| - dur, from att.duration.musical: (via _qlDurationFromAttr()) | |
| - dots, from att.augmentdots: [0..4] | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.facsimile (@facs) | |
| - att.rest.log | |
| - (att.event | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer))) | |
| - (att.fermatapresent (@fermata)) | |
| - (att.tupletpresent (@tuplet)) | |
| - (att.rest.log.cmn (att.beamed (@beam))) | |
| - att.rest.vis (all) | |
| - att.rest.ges (all) | |
| - att.rest.anl (all) | |
| **Contained Elements not Implemented:** none | |
| ''' | |
| # NOTE: keep this in sync with spaceFromElement() | |
| theDuration = _qlDurationFromAttr(elem.get('dur')) | |
| theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) | |
| theRest = note.Rest(duration=theDuration) | |
| if elem.get(_XMLID) is not None: | |
| theRest.id = elem.get(_XMLID) | |
| # tuplets | |
| if elem.get('m21TupletNum') is not None: | |
| theRest = scaleToTuplet(theRest, elem) | |
| return theRest | |
| def mRestFromElement(elem, slurBundle=None): | |
| ''' | |
| <mRest/> Complete measure rest in any meter. | |
| In MEI 2013: pg.375 (389 in PDF) (MEI.cmn module) | |
| This is a function wrapper for :func:`restFromElement`. | |
| .. note:: If the <mRest> element does not have a @dur attribute, it will have the default | |
| duration of 1.0. This must be fixed later, so the :class:`Rest` object returned from this | |
| method is given the :attr:`m21wasMRest` attribute, set to True. | |
| ''' | |
| # NOTE: keep this in sync with mSpaceFromElement() | |
| if elem.get('dur') is not None: | |
| return restFromElement(elem, slurBundle) | |
| else: | |
| theRest = restFromElement(elem, slurBundle) | |
| theRest.m21wasMRest = True | |
| return theRest | |
| def spaceFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <space> A placeholder used to fill an incomplete measure, layer, etc. most often so that the | |
| combined duration of the events equals the number of beats in the measure. | |
| Returns a Rest element with hideObjectOnPrint = True | |
| In MEI 2013: pg.440 (455 in PDF) (MEI.shared module) | |
| ''' | |
| # NOTE: keep this in sync with restFromElement() | |
| theDuration = _qlDurationFromAttr(elem.get('dur')) | |
| theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) | |
| theSpace = note.Rest(duration=theDuration) | |
| theSpace.style.hideObjectOnPrint = True | |
| if elem.get(_XMLID) is not None: | |
| theSpace.id = elem.get(_XMLID) | |
| # tuplets | |
| if elem.get('m21TupletNum') is not None: | |
| theSpace = scaleToTuplet(theSpace, elem) | |
| return theSpace | |
| def mSpaceFromElement(elem, slurBundle=None): | |
| ''' | |
| <mSpace/> A measure containing only empty space in any meter. | |
| In MEI 2013: pg.377 (391 in PDF) (MEI.cmn module) | |
| This is a function wrapper for :func:`spaceFromElement`. | |
| .. note:: If the <mSpace> element does not have a @dur attribute, it will have the default | |
| duration of 1.0. This must be fixed later, so the :class:`Space` object returned from this | |
| method is given the :attr:`m21wasMRest` attribute, set to True. | |
| ''' | |
| # NOTE: keep this in sync with mRestFromElement() | |
| if elem.get('dur') is not None: | |
| return spaceFromElement(elem, slurBundle) | |
| else: | |
| theSpace = spaceFromElement(elem, slurBundle) | |
| theSpace.m21wasMRest = True | |
| return theSpace | |
| def chordFromElement(elem, slurBundle=None): | |
| # NOTE: this function should stay in sync with noteFromElement() where sensible | |
| ''' | |
| <chord> is a simultaneous sounding of two or | |
| more notes in the same layer with the same duration. | |
| In MEI 2013: pg.280 (294 in PDF) (MEI.shared module) | |
| **Attributes/Elements Implemented:** | |
| - @xml:id (or id), an XML id (submitted as the Music21Object "id") | |
| - <note> contained within | |
| - @dur, from att.duration.musical: (via _qlDurationFromAttr()) | |
| - @dots, from att.augmentdots: [0..4] | |
| - @artic and <artic> | |
| - @tie, (many of "[i|m|t]") | |
| - @slur, (many of "[i|m|t][1-6]") | |
| - @grace, from att.note.ges.cmn: partial implementation (notes marked as grace, but the | |
| duration is 0 because we ignore the question of which neighbouring note to borrow time from) | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.facsimile (@facs) | |
| - att.chord.log | |
| - (att.event | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer))) | |
| - (att.fermatapresent (@fermata)) | |
| - (att.syltext (@syl)) | |
| - (att.chord.log.cmn | |
| - (att.tupletpresent (@tuplet)) | |
| - (att.beamed (@beam)) | |
| - (att.lvpresent (@lv)) | |
| - (att.ornam (@ornam))) | |
| - att.chord.vis (all) | |
| - att.chord.ges | |
| - (att.articulation.performed (@artic.ges)) | |
| - (att.duration.performed (@dur.ges)) | |
| - (att.instrumentident (@instr)) | |
| - (att.chord.ges.cmn (att.graced (@grace, @grace.time))) <-- partially implemented | |
| - att.chord.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.edittrans: (all) | |
| ''' | |
| tagToFunction = {f'{MEI_NS}note': lambda *x: None, | |
| f'{MEI_NS}artic': articFromElement} | |
| # start with a Chord with a bunch of Notes | |
| theChord = [] | |
| for eachNote in elem.iterfind(f'{MEI_NS}note'): | |
| theChord.append(noteFromElement(eachNote, slurBundle)) | |
| theChord = chord.Chord(notes=theChord) | |
| # set the Chord's duration | |
| theDuration = _qlDurationFromAttr(elem.get('dur')) | |
| theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) | |
| theChord.duration = theDuration | |
| # iterate all immediate children | |
| for subElement in _processEmbeddedElements(elem.findall('*'), | |
| tagToFunction, | |
| elem.tag, | |
| slurBundle): | |
| if isinstance(subElement, articulations.Articulation): | |
| theChord.articulations.append(subElement) | |
| # we can only process slurs if we got a SpannerBundle as the "slurBundle" argument | |
| if slurBundle is not None: | |
| addSlurs(elem, theChord, slurBundle) | |
| # id in the @xml:id attribute | |
| if elem.get(_XMLID) is not None: | |
| theChord.id = elem.get(_XMLID) | |
| # articulations in the @artic attribute | |
| if elem.get('artic') is not None: | |
| theChord.articulations.extend(_makeArticList(elem.get('artic'))) | |
| # ties in the @tie attribute | |
| if elem.get('tie') is not None: | |
| theChord.tie = _tieFromAttr(elem.get('tie')) | |
| # grace note (only mark as grace note---don't worry about "time-stealing") | |
| if elem.get('grace') is not None: | |
| theChord.duration = duration.GraceDuration(theChord.duration.quarterLength) | |
| # beams indicated by a <beamSpan> held elsewhere | |
| if elem.get('m21Beam') is not None: | |
| if duration.convertTypeToNumber(theChord.duration.type) > 4: | |
| theChord.beams.fill(theChord.duration.type, elem.get('m21Beam')) | |
| # tuplets | |
| if elem.get('m21TupletNum') is not None: | |
| theChord = scaleToTuplet(theChord, elem) | |
| return theChord | |
| def clefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <clef> Indication of the exact location of a particular note on the staff and, therefore, | |
| the other notes as well. | |
| In MEI 2013: pg.284 (298 in PDF) (MEI.shared module) | |
| **Attributes/Elements Implemented:** | |
| - @xml:id (or id), an XML id (submitted as the Music21Object "id") | |
| - @shape, from att.clef.gesatt.clef.log | |
| - @line, from att.clef.gesatt.clef.log | |
| - @dis, from att.clef.gesatt.clef.log | |
| - @dis.place, from att.clef.gesatt.clef.log | |
| **Attributes/Elements Ignored:** | |
| - @cautionary, since this has no obvious implication for a music21 Clef | |
| - @octave, since this is likely obscure | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.event | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer)) | |
| - att.facsimile (@facs) | |
| - att.clef.anl (all) | |
| - att.clef.vis (all) | |
| **Contained Elements not Implemented:** none | |
| ''' | |
| if 'perc' == elem.get('shape'): | |
| theClef = clef.PercussionClef() | |
| elif 'TAB' == elem.get('shape'): | |
| theClef = clef.TabClef() | |
| else: | |
| theClef = clef.clefFromString(elem.get('shape') + elem.get('line'), | |
| octaveShift=_getOctaveShift(elem.get('dis'), | |
| elem.get('dis.place'))) | |
| if elem.get(_XMLID) is not None: | |
| theClef.id = elem.get(_XMLID) | |
| return theClef | |
| def instrDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| # TODO: robuster handling of <instrDef>, including <instrGrp> and if held in a <staffGrp> | |
| ''' | |
| <instrDef> (instrument definition)---MIDI instrument declaration. | |
| In MEI 2013: pg.344 (358 in PDF) (MEI.midi module) | |
| :returns: An :class:`Instrument` | |
| **Attributes/Elements Implemented:** | |
| - @midi.instrname (att.midiinstrument) | |
| - @midi.instrnum (att.midiinstrument) | |
| **Attributes/Elements in Testing:** none | |
| **Attributes/Elements Ignored:** | |
| - @xml:id | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.channelized (@midi.channel, @midi.duty, @midi.port, @midi.track) | |
| - att.midiinstrument (@midi.pan, @midi.volume) | |
| **Contained Elements not Implemented:** none | |
| ''' | |
| if elem.get('midi.instrnum') is not None: | |
| return instrument.instrumentFromMidiProgram(int(elem.get('midi.instrnum'))) | |
| else: | |
| try: | |
| return instrument.fromString(elem.get('midi.instrname')) | |
| except (AttributeError, instrument.InstrumentException): | |
| theInstr = instrument.Instrument() | |
| theInstr.partName = elem.get('midi.instrname', '') | |
| return theInstr | |
| def beamFromElement(elem, slurBundle=None): | |
| ''' | |
| <beam> A container for a series of explicitly beamed events that begins and ends entirely | |
| within a measure. | |
| In MEI 2013: pg.264 (278 in PDF) (MEI.cmn module) | |
| :param elem: The ``<beam>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :returns: An iterable of all the objects contained within the ``<beam>`` container. | |
| :rtype: list of :class:`~music21.base.Music21Object` | |
| **Example** | |
| Here, three :class:`Note` objects are beamed together. Take note that the function returns | |
| a list of three objects, none of which is a :class:`Beam` or similar. | |
| >>> from xml.etree import ElementTree as ET | |
| >>> meiSnippet = """<beam xmlns="http://www.music-encoding.org/ns/mei"> | |
| ... <note pname='A' oct='7' dur='8'/> | |
| ... <note pname='B' oct='7' dur='8'/> | |
| ... <note pname='C' oct='6' dur='8'/> | |
| ... </beam>""" | |
| >>> meiSnippet = ET.fromstring(meiSnippet) | |
| >>> result = mei.base.beamFromElement(meiSnippet) | |
| >>> isinstance(result, list) | |
| True | |
| >>> len(result) | |
| 3 | |
| >>> result[0].pitch.nameWithOctave | |
| 'A7' | |
| >>> result[0].beams | |
| <music21.beam.Beams <music21.beam.Beam 1/start>> | |
| >>> result[1].pitch.nameWithOctave | |
| 'B7' | |
| >>> result[1].beams | |
| <music21.beam.Beams <music21.beam.Beam 1/continue>> | |
| >>> result[2].pitch.nameWithOctave | |
| 'C6' | |
| >>> result[2].beams | |
| <music21.beam.Beams <music21.beam.Beam 1/stop>> | |
| **Attributes/Elements Implemented:** | |
| - <clef>, <chord>, <note>, <rest>, <space>, <tuplet>, <beam>, <barLine> | |
| **Attributes/Elements Ignored:** | |
| - @xml:id | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.facsimile (@facs) | |
| - att.beam.log | |
| - (att.event | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer))) | |
| - (att.beamedwith (@beam.with)) | |
| - att.beam.vis (all) | |
| - att.beam.gesatt.beam.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: bTrem beatRpt fTrem halfmRpt meterSig meterSigGrp | |
| - MEI.critapp: app | |
| - MEI.edittrans: (all) | |
| - MEI.mensural: ligature mensur proport | |
| - MEI.shared: clefGrp custos keySig pad | |
| ''' | |
| # NB: The doctest is a sufficient integration test. Since there is no logic, I don't think we | |
| # need to bother with unit testing. | |
| # mapping from tag name to our converter function | |
| tagToFunction = { | |
| f'{MEI_NS}clef': clefFromElement, | |
| f'{MEI_NS}chord': chordFromElement, | |
| f'{MEI_NS}note': noteFromElement, | |
| f'{MEI_NS}rest': restFromElement, | |
| f'{MEI_NS}tuplet': tupletFromElement, | |
| f'{MEI_NS}beam': beamFromElement, | |
| f'{MEI_NS}space': spaceFromElement, | |
| f'{MEI_NS}barLine': barLineFromElement, | |
| } | |
| beamedStuff = _processEmbeddedElements(elem.findall('*'), tagToFunction, elem.tag, slurBundle) | |
| beamedStuff = beamTogether(beamedStuff) | |
| return beamedStuff | |
| def barLineFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | |
| ''' | |
| <barLine> Vertical line drawn through one or more staves that divides musical notation into | |
| metrical units. | |
| In MEI 2013: pg.262 (276 in PDF) (MEI.shared module) | |
| :returns: A :class:`music21.bar.Barline` or :class:`~music21.bar.Repeat`, depending on the | |
| value of @rend. If @rend is ``'rptboth'``, a 2-tuplet of :class:`Repeat` objects will be | |
| returned, represented an "end" and "start" barline, as specified in the :mod:`music21.bar` | |
| documentation. | |
| .. note:: The music21-to-other converters expect that a :class:`Barline` will be attached to a | |
| :class:`Measure`, which it will not be when imported from MEI as a <barLine> element. | |
| However, this function does import correctly to a :class:`Barline` that you can access from | |
| Python in the :class:`Stream` object as expected. | |
| **Attributes/Elements Implemented:** | |
| - @rend from att.barLine.log | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.pointing (@xlink:actuate, @xlink:role, @xlink:show, @target, @targettype, @xlink:title) | |
| - att.barLine.log | |
| - (att.meterconformance.bar (@metcon, @control)) | |
| - att.barLine.vis | |
| - (att.barplacement (@barplace, @taktplace)) | |
| - (att.color (@color)) | |
| - (att.measurement (@unit)) | |
| - (att.width (@width)) | |
| - att.barLine.ges (att.timestamp.musical (@tstamp)) | |
| - att.barLine.anl | |
| - (att.common.anl | |
| - (@copyof, @corresp, @next, @prev, @sameas, @synch) | |
| - (att.alignment (@when))) | |
| **Contained Elements not Implemented:** none | |
| ''' | |
| return _barlineFromAttr(elem.get('rend', 'single')) | |
| def tupletFromElement(elem, slurBundle=None): | |
| ''' | |
| <tuplet> A group of notes with "irregular" (sometimes called "irrational") rhythmic values, | |
| for example, three notes in the time normally occupied by two or nine in the time of five. | |
| In MEI 2013: pg.473 (487 in PDF) (MEI.cmn module) | |
| :param elem: The ``<tuplet>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :returns: An iterable of all the objects contained within the ``<tuplet>`` container. | |
| :rtype: tuple of :class:`~music21.base.Music21Object` | |
| **Attributes/Elements Implemented:** | |
| - <tuplet>, <beam>, <note>, <rest>, <chord>, <clef>, <space>, <barLine> | |
| - @num and @numbase | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.facsimile (@facs) | |
| - att.tuplet.log | |
| - (att.event | |
| - (att.timestamp.musical (@tstamp)) | |
| - (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - (att.staffident (@staff)) | |
| - (att.layerident (@layer))) | |
| - (att.beamedwith (@beam.with)) | |
| - (att.augmentdots (@dots)) | |
| - (att.duration.additive (@dur)) | |
| - (att.startendid (@endid) (att.startid (@startid))) | |
| - att.tuplet.vis (all) | |
| - att.tuplet.ges (att.duration.performed (@dur.ges)) | |
| - att.tuplet.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: bTrem beatRpt fTrem halfmRpt meterSig meterSigGrp | |
| - MEI.critapp: app | |
| - MEI.edittrans: (all) | |
| - MEI.mensural: ligature mensur proport | |
| - MEI.shared: clefGrp custos keySig pad | |
| ''' | |
| # mapping from tag name to our converter function | |
| tagToFunction = { | |
| f'{MEI_NS}tuplet': tupletFromElement, | |
| f'{MEI_NS}beam': beamFromElement, | |
| f'{MEI_NS}note': noteFromElement, | |
| f'{MEI_NS}rest': restFromElement, | |
| f'{MEI_NS}chord': chordFromElement, | |
| f'{MEI_NS}clef': clefFromElement, | |
| f'{MEI_NS}space': spaceFromElement, | |
| f'{MEI_NS}barLine': barLineFromElement, | |
| } | |
| # get the @num and @numbase attributes, without which we can't properly calculate the tuplet | |
| if elem.get('num') is None or elem.get('numbase') is None: | |
| raise MeiAttributeError(_MISSING_TUPLET_DATA) | |
| # iterate all immediate children | |
| tupletMembers = _processEmbeddedElements(elem.findall('*'), tagToFunction, elem.tag, slurBundle) | |
| # "tuplet-ify" the duration of everything held within | |
| newElem = Element('c', m21TupletNum=elem.get('num'), m21TupletNumbase=elem.get('numbase')) | |
| tupletMembers = scaleToTuplet(tupletMembers, newElem) | |
| # Set the Tuplet.type property for the first and final note in a tuplet. | |
| # We have to find the first and last duration-having thing, not just the first and last objects | |
| # between the <tuplet> tags. | |
| firstNote = None | |
| lastNote = None | |
| for i, eachObj in enumerate(tupletMembers): | |
| if firstNote is None and isinstance(eachObj, note.GeneralNote): | |
| firstNote = i | |
| elif isinstance(eachObj, note.GeneralNote): | |
| lastNote = i | |
| tupletMembers[firstNote].duration.tuplets[0].type = 'start' | |
| if lastNote is None: | |
| # when there is only one object in the tuplet | |
| tupletMembers[firstNote].duration.tuplets[0].type = 'stop' | |
| else: | |
| tupletMembers[lastNote].duration.tuplets[0].type = 'stop' | |
| # beam it all together | |
| tupletMembers = beamTogether(tupletMembers) | |
| return tuple(tupletMembers) | |
| def layerFromElement(elem, overrideN=None, slurBundle=None): | |
| ''' | |
| <layer> An independent stream of events on a staff. | |
| In MEI 2013: pg.353 (367 in PDF) (MEI.shared module) | |
| .. note:: The :class:`Voice` object's :attr:`~music21.stream.Voice.id` attribute must be set | |
| properly in order to ensure continuity of voices between measures. If the ``elem`` does not | |
| have an @n attribute, you can set one with the ``overrideN`` parameter in this function. If | |
| you provide a value for ``overrideN``, it will be used instead of the ``elemn`` object's | |
| @n attribute. | |
| Because improperly-set :attr:`~music21.stream.Voice.id` attributes nearly guarantees errors | |
| in the imported :class:`Score`, either ``overrideN`` or @n must be specified. | |
| :param elem: The ``<layer>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :param str overrideN: The value to be set as the ``id`` | |
| attribute in the outputted :class:`Voice`. | |
| :returns: A :class:`Voice` with the objects found in the provided :class:`Element`. | |
| :rtype: :class:`music21.stream.Voice` | |
| :raises: :exc:`MeiAttributeError` if neither ``overrideN`` nor @n are specified. | |
| **Attributes/Elements Implemented:** | |
| - <clef>, <chord>, <note>, <rest>, <mRest>, <beam>, <tuplet>, <space>, <mSpace> , and | |
| <barLine> contained within | |
| - @n, from att.common | |
| **Attributes Ignored:** | |
| - @xml:id | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @xml:base) | |
| - att.declaring (@decls) | |
| - att.facsimile (@facs) | |
| - att.layer.log (@def) and (att.meterconformance (@metcon)) | |
| - att.layer.vis (att.visibility (@visible)) | |
| - att.layer.gesatt.layer.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: arpeg bTrem beamSpan beatRpt bend breath fTrem fermata gliss hairpin halfmRpt | |
| harpPedal mRpt mRpt2 meterSig meterSigGrp multiRest multiRpt octave pedal | |
| reh slur tie tuplet tupletSpan | |
| - MEI.cmnOrnaments: mordent trill turn | |
| - MEI.critapp: app | |
| - MEI.edittrans: (all) | |
| - MEI.harmony: harm | |
| - MEI.lyrics: lyrics | |
| - MEI.mensural: ligature mensur proport | |
| - MEI.midi: midi | |
| - MEI.neumes: ineume syllable uneume | |
| - MEI.shared: accid annot artic barLine clefGrp custos dir dot dynam keySig pad pb phrase sb | |
| scoreDef staffDef tempo | |
| - MEI.text: div | |
| - MEI.usersymbols: anchoredText curve line symbol | |
| ''' | |
| # mapping from tag name to our converter function | |
| tagToFunction = { | |
| f'{MEI_NS}clef': clefFromElement, | |
| f'{MEI_NS}chord': chordFromElement, | |
| f'{MEI_NS}note': noteFromElement, | |
| f'{MEI_NS}rest': restFromElement, | |
| f'{MEI_NS}mRest': mRestFromElement, | |
| f'{MEI_NS}beam': beamFromElement, | |
| f'{MEI_NS}tuplet': tupletFromElement, | |
| f'{MEI_NS}space': spaceFromElement, | |
| f'{MEI_NS}mSpace': mSpaceFromElement, | |
| f'{MEI_NS}barLine': barLineFromElement, | |
| } | |
| # iterate all immediate children | |
| theLayer = _processEmbeddedElements(elem.iterfind('*'), tagToFunction, elem.tag, slurBundle) | |
| # adjust the <layer>'s elements for possible tuplets | |
| theLayer = _guessTuplets(theLayer) | |
| # make the Voice | |
| theVoice = stream.Voice() | |
| for each in theLayer: | |
| theVoice.coreAppend(each) | |
| theVoice.coreElementsChanged() | |
| # try to set the Voice's "id" attribute | |
| if overrideN: | |
| theVoice.id = overrideN | |
| elif elem.get('n') is not None: | |
| theVoice.id = elem.get('n') | |
| else: | |
| raise MeiAttributeError(_MISSING_VOICE_ID) | |
| return theVoice | |
| def staffFromElement(elem, slurBundle=None): | |
| ''' | |
| <staff> A group of equidistant horizontal lines on which notes are placed in order to | |
| represent pitch or a grouping element for individual 'strands' of notes, rests, etc. that may | |
| or may not actually be rendered on staff lines; that is, both diastematic and non-diastematic | |
| signs. | |
| In MEI 2013: pg.444 (458 in PDF) (MEI.shared module) | |
| :param elem: The ``<staff>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :returns: The :class:`Voice` classes corresponding to the ``<layer>`` tags in ``elem``. | |
| :rtype: list of :class:`music21.stream.Voice` | |
| **Attributes/Elements Implemented:** | |
| - <layer> contained within | |
| **Attributes Ignored:** | |
| - @xml:id | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) | |
| - att.declaring (@decls) | |
| - att.facsimile (@facs) | |
| - att.staff.log (@def) (att.meterconformance (@metcon)) | |
| - att.staff.vis (att.visibility (@visible)) | |
| - att.staff.gesatt.staff.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: ossia | |
| - MEI.critapp: app | |
| - MEI.edittrans: (all) | |
| - MEI.shared: annot pb sb scoreDef staffDef | |
| - MEI.text: div | |
| - MEI.usersymbols: anchoredText curve line symbol | |
| ''' | |
| # mapping from tag name to our converter function | |
| layerTagName = f'{MEI_NS}layer' | |
| tagToFunction = {} | |
| layers = [] | |
| # track the @n values given to layerFromElement() | |
| currentNValue = '1' | |
| # iterate all immediate children | |
| for eachTag in elem.iterfind('*'): | |
| if layerTagName == eachTag.tag: | |
| layers.append(layerFromElement(eachTag, currentNValue, slurBundle=slurBundle)) | |
| currentNValue = f'{int(currentNValue) + 1}' # inefficient, but we need a string | |
| elif eachTag.tag in tagToFunction: | |
| # NB: this won't be tested until there's something in tagToFunction | |
| layers.append(tagToFunction[eachTag.tag](eachTag, slurBundle)) | |
| elif eachTag.tag not in _IGNORE_UNPROCESSED: | |
| environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachTag.tag, elem.tag)) | |
| return layers | |
| def _correctMRestDurs(staves, targetLength): | |
| ''' | |
| Helper function for measureFromElement(), not intended to be used elsewhere. It's a separate | |
| function only (1) to reduce duplication, and (2) to improve testability. | |
| Iterate the imported objects of <layer> elements in the <staff> elements in a <measure>, | |
| detecting those with the "m21wasMRest" attribute and setting their duration to "targetLength." | |
| The "staves" argument should be a dictionary where the values are Measure objects with at least | |
| one Voice object inside. | |
| The "targetLength" argument should be the duration of the measure. | |
| Nothing is returned; the duration of affected objects is modified in-place. | |
| ''' | |
| for eachMeasure in staves.values(): | |
| for eachVoice in eachMeasure: | |
| if not isinstance(eachVoice, stream.Stream): | |
| continue | |
| for eachObject in eachVoice: | |
| if hasattr(eachObject, 'm21wasMRest'): | |
| eachObject.quarterLength = targetLength | |
| eachVoice.duration = duration.Duration(targetLength) | |
| eachMeasure.duration = duration.Duration(targetLength) | |
| del eachObject.m21wasMRest | |
| def _makeBarlines(elem, staves): | |
| ''' | |
| This is a helper function for :func:`measureFromElement`, made independent only to improve | |
| that function's ease of testing. | |
| Given a <measure> element and a dictionary with the :class:`Measure` objects that have already | |
| been processed, change the barlines of the :class:`Measure` objects in accordance with the | |
| element's @left and @right attributes. | |
| :param :class:`~xml.etree.ElementTree.Element` elem: The ``<measure>`` tag to process. | |
| :param dict staves: Dictionary where keys are @n attributes and values are corresponding | |
| :class:`~music21.stream.Measure` objects. | |
| :returns: The ``staves`` dictionary with properly-set barlines. | |
| :rtype: dict | |
| ''' | |
| if elem.get('left') is not None: | |
| bars = _barlineFromAttr(elem.get('left')) | |
| if hasattr(bars, '__len__'): | |
| # this means @left was "rptboth" | |
| bars = bars[1] | |
| for eachMeasure in staves.values(): | |
| if isinstance(eachMeasure, stream.Measure): | |
| eachMeasure.leftBarline = deepcopy(bars) | |
| if elem.get('right') is not None: | |
| bars = _barlineFromAttr(elem.get('right')) | |
| if hasattr(bars, '__len__'): | |
| # this means @right was "rptboth" | |
| staves['next @left'] = bars[1] | |
| bars = bars[0] | |
| for eachMeasure in staves.values(): | |
| if isinstance(eachMeasure, stream.Measure): | |
| eachMeasure.rightBarline = deepcopy(bars) | |
| return staves | |
| def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter=None): | |
| ''' | |
| <measure> Unit of musical time consisting of a fixed number of note-values of a given type, as | |
| determined by the prevailing meter, and delimited in musical notation by two bar lines. | |
| In MEI 2013: pg.365 (379 in PDF) (MEI.cmn module) | |
| :param elem: The ``<measure>`` element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :param int backupNum: A fallback value for the resulting | |
| :class:`~music21.stream.Measure` objects' number attribute. | |
| :param expectedNs: A list of the expected @n attributes for the <staff> tags in this <measure>. | |
| If an expected <staff> isn't in the <measure>, it will be created with a full-measure rest. | |
| :type expectedNs: iterable of str | |
| :param activeMeter: The :class:`~music21.meter.TimeSignature` active in this <measure>. This is | |
| used to adjust the duration of an <mRest> that was given without a @dur attribute. | |
| :returns: A dictionary where keys are the @n attributes for <staff> tags found in this | |
| <measure>, and values are :class:`~music21.stream.Measure` objects that should be appended | |
| to the :class:`~music21.stream.Part` instance with the value's @n attributes. | |
| :rtype: dict of :class:`~music21.stream.Measure` | |
| .. note:: When the right barline is set to ``'rptboth'`` in MEI, it requires adjusting the left | |
| barline of the following <measure>. If this happens, the :class:`Repeat` object is assigned | |
| to the ``'next @left'`` key in the returned dictionary. | |
| **Attributes/Elements Implemented:** | |
| - contained elements: <staff> and <staffDef> | |
| - @right and @left (att.measure.log) | |
| - @n (att.common) | |
| **Attributes Ignored:** | |
| - @xml:id (att.id) | |
| - <slur> and <tie> contained within. These spanners will usually be attached to their starting | |
| and ending notes with @xml:id attributes, so it's not necessary to process them when | |
| encountered in a <measure>. Furthermore, because the possibility exists for cross-measure | |
| slurs and ties, we can't guarantee we'll be able to process all spanners until all | |
| spanner-attachable objects are processed. So we manage these tags at a higher level. | |
| **Attributes/Elements in Testing:** none | |
| **Attributes not Implemented:** | |
| - att.common (@label, @xml:base) | |
| - att.declaring (@decls) | |
| - att.facsimile (@facs) | |
| - att.typed (@type, @subtype) | |
| - att.pointing (@xlink:actuate, @xlink:role, @xlink:show, @target, @targettype, @xlink:title) | |
| - att.measure.log (att.meterconformance.bar (@metcon, @control)) | |
| - att.measure.vis (all) | |
| - att.measure.ges (att.timestamp.performed (@tstamp.ges, @tstamp.real)) | |
| - att.measure.anl (all) | |
| **Contained Elements not Implemented:** | |
| - MEI.cmn: arpeg beamSpan bend breath fermata gliss hairpin harpPedal octave ossia pedal reh | |
| tupletSpan | |
| - MEI.cmnOrnaments: mordent trill turn | |
| - MEI.critapp: app | |
| - MEI.edittrans: add choice corr damage del gap handShift orig reg restore sic subst supplied | |
| unclear | |
| - MEI.harmony: harm | |
| - MEI.lyrics: lyrics | |
| - MEI.midi: midi | |
| - MEI.shared: annot dir dynam pb phrase sb tempo | |
| - MEI.text: div | |
| - MEI.usersymbols: anchoredText curve line symbol | |
| ''' | |
| staves = {} | |
| stavesWaiting = {} # for staff-specific objects processed before the corresponding staff | |
| # mapping from tag name to our converter function | |
| staffTag = f'{MEI_NS}staff' | |
| staffDefTag = f'{MEI_NS}staffDef' | |
| # track the bar's duration | |
| maxBarDuration = None | |
| # iterate all immediate children | |
| for eachElem in elem.iterfind('*'): | |
| if staffTag == eachElem.tag: | |
| staves[eachElem.get('n')] = stream.Measure(staffFromElement(eachElem, | |
| slurBundle=slurBundle), | |
| number=int(elem.get('n', backupNum))) | |
| thisBarDuration = staves[eachElem.get('n')].duration.quarterLength | |
| if maxBarDuration is None or maxBarDuration < thisBarDuration: | |
| maxBarDuration = thisBarDuration | |
| elif staffDefTag == eachElem.tag: | |
| whichN = eachElem.get('n') | |
| if whichN is None: | |
| environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<staffDef>', '@n')) | |
| else: | |
| stavesWaiting[whichN] = staffDefFromElement(eachElem, slurBundle) | |
| elif eachElem.tag not in _IGNORE_UNPROCESSED: | |
| environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) | |
| # Process objects from a <staffDef>... | |
| # We must process them now because, if we did it in the loop above, the respective <staff> may | |
| # not be processed before the <staffDef>. | |
| for whichN, eachDict in stavesWaiting.items(): | |
| for eachObj in eachDict.values(): | |
| # We must insert() these objects because a <staffDef> signals its changes for the | |
| # *start* of the <measure> in which it appears. | |
| staves[whichN].insert(0, eachObj) | |
| # create rest-filled measures for expected parts that had no <staff> tag in this <measure> | |
| for eachN in expectedNs: | |
| if eachN not in staves: | |
| restVoice = stream.Voice([note.Rest(quarterLength=maxBarDuration)]) | |
| restVoice.id = '1' | |
| # just in case (e.g., when all the other voices are <mRest>) | |
| restVoice[0].m21wasMRest = True | |
| staves[eachN] = stream.Measure([restVoice], number=int(elem.get('n', backupNum))) | |
| # First search for Rest objects created by an <mRest> element that didn't have @dur set. This | |
| # will only work in cases where not all of the parts are resting. However, it avoids a more | |
| # time-consuming search later. | |
| if (maxBarDuration == _DUR_ATTR_DICT[None] | |
| and activeMeter is not None | |
| and maxBarDuration != activeMeter.barDuration.quarterLength): | |
| # In this case, all the staves have <mRest> elements without a @dur. | |
| _correctMRestDurs(staves, activeMeter.barDuration.quarterLength) | |
| else: | |
| # In this case, some or none of the staves have an <mRest> element without a @dur. | |
| _correctMRestDurs(staves, maxBarDuration) | |
| # assign left and right barlines | |
| staves = _makeBarlines(elem, staves) | |
| return staves | |
| def sectionScoreCore(elem, allPartNs, slurBundle, *, | |
| activeMeter=None, nextMeasureLeft=None, backupMeasureNum=0): | |
| ''' | |
| This function is the "core" of both :func:`sectionFromElement` and :func:`scoreFromElement`, | |
| since both elements are treated quite similarly (though not identically). It's a separate and | |
| shared function to reduce code duplication and | |
| increase ease of testing. It's a "public" function | |
| to help spread the burden of API documentation complexity: while the parameters and return | |
| values are described in this function, the compliance with the MEI Guidelines is described in | |
| both :func:`sectionFromElement` and :func:`scoreFromElement`, as expected. | |
| **Required Parameters** | |
| :param elem: The <section> or <score> element to process. | |
| :type elem: :class:`xml.etree.ElementTree.Element` | |
| :param allPartNs: A list or tuple of the expected @n attributes for the <staff> tags in this | |
| <section>. This tells the function how many parts there are and what @n values they use. | |
| :type allPartNs: iterable of str | |
| :param slurBundle: This :class:`SpannerBundle` holds the :class:`~music21.spanner.Slur` objects | |
| created during pre-processing. The slurs are attached to their respective :class:`Note` and | |
| :class:`Chord` objects as they are processed. | |
| :type slurBundle: :class:`music21.spanner.SpannerBundle` | |
| **Optional Keyword Parameters** | |
| The following parameters are all optional, and must be specified as a keyword argument (i.e., | |
| you specify the parameter name before its value). | |
| :param activeMeter: The :class:`~music21.meter.TimeSignature` active at the start of this | |
| <section> or <score>. This is updated automatically as the music is processed, and the | |
| :class:`TimeSignature` active at the end of the element is returned. | |
| :type activeMeter: :class:`music21.meter.TimeSignature` | |
| :param nextMeasureLeft: The @left attribute to use for the next <measure> element encountered. | |
| This is used for situations where one <measure> element specified a @right attribute that | |
| must be imported by music21 as *both* the right barline of one measure and the left barline | |
| of the following; at the moment this is only @rptboth, which requires a :class:`Repeat` in | |
| both cases. | |
| :type nextMeasureLeft: :class:`music21.bar.Barline` or :class:`music21.bar.Repeat` | |
| :param backupMeasureNum: In case a <measure> element is missing its @n attribute, | |
| :func:`measureFromElement` will use this automatically-incremented number instead. The | |
| ``backupMeasureNum`` corresponding to the final <measure> in this <score> or <section> is | |
| returned from this function. | |
| :type backupMeasureNum: int | |
| :returns: Four-tuple with a dictionary of results, the new value of ``activeMeter``, the new | |
| value of ``nextMeasureLeft``, and the new value of ``backupMeasureNum``. | |
| :rtype: (dict, :class:`~music21.meter.TimeSignature`, :class:`~music21.bar.Barline`, int) | |
| **Return Value** | |
| In short, it's ``parsed``, ``activeMeter``, ``nextMeasureLeft``, ``backupMeasureNum``. | |
| - ``'parsed'`` is a dictionary where the keys are the values in ``allPartNs`` and the values are | |
| a list of all the :class:`Measure` objects in that part, as found in this <section> or | |
| <score>. | |
| - ``'activeMeter'`` is the :class:`~music21.meter.TimeSignature` in effect at the end of this | |
| <section> or <score>. | |
| - ``'nextMeasureLeft'`` is the value that should be | |
| assigned to the :attr:`leftBarline` attribute | |
| of the first :class:`Measure` found in the next <section>. This will almost always be None. | |
| - ``'backupMeasureNum'`` is equal to the ``backupMeasureNum`` argument plus the number of | |
| <measure> elements found in this <score> or <section>. | |
| ''' | |
| # pylint: disable=too-many-nested-blocks | |
| # ^^^ -- was not required at time of contribution | |
| # TODO: replace the returned 4-tuple with a namedtuple | |
| # NOTE: "activeMeter" holds the TimeSignature object that's currently active; it's used in the | |
| # loop below to help determine the proper duration of a full-measure rest. It must persist | |
| # between <section> elements, so it's a parameter for this function. | |
| scoreTag = f'{MEI_NS}score' | |
| sectionTag = f'{MEI_NS}section' | |
| measureTag = f'{MEI_NS}measure' | |
| scoreDefTag = f'{MEI_NS}scoreDef' | |
| staffDefTag = f'{MEI_NS}staffDef' | |
| # hold the music21.stream.Part that we're building | |
| parsed = {n: [] for n in allPartNs} | |
| # hold things that belong in the following "Thing" (either Measure or Section) | |
| inNextThing = {n: [] for n in allPartNs} | |
| for eachElem in elem.iterfind('*'): | |
| # only process <measure> elements if this is a <section> | |
| if measureTag == eachElem.tag and sectionTag == elem.tag: | |
| backupMeasureNum += 1 | |
| # process all the stuff in the <measure> | |
| measureResult = measureFromElement(eachElem, backupMeasureNum, allPartNs, | |
| slurBundle=slurBundle, | |
| activeMeter=activeMeter) | |
| # process and append each part's stuff to the staff | |
| for eachN in allPartNs: | |
| # insert objects specified in the immediately-preceding <scoreDef> | |
| for eachThing in inNextThing[eachN]: | |
| measureResult[eachN].insert(0, eachThing) | |
| inNextThing[eachN] = [] | |
| # if we got a left-side barline from the previous measure, use it | |
| if nextMeasureLeft is not None: | |
| measureResult[eachN].leftBarline = deepcopy(nextMeasureLeft) | |
| # add this Measure to the Part | |
| parsed[eachN].append(measureResult[eachN]) | |
| # if we got a barline for the next <measure> | |
| if 'next @left' in measureResult: | |
| nextMeasureLeft = measureResult['next @left'] | |
| else: | |
| nextMeasureLeft = None | |
| elif scoreDefTag == eachElem.tag: | |
| localResult = scoreDefFromElement(eachElem, slurBundle) | |
| for allPartObject in localResult['all-part objects']: | |
| if isinstance(allPartObject, meter.TimeSignature): | |
| activeMeter = allPartObject | |
| for i, eachN in enumerate(allPartNs): | |
| if i == 0: | |
| to_insert = allPartObject | |
| else: | |
| # a single Music21Object should not exist in multiple parts | |
| to_insert = deepcopy(allPartObject) | |
| inNextThing[eachN].append(to_insert) | |
| for eachN in allPartNs: | |
| if eachN in localResult: | |
| for eachObj in localResult[eachN].values(): | |
| inNextThing[eachN].append(eachObj) | |
| elif staffDefTag == eachElem.tag: | |
| if eachElem.get('n') is not None: | |
| for eachObj in staffDefFromElement(eachElem, slurBundle).values(): | |
| if isinstance(eachObj, meter.TimeSignature): | |
| activeMeter = eachObj | |
| inNextThing[eachElem.get('n')].append(eachObj) | |
| else: | |
| # At the moment, to process this here, we need an @n on the <staffDef>. A document | |
| # may have a still-valid <staffDef> if the <staffDef> has an @xml:id with which | |
| # <staff> elements may refer to it. | |
| environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<staffDef>', '@n')) | |
| elif sectionTag == eachElem.tag: | |
| # NOTE: same as scoreFE() (except the name of "inNextThing") | |
| localParsed, activeMeter, nextMeasureLeft, backupMeasureNum = sectionFromElement( | |
| eachElem, | |
| allPartNs, | |
| activeMeter=activeMeter, | |
| nextMeasureLeft=nextMeasureLeft, | |
| backupMeasureNum=backupMeasureNum, | |
| slurBundle=slurBundle) | |
| for eachN, eachList in localParsed.items(): | |
| # NOTE: "eachList" is a list of objects that will become a music21 Part. | |
| # | |
| # first: if there were objects from a previous <scoreDef> or <staffDef>, we need to | |
| # put those into the first Measure object we encounter in this Part | |
| # TODO: this is where the Instruments get added | |
| # TODO: I think "eachList" really means "each list that will become a Part" | |
| if inNextThing[eachN]: | |
| # we have to put Instrument objects just before the Measure to which they apply | |
| theInstr = None | |
| theInstrI = None | |
| for i, eachInsertion in enumerate(inNextThing[eachN]): | |
| if isinstance(eachInsertion, instrument.Instrument): | |
| theInstr = eachInsertion | |
| theInstrI = i | |
| break | |
| # Put the Instrument right in front, then remove it from "inNextThing" so it | |
| # doesn't show up twice. | |
| if theInstr: | |
| eachList.insert(0, theInstr) | |
| del inNextThing[eachN][theInstrI] | |
| for eachObj in eachList: | |
| # NOTE: "eachObj" is one of the things that will be in the Part, which are | |
| # probably but not necessarily Measures | |
| if isinstance(eachObj, stream.Stream): | |
| # NOTE: ... but now eachObj is virtually guaranteed to be a Measure | |
| for eachInsertion in inNextThing[eachN]: | |
| eachObj.insert(0.0, eachInsertion) | |
| break | |
| inNextThing[eachN] = [] | |
| # Then we can append the objects in this Part to the dict of all parsed objects, but | |
| # NOTE that this is different for <section> and <score>. | |
| if sectionTag == elem.tag: | |
| # If this is a <section>, which would really be nested <section> elements, we | |
| # must "flatten" everything so it doesn't cause a disaster when we try to make | |
| # a Part out of it. | |
| for eachObj in eachList: | |
| parsed[eachN].append(eachObj) | |
| elif scoreTag == elem.tag: | |
| # If this is a <score>, we can just append the result of each <section> to the | |
| # list that will become the Part. | |
| parsed[eachN].append(eachList) | |
| elif eachElem.tag not in _IGNORE_UNPROCESSED: | |
| environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) | |
| # TODO: write the <section @label=""> part | |
| # TODO: check if there's anything left in "inNextThing" | |
| return parsed, activeMeter, nextMeasureLeft, backupMeasureNum | |
| def sectionFromElement(elem, allPartNs, activeMeter, nextMeasureLeft, backupMeasureNum, slurBundle): | |
| ''' | |
| <section> Segment of music data. | |
| In MEI 2013: pg.432 (446 in PDF) (MEI.shared module) | |
| .. note:: The parameters and return values are exactly the same for :func:`sectionFromElement` | |
| and :func:`sectionScoreCore`, so refer to the latter function's documentation for more | |
| information. | |
| **Attributes/Elements Implemented:** | |
| **Attributes Ignored:** | |
| **Attributes/Elements in Testing:** | |
| - @label | |
| - contained <measure>, <scoreDef>, <staffDef>, <section> | |
| **Attributes not Implemented:** | |
| - att.common (@n, @xml:base) (att.id (@xml:id)) | |
| - att.declaring (@decls) | |
| - att.facsimile (@facs) | |
| - att.typed (@type, @subtype) | |
| - att.pointing (@xlink:actuate, @xlink:role, @xlink:show, @target, @targettype, @xlink:title) | |
| - att.section.vis (@restart) | |
| - att.section.anl (att.common.anl (@copyof, @corresp, @next, @prev, @sameas, @synch) | |
| (att.alignment (@when))) | |
| **Contained Elements not Implemented:** | |
| - MEI.critapp: app | |
| - MEI.edittrans: add choice corr damage del gap handShift orig reg | |
| restore sic subst supplied unclear | |
| - MEI.shared: annot ending expansion pb sb section staff | |
| - MEI.text: div | |
| - MEI.usersymbols: anchoredText curve line symbol | |
| ''' | |
| environLocal.printDebug('*** processing a <section>') | |
| return sectionScoreCore(elem, | |
| allPartNs, | |
| slurBundle, | |
| activeMeter=activeMeter, | |
| nextMeasureLeft=nextMeasureLeft, | |
| backupMeasureNum=backupMeasureNum) | |
| def scoreFromElement(elem, slurBundle): | |
| ''' | |
| <score> Full score view of the musical content. | |
| In MEI 2013: pg.430 (444 in PDF) (MEI.shared module) | |
| :param elem: The <score> element to process. | |
| :type elem: :class:`~xml.etree.ElementTree.Element` | |
| :param slurBundle: This :class:`SpannerBundle` holds the :class:`~music21.spanner.Slur` objects | |
| created during pre-processing. The slurs are attached to their respective :class:`Note` and | |
| :class:`Chord` objects as they are processed. | |
| :type slurBundle: :class:`music21.spanner.SpannerBundle` | |
| :returns: A completed :class:`~music21.stream.Score` object. | |
| **Attributes/Elements Implemented:** | |
| **Attributes Ignored:** | |
| **Attributes/Elements in Testing:** | |
| - contained <section>, <scoreDef>, and <staffDef> | |
| **Attributes not Implemented:** | |
| - att.common (@label, @n, @xml:base) (att.id (@xml:id)) | |
| - att.declaring (@decls) | |
| - att.typed (@type, @subtype) | |
| - att.score.anl (att.common.anl (@copyof, @corresp, @next, @prev, @sameas, @synch) | |
| (att.alignment (@when))) | |
| **Contained Elements not Implemented:** | |
| - MEI.critapp: app | |
| - MEI.edittrans: add choice corr damage del gap handShift orig | |
| reg restore sic subst supplied unclear | |
| - MEI.shared: annot ending pb sb | |
| - MEI.text: div | |
| - MEI.usersymbols: anchoredText curve line symbol | |
| ''' | |
| environLocal.printDebug('*** processing a <score>') | |
| # That's an outright lie. We're also processing <scoreDef>, <staffDef>, and other elements! | |
| # Get a tuple of all the @n attributes for the <staff> tags in this score. Each <staff> tag | |
| # corresponds to what will be a music21 Part. | |
| allPartNs = allPartsPresent(elem) | |
| # This is the actual processing. | |
| parsed = sectionScoreCore(elem, allPartNs, slurBundle=slurBundle)[0] | |
| # Convert the dict to a Score | |
| # We must iterate here over "allPartNs," which preserves the part-order found in the MEI | |
| # document. Iterating the keys in "parsed" would not preserve the order. | |
| environLocal.printDebug('*** making the Score') | |
| theScore = [stream.Part() for _ in range(len(allPartNs))] | |
| for i, eachN in enumerate(allPartNs): | |
| # set "atSoundingPitch" so transposition works | |
| theScore[i].atSoundingPitch = False | |
| for eachObj in parsed[eachN]: | |
| theScore[i].append(eachObj) | |
| theScore = stream.Score(theScore) | |
| # put slurs in the Score | |
| theScore.append(list(slurBundle)) | |
| # TODO: when all the Slur objects are at the end, they'll only be outputted properly if the | |
| # whole Score is outputted. show()-ing one Part or Measure won't display the slurs. | |
| return theScore | |
| # ----------------------------------------------------------------------------- | |
| _DOC_ORDER = [ | |
| accidFromElement, | |
| articFromElement, | |
| beamFromElement, | |
| chordFromElement, | |
| clefFromElement, | |
| dotFromElement, | |
| instrDefFromElement, | |
| layerFromElement, | |
| measureFromElement, | |
| noteFromElement, | |
| spaceFromElement, | |
| mSpaceFromElement, | |
| restFromElement, | |
| mRestFromElement, | |
| scoreFromElement, | |
| sectionFromElement, | |
| scoreDefFromElement, | |
| staffFromElement, | |
| staffDefFromElement, | |
| staffGrpFromElement, | |
| tupletFromElement, | |
| ] | |
| if __name__ == '__main__': | |
| import music21 | |
| music21.mainTest() |