Module music_df.xml_parser.repeats

Functions

def get_repeat_segments(repeats: Dict[int, Dict[str, Dict[str, Any]]],
measure_ends: Dict[int, numbers.Number],
warn: bool = False) ‑> Tuple[List[Tuple[numbers.Number, numbers.Number]], List[Tuple[numbers.Number, numbers.Number]], List[str]]
Expand source code
def get_repeat_segments(
    repeats: t.Dict[int, t.Dict[str, t.Dict[str, t.Any]]],
    measure_ends: t.Dict[int, Number],
    warn: bool = False,
) -> t.Tuple[
    t.List[t.Tuple[Number, Number]],
    t.List[t.Tuple[Number, Number]],
    t.List[str],
]:
    """
    Args:
        repeats: a dictionary:
            - keys are integers indicating bar numbers. (Expected to be
                1-indexed.)
            - values are dictionaries storing repeat attributes where:
                - keys indicate the repeat type ("backward", "forward",
                    or "start-ending")
                - values are a dict that stores the other attributes of the
                    repeat (e.g., "times")
        measure_ends: a dictionary:
            - keys are integers indicating bar numbers. (Expected to be
                1-indexed.)
            - values are Fractions indicating the onset time of the closing
                barline of each measure

    Returns:
        a 3-tuple of "orig_segments", "repeated_segments", and "segment_types".
            The first two items are lists of "start" and "end" times.
            The third item is a list of strings.



    >>> measure_ends = {i + 1: (i + 1) * 4 for i in range(16)}

    >>> get_repeat_segments({}, measure_ends)
    ([(0, inf)], [(0, inf)], ['no_repeat'])

    >>> get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"backward": {"times": 2}}},
    ...     measure_ends)
    ([(0, 8), (0, 8), (8, inf)], [(0, 8), (8, 16), (16, inf)], ['simple_repeat', 'simple_repeat', 'no_repeat'])

    Note that "times" attribute of forward repeats is ignored.

    >>> get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"backward": {"times": 3}}},
    ...     measure_ends)
    ([(0, 8), (0, 8), (0, 8), (8, inf)], [(0, 8), (8, 16), (16, 24), (24, inf)], ['simple_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat'])

    Repeat of a single measure:

    >>> get_repeat_segments(
    ...     {1: {"forward": {"times": 2}, "backward": {"times": 2}}},
    ...     measure_ends)
    ([(0, 4), (0, 4), (4, inf)], [(0, 4), (4, 8), (8, inf)], ['simple_repeat', 'simple_repeat', 'no_repeat'])

    First forward repeat not at start.

    >>> get_repeat_segments(
    ...     {2: {"forward": {"times": 2}},
    ...      3: {"backward": {"times": 2}}},
    ...     measure_ends)
    ([(0, 4), (4, 12), (4, 12), (12, inf)], [(0, 4), (4, 12), (12, 20), (20, inf)], ['no_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat'])

    Multiple repeats.

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"backward": {"times": 2}},
    ...      3: {"forward": {"times": 2}},
    ...      6: {"backward": {"times": 2}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 8), (0, 8), (8, 24), (8, 24), (24, inf)]
    >>> segment_types
    ['simple_repeat', 'simple_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat']

    Repeats with a gap.

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"backward": {"times": 2}},
    ...      4: {"forward": {"times": 2}},
    ...      6: {"backward": {"times": 2}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 8), (0, 8), (8, 12), (12, 24), (12, 24), (24, inf)]
    >>> segment_types
    ['simple_repeat', 'simple_repeat', 'no_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat']

    Endings.

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}},
    ...      3: {"start-ending": {"number": "2"}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 4), (4, 8), (0, 4), (8, inf)]
    >>> segment_types
    ['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2']

    Gap between endings and next repeat.

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}},
    ...      3: {"start-ending": {"number": "2"}},
    ...      5: {"forward": {"times": 2}, "backward": {"times": 2}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 4), (4, 8), (0, 4), (8, 16), (16, 20), (16, 20), (20, inf)]
    >>> segment_types # NB that all music after the start of the second ending goes into 'ending_2'
    ['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2', 'simple_repeat', 'simple_repeat', 'no_repeat']

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"start-ending": {"number": "1"}, "backward": {"times": 3}},
    ...      3: {"start-ending": {"number": "2"}},
    ...      5: {"forward": {"times": 2}, "backward": {"times": 2}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 4), (4, 8), (0, 4), (4, 8), (0, 4), (8, 16), (16, 20), (16, 20), (20, inf)]
    >>> segment_types
    ['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2', 'simple_repeat', 'simple_repeat', 'no_repeat']

    Missing endings after a backward repeat are inferred (note that the second
    ending here is missing):

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 4), (4, 8), (0, 4), (8, inf)]
    >>> segment_types
    ['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2']

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {1: {"forward": {"times": 2}},
    ...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}},
    ...      4: {"forward": {"times": 2}, "backward": {"times": 2}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 4), (4, 8), (0, 4), (8, 12), (12, 16), (12, 16), (16, inf)]
    >>> segment_types
    ['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2', 'simple_repeat', 'simple_repeat', 'no_repeat']

    The "number" attribute of the endings doesn't influence `orig_segments`
    or `repeated_segments`. It is included in `segment_types`, however.

    >>> orig_segments, _, segment_types = get_repeat_segments(
    ...     {2: {"forward": {"times": 2}},
    ...      3: {"start-ending": {"number": "1, 2"}, "backward": {"times": 3}},
    ...      4: {"start-ending": {"number": "3"}, "backward": {"times": 2}},
    ...      5: {"start-ending": {"number": "4"}},
    ...      6: {"forward": {"times": 2}},
    ...      8: {"start-ending": {"number": "1"}},
    ...      9: {"backward": {"times": 2}},
    ...      10: {"start-ending": {"number": "2"}}},
    ...     measure_ends)
    >>> orig_segments
    [(0, 4), (4, 8), (8, 12), (4, 8), (8, 12), (4, 8), (12, 16), (4, 8), (16, 20), (20, 28), (28, 36), (20, 28), (36, inf)]
    >>> segment_types
    ['no_repeat', 'pre_ending_repeat', 'ending_1, 2', 'pre_ending_repeat', 'ending_1, 2', 'pre_ending_repeat', 'ending_3', 'pre_ending_repeat', 'ending_4', 'pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2']

    We infer missing forward repeats:

    >>> get_repeat_segments(
    ...     {2: {"backward": {"times": 2}}},
    ...     measure_ends)
    ([(0, 8), (0, 8), (8, inf)], [(0, 8), (8, 16), (16, inf)], ['simple_repeat', 'simple_repeat', 'no_repeat'])

    >>> get_repeat_segments(
    ...     {2: {"backward": {"times": 2}},
    ...      4: {"backward": {"times": 2}}},
    ...     measure_ends)[0]
    [(0, 8), (0, 8), (8, 16), (8, 16), (16, inf)]

    Missing final repeat:
    >>> get_repeat_segments(
    ...     {1: {"forward": {"times": 2}}},
    ...     measure_ends)
    ([(0, 64), (0, 64)], [(0, 64), (64, 128)], ['simple_repeat', 'simple_repeat'])

    """
    if not repeats:
        return ([(0, float("inf"))], [(0, float("inf"))], ["no_repeat"])
    assert min(repeats) > 0
    assert min(measure_ends) > 0

    last_forward_repeat = 0
    last_backward_repeat = 0
    last_boundary = lambda: max(last_forward_repeat, last_backward_repeat)
    ending_jump_from = None
    segments = []
    segment_types = []
    ending_number = ""
    ending_number_used = True
    for measure_num in sorted(repeats.keys()):
        repeat_dict = repeats[measure_num]
        if "forward" in repeat_dict:
            if "start-ending" in repeat_dict:
                if warn:
                    warnings.warn(
                        f"M. {measure_num} has 'forward' repeat bar and "
                        "'start-ending'; ignoring 'start-ending'"
                    )
            onset = _get_onset_from_measure_num(measure_num - 1, measure_ends)
            if ending_jump_from is not None:
                # there are incomplete endings...
                segments.extend(
                    [
                        (last_forward_repeat, ending_jump_from),
                        (ending_jump_to, onset),
                    ]
                )
                if ending_number_used:
                    ending_number = _update_ending_number(ending_number, warn=warn)
                segment_types.extend(
                    ["pre_ending_repeat", f"ending_{ending_number}"]
                )
                ending_jump_from = None
            last_forward_repeat = onset
        elif "start-ending" in repeat_dict:
            onset = _get_onset_from_measure_num(measure_num - 1, measure_ends)
            if ending_jump_from is None:
                ending_jump_from = onset
            ending_jump_to = onset
            ending_number = repeat_dict["start-ending"]["number"]
            ending_number_used = False
        if "backward" in repeat_dict:
            onset = _get_onset_from_measure_num(measure_num, measure_ends)
            if not segments:
                if last_forward_repeat != 0:
                    segments.append((0, last_forward_repeat))
                    segment_types.append("no_repeat")
            if ending_jump_from is not None:
                for _ in range(repeat_dict["backward"]["times"] - 1):
                    segments.extend(
                        [
                            (last_forward_repeat, ending_jump_from),
                            (ending_jump_to, onset),
                        ]
                    )
                    segment_types.extend(
                        ["pre_ending_repeat", f"ending_{ending_number}"]
                    )
                    ending_number_used = True
                # we need to set "ending_jump_to" here to catch the case where
                #   there is an omitted 2nd ending after a first ending
                ending_jump_to = onset
            else:
                if last_forward_repeat < last_backward_repeat:
                    # there is a missing forward repeat
                    last_forward_repeat = last_backward_repeat
                if segments and last_forward_repeat != segments[-1][1]:
                    segments.append((segments[-1][1], last_forward_repeat))
                    segment_types.append("no_repeat")
                for _ in range(repeat_dict["backward"]["times"]):
                    segments.append((last_forward_repeat, onset))
                    segment_types.append("simple_repeat")
            last_backward_repeat = onset
    if ending_jump_from is not None:
        segments.extend(
            [
                (last_forward_repeat, ending_jump_from),
                (ending_jump_to, float("inf")),
            ]
        )
        if ending_number_used:
            ending_number = _update_ending_number(ending_number, warn=warn)
        segment_types.extend(["pre_ending_repeat", f"ending_{ending_number}"])
    if last_forward_repeat == last_boundary():
        # There is a missing backward repeat at the end of the score.
        segments.extend(
            [(last_forward_repeat, measure_ends[max(measure_ends)])] * 2
        )
        segment_types.extend(["simple_repeat", "simple_repeat"])
    if segments[-1][1] < measure_ends[max(measure_ends)]:
        segments.append((last_boundary(), float("inf")))
        segment_types.append("no_repeat")
    segment_durs = [segment[1] - segment[0] for segment in segments]
    segment_ends = list(itertools.accumulate(segment_durs))
    segment_starts = [0] + segment_ends[:-1]
    return segments, list(zip(segment_starts, segment_ends)), segment_types

Args

repeats
a dictionary: - keys are integers indicating bar numbers. (Expected to be 1-indexed.) - values are dictionaries storing repeat attributes where: - keys indicate the repeat type ("backward", "forward", or "start-ending") - values are a dict that stores the other attributes of the repeat (e.g., "times")
measure_ends
a dictionary: - keys are integers indicating bar numbers. (Expected to be 1-indexed.) - values are Fractions indicating the onset time of the closing barline of each measure

Returns

a 3-tuple of "orig_segments", "repeated_segments", and "segment_types". The first two items are lists of "start" and "end" times. The third item is a list of strings.

>>> measure_ends = {i + 1: (i + 1) * 4 for i in range(16)}
>>> get_repeat_segments({}, measure_ends)
([(0, inf)], [(0, inf)], ['no_repeat'])
>>> get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"backward": {"times": 2}}},
...     measure_ends)
([(0, 8), (0, 8), (8, inf)], [(0, 8), (8, 16), (16, inf)], ['simple_repeat', 'simple_repeat', 'no_repeat'])

Note that "times" attribute of forward repeats is ignored.

>>> get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"backward": {"times": 3}}},
...     measure_ends)
([(0, 8), (0, 8), (0, 8), (8, inf)], [(0, 8), (8, 16), (16, 24), (24, inf)], ['simple_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat'])

Repeat of a single measure:

>>> get_repeat_segments(
...     {1: {"forward": {"times": 2}, "backward": {"times": 2}}},
...     measure_ends)
([(0, 4), (0, 4), (4, inf)], [(0, 4), (4, 8), (8, inf)], ['simple_repeat', 'simple_repeat', 'no_repeat'])

First forward repeat not at start.

>>> get_repeat_segments(
...     {2: {"forward": {"times": 2}},
...      3: {"backward": {"times": 2}}},
...     measure_ends)
([(0, 4), (4, 12), (4, 12), (12, inf)], [(0, 4), (4, 12), (12, 20), (20, inf)], ['no_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat'])

Multiple repeats.

>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"backward": {"times": 2}},
...      3: {"forward": {"times": 2}},
...      6: {"backward": {"times": 2}}},
...     measure_ends)
>>> orig_segments
[(0, 8), (0, 8), (8, 24), (8, 24), (24, inf)]
>>> segment_types
['simple_repeat', 'simple_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat']

Repeats with a gap.

>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"backward": {"times": 2}},
...      4: {"forward": {"times": 2}},
...      6: {"backward": {"times": 2}}},
...     measure_ends)
>>> orig_segments
[(0, 8), (0, 8), (8, 12), (12, 24), (12, 24), (24, inf)]
>>> segment_types
['simple_repeat', 'simple_repeat', 'no_repeat', 'simple_repeat', 'simple_repeat', 'no_repeat']

Endings.

>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}},
...      3: {"start-ending": {"number": "2"}}},
...     measure_ends)
>>> orig_segments
[(0, 4), (4, 8), (0, 4), (8, inf)]
>>> segment_types
['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2']

Gap between endings and next repeat.

>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}},
...      3: {"start-ending": {"number": "2"}},
...      5: {"forward": {"times": 2}, "backward": {"times": 2}}},
...     measure_ends)
>>> orig_segments
[(0, 4), (4, 8), (0, 4), (8, 16), (16, 20), (16, 20), (20, inf)]
>>> segment_types # NB that all music after the start of the second ending goes into 'ending_2'
['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2', 'simple_repeat', 'simple_repeat', 'no_repeat']
>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"start-ending": {"number": "1"}, "backward": {"times": 3}},
...      3: {"start-ending": {"number": "2"}},
...      5: {"forward": {"times": 2}, "backward": {"times": 2}}},
...     measure_ends)
>>> orig_segments
[(0, 4), (4, 8), (0, 4), (4, 8), (0, 4), (8, 16), (16, 20), (16, 20), (20, inf)]
>>> segment_types
['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2', 'simple_repeat', 'simple_repeat', 'no_repeat']

Missing endings after a backward repeat are inferred (note that the second ending here is missing):

>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}}},
...     measure_ends)
>>> orig_segments
[(0, 4), (4, 8), (0, 4), (8, inf)]
>>> segment_types
['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2']
>>> orig_segments, _, segment_types = get_repeat_segments(
...     {1: {"forward": {"times": 2}},
...      2: {"start-ending": {"number": "1"}, "backward": {"times": 2}},
...      4: {"forward": {"times": 2}, "backward": {"times": 2}}},
...     measure_ends)
>>> orig_segments
[(0, 4), (4, 8), (0, 4), (8, 12), (12, 16), (12, 16), (16, inf)]
>>> segment_types
['pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2', 'simple_repeat', 'simple_repeat', 'no_repeat']

The "number" attribute of the endings doesn't influence orig_segments or repeated_segments. It is included in segment_types, however.

>>> orig_segments, _, segment_types = get_repeat_segments(
...     {2: {"forward": {"times": 2}},
...      3: {"start-ending": {"number": "1, 2"}, "backward": {"times": 3}},
...      4: {"start-ending": {"number": "3"}, "backward": {"times": 2}},
...      5: {"start-ending": {"number": "4"}},
...      6: {"forward": {"times": 2}},
...      8: {"start-ending": {"number": "1"}},
...      9: {"backward": {"times": 2}},
...      10: {"start-ending": {"number": "2"}}},
...     measure_ends)
>>> orig_segments
[(0, 4), (4, 8), (8, 12), (4, 8), (8, 12), (4, 8), (12, 16), (4, 8), (16, 20), (20, 28), (28, 36), (20, 28), (36, inf)]
>>> segment_types
['no_repeat', 'pre_ending_repeat', 'ending_1, 2', 'pre_ending_repeat', 'ending_1, 2', 'pre_ending_repeat', 'ending_3', 'pre_ending_repeat', 'ending_4', 'pre_ending_repeat', 'ending_1', 'pre_ending_repeat', 'ending_2']

We infer missing forward repeats:

>>> get_repeat_segments(
...     {2: {"backward": {"times": 2}}},
...     measure_ends)
([(0, 8), (0, 8), (8, inf)], [(0, 8), (8, 16), (16, inf)], ['simple_repeat', 'simple_repeat', 'no_repeat'])
>>> get_repeat_segments(
...     {2: {"backward": {"times": 2}},
...      4: {"backward": {"times": 2}}},
...     measure_ends)[0]
[(0, 8), (0, 8), (8, 16), (8, 16), (16, inf)]

Missing final repeat:

>>> get_repeat_segments(
...     {1: {"forward": {"times": 2}}},
...     measure_ends)
([(0, 64), (0, 64)], [(0, 64), (64, 128)], ['simple_repeat', 'simple_repeat'])