Source code for mutwo.parameters.abc

"""Abstract base classes for different parameters."""

import abc
import dataclasses
import functools
import math
import typing

try:
    import quicktions as fractions  # type: ignore
except ImportError:
    import fractions  # type: ignore

from mutwo.parameters import pitches_constants
from mutwo.parameters import volumes_constants
from mutwo.utilities import constants
from mutwo.utilities import tools

__all__ = ("Pitch", "Volume", "PlayingIndicator", "NotationIndicator")


class Parameter(abc.ABC):
    """Abstract base class for any parameter class."""

    pass


ParameterType = object
DurationType = constants.Real


[docs]@functools.total_ordering # type: ignore class Pitch(Parameter): """Abstract base class for any pitch class. If the user wants to define a new pitch class, the abstract property :attr:`frequency` has to be overridden. """ # conversion methods between different pitch describing units
[docs] @staticmethod def hertz_to_cents(frequency0: constants.Real, frequency1: constants.Real) -> float: """Calculates the difference in cents between two frequencies. :param frequency0: The first frequency in Hertz. :param frequency1: The second frequency in Hertz. :return: The difference in cents between the first and the second frequency. **Example:** >>> from mutwo.parameters import abc >>> abc.Pitch.hertz_to_cents(200, 400) 1200.0 """ return float(1200 * math.log(frequency1 / frequency0, 2))
[docs] @staticmethod def ratio_to_cents(ratio: fractions.Fraction) -> float: """Converts a frequency ratio to its respective cent value. :param ratio: The frequency ratio which cent value shall be calculated. **Example:** >>> from mutwo.parameters import abc >>> abc.Pitch.ratio_to_cents(fractions.Fraction(3, 2)) 701.9550008653874 """ return pitches_constants.CENT_CALCULATION_CONSTANT * math.log10(ratio)
[docs] @staticmethod def cents_to_ratio(cents: constants.Real) -> fractions.Fraction: """Converts a cent value to its respective frequency ratio. :param cents: Cents that shall be converted to a frequency ratio. **Example:** >>> from mutwo.parameters import abc >>> abc.Pitch.cents_to_ratio(1200) Fraction(2, 1) """ return fractions.Fraction( 10 ** (cents / pitches_constants.CENT_CALCULATION_CONSTANT) )
[docs] @staticmethod def hertz_to_midi_pitch_number(frequency: constants.Real) -> float: """Converts a frequency in hertz to its respective midi pitch. :param frequency: The frequency that shall be translated to a midi pitch number. :return: The midi pitch number (potentially a floating point number if the entered frequency isn't on the grid of the equal divided octave tuning with a = 440 Hertz). **Example:** >>> from mutwo.parameters import abc >>> abc.Pitch.hertz_to_midi_pitch_number(440) 69.0 >>> abc.Pitch.hertz_to_midi_pitch_number(440 * 3 / 2) 75.98044999134612 """ closest_frequency_index = tools.find_closest_index( frequency, pitches_constants.MIDI_PITCH_FREQUENCIES ) closest_frequency = pitches_constants.MIDI_PITCH_FREQUENCIES[ closest_frequency_index ] closest_midi_pitch_number = pitches_constants.MIDI_PITCH_NUMBERS[ closest_frequency_index ] difference_in_cents = Pitch.hertz_to_cents(frequency, closest_frequency) return float(closest_midi_pitch_number + (difference_in_cents / 100))
# properties @property @abc.abstractmethod def frequency(self) -> float: """The frequency in Hertz of the pitch.""" raise NotImplementedError @property def midi_pitch_number(self) -> float: """The midi pitch number (from 0 to 127) of the pitch.""" return self.hertz_to_midi_pitch_number(self.frequency) # comparison methods def __lt__(self, other: "Pitch") -> bool: return self.frequency < other.frequency def __eq__(self, other: object) -> bool: try: return self.frequency == other.frequency # type: ignore except AttributeError: return False
[docs]@functools.total_ordering # type: ignore class Volume(Parameter): """Abstract base class for any volume class. If the user wants to define a new volume class, the abstract property :attr:`` has to be overridden. """
[docs] @staticmethod def decibel_to_amplitude_ratio( decibel: constants.Real, reference_amplitude: constants.Real = 1 ) -> float: """Convert decibel to amplitude ratio. :param decibel: The decibel number that shall be converted. :param reference_amplitude: The amplitude for decibel == 0. **Example:** >>> from mutwo.parameters import abc >>> abc.Volume.decibel_to_amplitude_ratio(0) 1 >>> abc.Volume.decibel_to_amplitude_ratio(-6) 0.5011872336272722 >>> abc.Volume.decibel_to_amplitude_ratio(0, reference_amplitude=0.25) 0.25 """ return float(reference_amplitude * (10 ** (decibel / 20)))
[docs] @staticmethod def decibel_to_power_ratio( decibel: constants.Real, reference_amplitude: constants.Real = 1 ) -> float: """Convert decibel to power ratio. :param decibel: The decibel number that shall be converted. :param reference_amplitude: The amplitude for decibel == 0. **Example:** >>> from mutwo.parameters import abc >>> abc.Volume.decibel_to_power_ratio(0) 1 >>> abc.Volume.decibel_to_power_ratio(-6) 0.251188643150958 >>> abc.Volume.decibel_to_power_ratio(0, reference_amplitude=0.25) 0.25 """ return float(reference_amplitude * (10 ** (decibel / 10)))
[docs] @staticmethod def amplitude_ratio_to_decibel( amplitude: constants.Real, reference_amplitude: constants.Real = 1 ) -> float: """Convert amplitude ratio to decibel. :param amplitude: The amplitude that shall be converted. :param reference_amplitude: The amplitude for decibel == 0. **Example:** >>> from mutwo.parameters import abc >>> abc.Volume.amplitude_ratio_to_decibel(1) 0 >>> abc.Volume.amplitude_ratio_to_decibel(0) inf >>> abc.Volume.amplitude_ratio_to_decibel(0.5) -6.020599913279624 """ if amplitude == 0: return float("-inf") else: return float(20 * math.log10(amplitude / reference_amplitude))
[docs] @staticmethod def power_ratio_to_decibel( amplitude: constants.Real, reference_amplitude: constants.Real = 1 ) -> float: """Convert power ratio to decibel. :param amplitude: The amplitude that shall be converted. :param reference_amplitude: The amplitude for decibel == 0. **Example:** >>> from mutwo.parameters import abc >>> abc.Volume.power_ratio_to_decibel(1) 0 >>> abc.Volume.power_ratio_to_decibel(0) inf >>> abc.Volume.power_ratio_to_decibel(0.5) -3.010299956639812 """ if amplitude == 0: return float("-inf") else: return float(10 * math.log10(amplitude / reference_amplitude))
[docs] @staticmethod def amplitude_to_midi_velocity(amplitude: constants.Real) -> int: """Convert volume (floating point number from 0 to 1) to midi velocity. :param amplitude: A value from 0 to 1 that shall be converted to midi velocity (0 to 127). :return: The midi velocity. The method clips values that are higher than 1 / lower than 0. **Example:** >>> from mutwo.parameters import abc >>> abc.Volume.amplitude_to_midi_velocity(1) 127 >>> abc.Volume.amplitude_to_midi_velocity(0) 0 """ velocity = int(round(amplitude * volumes_constants.MAXIMUM_VELOCITY)) # clip velocity if velocity < volumes_constants.MINIMUM_VELOCITY: velocity = volumes_constants.MINIMUM_VELOCITY if velocity > volumes_constants.MAXIMUM_VELOCITY: velocity = volumes_constants.MAXIMUM_VELOCITY return velocity
# properties @property @abc.abstractmethod def amplitude(self) -> constants.Real: """The amplitude of the Volume (a number from 0 to 1).""" raise NotImplementedError @property def decibel(self) -> constants.Real: """The decibel of the volume (from -120 to 0)""" return self.amplitude_ratio_to_decibel(self.amplitude) @property def midi_velocity(self) -> int: """The velocity of the volume (from 0 to 1).""" return self.amplitude_to_midi_velocity(self.amplitude) # comparison methods def __lt__(self, other: "Volume") -> bool: return self.amplitude < other.amplitude def __eq__(self, other: object) -> bool: try: return self.amplitude == other.amplitude # type: ignore except AttributeError: return False
@dataclasses.dataclass() # type: ignore class Indicator(Parameter): @property @abc.abstractmethod def is_active(self) -> bool: raise NotImplementedError() def get_arguments_dict(self) -> typing.Dict[str, typing.Any]: return { key: getattr(self, key) for key in self.__dataclass_fields__.keys() # type: ignore }
[docs]class PlayingIndicator(Indicator): pass
class ExplicitPlayingIndicator(PlayingIndicator): def __init__(self, is_active: bool = False): self.is_active = is_active def __repr__(self): return "{}({})".format(type(self).__name__, self.is_active) def get_arguments_dict(self) -> typing.Dict[str, typing.Any]: return {"is_active": self.is_active} @property def is_active(self) -> bool: return self._is_active @is_active.setter def is_active(self, is_active: bool): self._is_active = is_active @dataclasses.dataclass() class ImplicitPlayingIndicator(PlayingIndicator): @property def is_active(self) -> bool: return all( tuple( argument is not None for argument in self.get_arguments_dict().values() ) )
[docs]class NotationIndicator(Indicator): @property def is_active(self) -> bool: return all( tuple( argument is not None for argument in self.get_arguments_dict().values() ) )
T = typing.TypeVar("T", PlayingIndicator, NotationIndicator) @dataclasses.dataclass(frozen=True) class IndicatorCollection(typing.Generic[T]): def get_all_indicator(self) -> typing.Tuple[T, ...]: return tuple( getattr(self, key) for key in self.__dataclass_fields__.keys() # type: ignore ) def get_indicator_dict(self) -> typing.Dict[str, Indicator]: return {key: getattr(self, key) for key in self.__dataclass_fields__.keys()} # type: ignore