cadence/src/shared_canvasjack.py

577 lines
20 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Common/Shared code related to Canvas and JACK
# Copyright (C) 2010-2018 Filipe Coelho <falktx@falktx.com>
#
# 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
from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import QCursor, QFontMetrics, QImage, QPainter
from PyQt5.QtWidgets import QMainWindow, QMenu
# ------------------------------------------------------------------------------------------------------------
# Imports (Custom Stuff)
import patchcanvas
import jacksettings
import logs
import render
from shared import *
from jacklib_helpers import *
# ------------------------------------------------------------------------------------------------------------
# Have JACK2 ?
if DEBUG and jacklib and jacklib.JACK2:
print("Using JACK2, version %s" % cString(jacklib.get_version_string()))
# ------------------------------------------------------------------------------------------------------------
# Static Variables
TRANSPORT_VIEW_HMS = 0
TRANSPORT_VIEW_BBT = 1
TRANSPORT_VIEW_FRAMES = 2
BUFFER_SIZE_LIST = (16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)
SAMPLE_RATE_LIST = (22050, 32000, 44100, 48000, 88200, 96000, 192000)
# ------------------------------------------------------------------------------------------------------------
# Global DBus object
class DBusObject(object):
__slots__ = [
'loop',
'bus',
'a2j',
'jack',
'ladish_control',
'ladish_studio',
'ladish_room',
'ladish_graph',
'ladish_manager',
'ladish_app_iface',
'ladish_app_daemon',
'patchbay'
]
gDBus = DBusObject()
gDBus.loop = None
gDBus.bus = None
gDBus.a2j = None
gDBus.jack = None
gDBus.ladish_control = None
gDBus.ladish_studio = None
gDBus.ladish_room = None
gDBus.ladish_graph = None
gDBus.ladish_app_iface = None
gDBus.ladish_app_daemon = None
gDBus.patchbay = None
# ------------------------------------------------------------------------------------------------------------
# Global JACK object
class JackObject(object):
__slots__ = [
'client'
]
gJack = JackObject()
gJack.client = None
# ------------------------------------------------------------------------------------------------------------
# Abstract Canvas and JACK Class
class AbstractCanvasJackClass(QMainWindow):
XRunCallback = pyqtSignal()
BufferSizeCallback = pyqtSignal(int)
SampleRateCallback = pyqtSignal(int)
ClientRenameCallback = pyqtSignal(str, str)
PortRegistrationCallback = pyqtSignal(int, bool)
PortConnectCallback = pyqtSignal(int, int, bool)
PortRenameCallback = pyqtSignal(int, str, str)
ShutdownCallback = pyqtSignal()
SIGTERM = pyqtSignal()
SIGUSR1 = pyqtSignal()
SIGUSR2 = pyqtSignal()
def __init__(self, appName, UiClass, parent):
QMainWindow.__init__(self, parent)
self.ui = UiClass()
self.ui.setupUi(self)
self.fAppName = appName
self.fCurTransportView = TRANSPORT_VIEW_HMS
self.fLastBPM = None
self.fLastTransportState = None
self.fXruns = -1
self.fBufferSize = 0
self.fSampleRate = 0.0
self.fNextSampleRate = 0.0
self.fLogsW = None
self.scene = None
# -----------------------------------------------------------------
# Abstract calls
def initPorts(self):
pass
def jackStopped(self):
pass
# -----------------------------------------------------------------
# JACK Property change calls
def jack_setBufferSize(self, bufferSize):
if self.fBufferSize == bufferSize:
return
if gJack.client:
failed = bool(jacklib.set_buffer_size(gJack.client, bufferSize) != 0)
else:
failed = bool(jacksettings.setBufferSize(bufferSize))
if failed:
print(self.tr("Failed to change buffer-size as %i, reset to %i") % (bufferSize, self.fBufferSize))
self.ui_setBufferSize(self.fBufferSize, True)
def jack_setSampleRate(self, sampleRate):
if gJack.client:
# Show change-in-future dialog
self.ui_setSampleRate(sampleRate, True)
else:
# Try to set sampleRate via dbus now
if jacksettings.setSampleRate(sampleRate):
self.ui_setSampleRate(sampleRate)
@pyqtSlot(bool)
def slot_jackBufferSize_Menu(self, clicked):
sender = self.sender()
# ignore non-clicks
if not clicked:
sender.blockSignals(True)
sender.setChecked(True)
sender.blockSignals(False)
return
text = sender.text()
if text and text.isdigit():
self.jack_setBufferSize(int(text))
@pyqtSlot(str)
def slot_jackBufferSize_ComboBox(self, text):
if text and text.isdigit():
self.jack_setBufferSize(int(text))
@pyqtSlot(str)
def slot_jackSampleRate_ComboBox(self, text):
if text and text.isdigit():
self.jack_setSampleRate(int(text))
# -----------------------------------------------------------------
# JACK Transport calls
def setTransportView(self, view):
if view == TRANSPORT_VIEW_HMS:
self.fCurTransportView = TRANSPORT_VIEW_HMS
self.ui.label_time.setMinimumWidth(QFontMetrics(self.ui.label_time.font()).width("00:00:00") + 3)
elif view == TRANSPORT_VIEW_BBT:
self.fCurTransportView = TRANSPORT_VIEW_BBT
self.ui.label_time.setMinimumWidth(QFontMetrics(self.ui.label_time.font()).width("000|00|0000") + 3)
elif view == TRANSPORT_VIEW_FRAMES:
self.fCurTransportView = TRANSPORT_VIEW_FRAMES
self.ui.label_time.setMinimumWidth(QFontMetrics(self.ui.label_time.font()).width("000'000'000") + 3)
else:
self.setTransportView(TRANSPORT_VIEW_HMS)
@pyqtSlot(bool)
def slot_transportPlayPause(self, play):
if not gJack.client:
return
if play:
jacklib.transport_start(gJack.client)
else:
jacklib.transport_stop(gJack.client)
self.refreshTransport()
@pyqtSlot()
def slot_transportStop(self):
if not gJack.client:
return
jacklib.transport_stop(gJack.client)
jacklib.transport_locate(gJack.client, 0)
self.refreshTransport()
@pyqtSlot()
def slot_transportBackwards(self):
if self.fSampleRate == 0.0 or not gJack.client:
return
curFrame = jacklib.get_current_transport_frame(gJack.client)
if curFrame == 0:
return
newFrame = curFrame - int(self.fSampleRate*2.5)
if newFrame < 0:
newFrame = 0
jacklib.transport_locate(gJack.client, newFrame)
@pyqtSlot()
def slot_transportForwards(self):
if self.fSampleRate == 0.0 or not gJack.client:
return
newFrame = jacklib.get_current_transport_frame(gJack.client) + int(self.fSampleRate*2.5)
jacklib.transport_locate(gJack.client, newFrame)
@pyqtSlot()
def slot_transportViewMenu(self):
menu = QMenu(self)
actHMS = menu.addAction("Hours:Minutes:Seconds")
actBBT = menu.addAction("Beat:Bar:Tick")
actFrames = menu.addAction("Frames")
actHMS.setCheckable(True)
actBBT.setCheckable(True)
actFrames.setCheckable(True)
if self.fCurTransportView == TRANSPORT_VIEW_HMS:
actHMS.setChecked(True)
elif self.fCurTransportView == TRANSPORT_VIEW_BBT:
actBBT.setChecked(True)
elif self.fCurTransportView == TRANSPORT_VIEW_FRAMES:
actFrames.setChecked(True)
actSelected = menu.exec_(QCursor().pos())
if actSelected == actHMS:
self.setTransportView(TRANSPORT_VIEW_HMS)
elif actSelected == actBBT:
self.setTransportView(TRANSPORT_VIEW_BBT)
elif actSelected == actFrames:
self.setTransportView(TRANSPORT_VIEW_FRAMES)
# -----------------------------------------------------------------
# Refresh JACK stuff
def refreshDSPLoad(self):
if not gJack.client:
return
self.ui_setDSPLoad(int(jacklib.cpu_load(gJack.client)))
def refreshTransport(self):
if not gJack.client:
return
pos = jacklib.jack_position_t()
pos.valid = 0
state = jacklib.transport_query(gJack.client, jacklib.pointer(pos))
if self.fCurTransportView == TRANSPORT_VIEW_HMS:
time = pos.frame / int(self.fSampleRate)
secs = time % 60
mins = (time / 60) % 60
hrs = (time / 3600) % 60
self.ui.label_time.setText("%02i:%02i:%02i" % (hrs, mins, secs))
elif self.fCurTransportView == TRANSPORT_VIEW_BBT:
if pos.valid & jacklib.JackPositionBBT:
bar = pos.bar
beat = pos.beat if bar != 0 else 0
tick = pos.tick if bar != 0 else 0
else:
bar = 0
beat = 0
tick = 0
self.ui.label_time.setText("%03i|%02i|%04i" % (bar, beat, tick))
elif self.fCurTransportView == TRANSPORT_VIEW_FRAMES:
frame1 = pos.frame % 1000
frame2 = (pos.frame / 1000) % 1000
frame3 = (pos.frame / 1000000) % 1000
self.ui.label_time.setText("%03i'%03i'%03i" % (frame3, frame2, frame1))
if pos.valid & jacklib.JackPositionBBT:
if self.fLastBPM != pos.beats_per_minute:
self.ui.sb_bpm.setValue(pos.beats_per_minute)
self.ui.sb_bpm.setStyleSheet("")
else:
pos.beats_per_minute = -1.0
if self.fLastBPM != pos.beats_per_minute:
self.ui.sb_bpm.setStyleSheet("QDoubleSpinBox { color: palette(mid); }")
self.fLastBPM = pos.beats_per_minute
if state != self.fLastTransportState:
self.fLastTransportState = state
if state == jacklib.JackTransportStopped:
icon = getIcon("media-playback-start")
self.ui.act_transport_play.setChecked(False)
self.ui.act_transport_play.setText(self.tr("&Play"))
self.ui.b_transport_play.setChecked(False)
else:
icon = getIcon("media-playback-pause")
self.ui.act_transport_play.setChecked(True)
self.ui.act_transport_play.setText(self.tr("&Pause"))
self.ui.b_transport_play.setChecked(True)
self.ui.act_transport_play.setIcon(icon)
self.ui.b_transport_play.setIcon(icon)
# -----------------------------------------------------------------
# Set JACK stuff
def ui_setBufferSize(self, bufferSize, forced=False):
if self.fBufferSize == bufferSize and not forced:
return
self.fBufferSize = bufferSize
if bufferSize:
self.ui.cb_buffer_size.blockSignals(True)
if bufferSize == 16:
self.ui.cb_buffer_size.setCurrentIndex(0)
elif bufferSize == 32:
self.ui.cb_buffer_size.setCurrentIndex(1)
elif bufferSize == 64:
self.ui.cb_buffer_size.setCurrentIndex(2)
elif bufferSize == 128:
self.ui.cb_buffer_size.setCurrentIndex(3)
elif bufferSize == 256:
self.ui.cb_buffer_size.setCurrentIndex(4)
elif bufferSize == 512:
self.ui.cb_buffer_size.setCurrentIndex(5)
elif bufferSize == 1024:
self.ui.cb_buffer_size.setCurrentIndex(6)
elif bufferSize == 2048:
self.ui.cb_buffer_size.setCurrentIndex(7)
elif bufferSize == 4096:
self.ui.cb_buffer_size.setCurrentIndex(8)
elif bufferSize == 8192:
self.ui.cb_buffer_size.setCurrentIndex(9)
else:
self.ui.cb_buffer_size.setCurrentIndex(-1)
self.ui.cb_buffer_size.blockSignals(False)
if self.fAppName == "Catia" and bufferSize:
for actBufSize in self.ui.act_jack_bf_list:
actBufSize.blockSignals(True)
actBufSize.setEnabled(True)
if actBufSize.text().replace("&", "") == str(bufferSize):
actBufSize.setChecked(True)
else:
actBufSize.setChecked(False)
actBufSize.blockSignals(False)
def ui_setSampleRate(self, sampleRate, future=False):
if self.fSampleRate == sampleRate:
return
if future:
ask = QMessageBox.question(self, self.tr("Change Sample Rate"),
self.tr("It's not possible to change Sample Rate while JACK is running.\n"
"Do you want to change as soon as JACK stops?"), QMessageBox.Ok | QMessageBox.Cancel)
if ask == QMessageBox.Ok:
self.fNextSampleRate = sampleRate
else:
self.fNextSampleRate = 0.0
# not future
else:
self.fSampleRate = sampleRate
self.fNextSampleRate = 0.0
for i in range(len(SAMPLE_RATE_LIST)):
sampleRateI = SAMPLE_RATE_LIST[i]
if self.fSampleRate == sampleRateI:
self.ui.cb_sample_rate.blockSignals(True)
self.ui.cb_sample_rate.setCurrentIndex(i)
self.ui.cb_sample_rate.blockSignals(False)
break
def ui_setRealTime(self, isRealtime):
self.ui.label_realtime.setText(" RT " if isRealtime else " <s>RT</s> ")
self.ui.label_realtime.setEnabled(isRealtime)
def ui_setDSPLoad(self, dspLoad):
self.ui.pb_dsp_load.setValue(dspLoad)
def ui_setXruns(self, xruns):
txt1 = str(xruns) if (xruns >= 0) else "--"
txt2 = "" if (xruns == 1) else "s"
self.ui.b_xruns.setText("%s Xrun%s" % (txt1, txt2))
# -----------------------------------------------------------------
# External Dialogs
@pyqtSlot()
def slot_showJackSettings(self):
jacksettingsW = jacksettings.JackSettingsW(self)
jacksettingsW.exec_()
del jacksettingsW
# Force update of gui widgets
if not gJack.client:
self.jackStopped()
@pyqtSlot()
def slot_showLogs(self):
if self.fLogsW is None:
self.fLogsW = logs.LogsW(self)
self.fLogsW.show()
@pyqtSlot()
def slot_showRender(self):
renderW = render.RenderW(self)
renderW.exec_()
del renderW
# -----------------------------------------------------------------
# Shared Canvas code
@pyqtSlot()
def slot_canvasArrange(self):
patchcanvas.arrange()
@pyqtSlot()
def slot_canvasRefresh(self):
patchcanvas.clear()
self.initPorts()
@pyqtSlot()
def slot_canvasZoomFit(self):
self.scene.zoom_fit()
@pyqtSlot()
def slot_canvasZoomIn(self):
self.scene.zoom_in()
@pyqtSlot()
def slot_canvasZoomOut(self):
self.scene.zoom_out()
@pyqtSlot()
def slot_canvasZoomReset(self):
self.scene.zoom_reset()
@pyqtSlot()
def slot_canvasSaveImage(self):
newPath = QFileDialog.getSaveFileName(self, self.tr("Save Image"), filter=self.tr("PNG Image (*.png);;JPEG Image (*.jpg)"))
newPath = newPath[0]
if not newPath:
return
self.scene.clearSelection()
if newPath.lower().endswith(".jpg"):
imgFormat = "JPG"
elif newPath.lower().endswith(".png"):
imgFormat = "PNG"
else:
# File-dialog may not auto-add the extension
imgFormat = "PNG"
newPath += ".png"
self.fExportImage = QImage(int(self.scene.sceneRect().width()), int(self.scene.sceneRect().height()), QImage.Format_RGB32)
painter = QPainter(self.fExportImage)
painter.save()
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.TextAntialiasing, True)
self.scene.render(painter)
self.fExportImage.save(newPath, imgFormat, 100)
painter.restore()
# -----------------------------------------------------------------
# Shared Connections
def setCanvasConnections(self):
self.ui.act_canvas_arrange.setEnabled(False) # TODO, later
self.ui.act_canvas_arrange.triggered.connect(self.slot_canvasArrange)
self.ui.act_canvas_refresh.triggered.connect(self.slot_canvasRefresh)
self.ui.act_canvas_zoom_fit.triggered.connect(self.slot_canvasZoomFit)
self.ui.act_canvas_zoom_in.triggered.connect(self.slot_canvasZoomIn)
self.ui.act_canvas_zoom_out.triggered.connect(self.slot_canvasZoomOut)
self.ui.act_canvas_zoom_100.triggered.connect(self.slot_canvasZoomReset)
self.ui.act_canvas_save_image.triggered.connect(self.slot_canvasSaveImage)
self.ui.b_canvas_zoom_fit.clicked.connect(self.slot_canvasZoomFit)
self.ui.b_canvas_zoom_in.clicked.connect(self.slot_canvasZoomIn)
self.ui.b_canvas_zoom_out.clicked.connect(self.slot_canvasZoomOut)
self.ui.b_canvas_zoom_100.clicked.connect(self.slot_canvasZoomReset)
def setJackConnections(self, modes):
if "jack" in modes:
self.ui.act_jack_clear_xruns.triggered.connect(self.slot_JackClearXruns)
self.ui.act_jack_render.triggered.connect(self.slot_showRender)
self.ui.act_jack_configure.triggered.connect(self.slot_showJackSettings)
self.ui.b_jack_clear_xruns.clicked.connect(self.slot_JackClearXruns)
self.ui.b_jack_configure.clicked.connect(self.slot_showJackSettings)
self.ui.b_jack_render.clicked.connect(self.slot_showRender)
self.ui.cb_buffer_size.currentIndexChanged[str].connect(self.slot_jackBufferSize_ComboBox)
self.ui.cb_sample_rate.currentIndexChanged[str].connect(self.slot_jackSampleRate_ComboBox)
self.ui.b_xruns.clicked.connect(self.slot_JackClearXruns)
if "buffer-size" in modes:
self.ui.act_jack_bf_16.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_32.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_64.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_128.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_256.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_512.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_1024.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_2048.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_4096.triggered.connect(self.slot_jackBufferSize_Menu)
self.ui.act_jack_bf_8192.triggered.connect(self.slot_jackBufferSize_Menu)
if "transport" in modes:
self.ui.act_transport_play.triggered.connect(self.slot_transportPlayPause)
self.ui.act_transport_stop.triggered.connect(self.slot_transportStop)
self.ui.act_transport_backwards.triggered.connect(self.slot_transportBackwards)
self.ui.act_transport_forwards.triggered.connect(self.slot_transportForwards)
self.ui.b_transport_play.clicked.connect(self.slot_transportPlayPause)
self.ui.b_transport_stop.clicked.connect(self.slot_transportStop)
self.ui.b_transport_backwards.clicked.connect(self.slot_transportBackwards)
self.ui.b_transport_forwards.clicked.connect(self.slot_transportForwards)
self.ui.label_time.customContextMenuRequested.connect(self.slot_transportViewMenu)
if "misc" in modes:
if LINUX:
self.ui.act_show_logs.triggered.connect(self.slot_showLogs)
else:
self.ui.act_show_logs.setEnabled(False)