Skip to content

Slice

The algorithms/slice directory contains software pertaining to breaking scores into sequences of “vertical slices,” e.g, the portions of all notes within a sequence of time intervals.

In the “Salami Slice” algorithm, time intervals are non-overlapping and their boundaries are all note onset and offset times. A list of slices can be created by calling the salami_slice function. The returned slices are instances of the Slice class.

A more basic way to slice a score is to construct a Window, which is basically just a Slice with a special constructor that selects and clips notes that fall within a given time interval, resulting in a single slice. You can create a sequence of Windows using any criteria for time intervals, including overlapping windows.

salami

Salami slice algorithm for segmenting musical scores.

This module implements the salami slice algorithm, which segments a musical score into vertical slices at each note onset and offset. Each slice contains all notes that are sounding at that point in time.

Author: Peter Harrison

Note: The algorithm is named after the way a salami sausage is sliced into thin, vertical segments.


Timepoint dataclass

Timepoint(
    time: float,
    note_ons: list[Note] = list(),
    note_offs: list[Note] = list(),
    sounding_notes: set[Note] = set(),
)

A point in time with associated note events.

Parameters:

  • time (float) –

    The time in seconds

  • note_ons (list[Note], default: list() ) –

    Notes that start at this timepoint

  • note_offs (list[Note], default: list() ) –

    Notes that end at this timepoint

  • sounding_notes (set[Note], default: set() ) –

    All notes that are sounding at this timepoint

Attributes

last_note_end property

last_note_end

Get the end time of the last note sounding at this timepoint.

Returns:

  • float

    The end time


salami_slice

salami_slice(
    passage: Union[Score, Iterable[Note]],
    remove_duplicated_pitches: bool = True,
    include_empty_slices: bool = False,
    include_note_end_slices: bool = True,
    min_slice_duration: float = 0.01,
) -> List[Slice]

Segment a musical passage into vertical slices at note onsets and offsets ('salami slices').

Parameters:

  • passage (Score or Iterable[Note]) –

    The musical passage to slice

  • remove_duplicated_pitches (bool, default: True ) –

    Whether to remove duplicate pitches within each slice

  • include_empty_slices (bool, default: False ) –

    Whether to include slices with no sounding notes

  • include_note_end_slices (bool, default: True ) –

    Whether to create slices at note ends

  • min_slice_duration (float, default: 0.01 ) –

    Minimum duration for a slice to be included

Returns:

  • List[Slice]

    The sequence of vertical slices

Source code in amads/algorithms/slice/salami.py
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def salami_slice(
    passage: Union[Score, Iterable[Note]],
    remove_duplicated_pitches: bool = True,
    include_empty_slices: bool = False,
    include_note_end_slices: bool = True,
    min_slice_duration: float = 0.01,
) -> List[Slice]:
    """Segment a musical passage into vertical slices at note onsets and offsets ('salami slices').

    Parameters
    ----------
    passage : Score or Iterable[Note]
        The musical passage to slice
    remove_duplicated_pitches : bool, default=True
        Whether to remove duplicate pitches within each slice
    include_empty_slices : bool, default=False
        Whether to include slices with no sounding notes
    include_note_end_slices : bool, default=True
        Whether to create slices at note ends
    min_slice_duration : float, default=0.01
        Minimum duration for a slice to be included

    Returns
    -------
    List[Slice]
        The sequence of vertical slices
    """
    if isinstance(passage, Score):
        notes = passage.get_sorted_notes()
    else:
        notes = passage

    timepoints = Timepoint.from_notes(notes)
    slices = []

    for i, timepoint in enumerate(timepoints):
        if len(timepoint.note_ons) > 0 or (
            include_note_end_slices and len(timepoint.note_offs) > 0
        ):
            try:
                next_timepoint = timepoints[i + 1]
            except IndexError:
                next_timepoint = None

            is_last_timepoint = next_timepoint is None
            is_empty_slice = len(timepoint.sounding_notes) == 0

            if is_empty_slice:
                if not include_empty_slices:
                    continue
                if is_last_timepoint:
                    # Don't include empty slices at the end of the score
                    continue

            slice_onset = timepoint.time

            if next_timepoint is None:
                if len(timepoint.sounding_notes) == 0:
                    continue
                else:
                    slice_end = timepoint.last_note_end
            else:
                slice_end = next_timepoint.time

            slice_duration = slice_end - slice_onset

            if slice_duration < min_slice_duration:
                continue

            pitches = [note.pitch for note in timepoint.sounding_notes]
            if remove_duplicated_pitches:
                pitches = sorted(set(pitches))

            slice = Slice(
                timepoint.sounding_notes, slice_onset, slice_end - slice_onset
            )

            # construct a new Note for each pitch and add it to the slice
            for pitch in pitches:
                Note(slice, slice_onset, slice_duration, pitch)

            slices.append(slice)

    return slices

Slice

Slice(
    original_notes: List[Note], onset: float = 0, duration: float = 0
)

Bases: Concurrence

A slice of a musical score between two timepoints.

This is the base class for different slicing algorithms like salami slicing and windowing. A slice contains a list of notes that are sounding between its start and end times, as well as references to the original notes from which these were derived.

Author: Peter Harrison

Parameters:

  • original_notes (List[Note]) –

    The original unmodified notes from which the slice notes were derived

  • onset (float, default: 0 ) –

    The start time offset of the slice

  • duration (float, default: 0 ) –

    The duration of the slice

Source code in amads/algorithms/slice/slice.py
26
27
28
29
30
31
32
33
34
35
def __init__(
    self,
    original_notes: List[Note],
    onset: float = 0,
    duration: float = 0,
):
    super().__init__(
        parent=None, onset=onset, duration=duration, content=[]
    )
    self.original_notes = original_notes

Window

Window(
    time: float,
    size: float,
    align: str,
    candidate_notes: Iterable[Note],
    skip: int = 0,
)

Bases: Slice

A fixed-size window of a musical score. candidate_notes that overlap with this interval are copied and clipped to fit within the window. Notes that overlap less than 1.0e-6 duration units (whether beats or seconds) are mostly excluded from the window to reduce numerical issues. An exception is made for notes that are so short that they do not overlap any window by at least 1.0e-6 duration units. (This seems far-fetched, but zero-length notes representing grace notes are one possibility to consider; there may be others.)

Additional details that you may not need: For very short notes, the window is considered closed on the left and open on the right, so that the window is considered to contain the note if it starts at the same time as the window, and a note is not in the window if it starts at the offset time of the window. To guarantee that zero-length notes are included in only one window, the offset of a window should be identical to the onset of the next window. sliding_window() takes care of this by default, but if the Window constructor is used directly where arithmetic with time and size is inexact, this may not be the case.

Author: Peter Harrison

Parameters:

  • time (float) –

    The reference time for this window

  • size (float) –

    The size of the window in time units

  • align (str) –

    How to align the window relative to the reference time: "left": window starts at reference time, "center": reference time is at window center, or "right": window ends at reference time.

  • candidate_notes (Iterable[Note]) –

    Notes to consider for inclusion in this window, sorted by onset time and pitch

  • skip (int, default: 0 ) –

    Index to start searching from in candidate_notes. This is used to optimize performance when iterating through multiple windows - each window can tell the next window which notes it can safely skip because they end before the window starts.

Source code in amads/algorithms/slice/window.py
 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
def __init__(
    self,
    time: float,
    size: float,
    align: str,
    candidate_notes: Iterable[Note],
    skip: int = 0,
):
    match align:
        case "left":
            onset = time
        case "center":
            onset = time - size / 2
        case "right":
            onset = time - size
        case _:
            raise ValueError(f"Invalid value passed to `align`: {align}")

    offset = onset + size

    super().__init__(
        original_notes=[],
        onset=onset,
        duration=size,
    )

    self.time = time
    self.align = align
    # skip logic: skip is the lowest index of candidate_notes for which
    # candidate_notes[skip].offset >= onset. The next window can start
    # searching from this index.

    candidate_notes = list(candidate_notes)
    self.skip = len(candidate_notes)

    for i in range(skip, len(candidate_notes)):
        note = candidate_notes[i]

        if note.offset < onset:
            # The note finished before the window starts.
            continue
        else:  # note overlaps window, start no later than this
            # when searching for overlaps with the next window
            self.skip = min(self.skip, i)

        if note.onset >= offset:
            # The note starts after the window finishes.
            # All the remaining notes in candidate_notes will have even later onsets,
            # so we don't need to check them for this window.
            # They might be caught by future windows though.
            break

        # note.onset < offset, so it starts in window
        if note.offset - offset < 1.0e-6:
            # The note will not overlap with the next window.
            # Since it starts in this window, include it.
            pass
        elif offset - note.onset < 1.0e-6:
            # after clipping to window boundaries, this note
            # will be shorter than 1.0e-6, but it extends into
            # the next window, so ignore the small overlap here
            continue

        self.original_notes.append(note)

        # We use deepcopy_into instead of creating a new Note because we want to
        # preserve any other attributes that might be useful in downstream tasks.
        note = note.insert_copy_into(parent=self)
        # Clip the note to fit within the window
        note.onset = max(note.onset, onset)
        note.offset = min(note.offset, offset)

sliding_window

sliding_window(
    passage: Union[Score, Iterable[Note]],
    size: float,
    step: float = 1.0,
    align: str = "right",
    onset: float = 0.0,
    offset: Optional[float] = None,
    times: Optional[Iterable[float]] = None,
) -> Iterator[Window]

Slice a score into (possibly overlapping) windows of a given size.

Author: Peter Harrison

Parameters:

  • passage (Score or Iterable[Note]) –

    The musical passage to be windowed

  • size (float) –

    The size (duration) of each window (time units)

  • step (float, default: 1.0 ) –

    The step size to take between windows (time units). For example, if step is 0.1, then a given slice will start 0.1 time units after the previous slice started. Note that if step is smaller than size, successive windows will overlap

  • align (str, default: "right" ) –

    Each generated window has a time property that points to a particular timepoint in the musical passage. The align parameter determines how the window is aligned to this timepoint: - "left": the window starts at window.time - "center": window.time corresponds to the midpoint of the window - "right": the window finishes at window.time

  • onset (float, default: 0.0 ) –

    The desired time of the first window

  • offset (float, default: None ) –

    If set, the windowing will stop once the offset time is reached. Following the behaviour of Python's built-in range function, offset is not treated inclusively, i.e. the last window will not include offset. The returned iterator will stop early the last window is empty (i.e. contains no notes) and there are no more notes to process.

  • times (Iterable[float], default: None ) –

    Optional iterable of times to generate windows for. If provided, onset and offset are ignored. The returned iterator will stop once all times have been processed or when an empty window is generated and there are no more notes to process.

Returns:

  • Iterator[Window]

    Iterator over the windows

Source code in amads/algorithms/slice/window.py
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def sliding_window(
    passage: Union[Score, Iterable[Note]],
    size: float,
    step: float = 1.0,
    align: str = "right",
    onset: float = 0.0,
    offset: Optional[float] = None,
    times: Optional[Iterable[float]] = None,
) -> Iterator[Window]:
    """Slice a score into (possibly overlapping) windows of a given size.

    <small>**Author**: Peter Harrison</small>

    Parameters
    ----------
    passage : Score or Iterable[Note]
        The musical passage to be windowed
    size : float
        The size (duration) of each window (time units)
    step : float, default=1.0
        The step size to take between windows (time units).
        For example, if step is 0.1, then a given slice will start 0.1 time units
        after the previous slice started. Note that if step is smaller than size,
        successive windows will overlap
    align : str, default="right"
        Each generated window has a `time` property that points to a
        particular timepoint in the musical passage. The `align` parameter determines
        how the window is aligned to this timepoint:
        - "left": the window starts at ``window.time``
        - "center": ``window.time`` corresponds to the midpoint of the window
        - "right": the window finishes at ``window.time``
    onset : float, default=0.0
        The desired time of the first window
    offset : float, optional
        If set, the windowing will stop once the offset time is reached.
        Following the behaviour of Python's built-in range function,
        ``offset`` is not treated inclusively, i.e. the last window will
        not include ``offset``. The returned iterator will stop early
        the last window is empty (i.e. contains no notes) and there are
        no more notes to process.
    times : Iterable[float], optional
        Optional iterable of times to generate windows for. If provided,
        `onset` and `offset` are ignored. The returned iterator will
        stop once all times have been processed or when an empty window
        is generated and there are no more notes to process.

    Returns
    -------
    Iterator[Window]
        Iterator over the windows
    """
    if isinstance(passage, Score):
        if not passage.is_flat_and_collapsed():
            raise NotImplementedError(
                "Currently this function only supports flat scores with a "
                "single Part. You can flatten a score using "
                "`score.flatten(collapse=True)`."
            )
        notes = passage.find_all(Note)
    else:
        notes = passage

    notes = cast(List[Note], list(notes))
    notes.sort(key=lambda n: (n.onset, n.pitch))

    # We could rely on Window to obey `align`, but here we convert onset and
    # offset to always use "left". By using left, we can guarantee when
    # step == size that each window time computed by repeated addition of
    # step will exactly equal each previous window offset, computed as
    # onset + size in Window. This avoids any floating point rounding error
    # that could affect the windowing. (This is not a problem in the typical
    # cases where window size is 1 or a power of 2 that will be computed
    # exactly due to binary representations of floats.)
    match align:
        case "left":
            pass
        case "center":
            onset -= size / 2
            if offset is not None:
                offset -= size / 2
        case "right":
            onset -= size
            if offset is not None:
                offset -= size
        case _:
            raise ValueError(f"Invalid value passed to `align`: {align}")

    if times is None:
        window_times = float_range(onset, offset, step)
    else:
        for par, default in [("onset", 0.0), ("offset", None), ("step", 1.0)]:
            provided = globals()[par]
            if provided != default:
                raise ValueError(
                    f"`{par}` was set to {provided} but `times` was also provided"
                )

        window_times = times

    skip = 0

    for time in window_times:
        window = Window(time, size, "left", notes, skip)

        yield window

        skip = window.skip

        if skip == len(notes):
            break

    skip = 0
    if times is not None:
        for par, default in [("onset", 0.0), ("offset", None), ("step", 1.0)]:
            provided = globals()[par]
            if provided != default:
                raise ValueError(
                    f"`{par}` was set to {provided} but `times` was also provided"
                )
        for time in times:
            window = Window(time, size, align, notes, skip)

            yield window

            skip = window.skip
            if skip == len(notes):
                break
    else:  # compute windows equally spaced by step
        match align:
            case "left":
                time = onset
            case "center":
                time = onset - size / 2
            case "right":
                time = onset - size
            case _:
                raise ValueError(f"Invalid value passed to `align`: {align}")
        while (offset is None) or (time < offset):
            window = Window(time, size, "left", notes, skip)

            yield window

            # if step == size, windows are back-to-back, so the next onset
            # is exactly equal to the previous offset. Avoid
            onset = window.offset if step == size else onset + step
            time += step
            skip = window.skip
            if skip == len(notes):
                break