Source code for decitala.hm.contour

####################################################################################################
# File:     contour.py
# Purpose:  Pitch contour tools for the birdsong transcriptions.
#
# Author:   Luke Poeppel
#
# Location: Kent, 2021
####################################################################################################
from .schultz import spc
from .contour_utils import (
	_track_extrema,
	_recheck_extrema,
	_pitch_contour,
	_adjacency_and_intervening_checks
)

NEUMES = {
	(1, 0): "Clivis",
	(0, 1): "Podatus",
	(0, 1, 2): "Scandicus",
	(2, 1, 0): "Climacus",
	(0, 1, 0): "Torculus",
	(1, 0, 1): "Porrectus"
}

# Morris's Prime Contour Classes (1993, 220-221)
# "Linear Prime Classes" (Schultz 92)
# NOTE: Schultz uses the same linear prime classes to refer to symmetries
# of these classes: e.g. <0 2 1> and <1 0 2> = L.
PRIME_CONTOUR_CLASSES = {
	(0,): "A",
	(0, 0): "B",
	(0, 1): "D",
	(0, 1, 0): "G",
	(0, 2, 1): "L",
	(1, 0, 2, 1): "P",
	(1, 0, 3, 2): "X",
	(1, 3, 0, 2): "Y",
	(1, 0, 2, 0, 1): "12a",
	(1, 0, 3, 0, 2): "12b"
}

[docs]class ContourException(Exception): pass
[docs]def strip_monotonic_pitch_content(pitch_content): """ The pitch content extracted in the :obj:`decitala.search` module consists of lists of tuples. This functions strips monotonic pitch content to a single list. If non-monotonic pitch content is provided, the function chooses the lowest pitch. :param list pitch_content: pitch content of the format returned in :obj:`decitala.search.rolling_hash_search`. :return: a list of MIDI tones. :rtype: list >>> pitch_content = [(60,), (61,), (65,)] >>> strip_monotonic_pitch_content(pitch_content) [60, 61, 65] """ return [x[0] for x in pitch_content]
[docs]def normalize_pitch_content(data, midi_start=60): """ Normalizes a list of MIDI tones to a a starting value. :param list data: a list of MIDI tones. :param int midi_start: the MIDI starting point to which the data are normalized. :return: a numpy array of the pitch content, normalized to the starting value. :rtype: numpy.array >>> normalize_pitch_content(data=[58, 60, 62], midi_start=60) [60, 62, 64] """ diff = data[0] - midi_start return [x - diff for x in data]
[docs]def uds_contour(data): """ Returns the for "up-down-stay" contour (UDS) of a given list of MIDI tones. Normalized to start at 0. :param list data: a list of MIDI tones. :return: a numpy array of the UDS contour of the given data. :rtype: numpy.array >>> midis = [47, 42, 45, 51, 51, 61, 58] >>> uds_contour(midis) [0, -1, 1, 1, 0, 1, -1] """ out = [0] i = 1 while i < len(data): prev = data[i - 1] curr = data[i] if curr > prev: out.append(1) elif curr < prev: out.append(-1) elif curr == prev: out.append(0) i += 1 return out
[docs]def pitch_contour(pitch_content, as_str=False): """ This function returns the contour of given pitch content. It accepts either a list of MIDI tones, or the data returned in the :obj:`decitala.search` module. Like :obj:`decitala.hm.contour.strip_monotonic_pitch_content`, if non-monotonic pitch content is provided, it chooses the lowest pitch. :param list pitch_content: pitch content from the output of rolling_search." :param bool as_str: whether to return the pitch content as a string (standard format), like ``"<0 1 1>"``. :return: the contour of the given ``pitch_content``. :rtype: numpy.array or str >>> pitch_content_1 = [(80,), (91,), (78,), (85,)] >>> pitch_contour(pitch_content_1) [1, 3, 0, 2] >>> pitch_content_2 = [80, 84, 84] >>> pitch_contour(pitch_content_2, as_str=True) '<0 1 1>' """ return _pitch_contour(pitch_content=pitch_content, as_str=as_str)
[docs]def contour_to_neume(contour): """ Oversimplified function for checking the associated neume of a given pitch contour. Only two and three onset contours are supported. :param contour: A pitch contour (iterable). :return: The associated neume or ``None``. :rtype: str or None >>> contour = [1, 0, 1] >>> contour_to_neume(contour) 'Porrectus' """ assert len(contour) <= 3, ContourException("Contour input must be of length three.") try: return NEUMES[tuple(contour)] except KeyError: raise ContourException(f"The contour {contour} was not found in the given current set.")
[docs]def contour_class( contour, allow_symmetries=False ): """ Returns the associated pitch contour class (a letter) from Morris (1993, 220-221) of a contour. :param contour: a pitch contour (iterable). :param bool allow_symmetries: whether to allow permutations of the given contour to be found. Default is ``False``. Note that ``X`` and ``Y`` are weird cases for this symmetry. May currently fail (don't understand it). :rtype: str >>> contour_class((1, 0, 3, 2)) 'X' >>> contour_class((0, 1, 0), allow_symmetries=False) 'G' >>> contour_class((0, 0, 1), allow_symmetries=True) 'G' """ try: if not(allow_symmetries): return PRIME_CONTOUR_CLASSES[contour] elif contour in {(1, 0, 3, 2), (1, 3, 0, 2)}: # IDK about this case. return PRIME_CONTOUR_CLASSES[contour] else: match = None for key in PRIME_CONTOUR_CLASSES.keys(): if len(key) == len(contour) and len(set(key)) == len(set(contour)): match = PRIME_CONTOUR_CLASSES[key] break return match except KeyError: ContourException(f"The contour {contour} is not prime.")
#################################################################################################### # Contour reduction tools. # Implementation of Morris contour reduction algorithm (1993). def _morris_reduce(contour, depth): """ Steps 4-7 of the contour reduction algorithm. """ contour = [x for x in contour if x[1]] # Step 4 depth += 1 # Step 5 # Step 6. Flag maxima and *delete* repetitions. _recheck_extrema(contour=contour, mode="max") _adjacency_and_intervening_checks(contour, mode="max", algorithm="morris") # Step 7. Flag minima and *delete* repetitions. _recheck_extrema(contour=contour, mode="min") _adjacency_and_intervening_checks(contour, mode="min", algorithm="morris") return contour, depth
[docs]def prime_contour(contour): """ Implementation of Robert Morris' Contour-Reduction algorithm (Morris, 1993). "The algorithm prunes pitches from a contour until it is reduced to a prime." (Schultz) :param contour: A pitch contour (iterable). :return: the prime contour of the given pitch contour, along with the depth of the reduction. :rtype: tuple >>> contour_a = [0, 1] >>> prime_contour(contour_a) ([0, 1], 0) >>> contour_b = [0, 4, 3, 2, 5, 5, 1] >>> prime_contour(contour_b)[0] [0, 2, 1] """ depth = 0 # If the segment is of length <= 2, it is prime by definition. if len(contour) <= 2: return (pitch_contour(contour), depth) # If all the values are extremas, it is already prime. prime_contour = _track_extrema(contour) initial_flags = [x[1] for x in prime_contour] if all(x for x in initial_flags): return (pitch_contour(contour), depth) still_unflagged_values = True while still_unflagged_values: prime_contour, depth = _morris_reduce(prime_contour, depth) if all(x[1] for x in prime_contour): # Step 3 still_unflagged_values = False # Remove flags. prime_contour = [x[0] for x in prime_contour] return (pitch_contour(prime_contour), depth)
[docs]def schultz_prime_contour(contour): """ Implementation of Schultz's (2008) modification of Morris' contour-reduction algorithm. See the Schultz module for the implementation. (It was complicated enough to warrent its own module...) :param contour: A pitch contour (iterable). :return: the Schultz Prime Contour (SPC) of the given contour, along with the depth of the reduction. :rtype: tuple >>> nightingale_5 = [2, 5, 3, 1, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0] >>> spc(nightingale_5) ([1, 2, 0], 3) """ return spc(contour)