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.

397 lines
15KB

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