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.

538 lines
20KB

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