"""Apply tempo curve on any :class:`~mutwo.core_events.abc.Event` and convert :class:`~mutwo.core_parameters.abc.TempoPoint` to beat-length-in-seconds.
"""
import functools
import typing
from mutwo import core_constants
from mutwo import core_converters
from mutwo import core_events
from mutwo import core_parameters
from mutwo import core_utilities
__all__ = (
"TempoPointToBeatLengthInSeconds",
"TempoConverter",
"EventToMetrizedEvent",
)
[docs]class TempoPointToBeatLengthInSeconds(core_converters.abc.Converter):
"""Convert a :class:`~mutwo.core_parameters.abc.TempoPoint` with BPM to beat-length-in-seconds.
A :class:`TempoPoint` is defined as an object that has a particular tempo in
beats per seconds (BPM) and a reference value (1 for a quarter note, 4
for a whole note, etc.). Besides elaborate :class:`mutwo.core_parameters.abc.TempoPoint`
objects, any number can also be interpreted as a `TempoPoint`. In this case
the number simply represents the BPM number and the reference will be set to 1.
The returned beat-length-in-seconds always indicates the length for one quarter
note.
**Example:**
>>> from mutwo import core_converters
>>> tempo_point_converter = core_converters.TempoPointToBeatLengthInSeconds()
"""
TempoPoint = core_parameters.abc.TempoPoint | core_constants.Real
def __init__(self):
self._logger = core_utilities.get_cls_logger(type(self))
@staticmethod
def _beats_per_minute_to_seconds_per_beat(
beats_per_minute: core_constants.Real,
) -> float:
return float(60 / beats_per_minute)
def _extract_beats_per_minute_and_reference_from_tempo_point(
self, tempo_point: TempoPoint
) -> tuple[core_constants.Real, core_constants.Real]:
try:
beats_per_minute = tempo_point.tempo_in_beats_per_minute # type: ignore
except AttributeError:
beats_per_minute = float(tempo_point) # type: ignore
try:
reference = tempo_point.reference # type: ignore
except AttributeError:
self._logger.warning(core_utilities.UndefinedReferenceWarning(tempo_point))
reference = 1
return beats_per_minute, reference
[docs] def convert(self, tempo_point_to_convert: TempoPoint) -> float:
"""Converts a :class:`TempoPoint` to beat-length-in-seconds.
:param tempo_point_to_convert: A tempo point defines the active tempo
from which the beat-length-in-seconds shall be calculated. The argument
can either be any number (which will be interpreted as beats per
minute [BPM]) or a ``mutwo.core_parameters.abc.TempoPoint`` object.
:return: The duration of one beat in seconds within the passed tempo.
**Example:**
>>> from mutwo import core_converters
>>> converter = core_converters.TempoPointToBeatLengthInSeconds()
>>> converter.convert(60) # one beat in tempo 60 bpm takes 1 second
1.0
>>> converter.convert(120) # one beat in tempo 120 bpm takes 0.5 second
0.5
"""
(
beats_per_minute,
reference,
) = self._extract_beats_per_minute_and_reference_from_tempo_point(
tempo_point_to_convert
)
return (
TempoPointToBeatLengthInSeconds._beats_per_minute_to_seconds_per_beat(
beats_per_minute
)
/ reference
)
[docs]class TempoConverter(core_converters.abc.EventConverter):
"""Apply tempo curves on mutwo events
:param tempo_envelope: The tempo curve that shall be applied on the
mutwo events. This is expected to be a :class:`core_events.TempoEnvelope`
which values are filled with numbers that will be interpreted as BPM
[beats per minute]) or with :class:`mutwo.core_parameters.abc.TempoPoint`
objects.
:param apply_converter_on_events_tempo_envelope: If set to `True` the
converter will also adjust the :attr:`tempo_envelope` attribute of
each converted event. Default to `True`.
**Example:**
>>> from mutwo import core_converters
>>> from mutwo import core_events
>>> from mutwo import core_parameters
>>> tempo_envelope = core_events.Envelope(
... [[0, core_parameters.DirectTempoPoint(60)], [3, 60], [3, 30], [5, 50]],
... )
>>> my_tempo_converter = core_converters.TempoConverter(tempo_envelope)
"""
_tempo_point_to_beat_length_in_seconds = TempoPointToBeatLengthInSeconds().convert
# Define private tempo envelope class which catches its
# '_absolute_time_in_floats_tuple_and_duration'. With this we can
# improve the performance of the 'value_at' method and with this
# improvment we can have a faster converter.
#
# This is actually not safe, because the envelope is still mutable.
# But we trust that no one changes anything with our internal envelope
# and hope everything goes well. The long term solution is to implement
# a 'freeze' method for all mutwo objects, which auto-converts all
# properties to catched properties. But this may still takes some time
# and we already want to have faster converters now.
class _CatchedTempoEnvelope(core_events.TempoEnvelope):
@functools.cached_property
def _absolute_time_in_floats_tuple_and_duration(
self,
) -> tuple[tuple[float, ...], float]:
return super()._absolute_time_in_floats_tuple_and_duration
def __init__(
self,
tempo_envelope: core_events.TempoEnvelope,
apply_converter_on_events_tempo_envelope: bool = True,
):
self._tempo_envelope = tempo_envelope
self._beat_length_in_seconds_envelope = (
TempoConverter._tempo_envelope_to_beat_length_in_seconds_envelope(
tempo_envelope
)
)
self._apply_converter_on_events_tempo_envelope = (
apply_converter_on_events_tempo_envelope
)
# Catches for better performance
self._start_and_end_to_tempo_converter_dict = {}
self._start_and_end_to_integration = {}
# ###################################################################### #
# static methods #
# ###################################################################### #
@staticmethod
def _tempo_envelope_to_beat_length_in_seconds_envelope(
tempo_envelope: core_events.Envelope,
) -> core_events.Envelope:
"""Convert bpm / TempoPoint based env to beat-length-in-seconds env."""
level_list: list[float] = []
for tempo_point in tempo_envelope.parameter_tuple:
beat_length_in_seconds = (
TempoConverter._tempo_point_to_beat_length_in_seconds(tempo_point)
)
level_list.append(beat_length_in_seconds)
return TempoConverter._CatchedTempoEnvelope(
[
[absolute_time, value, curve_shape]
for absolute_time, value, curve_shape in zip(
tempo_envelope.absolute_time_tuple,
level_list,
tempo_envelope.curve_shape_tuple,
)
]
)
# ###################################################################### #
# private methods #
# ###################################################################### #
def _start_and_end_to_tempo_converter(self, start, end):
key = (start.duration, end.duration)
try:
t = self._start_and_end_to_tempo_converter_dict[key]
except KeyError:
t = self._start_and_end_to_tempo_converter_dict[key] = TempoConverter(
self._tempo_envelope.cut_out(
start,
end,
mutate=False,
),
apply_converter_on_events_tempo_envelope=False,
)
return t
def _integrate(
self, start: core_parameters.abc.Duration, end: core_parameters.abc.Duration
):
key = (start.duration, end.duration)
try:
i = self._start_and_end_to_integration[key]
except KeyError:
i = self._start_and_end_to_integration[
key
] = self._beat_length_in_seconds_envelope.integrate_interval(start, end)
return i
def _convert_chronon(
self,
chronon: core_events.SimpleEvent,
absolute_entry_delay: core_parameters.abc.Duration | float | int,
depth: int = 0,
) -> tuple[typing.Any, ...]:
chronon.duration = self._integrate(
absolute_entry_delay, absolute_entry_delay + chronon.duration
)
return tuple([])
def _convert_event(
self,
event_to_convert: core_events.abc.Event,
absolute_entry_delay: core_parameters.abc.Duration | float | int,
depth: int = 0,
) -> core_events.abc.ComplexEvent[core_events.abc.Event]:
tempo_envelope = event_to_convert.tempo_envelope
is_tempo_envelope_effectless = (
tempo_envelope.is_static and tempo_envelope.value_tuple[0] == 60
)
if (
self._apply_converter_on_events_tempo_envelope
and not is_tempo_envelope_effectless
):
start, end = (
absolute_entry_delay,
absolute_entry_delay + event_to_convert.duration,
)
local_tempo_converter = self._start_and_end_to_tempo_converter(start, end)
event_to_convert.tempo_envelope = local_tempo_converter(tempo_envelope)
rvalue = super()._convert_event(event_to_convert, absolute_entry_delay, depth)
if is_tempo_envelope_effectless:
# Yes we simply override the tempo_envelope of the event which we
# just converted. This is because the TempoConverter copies the
# event at the start of the algorithm and simply mutates this
# copied event.
event_to_convert.tempo_envelope.duration = event_to_convert.duration
return rvalue
# ###################################################################### #
# public methods for interaction with the user #
# ###################################################################### #
[docs] def convert(self, event_to_convert: core_events.abc.Event) -> core_events.abc.Event:
"""Apply tempo curve of the converter to the entered event.
The method doesn't change the original event, but returns a copied
version with different values for its duration attributes depending
on the tempo curve.
:param event_to_convert: The event to convert. Can be any object
that inherits from ``mutwo.core_events.abc.Event``. If the event that
shall be converted is longer than the tempo curve of the
``TempoConverter``, then the last tempo of the curve will be hold.
:return: A new ``Event`` object which duration property has been adapted
by the tempo curve of the ``TempoConverter``.
**Example:**
>>> from mutwo import core_converters
>>> from mutwo import core_events
>>> from mutwo import core_parameters
>>> tempo_envelope = core_events.Envelope(
... [[0, core_parameters.DirectTempoPoint(60)], [3, 60], [3, 30], [5, 50]],
... )
>>> my_tempo_converter = core_converters.TempoConverter(tempo_envelope)
>>> my_events = core_events.Consecution([core_events.SimpleEvent(d) for d in (3, 2, 5)])
>>> my_tempo_converter.convert(my_events)
Consecution([SimpleEvent(duration = DirectDuration(duration = 3)), SimpleEvent(duration = DirectDuration(duration = 7205759403792795/2251799813685248)), SimpleEvent(duration = DirectDuration(duration = 6))])
"""
copied_event_to_convert = event_to_convert.destructive_copy()
self._convert_event(copied_event_to_convert, core_parameters.DirectDuration(0))
return copied_event_to_convert
[docs]class EventToMetrizedEvent(core_converters.abc.SymmetricalEventConverter):
"""Apply tempo envelope of event on itself"""
def __init__(
self,
skip_level_count: typing.Optional[int] = None,
maxima_depth_count: typing.Optional[int] = None,
):
self._skip_level_count = skip_level_count
self._maxima_depth_count = maxima_depth_count
def _convert_chronon(
self,
event_to_convert: core_events.SimpleEvent,
absolute_entry_delay: core_parameters.abc.Duration | float | int,
depth: int = 0,
) -> core_events.SimpleEvent:
return event_to_convert
def _convert_event(
self,
event_to_convert: core_events.abc.Event,
absolute_entry_delay: core_parameters.abc.Duration | float | int,
depth: int = 0,
) -> core_events.abc.ComplexEvent[core_events.abc.Event]:
if (self._skip_level_count is None or self._skip_level_count < depth) and (
self._maxima_depth_count is None or depth < self._maxima_depth_count
):
tempo_converter = TempoConverter(event_to_convert.tempo_envelope)
event_to_convert = tempo_converter.convert(event_to_convert)
event_to_convert.reset_tempo_envelope()
else:
# Ensure we return copied event!
event_to_convert = event_to_convert.destructive_copy()
return super()._convert_event(event_to_convert, absolute_entry_delay, depth)
[docs] def convert(self, event_to_convert: core_events.abc.Event) -> core_events.abc.Event:
"""Apply tempo envelope of event on itself"""
return self._convert_event(event_to_convert, 0, 0)