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.

434 lines
17KB

  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(CarlaHostPlugin):
  24. def __init__(self):
  25. CarlaHostPlugin.__init__(self)
  26. if False:
  27. # kdevelop likes this :)
  28. self.fExternalUI = ExternalUI()
  29. # ---------------------------------------------------------------
  30. self.fExternalUI = None
  31. self.fIsRunning = True
  32. self.fSampleRate = float(sys.argv[1]) if len(sys.argv) > 1 else 44100.0
  33. # -------------------------------------------------------------------
  34. def setExternalUI(self, extUI):
  35. self.fExternalUI = extUI
  36. def sendMsg(self, lines):
  37. if self.fExternalUI is None:
  38. return False
  39. self.fExternalUI.send(lines)
  40. return True
  41. # -------------------------------------------------------------------
  42. def engine_init(self, driverName, clientName):
  43. self.fIsRunning = True
  44. return True
  45. def engine_close(self):
  46. self.fIsRunning = False
  47. return True
  48. def engine_idle(self):
  49. if self.fExternalUI.idleExternalUI():
  50. return
  51. self.fIsRunning = False
  52. self.fExternalUI.d_uiQuit()
  53. def is_engine_running(self):
  54. return self.fIsRunning
  55. def set_engine_about_to_close(self):
  56. return
  57. # ------------------------------------------------------------------------------------------------------------
  58. # Main Window
  59. class CarlaMiniW(ExternalUI, HostWindow):
  60. def __init__(self, host, parent=None):
  61. ExternalUI.__init__(self)
  62. HostWindow.__init__(self, host, parent)
  63. self.host = host
  64. if False:
  65. # kdevelop likes this :)
  66. host = PluginHost()
  67. self.host = host
  68. host.setExternalUI(self)
  69. if sys.argv[0].lower().endswith("/carla-plugin-patchbay"):
  70. from carla_patchbay import CarlaPatchbayW
  71. self.fContainer = CarlaPatchbayW(self, host)
  72. self.setupContainer(True, self.fContainer.themeData)
  73. else:
  74. from carla_rack import CarlaRackW
  75. self.fContainer = CarlaRackW(self, host)
  76. self.setupContainer(False)
  77. self.setWindowTitle(self.fUiName)
  78. self.ready()
  79. # -------------------------------------------------------------------
  80. # ExternalUI Callbacks
  81. def d_uiShow(self):
  82. if self.parent() is None:
  83. self.show()
  84. def d_uiHide(self):
  85. if self.parent() is None:
  86. self.hide()
  87. def d_uiQuit(self):
  88. self.close()
  89. app.quit()
  90. def d_uiTitleChanged(self, uiTitle):
  91. self.setWindowTitle(uiTitle)
  92. # -------------------------------------------------------------------
  93. # Qt events
  94. def closeEvent(self, event):
  95. self.closeExternalUI()
  96. HostWindow.closeEvent(self, event)
  97. # -------------------------------------------------------------------
  98. # Custom idler
  99. def idleExternalUI(self):
  100. while True:
  101. if self.fPipeRecv is None:
  102. return True
  103. try:
  104. msg = self.fPipeRecv.readline().strip()
  105. except IOError:
  106. return False
  107. if not msg:
  108. return True
  109. elif msg.startswith("PEAKS_"):
  110. pluginId = int(msg.replace("PEAKS_", ""))
  111. in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
  112. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  113. elif msg.startswith("PARAMVAL_"):
  114. pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
  115. paramValue = float(self.readlineblock())
  116. self.host._set_parameterValue(pluginId, paramId, paramValue)
  117. elif msg.startswith("ENGINE_CALLBACK_"):
  118. action = int(msg.replace("ENGINE_CALLBACK_", ""))
  119. pluginId = int(self.readlineblock())
  120. value1 = int(self.readlineblock())
  121. value2 = int(self.readlineblock())
  122. value3 = float(self.readlineblock())
  123. valueStr = self.readlineblock().replace("\r", "\n")
  124. if action == ENGINE_CALLBACK_PLUGIN_RENAMED:
  125. self.host._set_pluginName(pluginId, valueStr)
  126. elif action == ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED:
  127. if value1 < 0:
  128. self.host._set_internalValue(pluginId, value1, value3)
  129. else:
  130. self.host._set_parameterValue(pluginId, value1, value3)
  131. elif action == ENGINE_CALLBACK_PARAMETER_DEFAULT_CHANGED:
  132. self.host._set_parameterDefault(pluginId, value1, value3)
  133. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CC_CHANGED:
  134. self.host._set_parameterMidiCC(pluginId, value1, value2)
  135. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CHANNEL_CHANGED:
  136. self.host._set_parameterMidiChannel(pluginId, value1, value2)
  137. elif action == ENGINE_CALLBACK_PROGRAM_CHANGED:
  138. self.host._set_currentProgram(pluginId, value1)
  139. elif action == ENGINE_CALLBACK_MIDI_PROGRAM_CHANGED:
  140. self.host._set_currentMidiProgram(pluginId, value1)
  141. engineCallback(self.host, action, pluginId, value1, value2, value3, valueStr)
  142. elif msg.startswith("ENGINE_OPTION_"):
  143. option = int(msg.replace("ENGINE_OPTION_", ""))
  144. value = self.readlineblock()
  145. if option == ENGINE_OPTION_PROCESS_MODE:
  146. self.host.processMode = int(value)
  147. elif option == ENGINE_OPTION_TRANSPORT_MODE:
  148. self.host.transportMode = int(value)
  149. elif option == ENGINE_OPTION_FORCE_STEREO:
  150. self.host.forceStereo = bool(value == "true")
  151. elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
  152. self.host.preferPluginBridges = bool(value == "true")
  153. elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
  154. self.host.preferUIBridges = bool(value == "true")
  155. elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
  156. self.host.uisAlwaysOnTop = bool(value == "true")
  157. elif option == ENGINE_OPTION_MAX_PARAMETERS:
  158. self.host.maxParameters = int(value)
  159. elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
  160. self.host.uiBridgesTimeout = int(value)
  161. elif option == ENGINE_OPTION_PATH_BINARIES:
  162. self.host.pathBinaries = value
  163. elif option == ENGINE_OPTION_PATH_RESOURCES:
  164. self.host.pathResources = value
  165. elif msg.startswith("PLUGIN_INFO_"):
  166. pluginId = int(msg.replace("PLUGIN_INFO_", ""))
  167. self.host._add(pluginId)
  168. type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
  169. filename = self.readlineblock().replace("\r", "\n")
  170. name = self.readlineblock().replace("\r", "\n")
  171. iconName = self.readlineblock().replace("\r", "\n")
  172. realName = self.readlineblock().replace("\r", "\n")
  173. label = self.readlineblock().replace("\r", "\n")
  174. maker = self.readlineblock().replace("\r", "\n")
  175. copyright = self.readlineblock().replace("\r", "\n")
  176. pinfo = {
  177. 'type': type_,
  178. 'category': category,
  179. 'hints': hints,
  180. 'optionsAvailable': optsAvail,
  181. 'optionsEnabled': optsEnabled,
  182. 'filename': filename,
  183. 'name': name,
  184. 'label': label,
  185. 'maker': maker,
  186. 'copyright': copyright,
  187. 'iconName': iconName,
  188. 'patchbayClientId': 0,
  189. 'uniqueId': uniqueId
  190. }
  191. self.host._set_pluginInfo(pluginId, pinfo)
  192. self.host._set_pluginRealName(pluginId, realName)
  193. elif msg.startswith("AUDIO_COUNT_"):
  194. pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
  195. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  196. elif msg.startswith("MIDI_COUNT_"):
  197. pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
  198. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  199. elif msg.startswith("PARAMETER_COUNT_"):
  200. pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
  201. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  202. elif msg.startswith("PARAMETER_DATA_"):
  203. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
  204. paramType, paramHints, midiChannel, midiCC = [int(i) for i in self.readlineblock().split(":")]
  205. paramName = self.readlineblock().replace("\r", "\n")
  206. paramUnit = self.readlineblock().replace("\r", "\n")
  207. paramInfo = {
  208. 'name': paramName,
  209. 'symbol': "",
  210. 'unit': paramUnit,
  211. 'scalePointCount': 0,
  212. }
  213. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  214. paramData = {
  215. 'type': paramType,
  216. 'hints': paramHints,
  217. 'index': paramId,
  218. 'rindex': -1,
  219. 'midiCC': midiCC,
  220. 'midiChannel': midiChannel
  221. }
  222. self.host._set_parameterData(pluginId, paramId, paramData)
  223. elif msg.startswith("PARAMETER_RANGES_"):
  224. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
  225. def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]
  226. paramRanges = {
  227. 'def': def_,
  228. 'min': min_,
  229. 'max': max_,
  230. 'step': step,
  231. 'stepSmall': stepSmall,
  232. 'stepLarge': stepLarge
  233. }
  234. self.host._set_parameterRanges(pluginId, paramId, paramRanges)
  235. elif msg.startswith("PROGRAM_COUNT_"):
  236. pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
  237. self.host._set_programCount(pluginId, count)
  238. self.host._set_currentProgram(pluginId, current)
  239. elif msg.startswith("PROGRAM_NAME_"):
  240. pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
  241. progName = self.readlineblock().replace("\r", "\n")
  242. self.host._set_programName(pluginId, progId, progName)
  243. elif msg.startswith("MIDI_PROGRAM_COUNT_"):
  244. pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
  245. self.host._set_midiProgramCount(pluginId, count)
  246. self.host._set_currentMidiProgram(pluginId, current)
  247. elif msg.startswith("MIDI_PROGRAM_DATA_"):
  248. pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
  249. bank, program = [int(i) for i in self.readlineblock().split(":")]
  250. name = self.readlineblock().replace("\r", "\n")
  251. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  252. elif msg == "error":
  253. error = self.readlineblock().replace("\r", "\n")
  254. engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0.0, error)
  255. elif msg == "show":
  256. self.d_uiShow()
  257. elif msg == "hide":
  258. self.d_uiHide()
  259. elif msg == "quit":
  260. self.fQuitReceived = True
  261. self.d_uiQuit()
  262. elif msg == "uiTitle":
  263. uiTitle = self.readlineblock().replace("\r", "\n")
  264. self.d_uiTitleChanged(uiTitle)
  265. else:
  266. print("unknown message: \"" + msg + "\"")
  267. return True
  268. # ------------------------------------------------------------------------------------------------------------
  269. # Embed plugin UI
  270. if LINUX and not config_UseQt5:
  271. from PyQt4.QtGui import QLabel, QHBoxLayout, QX11EmbedWidget
  272. class CarlaEmbedW(QX11EmbedWidget):
  273. def __init__(self, host, winId):
  274. QX11EmbedWidget.__init__(self)
  275. self.host = host
  276. self.fWinId = winId
  277. self.fLayout = QVBoxLayout(self)
  278. self.fLayout.setContentsMargins(0, 0, 0, 0)
  279. self.fLayout.setSpacing(0)
  280. self.setLayout(self.fLayout)
  281. gCarla.gui = CarlaMiniW(host, self)
  282. gCarla.gui.hide()
  283. gCarla.gui.ui.act_file_quit.setEnabled(False)
  284. gCarla.gui.ui.menu_File.setEnabled(False)
  285. gCarla.gui.ui.menu_File.setVisible(False)
  286. #menuBar = gCarla.gui.menuBar()
  287. #menuBar.removeAction(gCarla.gui.ui.menu_File.menuAction())
  288. self.addWidget(gCarla.gui.menuBar())
  289. self.addWidget(gCarla.gui.ui.toolBar)
  290. self.addWidget(gCarla.gui.centralWidget())
  291. self.setFixedSize(740, 512)
  292. self.embedInto(winId)
  293. self.show()
  294. def addWidget(self, widget):
  295. widget.setParent(self)
  296. self.fLayout.addWidget(widget)
  297. def showEvent(self, event):
  298. QX11EmbedWidget.showEvent(self, event)
  299. # set our gui as parent for all plugins UIs
  300. winIdStr = "%x" % self.fWinId
  301. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)
  302. def hideEvent(self, event):
  303. # disable parent
  304. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")
  305. QX11EmbedWidget.hideEvent(self, event)
  306. # ------------------------------------------------------------------------------------------------------------
  307. # Main
  308. if __name__ == '__main__':
  309. # -------------------------------------------------------------
  310. # App initialization
  311. app = CarlaApplication("Carla2-Plugin")
  312. # -------------------------------------------------------------
  313. # Set-up custom signal handling
  314. setUpSignals()
  315. # -------------------------------------------------------------
  316. # Init host backend
  317. host = initHost("Carla-Plugin", PluginHost, False, True, True)
  318. host.processMode = ENGINE_PROCESS_MODE_PATCHBAY if sys.argv[0].lower().endswith("/carla-plugin-patchbay") else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
  319. host.processModeForced = True
  320. # FIXME
  321. loadHostSettings(host)
  322. # -------------------------------------------------------------
  323. # Create GUI
  324. try:
  325. winId = int(os.getenv("CARLA_PLUGIN_EMBED_WINID"))
  326. except:
  327. winId = 0
  328. host.setenv("CARLA_PLUGIN_EMBED_WINID", "0")
  329. if LINUX and winId != 0 and not config_UseQt5:
  330. gui = CarlaEmbedW(host, winId)
  331. else:
  332. gui = CarlaMiniW(host)
  333. # -------------------------------------------------------------
  334. # simulate an engire started callback FIXME
  335. engineCallback(host, ENGINE_CALLBACK_ENGINE_STARTED, 0, host.processMode, ENGINE_TRANSPORT_MODE_PLUGIN, 0.0, "Plugin")
  336. # -------------------------------------------------------------
  337. # App-Loop
  338. app.exit_exec()