Contour
Contour representation classes used by contour features. User guide: Contour representations.
- class StepContour(pitches, durations, step_contour_length=64, method='amads')[source]
Bases:
objectClass 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...
- __init__(pitches, durations, step_contour_length=64, method='amads')[source]
Initialize StepContour with melody data.
- Parameters:
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:
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:
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:
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:
objectClass 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.
- __init__(pitches, times, method='amads')[source]
Initialize with pitch and time values.
- Parameters:
- 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].
- 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:
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:
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:
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:
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:
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:
objectA 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)
- 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.
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.
- calculate_coefficients(onsets, pitches)[source]
The first 3 non-constant coefficients of the polynomial contour.
- Parameters:
- 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:
- 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.
- 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.
- class HuronContour(melody)[source]
Bases:
objectA 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)
- __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.
- 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.
- 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:
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’