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
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
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