Skip to content

Remove overlap

remove_overlap

remove_overlap(
    score: Score,
    tolerance: float = 0.1,
    min_separation: float = 0.0,
    keep_ornaments: bool = True,
    collapse_parts: bool = False,
) -> 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. By default, this function will not remove ornaments, even if they have the same pitch and onset as another note. Ornaments other than grace notes are stored as properties of the note they are attached to, but grace notes are independent notes with the 'is_grace' property set to True. By default, grace notes and any ornamented note cannot be merged with overlapping notes, but to allow this, set keep_ornaments=False.

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, so you must collapse parts (e.g., left and right hand in piano parts) if you want to remove overlap across parts. You can do this by passing collapse_parts=True to this function, which is more efficient that collapsing parts before calling this function.

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.

  • keep_ornaments (bool, default: True ) –

    If True, notes with is_grace or other ornament properties (including has_trill) are not merged with other notes, even if they have the same pitch and onset as another note. Defaults to True.

  • collapse_parts (bool, default: False ) –

    If True, all parts are merged into a single part before removing overlap. Defaults to False.

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def remove_overlap(
    score: Score,
    tolerance: float = 0.1,
    min_separation: float = 0.0,
    keep_ornaments: bool = True,
    collapse_parts: bool = False,
) -> 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. By
    default, this function will not remove ornaments, even if they have
    the same pitch and onset as another note. Ornaments other than
    grace notes are stored as properties of the note they are attached
    to, but grace notes are independent notes with the 'is_grace'
    property set to `True`. By default, grace notes and any ornamented
    note cannot be merged with overlapping notes, but to allow this,
    set `keep_ornaments=False`.

    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, so you must collapse parts
    (e.g., left and right hand in piano parts) if you want to remove
    overlap across parts. You can do this by passing `collapse_parts=True`
    to this function, which is more efficient that collapsing parts before
    calling this function.

    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.
    keep_ornaments : bool
        If `True`, notes with `is_grace` or other ornament properties
        (including `has_trill`) are not merged with other notes, even
        if they have the same pitch and onset as another note.
        Defaults to `True`.
    collapse_parts : bool
        If `True`, all parts are merged into a single part before removing
        overlap.  Defaults to `False`.

    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
    """
    if collapse_parts:
        score = score.collapse_parts()  # also flattens
    else:
        score = score.flatten()  # retains parts

    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, keep_ornaments)

    return score