summaryrefslogtreecommitdiff
path: root/blc-qt.py
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-07-02 16:37:21 -0400
committerBen Connors <benconnors@outlook.com>2019-07-02 16:37:21 -0400
commit94da9ad80926a4ff38ced487f6246c819e0e4413 (patch)
tree3ef454dacbb93293cbd5fcdf3e46633783ebeba8 /blc-qt.py
parent4b803acbb9b4c50efe8bc11efbfb57590ea76af2 (diff)
Add Qt interface prototypeHEADmaster
Diffstat (limited to 'blc-qt.py')
-rwxr-xr-xblc-qt.py422
1 files changed, 422 insertions, 0 deletions
diff --git a/blc-qt.py b/blc-qt.py
new file mode 100755
index 0000000..325c1d9
--- /dev/null
+++ b/blc-qt.py
@@ -0,0 +1,422 @@
+#!/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)