Source code for mutwo.music_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 core_events
from mutwo import core_constants
from mutwo import music_events
from mutwo import music_parameters


__all__ = ("NoteLike",)

PitchOrPitchSequence = typing.Union[
    music_parameters.abc.Pitch, typing.Sequence, core_constants.Real, None
]

Volume = typing.Union[music_parameters.abc.Volume, core_constants.Real, str]
GraceNotes = core_events.SequentialEvent[core_events.SimpleEvent]


[docs]class NoteLike(core_events.SimpleEvent): """NoteLike represents traditional discreet musical objects. :param pitch_list: The pitch or pitches of the event. This can be a pitch object (any class that inherits from ``mutwo.music_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.music_parameters.WesternPitch` objects or as ratios to build :class:`mutwo.music_parameters.JustIntonationPitch` objects. Fraction will also build :class:`mutwo.music_parameters.JustIntonationPitch` objects. Other numbers (integer and float) will be read as pitch class numbers to make :class:`mutwo.music_parameters.WesternPitch` objects. :param 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.music_parameters.abc.Volume`, a number or a string. If the number ranges from 0 to 1, mutwo automatically generates a :class:`mutwo.music_parameters.DirectVolume` object (and the number will be interpreted as the amplitude). If the number is smaller than 0, automatically generates a :class:`mutwo.music_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.music_parameters.volumes.WesternVolume` object. :param grace_note_sequential_event: :type grace_note_sequential_event: core_events.SequentialEvent[NoteLike] :param after_grace_note_sequential_event: :type after_grace_note_sequential_event: core_events.SequentialEvent[NoteLike] :param playing_indicator_collection: A :class:`~mutwo.music_parameters.playing_indicator_collection.PlayingIndicatorCollection`. Playing indicators alter the sound of :class:`NoteLike` (e.g. tremolo, fermata, pizzicato). :type playing_indicator_collection: music_parameters.playing_indicator_collection.PlayingIndicatorCollection :param notation_indicator_collection: A :class:`~mutwo.music_parameters.notation_indicator_collection.NotationIndicatorCollection`. Notation indicators alter the visual representation of :class:`NoteLike` (e.g. ottava, clefs) without affecting the resulting sound. :type notation_indicator_collection: music_parameters.notation_indicator_collection.NotationIndicatorCollection :param lyric: :type lyric: core_parameters.abc.Lyric 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 import music_parameters >>> from mutwo import music_events >>> tone = music_events.NoteLike(music_parameters.WesternPitch('a'), 1, 1) >>> other_tone = music_events.NoteLike('3/2', 1, 0.5) >>> chord = music_events.NoteLike( [music_parameters.WesternPitch('a'), music_parameters.JustIntonationPitch('3/2')], 1, 1 ) >>> other_chord = music_events.NoteLike('c4 dqs3 10/7', 1, 3) """ def __init__( self, pitch_list: PitchOrPitchSequence = "c", duration: core_constants.DurationType = 1, volume: Volume = "mf", grace_note_sequential_event: typing.Optional[GraceNotes] = None, after_grace_note_sequential_event: typing.Optional[GraceNotes] = None, playing_indicator_collection: music_parameters.PlayingIndicatorCollection = None, notation_indicator_collection: music_parameters.NotationIndicatorCollection = None, lyric: music_parameters.abc.Lyric = music_parameters.DirectLyric(""), ): if playing_indicator_collection is None: playing_indicator_collection = ( music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS() ) if notation_indicator_collection is None: notation_indicator_collection = ( music_events.configurations.DEFAULT_NOTATION_INDICATORS_COLLECTION_CLASS() ) if grace_note_sequential_event is None: grace_note_sequential_event = core_events.SequentialEvent([]) if after_grace_note_sequential_event is None: after_grace_note_sequential_event = core_events.SequentialEvent([]) self.pitch_list = pitch_list self.volume = volume super().__init__(duration) self.grace_note_sequential_event = grace_note_sequential_event self.after_grace_note_sequential_event = after_grace_note_sequential_event self.playing_indicator_collection = playing_indicator_collection self.notation_indicator_collection = notation_indicator_collection self.lyric = lyric # ###################################################################### # # static methods # # ###################################################################### # @staticmethod def _convert_string_to_pitch(pitch_indication: str) -> music_parameters.abc.Pitch: # assumes it is a ratio if "/" in pitch_indication: return music_parameters.JustIntonationPitch(pitch_indication) # assumes it is a WesternPitch name elif ( pitch_indication[0] in music_parameters.constants.DIATONIC_PITCH_CLASS_CONTAINER ): if pitch_indication[-1].isdigit(): pitch_name, octave = pitch_indication[:-1], int(pitch_indication[-1]) pitch = music_parameters.WesternPitch(pitch_name, octave) else: pitch = music_parameters.WesternPitch(pitch_indication) return pitch else: raise NotImplementedError( f"Can't build pitch from pitch_indication '{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')." ) @staticmethod def _convert_fraction_to_pitch( pitch_indication: fractions.Fraction, ) -> music_parameters.abc.Pitch: return music_parameters.JustIntonationPitch(pitch_indication) @staticmethod def _convert_float_or_integer_to_pitch( pitch_indication: float, ) -> music_parameters.abc.Pitch: return music_parameters.WesternPitch(pitch_indication) @staticmethod def _convert_unknown_object_to_pitch( unknown_object: typing.Any, ) -> list[music_parameters.abc.Pitch]: if unknown_object is None: pitch_list = [] elif isinstance(unknown_object, music_parameters.abc.Pitch): pitch_list = [unknown_object] elif isinstance(unknown_object, str): pitch_list = [ NoteLike._convert_string_to_pitch(pitch_indication) for pitch_indication in unknown_object.split(" ") ] elif isinstance(unknown_object, fractions.Fraction): pitch_list = [NoteLike._convert_fraction_to_pitch(unknown_object)] elif isinstance(unknown_object, float) or isinstance(unknown_object, int): pitch_list = [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 pitch_list @staticmethod def _convert_unknown_object_to_grace_note_sequential_event( unknown_object: typing.Union[GraceNotes, core_events.SimpleEvent] ) -> GraceNotes: if isinstance(unknown_object, core_events.SimpleEvent): return core_events.SequentialEvent([unknown_object]) elif isinstance(unknown_object, core_events.SequentialEvent): return unknown_object else: raise TypeError(f"Can't set grace notes to {unknown_object}") # ###################################################################### # # properties # # ###################################################################### # @property def _parameter_to_print_tuple(self) -> tuple[str, ...]: """Return tuple of attribute names which shall be printed for repr.""" return tuple( attribute for attribute in self._parameter_to_compare_tuple if attribute # Avoid too verbose and long attributes not in ( "playing_indicator_collection", "notation_indicator_collection", "grace_note_sequential_event", "after_grace_note_sequential_event", ) ) @property def pitch_list(self) -> typing.Any: """The pitch or pitches of the event.""" return self._pitch_list @pitch_list.setter def pitch_list(self, pitch_list: typing.Any): # make sure pitch_list always become assigned to a list of pitches, # to be certain of the returned type if not isinstance(pitch_list, str) and isinstance(pitch_list, typing.Iterable): # several pitches pitches_per_element = ( NoteLike._convert_unknown_object_to_pitch(pitch) for pitch in pitch_list ) pitch_list = [] for pitches in pitches_per_element: pitch_list.extend(pitches) else: pitch_list = NoteLike._convert_unknown_object_to_pitch(pitch_list) self._pitch_list = pitch_list @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 = music_parameters.DirectVolume(volume) # type: ignore else: volume = music_parameters.DecibelVolume(volume) # type: ignore elif isinstance(volume, str): volume = music_parameters.WesternVolume(volume) elif not isinstance(volume, music_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 @property def grace_note_sequential_event(self) -> GraceNotes: """:class:`core_events.SequentialEvent` before :class:`NoteLike`""" return self._grace_note_sequential_event @grace_note_sequential_event.setter def grace_note_sequential_event( self, grace_note_sequential_event: typing.Union[GraceNotes, core_events.SimpleEvent], ): self._grace_note_sequential_event = ( NoteLike._convert_unknown_object_to_grace_note_sequential_event( grace_note_sequential_event ) ) @property def after_grace_note_sequential_event(self) -> GraceNotes: """:class:`core_events.SequentialEvent` after :class:`NoteLike`""" return self._after_grace_note_sequential_event @after_grace_note_sequential_event.setter def after_grace_note_sequential_event( self, after_grace_note_sequential_event: typing.Union[ GraceNotes, core_events.SimpleEvent ], ): self._after_grace_note_sequential_event = ( NoteLike._convert_unknown_object_to_grace_note_sequential_event( after_grace_note_sequential_event ) )