diff options
-rw-r--r-- | blc/__init__.py (renamed from __init__.py) | 0 | ||||
-rwxr-xr-x | blc/audio.py (renamed from audio.py) | 12 | ||||
-rwxr-xr-x | blc/image.py (renamed from image.py) | 0 | ||||
-rw-r--r-- | blc/ola.py | 37 | ||||
-rw-r--r-- | blc/output.py (renamed from output.py) | 15 | ||||
-rw-r--r-- | blc/render.py (renamed from render.py) | 56 | ||||
-rwxr-xr-x | blc/tk.py (renamed from tk.py) | 8 | ||||
-rwxr-xr-x | blc/workspace.py (renamed from workspace.py) | 0 |
8 files changed, 106 insertions, 22 deletions
diff --git a/__init__.py b/blc/__init__.py index 887a920..887a920 100644 --- a/__init__.py +++ b/blc/__init__.py @@ -90,6 +90,8 @@ class FFPlayer(AudioPlayer): if start != -1: self.start = titot(start) + if self.start <= 0.1: + self.start = 0 self.player = subp.Popen(["ffplay", "-nodisp", "-autoexit", "-ss", str(self.start), *self.args, self.fname], stdin=subp.DEVNULL, stdout=subp.DEVNULL, stderr=subp.DEVNULL) atexit.register(self.stop) @@ -146,9 +148,9 @@ class FFPlayer(AudioPlayer): self.start = 0 self.start_time = 0 -try: - import mpv -except (OSError, ImportError): - warnings.warn("mpv backend unavailable, falling back to ffplay", RuntimeWarning) +## try: +## import mpv +## except (OSError, ImportError): +## warnings.warn("mpv backend unavailable, falling back to ffplay", RuntimeWarning) - DefaultAudioPlayer = FFPlayer +DefaultAudioPlayer = FFPlayer diff --git a/blc/ola.py b/blc/ola.py new file mode 100644 index 0000000..68a294c --- /dev/null +++ b/blc/ola.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +"""Classes for use with the OLA project.""" + +import array + +from ola.OlaClient import OlaClient + +from .output import LightingOutput + +class OLAOutput(LightingOutput): + """An OLA client for BLC. + + universe_map must be a dictionary associating the numeric QLC+ universe ID with a numeric + OLA universe. + """ + def set_values(self, values): + send = set() + for c, v in values: + if c.universe.id in self.universe_map: + au = self.universe_map[c.universe.id] + else: + au = c.universe.id + if au not in self.universes: + self.universes[au] = array.array('B', (0 for i in range(512))) + uni = self.universes[au] + if uni[c.address] != v: + uni[c.address] = v + send.add(au) + for au in send: + self.client.SendDmx(au, self.universes[au]) + + def __init__(self, universe_map=None): + self.universe_map = universe_map if universe_map is not None else {} + self.client = OlaClient() + + self.universes = {0: array.array('B', (0 for i in range(512)))} diff --git a/output.py b/blc/output.py index b1e88e1..e56c8ab 100644 --- a/output.py +++ b/blc/output.py @@ -11,7 +11,6 @@ class LightingOutput(ABC): ## Set this to how long it takes to transmit one set of values. May be ignored by client ## code trans_time = 1 - @abstractmethod def set_values(self, values): @@ -25,3 +24,17 @@ class LightingOutput(ABC): workspace.Channel. value must be between 0 and 255, inclusive. """ return + +class ChainedLightingOutput(LightingOutput): + """Class for combining lighting outputs. + + Useful for having one output display current light values, while another actually outputs + the values. + """ + def set_values(self, values): + for o in self.outputs: + o.set_values(values) + + def __init__(self, *outputs): + self.outputs = outputs + self.trans_time = min((i.trans_time for i in outputs)) diff --git a/render.py b/blc/render.py index 833b5f5..d87e8b6 100644 --- a/render.py +++ b/blc/render.py @@ -6,7 +6,7 @@ import threading from .audio import DefaultAudioPlayer, AudioPlayer from .output import LightingOutput -from .workspace import SHOW, CHASER, Advanceable +from .workspace import SHOW, CHASER, Advanceable, QLC_INFTY class FunctionQueue: def after(self, t: float, f: callable): @@ -17,7 +17,7 @@ class FunctionQueue: """Run until the queue is empty.""" while not self.queue.empty(): t, f = self.queue.get() - time.sleep(max(0, time.monotonic() - t)) + time.sleep(max(0, t-time.monotonic())) f() def __init__(self): @@ -32,28 +32,48 @@ class Renderer: which may be called from other threads. """ def start(self): - if self.start_time != -1: + """Start the function.""" + if self.start_time is not None: raise ValueError("Already running") - self.f.after(0, self.render_step) - self.f.start() + self.fq.after(0, self.render_step) + self.fq.start() self.nx = None self.data = None - self.vals = {} def render_step(self): """Output the current step and render the next one.""" - if self.nx not in (None, -1): - self.fq.after((max(self.minnx, self.nx), self.render_step)) - elif self.nx is None: + if self.nx not in (None, -1, QLC_INFTY): + self.fq.after(max(self.minnx, self.nx), self.render_step) + if self.nx is None: + self.nx = 0 + self.fq.after(0, self.render_step) + elif self.start_time is None: self.start_time = time.monotonic() self.lo.set_values(tuple(self.values.items())) + for st, ap in self.anext: + ap.play(max(int((time.monotonic() - self.start_time)*1000+1)-st, 0)) + self.anext = [] + + if self.nx == QLC_INFTY: + ## Acquire the lock twice and block the process, we're stalled + self.stall_lock.acquire() + self.stall_lock.acquire() + ## Restart the rendering + self.fq.after(0, self.render_step) with self.data_lock: - t = 1000*(int((time.monotonic() - self.start_time)/1000 + 1) + self.nx) + if self.start_time is not None: + t = int((time.monotonic() - self.start_time)*1000 + 1) + self.nx + else: + t = 0 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: + if aid not in self.aplayers: + self.aplayers.add(aid) + self.anext.append((st, self.ap(fname))) def advance(self): """Advance the function, if possible. @@ -65,14 +85,18 @@ class Renderer: 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(self.monotonic() - self.start_time) + t = 1000*int(time.monotonic() - self.start_time) self.data = self.f.advance(self.data, time.monotonic() - self.start_time) *_, self.data = self.f.render(t) - def __init__(self, f, lo:LightingOutput, ao: AudioPlayer=DefaultAudioPlayer, minnx=-1): + ## This will make the lock unlocked + self.stall_lock.acquire(blocking=False) + self.stall_lock.release() + + def __init__(self, f, lo:LightingOutput, ap: AudioPlayer=DefaultAudioPlayer, minnx=-1): if f.type not in (SHOW, CHASER): raise ValueError("Only Shows and Chasers may be used as toplevel functions") - self.start_time = -1 + self.start_time = None self.f = f self.fq = FunctionQueue() self.minnx = minnx @@ -81,5 +105,7 @@ class Renderer: self.data_lock = threading.Lock() self.values = {c: 0 for c in self.f.scope} self.lo = lo - self.ao = ao - self.aplayers = {} + self.ap = ap + self.aplayers = set() + self.anext = [] + self.stall_lock = threading.Lock() @@ -2,12 +2,14 @@ """Module containing Tk widgets for BLC.""" +from .output import LightingOutput + from tkinter import Frame, N, E, S, W, VERTICAL from tkinter.ttk import Label, Scale class DMXView(Frame): """Class for viewing DMX values.""" - def update_vals(self, vals:tuple): + def update_values(self, vals:tuple): """Update the current values. Parameters: @@ -30,3 +32,7 @@ class DMXView(Frame): s.set(255) Label(self, text=str(c+1+offset)).grid(row=1, column=c,sticky=N+E+S+W) self.channels.append(s) + +class TkOutput(LightingOutput, DMXView): + def set_values(self, values): + self.update_values(((c.address, v) for c,v in values)) diff --git a/workspace.py b/blc/workspace.py index c9f9a35..c9f9a35 100755 --- a/workspace.py +++ b/blc/workspace.py |