diff options
-rw-r--r-- | blc2/functions/chaser.py | 7 | ||||
-rw-r--r-- | interface/dummy.py | 17 | ||||
-rwxr-xr-x | interface/dummyserver.py | 73 | ||||
-rw-r--r-- | interface/render.py | 130 |
4 files changed, 224 insertions, 3 deletions
diff --git a/blc2/functions/chaser.py b/blc2/functions/chaser.py index c617b4f..1190024 100644 --- a/blc2/functions/chaser.py +++ b/blc2/functions/chaser.py @@ -129,7 +129,7 @@ class Chaser(Function): n = (n + 1) % len(self.steps) yield n else: - return range(n, len(self.steps)) + yield from range(n, len(self.steps)) def _fix_indices(self): for i, s in enumerate(self._steps): @@ -230,8 +230,9 @@ class Chaser(Function): if not self._steps: return (), (), data elif not data.steps: - if data.audio_id != 0: - raise ValueError("Audio ID must be zero") + ## Cross your fingers this is unnecessary... + # if data.audio_id != 0: + # raise ValueError("Audio ID must be zero") n = self.first_step sd = self.steps[n]._get_data(0, 0) #pylint: disable=protected-access data.audio_id += 1 diff --git a/interface/dummy.py b/interface/dummy.py new file mode 100644 index 0000000..ab03ae0 --- /dev/null +++ b/interface/dummy.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import array +import socket +import threading + +class DummyOutput: + def set_values(self, values): + v = {c.address[1]: v for c, v in values.items()} + with self._lock: + self.s.sendall(array.array('B', ((0 if i not in v else v[i]) for i in range(64))).tobytes()) + + def __init__(self): + self._lock = threading.RLock() + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect(("", 6969)) + diff --git a/interface/dummyserver.py b/interface/dummyserver.py new file mode 100755 index 0000000..cfd1451 --- /dev/null +++ b/interface/dummyserver.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import array +import socket +import threading +import time + +from tkinter import * +from tkinter.ttk import * + +CHANNEL_COUNT = 64 + +class Main(Frame): + def _update_display(self): + with self.lock: + for i, s in zip(self.channels, self.sliders): + s.config(state=NORMAL) + s.set(i) + s.config(state=DISABLED) + + self.master.after(16, self._update_display) + + + def update(self, b: bytes): + with self.lock: + self.channels = array.array('B', b) + + def __init__(self, root): + super().__init__(root) + + self.sliders = [] + self.rowconfigure(0, weight=1) + for i in range(CHANNEL_COUNT): + self.columnconfigure(i, weight=1) + self.sliders.append(Scale(self, from_=255, to=0, orient=VERTICAL, state=DISABLED)) + self.sliders[-1].grid(row=0, column=i, sticky=N+E+S+W) + Label(self, text=str(i+1)).grid(row=1, column=i, sticky=N+E+S+W) + + self.channels = array.array('B', (0 for i in range(CHANNEL_COUNT))) + self.lock = threading.RLock() + + self.master.after(0, self._update_display) + +def handle_conn(conn, m): + while True: + a = conn.recv(1024) + if not a: + break + m.update(a) + +def socket_main(m): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 6969)) + s.listen() + print("Listening") + while True: + conn, addr = s.accept() + threading.Thread(target=handle_conn, args=(conn, m,)).start() + +if __name__ == "__main__": + root = Tk() + root.rowconfigure(0, weight=1) + root.columnconfigure(0, weight=1) + + root.wm_title("Lighting Output") + + + main = Main(root) + main.grid(row=0, column=0, sticky=N+E+S+W) + + root.after(0, threading.Thread(target=socket_main, args=(main,)).start) + + main.mainloop() diff --git a/interface/render.py b/interface/render.py new file mode 100644 index 0000000..5b6c934 --- /dev/null +++ b/interface/render.py @@ -0,0 +1,130 @@ +import threading +import time + +import mpv + +from blc2.workspace import Workspace +from blc2.topology import Fixture + +class Renderer: + def hold(self, values): + with self._lock: + for c, v in values: + if v is None: + if c in self._hold: + del self._hold[c] + else: + self._hold[c] = v + ## Rely on the run thread to update when possible + if not self._running: + ## If not, just update now + self._update() + + def _update(self): + with self._lock: + self._values = {c: (v if c not in self._hold else self._hold[c]) for c, v in self._last.items()} + self.output.set_values(self._values) + + def start(self): + with self._lock: + if self._run_thread is not None: + raise ValueError("Already running") + self._run_thread = threading.Thread(target=self._run) + self._running = True + self._run_thread.start() + + def stop(self): + with self._lock: + self._running = False + + def set_functions(self, f): + with self._lock: + if self._running: + raise ValueError("Can't change while running") + self._functions, self._data = [i[0] for i in f], [i[1] for i in f] + + @property + def time(self): + with self._lock: + return self._current + + def _run(self): + audio_cache = {} + + sleep = 1/60 + next_ap = [] + ap = [] + running_ap = set() + t = 0 + start = time.monotonic() + while True: + ## w_lock here is a formality: by assumption, we're not editing while we're + ## running a show + with self._lock, self.w_lock: + self._update() + for a in next_ap: + a.pause = False + next_ap = [] + + if self._callback is not None: + self._callback(t, self._values) + + ## FIXME: Cleanup finished audio players? + ## FIXME: Handle audio fades and jumps + + next_t = sleep + time.monotonic() + self._current = next_t - start + t = int(1000*self._current) + _last = {c: 0 for c in self._channels} + for n, (f, d) in enumerate(zip(self._functions, self._data)): + lc, ac, self._data[n] = f.render(t, d) + for c, v in lc: + if _last[c] < v: + _last[c] = v + + for guid, filename, *_ in ac: + if guid in running_ap: + continue + running_ap.add(guid) + nap = mpv.MPV() + nap.pause = True + nap.play(filename) + next_ap.append(nap) + ap.append(nap) + + self._last = _last + self._update() + + if not self._running: + ## We're done, clean up + for a in ap: + a.pause = True + del a + self._last = {c: 0 for c in self._channels} + self._current = 0 + self._update() + if self._callback is not None: + self._callback(0, self._values) + self._run_thread = None + break + + ## END locked block + time.sleep(max(0, next_t - time.monotonic())) + + def __init__(self, w: Workspace, w_lock: threading.RLock, output, callback=None): + self.output = output + self.w = w + self.w_lock = w_lock + + self._lock = threading.RLock() + self._hold = {} + self._channels = frozenset().union(*((c for c in f.channels) for f in w.fixtures.values())) + self._last = {c: 0 for c in self._channels} + self._functions = [] + self._data = [] + self._current = 0 + self._running = False + self._run_thread = None + self._values = {} + + self._callback = callback |