blc - Simple program for rendering QLC+ workspaces properly/without crashing. Developed into BLC2.

git clone https://benconnors.ca/git-repos/blc

About | Log | Files | Refs

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)