"""Apply tempo curve on any :class:`~mutwo.events.abc.Event` and convert :class:`~mutwo.parameters.tempos.TempoPoint` to beat-length-in-seconds.
"""
import typing
import warnings
import expenvelope # type: ignore
from mutwo import converters, events, parameters
from mutwo.utilities import constants
TempoEvents = expenvelope.Envelope
TempoPoint = typing.Union[parameters.tempos.TempoPoint, constants.Real]
__all__ = ("TempoPointConverter", "TempoConverter",)
[docs]class TempoPointConverter(converters.abc.Converter):
"""Convert a :class:`~mutwo.parameters.tempos.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.parameters.tempos.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.converters import symmetrical
>>> tempo_point_converter = symmetrical.tempos.TempoPointConverter()
"""
@staticmethod
def _beats_per_minute_to_seconds_per_beat(
beats_per_minute: constants.Real,
) -> float:
return float(60 / beats_per_minute)
@staticmethod
def _extract_beats_per_minute_and_reference_from_tempo_point(
tempo_point: TempoPoint,
) -> typing.Tuple[constants.Real, 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:
message = (
"Tempo point {} of type {} doesn't know attribute 'reference'.".format(
tempo_point, type(tempo_point)
)
)
message += " Therefore reference has been set to 1."
warnings.warn(message)
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.parameters.tempos.TempoPoint`` object.
:return: The duration of one beat in seconds within the passed tempo.
**Example:**
>>> from mutwo.converters import symmetrical
>>> converter = symmetrical.tempos.TempoPointConverter()
>>> converter.convert(60) # one beat in tempo 60 bpm takes 1 second
1
>>> 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 (
TempoPointConverter._beats_per_minute_to_seconds_per_beat(beats_per_minute)
/ reference
)
[docs]class TempoConverter(converters.abc.EventConverter):
"""Class for applying 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:`expenvelope.Envelope`
which levels arefilled with numbers that will be interpreted as BPM
[beats per minute]) or with :class:`mutwo.parameters.tempos.TempoPoint`
objects.
**Example:**
>>> import expenvelope
>>> from mutwo.converters import symmetrical
>>> from mutwo.parameters import tempos
>>> tempo_envelope = expenvelope.Envelope.from_levels_and_durations(
>>> levels=[tempos.TempoPoint(60), 60, 30, 50],
>>> durations=[3, 0, 2],
>>> )
>>> my_tempo_converter = symmetrical.tempos.TempoConverter(tempo_envelope)
"""
_tempo_point_converter = TempoPointConverter()
def __init__(self, tempo_envelope: expenvelope.Envelope):
self._envelope = TempoConverter._tempo_envelope_to_beat_length_in_seconds_envelope(
tempo_envelope
)
# ###################################################################### #
# static methods #
# ###################################################################### #
@staticmethod
def _tempo_envelope_to_beat_length_in_seconds_envelope(
tempo_envelope: expenvelope.Envelope,
) -> expenvelope.Envelope:
"""Convert bpm / TempoPoint based env to beat-length-in-seconds env."""
levels: typing.List[float] = []
for tempo_point in tempo_envelope.levels:
beat_length_in_seconds = TempoConverter._tempo_point_converter.convert(
tempo_point
)
levels.append(beat_length_in_seconds)
return expenvelope.Envelope.from_levels_and_durations(
levels, tempo_envelope.durations, tempo_envelope.curve_shapes
)
# ###################################################################### #
# private methods #
# ###################################################################### #
def _convert_simple_event(
self,
simple_event: events.basic.SimpleEvent,
absolute_entry_delay: parameters.abc.DurationType,
) -> typing.Tuple[typing.Any, ...]:
simple_event.duration = self._envelope.integrate_interval(
absolute_entry_delay, simple_event.duration + absolute_entry_delay
)
return tuple([])
# ###################################################################### #
# public methods for interaction with the user #
# ###################################################################### #
[docs] def convert(self, event_to_convert: events.abc.Event) -> 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.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:**
>>> import expenvelope
>>> from mutwo.events import basic
>>> from mutwo.parameters import tempos
>>> from mutwo.converters import symmetrical
>>> tempo_envelope = expenvelope.Envelope.from_levels_and_durations(
>>> levels=[tempos.TempoPoint(60), 60, 120, 120],
>>> durations=[3, 2, 5],
>>> )
>>> my_tempo_converter = symmetrical.tempos.TempoConverter(tempo_envelope)
>>> my_events = basic.SequentialEvent([basic.SimpleEvent(d) for d in (3, 2, 5)])
>>> my_tempo_converter.convert(my_events)
SequentialEvent([SimpleEvent(duration = 3.0), SimpleEvent(duration = 1.5), SimpleEvent(duration = 2.5)])
"""
copied_event_to_convert = event_to_convert.destructive_copy()
self._convert_event(copied_event_to_convert, 0)
return copied_event_to_convert