summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--blc2/constants.py4
-rw-r--r--blc2/functions/audio.py13
-rw-r--r--blc2/functions/chaser.py160
-rw-r--r--blc2/functions/chaserstep.py222
4 files changed, 395 insertions, 4 deletions
diff --git a/blc2/constants.py b/blc2/constants.py
index 792a877..ad6d174 100644
--- a/blc2/constants.py
+++ b/blc2/constants.py
@@ -67,3 +67,7 @@ EXTERNAL = "External"
INHERIT = "Inherit"
MANUAL = "Manual"
+
+LOOP = "Loop"
+RANDOM = "Random"
+ONESHOT = "Oneshot"
diff --git a/blc2/functions/audio.py b/blc2/functions/audio.py
index e41806e..0d59856 100644
--- a/blc2/functions/audio.py
+++ b/blc2/functions/audio.py
@@ -27,6 +27,7 @@ class Audio(Function):
super().__init__(w=w, id_=id_, name=name)
self._filename = filename
+ self._audio_id = self.id
if fade_in < 0 or fade_out < 0:
raise ValueError("Fades must be nonnegative")
@@ -34,10 +35,12 @@ class Audio(Function):
self._fade_in = fade_in
if filename is not None:
- self._duration = self.w.get_audio_length(filename)
+ self._actual_duration = self.w.get_audio_length(filename)
+ self._duration = max(0, self._actual_duration - self._fade_out)
self._audio_scope = frozenset(((filename,),))
else:
self._duration = 0
+ self._actual_duration = 0
self._audio_scope = frozenset()
@property
@@ -76,7 +79,7 @@ class Audio(Function):
@property
def actual_duration(self):
- return self._duration
+ return self._actual_duration
@property
def duration(self):
@@ -90,7 +93,8 @@ class Audio(Function):
library.
"""
if value != self._duration:
- self._duration = value
+ self._duration = max(value-self._fade_out, 0)
+ self._actual_duration = value
self.w.function_changed(self)
def get_data(self):
@@ -101,7 +105,7 @@ class Audio(Function):
def render(self, t, data = None):
return ((),
- ((self.id, self._filename, 0, self.fade_in, self.duration, self.fade_out),),
+ ((self._audio_id, self._filename, 0, self.fade_in, max(0, self.duration-self.fade_out), self.fade_out),),
None)
@property
@@ -120,6 +124,7 @@ class Audio(Function):
self.audio_scope = frozenset()
self._filename = value
+ self._audio_id = hash((self._audio_id, self._filename))
self.w.function_changed(self)
def serialize(self) -> et.Element:
diff --git a/blc2/functions/chaser.py b/blc2/functions/chaser.py
new file mode 100644
index 0000000..d17b8f4
--- /dev/null
+++ b/blc2/functions/chaser.py
@@ -0,0 +1,160 @@
+"""Module for basic chasers."""
+
+import random
+
+from ..constants import CHASER, INFTY, ONESHOT, LOOP, RANDOM
+from .function import Function
+from .chaserstep import ChaserStep
+
+
+class Chaser(Function):
+ """Class for chasers."""
+ type = CHASER
+
+ def __init__(self, w, id_ = None, name = None, advance_mode = ONESHOT):
+ super().__init__(w=w, id_=id_, name=name)
+
+ self._steps = []
+ self._duration = 0
+ self._actual_duration = 0
+ self._advance_mode = advance_mode
+ self._scope = frozenset()
+ self._audio_scope = frozenset()
+
+ class ChaserData:
+ """Data for a Chaser render."""
+ def __init__(self, chaser, steps=()):
+ self._chaser = chaser
+ self.steps = list(steps)
+ self.audio_id = 0
+
+ @property
+ def fade_in(self):
+ return 0
+
+ @property
+ def fade_out(self):
+ return 0
+
+ def get_data(self):
+ return self.ChaserData(self)
+
+ def copy_data(self, data):
+ return self.ChaserData(self, steps=[i.copy() for i in data.steps])
+
+ @property
+ def scope(self):
+ return self._scope
+
+ @property
+ def audio_scope(self):
+ return self._audio_scope
+
+ def _recalculate(self, update=True):
+ if self._advance_mode != ONESHOT:
+ self._duration = INFTY
+ self._actual_duration = INFTY
+ else:
+ self._duration = sum((s.duration for s in self.steps))
+ self._actual_duration = self._duration
+ if self._steps:
+ self._actual_duration += self.steps[-1].actual_duration - self.steps[-1].duration
+
+ scope = set()
+ ascope = set()
+ for s in self._steps:
+ scope.update(s.scope)
+ ascope.update(s.audio_scope)
+ self._scope = frozenset(scope)
+ self._audio_scope = frozenset(ascope)
+
+ if update:
+ self.w.function_changed(self)
+
+ @property
+ def actual_duration(self):
+ return self._actual_duration
+
+ @property
+ def duration(self):
+ return self._duration
+
+ @property
+ def steps(self):
+ """Return the steps in the chaser."""
+ return tuple(self._steps)
+
+ def _function_changed(self, _):
+ self._recalculate()
+
+ @property
+ def first_step(self):
+ """Return the first step number of the chaser."""
+ if self._advance_mode == RANDOM:
+ return random.randint(0, len(self.steps)-1)
+ return 0
+
+ def next_steps(self, data):
+ """Iterate over the next steps in the chaser."""
+ n = data.steps[-1].index
+ if self._advance_mode == RANDOM:
+ while True:
+ yield random.randint(0, len(self.steps)-1)
+ elif self._advance_mode == LOOP:
+ while True:
+ n = (n + 1) % len(self.steps)
+ yield n
+ else:
+ return range(n, len(self.steps))
+
+ def render(self, t, data=None):
+ if data is None:
+ data = self.get_data()
+ if not self._steps:
+ return (), (), data
+ elif not data.steps:
+ 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
+ data.steps.append(sd)
+
+ ## 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:
+ for n in self.next_steps(data):
+ 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
+ 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]
+ if not data.steps:
+ return (), (), data
+
+ ## Render
+ lc = {c: 0 for c in self._scope}
+ ac = []
+ for n, d in enumerate(data.steps):
+ s = d.step
+ slc, sac, data.steps[n] = s.render(t, data=data.steps[n])
+ lc.update(slc)
+ ac.extend(sac)
+
+ return tuple(lc.items()), ac, data
+
+ def serialize(self):
+ ## TODO: Implement this
+ raise NotImplementedError("Not done yet")
+
+ @classmethod
+ def deserialize(cls, w, e):
+ ## TODO: Implement this
+ raise NotImplementedError("Not done yet")
diff --git a/blc2/functions/chaserstep.py b/blc2/functions/chaserstep.py
new file mode 100644
index 0000000..9884cc6
--- /dev/null
+++ b/blc2/functions/chaserstep.py
@@ -0,0 +1,222 @@
+"""Module for chaser steps."""
+
+from ..constants import CHASERSTEP, EXTERNAL, INHERIT, MANUAL, INFTY
+from .function import Function
+
+class ChaserStep(Function):
+ """Class representing a single chaser step."""
+ type: CHASERSTEP
+
+ def __init__(self, w: "Workspace", 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)
+ self._function = None
+ self._duration_mode = duration_mode
+ self._duration = 0
+ self._actual_duration = 0
+ self._fade_out = fade_out
+ self._fade_in = fade_in
+ self._fade_out_mode = EXTERNAL
+ self._index = index
+ self._set_function(function, update=False)
+
+ class ChaserStepData:
+ """Data for a ChaserStep.
+
+ This data is transparent: the interface can be expected not to change
+ significantly. This is to provide information about the state of the chaser to
+ managing programs.
+
+ Note that it is an error to modify any attribute of this class: this may mess up
+ the render call and is not supported in any way. Also, do not call any functions
+ on this class, regardless of visibility.
+
+ ``end_time`` is relative to t=0, not the start time of this specific step.
+ """
+ @property
+ def actual_duration(self):
+ """Return the step's actual duration."""
+ return min(self.end_time+self.step.fade_out, self.step.actual_duration)
+
+ def copy(self):
+ """Duplicate the data."""
+ return self.__class__(self.start_time, self.end_time, self.step, self.index,
+ self.data, self.audio_id)
+
+ def __init__(self, start_time, end_time, step, index, data, audio_id):
+ self.start_time = start_time
+ self.end_time = end_time
+ self.step = step
+ self.index = index
+ self.data = data
+ self.audio_id = audio_id
+
+ @property
+ def index(self):
+ """Return this step's index in the chaser."""
+ return self._index
+
+ def _set_index(self, value):
+ """Change this step's index.
+
+ This may only be called from within Chaser.
+ """
+ self._index = value
+ self.w.function_changed(self)
+
+ @property
+ def fade_out_mode(self):
+ """Return the fade out mode."""
+ return self._fade_out_mode
+
+ def _recalculate_duration(self, update=True):
+ if self._duration_mode == INHERIT:
+ ## Load the duration from the function, if any
+ if self._function is None:
+ self._actual_duration = 0
+ self._duration = 0
+ self._fade_out_mode = EXTERNAL
+ else:
+ self._actual_duration = self._function.actual_duration
+ self._duration = self._function.duration
+ if self._function.fade_out_mode == EXTERNAL:
+ self.actual_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._fade_out_mode = EXTERNAL
+ if update:
+ self.w.function_changed(self)
+
+ @property
+ def fade_in(self):
+ return self._fade_in
+
+ @fade_in.setter
+ def fade_in(self, v):
+ if v < 0:
+ raise ValueError("Fades must be nonnegative")
+ self._fade_in = v
+ self._recalculate_duration()
+
+ @property
+ def fade_out(self):
+ return self._fade_out
+
+ @fade_out.setter
+ def fade_out(self, v):
+ if v < 0:
+ raise ValueError("Fades must be nonnegative")
+ self._fade_out = v
+ self._recalculate_duration()
+
+ @property
+ def duration_mode(self):
+ """Return the step's duration mode.
+
+ If the mode is ``INHERIT``, the duration is computed from the duration of the
+ associated function and the fades. If the mode is ``MANUAL``, the duration is
+ computed from the manually set duration and the fades.
+
+ Note that ``fade_out_mode`` is inherited from the function when the mode is
+ ``INHERIT`` and is always ``EXTERNAL`` when manual.
+ """
+ return self._duration_mode
+
+ @duration_mode.setter
+ def duration_mode(self, v):
+ if self._duration_mode != v:
+ self._duration_mode = v
+ self._recalculate_duration()
+
+ def _function_changed(self, _):
+ self._recalculate_duration()
+
+ def _function_deleted(self, _):
+ self._function = None
+ self._recalculate_duration()
+
+ @property
+ def function(self):
+ """Return the step's function."""
+ return self._function
+
+ def _set_function(self, v, update=True):
+ if v != self._function:
+ if self._function is not None: ## Clear old callbacks
+ self.w.delete_callbacks(self, self._function.id)
+ self._function = v
+ 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._recalculate_duration(update=update)
+
+ @function.setter
+ def function(self, v):
+ self._set_function(v=v, update=True)
+
+ @property
+ def duration(self):
+ return self._duration
+
+ @property
+ def actual_duration(self):
+ return self._actual_duration
+
+ def get_data(self):
+ raise TypeError("Cannot retrieve data of a ChaserStep directly")
+
+ 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)
+ return data
+
+ def copy_data(self, data):
+ return data.copy()
+
+ @property
+ def audio_scope(self):
+ if self._function is not None:
+ return self._function.audio_scope
+ return frozenset()
+
+ @property
+ def scope(self):
+ if self._function is not None:
+ return self._function.scope
+ return frozenset()
+
+ def render(self, t, data=None):
+ if self._function is None:
+ return (), (), data
+ if data is None:
+ raise ValueError("Data cannot be None for ChaserStep")
+
+ 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
+
+ ## 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))
+
+ return lc, ac, data
+
+ def serialize(self):
+ ## TODO: Implement this
+ raise NotImplementedError("Not done yet")
+
+ @classmethod
+ def deserialize(cls, w, e):
+ ## TODO: Implement this
+ raise NotImplementedError("Not done yet")