Skip to content

Commit

Permalink
parameters: Unify/simplify dynamic/continuous parameters
Browse files Browse the repository at this point in the history
Mutwo parameters that can be identified by only one value inherit from
'core_parameters.abc.SingleValueParameter'. But what if these parameters
are dynamically changing during the duration of one event (e.g.
glissando, ritardando, crescendo, ...) ? Before this patch this problem
was approached in two different ways:

    (1) The 'SingleValueParameter' Pitch has the attribute '.envelope'.
        This '.envelope' attribute can host a pitch interval trajectory
        that can be applied on the main pitch to create a glissando.
        Therefore all pitch classes have the option of an optional
        glissando. Whether this '.envelope' attribute is ignored or
        used is a decision of a given converter (or other (mutwo) code).

    (2) The 'SingleValueParameter' Tempo doesn't have the attribute
        '.envelope', but it is still possible to represent dynamic
        tempo trajectories by an additional 'TempoEnvelope'. This
        additional 'TempoEnvelope' itself isn't a 'Parameter' and
        certainly not a 'Tempo', but an 'Event' (that inherits from
        'Envelope'). Mutwo infrastructure either expects a
        'TempoEnvelope' or a 'Tempo', but never both.

This patch aims to unify the way how 'SingleValueParameter' approach
representations of continuous states. Instead of (1) or (2) another
solution is declared, that also aims to significantly simplify how
users can define continuous and discreet states of
'SingleValueParameter'. The unification aims to reduce code duplications
and to make it easier to understand the 'mutwo' framework (provide only
one way that needs to be learned and apply it for everything).

Generally we could say that (1) is better than (2), because it treats
continuous and discreet parameters in the same way and therefore makes
it flexible to switch between them as needed. But more precisely, the
solution (1) actually generalizes all discreet parameters to be
continuous by giving all parameter classes an '.envelope' attribute:
therefore actual discreet values don't exist anymore. This means that
if users only want to use (the simpler) static parameters they still
carried within their code continuous representations while they aren't
needed. Furthermore this approach has the problem that the trajectory
always needs to be relative to the identity of the parameter itself
to be generally apply-able to all parameter implementations. This mean
practically that trajectories can only be described in relative to
the central value via deltas. In case of 'Pitch' this means
we can only describe glissandi via pitch intervals that represent
the moving delta to the 'Pitch' identity, but we can't describe
glissandi by Pitch interpolations.

The unifying solution therefore is different than approach (1) or (2).
Neither do we add an '.envelope' attribute to all 'SingleValueParameter'
nor do we use external events to describe continuous parameters. Instead,
we do add a new 'parameters.abc' class: 'ContinuousParameterMixin'.
In order to create a continuous version of a parameter, the new
class can be created by inheriting from the parameter abc and the
'ContinuousParameterMixin'. This newly created class is both a
'Parameter' and an 'Event'. It must equally well implement the 'Parameters'
interface, so that it is usable just like any other implementation
of the given 'Parameter', but it must also behaves like an 'Envelope' so
that dynamic trajectories of the parameter can be represented and
interpreted if needed. This default version is an absolute envelope
(as this is simpler), but a relative version could also easily be
implemented if a user prefers this representation.
  • Loading branch information
levinericzimmermann committed Mar 19, 2024
1 parent 5972c97 commit 05a919c
Show file tree
Hide file tree
Showing 17 changed files with 373 additions and 516 deletions.
92 changes: 39 additions & 53 deletions mutwo/core_converters/tempos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = {}
Expand All @@ -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(
Expand All @@ -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

Expand Down Expand Up @@ -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

# ###################################################################### #
Expand All @@ -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))])
Expand All @@ -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,
Expand All @@ -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)
8 changes: 5 additions & 3 deletions mutwo/core_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -37,7 +37,6 @@
from . import abc

from .basic import *
from .tempos import *
from .envelopes import *

from . import basic, envelopes
Expand All @@ -48,3 +47,6 @@

# Force flat structure
del basic, core_utilities, envelopes

from . import patchparameters
del patchparameters
Loading

0 comments on commit 05a919c

Please sign in to comment.