Example #1
0
def add_key_signatures(nodes: List[Node]) -> List[Node]:
    """Heuristic for deciding which accidentals are inline,
    and which should be interpreted as components of a key signature.

    Assumes staffline relationships have already been inferred.

    The heuristic is defined for each staff S as the following:

    * Take the leftmost clef C.
    * Take the leftmost notehead (incl. grace notes) N.
    * Take all accidentals A_S that overlap the space between C and N
      horizontally (including C, not including N), and overlap the staff.
    * Order A_S left-to-right
    * Set m = C
    * Initialize key signature K_S = {}
    * For each a_S in A_S:
    * if it is closer to m than to N, then:
    *   add a_S to K_S,
    *   set m = a_S
    * else:
    *   break

    Note that this modifies the accidentals and staffs in-place: they get inlinks
    from their respective key signatures.
    """
    graph = NotationGraph(nodes)

    new_node_id = max([m.id for m in nodes]) + 1

    key_signatures = []

    staffs = [m for m in nodes if m.class_name == _CONST.STAFF]
    for s in staffs:

        # Take the leftmost clef C.
        clefs = graph.parents(s.id, class_filter=_CONST.CLEF_CLASS_NAMES)
        if len(clefs) == 0:
            continue
        leftmost_clef = min(clefs, key=lambda x: x.left)

        # Take the leftmost notehead (incl. grace notes) N.
        noteheads = graph.parents(s.id,
                                  class_filter=_CONST.NOTEHEAD_CLASS_NAMES)
        if len(noteheads) == 0:
            continue
        leftmost_notehead = min(noteheads, key=lambda x: x.left)

        # Take all accidentals A_S that fall between C and N
        # horizontally, and overlap the staff.
        all_accidentals = [
            m for m in nodes if m.class_name in _CONST.ACCIDENTAL_CLASS_NAMES
        ]
        relevant_acc_bbox = s.top, leftmost_clef.left, s.bottom, leftmost_notehead.left
        relevant_accidentals = [
            m for m in all_accidentals if bounding_box_intersection(
                m.bounding_box, relevant_acc_bbox) is not None
        ]

        # Order A_S left-to-right.
        ordered_accidentals = sorted(relevant_accidentals,
                                     key=lambda x: x.left)

        # Set m = C
        current_lstop = leftmost_clef.right
        key_signature_accidentals = []

        # Iterate over accidentals; check if they are closer to lstop
        # than to the first notehead
        for a in ordered_accidentals:
            if (a.left - current_lstop) < (leftmost_notehead.left - a.right):
                key_signature_accidentals.append(a)
                current_lstop = a.right
            else:
                break

        # Build key signature and connect it to staff
        if len(key_signature_accidentals) > 0:
            key_signature_class_names = list(_CONST.KEY_SIGNATURE)[0]
            # Note: there might be spurious links from the accidentals
            # to notheads, if they were mis-interpreted during parsing.
            # This actually might not matter; needs testing.
            key_signature = merge_multiple_nodes(
                key_signature_accidentals,
                class_name=key_signature_class_names,
                id_=new_node_id)
            new_node_id += 1
            link_nodes(key_signature, s)
            for a in key_signature_accidentals:
                link_nodes(key_signature, a)

            key_signatures.append(key_signature)

    logging.info('Adding {} key signatures'.format(len(key_signatures)))
    return nodes + key_signatures
Example #2
0
def add_staff_relationships(nodes: List[Node],
                            notehead_staffspace_threshold: float = 0.2) -> List[Node]:
    id_to_node_mapping = {node.id: node for node in nodes}

    ON_STAFFLINE_RATIO_THRESHOLD = notehead_staffspace_threshold

    ##########################################################################
    logging.info('Find the staff-related symbols')
    staffs = [c for c in nodes if c.class_name == _CONST.STAFF]

    staff_related_symbols = defaultdict(list)  # type: defaultdict[str, List[Node]]
    notehead_symbols = defaultdict(list)  # type: defaultdict[str, List[Node]]
    rest_symbols = defaultdict(list)  # type: defaultdict[str, List[Node]]
    for node in nodes:
        if node.class_name in _CONST.STAFF_RELATED_CLASS_NAMES:
            staff_related_symbols[node.class_name].append(node)
        if node.class_name in _CONST.NOTEHEAD_CLASS_NAMES:
            notehead_symbols[node.class_name].append(node)
        if node.class_name in _CONST.REST_CLASS_NAMES:
            rest_symbols[node.class_name].append(node)

    ##########################################################################
    logging.info('Adding staff relationships')
    #  - Which direction do the relationships lead in?
    #    Need to define this.
    #
    # Staff -> symbol?
    # Symbol -> staff?
    # It does not really matter, but it's more intuitive to attach symbols
    # onto a pre-existing staff. So, symbol -> staff.
    for class_name, nodes in list(staff_related_symbols.items()):
        for node in nodes:  # type: Node
            # Find the related staff. Relatedness is measured by row overlap.
            # That means we have to modify the staff bounding box to lead
            # from the leftmost to the rightmost column. This holds
            # especially for the staff_grouping symbols.
            for staff in staffs:
                top, left, bottom, right = staff.bounding_box
                left = 0
                right = max(right, node.right)
                if node.overlaps((top, left, bottom, right)):
                    link_nodes(node, staff)

    ##########################################################################
    logging.info('Adding rest --> staff relationships.')
    for class_name, nodes in list(rest_symbols.items()):
        for node in nodes:  # type: Node
            closest_staff = min([s for s in staffs],
                                key=lambda x: ((x.bottom + x.top) / 2. - (node.bottom + node.top) / 2.) ** 2)
            link_nodes(node, closest_staff)

    ##########################################################################
    logging.info('Adding notehead relationships.')

    # NOTE:
    # This part should NOT rely on staffspace masks in any way!
    # They are highly unreliable.

    # Sort the staff objects top-down. Assumes stafflines do not cross,
    # and that there are no crazy curves at the end that would make the lower
    # stafflines stick out over the ones above them...
    stafflines = [c for c in nodes if c.class_name == _CONST.STAFFLINE]
    stafflines = sorted(stafflines, key=lambda c: c.top)
    staffspaces = [c for c in nodes if c.class_name == _CONST.STAFFSPACE]
    staffspaces = sorted(staffspaces, key=lambda c: c.top)
    staves = [c for c in nodes if c.class_name == _CONST.STAFF]
    staves = sorted(staves, key=lambda c: c.top)

    # Indexing data structures.
    #
    # We need to know:
    #  - per staffline and staffspace: its containing staff
    _staff_per_ss_sl = {}
    #  - per staffline and staffspace: its index (top to bottom) within the staff
    _ss_sl_idx_wrt_staff = {}
    # Reverse indexes:
    # If I know which staff (by id) and which index of staffline/staffspace,
    # I want to retrieve the given staffline/staffspace Node:
    _staff_and_idx2ss = defaultdict(dict)
    _staff_and_idx2sl = defaultdict(dict)

    # Build the indexes
    for _staff in staves:
        # Keep the top-down ordering from above:
        _s_stafflines = [_staffline for _staffline in stafflines
                         if _staff.id in _staffline.inlinks]
        _s_staffspaces = [_staffspace for _staffspace in staffspaces
                          if _staff.id in _staffspace.inlinks]
        for i, _sl in enumerate(_s_stafflines):
            _staff_per_ss_sl[_sl.id] = _staff
            _ss_sl_idx_wrt_staff[_sl.id] = i
            _staff_and_idx2sl[_staff.id][i] = _sl
            logging.debug('Staff {0}: stafflines {1}'.format(_staff.id,
                                                             _staff_and_idx2sl[_staff.id]))
        for i, _ss in enumerate(_s_staffspaces):
            _staff_per_ss_sl[_ss.id] = _staff
            _ss_sl_idx_wrt_staff[_ss.id] = i
            _staff_and_idx2ss[_staff.id][i] = _ss

    logging.debug(pprint.pformat(dict(_staff_and_idx2ss)))

    for class_name, nodes in list(notehead_symbols.items()):
        for node in nodes:

            ct, cl, cb, cr = node.bounding_box

            ################
            # Add relationship to given staffline or staffspace.

            # If notehead has leger lines, skip it for now.
            has_leger_line = False
            for o in node.outlinks:
                if id_to_node_mapping[o].class_name == _CONST.LEGER_LINE:
                    has_leger_line = True
                    break

            if has_leger_line:
                # Attach to the appropriate staff:
                # meaning, staff closest to the innermost leger line.
                lls = [id_to_node_mapping[o] for o in node.outlinks
                       if id_to_node_mapping[o].class_name == _CONST.LEGER_LINE]
                # Furthest from notehead's top is innermost.
                # (If notehead is below staff and crosses a ll., one
                #  of these numbers will be negative. But that doesn't matter.)
                ll_max_dist = max(lls, key=lambda ll: ll.top - node.top)
                # Find closest staff to max-dist leger ine
                staff_min_dist = min(staves,
                                     key=lambda ss: min((ll_max_dist.bottom - ss.top) ** 2,
                                                        (ll_max_dist.top - ss.bottom) ** 2))
                link_nodes(node, staff_min_dist)
                continue

            # - Find the related staffline.
            # - Because of curved stafflines, this has to be done w.r.t.
            #   the horizontal position of the notehead.
            # - Also, because stafflines are NOT filled in (they do not have
            #   intersections annotated), it is necessary to use a wider
            #   window than just the notehead.
            # - We will assume that STAFFLINES DO NOT CROSS.
            #   (That is a reasonable assumption.)
            #
            # - For now, we only work with more or less straight stafflines.

            overlapped_stafflines = []
            overlapped_staffline_idxs = []
            for i, staff in enumerate(stafflines):
                # This is the assumption of straight stafflines!
                if (ct <= staff.top <= cb) or (ct <= staff.bottom <= cb):
                    overlapped_stafflines.append(staff)
                    overlapped_staffline_idxs.append(i)

            if node.id < 10:
                logging.debug('Notehead {0} ({1}): overlaps {2} stafflines'.format(node.id,
                                                                                   node.bounding_box,
                                                                                   len(overlapped_stafflines), ))

            if len(overlapped_stafflines) == 1:
                staff = overlapped_stafflines[0]
                dtop = staff.top - ct
                dbottom = cb - staff.bottom
                if min(dtop, dbottom) / max(dtop, dbottom) < ON_STAFFLINE_RATIO_THRESHOLD:
                    logging.info('Notehead {0}, staffline {1}: very small ratio {2:.2f}'
                                 ''.format(node.id, staff.id,
                                           min(dtop, dbottom) / max(dtop, dbottom)))
                    # Staffspace?
                    #
                    # To get staffspace:
                    #  - Get orientation (below? above?)
                    _is_staffspace_above = False
                    if dtop > dbottom:
                        _is_staffspace_above = True

                    #  - Find staffspaces adjacent to the overlapped staffline.
                    # NOTE: this will fail with single-staffline staves, because
                    #       they do NOT have the surrounding staffspaces defined...
                    _staffline_idx_wrt_staff = _ss_sl_idx_wrt_staff[staff.id]
                    if _is_staffspace_above:
                        _staffspace_idx_wrt_staff = _staffline_idx_wrt_staff
                    else:
                        _staffspace_idx_wrt_staff = _staffline_idx_wrt_staff + 1

                    # Retrieve the given staffsapce
                    _staff = _staff_per_ss_sl[staff.id]
                    tgt_staffspace = _staff_and_idx2ss[_staff.id][_staffspace_idx_wrt_staff]
                    # Link to staffspace
                    link_nodes(node, tgt_staffspace)
                    # And link to staff
                    _c_staff = _staff_per_ss_sl[tgt_staffspace.id]
                    link_nodes(node, _c_staff)

                else:
                    # Staffline!
                    link_nodes(node, staff)
                    # And staff:
                    _c_staff = _staff_per_ss_sl[staff.id]
                    link_nodes(node, _c_staff)

            elif len(overlapped_stafflines) == 0:
                # Staffspace!
                # Link to the staffspace with which the notehead has
                # greatest vertical overlap.
                #
                # Interesting corner case:
                # Sometimes noteheads "hang out" of the upper/lower
                # staffspace, so they are not entirely covered.
                overlapped_staffspaces = {}
                for _ss_i, staff in enumerate(staffspaces):
                    if staff.top <= node.top <= staff.bottom:
                        overlapped_staffspaces[_ss_i] = min(staff.bottom, node.bottom) - node.top
                    elif node.top <= staff.top <= node.bottom:
                        overlapped_staffspaces[_ss_i] = staff.bottom - max(node.top, staff.top)

                if len(overlapped_staffspaces) == 0:
                    logging.warning('Notehead {0}: no overlapped staffline object, no leger line!'
                                    ''.format(node.id))
                _ss_i_max = max(list(overlapped_staffspaces.keys()),
                                key=lambda x: overlapped_staffspaces[x])
                max_overlap_staffspace = staffspaces[_ss_i_max]
                link_nodes(node, max_overlap_staffspace)
                _c_staff = _staff_per_ss_sl[max_overlap_staffspace.id]
                link_nodes(node, _c_staff)

            elif len(overlapped_stafflines) == 2:
                # Staffspace between those two lines.
                s1 = overlapped_stafflines[0]
                s2 = overlapped_stafflines[1]

                _staff1 = _staff_per_ss_sl[s1.id]
                _staff2 = _staff_per_ss_sl[s2.id]
                if _staff1.id != _staff2.id:
                    raise ValueError('Really weird notehead overlapping two stafflines'
                                     ' from two different staves: {0}'.format(node.id))

                _staffspace_idx = _ss_sl_idx_wrt_staff[s2.id]
                staff = _staff_and_idx2ss[_staff2.id][_staffspace_idx]
                link_nodes(node, staff)
                # And link to staff:
                _c_staff = _staff_per_ss_sl[staff.id]
                link_nodes(node, _c_staff)

            elif len(overlapped_stafflines) > 2:
                raise ValueError('Really weird notehead overlapping more than 2 stafflines:'
                                 ' {0}'.format(node.id))

    return nodes
Example #3
0
def add_staff_relationships(nodes: List[Node],
                            notehead_staffspace_threshold: float = 0.2,
                            reprocess_noteheads_inside_staff_with_lls: bool = True):
    """Adds the relationships from various symbols to staff objects:
    stafflines, staffspaces, and staffs.

    :param nodes: The list of Nodes in the document. Must
        include the staff objects.

    :param notehead_staffspace_threshold: A notehead is considered to be
        on a staffline if it intersects a staffline, and the ratio between
        how far above the staffline and how far below the staffline it reaches
        is at least this (default: 0.2). The ratio is computed both ways:
        d_top / d_bottom and d_bottom / d_top, and the minimum is taken,
        so the default in effect restricts d_top / d_bottom between 0.2 and 0.8:
        in other words, the imbalance of the notehead's bounding box around
        the staffline should be less than 1:4.

    :param reprocess_noteheads_inside_staff_with_lls: If set to True, will check against noteheads
        that are connected to leger lines, but intersect a staffline. If found,
        will remove their edges before further processing, so that the noteheads
        will seem properly unprocessed.

        Note that this handling is a bit ad-hoc for now. However, we currently
        do not have a better place to fit this in, since the Best Practices
        currently call for first applying the syntactic parser and then
        adding staff relationships.

    :return: The list of Nodes corresponding to the new graph.
    """
    graph = NotationGraph(nodes)
    id_to_node_mapping = {node.id: node for node in nodes}

    ON_STAFFLINE_RATIO_TRHESHOLD = notehead_staffspace_threshold

    ##########################################################################
    if reprocess_noteheads_inside_staff_with_lls:
        ll_noteheads_on_staff = find_noteheads_on_staff_linked_to_leger_line(nodes)
        logging.info('Reprocessing noteheads that are inside a staff, but have links'
                     ' to leger lines. Found {0} such noteheads.'
                     ''.format(len(ll_noteheads_on_staff)))
        for n in ll_noteheads_on_staff:
            # Remove all links to leger lines.
            leger_lines = graph.children(n, class_filter=[_CONST.LEGER_LINE_CLASS_NAME])
            for ll in leger_lines:
                graph.remove_edge(n.id, ll.id)

    ##########################################################################
    logging.info('Find the staff-related symbols')
    staffs = [c for c in nodes if c.class_name == _CONST.STAFF_CLASS_NAME]

    staff_related_symbols = collections.defaultdict(list)
    notehead_symbols = collections.defaultdict(list)
    rest_symbols = collections.defaultdict(list)
    for node in nodes:
        if node.class_name in _CONST.STAFF_RELATED_CLASS_NAMES:
            # Check if it already has a staff link
            if not graph.has_children(node, [_CONST.STAFF_CLASS_NAME]):
                staff_related_symbols[node.class_name].append(node)
        if node.class_name in _CONST.NOTEHEAD_CLASS_NAMES:
            if not graph.has_children(node, [_CONST.STAFF_CLASS_NAME]):
                notehead_symbols[node.class_name].append(node)
        if node.class_name in _CONST.REST_CLASS_NAMES:
            if not graph.has_children(node, [_CONST.STAFF_CLASS_NAME]):
                rest_symbols[node.class_name].append(node)

    ##########################################################################
    logging.info('Adding staff relationships')
    #  - Which direction do the relationships lead in?
    #    Need to define this.
    #
    # Staff -> symbol?
    # Symbol -> staff?
    # It does not really matter, but it's more intuitive to attach symbols
    # onto a pre-existing staff. So, symbol -> staff.
    for class_name, cs in list(staff_related_symbols.items()):
        for node in cs:
            # Find the related staff. Relatedness is measured by row overlap.
            # That means we have to modify the staff bounding box to lead
            # from the leftmost to the rightmost column. This holds
            # especially for the staff_grouping symbols.
            for s in staffs:
                st, sl, sb, sr = s.bounding_box
                sl = 0
                sr = max(sr, node.right)
                if node.overlaps((st, sl, sb, sr)):
                    link_nodes(node, s, check_that_nodes_have_the_same_document=False)

    ##########################################################################
    logging.info('Adding rest --> staff relationships.')
    for class_name, cs in list(rest_symbols.items()):
        for node in cs:
            closest_staff = min([s for s in staffs],
                                key=lambda x: ((x.bottom + x.top) / 2. - (
                                        node.bottom + node.top) / 2.) ** 2)
            link_nodes(node, closest_staff, check_that_nodes_have_the_same_document=False)

    ##########################################################################
    logging.info('Adding notehead relationships.')

    # NOTE:
    # This part should NOT rely on staffspace masks in any way!
    # They are highly unreliable.

    # Sort the staff objects top-down. Assumes stafflines do not cross,
    # and that there are no crazy curves at the end that would make the lower
    # stafflines stick out over the ones above them...
    stafflines = [c for c in nodes if c.class_name == _CONST.STAFFLINE_CLASS_NAME]
    stafflines = sorted(stafflines, key=lambda c: c.top)
    staffspaces = [c for c in nodes if c.class_name == _CONST.STAFFSPACE_CLASS_NAME]
    staffspaces = sorted(staffspaces, key=lambda c: c.top)
    staves = [c for c in nodes if c.class_name == _CONST.STAFF_CLASS_NAME]
    staves = sorted(staves, key=lambda c: c.top)

    logging.info('Stafflines: {0}'.format(len(stafflines)))
    logging.info('Staffspaces: {0}'.format(len(staffspaces)))
    logging.info('Staves: {0}'.format(len(staves)))

    # Indexing data structures.
    #
    # We need to know:
    #  - per staffline and staffspace: its containing staff
    _staff_per_ss_sl = {}
    #  - per staffline and staffspace: its index (top to bottom) within the staff
    _ss_sl_idx_wrt_staff = {}
    # Reverse indexes:
    # If I know which staff (by id) and which index of staffline/staffspace,
    # I want to retrieve the given staffline/staffspace Node:
    _staff_and_idx2ss = collections.defaultdict(dict)
    _staff_and_idx2sl = collections.defaultdict(dict)

    # Build the indexes
    for _staff in staves:
        # Keep the top-down ordering from above:
        _s_stafflines = [_staffline for _staffline in stafflines
                         if _staff.id in _staffline.inlinks]
        _s_staffspaces = [_staffspace for _staffspace in staffspaces
                          if _staff.id in _staffspace.inlinks]
        for i, _sl in enumerate(_s_stafflines):
            _staff_per_ss_sl[_sl.id] = _staff
            _ss_sl_idx_wrt_staff[_sl.id] = i
            _staff_and_idx2sl[_staff.id][i] = _sl
            logging.debug('Staff {0}: stafflines {1}'.format(_staff.id,
                                                             _staff_and_idx2sl[_staff.id]))
        for i, _ss in enumerate(_s_staffspaces):
            _staff_per_ss_sl[_ss.id] = _staff
            _ss_sl_idx_wrt_staff[_ss.id] = i
            _staff_and_idx2ss[_staff.id][i] = _ss

    logging.debug(pprint.pformat(dict(_staff_and_idx2ss)))

    for class_name, cs in list(notehead_symbols.items()):
        for node in cs:

            ct, cl, cb, cr = node.bounding_box

            ################
            # Add relationship to given staffline or staffspace.

            # If notehead has leger lines, skip it for now.
            has_leger_line = False
            for outlink in node.outlinks:
                if id_to_node_mapping[outlink].class_name == _CONST.LEGER_LINE_CLASS_NAME:
                    has_leger_line = True
                    break

            if has_leger_line:
                # Attach to the appropriate staff:
                # meaning, staff closest to the innermost leger line.
                leger_lines = [id_to_node_mapping[o] for o in node.outlinks
                               if id_to_node_mapping[o].class_name == _CONST.LEGER_LINE_CLASS_NAME]
                # Furthest from notehead's top is innermost.
                # (If notehead is below staff and crosses a ll., one
                #  of these numbers will be negative. But that doesn't matter.)
                ll_max_dist = max(leger_lines, key=lambda leger_line: leger_line.top - node.top)
                # Find closest staff to max-dist leger ine
                staff_min_dist = min(staves,
                                     key=lambda ss: min((ll_max_dist.bottom - ss.top) ** 2,
                                                        (ll_max_dist.top - ss.bottom) ** 2))
                distance_of_closest_staff = (ll_max_dist.top + ll_max_dist.bottom) / 2 \
                                            - (staff_min_dist.top + staff_min_dist.bottom) / 2
                if numpy.abs(distance_of_closest_staff) > (50 + 0.5 * staff_min_dist.height):
                    logging.debug('Trying to join notehead with leger line to staff,'
                                  ' but the distance is larger than 50. Notehead: {0},'
                                  ' leger line: {1}, staff: {2}, distance: {3}'
                                  ''.format(node.uid, ll_max_dist.uid, staff_min_dist.id,
                                            distance_of_closest_staff))
                else:
                    link_nodes(node, staff_min_dist, check_that_nodes_have_the_same_document=False)
                continue

            # - Find the related staffline.
            # - Because of curved stafflines, this has to be done w.r.t.
            #   the horizontal position of the notehead.
            # - Also, because stafflines are NOT filled in (they do not have
            #   intersections annotated), it is necessary to use a wider
            #   window than just the notehead.
            # - We will assume that STAFFLINES DO NOT CROSS.
            #   (That is a reasonable assumption.)
            #
            # - For now, we only work with more or less straight stafflines.

            overlapped_stafflines = []
            overlapped_staffline_idxs = []
            for i, s in enumerate(stafflines):
                # This is the assumption of straight stafflines!
                if (ct <= s.top <= cb) or (ct <= s.bottom <= cb):
                    overlapped_stafflines.append(s)
                    overlapped_staffline_idxs.append(i)

            if node.id < 10:
                logging.info('Notehead {0} ({1}): overlaps {2} stafflines'.format(node.uid, node.bounding_box,
                                                                                  len(overlapped_stafflines)))

            if len(overlapped_stafflines) == 1:
                s = overlapped_stafflines[0]
                dtop = s.top - ct
                dbottom = cb - s.bottom

                logging.info('Notehead {0}, staffline {1}: ratio {2:.2f}'
                             ''.format(node.id, s.id, min(dtop, dbottom) / max(dtop, dbottom)))
                if min(dtop, dbottom) / max(dtop, dbottom) < ON_STAFFLINE_RATIO_TRHESHOLD:
                    # Staffspace?
                    #
                    # To get staffspace:
                    #  - Get orientation (below? above?)
                    _is_staffspace_above = False
                    if dtop > dbottom:
                        _is_staffspace_above = True

                    #  - Find staffspaces adjacent to the overlapped staffline.
                    # NOTE: this will fail with single-staffline staves, because
                    #       they do NOT have the surrounding staffspaces defined...
                    _staffline_idx_wrt_staff = _ss_sl_idx_wrt_staff[s.id]
                    if _is_staffspace_above:
                        _staffspace_idx_wrt_staff = _staffline_idx_wrt_staff
                    else:
                        _staffspace_idx_wrt_staff = _staffline_idx_wrt_staff + 1

                    # Retrieve the given staffsapce
                    _staff = _staff_per_ss_sl[s.id]
                    tgt_staffspace = _staff_and_idx2ss[_staff.id][_staffspace_idx_wrt_staff]
                    # Link to staffspace
                    link_nodes(node, tgt_staffspace, check_that_nodes_have_the_same_document=False)
                    # And link to staff
                    _c_staff = _staff_per_ss_sl[tgt_staffspace.id]
                    link_nodes(node, _c_staff, check_that_nodes_have_the_same_document=False)

                else:
                    # Staffline!
                    link_nodes(node, s, check_that_nodes_have_the_same_document=False)
                    # And staff:
                    _c_staff = _staff_per_ss_sl[s.id]
                    link_nodes(node, _c_staff, check_that_nodes_have_the_same_document=False)
            elif len(overlapped_stafflines) == 0:
                # Staffspace!
                # Link to the staffspace with which the notehead has
                # greatest vertical overlap.
                #
                # Interesting corner case:
                # Sometimes noteheads "hang out" of the upper/lower
                # staffspace, so they are not entirely covered.
                overlapped_staffspaces = {}
                for _ss_i, s in enumerate(staffspaces):
                    if s.top <= node.top <= s.bottom:
                        overlapped_staffspaces[_ss_i] = min(s.bottom, node.bottom) - node.top
                    elif node.top <= s.top <= node.bottom:
                        overlapped_staffspaces[_ss_i] = s.bottom - max(node.top, s.top)

                if len(overlapped_staffspaces) == 0:
                    logging.warning('Notehead {0}: no overlapped staffline object, no leger line!'
                                    ' Expecting it will be attached to leger line and staff later on.'
                                    ''.format(node.uid))
                    continue

                _ss_i_max = max(list(overlapped_staffspaces.keys()),
                                key=lambda x: overlapped_staffspaces[x])
                max_overlap_staffspace = staffspaces[_ss_i_max]
                link_nodes(node, max_overlap_staffspace, check_that_nodes_have_the_same_document=False)
                _c_staff = _staff_per_ss_sl[max_overlap_staffspace.id]
                link_nodes(node, _c_staff, check_that_nodes_have_the_same_document=False)

            elif len(overlapped_stafflines) == 2:
                # Staffspace between those two lines.
                s1 = overlapped_stafflines[0]
                s2 = overlapped_stafflines[1]

                _staff1 = _staff_per_ss_sl[s1.id]
                _staff2 = _staff_per_ss_sl[s2.id]
                if _staff1.id != _staff2.id:
                    raise ValueError('Really weird notehead overlapping two stafflines'
                                     ' from two different staves: {0}'.format(node.uid))

                _staffspace_idx = _ss_sl_idx_wrt_staff[s2.id]
                s = _staff_and_idx2ss[_staff2.id][_staffspace_idx]
                link_nodes(node, s, check_that_nodes_have_the_same_document=False)
                # And link to staff:
                _c_staff = _staff_per_ss_sl[s.id]
                link_nodes(node, _c_staff, check_that_nodes_have_the_same_document=False)

            elif len(overlapped_stafflines) > 2:
                logging.warning('Really weird notehead overlapping more than 2 stafflines:'
                                ' {0} (permissive: linking to middle staffline)'.format(node.uid))
                # use the middle staffline -- this will be an error anyway,
                # but we want to export some MIDI more or less no matter what
                s_middle = overlapped_stafflines[len(overlapped_stafflines) // 2]
                link_nodes(node, s_middle, check_that_nodes_have_the_same_document=False)
                # And staff:
                _c_staff = _staff_per_ss_sl[s_middle.id]
                link_nodes(node, _c_staff, check_that_nodes_have_the_same_document=False)

    return nodes