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.

477 lines
15KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla bridge for LV2 modguis
  4. # Copyright (C) 2015 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 doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Config)
  19. from carla_config import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (Global)
  22. if config_UseQt5:
  23. from PyQt5.QtCore import pyqtSlot, QThread, QUrl
  24. from PyQt5.QtWidgets import QMainWindow
  25. from PyQt5.QtWebKitWidgets import QWebView
  26. else:
  27. from PyQt4.QtCore import pyqtSlot, QPoint, QTimer, QThread, QUrl
  28. from PyQt4.QtGui import QMainWindow
  29. from PyQt4.QtWebKit import QWebElement, QWebSettings, QWebView
  30. # ------------------------------------------------------------------------------------------------------------
  31. # Imports (Custom)
  32. from carla_app import *
  33. from carla_utils import *
  34. # ------------------------------------------------------------------------------------------------------------
  35. # Generate a random port number between 9000 and 18000
  36. from random import random
  37. PORTn = 8998 + int(random()*9000)
  38. # ------------------------------------------------------------------------------------------------------------
  39. # Set up environment for the webserver
  40. PORT = str(PORTn)
  41. ROOT = "/usr/share"
  42. ROOT = "/home/falktx/Personal/FOSS/Git-mine/mod-app/source/modules"
  43. DATA_DIR = os.path.expanduser("~/.local/share/mod-data/")
  44. os.environ['MOD_DEV_HOST'] = "1"
  45. os.environ['MOD_DEV_HMI'] = "1"
  46. os.environ['MOD_DESKTOP'] = "1"
  47. os.environ['MOD_LOG'] = "0"
  48. os.environ['MOD_DATA_DIR'] = DATA_DIR
  49. os.environ['MOD_HTML_DIR'] = os.path.join(ROOT, "mod-ui", "html")
  50. os.environ['MOD_PLUGIN_LIBRARY_DIR'] = os.path.join(DATA_DIR, 'lib')
  51. os.environ['MOD_PHANTOM_BINARY'] = "/usr/bin/phantomjs"
  52. os.environ['MOD_SCREENSHOT_JS'] = os.path.join(ROOT, "mod-ui", "screenshot.js")
  53. os.environ['MOD_DEVICE_WEBSERVER_PORT'] = PORT
  54. #sys.path = [os.path.join(ROOT, "mod-ui")] + sys.path
  55. # ------------------------------------------------------------------------------------------------------------
  56. # Imports (MOD)
  57. from mod import webserver
  58. from mod.lv2 import PluginSerializer
  59. from mod.session import SESSION
  60. # Dummy monitor var, we don't need it
  61. SESSION.monitor_server = "skip"
  62. # ------------------------------------------------------------------------------------------------------------
  63. # WebServer Thread
  64. class WebServerThread(QThread):
  65. def __init__(self, parent=None):
  66. QThread.__init__(self, parent)
  67. def run(self):
  68. webserver.prepare()
  69. webserver.start()
  70. def stopWait(self):
  71. webserver.stop()
  72. return self.wait(5000)
  73. # ------------------------------------------------------------------------------------------------------------
  74. # Host Window
  75. class HostWindow(QMainWindow):
  76. # signals
  77. SIGTERM = pyqtSignal()
  78. SIGUSR1 = pyqtSignal()
  79. # --------------------------------------------------------------------------------------------------------
  80. def __init__(self):
  81. QMainWindow.__init__(self)
  82. gCarla.gui = self
  83. URI = sys.argv[1]
  84. # ----------------------------------------------------------------------------------------------------
  85. # Internal stuff
  86. self.fDocElemement = None
  87. self.fNeedsShow = False
  88. self.fSizeSetup = False
  89. self.fQuitReceived = False
  90. self.fControlBypass = None
  91. self.fControlPorts = []
  92. self.fPlugin = PluginSerializer(URI)
  93. self.fPorts = self.fPlugin.data['ports']
  94. # ----------------------------------------------------------------------------------------------------
  95. # Init pipe
  96. if len(sys.argv) == 7:
  97. self.fPipeClient = gCarla.utils.pipe_client_new(lambda s,msg: self.msgCallback(msg))
  98. else:
  99. self.fPipeClient = None
  100. # ----------------------------------------------------------------------------------------------------
  101. # Init Web server
  102. self.fWebServerThread = WebServerThread(self)
  103. self.fWebServerThread.start()
  104. # ----------------------------------------------------------------------------------------------------
  105. # Set up GUI
  106. self.fWebview = QWebView(self)
  107. self.setCentralWidget(self.fWebview)
  108. self.setContentsMargins(0, 0, 0, 0)
  109. mainFrame = self.fWebview.page().mainFrame()
  110. mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
  111. mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
  112. settings = self.fWebview.settings()
  113. settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
  114. self.fWebview.loadFinished.connect(self.slot_webviewLoadFinished)
  115. url = "http://127.0.0.1:%s/icon.html?v=0#%s" % (PORT, URI)
  116. self.fWebview.load(QUrl(url))
  117. # ----------------------------------------------------------------------------------------------------
  118. # Connect actions to functions
  119. self.SIGTERM.connect(self.slot_handleSIGTERM)
  120. # ----------------------------------------------------------------------------------------------------
  121. # Final setup
  122. self.fIdleTimer = self.startTimer(30)
  123. if self.fPipeClient is None:
  124. # testing, show UI only
  125. self.setWindowTitle("TestUI")
  126. self.fNeedsShow = True
  127. # --------------------------------------------------------------------------------------------------------
  128. @pyqtSlot(bool)
  129. def slot_webviewLoadFinished(self, ok):
  130. print("webview finished", ok, self.fWebview.title())
  131. self.fDocElemement = self.fWebview.page().currentFrame().documentElement()
  132. def trySetSizeIfNeeded(self):
  133. if self.fSizeSetup:
  134. return
  135. if self.fDocElemement is None or self.fDocElemement.isNull():
  136. return
  137. pedal = self.fDocElemement.findFirst(".mod-pedal")
  138. if pedal.isNull():
  139. return
  140. size = pedal.geometry().size()
  141. if size.width() <= 10 or size.height() <= 10:
  142. return
  143. self.fSizeSetup = True
  144. self.fDocElemement = None
  145. self.setFixedSize(size)
  146. self.fWebview.page().currentFrame().setScrollPosition(QPoint(15, 0))
  147. if self.fNeedsShow:
  148. self.show()
  149. for i in pedal.findAll("*"):
  150. if "mod-port-symbol" in i.attributeNames():
  151. if i.attribute("mod-role") == "input-control-port":
  152. self.fControlPorts.append((i.attribute("mod-port-symbol"), i))
  153. elif "mod-role" in i.attributeNames():
  154. if i.attribute("mod-role") == "bypass":
  155. self.fControlBypass = i
  156. def getPortByIndex(self, index):
  157. for port in self.fPorts['control']['input']:
  158. if port['index'] == index:
  159. return port
  160. return None
  161. def setKnobValue(self, port, value):
  162. for portSymbol, portElem in self.fControlPorts:
  163. if portSymbol != port['symbol']:
  164. continue
  165. height = int(portElem.styleProperty("height", QWebElement.ComputedStyle).replace("px",""))
  166. norm = (value-port['minimum'])/(port['maximum']-port['minimum'])
  167. real = int(norm*height*64)
  168. aprox = real-(real%height)
  169. valueStr = "%s%ipx 0px" % ("-" if aprox > 0 else "", aprox)
  170. portElem.setStyleProperty("background-position", valueStr)
  171. break
  172. # --------------------------------------------------------------------------------------------------------
  173. def closeExternalUI(self):
  174. self.fWebServerThread.stopWait()
  175. if self.fPipeClient is None:
  176. return
  177. if not self.fQuitReceived:
  178. self.send(["exiting"])
  179. gCarla.utils.pipe_client_destroy(self.fPipeClient)
  180. self.fPipeClient = None
  181. def idleExternalUI(self):
  182. if self.fPipeClient is not None:
  183. gCarla.utils.pipe_client_idle(self.fPipeClient)
  184. # --------------------------------------------------------------------------------------------------------
  185. # Callback
  186. def msgCallback(self, msg):
  187. msg = charPtrToString(msg)
  188. if msg == "control":
  189. index = int(self.readlineblock())
  190. value = float(self.readlineblock())
  191. self.dspParameterChanged(index, value)
  192. elif msg == "program":
  193. index = int(self.readlineblock())
  194. self.dspProgramChanged(index)
  195. elif msg == "midiprogram":
  196. bank = int(self.readlineblock())
  197. program = float(self.readlineblock())
  198. self.dspMidiProgramChanged(bank, program)
  199. elif msg == "configure":
  200. key = self.readlineblock()
  201. value = self.readlineblock()
  202. self.dspStateChanged(key, value)
  203. elif msg == "note":
  204. onOff = bool(self.readlineblock() == "true")
  205. channel = int(self.readlineblock())
  206. note = int(self.readlineblock())
  207. velocity = int(self.readlineblock())
  208. self.dspNoteReceived(onOff, channel, note, velocity)
  209. elif msg == "atom":
  210. index = int(self.readlineblock())
  211. size = int(self.readlineblock())
  212. base64atom = self.readlineblock()
  213. # nothing to do yet
  214. elif msg == "urid":
  215. urid = int(self.readlineblock())
  216. uri = self.readlineblock()
  217. # nothing to do yet
  218. elif msg == "uiOptions":
  219. sampleRate = float(self.readlineblock())
  220. useTheme = bool(self.readlineblock() == "true")
  221. useThemeColors = bool(self.readlineblock() == "true")
  222. windowTitle = self.readlineblock()
  223. transWindowId = int(self.readlineblock())
  224. self.uiTitleChanged(windowTitle)
  225. elif msg == "show":
  226. self.uiShow()
  227. elif msg == "focus":
  228. self.uiFocus()
  229. elif msg == "hide":
  230. self.uiHide()
  231. elif msg == "quit":
  232. self.fQuitReceived = True
  233. self.uiQuit()
  234. elif msg == "uiTitle":
  235. uiTitle = self.readlineblock()
  236. self.uiTitleChanged(uiTitle)
  237. else:
  238. print("unknown message: \"" + msg + "\"")
  239. # --------------------------------------------------------------------------------------------------------
  240. def dspParameterChanged(self, index, value):
  241. print("dspParameterChanged", index, value)
  242. self.setKnobValue(self.getPortByIndex(index), value)
  243. def dspProgramChanged(self, index):
  244. return
  245. def dspMidiProgramChanged(self, bank, program):
  246. return
  247. def dspStateChanged(self, key, value):
  248. return
  249. def dspNoteReceived(self, onOff, channel, note, velocity):
  250. return
  251. # --------------------------------------------------------------------------------------------------------
  252. def uiShow(self):
  253. if self.fSizeSetup:
  254. self.show()
  255. else:
  256. self.fNeedsShow = True
  257. def uiFocus(self):
  258. if not self.fSizeSetup:
  259. return
  260. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  261. self.show()
  262. self.raise_()
  263. self.activateWindow()
  264. def uiHide(self):
  265. self.hide()
  266. def uiQuit(self):
  267. self.closeExternalUI()
  268. self.close()
  269. app.quit()
  270. def uiTitleChanged(self, uiTitle):
  271. self.setWindowTitle(uiTitle)
  272. # --------------------------------------------------------------------------------------------------------
  273. # Qt events
  274. def closeEvent(self, event):
  275. self.closeExternalUI()
  276. QMainWindow.closeEvent(self, event)
  277. def timerEvent(self, event):
  278. if event.timerId() == self.fIdleTimer:
  279. self.trySetSizeIfNeeded()
  280. self.idleExternalUI()
  281. QMainWindow.timerEvent(self, event)
  282. # --------------------------------------------------------------------------------------------------------
  283. @pyqtSlot()
  284. def slot_handleSIGTERM(self):
  285. print("Got SIGTERM -> Closing now")
  286. self.close()
  287. # --------------------------------------------------------------------------------------------------------
  288. # Internal stuff
  289. def readlineblock(self):
  290. if self.fPipeClient is None:
  291. return ""
  292. return gCarla.utils.pipe_client_readlineblock(self.fPipeClient, 5000)
  293. def send(self, lines):
  294. if self.fPipeClient is None or len(lines) == 0:
  295. return
  296. gCarla.utils.pipe_client_lock(self.fPipeClient)
  297. # this must never fail, we need to unlock at the end
  298. try:
  299. for line in lines:
  300. if line is None:
  301. line2 = "(null)"
  302. elif isinstance(line, str):
  303. line2 = line.replace("\n", "\r")
  304. elif isinstance(line, bool):
  305. line2 = "true" if line else "false"
  306. elif isinstance(line, int):
  307. line2 = "%i" % line
  308. elif isinstance(line, float):
  309. line2 = "%.10f" % line
  310. else:
  311. print("unknown data type to send:", type(line))
  312. return
  313. gCarla.utils.pipe_client_write_msg(self.fPipeClient, line2 + "\n")
  314. except:
  315. pass
  316. gCarla.utils.pipe_client_flush_and_unlock(self.fPipeClient)
  317. # ------------------------------------------------------------------------------------------------------------
  318. # Main
  319. if __name__ == '__main__':
  320. # -------------------------------------------------------------
  321. # Read CLI args
  322. if len(sys.argv) < 3:
  323. print("usage: %s <plugin-uri> <ui-uri>" % sys.argv[0])
  324. sys.exit(1)
  325. libPrefix = os.getenv("CARLA_LIB_PREFIX")
  326. # -------------------------------------------------------------
  327. # App initialization
  328. app = CarlaApplication("Carla2-MODGUI", libPrefix)
  329. # -------------------------------------------------------------
  330. # Init utils
  331. pathBinaries, pathResources = getPaths(libPrefix)
  332. utilsname = "libcarla_utils.%s" % (DLL_EXTENSION)
  333. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, utilsname))
  334. gCarla.utils.set_process_name("carla-bridge-lv2-modgui")
  335. # -------------------------------------------------------------
  336. # Set-up custom signal handling
  337. setUpSignals()
  338. # -------------------------------------------------------------
  339. # Create GUI
  340. gui = HostWindow()
  341. # --------------------------------------------------------------------------------------------------------
  342. # App-Loop
  343. app.exit_exec()
  344. # ------------------------------------------------------------------------------------------------------------