import threading import time import mpv from blc2.workspace import Workspace from blc2.topology import Fixture from blc2.constants import ONESHOT 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} this_ap = set() 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, start_t, fin, fstart, fout in ac: this_ap.add(guid) fstart += start_t if guid in running_ap: mul = 100 if t < fin: mul = max(0, int(100*(t/fin))) elif t > fstart+fout: mul = -1 elif t > fstart: mul = max(0, int(100*(1 - (t-fstart)/fout))) if mul == -1: ap[guid].pause = True else: ap[guid].volume = mul else: running_ap.add(guid) nap = mpv.MPV() nap.pause = True nap.play(filename) next_ap.append(nap) ap[guid] = nap for a, p in tuple(ap.items()): if a not in this_ap: p.pause = True del ap[a] running_ap.remove(a) self._last = _last self._update() if not self._running: ## We're done, clean up for a in ap.values(): 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) if f.advance_mode == ONESHOT and self._data[a].steps and self._data[a].steps[-1].index+1 == len(f.steps): continue try: d2 = f.advance(t, self._data[a], n=p) except ValueError: ## Done pass else: d = d2 _, _, 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