#!/usr/bin/env python3 # -*- coding: utf-8 -*- # XY Controller UI, taken from Cadence # Copyright (C) 2011-2020 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 doc/GPL.txt file. # ------------------------------------------------------------------------------------------------------------ # Imports (Global) from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QSize, QTimer from PyQt5.QtGui import QColor, QPainter, QPen from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow # ----------------------------------------------------------------------- # Imports (Custom) from carla_shared import * from carla_utils import * import ui_xycontroller # ----------------------------------------------------------------------- # Imports (ExternalUI) from carla_app import CarlaApplication from externalui import ExternalUI from widgets.paramspinbox import ParamSpinBox # ------------------------------------------------------------------------------------------------------------ XYCONTROLLER_PARAMETER_X = 0 XYCONTROLLER_PARAMETER_Y = 1 # ------------------------------------------------------------------------------------------------------------ class XYGraphicsScene(QGraphicsScene): # signals cursorMoved = pyqtSignal(float,float) def __init__(self, parent): QGraphicsScene.__init__(self, parent) self.cc_x = 1 self.cc_y = 2 self.rparent = parent self.m_channels = [] self.m_mouseLock = False self.m_smooth = False self.m_smooth_x = 0.0 self.m_smooth_y = 0.0 self.setBackgroundBrush(Qt.black) cursorPen = QPen(QColor(255, 255, 255), 2) cursorBrush = QColor(255, 255, 255, 50) self.m_cursor = self.addEllipse(QRectF(-10, -10, 20, 20), cursorPen, cursorBrush) linePen = QPen(QColor(200, 200, 200, 100), 1, Qt.DashLine) self.m_lineH = self.addLine(-9999, 0, 9999, 0, linePen) self.m_lineV = self.addLine(0, -9999, 0, 9999, linePen) self.p_size = QRectF(-100, -100, 100, 100) # ------------------------------------------------------------------- def setControlX(self, x: int): self.cc_x = x def setControlY(self, y: int): self.cc_y = y def setChannels(self, channels): self.m_channels = channels def setPosX(self, x: float, forward: bool = True): if self.m_mouseLock: return posX = x * (self.p_size.x() + self.p_size.width()) self.m_cursor.setPos(posX, self.m_cursor.y()) self.m_lineV.setX(posX) if forward: value = posX / (self.p_size.x() + self.p_size.width()); self.sendMIDI(value, None) else: self.m_smooth_x = posX; def setPosY(self, y: float, forward: bool = True): if self.m_mouseLock: return; posY = y * (self.p_size.y() + self.p_size.height()) self.m_cursor.setPos(self.m_cursor.x(), posY) self.m_lineH.setY(posY) if forward: value = posY / (self.p_size.y() + self.p_size.height()) self.sendMIDI(None, value) else: self.m_smooth_y = posY def setSmooth(self, smooth: bool): self.m_smooth = smooth def setSmoothValues(self, x: float, y: float): self.m_smooth_x = x * (self.p_size.x() + self.p_size.width()); self.m_smooth_y = y * (self.p_size.y() + self.p_size.height()); # ------------------------------------------------------------------- def updateSize(self, size: QSize): self.p_size.setRect(-(float(size.width())/2), -(float(size.height())/2), size.width(), size.height()); def updateSmooth(self): if not self.m_smooth: return if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y: return same = 0 if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005: self.m_smooth_x = self.m_cursor.x() same += 1 if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005: self.m_smooth_y = self.m_cursor.y() same += 1 if same == 2: return newX = float(self.m_smooth_x + self.m_cursor.x()*7) / 8 newY = float(self.m_smooth_y + self.m_cursor.y()*7) / 8 pos = QPointF(newX, newY) self.m_cursor.setPos(pos) self.m_lineH.setY(pos.y()) self.m_lineV.setX(pos.x()) xp = pos.x() / (self.p_size.x() + self.p_size.width()) yp = pos.y() / (self.p_size.y() + self.p_size.height()) self.sendMIDI(xp, yp) self.cursorMoved.emit(xp, yp) # ------------------------------------------------------------------- def handleMousePos(self, pos: QPointF): if not self.p_size.contains(pos): if pos.x() < self.p_size.x(): pos.setX(self.p_size.x()) elif pos.x() > (self.p_size.x() + self.p_size.width()): pos.setX(self.p_size.x() + self.p_size.width()); if pos.y() < self.p_size.y(): pos.setY(self.p_size.y()) elif pos.y() > (self.p_size.y() + self.p_size.height()): pos.setY(self.p_size.y() + self.p_size.height()) self.m_smooth_x = pos.x() self.m_smooth_y = pos.y() if not self.m_smooth: self.m_cursor.setPos(pos) self.m_lineH.setY(pos.y()) self.m_lineV.setX(pos.x()) xp = pos.x() / (self.p_size.x() + self.p_size.width()); yp = pos.y() / (self.p_size.y() + self.p_size.height()); self.sendMIDI(xp, yp) self.cursorMoved.emit(xp, yp) def sendMIDI(self, xp, yp): rate = float(0xff) / 4 msgd = ["cc2" if xp is not None and yp is not None else "cc"] if xp is not None: msgd.append(self.cc_x) msgd.append(int(xp * rate + rate)) if yp is not None: msgd.append(self.cc_y) msgd.append(int(yp * rate + rate)) self.rparent.send(msgd) # ------------------------------------------------------------------- def keyPressEvent(self, event): # QKeyEvent event.accept() def wheelEvent(self, event): # QGraphicsSceneWheelEvent event.accept() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = True self.handleMousePos(event.scenePos()) self.rparent.setCursor(Qt.CrossCursor) QGraphicsScene.mousePressEvent(self, event); def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): self.handleMousePos(event.scenePos()) QGraphicsScene.mouseMoveEvent(self, event); def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = False self.rparent.setCursor(Qt.ArrowCursor) QGraphicsScene.mouseReleaseEvent(self, event) # ----------------------------------------------------------------------- # External UI class XYControllerUI(ExternalUI, QMainWindow): def __init__(self): ExternalUI.__init__(self) QMainWindow.__init__(self) self.ui = ui_xycontroller.Ui_XYControllerW() self.ui.setupUi(self) self.fSaveSizeNowChecker = -1 # --------------------------------------------------------------- # Set-up GUI stuff self.scene = XYGraphicsScene(self) self.ui.dial_x.setImage(2) self.ui.dial_y.setImage(2) self.ui.dial_x.setLabel("X") self.ui.dial_y.setLabel("Y") self.ui.keyboard.setOctaves(10) self.ui.graphicsView.setScene(self.scene) self.ui.graphicsView.setRenderHints(QPainter.Antialiasing) for MIDI_CC in MIDI_CC_LIST: self.ui.cb_control_x.addItem(MIDI_CC) self.ui.cb_control_y.addItem(MIDI_CC) # --------------------------------------------------------------- # Initial state self.m_channels = [1] self.ui.act_ch_01.setChecked(True) self.ui.act_show_keyboard.setChecked(True) self.ui.cb_control_y.setCurrentIndex(1) # --------------------------------------------------------------- # Connect actions to functions self.scene.cursorMoved.connect(self.slot_sceneCursorMoved) self.ui.keyboard.noteOn.connect(self.slot_noteOn) self.ui.keyboard.noteOff.connect(self.slot_noteOff) self.ui.cb_smooth.clicked.connect(self.slot_setSmooth) self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX) self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY) self.ui.cb_control_x.currentIndexChanged[str].connect(self.slot_checkCC_X) self.ui.cb_control_y.currentIndexChanged[str].connect(self.slot_checkCC_Y) self.ui.act_ch_01.triggered.connect(self.slot_checkChannel) self.ui.act_ch_02.triggered.connect(self.slot_checkChannel) self.ui.act_ch_03.triggered.connect(self.slot_checkChannel) self.ui.act_ch_04.triggered.connect(self.slot_checkChannel) self.ui.act_ch_05.triggered.connect(self.slot_checkChannel) self.ui.act_ch_06.triggered.connect(self.slot_checkChannel) self.ui.act_ch_07.triggered.connect(self.slot_checkChannel) self.ui.act_ch_08.triggered.connect(self.slot_checkChannel) self.ui.act_ch_09.triggered.connect(self.slot_checkChannel) self.ui.act_ch_10.triggered.connect(self.slot_checkChannel) self.ui.act_ch_11.triggered.connect(self.slot_checkChannel) self.ui.act_ch_12.triggered.connect(self.slot_checkChannel) self.ui.act_ch_13.triggered.connect(self.slot_checkChannel) self.ui.act_ch_14.triggered.connect(self.slot_checkChannel) self.ui.act_ch_15.triggered.connect(self.slot_checkChannel) self.ui.act_ch_16.triggered.connect(self.slot_checkChannel) self.ui.act_ch_all.triggered.connect(self.slot_checkChannel_all) self.ui.act_ch_none.triggered.connect(self.slot_checkChannel_none) self.ui.act_show_keyboard.triggered.connect(self.slot_showKeyboard) # --------------------------------------------------------------- # Final stuff self.fIdleTimer = self.startTimer(60) self.setWindowTitle(self.fUiName) self.ready() # ------------------------------------------------------------------- @pyqtSlot() def slot_updateScreen(self): self.ui.graphicsView.centerOn(0, 0) self.scene.updateSize(self.ui.graphicsView.size()) dial_x = self.ui.dial_x.rvalue() dial_y = self.ui.dial_y.rvalue() self.scene.setPosX(dial_x / 100, False) self.scene.setPosY(dial_y / 100, False) self.scene.setSmoothValues(dial_x / 100, dial_y / 100) @pyqtSlot(int) def slot_noteOn(self, note): self.send(["note", True, note]) @pyqtSlot(int) def slot_noteOff(self, note): self.send(["note", False, note]) @pyqtSlot(float) def slot_knobValueChangedX(self, x: float): self.sendControl(XYCONTROLLER_PARAMETER_X, x) self.scene.setPosX(x / 100, True) self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100) @pyqtSlot(float) def slot_knobValueChangedY(self, y: float): self.sendControl(XYCONTROLLER_PARAMETER_Y, y) self.scene.setPosY(y / 100, True) self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100) @pyqtSlot(str) def slot_checkCC_X(self, text: str): if not text: return cc_x = int(text.split(" ",1)[0]) self.scene.setControlX(cc_x) self.sendConfigure("cc_x", str(cc_x)) @pyqtSlot(str) def slot_checkCC_Y(self, text: str): if not text: return cc_y = int(text.split(" ",1)[0]) self.scene.setControlY(cc_y) self.sendConfigure("cc_y", str(cc_y)) @pyqtSlot(bool) def slot_checkChannel(self, clicked): if not self.sender(): return channel = int(self.sender().text()) if clicked and channel not in self.m_channels: self.m_channels.append(channel) elif not clicked and channel in self.m_channels: self.m_channels.remove(channel) self.scene.setChannels(self.m_channels) self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels)) @pyqtSlot() def slot_checkChannel_all(self): self.ui.act_ch_01.setChecked(True) self.ui.act_ch_02.setChecked(True) self.ui.act_ch_03.setChecked(True) self.ui.act_ch_04.setChecked(True) self.ui.act_ch_05.setChecked(True) self.ui.act_ch_06.setChecked(True) self.ui.act_ch_07.setChecked(True) self.ui.act_ch_08.setChecked(True) self.ui.act_ch_09.setChecked(True) self.ui.act_ch_10.setChecked(True) self.ui.act_ch_11.setChecked(True) self.ui.act_ch_12.setChecked(True) self.ui.act_ch_13.setChecked(True) self.ui.act_ch_14.setChecked(True) self.ui.act_ch_15.setChecked(True) self.ui.act_ch_16.setChecked(True) self.m_channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] self.scene.setChannels(self.m_channels) self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels)) @pyqtSlot() def slot_checkChannel_none(self): self.ui.act_ch_01.setChecked(False) self.ui.act_ch_02.setChecked(False) self.ui.act_ch_03.setChecked(False) self.ui.act_ch_04.setChecked(False) self.ui.act_ch_05.setChecked(False) self.ui.act_ch_06.setChecked(False) self.ui.act_ch_07.setChecked(False) self.ui.act_ch_08.setChecked(False) self.ui.act_ch_09.setChecked(False) self.ui.act_ch_10.setChecked(False) self.ui.act_ch_11.setChecked(False) self.ui.act_ch_12.setChecked(False) self.ui.act_ch_13.setChecked(False) self.ui.act_ch_14.setChecked(False) self.ui.act_ch_15.setChecked(False) self.ui.act_ch_16.setChecked(False) self.m_channels = [] self.scene.setChannels(self.m_channels) self.sendConfigure("channels", "") @pyqtSlot(bool) def slot_setSmooth(self, smooth): self.scene.setSmooth(smooth) self.sendConfigure("smooth", "yes" if smooth else "no") if smooth: dial_x = self.ui.dial_x.rvalue() dial_y = self.ui.dial_y.rvalue() self.scene.setSmoothValues(dial_x / 100, dial_y / 100) @pyqtSlot(float, float) def slot_sceneCursorMoved(self, xp: float, yp: float): self.ui.dial_x.setValue(xp * 100, False) self.ui.dial_y.setValue(yp * 100, False) self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100) self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100) @pyqtSlot(bool) def slot_showKeyboard(self, yesno): self.ui.scrollArea.setVisible(yesno) self.sendConfigure("show-midi-keyboard", "yes" if yesno else "no") QTimer.singleShot(0, self.slot_updateScreen) # ------------------------------------------------------------------- # DSP Callbacks def dspParameterChanged(self, index: int, value: float): if index == XYCONTROLLER_PARAMETER_X: self.ui.dial_x.setValue(value, False) self.scene.setPosX(value / 100, False) elif index == XYCONTROLLER_PARAMETER_Y: self.ui.dial_y.setValue(value, False) self.scene.setPosY(value / 100, False) else: return self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, self.ui.dial_y.rvalue() / 100) def dspStateChanged(self, key: str, value: str): if key == "guiWidth": try: width = int(value) except: width = 0 if width > 0: self.resize(width, self.height()) elif key == "guiHeight": try: height = int(value) except: height = 0 if height > 0: self.resize(self.width(), height) elif key == "smooth": smooth = (value == "yes") self.ui.cb_smooth.blockSignals(True) self.ui.cb_smooth.setChecked(smooth) self.ui.cb_smooth.blockSignals(False) self.scene.setSmooth(smooth) if smooth: dial_x = self.ui.dial_x.rvalue() dial_y = self.ui.dial_y.rvalue() self.scene.setSmoothValues(dial_x / 100, dial_y / 100) elif key == "show-midi-keyboard": show = (value == "yes") self.ui.act_show_keyboard.blockSignals(True) self.ui.act_show_keyboard.setChecked(show) self.ui.act_show_keyboard.blockSignals(False) self.ui.scrollArea.setVisible(show) elif key == "channels": if value: self.m_channels = [int(c) for c in value.split(",")] else: self.m_channels = [] self.scene.setChannels(self.m_channels) self.ui.act_ch_01.setChecked(bool(1 in self.m_channels)) self.ui.act_ch_02.setChecked(bool(2 in self.m_channels)) self.ui.act_ch_03.setChecked(bool(3 in self.m_channels)) self.ui.act_ch_04.setChecked(bool(4 in self.m_channels)) self.ui.act_ch_05.setChecked(bool(5 in self.m_channels)) self.ui.act_ch_06.setChecked(bool(6 in self.m_channels)) self.ui.act_ch_07.setChecked(bool(7 in self.m_channels)) self.ui.act_ch_08.setChecked(bool(8 in self.m_channels)) self.ui.act_ch_09.setChecked(bool(9 in self.m_channels)) self.ui.act_ch_10.setChecked(bool(10 in self.m_channels)) self.ui.act_ch_11.setChecked(bool(11 in self.m_channels)) self.ui.act_ch_12.setChecked(bool(12 in self.m_channels)) self.ui.act_ch_13.setChecked(bool(13 in self.m_channels)) self.ui.act_ch_14.setChecked(bool(14 in self.m_channels)) self.ui.act_ch_15.setChecked(bool(15 in self.m_channels)) self.ui.act_ch_16.setChecked(bool(16 in self.m_channels)) elif key == "cc_x": cc_x = int(value) self.scene.setControlX(cc_x) for cc_index in range(len(MIDI_CC_LIST)): if cc_x == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]): self.ui.cb_control_x.blockSignals(True) self.ui.cb_control_x.setCurrentIndex(cc_index) self.ui.cb_control_x.blockSignals(False) break elif key == "cc_y": cc_y = int(value) self.scene.setControlY(cc_y) for cc_index in range(len(MIDI_CC_LIST)): if cc_y == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]): self.ui.cb_control_y.blockSignals(True) self.ui.cb_control_y.setCurrentIndex(cc_index) self.ui.cb_control_y.blockSignals(False) break def dspNoteReceived(self, onOff, channel, note, velocity): if channel+1 not in self.m_channels: return if onOff: self.ui.keyboard.sendNoteOn(note, False) else: self.ui.keyboard.sendNoteOff(note, False) # ------------------------------------------------------------------- # ExternalUI Callbacks def uiShow(self): self.show() def uiFocus(self): self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.show() self.raise_() self.activateWindow() def uiHide(self): self.hide() def uiQuit(self): self.closeExternalUI() self.close() app.quit() def uiTitleChanged(self, uiTitle): self.setWindowTitle(uiTitle) # ------------------------------------------------------------------- # Qt events def showEvent(self, event): self.slot_updateScreen() QMainWindow.showEvent(self, event) def resizeEvent(self, event): self.fSaveSizeNowChecker = 0 self.slot_updateScreen() QMainWindow.resizeEvent(self, event) def timerEvent(self, event): if event.timerId() == self.fIdleTimer: self.idleExternalUI() self.scene.updateSmooth() if self.fSaveSizeNowChecker == 11: self.sendConfigure("guiWidth", str(self.width())) self.sendConfigure("guiHeight", str(self.height())) self.fSaveSizeNowChecker = -1 elif self.fSaveSizeNowChecker >= 0: self.fSaveSizeNowChecker += 1 QMainWindow.timerEvent(self, event) def closeEvent(self, event): self.closeExternalUI() QMainWindow.closeEvent(self, event) # there might be other qt windows open which will block the UI from quitting app.quit() #--------------- main ------------------ if __name__ == '__main__': import resources_rc pathBinaries, _ = getPaths() gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION)) gCarla.utils.set_process_name("XYController") app = CarlaApplication("XYController") gui = XYControllerUI() app.exit_exec()