Contour

Contour representation classes used by contour features. User guide: Contour representations.

class StepContour(pitches, durations, step_contour_length=64, method='amads')[source]

Bases: object

Class for calculating and analyzing the step contour of a melody. A step contour is a list of MIDI pitch values, repeated proportionally to the duration (measured in tatums) of each note relative to the total melody length. This list is normalized to a user defined length, defaulting to 64 steps as used in FANTASTIC. Rests are considered as extending the duration of the previous note.

Examples

>>> pitches = [60, 64, 67]  # C4, E4, G4
>>> durations = [2.0, 1.0, 1.0]  # First note is 2 beats, others are 1 beat
>>> sc = StepContour(pitches, durations)
>>> len(sc.contour)  # Default length is 64
64
>>> pitches = [60, 62, 64, 65, 67]  # C4, D4, E4, F4, G4
>>> durations = [1.0, 1.0, 1.0, 1.0, 1.0]  # Notes have equal durations
>>> sc = StepContour(pitches, durations)
>>> sc.contour[:8]  # First 8 values of 64-length contour
[60, 60, 60, 60, 60, 60, 60, 60]
>>> sc.global_variation  # Standard deviation of contour
2.3974...
>>> sc.global_direction  # Correlation with ascending line
0.9746...
>>> sc.local_variation  # Average absolute difference between adjacent values
0.1111...
Parameters:
__init__(pitches, durations, step_contour_length=64, method='amads')[source]

Initialize StepContour with melody data.

Parameters:
  • pitches (list[int]) – List of pitch values

  • durations (list[float]) – List of note durations measured in tatums

  • step_contour_length (int, optional) – Length of the output step contour vector (default is 64)

  • method (str, optional) – Method for contour statistics, either “amads” or “fantastic”. Defaults to “amads”.

References

[1] Müllensiefen, D. (2009). Fantastic: Feature ANalysis Technology Accessing STatistics (In a Corpus): Technical Report v1.5 [2] W. Steinbeck, Struktur und Ähnlichkeit: Methoden automatisierter Melodieanalyse. Bärenreiter, 1982. [3] Juhász, Z. 2000. A model of variation in the music of a Hungarian ethnic group. Journal of New Music Research 29(2):159-172. [4] Eerola, T. & Toiviainen, P. (2004). MIDI Toolbox: MATLAB Tools for Music Research. University of Jyväskylä: Kopijyvä, Jyväskylä, Finland.

Examples

>>> sc = StepContour([60, 62], [2.0, 2.0], step_contour_length=4)
>>> sc.contour
[60, 60, 62, 62]
property global_variation: float

The overall pitch variability of the step contour.

The step contour is a fixed-length pitch vector in which each note’s MIDI pitch is repeated in proportion to its duration. This feature is the standard deviation of that vector, so long notes influence the result more than short notes. Larger values indicate a wider duration-weighted pitch spread.

Returns:

Standard deviation of the duration-weighted step-contour pitch vector.

Return type:

float

Note

The “amads” method uses population standard deviation (ddof=0), while “fantastic” uses sample standard deviation (ddof=1) for compatibility with the original FANTASTIC convention.

Examples

>>> sc = StepContour([60, 62, 64], [1.0, 1.0, 1.0])
>>> sc.global_variation
1.639...
property global_direction: float

The overall ascending or descending tendency of the step contour.

This is the Pearson correlation between the fixed-length step-contour pitch vector and an ascending linear ramp. Positive values indicate an overall upward trajectory, negative values indicate an overall downward trajectory, and values near zero indicate little linear pitch direction. A flat contour returns 0.0.

Returns:

Correlation with an ascending linear ramp.

Return type:

float

Examples

>>> sc = StepContour([60, 62, 64], [1.0, 1.0, 1.0])
>>> sc.global_direction
0.942...
>>> sc = StepContour([60, 60, 60], [1.0, 1.0, 1.0])
>>> sc.global_direction
0.0
>>> sc = StepContour([64, 62, 60], [1.0, 1.0, 1.0])  # Descending melody
>>> sc.global_direction
-0.942...
property local_variation: float

The average adjacent-sample change in the step contour.

This feature compares each neighboring pair of samples in the fixed-length step-contour pitch vector and averages the absolute pitch differences. Because adjacent samples inside the same sustained note have difference zero, the measure emphasizes local pitch changes after duration-weighted resampling rather than the raw sequence of note-to-note intervals.

Returns:

Mean absolute difference between adjacent step-contour samples.

Return type:

float

Examples

>>> sc = StepContour([60, 62, 64], [1.0, 1.0, 1.0])
>>> sc.local_variation
0.0634...
class InterpolationContour(pitches, times, method='amads')[source]

Bases: object

Class for calculating and analyzing the interpolation contours of melodies, according to Müllensiefen (2009) [1]. This representation was first formalised by Steinbeck (1982) [2], and informed a variant of the present implementation in Müllensiefen & Frieler (2006) [3]. An interpolation contour is produced by first identifying turning points in the melody, and then interpolating a linear gradient between each turning point. The resulting list of values represents the gradient of the melody at evenly spaced points in time.

Parameters:
__init__(pitches, times, method='amads')[source]

Initialize with pitch and time values.

Parameters:
  • pitches (list[int]) – Array of pitch values

  • times (list[float]) – Array of onset times in seconds

  • method (str, optional) – Method to use for contour calculation, either “fantastic” or “amads”. Defaults to “amads”.

Raises:

ValueError – If times and pitches are not the same length If method is not “fantastic” or “amads”

Examples

>>> happy_birthday_pitches = [
...     60, 60, 62, 60, 65, 64, 60, 60, 62, 60, 67, 65,
...     60, 60, 72, 69, 67, 65, 64, 70, 69, 65, 67, 65
... ]
>>> happy_birthday_times = [
...     0, 0.75, 1, 2, 3, 4, 6, 6.75, 7, 8, 9, 10,
...     12, 12.75, 13, 14, 15, 16, 17, 18, 18.75, 19, 20, 21
... ]
>>> ic = InterpolationContour(
...     happy_birthday_pitches,
...     happy_birthday_times,
...     method="fantastic",
... )
>>> ic.direction_changes
0.6
>>> ic.class_label
'ccbc'
>>> ic.mean_gradient
2.512...
>>> ic.gradient_std
5.496...
>>> ic.global_direction
1

References

[1] Müllensiefen, D. (2009). Fantastic: Feature ANalysis Technology Accessing STatistics (In a Corpus): Technical Report v1.5 [2] W. Steinbeck, Struktur und Ähnlichkeit: Methoden automatisierter Melodieanalyse. Bärenreiter, 1982. [3] Müllensiefen, D. & Frieler, K. (2006). Cognitive Adequacy in the Measurement of Melodic Similarity: Algorithmic vs. Human Judgments

static calculate_interpolation_contour(pitches, times, method='amads')[source]

Calculate the interpolation contour representation of a melody [1].

Returns:

Array containing the interpolation contour representation

Return type:

list[float]

Parameters:
property global_direction: int

The overall direction of the interpolation-contour gradients.

This feature sums the interpolation-contour gradient samples and returns the sign of that sum. A positive value indicates that upward gradients dominate overall, a negative value indicates that downward gradients dominate overall, and zero indicates that the sampled upward and downward gradients balance out.

Returns:

1 if the summed gradients are positive, 0 if the sum is zero, and -1 if the summed gradients are negative.

Return type:

int

Note

This is a net direction metric. Opposing upward and downward sections can cancel, resulting in 0 even when the contour is not flat. Defaults to the “amads” turning-point method (reversals), which is more robust for short melodies than the original FANTASTIC contour-extrema rules. Pass method=”fantastic” for the original behaviour.

Examples

Flat overall contour direction (returns the same using FANTASTIC method) >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4]) >>> ic.global_direction 0

Upwards contour direction (returns the same using FANTASTIC method) >>> ic = InterpolationContour([60, 62, 64, 65, 67], [0, 1, 2, 3, 4]) >>> ic.global_direction 1

Downwards contour direction (returns the same using FANTASTIC method) >>> ic = InterpolationContour([67, 65, 67, 62, 60], [0, 1, 2, 3, 4]) >>> ic.global_direction -1

property mean_gradient: float

The mean absolute gradient of the interpolation contour.

The interpolation contour is represented as a sequence of local gradients between contour turning points. This feature takes the absolute value of each sampled gradient and averages the result, so upward and downward slopes contribute equally. Larger values indicate steeper pitch movement between turning points.

Returns:

Mean absolute interpolation-contour gradient.

Return type:

float

Note

Defaults to the “amads” turning-point method (reversals), which is more robust for short melodies than the original FANTASTIC contour-extrema rules. Pass method=”fantastic” for the original behaviour.

Examples

Steps of 2 semitones per second >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4]) >>> ic.mean_gradient 2.0

FANTASTIC method returns 0.0 for this example >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4], method=”fantastic”) >>> ic.mean_gradient 0.0

property gradient_std: float

The sample standard deviation of interpolation-contour gradients.

This feature measures how much the sampled interpolation-contour gradients vary around their mean. It uses Bessel’s correction (ddof=1), matching the usual sample-standard-deviation convention. Contours with fewer than two gradient samples have no gradient variability and return 0.0.

Returns:

Sample standard deviation of the gradient values.

Return type:

float

Note

Defaults to the “amads” turning-point method (reversals), which is more robust for short melodies than the original FANTASTIC contour-extrema rules. Pass method=”fantastic” for the original behaviour.

Examples

>>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4])
>>> ic.gradient_std
2.0254...

FANTASTIC method returns 0.0 for this example >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4], method=”fantastic”) >>> ic.gradient_std 0.0

Single gradient value returns 0.0 (no variation) >>> ic = InterpolationContour([60, 67], [0, 1]) >>> ic.gradient_std 0.0

property direction_changes: float

The proportion of interpolation-contour transitions that reverse direction.

The interpolation contour is a sequence of gradient samples. Consecutive samples with the same gradient belong to the same interpolation-gradient run, so they do not increase the denominator. The denominator is the number of transitions between distinct gradient runs; the numerator is the number of those transitions where the gradient changes sign.

For example, the sampled contour [2, 2, 1, 1, -1, -1] has three gradient runs, [2, 1, -1]. It has two transitions between runs, but only 1 -> -1 reverses direction, so the feature value is 1 / 2 = 0.5.

Returns:

The number of sign-reversing transitions divided by the number of transitions between distinct interpolation-gradient runs. Returns 0.0 when there are no transitions between distinct gradient runs.

Return type:

float

Note

Defaults to the “amads” turning-point method (reversals), which is more robust for short melodies than the original FANTASTIC contour-extrema rules. Pass method=”fantastic” for the original behaviour.

Examples

>>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4])
>>> ic.direction_changes
1.0

FANTASTIC method returns 0.0 for this example >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4], method=”fantastic”) >>> ic.direction_changes 0.0

property class_label: str

A four-symbol categorical summary of interpolation-contour gradients.

The interpolation contour is sampled at four equally spaced positions. Each sampled gradient is measured in semitones per second, rescaled to semitones per 0.25 seconds, and assigned to one of five ordered categories from strong downward motion to strong upward motion. The returned string preserves temporal order, so “ddbb” means that the first two sampled regions are upward and the last two are downward.

Categories are defined as follows:

  • “a”: strong downward motion; normalized gradient <= -1.45.

  • “b”: downward motion; -1.45 < normalized gradient <= -0.45.

  • “c”: approximately flat; -0.45 < normalized gradient < 0.45.

  • “d”: upward motion; 0.45 <= normalized gradient < 1.45.

  • “e”: strong upward motion; normalized gradient >= 1.45.

Returns:

A string of length four whose characters are drawn from “a” to “e”. Each character gives the gradient category at one sampled position in the interpolation contour.

Return type:

str

Note

Returned labels are letters (“a”-“e”). Numeric codes (-2 to 2) are threshold descriptions only and are not returned by this property. Defaults to the “amads” turning-point method (reversals), which is more robust for short melodies than the original FANTASTIC contour-extrema rules. Pass method=”fantastic” for the original behaviour.

Examples

Upwards, then downwards contour >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4]) >>> ic.class_label ‘ddbb’

FANTASTIC method returns ‘cccc’ for this example, as though the contour is flat >>> ic = InterpolationContour([60, 62, 64, 62, 60], [0, 1, 2, 3, 4], method=”fantastic”) >>> ic.class_label ‘cccc’

class PolynomialContour(melody)[source]

Bases: object

A class for computing polynomial contour, as described in the FANTASTIC toolbox [1]. This approach is discussed in detail in Müllensiefen and Wiggins (2011) [2].

Polynomial Contour is constructed in 3 simple steps: First, the onsets are first centred around the origin of the time axis, making a symmetry between the first onset and the last. Then, a polynomial model is fit, seeking to predict the pitch values from a least squares regression of the centred onset times. Finally, the best model is selected using Bayes’ Information Criterion, stepwise and in a backwards direction.

The final output is the coefficients of the first three non-constant terms, i.e. [c1, c2, c3] from p = c0 + c1t + c2t^2 + c3t^3.

Parameters:

melody (Melody)

melody

The melody object containing the melody to analyze.

Type:

Melody

coefficients

The polynomial contour coefficients. Returns the first 3 non-constant coefficients [c1, c2, c3] of the final selected polynomial contour model. The constant term is not included as per the FANTASTIC toolbox specification.

Type:

list[float]

References

[1] Müllensiefen, D. (2009). Fantastic: Feature ANalysis Technology Accessing STatistics (In a Corpus): Technical Report v1.5 [2] Müllensiefen, D., & Wiggins, G.A. (2011). Polynomial functions as a representation of melodic phrase contour.

Examples

Single note melodies return [0.0, 0.0, 0.0] since there is no contour: >>> single_note_data = {“MIDI Sequence”: “Note(start=0.0, end=1.0, pitch=60, velocity=100)”} >>> single_note = Melody(single_note_data) >>> pc = PolynomialContour(single_note) >>> pc.coefficients [0.0, 0.0, 0.0]

Real melody examples (coefficients verified against FANTASTIC toolbox): >>> lick_data = {“MIDI Sequence”: “Note(start=0.0, end=1.0, pitch=62, velocity=100)Note(start=1.0, end=2.0, pitch=64, velocity=100)Note(start=2.0, end=3.0, pitch=65, velocity=100)Note(start=3.0, end=4.0, pitch=67, velocity=100)Note(start=4.0, end=6.0, pitch=64, velocity=100)Note(start=6.0, end=7.0, pitch=60, velocity=100)Note(start=7.0, end=8.0, pitch=62, velocity=100)”} >>> the_lick = Melody(lick_data) >>> pc2 = PolynomialContour(the_lick) >>> pc2.coefficients # Verified against FANTASTIC toolbox [-1.501482…, -0.266153…, 0.122057…]

__init__(melody)[source]

Initialize the polynomial contour using a Melody object and calculate the Polynomial Contour coefficients. Only the first three non-constant coefficients are returned, as the constant term is not used in the FANTASTIC toolbox. It is believed that the first three polynomial coefficients capture enough variation in the contour to be useful.

Parameters:

melody (Melody) – The melody object containing the melody to analyze.

property coefficients: list[float]

The first three non-constant polynomial-contour coefficients.

Polynomial contour fits pitch as a polynomial function of centered onset time, then selects a model by Bayesian information criterion. This feature returns the linear, quadratic, and cubic terms [c1, c2, c3] from p = c0 + c1*t + c2*t**2 + c3*t**3. The intercept c0 is omitted because it represents absolute pitch height rather than contour shape.

Returns:

First three non-constant coefficients [c1, c2, c3] of the selected polynomial model, padded with zeros if needed. Melodies with fewer than two notes return [0.0, 0.0, 0.0].

Return type:

list[float]

calculate_coefficients(onsets, pitches)[source]

The first 3 non-constant coefficients of the polynomial contour.

Parameters:
  • onsets (list[float]) – List of onset times from the score

  • pitches (list[int]) – List of pitch values from the score

Returns:

First 3 coefficients [c1, c2, c3] of the polynomial contour, with zeros padded if needed. For melodies with fewer than 2 notes, returns [0.0, 0.0, 0.0] since there is no meaningful contour to analyze.

Return type:

list[float]

get_onsets_and_pitches(melody)[source]

Extract onset times and pitches from a Melody object.

Parameters:

melody (Melody) – The Melody object to extract data from

Returns:

A tuple containing (onset_times, pitch_values)

Return type:

tuple[list[float], list[int]]

center_onset_times(onsets)[source]

Center onset times around their midpoint. This produces a symmetric axis of onset times, which is used later to fit the polynomial.

For single-note melodies, returns [0.0] since there is no meaningful contour to analyze.

Parameters:

onsets (list[float]) – List of onset times to center

Returns:

List of centered onset times. Returns [0.0] for single-note melodies.

Return type:

list[float]

fit_polynomial(centered_onsets, pitches, m)[source]

Fit a polynomial model to the melody contour using least squares regression.

The polynomial has the form: p = c0 + c1*t + c2*t^2 + … + cm*t^m

where m = n // 2 (n = number of notes) and t are centered onset times.

Parameters:
  • centered_onsets (list[float]) – List of centered onset times

  • pitches (list[int]) – List of pitch values

  • m (int) – Maximum polynomial degree to use

Returns:

The coefficients [c0, c1, …, cm] of the fitted polynomial

Return type:

list[float]

select_model(centered_onsets, pitches, m)[source]

Select the best polynomial model using BIC in a step-wise backwards fashion. Tests polynomials of decreasing degree and selects the one with the best BIC. The max degree is the same as m in the fit_polynomial method.

Parameters:
  • centered_onsets (list[float]) – List of centered onset times

  • pitches (list[int]) – List of pitch values

  • m (int) – Maximum polynomial degree to consider

Returns:

The coefficients [c1, c2, c3] of the selected polynomial model

Return type:

list[float]

class HuronContour(melody)[source]

Bases: object

A class for computing the Huron Contour of a melody, as described in the FANTASTIC toolbox. Huron Contour classifies melodies based on the shape of the contour between the first pitch, the mean pitch, and the last pitch.

Parameters:

melody (Melody)

melody

The melody object containing the melody to analyze.

Type:

Melody

huron_contour

The Huron contour classification for the melody.

Type:

str

__init__(melody)[source]

Initialize the Huron Contour using a Melody object and calculate the Huron Contour classification.

Parameters:

melody (Melody) – The melody object containing the melody to analyze.

get_contour_points(melody)[source]

Get the contour points of a melody.

Calculates the first pitch, weighted mean pitch, and last pitch of the melody.

Parameters:

melody (Melody) – The melody object to analyze

Returns:

The three relevant contour points: [first_pitch, mean_pitch, last_pitch]

Return type:

list[float]

get_contour_class(contour_points)[source]

The classification of a contour based on the relationship between the first, mean, and last pitch of the melody.

Parameters:

contour_points (list[float]) – The three relevant contour points: [first_pitch, mean_pitch, last_pitch]

Returns:

The contour classification according to Huron’s system

Return type:

str

property class_label: str

The Huron three-point contour classification for the melody.

The melody is reduced to three pitch points: the first pitch, a rounded duration-weighted mean pitch, and the last pitch. Their relative ordering determines the contour class. For example, first < mean < last is “ascending”, first > mean > last is “descending”, first < mean > last is “convex”, and first > mean < last is “concave”.

Citation

Huron (1996)

returns:

One of “ascending”, “descending”, “convex”, “concave”, “horizontal”, “ascending-horizontal”, “horizontal-ascending”, “descending-horizontal”, “horizontal-descending”, or “unclassified”.

rtype:

str

get_huron_contour(melody)[source]

Calculate Huron contour classification for an arbitrary melody. Used here for doctesting and offered for user convenience.

Parameters:

melody (Melody) – The melody object to analyze

Returns:

The Huron contour classification

Return type:

str

Examples

Single note melody: >>> single_note_data = {“MIDI Sequence”: “Note(start=0.0, end=1.0, pitch=60, velocity=100)”} >>> single_note = Melody(single_note_data) >>> get_huron_contour(single_note) ‘horizontal’

The lick - convex contour (62, 64, 65, 67, 64, 60, 62): >>> lick_data = {“MIDI Sequence”: “Note(start=0.0, end=1.0, pitch=62, velocity=100)Note(start=1.0, end=2.0, pitch=64, velocity=100)Note(start=2.0, end=3.0, pitch=65, velocity=100)Note(start=3.0, end=4.0, pitch=67, velocity=100)Note(start=4.0, end=6.0, pitch=64, velocity=100)Note(start=6.0, end=7.0, pitch=60, velocity=100)Note(start=7.0, end=8.0, pitch=62, velocity=100)”} >>> lick_melody = Melody(lick_data) >>> get_huron_contour(lick_melody) ‘convex’

Ascending melody (60, 62, 64, 67): >>> asc_data = {“MIDI Sequence”: “Note(start=0.0, end=1.0, pitch=60, velocity=100)Note(start=1.0, end=2.0, pitch=62, velocity=100)Note(start=2.0, end=3.0, pitch=64, velocity=100)Note(start=3.0, end=4.0, pitch=67, velocity=100)”} >>> asc_melody = Melody(asc_data) >>> get_huron_contour(asc_melody) ‘ascending’

Descending melody (67, 64, 62, 60): >>> desc_data = {“MIDI Sequence”: “Note(start=0.0, end=1.0, pitch=67, velocity=100)Note(start=1.0, end=2.0, pitch=64, velocity=100)Note(start=2.0, end=3.0, pitch=62, velocity=100)Note(start=3.0, end=4.0, pitch=60, velocity=100)”} >>> desc_melody = Melody(desc_data) >>> get_huron_contour(desc_melody) ‘descending’

polynomial_contour_coefficients(melody)[source]

Calculate polynomial contour coefficients for a melody.

This is a convenience function that creates a PolynomialContour object and returns its coefficients. Useful for integration with the features module.

Parameters:

melody (Melody) – The melody object to analyze

Returns:

The first 3 polynomial contour coefficients [c1, c2, c3]

Return type:

list[float]