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.

460 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. # -------------------------------------------------------------------
  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 None:
  69. self.show()
  70. def uiHide(self):
  71. if self.parent() is None:
  72. self.hide()
  73. def uiQuit(self):
  74. self.closeExternalUI()
  75. self.close()
  76. app.quit()
  77. def uiTitleChanged(self, uiTitle):
  78. self.setWindowTitle(uiTitle)
  79. # -------------------------------------------------------------------
  80. # Qt events
  81. def closeEvent(self, event):
  82. self.closeExternalUI()
  83. HostWindow.closeEvent(self, event)
  84. # -------------------------------------------------------------------
  85. # Custom callback
  86. def msgCallback(self, msg):
  87. try:
  88. self.msgCallback2(msg)
  89. except:
  90. print("msgCallback error, skipped for", msg)
  91. def msgCallback2(self, msg):
  92. msg = charPtrToString(msg)
  93. #if not msg:
  94. #return
  95. if msg.startswith("PEAKS_"):
  96. pluginId = int(msg.replace("PEAKS_", ""))
  97. in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
  98. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  99. elif msg.startswith("PARAMVAL_"):
  100. pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
  101. paramValue = float(self.readlineblock())
  102. if paramId < 0:
  103. self.host._set_internalValue(pluginId, paramId, paramValue)
  104. else:
  105. self.host._set_parameterValue(pluginId, paramId, paramValue)
  106. elif msg.startswith("ENGINE_CALLBACK_"):
  107. action = int(msg.replace("ENGINE_CALLBACK_", ""))
  108. pluginId = int(self.readlineblock())
  109. value1 = int(self.readlineblock())
  110. value2 = int(self.readlineblock())
  111. value3 = float(self.readlineblock())
  112. valueStr = self.readlineblock().replace("\r", "\n")
  113. if action == ENGINE_CALLBACK_PLUGIN_RENAMED:
  114. self.host._set_pluginName(pluginId, valueStr)
  115. elif action == ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED:
  116. if value1 < 0:
  117. self.host._set_internalValue(pluginId, value1, value3)
  118. else:
  119. self.host._set_parameterValue(pluginId, value1, value3)
  120. elif action == ENGINE_CALLBACK_PARAMETER_DEFAULT_CHANGED:
  121. self.host._set_parameterDefault(pluginId, value1, value3)
  122. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CC_CHANGED:
  123. self.host._set_parameterMidiCC(pluginId, value1, value2)
  124. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CHANNEL_CHANGED:
  125. self.host._set_parameterMidiChannel(pluginId, value1, value2)
  126. elif action == ENGINE_CALLBACK_PROGRAM_CHANGED:
  127. self.host._set_currentProgram(pluginId, value1)
  128. elif action == ENGINE_CALLBACK_MIDI_PROGRAM_CHANGED:
  129. self.host._set_currentMidiProgram(pluginId, value1)
  130. engineCallback(self.host, action, pluginId, value1, value2, value3, valueStr)
  131. elif msg.startswith("ENGINE_OPTION_"):
  132. option = int(msg.replace("ENGINE_OPTION_", ""))
  133. forced = bool(self.readlineblock() == "true")
  134. value = self.readlineblock()
  135. if self.fFirstInit and not forced:
  136. return
  137. if option == ENGINE_OPTION_PROCESS_MODE:
  138. self.host.processMode = int(value)
  139. elif option == ENGINE_OPTION_TRANSPORT_MODE:
  140. self.host.transportMode = int(value)
  141. elif option == ENGINE_OPTION_FORCE_STEREO:
  142. self.host.forceStereo = bool(value == "true")
  143. elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
  144. self.host.preferPluginBridges = bool(value == "true")
  145. elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
  146. self.host.preferUIBridges = bool(value == "true")
  147. elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
  148. self.host.uisAlwaysOnTop = bool(value == "true")
  149. elif option == ENGINE_OPTION_MAX_PARAMETERS:
  150. self.host.maxParameters = int(value)
  151. elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
  152. self.host.uiBridgesTimeout = int(value)
  153. elif option == ENGINE_OPTION_PATH_BINARIES:
  154. self.host.pathBinaries = value
  155. elif option == ENGINE_OPTION_PATH_RESOURCES:
  156. self.host.pathResources = value
  157. elif msg.startswith("PLUGIN_INFO_"):
  158. pluginId = int(msg.replace("PLUGIN_INFO_", ""))
  159. self.host._add(pluginId)
  160. type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
  161. filename = self.readlineblock().replace("\r", "\n")
  162. name = self.readlineblock().replace("\r", "\n")
  163. iconName = self.readlineblock().replace("\r", "\n")
  164. realName = self.readlineblock().replace("\r", "\n")
  165. label = self.readlineblock().replace("\r", "\n")
  166. maker = self.readlineblock().replace("\r", "\n")
  167. copyright = self.readlineblock().replace("\r", "\n")
  168. pinfo = {
  169. 'type': type_,
  170. 'category': category,
  171. 'hints': hints,
  172. 'optionsAvailable': optsAvail,
  173. 'optionsEnabled': optsEnabled,
  174. 'filename': filename,
  175. 'name': name,
  176. 'label': label,
  177. 'maker': maker,
  178. 'copyright': copyright,
  179. 'iconName': iconName,
  180. 'patchbayClientId': 0,
  181. 'uniqueId': uniqueId
  182. }
  183. self.host._set_pluginInfo(pluginId, pinfo)
  184. self.host._set_pluginRealName(pluginId, realName)
  185. elif msg.startswith("AUDIO_COUNT_"):
  186. pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
  187. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  188. elif msg.startswith("MIDI_COUNT_"):
  189. pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
  190. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  191. elif msg.startswith("PARAMETER_COUNT_"):
  192. pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
  193. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  194. elif msg.startswith("PARAMETER_DATA_"):
  195. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
  196. paramType, paramHints, midiChannel, midiCC = [int(i) for i in self.readlineblock().split(":")]
  197. paramName = self.readlineblock().replace("\r", "\n")
  198. paramUnit = self.readlineblock().replace("\r", "\n")
  199. paramInfo = {
  200. 'name': paramName,
  201. 'symbol': "",
  202. 'unit': paramUnit,
  203. 'scalePointCount': 0,
  204. }
  205. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  206. paramData = {
  207. 'type': paramType,
  208. 'hints': paramHints,
  209. 'index': paramId,
  210. 'rindex': -1,
  211. 'midiCC': midiCC,
  212. 'midiChannel': midiChannel
  213. }
  214. self.host._set_parameterData(pluginId, paramId, paramData)
  215. elif msg.startswith("PARAMETER_RANGES_"):
  216. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
  217. def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]
  218. paramRanges = {
  219. 'def': def_,
  220. 'min': min_,
  221. 'max': max_,
  222. 'step': step,
  223. 'stepSmall': stepSmall,
  224. 'stepLarge': stepLarge
  225. }
  226. self.host._set_parameterRanges(pluginId, paramId, paramRanges)
  227. elif msg.startswith("PROGRAM_COUNT_"):
  228. pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
  229. self.host._set_programCount(pluginId, count)
  230. self.host._set_currentProgram(pluginId, current)
  231. elif msg.startswith("PROGRAM_NAME_"):
  232. pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
  233. progName = self.readlineblock().replace("\r", "\n")
  234. self.host._set_programName(pluginId, progId, progName)
  235. elif msg.startswith("MIDI_PROGRAM_COUNT_"):
  236. pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
  237. self.host._set_midiProgramCount(pluginId, count)
  238. self.host._set_currentMidiProgram(pluginId, current)
  239. elif msg.startswith("MIDI_PROGRAM_DATA_"):
  240. pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
  241. bank, program = [int(i) for i in self.readlineblock().split(":")]
  242. name = self.readlineblock().replace("\r", "\n")
  243. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  244. elif msg == "complete-license":
  245. license = self.readlineblock().replace("\r", "\n")
  246. self.host.fCompleteLicenseText = license
  247. elif msg == "juce-version":
  248. version = self.readlineblock().replace("\r", "\n")
  249. self.host.fJuceVersion = version
  250. elif msg == "file-exts":
  251. exts = self.readlineblock().replace("\r", "\n")
  252. self.host.fSupportedFileExts = exts
  253. # only now we know the supported extensions
  254. self.fDirModel.setNameFilters(exts.split(";"))
  255. elif msg == "max-plugin-number":
  256. maxnum = int(self.readlineblock())
  257. self.host.fMaxPluginNumber = maxnum
  258. elif msg == "buffer-size":
  259. bufsize = int(self.readlineblock())
  260. self.host.fBufferSize = bufsize
  261. elif msg == "sample-rate":
  262. srate = float(self.readlineblock())
  263. self.host.fSampleRate = srate
  264. elif msg == "transport":
  265. playing = bool(self.readlineblock() == "true")
  266. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  267. bpm = float(self.readlineblock())
  268. self.host._set_transport(playing, frame, bar, beat, tick, bpm)
  269. elif msg == "error":
  270. error = self.readlineblock().replace("\r", "\n")
  271. engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0.0, error)
  272. elif msg == "show":
  273. self.fFirstInit = False
  274. self.uiShow()
  275. elif msg == "hide":
  276. self.uiHide()
  277. elif msg == "quit":
  278. self.fQuitReceived = True
  279. self.uiQuit()
  280. elif msg == "uiTitle":
  281. uiTitle = self.readlineblock().replace("\r", "\n")
  282. self.uiTitleChanged(uiTitle)
  283. else:
  284. print("unknown message: \"" + msg + "\"")
  285. # ------------------------------------------------------------------------------------------------------------
  286. # Embed plugin UI
  287. if LINUX and not config_UseQt5:
  288. from PyQt4.QtGui import QHBoxLayout, QX11EmbedWidget
  289. class CarlaEmbedW(QX11EmbedWidget):
  290. def __init__(self, host, winId):
  291. QX11EmbedWidget.__init__(self)
  292. self.host = host
  293. self.fWinId = winId
  294. self.fLayout = QVBoxLayout(self)
  295. self.fLayout.setContentsMargins(0, 0, 0, 0)
  296. self.fLayout.setSpacing(0)
  297. self.setLayout(self.fLayout)
  298. gui = CarlaMiniW(host, self)
  299. gui.hide()
  300. gui.ui.act_file_quit.setEnabled(False)
  301. gui.ui.menu_File.setEnabled(False)
  302. gui.ui.menu_File.setVisible(False)
  303. #menuBar = gui.menuBar()
  304. #menuBar.removeAction(gui.ui.menu_File.menuAction())
  305. self.addWidget(gui.menuBar())
  306. self.addLine()
  307. self.addWidget(gui.ui.toolBar)
  308. self.addLine()
  309. self.addWidget(gui.centralWidget())
  310. self.setFixedSize(740, 512)
  311. self.embedInto(winId)
  312. self.show()
  313. def addWidget(self, widget):
  314. widget.setParent(self)
  315. self.fLayout.addWidget(widget)
  316. def addLine(self):
  317. line = QFrame(self)
  318. line.setFrameShadow(QFrame.Sunken)
  319. line.setFrameShape(QFrame.HLine)
  320. line.setLineWidth(0)
  321. line.setMidLineWidth(1)
  322. self.fLayout.addWidget(line)
  323. def showEvent(self, event):
  324. QX11EmbedWidget.showEvent(self, event)
  325. # set our gui as parent for all plugins UIs
  326. winIdStr = "%x" % self.fWinId
  327. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)
  328. def hideEvent(self, event):
  329. # disable parent
  330. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")
  331. QX11EmbedWidget.hideEvent(self, event)
  332. # ------------------------------------------------------------------------------------------------------------
  333. # Main
  334. if __name__ == '__main__':
  335. # -------------------------------------------------------------
  336. # App initialization
  337. app = CarlaApplication("Carla2-Plugin")
  338. # -------------------------------------------------------------
  339. # Set-up custom signal handling
  340. setUpSignals()
  341. # -------------------------------------------------------------
  342. # Init host backend
  343. host = initHost("Carla-Plugin", PluginHost, False, True, True)
  344. host.processMode = ENGINE_PROCESS_MODE_PATCHBAY if sys.argv[0].lower().endswith("/carla-plugin-patchbay") else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
  345. host.processModeForced = True
  346. loadHostSettings(host)
  347. # -------------------------------------------------------------
  348. # Create GUI
  349. try:
  350. winId = int(os.getenv("CARLA_PLUGIN_EMBED_WINID"))
  351. except:
  352. winId = 0
  353. host.setenv("CARLA_PLUGIN_EMBED_WINID", "0")
  354. if LINUX and winId != 0 and not config_UseQt5:
  355. gui = CarlaEmbedW(host, winId)
  356. else:
  357. gui = CarlaMiniW(host)
  358. # -------------------------------------------------------------
  359. # App-Loop
  360. app.exit_exec()