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.

441 lines
16KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla plugin host (plugin UI)
  4. # Copyright (C) 2013-2019 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 (Global)
  19. from PyQt5.QtGui import QKeySequence
  20. from PyQt5.QtWidgets import QHBoxLayout
  21. # ------------------------------------------------------------------------------------------------------------
  22. # Imports (Custom Stuff)
  23. from carla_host import *
  24. from externalui import ExternalUI
  25. # ------------------------------------------------------------------------------------------------------------
  26. # Host Plugin object
  27. class PluginHost(CarlaHostQtPlugin):
  28. def __init__(self):
  29. CarlaHostQtPlugin.__init__(self)
  30. if False:
  31. # kdevelop likes this :)
  32. self.fExternalUI = ExternalUI()
  33. # ---------------------------------------------------------------
  34. self.fExternalUI = None
  35. # -------------------------------------------------------------------
  36. def setExternalUI(self, extUI):
  37. self.fExternalUI = extUI
  38. def sendMsg(self, lines):
  39. if self.fExternalUI is None:
  40. return False
  41. return self.fExternalUI.send(lines)
  42. # -------------------------------------------------------------------
  43. def engine_init(self, driverName, clientName):
  44. return True
  45. def engine_close(self):
  46. return True
  47. def engine_idle(self):
  48. self.fExternalUI.idleExternalUI()
  49. def is_engine_running(self):
  50. if self.fExternalUI is None:
  51. return False
  52. return self.fExternalUI.isRunning()
  53. def set_engine_about_to_close(self):
  54. return True
  55. # ------------------------------------------------------------------------------------------------------------
  56. # Main Window
  57. class CarlaMiniW(ExternalUI, HostWindow):
  58. def __init__(self, host, isPatchbay, parent=None):
  59. ExternalUI.__init__(self)
  60. HostWindow.__init__(self, host, isPatchbay, parent)
  61. self.host = host
  62. if False:
  63. # kdevelop likes this :)
  64. host = PluginHost()
  65. self.host = host
  66. host.setExternalUI(self)
  67. self.fFirstInit = True
  68. self.setWindowTitle(self.fUiName)
  69. self.ready()
  70. # Override this as it can be called from several places.
  71. # We really need to close all UIs as events are driven by host idle which is only available when UI is visible
  72. def closeExternalUI(self):
  73. for i in reversed(range(self.fPluginCount)):
  74. self.host.show_custom_ui(i, False)
  75. ExternalUI.closeExternalUI(self)
  76. # -------------------------------------------------------------------
  77. # ExternalUI Callbacks
  78. def uiShow(self):
  79. if self.parent() is not None:
  80. return
  81. self.show()
  82. def uiFocus(self):
  83. if self.parent() is not None:
  84. return
  85. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  86. self.show()
  87. self.raise_()
  88. self.activateWindow()
  89. def uiHide(self):
  90. if self.parent() is not None:
  91. return
  92. self.hide()
  93. def uiQuit(self):
  94. self.closeExternalUI()
  95. self.close()
  96. if self != gui:
  97. gui.close()
  98. # there might be other qt windows open which will block carla-plugin from quitting
  99. app.quit()
  100. def uiTitleChanged(self, uiTitle):
  101. self.setWindowTitle(uiTitle)
  102. # -------------------------------------------------------------------
  103. # Qt events
  104. def closeEvent(self, event):
  105. self.closeExternalUI()
  106. HostWindow.closeEvent(self, event)
  107. # there might be other qt windows open which will block carla-plugin from quitting
  108. app.quit()
  109. # -------------------------------------------------------------------
  110. # Custom callback
  111. def msgCallback(self, msg):
  112. try:
  113. self.msgCallback2(msg)
  114. except:
  115. print("msgCallback error, skipped for", msg)
  116. def msgCallback2(self, msg):
  117. msg = charPtrToString(msg)
  118. #if not msg:
  119. #return
  120. if msg == "runtime-info":
  121. values = self.readlineblock().split(":")
  122. load = float(values[0])
  123. xruns = int(values[1])
  124. self.host._set_runtime_info(load, xruns)
  125. elif msg == "transport":
  126. playing = bool(self.readlineblock() == "true")
  127. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  128. bpm = float(self.readlineblock())
  129. self.host._set_transport(playing, frame, bar, beat, tick, bpm)
  130. elif msg.startswith("PEAKS_"):
  131. pluginId = int(msg.replace("PEAKS_", ""))
  132. in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
  133. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  134. elif msg.startswith("PARAMVAL_"):
  135. pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
  136. paramValue = float(self.readlineblock())
  137. if paramId < 0:
  138. self.host._set_internalValue(pluginId, paramId, paramValue)
  139. else:
  140. self.host._set_parameterValue(pluginId, paramId, paramValue)
  141. elif msg.startswith("ENGINE_CALLBACK_"):
  142. action = int(msg.replace("ENGINE_CALLBACK_", ""))
  143. pluginId = int(self.readlineblock())
  144. value1 = int(self.readlineblock())
  145. value2 = int(self.readlineblock())
  146. value3 = int(self.readlineblock())
  147. valuef = float(self.readlineblock())
  148. valueStr = self.readlineblock().replace("\r", "\n")
  149. if action == ENGINE_CALLBACK_PLUGIN_RENAMED:
  150. self.host._set_pluginName(pluginId, valueStr)
  151. elif action == ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED:
  152. if value1 < 0:
  153. self.host._set_internalValue(pluginId, value1, valuef)
  154. else:
  155. self.host._set_parameterValue(pluginId, value1, valuef)
  156. elif action == ENGINE_CALLBACK_PARAMETER_DEFAULT_CHANGED:
  157. self.host._set_parameterDefault(pluginId, value1, valuef)
  158. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CC_CHANGED:
  159. self.host._set_parameterMidiCC(pluginId, value1, value2)
  160. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CHANNEL_CHANGED:
  161. self.host._set_parameterMidiChannel(pluginId, value1, value2)
  162. elif action == ENGINE_CALLBACK_PROGRAM_CHANGED:
  163. self.host._set_currentProgram(pluginId, value1)
  164. elif action == ENGINE_CALLBACK_MIDI_PROGRAM_CHANGED:
  165. self.host._set_currentMidiProgram(pluginId, value1)
  166. engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)
  167. elif msg.startswith("ENGINE_OPTION_"):
  168. option = int(msg.replace("ENGINE_OPTION_", ""))
  169. forced = bool(self.readlineblock() == "true")
  170. value = self.readlineblock()
  171. if self.fFirstInit and not forced:
  172. return
  173. if option == ENGINE_OPTION_PROCESS_MODE:
  174. self.host.processMode = int(value)
  175. elif option == ENGINE_OPTION_TRANSPORT_MODE:
  176. self.host.transportMode = int(value)
  177. elif option == ENGINE_OPTION_FORCE_STEREO:
  178. self.host.forceStereo = bool(value == "true")
  179. elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
  180. self.host.preferPluginBridges = bool(value == "true")
  181. elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
  182. self.host.preferUIBridges = bool(value == "true")
  183. elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
  184. self.host.uisAlwaysOnTop = bool(value == "true")
  185. elif option == ENGINE_OPTION_MAX_PARAMETERS:
  186. self.host.maxParameters = int(value)
  187. elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
  188. self.host.uiBridgesTimeout = int(value)
  189. elif option == ENGINE_OPTION_PATH_BINARIES:
  190. self.host.pathBinaries = value
  191. elif option == ENGINE_OPTION_PATH_RESOURCES:
  192. self.host.pathResources = value
  193. elif msg.startswith("PLUGIN_INFO_"):
  194. pluginId = int(msg.replace("PLUGIN_INFO_", ""))
  195. self.host._add(pluginId)
  196. type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
  197. filename = self.readlineblock().replace("\r", "\n")
  198. name = self.readlineblock().replace("\r", "\n")
  199. iconName = self.readlineblock().replace("\r", "\n")
  200. realName = self.readlineblock().replace("\r", "\n")
  201. label = self.readlineblock().replace("\r", "\n")
  202. maker = self.readlineblock().replace("\r", "\n")
  203. copyright = self.readlineblock().replace("\r", "\n")
  204. pinfo = {
  205. 'type': type_,
  206. 'category': category,
  207. 'hints': hints,
  208. 'optionsAvailable': optsAvail,
  209. 'optionsEnabled': optsEnabled,
  210. 'filename': filename,
  211. 'name': name,
  212. 'label': label,
  213. 'maker': maker,
  214. 'copyright': copyright,
  215. 'iconName': iconName,
  216. 'patchbayClientId': 0,
  217. 'uniqueId': uniqueId
  218. }
  219. self.host._set_pluginInfo(pluginId, pinfo)
  220. self.host._set_pluginRealName(pluginId, realName)
  221. elif msg.startswith("AUDIO_COUNT_"):
  222. pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
  223. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  224. elif msg.startswith("MIDI_COUNT_"):
  225. pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
  226. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  227. elif msg.startswith("PARAMETER_COUNT_"):
  228. pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
  229. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  230. elif msg.startswith("PARAMETER_DATA_"):
  231. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
  232. paramType, paramHints, midiChannel, midiCC = [int(i) for i in self.readlineblock().split(":")]
  233. paramName = self.readlineblock().replace("\r", "\n")
  234. paramUnit = self.readlineblock().replace("\r", "\n")
  235. paramInfo = {
  236. 'name': paramName,
  237. 'symbol': "",
  238. 'unit': paramUnit,
  239. 'scalePointCount': 0,
  240. }
  241. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  242. paramData = {
  243. 'type': paramType,
  244. 'hints': paramHints,
  245. 'index': paramId,
  246. 'rindex': -1,
  247. 'midiCC': midiCC,
  248. 'midiChannel': midiChannel
  249. }
  250. self.host._set_parameterData(pluginId, paramId, paramData)
  251. elif msg.startswith("PARAMETER_RANGES_"):
  252. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
  253. def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]
  254. paramRanges = {
  255. 'def': def_,
  256. 'min': min_,
  257. 'max': max_,
  258. 'step': step,
  259. 'stepSmall': stepSmall,
  260. 'stepLarge': stepLarge
  261. }
  262. self.host._set_parameterRanges(pluginId, paramId, paramRanges)
  263. elif msg.startswith("PROGRAM_COUNT_"):
  264. pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
  265. self.host._set_programCount(pluginId, count)
  266. self.host._set_currentProgram(pluginId, current)
  267. elif msg.startswith("PROGRAM_NAME_"):
  268. pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
  269. progName = self.readlineblock().replace("\r", "\n")
  270. self.host._set_programName(pluginId, progId, progName)
  271. elif msg.startswith("MIDI_PROGRAM_COUNT_"):
  272. pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
  273. self.host._set_midiProgramCount(pluginId, count)
  274. self.host._set_currentMidiProgram(pluginId, current)
  275. elif msg.startswith("MIDI_PROGRAM_DATA_"):
  276. pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
  277. bank, program = [int(i) for i in self.readlineblock().split(":")]
  278. name = self.readlineblock().replace("\r", "\n")
  279. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  280. elif msg.startswith("CUSTOM_DATA_COUNT_"):
  281. pluginId, count = [int(i) for i in msg.replace("CUSTOM_DATA_COUNT_", "").split(":")]
  282. self.host._set_customDataCount(pluginId, count)
  283. elif msg.startswith("CUSTOM_DATA_"):
  284. pluginId, customDataId = [int(i) for i in msg.replace("CUSTOM_DATA_", "").split(":")]
  285. type_ = self.readlineblock().replace("\r", "\n")
  286. key = self.readlineblock().replace("\r", "\n")
  287. value = self.readlineblock().replace("\r", "\n")
  288. self.host._set_customData(pluginId, customDataId, {'type': type_, 'key': key, 'value': value})
  289. elif msg == "osc-urls":
  290. tcp = self.readlineblock().replace("\r", "\n")
  291. udp = self.readlineblock().replace("\r", "\n")
  292. self.host.fOscTCP = tcp
  293. self.host.fOscUDP = udp
  294. elif msg == "max-plugin-number":
  295. maxnum = int(self.readlineblock())
  296. self.host.fMaxPluginNumber = maxnum
  297. elif msg == "buffer-size":
  298. bufsize = int(self.readlineblock())
  299. self.host.fBufferSize = bufsize
  300. elif msg == "sample-rate":
  301. srate = float(self.readlineblock())
  302. self.host.fSampleRate = srate
  303. elif msg == "error":
  304. error = self.readlineblock().replace("\r", "\n")
  305. engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0, 0.0, error)
  306. elif msg == "show":
  307. self.fFirstInit = False
  308. self.uiShow()
  309. elif msg == "focus":
  310. self.uiFocus()
  311. elif msg == "hide":
  312. self.uiHide()
  313. elif msg == "quit":
  314. self.fQuitReceived = True
  315. self.uiQuit()
  316. elif msg == "uiTitle":
  317. uiTitle = self.readlineblock().replace("\r", "\n")
  318. self.uiTitleChanged(uiTitle)
  319. else:
  320. print("unknown message: \"" + msg + "\"")
  321. # ------------------------------------------------------------------------------------------------------------
  322. # Main
  323. if __name__ == '__main__':
  324. # -------------------------------------------------------------
  325. # App initialization
  326. app = CarlaApplication("Carla2-Plugin")
  327. # -------------------------------------------------------------
  328. # Set-up custom signal handling
  329. setUpSignals()
  330. # -------------------------------------------------------------
  331. # Init host backend
  332. isPatchbay = sys.argv[0].rsplit(os.path.sep)[-1].lower().replace(".exe","") == "carla-plugin-patchbay"
  333. host = initHost("Carla-Plugin", None, False, True, True, PluginHost)
  334. host.processMode = ENGINE_PROCESS_MODE_PATCHBAY if isPatchbay else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
  335. host.processModeForced = True
  336. host.nextProcessMode = host.processMode
  337. loadHostSettings(host)
  338. # -------------------------------------------------------------
  339. # Create GUI
  340. gui = CarlaMiniW(host, isPatchbay)
  341. # -------------------------------------------------------------
  342. # App-Loop
  343. app.exit_exec()