Source code for mutwo.music_parameters.abc

"""Abstract base classes for different parameters.

This module defines the public API of parameters.
Most other mutwo classes rely on this API. This means
when someone creates a new class inheriting from any of the
abstract parameter classes which are defined in this module,
she or he can make use of all other mutwo modules with this
newly created parameter class.
"""

from __future__ import annotations

import abc
import copy
import dataclasses
import functools
import math
import typing

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

import ranges

from mutwo import core_constants
from mutwo import core_events
from mutwo import core_parameters
from mutwo import core_utilities
from mutwo import music_parameters

__all__ = (
    "PitchInterval",
    "Pitch",
    "Volume",
    "PitchAmbitus",
    "PlayingIndicator",
    "NotationIndicator",
    "Lyric",
    "Syllable",
    "Instrument",
    "PitchedInstrument",
)


[docs]class PitchInterval( core_parameters.abc.SingleNumberParameter, value_name="interval", value_return_type=float, ): """Abstract base class for any pitch interval class If the user wants to define a new pitch interval class, the abstract property :attr:`interval` and the abstract method `inverse` have to be overridden. :attr:`interval` is stored in unit `cents`. See `wikipedia entry <https://en.wikipedia.org/wiki/Cent_(music)>`_ for definition of 'cents'. """ def __repr__(self) -> str: return str(self)
[docs] @abc.abstractmethod def inverse(self, mutate: bool = False) -> PitchInterval: """Makes falling interval to rising and vice versa. In `music21` the method for equal semantics is called `reverse <https://web.mit.edu/music21/doc/moduleReference/moduleInterval.html#music21.interval.Interval.reverse>`_. """
def __add__(self, other: PitchInterval) -> PitchInterval: return music_parameters.DirectPitchInterval(self.interval + other.interval) def __sub__(self, other: PitchInterval) -> PitchInterval: return music_parameters.DirectPitchInterval(self.interval - other.interval)
[docs]class Pitch( core_parameters.abc.SingleNumberParameter, core_parameters.abc.ParameterWithEnvelope, value_name="frequency", value_return_type=float, ): """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. Starting from mutwo version = 0.46.0 the user will furthermore have to define an :func:`add` method. """
[docs] class PitchEnvelope(core_events.Envelope): """Default resolution envelope class for :class:`Pitch`""" def __init__( self, *args, event_to_parameter: typing.Optional[ typing.Callable[[core_events.abc.Event], core_constants.ParameterType] ] = None, value_to_parameter: typing.Optional[ typing.Callable[ [core_events.Envelope.Value], core_constants.ParameterType ] ] = None, parameter_to_value: typing.Optional[ typing.Callable[ [core_constants.ParameterType], core_events.Envelope.Value ] ] = None, apply_parameter_on_event: typing.Optional[ typing.Callable[ [core_events.abc.Event, core_constants.ParameterType], None ] ] = None, **kwargs, ): event_to_parameter = event_to_parameter or self._event_to_parameter value_to_parameter = value_to_parameter or self._value_to_parameter apply_parameter_on_event = ( apply_parameter_on_event or self._apply_parameter_on_event ) parameter_to_value = parameter_to_value or self._parameter_to_value super().__init__( *args, event_to_parameter=event_to_parameter, value_to_parameter=value_to_parameter, parameter_to_value=parameter_to_value, apply_parameter_on_event=apply_parameter_on_event, **kwargs, )
[docs] @classmethod def frequency_and_envelope_to_pitch( cls, frequency: core_constants.Real, envelope: typing.Optional[ Pitch.PitchIntervalEnvelope | typing.Sequence ] = None, ) -> Pitch: return music_parameters.DirectPitch(frequency, envelope=envelope)
@classmethod def _value_to_parameter( cls, value: core_events.Envelope.Value, # type: ignore ) -> core_constants.ParameterType: # For inner calculation (value) cents are used instead # of frequencies. In this way we can ensure that the transitions # are closer to the human logarithmic hearing. # See als `_parameter_to_value`. frequency = ( Pitch.cents_to_ratio(value) * music_parameters.constants.PITCH_ENVELOPE_REFERENCE_FREQUENCY ) return cls.frequency_and_envelope_to_pitch(frequency) @classmethod def _event_to_parameter( cls, event: core_events.abc.Event ) -> core_constants.ParameterType: if hasattr( event, music_parameters.configurations.DEFAULT_PITCH_ENVELOPE_PARAMETER_NAME, ): return getattr( event, music_parameters.configurations.DEFAULT_PITCH_ENVELOPE_PARAMETER_NAME, ) else: return cls.frequency_and_envelope_to_pitch( music_parameters.configurations.DEFAULT_CONCERT_PITCH ) @classmethod def _apply_parameter_on_event( cls, event: core_events.abc.Event, parameter: core_constants.ParameterType ): setattr( event, music_parameters.configurations.DEFAULT_PITCH_ENVELOPE_PARAMETER_NAME, parameter, ) @classmethod def _parameter_to_value( cls, parameter: core_constants.ParameterType ) -> core_constants.Real: # For inner calculation (value) cents are used instead # of frequencies. In this way we can ensure that the transitions # are closer to the human logarithmic hearing. # See als `_value_to_parameter`. return Pitch.hertz_to_cents( music_parameters.constants.PITCH_ENVELOPE_REFERENCE_FREQUENCY, parameter.frequency, )
[docs] class PitchIntervalEnvelope(core_events.RelativeEnvelope): """Default envelope class for :class:`Pitch` Resolves into :class:`Pitch.PitchEnvelope`. """ def __init__( self, *args, event_to_parameter: typing.Optional[ typing.Callable[[core_events.abc.Event], core_constants.ParameterType] ] = None, value_to_parameter: typing.Optional[ typing.Callable[ [core_events.Envelope.Value], core_constants.ParameterType ] ] = None, parameter_to_value: typing.Callable[ [core_constants.ParameterType], core_events.Envelope.Value ] = lambda parameter: parameter.interval, apply_parameter_on_event: typing.Optional[ typing.Callable[ [core_events.abc.Event, core_constants.ParameterType], None ] ] = None, base_parameter_and_relative_parameter_to_absolute_parameter: typing.Optional[ typing.Callable[ [core_constants.ParameterType, core_constants.ParameterType], core_constants.ParameterType, ] ] = None, **kwargs, ): if not event_to_parameter: event_to_parameter = self._event_to_parameter if not value_to_parameter: value_to_parameter = self._value_to_parameter if not apply_parameter_on_event: apply_parameter_on_event = self._apply_parameter_on_event if not base_parameter_and_relative_parameter_to_absolute_parameter: base_parameter_and_relative_parameter_to_absolute_parameter = ( self._base_parameter_and_relative_parameter_to_absolute_parameter ) super().__init__( *args, event_to_parameter=event_to_parameter, value_to_parameter=value_to_parameter, parameter_to_value=parameter_to_value, apply_parameter_on_event=apply_parameter_on_event, base_parameter_and_relative_parameter_to_absolute_parameter=base_parameter_and_relative_parameter_to_absolute_parameter, **kwargs, )
[docs] @classmethod def cents_to_pitch_interval(cls, cents: core_constants.Real) -> PitchInterval: return music_parameters.DirectPitchInterval(cents)
@classmethod def _event_to_parameter( cls, event: core_events.abc.Event ) -> core_constants.ParameterType: if hasattr( event, music_parameters.configurations.DEFAULT_PITCH_INTERVAL_ENVELOPE_PARAMETER_NAME, ): return getattr( event, music_parameters.configurations.DEFAULT_PITCH_INTERVAL_ENVELOPE_PARAMETER_NAME, ) else: return cls.cents_to_pitch_interval(0) @classmethod def _value_to_parameter( cls, value: core_events.Envelope.Value ) -> core_constants.ParameterType: return cls.cents_to_pitch_interval(value) @classmethod def _apply_parameter_on_event( cls, event: core_events.abc.Event, parameter: core_constants.ParameterType ): setattr( event, music_parameters.configurations.DEFAULT_PITCH_INTERVAL_ENVELOPE_PARAMETER_NAME, parameter, ), @classmethod def _base_parameter_and_relative_parameter_to_absolute_parameter( cls, base_parameter: Pitch, relative_parameter: PitchInterval ) -> Pitch: return base_parameter + relative_parameter
def __init__( self, envelope: typing.Optional[Pitch.PitchIntervalEnvelope | typing.Sequence] = None, ): self.envelope = envelope # ###################################################################### # # conversion methods between different pitch describing units # # ###################################################################### #
[docs] @staticmethod def hertz_to_cents( frequency0: core_constants.Real, frequency1: core_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 import music_parameters >>> music_parameters.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 import music_parameters >>> music_parameters.abc.Pitch.ratio_to_cents(fractions.Fraction(3, 2)) 701.9550008653874 """ return music_parameters.constants.CENT_CALCULATION_CONSTANT * math.log10(ratio)
[docs] @staticmethod def cents_to_ratio(cents: core_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 import music_parameters >>> music_parameters.abc.Pitch.cents_to_ratio(1200) Fraction(2, 1) """ return fractions.Fraction( 10 ** (cents / music_parameters.constants.CENT_CALCULATION_CONSTANT) )
[docs] @staticmethod def hertz_to_midi_pitch_number(frequency: core_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 import music_parameters >>> music_parameters.abc.Pitch.hertz_to_midi_pitch_number(440) 69.0 >>> music_parameters.abc.Pitch.hertz_to_midi_pitch_number(440 * 3 / 2) 75.98044999134612 """ closest_frequency_index = core_utilities.find_closest_index( frequency, music_parameters.constants.MIDI_PITCH_FREQUENCY_TUPLE ) closest_frequency = music_parameters.constants.MIDI_PITCH_FREQUENCY_TUPLE[ closest_frequency_index ] closest_midi_pitch_number = music_parameters.constants.MIDI_PITCH_NUMBER_TUPLE[ closest_frequency_index ] difference_in_cents = Pitch.hertz_to_cents(frequency, closest_frequency) return float(closest_midi_pitch_number + (difference_in_cents / 100))
# ###################################################################### # # public properties # # ###################################################################### # @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) @core_parameters.abc.ParameterWithEnvelope.envelope.setter def envelope( self, envelope_or_envelope_argument: typing.Optional[ Pitch.PitchIntervalEnvelope | typing.Sequence ], ): if not envelope_or_envelope_argument: generic_pitch_interval = self.PitchIntervalEnvelope.cents_to_pitch_interval( 0 ) envelope = self.PitchIntervalEnvelope([[0, generic_pitch_interval]]) elif isinstance(envelope_or_envelope_argument, core_events.RelativeEnvelope): envelope = envelope_or_envelope_argument else: envelope = self.PitchIntervalEnvelope(envelope_or_envelope_argument) self._envelope = envelope # ###################################################################### # # comparison methods # # ###################################################################### #
[docs] @abc.abstractmethod def add(self, pitch_interval: PitchInterval, mutate: bool = True) -> Pitch: ...
[docs] @core_utilities.add_copy_option def subtract(self, pitch_interval: music_parameters.abc.PitchInterval) -> Pitch: return self.add(music_parameters.DirectPitchInterval(-pitch_interval.interval)) # type: ignore
def __add__(self, pitch_interval: PitchInterval) -> Pitch: return self.add(pitch_interval, mutate=False) def __sub__(self, pitch_interval: PitchInterval) -> Pitch: return self.subtract(pitch_interval, mutate=False)
[docs] def resolve_envelope( self, duration: core_constants.DurationType, resolve_envelope_class: typing.Optional[type[core_events.Envelope]] = None, ) -> core_events.Envelope: if not resolve_envelope_class: resolve_envelope_class = Pitch.PitchEnvelope return super().resolve_envelope(duration, resolve_envelope_class)
[docs] def get_pitch_interval(self, pitch_to_compare: Pitch) -> PitchInterval: """Get :class:`PitchInterval` between itself and other pitch :param pitch_to_compare: The pitch which shall be compared to the active pitch. :type pitch_to_compare: Pitch :return: :class:`PitchInterval` between **Example:** >>> from mutwo import music_parameters >>> a4 = music_parameters.DirectPitch(frequency=440) >>> a5 = music_parameters.DirectPitch(frequency=880) >>> pitch_interval = a4.get_pitch_interval(a5) """ cent_difference = self.ratio_to_cents( pitch_to_compare.frequency / self.frequency ) return music_parameters.DirectPitchInterval(cent_difference)
[docs]@functools.total_ordering # type: ignore class Volume( core_parameters.abc.SingleNumberParameter, value_name="amplitude", value_return_type=float, ): """Abstract base class for any volume class. If the user wants to define a new volume class, the abstract property :attr:`amplitude` has to be overridden. """
[docs] @staticmethod def decibel_to_amplitude_ratio( decibel: core_constants.Real, reference_amplitude: core_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 import music_parameters >>> music_parameters.abc.Volume.decibel_to_amplitude_ratio(0) 1.0 >>> music_parameters.abc.Volume.decibel_to_amplitude_ratio(-6) 0.5011872336272722 >>> music_parameters.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: core_constants.Real, reference_amplitude: core_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 import music_parameters >>> music_parameters.abc.Volume.decibel_to_power_ratio(0) 1.0 >>> music_parameters.abc.Volume.decibel_to_power_ratio(-6) 0.251188643150958 >>> music_parameters.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: core_constants.Real, reference_amplitude: core_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 import music_parameters >>> music_parameters.abc.Volume.amplitude_ratio_to_decibel(1) 0.0 >>> music_parameters.abc.Volume.amplitude_ratio_to_decibel(0) -inf >>> music_parameters.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: core_constants.Real, reference_amplitude: core_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 import music_parameters >>> music_parameters.abc.Volume.power_ratio_to_decibel(1) 0.0 >>> music_parameters.abc.Volume.power_ratio_to_decibel(0) -inf >>> music_parameters.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_ratio_to_midi_velocity( amplitude: core_constants.Real, reference_amplitude: core_constants.Real = 1 ) -> int: """Convert amplitude ratio to midi velocity. :param amplitude: The amplitude which shall be converted. :type amplitude: core_constants.Real :param reference_amplitude: The amplitude for decibel == 0. :return: The midi velocity. The method clips values that are higher than 1 / lower than 0. **Example:** >>> from mutwo import music_parameters >>> music_parameters.abc.Volume.amplitude_ratio_to_midi_velocity(1) 127 >>> music_parameters.abc.Volume.amplitude_ratio_to_midi_velocity(0) 0 """ return Volume.decibel_to_midi_velocity( Volume.amplitude_ratio_to_decibel( amplitude, reference_amplitude=reference_amplitude ) )
[docs] @staticmethod def decibel_to_midi_velocity( decibel_to_convert: core_constants.Real, minimum_decibel: typing.Optional[core_constants.Real] = None, maximum_decibel: typing.Optional[core_constants.Real] = None, ) -> int: """Convert decibel to midi velocity (0 to 127). :param decibel: The decibel value which shall be converted.. :type decibel: core_constants.Real :param minimum_decibel: The decibel value which is equal to the lowest midi velocity (0). :type minimum_decibel: core_constants.Real, optional :param maximum_decibel: The decibel value which is equal to the highest midi velocity (127). :type maximum_decibel: core_constants.Real, optional :return: The midi velocity. The method clips values which are higher than 'maximum_decibel' and lower than 'minimum_decibel'. **Example:** >>> from mutwo import music_parameters >>> music_parameters.abc.Volume.decibel_to_midi_velocity(0) 127 >>> music_parameters.abc.Volume.decibel_to_midi_velocity(-40) 0 """ minimum_decibel = ( minimum_decibel or music_parameters.configurations.DEFAULT_MINIMUM_DECIBEL_FOR_MIDI_VELOCITY_AND_STANDARD_DYNAMIC_INDICATOR ) maximum_decibel = ( maximum_decibel or music_parameters.configurations.DEFAULT_MAXIMUM_DECIBEL_FOR_MIDI_VELOCITY_AND_STANDARD_DYNAMIC_INDICATOR ) if decibel_to_convert > maximum_decibel: decibel_to_convert = maximum_decibel if decibel_to_convert < minimum_decibel: decibel_to_convert = minimum_decibel velocity = int( core_utilities.scale( decibel_to_convert, minimum_decibel, maximum_decibel, music_parameters.constants.MINIMUM_VELOCITY, music_parameters.constants.MAXIMUM_VELOCITY, ) ) return velocity
# properties @property def decibel(self) -> core_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 127).""" return self.decibel_to_midi_velocity(self.decibel)
[docs]class PitchAmbitus(abc.ABC): """Abstract base class for all pitch ambituses. To setup a new PitchAmbitus class override the abstract method `pitch_to_period`. """ def __init__(self, minima_pitch: Pitch, maxima_pitch: Pitch) -> None: try: assert minima_pitch < maxima_pitch except AssertionError: raise ValueError( ( f"Found minima_pitch: {minima_pitch} and " f"maxima_pitch={maxima_pitch}. The minima pitch has to be " "a lower pitch than the maxima pitch!" ) ) self.minima_pitch = minima_pitch self.maxima_pitch = maxima_pitch # ######################################################## # # abstract methods # # ######################################################## #
[docs] @abc.abstractmethod def pitch_to_period(self, pitch: Pitch) -> PitchInterval: ...
# ######################################################## # # magic methods # # ######################################################## # def __repr__(self) -> str: return f"{type(self).__name__}{self.border_tuple}" def __str__(self) -> str: return repr(self) def __iter__(self) -> typing.Iterator[Pitch]: return iter(self.border_tuple) def __getitem__(self, index: int) -> Pitch: return self.border_tuple[index] def __contains__(self, pitch: typing.Any) -> bool: return bool(self.filter_pitch_sequence((pitch,))) # ######################################################## # # properties # # ######################################################## # @property def border_tuple(self) -> tuple[Pitch, Pitch]: return (self.minima_pitch, self.maxima_pitch) @property def range(self) -> PitchInterval: return self.minima_pitch.get_pitch_interval(self.maxima_pitch) # ######################################################## # # public methods # # ######################################################## #
[docs] def get_pitch_variant_tuple( self, pitch: Pitch, period: typing.Optional[PitchInterval] = None ) -> tuple[Pitch, ...]: """Find all pitch variants (in all octaves) of the given pitch :param pitch: The pitch which variants shall be found. :type pitch: Pitch :param period: The repeating period (usually an octave). If the period is set to `None` the function will fallback to them objects method :meth:`PitchAmbitus.pitch_to_period`. Default to `None`. :type period: typing.Optional[PitchInterval] """ period = period or self.pitch_to_period(pitch) pitch_variant_list = [] is_first = True for loop_condition, append_condition, change_pitch in ( ( lambda dummy_pitch: dummy_pitch <= self.maxima_pitch, lambda dummy_pitch: dummy_pitch >= self.minima_pitch, lambda dummy_pitch: dummy_pitch.add(period), ), ( lambda dummy_pitch: dummy_pitch >= self.minima_pitch, lambda dummy_pitch: dummy_pitch <= self.maxima_pitch, lambda dummy_pitch: dummy_pitch.subtract(period), ), ): dummy_pitch = copy.copy(pitch) if not is_first: change_pitch(dummy_pitch) while loop_condition(dummy_pitch): if append_condition(dummy_pitch): pitch_variant_list.append(copy.copy(dummy_pitch)) change_pitch(dummy_pitch) is_first = False return tuple(sorted(pitch_variant_list))
[docs] def filter_pitch_sequence( self, pitch_to_filter_sequence: typing.Sequence[Pitch], ) -> tuple[Pitch, ...]: """Filter all pitches in a sequence which aren't inside the ambitus. :param pitch_to_filter_sequence: A sequence with pitches which shall be filtered. :type pitch_to_filter_sequence: typing.Sequence[Pitch] **Example:** >>> from mutwo import music_parameters >>> ambitus0 = music_parameters.OctaveAmbitus( ... music_parameters.JustIntonationPitch('1/2'), ... music_parameters.JustIntonationPitch('2/1'), ... ) >>> ambitus0.filter_pitch_sequence( ... [ ... music_parameters.JustIntonationPitch("3/8"), ... music_parameters.JustIntonationPitch("3/4"), ... music_parameters.JustIntonationPitch("3/2"), ... music_parameters.JustIntonationPitch("3/1"), ... ] ... ) (JustIntonationPitch('3/4'), JustIntonationPitch('3/2')) """ return tuple( filter( lambda pitch: pitch >= self.minima_pitch and pitch <= self.maxima_pitch, pitch_to_filter_sequence, ) )
[docs]@dataclasses.dataclass() # type: ignore class Indicator(abc.ABC): @property @abc.abstractmethod def is_active(self) -> bool: ...
[docs] def get_arguments_dict(self) -> dict[str, typing.Any]: return { key: getattr(self, key) for key in self.__dataclass_fields__.keys() # type: ignore }
[docs]class PlayingIndicator(Indicator): """Abstract base class for any playing indicator."""
[docs]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)
[docs] def get_arguments_dict(self) -> 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
[docs]@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): """Abstract base class for any notation 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)
[docs]@dataclasses.dataclass class IndicatorCollection(typing.Generic[T]):
[docs] def get_all_indicator(self) -> tuple[T, ...]: return tuple( getattr(self, key) for key in self.__dataclass_fields__.keys() # type: ignore )
[docs] def get_indicator_dict(self) -> dict[str, Indicator]: return {key: getattr(self, key) for key in self.__dataclass_fields__.keys()} # type: ignore
[docs]class Lyric( core_parameters.abc.SingleValueParameter, value_name="phonetic_representation", value_return_type=str, ): """Abstract base class for any spoken, sung or written text. If the user wants to define a new lyric class, the abstract properties :attr:`phonetic_representation` and :attr:`written_representation` have to be overridden. The :attr:`phonetic_representation` should return a string of X-SAMPA format phonemes, separated by space to indicate new words. Consult `wikipedia entry <https://en.wikipedia.org/wiki/X-SAMPA>`_ for detailed information regarding X-SAMPA. The :attr:`written_representation` should return a string of normal written text, separated by space to indicate new words. """ @property def written_representation(self) -> str: """Get text as it would be written in natural language"""
[docs]class Syllable(Lyric): """Syllable mixin for classes which inherit from :class:`Lyric`. This adds the new attribute :attr:`is_last_syllable`. This should be `True` if it is the last syllable of a word and `False` if it isn't. """ def __init__(self, is_last_syllable: bool): self.is_last_syllable = is_last_syllable
[docs]@dataclasses.dataclass(frozen=True) class Instrument(abc.ABC): """Model a musical instrument. :param name: The name of the instrument. :type name: str :param short_name: The abbreviation of the instrument. If set to ``None`` it will be the same like `name`. Default to ``None``. :type short_name: typing.Optional[str] This is an abstract class. To create a new concrete class you need to override the abstract `is_pitched` property. Alternatively you can use the ready-to-go classes :class:`mutwo.music_parameters.UnpitchedInstrument` or :class:`mutwo.music_parameters.ContinuousPitchedInstrument` or :class:`mutwo.music_parameters.DiscreetPitchedInstrument`. """ name: str short_name: typing.Optional[str] = None def __post_init__(self): # Auto set short_name to name if not declared object.__setattr__(self, "short_name", self.short_name or self.name) @property @abc.abstractmethod def is_pitched(self) -> bool: """Return ``True`` if instrument is pitched, ``False`` otherwise."""
[docs]@dataclasses.dataclass(frozen=True) class PitchedInstrument(Instrument): """Model a pitched musical instrument. :param name: The name of the instrument. :type name: str :param short_name: The abbreviation of the instrument. If set to ``None`` it will be the same like `name`. Default to ``None``. :type short_name: typing.Optional[str] :param pitch_count_range: Set how many simultaneous pitches the instrument can play. Default to `ranges.Range(1, 2)`, which means that the instrument is monophonic. :type pitch_count_range: ranges.Range :param transposition_pitch_interval: Some instruments are written with a transposition (so sounding pitch and written pitch differs). This parameter can be used to set the transposition interval in case sounding and written differs. The `transposition_pitch_interval` is added to the sounding pitches in order to reach the written pitches. If set to ``None`` this will be set to `DirectPitchInterval(0)` which is no transposition. Default to ``None``. :type transposition_pitch_interval: typing.Optional[PitchInterval] You can use pythons `in` syntax to find out if a pitch is playable by the given instrument. This is an abstract class. You need to override abstract method `__contains__` and abstract property `pitch_ambitus`. """ pitch_count_range: ranges.Range = ranges.Range(1, 2) transposition_pitch_interval: typing.Optional[ music_parameters.abc.DirectPitchInterval ] = None def __post_init__(self): super().__post_init__() object.__setattr__( self, "transposition_pitch_interval", music_parameters.DirectPitchInterval(0), ) @abc.abstractmethod def __contains__(self, pitch: typing.Any) -> bool: ... @property @abc.abstractmethod def pitch_ambitus(self) -> music_parameters.abc.PitchAmbitus: ... @property def is_pitched(self) -> bool: return True
[docs] @abc.abstractmethod def get_pitch_variant_tuple( self, pitch: Pitch, period: typing.Optional[PitchInterval] = None ) -> tuple[Pitch, ...]: """Find all pitch variants (in all octaves) of the given pitch :param pitch: The pitch which variants shall be found. :type pitch: Pitch :param period: The repeating period (usually an octave). If the period is set to `None` the function will fallback to them objects method :meth:`PitchAmbitus.pitch_to_period`. Default to `None`. :type period: typing.Optional[PitchInterval] This is not necessarily the same as ``instrument.pitch_ambitus.get_pitch_variant_tuple()``, because a :class:`mutwo.music_parameters.DiscreetPitchedInstrument` may not be capable of playing a pitch even if the given pitch is within the ambitus of an instrument. It's therefore recommended to use ``instrument.get_pitch_variant_tuple`` if one wants to find out in which octaves the given pitch is actually playable on the instrument. """