Skip to content

Commit

Permalink
ComplexEvent/time-axis concat: Fix tempo envelope
Browse files Browse the repository at this point in the history
Before this patch ComplexEvents time-axis concatenations
methods [1] ignored the tempo envelope of the added event.
This can be considered as a bug, because when concatenating events
we would expect that this concatenation persists the duration
information of both events.

[1] This includes `SequentialEvent.__add__` and
`SimultaneousEvent.concatenate_by_...` methods.
  • Loading branch information
levinericzimmermann committed Mar 16, 2023
1 parent c0147de commit 881b7e9
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 9 deletions.
23 changes: 23 additions & 0 deletions mutwo/core_events/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,29 @@ def _mutate_parameter( # type: ignore
id_set=id_set,
)

def _concatenate_tempo_envelope(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
ensure that the tempo information is not lost.
This includes the `+` magic method of ``SequentialEvent``,
but also the `concatenate_by...` methods of ``SimultaneousEvent``.
It's important to first call this method before appending the
child events of the other container, because we still need
to know the original duration of the target event. Due to this
difficulty this method is private.
"""
other_tempo_envelope = other.tempo_envelope.copy()
# We first set the duration of the tempo envelopes to the
# duration of the given event. This is necessary, because the
# tempo envelope duration is always relative to the events duration.
# If we don't set them to this absolute value, the inner
# relationships may be distorted after concatenation.
self.tempo_envelope.duration = self.duration
other_tempo_envelope.duration = other.duration
self.tempo_envelope.extend(other_tempo_envelope)

# ###################################################################### #
# public methods #
# ###################################################################### #
Expand Down
21 changes: 19 additions & 2 deletions mutwo/core_events/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ def cut_off( # type: ignore
class SequentialEvent(core_events.abc.ComplexEvent, typing.Generic[T]):
"""Event-Object which contains other Events which happen in a linear order."""

# ###################################################################### #
# magic methods #
# ###################################################################### #

def __add__(self, event: list[T]) -> SequentialEvent[T]:
e = self.copy()
e._concatenate_tempo_envelope(event)
e.extend(event)
return e

# ###################################################################### #
# private static methods #
# ###################################################################### #
Expand Down Expand Up @@ -733,6 +743,12 @@ class SimultaneousEvent(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)
# We can't concatenate to a simple event.
# We also can't concatenate to anything else.
except AttributeError:
raise core_utilities.ConcatenationError(ancestor, event)
match ancestor:
case core_events.SequentialEvent():
ancestor.extend(event)
Expand All @@ -741,8 +757,9 @@ def _extend_ancestor(ancestor: core_events.abc.Event, event: core_events.abc.Eve
ancestor.concatenate_by_tag(event)
except core_utilities.NoTagError:
ancestor.concatenate_by_index(event)
# We can't concatenate to a simple event.
# We also can't concatenate to anything else.
# This should already fail above, but if this strange object
# somehow owned '_concatenate_tempo_envelope', it should
# fail here.
case _:
raise core_utilities.ConcatenationError(ancestor, event)

Expand Down
91 changes: 84 additions & 7 deletions tests/events/basic_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,39 @@ def test_metrize(self):
core_converters.EventToMetrizedEvent().convert(sequential_event),
)

def test_concatenate_tempo_envelope(self):
seq0 = self.get_event_class()(
[core_events.SimpleEvent(1)],
tempo_envelope=core_events.TempoEnvelope([[0, 20], [100, 30]]),
)
seq1 = self.get_event_class()(
[core_events.SimpleEvent(2)],
tempo_envelope=core_events.TempoEnvelope([[0, 50], [1, 10]]),
)
seq0._concatenate_tempo_envelope(seq1)
self.assertEqual(seq0.tempo_envelope.value_tuple, (20, 30, 50, 10))
self.assertEqual(
seq0.tempo_envelope.absolute_time_in_floats_tuple, (0, 1, 1, 3)
)

def test_magic_method_add(self):
self.assertEqual(
type(core_events.SequentialEvent([]) + core_events.SequentialEvent([])),
core_events.SequentialEvent,
)

def test_magic_method_add_children(self):
"""Ensure children and tempo envelope are concatenated"""
seq, s = core_events.SequentialEvent, core_events.SimpleEvent
seq0, seq1 = seq([s(1)]), seq([s(1), s(2)])
seq_ok = seq(
[s(1), s(1), s(2)],
tempo_envelope=core_events.TempoEnvelope(
[[0, 60], [1, 60], [1, 60], [4, 60]]
),
)
self.assertEqual(seq0 + seq1, seq_ok)

def test_magic_method_mul(self):
self.assertEqual(
type(core_events.SequentialEvent([]) * 5), core_events.SequentialEvent
Expand Down Expand Up @@ -1068,6 +1095,14 @@ def test_extend_until(self):
self.assertEqual(si([s(1), s(3)]).extend_until(), si([s(3), s(3)]))

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).
#
# We already carefully test the tempo_envelope concatenation
# feature of 'conatenate_by_tag' in
# 'test_concatenate_by_index_persists_tempo_envelope'.
s, se, si = (
core_events.SimpleEvent,
core_events.SequentialEvent,
Expand All @@ -1078,7 +1113,7 @@ def test_concatenate_by_index(self):
self.assertEqual(
self.nested_sequence.concatenate_by_index(
self.nested_sequence, mutate=False
),
).metrize(),
si(
[
se([s(1), s(2), s(3), s(1), s(2), s(3)]),
Expand All @@ -1095,7 +1130,7 @@ def test_concatenate_by_index(self):
se([s(2), s(1), s(2), s(3)]),
]
)
self.assertEqual(si_test.concatenate_by_index(self.nested_sequence), si_ok)
self.assertEqual(si_test.concatenate_by_index(self.nested_sequence).metrize(), si_ok)
# Mutate inplace!
self.assertEqual(si_test, si_ok)

Expand All @@ -1108,7 +1143,7 @@ def test_concatenate_by_index(self):
se([s(2)]),
]
)
self.assertEqual(si_test.concatenate_by_index(self.nested_sequence), si_ok)
self.assertEqual(si_test.concatenate_by_index(self.nested_sequence).metrize(), si_ok)

def test_concatenate_by_index_exception(self):
self.assertRaises(
Expand All @@ -1125,35 +1160,77 @@ def test_concatenate_by_index_to_empty_event(self):
empty_se.concatenate_by_index(filled_se)
self.assertEqual(empty_se, filled_se)

def test_concatenate_by_index_persists_tempo_envelope(self):
"""Verify that concatenation also concatenates the tempos"""
sim0 = core_events.SimultaneousEvent(
[
core_events.SequentialEvent(
[core_events.SimpleEvent(1)],
tempo_envelope=core_events.TempoEnvelope([[0, 1], [10, 100]]),
)
]
)
sim1 = core_events.SimultaneousEvent(
[
core_events.SequentialEvent(
[core_events.SimpleEvent(1)],
tempo_envelope=core_events.TempoEnvelope([[0, 1000], [1, 10]]),
)
]
)
sim0.concatenate_by_index(sim1)
self.assertEqual(sim0[0].tempo_envelope.value_tuple, (1, 100, 1000, 10))
self.assertEqual(
sim0[0].tempo_envelope.absolute_time_in_floats_tuple, (0, 1, 1, 2)
)

def test_concatenate_by_tag(self):
s, tse, si = (
s, tse, si, t = (
core_events.SimpleEvent,
core_events.TaggedSequentialEvent,
core_events.SimultaneousEvent,
core_events.TempoEnvelope,
)

s1 = si([tse([s(1), s(1)], tag="a")])
s2 = si([tse([s(2), s(1)], tag="a"), tse([s(0.5)], tag="b")])

# Concatenation tempo envelopes
t0 = t([[0, 60], [2, 60], [2, 60], [4, 60]])
t1 = t([[0, 60], [2, 60], [2, 60], [5, 60]])
t2 = t([[0, 60], [3, 60], [3, 60], [5, 60]])

# Equal size concatenation
self.assertEqual(
s1.concatenate_by_tag(s1, mutate=False),
si([tse([s(1), s(1), s(1), s(1)], tag="a")]),
si([tse([s(1), s(1), s(1), s(1)], tag="a", tempo_envelope=t0)]),
)

# Smaller self
s2.reverse() # verify order doesn't matter
self.assertEqual(
s1.concatenate_by_tag(s2, mutate=False),
si([tse([s(1), s(1), s(2), s(1)], tag="a"), tse([s(2), s(0.5)], tag="b")]),
si(
[
tse([s(1), s(1), s(2), s(1)], tag="a", tempo_envelope=t1),
# Tempo envelope is default, because no ancestor existed
# (so '_concatenate_tempo_envelope' wasn't called)
tse([s(2), s(0.5)], tag="b"),
]
),
)

# Smaller other
s2.reverse() # reverse to original order
self.assertEqual(
s2.concatenate_by_tag(s1, mutate=False),
si(
[tse([s(2), s(1), s(1), s(1)], tag="a"), tse([s(0.5), s(2.5)], tag="b")]
[
tse([s(2), s(1), s(1), s(1)], tag="a", tempo_envelope=t2),
# Tempo envelope is default, because no successor existed
# (so '_concatenate_tempo_envelope' wasn't called)
tse([s(0.5), s(2.5)], tag="b"),
]
),
)

Expand Down

0 comments on commit 881b7e9

Please sign in to comment.