summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-09-26 21:42:14 -0400
committerBen Connors <benconnors@outlook.com>2019-09-26 21:42:14 -0400
commitdfe20c0430c7d58b57c44026102cf8b3c52ac1b3 (patch)
tree1c0ad08b16c0183b4bb3c7ebff358950e3971534
parentcefc580a2f38f14c0245c9d6a5acbaa67feaf8d4 (diff)
Lots of stuff
- Add tests for chaser steps - Finish preliminary implementation of chasers - Implement (de)serialization on chasers and steps - Various bugfixes from testing
-rw-r--r--blc2/functions/chaser.py148
-rw-r--r--blc2/functions/chaserstep.py112
-rw-r--r--blc2/functions/function.py12
-rw-r--r--blc2/functions/scene.py2
-rw-r--r--blc2/workspace.py37
-rw-r--r--examples/workspace.xml4
-rw-r--r--tests/test_functions_chaserstep.py124
7 files changed, 391 insertions, 48 deletions
diff --git a/blc2/functions/chaser.py b/blc2/functions/chaser.py
index d17b8f4..a6699f8 100644
--- a/blc2/functions/chaser.py
+++ b/blc2/functions/chaser.py
@@ -1,11 +1,11 @@
"""Module for basic chasers."""
import random
+import xml.etree.ElementTree as et
-from ..constants import CHASER, INFTY, ONESHOT, LOOP, RANDOM
+from ..constants import CHASER, INFTY, ONESHOT, LOOP, RANDOM, BXW
from .function import Function
-from .chaserstep import ChaserStep
-
+from ..exceptions import LoadError
class Chaser(Function):
"""Class for chasers."""
@@ -29,6 +29,17 @@ class Chaser(Function):
self.audio_id = 0
@property
+ def advance_mode(self):
+ """Return the function's current advance mode."""
+ return self._advance_mode
+
+ @advance_mode.setter
+ def advance_mode(self, v):
+ if v != self._advance_mode:
+ self._advance_mode = v
+ self._recalculate()
+
+ @property
def fade_in(self):
return 0
@@ -36,8 +47,12 @@ class Chaser(Function):
def fade_out(self):
return 0
- def get_data(self):
- return self.ChaserData(self)
+ def get_data(self, start_at=None): #pylint: disable=arguments-differ
+ data = self.ChaserData(self)
+ if start_at is not None:
+ data = self.advance(0, data, n=start_at)
+
+ return data
def copy_data(self, data):
return self.ChaserData(self, steps=[i.copy() for i in data.steps])
@@ -107,6 +122,77 @@ class Chaser(Function):
else:
return range(n, len(self.steps))
+ def _fix_indices(self):
+ for i, s in enumerate(self._steps):
+ if s.index != i:
+ s.index._set_index(i) #pylint: disable=protected-access
+
+ def register_step(self, step):
+ """Register a new step."""
+ if step.index == -1:
+ step._index = len(self._steps) #pylint: disable=protected-access
+ elif step.index is None:
+ ## Add it to the end
+ step._index = len(self._steps) #pylint: disable=protected-access
+ elif step.index > len(self._steps):
+ step._index = len(self._steps) #pylint: disable=protected-access
+ self._steps.insert(step.index, step)
+ self.w.register_function_delete_callback(step, self._step_deleted, self)
+ self._fix_indices()
+
+ def move_step(self, step, position):
+ """Move a step around."""
+ if isinstance(step, int):
+ step = self._steps[step]
+ elif step not in self._steps:
+ raise ValueError("No such step")
+
+ if position == -1 or position >= len(self._steps):
+ position = len(self._steps)-1
+
+ if position < 0:
+ raise ValueError("Step index must be nonnegative")
+ elif step.index == position: ## Pointless change
+ return
+
+ self._steps.pop(step.index)
+ self._steps.insert(position, step)
+ self._fix_indices()
+
+ def delete_step(self, step):
+ """Delete a step."""
+ if isinstance(step, int):
+ step = self._steps[step]
+ elif step not in self._steps:
+ raise ValueError("No such step")
+
+ step.delete()
+
+ def advance(self, t, data, n=None):
+ """Advance the chaser at the given time.
+
+ If ``n`` is ``None``, the chaser is advanced one step. Otherwise, it is advanced to
+ the given position.
+ """
+ if n is None:
+ for i in self.next_steps(data):
+ n = i
+ break
+ else:
+ raise ValueError("Chaser is finished")
+ if data.steps:
+ data.steps[-1].end_time = t
+ data.steps.append(self.steps[n]._get_data(t, n))
+ data.audio_id += 1
+
+ return data
+
+ def _step_deleted(self, step):
+ self._steps.pop(step.index)
+ ## Nullify the step's index so we'll catch it when rendering
+ step._index = -1 #pylint: disable=protected-access
+ self._fix_indices()
+
def render(self, t, data=None):
if data is None:
data = self.get_data()
@@ -116,9 +202,17 @@ class Chaser(Function):
if data.audio_id != 0:
raise ValueError("Audio ID must be zero")
n = self.first_step
- sd = self.steps[n]._get_data(0, n) #pylint: disable=protected-access
+ sd = self.steps[n]._get_data(0, 0) #pylint: disable=protected-access
+ data.audio_id += 1
data.steps.append(sd)
+ if data.steps[-1].step.index == -1:
+ ## Have to do some surgery, the last step was deleted
+ ## Just use the closest one, don't worry too much
+ n = max(len(self.steps)-1, data.steps[-1].index)
+ data.steps.append(self.steps[n]._get_data(t, data.audio_id)) #pylint: disable=protected-access
+ data.audio_id += 1
+
## Make sure we have all the steps we need
st = data.steps[-1].duration + data.steps[-1].start_time
if st < INFTY and st <= t:
@@ -126,16 +220,16 @@ class Chaser(Function):
s = self.steps[n]
st += s.duration
et += s.actual_duration
- data.audio_id += 1
if et >= t:
## We need this one
- data.steps.append(self.steps[n]._get_data(0, n)) #pylint: disable=protected-access
+ data.steps.append(self.steps[n]._get_data(0, data.audio_id)) #pylint: disable=protected-access
+ data.audio_id += 1
if st > t:
## We're done
break
## Clean up the old steps
- data.steps = [i for i in data.steps if i.start_time+i.actual_duration >= t]
+ data.steps = [i for i in data.steps if i.start_time+i.actual_duration >= t and i.step.index != -1]
if not data.steps:
return (), (), data
@@ -151,10 +245,38 @@ class Chaser(Function):
return tuple(lc.items()), ac, data
def serialize(self):
- ## TODO: Implement this
- raise NotImplementedError("Not done yet")
+ e = et.Element(BXW+"function")
+ e.set("type", self.type)
+ e.set("id", str(self.id))
+ e.set("name", self.name)
+ e.set("advance-mode", self.advance_mode)
+ for n, s in enumerate(self.steps):
+ se = s.serialize()
+ e.insert(n, se)
+
+ return e
@classmethod
def deserialize(cls, w, e):
- ## TODO: Implement this
- raise NotImplementedError("Not done yet")
+ from .chaserstep import ChaserStep
+
+ if e.tag != BXW+"function":
+ raise LoadError("Invalid function tag")
+ elif e.get("type") != CHASER:
+ raise LoadError("Load delegated to wrong class (this is a bug)")
+
+ id_ = cls.int_or_none(e.get("id"))
+ if id_ is None:
+ raise LoadError("Function tag has invalid/missing ID")
+
+ name = e.get("name")
+ advance_mode = e.get("advance-mode")
+ if advance_mode not in (LOOP, RANDOM, ONESHOT):
+ raise ValueError("Invalid advance mode")
+
+ chaser = cls(w=w, id_=id_, name=name, advance_mode=advance_mode)
+
+ for step in e:
+ ChaserStep.deserialize(w, step, chaser)
+
+ return chaser
diff --git a/blc2/functions/chaserstep.py b/blc2/functions/chaserstep.py
index 9884cc6..005f76e 100644
--- a/blc2/functions/chaserstep.py
+++ b/blc2/functions/chaserstep.py
@@ -1,19 +1,25 @@
"""Module for chaser steps."""
-from ..constants import CHASERSTEP, EXTERNAL, INHERIT, MANUAL, INFTY
+import xml.etree.ElementTree as et
+
+from ..constants import CHASERSTEP, EXTERNAL, INHERIT, INFTY, MANUAL, BXW
from .function import Function
+from ..exceptions import LoadError
class ChaserStep(Function):
"""Class representing a single chaser step."""
- type: CHASERSTEP
+ type = CHASERSTEP
- def __init__(self, w: "Workspace", id_: int = None, name: str = None,
+ def __init__(self, chaser: "Chaser", id_: int = None, name: str = None,
fade_in = 0, fade_out = 0, function: Function = None, index: int = None,
- duration_mode=MANUAL):
- super().__init__(w=w, id_=id_, name=name)
+ duration_mode = INHERIT, duration = 0):
+ if index is not None and index < -2:
+ raise ValueError("Step index must be nonnegative")
+ self.chaser = chaser
+ super().__init__(w=chaser.w, id_=id_, name=name)
self._function = None
self._duration_mode = duration_mode
- self._duration = 0
+ self._duration = duration
self._actual_duration = 0
self._fade_out = fade_out
self._fade_in = fade_in
@@ -21,6 +27,12 @@ class ChaserStep(Function):
self._index = index
self._set_function(function, update=False)
+ try:
+ self.chaser.register_step(self)
+ except Exception as e:
+ self.w.function_deleted(self)
+ raise e
+
class ChaserStepData:
"""Data for a ChaserStep.
@@ -42,21 +54,26 @@ class ChaserStep(Function):
def copy(self):
"""Duplicate the data."""
return self.__class__(self.start_time, self.end_time, self.step, self.index,
- self.data, self.audio_id)
+ self.data, self.audio_id, self.function)
- def __init__(self, start_time, end_time, step, index, data, audio_id):
+ def __init__(self, start_time, end_time, step, index, data, audio_id, function):
self.start_time = start_time
self.end_time = end_time
self.step = step
self.index = index
self.data = data
self.audio_id = audio_id
+ self.function = function
@property
def index(self):
"""Return this step's index in the chaser."""
return self._index
+ @index.setter
+ def index(self, value):
+ self.chaser.move_step(self, value)
+
def _set_index(self, value):
"""Change this step's index.
@@ -79,13 +96,15 @@ class ChaserStep(Function):
self._fade_out_mode = EXTERNAL
else:
self._actual_duration = self._function.actual_duration
- self._duration = self._function.duration
+ self._duration = self._function.actual_duration
if self._function.fade_out_mode == EXTERNAL:
- self.actual_duration += self._fade_out
+ self._actual_duration += self._fade_out
+ else:
+ self._duration = max(0, self._duration - self.fade_out)
self._fade_out_mode = self._function.fade_out_mode
else:
## Manual duration
- self.actual_duration = self.duration + self._fade_out
+ self._actual_duration = self.duration + self._fade_out
self._fade_out_mode = EXTERNAL
if update:
self.w.function_changed(self)
@@ -144,6 +163,8 @@ class ChaserStep(Function):
return self._function
def _set_function(self, v, update=True):
+ if v is not None and v.type == CHASERSTEP:
+ raise ValueError("ChaserStep cannot be used as a ChaserStep's function")
if v != self._function:
if self._function is not None: ## Clear old callbacks
self.w.delete_callbacks(self, self._function.id)
@@ -151,7 +172,7 @@ class ChaserStep(Function):
if v is not None:
self.w.register_function_change_callback(v.id, self._function_changed, self)
self.w.register_function_delete_callback(v.id, self._function_deleted, self)
- self.fade_out_mode = v.fade_out_mode
+ self._fade_out_mode = v.fade_out_mode
self._recalculate_duration(update=update)
@function.setter
@@ -162,6 +183,15 @@ class ChaserStep(Function):
def duration(self):
return self._duration
+ @duration.setter
+ def duration(self, value):
+ if self._duration_mode != MANUAL:
+ raise AttributeError("Can't set duration in inherit mode")
+ elif value < 0:
+ raise ValueError("Duration must be nonnegative")
+ self._duration = value
+ self._recalculate_duration()
+
@property
def actual_duration(self):
return self._actual_duration
@@ -171,7 +201,7 @@ class ChaserStep(Function):
def _get_data(self, start_time, audio_id):
fd = None if self._function is None else self._function.get_data()
- data = self.ChaserStepData(start_time, INFTY, self, self._index, fd, audio_id)
+ data = self.ChaserStepData(start_time, INFTY, self, self._index, fd, audio_id, self._function)
return data
def copy_data(self, data):
@@ -190,33 +220,71 @@ class ChaserStep(Function):
return frozenset()
def render(self, t, data=None):
+ if data.function != self._function:
+ data.function = self._function
+ data.data = self._function.get_data() if self._function is not None else None
if self._function is None:
return (), (), data
if data is None:
raise ValueError("Data cannot be None for ChaserStep")
+ if data.index != self._index:
+ data.index = self._index
+
+ if t > min(self._actual_duration, data.end_time+self._fade_out):
+ return (), (), data
+
fade_start = min(data.end_time, self._duration)
t -= data.start_time
## Compute lighting multiplier
mul = 1
if t < self.fade_in:
- mul *= t/self.fade_in
- if t > fade_start and t <= data.actual_duration:
- mul *= 1 - (t-fade_start)/self.fade_out
+ mul *= min(1, t/self.fade_in)
+ if fade_start < t <= data.actual_duration:
+ mul *= 1 - min(1, (t-fade_start)/self.fade_out)
## Render and fade cues
lc, ac, data.data = self._function.render(t, data=data.data)
lc = tuple(((c, int(v*mul)) for c, v in lc))
- ac = tuple(((hash((data.audio_id, aid)), fname, max(self.fade_out, fout), min(fade_start, fstart), min(fout, self.fade_out)) for aid, fname, fin, fstart, fout in ac))
+ ac = tuple(((hash((data.audio_id, aid)), fname, st+data.start_time, max(self.fade_in, fin), min(fade_start, fstart), max(fout, self.fade_out)) for aid, fname, st, fin, fstart, fout in ac))
return lc, ac, data
def serialize(self):
- ## TODO: Implement this
- raise NotImplementedError("Not done yet")
+ e = et.Element(BXW+"step")
+ e.set("id", str(self.id))
+ e.set("name", self.name)
+ e.set("fade-in", str(self.fade_in))
+ e.set("fade-out", str(self.fade_out))
+ e.set("duration-mode", self.duration_mode)
+ if self.duration_mode == MANUAL:
+ e.set("duration", str(self.duration))
+ if self._function is not None:
+ e.set("function", str(self._function.id))
+
+ return e
@classmethod
- def deserialize(cls, w, e):
- ## TODO: Implement this
- raise NotImplementedError("Not done yet")
+ def deserialize(cls, w, e, c): #pylint: disable=arguments-differ
+ if e.tag != BXW+"step":
+ raise LoadError("Invalid chaser step tag")
+
+ id_ = cls.int_or_none(e.get("id"))
+ if id_ is None:
+ raise LoadError("Step data has invalid/missing ID")
+
+ name = e.get("name")
+ fade_in = e.get("fade-in")
+ fade_in = int(fade_in) if fade_in is not None else 0
+ fade_out = e.get("fade-out")
+ fade_out = int(fade_out) if fade_out is not None else 0
+ duration_mode = e.get("duration-mode")
+ if duration_mode not in (MANUAL, INHERIT):
+ raise LoadError("Invalid duration mode")
+ duration = int(e.get("duration")) if duration_mode == MANUAL else 0
+ function = int(e.get("function"))
+
+ return cls(c, id_=id_, name=name, fade_in=fade_in, fade_out=fade_out,
+ function=w.functions[function], duration_mode=duration_mode,
+ duration=duration)
diff --git a/blc2/functions/function.py b/blc2/functions/function.py
index 79d3b2b..21632a7 100644
--- a/blc2/functions/function.py
+++ b/blc2/functions/function.py
@@ -7,7 +7,7 @@ from abc import ABCMeta, abstractmethod, abstractproperty
from typing import Set, Any
from ..topology import Fixture
-from ..constants import INFTY, EXTERNAL, FUNCTION
+from ..constants import EXTERNAL, FUNCTION
from ..interfaces import XMLSerializable
class Function(XMLSerializable, metaclass=ABCMeta):
@@ -26,14 +26,14 @@ class Function(XMLSerializable, metaclass=ABCMeta):
It is an error to change Function.type or Function.fade_out_mode in user code. These
values are public for informational purposes.
"""
- type: FUNCTION
- fade_out_mode: EXTERNAL
+ type = FUNCTION
+ fade_out_mode = EXTERNAL
def __init__(self, w: "Workspace", id_: int = None, name: str = None):
self.w = w
self._id = id_ if id_ is not None else w.next_function_id
self._name = name if name else "%s %s" % (self.type, self.id)
-
+
self.w.register_function(self)
@property
@@ -112,6 +112,10 @@ class Function(XMLSerializable, metaclass=ABCMeta):
lights, sound, data = f.render(t, data)
+ Once a specific ``data`` instance has been used to render at a time, it must not be
+ used to render at a previous time: this is undefined behaviour and will break at
+ least Chasers.
+
:param t: the time to render at, in milliseconds
:param data: the function data to use
"""
diff --git a/blc2/functions/scene.py b/blc2/functions/scene.py
index c786287..239df3c 100644
--- a/blc2/functions/scene.py
+++ b/blc2/functions/scene.py
@@ -126,7 +126,7 @@ class Scene(Function):
e.set("type", self.type)
e.set("id", str(self.id))
e.set("name", self.name)
- for c, v in self.values:
+ for c, v in self.values.items():
ce = et.SubElement(e, BXW+"value")
ce.set("fixture", str(c.f.id))
ce.set("channel", str(c.id))
diff --git a/blc2/workspace.py b/blc2/workspace.py
index 55757d3..83dba5b 100644
--- a/blc2/workspace.py
+++ b/blc2/workspace.py
@@ -8,7 +8,7 @@ import json
import subprocess as subp
import xml.etree.ElementTree as et
-from .constants import AUDIO, SCENE, BXW
+from .constants import AUDIO, SCENE, BXW, CHASER, CHASERSTEP
from .functions.function import Function
from .exceptions import LoadError
from .interfaces import XMLSerializable
@@ -177,7 +177,7 @@ class Workspace(XMLSerializable):
def topology_changed(self):
"""Call when a topology change has been made."""
- for f in self._topology_callbacks.values():
+ for _, f in self._topology_callbacks.values():
f(self)
def function_changed(self, f: Function):
@@ -187,7 +187,7 @@ class Workspace(XMLSerializable):
"""
if f.id not in self._change_callbacks:
return
- for callback in self._change_callbacks[f.id]:
+ for _, callback in self._change_callbacks[f.id]:
callback(f)
def function_deleted(self, f: Function):
@@ -197,7 +197,7 @@ class Workspace(XMLSerializable):
"""
if f.id not in self._change_callbacks:
return
- for callback in self._delete_callbacks[f.id]:
+ for _, callback in self._delete_callbacks[f.id]:
callback(f)
self.delete_callbacks(f)
@@ -217,6 +217,7 @@ class Workspace(XMLSerializable):
from .topology import Fixture
from .functions.scene import Scene
from .functions.audio import Audio
+ from .functions.chaser import Chaser
if e.tag != BXW+"workspace":
raise LoadError("Root tag must be workspace")
@@ -239,12 +240,15 @@ class Workspace(XMLSerializable):
Fixture.deserialize(w, fixture)
## Finally, load the functions
+ ## TODO: Find a working order before trying to load
for function in functions:
type_ = function.get("type")
if type_ == AUDIO:
Audio.deserialize(w, function)
elif type_ == SCENE:
Scene.deserialize(w, function)
+ elif type_ == CHASER:
+ Chaser.deserialize(w, function)
else:
raise LoadError("Unknown function type \"%s\"" % type_)
@@ -264,13 +268,34 @@ class Workspace(XMLSerializable):
fixtures.insert(n, fe)
functions = et.SubElement(root, BXW+"functions")
- for n, function in enumerate(self.functions.values()):
+ f_order = []
+ done = set()
+ all_f = list(self.functions.values())
+ while all_f:
+ f = all_f.pop(0)
+ if f.type == CHASERSTEP:
+ continue
+ elif f.type in (AUDIO, SCENE):
+ f_order.append(f)
+ done.add(f.id)
+ elif f.type == CHASER:
+ for step in f.steps:
+ if step.function is not None and step.function.id not in done:
+ break
+ else:
+ f_order.append(f)
+ done.add(f.id)
+ continue
+ all_f.append(f)
+ else:
+ raise ValueError("Unknown function type "+f.type)
+
+ for n, function in enumerate(f_order):
fe = function.serialize()
functions.insert(n, fe)
return root
-
def save(self, filename):
"""Save the workspace to a file."""
et.register_namespace("", BXW.strip("{}"))
diff --git a/examples/workspace.xml b/examples/workspace.xml
index a2be9e0..c0161fb 100644
--- a/examples/workspace.xml
+++ b/examples/workspace.xml
@@ -35,8 +35,8 @@
<filename>test.wav</filename>
</function>
- <function type="Chaser" id="2" name="Chaser 1">
- <step id="0" name="Step 1" fade-in="0" fade-out="0" duration-mode="Manual" duration="123" function="0"/>
+ <function type="Chaser" id="2" name="Chaser 1" advance-mode="Loop">
+ <step id="3" name="Step 1" fade-in="0" fade-out="0" duration-mode="Manual" duration="123" function="0"/>
</function>
</functions>
</workspace>
diff --git a/tests/test_functions_chaserstep.py b/tests/test_functions_chaserstep.py
new file mode 100644
index 0000000..134b893
--- /dev/null
+++ b/tests/test_functions_chaserstep.py
@@ -0,0 +1,124 @@
+import datetime as dt
+
+import pytest
+
+from blc2.functions.chaserstep import ChaserStep
+from blc2.topology import Fixture
+from blc2.functions.scene import Scene
+from blc2.functions.audio import Audio
+from blc2.workspace import Workspace
+from blc2.constants import INHERIT, MANUAL, EXTERNAL, INTERNAL, INFTY
+
+class DummyChaser:
+ def register_step(self, *args, **kwargs):
+ return
+
+ def __init__(self, w):
+ self.w = w
+
+@pytest.fixture
+def cw():
+ w = Workspace("", "", "", dt.datetime.now())
+ f = Fixture(w=w, id_=0, channel_count=1)
+ c0, = f.channels
+ f2 = Fixture(w=w, id_=1, channel_count=4)
+ Scene(w=w, id_=0, values={c0: 255})
+ Scene(w=w, id_=1, values={c: 255-i for i, c in enumerate(f2.channels)})
+ Audio(w=w, id_=2, filename="tests/silence.m4a")
+
+ return w
+
+def test_chaserstep(cw):
+ c = DummyChaser(cw)
+ s0 = cw.functions[0]
+ s1 = cw.functions[1]
+ a = cw.functions[2]
+ c0, = cw.fixtures[0].channels
+
+ ## Test how it handles inherit
+ cs1 = ChaserStep(c, function=s0, duration_mode=INHERIT)
+ assert cs1.duration == INFTY
+ assert cs1.actual_duration == INFTY
+ assert cs1.fade_out_mode == s0.fade_out_mode
+ assert cs1.scope == s0.scope
+ assert cs1.audio_scope == s0.audio_scope
+
+ cs1.fade_in = 1000
+ cs1.fade_out = 1000
+ assert cs1.duration == INFTY
+ assert cs1.actual_duration == INFTY
+
+ data = cs1._get_data(0, 0)
+ lc, ac, data = cs1.render(500, data)
+ assert not ac
+ assert lc == ((c0, 127),)
+
+ lc, ac, data = cs1.render(1000, data)
+ assert not ac
+ assert lc == ((c0, 255),)
+
+ data.end_time = 1500
+ lc, ac, data = cs1.render(2000, data)
+ assert not ac
+ assert lc == ((c0, 127),)
+
+ lc, ac, data = cs1.render(2501, data)
+ assert not ac
+ assert not lc
+
+ ## Test how it handles manual mode
+ cs1.duration_mode = MANUAL
+ cs1.duration = 1500
+ assert cs1.duration == 1500
+ assert cs1.actual_duration == 2500
+
+ data = cs1._get_data(0, 0)
+ lc, ac, data = cs1.render(500, data)
+ assert not ac
+ assert lc == ((c0, 127),)
+
+ lc, ac, data = cs1.render(1000, data)
+ assert not ac
+ assert lc == ((c0, 255),)
+
+ lc, ac, data = cs1.render(2000, data)
+ assert not ac
+ assert lc == ((c0, 127),)
+
+ lc, ac, data = cs1.render(2501, data)
+ assert not ac
+ assert not lc
+
+ ## Test how it handles inherit and a function change
+ cs1.duration_mode = INHERIT
+ assert cs1.duration == INFTY
+ assert cs1.actual_duration == INFTY
+
+ data = cs1._get_data(0, 0)
+
+ cs1.fade_out = 0
+ cs1.fade_in = 0
+ cs1.function = a
+ if a.actual_duration != 3024:
+ raise ValueError("silence.m4a is wrong duration, fix the tests")
+ assert cs1.audio_scope == a.audio_scope
+ assert cs1.scope == a.scope
+ assert cs1.duration == a.actual_duration
+ assert cs1.actual_duration == a.actual_duration
+ cs1.fade_out = 1000
+ cs1.fade_in = 1000
+ assert cs1.duration == a.actual_duration-1000
+
+ lc, ac, data = cs1.render(500, data)
+ assert not lc
+ assert len(ac) == 1
+ assert ac[0][1:] == ("tests/silence.m4a", 0, 1000, 2024, 1000)
+ a.fade_out = 2000
+ lc, ac, data = cs1.render(501, data)
+ assert not lc
+ assert len(ac) == 1
+ assert ac[0][1:] == ("tests/silence.m4a", 0, 1000, 1024, 2000)
+
+ lc, ac, data = cs1.render(3025, data)
+ assert not lc
+ assert not ac