Skip to content

Skyline

skyline

skyline(score: Score, threshold: float = 0.1) -> Score

Finds the skyline of a musical score.

Filters a score, removing any note that is below another note. There are tricky edge cases:

  • A higher note can occur while a lower note is still sounding. The lower note is shortened to end at the onset of the upper note.
  • A higher note can follow a lower note very quickly: Rather than setting the lower note's duration to a very small value, the lower note is completely removed and ignored. The lower bound on duration is set by the threshold parameter.
  • A rolled chord with 10 notes starts at the bottom, and every 0.05 quarter notes, a new note enters. So the previous rule applies to each note, making the top note is a full 0.45 quarters after the first one. Even in this case, the previous rule is applied repeatedly, leaving a gap (rest) of at least 0.45 quarters.
  • An upper note of a melody sustains in a legato fashion past the next, but lower, note of the melody. Although, musically, the upper note should be shortened and we should keep the lower note, the “skyline” concept says the top note has priority, so the lower note is ignored if the overlap is greater than threshold.
  • It is common to have melodies in lower voices or in MIDI arrangements to have very high accompaniment notes in a non-melodic track. This algorithm just fails to find the melody in those cases.

Parameters:

  • score (Score) –

    The musical score to filter

  • threshold (float, default: 0.1 ) –

    The threshold for quickly followed notes (default 0.1) and allowed overlap. Processing onsets in time order, if an onset is within threshold of the previous onset, the two notes are considered to be conconcurrent, and only the top note is considered in constructing the skyline. In the case of processing a note that is lower in pitch than the current skyline, we ignore the note if the skyline extends more than threshold beyond the note's onset. Otherwise, we shorten the skyline duration to end at the note onset and append the note to the skyline.

Returns:

  • Score

    A new score containing the “skyline” notes

Algorithm

The basic idea is to scan notes and copy them to skyline, a Part object belonging to a new Score. We can use shallow copy because notes are already deep-copied from score after merge_tied_notes.

In the outer looop, we test each note to see if it is below the skyline as it exists so far. Since we process in order, we know each note cannot start before any note in the skyline. If the note is higher than the most recent skyline note (so far), there are two cases:

  1. The new note is approximately concurrent with the most recent skyline note: Replace the most recent skyline note with this one.
  2. The new note is after the most recent skyline note: Append the new note to the skyline. If the previous note overlaps the new note, adjust the previous note's duration to end at the onset of the new note.

A consequence of this algorithm is that a very long low note will be shortened to the onset time of a new note, so a piano roll like this:

                           ----------
         ------------------------------------------

will result in this:

                           ----------
         ------------------          (nothing here)

rather than this:

         ------------------          --------------

Another consequence is that since skyline notes are never lengthened, there can be gaps in the skyline. It can look like this, where gaps (rests) can occur between notes:

                       ------      -----
         --------            ------         -------
Source code in amads/polyphony/skyline.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def skyline(score: Score, threshold: float = 0.1) -> Score:
    """
    Finds the skyline of a musical score.

    Filters a score, removing any note that is below another note.
    There are tricky edge cases:

    - A higher note can occur while a lower note is still sounding. The lower
      note is shortened to end at the onset of the upper note.
    - A higher note can follow a lower note very quickly: Rather than setting
      the lower note's duration to a very small value, the lower note is
      completely removed and ignored. The lower bound on duration is set by
      the `threshold` parameter.
    - A rolled chord with 10 notes starts at the bottom, and every 0.05
      quarter notes, a new note enters. So the previous rule applies to
      each note, making the top note is a full 0.45 quarters after the
      first one. Even in this case, the previous rule is applied repeatedly,
      leaving a gap (rest) of at least 0.45 quarters.
    - An upper note of a melody sustains in a legato fashion past the next,
      but lower, note of the melody. Although, musically, the upper note
      should be shortened and we should keep the lower note, the “skyline”
      concept says the top note has priority, so the lower note is ignored
      if the overlap is greater than `threshold`.
    - It is common to have melodies in lower voices or in MIDI arrangements
      to have very high accompaniment notes in a non-melodic track. This
      algorithm just fails to find the melody in those cases.

    Parameters
    ----------
    score : Score
        The musical score to filter
    threshold : float
        The threshold for quickly followed notes (default 0.1) and allowed
        overlap. Processing onsets in time order, if an onset is within
        `threshold` of the previous onset, the two notes are considered
        to be conconcurrent, and only the top note is considered in
        constructing the skyline. In the case of processing a note that
        is lower in pitch than the current skyline, we ignore the note
        if the skyline extends more than `threshold` beyond the note's
        onset. Otherwise, we shorten the skyline duration to end at the
        note onset and append the note to the skyline.

    Returns
    -------
    Score
        A new score containing the “skyline” notes

    Algorithm
    ---------
    The basic idea is to scan notes and copy them to skyline, a Part object
    belonging to a new Score. We can use shallow copy because notes are
    already deep-copied from score after merge_tied_notes.

    In the outer looop, we test each note to see if it is below the skyline
    as it exists so far. Since we process in order, we know each note cannot
    start before any note in the skyline. If the note is higher than the
    most recent skyline note (so far), there are two cases:

    1. The new note is approximately concurrent with the most recent skyline
       note: Replace the most recent skyline note with this one.
    2. The new note is after the most recent skyline note: Append the new
       note to the skyline. If the previous note overlaps the new note,
       adjust the previous note's duration to end at the onset of the new
       note.

    A consequence of this algorithm is that a very long low note will
    be shortened to the onset time of a new note, so a piano roll like
    this:
    ```
                               ----------
             ------------------------------------------
    ```
    will result in this:
    ```
                               ----------
             ------------------          (nothing here)
    ```
    rather than this:
    ```                        ----------
             ------------------          --------------
    ```

    Another consequence is that since skyline notes are never lengthened,
    there can be gaps in the skyline. It can look like this, where gaps
    (rests) can occur between notes:
    ```
                           ------      -----
             --------            ------         -------
    ```
    """
    # this code is based on get_sorted_notes():
    score = score.flatten(collapse=True)  # deep copies the score
    skyline: Part = cast(
        Part, score.content[0]
    )  # types: ignore (retrieves the Part)
    notes: List[Note] = cast(
        List[Note], skyline.content
    )  # (content is all Notes)
    skyline.content = []  # we will construct skyline from notes here

    prev_note: Optional[Note] = None
    for note in notes:
        if prev_note is None:
            skyline.content.append(note)
            prev_note = note

        # ignore notes that are below last note in skyline
        elif note.key_num < prev_note.key_num and (
            note.onset < prev_note.offset - threshold  # overlap
            or note.onset < prev_note.onset + threshold
        ):  # concurrent
            continue

        elif note.duration < threshold:
            continue

        # see if note is concurrent and higher
        if (
            note.onset < prev_note.onset + threshold
            and note.key_num >= prev_note.key_num
        ):
            skyline.content.pop()  # replace prev_note
            skyline.content.append(note)
            prev_note = note

        elif (
            note.onset >= prev_note.offset - threshold
            or note.key_num >= prev_note.key_num
        ):
            skyline.content.append(note)
            prev_note = note
            if prev_note.offset > note.onset:
                prev_note.offset = note.onset

        else:
            assert False, "Unexpected condition, implementation error detected"
    return score