diff options
author | Ben Connors <benconnors@outlook.com> | 2019-02-21 00:29:36 -0500 |
---|---|---|
committer | Ben Connors <benconnors@outlook.com> | 2019-02-21 00:30:40 -0500 |
commit | 170b300f10133314cad0f34ba1087327df96a6c2 (patch) | |
tree | f57307394ace2c95b0111de638e0773388b1dd09 | |
parent | b03927227e20959dec9c1e486555ca616af01f5e (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.py | 20 | ||||
-rwxr-xr-x | blc/workspace.py | 90 |
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 |