From b03927227e20959dec9c1e486555ca616af01f5e Mon Sep 17 00:00:00 2001 From: Ben Connors Date: Thu, 31 Jan 2019 00:29:16 -0500 Subject: Some fixes --- blc/render.py | 58 ++++++++++++++++++++++++++++++++++++++++++--------- blc/workspace.py | 63 ++++++++++++++++++++++++++++++++------------------------ tktest.py | 19 +++++++++++++---- 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 " % 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() -- cgit v1.2.3