Source code for astronify.utils.pitch_mapping

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Pitch mapping functionality
===========================

Functionality for taking arbitrary data values to audible pitches.
"""

import warnings

import numpy as np

from astropy.visualization import (
    SqrtStretch,
    LogStretch,
    AsinhStretch,
    SinhStretch,
    LinearStretch,
    MinMaxInterval,
    ManualInterval,
    AsymmetricPercentileInterval,
)

from .exceptions import InputWarning, InvalidInputError

__all__ = ["data_to_pitch"]


[docs] def data_to_pitch( data_array, pitch_range=[100, 10000], center_pitch=440, zero_point="median", stretch="linear", minmax_percent=None, minmax_value=None, invert=False, ): """ Map data array to audible pitches in the given range, and apply stretch and scaling as required. Parameters ---------- data_array : array-like Data to map to pitch values. Individual data values should be floats. pitch_range : array Optional, default [100,10000]. Range of acceptable pitches in Hz. center_pitch : float Optional, default 440. The pitch in Hz where that the the zero point of the data will be mapped to. zero_point : str or float Optional, default "median". The data value that will be mapped to the center pitch. Options are mean, median, or a specified data value (float). stretch : str Optional, default 'linear'. The stretch to apply to the data array. Valid values are: asinh, sinh, sqrt, log, linear minmax_percent : array Optional. Interval based on a keeping a specified fraction of data values (can be asymmetric) when scaling the data. The format is [lower percentile, upper percentile], where data values below the lower percentile and above the upper percentile are clipped. Only one of minmax_percent and minmax_value should be specified. minmax_value : array Optional. Interval based on user-specified data values when scaling the data array. The format is [min value, max value], where data values below the min value and above the max value are clipped. Only one of minmax_percent and minmax_value should be specified. invert : bool Optional, default False. If True the pitch array is inverted (low pitches become high and vice versa). Returns ------- response : array The normalized data array, with values in given pitch range. """ # Parsing the zero point if zero_point in ("med", "median"): zero_point = np.median(data_array) if zero_point in ("ave", "mean", "average"): zero_point = np.mean(data_array) # The center pitch cannot be >= max() pitch range, or <= min() of pitch range. # If it is, fall back to using the mean of the pitch range provided. if center_pitch <= pitch_range[0] or center_pitch >= pitch_range[1]: warnings.warn( "Given center pitch is outside the pitch range, defaulting to the mean.", InputWarning, ) center_pitch = np.mean(pitch_range) if (data_array == zero_point).all(): # All values are the same, no more calculation needed return np.full(len(data_array), center_pitch) # Normalizing the data_array and adding the zero point (so it can go through the same transform) data_array = np.append(np.array(data_array), zero_point) # Setting up the transform with the stretch if stretch == "asinh": transform = AsinhStretch() elif stretch == "sinh": transform = SinhStretch() elif stretch == "sqrt": transform = SqrtStretch() elif stretch == "log": transform = LogStretch() elif stretch == "linear": transform = LinearStretch() else: raise InvalidInputError("Stretch {} is not supported!".format(stretch)) # Adding the scaling to the transform if minmax_percent is not None: transform += AsymmetricPercentileInterval(*minmax_percent) if minmax_value is not None: warnings.warn( "Both minmax_percent and minmax_value are set, minmax_value will be ignored.", InputWarning, ) elif minmax_value is not None: transform += ManualInterval(*minmax_value) else: # Default, scale the entire image range to [0,1] transform += MinMaxInterval() # Performing the transform and then putting it into the pich range pitch_array = transform(data_array) if invert: pitch_array = 1 - pitch_array zero_point = pitch_array[-1] pitch_array = pitch_array[:-1] # In rare cases, the zero-point at this stage might be 0.0. # One example is an input array of two values where the median() is the same as the # lowest of the two values. In this case, the zero-point is 0.0 and will lead to error # (divide by zero). Change to small value to avoid dividing by zero (in reality the choice # of zero-point calculation by the user was probably poor, but not in purview to mandate or # change user's choice here. May want to consider providing info back to the user about the # distribution of pitches actually used based on their sonification options in some way. if zero_point == 0.0: zero_point = 1e-6 if ( (1 / zero_point) * (center_pitch - pitch_range[0]) + pitch_range[0] ) <= pitch_range[1]: pitch_array = (pitch_array / zero_point) * ( center_pitch - pitch_range[0] ) + pitch_range[0] else: pitch_array = ((pitch_array - zero_point) / (1 - zero_point)) * ( pitch_range[1] - center_pitch ) + center_pitch return pitch_array