Skip to content

Remove overlap

remove_overlap

remove_overlap(
    score: Score, tolerance: float = 0.1, min_separation: float = 0.0
) -> Score

Remove overlapping notes with the same pitch from a score.

Sometimes in piano parts, a note participates in two voices and is printed as if two voices play the same note in unison, when of course the pianist only plays the pitch once. After reading a musicXML score, these doubled notes will be represented in the score. You can use this function to remove overlap.

Other cases can arise from reductions or merging of parts, or from rounding errors. This function can be used to clean up these cases.

Caution: Grace notes read from musicXML files are often represented as starting at the same time as the main note they ornament. Depending on pitches, this could cause the grace note to be removed by this function.

When producing MIDI output for playback, there is a risk of rounding error causing a note-on to be placed immediately before the note-off of a prceding note of the same pitch. This can cause a "stuck note." A simple solution is to shorten notes by a small amount when the same pitch follows immediately. Then, their note-off event will be sorted before the next note's note-on event. (A very slightly reduced duration is rarely audible, and separation must be introduced in a physical piano performance.) The min_separation parameter can be used to ensure that adjacent notes with the same pitch are separated by at least that amount. 0.02 is a reasonable choice: no one can play 50 notes per beat, but 0.02 is certain to quantize to at least one Standard MIDI File tick at any reasonable tick size. If units are seconds, 0.02 is just 20 milliseconds.

The score is first flattened (ties merged, notes extracted from measures and staves). Within each part, notes sharing the same key_num that overlap in time are resolved:

  • If two notes start within tolerance of each other, the note that starts first (or the first in sorted order when onsets are equal) is kept and extended to the maximum offset of the two; the second note is removed.
  • If the notes do not start within tolerance but the first note extends past the onset of the second, the first note is shortened so that it ends at the onset of the second note. The second note is extended to the offset of the first if necessary.

Each part is processed independently.

Parameters:

  • score (Score) –

    The score to process. The original score is not modified; a new flat score is returned.

  • tolerance (float, default: 0.1 ) –

    Maximum onset difference (in seconds or beats depending on the score's time unit) for two notes with the same key_num to be considered simultaneous. Defaults to 0.1.

  • min_separation (float, default: 0.0 ) –

    Minimum separation (in seconds or beats depending on the score's time unit) between adjacent notes with the same key_num. If 0.0, no minimum separation is enforced. Defaults to 0.0. Must be less than half of tolerance to avoid introducing note durations less than min_separation.

Returns:

  • Score

    A new flat score with overlapping same-pitch notes resolved.

Examples:

Two notes with the same pitch that start at the same time are merged into a single note whose duration is the maximum of the two:

>>> from amads.core.basics import Note, Part, Score
>>> score = Score(Part(Note(pitch="C4", onset=0.0, duration=2.0),
...                    Note(pitch="C4", onset=0.0, duration=3.0)))
>>> result = remove_overlap(score)
>>> notes = result.content[0].content
>>> len(notes)
1
>>> notes[0].duration
3.0

A note that overlaps a later note with the same pitch is trimmed so that it ends exactly when the later note begins. The later note is extended if necessary to cover the full duration of the first note:

>>> score = Score(Part(Note(pitch="C4", onset=0.0, duration=4.0),
...                    Note(pitch="C4", onset=1.0, duration=2.0)))
>>> result = remove_overlap(score)
>>> notes = result.content[0].content
>>> len(notes)
2
>>> notes[0].duration
1.0
>>> notes[1].onset
1.0
>>> notes[1].duration
3.0

Use min_separation to ensure that adjacent notes with the same pitch are separated by at least that amount:

>>> score = Score(Part(Note(pitch="C4", onset=0.0, duration=4.0),
...                    Note(pitch="C4", onset=4.0, duration=2.0)))
>>> result = remove_overlap(score, min_separation=0.02)
>>> notes = result.content[0].content
>>> len(notes)
2
>>> notes[0].duration
3.98
>>> notes[1].onset
4.0
>>> notes[1].duration
2.0
Source code in amads/algorithms/remove_overlap.py
  8
  9
 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
def remove_overlap(
    score: Score, tolerance: float = 0.1, min_separation: float = 0.0
) -> Score:
    """Remove overlapping notes with the same pitch from a score.

    Sometimes in piano parts, a note participates in two voices and
    is printed as if two voices play the same note in unison, when
    of course the pianist only plays the pitch once. After reading
    a musicXML score, these doubled notes will be represented in the
    score. You can use this function to remove overlap.

    Other cases can arise from reductions or merging of parts, or
    from rounding errors. This function can be used to clean up
    these cases.

    Caution: Grace notes read from musicXML files are often represented
    as starting at the same time as the main note they ornament. Depending
    on pitches, this could cause the grace note to be removed by this function.

    When producing MIDI output for playback, there is a risk of
    rounding error causing a note-on to be placed immediately *before*
    the note-off of a prceding note of the same pitch. This can cause
    a "stuck note." A simple solution is to shorten notes by a small
    amount when the same pitch follows immediately. Then, their note-off
    event will be sorted *before* the next note's note-on event. (A
    very slightly reduced duration is rarely audible, and separation
    must be introduced in a physical piano performance.) The
    `min_separation` parameter can be used to ensure that adjacent
    notes with the same pitch are separated by at least that amount.
    0.02 is a reasonable choice: no one can play 50 notes per beat,
    but 0.02 is certain to quantize to at least one Standard MIDI File
    tick at any reasonable tick size. If units are seconds, 0.02 is
    just 20 milliseconds.

    The score is first flattened (ties merged, notes extracted from
    measures and staves). Within each part, notes sharing the same
    `key_num` that overlap in time are resolved:

    - If two notes start within `tolerance` of each other, the note that
      starts first (or the first in sorted order when onsets are equal)
      is kept and extended to the maximum offset of the two; the second
      note is removed.
    - If the notes do not start within `tolerance` but the first note
      extends past the onset of the second, the first note is shortened
      so that it ends at the onset of the second note. The second note
      is extended to the offset of the first if necessary.

    Each part is processed independently.

    Parameters
    ----------
    score : Score
        The score to process.  The original score is not modified; a
        new flat score is returned.
    tolerance : float
        Maximum onset difference (in seconds or beats depending on the
        score's time unit) for two notes with the same `key_num` to be
        considered simultaneous.  Defaults to `0.1`.
    min_separation : float
        Minimum separation (in seconds or beats depending on the score's
        time unit) between adjacent notes with the same `key_num`.  If
        `0.0`, no minimum separation is enforced.  Defaults to `0.0`.
        Must be less than half of `tolerance` to avoid introducing note
        durations less than min_separation.

    Returns
    -------
    Score
        A new flat score with overlapping same-pitch notes resolved.

    Examples
    --------
    Two notes with the same pitch that start at the same time are merged
    into a single note whose duration is the maximum of the two:

    >>> from amads.core.basics import Note, Part, Score
    >>> score = Score(Part(Note(pitch="C4", onset=0.0, duration=2.0),
    ...                    Note(pitch="C4", onset=0.0, duration=3.0)))
    >>> result = remove_overlap(score)
    >>> notes = result.content[0].content
    >>> len(notes)
    1
    >>> notes[0].duration
    3.0

    A note that overlaps a later note with the same pitch is trimmed so
    that it ends exactly when the later note begins. The later note is
    extended if necessary to cover the full duration of the first note:

    >>> score = Score(Part(Note(pitch="C4", onset=0.0, duration=4.0),
    ...                    Note(pitch="C4", onset=1.0, duration=2.0)))
    >>> result = remove_overlap(score)
    >>> notes = result.content[0].content
    >>> len(notes)
    2
    >>> notes[0].duration
    1.0
    >>> notes[1].onset
    1.0
    >>> notes[1].duration
    3.0

    Use min_separation to ensure that adjacent notes with the same pitch
    are separated by at least that amount:

    >>> score = Score(Part(Note(pitch="C4", onset=0.0, duration=4.0),
    ...                    Note(pitch="C4", onset=4.0, duration=2.0)))
    >>> result = remove_overlap(score, min_separation=0.02)
    >>> notes = result.content[0].content
    >>> len(notes)
    2
    >>> notes[0].duration
    3.98
    >>> notes[1].onset
    4.0
    >>> notes[1].duration
    2.0
    """
    score = score.flatten()
    assert min_separation * 2 < tolerance, (
        "min_separation must be less " "than half of tolerance"
    )

    for part in score.find_all(Part):
        _remove_overlap_in_part(part, tolerance, min_separation)

    return score