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.

600 lines
21KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla plugin host (plugin UI)
  4. # Copyright (C) 2013-2019 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 GPL.txt file
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. from PyQt5.QtGui import QKeySequence, QMouseEvent
  20. from PyQt5.QtWidgets import QSplitter
  21. # ------------------------------------------------------------------------------------------------------------
  22. # Imports (Custom Stuff)
  23. from carla_host import *
  24. from externalui import ExternalUI
  25. # ------------------------------------------------------------------------------------------------------------
  26. # Host Plugin object
  27. class PluginHost(CarlaHostQtPlugin):
  28. def __init__(self):
  29. CarlaHostQtPlugin.__init__(self)
  30. if False:
  31. # kdevelop likes this :)
  32. self.fExternalUI = ExternalUI()
  33. # ---------------------------------------------------------------
  34. self.fExternalUI = None
  35. # -------------------------------------------------------------------
  36. def setExternalUI(self, extUI):
  37. self.fExternalUI = extUI
  38. def sendMsg(self, lines):
  39. if self.fExternalUI is None:
  40. return False
  41. return self.fExternalUI.send(lines)
  42. # -------------------------------------------------------------------
  43. def engine_init(self, driverName, clientName):
  44. return True
  45. def engine_close(self):
  46. return True
  47. def engine_idle(self):
  48. self.fExternalUI.idleExternalUI()
  49. def is_engine_running(self):
  50. if self.fExternalUI is None:
  51. return False
  52. return self.fExternalUI.isRunning()
  53. def set_engine_about_to_close(self):
  54. return True
  55. def get_host_osc_url_tcp(self):
  56. return self.tr("(OSC TCP port not provided in Plugin version)")
  57. # ------------------------------------------------------------------------------------------------------------
  58. # Main Window
  59. class CarlaMiniW(ExternalUI, HostWindow):
  60. def __init__(self, host, isPatchbay, parent=None):
  61. ExternalUI.__init__(self)
  62. HostWindow.__init__(self, host, isPatchbay, parent)
  63. if False:
  64. # kdevelop likes this :)
  65. host = PluginHost()
  66. self.host = host
  67. host.setExternalUI(self)
  68. self.fFirstInit = True
  69. self.setWindowTitle(self.fUiName)
  70. self.ready()
  71. # Override this as it can be called from several places.
  72. # We really need to close all UIs as events are driven by host idle which is only available when UI is visible
  73. def closeExternalUI(self):
  74. for i in reversed(range(self.fPluginCount)):
  75. self.host.show_custom_ui(i, False)
  76. ExternalUI.closeExternalUI(self)
  77. # -------------------------------------------------------------------
  78. # ExternalUI Callbacks
  79. def uiShow(self):
  80. if self.parent() is not None:
  81. return
  82. self.show()
  83. def uiFocus(self):
  84. if self.parent() is not None:
  85. return
  86. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  87. self.show()
  88. self.raise_()
  89. self.activateWindow()
  90. def uiHide(self):
  91. if self.parent() is not None:
  92. return
  93. self.hide()
  94. def uiQuit(self):
  95. self.closeExternalUI()
  96. self.close()
  97. if self != gui:
  98. gui.close()
  99. # there might be other qt windows open which will block carla-plugin from quitting
  100. app.quit()
  101. def uiTitleChanged(self, uiTitle):
  102. self.setWindowTitle(uiTitle)
  103. # -------------------------------------------------------------------
  104. # Qt events
  105. def closeEvent(self, event):
  106. self.closeExternalUI()
  107. HostWindow.closeEvent(self, event)
  108. # there might be other qt windows open which will block carla-plugin from quitting
  109. app.quit()
  110. # -------------------------------------------------------------------
  111. # Custom callback
  112. def msgCallback(self, msg):
  113. try:
  114. self.msgCallback2(msg)
  115. except:
  116. print("msgCallback error, skipped for", msg)
  117. def msgCallback2(self, msg):
  118. msg = charPtrToString(msg)
  119. #if not msg:
  120. #return
  121. if msg == "runtime-info":
  122. values = self.readlineblock().split(":")
  123. load = float(values[0])
  124. xruns = int(values[1])
  125. self.host._set_runtime_info(load, xruns)
  126. elif msg == "transport":
  127. playing = bool(self.readlineblock() == "true")
  128. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  129. bpm = float(self.readlineblock())
  130. self.host._set_transport(playing, frame, bar, beat, tick, bpm)
  131. elif msg.startswith("PEAKS_"):
  132. pluginId = int(msg.replace("PEAKS_", ""))
  133. in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
  134. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  135. elif msg.startswith("PARAMVAL_"):
  136. pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
  137. paramValue = float(self.readlineblock())
  138. if paramId < 0:
  139. self.host._set_internalValue(pluginId, paramId, paramValue)
  140. else:
  141. self.host._set_parameterValue(pluginId, paramId, paramValue)
  142. elif msg.startswith("ENGINE_CALLBACK_"):
  143. action = int(msg.replace("ENGINE_CALLBACK_", ""))
  144. pluginId = int(self.readlineblock())
  145. value1 = int(self.readlineblock())
  146. value2 = int(self.readlineblock())
  147. value3 = int(self.readlineblock())
  148. valuef = float(self.readlineblock())
  149. valueStr = self.readlineblock().replace("\r", "\n")
  150. self.host._setViaCallback(action, pluginId, value1, value2, value3, valuef, valueStr)
  151. engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)
  152. elif msg.startswith("ENGINE_OPTION_"):
  153. option = int(msg.replace("ENGINE_OPTION_", ""))
  154. forced = bool(self.readlineblock() == "true")
  155. value = self.readlineblock()
  156. if self.fFirstInit and not forced:
  157. return
  158. if option == ENGINE_OPTION_PROCESS_MODE:
  159. self.host.processMode = int(value)
  160. elif option == ENGINE_OPTION_TRANSPORT_MODE:
  161. self.host.transportMode = int(value)
  162. elif option == ENGINE_OPTION_FORCE_STEREO:
  163. self.host.forceStereo = bool(value == "true")
  164. elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
  165. self.host.preferPluginBridges = bool(value == "true")
  166. elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
  167. self.host.preferUIBridges = bool(value == "true")
  168. elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
  169. self.host.uisAlwaysOnTop = bool(value == "true")
  170. elif option == ENGINE_OPTION_MAX_PARAMETERS:
  171. self.host.maxParameters = int(value)
  172. elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
  173. self.host.uiBridgesTimeout = int(value)
  174. elif option == ENGINE_OPTION_PATH_BINARIES:
  175. self.host.pathBinaries = value
  176. elif option == ENGINE_OPTION_PATH_RESOURCES:
  177. self.host.pathResources = value
  178. elif msg.startswith("PLUGIN_INFO_"):
  179. pluginId = int(msg.replace("PLUGIN_INFO_", ""))
  180. self.host._add(pluginId)
  181. type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
  182. filename = self.readlineblock().replace("\r", "\n")
  183. name = self.readlineblock().replace("\r", "\n")
  184. iconName = self.readlineblock().replace("\r", "\n")
  185. realName = self.readlineblock().replace("\r", "\n")
  186. label = self.readlineblock().replace("\r", "\n")
  187. maker = self.readlineblock().replace("\r", "\n")
  188. copyright = self.readlineblock().replace("\r", "\n")
  189. pinfo = {
  190. 'type': type_,
  191. 'category': category,
  192. 'hints': hints,
  193. 'optionsAvailable': optsAvail,
  194. 'optionsEnabled': optsEnabled,
  195. 'filename': filename,
  196. 'name': name,
  197. 'label': label,
  198. 'maker': maker,
  199. 'copyright': copyright,
  200. 'iconName': iconName,
  201. 'patchbayClientId': 0,
  202. 'uniqueId': uniqueId
  203. }
  204. self.host._set_pluginInfo(pluginId, pinfo)
  205. self.host._set_pluginRealName(pluginId, realName)
  206. elif msg.startswith("AUDIO_COUNT_"):
  207. pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
  208. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  209. elif msg.startswith("MIDI_COUNT_"):
  210. pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
  211. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  212. elif msg.startswith("PARAMETER_COUNT_"):
  213. pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
  214. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  215. elif msg.startswith("PARAMETER_DATA_"):
  216. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
  217. paramType, paramHints, midiChannel, midiCC = [int(i) for i in self.readlineblock().split(":")]
  218. paramName = self.readlineblock().replace("\r", "\n")
  219. paramUnit = self.readlineblock().replace("\r", "\n")
  220. paramInfo = {
  221. 'name': paramName,
  222. 'symbol': "",
  223. 'unit': paramUnit,
  224. 'scalePointCount': 0,
  225. }
  226. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  227. paramData = {
  228. 'type': paramType,
  229. 'hints': paramHints,
  230. 'index': paramId,
  231. 'rindex': -1,
  232. 'midiCC': midiCC,
  233. 'midiChannel': midiChannel
  234. }
  235. self.host._set_parameterData(pluginId, paramId, paramData)
  236. elif msg.startswith("PARAMETER_RANGES_"):
  237. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
  238. def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]
  239. paramRanges = {
  240. 'def': def_,
  241. 'min': min_,
  242. 'max': max_,
  243. 'step': step,
  244. 'stepSmall': stepSmall,
  245. 'stepLarge': stepLarge
  246. }
  247. self.host._set_parameterRanges(pluginId, paramId, paramRanges)
  248. elif msg.startswith("PROGRAM_COUNT_"):
  249. pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
  250. self.host._set_programCount(pluginId, count)
  251. self.host._set_currentProgram(pluginId, current)
  252. elif msg.startswith("PROGRAM_NAME_"):
  253. pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
  254. progName = self.readlineblock().replace("\r", "\n")
  255. self.host._set_programName(pluginId, progId, progName)
  256. elif msg.startswith("MIDI_PROGRAM_COUNT_"):
  257. pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
  258. self.host._set_midiProgramCount(pluginId, count)
  259. self.host._set_currentMidiProgram(pluginId, current)
  260. elif msg.startswith("MIDI_PROGRAM_DATA_"):
  261. pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
  262. bank, program = [int(i) for i in self.readlineblock().split(":")]
  263. name = self.readlineblock().replace("\r", "\n")
  264. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  265. elif msg.startswith("CUSTOM_DATA_COUNT_"):
  266. pluginId, count = [int(i) for i in msg.replace("CUSTOM_DATA_COUNT_", "").split(":")]
  267. self.host._set_customDataCount(pluginId, count)
  268. elif msg.startswith("CUSTOM_DATA_"):
  269. pluginId, customDataId = [int(i) for i in msg.replace("CUSTOM_DATA_", "").split(":")]
  270. type_ = self.readlineblock().replace("\r", "\n")
  271. key = self.readlineblock().replace("\r", "\n")
  272. value = self.readlineblock().replace("\r", "\n")
  273. self.host._set_customData(pluginId, customDataId, {'type': type_, 'key': key, 'value': value})
  274. elif msg == "osc-urls":
  275. tcp = self.readlineblock().replace("\r", "\n")
  276. udp = self.readlineblock().replace("\r", "\n")
  277. self.host.fOscTCP = tcp
  278. self.host.fOscUDP = udp
  279. elif msg == "max-plugin-number":
  280. maxnum = int(self.readlineblock())
  281. self.host.fMaxPluginNumber = maxnum
  282. elif msg == "buffer-size":
  283. bufsize = int(self.readlineblock())
  284. self.host.fBufferSize = bufsize
  285. elif msg == "sample-rate":
  286. srate = float(self.readlineblock())
  287. self.host.fSampleRate = srate
  288. elif msg == "error":
  289. error = self.readlineblock().replace("\r", "\n")
  290. engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0, 0.0, error)
  291. elif msg == "show":
  292. self.fFirstInit = False
  293. self.uiShow()
  294. elif msg == "focus":
  295. self.uiFocus()
  296. elif msg == "hide":
  297. self.uiHide()
  298. elif msg == "quit":
  299. self.fQuitReceived = True
  300. self.uiQuit()
  301. elif msg == "uiTitle":
  302. uiTitle = self.readlineblock().replace("\r", "\n")
  303. self.uiTitleChanged(uiTitle)
  304. else:
  305. print("unknown message: \"" + msg + "\"")
  306. # ------------------------------------------------------------------------------------------------------------
  307. # Embed Widget
  308. class QEmbedWidget(QWidget):
  309. def __init__(self, winId):
  310. QWidget.__init__(self)
  311. self.setAttribute(Qt.WA_LayoutUsesWidgetRect)
  312. self.move(0, 0)
  313. self.fPos = (0, 0)
  314. self.fWinId = 0
  315. def finalSetup(self, gui, winId):
  316. self.fWinId = int(self.winId())
  317. gui.ui.centralwidget.installEventFilter(self)
  318. gui.ui.menubar.installEventFilter(self)
  319. gCarla.utils.x11_reparent_window(self.fWinId, winId)
  320. self.show()
  321. def fixPosition(self):
  322. pos = gCarla.utils.x11_get_window_pos(self.fWinId)
  323. if self.fPos == pos:
  324. return
  325. self.fPos = pos
  326. self.move(pos[0], pos[1])
  327. gCarla.utils.x11_move_window(self.fWinId, pos[2], pos[3])
  328. def eventFilter(self, obj, ev):
  329. if isinstance(ev, QMouseEvent):
  330. self.fixPosition()
  331. return False
  332. def enterEvent(self, ev):
  333. self.fixPosition()
  334. QWidget.enterEvent(self, ev)
  335. # ------------------------------------------------------------------------------------------------------------
  336. # Embed plugin UI
  337. class CarlaEmbedW(QEmbedWidget):
  338. def __init__(self, host, winId, isPatchbay):
  339. QEmbedWidget.__init__(self, winId)
  340. if False:
  341. host = CarlaHostPlugin()
  342. self.host = host
  343. self.fWinId = winId
  344. self.setFixedSize(1024, 712)
  345. self.fLayout = QVBoxLayout(self)
  346. self.fLayout.setContentsMargins(0, 0, 0, 0)
  347. self.fLayout.setSpacing(0)
  348. self.setLayout(self.fLayout)
  349. self.gui = CarlaMiniW(host, isPatchbay, self)
  350. self.gui.hide()
  351. self.gui.ui.act_file_quit.setEnabled(False)
  352. self.gui.ui.act_file_quit.setVisible(False)
  353. self.fShortcutActions = []
  354. self.addShortcutActions(self.gui.ui.menu_File.actions())
  355. self.addShortcutActions(self.gui.ui.menu_Plugin.actions())
  356. self.addShortcutActions(self.gui.ui.menu_PluginMacros.actions())
  357. self.addShortcutActions(self.gui.ui.menu_Settings.actions())
  358. self.addShortcutActions(self.gui.ui.menu_Help.actions())
  359. if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
  360. self.addShortcutActions(self.gui.ui.menu_Canvas.actions())
  361. self.addShortcutActions(self.gui.ui.menu_Canvas_Zoom.actions())
  362. self.addWidget(self.gui.ui.menubar)
  363. self.addLine()
  364. self.addWidget(self.gui.ui.toolBar)
  365. if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
  366. self.addLine()
  367. self.fCentralSplitter = QSplitter(self)
  368. policy = self.fCentralSplitter.sizePolicy()
  369. policy.setVerticalStretch(1)
  370. self.fCentralSplitter.setSizePolicy(policy)
  371. self.addCentralWidget(self.gui.ui.dockWidget)
  372. self.addCentralWidget(self.gui.centralWidget())
  373. self.fLayout.addWidget(self.fCentralSplitter)
  374. self.finalSetup(self.gui, winId)
  375. def addShortcutActions(self, actions):
  376. for action in actions:
  377. if not action.shortcut().isEmpty():
  378. self.fShortcutActions.append(action)
  379. def addWidget(self, widget):
  380. widget.setParent(self)
  381. self.fLayout.addWidget(widget)
  382. def addCentralWidget(self, widget):
  383. widget.setParent(self)
  384. self.fCentralSplitter.addWidget(widget)
  385. def addLine(self):
  386. line = QFrame(self)
  387. line.setFrameShadow(QFrame.Sunken)
  388. line.setFrameShape(QFrame.HLine)
  389. line.setLineWidth(0)
  390. line.setMidLineWidth(1)
  391. self.fLayout.addWidget(line)
  392. def keyPressEvent(self, event):
  393. modifiers = event.modifiers()
  394. modifiersStr = ""
  395. if modifiers & Qt.ShiftModifier:
  396. modifiersStr += "Shift+"
  397. if modifiers & Qt.ControlModifier:
  398. modifiersStr += "Ctrl+"
  399. if modifiers & Qt.AltModifier:
  400. modifiersStr += "Alt+"
  401. if modifiers & Qt.MetaModifier:
  402. modifiersStr += "Meta+"
  403. keyStr = QKeySequence(event.key()).toString()
  404. keySeq = QKeySequence(modifiersStr + keyStr)
  405. for action in self.fShortcutActions:
  406. if not action.isEnabled():
  407. continue
  408. if keySeq.matches(action.shortcut()) != QKeySequence.ExactMatch:
  409. continue
  410. event.accept()
  411. action.trigger()
  412. return
  413. QEmbedWidget.keyPressEvent(self, event)
  414. def showEvent(self, event):
  415. QEmbedWidget.showEvent(self, event)
  416. # set our gui as parent for all plugins UIs
  417. if self.host.manageUIs:
  418. winIdStr = "%x" % self.fWinId
  419. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)
  420. def hideEvent(self, event):
  421. # disable parent
  422. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")
  423. QEmbedWidget.hideEvent(self, event)
  424. def closeEvent(self, event):
  425. self.gui.close()
  426. self.gui.closeExternalUI()
  427. QEmbedWidget.closeEvent(self, event)
  428. # there might be other qt windows open which will block carla-plugin from quitting
  429. app.quit()
  430. def setLoadRDFsNeeded(self):
  431. self.gui.setLoadRDFsNeeded()
  432. # ------------------------------------------------------------------------------------------------------------
  433. # Main
  434. if __name__ == '__main__':
  435. # -------------------------------------------------------------
  436. # App initialization
  437. app = CarlaApplication("Carla2-Plugin")
  438. # -------------------------------------------------------------
  439. # Set-up custom signal handling
  440. setUpSignals()
  441. # -------------------------------------------------------------
  442. # Init host backend
  443. isPatchbay = sys.argv[0].rsplit(os.path.sep)[-1].lower().replace(".exe","") == "carla-plugin-patchbay"
  444. host = initHost("Carla-Plugin", None, False, True, True, PluginHost)
  445. host.processMode = ENGINE_PROCESS_MODE_PATCHBAY if isPatchbay else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
  446. host.processModeForced = True
  447. host.nextProcessMode = host.processMode
  448. loadHostSettings(host)
  449. # -------------------------------------------------------------
  450. # Create GUI
  451. try:
  452. winId = int(os.getenv("CARLA_PLUGIN_EMBED_WINID"))
  453. except:
  454. winId = 0
  455. gCarla.utils.setenv("CARLA_PLUGIN_EMBED_WINID", "0")
  456. if LINUX and winId != 0:
  457. gui = CarlaEmbedW(host, winId, isPatchbay)
  458. else:
  459. gui = CarlaMiniW(host, isPatchbay)
  460. # -------------------------------------------------------------
  461. # App-Loop
  462. app.exit_exec()