summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--blc/render.py58
-rwxr-xr-xblc/workspace.py63
-rwxr-xr-xtktest.py19
3 files changed, 99 insertions, 41 deletions
diff --git a/blc/render.py b/blc/render.py
index 7dbeee2..592899d 100644
--- a/blc/render.py
+++ b/blc/render.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
+from dataclasses import dataclass, field
import queue
import time
import threading
@@ -8,21 +9,52 @@ from .audio import DefaultAudioPlayer, AudioPlayer
from .output import LightingOutput
from .workspace import SHOW, CHASER, Advanceable, QLC_INFTY
+@dataclass(order=True)
+class FunctionEntry:
+ time: float
+ f: callable = field(compare=False)
+
class FunctionQueue:
- """Queue for executing functions in sequence."""
+ """Queue for executing functions in sequence.
+
+ Note that functions do not necessarily run in the sequence they are given, they are run
+ (approximately) in the order of the time they should be called. For example, given the
+ following sequence of calls:
+
+ fq.after(100, f_a)
+ fq.after(0, f_b)
+
+ Assuming very little time passes between the two calls and a small maxsleep setting when
+ constructing fq, f_b will be run before f_a. This is not guaranteed
+ """
def after(self, t: float, f: callable):
"""Run the given function after t milliseconds."""
- self.queue.put((time.monotonic() + t/1000, f))
+ self.queue.put(FunctionEntry(t/1000+time.monotonic(), f))
def start(self):
"""Run until the queue is empty."""
while not self.queue.empty():
- t, f = self.queue.get()
- time.sleep(max(0, t-time.monotonic()))
- f()
-
- def __init__(self):
- self.queue = queue.SimpleQueue()
+ print("outerloop")
+ entry = self.queue.get()
+ while True:
+ print("innerloop")
+ ct = time.monotonic()
+ if ct > entry.time:
+ break
+ if not self.queue.empty():
+ nentry = self.queue.get()
+ if nentry.time < entry.time:
+ self.queue.put(entry)
+ entry = nentry
+ else:
+ 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()
+ self.maxsleep = maxsleep/1000
class BasicRenderer:
"""Basic renderer for functions.
@@ -31,6 +63,10 @@ class BasicRenderer:
Instances of this class are NOT thread-safe, with the exception of the advance() method,
which may be called from other threads.
+
+ When using Chasers with infinite hold times, a second thread must be used: the thread
+ running start() (the "render" thread) will block until such a time as the Chaser has been
+ advanced.
"""
def start(self):
"""Start the function."""
@@ -61,6 +97,7 @@ class BasicRenderer:
self.stall_lock.acquire()
self.stall_lock.acquire()
## Restart the rendering
+ print("Restarting render")
self.fq.after(0, self.render_step)
with self.data_lock:
@@ -71,7 +108,7 @@ class BasicRenderer:
vals, acues, self.nx, self.data = self.f.render(t)
for c, v in vals:
self.values[c] = v
- for aid, st, fname, *_ in acues:
+ for st, aid, fname, *_ in acues:
if aid not in self.aplayers:
self.aplayers.add(aid)
self.anext.append((st, self.ap(fname)))
@@ -82,12 +119,13 @@ class BasicRenderer:
It is not an error to call this function when dealing with non-Advanceable toplevel
functions; this will just do nothing.
"""
+ ## TODO: Make this work when not stalled
with self.data_lock:
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)
- self.data = self.f.advance(self.data, time.monotonic() - self.start_time)
+ self.data = self.f.advance(t, self.data)
*_, self.data = self.f.render(t)
## This will make the lock unlocked
diff --git a/blc/workspace.py b/blc/workspace.py
index ac095ed..e20575d 100755
--- a/blc/workspace.py
+++ b/blc/workspace.py
@@ -99,7 +99,7 @@ of single-shot chasers presently. Additionally, time is reset to 0 at the start
workspace on disk doesn't change
"""
-from abc import ABC, abstractmethod
+from abc import ABC, abstractmethod, abstractstaticmethod
import json
import logging
from multiprocessing.pool import ThreadPool
@@ -235,6 +235,9 @@ class Function(ABC):
scope must be an iterable of channels representing all of the channels used by this function
regardless of whether or not they are currently being used.
+ For cases where a globally-unique identifier is required, hash(Function) provides such a
+ value.
+
This class itself must be stateless: anything that requires storage of state must also
require the caller to store that state.
"""
@@ -320,8 +323,8 @@ class FadeFunction(Function):
class Advanceable(ABC):
"""Function that may be advanced."""
- @abstractmethod
- def advance(self, data):
+ @abstractstaticmethod
+ def advance(t, data):
"""Advance the function."""
return
@@ -405,12 +408,13 @@ class ChaserStep(FadeFunction):
mul = 1-min(1,ft/(self.fade_out))
nx = -1 if ft > self.fade_out else 1 ## Check if we're done
else:
- nx = min(nx, -ft + 1)
+ nx = min(nx if nx != -1 else QLC_INFTY, -ft + 1)
elif t >= data.end_time:
mul = 0
if t < data.end_time:
- nx = min(nx, data.end_time-t)
+ if data.end_time == QLC_INFTY:
+ nx = min(nx if nx != -1 else QLC_INFTY, data.end_time-t if data.end_time != QLC_INFTY else QLC_INFTY)
nacues = []
for s, aid, f, fin, fout, fstart in acues:
@@ -544,7 +548,7 @@ class Chaser(Function, Advanceable):
start_time = 0
data = None
current = []
- acurrent = []
+ acurrent = set()
values = {c: 0 for c in self.scope}
@@ -556,20 +560,22 @@ class Chaser(Function, Advanceable):
if t == 0 or values[c] != v:
values[c] = v
changes.append((c, v))
- current.append((t-start_time, tuple(changes)))
+ if changes:
+ current.append((t-start_time, tuple(changes)))
- acurrent += [(t-start_time, *others) for t,*others in acues]
+ acurrent.update(((t-start_time, *others) for t,*others in acues))
if nx == -1 or nx >= QLC_INFTY:
## Done the current step
- steps.append((tuple(current), tuple(acurrent)))
+ steps.append((tuple(current), tuple(sorted(acurrent, key=lambda a: a[0]))))
if nx == -1:
## Done
break
## Reached an infinite segment, advance
current = []
- acurrent = []
+ acurrent = set()
t += 1
+ values = {c: 0 for c in self.scope}
start_time = t
data = self.advance(t, data)
else:
@@ -652,7 +658,9 @@ class ShowTrack(Function):
elif snx < nx:
nx = snx
if nx == QLC_INFTY:
- nx = min((f.start_time-t for f in self.functions if f.start_time > t), default=-1)
+ check = min((f.start_time-t for f in self.functions if f.start_time > t), default=-1)
+ if check != -1:
+ nx = check
return tuple(values.items()), tuple(sorted(acues, key=lambda a: a[0])), nx, data
@@ -892,6 +900,7 @@ class Workspace:
if speed is not None:
fin = int(speed.attrib["FadeIn"])
fout = int(speed.attrib["FadeOut"])
+ duration = int(speed.attrib["Duration"])
else:
fin = None
fout = None
@@ -920,17 +929,17 @@ class Workspace:
func = Audio(sid, name, fname, fin, fout, audio_lengths[fname], run_order=ro,
hidden=hidden)
elif ftype == SEQUENCE:
- ## smodes = func.find(QXW+"SpeedModes")
- ## sfin = smodes.attrib["FadeIn"]
- ## sfout = smodes.attrib["FadeOut"]
- ## sdur = smodes.attrib["Duration"]
+ smodes = func.find(QXW+"SpeedModes")
+ sfin = smodes.attrib["FadeIn"]
+ sfout = smodes.attrib["FadeOut"]
+ sdur = smodes.attrib["Duration"]
## bound_scene = self.functions[int(func.attrib["BoundScene"])]
steps = []
for step in func.iterfind(QXW+"Step"):
- stfin = int(step.attrib["FadeIn"])
- stnum = int(step.attrib["Number"])
- stfout = int(step.attrib["FadeOut"])
- sthold = int(step.attrib["Hold"])
+ stfin = int(step.attrib["FadeIn"]) if sfin == "PerStep" else fin
+ stnum = int(step.attrib["Number"])
+ stfout = int(step.attrib["FadeOut"]) if sfout == "PerStep" else fout
+ sthold = int(step.attrib["Hold"]) if sdur == "PerStep" else duration
used = set()
values = []
if step.text is not None:
@@ -977,16 +986,16 @@ class Workspace:
continue
func = Show(sid, name, tracks)
elif ftype == CHASER:
- ## smodes = func.find(QXW+"SpeedModes")
- ## sfin = smodes.attrib["FadeIn"]
- ## sfout = smodes.attrib["FadeOut"]
- ## sdur = smodes.attrib["Duration"]
+ smodes = func.find(QXW+"SpeedModes")
+ sfin = smodes.attrib["FadeIn"]
+ sfout = smodes.attrib["FadeOut"]
+ sdur = smodes.attrib["Duration"]
steps = []
for step in func.iterfind(QXW+"Step"):
- stfin = int(step.attrib["FadeIn"])
- stnum = int(step.attrib["Number"])
- stfout = int(step.attrib["FadeOut"])
- sthold = int(step.attrib["Hold"])
+ stfin = int(step.attrib["FadeIn"]) if sfin == "PerStep" else fin
+ stnum = int(step.attrib["Number"])
+ stfout = int(step.attrib["FadeOut"]) if sfout == "PerStep" else fout
+ sthold = int(step.attrib["Hold"]) if sdur == "PerStep" else duration
stid = int(step.text)
step = ChaserStep(stid, stfin, stfout, sthold, self.functions[stid])
steps.append(step)
diff --git a/tktest.py b/tktest.py
index d8f759c..8f9bf1e 100755
--- a/tktest.py
+++ b/tktest.py
@@ -4,16 +4,22 @@
import threading
import time
-from tkinter import Tk, N, E, S, W, Label
+from tkinter import Tk, N, E, S, W, Label, Button
import sys
from blc.workspace import Workspace
from blc.tk import TkOutput
from blc.render import BasicRenderer
-def update_time(m, l, r):
+def update(m, l, sl, r):
+ """Update GUI elements."""
+ if r.stall_lock.acquire(blocking=False):
+ r.stall_lock.release()
+ sl.config(text="Running")
+ else:
+ sl.config(text="Stalled")
l.config(text="%.1f" % ((time.monotonic() - r.start_time) if r.start_time is not None else 0))
- m.after(50, lambda: update_time(m,l,r))
+ m.after(50, lambda: update(m,l,sl,r))
if len(sys.argv) != 3:
print("Usage: %s <workspace> <show id>" % sys.argv[0])
@@ -32,12 +38,17 @@ output.grid(row=0, sticky=N+E+S+W)
label = Label(root)
label.grid(row=1,sticky=N+E+S+W)
+slabel = Label(root)
+slabel.grid(row=2,sticky=N+E+S+W)
+
root.wm_title("BLC+ TkTest - "+s.name)
renderer = BasicRenderer(s, output, minnx=13)
rthread = threading.Thread(target=renderer.start)
rthread.start()
-root.after(50, lambda: update_time(root, label, renderer))
+Button(root,text="Advance",command=lambda *args: renderer.advance()).grid(row=3, sticky=N+E+S+W)
+
+root.after(50, lambda: update(root, label, slabel, renderer))
root.mainloop()