class QJackCaptureMainWindow(QDialog): sample_formats = { "32-bit float": "FLOAT", "8-bit integer": "8", "16-bit integer": "16", "24-bit integer": "24", "32-bit integer": "32", } def __init__(self, parent, jack_client, jack_name=PROGRAM): QDialog.__init__(self, parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.fFreewheel = False self.fLastTime = -1 self.fMaxTime = 180 self.fTimer = QTimer(self) self.fProcess = QProcess(self) self.fJackClient = jack_client self.fJackName = jack_name self.fBufferSize = self.fJackClient.get_buffer_size() self.fSampleRate = self.fJackClient.get_sample_rate() # Selected ports used as recording sources self.rec_sources = set() self.createUi() self.loadSettings() self.populatePortLists(init=True) # listen to changes to JACK ports self._refresh_timer = None self.fJackClient.ports_changed.connect(self.slot_refreshPortsLists) @Slot() def slot_refreshPortsLists(self, delay=200): if not self._refresh_timer or not self._refresh_timer.isActive(): log.debug("Scheduling port lists refresh in %i ms...", delay) self._refresh_timer = QTimer() self._refresh_timer.setSingleShot(True) self._refresh_timer.timeout.connect(self.populatePortLists) self._refresh_timer.start(delay) def populateFileFormats(self): # Get list of supported file formats self.fProcess.start(gJackCapturePath, ["-pf"]) self.fProcess.waitForFinished() formats = [] for fmt in str(self.fProcess.readAllStandardOutput(), encoding="utf-8").split(): fmt = fmt.strip() if fmt: formats.append(fmt) # Put all file formats in combo-box, select 'wav' option self.ui.cb_format.clear() for i, fmt in enumerate(sorted(formats)): self.ui.cb_format.addItem(fmt) if fmt == "wav": self.ui.cb_format.setCurrentIndex(i) def populateSampleFormats(self): # Put all sample formats in combo-box, select 'FLOAT' option self.ui.cb_depth.clear() for i, (label, fmt) in enumerate(self.sample_formats.items()): self.ui.cb_depth.addItem(label, fmt) if fmt == "FLOAT": self.ui.cb_depth.setCurrentIndex(i) def populatePortLists(self, init=False): log.debug("Populating port lists (init=%s)...", init) if init: self.outputs_model = QStandardItemModel(0, 1, self) self.inputs_model = QStandardItemModel(0, 1, self) else: self.outputs_model.clear() self.inputs_model.clear() output_ports = list(self.fJackClient.get_output_ports()) self.populatePortList(self.outputs_model, self.ui.tree_outputs, output_ports) input_ports = list(self.fJackClient.get_input_ports()) self.populatePortList(self.inputs_model, self.ui.tree_inputs, input_ports) # Remove ports, which are no longer present, from recording sources all_ports = set((p.client, p.name) for p in output_ports) all_ports |= set((p.client, p.name) for p in input_ports) self.rec_sources.intersection_update(all_ports) self.slot_toggleRecordingSource() def makePortTooltip(self, port): s = [] if port.pretty_name: s.append(f"<b>Pretty name:</b> <em>{port.pretty_name}</em><br>") s.append(f"<b>Port:</b> <tt>{port.client}:{port.name}</tt><br>") for i, alias in enumerate(port.aliases, 1): s.append(f"<b>Alias {i}:</b> <tt>{alias}</tt><br>") s.append(f"<b>UUID:</b> <tt>{port.uuid}</tt>") return "<small>{}</small>".format("\n".join(s)) def populatePortList(self, model, tv, ports): tv.setModel(model) root = model.invisibleRootItem() portsdict = {} for port in ports: if port.client not in portsdict: portsdict[port.client] = [] portsdict[port.client].append(port) for client in humansorted(portsdict): clientitem = QStandardItem(client) for port in humansorted(portsdict[client], key=attrgetter("group", "order", "name")): portspec = (port.client, port.name) if port.pretty_name: label = "%s (%s)" % (port.pretty_name, port.name) else: label = port.name portitem = QStandardItem(label) portitem.setData(portspec) portitem.setCheckable(True) portitem.setUserTristate(False) # Check box toggling is done in the treeview clicked handler "on_port_clicked" portitem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) portitem.setToolTip(self.makePortTooltip(port)) if portspec in self.rec_sources: portitem.setCheckState(2) clientitem.appendRow(portitem) root.appendRow(clientitem) tv.expandAll() def createUi(self): # ------------------------------------------------------------- # Set-up GUI stuff for i in range(self.ui.cb_buffer_size.count()): if int(self.ui.cb_buffer_size.itemText(i)) == self.fBufferSize: self.ui.cb_buffer_size.setCurrentIndex(i) break else: self.ui.cb_buffer_size.addItem(str(self.fBufferSize)) self.ui.cb_buffer_size.setCurrentIndex( self.ui.cb_buffer_size.count() - 1) self.populateFileFormats() self.populateSampleFormats() self.ui.rb_stereo.setChecked(True) self.ui.te_end.setTime(QTime(0, 3, 0)) self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(1) self.ui.progressBar.setValue(0) self.ui.b_render.setIcon(get_icon("media-record")) self.ui.b_stop.setIcon(get_icon("media-playback-stop")) self.ui.b_close.setIcon(get_icon("window-close")) self.ui.b_open.setIcon(get_icon("document-open")) self.ui.b_stop.setVisible(False) self.ui.le_folder.setText(expanduser("~")) # ------------------------------------------------------------- # Set-up connections self.ui.b_render.clicked.connect(self.slot_renderStart) self.ui.b_stop.clicked.connect(self.slot_renderStop) self.ui.b_open.clicked.connect(self.slot_getAndSetPath) self.ui.b_now_start.clicked.connect(self.slot_setStartNow) self.ui.b_now_end.clicked.connect(self.slot_setEndNow) self.ui.te_start.timeChanged.connect(self.slot_updateStartTime) self.ui.te_end.timeChanged.connect(self.slot_updateEndTime) self.ui.group_time.clicked.connect(self.slot_transportChecked) self.ui.rb_source_default.toggled.connect( self.slot_toggleRecordingSource) self.ui.rb_source_manual.toggled.connect( self.slot_toggleRecordingSource) self.ui.rb_source_selected.toggled.connect( self.slot_toggleRecordingSource) self.fTimer.timeout.connect(self.slot_updateProgressbar) for tv in (self.ui.tree_outputs, self.ui.tree_inputs): menu = QMenu() menu.addAction(get_icon("expand-all"), self.tr("E&xpand all"), tv.expandAll) menu.addAction(get_icon("collapse-all"), self.tr("&Collapse all"), tv.collapseAll) menu.addSeparator() menu.addAction( get_icon("list-select-all"), self.tr("&Select all in group"), partial(self.on_select_port_group, tv), ) menu.addAction( get_icon("list-select-none"), self.tr("&Unselect all in group"), partial(self.on_select_port_group, tv, enable=False), ) menu.addSeparator() if tv is self.ui.tree_outputs: menu.addAction( get_icon("select-none"), self.tr("Unselect all &outputs"), partial(self.on_clear_all_ports, tv), ) else: menu.addAction( get_icon("select-none"), self.tr("Unselect all &inputs"), partial(self.on_clear_all_ports, tv), ) tv.setContextMenuPolicy(Qt.CustomContextMenu) tv.customContextMenuRequested.connect( partial(self.on_port_menu, treeview=tv, menu=menu)) tv.clicked.connect(self.on_port_clicked) def enable_port(self, item, enable=True): item.setCheckState(2 if enable else 0) port = item.data() if enable: self.rec_sources.add(port) else: self.rec_sources.discard(port) def on_port_menu(self, pos, treeview=None, menu=None): if treeview and menu: menu.popup(treeview.viewport().mapToGlobal(pos)) def foreach_item(self, model, parent, func, leaves_only=True): for row in range(model.rowCount(parent)): index = model.index(row, 0, parent) is_leaf = not model.hasChildren(index) if is_leaf or not leaves_only: func(model.itemFromIndex(index)) if not is_leaf: self.foreach_item(model, index, func) def on_clear_all_ports(self, treeview): self.foreach_item(treeview.model(), QModelIndex(), partial(self.enable_port, enable=False)) self.checkRecordEnable() def on_port_clicked(self, index): model = index.model() item = model.itemFromIndex(index) if not model.hasChildren(index): self.enable_port(item, not item.checkState()) self.checkRecordEnable() def on_select_port_group(self, treeview, enable=True): index = treeview.currentIndex() model = index.model() if not model.hasChildren(index): index = index.parent() self.foreach_item(model, index, partial(self.enable_port, enable=enable)) self.checkRecordEnable() @Slot() def slot_renderStart(self): if not exists(self.ui.le_folder.text()): QMessageBox.warning( self, self.tr("Warning"), self. tr("The selected directory does not exist. Please choose a valid one." ), ) return timeStart = self.ui.te_start.time() timeEnd = self.ui.te_end.time() minTime = (timeStart.hour() * 3600) + (timeStart.minute() * 60) + (timeStart.second()) maxTime = (timeEnd.hour() * 3600) + (timeEnd.minute() * 60) + (timeEnd.second()) newBufferSize = int(self.ui.cb_buffer_size.currentText()) useTransport = self.ui.group_time.isChecked() self.fFreewheel = self.ui.rb_freewheel.isChecked() self.fLastTime = -1 self.fMaxTime = maxTime if self.fFreewheel: self.fTimer.setInterval(100) else: self.fTimer.setInterval(500) self.ui.group_render.setEnabled(False) self.ui.group_time.setEnabled(False) self.ui.group_encoding.setEnabled(False) self.ui.b_render.setVisible(False) self.ui.b_stop.setVisible(True) self.ui.b_close.setEnabled(False) if useTransport: self.ui.progressBar.setFormat("%p%") self.ui.progressBar.setMinimum(minTime) self.ui.progressBar.setMaximum(maxTime) self.ui.progressBar.setValue(minTime) else: self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(0) self.ui.progressBar.setValue(0) self.ui.progressBar.update() arguments = [] # JACK client name arguments.append("-jn") arguments.append(self.fJackName) # Filename prefix arguments.append("-fp") arguments.append(self.ui.le_prefix.text()) # File format arguments.append("-f") arguments.append(self.ui.cb_format.currentText()) # Sanple format (bit depth, int/float) arguments.append("-b") arguments.append(self.ui.cb_depth.currentData()) # Channels arguments.append("-c") if self.ui.rb_mono.isChecked(): arguments.append("1") elif self.ui.rb_stereo.isChecked(): arguments.append("2") else: arguments.append(str(self.ui.sb_channels.value())) # Recording sources if self.ui.rb_source_manual.isChecked(): arguments.append("-mc") elif self.ui.rb_source_selected.isChecked(): for client, port in self.rec_sources: arguments.append("-p") arguments.append("{}:{}".format(client, port)) # Controlled only by freewheel if self.fFreewheel: arguments.append("-jf") # Controlled by transport elif useTransport: arguments.append("-jt") # Silent mode arguments.append("--daemon") # Extra arguments extra_args = self.ui.le_extra_args.text().strip() if extra_args: arg_list = shlex.split(extra_args) arguments.extend(arg_list) # Change current directory os.chdir(self.ui.le_folder.text()) if newBufferSize != self.fJackClient.get_buffer_size(): log.info("Buffer size changed before render.") self.fJackClient.set_buffer_size(newBufferSize) if useTransport: if self.fJackClient.transport_running(): # rolling or starting self.fJackClient.transport_stop() self.fJackClient.transport_locate(minTime * self.fSampleRate) log.debug("jack_capture command line args: %r", arguments) self.fProcess.start(gJackCapturePath, arguments) status = self.fProcess.waitForStarted() if not status: self.fProcess.close() log.error("Could not start jack_capture.") return if self.fFreewheel: log.info("Rendering in freewheel mode.") sleep(1) self.fJackClient.set_freewheel(True) if useTransport: log.info("Rendering using JACK transport.") self.fTimer.start() self.fJackClient.transport_start() @Slot() def slot_renderStop(self): useTransport = self.ui.group_time.isChecked() if useTransport: self.fJackClient.transport_stop() if self.fFreewheel: self.fJackClient.set_freewheel(False) sleep(1) self.fProcess.terminate() # self.fProcess.waitForFinished(5000) if useTransport: self.fTimer.stop() self.ui.group_render.setEnabled(True) self.ui.group_time.setEnabled(True) self.ui.group_encoding.setEnabled(True) self.ui.b_render.setVisible(True) self.ui.b_stop.setVisible(False) self.ui.b_close.setEnabled(True) self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(1) self.ui.progressBar.setValue(0) self.ui.progressBar.update() # Restore buffer size newBufferSize = self.fJackClient.get_buffer_size() if newBufferSize != self.fBufferSize: self.fJackClient.set_buffer_size(newBufferSize) @Slot() def slot_getAndSetPath(self): new_path = QFileDialog.getExistingDirectory(self, self.tr("Set Path"), self.ui.le_folder.text(), QFileDialog.ShowDirsOnly) if new_path: self.ui.le_folder.setText(new_path) @Slot() def slot_setStartNow(self): time = self.fJackClient.transport_frame() // self.fSampleRate secs = time % 60 mins = int(time / 60) % 60 hrs = int(time / 3600) % 60 self.ui.te_start.setTime(QTime(hrs, mins, secs)) @Slot() def slot_setEndNow(self): time = self.fJackClient.transport_frame() // self.fSampleRate secs = time % 60 mins = int(time / 60) % 60 hrs = int(time / 3600) % 60 self.ui.te_end.setTime(QTime(hrs, mins, secs)) @Slot(QTime) def slot_updateStartTime(self, time): if time >= self.ui.te_end.time(): self.ui.te_end.setTime(time) renderEnabled = False else: renderEnabled = True if self.ui.group_time.isChecked(): self.ui.b_render.setEnabled(renderEnabled) @Slot(QTime) def slot_updateEndTime(self, time): if time <= self.ui.te_start.time(): self.ui.te_start.setTime(time) renderEnabled = False else: renderEnabled = True if self.ui.group_time.isChecked(): self.ui.b_render.setEnabled(renderEnabled) @Slot(bool) def slot_toggleRecordingSource(self, dummy=None): enabled = self.ui.rb_source_selected.isChecked() self.ui.tree_outputs.setEnabled(enabled) self.ui.tree_inputs.setEnabled(enabled) self.checkRecordEnable() @Slot(bool) def slot_transportChecked(self, dummy=None): self.checkRecordEnable() @Slot() def slot_updateProgressbar(self): time = self.fJackClient.transport_frame() / self.fSampleRate self.ui.progressBar.setValue(time) if time > self.fMaxTime or (self.fLastTime > time and not self.fFreewheel): self.slot_renderStop() self.fLastTime = time def checkRecordEnable(self): enable = True if self.ui.rb_source_selected.isChecked() and not self.rec_sources: enable = False if self.ui.group_time.isChecked( ) and self.ui.te_end.time() <= self.ui.te_start.time(): enable = False self.ui.b_render.setEnabled(enable) log.debug("Recording sources: %s", ", ".join( ("%s:%s" % (c, p) for c, p in self.rec_sources))) def saveSettings(self): settings = QSettings(ORGANIZATION, PROGRAM) if self.ui.rb_mono.isChecked(): channels = 1 elif self.ui.rb_stereo.isChecked(): channels = 2 else: channels = self.ui.sb_channels.value() settings.setValue("Geometry", self.saveGeometry()) settings.setValue("OutputFolder", self.ui.le_folder.text()) settings.setValue("FilenamePrefix", self.ui.le_prefix.text()) settings.setValue("EncodingFormat", self.ui.cb_format.currentText()) settings.setValue("EncodingDepth", self.ui.cb_depth.currentData()) settings.setValue("EncodingChannels", channels) settings.setValue("UseTransport", self.ui.group_time.isChecked()) settings.setValue("StartTime", self.ui.te_start.time()) settings.setValue("EndTime", self.ui.te_end.time()) settings.setValue("ExtraArgs", self.ui.le_extra_args.text().strip()) if self.ui.rb_source_default.isChecked(): settings.setValue("RecordingSource", 0) elif self.ui.rb_source_manual.isChecked(): settings.setValue("RecordingSource", 1) elif self.ui.rb_source_selected.isChecked(): settings.setValue("RecordingSource", 2) settings.beginWriteArray("Sources") for i, (client, port) in enumerate(self.rec_sources): settings.setArrayIndex(i) settings.setValue("Client", client) settings.setValue("Port", port) settings.endArray() def loadSettings(self): settings = QSettings(ORGANIZATION, PROGRAM) self.restoreGeometry(settings.value("Geometry", b"")) outputFolder = settings.value("OutputFolder", get_user_dir("MUSIC")) if isdir(outputFolder): self.ui.le_folder.setText(outputFolder) self.ui.le_prefix.setText( settings.value("FilenamePrefix", "jack_capture_")) encFormat = settings.value("EncodingFormat", "wav", type=str) for i in range(self.ui.cb_format.count()): if self.ui.cb_format.itemText(i) == encFormat: self.ui.cb_format.setCurrentIndex(i) break encDepth = settings.value("EncodingDepth", "FLOAT", type=str) for i in range(self.ui.cb_depth.count()): if self.ui.cb_depth.itemData(i) == encDepth: self.ui.cb_depth.setCurrentIndex(i) break encChannels = settings.value("EncodingChannels", 2, type=int) if encChannels == 1: self.ui.rb_mono.setChecked(True) elif encChannels == 2: self.ui.rb_stereo.setChecked(True) else: self.ui.rb_outro.setChecked(True) self.ui.sb_channels.setValue(encChannels) recSource = settings.value("RecordingSource", 0, type=int) if recSource == 1: self.ui.rb_source_manual.setChecked(True) elif recSource == 2: self.ui.rb_source_selected.setChecked(True) else: self.ui.rb_source_default.setChecked(True) self.ui.group_time.setChecked( settings.value("UseTransport", False, type=bool)) self.ui.te_start.setTime( settings.value("StartTime", self.ui.te_start.time(), type=QTime)) self.ui.te_end.setTime( settings.value("EndTime", self.ui.te_end.time(), type=QTime)) self.ui.le_extra_args.setText(settings.value("ExtraArgs", "", type=str)) size = settings.beginReadArray("Sources") for i in range(size): settings.setArrayIndex(i) client = settings.value("Client", type=str) port = settings.value("Port", type=str) if client and port: self.rec_sources.add((client, port)) settings.endArray() def closeEvent(self, event): self.saveSettings() self.fJackClient.close() QDialog.closeEvent(self, event) def done(self, r): QDialog.done(self, r) self.close()
class ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _get_encoding(self): """Return the encoding/codepage to use.""" enco = 'utf-8' # Currently only cp1252 is allowed? if WIN: import ctypes codepage = to_text_string(ctypes.cdll.kernel32.GetACP()) # import locale # locale.getpreferredencoding() # Differences? enco = 'cp' + codepage return enco def _set_environment(self, environ): """Set the environment on the QProcess.""" if environ: q_environ = self._process.processEnvironment() for k, v in environ.items(): q_environ.insert(k, v) self._process.setProcessEnvironment(q_environ) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() enco = self._get_encoding() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, enco) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) result = [stdout.encode(enco), stderr.encode(enco)] if PY2: stderr = stderr.decode() result[-1] = '' self._result = result if not self._fired: self.sig_finished.emit(self, result[0], result[-1]) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def _start(self): """Start process.""" if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() def terminate(self): """Terminate running processes.""" if self._process.state() == QProcess.Running: try: self._process.terminate() except Exception: pass self._fired = True def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True