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.

502 lines
16KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla patchbay widget code
  4. # Copyright (C) 2011-2013 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. try:
  20. from PyQt5.QtWidgets import QGraphicsView
  21. except:
  22. from PyQt4.QtGui import QGraphicsView
  23. # ------------------------------------------------------------------------------------------------------------
  24. # Imports (Custom Stuff)
  25. import patchcanvas
  26. from carla_widgets import *
  27. # ------------------------------------------------------------------------------------------------------------
  28. # Try Import OpenGL
  29. try:
  30. #try:
  31. from PyQt5.QtOpenGL import QGLWidget
  32. #except:
  33. #from PyQt4.QtOpenGL import QGLWidget
  34. hasGL = True
  35. except:
  36. hasGL = False
  37. # ------------------------------------------------------------------------------------------------
  38. # Patchbay widget
  39. class CarlaPatchbayW(QGraphicsView):
  40. def __init__(self, parent):
  41. QGraphicsView.__init__(self, parent)
  42. # -------------------------------------------------------------
  43. # Internal stuff
  44. self.fPluginCount = 0
  45. self.fPluginList = []
  46. # -------------------------------------------------------------
  47. # Set-up Canvas
  48. self.scene = patchcanvas.PatchScene(self, self) # FIXME?
  49. self.setScene(self.scene)
  50. #self.setRenderHint(QPainter.Antialiasing, bool(self.fSavedSettings["Canvas/Antialiasing"] == patchcanvas.ANTIALIASING_FULL))
  51. #if self.fSavedSettings["Canvas/UseOpenGL"] and hasGL:
  52. #self.setViewport(QGLWidget(self))
  53. #self.setRenderHint(QPainter.HighQualityAntialiasing, self.fSavedSettings["Canvas/HighQualityAntialiasing"])
  54. #pOptions = patchcanvas.options_t()
  55. #pOptions.theme_name = self.fSavedSettings["Canvas/Theme"]
  56. #pOptions.auto_hide_groups = self.fSavedSettings["Canvas/AutoHideGroups"]
  57. #pOptions.use_bezier_lines = self.fSavedSettings["Canvas/UseBezierLines"]
  58. #pOptions.antialiasing = self.fSavedSettings["Canvas/Antialiasing"]
  59. #pOptions.eyecandy = self.fSavedSettings["Canvas/EyeCandy"]
  60. pFeatures = patchcanvas.features_t()
  61. pFeatures.group_info = False
  62. pFeatures.group_rename = False
  63. pFeatures.port_info = False
  64. pFeatures.port_rename = False
  65. pFeatures.handle_group_pos = True
  66. #patchcanvas.setOptions(pOptions)
  67. patchcanvas.setFeatures(pFeatures)
  68. patchcanvas.init("Carla", self.scene, CanvasCallback, True)
  69. #patchcanvas.setCanvasSize(0, 0, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT)
  70. #patchcanvas.setInitialPos(DEFAULT_CANVAS_WIDTH / 2, DEFAULT_CANVAS_HEIGHT / 2)
  71. #self.setSceneRect(0, 0, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT)
  72. # -----------------------------------------------------------------
  73. def recheckPluginHints(self, hints):
  74. pass
  75. # -----------------------------------------------------------------
  76. def getPluginCount(self):
  77. return self.fPluginCount
  78. def getPlugin(self, pluginId):
  79. if pluginId >= self.fPluginCount:
  80. return None
  81. pitem = self.fPluginList[pluginId]
  82. if pitem is None:
  83. return None
  84. return pitem
  85. # -----------------------------------------------------------------
  86. def addPlugin(self, pluginId, isProjectLoading):
  87. pitem = PluginEdit(self, pluginId)
  88. self.fPluginList.append(pitem)
  89. self.fPluginCount += 1
  90. def removePlugin(self, pluginId):
  91. if pluginId >= self.fPluginCount:
  92. return
  93. pitem = self.fPluginList[pluginId]
  94. if pitem is None:
  95. return
  96. self.fPluginCount -= 1
  97. self.fPluginList.pop(pluginId)
  98. pitem.close()
  99. del pitem
  100. # push all plugins 1 slot back
  101. for i in range(pluginId, self.fPluginCount):
  102. self.fPluginList[i].fPluginId = i # FIXME ?
  103. def renamePlugin(self, pluginId, newName):
  104. if pluginId >= self.fPluginCount:
  105. return
  106. pitem = self.fPluginList[pluginId]
  107. if pitem is None:
  108. return
  109. pitem.setName(name)
  110. def removeAllPlugins(self):
  111. for i in range(self.fPluginCount):
  112. pitem = self.fPluginList[i]
  113. if pitem is None:
  114. break
  115. pitem.close()
  116. del pitem
  117. self.fPluginCount = 0
  118. self.fPluginList = []
  119. # -----------------------------------------------------------------
  120. def setParameterValue(self, pluginId, index, value):
  121. if pluginId >= self.fPluginCount:
  122. return
  123. pitem = self.fPluginList[pluginId]
  124. if pitem is None:
  125. return
  126. pitem.setParameterValue(index, value)
  127. def setParameterDefault(self, pluginId, index, value):
  128. if pluginId >= self.fPluginCount:
  129. return
  130. pitem = self.fPluginList[pluginId]
  131. if pitem is None:
  132. return
  133. pitem.setParameterDefault(index, value)
  134. def setParameterMidiChannel(self, pluginId, index, channel):
  135. if pluginId >= self.fPluginCount:
  136. return
  137. pitem = self.fPluginList[pluginId]
  138. if pitem is None:
  139. return
  140. pitem.setParameterMidiChannel(index, channel)
  141. def setParameterMidiCC(self, pluginId, index, cc):
  142. if pluginId >= self.fPluginCount:
  143. return
  144. pitem = self.fPluginList[pluginId]
  145. if pitem is None:
  146. return
  147. pitem.setParameterMidiControl(index, cc)
  148. # -----------------------------------------------------------------
  149. def setProgram(self, pluginId, index):
  150. if pluginId >= self.fPluginCount:
  151. return
  152. pitem = self.fPluginList[pluginId]
  153. if pitem is None:
  154. return
  155. pitem.setProgram(index)
  156. def setMidiProgram(self, pluginId, index):
  157. if pluginId >= self.fPluginCount:
  158. return
  159. pitem = self.fPluginList[pluginId]
  160. if pitem is None:
  161. return
  162. pitem.setMidiProgram(index)
  163. # -----------------------------------------------------------------
  164. def noteOn(self, pluginId, channel, note, velocity):
  165. if pluginId >= self.fPluginCount:
  166. return
  167. pitem = self.fPluginList[pluginId]
  168. if pitem is None:
  169. return
  170. pitem.sendNoteOn(channel, note)
  171. def noteOff(self, pluginId, channel, note):
  172. if pluginId >= self.fPluginCount:
  173. return
  174. pitem = self.fPluginList[pluginId]
  175. if pitem is None:
  176. return
  177. pitem.sendNoteOff(channel, note)
  178. # -----------------------------------------------------------------
  179. def setGuiState(self, pluginId, state):
  180. pass
  181. # -----------------------------------------------------------------
  182. def updateInfo(self, pluginId):
  183. if pluginId >= self.fPluginCount:
  184. return
  185. pitem = self.fPluginList[pluginId]
  186. if pitem is None:
  187. return
  188. pitem.updateInfo()
  189. def reloadInfo(self, pluginId):
  190. if pluginId >= self.fPluginCount:
  191. return
  192. pitem = self.fPluginList[pluginId]
  193. if pitem is None:
  194. return
  195. pitem.reloadInfo()
  196. def reloadParameters(self, pluginId):
  197. if pluginId >= self.fPluginCount:
  198. return
  199. pitem = self.fPluginList[pluginId]
  200. if pitem is None:
  201. return
  202. pitem.reloadParameters()
  203. def reloadPrograms(self, pluginId):
  204. if pluginId >= self.fPluginCount:
  205. return
  206. pitem = self.fPluginList[pluginId]
  207. if pitem is None:
  208. return
  209. pitem.reloadPrograms()
  210. def reloadAll(self, pluginId):
  211. if pluginId >= self.fPluginCount:
  212. return
  213. pitem = self.fPluginList[pluginId]
  214. if pitem is None:
  215. return
  216. pitem.reloadAll()
  217. # -----------------------------------------------------------------
  218. def patchbayClientAdded(self, clientId, clientIcon, clientName):
  219. pcSplit = patchcanvas.SPLIT_UNDEF
  220. pcIcon = patchcanvas.ICON_APPLICATION
  221. if clientIcon == PATCHBAY_ICON_HARDWARE:
  222. pcSplit = patchcanvas.SPLIT_YES
  223. pcIcon = patchcanvas.ICON_HARDWARE
  224. elif clientIcon == PATCHBAY_ICON_DISTRHO:
  225. pcIcon = patchcanvas.ICON_DISTRHO
  226. elif clientIcon == PATCHBAY_ICON_FILE:
  227. pcIcon = patchcanvas.ICON_FILE
  228. elif clientIcon == PATCHBAY_ICON_PLUGIN:
  229. pcIcon = patchcanvas.ICON_PLUGIN
  230. patchcanvas.addGroup(clientId, clientName, pcSplit, pcIcon)
  231. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  232. def patchbayClientRemoved(self, clientId, clientName):
  233. #if not self.fEngineStarted: return
  234. patchcanvas.removeGroup(clientId)
  235. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  236. def patchbayClientRenamed(self, clientId, newClientName):
  237. patchcanvas.renameGroup(clientId, newClientName)
  238. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  239. def patchbayPortAdded(self, clientId, portId, portFlags, portName):
  240. if (portFlags & PATCHBAY_PORT_IS_INPUT):
  241. portMode = patchcanvas.PORT_MODE_INPUT
  242. elif (portFlags & PATCHBAY_PORT_IS_OUTPUT):
  243. portMode = patchcanvas.PORT_MODE_OUTPUT
  244. else:
  245. portMode = patchcanvas.PORT_MODE_NULL
  246. if (portFlags & PATCHBAY_PORT_IS_AUDIO):
  247. portType = patchcanvas.PORT_TYPE_AUDIO_JACK
  248. elif (portFlags & PATCHBAY_PORT_IS_MIDI):
  249. portType = patchcanvas.PORT_TYPE_MIDI_JACK
  250. else:
  251. portType = patchcanvas.PORT_TYPE_NULL
  252. patchcanvas.addPort(clientId, portId, portName, portMode, portType)
  253. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  254. def patchbayPortRemoved(self, groupId, portId, fullPortName):
  255. #if not self.fEngineStarted: return
  256. patchcanvas.removePort(portId)
  257. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  258. def patchbayPortRenamed(self, groupId, portId, newPortName):
  259. patchcanvas.renamePort(portId, newPortName)
  260. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  261. def patchbayConnectionAdded(self, connectionId, portOutId, portInId):
  262. patchcanvas.connectPorts(connectionId, portOutId, portInId)
  263. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  264. def patchbayConnectionRemoved(self, connectionId):
  265. #if not self.fEngineStarted: return
  266. patchcanvas.disconnectPorts(connectionId)
  267. #QTimer.singleShot(0, self.ui.miniCanvasPreview, SLOT("update()"))
  268. def patchbayIconChanged(self, clientId, clientIcon):
  269. pcIcon = patchcanvas.ICON_APPLICATION
  270. if clientIcon == PATCHBAY_ICON_HARDWARE:
  271. pcIcon = patchcanvas.ICON_HARDWARE
  272. elif clientIcon == PATCHBAY_ICON_DISTRHO:
  273. pcIcon = patchcanvas.ICON_DISTRHO
  274. elif clientIcon == PATCHBAY_ICON_FILE:
  275. pcIcon = patchcanvas.ICON_FILE
  276. elif clientIcon == PATCHBAY_ICON_PLUGIN:
  277. pcIcon = patchcanvas.ICON_PLUGIN
  278. patchcanvas.setGroupIcon(clientId, pcIcon)
  279. # -----------------------------------------------------------------
  280. def idleFast(self):
  281. pass
  282. def idleSlow(self):
  283. for i in range(self.fPluginCount):
  284. pitem = self.fPluginList[i]
  285. if pitem is None:
  286. break
  287. pitem.idleSlow()
  288. # -----------------------------------------------------------------
  289. @pyqtSlot()
  290. def slot_canvasArrange(self):
  291. patchcanvas.arrange()
  292. @pyqtSlot()
  293. def slot_canvasRefresh(self):
  294. patchcanvas.clear()
  295. if Carla.host.is_engine_running():
  296. Carla.host.patchbay_refresh()
  297. QTimer.singleShot(1000 if self.fSavedSettings['Canvas/EyeCandy'] else 0, self.ui.miniCanvasPreview, SLOT("update()"))
  298. @pyqtSlot()
  299. def slot_canvasZoomFit(self):
  300. self.scene.zoom_fit()
  301. @pyqtSlot()
  302. def slot_canvasZoomIn(self):
  303. self.scene.zoom_in()
  304. @pyqtSlot()
  305. def slot_canvasZoomOut(self):
  306. self.scene.zoom_out()
  307. @pyqtSlot()
  308. def slot_canvasZoomReset(self):
  309. self.scene.zoom_reset()
  310. @pyqtSlot()
  311. def slot_canvasPrint(self):
  312. self.scene.clearSelection()
  313. self.fExportPrinter = QPrinter()
  314. dialog = QPrintDialog(self.fExportPrinter, self)
  315. if dialog.exec_():
  316. painter = QPainter(self.fExportPrinter)
  317. painter.save()
  318. painter.setRenderHint(QPainter.Antialiasing)
  319. painter.setRenderHint(QPainter.TextAntialiasing)
  320. self.scene.render(painter)
  321. painter.restore()
  322. @pyqtSlot()
  323. def slot_canvasSaveImage(self):
  324. newPath = QFileDialog.getSaveFileName(self, self.tr("Save Image"), filter=self.tr("PNG Image (*.png);;JPEG Image (*.jpg)"))
  325. if newPath:
  326. self.scene.clearSelection()
  327. # FIXME - must be a better way...
  328. if newPath.endswith((".jpg", ".jpG", ".jPG", ".JPG", ".JPg", ".Jpg")):
  329. imgFormat = "JPG"
  330. elif newPath.endswith((".png", ".pnG", ".pNG", ".PNG", ".PNg", ".Png")):
  331. imgFormat = "PNG"
  332. else:
  333. # File-dialog may not auto-add the extension
  334. imgFormat = "PNG"
  335. newPath += ".png"
  336. self.fExportImage = QImage(self.scene.sceneRect().width(), self.scene.sceneRect().height(), QImage.Format_RGB32)
  337. painter = QPainter(self.fExportImage)
  338. painter.save()
  339. painter.setRenderHint(QPainter.Antialiasing) # TODO - set true, cleanup this
  340. painter.setRenderHint(QPainter.TextAntialiasing)
  341. self.scene.render(painter)
  342. self.fExportImage.save(newPath, imgFormat, 100)
  343. painter.restore()
  344. # ------------------------------------------------------------------------------------------------
  345. # Canvas callback
  346. def CanvasCallback(action, value1, value2, valueStr):
  347. if action == patchcanvas.ACTION_GROUP_INFO:
  348. pass
  349. elif action == patchcanvas.ACTION_GROUP_RENAME:
  350. pass
  351. elif action == patchcanvas.ACTION_GROUP_SPLIT:
  352. groupId = value1
  353. patchcanvas.splitGroup(groupId)
  354. Carla.gui.ui.miniCanvasPreview.update()
  355. elif action == patchcanvas.ACTION_GROUP_JOIN:
  356. groupId = value1
  357. patchcanvas.joinGroup(groupId)
  358. Carla.gui.ui.miniCanvasPreview.update()
  359. elif action == patchcanvas.ACTION_PORT_INFO:
  360. pass
  361. elif action == patchcanvas.ACTION_PORT_RENAME:
  362. pass
  363. elif action == patchcanvas.ACTION_PORTS_CONNECT:
  364. portIdA = value1
  365. portIdB = value2
  366. if not Carla.host.patchbay_connect(portIdA, portIdB):
  367. print("Connection failed:", cString(Carla.host.get_last_error()))
  368. elif action == patchcanvas.ACTION_PORTS_DISCONNECT:
  369. connectionId = value1
  370. if not Carla.host.patchbay_disconnect(connectionId):
  371. print("Disconnect failed:", cString(Carla.host.get_last_error()))