| blc2 - Python library and frontend for running theatrical lighting and SFX. Written for the English 2041F fall 2019 theatre production of "The Cenci".
git clone https://benconnors.ca/git-repos/blc2 |
render.py (6241B) - raw
1 import threading 2 import time 3 4 import mpv 5 6 from blc2.workspace import Workspace 7 from blc2.topology import Fixture 8 from blc2.constants import ONESHOT 9 10 class Renderer: 11 def hold(self, values): 12 with self._lock: 13 if isinstance(values, dict): 14 values = values.items() 15 for c, v in values: 16 if v is None: 17 if c in self._hold: 18 del self._hold[c] 19 else: 20 self._hold[c] = v 21 ## Rely on the run thread to update when possible 22 if not self._running: 23 ## If not, just update now 24 self._update() 25 26 def clear_hold(self): 27 with self._lock: 28 self._hold = {} 29 self.hold(()) 30 31 def _update(self): 32 with self._lock: 33 self._values = {c: (v if c not in self._hold else self._hold[c]) for c, v in self._last.items()} 34 self.output.set_values(self._values) 35 36 def start(self): 37 with self._lock: 38 if self._run_thread is not None: 39 raise ValueError("Already running") 40 self._run_thread = threading.Thread(target=self._run) 41 self._running = True 42 self._run_thread.start() 43 44 def stop(self): 45 with self._lock: 46 self._running = False 47 48 49 def set_functions(self, *args): 50 with self._lock: 51 if self._running: 52 raise ValueError("Can't change while running") 53 self._functions, self._data = [i[0] for i in args], [i[1] for i in args] 54 55 @property 56 def time(self): 57 with self._lock: 58 return self._current 59 60 def _run(self): 61 audio_cache = {} 62 63 sleep = 1/60 64 next_ap = [] 65 ap = {} 66 running_ap = set() 67 t = 0 68 start = time.monotonic() 69 while True: 70 ## w_lock here is a formality: by assumption, we're not editing while we're 71 ## running a show 72 with self._lock, self.w_lock: 73 self._update() 74 for a in next_ap: 75 a.pause = False 76 next_ap = [] 77 78 if self._callback is not None: 79 self._callback(self._current, self._values) 80 81 ## FIXME: Cleanup finished audio players? 82 ## FIXME: Handle audio fades and jumps 83 84 next_t = sleep + time.monotonic() 85 self._current = next_t - start 86 t = int(1000*self._current) 87 _last = {c: 0 for c in self._channels} 88 this_ap = set() 89 for n, (f, d) in enumerate(zip(self._functions, self._data)): 90 lc, ac, self._data[n] = f.render(t, d) 91 for c, v in lc: 92 if _last[c] < v: 93 _last[c] = v 94 95 for guid, filename, start_t, fin, fstart, fout in ac: 96 this_ap.add(guid) 97 fstart += start_t 98 if guid in running_ap: 99 mul = 100 100 if t < fin: 101 mul = max(0, int(100*(t/fin))) 102 elif t > fstart+fout: 103 mul = -1 104 elif t > fstart: 105 mul = max(0, int(100*(1 - (t-fstart)/fout))) 106 107 if mul == -1: 108 ap[guid].pause = True 109 else: 110 ap[guid].volume = mul 111 else: 112 running_ap.add(guid) 113 nap = mpv.MPV() 114 nap.pause = True 115 nap.play(filename) 116 next_ap.append(nap) 117 ap[guid] = nap 118 119 for a, p in tuple(ap.items()): 120 if a not in this_ap: 121 p.pause = True 122 del ap[a] 123 running_ap.remove(a) 124 125 self._last = _last 126 self._update() 127 128 if not self._running: 129 ## We're done, clean up 130 for a in ap.values(): 131 a.pause = True 132 del a 133 self._last = {c: 0 for c in self._channels} 134 self._current = 0 135 self._update() 136 if self._callback is not None: 137 self._callback(0, self._values) 138 self._run_thread = None 139 break 140 141 ## END locked block 142 time.sleep(max(0, next_t - time.monotonic())) 143 144 def advance(self, *args): 145 with self._lock: 146 if self._run_thread is None: 147 raise ValueError("Not running") 148 for a in args: 149 if isinstance(a, int): 150 p = None 151 else: 152 a, p = a 153 f = self._functions[a] 154 t = int(1000*self._current) 155 if f.advance_mode == ONESHOT and self._data[a].steps and self._data[a].steps[-1].index+1 == len(f.steps): 156 continue 157 try: 158 d2 = f.advance(t, self._data[a], n=p) 159 except ValueError: 160 ## Done 161 pass 162 else: 163 d = d2 164 _, _, self._data[a] = f.render(t, d) 165 166 def __init__(self, w: Workspace, w_lock: threading.RLock, output, callback=None): 167 self.output = output 168 self.w = w 169 self.w_lock = w_lock 170 171 self._lock = threading.RLock() 172 self._stop_lock = threading.Lock() 173 self._hold = {} 174 self._channels = frozenset().union(*((c for c in f.channels) for f in w.fixtures.values())) 175 self._last = {c: 0 for c in self._channels} 176 self._functions = [] 177 self._data = [] 178 self._current = 0 179 self._running = False 180 self._run_thread = None 181 self._values = {} 182 183 self._callback = callback