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.

607 lines
22KB

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