Source code for partitura.io.importmei

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module contains methods for importing MEI files.
"""
from lxml import etree
from xmlschema.names import XML_NAMESPACE
import partitura.score as score
from partitura.utils.music import (
    MEI_DURS_TO_SYMBOLIC,
    SYMBOLIC_TO_INT_DURS,
    SIGN_TO_ALTER,
    estimate_symbolic_duration,
)
from partitura.utils import PathLike, get_document_name
from partitura.utils.misc import deprecated_alias
import partitura as pt

try:
    import verovio

    VEROVIO_AVAILABLE = True
except:
    VEROVIO_AVAILABLE = False

import re
import warnings

import numpy as np


[docs]@deprecated_alias(mei_path="filename") def load_mei(filename: PathLike) -> score.Score: """ Loads a Mei score from path and returns a partitura Score object. Parameters ---------- filename : PathLike The path to an MEI score. Returns ------- scr: :class:`partitura.score.Score` A `Score` object """ parser = MeiParser(filename) doc_name = get_document_name(filename) # create parts from the specifications in the mei parser.create_parts() # fill parts with the content from the mei parser.fill_parts() # TODO: Parse score info (composer, lyricist, etc.) scr = score.Score( id=doc_name, partlist=parser.parts, ) return scr
class MeiParser(object): def __init__(self, mei_path: PathLike) -> None: document, ns = self._parse_mei(mei_path, use_verovio=VEROVIO_AVAILABLE) self.document = document self.ns = ns # the namespace in the MEI file self.parts = ( None # parts get initialized in create_parts() and filled in fill_parts() ) self.repetitions = ( [] ) # to be filled when we encounter repetitions and process in the end self.barlines = ( [] ) # to be filled when we encounter barlines and process in the end self.endings = [] def create_parts(self): # handle main scoreDef info: create the part list main_partgroup_el = self.document.find(self._ns_name("staffGrp", all=True)) self.parts = self._handle_main_staff_group(main_partgroup_el) def fill_parts(self): # fill parts with the content of the score scores_el = self.document.findall(self._ns_name("score", all=True)) if len(scores_el) != 1: raise Exception("Only MEI with a single score element are supported") sections_el = scores_el[0].findall(self._ns_name("section")) position = 0 for section_el in sections_el: # insert in parts all elements except ties position = self._handle_section( section_el, list(score.iter_parts(self.parts)), position ) # handles ties self._tie_notes(scores_el[0], self.parts) # handle repetitions self._insert_repetitions() # handle barlines self._insert_barlines() # -------------- Functions to initialize the xml tree ----------------- def _ns_name(self, name, ns=None, all=False): """ Combines document namespace tag with element to fetch object from MEI lxml trees. Parameters ---------- name : str Name of MEI element. ns : str or None The namespace tag of the document. Default to None. all : bool if True, search the entire subtree, otherwise only the first level. """ if ns is None: ns = self.ns if not all: return "{" + ns + "}" + name else: return ".//{" + ns + "}" + name def _parse_mei(self, mei_path, use_verovio=True): """ Parses an MEI file from path to an lxml tree. Parameters ---------- mei_path : str The path of the MEI document. Returns ------- document : lxml tree An lxml tree of the MEI score. """ parser = etree.XMLParser( resolve_entities=False, huge_tree=False, remove_comments=True, remove_blank_text=True, recover=True, ) if use_verovio: tk = verovio.toolkit(True) tk.loadFile(mei_path) mei_score = tk.getMEI("basic") # document = etree.parse(mei_score, parser) root = etree.fromstring(mei_score.encode("utf-8"), parser) tree = etree.ElementTree(root) else: tree = etree.parse(mei_path, parser) root = tree.getroot() # find the namespace ns = root.nsmap[None] # --> nsmap fetches a dict of the namespace Map, generally for root the key `None` fetches the namespace of the document. return tree, ns # functions to parse staves info def _handle_metersig(self, staffdef_el, position, part): """ Handles meter signature and adds to part. Parameters ---------- staffdef_el : lxml etree A lxml substree of a staff's mei score. position : int Is the current position of the note on the timeline. part : particular.Part The created Partitura Part object. """ metersig_el = staffdef_el.find(self._ns_name("meterSig")) if metersig_el is not None: # new element inside numerator = int(metersig_el.attrib["count"]) denominator = int(metersig_el.attrib["unit"]) elif ( staffdef_el.get("meter.count") is not None ): # all encoded as attributes in staffdef numerator = int(staffdef_el.attrib["meter.count"]) denominator = int(staffdef_el.attrib["meter.unit"]) else: # the informatio is encoded in a parent scoredef found_ancestor_with_metrical_info = False for anc in staffdef_el.iterancestors(tag=self._ns_name("scoreDef")): if anc.get("meter.count") is not None: found_ancestor_with_metrical_info = True break if found_ancestor_with_metrical_info: numerator = int(anc.attrib["meter.count"]) denominator = int(anc.attrib["meter.unit"]) else: raise Exception( f"The time signature is not encoded in {staffdef_el.get(self._ns_name('id'))} or in any ancestor scoreDef" ) new_time_signature = score.TimeSignature(numerator, denominator) part.add(new_time_signature, position) def _handle_keysig(self, staffdef_el, position, part): """ Handles key signature and adds to part. Parameters ---------- staffdef_el : lxml tree A lxml substree of a staff's mei score. position : int Is the current position of the note on the timeline. part : particular.Part The created Partitura Part object. """ keysig_el = staffdef_el.find(self._ns_name("keySig")) if keysig_el is not None: # new element inside sig = keysig_el.attrib["sig"] # now extract partitura keysig parameters fifths = self._mei_sig_to_fifths(sig) mode = keysig_el.get("mode") elif ( staffdef_el.get("key.sig") is not None ): # all encoded as attributes in staffdef sig = staffdef_el.attrib["key.sig"] # now extract partitura keysig parameters fifths = self._mei_sig_to_fifths(sig) mode = staffdef_el.get("key.mode") else: # the information is encoded in a parent scoredef found_ancestor_with_key_info = False for anc in staffdef_el.iterancestors(tag=self._ns_name("scoreDef")): if anc.get("key.sig") is not None: found_ancestor_with_key_info = True break if found_ancestor_with_key_info: sig = anc.attrib["key.sig"] # now extract partitura keysig parameters fifths = self._mei_sig_to_fifths(sig) mode = anc.get("key.mode") else: warnings.warn( f"The key signature is not encoded in {staffdef_el.get(self._ns_name('id'))} or in any ancestor scoreDef." ) warnings.warn("A default key signature of C maj is set.") fifths = 0 mode = "major" new_key_signature = score.KeySignature(fifths, mode) part.add(new_key_signature, position) def _compute_clef_octave(self, dis, dis_place): if dis is not None: sign = -1 if dis_place == "below" else 1 octave = sign * int(int(dis) / 8) else: octave = 0 return octave def _mei_sig_to_fifths(self, sig): """Produces partitura KeySignature.fifths parameter from the MEI sig attribute.""" if sig[0] == "0": fifths = 0 else: sign = 1 if sig[-1] == "s" else -1 fifths = sign * int(sig[:-1]) return fifths def _handle_clef(self, element, position, part): """Inserts a clef. Element can be either a cleff element or staffdef element. Parameters ---------- staffdef_el : lxml tree A lxml substree of a mei score. position : int Is the current position of the note on the timeline. part : particular.Part The created Partitura Part object. Returns ------- position : int The current position of the note on the timeline. """ # handle the case where we have clef informations inside staffdef el if element.tag == self._ns_name("staffDef"): clef_el = element.find(self._ns_name("clef")) if clef_el is not None: # if there is a clef element inside return self._handle_clef(clef_el, position, part) else: # if all info are in the staffdef element number = element.get("n") sign = element.get("clef.shape") line = element.get("clef.line") if ( number is not None and sign is not None and line is not None ): # if there is clef info octave = self._compute_clef_octave( element.get("dis"), element.get("dis.place") ) else: # no clef info available, go for default warnings.warn("No clef information found, setting G2 as default.") sign = "G" line = 2 number = 1 octave = 0 elif element.tag == self._ns_name("clef"): if element.get("sameas") is not None: # this is a copy of another clef # it seems this is used in different layers for the same staff # we don't handle it to avoid clef duplications return position else: # find the staff number parent = element.getparent() if parent.tag == self._ns_name("staffDef"): # number = parent.attrib["n"] number = 1 else: # go back another level to staff element # number = parent.getparent().attrib["n"] number = 1 sign = element.attrib["shape"] line = element.attrib["line"] octave = self._compute_clef_octave( element.get("dis"), element.get("dis.place") ) else: raise Exception("_handle_clef only accepts staffDef or clef elements") new_clef = score.Clef(int(number), sign, int(line), octave) part.add(new_clef, position) return position def _handle_staffdef(self, staffdef_el, position, part): """ Derives meter, key and clef from lxml substree and pass them to part. Parameters ---------- staffdef_el : lxml tree A lxml substree of a mei score. position : int Is the current position of the note on the timeline. part : particular.Part The created Partitura Part object. """ # fill with time signature info self._handle_metersig(staffdef_el, position, part) # fill with key signature info self._handle_keysig(staffdef_el, position, part) # fill with clef info self._handle_clef(staffdef_el, position, part) def _intsymdur_from_symbolic(self, symbolic_dur): """Produce a int symbolic dur (e.g. 12 is a eight note triplet) and a dot number by looking at the symbolic dur dictionary: i.e., symbol, eventual tuplet ancestors.""" intsymdur = SYMBOLIC_TO_INT_DURS[symbolic_dur["type"]] # deals with tuplets if symbolic_dur.get("actual_notes") is not None: assert symbolic_dur.get("normal_notes") is not None intsymdur = ( intsymdur * symbolic_dur["actual_notes"] / symbolic_dur["normal_notes"] ) # deals with dots dots = symbolic_dur.get("dots") if symbolic_dur.get("dots") is not None else 0 return intsymdur, dots def _find_ppq(self): """Finds the ppq for MEI filed that do not explicitely encode this information""" els_with_dur = self.document.xpath(".//*[@dur]") durs = [] durs_ppq = [] for el in els_with_dur: symbolic_duration = self._get_symbolic_duration(el) intsymdur, dots = self._intsymdur_from_symbolic(symbolic_duration) # double the value if we have dots, to be sure be able to encode that with integers in partitura durs.append(intsymdur * (2**dots)) durs_ppq.append( None if el.get("dur.ppq") is None else int(el.get("dur.ppq")) ) if any([dppq is not None for dppq in durs_ppq]): # there is at least one element with both dur and dur.ppq for dur, dppq in zip(durs, durs_ppq): if dppq is not None: return dppq * dur / 4 else: # compute the ppq from the durations # add 4 to be sure to not go under 1 ppq durs.append(4) durs = np.array(durs) # remove elements smaller than 1 durs = durs[durs >= 1] least_common_multiple = np.lcm.reduce(durs.astype(int)) return least_common_multiple / 4 def _handle_initial_staffdef(self, staffdef_el): """ Handles the definition of a single staff. Parameters ---------- staffdef_el : Element tree A subtree of a particular Staff from a score. Returns ------- part : partitura.Part Returns a partitura part filled with meter, time signature, key signature information. """ # Fetch the namespace of the staff. id = staffdef_el.attrib[self._ns_name("id", XML_NAMESPACE)] label_el = staffdef_el.find(self._ns_name("label")) name = label_el.text if label_el is not None else "" ppq_attrib = staffdef_el.get("ppq") if ppq_attrib is not None: ppq = int(ppq_attrib) else: ppq = self._find_ppq() # generate the part part = score.Part(id, name, quarter_duration=ppq) # fill it with other info, e.g. meter, time signature, key signature self._handle_staffdef(staffdef_el, 0, part) return part def _handle_staffgroup(self, staffgroup_el): """ Handles a staffGrp. WARNING: in MEI piano staves are a staffGrp Parameters ---------- staffgroup_el : element tree A subtree of Staff Group from a score. Returns ------- staff_group : Partitura.PartGroup A partitura PartGroup object made by calling and appending as children ever staff separately. """ group_symbol_el = staffgroup_el.find(self._ns_name("grpSym")) if group_symbol_el is None: group_symbol = staffgroup_el.attrib["symbol"] else: group_symbol = group_symbol_el.attrib["symbol"] label_el = staffgroup_el.find(self._ns_name("label")) name = label_el.text if label_el is not None else None id = staffgroup_el.attrib[self._ns_name("id", XML_NAMESPACE)] staff_group = score.PartGroup(group_symbol, group_name=name, id=id) staves_el = staffgroup_el.findall(self._ns_name("staffDef")) for s_el in staves_el: new_part = self._handle_initial_staffdef(s_el) staff_group.children.append(new_part) staff_groups_el = staffgroup_el.findall(self._ns_name("staffGrp")) for sg_el in staff_groups_el: new_staffgroup = self._handle_staffgroup(sg_el) staff_group.children.append(new_staffgroup) return staff_group def _handle_main_staff_group(self, main_staffgrp_el): """ Handles the main staffGrp that contains all other staves or staff groups. Parameters ---------- main_staffgrp_el : element_tree Returns ------- part_list : list Created list of parts filled with key and time signature information. """ staves_el = main_staffgrp_el.findall(self._ns_name("staffDef")) staff_groups_el = main_staffgrp_el.findall(self._ns_name("staffGrp")) # the list of parts or part groups part_list = [] # process the parts # TODO add Parallelization to handle part parsing in parallel for s_el in staves_el: new_part = self._handle_initial_staffdef(s_el) part_list.append(new_part) # process the part groups for sg_el in staff_groups_el: new_staffgroup = self._handle_staffgroup(sg_el) part_list.append(new_staffgroup) return part_list # functions to parse the content of parts def _note_el_to_accid_int(self, note_el) -> int: """Accidental strings to integer pitch. It consider the two values of accid and accid.ges (when the accidental is implicit in the bar) """ if note_el.get("accid") is not None: return SIGN_TO_ALTER[note_el.get("accid")] elif note_el.get("accid.ges") is not None: return SIGN_TO_ALTER[note_el.get("accid.ges")] elif note_el.find(self._ns_name("accid")) is not None: if note_el.find(self._ns_name("accid")).get("accid") is not None: return SIGN_TO_ALTER[note_el.find(self._ns_name("accid")).get("accid")] else: return SIGN_TO_ALTER[ note_el.find(self._ns_name("accid")).get("accid.ges") ] else: return None def _pitch_info(self, note_el): """ Given a note element fetches PitchClassName, octave and accidental. Parameters ---------- note_el Returns ------- step : str The note Pitch class name. octave : int The number of octave alter : int Accidental string transformed to number. """ step = note_el.attrib["pname"] octave = int(note_el.attrib["oct"]) # accidentals can be accid, accid.ges or accid children elements alter = self._note_el_to_accid_int(note_el) return step, octave, alter def _get_symbolic_duration(self, el): symbolic_duration = {} symbolic_duration["type"] = MEI_DURS_TO_SYMBOLIC[el.attrib["dur"]] if not el.get("dots") is None: symbolic_duration["dots"] = int(el.get("dots")) # find eventual time modifications tuplet_ancestors = list(el.iterancestors(tag=self._ns_name("tuplet"))) if len(tuplet_ancestors) == 0: pass elif len(tuplet_ancestors) == 1: symbolic_duration["actual_notes"] = int(tuplet_ancestors[0].attrib["num"]) symbolic_duration["normal_notes"] = int( tuplet_ancestors[0].attrib["numbase"] ) else: raise Exception("Nested tuplets are not yet supported.") return symbolic_duration def _duration_info(self, el, part): """ Extract duration info from a xml element. It works for example with note_el, chord_el Parameters ---------- el : lxml tree the xml element to analyze part : partitura.Part The created partitura part object. Returns ------- id : duration : symbolic_duration : """ # symbolic duration symbolic_duration = self._get_symbolic_duration(el) # duration in ppq if el.get("dur.ppq") is not None or el.get("grace") is not None: # find duration in ppq. For grace notes is 0 duration = 0 if el.get("grace") is not None else int(el.get("dur.ppq")) else: # compute the duration from the symbolic duration intsymdur, dots = self._intsymdur_from_symbolic(symbolic_duration) divs = part._quarter_durations[0] # divs is the same as ppq duration = divs * 4 / intsymdur for d in range(dots): duration = duration + 0.5 * duration # sanity check to verify the divs are correctly set assert duration == int(duration) # find id id = el.attrib[self._ns_name("id", XML_NAMESPACE)] return id, int(duration), symbolic_duration def _handle_note(self, note_el, position, voice, staff, part) -> int: """ Handles note elements and imports the to part. Parameters ---------- note_el : lxml substree The lxml substree of a note element. position : int The current position on the timeline. voice : int The currect voice index. staff : int The current staff index. part : partitura.Part The created partitura part object. Returns ------- position + duration : into The updated position on the timeline. """ # find pitch info step, octave, alter = self._pitch_info(note_el) # find duration info note_id, duration, symbolic_duration = self._duration_info(note_el, part) # find if it's grace grace_attr = note_el.get("grace") if grace_attr is None: # create normal note note = score.Note( step=step, octave=octave, alter=alter, id=note_id, voice=voice, staff=1, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) else: # create grace note if grace_attr == "unacc": grace_type = "acciaccatura" elif grace_attr == "acc": grace_type = "appoggiatura" else: # unknow type grace_type = "grace" note = score.GraceNote( grace_type=grace_type, step=step, octave=octave, alter=alter, id=note_id, voice=voice, staff=1, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) # add note to the part part.add(note, position, position + duration) # return duration to update the position in the layer return position + duration def _handle_rest(self, rest_el, position, voice, staff, part): """ Handles the rest element updates part and position. Parameters ---------- rest_el : lxml tree A rest element in the lxml tree. position : int The current position on the timeline. voice : int The voice of the section. staff : int The current staff also refers to a Part. part : Partitura.Part The created part to add elements to. Returns ------- position + duration : int Next position on the timeline. Also adds the rest to the partitura part object. """ # find duration info rest_id, duration, symbolic_duration = self._duration_info(rest_el, part) # create rest rest = score.Rest( id=rest_id, voice=voice, staff=1, symbolic_duration=symbolic_duration, articulations=None, ) # add rest to the part part.add(rest, position, position + duration) # return duration to update the position in the layer return position + duration def _handle_mrest(self, mrest_el, position, voice, staff, part): """ Handles a rest that spawn the entire measure Parameters ---------- mrest_el : lxml tree A mrest element in the lxml tree. position : int The current position on the timeline. voice : int The voice of the section. staff : int The current staff also refers to a Part. part : Partitura.Part The created part to add elements to. Returns ------- position + duration : int Next position on the timeline. """ # find id mrest_id = mrest_el.attrib[self._ns_name("id", XML_NAMESPACE)] # find closest time signature last_ts = list(part.iter_all(cls=score.TimeSignature))[-1] # find divs per measure ppq = part.quarter_duration_map(position) parts_per_measure = int(ppq * 4 * last_ts.beats / last_ts.beat_type) # create dummy rest to insert in the timeline rest = score.Rest( id=mrest_id, voice=voice, staff=1, symbolic_duration=estimate_symbolic_duration(parts_per_measure, ppq), articulations=None, ) # add mrest to the part part.add(rest, position, position + parts_per_measure) # now iterate # return duration to update the position in the layer return position + parts_per_measure def _handle_chord(self, chord_el, position, voice, staff, part): """ Handles a rest that spawn the entire measure Parameters ---------- chord_el : lxml tree A chord element in the lxml tree. position : int The current position on the timeline. voice : int The voice of the section. staff : int The current staff also refers to a Part. part : Partitura.Part The created part to add elements to. Returns ------- position + duration : int Next position on the timeline. """ # find duration info chord_id, duration, symbolic_duration = self._duration_info(chord_el, part) # find notes info notes_el = chord_el.findall(self._ns_name("note")) for note_el in notes_el: note_id = note_el.attrib[self._ns_name("id", XML_NAMESPACE)] # find pitch info step, octave, alter = self._pitch_info(note_el) # create note note = score.Note( step=step, octave=octave, alter=alter, id=note_id, voice=voice, staff=1, symbolic_duration=symbolic_duration, articulations=None, # TODO : add articulation ) # add note to the part part.add(note, position, position + duration) # return duration to update the position in the layer return position + duration def _handle_space(self, e, position, part): """Moves current position.""" space_id, duration, symbolic_duration = self._duration_info(e, part) return position + duration def _handle_barline_symbols(self, measure_el, position: int, left_or_right: str): barline = measure_el.get(left_or_right) if barline is not None: if barline == "rptstart": self.repetitions.append({"type": "start", "pos": position}) self.barlines.append({"type": "heavy-light", "pos": position}) elif barline == "rptend": self.repetitions.append({"type": "stop", "pos": position}) self.barlines.append({"type": "light-heavy", "pos": position}) elif barline == "dbl": self.barlines.append({"type": "light-light", "pos": position}) elif barline == "end": self.barlines.append({"type": "light-heavy", "pos": position}) elif barline == "dashed": self.barlines.append({"type": "dashed", "pos": position}) else: print( f"{barline} in measure {measure_el.attrib[self._ns_name('id', XML_NAMESPACE)]} is a non supported barline type." ) def _handle_layer_in_staff_in_measure( self, layer_el, ind_layer: int, ind_staff: int, position: int, part ) -> int: for i, e in enumerate(layer_el): if e.tag == self._ns_name("note"): new_position = self._handle_note( e, position, ind_layer, ind_staff, part ) elif e.tag == self._ns_name("chord"): new_position = self._handle_chord( e, position, ind_layer, ind_staff, part ) elif e.tag == self._ns_name("rest"): new_position = self._handle_rest( e, position, ind_layer, ind_staff, part ) elif e.tag == self._ns_name("mRest"): # rest that spawn the entire measure new_position = self._handle_mrest( e, position, ind_layer, ind_staff, part ) elif e.tag == self._ns_name("beam"): # TODO : add Beam element # recursive call to the elements inside beam new_position = self._handle_layer_in_staff_in_measure( e, ind_layer, ind_staff, position, part ) elif e.tag == self._ns_name("tuplet"): # TODO : add Tuplet element # recursive call to the elements inside Tuplet new_position = self._handle_layer_in_staff_in_measure( e, ind_layer, ind_staff, position, part ) elif e.tag == self._ns_name("clef"): new_position = self._handle_clef(e, position, part) elif e.tag == self._ns_name("space"): new_position = self._handle_space(e, position, part) else: raise Exception("Tag " + e.tag + " not supported") # update the current position position = new_position return position def _handle_staff_in_measure(self, staff_el, staff_ind, position: int, part): """ Handles staffs inside a measure element. Parameters ---------- staff_el : lxml etree The lxml subtree for a staff element. staff_ind : int The Staff index. position : int The current position on the timeline. part : Partitura.Part The created partitura part object. Returns ------- end_positions[0] : int The final position on the timeline. """ # add measure measure = score.Measure(number=staff_el.getparent().get("n")) part.add(measure, position) layers_el = staff_el.findall(self._ns_name("layer")) end_positions = [] for i_layer, layer_el in enumerate(layers_el): end_positions.append( self._handle_layer_in_staff_in_measure( layer_el, i_layer + 1, staff_ind, position, part ) ) # check if layers have equal duration (bad encoding, but it often happens) if not all([e == end_positions[0] for e in end_positions]): warnings.warn( f"Warning: voices have different durations in staff {staff_el.attrib[self._ns_name('id',XML_NAMESPACE)]}" ) if ( len(end_positions) == 0 ): # if a measure contains no elements (e.g., a forgotten rest) end_positions.append(position) # add end time of measure part.add(measure, None, max(end_positions)) return max(end_positions) def _find_dir_positions(self, dir_el, bar_position): """Compute the position for a <dir> element. Returns an array, one position for each part.""" delta_position_beat = float(dir_el.get("tstamp")) return [ p.inv_beat_map(p.beat_map(bar_position) + delta_position_beat - 1) for p in score.iter_parts(self.parts) ] def _add_in_all_parts(self, tobj, starts): for part, start in zip(score.iter_parts(self.parts), starts): part.add(tobj, start) def _handle_dir_element(self, dir_el, position): # find the kind of element kind = dir_el.get("type") if kind is None: return dir_pos = self._find_dir_positions(dir_el, position) if kind == "fine": self._add_in_all_parts(score.Fine(), dir_pos) elif kind == "dacapo": self._add_in_all_parts(score.DaCapo(), dir_pos) def _handle_directives(self, measure_el, position): dir_els = measure_el.findall(self._ns_name("dir")) for dir_el in dir_els: self._handle_dir_element(dir_el, position) def _handle_section(self, section_el, parts, position: int): """ Returns position and fills parts with elements. Parameters ---------- section_el : lxml tree An lxml substree of a MEI score reffering to a section. parts : list() A list of partitura Parts. position : int The current position on the timeline. Returns ------- position : int The end position of the section. """ for i_el, element in enumerate(section_el): # handle measures if element.tag == self._ns_name("measure"): # handle left barline symbols self._handle_barline_symbols(element, position, "left") # handle staves staves_el = element.findall(self._ns_name("staff")) if len(list(staves_el)) != len(list(parts)): raise Exception(f"Not all parts are specified in measure {i_el}") end_positions = [] for i_s, (part, staff_el) in enumerate(zip(parts, staves_el)): end_positions.append( self._handle_staff_in_measure(staff_el, i_s + 1, position, part) ) # handle directives (dir elements) self._handle_directives(element, position) # sanity check that all layers have equal duration max_position = max(end_positions) if not all([e == max_position for e in end_positions]): warnings.warn( f"Warning : parts have measures of different duration in measure {element.attrib[self._ns_name('id',XML_NAMESPACE)]}" ) # enlarge measures to the max for part in parts: last_measure = list(part.iter_all(pt.score.Measure))[-1] if last_measure.end.t != max_position: part.add( pt.score.Measure(number=last_measure.number), position, max_position, ) part.remove(last_measure) position = max_position # handle right barline symbol self._handle_barline_symbols(element, position, "right") # handle staffDef elements elif element.tag == self._ns_name("scoreDef"): # meter modifications metersig_el = element.find(self._ns_name("meterSig")) if (metersig_el is not None) or ( element.get("meter.count") is not None ): for part in parts: self._handle_metersig(element, position, part) # key signature modifications keysig_el = element.find(self._ns_name("keySig")) if (keysig_el is not None) or (element.get("key.sig") is not None): for part in parts: self._handle_keysig(element, position, part) # handle nested section elif element.tag == self._ns_name("section"): position = self._handle_section(element, parts, position) elif element.tag == self._ns_name("ending"): ending_start = position position = self._handle_section(element, parts, position) # insert the ending element ending_number = int(re.sub("[^0-9]", "", element.attrib["n"])) self._add_ending(ending_start, position, ending_number, parts) # explicit repetition expansions elif element.tag == self._ns_name("expansion"): pass # system break elif element.tag == self._ns_name("sb"): pass # page break elif element.tag == self._ns_name("pb"): pass else: raise Exception(f"element {element.tag} is not yet supported") return position def _add_ending(self, start_ending, end_ending, ending_string, parts): for part in score.iter_parts(parts): part.add(score.Ending(ending_string), start_ending, end_ending) def _tie_notes(self, section_el, part_list): """Ties all notes in a part. This function must be run after the parts are completely created.""" # TODO : support ties written as attributes with @tie sintax ties_el = section_el.findall(self._ns_name("tie", all=True)) # create a dict of id : note, to speed up search all_notes = [ note for part in score.iter_parts(part_list) for note in part.iter_all(cls=score.Note) ] all_notes_dict = {note.id: note for note in all_notes} for tie_el in ties_el: start_id = tie_el.get("startid") end_id = tie_el.get("endid") if start_id is None or end_id is None: warnings.warn( f"Warning: tie {tie_el.attrib[self._ns_name('id',XML_NAMESPACE)]} is missing the a startid or endid" ) else: # remove the # in first position start_id = start_id[1:] end_id = end_id[1:] # set tie prev and tie next in partitura note objects all_notes_dict[start_id].tie_next = all_notes_dict[end_id] all_notes_dict[end_id].tie_prev = all_notes_dict[start_id] def _insert_repetitions(self): if len(self.repetitions) == 0: return ## sanitize the found repetitions in case a starting rep is missing if self.repetitions[0]["type"] == "stop": # add a start symbol at 0 print( "WARNING : unmatched repetitions. adding a repetition start at position 0" ) self.repetitions.insert(0, {"type": "start", "pos": 0}) status = "stop" sanitized_repetition_list = [] # check if start-stop are alternate for i_rep, rep in enumerate(self.repetitions): if rep["type"] != status: sanitized_repetition_list.append(rep) else: if ( rep["type"] == "start" ): # missing stop, inserting one right before start print( f"WARNING : unmatched repetitions. adding a repetition stop at position {rep['pos']}" ) sanitized_repetition_list.append( {"type": "stop", "pos": rep["pos"]} ) else: # missing start, inserting one at the last stop print( f"WARNING : unmatched repetitions. adding a repetition start at position {sanitized_repetition_list[-1]['pos']}" ) sanitized_repetition_list.append( {"type": "start", "pos": sanitized_repetition_list[-1]["pos"]} ) # proceed by inserting rep sanitized_repetition_list.append(rep) # switch the status status = "stop" if status == "start" else "start" # check if ending with a start if sanitized_repetition_list[-1] == "start": print("WARNING : unmatched repetitions. Ignoring last start") self.repetitions = sanitized_repetition_list ## insert the repetitions to all parts for rep_start, rep_stop in zip(self.repetitions[:-1:2], self.repetitions[1::2]): assert rep_start["type"] == "start" and rep_stop["type"] == "stop" for part in score.iter_parts(self.parts): part.add(score.Repeat(), rep_start["pos"], rep_stop["pos"]) def _insert_barlines(self): for bl in self.barlines: for part in score.iter_parts(self.parts): part.add(score.Barline(bl["type"]), bl["pos"])