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.

555 lines
18KB

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