summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--blc/__init__.py (renamed from __init__.py)0
-rwxr-xr-xblc/audio.py (renamed from audio.py)12
-rwxr-xr-xblc/image.py (renamed from image.py)0
-rw-r--r--blc/ola.py37
-rw-r--r--blc/output.py (renamed from output.py)15
-rw-r--r--blc/render.py (renamed from render.py)56
-rwxr-xr-xblc/tk.py (renamed from tk.py)8
-rwxr-xr-xblc/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
diff --git a/audio.py b/blc/audio.py
index 37b0f7d..9df8853 100755
--- a/audio.py
+++ b/blc/audio.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/image.py b/blc/image.py
index 6e47332..6e47332 100755
--- a/image.py
+++ b/blc/image.py
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()
diff --git a/tk.py b/blc/tk.py
index e9ba669..efb0459 100755
--- a/tk.py
+++ b/blc/tk.py
@@ -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