#!/usr/bin/env python3 import locale import argparse import sys import time from PyQt5.QtCore import Qt, QVariant, QTimer from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QGroupBox, QLabel, QDialog, QSlider, QVBoxLayout, QHBoxLayout, QProxyStyle, QStyle, QMainWindow, QMenu, QAction, QFileDialog, QComboBox from blc.workspace import Workspace, CHASER, SHOW, QLC_INFTY from blc.audio import DefaultAudioPlayer, AudioPlayer, FFPlayer from blc.output import LightingOutput from blc.ola import OLAOutput class DirectSliderStyle(QProxyStyle): def styleHint(self, hint, option=0, widget=0, returnData=0): if hint == QStyle.SH_Slider_AbsoluteSetButtons: return (Qt.LeftButton | Qt.MidButton | Qt.RightButton) return super().styleHint(hint, option, widget, returnData) class QFader(QGroupBox): """Class for creating a single fader. Allows for the value to be set by the application and overriden by the user. @param title the title for the fader @param parent the parent widget """ @property def sliderValue(self): """Return the fader's current value.""" return self.slider.value() @sliderValue.setter def sliderValue(self, value): self._sliderValue = value if not self.held: self.slider.setValue(value) def _sliderPressed(self): if not self.held: self.held = True self.button.setDisabled(False) self._sliderMoved(self.slider.value()) def _releaseHold(self): self.held = False self.button.setDisabled(True) self.slider.setValue(self._sliderValue) self._sliderMoved(self._sliderValue) def _sliderMoved(self, value): if self.on_update is not None: self.on_update(self.channel, value) def __init__(self, title, channel=None, on_update=None, parent=None): super().__init__(title=title, parent=parent) self.slider = QSlider(Qt.Vertical, self) self.slider.setStyle(DirectSliderStyle(self.slider.style())) self.slider.setMaximum(255) self.slider.setMinimum(0) self.slider.setMinimumHeight(200) self.channel = channel self.on_update = on_update self.slider.sliderMoved.connect(self._sliderMoved) self._sliderValue = 0 self.held = False self.label = QLabel(self) self.label.setText('0') self.label.setAlignment(Qt.AlignCenter) self.slider.valueChanged.connect(lambda a: self.label.setText(str(a))) self.slider.sliderPressed.connect(self._sliderPressed) self.button = QPushButton("", self) self.button.setDisabled(True) self.button.clicked.connect(self._releaseHold) layout = QVBoxLayout() layout.setAlignment(Qt.AlignCenter) layout.addWidget(self.slider) layout.addWidget(self.label) layout.addWidget(self.button) self.setLayout(layout) self.setFixedWidth(45) class QFaderBank(QGroupBox): """Class for creating a bank of faders. @param title the title of the fader bank @param faders a list of (address, title) elements, where address is unique @param parent the parent widget """ def set_values(self, values): values = tuple(values) for c, v in values: ua = (*c,) if ua not in self.sliders: continue self.sliders[ua].sliderValue = v if self.output is not None: self.output.set_values(values) def slider_changed(self, channel, value): if self.output is not None: self.output.set_values(((channel, value),)) def __init__(self, title="Faders", faders=(), output=None, parent=None): super().__init__(title=title, parent=parent) layout = QHBoxLayout() layout.setSpacing(0) self.sliders = {} for a, t in faders: ua = (*a,) slider = QFader(t, channel=ua, on_update=self.slider_changed, parent=self) layout.addWidget(slider) self.sliders[ua] = slider self.setLayout(layout) self.output = output class QtRenderer: def _start(self): """Start the function.""" if self.running: raise ValueError("Already running") self.nx = 0 self.tnx = 0 self.data = None self.time_elapsed = 0 self.last_rendered = 0 self.last_time = None self.running = True self.aplayers = set() self.audio_players = [] def play(self): if not self.running: self._start() else: self.last_time = time.monotonic() for ap in self.audio_players: ap.play() self.timer.start(self.minnx) def _prune_ap(self): nap = [] for i in self.audio_players: if not i.playing: i.terminate() else: nap.append(i) self.audio_players = nap def pause(self): self.timer.stop() self._prune_ap() for ap in self.audio_players: ap.pause() def stop(self): self.timer.stop() self.running = False self._prune_ap() for ap in self.audio_players: ap.stop() def _do_render(self): t = int(1000*self.time_elapsed+1) + self.minnx vals, acues, self.nx, self.data = self.f.render(t, data=self.data) self.nx = max(self.minnx, self.nx) self.tnx = self.nx/1000 for c, v in vals: self.values[c] = v for st, aid, fname, *_ in acues: if aid not in self.aplayers: self.aplayers.add(aid) ap = self.ap(fname) ap.volume = 100 self.audio_players.append(ap) self.anext.append((st, ap)) self.last_rendered = self.time_elapsed def render_step(self): """Output the current step and render the next one.""" if self.last_time is None: self._do_render() self.nx = 0 self.tnx = 0 self.last_time = time.monotonic() else: t = time.monotonic() self.time_elapsed += t - self.last_time self.last_time = t self.timestep(self.time_elapsed) self.lo.set_values(tuple(self.values.items())) for st, ap in self.anext: ap.play(max(int(self.time_elapsed*1000+1)-st, 0)) self.anext = [] self._do_render() def advance(self): """Advance the function, if possible. It is not an error to call this function when dealing with non-Advanceable toplevel functions; this will just do nothing. """ ## TODO: Make this work when not stalled if not self.running: raise ValueError("Cannot advance a function that has not been started!") t = 1000*self.time_elapsed self.data = self.f.advance(t, self.data) *_, self.data = self.f.render(t, data=self.data) self.nx = self.minnx def terminate(self): for i in self.audio_players: i.terminate() def __init__(self, f, timer: QTimer, timestep, lo:LightingOutput, ap: AudioPlayer=DefaultAudioPlayer, minnx=20): if f.type not in (SHOW, CHASER): raise ValueError("Only Shows and Chasers may be used as toplevel functions") self.time_elapsed = 0 self.last_rendered = 0 self.last_time = 0 self.f = f self.minnx = minnx self.nx = 0 self.data = None self.values = {c: 0 for c in self.f.scope} self.lo = lo self.ap = ap self.aplayers = set() self.audio_players = [] self.anext = [] self.timestep = timestep self.timer = timer self.running = False self.tnx = 0 self.tminnx = self.minnx/1000 self.timer.timeout.connect(self.render_step) class BLCQtMain(QMainWindow): def blackout(self): self.faders.set_values(((i, 0) for i in self.channels)) def ask_load_workspace(self): fname, _ = QFileDialog.getOpenFileName(self, "Open File", "", "QLC Workspaces (*.qxw)") if not fname: return self.load_workspace(fname) def load_workspace(self, fname): ## Load the workspace self.workspace = Workspace.load(fname) ## Iterate over all fixtures and set them to 0 self.channels = sum((tuple(f.channels) for f in self.workspace.fixtures.values()), ()) self.blackout() ## Update the list of functions functions = sorted((f for f in self.workspace.functions.values() if f.type == SHOW or (f.type == CHASER and f.is_chaser) ), key=lambda f: (f.type, f.name, f.id)) self.function_select.clear() for n, f in enumerate(functions): self.function_select.insertItem(n, ("[C] " if f.type == CHASER else "[S] ")+f.name, QVariant((f.id))) if functions: self._choose_function(0) def _choose_function(self, index: int): fid = self.function_select.currentData() if self.function is not None and self.function.id == fid: return self.function = self.workspace.functions[fid] self.timer = QTimer() if self.renderer is not None: self.renderer.terminate() self.renderer = QtRenderer(self.function, self.timer, self._update_time, self.faders) self.func_label.setText("Current Function: None") self._set_controls(True, False, False, False) def _update_time(self, t: float): if 1000*t > self.function.actual_duration: self._stop() elif self.function.type == CHASER and self.renderer.data is not None: cs = self.renderer.data.current_step self.func_label.setText("Current Function: "+self.function.steps[cs].function.name+(" (%d)" % (cs+1))) self.time_label.setText("Time: %.1fs" % t) def _set_controls(self, play, pause, stop, advance): self.play_button.setEnabled(play) self.pause_button.setEnabled(pause) self.stop_button.setEnabled(stop) self.advance_button.setEnabled(advance) def _play(self): self._set_controls(False, True, True, True) if self.function.type == SHOW: self.func_label.setText("Current Function: Show") self.function_select.setEnabled(False) self.renderer.play() def _stop(self): self.renderer.stop() self._set_controls(True, False, False, False) self.function_select.setEnabled(True) self._update_time(0) self.func_label.setText("Current Function: None") self.blackout() def _pause(self): self.renderer.pause() self._set_controls(True, False, True, False) def _advance(self): self.renderer.advance() def terminate(self): if self.renderer is not None: self.renderer.terminate() def __init__(self): super().__init__() self.root = QWidget(self) self.setWindowTitle("BLC-Qt") self.menu = self.menuBar() self.fileMenu = self.menu.addMenu("File") loadw = QAction("Load Workspace", self.root) loadw.triggered.connect(self.ask_load_workspace) self.fileMenu.addAction(loadw) self.faders = QFaderBank(title="Channels", faders=(((0, i), str(i)) for i in range(1,37)), parent=self.root) function_box = QGroupBox(self, title="Functions") self.function_select = QComboBox(function_box) self.function_select.activated.connect(self._choose_function) self.time_label = QLabel(function_box) self.time_label.setText("Time: 0.0s") self.func_label = QLabel(function_box) self.func_label.setText("Current Function: None") self.play_button = QPushButton("Play", function_box) self.play_button.clicked.connect(self._play) self.pause_button = QPushButton("Pause", function_box) self.pause_button.clicked.connect(self._pause) self.stop_button = QPushButton("Stop", function_box) self.stop_button.clicked.connect(self._stop) self.advance_button = QPushButton("Advance", function_box) self.advance_button.clicked.connect(self._advance) self._set_controls(False, False, False, False) fblayout = QVBoxLayout() fblayout.addWidget(self.function_select) fblayout.addWidget(self.func_label) fblayout.addWidget(self.play_button) fblayout.addWidget(self.stop_button) fblayout.addWidget(self.pause_button) fblayout.addWidget(self.advance_button) fblayout.addWidget(self.time_label) function_box.setLayout(fblayout) layout = QVBoxLayout() layout.addWidget(self.faders) layout.addWidget(function_box) self.root.setLayout(layout) self.setCentralWidget(self.root) self.workspace = None self.function = None self.timer = None self.renderer = None self.channels = [] class PrintOutput: def set_values(self, values): for c, v in values: _, a = c #print("%3d to %3d" % (a, v)) parser = argparse.ArgumentParser() parser.add_argument("workspace", nargs="?", action="store", type=str, help="workspace file to load") args = parser.parse_args() app = QApplication([]) ## Necessary for MPV to work, the QApplication constructor changes this locale.setlocale(locale.LC_NUMERIC, "C") main = BLCQtMain() if args.workspace is not None: main.load_workspace(args.workspace) main.faders.output = OLAOutput() main.show() ret = app.exec_() main.terminate() sys.exit(ret)