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

Log | Files | Refs

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