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)