Source code for mutwo.events.music

"""Event classes which are designated for musical usage."""

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

import numbers
import typing

from mutwo import events
from mutwo import parameters
from mutwo.utilities import constants

__all__ = ("NoteLike",)

PitchOrPitches = typing.Union[
    parameters.abc.Pitch, typing.Iterable, constants.Real, None
]

Volume = typing.Union[parameters.abc.Volume, constants.Real, str]


[docs]class NoteLike(events.basic.SimpleEvent): """NoteLike represents traditional discreet musical objects. :param pitch_or_pitches: The pitch or pitches of the event. This can be a pitch object (any class that inherits from ``mutwo.parameters.abc.Pitch``) or a list of pitch objects. Furthermore mutwo supports syntactic sugar to convert other objects on the fly to pitch objects: Atring can be read as pitch class names to build :class:`mutwo.parameters.pitches.WesternPitch` objects or as ratios to build :class:`mutwo.parameters.pitches.JustIntonationPitch` objects. Fraction will also build :class:`mutwo.parameters.pitches.JustIntonationPitch` objects. Other numbers (integer and float) will be read as pitch class numbers to make :class:`mutwo.parameters.pitches.WesternPitch` objects. :param new_duration: The duration of ``NoteLike``. This can be any number. The unit of the duration is up to the interpretation of the user and the respective converter routine that will be used. :param volume: The volume of the event. Can either be a object of :mod:`mutwo.parameters.volumes`, a number or a string. If the number ranges from 0 to 1, mutwo automatically generates a :class:`mutwo.parameters.volumes.DirectVolume` object (and the number will be interpreted as the amplitude). If the number is smaller than 0, automatically generates a :class:`mutwo.parameters.volumes.DecibelVolume` object (and the number will be interpreted as decibel). If the argument is a string, `mutwo` will try to initialise a :class:`mutwo.parameters.volumes.WesternVolume` object. :param playing_indicators: :param notation_indicators: By default mutwo doesn't differentiate between Tones, Chords and Rests, but rather simply implements one general class which can represent any of the mentioned definitions (e.g. a NoteLike object with several pitches may be called a 'Chord' and a NoteLike object with only one pitch may be called a 'Tone'). **Example:** >>> from mutwo.parameters import pitches >>> from mutwo.events import music >>> tone = music.NoteLike(pitches.WesternPitch('a'), 1, 1) >>> other_tone = music.NoteLike('3/2', 1, 0.5) >>> chord = music.NoteLike( [pitches.WesternPitch('a'), pitches.JustIntonationPitch('3/2')], 1, 1 ) >>> other_chord = music.NoteLike('c4 dqs3 10/7', 1, 3) """ def __init__( self, pitch_or_pitches: PitchOrPitches = "c", duration: parameters.abc.DurationType = 1, volume: Volume = "mf", playing_indicators: parameters.playing_indicators.PlayingIndicatorCollection = None, notation_indicators: parameters.notation_indicators.NotationIndicatorCollection = None, # before_grace_notes # TODO(add grace note container!) # after_grace_notes ): if playing_indicators is None: playing_indicators = ( events.music_constants.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS() ) if notation_indicators is None: notation_indicators = ( events.music_constants.DEFAULT_NOTATION_INDICATORS_COLLECTION_CLASS() ) self.pitch_or_pitches = pitch_or_pitches self.volume = volume super().__init__(duration) self.playing_indicators = playing_indicators self.notation_indicators = notation_indicators # ###################################################################### # # static methods # # ###################################################################### # @staticmethod def _convert_string_to_pitch(pitch_indication: str) -> parameters.abc.Pitch: # assumes it is a ratio if "/" in pitch_indication: return parameters.pitches.JustIntonationPitch(pitch_indication) # assumes it is a WesternPitch name elif ( pitch_indication[0] in parameters.pitches_constants.DIATONIC_PITCH_NAME_TO_PITCH_CLASS.keys() ): if pitch_indication[-1].isdigit(): pitch_name, octave = pitch_indication[:-1], int(pitch_indication[-1]) pitch = parameters.pitches.WesternPitch(pitch_name, octave) else: pitch = parameters.pitches.WesternPitch(pitch_indication) return pitch else: message = ( "Can't build pitch from pitch_indication '{}'. Supported string formats" " are (1) ratios divided by a forward slash (for instance '3/2' or" " '4/3') and (2) names of western pitch classes with an optional number" " to indicate the octave (for instance 'c4', 'as' or 'fqs2')." ) raise NotImplementedError(message) @staticmethod def _convert_fraction_to_pitch( pitch_indication: fractions.Fraction, ) -> parameters.abc.Pitch: return parameters.pitches.JustIntonationPitch(pitch_indication) @staticmethod def _convert_float_or_integer_to_pitch( pitch_indication: float, ) -> parameters.abc.Pitch: return parameters.pitches.WesternPitch(pitch_indication) @staticmethod def _convert_unknown_object_to_pitch( unknown_object: typing.Any, ) -> typing.List[parameters.abc.Pitch]: if unknown_object is None: pitches = [] elif isinstance(unknown_object, parameters.abc.Pitch): pitches = [unknown_object] elif isinstance(unknown_object, str): pitches = [ NoteLike._convert_string_to_pitch(pitch_indication) for pitch_indication in unknown_object.split(" ") ] elif isinstance(unknown_object, fractions.Fraction): pitches = [NoteLike._convert_fraction_to_pitch(unknown_object)] elif isinstance(unknown_object, float) or isinstance(unknown_object, int): pitches = [NoteLike._convert_float_or_integer_to_pitch(unknown_object)] else: message = "Can't build pitch object from object '{}' of type '{}'.".format( unknown_object, type(unknown_object) ) raise NotImplementedError(message) return pitches # ###################################################################### # # properties # # ###################################################################### # @property def _parameters_to_print(self) -> typing.Tuple[str, ...]: """Return tuple of attribute names which shall be printed for repr. """ return tuple( attribute for attribute in self._parameters_to_compare if attribute not in ("playing_indicators", "notation_indicators") ) @property def pitch_or_pitches(self) -> typing.Any: """The pitch or pitches of the event.""" return self._pitch_or_pitches @pitch_or_pitches.setter def pitch_or_pitches(self, pitch_or_pitches: typing.Any): # make sure pitch_or_pitches always become assigned to a list of pitches, # to be certain of the returned type if not isinstance(pitch_or_pitches, str) and isinstance( pitch_or_pitches, typing.Iterable ): # several pitches pitches_per_element = ( NoteLike._convert_unknown_object_to_pitch(pitch) for pitch in pitch_or_pitches ) pitch_or_pitches = [] for pitches in pitches_per_element: pitch_or_pitches.extend(pitches) else: pitch_or_pitches = NoteLike._convert_unknown_object_to_pitch( pitch_or_pitches ) self._pitch_or_pitches = pitch_or_pitches @property def volume(self) -> typing.Any: """The volume of the event.""" return self._volume @volume.setter def volume(self, volume: typing.Any): if isinstance(volume, numbers.Real): if volume >= 0: # type: ignore volume = parameters.volumes.DirectVolume(volume) # type: ignore else: volume = parameters.volumes.DecibelVolume(volume) # type: ignore elif isinstance(volume, str): volume = parameters.volumes.WesternVolume(volume) elif not isinstance(volume, parameters.abc.Volume): message = ( "Can't initialise '{}' with value '{}' of type '{}' for argument" " 'volume'. The type for 'volume' should be '{}'.".format( type(self).__name__, volume, type(volume), Volume ) ) raise TypeError(message) self._volume = volume