summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-02-21 00:29:36 -0500
committerBen Connors <benconnors@outlook.com>2019-02-21 00:30:40 -0500
commit170b300f10133314cad0f34ba1087327df96a6c2 (patch)
treef57307394ace2c95b0111de638e0773388b1dd09
parentb03927227e20959dec9c1e486555ca616af01f5e (diff)
Fix BasicRenderer with Chasers; cleanup & comments
- BasicRenderer passing the function's state when rendering/advancing o Chasers with infinite-duration steps work now - Fix Chaser rendering returning incorrect nx on infinite steps - Add PreRenderable ABC o Defines render_all method for Shows and Chasers - General commenting
-rw-r--r--blc/render.py20
-rwxr-xr-xblc/workspace.py90
2 files changed, 81 insertions, 29 deletions
diff --git a/blc/render.py b/blc/render.py
index 592899d..f482a8e 100644
--- a/blc/render.py
+++ b/blc/render.py
@@ -34,10 +34,8 @@ class FunctionQueue:
def start(self):
"""Run until the queue is empty."""
while not self.queue.empty():
- print("outerloop")
entry = self.queue.get()
while True:
- print("innerloop")
ct = time.monotonic()
if ct > entry.time:
break
@@ -50,7 +48,6 @@ class FunctionQueue:
self.queue.put(nentry)
time.sleep(min(max(0, entry.time-ct), self.maxsleep))
entry.f()
- print("byeee")
def __init__(self, maxsleep=100):
self.queue = queue.PriorityQueue()
@@ -84,7 +81,7 @@ class BasicRenderer:
if self.nx is None:
self.nx = 0
self.fq.after(0, self.render_step)
- elif self.start_time is None:
+ if self.start_time is None:
self.start_time = time.monotonic()
self.lo.set_values(tuple(self.values.items()))
@@ -96,16 +93,19 @@ class BasicRenderer:
## Acquire the lock twice and block the process, we're stalled
self.stall_lock.acquire()
self.stall_lock.acquire()
+ self.stall_lock.release()
## Restart the rendering
- print("Restarting render")
self.fq.after(0, self.render_step)
+ self.nx = 1
with self.data_lock:
if self.start_time is not None:
- t = int((time.monotonic() - self.start_time)*1000 + 1) + self.nx
+ t = int(1000*(time.monotonic() - self.start_time)) + self.nx
else:
t = 0
- vals, acues, self.nx, self.data = self.f.render(t)
+ vals, acues, self.nx, self.data = self.f.render(t, data=self.data)
+ self.nx = max(self.minnx, self.nx)
+
for c, v in vals:
self.values[c] = v
for st, aid, fname, *_ in acues:
@@ -124,9 +124,9 @@ class BasicRenderer:
if self.start_time == -1:
raise ValueError("Cannot advance a function that has not been started!")
if issubclass(type(self.f), Advanceable):
- t = 1000*int(time.monotonic() - self.start_time)
+ t = int(1000*(time.monotonic() - self.start_time))
self.data = self.f.advance(t, self.data)
- *_, self.data = self.f.render(t)
+ *_, self.data = self.f.render(t, data=self.data)
## This will make the lock unlocked
self.stall_lock.acquire(blocking=False)
@@ -137,7 +137,7 @@ class BasicRenderer:
"""Return the current time in the function."""
return (time.monotonic() - self.start_time) if self.start_time is not None else -1
- def __init__(self, f, lo:LightingOutput, ap: AudioPlayer=DefaultAudioPlayer, minnx=-1):
+ def __init__(self, f, lo:LightingOutput, ap: AudioPlayer=DefaultAudioPlayer, minnx=10):
if f.type not in (SHOW, CHASER):
raise ValueError("Only Shows and Chasers may be used as toplevel functions")
self.start_time = None
diff --git a/blc/workspace.py b/blc/workspace.py
index e20575d..7a1e073 100755
--- a/blc/workspace.py
+++ b/blc/workspace.py
@@ -93,10 +93,6 @@ of single-shot chasers presently. Additionally, time is reset to 0 at the start
- This library is thread-safe except for the Function "data" objects: these objects may only be
used in one thread at a time.
-
-- The hash function on each class is likely to be slow: use it to prevent running an even slower
- operation if a function hasn't changed; a Function's hash will be consistent as long as the
- workspace on disk doesn't change
"""
from abc import ABC, abstractmethod, abstractstaticmethod
@@ -240,6 +236,13 @@ class Function(ABC):
This class itself must be stateless: anything that requires storage of state must also
require the caller to store that state.
+
+ For functions relating to the state (called 'data' where used), those functions always
+ return a state object: this may not be the same one that was passed in, so the caller must
+ always discard the old state and use the new, returned one.
+
+ As id is not necessarily unique, hash may be used to differentiate functions: this is
+ guaranteed to be globally unique and is calculated once on instantiation.
"""
repr_attr = ("id", "name",)
def __hash__(self):
@@ -247,7 +250,18 @@ class Function(ABC):
@staticmethod
def get_data():
- """Return an initial state for the function."""
+ """Return an initial state for the function.
+
+ This function may not be a static method. Always invoke on an instance.
+ """
+ return None
+
+ @staticmethod
+ def copy_data(data):
+ """Copy the given function state.
+
+ This function may not be a static method. Always invoke on an instance.
+ """
return None
def __repr__(self):
@@ -326,7 +340,25 @@ class Advanceable(ABC):
@abstractstaticmethod
def advance(t, data):
"""Advance the function."""
- return
+ return data
+
+class PreRenderable(ABC):
+ """Function that may be pre-rendered."""
+ @abstractmethod
+ def render_all(self, minnx=10):
+ """Render the entire function.
+
+ This may raise a ValueError if the Function is not actually PreRenderable; inheritance
+ from this class indicates that the Function may be prerendered.
+
+ This function returns a tuple containing tuples of the form:
+
+ ( (cues 1, audio cues 1), (cues 2, audio cues 2), ... )
+
+ Each segment of the render indicates a section with infinite length, i.e. the last
+ values in the section should be held until some condition is met (user input, etc.).
+ """
+ return ()
## END Base classes
@@ -364,7 +396,7 @@ class Scene(Function):
"""
def render(self, t, data=None):
"""All arguments are unused."""
- return self.values, (), QLC_INFTY, None
+ return self.values, (), QLC_INFTY, data
def __init__(self, id_, name, values, hidden=False):
super().__init__(id_, SCENE, name, (c for c,v in values), hidden=hidden, duration=-1, actual_duration=-1)
@@ -384,14 +416,21 @@ class ChaserStep(FadeFunction):
def get_data(self, start_time=0):
return self.ChaserStepData(fd=self.function.get_data(), start_time=start_time, end_time=self.duration)
+ @classmethod
+ def copy_data(cls, data):
+ return cls.ChaserStepData(fd=data.fd, start_time=data.start_time, end_time=data.end_time)
+
def render(self, t, data:ChaserStepData=None):
- ## The logic is different here: we never check the actual duration of this function and
- ## never return -1, the responsibility for determining if this step is over lies with
- ## the Chaser. The return value is also different: we return (vals, mul) instead of just
- ## vals. mul is the "multiplier" for the function, i.e. what we think that this function
- ## should be rendered at. If t > actual_duration, then mul will be 0 (this function is
- ## done), but we still need to return the values because the next step might be fading
- ## in and so will need to know the values of this function.
+ """DO NOT CALL OUTSIDE OF Chaser.
+
+ The logic is different here: we never check the actual duration of this function and
+ never return -1, the responsibility for determining if this step is over lies with the
+ Chaser. The return value is also different: we return (vals, mul) instead of just vals.
+ mul is the "multiplier" for the function, i.e. what we think that this function should
+ be rendered at. If t > actual_duration, then mul will be 0 (this function is done), but
+ we still need to return the values because the next step might be fading in and so will
+ need to know the values of this function.
+ """
if data is None:
data = self.get_data()
t -= data.start_time
@@ -407,7 +446,7 @@ class ChaserStep(FadeFunction):
if ft > 0:
mul = 1-min(1,ft/(self.fade_out))
nx = -1 if ft > self.fade_out else 1 ## Check if we're done
- else:
+ elif nx != QLC_INFTY:
nx = min(nx if nx != -1 else QLC_INFTY, -ft + 1)
elif t >= data.end_time:
mul = 0
@@ -435,7 +474,7 @@ class ChaserStep(FadeFunction):
self.function = function
self._hash = hash((self._hash, self.function, self.hold))
-class Chaser(Function, Advanceable):
+class Chaser(Function, Advanceable, PreRenderable):
"""Class for representing a QLC+ Chaser or Sequence.
Since they essentially do the same thing (Chaser being more general), they have only one
@@ -452,7 +491,7 @@ class Chaser(Function, Advanceable):
"""End the current chaser step.
After calling this function, the chaser must be rendered at a time at least t before
- calling it again.
+ calling advance again.
"""
if data.step_data:
data.step_data[-1][1].end_time = t - data.step_data[-1][1].start_time
@@ -462,6 +501,10 @@ class Chaser(Function, Advanceable):
def get_data(self):
return self.ChaserData(step_data=[], obey_loop=True)
+ def copy_data(self, data):
+ return self.ChaserData(step_data=[(a,self.steps[a].copy_data(b)) for a,b in data.step_data],
+ obey_loop=data.obey_loop)
+
def next_step(self, n) -> int: ## TODO: Implement other chaser types
"""Return the next step in the chaser."""
if self.run_order == LOOP:
@@ -635,6 +678,9 @@ class ShowTrack(Function):
def get_data(self):
return tuple((f.function.get_data() for f in self.functions))
+ def copy_data(self, data):
+ return tuple((f.copy_data(d) for f,d in zip(self.functions, data)))
+
def render(self, t, data=None):
if t > self.actual_duration:
return (), (), -1, data
@@ -678,14 +724,20 @@ class ShowTrack(Function):
super().__init__(id_, "ShowTrack", name, scope, duration=dur, actual_duration=adur)
self._hash = hash((self._hash, self.functions))
-class Show(Function):
+class Show(Function, PreRenderable):
"""Class representing a QLC+ show."""
+ def get_data(self):
+ return tuple((t.get_data() for t in self.tracks))
+
+ def copy_data(self, data):
+ return tuple((t.copy_data(d) for t,d in zip(self.tracks, data)))
+
def render(self, t, data=None):
if t > self.actual_duration:
return (), (), -1, data
if data is None:
- data = tuple((t.get_data() for t in self.tracks))
+ data = self.get_data()
values = {c: 0 for c in self.scope}
nx = QLC_INFTY