diff --git a/mutwo/core_converters/tempos.py b/mutwo/core_converters/tempos.py
index 2e39f656..3e6fc88c 100644
--- a/mutwo/core_converters/tempos.py
+++ b/mutwo/core_converters/tempos.py
@@ -43,9 +43,7 @@ class TempoToBeatLengthInSeconds(core_converters.abc.Converter):
>>> tempo_converter = core_converters.TempoToBeatLengthInSeconds()
"""
- def convert(
- self, tempo_to_convert: core_parameters.abc.Tempo.Type
- ) -> float:
+ def convert(self, tempo_to_convert: core_parameters.abc.Tempo.Type) -> float:
"""Converts a :class:`Tempo` to beat-length-in-seconds.
:param tempo_to_convert: A tempo defines the active tempo
@@ -66,39 +64,35 @@ def convert(
class TempoConverter(core_converters.abc.EventConverter):
- """Apply tempo curve on an :class:`~mutwo.core_events.abc.Event`.
-
- :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.Tempo`
- objects.
- :param apply_converter_on_events_tempo_envelope: If set to `True` the
- converter adjusts the :attr:`tempo_envelope` attribute of each
+ """Apply tempo on an :class:`~mutwo.core_events.abc.Event`.
+
+ :param tempo: The tempo that shall be applied on the mutwo events.
+ This must be a :class:`~mutwo.core_parameters.abc.Tempo` object.
+ :param apply_converter_on_events_tempo: If set to `True` the
+ converter adjusts the :attr:`tempo` 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(
+ >>> tempo = core_parameters.ContinuousTempo(
... [[0, core_parameters.DirectTempo(60)], [3, 60], [3, 30], [5, 50]],
... )
- >>> c = core_converters.TempoConverter(tempo_envelope)
+ >>> c = core_converters.TempoConverter(tempo)
"""
_tempo_to_beat_length_in_seconds = TempoToBeatLengthInSeconds().convert
- # Define private tempo envelope class which catches its
- # absolute times and durations. With this we can
- # improve the performance of the 'value_at' method and with this
- # improvment we can have a faster converter.
+ # Define private envelope class which catches its absolute times
+ # and durations. With this we can improve the performance of the
+ # 'value_at' method and with this improvement 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.
- class _CatchedTempoEnvelope(core_events.TempoEnvelope):
+ class _CatchedEnvelope(core_events.Envelope):
@functools.cached_property
def _abstf_tuple_and_dur(self) -> tuple[tuple[float, ...], float]:
return super()._abstf_tuple_and_dur
@@ -109,18 +103,14 @@ def _abst_tuple_and_dur(self) -> tuple[tuple[float, ...], float]:
def __init__(
self,
- tempo_envelope: core_events.TempoEnvelope,
- apply_converter_on_events_tempo_envelope: bool = True,
+ tempo: core_parameters.abc.Tempo,
+ apply_converter_on_events_tempo: bool = True,
):
- self._tempo_envelope = tempo_envelope
+ self._tempo = core_parameters.ContinuousTempo.from_parameter(tempo)
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
+ TempoConverter._tempo_to_beat_length_in_seconds_envelope(self._tempo)
)
+ self._apply_converter_on_events_tempo = apply_converter_on_events_tempo
# Catches for better performance
self._start_and_end_to_tempo_converter_dict = {}
self._start_and_end_to_integration = {}
@@ -130,16 +120,16 @@ def __init__(
# ###################################################################### #
@staticmethod
- def _tempo_envelope_to_beat_length_in_seconds_envelope(
- tempo_envelope: core_events.Envelope,
+ def _tempo_to_beat_length_in_seconds_envelope(
+ tempo: core_events.Envelope,
) -> core_events.Envelope:
"""Convert bpm / Tempo based env to beat-length-in-seconds env."""
- e = tempo_envelope
+ e = tempo
value_list: list[float] = []
for tp in e.parameter_tuple:
value_list.append(TempoConverter._tempo_to_beat_length_in_seconds(tp))
- return TempoConverter._CatchedTempoEnvelope(
+ return TempoConverter._CatchedEnvelope(
[
[t, v, cs]
for t, v, cs in zip(
@@ -158,11 +148,11 @@ def _start_and_end_to_tempo_converter(self, start, end):
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.copy().cut_out(
+ self._tempo.copy().cut_out(
start,
end,
),
- apply_converter_on_events_tempo_envelope=False,
+ apply_converter_on_events_tempo=False,
)
return t
@@ -195,27 +185,23 @@ def _convert_event(
absolute_time: 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
- ):
+ tempo = core_parameters.ContinuousTempo.from_parameter(event_to_convert.tempo)
+ is_tempo_effectless = tempo.is_static and tempo.value_tuple[0] == 60
+ if self._apply_converter_on_events_tempo and not is_tempo_effectless:
start, end = (
absolute_time,
absolute_time + 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)
+ event_to_convert.tempo = local_tempo_converter(tempo)
rvalue = super()._convert_event(event_to_convert, absolute_time, depth)
- if is_tempo_envelope_effectless:
- # Yes we simply override the tempo_envelope of the event which we
+ if is_tempo_effectless:
+ # Yes we simply override the tempo 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
+ event_to_convert.tempo = tempo
+ event_to_convert.tempo.duration = event_to_convert.duration
return rvalue
# ###################################################################### #
@@ -237,10 +223,10 @@ def convert(self, event_to_convert: core_events.abc.Event) -> core_events.abc.Ev
>>> from mutwo import core_converters
>>> from mutwo import core_events
>>> from mutwo import core_parameters
- >>> tempo_envelope = core_events.Envelope(
+ >>> tempo = core_parameters.ContinuousTempo(
... [[0, core_parameters.DirectTempo(60)], [3, 60], [3, 30], [5, 50]],
... )
- >>> my_tempo_converter = core_converters.TempoConverter(tempo_envelope)
+ >>> my_tempo_converter = core_converters.TempoConverter(tempo)
>>> my_events = core_events.Consecution([core_events.Chronon(d) for d in (3, 2, 5)])
>>> my_tempo_converter.convert(my_events)
Consecution([Chronon(duration=DirectDuration(3.0)), Chronon(duration=DirectDuration(3.2)), Chronon(duration=DirectDuration(6.0))])
@@ -251,7 +237,7 @@ def convert(self, event_to_convert: core_events.abc.Event) -> core_events.abc.Ev
class EventToMetrizedEvent(core_converters.abc.SymmetricalEventConverter):
- """Apply tempo envelope of event on copy of itself"""
+ """Apply tempo of event on copy of itself"""
def __init__(
self,
@@ -278,14 +264,14 @@ def _convert_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)
+ tempo_converter = TempoConverter(event_to_convert.tempo)
e = tempo_converter.convert(event_to_convert)
- e.reset_tempo_envelope()
+ e.reset_tempo()
else:
# Ensure we return copied event!
e = event_to_convert.destructive_copy()
return super()._convert_event(e, absolute_time, depth)
def convert(self, event_to_convert: core_events.abc.Event) -> core_events.abc.Event:
- """Apply tempo envelope of event on copy of itself"""
+ """Apply tempo of event on copy of itself"""
return self._convert_event(event_to_convert, 0, 0)
diff --git a/mutwo/core_events/__init__.py b/mutwo/core_events/__init__.py
index 59f3bc95..e0732a15 100644
--- a/mutwo/core_events/__init__.py
+++ b/mutwo/core_events/__init__.py
@@ -19,8 +19,8 @@
Event objects can be understood as the core objects
of the :mod:`mutwo` framework. They all own a :attr:`~mutwo.core_events.abc.Event.duration`
-(of type :class:`~mutwo.core_parameters.abc.Duration`), a :attr:`~mutwo.core_events.abc.Event.tempo_envelope`
-(of type :class:`~mutwo.core_events.TempoEnvelope`) and a :attr:`~mutwo.core_events.abc.Event.tag`
+(of type :class:`~mutwo.core_parameters.abc.Duration`), a :attr:`~mutwo.core_events.abc.Event.tempo`
+(of type :class:`~mutwo.core_parameters.abc.Tempo`) and a :attr:`~mutwo.core_events.abc.Event.tag`
(of type ``str`` or ``None``).
The most often used classes are:
@@ -37,7 +37,6 @@
from . import abc
from .basic import *
-from .tempos import *
from .envelopes import *
from . import basic, envelopes
@@ -48,3 +47,6 @@
# Force flat structure
del basic, core_utilities, envelopes
+
+from . import patchparameters
+del patchparameters
diff --git a/mutwo/core_events/abc.py b/mutwo/core_events/abc.py
index c4bf5d1a..62a7e0e1 100644
--- a/mutwo/core_events/abc.py
+++ b/mutwo/core_events/abc.py
@@ -35,29 +35,29 @@
class Event(core_utilities.MutwoObject, abc.ABC):
"""Abstract Event-Object
- :param tempo_envelope: An envelope which describes the dynamic tempo of an event.
+ :param tempo: An envelope which describes the dynamic tempo of an event.
:param tag: The name of the event. This can be used to find the event
inside a :class:`ComplexEvent`.
"""
- # It looks tempting to drop the 'tempo_envelope' attribute of events.
+ # It looks tempting to drop the 'tempo' attribute of events.
# It may look simpler (and therefore more elegant) if events are only
# defined by one attribute: their duration. Let's remember why the
- # 'tempo_envelope' attribute was initially introduced [1]:
+ # 'tempo' attribute was initially introduced [1]:
#
# - With [1] it was decided that durations are represented in the unit
- # 'beats'.
+ # 'beat_count'.
#
# - An event should have an unambiguous duration, so that converters
# (and all other 'mutwo' parts) can treat an event consistently.
#
- # - The unit of 'beats' doesn't say anything about the real duration: only
- # in cooperation with a specified tempo it can be clearly stated how long
- # an event is.
+ # - The unit of 'beat_count' doesn't say anything about the real duration:
+ # only in cooperation with a specified tempo it can be clearly stated how
+ # long an event is.
#
# - Therefore the combination of (a) having duration specified in the unit
- # 'beats' and (b) wanting to have events with unambiguous duration leads
- # to the necessity to attach tempo envelopes to events.
+ # 'beat_count' and (b) wanting to have events with unambiguous duration
+ # leads to the necessity to attach tempos to events.
#
# In the early days of mutwo (b) wasn't considered to be an objective:
# it was the opposite, an implicit ambiguity was considered to be a good
@@ -65,13 +65,13 @@ class Event(core_utilities.MutwoObject, abc.ABC):
# this approach rather increased complexity, as other code bits are unable
# to treat an event consistently and a user constantly has to keep in mind
# the specific way how each converter interprets a duration. To fix this
- # complexity, the 'beat' unit was specified and a 'tempo_envelope'
- # attribute has been added. Now converters could be reliable to produce
+ # complexity, the 'beat' unit was specified and a 'tempo'
+ # attribute has been added. Now converters could reliably produce
# results which match the duration of an event.
#
- # Now we could change durations to be no longer in the unit 'beats', but in
- # the unit 'seconds'. Then the duration of an event would still be
- # unambiguous without the need of a tempo envelope attribute. We could
+ # Now we could change durations to be no longer in the unit 'beat_count',
+ # but in the unit 'seconds'. Then the duration of an event would still be
+ # unambiguous without the need of a tempo attribute. We could
# furthermore implement duration representations with beat & tempo as a
# subclass of a more general 'duration=seconds' approach. This has two
# problems:
@@ -88,15 +88,22 @@ class Event(core_utilities.MutwoObject, abc.ABC):
# tempo - and wouldn't resonate with how we usually think about music.
#
# (3) If we think of tempo, it's rather a global trajectory independent
- # from single notes. Therefore a 'TempoEnvelope' object seems to be
- # more consistent with how we usually approach tempo in music than a
- # specific tempo for each note. To still be able to have this global
- # trajectory, a 'duration=seconds' approach would need additional
- # helper functions, to apply a tempo envelope on an event with beat
- # based durations.
+ # from single notes. So we usually think of a tempo trajectory as
+ # something that belongs to a nested event (e.g. a 'Consecution' or
+ # a 'Concurrence'). But with the duration=seconds approach such a
+ # tempo trajectory couldn't be persistently mapped to a nested event,
+ # because the duration of a complex event isn't a statically mapped and
+ # available entity, but ephemerally and dynamically calculated when
+ # needed. When the duration of a complex event is set, it becomes
+ # propagated to the duration of its children until it finds a leaf that
+ # statically declares its duration and then it's lost. So in order to
+ # have a persistently available tempo trajectory on a complex event
+ # that can be read and modified-in-place, we need an extra tempo
+ # attribute. Otherwise we would break the rule that the duration of
+ # a complex event is only a sum or max of its children duration.
#
# Due to these reasons, that describe new complexities by switching to a
- # 'duration=seconds' model, we should stick to the beats/tempo_envelope
+ # 'duration=seconds' model, we should stick to the beats/tempo
# approach until we can find a better solution.
#
# Now we could also ask the other way around, because if durations are in
@@ -106,7 +113,7 @@ class Event(core_utilities.MutwoObject, abc.ABC):
# true vice versa: if the default tempo of an event (which is 60 BPM)
# isn't changed, the beats of a duration does in fact equal seconds.
# So for users who don't care about splitting duration into beats+tempo,
- # they can simply avoid any 'tempo_envelope' attribute and directly write
+ # they can simply avoid any 'tempo' attribute and directly write
# their duration in seconds.
#
# ---
@@ -122,10 +129,10 @@ class Event(core_utilities.MutwoObject, abc.ABC):
def __init__(
self,
- tempo_envelope: typing.Optional[core_events.TempoEnvelope] = None,
+ tempo: typing.Optional[core_parameters.abc.Tempo] = None,
tag: typing.Optional[str] = None,
):
- self.tempo_envelope = tempo_envelope
+ self.tempo = tempo
self.tag = tag
# ###################################################################### #
@@ -198,22 +205,15 @@ def _mutate_parameter(
# ###################################################################### #
@property
- def tempo_envelope(self) -> core_events.TempoEnvelope:
- """The dynamic tempo of an event; specified as an envelope.
+ def tempo(self) -> core_parameters.abc.Tempo:
+ """The tempo of an event."""
+ if self._tempo is None:
+ self.reset_tempo()
+ return self._tempo
- Tempo envelopes are represented as :class:`core_events.TempoEnvelope`
- objects. Tempo envelopes are valid for its respective event and all its
- children events.
- """
- if self._tempo_envelope is None:
- self.reset_tempo_envelope()
- return self._tempo_envelope
-
- @tempo_envelope.setter
- def tempo_envelope(
- self, tempo_envelope: typing.Optional[core_events.TempoEnvelope]
- ):
- self._tempo_envelope = tempo_envelope
+ @tempo.setter
+ def tempo(self, tempo: typing.Optional[core_parameters.abc.Tempo]):
+ self._tempo = tempo
# ###################################################################### #
# public methods #
@@ -408,27 +408,27 @@ def mutate_parameter(
id_set=set([]),
)
- def reset_tempo_envelope(self) -> Event:
- """Set events tempo envelope so that one beat equals one second (tempo 60).
+ def reset_tempo(self) -> Event:
+ """Set events tempo so that one beat equals one second (tempo 60).
**Example:**
>>> from mutwo import core_events
>>> chr = core_events.Chronon(duration = 1)
- >>> chr.tempo_envelope[0].value = 100
- >>> print(chr.tempo_envelope)
- Tem(T(cur=0, dur=D(1.0), tem=D(60.0), val=100), T(cur=0, dur=D(0.0), tem=D(60.0)))
- >>> chr.reset_tempo_envelope()
+ >>> chr.tempo.bpm = 100
+ >>> print(chr.tempo)
+ D(100.0)
+ >>> chr.reset_tempo()
Chronon(duration=DirectDuration(1.0))
- >>> print(chr.tempo_envelope)
- Tem(T(cur=0, dur=D(1.0), tem=D(60.0)), T(cur=0, dur=D(0.0), tem=D(60.0)))
+ >>> print(chr.tempo)
+ D(60.0)
"""
- self.tempo_envelope = core_events.TempoEnvelope([[0, 60], [1, 60]])
+ self.tempo = core_parameters.DirectTempo(60)
return self
@abc.abstractmethod
def metrize(self) -> typing.Optional[Event]:
- """Apply tempo envelope of event on itself
+ """Apply tempo of event on itself
Metrize is only syntactic sugar for a call of
:class:`EventToMetrizedEvent`:
@@ -436,7 +436,7 @@ def metrize(self) -> typing.Optional[Event]:
>>> from mutwo import core_converters
>>> from mutwo import core_events
>>> chr = core_events.Chronon(1)
- >>> chr.tempo_envelope = core_events.TempoEnvelope([[0, 100], [1, 40]])
+ >>> chr.tempo = core_parameters.ContinuousTempo([[0, 100], [1, 40]])
>>> core_converters.EventToMetrizedEvent().convert(chr) == chr.metrize()
True
"""
@@ -560,10 +560,10 @@ def __init__(
self,
iterable: typing.Iterable[T] = [],
*,
- tempo_envelope: typing.Optional[core_events.TempoEnvelope] = None,
+ tempo: typing.Optional[core_parameters.abc.Tempo] = None,
tag: typing.Optional[str] = None,
):
- Event.__init__(self, tempo_envelope, tag)
+ Event.__init__(self, tempo, tag)
list.__init__(self, iterable)
def __init_subclass__(
@@ -587,7 +587,7 @@ def __init_subclass__(
# ): pass
#
super_class_attr_tuple = getattr(
- cls, "_class_specific_side_attribute_tuple", ("tempo_envelope", "tag")
+ cls, "_class_specific_side_attribute_tuple", ("tempo", "tag")
)
class_attr_tuple = super_class_attr_tuple + class_specific_side_attribute_tuple
cls._class_specific_side_attribute_tuple = class_attr_tuple
@@ -794,7 +794,7 @@ def _mutate_parameter( # type: ignore
id_set=id_set,
)
- def _concatenate_tempo_envelope(self, other: ComplexEvent):
+ def _concatenate_tempo(self, other: ComplexEvent):
"""Concatenate the tempo of event with tempo of other event.
If we concatenate events on the time axis, we also want to
@@ -807,24 +807,36 @@ def _concatenate_tempo_envelope(self, other: ComplexEvent):
to know the original duration of the target event. Due to this
difficulty this method is private.
"""
- # We need to ensure the tempo envelope of the event
- # is as long as it's duration, otherwise the others tempo
- # envelope may be postponed (if our envelope is longer
- # than the event) or may be too early (if our envelope
- # is shorted than the event).
- # We don't care here if the others event envelope is too
+ # Trivial case: if tempo doesn't change and isn't continuous, we
+ # don't need to do anything to preserve the tempo of the other event.
+ is_not_continuous = map(
+ lambda t: not isinstance(t, core_parameters.ContinuousTempo),
+ (self.tempo, other.tempo),
+ )
+ if all(is_not_continuous) and self.tempo == other.tempo:
+ return
+
+ # Convert to continuous tempo, to easily handle tempos.
+ for o in (self, other):
+ o.tempo = core_parameters.ContinuousTempo.from_parameter(o.tempo)
+
+ # We need to ensure the tempo of the event is as long as
+ # it's duration, otherwise the others tempo may be
+ # postponed (if our envelope is longer than the event)
+ # or may be too early (if our tempo is shorted than the event).
+ # We don't care here if the others event tempo is too
# short or too long, because the relationships are still
# the same.
- if (d := self.duration) < (d_env := self.tempo_envelope.duration):
+ if (d := self.duration) < (d_env := self.tempo.duration):
self._logger.warning(
f"Tempo envelope of '{str(self)[:35]}...' needed "
"to be truncated because the envelope was "
"longer than the actual event."
)
- self.tempo_envelope.cut_out(0, d)
+ self.tempo.cut_out(0, d)
elif d > d_env:
- self.tempo_envelope.extend_until(d)
- self.tempo_envelope.extend(other.tempo_envelope.copy())
+ self.tempo.extend_until(d)
+ self.tempo.extend(other.tempo.copy())
# ###################################################################### #
# public methods #
@@ -1000,7 +1012,7 @@ def tie_by_if_available(e: Event):
def metrize(self) -> ComplexEvent:
metrized_event = self._event_to_metrized_event(self)
- self.tempo_envelope = metrized_event.tempo_envelope
+ self.tempo = metrized_event.tempo
self[:] = metrized_event[:]
return self
diff --git a/mutwo/core_events/basic.py b/mutwo/core_events/basic.py
index 68ae734f..8dc97856 100644
--- a/mutwo/core_events/basic.py
+++ b/mutwo/core_events/basic.py
@@ -60,7 +60,7 @@ class Chronon(core_events.abc.Event):
C(dur=D(2.0))
"""
- parameter_to_exclude_from_representation_tuple = ("tempo_envelope", "tag")
+ parameter_to_exclude_from_representation_tuple = ("tempo", "tag")
def __init__(self, duration: core_parameters.abc.Duration.Type, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -128,8 +128,7 @@ def _mutate_parameter(
@property
def _parameter_to_print_tuple(self) -> tuple[str, ...]:
"""Return tuple of attribute names which shall be printed for repr."""
- # Fix infinite circular loop (due to 'tempo_envelope')
- # and avoid printing too verbose parameters.
+ # Avoid printing too verbose parameters.
return tuple(
filter(
lambda attribute: attribute
@@ -236,7 +235,7 @@ def set_parameter( # type: ignore
def metrize(self) -> Chronon:
metrized_event = self._event_to_metrized_event(self)
self.duration = metrized_event.duration
- self.tempo_envelope = metrized_event.tempo_envelope
+ self.tempo = metrized_event.tempo
return self
def cut_out( # type: ignore
@@ -295,7 +294,7 @@ class Consecution(core_events.abc.ComplexEvent, typing.Generic[T]):
def __add__(self, event: list[T]) -> Consecution[T]:
e = self.copy()
- e._concatenate_tempo_envelope(event)
+ e._concatenate_tempo(event)
e.extend(event)
return e
@@ -749,7 +748,7 @@ class Concurrence(core_events.abc.ComplexEvent, typing.Generic[T]):
@staticmethod
def _extend_ancestor(ancestor: core_events.abc.Event, event: core_events.abc.Event):
try:
- ancestor._concatenate_tempo_envelope(event)
+ ancestor._concatenate_tempo(event)
# We can't concatenate to a chronon.
# We also can't concatenate to anything else.
except AttributeError:
@@ -763,7 +762,7 @@ def _extend_ancestor(ancestor: core_events.abc.Event, event: core_events.abc.Eve
except core_utilities.NoTagError:
ancestor.concatenate_by_index(event)
# This should already fail above, but if this strange object
- # somehow owned '_concatenate_tempo_envelope', it should
+ # somehow owned '_concatenate_tempo', it should
# fail here.
case _:
raise core_utilities.ConcatenationError(ancestor, event)
diff --git a/mutwo/core_events/envelopes.py b/mutwo/core_events/envelopes.py
index 80b2fb88..e359cdb6 100644
--- a/mutwo/core_events/envelopes.py
+++ b/mutwo/core_events/envelopes.py
@@ -32,7 +32,7 @@
from mutwo import core_utilities
-__all__ = ("Envelope", "RelativeEnvelope", "TempoEnvelope")
+__all__ = ("Envelope", "RelativeEnvelope")
T = typing.TypeVar("T", bound=core_events.abc.Event)
@@ -73,9 +73,7 @@ class Envelope(core_events.Consecution, typing.Generic[T]):
Value: typing.TypeAlias = core_constants.Real
Parameter: typing.TypeAlias = typing.Any
CurveShape: typing.TypeAlias = core_constants.Real
- IncompletePoint: typing.TypeAlias = tuple[
- "core_parameters.abc.Duration", Parameter
- ]
+ IncompletePoint: typing.TypeAlias = tuple["core_parameters.abc.Duration", Parameter]
CompletePoint: typing.TypeAlias = tuple[
"core_parameters.abc.Duration", Parameter, CurveShape # type: ignore
]
@@ -799,87 +797,3 @@ def resolve(
p = (abst * fact, new_param, self.event_to_curve_shape(e))
plist.append(p)
return resolve_envelope_class(plist)
-
-
-class TempoEnvelope(Envelope):
- """Define dynamic or static tempo trajectories.
-
- You can either define a new `TempoEnvelope` with instances
- of classes which inherit from :class:`mutwo.core_parameters.abc.Tempo`
- (for instance :class:`mutwo.core_parameters.DirectTempo`) or with
- `float` or `int` objects which represent beats per minute.
-
- Please see the :class:`mutwo.core_events.Envelope` for full documentation
- for initialization attributes.
-
- The default parameters of the `TempoEnvelope` class expects
- :class:`mutwo.core_events.Chronon` to which a tempo
- was assigned by the name "tempo".
-
- **Example:**
-
- >>> from mutwo import core_events
- >>> from mutwo import core_parameters
- >>> # (1) define with floats
- >>> # So we have an envelope which moves from tempo 60 to 30
- >>> # and back to 60.
- >>> tempo_envelope_with_float = core_events.TempoEnvelope(
- ... [[0, 60], [1, 30], [2, 60]]
- ... )
- >>> # (2) define with tempos
- >>> tempo_envelope_with_tempos = core_events.TempoEnvelope(
- ... [
- ... [0, core_parameters.DirectTempo(60)],
- ... [1, core_parameters.DirectTempo(30)],
- ... [2, core_parameters.WesternTempo(30, reference=2)],
- ... ]
- ... )
- """
-
- default_event_class = core_events.TempoChronon
-
- def __eq__(self, other: typing.Any):
- # TempoEnvelope can't use the default '__eq__' method inherited
- # from list, because this would create endless recursion
- # (because every event has a TempoEnvelope, so Python would forever
- # compare the TempoEnvelopes of TempoEnvelopes).
- try:
- return (
- # Prefer lazy evaluation for better performance
- # (use 'and' instead of 'all').
- self.absolute_time_tuple == other.absolute_time_tuple
- and self.curve_shape_tuple == other.curve_shape_tuple
- and self.value_tuple == other.value_tuple
- )
- except AttributeError:
- return False
-
- def event_to_parameter(
- self, event: core_events.abc.Event
- ) -> core_parameters.abc.Tempo:
- return event.tempo
-
- def value_to_parameter(self, value: float) -> core_parameters.abc.Tempo:
- return core_parameters.DirectTempo(value)
-
- def parameter_to_value(
- self, parameter: core_parameters.abc.Tempo.Type
- ) -> float:
- # Here we specify, that we allow either core_parameters.abc.Tempo
- # or float/number objects.
- # So in case we have a core_parameters.abc.Tempo 'getattr' is
- # successful, if not it will return 'parameter', because it
- # will assume that we have a number based tempo.
- return float(
- getattr(parameter, "bpm", parameter)
- )
-
- def apply_parameter_on_event(
- self, event: core_events.abc.Event, parameter: core_parameters.abc.Tempo
- ):
- event.tempo = parameter
-
- def initialise_default_event_class(
- self, duration: core_parameters.abc.Duration
- ) -> core_events.abc.Event:
- return self.default_event_class(tempo=1, duration=duration)
diff --git a/mutwo/core_events/patchparameters.py b/mutwo/core_events/patchparameters.py
new file mode 100644
index 00000000..80c895e1
--- /dev/null
+++ b/mutwo/core_events/patchparameters.py
@@ -0,0 +1,122 @@
+# This file is part of mutwo, ecosystem for time-based arts.
+#
+# Copyright (C) 2024-
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""This file defines the continuous parameter abc and an implementation for a
+continuous tempo trajectory. These bits should actually be inside of
+
+ core_parameters/abc.py
+ core_parameters/tempos.py
+
+but we can't put them there, because the classes inherit from an event and
+it would create a circular import error if we would move these definitions
+there. Therefore we fix the circular import error by monkey-patching
+'core_parameters' module inside the 'core_events' module. In order to ensure
+this patch is applied whenever loading 'core_parameters', we also import
+'core_events' inside 'core_parameters/__init__.py'.
+
+While this solution is far from ideal, it's still the best way how this can
+be archived:
+
+(1) We can't force an import order (inside 'mutwo/__init__.py' because we use
+ a namespace package (breaking this would break the whole mutwo
+ infrastructure).
+
+(2) We can't defer event import to call time, because it's needed at class
+ creation (and we need proper classes and not some functions to construct
+ classes to support inheritance, to preserve pickle-ability and to have
+ better documentation).
+
+(3) We also can't defer the import of these bits inside 'core_parameters',
+ because mutwos module structure prohibits deeper nesting apart from
+ 'abc', 'constants' and 'configurations': this *needs* to be present
+ inside 'core_parameters'.
+
+(4) We can't export this part to another package, as this clearly belongs
+ to 'mutwo.core' and nothing else.
+"""
+
+import abc
+import typing
+
+from mutwo import core_parameters
+
+from .envelopes import Envelope
+
+
+# Extend 'mutwo/core_parameters/abc.py'
+class ContinuousParameterMixin(Envelope):
+ """Continuous mixin for any :class:`SingleValueParameter`"""
+
+ def __init__(self, event_iterable_or_point_sequence=[], *args, **kwargs):
+ super().__init__(event_iterable_or_point_sequence, *args, **kwargs)
+
+ @property
+ @abc.abstractmethod
+ def parameter_name(self) -> str:
+ ...
+
+ @property
+ @abc.abstractmethod
+ def default_parameter(self) -> str:
+ ...
+
+ @classmethod
+ def from_parameter(cls, parameter):
+ if isinstance(parameter, cls):
+ return parameter
+ return cls([[0, parameter]])
+
+ def value_to_parameter(self, value: typing.Any):
+ return self.from_any(value)
+
+ def parameter_to_value(self, parameter: typing.Any):
+ return getattr(parameter, self.value_name)
+
+ def apply_parameter_on_event(self, event, parameter: typing.Any):
+ setattr(event, self.parameter_name, self.from_any(parameter))
+
+ def event_to_parameter(self, event):
+ return getattr(event, self.parameter_name, self.default_parameter)
+
+
+core_parameters.abc.ContinuousParameterMixin = ContinuousParameterMixin
+core_parameters.abc.__all__ += ("ContinuousParameterMixin",)
+
+
+# Extend 'mutwo/core_parameters/tempos.py'
+class ContinuousTempo(
+ core_parameters.abc.Tempo, core_parameters.abc.ContinuousParameterMixin
+):
+ """A continuous tempo."""
+
+ @classmethod
+ @property
+ def parameter_name(cls) -> str:
+ return "tempo"
+
+ @classmethod
+ @property
+ def default_parameter(cls) -> core_parameters.abc.Tempo:
+ return core_parameters.DirectTempo(60)
+
+ @property
+ def bpm(self):
+ return self.value_at(0)
+
+
+core_parameters.ContinuousTempo = ContinuousTempo
+core_parameters.__all__ += ("ContinuousTempo",)
diff --git a/mutwo/core_events/tempos.py b/mutwo/core_events/tempos.py
deleted file mode 100644
index abb531ea..00000000
--- a/mutwo/core_events/tempos.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# This file is part of mutwo, ecosystem for time-based arts.
-#
-# Copyright (C) 2020-2024
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-from mutwo import core_events
-from mutwo import core_parameters
-
-__all__ = ("TempoChronon",)
-
-
-# XXX: Currently type hints are deactivated here, because otherwise we get
-# problems with circular imports (because 'TempoChronon' is needed by envelopes
-# and because envelopes are needed by parameters). Because this code is very
-# short, it may not matter so much.
-class TempoChronon(core_events.Chronon):
- """A :class:`TempoChronon` describes the tempo for a given time."""
-
- def __init__(self, tempo, *args, **kwargs):
- self.tempo = tempo
- super().__init__(*args, **kwargs)
-
- @property
- def tempo(self):
- return self._tempo
-
- @tempo.setter
- def tempo(self, tempo):
- self._tempo = core_parameters.abc.Tempo.from_any(tempo)
diff --git a/mutwo/core_parameters/__init__.py b/mutwo/core_parameters/__init__.py
index 602a5083..b15ed959 100644
--- a/mutwo/core_parameters/__init__.py
+++ b/mutwo/core_parameters/__init__.py
@@ -34,3 +34,8 @@
# Force flat structure
del core_utilities, durations, tempos
+
+# Force core_parameters patch in core_events
+from mutwo import core_events
+
+del core_events
diff --git a/mutwo/core_parameters/abc.py b/mutwo/core_parameters/abc.py
index c9e89b74..d647c726 100644
--- a/mutwo/core_parameters/abc.py
+++ b/mutwo/core_parameters/abc.py
@@ -42,7 +42,6 @@
import fractions as _fractions
from mutwo import core_constants
-from mutwo import core_events
from mutwo import core_parameters
from mutwo import core_utilities
@@ -50,7 +49,6 @@
"Parameter",
"SingleValueParameter",
"SingleNumberParameter",
- "ParameterWithEnvelope",
"Duration",
"Tempo",
)
@@ -81,41 +79,6 @@ def from_any(cls: typing.Type[T], object) -> T:
return object
-class ParameterWithEnvelope(Parameter):
- """Abstract base class for all parameters with an envelope."""
-
- def __init__(self, envelope: core_events.RelativeEnvelope):
- self.envelope = envelope
-
- @property
- def envelope(self) -> core_events.RelativeEnvelope:
- return self._envelope
-
- @envelope.setter
- def envelope(self, new_envelope: typing.Any):
- if not isinstance(new_envelope, core_events.RelativeEnvelope):
- raise TypeError(
- f"Found illegal object '{new_envelope}' of not "
- f"supported type '{type(new_envelope)}'. "
- f"Only instances of '{core_events.RelativeEnvelope}'"
- " are allowed!"
- )
- self._envelope = new_envelope
-
- def resolve_envelope(
- self,
- duration: core_parameters.abc.Duration,
- # NOTE: We can't directly set the default attribute value,
- # but we have to do it with `None` and resolve it later,
- # because otherwise we will get a circular import
- # (core_parameters need to be imported before core_events,
- # because we need core_parameters.Duration in core_events).
- resolve_envelope_class: typing.Optional[type[core_events.Envelope]] = None,
- ) -> core_events.Envelope:
- resolve_envelope_class = resolve_envelope_class or core_events.Envelope
- return self.envelope.resolve(duration, self, resolve_envelope_class)
-
-
class SingleValueParameter(Parameter):
"""Abstract base class for all parameters which are defined by one value.
@@ -185,7 +148,7 @@ def abstract_method(_) -> value_return_type:
if hasattr(cls, "value_name"):
raise core_utilities.AlreadyDefinedValueNameError(cls)
- setattr(cls, "value_name", property(lambda _: value_name))
+ setattr(cls, "value_name", classmethod(property(lambda _: value_name)))
def __repr_content__(self) -> str:
return f"{getattr(self, self.value_name)}" # type: ignore
@@ -288,7 +251,9 @@ def __lt__(self, other: typing.Any) -> bool:
return self._compare(other, lambda value0, value1: value0 < value1, True)
-class Duration(SingleNumberParameter, value_name="beat_count", value_return_type="float"):
+class Duration(
+ SingleNumberParameter, value_name="beat_count", value_return_type="float"
+):
"""Abstract base class for any duration.
If the user wants to define a Duration class, the abstract
diff --git a/mutwo/core_parameters/tempos.py b/mutwo/core_parameters/tempos.py
index c7b92a5f..41da57dc 100644
--- a/mutwo/core_parameters/tempos.py
+++ b/mutwo/core_parameters/tempos.py
@@ -40,11 +40,9 @@ class DirectTempo(core_parameters.abc.Tempo):
**Example:**
- >>> from mutwo import core_events
>>> from mutwo import core_parameters
- >>> tempo_envelope = core_events.TempoEnvelope([
- ... [0, core_parameters.DirectTempo(60)]
- ... ])
+ >>> core_parameters.DirectTempo(60)
+ DirectTempo(60.0)
"""
def __init__(self, bpm: core_constants.Real | str):
@@ -81,11 +79,9 @@ class WesternTempo(core_parameters.abc.Tempo):
**Example:**
- >>> from mutwo import core_events
>>> from mutwo import core_parameters
- >>> tempo_envelope = core_events.TempoEnvelope([
- ... [0, core_parameters.WesternTempo(60, reference=2)]
- ... ])
+ >>> core_parameters.WesternTempo(60, reference=2)
+ WesternTempo(120.0)
"""
def __init__(
diff --git a/performance_tests/performance_tests.py b/performance_tests/performance_tests.py
index 090b0504..ac820836 100644
--- a/performance_tests/performance_tests.py
+++ b/performance_tests/performance_tests.py
@@ -8,6 +8,7 @@
import unittest
from mutwo import core_events
+from mutwo import core_parameters
conc = core_events.Concurrence
cons = core_events.Consecution
@@ -62,7 +63,7 @@ def test_metrize(self):
e = conc(
[cons([ch(random.uniform(0.9, 1.2)) for _ in range(20)]) for _ in range(3)]
)
- e.tempo_envelope = core_events.TempoEnvelope(
+ e.tempo = core_parameters.ContinuousTempo(
[[i, 60] if i % 2 == 0 else [i, 50] for i in range(5)]
)
e.metrize()
diff --git a/performance_tests/tempo_envelopes.py b/performance_tests/tempo_envelopes.py
index 70f85524..0ef6a35d 100644
--- a/performance_tests/tempo_envelopes.py
+++ b/performance_tests/tempo_envelopes.py
@@ -2,6 +2,7 @@
import random
from mutwo import core_events
+from mutwo import core_parameters
def t():
@@ -13,7 +14,7 @@ def t():
for _ in range(3)
]
)
- e.tempo_envelope = core_events.TempoEnvelope(
+ e.tempo = core_parameters.ContinuousTempo(
[[i, 60] if i % 2 == 0 else [i, 50] for i in range(20)]
)
e.metrize()
diff --git a/tests/converters/tempos_tests.py b/tests/converters/tempos_tests.py
index a7f2878f..b41c036c 100644
--- a/tests/converters/tempos_tests.py
+++ b/tests/converters/tempos_tests.py
@@ -4,6 +4,12 @@
from mutwo import core_events
from mutwo import core_parameters
+cns = core_events.Consecution
+cnc = core_events.Concurrence
+chn = core_events.Chronon
+
+cntTemp = core_parameters.ContinuousTempo
+
class TempoToBeatLengthInSecondsTest(unittest.TestCase):
def setUp(self):
@@ -27,17 +33,15 @@ def test_convert(self):
class TempoConverterTest(unittest.TestCase):
def test_convert_chronon(self):
- tempo_envelope = core_events.Envelope([[0, 30], [4, 60]])
- chronon = core_events.Chronon(4)
- converter = core_converters.TempoConverter(tempo_envelope)
+ tempo = cntTemp([[0, 30], [4, 60]])
+ chronon = chn(4)
+ converter = core_converters.TempoConverter(tempo)
converted_chronon = converter.convert(chronon)
expected_duration = 6
self.assertEqual(converted_chronon.duration, expected_duration)
def test_convert_consecution(self):
- consecution = core_events.Consecution(
- [core_events.Chronon(2) for _ in range(5)]
- )
+ consecution = cns([chn(2) for _ in range(5)])
tempo_list = [
# Event 0
(0, 30, 0),
@@ -55,8 +59,8 @@ def test_convert_consecution(self):
(8, core_parameters.WesternTempo(30, reference=1), -10),
(10, core_parameters.WesternTempo(30, reference=2), 0),
]
- tempo_envelope = core_events.Envelope(tempo_list)
- converter = core_converters.TempoConverter(tempo_envelope)
+ tempo = cntTemp(tempo_list)
+ converter = core_converters.TempoConverter(tempo)
converted_consecution = converter.convert(consecution)
expected_duration_tuple = (4.0, 3.0, 3.0, 3.800090804, 2.199909196)
self.assertEqual(
@@ -68,11 +72,11 @@ def test_convert_consecution(self):
)
def test_convert_concurrence(self):
- tempo_envelope = core_events.Envelope([[0, 30], [4, 60]])
- chronon0 = core_events.Chronon(4)
- chronon1 = core_events.Chronon(8)
- concurrence = core_events.Concurrence([chronon0, chronon0, chronon1])
- converter = core_converters.TempoConverter(tempo_envelope)
+ tempo = cntTemp([[0, 30], [4, 60]])
+ chronon0 = chn(4)
+ chronon1 = chn(8)
+ concurrence = cnc([chronon0, chronon0, chronon1])
+ converter = core_converters.TempoConverter(tempo)
converted_concurrence = converter.convert(concurrence)
expected_duration0 = concurrence[0].duration * 1.5
expected_duration1 = 10
@@ -80,60 +84,46 @@ def test_convert_concurrence(self):
self.assertEqual(converted_concurrence[1].duration, expected_duration0)
self.assertEqual(converted_concurrence[2].duration, expected_duration1)
- def test_convert_tempo_envelope(self):
- tempo_envelope = core_events.Envelope([[0, 30], [4, 60]])
- chronon = core_events.Chronon(4)
- chronon.tempo_envelope = tempo_envelope.copy()
- converter = core_converters.TempoConverter(tempo_envelope)
+ def test_convert_tempo(self):
+ tempo = cntTemp([[0, 30], [4, 60]])
+ chronon = chn(4)
+ chronon.tempo = tempo.copy()
+ converter = core_converters.TempoConverter(tempo)
converted_chronon = converter.convert(chronon)
- self.assertEqual(converted_chronon.tempo_envelope.duration, 6)
+ self.assertEqual(converted_chronon.tempo.duration, 6)
- def test_convert_tempo_envelope_with_too_short_global_tempo_envelope(self):
- tempo_envelope = core_events.Envelope([[0, 30], [0.5, 30]])
- consecution = core_events.Consecution(
- [core_events.Chronon(1), core_events.Chronon(1)]
- )
- converter = core_converters.TempoConverter(tempo_envelope)
+ def test_convert_tempo_with_too_short_global_tempo(self):
+ tempo = cntTemp([[0, 30], [0.5, 30]])
+ consecution = cns([chn(1), chn(1)])
+ converter = core_converters.TempoConverter(tempo)
converted_consecution = converter.convert(consecution)
# This has to be 2 instead of 1 because of tempo 30 BPM
# (which is half of normal tempo 60, so duration should be
# doubled).
- self.assertEqual(converted_consecution[1].tempo_envelope.duration, 2)
+ self.assertEqual(converted_consecution[1].tempo.duration, 2)
class EventToMetrizedEventTest(unittest.TestCase):
def test_convert_chronon(self):
- chronon = core_events.Chronon(
- 2, tempo_envelope=core_events.TempoEnvelope([[0, 30], [2, 30]])
- )
- expected_chronon = core_events.Chronon(4)
+ chronon = chn(2, tempo=cntTemp([[0, 30], [2, 30]]))
+ expected_chronon = chn(4)
event_to_metrized_event = core_converters.EventToMetrizedEvent()
self.assertEqual(event_to_metrized_event.convert(chronon), expected_chronon)
def test_convert_nested_event_with_simple_hierarchy(self):
"""
- Test that tempo envelopes are propagated to all children events.
+ Test that tempos are propagated to all children events.
"""
- consecution = core_events.Consecution(
+ consecution = cns(
[
- core_events.Consecution(
- [
- core_events.Chronon(
- 1,
- tempo_envelope=core_events.TempoEnvelope(
- [[0, 30], [1, 30]]
- ),
- ),
- core_events.Chronon(1),
- ],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 30]]),
+ cns(
+ [chn(1, tempo=core_parameters.DirectTempo(30)), chn(1)],
+ tempo=core_parameters.DirectTempo(30),
)
],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 30]]),
- )
- expected_consecution = core_events.Consecution(
- [core_events.Consecution([core_events.Chronon(8), core_events.Chronon(4)])]
+ tempo=core_parameters.DirectTempo(30),
)
+ expected_consecution = cns([cns([chn(8), chn(4)])])
event_to_metrized_event = core_converters.EventToMetrizedEvent()
self.assertEqual(
event_to_metrized_event.convert(consecution), expected_consecution
@@ -141,32 +131,20 @@ def test_convert_nested_event_with_simple_hierarchy(self):
def test_convert_nested_event_with_complex_hierarchy(self):
"""
- Ensure tempo envelopes only influence deeper nested events
+ Ensure tempos only influence deeper nested events
and no events on the same level.
"""
- consecution = core_events.Consecution(
+ consecution = cns(
[
- core_events.Consecution(
- [
- core_events.Chronon(
- 1,
- tempo_envelope=core_events.TempoEnvelope(
- [[0, 30], [1, 30]]
- ),
- )
- ],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 30]]),
+ cns(
+ [chn(1, tempo=cntTemp([[0, 30], [1, 30]]))],
+ tempo=cntTemp([[0, 30], [1, 30]]),
),
- core_events.Chronon(1),
+ chn(1),
],
)
- expected_consecution = core_events.Consecution(
- [
- core_events.Consecution([core_events.Chronon(4)]),
- core_events.Chronon(1),
- ]
- )
+ expected_consecution = cns([cns([chn(4)]), chn(1)])
event_to_metrized_event = core_converters.EventToMetrizedEvent()
self.assertEqual(
event_to_metrized_event.convert(consecution), expected_consecution
@@ -176,26 +154,17 @@ def test_convert_with_skip_level_count(self):
"""
Ensure skip_level_count takes effect
"""
- consecution = core_events.Consecution(
+ consecution = cns(
[
- core_events.Consecution(
- [
- core_events.Chronon(
- 1,
- tempo_envelope=core_events.TempoEnvelope(
- [[0, 30], [1, 30]]
- ),
- ),
- core_events.Chronon(1),
- ],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 30]]),
+ cns(
+ [chn(1, tempo=cntTemp([[0, 30], [1, 30]])), chn(1)],
+ tempo=cntTemp([[0, 30], [1, 30]]),
)
],
- tempo_envelope=core_events.TempoEnvelope([[0, 11], [1, 11]]),
+ tempo=cntTemp([[0, 11], [1, 11]]),
)
- expected_consecution = core_events.Consecution(
- [core_events.Consecution([core_events.Chronon(4), core_events.Chronon(2)])],
- tempo_envelope=core_events.TempoEnvelope([[0, 11], [1, 11]]),
+ expected_consecution = cns(
+ [cns([chn(4), chn(2)])], tempo=cntTemp([[0, 11], [1, 11]])
)
event_to_metrized_event = core_converters.EventToMetrizedEvent(
skip_level_count=0
@@ -208,35 +177,21 @@ def test_convert_with_maxima_depth_count(self):
"""
Ensure maxima_depth_count takes effect
"""
- consecution = core_events.Consecution(
+ consecution = cns(
[
- core_events.Consecution(
- [
- core_events.Chronon(
- 1,
- tempo_envelope=core_events.TempoEnvelope([[0, 4], [1, 4]]),
- ),
- core_events.Chronon(1),
- ],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 30]]),
+ cns(
+ [chn(1, tempo=cntTemp([[0, 4], [1, 4]])), chn(1)],
+ tempo=cntTemp([[0, 30], [1, 30]]),
)
],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 30]]),
+ tempo=cntTemp([[0, 30], [1, 30]]),
)
- expected_consecution = core_events.Consecution(
+ expected_consecution = cns(
[
- core_events.Consecution(
+ cns(
[
- core_events.Chronon(
- 4,
- tempo_envelope=core_events.TempoEnvelope([[0, 4], [4, 4]]),
- ),
- core_events.Chronon(
- 4,
- tempo_envelope=core_events.TempoEnvelope(
- [[0, 60], [4, 60]]
- ),
- ),
+ chn(4, tempo=cntTemp([[0, 4], [4, 4]])),
+ chn(4, tempo=cntTemp([[0, 60], [4, 60]])),
]
)
]
diff --git a/tests/events/basic_tests.py b/tests/events/basic_tests.py
index bc5fb61f..023d593a 100644
--- a/tests/events/basic_tests.py
+++ b/tests/events/basic_tests.py
@@ -29,15 +29,15 @@ def get_event_class(self) -> typing.Type:
def get_event_instance(self) -> core_events.abc.Event:
...
- def test_tempo_envelope_auto_initialization(self):
- self.assertTrue(bool(self.event.tempo_envelope))
+ def test_tempo_auto_initialization(self):
+ self.assertTrue(bool(self.event.tempo))
self.assertTrue(
- isinstance(self.event.tempo_envelope, core_events.TempoEnvelope)
+ isinstance(self.event.tempo, core_parameters.abc.Tempo)
)
- def test_tempo_envelope_auto_initialization_and_settable(self):
- self.event.tempo_envelope[0].duration = 100
- self.assertEqual(self.event.tempo_envelope[0].duration, 100)
+ def test_tempo_auto_initialization_and_settable(self):
+ self.event.tempo.bpm = 20
+ self.assertEqual(self.event.tempo.bpm, 20)
def test_split_at_start(self):
self.assertEqual(self.event.split_at(0), (self.event,))
@@ -149,19 +149,19 @@ def test_metrize(self):
"""
chronon = core_events.Chronon(
- 1, tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 120]])
+ 1, tempo=core_parameters.ContinuousTempo([[0, 30], [1, 120]])
)
self.assertEqual(
chronon.copy().metrize(),
core_converters.EventToMetrizedEvent().convert(chronon),
)
- def test_reset_tempo_envelope(self):
+ def test_reset_tempo(self):
chronon = self.get_event_instance()
- chronon.tempo_envelope[0].value = 100
- self.assertEqual(chronon.tempo_envelope[0].value, 100)
- chronon.reset_tempo_envelope()
- self.assertEqual(chronon.tempo_envelope.value_tuple[0], 60)
+ chronon.tempo.bpm = 20
+ self.assertEqual(chronon.tempo.bpm, 20)
+ chronon.reset_tempo()
+ self.assertEqual(chronon.tempo.bpm, 60)
def test_get_assigned_parameter(self):
duration = core_parameters.DirectDuration(10)
@@ -207,7 +207,7 @@ def test_set_not_assigned_parameter(self):
def test_parameter_to_compare_tuple(self):
chronon = core_events.Chronon(1)
- expected_parameter_to_compare_tuple = ("duration", "tag", "tempo_envelope")
+ expected_parameter_to_compare_tuple = ("duration", "tag", "tempo")
self.assertEqual(
chronon._parameter_to_compare_tuple,
expected_parameter_to_compare_tuple,
@@ -373,12 +373,12 @@ def test_equal_with_different_side_attributes(self):
self.assertEqual(consecution0, consecution1)
- consecution0.tempo_envelope = core_events.TempoEnvelope(
+ consecution0.tempo = core_parameters.ContinuousTempo(
[[0, 100], [10, 100]]
)
self.assertNotEqual(
- consecution0.tempo_envelope, consecution1.tempo_envelope
+ consecution0.tempo, consecution1.tempo
)
self.assertNotEqual(consecution0, consecution1)
self.assertTrue(list.__eq__(consecution0, consecution1))
@@ -393,29 +393,29 @@ def test_metrize(self):
consecution = core_events.Consecution(
[
core_events.Chronon(
- 1, tempo_envelope=core_events.TempoEnvelope([[0, 120], [1, 120]])
+ 1, tempo=core_parameters.ContinuousTempo([[0, 120], [1, 120]])
)
],
- tempo_envelope=core_events.TempoEnvelope([[0, 30], [1, 120]]),
+ tempo=core_parameters.ContinuousTempo([[0, 30], [1, 120]]),
)
self.assertEqual(
consecution.copy().metrize(),
core_converters.EventToMetrizedEvent().convert(consecution),
)
- def test_concatenate_tempo_envelope(self):
+ def test_concatenate_tempo(self):
cons0 = self.get_event_class()(
[core_events.Chronon(1)],
- tempo_envelope=core_events.TempoEnvelope([[0, 20], [1, 20], [3, 100]]),
+ tempo=core_parameters.ContinuousTempo([[0, 20], [1, 20], [3, 100]]),
)
cons1 = self.get_event_class()(
[core_events.Chronon(2)],
- tempo_envelope=core_events.TempoEnvelope([[0, 50], [1, 10]]),
+ tempo=core_parameters.ContinuousTempo([[0, 50], [1, 10]]),
)
- cons0._concatenate_tempo_envelope(cons1)
- self.assertEqual(cons0.tempo_envelope.value_tuple, (20, 20, 50, 10))
+ cons0._concatenate_tempo(cons1)
+ self.assertEqual(cons0.tempo.value_tuple, (20, 20, 50, 10))
self.assertEqual(
- cons0.tempo_envelope.absolute_time_in_floats_tuple, (0, 1, 1, 2)
+ cons0.tempo.absolute_time_in_floats_tuple, (0, 1, 1, 2)
)
def test_magic_method_add(self):
@@ -425,13 +425,13 @@ def test_magic_method_add(self):
)
def test_magic_method_add_children(self):
- """Ensure children and tempo envelope are concatenated"""
+ """Ensure children and tempos are concatenated"""
cons, chr = core_events.Consecution, core_events.Chronon
- cons0 = cons([chr(1)], tempo_envelope=core_events.TempoEnvelope([[0, 50], [1, 50]]))
+ cons0 = cons([chr(1)], tempo=core_parameters.ContinuousTempo([[0, 50], [1, 50]]))
cons1 = cons([chr(1), chr(2)])
cons_ok = cons(
[chr(1), chr(1), chr(2)],
- tempo_envelope=core_events.TempoEnvelope(
+ tempo=core_parameters.ContinuousTempo(
[[0, 50], [1, 50], [1, 60], [2, 60]]
),
)
@@ -1207,13 +1207,13 @@ def test_extend_until(self):
def test_concatenate_by_index(self):
# In this test we call 'metrize()' on each concatenated
- # event, so for each layer 'reset_tempo_envelope' is called
- # and we don't have to provide the concatenated tempo envelope
- # (which is != the default tempo envelope when constructing events).
+ # event, so for each layer 'reset_tempo' is called
+ # and we don't have to provide the concatenated tempo
+ # (which is != the default tempo when constructing events).
#
- # We already carefully test the tempo_envelope concatenation
+ # We already carefully test the tempo concatenation
# feature of 'conatenate_by_tag' in
- # 'test_concatenate_by_index_persists_tempo_envelope'.
+ # 'test_concatenate_by_index_persists_tempo'.
s, se, si = (
core_events.Chronon,
core_events.Consecution,
@@ -1276,13 +1276,13 @@ def test_concatenate_by_index_to_empty_event(self):
empty_conc.concatenate_by_index(filled_conc)
self.assertEqual(empty_conc, filled_conc)
- def test_concatenate_by_index_persists_tempo_envelope(self):
+ def test_concatenate_by_index_persists_tempo(self):
"""Verify that concatenation also concatenates the tempos"""
conc0 = core_events.Concurrence(
[
core_events.Consecution(
[core_events.Chronon(1)],
- tempo_envelope=core_events.TempoEnvelope(
+ tempo=core_parameters.ContinuousTempo(
[[0, 1], [1, 20], [10, 100]]
),
)
@@ -1292,14 +1292,14 @@ def test_concatenate_by_index_persists_tempo_envelope(self):
[
core_events.Consecution(
[core_events.Chronon(1)],
- tempo_envelope=core_events.TempoEnvelope([[0, 1000], [1, 10]]),
+ tempo=core_parameters.ContinuousTempo([[0, 1000], [1, 10]]),
)
]
)
conc0.concatenate_by_index(conc1)
- self.assertEqual(conc0[0].tempo_envelope.value_tuple, (1, 20, 1000, 10))
+ self.assertEqual(conc0[0].tempo.value_tuple, (1, 20, 1000, 10))
self.assertEqual(
- conc0[0].tempo_envelope.absolute_time_in_floats_tuple, (0, 1, 1, 2)
+ conc0[0].tempo.absolute_time_in_floats_tuple, (0, 1, 1, 2)
)
def test_concatenate_by_tag(self):
@@ -1307,13 +1307,13 @@ def test_concatenate_by_tag(self):
core_events.Chronon,
core_events.Consecution,
core_events.Concurrence,
- core_events.TempoEnvelope,
+ core_parameters.ContinuousTempo,
)
- chr1 = si([tse([s(1), s(1)], tag="a", tempo_envelope=t([[0, 50], [1, 50]]))])
+ chr1 = si([tse([s(1), s(1)], tag="a", tempo=t([[0, 50], [1, 50]]))])
chr2 = si([tse([s(2), s(1)], tag="a"), tse([s(0.5)], tag="b")])
- # Concatenation tempo envelopes
+ # Concatenation continuous tempos
t0 = t([[0, 50], [1, 50], [2, 50], [2, 50], [3, 50]])
t1 = t([[0, 50], [1, 50], [2, 50], [2, 60], [3, 60]])
t2 = t([[0, 60], [1, 60], [3, 60], [3, 50], [4, 50]])
@@ -1321,7 +1321,7 @@ def test_concatenate_by_tag(self):
# Equal size concatenation
self.assertEqual(
chr1.copy().concatenate_by_tag(chr1),
- si([tse([s(1), s(1), s(1), s(1)], tag="a", tempo_envelope=t0)]),
+ si([tse([s(1), s(1), s(1), s(1)], tag="a", tempo=t0)]),
)
# Smaller self
@@ -1330,9 +1330,9 @@ def test_concatenate_by_tag(self):
chr1.copy().concatenate_by_tag(chr2),
si(
[
- tse([s(1), s(1), s(2), s(1)], tag="a", tempo_envelope=t1),
+ tse([s(1), s(1), s(2), s(1)], tag="a", tempo=t1),
# Tempo envelope is default, because no ancestor existed
- # (so '_concatenate_tempo_envelope' wasn't called)
+ # (so '_concatenate_tempo' wasn't called)
tse([s(2), s(0.5)], tag="b"),
]
),
@@ -1344,9 +1344,9 @@ def test_concatenate_by_tag(self):
chr2.copy().concatenate_by_tag(chr1),
si(
[
- tse([s(2), s(1), s(1), s(1)], tag="a", tempo_envelope=t2),
+ tse([s(2), s(1), s(1), s(1)], tag="a", tempo=t2),
# Tempo envelope is default, because no successor existed
- # (so '_concatenate_tempo_envelope' wasn't called)
+ # (so '_concatenate_tempo' wasn't called)
tse([s(0.5), s(2.5)], tag="b"),
]
),
diff --git a/tests/events/envelopes_tests.py b/tests/events/envelopes_tests.py
index cbca97cf..8081acac 100644
--- a/tests/events/envelopes_tests.py
+++ b/tests/events/envelopes_tests.py
@@ -370,40 +370,5 @@ def test_resolve(self):
self.assertEqual(resolved_envelope.value_tuple, (100, 105, 110))
-class TempoEnvelopeTest(unittest.TestCase):
- def setUp(self):
- self.tempo_envelope_with_float = core_events.TempoEnvelope(
- [[0, 60], [1, 30], [2, 60]]
- )
-
- self.tempo_envelope_with_tempos = core_events.TempoEnvelope(
- [
- [0, core_parameters.DirectTempo(60)],
- [1, core_parameters.DirectTempo(30)],
- [2, core_parameters.WesternTempo(30, reference=2)],
- ]
- )
-
- self.mixed_tempo_envelope = core_events.TempoEnvelope(
- [[0, 60], [1, core_parameters.DirectTempo(30)], [2, 60]]
- )
-
- def _test_value_at(self, tempo_envelope: core_events.TempoEnvelope):
- self.assertEqual(tempo_envelope.value_at(0), 60)
- self.assertEqual(tempo_envelope.value_at(0.5), 45)
- self.assertEqual(tempo_envelope.value_at(1), 30)
- self.assertEqual(tempo_envelope.value_at(1.5), 45)
- self.assertEqual(tempo_envelope.value_at(2), 60)
-
- def test_value_at_with_float(self):
- self._test_value_at(self.tempo_envelope_with_float)
-
- def test_value_at_with_tempos(self):
- self._test_value_at(self.tempo_envelope_with_tempos)
-
- def test_value_at_with_mixed(self):
- self._test_value_at(self.mixed_tempo_envelope)
-
-
if __name__ == "__main__":
unittest.main()
diff --git a/tests/events/tempos_tests.py b/tests/events/tempos_tests.py
deleted file mode 100644
index 1800fbb2..00000000
--- a/tests/events/tempos_tests.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import typing
-import unittest
-
-from mutwo import core_events
-from mutwo import core_parameters
-
-from .basic_tests import EventTest
-
-
-class TempoChrononTest(unittest.TestCase, EventTest):
- def setUp(self):
- EventTest.setUp(self)
-
- def get_event_class(self) -> typing.Type:
- return core_events.TempoChronon
-
- def get_event_instance(self) -> core_events.TempoChronon:
- return self.get_event_class()(tempo=60, duration=5)
-
- def test_initialization(self):
- # Ensure tempo conversion works
- self.assertEqual(
- core_events.TempoChronon(60, 1),
- core_events.TempoChronon(core_parameters.DirectTempo(60), 1),
- )
diff --git a/tests/parameters/tempos_tests.py b/tests/parameters/tempos_tests.py
index 6bd3675f..440dac4c 100644
--- a/tests/parameters/tempos_tests.py
+++ b/tests/parameters/tempos_tests.py
@@ -60,7 +60,7 @@ def test_default_tempo(self):
This is the expected standard in western music (notation).
"""
e = core_events.Chronon("1/4")
- e.tempo_envelope = core_events.TempoEnvelope(
+ e.tempo = core_parameters.ContinuousTempo(
[[0, core_parameters.WesternTempo(60)]]
)
self.assertEqual(e.metrize().duration, 1)