| blc - Simple program for rendering QLC+ workspaces properly/without crashing. Developed into BLC2.
git clone https://benconnors.ca/git-repos/blc |
blc-qt.py (13936B) - raw
1 #!/usr/bin/env python3 2 3 import locale 4 import argparse 5 import sys 6 import time 7 8 from PyQt5.QtCore import Qt, QVariant, QTimer 9 from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QGroupBox, QLabel, QDialog, QSlider, QVBoxLayout, QHBoxLayout, QProxyStyle, QStyle, QMainWindow, QMenu, QAction, QFileDialog, QComboBox 10 11 from blc.workspace import Workspace, CHASER, SHOW, QLC_INFTY 12 from blc.audio import DefaultAudioPlayer, AudioPlayer, FFPlayer 13 from blc.output import LightingOutput 14 from blc.ola import OLAOutput 15 16 class DirectSliderStyle(QProxyStyle): 17 def styleHint(self, hint, option=0, widget=0, returnData=0): 18 if hint == QStyle.SH_Slider_AbsoluteSetButtons: 19 return (Qt.LeftButton | Qt.MidButton | Qt.RightButton) 20 return super().styleHint(hint, option, widget, returnData) 21 22 class QFader(QGroupBox): 23 """Class for creating a single fader. 24 25 Allows for the value to be set by the application and overriden by the user. 26 27 @param title the title for the fader 28 @param parent the parent widget 29 """ 30 @property 31 def sliderValue(self): 32 """Return the fader's current value.""" 33 return self.slider.value() 34 35 @sliderValue.setter 36 def sliderValue(self, value): 37 self._sliderValue = value 38 if not self.held: 39 self.slider.setValue(value) 40 41 def _sliderPressed(self): 42 if not self.held: 43 self.held = True 44 self.button.setDisabled(False) 45 self._sliderMoved(self.slider.value()) 46 47 def _releaseHold(self): 48 self.held = False 49 self.button.setDisabled(True) 50 self.slider.setValue(self._sliderValue) 51 self._sliderMoved(self._sliderValue) 52 53 def _sliderMoved(self, value): 54 if self.on_update is not None: 55 self.on_update(self.channel, value) 56 57 def __init__(self, title, channel=None, on_update=None, parent=None): 58 super().__init__(title=title, parent=parent) 59 self.slider = QSlider(Qt.Vertical, self) 60 self.slider.setStyle(DirectSliderStyle(self.slider.style())) 61 self.slider.setMaximum(255) 62 self.slider.setMinimum(0) 63 self.slider.setMinimumHeight(200) 64 65 self.channel = channel 66 self.on_update = on_update 67 68 self.slider.sliderMoved.connect(self._sliderMoved) 69 70 self._sliderValue = 0 71 self.held = False 72 73 self.label = QLabel(self) 74 self.label.setText('0') 75 self.label.setAlignment(Qt.AlignCenter) 76 self.slider.valueChanged.connect(lambda a: self.label.setText(str(a))) 77 self.slider.sliderPressed.connect(self._sliderPressed) 78 79 self.button = QPushButton("", self) 80 self.button.setDisabled(True) 81 self.button.clicked.connect(self._releaseHold) 82 83 layout = QVBoxLayout() 84 layout.setAlignment(Qt.AlignCenter) 85 layout.addWidget(self.slider) 86 layout.addWidget(self.label) 87 layout.addWidget(self.button) 88 self.setLayout(layout) 89 90 self.setFixedWidth(45) 91 92 class QFaderBank(QGroupBox): 93 """Class for creating a bank of faders. 94 95 @param title the title of the fader bank 96 @param faders a list of (address, title) elements, where address is unique 97 @param parent the parent widget 98 """ 99 def set_values(self, values): 100 values = tuple(values) 101 for c, v in values: 102 ua = (*c,) 103 if ua not in self.sliders: 104 continue 105 self.sliders[ua].sliderValue = v 106 if self.output is not None: 107 self.output.set_values(values) 108 109 def slider_changed(self, channel, value): 110 if self.output is not None: 111 self.output.set_values(((channel, value),)) 112 113 def __init__(self, title="Faders", faders=(), output=None, parent=None): 114 super().__init__(title=title, parent=parent) 115 116 layout = QHBoxLayout() 117 layout.setSpacing(0) 118 119 self.sliders = {} 120 for a, t in faders: 121 ua = (*a,) 122 slider = QFader(t, channel=ua, on_update=self.slider_changed, parent=self) 123 layout.addWidget(slider) 124 self.sliders[ua] = slider 125 126 self.setLayout(layout) 127 128 self.output = output 129 130 class QtRenderer: 131 def _start(self): 132 """Start the function.""" 133 if self.running: 134 raise ValueError("Already running") 135 self.nx = 0 136 self.tnx = 0 137 self.data = None 138 self.time_elapsed = 0 139 self.last_rendered = 0 140 self.last_time = None 141 self.running = True 142 self.aplayers = set() 143 self.audio_players = [] 144 145 def play(self): 146 if not self.running: 147 self._start() 148 else: 149 self.last_time = time.monotonic() 150 for ap in self.audio_players: 151 ap.play() 152 self.timer.start(self.minnx) 153 154 def _prune_ap(self): 155 nap = [] 156 for i in self.audio_players: 157 if not i.playing: 158 i.terminate() 159 else: 160 nap.append(i) 161 self.audio_players = nap 162 163 def pause(self): 164 self.timer.stop() 165 self._prune_ap() 166 for ap in self.audio_players: 167 ap.pause() 168 169 def stop(self): 170 self.timer.stop() 171 self.running = False 172 self._prune_ap() 173 for ap in self.audio_players: 174 ap.stop() 175 176 def _do_render(self): 177 t = int(1000*self.time_elapsed+1) + self.minnx 178 179 vals, acues, self.nx, self.data = self.f.render(t, data=self.data) 180 self.nx = max(self.minnx, self.nx) 181 self.tnx = self.nx/1000 182 183 for c, v in vals: 184 self.values[c] = v 185 for st, aid, fname, *_ in acues: 186 if aid not in self.aplayers: 187 self.aplayers.add(aid) 188 ap = self.ap(fname) 189 ap.volume = 100 190 self.audio_players.append(ap) 191 self.anext.append((st, ap)) 192 193 self.last_rendered = self.time_elapsed 194 195 def render_step(self): 196 """Output the current step and render the next one.""" 197 if self.last_time is None: 198 self._do_render() 199 self.nx = 0 200 self.tnx = 0 201 self.last_time = time.monotonic() 202 else: 203 t = time.monotonic() 204 self.time_elapsed += t - self.last_time 205 self.last_time = t 206 self.timestep(self.time_elapsed) 207 208 self.lo.set_values(tuple(self.values.items())) 209 for st, ap in self.anext: 210 ap.play(max(int(self.time_elapsed*1000+1)-st, 0)) 211 self.anext = [] 212 213 self._do_render() 214 215 def advance(self): 216 """Advance the function, if possible. 217 218 It is not an error to call this function when dealing with non-Advanceable toplevel 219 functions; this will just do nothing. 220 """ 221 ## TODO: Make this work when not stalled 222 if not self.running: 223 raise ValueError("Cannot advance a function that has not been started!") 224 225 t = 1000*self.time_elapsed 226 self.data = self.f.advance(t, self.data) 227 *_, self.data = self.f.render(t, data=self.data) 228 self.nx = self.minnx 229 230 def terminate(self): 231 for i in self.audio_players: 232 i.terminate() 233 234 def __init__(self, f, timer: QTimer, timestep, lo:LightingOutput, 235 ap: AudioPlayer=DefaultAudioPlayer, minnx=20): 236 if f.type not in (SHOW, CHASER): 237 raise ValueError("Only Shows and Chasers may be used as toplevel functions") 238 239 self.time_elapsed = 0 240 self.last_rendered = 0 241 self.last_time = 0 242 self.f = f 243 self.minnx = minnx 244 self.nx = 0 245 self.data = None 246 self.values = {c: 0 for c in self.f.scope} 247 self.lo = lo 248 self.ap = ap 249 self.aplayers = set() 250 self.audio_players = [] 251 self.anext = [] 252 self.timestep = timestep 253 self.timer = timer 254 self.running = False 255 self.tnx = 0 256 self.tminnx = self.minnx/1000 257 258 self.timer.timeout.connect(self.render_step) 259 260 class BLCQtMain(QMainWindow): 261 def blackout(self): 262 self.faders.set_values(((i, 0) for i in self.channels)) 263 264 def ask_load_workspace(self): 265 fname, _ = QFileDialog.getOpenFileName(self, "Open File", "", "QLC Workspaces (*.qxw)") 266 if not fname: 267 return 268 self.load_workspace(fname) 269 270 def load_workspace(self, fname): 271 ## Load the workspace 272 self.workspace = Workspace.load(fname) 273 274 ## Iterate over all fixtures and set them to 0 275 self.channels = sum((tuple(f.channels) for f in self.workspace.fixtures.values()), ()) 276 self.blackout() 277 278 ## Update the list of functions 279 functions = sorted((f for f in self.workspace.functions.values() if f.type == SHOW or (f.type == CHASER and f.is_chaser) ), 280 key=lambda f: (f.type, f.name, f.id)) 281 self.function_select.clear() 282 for n, f in enumerate(functions): 283 self.function_select.insertItem(n, ("[C] " if f.type == CHASER else "[S] ")+f.name, QVariant((f.id))) 284 if functions: 285 self._choose_function(0) 286 287 def _choose_function(self, index: int): 288 fid = self.function_select.currentData() 289 if self.function is not None and self.function.id == fid: 290 return 291 292 self.function = self.workspace.functions[fid] 293 self.timer = QTimer() 294 if self.renderer is not None: 295 self.renderer.terminate() 296 self.renderer = QtRenderer(self.function, self.timer, self._update_time, self.faders) 297 self.func_label.setText("Current Function: None") 298 self._set_controls(True, False, False, False) 299 300 def _update_time(self, t: float): 301 if 1000*t > self.function.actual_duration: 302 self._stop() 303 elif self.function.type == CHASER and self.renderer.data is not None: 304 cs = self.renderer.data.current_step 305 self.func_label.setText("Current Function: "+self.function.steps[cs].function.name+(" (%d)" % (cs+1))) 306 self.time_label.setText("Time: %.1fs" % t) 307 308 def _set_controls(self, play, pause, stop, advance): 309 self.play_button.setEnabled(play) 310 self.pause_button.setEnabled(pause) 311 self.stop_button.setEnabled(stop) 312 self.advance_button.setEnabled(advance) 313 314 def _play(self): 315 self._set_controls(False, True, True, True) 316 if self.function.type == SHOW: 317 self.func_label.setText("Current Function: Show") 318 self.function_select.setEnabled(False) 319 self.renderer.play() 320 321 def _stop(self): 322 self.renderer.stop() 323 self._set_controls(True, False, False, False) 324 self.function_select.setEnabled(True) 325 self._update_time(0) 326 self.func_label.setText("Current Function: None") 327 self.blackout() 328 329 def _pause(self): 330 self.renderer.pause() 331 self._set_controls(True, False, True, False) 332 333 def _advance(self): 334 self.renderer.advance() 335 336 def terminate(self): 337 if self.renderer is not None: 338 self.renderer.terminate() 339 340 def __init__(self): 341 super().__init__() 342 343 self.root = QWidget(self) 344 self.setWindowTitle("BLC-Qt") 345 346 self.menu = self.menuBar() 347 self.fileMenu = self.menu.addMenu("File") 348 349 loadw = QAction("Load Workspace", self.root) 350 loadw.triggered.connect(self.ask_load_workspace) 351 self.fileMenu.addAction(loadw) 352 353 self.faders = QFaderBank(title="Channels", faders=(((0, i), str(i)) for i in range(1,37)), 354 parent=self.root) 355 356 function_box = QGroupBox(self, title="Functions") 357 self.function_select = QComboBox(function_box) 358 self.function_select.activated.connect(self._choose_function) 359 360 self.time_label = QLabel(function_box) 361 self.time_label.setText("Time: 0.0s") 362 self.func_label = QLabel(function_box) 363 self.func_label.setText("Current Function: None") 364 self.play_button = QPushButton("Play", function_box) 365 self.play_button.clicked.connect(self._play) 366 self.pause_button = QPushButton("Pause", function_box) 367 self.pause_button.clicked.connect(self._pause) 368 self.stop_button = QPushButton("Stop", function_box) 369 self.stop_button.clicked.connect(self._stop) 370 self.advance_button = QPushButton("Advance", function_box) 371 self.advance_button.clicked.connect(self._advance) 372 self._set_controls(False, False, False, False) 373 374 fblayout = QVBoxLayout() 375 fblayout.addWidget(self.function_select) 376 fblayout.addWidget(self.func_label) 377 fblayout.addWidget(self.play_button) 378 fblayout.addWidget(self.stop_button) 379 fblayout.addWidget(self.pause_button) 380 fblayout.addWidget(self.advance_button) 381 fblayout.addWidget(self.time_label) 382 function_box.setLayout(fblayout) 383 384 layout = QVBoxLayout() 385 layout.addWidget(self.faders) 386 layout.addWidget(function_box) 387 self.root.setLayout(layout) 388 389 self.setCentralWidget(self.root) 390 391 self.workspace = None 392 self.function = None 393 394 self.timer = None 395 self.renderer = None 396 397 self.channels = [] 398 399 class PrintOutput: 400 def set_values(self, values): 401 for c, v in values: 402 _, a = c 403 #print("%3d to %3d" % (a, v)) 404 405 parser = argparse.ArgumentParser() 406 parser.add_argument("workspace", nargs="?", action="store", type=str, help="workspace file to load") 407 408 args = parser.parse_args() 409 410 app = QApplication([]) 411 412 ## Necessary for MPV to work, the QApplication constructor changes this 413 locale.setlocale(locale.LC_NUMERIC, "C") 414 415 main = BLCQtMain() 416 if args.workspace is not None: 417 main.load_workspace(args.workspace) 418 main.faders.output = OLAOutput() 419 main.show() 420 ret = app.exec_() 421 main.terminate() 422 sys.exit(ret)