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: if isinstance(values, dict): values = values.items() 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 clear_hold(self): with self._lock: self._hold = {} self.hold(()) 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, *args): with self._lock: if self._running: raise ValueError("Can't change while running") self._functions, self._data = [i[0] for i in args], [i[1] for i in args] @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(self._current, 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 advance(self, *args): with self._lock: if self._run_thread is None: raise ValueError("Not running") for a in args: if isinstance(a, int): p = None else: a, p = a f = self._functions[a] t = int(1000*self._current) d = f.advance(t, self._data[a], n=p) _, _, self._data[a] = f.render(t, d) 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._stop_lock = threading.Lock() 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