Skip to content

Envelope

envelope

In this directory: - skyline returns a score with the highest sounding notes at any given point. - extreme is similar, and designed to match the MIDI toolkit as exactly as possible. - [this module] envelope is a variant on skyline that could be said to constitute a "smoothed" form, and (currently) operates on pitch-onset pairs only (analysis only, no score return). - superlative is the most reductive, returning only the single highest/lowest/sharpest/flattest value.

The skyline.py module demonstrates a strict case of retrieving the highest sounding notes at any given point (with caveats as noted there).

Here, we implement a variant that could be said to constitute a "smoothed" form of the same.

Consider a musical line in which only one note sounds at any time. Musically, this may still outline more than one 'voice'. Fugues for solo violin provide an example. Notwithstanding some simultaneities, most of the 'different line' are interleaves sequentially. The clue to these different lines is often in the relative separation of pitch-onset pairs in pitch-time space. Highly relevant here is the field of auditory stream segregation.

Moreover, even in the absence of this implied polyphony, a single monodic melody often outlines a simpler shape with elaborations. Mozart's first piano sonata (K279) begins with the line C B D C E D F E G F A which clearly outlines (or is an elaborated form of) C D E F G A.

Both of these scenarios are captures by the envelope.

In the language of data science, we seek the "upper and lower envelope for sequential data".

Terminology

The skyline is used here to denote the upper envelope (aka the "ceiling", or "roofline"). The valleyline refers to the lower envelope (aka the "valley floor").

The algorithm

A point belongs to the upper envelope if and only if it is NOT a strict local minimum — i.e. it is not strictly below BOTH of its nearest retained neighbours. Equivalently: we iteratively remove strict local minima until none remain.

This produces a piecewise-linear boundary that: - passes through actual data points (not a fitted curve unlike in some other approaches to contour) - retains an arbitrary number of direction changes - excludes only points that genuinely dip below (or rise above) their context - is parameter-free at tolerance=0 (a tolerance>0 also removes near-flat dips)

The valleyline is the symmetric pair: all of the above applies to the removal of strict local maxima.

Supported input forms
  1. String of digit characters e.g. "313131" as a toy shorthand for test cases
  2. Sequence of scalar values e.g. [3, 1, 3, 1, ...] which is more usable for MIDI pitch numbers for instance (in this case a fake onset sequence is created with natural numbers 1, 2, 3, ...)
  3. Sequence of (onset, pitch) pairs which data source gives us user- or score-specified x-spacing.
  4. A Score object or equivalent, in which case we internally use the .find_all(Note) functionality to get the above data.

Author: Mark Gotham

skyline_envelope

skyline_envelope(source, tolerance: float = 0.0) -> PointList

Upper envelope of the data (not to be confused with polyphony.skyline).

Returns the subset of input points that form the "roofline": no omitted point lies above the piecewise-linear boundary between its neighbours.

Parameters:

  • source

    (See "Supported input forms" at the top of this module.)

    • str of digits,
    • sequence of scalars,
    • sequence of (onset, pitch) pairs,
    • score object with .find_all(Note)
  • tolerance (float, default: 0.0 ) –

    points that dip no more than this amount below their neighbours are also removed (default 0 = exact envelope)

Returns:

  • PointList

    list of (onset, pitch) float pairs on the upper envelope

Examples:

Test some inpute types.

>>> test_case = skyline_envelope("31513")
>>> test_case
[(1.0, 3.0), (3.0, 5.0), (5.0, 3.0)]
>>> skyline_envelope([3, 1, 5, 1, 3]) == test_case
True
>>> skyline_envelope([(1.0, 3.0), (2.0, 1.0), (3.0, 5.0), (4.0, 1.0), (5.0, 3.0)]) == test_case
True

Test de-duplication of simultaneities

>>> skyline_envelope([(1.0, 5.0), (1.0, 2.0), (2.0, 3.0)])
[(1.0, 5.0), (2.0, 3.0)]
Source code in amads/polyphony/envelope.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def skyline_envelope(source, tolerance: float = 0.0) -> PointList:
    """
    Upper envelope of the data (not to be confused with polyphony.skyline).

    Returns the subset of input points that form the "roofline": no omitted
    point lies above the piecewise-linear boundary between its neighbours.

    Parameters
    ----------
    source
        (See "Supported input forms" at the top of this module.)

        - str of digits,
        - sequence of scalars,
        - sequence of (onset, pitch) pairs,
        - score object with .find_all(Note)
    tolerance: float
        points that dip no more than this amount below their neighbours
        are also removed (default 0 = exact envelope)

    Returns
    -------
    PointList
        list of (onset, pitch) float pairs on the upper envelope

    Examples
    --------
    Test some inpute types.

    >>> test_case = skyline_envelope("31513")
    >>> test_case
    [(1.0, 3.0), (3.0, 5.0), (5.0, 3.0)]

    >>> skyline_envelope([3, 1, 5, 1, 3]) == test_case
    True

    >>> skyline_envelope([(1.0, 3.0), (2.0, 1.0), (3.0, 5.0), (4.0, 1.0), (5.0, 3.0)]) == test_case
    True

    Test de-duplication of simultaneities

    >>> skyline_envelope([(1.0, 5.0), (1.0, 2.0), (2.0, 3.0)])
    [(1.0, 5.0), (2.0, 3.0)]
    """
    return _envelope(
        _to_points(source), upper_not_lower=True, tolerance=tolerance
    )

valleyline_envelope

valleyline_envelope(source, tolerance: float = 0.0) -> PointList

Lower envelope of the data.

Returns the subset of input points that form the "valley floor": no omitted point lies below the piecewise-linear boundary between its neighbours.

Parameters:

  • source

    (See "Supported input forms" at the top of this module.)

    • str of digits,
    • sequence of scalars,
    • sequence of (onset, pitch) pairs,
    • score object with .find_all(Note)
  • tolerance (float, default: 0.0 ) –

    points that dip no more than this amount below their neighbours are also removed (default 0 = exact envelope)

Returns:

  • PointList

    list of (onset, pitch) float pairs on the upper envelope

Examples:

Test some inpute types.

>>> test_case = valleyline_envelope("31513")
>>> test_case
[(1.0, 3.0), (2.0, 1.0), (4.0, 1.0), (5.0, 3.0)]
>>> valleyline_envelope([3, 1, 5, 1, 3]) == test_case
True
>>> valleyline_envelope([(1.0, 3.0), (2.0, 1.0), (3.0, 5.0), (4.0, 1.0), (5.0, 3.0)]) == test_case
True

Test de-duplication of simultaneities

>>> valleyline_envelope([(1.0, 5.0), (1.0, 2.0), (2.0, 3.0)])
[(1.0, 2.0), (2.0, 3.0)]
Source code in amads/polyphony/envelope.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
def valleyline_envelope(source, tolerance: float = 0.0) -> PointList:
    """
    Lower envelope of the data.

    Returns the subset of input points that form the "valley floor": no omitted
    point lies below the piecewise-linear boundary between its neighbours.

    Parameters
    ----------
    source
        (See "Supported input forms" at the top of this module.)

        - str of digits,
        - sequence of scalars,
        - sequence of (onset, pitch) pairs,
        - score object with .find_all(Note)
    tolerance: float
        points that dip no more than this amount below their neighbours
        are also removed (default 0 = exact envelope)

    Returns
    -------
    PointList
        list of (onset, pitch) float pairs on the upper envelope

    Examples
    --------

    Test some inpute types.

    >>> test_case = valleyline_envelope("31513")
    >>> test_case
    [(1.0, 3.0), (2.0, 1.0), (4.0, 1.0), (5.0, 3.0)]

    >>> valleyline_envelope([3, 1, 5, 1, 3]) == test_case
    True

    >>> valleyline_envelope([(1.0, 3.0), (2.0, 1.0), (3.0, 5.0), (4.0, 1.0), (5.0, 3.0)]) == test_case
    True

    Test de-duplication of simultaneities

    >>> valleyline_envelope([(1.0, 5.0), (1.0, 2.0), (2.0, 3.0)])
    [(1.0, 2.0), (2.0, 3.0)]
    """
    return _envelope(
        _to_points(source), upper_not_lower=False, tolerance=tolerance
    )

skyline_values

skyline_values(source, tolerance: float = 0.0) -> list[float]

Upper envelope as a plain list of y-values (onset coordinates omitted).

Parameters:

  • source

    (See "Supported input forms" at the top of this module.)

    • str of digits,
    • sequence of scalars,
    • sequence of (onset, pitch) pairs,
    • score object with .find_all(Note)
  • tolerance (float, default: 0.0 ) –

    points that dip no more than this amount below their neighbours are also removed (default 0 = exact envelope)

Returns:

  • PointList

    list of (onset, pitch) float pairs on the upper envelope

Examples:

>>> skyline_values("31513")
[3.0, 5.0, 3.0]
>>> skyline_values([1, 3, 1])
[1.0, 3.0, 1.0]
Source code in amads/polyphony/envelope.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def skyline_values(source, tolerance: float = 0.0) -> list[float]:
    """Upper envelope as a plain list of y-values (onset coordinates omitted).

    Parameters
    ----------
    source
        (See "Supported input forms" at the top of this module.)

        - str of digits,
        - sequence of scalars,
        - sequence of (onset, pitch) pairs,
        - score object with .find_all(Note)
    tolerance: float
        points that dip no more than this amount below their neighbours
        are also removed (default 0 = exact envelope)

    Returns
    -------
    PointList
        list of (onset, pitch) float pairs on the upper envelope

    Examples
    --------
    >>> skyline_values("31513")
    [3.0, 5.0, 3.0]

    >>> skyline_values([1, 3, 1])
    [1.0, 3.0, 1.0]
    """
    return [y for _, y in skyline_envelope(source, tolerance)]

valleyline_values

valleyline_values(source, tolerance: float = 0.0) -> list[float]

Lower envelope as a plain list of y-values (onset coordinates omitted).

Parameters:

  • source

    (See "Supported input forms" at the top of this module.)

    • str of digits,
    • sequence of scalars,
    • sequence of (onset, pitch) pairs,
    • score object with .find_all(Note)
  • tolerance (float, default: 0.0 ) –

    points that dip no more than this amount below their neighbours are also removed (default 0 = exact envelope)

Returns:

  • PointList

    list of (onset, pitch) float pairs on the upper envelope

Examples:

>>> valleyline_values("31513")
[3.0, 1.0, 1.0, 3.0]
Source code in amads/polyphony/envelope.py
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def valleyline_values(source, tolerance: float = 0.0) -> list[float]:
    """Lower envelope as a plain list of y-values (onset coordinates omitted).

    Parameters
    ----------
    source
        (See "Supported input forms" at the top of this module.)

        - str of digits,
        - sequence of scalars,
        - sequence of (onset, pitch) pairs,
        - score object with .find_all(Note)
    tolerance: float
        points that dip no more than this amount below their neighbours
        are also removed (default 0 = exact envelope)

    Returns
    -------
    PointList
        list of (onset, pitch) float pairs on the upper envelope

    Examples
    --------
    >>> valleyline_values("31513")
    [3.0, 1.0, 1.0, 3.0]
    """
    return [y for _, y in valleyline_envelope(source, tolerance)]