# 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