Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

566 lines
19KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # XY Controller UI, taken from Cadence
  4. # Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QSize, QTimer
  20. from PyQt5.QtGui import QColor, QPainter, QPen
  21. from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow
  22. # -----------------------------------------------------------------------
  23. # Imports (Custom)
  24. from carla_shared import *
  25. from carla_utils import *
  26. import ui_xycontroller
  27. # -----------------------------------------------------------------------
  28. # Imports (ExternalUI)
  29. from carla_app import CarlaApplication
  30. from externalui import ExternalUI
  31. from widgets.paramspinbox import ParamSpinBox
  32. # ------------------------------------------------------------------------------------------------------------
  33. XYCONTROLLER_PARAMETER_X = 0
  34. XYCONTROLLER_PARAMETER_Y = 1
  35. # ------------------------------------------------------------------------------------------------------------
  36. class XYGraphicsScene(QGraphicsScene):
  37. # signals
  38. cursorMoved = pyqtSignal(float,float)
  39. def __init__(self, parent):
  40. QGraphicsScene.__init__(self, parent)
  41. self.cc_x = 1
  42. self.cc_y = 2
  43. self.m_channels = []
  44. self.m_mouseLock = False
  45. self.m_smooth = False
  46. self.m_smooth_x = 0.0
  47. self.m_smooth_y = 0.0
  48. self.setBackgroundBrush(Qt.black)
  49. cursorPen = QPen(QColor(255, 255, 255), 2)
  50. cursorBrush = QColor(255, 255, 255, 50)
  51. self.m_cursor = self.addEllipse(QRectF(-10, -10, 20, 20), cursorPen, cursorBrush)
  52. linePen = QPen(QColor(200, 200, 200, 100), 1, Qt.DashLine)
  53. self.m_lineH = self.addLine(-9999, 0, 9999, 0, linePen)
  54. self.m_lineV = self.addLine(0, -9999, 0, 9999, linePen)
  55. self.p_size = QRectF(-100, -100, 100, 100)
  56. # -------------------------------------------------------------------
  57. def setControlX(self, x: int):
  58. self.cc_x = x
  59. def setControlY(self, y: int):
  60. self.cc_y = y
  61. def setChannels(self, channels):
  62. self.m_channels = channels
  63. def setPosX(self, x: float, forward: bool = True):
  64. if self.m_mouseLock:
  65. return
  66. posX = x * (self.p_size.x() + self.p_size.width())
  67. self.m_cursor.setPos(posX, self.m_cursor.y())
  68. self.m_lineV.setX(posX)
  69. if forward:
  70. value = posX / (self.p_size.x() + self.p_size.width());
  71. self.sendMIDI(value, None)
  72. else:
  73. self.m_smooth_x = posX;
  74. def setPosY(self, y: float, forward: bool = True):
  75. if self.m_mouseLock:
  76. return;
  77. posY = y * (self.p_size.y() + self.p_size.height())
  78. self.m_cursor.setPos(self.m_cursor.x(), posY)
  79. self.m_lineH.setY(posY)
  80. if forward:
  81. value = posY / (self.p_size.y() + self.p_size.height())
  82. self.sendMIDI(None, value)
  83. else:
  84. self.m_smooth_y = posY
  85. def setSmooth(self, smooth: bool):
  86. self.m_smooth = smooth
  87. def setSmoothValues(self, x: float, y: float):
  88. self.m_smooth_x = x * (self.p_size.x() + self.p_size.width());
  89. self.m_smooth_y = y * (self.p_size.y() + self.p_size.height());
  90. # -------------------------------------------------------------------
  91. def updateSize(self, size: QSize):
  92. self.p_size.setRect(-(float(size.width())/2),
  93. -(float(size.height())/2),
  94. size.width(),
  95. size.height());
  96. def updateSmooth(self):
  97. if not self.m_smooth:
  98. return
  99. if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y:
  100. return
  101. same = 0
  102. if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005:
  103. self.m_smooth_x = self.m_cursor.x()
  104. same += 1
  105. if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005:
  106. self.m_smooth_y = self.m_cursor.y()
  107. same += 1
  108. if same == 2:
  109. return
  110. newX = float(self.m_smooth_x + self.m_cursor.x()*7) / 8
  111. newY = float(self.m_smooth_y + self.m_cursor.y()*7) / 8
  112. pos = QPointF(newX, newY)
  113. self.m_cursor.setPos(pos)
  114. self.m_lineH.setY(pos.y())
  115. self.m_lineV.setX(pos.x())
  116. xp = pos.x() / (self.p_size.x() + self.p_size.width())
  117. yp = pos.y() / (self.p_size.y() + self.p_size.height())
  118. self.sendMIDI(xp, yp)
  119. self.cursorMoved.emit(xp, yp)
  120. # -------------------------------------------------------------------
  121. def handleMousePos(self, pos: QPointF):
  122. if not self.p_size.contains(pos):
  123. if pos.x() < self.p_size.x():
  124. pos.setX(self.p_size.x())
  125. elif pos.x() > (self.p_size.x() + self.p_size.width()):
  126. pos.setX(self.p_size.x() + self.p_size.width());
  127. if pos.y() < self.p_size.y():
  128. pos.setY(self.p_size.y())
  129. elif pos.y() > (self.p_size.y() + self.p_size.height()):
  130. pos.setY(self.p_size.y() + self.p_size.height())
  131. self.m_smooth_x = pos.x()
  132. self.m_smooth_y = pos.y()
  133. if not self.m_smooth:
  134. self.m_cursor.setPos(pos)
  135. self.m_lineH.setY(pos.y())
  136. self.m_lineV.setX(pos.x())
  137. xp = pos.x() / (self.p_size.x() + self.p_size.width());
  138. yp = pos.y() / (self.p_size.y() + self.p_size.height());
  139. self.sendMIDI(xp, yp)
  140. self.cursorMoved.emit(xp, yp)
  141. def sendMIDI(self, xp, yp):
  142. rate = float(0xff) / 4;
  143. if xp is not None:
  144. value = xp * rate + rate
  145. #foreach (const int& channel, m_channels)
  146. #qMidiOutData.put(0xB0 + channel - 1, cc_x, value);
  147. if yp is not None:
  148. value = yp * rate + rate
  149. #foreach (const int& channel, m_channels)
  150. #qMidiOutData.put(0xB0 + channel - 1, cc_y, value);
  151. # -------------------------------------------------------------------
  152. def keyPressEvent(self, event): # QKeyEvent
  153. event.accept()
  154. def wheelEvent(self, event): # QGraphicsSceneWheelEvent
  155. event.accept()
  156. def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
  157. self.m_mouseLock = True
  158. self.handleMousePos(event.scenePos())
  159. self.parent().setCursor(Qt.CrossCursor)
  160. QGraphicsScene.mousePressEvent(self, event);
  161. def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
  162. self.handleMousePos(event.scenePos())
  163. QGraphicsScene.mouseMoveEvent(self, event);
  164. def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
  165. self.m_mouseLock = False
  166. self.parent().setCursor(Qt.ArrowCursor)
  167. QGraphicsScene.mouseReleaseEvent(self, event)
  168. # -----------------------------------------------------------------------
  169. # External UI
  170. class XYControllerUI(ExternalUI, QMainWindow):
  171. def __init__(self):
  172. ExternalUI.__init__(self)
  173. QMainWindow.__init__(self)
  174. self.ui = ui_xycontroller.Ui_XYControllerW()
  175. self.ui.setupUi(self)
  176. self.fSaveSizeNowChecker = -1
  177. # ---------------------------------------------------------------
  178. # Internal stuff
  179. self.cc_x = 1
  180. self.cc_y = 2
  181. self.m_channels = []
  182. # ---------------------------------------------------------------
  183. # Set-up GUI stuff
  184. self.scene = XYGraphicsScene(self)
  185. self.ui.dial_x.setImage(2)
  186. self.ui.dial_y.setImage(2)
  187. self.ui.dial_x.setLabel("X")
  188. self.ui.dial_y.setLabel("Y")
  189. self.ui.keyboard.setOctaves(10)
  190. self.ui.graphicsView.setScene(self.scene)
  191. self.ui.graphicsView.setRenderHints(QPainter.Antialiasing)
  192. for MIDI_CC in MIDI_CC_LIST:
  193. self.ui.cb_control_x.addItem(MIDI_CC)
  194. self.ui.cb_control_y.addItem(MIDI_CC)
  195. self.ui.graphicsView.centerOn(0, 0)
  196. # ---------------------------------------------------------------
  197. # Connect actions to functions
  198. self.scene.cursorMoved.connect(self.slot_sceneCursorMoved)
  199. self.ui.keyboard.noteOn.connect(self.slot_noteOn)
  200. self.ui.keyboard.noteOff.connect(self.slot_noteOff)
  201. self.ui.cb_smooth.clicked.connect(self.slot_setSmooth)
  202. self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX)
  203. self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY)
  204. self.ui.cb_control_x.currentIndexChanged[str].connect(self.slot_checkCC_X)
  205. self.ui.cb_control_y.currentIndexChanged[str].connect(self.slot_checkCC_Y)
  206. self.ui.act_ch_01.triggered.connect(self.slot_checkChannel)
  207. self.ui.act_ch_02.triggered.connect(self.slot_checkChannel)
  208. self.ui.act_ch_03.triggered.connect(self.slot_checkChannel)
  209. self.ui.act_ch_04.triggered.connect(self.slot_checkChannel)
  210. self.ui.act_ch_05.triggered.connect(self.slot_checkChannel)
  211. self.ui.act_ch_06.triggered.connect(self.slot_checkChannel)
  212. self.ui.act_ch_07.triggered.connect(self.slot_checkChannel)
  213. self.ui.act_ch_08.triggered.connect(self.slot_checkChannel)
  214. self.ui.act_ch_09.triggered.connect(self.slot_checkChannel)
  215. self.ui.act_ch_10.triggered.connect(self.slot_checkChannel)
  216. self.ui.act_ch_11.triggered.connect(self.slot_checkChannel)
  217. self.ui.act_ch_12.triggered.connect(self.slot_checkChannel)
  218. self.ui.act_ch_13.triggered.connect(self.slot_checkChannel)
  219. self.ui.act_ch_14.triggered.connect(self.slot_checkChannel)
  220. self.ui.act_ch_15.triggered.connect(self.slot_checkChannel)
  221. self.ui.act_ch_16.triggered.connect(self.slot_checkChannel)
  222. self.ui.act_ch_all.triggered.connect(self.slot_checkChannel_all)
  223. self.ui.act_ch_none.triggered.connect(self.slot_checkChannel_none)
  224. self.ui.act_show_keyboard.triggered.connect(self.slot_showKeyboard)
  225. # ---------------------------------------------------------------
  226. # Final stuff
  227. self.fIdleTimer = self.startTimer(50)
  228. self.setWindowTitle(self.fUiName)
  229. self.ready()
  230. QTimer.singleShot(0, self.slot_updateScreen)
  231. # -------------------------------------------------------------------
  232. @pyqtSlot()
  233. def slot_updateScreen(self):
  234. self.scene.updateSize(self.ui.graphicsView.size())
  235. dial_x = self.ui.dial_x.rvalue()
  236. dial_y = self.ui.dial_y.rvalue()
  237. self.scene.setPosX(dial_x / 100, False)
  238. self.scene.setPosY(dial_y / 100, False)
  239. self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
  240. @pyqtSlot(int)
  241. def slot_noteOn(self, note):
  242. pass
  243. #foreach (const int& channel, m_channels)
  244. #qMidiOutData.put(0x90 + channel - 1, note, 100);
  245. @pyqtSlot(int)
  246. def slot_noteOff(self, note):
  247. pass
  248. #foreach (const int& channel, m_channels)
  249. #qMidiOutData.put(0x80 + channel - 1, note, 0);
  250. @pyqtSlot(float)
  251. def slot_knobValueChangedX(self, x: float):
  252. self.sendControl(XYCONTROLLER_PARAMETER_X, x)
  253. self.scene.setPosX(x / 100, True)
  254. self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100)
  255. @pyqtSlot(float)
  256. def slot_knobValueChangedY(self, y: float):
  257. self.sendControl(XYCONTROLLER_PARAMETER_Y, y)
  258. self.scene.setPosY(y / 100, True)
  259. self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100)
  260. @pyqtSlot(str)
  261. def slot_checkCC_X(self, text):
  262. if not text:
  263. return
  264. tmp_cc_x = int(text.split(" ",1)[0], 16)
  265. self.cc_x = tmp_cc_x;
  266. self.scene.setControlX(self.cc_x);
  267. @pyqtSlot(str)
  268. def slot_checkCC_Y(self, text):
  269. if not text:
  270. return
  271. tmp_cc_y = int(text.split(" ",1)[0], 16)
  272. self.cc_y = tmp_cc_y;
  273. self.scene.setControlY(self.cc_y);
  274. @pyqtSlot(bool)
  275. def slot_checkChannel(self, clicked):
  276. if not self.sender():
  277. return
  278. channel = int(self.sender().text())
  279. if clicked and channel not in self.m_channels:
  280. self.m_channels.append(channel)
  281. elif not clicked and channel in self.m_channels:
  282. self.m_channels.remove(channel);
  283. self.scene.setChannels(self.m_channels);
  284. @pyqtSlot()
  285. def slot_checkChannel_all(self):
  286. self.ui.act_ch_01.setChecked(True)
  287. self.ui.act_ch_02.setChecked(True)
  288. self.ui.act_ch_03.setChecked(True)
  289. self.ui.act_ch_04.setChecked(True)
  290. self.ui.act_ch_05.setChecked(True)
  291. self.ui.act_ch_06.setChecked(True)
  292. self.ui.act_ch_07.setChecked(True)
  293. self.ui.act_ch_08.setChecked(True)
  294. self.ui.act_ch_09.setChecked(True)
  295. self.ui.act_ch_10.setChecked(True)
  296. self.ui.act_ch_11.setChecked(True)
  297. self.ui.act_ch_12.setChecked(True)
  298. self.ui.act_ch_13.setChecked(True)
  299. self.ui.act_ch_14.setChecked(True)
  300. self.ui.act_ch_15.setChecked(True)
  301. self.ui.act_ch_16.setChecked(True)
  302. self.m_channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
  303. self.scene.setChannels(self.m_channels)
  304. @pyqtSlot()
  305. def slot_checkChannel_none(self):
  306. self.ui.act_ch_01.setChecked(False)
  307. self.ui.act_ch_02.setChecked(False)
  308. self.ui.act_ch_03.setChecked(False)
  309. self.ui.act_ch_04.setChecked(False)
  310. self.ui.act_ch_05.setChecked(False)
  311. self.ui.act_ch_06.setChecked(False)
  312. self.ui.act_ch_07.setChecked(False)
  313. self.ui.act_ch_08.setChecked(False)
  314. self.ui.act_ch_09.setChecked(False)
  315. self.ui.act_ch_10.setChecked(False)
  316. self.ui.act_ch_11.setChecked(False)
  317. self.ui.act_ch_12.setChecked(False)
  318. self.ui.act_ch_13.setChecked(False)
  319. self.ui.act_ch_14.setChecked(False)
  320. self.ui.act_ch_15.setChecked(False)
  321. self.ui.act_ch_16.setChecked(False)
  322. self.m_channels = []
  323. self.scene.setChannels(self.m_channels)
  324. @pyqtSlot(bool)
  325. def slot_setSmooth(self, smooth):
  326. self.sendConfigure("smooth", "yes" if smooth else "no")
  327. self.scene.setSmooth(smooth)
  328. if smooth:
  329. dial_x = self.ui.dial_x.rvalue()
  330. dial_y = self.ui.dial_y.rvalue()
  331. self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
  332. @pyqtSlot(float, float)
  333. def slot_sceneCursorMoved(self, xp, yp):
  334. self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100)
  335. self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100)
  336. self.ui.dial_x.setValue(xp * 100, False)
  337. self.ui.dial_y.setValue(yp * 100, False)
  338. @pyqtSlot(bool)
  339. def slot_showKeyboard(self, yesno):
  340. self.ui.scrollArea.setVisible(yesno)
  341. QTimer.singleShot(0, self.slot_updateScreen)
  342. # -------------------------------------------------------------------
  343. # DSP Callbacks
  344. def dspParameterChanged(self, index, value):
  345. if index == XYCONTROLLER_PARAMETER_X:
  346. self.ui.dial_x.setValue(value, False)
  347. self.scene.setPosX(value / 100, False)
  348. elif index == XYCONTROLLER_PARAMETER_Y:
  349. self.ui.dial_y.setValue(value, False)
  350. self.scene.setPosY(value / 100, False)
  351. else:
  352. return
  353. self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100,
  354. self.ui.dial_y.rvalue() / 100)
  355. def dspStateChanged(self, key, value):
  356. print("dspStateChanged", key, value)
  357. if key == "guiWidth":
  358. try:
  359. width = int(value)
  360. except:
  361. width = 0
  362. if width > 0:
  363. self.resize(width, self.height())
  364. elif key == "guiHeight":
  365. try:
  366. height = int(value)
  367. except:
  368. height = 0
  369. if height > 0:
  370. self.resize(self.width(), height)
  371. elif key == "smooth":
  372. smooth = (value == "yes")
  373. self.ui.cb_smooth.blockSignals(True)
  374. self.ui.cb_smooth.setChecked(smooth)
  375. self.ui.cb_smooth.blockSignals(False)
  376. self.scene.setSmooth(smooth)
  377. if smooth:
  378. dial_x = self.ui.dial_x.rvalue()
  379. dial_y = self.ui.dial_y.rvalue()
  380. self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
  381. # -------------------------------------------------------------------
  382. # ExternalUI Callbacks
  383. def uiShow(self):
  384. self.show()
  385. def uiFocus(self):
  386. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  387. self.show()
  388. self.raise_()
  389. self.activateWindow()
  390. def uiHide(self):
  391. self.hide()
  392. def uiQuit(self):
  393. self.closeExternalUI()
  394. self.close()
  395. app.quit()
  396. def uiTitleChanged(self, uiTitle):
  397. self.setWindowTitle(uiTitle)
  398. # -------------------------------------------------------------------
  399. # Qt events
  400. def showEvent(self, event):
  401. self.slot_updateScreen()
  402. QMainWindow.showEvent(self, event)
  403. def resizeEvent(self, event):
  404. self.fSaveSizeNowChecker = 0
  405. self.slot_updateScreen()
  406. QMainWindow.resizeEvent(self, event)
  407. def timerEvent(self, event):
  408. if event.timerId() == self.fIdleTimer:
  409. self.idleExternalUI()
  410. self.scene.updateSmooth()
  411. if self.fSaveSizeNowChecker == 11:
  412. self.sendConfigure("guiWidth", str(self.width()))
  413. self.sendConfigure("guiHeight", str(self.height()))
  414. self.fSaveSizeNowChecker = -1
  415. elif self.fSaveSizeNowChecker >= 0:
  416. self.fSaveSizeNowChecker += 1
  417. QMainWindow.timerEvent(self, event)
  418. def closeEvent(self, event):
  419. self.closeExternalUI()
  420. QMainWindow.closeEvent(self, event)
  421. # there might be other qt windows open which will block the UI from quitting
  422. app.quit()
  423. #--------------- main ------------------
  424. if __name__ == '__main__':
  425. import resources_rc
  426. pathBinaries, _ = getPaths()
  427. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  428. gCarla.utils.set_process_name("XYController")
  429. app = CarlaApplication("XYController")
  430. gui = XYControllerUI()
  431. app.exit_exec()