algorithms/asa.py
def _match_fronts(onset_fronts, offset_fronts, onsets, offsets, debug=False):
    """
    Returns a segmentation mask, which looks like this:
    frequency 1: 0 0 4 4 4 4 4 0 0 5 5 5
    frequency 2: 0 4 4 4 4 4 0 0 0 0 5 5
    frequency 3: 0 4 4 4 4 4 4 4 5 5 5 5

    That is, each item in the array is either a 0 (not part of a segment) or a positive
    integer which indicates which segment the sample in that frequency band belongs to.
    """
    def printd(*args, **kwargs):
        if debug:
            print(*args, **kwargs)

    # Make copies of everything, so we can do whatever we want with them
    onset_fronts = np.copy(onset_fronts)
    offset_fronts = np.copy(offset_fronts)
    onsets = np.copy(onsets)
    offsets = np.copy(offsets)

    # This is what we will return
    segmentation_mask = np.zeros_like(onset_fronts)

    # - Take the first frequency in the onset_fronts matrix
    #     [ s s s s s s s s s] <-- This frequency
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]

    # - Follow it along in time like this:

    #     first sample    last sample
    #       v      -->      v
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]

    # until you get to the first onset front in that frequency

    #     Here it is!
    #         v
    #     [ . O . . . . . . .]
    #     [ . . O . . . . . .]
    #     [ . . O . . . . . .]
    #     [ . O . . . . . . .]
    #     [ O . . . . . . . .]

    resulting_onset_fronts = np.copy(onset_fronts)
    printd("    -> Dealing with onset fronts...")
    for onset_front_id in _get_front_ids_one_at_a_time(onset_fronts):
        printd("      -> Dealing with onset front", int(onset_front_id))
        front_is_complete = False
        while not front_is_complete:
            # - Now, starting at this onset front in each frequency, find that onset's corresponding offset

            #     [ . O . . . . F . .]
            #     [ . . O . . . F . .]
            #     [ . . O . F . . . .]
            #     [ . O . F . . . . .]
            #     [ O F . . . . . . .]

            corresponding_offsets = _get_corresponding_offsets(resulting_onset_fronts, onset_front_id, onsets, offsets)

            # It is possible that onset_front_id has been removed from resulting_onset_fronts,
            # if so, skip it and move on to the next onset front (we are iterating over the original
            # to keep the iterator valid)
            if not corresponding_offsets:
                break

            # - Get all the offset fronts that are composed of at least one of these offset times

            #     [ . O . . . . 1 . .]
            #     [ . . O 3 . . 1 . .]
            #     [ . . O 3 F . 1 . .]
            #     [ . O . 3 . . . 1 .]
            #     [ O F 3 . . . . . .]

            _all_offset_fronts_of_interest, ids_ntimes_seen = _get_all_offset_fronts_from_offsets(offset_fronts, corresponding_offsets)

            # - Check how many of these offset times each of the offset fronts are composed of:

            #     [ . O . . . . Y . .]
            #     [ . . O 3 . . Y . .]
            #     [ . . O 3 F . 1 . .]
            #     [ . O . X . . . 1 .]
            #     [ O F 3 . . . . . .]

            # In this example, offset front 1 is made up of 4 offset times, 2 of which (the Y's) are offset times
            # that correspond to onsets in the onset front we are currently dealing with. Meanwhile, offset
            # front 3 is made up of 4 offset times, only one of which (the X) is one of the offsets that corresponds
            # to the onset front.

            # - Choose the offset front which matches the most offset time candidates. In this example, offset front 1
            #   is chosen because it has 2 of these offset times.
            #   If there is a tie, we choose the ID with the lower number
            ntimes_seen_sorted = sorted([(k, v) for k, v in ids_ntimes_seen.items()], key=lambda tup: (-1 * tup[1], tup[0]))
            assert len(ntimes_seen_sorted) > 0, "We somehow got an empty dict of offset front IDs"

            # Only use the special final front (the -1, catch-all front composed of final samples in each frequency) if necessary
            offset_front_id, _ntimes_seen = ntimes_seen_sorted[0]
            if offset_front_id == -1 and len(ntimes_seen_sorted) > 1:
                offset_front_id, _ntimes_seen = ntimes_seen_sorted[1]
            offset_front_id_most_overlap = offset_front_id

            # - Finally, update the segmentation mask to follow the offset
            #   front from where it first overlaps in frequency with the onset front to where it ends or to where
            #   the onset front ends, whichever happens first.

            #     [ . S S S S S S . .]
            #     [ . . S S S S S . .]
            #     [ . . S S S S S . .]
            #     [ . S S S S S S S .]
            #     [ O F 3 . . . . . .]  <-- This frequency has not yet been matched with an offset front
            front_is_complete = _update_segmentation_mask(segmentation_mask,
                                                            resulting_onset_fronts,
                                                            offset_fronts,
                                                            onset_front_id,
                                                            offset_front_id_most_overlap)

            # Remove any onsets that are covered by the new segmentation mask
            _remove_overlaps(segmentation_mask, resulting_onset_fronts)

            # Remove any offsets that are covered by the new segmentaion mask
            _remove_overlaps(segmentation_mask, offset_fronts)

            # - Repeat this algorithm, restarting in the first frequency channel that did not match (the last frequency in
            #   the above example). Do this until you have finished with this onset front.

        # - Repeat for each onset front in the rest of this frequency
        # - Repeat for each frequency

    return segmentation_mask
def _match_fronts(onset_fronts, offset_fronts, onsets, offsets, debug=False):
    """
    Returns a segmentation mask, which looks like this:
    frequency 1: 0 0 4 4 4 4 4 0 0 5 5 5
    frequency 2: 0 4 4 4 4 4 0 0 0 0 5 5
    frequency 3: 0 4 4 4 4 4 4 4 5 5 5 5

    That is, each item in the array is either a 0 (not part of a segment) or a positive
    integer which indicates which segment the sample in that frequency band belongs to.
    """
    def printd(*args, **kwargs):
        if debug:
            print(*args, **kwargs)

    # Make copies of everything, so we can do whatever we want with them
    onset_fronts = np.copy(onset_fronts)
    offset_fronts = np.copy(offset_fronts)
    onsets = np.copy(onsets)
    offsets = np.copy(offsets)

    # This is what we will return
    segmentation_mask = np.zeros_like(onset_fronts)

    # - Take the first frequency in the onset_fronts matrix
    #     [ s s s s s s s s s] <-- This frequency
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]

    # - Follow it along in time like this:

    #     first sample    last sample
    #       v      -->      v
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]
    #     [ s s s s s s s s s]

    # until you get to the first onset front in that frequency

    #     Here it is!
    #         v
    #     [ . O . . . . . . .]
    #     [ . . O . . . . . .]
    #     [ . . O . . . . . .]
    #     [ . O . . . . . . .]
    #     [ O . . . . . . . .]

    resulting_onset_fronts = np.copy(onset_fronts)
    printd("    -> Dealing with onset fronts...")
    for onset_front_id in _get_front_ids_one_at_a_time(onset_fronts):
        printd("      -> Dealing with onset front", int(onset_front_id))
        front_is_complete = False
        while not front_is_complete:
            # - Now, starting at this onset front in each frequency, find that onset's corresponding offset

            #     [ . O . . . . F . .]
            #     [ . . O . . . F . .]
            #     [ . . O . F . . . .]
            #     [ . O . F . . . . .]
            #     [ O F . . . . . . .]

            corresponding_offsets = _get_corresponding_offsets(resulting_onset_fronts, onset_front_id, onsets, offsets)

            # It is possible that onset_front_id has been removed from resulting_onset_fronts,
            # if so, skip it and move on to the next onset front (we are iterating over the original
            # to keep the iterator valid)
            if not corresponding_offsets:
                break

            # - Get all the offset fronts that are composed of at least one of these offset times

            #     [ . O . . . . 1 . .]
            #     [ . . O 3 . . 1 . .]
            #     [ . . O 3 F . 1 . .]
            #     [ . O . 3 . . . 1 .]
            #     [ O F 3 . . . . . .]

            _all_offset_fronts_of_interest, ids_ntimes_seen = _get_all_offset_fronts_from_offsets(offset_fronts, corresponding_offsets)

            # - Check how many of these offset times each of the offset fronts are composed of:

            #     [ . O . . . . Y . .]
            #     [ . . O 3 . . Y . .]
            #     [ . . O 3 F . 1 . .]
            #     [ . O . X . . . 1 .]
            #     [ O F 3 . . . . . .]

            # In this example, offset front 1 is made up of 4 offset times, 2 of which (the Y's) are offset times
            # that correspond to onsets in the onset front we are currently dealing with. Meanwhile, offset
            # front 3 is made up of 4 offset times, only one of which (the X) is one of the offsets that corresponds
            # to the onset front.

            # - Choose the offset front which matches the most offset time candidates. In this example, offset front 1
            #   is chosen because it has 2 of these offset times.
            #   If there is a tie, we choose the ID with the lower number
            ntimes_seen_sorted = sorted([(k, v) for k, v in ids_ntimes_seen.items()], key=lambda tup: (-1 * tup[1], tup[0]))
            assert len(ntimes_seen_sorted) > 0, "We somehow got an empty dict of offset front IDs"

            # Only use the special final front (the -1, catch-all front composed of final samples in each frequency) if necessary
            offset_front_id, _ntimes_seen = ntimes_seen_sorted[0]
            if offset_front_id == -1 and len(ntimes_seen_sorted) > 1:
                offset_front_id, _ntimes_seen = ntimes_seen_sorted[1]
            offset_front_id_most_overlap = offset_front_id

            # - Finally, update the segmentation mask to follow the offset
            #   front from where it first overlaps in frequency with the onset front to where it ends or to where
            #   the onset front ends, whichever happens first.

            #     [ . S S S S S S . .]
            #     [ . . S S S S S . .]
            #     [ . . S S S S S . .]
            #     [ . S S S S S S S .]
            #     [ O F 3 . . . . . .]  <-- This frequency has not yet been matched with an offset front
            front_is_complete = _update_segmentation_mask(segmentation_mask,
                                                            resulting_onset_fronts,
                                                            offset_fronts,
                                                            onset_front_id,
                                                            offset_front_id_most_overlap)

            # Remove any onsets that are covered by the new segmentation mask
            _remove_overlaps(segmentation_mask, resulting_onset_fronts)

            # Remove any offsets that are covered by the new segmentaion mask
            _remove_overlaps(segmentation_mask, offset_fronts)

            # - Repeat this algorithm, restarting in the first frequency channel that did not match (the last frequency in
            #   the above example). Do this until you have finished with this onset front.

        # - Repeat for each onset front in the rest of this frequency
        # - Repeat for each frequency

    return segmentation_mask
Returns a segmentation mask, which looks like this:
    frequency 1: 0 0 4 4 4 4 4 0 0 5 5 5
    frequency 2: 0 4 4 4 4 4 0 0 0 0 5 5
    frequency 3: 0 4 4 4 4 4 4 4 5 5 5 5

    That is, each item in the array is either a 0 (not part of a segment) or a positive
    integer which indicates which segment the sample in that frequency band belongs to.