summaryrefslogtreecommitdiff
path: root/interface
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-10-29 22:11:00 -0400
committerBen Connors <benconnors@outlook.com>2019-10-29 22:11:00 -0400
commit91bbc033d03df9b71637fb6bb6ad9d8ec8bbfa9c (patch)
tree87a450615c72fdbd0e794030f638acc99a6526e9 /interface
parent0e845a0dca629b40166f95cf6d266181af605555 (diff)
Fixes; Implement basic renderer
- Fix chaser oneshot a bit (still doesn't work) - Basic implementation of a live renderer - Still needs functions for manipulating function data - Lacks audio fades, pause, seek
Diffstat (limited to 'interface')
-rw-r--r--interface/dummy.py17
-rwxr-xr-xinterface/dummyserver.py73
-rw-r--r--interface/render.py130
3 files changed, 220 insertions, 0 deletions
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