示例#1
0
    def __init__(self, doc, req_m, req_s, req_b, completeness=None):
        self.doc = doc
        self.flat_doc = doc.getFlattenedTree()
        self.flat_doc_static = list(doc.getFlattenedTree())
        self.docInfo = MusDocInfo(doc).get()
        self.musicEl = doc.getElementsByName("music")[0]
        self.measures = self.musicEl.getDescendantsByName("measure")
        self.ema_exp = EmaExpression(self.docInfo, req_m, req_s, req_b,
                                     completeness)

        self.ema_measures = self.ema_exp.get()
        self.compiled_exp = self.ema_exp.getCompiled()
示例#2
0
文件: meislicer.py 项目: edsu/ema
    def __init__(self, doc, req_m, req_s, req_b, completeness=None):
        self.doc = doc
        self.flat_doc = doc.getFlattenedTree()
        self.flat_doc_static = list(doc.getFlattenedTree())
        self.docInfo = MusDocInfo(doc).get()
        self.musicEl = doc.getElementsByName("music")[0]
        self.measures = self.musicEl.getDescendantsByName("measure")
        self.ema_exp = EmaExpression(self.docInfo,
                                     req_m,
                                     req_s,
                                     req_b,
                                     completeness)

        self.ema_measures = self.ema_exp.get()
        self.compiled_exp = self.ema_exp.getCompiled()
示例#3
0
class MeiSlicer(object):
    """Class for slicing an MEI doc given a split EMA expresion. """
    def __init__(self, doc, req_m, req_s, req_b, completeness=None):
        self.doc = doc
        self.flat_doc = doc.getFlattenedTree()
        self.flat_doc_static = list(doc.getFlattenedTree())
        self.docInfo = MusDocInfo(doc).get()
        self.musicEl = doc.getElementsByName("music")[0]
        self.measures = self.musicEl.getDescendantsByName("measure")
        self.ema_exp = EmaExpression(self.docInfo, req_m, req_s, req_b,
                                     completeness)

        self.ema_measures = self.ema_exp.get()
        self.compiled_exp = self.ema_exp.getCompiled()

    def slice(self):
        """ Return a modified MEI doc containing the selected notation
            provided a EMA expression of measures, staves, and beats."""

        # parse general beats information
        self.beatsInfo = self.docInfo["beats"]
        self.timeChanges = self.beatsInfo.keys()
        self.timeChanges.sort(key=int)

        # Process measure ranges and store boundary measures
        boundary_mm = []
        for em in self.ema_measures:
            boundary_mm.append(em.measures[0])
            boundary_mm.append(em.measures[-1])
            self.processContigRange(em)

        # Recursively remove remaining data in between
        # measure ranges before returning modified doc

        # First remove measures in between
        to_remove = []
        middle_boundaries = boundary_mm[1:-1]
        for bm in middle_boundaries[::2]:
            i = middle_boundaries.index(bm)
            try:
                start_m = self.measures[bm.idx - 1]
                start_m_pos = start_m.getPositionInDocument()
                end_m = self.measures[middle_boundaries[i + 1].idx - 1]
                end_m_pos = end_m.getPositionInDocument()
                for el in self.flat_doc[start_m_pos + 1:end_m_pos]:
                    if el.hasAncestor("measure"):
                        if el.getAncestor(
                                "measure").getId() != start_m.getId():
                            to_remove.append(el)
                    else:
                        to_remove.append(el)
            except IndexError:
                pass

        m_first = self.measures[boundary_mm[0].idx - 1]
        m_final = self.measures[boundary_mm[-1].idx - 1]

        # List of elements to keep
        keep = ["meiHead"]

        def _removeBefore(curEl):
            parent = curEl.getParent()
            if parent:
                # Casting to list to avoid modfying the sequence
                for el in list(curEl.getPeers()):
                    if el == curEl:
                        break
                    else:
                        if not el.getName() in keep:
                            parent.removeChild(el)
                return _removeBefore(parent)
            return curEl

        def _removeAfter(curEl):
            parent = curEl.getParent()
            if parent:
                removing = False
                # Casting to list to avoid modfying the sequence
                for el in list(curEl.getPeers()):
                    if removing:
                        if not el.getName() in keep:
                            parent.removeChild(el)
                    elif el == curEl:
                        removing = True
                return _removeAfter(parent)
            return curEl

        def _findLowestCommonAncestor(el1, el2):
            while not el1 == el2:
                el1 = el1.getParent()
                el2 = el2.getParent()
            return el1

        def _removeUnusedStaffDefs(scoredef, start_ema_m):
            numbers = set()
            for em in self.ema_measures:
                if start_ema_m == em.measures[0]:
                    for m in em.measures:
                        for ema_s in m.staves:
                            numbers.add(ema_s.number)
                    break

            for staffd in scoredef.getDescendantsByName("staffDef"):
                if staffd.hasAttribute("n"):
                    val = staffd.getAttribute("n").getValue()
                    if int(val) not in numbers:
                        staffd.getParent().removeChild(staffd)

        # TODO! Remove definitions of unselected staves WITHIN RANGE

        # Compute closest score definition to start measure
        preceding = self.flat_doc_static[:m_first.getPositionInDocument()]

        first_scoreDef = None

        for el in reversed(preceding):
            if el.getName() == "scoreDef":
                first_scoreDef = el
                break

        # Recursively remove elements before and after selected measures
        _removeBefore(m_first)
        _removeAfter(m_final)

        # Compute closest score definition to start measure of each range
        b_scoreDef = first_scoreDef
        for i, bm in enumerate(
                boundary_mm[::2]):  # list comprehension get only start mm
            b_measure = self.measures[bm.idx - 1]
            preceding = self.flat_doc_static[:b_measure.getPositionInDocument(
            )]

            for el in reversed(preceding):
                if el.getName() == "scoreDef":
                    try:
                        s_id = b_scoreDef.getId()
                        if not s_id == el.getId():
                            b_scoreDef = el
                        # Re-attach computed score definition
                        sd_copy = MeiElement(b_scoreDef)
                        _removeUnusedStaffDefs(sd_copy, bm)
                        b_measure.getParent().addChildBefore(
                            b_measure, sd_copy)
                    except AttributeError:
                        b_scoreDef = el
                    break

        for el in to_remove:
            el.getParent().removeChild(el)

        if "raw" in self.ema_exp.completenessOptions:
            lca = _findLowestCommonAncestor(m_first, m_final)
            self.doc.setRootElement(lca)

            # re-attach computed score definition if required by
            # completeness = signature
            if "signature" in self.ema_exp.completenessOptions:
                m_first.getParent().addChildBefore(m_first, first_scoreDef)

        return self.doc

    def processContigRange(self, ema_range):
        """ Process a contigous range of measures give an MEI doc and an
            EmaExpression.EmaMeasureRange object """

        # get all the spanners for total extension of meaure selection
        # (including gap measures, if present)
        # NB: Doing this for every range may be inefficient for larger files
        spanners = self.getMultiMeasureSpanners(ema_range.measures[0].idx - 1,
                                                ema_range.measures[-1].idx - 1)

        # Let's start with measures
        for i, ema_m in enumerate(ema_range.measures):

            is_first_m = i == 0
            is_last_m = i == len(ema_range.measures) - 1

            # Get requested measure
            measure = self.measures[ema_m.idx - 1]
            events = measure.getChildren()

            # determine current measure beat info
            meter = None
            for change in self.timeChanges:
                if int(change) + 1 <= ema_m.idx:
                    meter = self.beatsInfo[change]

            # Get list of staff numbers in current measure
            sds = [
                int(sd.getAttribute("n").getValue())
                for sd in measure.getClosestStaffDefs()
            ]

            # Set aside selected staff numbers
            s_nos = []

            # Proceed to locate requested staves
            for ema_s in ema_m.staves:
                if ema_s.number not in sds:
                    # CAREFUL: there may be issues with "silent" staves
                    # that may be defined by missing from current measure.
                    # TODO: Write test, fix.
                    raise BadApiRequest("Requested staff is not defined")
                s_nos.append(ema_s.number)

            for s_i, staff in enumerate(measure.getChildrenByName("staff")):
                s_no = s_i
                if staff.hasAttribute("n"):
                    s_no = int(staff.getAttribute("n").getValue())

                if s_no in s_nos:
                    ema_s = ema_m.staves[s_nos.index(s_no)]

                    # Get other elements affecting the staff, e.g. slurs
                    around = []

                    for el in list(events):
                        if self._isInStaff(el, s_no):
                            around.append(el)

                    # Create sets of elements marked for selection, removal and
                    # cutting. Elements are not removed or cut immediately to make
                    # sure that beat calcualtions are accurate.
                    marked_as_selected = MeiElementSet()
                    marked_as_space = MeiElementSet()
                    marked_for_removal = MeiElementSet()
                    marked_for_cutting = MeiElementSet()

                    # Now locate the requested beat ranges within the staff
                    for b_i, ema_beat_range in enumerate(ema_s.beat_ranges):

                        is_first_b = i == 0
                        is_last_b = i == len(ema_s.beat_ranges) - 1

                        def _remove(el):
                            """ Determine whether a removed element needs to
                            be converted to a space or removed altogether"""
                            if is_last_b and is_last_m:
                                marked_for_removal.add(el)
                            else:
                                marked_as_space.add(el)

                        # shorten them names
                        tstamp_first = ema_beat_range.tstamp_first
                        tstamp_final = ema_beat_range.tstamp_final
                        co = self.ema_exp.completenessOptions

                        # check that the requested beats actually fit in the meter
                        if tstamp_first > int(meter["count"])+1 or \
                           tstamp_final > int(meter["count"])+1:
                            raise BadApiRequest(
                                "Request beat is out of measure bounds")

                        # Find all descendants with att.duration.musical (@dur)
                        for layer in staff.getDescendantsByName("layer"):
                            cur_beat = 1.0
                            is_first_match = True

                            for el in layer.getDescendants():

                                if el.hasAttribute(
                                        "dur"
                                ) and not el.hasAttribute("grace"):
                                    dur = self._calculateDur(el, meter)
                                    # TODO still problems with non-consecutive beat ranges
                                    # e.g. @1@3
                                    # exclude descendants at and in between tstamps
                                    if cur_beat >= tstamp_first:
                                        # We round to 4 decimal places to avoid issues caused by
                                        # tuplet-related calculations, which are admittedly not
                                        # well expressed in floating numbers.
                                        if round(cur_beat, 4) <= round(
                                                tstamp_final, 4):
                                            marked_as_selected.add(el)
                                            if is_first_match and "cut" in co:
                                                marked_for_cutting.add(el)
                                                is_first_match = False

                                            # discard from removal set if it had
                                            # been placed there from other beat
                                            # range
                                            marked_as_space.discard(el)

                                            # Cut the duration of the last element
                                            # if completeness = cut
                                            needs_cut = cur_beat + dur > tstamp_final + 1
                                            if needs_cut and "cut" in co:
                                                marked_for_cutting.add(el)
                                                is_first_match = False
                                        elif not marked_as_selected.get(el):
                                            _remove(el)
                                    elif not marked_as_selected.get(el):
                                        marked_as_space.add(el)

                                    # continue
                                    cur_beat += dur

                        # select elements affecting the staff occurring
                        # within beat range
                        for event in around:
                            if not marked_as_selected.get(event):
                                if event.hasAttribute("tstamp"):
                                    ts = float(
                                        event.getAttribute(
                                            "tstamp").getValue())
                                    if ts < 1 and "cut" not in self.ema_exp.completenessOptions:
                                        ts = 1
                                    ts2_att = None
                                    if event.hasAttribute("tstamp2"):
                                        ts2_att = event.getAttribute("tstamp2")
                                    if ts > tstamp_final or (
                                            not ts2_att and ts < tstamp_first):
                                        marked_for_removal.add(event)
                                    elif ts2_att:
                                        ts2 = ts2_att.getValue()
                                        if "+" not in ts2:
                                            if ts2 < tstamp_first:
                                                marked_for_removal.add(event)
                                            elif ts2 == tstamp_final:
                                                marked_as_selected.add(event)
                                                marked_for_removal.discard(
                                                    event)
                                            if ts < tstamp_first and ts2 >= tstamp_final:
                                                marked_as_selected.add(event)
                                                marked_for_removal.discard(
                                                    event)
                                            else:
                                                marked_for_removal.add(event)
                                        else:
                                            marked_as_selected.add(event)
                                    else:
                                        marked_as_selected.add(event)

                                elif event.hasAttribute("startid"):
                                    startid = (event.getAttribute(
                                        "startid").getValue().replace("#", ""))
                                    target = self.doc.getElementById(startid)
                                    if not target:
                                        msg = """Unsupported Encoding: attribute
                                        startid on element {0} does not point to any
                                        element in the document.""".format(
                                            event.getName())
                                        raise UnsupportedEncoding(
                                            re.sub(r'\s+', ' ', msg.strip()))
                                    # Make sure the target event is in the same measure
                                    event_m = event.getAncestor(
                                        "measure").getId()
                                    target_m = target.getAncestor(
                                        "measure").getId()
                                    if not event_m == target_m:
                                        msg = """Unsupported Encoding: attribute
                                        startid on element {0} does not point to an
                                        element in the same measure.""".format(
                                            event.getName())
                                        raise UnsupportedEncoding(
                                            re.sub(r'\s+', ' ', msg.strip()))
                                    else:
                                        if marked_as_selected.get(target):
                                            marked_as_selected.add(event)
                                            marked_for_removal.discard(event)
                                        elif not event.hasAttribute("endid"):
                                            marked_for_removal.add(event)
                                        else:
                                            # Skip if event starts after latest
                                            # selected element with duration
                                            pos = target.getPositionInDocument(
                                            )
                                            is_ahead = False
                                            for i in reversed(
                                                    marked_as_selected.
                                                    getElements()):
                                                if i.hasAttribute("dur"):
                                                    if pos > i.getPositionInDocument(
                                                    ):
                                                        marked_for_removal.add(
                                                            event)
                                                        is_ahead = True
                                                    break

                                            if not is_ahead:
                                                # last chance to keep it:
                                                # must start before and end after
                                                # latest selected element with duration

                                                endid = (
                                                    event.getAttribute("endid")
                                                    .getValue().replace(
                                                        "#", ""))
                                                target2 = self.doc.getElementById(
                                                    endid)
                                                if marked_as_selected.get(
                                                        target2):
                                                    marked_as_selected.add(
                                                        event)
                                                    marked_for_removal.discard(
                                                        event)
                                                else:
                                                    pos2 = target2.getPositionInDocument(
                                                    )
                                                    for i in reversed(
                                                            marked_as_selected.
                                                            getElements()):
                                                        if i.hasAttribute(
                                                                "dur"):
                                                            if pos2 > i.getPositionInDocument(
                                                            ):
                                                                marked_as_selected.add(
                                                                    event)
                                                                marked_for_removal.discard(
                                                                    event)
                                                            else:
                                                                marked_for_removal.add(
                                                                    event)
                                                            break

                    # Remove elements marked for removal
                    for el in marked_for_removal:
                        el.getParent().removeChild(el)

                    # Replace elements marked as spaces with actual spaces,
                    # unless completion = nospace, then remove the elements.
                    for el in marked_as_space:
                        parent = el.getParent()
                        if "nospace" not in self.ema_exp.completenessOptions:
                            space = MeiElement("space")
                            space.setId(el.id)
                            space.addAttribute(el.getAttribute("dur"))
                            if el.getAttribute("dots"):
                                space.addAttribute(el.getAttribute("dots"))
                            elif el.getChildrenByName("dot"):
                                dots = str(len(el.getChildrenByName("dot")))
                                space.addAttribute(MeiAttribute("dots", dots))
                            parent.addChildBefore(el, space)
                        el.getParent().removeChild(el)

                else:
                    # Remove this staff and its attached events
                    staff.getParent().removeChild(staff)

                    for el in list(events):
                        if self._isInStaff(el, s_no):
                            el.getParent().removeChild(el)

            # At the first measure, also add relevant multi-measure spanners
            # for each selected staff
            if is_first_m:
                for evs in spanners.values():
                    for event_id in evs:
                        ev = self.doc.getElementById(event_id)
                        # Spanners starting outside of beat ranges
                        # may be already gone
                        if ev:
                            # Determine staff of event for id changes
                            for ema_s in ema_m.staves:
                                staff_no = self._isInStaff(ev, ema_s.number)

                            if staff_no and staff_no in s_nos:
                                # If the event is attached to more than one staff, just
                                # consider it attached to the its first one
                                staff_no = staff_no[0]

                                staff = None
                                for staff_candidate in measure.getDescendantsByName(
                                        "staff"):
                                    if staff_candidate.hasAttribute("n"):
                                        n = int(
                                            staff_candidate.getAttribute(
                                                "n").getValue())
                                        if n == staff_no:
                                            staff = staff_candidate

                                # Truncate event to start at the beginning of the beat range
                                if ev.hasAttribute("startid"):
                                    # Set startid to the first event still on staff,
                                    # at the first available layer
                                    try:
                                        layer = (
                                            staff.getChildrenByName("layer"))
                                        first_id = layer[0].getChildren(
                                        )[0].getId()
                                        ev.getAttribute("startid").setValue(
                                            "#" + first_id)
                                    except IndexError:
                                        msg = """
                                            Unsupported encoding. Omas attempted to adjust the
                                            starting point of a selected multi-measure element
                                            that starts before the selection, but the staff or
                                            layer could not be located.
                                            """
                                        msg = re.sub(r'\s+', ' ', msg.strip())
                                        raise UnsupportedEncoding(msg)

                                if ev.hasAttribute("tstamp"):
                                    # Set tstamp to first in beat selection
                                    tstamp_first = 0
                                    for e_s in ema_m.staves:
                                        if e_s.number == staff_no:
                                            tstamp_first = e_s.beat_ranges[
                                                0].tstamp_first
                                    ev.getAttribute("tstamp").setValue(
                                        str(tstamp_first))

                                # Truncate to end of range if completeness = cut
                                # (actual beat cutting will happen when beat ranges are procesed)
                                if "cut" in self.ema_exp.completenessOptions:
                                    if ev.hasAttribute("tstamp2"):
                                        att = ev.getAttribute("tstamp2")
                                        t2 = att.getValue()
                                        p = re.compile(r"([1-9]+)(?=m\+)")
                                        multimeasure = p.match(t2)
                                        if multimeasure:
                                            new_val = len(mm) - 1
                                            att.setValue(
                                                p.sub(str(new_val), t2))

                                # Otherwise adjust tspan2 value to correct distance.
                                # E.g. given 4 measures with a spanner originating
                                # in 1 and ending in 4 and a selection of measures 2 and 3,
                                # change @tspan2 from 3m+X to 2m+X
                                else:
                                    if ev.hasAttribute("tstamp2"):
                                        att = ev.getAttribute("tstamp2")
                                        t2 = att.getValue()
                                        p = re.compile(r"([1-9]+)(?=m\+)")
                                        multimeasure = p.match(t2)
                                        if multimeasure:
                                            dis = evs[event_id]["distance"]
                                            new_val = int(
                                                multimeasure.group(1)) - dis
                                            att.setValue(
                                                p.sub(str(new_val), t2))

                                # move element to first measure and add it to selected
                                # events "around" the staff.
                                ev.moveTo(measure)

        return self.doc

    def getMultiMeasureSpanners(self, start, end=-1):
        """Return a dictionary of spanning elements encompassing,
           landing or starting within selected measures"""
        mm = self.musicEl.getDescendantsByName("measure")
        try:
            start_m = mm[start]
        except IndexError:
            raise BadApiRequest("Requested measure does not exist.")
        table = {}

        # Template of table dictionary (for reference):
        # {
        #     "_targetMeasureID_" : {
        #         "_eventID_" : {
        #             "origin" : "_originMeasureID_",
        #             "distance" : 0,
        #             "startid" : "_startID_",
        #             "endid" : "_endID_",
        #             "tstamp" : "_beat_",
        #             "tstamp2" : "_Xm+beat_"
        #         }
        #     }
        # }

        def _calculateDistance(origin):
            """ Calcualte distance of origin measure from
                first measure in selection """
            # Cast MeiElementList to python list
            list_mm = list(mm)
            return list_mm.index(start_m) - list_mm.index(origin)

        # Exclude end measure index from request,
        # unless last measure is requested (creates table for whole file).
        if end == -1:
            end = len(mm) + 1

        # Look back through measures and build a table of events
        # spanning multiple measures via tstamp/tstamp2 or startid/endid pairs.
        for i, prev_m in enumerate(mm[:end]):
            m_id = prev_m.getId()

            for el in prev_m.getDescendants():

                # Determine destination based on startid/endid pairs.
                # If not found, look for tstamp/tstamp2 pairs before moving on.

                if el.hasAttribute("endid"):
                    endid = el.getAttribute("endid").getValue()
                    endid = endid.replace("#", "")

                    target_el = self.doc.getElementById(endid)

                    if target_el:
                        if target_el.hasAncestor("measure"):
                            # This could be a comparison of objects,
                            # but comparing ids feels safer.
                            destination = target_el.getAncestor("measure")
                            if destination.getId() != prev_m.getId():
                                # Create table entry
                                dest_id = destination.getId()
                                distance = _calculateDistance(prev_m)
                                el_id = el.getId()

                                if dest_id not in table:
                                    table[dest_id] = {}
                                table[dest_id][el_id] = {
                                    "origin": m_id,
                                    "distance": distance,
                                    "endid": endid
                                }
                                if el.hasAttribute("startid"):
                                    startid = (el.getAttribute(
                                        "startid").getValue().replace("#", ""))
                                    table[dest_id][el_id]["startid"] = startid
                elif el.hasAttribute("tstamp2"):
                    t2 = el.getAttribute("tstamp2").getValue()
                    multiMesSpan = re.match(r"([1-9]+)m\+", t2)
                    if multiMesSpan:
                        destination = mm[i + int(multiMesSpan.group(1))]
                        # Create table entry
                        dest_id = destination.getId()
                        distance = _calculateDistance(prev_m)
                        el_id = el.getId()

                        if dest_id not in table:
                            table[dest_id] = {}
                        table[dest_id][el_id] = {
                            "origin": m_id,
                            "distance": distance,
                            "tstamp2": t2
                        }
                        if el.hasAttribute("tstamp"):
                            table[dest_id][el_id]["tstamp"] = (
                                el.getAttribute("tstamp").getValue())

        return table

    def _calculateDur(self, element, meter):
        """ Determine the duration of an element given a meter """
        # TODO: beware of @duration.default - though not very common
        dur_val = element.getAttribute("dur").getValue()
        if dur_val == "breve":
            dur_val = "0.5"
        elif dur_val == "long":
            dur_val = "0.25"
        duration = float(dur_val)
        relative_dur = float(int(meter["unit"]) / duration)

        dots = 0
        if element.getAttribute("dots"):
            dots = int(element.getAttribute("dots").getValue())
        elif element.getChildrenByName("dot"):
            dots = len(element.getChildrenByName("dot"))

        dot_dur = duration
        for d in range(1, int(dots) + 1):
            dot_dur = dot_dur * 2
            relative_dur += float(int(meter["unit"]) / dot_dur)

        # Is this element contained in a tuplet element?
        # (TODO account for tupletspan)
        if element.hasAncestor("tuplet"):
            tupl = element.getAncestor("tuplet")

            numbase = tupl.getAttribute("numbase")
            num = tupl.getAttribute("num")

            if not num or not numbase:
                raise UnsupportedEncoding(
                    "Cannot understand tuplet beat: both @num and @numbase must be present"
                )
            else:
                tupl_ratio = float(numbase.getValue()) / float(num.getValue())
                relative_dur = relative_dur * tupl_ratio

        return relative_dur

    def _cutDuration(self, element, meter):
        """ Cut the duration of given element to the final beat """
        element.getAttribute("dur").setValue(str(meter["unit"]))
        # Remove dots if any
        element.removeAttribute("dots")
        element.removeChildrenByName("dot")

    def _isInStaff(self, el, s_no):
        """ Get all staff numbers of element if element is in given staff"""
        values = self._getSelectedStaffNosFor(el)

        if s_no in values:
            return values
        else:
            values = []
        return values

    def _getSelectedStaffNosFor(self, el):
        """ Get staff numbers of element if the staves in given measure
            are selected"""
        # TODO: CAREFUL - EDITORIAL MARKUP MAY OBFUSCATE THIS
        values = []

        if el.hasAttribute("staff"):
            # Split value of @staff, as it may contain multiple values.
            values = el.getAttribute("staff").getValue().split()
            values = [int(x) for x in values]

        return values
示例#4
0
文件: meislicer.py 项目: edsu/ema
class MeiSlicer(object):
    """Class for slicing an MEI doc given a split EMA expresion. """
    def __init__(self, doc, req_m, req_s, req_b, completeness=None):
        self.doc = doc
        self.flat_doc = doc.getFlattenedTree()
        self.flat_doc_static = list(doc.getFlattenedTree())
        self.docInfo = MusDocInfo(doc).get()
        self.musicEl = doc.getElementsByName("music")[0]
        self.measures = self.musicEl.getDescendantsByName("measure")
        self.ema_exp = EmaExpression(self.docInfo,
                                     req_m,
                                     req_s,
                                     req_b,
                                     completeness)

        self.ema_measures = self.ema_exp.get()
        self.compiled_exp = self.ema_exp.getCompiled()

    def slice(self):
        """ Return a modified MEI doc containing the selected notation
            provided a EMA expression of measures, staves, and beats."""

        # parse general beats information
        self.beatsInfo = self.docInfo["beats"]
        self.timeChanges = self.beatsInfo.keys()
        self.timeChanges.sort(key=int)

        # Process measure ranges and store boundary measures
        boundary_mm = []
        for em in self.ema_measures:
            boundary_mm.append(em.measures[0])
            boundary_mm.append(em.measures[-1])
            self.processContigRange(em)

        # Recursively remove remaining data in between
        # measure ranges before returning modified doc

        # First remove measures in between
        to_remove = []
        middle_boundaries = boundary_mm[1:-1]
        for bm in middle_boundaries[::2]:
            i = middle_boundaries.index(bm)
            try:
                start_m = self.measures[bm.idx-1]
                start_m_pos = start_m.getPositionInDocument()
                end_m = self.measures[middle_boundaries[i+1].idx-1]
                end_m_pos = end_m.getPositionInDocument()
                for el in self.flat_doc[start_m_pos+1:end_m_pos]:
                    if el.hasAncestor("measure"):
                        if el.getAncestor("measure").getId() != start_m.getId():
                            to_remove.append(el)
                    else:
                        to_remove.append(el)
            except IndexError:
                pass

        m_first = self.measures[boundary_mm[0].idx-1]
        m_final = self.measures[boundary_mm[-1].idx-1]

        # List of elements to keep
        keep = ["meiHead"]

        def _removeBefore(curEl):
            parent = curEl.getParent()
            if parent:
                # Casting to list to avoid modfying the sequence
                for el in list(curEl.getPeers()):
                    if el == curEl:
                        break
                    else:
                        if not el.getName() in keep:
                            parent.removeChild(el)
                return _removeBefore(parent)
            return curEl

        def _removeAfter(curEl):
            parent = curEl.getParent()
            if parent:
                removing = False
                # Casting to list to avoid modfying the sequence
                for el in list(curEl.getPeers()):
                    if removing:
                        if not el.getName() in keep:
                            parent.removeChild(el)
                    elif el == curEl:
                        removing = True
                return _removeAfter(parent)
            return curEl

        def _findLowestCommonAncestor(el1, el2):
            while not el1 == el2:
                el1 = el1.getParent()
                el2 = el2.getParent()
            return el1

        def _removeUnusedStaffDefs(scoredef, start_ema_m):
            numbers = set()
            for em in self.ema_measures:
                if start_ema_m == em.measures[0]:
                    for m in em.measures:
                        for ema_s in m.staves:
                            numbers.add(ema_s.number)
                    break

            for staffd in scoredef.getDescendantsByName("staffDef"):
                if staffd.hasAttribute("n"):
                    val = staffd.getAttribute("n").getValue()
                    if int(val) not in numbers:
                        staffd.getParent().removeChild(staffd)

        # TODO! Remove definitions of unselected staves WITHIN RANGE

        # Compute closest score definition to start measure
        preceding = self.flat_doc_static[:m_first.getPositionInDocument()]

        first_scoreDef = None

        for el in reversed(preceding):
            if el.getName() == "scoreDef":
                first_scoreDef = el
                break

        # Recursively remove elements before and after selected measures
        _removeBefore(m_first)
        _removeAfter(m_final)

        # Compute closest score definition to start measure of each range
        b_scoreDef = first_scoreDef
        for i, bm in enumerate(boundary_mm[::2]):  # list comprehension get only start mm
            b_measure = self.measures[bm.idx-1]
            preceding = self.flat_doc_static[:b_measure.getPositionInDocument()]

            for el in reversed(preceding):
                if el.getName() == "scoreDef":
                    try:
                        s_id = b_scoreDef.getId()
                        if not s_id == el.getId():
                            b_scoreDef = el
                        # Re-attach computed score definition
                        sd_copy = MeiElement(b_scoreDef)
                        _removeUnusedStaffDefs(sd_copy, bm)
                        b_measure.getParent().addChildBefore(b_measure, sd_copy)
                    except AttributeError:
                        b_scoreDef = el
                    break

        for el in to_remove:
            el.getParent().removeChild(el)

        if "raw" in self.ema_exp.completenessOptions:
            lca = _findLowestCommonAncestor(m_first, m_final)
            self.doc.setRootElement(lca)

            # re-attach computed score definition if required by
            # completeness = signature
            if "signature" in self.ema_exp.completenessOptions:
                m_first.getParent().addChildBefore(m_first, first_scoreDef)

        return self.doc

    def processContigRange(self, ema_range):
        """ Process a contigous range of measures give an MEI doc and an
            EmaExpression.EmaMeasureRange object """

        # get all the spanners for total extension of meaure selection
        # (including gap measures, if present)
        # NB: Doing this for every range may be inefficient for larger files
        spanners = self.getMultiMeasureSpanners(ema_range.measures[0].idx-1,
                                                ema_range.measures[-1].idx-1)

        # Let's start with measures
        for i, ema_m in enumerate(ema_range.measures):

            is_first_m = i == 0
            is_last_m = i == len(ema_range.measures)-1

            # Get requested measure
            measure = self.measures[ema_m.idx-1]
            events = measure.getChildren()

            # determine current measure beat info
            meter = None
            for change in self.timeChanges:
                if int(change)+1 <= ema_m.idx:
                    meter = self.beatsInfo[change]

            # Get list of staff numbers in current measure
            sds = [int(sd.getAttribute("n").getValue())
                   for sd in measure.getClosestStaffDefs()]

            # Set aside selected staff numbers
            s_nos = []

            # Proceed to locate requested staves
            for ema_s in ema_m.staves:
                if ema_s.number not in sds:
                    # CAREFUL: there may be issues with "silent" staves
                    # that may be defined by missing from current measure.
                    # TODO: Write test, fix.
                    raise BadApiRequest("Requested staff is not defined")
                s_nos.append(ema_s.number)

            for s_i, staff in enumerate(measure.getChildrenByName("staff")):
                s_no = s_i
                if staff.hasAttribute("n"):
                    s_no = int(staff.getAttribute("n").getValue())

                if s_no in s_nos:
                    ema_s = ema_m.staves[s_nos.index(s_no)]

                    # Get other elements affecting the staff, e.g. slurs
                    around = []

                    for el in list(events):
                        if self._isInStaff(el, s_no):
                            around.append(el)

                    # Create sets of elements marked for selection, removal and
                    # cutting. Elements are not removed or cut immediately to make
                    # sure that beat calcualtions are accurate.
                    marked_as_selected = MeiElementSet()
                    marked_as_space = MeiElementSet()
                    marked_for_removal = MeiElementSet()
                    marked_for_cutting = MeiElementSet()

                    # Now locate the requested beat ranges within the staff
                    for b_i, ema_beat_range in enumerate(ema_s.beat_ranges):

                        is_first_b = i == 0
                        is_last_b = i == len(ema_s.beat_ranges)-1

                        def _remove(el):
                            """ Determine whether a removed element needs to
                            be converted to a space or removed altogether"""
                            if is_last_b and is_last_m:
                                marked_for_removal.add(el)
                            else:
                                marked_as_space.add(el)

                        # shorten them names
                        tstamp_first = ema_beat_range.tstamp_first
                        tstamp_final = ema_beat_range.tstamp_final
                        co = self.ema_exp.completenessOptions

                        # check that the requested beats actually fit in the meter
                        if tstamp_first > int(meter["count"])+1 or \
                           tstamp_final > int(meter["count"])+1:
                            raise BadApiRequest(
                                "Request beat is out of measure bounds")

                        # Find all descendants with att.duration.musical (@dur)
                        for layer in staff.getDescendantsByName("layer"):
                            cur_beat = 1.0
                            is_first_match = True

                            for el in layer.getDescendants():

                                if el.hasAttribute("dur") and not el.hasAttribute("grace"):
                                    dur = self._calculateDur(el, meter)
                                    # TODO still problems with non-consecutive beat ranges
                                    # e.g. @1@3
                                    # exclude descendants at and in between tstamps
                                    if cur_beat >= tstamp_first:
                                        # We round to 4 decimal places to avoid issues caused by
                                        # tuplet-related calculations, which are admittedly not
                                        # well expressed in floating numbers.
                                        if round(cur_beat, 4) <= round(tstamp_final, 4):
                                            marked_as_selected.add(el)
                                            if is_first_match and "cut" in co:
                                                marked_for_cutting.add(el)
                                                is_first_match = False

                                            # discard from removal set if it had
                                            # been placed there from other beat
                                            # range
                                            marked_as_space.discard(el)

                                            # Cut the duration of the last element
                                            # if completeness = cut
                                            needs_cut = cur_beat+dur > tstamp_final+1
                                            if needs_cut and "cut" in co:
                                                marked_for_cutting.add(el)
                                                is_first_match = False
                                        elif not marked_as_selected.get(el):
                                            _remove(el)
                                    elif not marked_as_selected.get(el):
                                        marked_as_space.add(el)

                                    # continue
                                    cur_beat += dur

                        # select elements affecting the staff occurring
                        # within beat range
                        for event in around:
                            if not marked_as_selected.get(event):
                                if event.hasAttribute("tstamp"):
                                    ts = float(event.getAttribute("tstamp").getValue())
                                    if ts < 1 and "cut" not in self.ema_exp.completenessOptions:
                                        ts = 1
                                    ts2_att = None
                                    if event.hasAttribute("tstamp2"):
                                        ts2_att = event.getAttribute("tstamp2")
                                    if ts > tstamp_final or (not ts2_att and ts < tstamp_first):
                                        marked_for_removal.add(event)
                                    elif ts2_att:
                                        ts2 = ts2_att.getValue()
                                        if "+" not in ts2:
                                            if ts2 < tstamp_first:
                                                marked_for_removal.add(event)
                                            elif ts2 == tstamp_final:
                                                marked_as_selected.add(event)
                                                marked_for_removal.discard(event)
                                            if ts < tstamp_first and ts2 >= tstamp_final:
                                                marked_as_selected.add(event)
                                                marked_for_removal.discard(event)
                                            else:
                                                marked_for_removal.add(event)
                                        else:
                                            marked_as_selected.add(event)
                                    else:
                                        marked_as_selected.add(event)

                                elif event.hasAttribute("startid"):
                                    startid = (
                                        event.getAttribute("startid")
                                        .getValue()
                                        .replace("#", "")
                                    )
                                    target = self.doc.getElementById(startid)
                                    if not target:
                                        msg = """Unsupported Encoding: attribute
                                        startid on element {0} does not point to any
                                        element in the document.""".format(
                                            event.getName())
                                        raise UnsupportedEncoding(
                                            re.sub(r'\s+', ' ', msg.strip()))
                                    # Make sure the target event is in the same measure
                                    event_m = event.getAncestor("measure").getId()
                                    target_m = target.getAncestor("measure").getId()
                                    if not event_m == target_m:
                                        msg = """Unsupported Encoding: attribute
                                        startid on element {0} does not point to an
                                        element in the same measure.""".format(
                                            event.getName())
                                        raise UnsupportedEncoding(
                                            re.sub(r'\s+', ' ', msg.strip()))
                                    else:
                                        if marked_as_selected.get(target):
                                            marked_as_selected.add(event)
                                            marked_for_removal.discard(event)
                                        elif not event.hasAttribute("endid"):
                                            marked_for_removal.add(event)
                                        else:
                                            # Skip if event starts after latest
                                            # selected element with duration
                                            pos = target.getPositionInDocument()
                                            is_ahead = False
                                            for i in reversed(marked_as_selected.getElements()):
                                                if i.hasAttribute("dur"):
                                                    if pos > i.getPositionInDocument():
                                                        marked_for_removal.add(event)
                                                        is_ahead = True
                                                    break

                                            if not is_ahead:
                                                # last chance to keep it:
                                                # must start before and end after
                                                # latest selected element with duration

                                                endid = (
                                                    event.getAttribute("endid")
                                                    .getValue()
                                                    .replace("#", "")
                                                )
                                                target2 = self.doc.getElementById(endid)
                                                if marked_as_selected.get(target2):
                                                    marked_as_selected.add(event)
                                                    marked_for_removal.discard(event)
                                                else:
                                                    pos2 = target2.getPositionInDocument()
                                                    for i in reversed(marked_as_selected.getElements()):
                                                        if i.hasAttribute("dur"):
                                                            if pos2 > i.getPositionInDocument():
                                                                marked_as_selected.add(event)
                                                                marked_for_removal.discard(event)
                                                            else:
                                                                marked_for_removal.add(event)
                                                            break

                    # Remove elements marked for removal
                    for el in marked_for_removal:
                        el.getParent().removeChild(el)

                    # Replace elements marked as spaces with actual spaces,
                    # unless completion = nospace, then remove the elements.
                    for el in marked_as_space:
                        parent = el.getParent()
                        if "nospace" not in self.ema_exp.completenessOptions:
                            space = MeiElement("space")
                            space.setId(el.id)
                            space.addAttribute(el.getAttribute("dur"))
                            if el.getAttribute("dots"):
                                space.addAttribute(el.getAttribute("dots"))
                            elif el.getChildrenByName("dot"):
                                dots = str(len(el.getChildrenByName("dot")))
                                space.addAttribute(MeiAttribute("dots", dots))
                            parent.addChildBefore(el, space)
                        el.getParent().removeChild(el)

                else:
                    # Remove this staff and its attached events
                    staff.getParent().removeChild(staff)

                    for el in list(events):
                        if self._isInStaff(el, s_no):
                            el.getParent().removeChild(el)

            # At the first measure, also add relevant multi-measure spanners
            # for each selected staff
            if is_first_m:
                for evs in spanners.values():
                    for event_id in evs:
                        ev = self.doc.getElementById(event_id)
                        # Spanners starting outside of beat ranges
                        # may be already gone
                        if ev:
                            # Determine staff of event for id changes
                            for ema_s in ema_m.staves:
                                staff_no = self._isInStaff(ev, ema_s.number)

                            if staff_no and staff_no in s_nos:
                                # If the event is attached to more than one staff, just
                                # consider it attached to the its first one
                                staff_no = staff_no[0]

                                staff = None
                                for staff_candidate in measure.getDescendantsByName("staff"):
                                    if staff_candidate.hasAttribute("n"):
                                        n = int(staff_candidate.getAttribute("n").getValue())
                                        if n == staff_no:
                                            staff = staff_candidate

                                # Truncate event to start at the beginning of the beat range
                                if ev.hasAttribute("startid"):
                                    # Set startid to the first event still on staff,
                                    # at the first available layer
                                    try:
                                        layer = (
                                            staff.getChildrenByName("layer")
                                        )
                                        first_id = layer[0].getChildren()[0].getId()
                                        ev.getAttribute("startid").setValue("#"+first_id)
                                    except IndexError:
                                        msg = """
                                            Unsupported encoding. Omas attempted to adjust the
                                            starting point of a selected multi-measure element
                                            that starts before the selection, but the staff or
                                            layer could not be located.
                                            """
                                        msg = re.sub(r'\s+', ' ', msg.strip())
                                        raise UnsupportedEncoding(msg)

                                if ev.hasAttribute("tstamp"):
                                    # Set tstamp to first in beat selection
                                    tstamp_first = 0
                                    for e_s in ema_m.staves:
                                        if e_s.number == staff_no:
                                            tstamp_first = e_s.beat_ranges[0].tstamp_first
                                    ev.getAttribute("tstamp").setValue(str(tstamp_first))

                                # Truncate to end of range if completeness = cut
                                # (actual beat cutting will happen when beat ranges are procesed)
                                if "cut" in self.ema_exp.completenessOptions:
                                    if ev.hasAttribute("tstamp2"):
                                        att = ev.getAttribute("tstamp2")
                                        t2 = att.getValue()
                                        p = re.compile(r"([1-9]+)(?=m\+)")
                                        multimeasure = p.match(t2)
                                        if multimeasure:
                                            new_val = len(mm) - 1
                                            att.setValue(p.sub(str(new_val), t2))

                                # Otherwise adjust tspan2 value to correct distance.
                                # E.g. given 4 measures with a spanner originating
                                # in 1 and ending in 4 and a selection of measures 2 and 3,
                                # change @tspan2 from 3m+X to 2m+X
                                else:
                                    if ev.hasAttribute("tstamp2"):
                                        att = ev.getAttribute("tstamp2")
                                        t2 = att.getValue()
                                        p = re.compile(r"([1-9]+)(?=m\+)")
                                        multimeasure = p.match(t2)
                                        if multimeasure:
                                            dis = evs[event_id]["distance"]
                                            new_val = int(multimeasure.group(1)) - dis
                                            att.setValue(p.sub(str(new_val), t2))

                                # move element to first measure and add it to selected
                                # events "around" the staff.
                                ev.moveTo(measure)

        return self.doc

    def getMultiMeasureSpanners(self, start, end=-1):
        """Return a dictionary of spanning elements encompassing,
           landing or starting within selected measures"""
        mm = self.musicEl.getDescendantsByName("measure")
        try:
            start_m = mm[start]
        except IndexError:
            raise BadApiRequest("Requested measure does not exist.")
        table = {}

        # Template of table dictionary (for reference):
        # {
        #     "_targetMeasureID_" : {
        #         "_eventID_" : {
        #             "origin" : "_originMeasureID_",
        #             "distance" : 0,
        #             "startid" : "_startID_",
        #             "endid" : "_endID_",
        #             "tstamp" : "_beat_",
        #             "tstamp2" : "_Xm+beat_"
        #         }
        #     }
        # }

        def _calculateDistance(origin):
            """ Calcualte distance of origin measure from
                first measure in selection """
            # Cast MeiElementList to python list
            list_mm = list(mm)
            return list_mm.index(start_m) - list_mm.index(origin)

        # Exclude end measure index from request,
        # unless last measure is requested (creates table for whole file).
        if end == -1:
            end = len(mm) + 1

        # Look back through measures and build a table of events
        # spanning multiple measures via tstamp/tstamp2 or startid/endid pairs.
        for i, prev_m in enumerate(mm[:end]):
            m_id = prev_m.getId()

            for el in prev_m.getDescendants():

                # Determine destination based on startid/endid pairs.
                # If not found, look for tstamp/tstamp2 pairs before moving on.

                if el.hasAttribute("endid"):
                    endid = el.getAttribute("endid").getValue()
                    endid = endid.replace("#", "")

                    target_el = self.doc.getElementById(endid)

                    if target_el:
                        if target_el.hasAncestor("measure"):
                            # This could be a comparison of objects,
                            # but comparing ids feels safer.
                            destination = target_el.getAncestor("measure")
                            if destination.getId() != prev_m.getId():
                                # Create table entry
                                dest_id = destination.getId()
                                distance = _calculateDistance(prev_m)
                                el_id = el.getId()

                                if dest_id not in table:
                                    table[dest_id] = {}
                                table[dest_id][el_id] = {
                                    "origin": m_id,
                                    "distance": distance,
                                    "endid": endid
                                }
                                if el.hasAttribute("startid"):
                                    startid = (
                                        el
                                        .getAttribute("startid")
                                        .getValue()
                                        .replace("#", "")
                                    )
                                    table[dest_id][el_id]["startid"] = startid
                elif el.hasAttribute("tstamp2"):
                    t2 = el.getAttribute("tstamp2").getValue()
                    multiMesSpan = re.match(r"([1-9]+)m\+", t2)
                    if multiMesSpan:
                        destination = mm[i + int(multiMesSpan.group(1))]
                        # Create table entry
                        dest_id = destination.getId()
                        distance = _calculateDistance(prev_m)
                        el_id = el.getId()

                        if dest_id not in table:
                            table[dest_id] = {}
                        table[dest_id][el_id] = {
                            "origin": m_id,
                            "distance": distance,
                            "tstamp2": t2
                        }
                        if el.hasAttribute("tstamp"):
                            table[dest_id][el_id]["tstamp"] = (
                                el
                                .getAttribute("tstamp")
                                .getValue()
                            )

        return table

    def _calculateDur(self, element, meter):
        """ Determine the duration of an element given a meter """
        # TODO: beware of @duration.default - though not very common
        dur_val = element.getAttribute("dur").getValue()
        if dur_val == "breve":
            dur_val = "0.5"
        elif dur_val == "long":
            dur_val = "0.25"
        duration = float(dur_val)
        relative_dur = float(int(meter["unit"]) / duration)

        dots = 0
        if element.getAttribute("dots"):
            dots = int(element.getAttribute("dots").getValue())
        elif element.getChildrenByName("dot"):
            dots = len(element.getChildrenByName("dot"))

        dot_dur = duration
        for d in range(1, int(dots)+1):
            dot_dur = dot_dur * 2
            relative_dur += float(int(meter["unit"]) / dot_dur)

        # Is this element contained in a tuplet element?
        # (TODO account for tupletspan)
        if element.hasAncestor("tuplet"):
            tupl = element.getAncestor("tuplet")

            numbase = tupl.getAttribute("numbase")
            num = tupl.getAttribute("num")

            if not num or not numbase:
                raise UnsupportedEncoding(
                    "Cannot understand tuplet beat: both @num and @numbase must be present")
            else:
                tupl_ratio = float(numbase.getValue()) / float(num.getValue())
                relative_dur = relative_dur * tupl_ratio

        return relative_dur


    def _cutDuration(self, element, meter):
        """ Cut the duration of given element to the final beat """
        element.getAttribute("dur").setValue(str(meter["unit"]))
        # Remove dots if any
        element.removeAttribute("dots")
        element.removeChildrenByName("dot")

    def _isInStaff(self, el, s_no):
        """ Get all staff numbers of element if element is in given staff"""
        values = self._getSelectedStaffNosFor(el)

        if s_no in values:
                return values
        else:
            values = []
        return values

    def _getSelectedStaffNosFor(self, el):
        """ Get staff numbers of element if the staves in given measure
            are selected"""
        # TODO: CAREFUL - EDITORIAL MARKUP MAY OBFUSCATE THIS
        values = []

        if el.hasAttribute("staff"):
            # Split value of @staff, as it may contain multiple values.
            values = el.getAttribute("staff").getValue().split()
            values = [int(x) for x in values]

        return values