diff --git a/Makefile b/Makefile index 1323064..8cdaff8 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ # Created by falkTX # -PYUIC = pyuic4 -PYRCC = pyrcc4 +PYUIC = pyuic4 --pyqt3-wrapper +PYRCC = pyrcc4 -py3 all: build @@ -14,21 +14,24 @@ build: UI RES LANG UI: tools tools: \ - src/ui_logs.py + src/ui_logs.py src/ui_render.py src/ui_logs.py: src/ui/logs.ui - $(PYUIC) -w -o src/ui_logs.py $< + $(PYUIC) -o src/ui_logs.py $< + +src/ui_render.py: src/ui/render.ui + $(PYUIC) -o src/ui_render.py $< RES: src/icons_rc.py src/icons_rc.py: src/icons/icons.qrc - $(PYRCC) -py3 -o src/icons_rc.py $< + $(PYRCC) -o src/icons_rc.py $< LANG: # pylupdate4 -verbose src/lang/lang.pro # lrelease src/lang/lang.pro clean: - rm -f *~ src/*~ src/*.pyc src/*.so src/ui_*.py src/icons_rc.py + rm -f *~ src/*~ src/*.pyc src/*.dll src/*.so src/ui_*.py src/icons_rc.py distclean: clean diff --git a/src/icons/16x16/document-open.png b/src/icons/16x16/document-open.png new file mode 100644 index 0000000..530940c Binary files /dev/null and b/src/icons/16x16/document-open.png differ diff --git a/src/icons/16x16/media-playback-stop.png b/src/icons/16x16/media-playback-stop.png new file mode 100644 index 0000000..180280e Binary files /dev/null and b/src/icons/16x16/media-playback-stop.png differ diff --git a/src/icons/16x16/media-record.png b/src/icons/16x16/media-record.png new file mode 100644 index 0000000..4819a0c Binary files /dev/null and b/src/icons/16x16/media-record.png differ diff --git a/src/icons/48x48/media-record.png b/src/icons/48x48/media-record.png new file mode 100644 index 0000000..a6e792f Binary files /dev/null and b/src/icons/48x48/media-record.png differ diff --git a/src/icons/icons.qrc b/src/icons/icons.qrc index fe86639..45b070e 100644 --- a/src/icons/icons.qrc +++ b/src/icons/icons.qrc @@ -1,6 +1,10 @@ + 16x16/document-open.png 16x16/edit-delete.png + 16x16/media-record.png + 16x16/media-playback-stop.png 16x16/window-close.png + 48x48/media-record.png diff --git a/src/jacklib.py b/src/jacklib.py index f0ebaa4..9f299a8 100644 --- a/src/jacklib.py +++ b/src/jacklib.py @@ -781,7 +781,6 @@ def midi_get_lost_event_count(port_buffer): # Session - def set_timebase_callback(client, session_callback, arg): jacklib.jack_set_timebase_callback.argtypes = [POINTER(jack_client_t), JackSessionCallback, c_void_p] jacklib.jack_set_timebase_callback.restype = c_int diff --git a/src/jacklib_helpers.py b/src/jacklib_helpers.py new file mode 100644 index 0000000..548807f --- /dev/null +++ b/src/jacklib_helpers.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Helper functions for extra jacklib functionality +# Copyright (C) 2012 Filipe Coelho +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# For a full copy of the GNU General Public License see the COPYING file + +import jacklib + +def get_jack_status_error_string(c_status): + status = c_status.value + error_string = "" + + if (status & jacklib.JackFailure): + error_string += "Overall operation failed;\n" + if (status & jacklib.JackInvalidOption): + error_string += "The operation contained an invalid or unsupported option;\n" + if (status & jacklib.JackNameNotUnique): + error_string += "The desired client name was not unique;\n" + if (status & jacklib.JackServerStarted): + error_string += "The JACK server was started as a result of this operation;\n" + if (status & jacklib.JackServerFailed): + error_string += "Unable to connect to the JACK server;\n" + if (status & jacklib.JackServerError): + error_string += "Communication error with the JACK server;\n" + if (status & jacklib.JackNoSuchClient): + error_string += "Requested client does not exist;\n" + if (status & jacklib.JackLoadFailure): + error_string += "Unable to load internal client;\n" + if (status & jacklib.JackInitFailure): + error_string += "Unable to initialize client;\n" + if (status & jacklib.JackShmFailure): + error_string += "Unable to access shared memory;\n" + if (status & jacklib.JackVersionError): + error_string += "Client's protocol version does not match;\n" + if (status & jacklib.JackBackendError): + error_string += "Backend Error;\n" + if (status & jacklib.JackClientZombie): + error_string += "Client is being shutdown against its will;\n" + + if (error_string): + error_string = error_string.strip().rsplit(";", 1)[0]+"." + + return error_string diff --git a/src/logs.py b/src/logs.py index 01033b1..6f43fad 100644 --- a/src/logs.py +++ b/src/logs.py @@ -215,7 +215,7 @@ class LogsReadThread(QThread): if (self.LOG_FILE_LADISH): self.log_ladish_file.close() -# Class Window +# Logs Window class LogsW(QDialog, ui_logs.Ui_LogsW): LOG_PATH = os.path.join(HOME, ".log") diff --git a/src/render.py b/src/render.py new file mode 100644 index 0000000..6b8bb93 --- /dev/null +++ b/src/render.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# JACK-Capture frontend, with freewheel and transport support +# Copyright (C) 2012 Filipe Coelho +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# For a full copy of the GNU General Public License see the COPYING file + +# Imports (Global) +from PyQt4.QtCore import pyqtSlot, Qt, QProcess, QTime, QTimer, SIGNAL, SLOT +from PyQt4.QtGui import QDialog +from time import sleep + +# Imports (Custom Stuff) +import ui_render +from shared import * +from jacklib_helpers import * + +global jack_client +jack_client = None + +# Render Window +class RenderW(QDialog, ui_render.Ui_RenderW): + def __init__(self, parent, flags): + QDialog.__init__(self, parent, flags) + self.setupUi(self) + + # ------------------------------------------------------------- + # Get JACK client and base information + + global jack_client + if (jack_client): + self.m_jack_client = jack_client + self.m_closeClient = False + else: + self.m_jack_client = jacklib.client_open("Render-Dialog", jacklib.JackNoStartServer, None) + self.m_closeClient = True + + self.m_buffer_size = jacklib.get_buffer_size(self.m_jack_client) + for i in range(self.cb_buffer_size.count()): + if (int(self.cb_buffer_size.itemText(i)) == self.m_buffer_size): + self.cb_buffer_size.setCurrentIndex(i) + + self.m_sample_rate = jacklib.get_sample_rate(self.m_jack_client) + + # ------------------------------------------------------------- + # Internal stuff + + self.m_max_time = 180 + self.m_last_time = 0 + self.m_freewheel = False + + self.m_timer = QTimer(self) + self.m_process = QProcess(self) + + # ------------------------------------------------------------- + # Set-up GUI stuff + + # Get List of formats + self.m_process.start("jack_capture", ["-pf"]) + self.m_process.waitForFinished() + + formats = str(self.m_process.readAllStandardOutput(), encoding="ascii").split(" ") + for i in range(len(formats)-1): + self.cb_format.addItem(formats[i]) + if (formats[i] == "wav"): + self.cb_format.setCurrentIndex(i) + + self.cb_depth.setCurrentIndex(4) #Float + self.rb_stereo.setChecked(True) + + self.te_end.setTime(QTime(0, 3, 0)) + self.progressBar.setMinimum(0) + self.progressBar.setMaximum(0) + self.progressBar.setValue(0) + + self.b_render.setIcon(getIcon("media-record")) + self.b_stop.setIcon(getIcon("media-playback-stop")) + self.b_close.setIcon(getIcon("window-close")) + self.b_open.setIcon(getIcon("document-open")) + self.b_stop.setVisible(False) + self.le_folder.setText(HOME) + + # ------------------------------------------------------------- + # Set-up connections + + self.connect(self.b_render, SIGNAL("clicked()"), SLOT("slot_renderStart()")) + self.connect(self.b_stop, SIGNAL("clicked()"), SLOT("slot_renderStop()")) + self.connect(self.b_open, SIGNAL("clicked()"), SLOT("slot_getAndSetPath()")) + self.connect(self.b_now_start, SIGNAL("clicked()"), SLOT("slot_setStartNow()")) + self.connect(self.b_now_end, SIGNAL("clicked()"), SLOT("slot_setEndNow()")) + self.connect(self.te_start, SIGNAL("timeChanged(const QTime)"), SLOT("slot_updateStartTime(const QTime)")) + self.connect(self.te_end, SIGNAL("timeChanged(const QTime)"), SLOT("slot_updateEndTime(const QTime)")) + self.connect(self.m_timer, SIGNAL("timeout()"), SLOT("slot_updateProgressbar()")) + + @pyqtSlot() + def slot_renderStart(self): + if (os.path.exists(self.le_folder.text()) == False): + QMessageBox.warning(self, self.tr("Warning"), self.tr("The selected directory does not exist. Please choose a valid one.")) + return + + self.group_render.setEnabled(False) + self.group_time.setEnabled(False) + self.group_encoding.setEnabled(False) + self.b_render.setVisible(False) + self.b_stop.setVisible(True) + self.b_close.setEnabled(False) + + self.m_freewheel = (self.cb_render_mode.currentIndex() == 1) + new_buffer_size = int(self.cb_buffer_size.currentText()) + + time_start = self.te_start.time() + time_end = self.te_end.time() + min_time = (time_start.hour()*3600)+(time_start.minute()*60)+(time_start.second()) + max_time = (time_end.hour()*3600)+(time_end.minute()*60)+(time_end.second()) + self.m_max_time = max_time + + self.progressBar.setMinimum(min_time) + self.progressBar.setMaximum(max_time) + self.progressBar.setValue(min_time) + self.progressBar.update() + + if (self.m_freewheel): + self.m_timer.setInterval(100) + else: + self.m_timer.setInterval(500) + + arguments = [] + + # Bit depth + arguments.append("-b") + arguments.append(self.cb_depth.currentText()) + + # Channels + arguments.append("-c") + if (self.rb_mono.isChecked()): + arguments.append("1") + elif (self.rb_stereo.isChecked()): + arguments.append("2") + else: + arguments.append(str(self.sb_channels.value())) + + # Format + arguments.append("-f") + arguments.append(self.cb_format.currentText()) + + # Controlled by transport + arguments.append("-jt") + + # Silent mode + arguments.append("-dc") + arguments.append("-s") + + # Change current directory + os.chdir(self.le_folder.text()) + + if (new_buffer_size != jacklib.get_buffer_size(self.m_jack_client)): + print("NOTICE: buffer size changed before render") + jacklib.set_buffer_size(self.m_jack_client, new_buffer_size) + + if (jacklib.transport_query(self.m_jack_client, None) > jacklib.JackTransportStopped): # >TransportStopped is rolling/starting + jacklib.transport_stop(self.m_jack_client) + + jacklib.transport_locate(self.m_jack_client, min_time*self.m_sample_rate) + self.m_last_time = -1 + + self.m_process.start("jack_capture", arguments) + self.m_process.waitForStarted() + + if (self.m_freewheel): + sleep(1) + print("NOTICE: rendering in freewheel mode") + jacklib.set_freewheel(jack_client, 1) + + self.m_timer.start() + jacklib.transport_start(self.m_jack_client) + + @pyqtSlot() + def slot_renderStop(self): + jacklib.transport_stop(self.m_jack_client) + + if (self.m_freewheel): + jacklib.set_freewheel(self.m_jack_client, 0) + + sleep(1) + + self.m_process.close() + self.m_timer.stop() + + self.group_render.setEnabled(True) + self.group_time.setEnabled(True) + self.group_encoding.setEnabled(True) + self.b_render.setVisible(True) + self.b_stop.setVisible(False) + self.b_close.setEnabled(True) + + self.progressBar.setMinimum(0) + self.progressBar.setMaximum(0) + self.progressBar.setValue(0) + self.progressBar.update() + + # Restore buffer size + new_buffer_size = jacklib.get_buffer_size(self.m_jack_client) + if (new_buffer_size != self.m_buffer_size): + jacklib.set_buffer_size(self.m_jack_client, new_buffer_size) + + @pyqtSlot() + def slot_getAndSetPath(self): + getAndSetPath(self, self.le_folder.text(), self.le_folder) + + @pyqtSlot() + def slot_setStartNow(self): + time = jacklib.get_current_transport_frame(self.m_jack_client)/self.m_sample_rate + secs = time % 60 + mins = (time / 60) % 60 + hrs = (time / 3600) % 60 + self.te_start.setTime(QTime(hrs, mins, secs)) + + @pyqtSlot() + def slot_setEndNow(self): + time = jacklib.get_current_transport_frame(self.m_jack_client)/self.m_sample_rate + secs = time % 60 + mins = (time / 60) % 60 + hrs = (time / 3600) % 60 + self.te_end.setTime(QTime(hrs, mins, secs)) + + @pyqtSlot(QTime) + def slot_updateStartTime(self, time): + if (time >= self.te_end.time()): + self.te_end.setTime(time) + self.b_render.setEnabled(False) + else: + self.b_render.setEnabled(True) + + @pyqtSlot(QTime) + def slot_updateEndTime(self, time): + if (time <= self.te_start.time()): + time = self.te_start.setTime(time) + self.b_render.setEnabled(False) + else: + self.b_render.setEnabled(True) + + @pyqtSlot() + def slot_updateProgressbar(self): + time = jacklib.get_current_transport_frame(self.m_jack_client)/self.m_sample_rate + self.progressBar.setValue(time) + + if (time > self.m_max_time or (self.m_last_time > time and self.m_freewheel == False)): + self.slot_renderStop() + + self.m_last_time = time + + def closeEvent(self, event): + if (self.m_closeClient): + jacklib.client_close(self.m_jack_client) + QDialog.closeEvent(self, event) + +# ------------------------------------------------------------- +# Allow to use this as a standalone app +if __name__ == '__main__': + + # Additional imports + import sys + from PyQt4.QtGui import QApplication + + # App initialization + app = QApplication(sys.argv) + + for iPATH in PATH: + if os.path.exists(os.path.join(iPATH, "jack_capture")): + break + else: + QMessageBox.critical(None, app.translate("RenderW", "Error"), app.translate("RenderW", "The 'jack_capture' application is not available.\nIs not possible to render without it!")) + sys.exit(1) + + jack_status = jacklib.jack_status_t(0) + jack_client = jacklib.client_open("Render", jacklib.JackNoStartServer, jacklib.pointer(jack_status)) + + if not jack_client: + QMessageBox.critical(None, app.translate("RenderW", "Error"), app.translate("RenderW", "Could not connect to JACK, possible errors:\n%s" % (get_jack_status_error_string(jack_status)))) + sys.exit(1) + + # Show GUI + gui = RenderW(None, Qt.WindowFlags()) + gui.setWindowIcon(getIcon("media-record", 48)) + gui.show() + + # App-Loop + ret = app.exec_() + + if (jack_client): + jacklib.client_close(jack_client) + + # Exit properly + sys.exit(ret) diff --git a/src/shared.py b/src/shared.py index 1bb1112..6c671b6 100644 --- a/src/shared.py +++ b/src/shared.py @@ -19,7 +19,7 @@ # Imports (Global) import os from PyQt4.QtCore import qDebug, qWarning -from PyQt4.QtGui import QIcon +from PyQt4.QtGui import QIcon, QMessageBox, QFileDialog # Small integrity tests HOME = os.getenv("HOME") @@ -41,3 +41,10 @@ else: # Get Icon from user theme, using our own as backup (Oxygen) def getIcon(icon, size=16): return QIcon.fromTheme(icon, QIcon(":/%ix%i/%s.png" % (size, size, icon))) + +# QLineEdit and QPushButtom combo +def getAndSetPath(self, currentPath, lineEdit): + newPath = QFileDialog.getExistingDirectory(self, self.tr("Set Path"), currentPath, QFileDialog.ShowDirsOnly) + if (newPath): + lineEdit.setText(newPath) + return newPath diff --git a/src/ui/render.ui b/src/ui/render.ui new file mode 100644 index 0000000..a4bd62c --- /dev/null +++ b/src/ui/render.ui @@ -0,0 +1,439 @@ + + + RenderW + + + + 0 + 0 + 585 + 332 + + + + Render + + + + + + + + 24 + + + + + + + &Render + + + + :/16x16/media-record.png:/16x16/media-record.png + + + + + + + &Stop + + + + :/16x16/media-playback-stop.png:/16x16/media-playback-stop.png + + + + + + + &Close + + + + :/16x16/window-close.png:/16x16/window-close.png + + + + + + + + + Qt::Horizontal + + + + + + + Render Options + + + + + + Render &Mode: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + cb_render_mode + + + + + + + + Realtime + + + + + Freewheel + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 275 + 20 + + + + + + + + &Buffer Size: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + cb_buffer_size + + + + + + + true + + + + 32 + + + + + 64 + + + + + 128 + + + + + 256 + + + + + 512 + + + + + 1024 + + + + + 2048 + + + + + 4096 + + + + + 8192 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 275 + 20 + + + + + + + + Output folder: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + :/16x16/document-open.png:/16x16/document-open.png + + + + + + + + + + + + Time + + + + + + &Start Time: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + te_start + + + + + + + hh:mm:ss + + + + + + + &End Time: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + te_end + + + + + + + hh:mm:ss + + + + + + + now + + + + + + + now + + + + + + + + + + Encoding + + + + + + &Format: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + cb_format + + + + + + + + + + false + + + ... + + + + + + + Bit &Depth: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + cb_depth + + + + + + + + 8 + + + + + 16 + + + + + 24 + + + + + 32 + + + + + Float + + + + + + + + Mono + + + + + + + Stereo + + + + + + + Outro: + + + + + + + false + + + 1 + + + + + + + + + + + + + + b_close + clicked() + RenderW + close() + + + 465 + 343 + + + 258 + 183 + + + + + rb_outro + toggled(bool) + sb_channels + setEnabled(bool) + + + 405 + 259 + + + 484 + 255 + + + + +