"""Apply :class:`~mutwo.music_parameters.abc.PlayingIndicator` on :class:`~mutwo.core_events.abc.Event`.
"""
import abc
import copy
import itertools
import typing
try:
import quicktions as fractions # type: ignore
except ImportError:
import fractions # type: ignore
import numpy as np
from mutwo import core_constants
from mutwo import core_converters
from mutwo import core_events
from mutwo import core_utilities
from mutwo import music_converters
from mutwo import music_parameters
from mutwo import music_utilities
__all__ = (
"PlayingIndicatorConverter",
"ArpeggioConverter",
"StacattoConverter",
"ArticulationConverter",
"TrillConverter",
"OptionalConverter",
"PlayingIndicatorsConverter",
)
[docs]class PlayingIndicatorConverter(core_converters.abc.Converter):
"""Abstract base class to apply :class:`~mutwo.music_parameters.abc.PlayingIndicator` on a :class:`~mutwo.core_events.SimpleEvent`.
:param chronon_to_playing_indicator_collection: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a
:class:`mutwo.music_parameters.PlayingIndicatorCollection`
object. By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.playing_indicator_collection`
attribute (because by default :class:`mutwo.music_events.NoteLike`
objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their playing_indicator_collection property, this argument
should be overridden. If the
function call raises an :obj:`AttributeError` (e.g. if no playing indicator
collection can be extracted), mutwo will build a playing indicator collection
from :const:`~mutwo.music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS`.
:type chronon_to_playing_indicator_collection: typing.Callable[[core_events.SimpleEvent], music_parameters.PlayingIndicatorCollection], optional
To write a new PlayingIndicatorConverter the abstract method
:func:`_apply_playing_indicator` and the abstract properties
`playing_indicator_name` and `default_playing_indicator` have
to be overridden.
"""
def __init__(
self,
chronon_to_playing_indicator_collection: typing.Callable[
[core_events.SimpleEvent],
music_parameters.PlayingIndicatorCollection,
] = music_converters.SimpleEventToPlayingIndicatorCollection(), # type: ignore
):
self._chronon_to_playing_indicator_collection = (
chronon_to_playing_indicator_collection
)
@abc.abstractmethod
def _apply_playing_indicator(
self,
chronon_to_convert: core_events.SimpleEvent,
playing_indicator: music_parameters.abc.PlayingIndicator,
) -> core_events.Consecution[core_events.SimpleEvent]:
...
@property
@abc.abstractmethod
def playing_indicator_name(self) -> str:
...
@property
@abc.abstractmethod
def default_playing_indicator(self) -> music_parameters.abc.PlayingIndicator:
...
[docs] def convert(
self, chronon_to_convert: core_events.SimpleEvent
) -> core_events.Consecution[core_events.SimpleEvent]:
"""Apply PlayingIndicator on chronon.
:param chronon_to_convert: The event which shall be converted.
:type chronon_to_convert: core_events.SimpleEvent
"""
playing_indicator_collection = (
self._chronon_to_playing_indicator_collection(
chronon_to_convert,
)
)
playing_indicator = core_utilities.call_function_except_attribute_error(
lambda playing_indicator_collection: getattr(
playing_indicator_collection, self.playing_indicator_name
),
playing_indicator_collection,
self.default_playing_indicator,
)
if playing_indicator.is_active:
return self._apply_playing_indicator(
chronon_to_convert, playing_indicator
)
else:
return core_events.Consecution([copy.deepcopy(chronon_to_convert)])
[docs]class ArpeggioConverter(PlayingIndicatorConverter):
"""Apply arpeggio on :class:`~mutwo.core_events.SimpleEvent`.
:param duration_for_each_attack: Set how long each attack of the
Arpeggio lasts. Default to 0.1.
:type duration_for_each_attack: constants.DurationType
:param chronon_to_pitch_list: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a tuple that contains pitch objects
(objects that inherit from :class:`mutwo.music_parameters.abc.Pitch`).
By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.pitch_list` attribute
(because by default :class:`mutwo.music_events.NoteLike` objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their pitch property, this argument
should be overridden.
If the function call raises an :obj:`AttributeError` (e.g. if no pitch can be
extracted), mutwo will assume an event without any pitches.
:type chronon_to_pitch_list: typing.Callable[[core_events.SimpleEvent], music_parameters.abc.Pitch], optional
:param chronon_to_playing_indicator_collection: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a
:class:`mutwo.music_parameters.PlayingIndicatorCollection`
object. By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.playing_indicator_collection`
attribute (because by default :class:`mutwo.music_events.NoteLike`
objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their playing_indicator_collection property, this argument
should be overridden. If the
function call raises an :obj:`AttributeError` (e.g. if no playing indicator
collection can be extracted), mutwo will build a playing indicator collection
from :const:`~mutwo.music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS`.
:type chronon_to_playing_indicator_collection: typing.Callable[[core_events.SimpleEvent], music_parameters.PlayingIndicatorCollection,], optional
:param set_pitch_list_for_chronon: Function which assigns
a list of :class:`~mutwo.music_parameters.abc.Pitch` objects to a
:class:`~mutwo.core_events.SimpleEvent`. By default the
function assigns the passed pitches to the
:attr:`~mutwo.music_events.NoteLike.pitch_list` attribute
(because by default :class:`mutwo.music_events.NoteLike` objects
are expected).
:type set_pitch_list_for_chronon: typing.Callable[[core_events.SimpleEvent, list[music_parameters.abc.Pitch]], None]
"""
def __init__(
self,
duration_for_each_attack: core_constants.DurationType = 0.1,
chronon_to_pitch_list: typing.Callable[
[core_events.SimpleEvent], list[music_parameters.abc.Pitch]
] = music_converters.SimpleEventToPitchList(),
chronon_to_playing_indicator_collection: typing.Callable[
[core_events.SimpleEvent],
music_parameters.PlayingIndicatorCollection,
] = music_converters.SimpleEventToPlayingIndicatorCollection(),
set_pitch_list_for_chronon: typing.Callable[
[core_events.SimpleEvent, list[music_parameters.abc.Pitch]], None
] = lambda chronon, pitch_list: chronon.set_parameter( # type: ignore
"pitch_list", pitch_list, set_unassigned_parameter=True
),
):
super().__init__(
chronon_to_playing_indicator_collection=chronon_to_playing_indicator_collection
)
self._duration_for_each_attack = (
core_events.configurations.UNKNOWN_OBJECT_TO_DURATION(
duration_for_each_attack
)
)
self._chronon_to_pitch_list = chronon_to_pitch_list
self._set_pitch_list_for_chronon = set_pitch_list_for_chronon
@property
def playing_indicator_name(self) -> str:
return "arpeggio"
@property
def default_playing_indicator(self) -> music_parameters.abc.PlayingIndicator:
return music_parameters.Arpeggio()
def _apply_playing_indicator(
self,
chronon_to_convert: core_events.SimpleEvent,
playing_indicator: music_parameters.Arpeggio,
) -> core_events.Consecution[core_events.SimpleEvent]:
pitch_list = list(self._chronon_to_pitch_list(chronon_to_convert))
# sort pitches according to Arpeggio direction
pitch_list.sort(reverse=playing_indicator.direction != "up")
converted_event: core_events.Consecution[
core_events.SimpleEvent
] = core_events.Consecution(
[copy.copy(chronon_to_convert) for _ in pitch_list]
)
# apply pitches on events
for event_index, pitch in enumerate(pitch_list):
self._set_pitch_list_for_chronon(converted_event[event_index], [pitch])
# set correct duration for each event
event_count = len(converted_event)
duration_of_each_attack = self._duration_for_each_attack
if duration_of_each_attack * event_count > chronon_to_convert.duration:
duration_of_each_attack = chronon_to_convert.duration / event_count
for event_index in range(event_count - 1):
converted_event[event_index].duration = duration_of_each_attack
converted_event[-1].duration -= (
converted_event.duration - chronon_to_convert.duration
)
return converted_event
[docs]class StacattoConverter(PlayingIndicatorConverter):
"""Apply staccato on :class:`~mutwo.core_events.SimpleEvent`.
:param factor:
:param allowed_articulation_name_sequence:
:param chronon_to_playing_indicator_collection: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a
:class:`mutwo.music_parameters.PlayingIndicatorCollection`
object. By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.playing_indicator_collection`
attribute (because by default :class:`mutwo.music_events.NoteLike`
objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their playing_indicator_collection property, this argument
should be overridden. If the
function call raises an :obj:`AttributeError` (e.g. if no playing indicator
collection can be extracted), mutwo will build a playing indicator collection
from :const:`~mutwo.music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS`.
:type chronon_to_playing_indicator_collection: typing.Callable[[core_events.SimpleEvent], music_parameters.PlayingIndicatorCollection,], optional
"""
def __init__(
self,
factor: float = 0.5,
allowed_articulation_name_sequence: typing.Sequence[str] = ("staccato", "."),
chronon_to_playing_indicator_collection: typing.Callable[
[core_events.SimpleEvent],
music_parameters.PlayingIndicatorCollection,
] = music_converters.SimpleEventToPlayingIndicatorCollection(),
):
self._allowed_articulation_name_sequence = allowed_articulation_name_sequence
self._factor = factor
super().__init__(chronon_to_playing_indicator_collection)
def _apply_playing_indicator(
self,
chronon_to_convert: core_events.SimpleEvent,
_: music_parameters.abc.PlayingIndicator,
) -> core_events.Consecution[core_events.SimpleEvent]:
duration = chronon_to_convert.duration * self._factor
consecution = core_events.Consecution(
[
chronon_to_convert.set_parameter(
"duration", duration, mutate=False
),
core_events.SimpleEvent(duration),
]
)
return consecution
@property
def playing_indicator_name(self) -> str:
return "articulation"
@property
def default_playing_indicator(self) -> music_parameters.abc.PlayingIndicator:
return music_parameters.Articulation()
[docs]class ArticulationConverter(PlayingIndicatorConverter):
"""Apply articulation on :class:`~mutwo.core_events.SimpleEvent`.
:param articulation_name_tuple_to_playing_indicator_converter:
:type articulation_name_tuple_to_playing_indicator_converter: dict[tuple[str, ...], PlayingIndicatorConverter]
:param chronon_to_playing_indicator_collection: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a
:class:`mutwo.music_parameters.PlayingIndicatorCollection`
object. By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.playing_indicator_collection`
attribute (because by default :class:`mutwo.music_events.NoteLike`
objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their playing_indicator_collection property, this argument
should be overridden. If the
function call raises an :obj:`AttributeError` (e.g. if no playing indicator
collection can be extracted), mutwo will build a playing indicator collection
from :const:`~mutwo.music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS`.
:type chronon_to_playing_indicator_collection: typing.Callable[[core_events.SimpleEvent], music_parameters.PlayingIndicatorCollection,], optional
"""
def __init__(
self,
articulation_name_tuple_to_playing_indicator_converter: dict[
tuple[str, ...], PlayingIndicatorConverter
] = {("staccato", "."): StacattoConverter()},
chronon_to_playing_indicator_collection: typing.Callable[
[core_events.SimpleEvent],
music_parameters.PlayingIndicatorCollection,
] = music_converters.SimpleEventToPlayingIndicatorCollection(),
):
self._logger = core_utilities.get_cls_logger(type(self))
articulation_name_to_playing_indicator_converter = {}
for (
articulation_name_tuple,
playing_indicator_converter,
) in articulation_name_tuple_to_playing_indicator_converter.items():
for articulation_name in articulation_name_tuple:
if (
articulation_name
not in articulation_name_to_playing_indicator_converter
):
self._logger.warning(
music_utilities.DuplicatePlayingIndicatorConverterMappingWarning(
articulation_name, playing_indicator_converter
)
)
articulation_name_to_playing_indicator_converter.update(
{articulation_name: playing_indicator_converter}
)
self._articulation_name_to_playing_indicator_converter = (
articulation_name_to_playing_indicator_converter
)
super().__init__(chronon_to_playing_indicator_collection)
def _apply_playing_indicator(
self,
chronon_to_convert: core_events.SimpleEvent,
playing_indicator: music_parameters.Articulation,
) -> core_events.Consecution[core_events.SimpleEvent]:
if (
playing_indicator.name
in self._articulation_name_to_playing_indicator_converter
):
return self._articulation_name_to_playing_indicator_converter[
playing_indicator.name
].convert(chronon_to_convert)
else:
return core_events.Consecution([copy.deepcopy(chronon_to_convert)])
@property
def playing_indicator_name(self) -> str:
return "articulation"
@property
def default_playing_indicator(self) -> music_parameters.abc.PlayingIndicator:
return music_parameters.Articulation()
[docs]class TrillConverter(PlayingIndicatorConverter):
"""Apply trill on :class:`~mutwo.core_events.SimpleEvent`.
:param trill_size:
:type trill_size: constants.DurationType
:param chronon_to_pitch_list: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a tuple that contains pitch objects
(objects that inherit from :class:`mutwo.music_parameters.abc.Pitch`).
By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.pitch_list` attribute
(because by default :class:`mutwo.music_events.NoteLike` objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their pitch property, this argument
should be overridden.
If the function call raises an :obj:`AttributeError` (e.g. if no pitch can be
extracted), mutwo will assume an event without any pitches.
:type chronon_to_pitch_list: typing.Callable[[core_events.SimpleEvent], music_parameters.abc.Pitch], optional
:param chronon_to_playing_indicator_collection: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a
:class:`mutwo.music_parameters.PlayingIndicatorCollection`
object. By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.playing_indicator_collection`
attribute (because by default :class:`mutwo.music_events.NoteLike`
objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their playing_indicator_collection property, this argument
should be overridden. If the
function call raises an :obj:`AttributeError` (e.g. if no playing indicator
collection can be extracted), mutwo will build a playing indicator collection
from :const:`~mutwo.music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS`.
:type chronon_to_playing_indicator_collection: typing.Callable[[core_events.SimpleEvent], music_parameters.PlayingIndicatorCollection,], optional
"""
def __init__(
self,
trill_size: core_constants.DurationType = fractions.Fraction(1, 16),
chronon_to_pitch_list: typing.Callable[
[core_events.SimpleEvent], list[music_parameters.abc.Pitch]
] = music_converters.SimpleEventToPitchList(),
chronon_to_playing_indicator_collection: typing.Callable[
[core_events.SimpleEvent],
music_parameters.PlayingIndicatorCollection,
] = music_converters.SimpleEventToPlayingIndicatorCollection(),
):
self._trill_size = trill_size
self._chronon_to_pitch_list = chronon_to_pitch_list
super().__init__(chronon_to_playing_indicator_collection)
def _apply_trill(
self,
chronon_to_convert: core_events.SimpleEvent,
trill: music_parameters.Trill,
pitch_list: list[music_parameters.abc.Pitch],
) -> core_events.Consecution[core_events.SimpleEvent]:
trill_item_count = chronon_to_convert.duration // self._trill_size
remaining = chronon_to_convert.duration - (
trill_item_count * self._trill_size
)
consecution = core_events.Consecution([])
pitch_cycle = itertools.cycle((pitch_list, trill.pitch))
for _ in range(int(trill_item_count)):
chronon = chronon_to_convert.set_parameter(
"duration", self._trill_size, mutate=False
).set_parameter("pitch_list", next(pitch_cycle))
consecution.append(chronon)
consecution[-1].duration += remaining
return consecution
def _apply_playing_indicator(
self,
chronon_to_convert: core_events.SimpleEvent,
playing_indicator: music_parameters.Trill,
) -> core_events.Consecution[core_events.SimpleEvent]:
pitch_list = self._chronon_to_pitch_list(chronon_to_convert)
if pitch_list:
return self._apply_trill(
chronon_to_convert, playing_indicator, pitch_list
)
else:
return core_events.Consecution([copy.copy(chronon_to_convert)])
@property
def playing_indicator_name(self) -> str:
return "trill"
@property
def default_playing_indicator(self) -> music_parameters.abc.PlayingIndicator:
return music_parameters.Trill()
[docs]class OptionalConverter(PlayingIndicatorConverter):
"""Apply optional on :class:`~mutwo.core_events.SimpleEvent`.
:param likelihood: A number between 0 - 1. 1 means that each optional
note is played, 0 means no optional note is played. Default to 0.5.
:type likelihood: float
:param random_seed: Set inner random process. Default to 100.
:type random_seed: int
:param make_rest: A function which takes the original :class:`~mutwo.core_events.SimpleEvent`
and returns a new `SimpleEvent` with the same duration which represents a rest.
By default, `mutwo` simply creates a `SimpleEvent` with the same duration.
:type make_rest: typing.Callable[[core_events.SimpleEvent], core_events.SimpleEvent]
:param chronon_to_playing_indicator_collection: Function to extract from a
:class:`mutwo.core_events.SimpleEvent` a
:class:`mutwo.music_parameters.PlayingIndicatorCollection`
object. By default it asks the Event for its
:attr:`~mutwo.music_events.NoteLike.playing_indicator_collection`
attribute (because by default :class:`mutwo.music_events.NoteLike`
objects are expected).
When using different Event classes than :class:`~mutwo.music_events.NoteLike`
with a different name for their playing_indicator_collection property, this argument
should be overridden. If the
function call raises an :obj:`AttributeError` (e.g. if no playing indicator
collection can be extracted), mutwo will build a playing indicator collection
from :const:`~mutwo.music_events.configurations.DEFAULT_PLAYING_INDICATORS_COLLECTION_CLASS`.
:type chronon_to_playing_indicator_collection: typing.Callable[[core_events.SimpleEvent], music_parameters.PlayingIndicatorCollection,], optional
"""
def __init__(
self,
likelihood: float = 0.5,
random_seed: int = 100,
make_rest: typing.Callable[
[core_events.SimpleEvent], core_events.SimpleEvent
] = lambda chronon: core_events.SimpleEvent(chronon.duration),
chronon_to_playing_indicator_collection: typing.Callable[
[core_events.SimpleEvent],
music_parameters.PlayingIndicatorCollection,
] = music_converters.SimpleEventToPlayingIndicatorCollection(),
):
self._make_rest = make_rest
self._likelihood = likelihood
self._random = np.random.default_rng(random_seed)
super().__init__(chronon_to_playing_indicator_collection)
def _apply_playing_indicator(
self,
chronon_to_convert: core_events.SimpleEvent,
playing_indicator: music_parameters.abc.ExplicitPlayingIndicator,
) -> core_events.Consecution[core_events.SimpleEvent]:
consecution = core_events.Consecution([])
if playing_indicator.is_active and self._random.random() > self._likelihood:
rest = self._make_rest(chronon_to_convert)
consecution.append(rest)
else:
consecution.append(chronon_to_convert.copy())
return consecution
@property
def playing_indicator_name(self) -> str:
return "optional"
@property
def default_playing_indicator(self) -> music_parameters.abc.PlayingIndicator:
return music_parameters.abc.ExplicitPlayingIndicator()
[docs]class PlayingIndicatorsConverter(core_converters.abc.SymmetricalEventConverter):
"""Apply :class:`mutwo.music_parameters.abc.PlayingIndicator` on any :class:`~mutwo.core_events.abc.Event`.
:param playing_indicator_converter_sequence: A sequence of :class:`PlayingIndicatorConverter` which shall
be applied on each :class:`~mutwo.core_events.SimpleEvent`.
:type playing_indicator_converter_sequence: typing.Sequence[PlayingIndicatorConverter]
"""
def __init__(
self,
playing_indicator_converter_sequence: typing.Sequence[
PlayingIndicatorConverter
],
):
self._playing_indicator_converter_tuple = tuple(
playing_indicator_converter_sequence
)
def _convert_chronon(
self,
event_to_convert: core_events.SimpleEvent,
_: core_constants.DurationType,
) -> core_events.Consecution:
"""Convert instance of :class:`mutwo.core_events.SimpleEvent`."""
converted_event = [event_to_convert]
for playing_indicator_converter in self._playing_indicator_converter_tuple:
new_converted_event: list[core_events.SimpleEvent] = []
for chronon in converted_event:
converted_chronon = playing_indicator_converter.convert(
chronon
)
new_converted_event.extend(converted_chronon)
converted_event = new_converted_event
return core_events.Consecution(converted_event)
[docs] def convert(self, event_to_convert: core_events.abc.Event) -> core_events.abc.Event:
converted_event = self._convert_event(event_to_convert, 0)
return converted_event