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.

489 lines
16KB

  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, QPoint, QThread, QUrl
  24. from PyQt5.QtWidgets import QMainWindow
  25. from PyQt5.QtWebKitWidgets import QWebElement, QWebSettings, QWebView
  26. else:
  27. from PyQt4.QtCore import pyqtSlot, QPoint, 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. # FIXME
  55. os.environ['MOD_DEFAULT_JACK_BUFSIZE'] = "0"
  56. #sys.path = [os.path.join(ROOT, "mod-ui")] + sys.path
  57. # ------------------------------------------------------------------------------------------------------------
  58. # Imports (MOD)
  59. from mod import webserver
  60. from mod.lv2 import PluginSerializer
  61. from mod.session import SESSION
  62. # Dummy monitor var, we don't need it
  63. SESSION.monitor_server = "skip"
  64. # ------------------------------------------------------------------------------------------------------------
  65. # WebServer Thread
  66. class WebServerThread(QThread):
  67. def __init__(self, parent=None):
  68. QThread.__init__(self, parent)
  69. def run(self):
  70. webserver.run()
  71. def stopWait(self):
  72. webserver.stop()
  73. return self.wait(5000)
  74. # ------------------------------------------------------------------------------------------------------------
  75. # Host Window
  76. class HostWindow(QMainWindow):
  77. # signals
  78. SIGTERM = pyqtSignal()
  79. SIGUSR1 = pyqtSignal()
  80. # --------------------------------------------------------------------------------------------------------
  81. def __init__(self):
  82. QMainWindow.__init__(self)
  83. gCarla.gui = self
  84. URI = sys.argv[1]
  85. # ----------------------------------------------------------------------------------------------------
  86. # Internal stuff
  87. self.fCurrentFrame = None
  88. self.fDocElemement = None
  89. self.fCanSetValues = False
  90. self.fNeedsShow = False
  91. self.fSizeSetup = False
  92. self.fQuitReceived = False
  93. self.fWasRepainted = False
  94. self.fPlugin = PluginSerializer(URI)
  95. self.fPorts = self.fPlugin.data['ports']
  96. self.fPortSymbols = {}
  97. self.fPortValues = {}
  98. for port in self.fPorts['control']['input'] + self.fPorts['control']['output']:
  99. self.fPortSymbols[port['index']] = port['symbol']
  100. self.fPortValues [port['index']] = port['default']
  101. # ----------------------------------------------------------------------------------------------------
  102. # Init pipe
  103. if len(sys.argv) == 7:
  104. self.fPipeClient = gCarla.utils.pipe_client_new(lambda s,msg: self.msgCallback(msg))
  105. else:
  106. self.fPipeClient = None
  107. # ----------------------------------------------------------------------------------------------------
  108. # Init Web server
  109. self.fWebServerThread = WebServerThread(self)
  110. self.fWebServerThread.start()
  111. # ----------------------------------------------------------------------------------------------------
  112. # Set up GUI
  113. self.fWebview = QWebView(self)
  114. self.setCentralWidget(self.fWebview)
  115. self.setContentsMargins(0, 0, 0, 0)
  116. mainFrame = self.fWebview.page().mainFrame()
  117. mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
  118. mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
  119. settings = self.fWebview.settings()
  120. settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
  121. self.fWebview.loadFinished.connect(self.slot_webviewLoadFinished)
  122. url = "http://127.0.0.1:%s/icon.html?v=0#%s" % (PORT, URI)
  123. self.fWebview.load(QUrl(url))
  124. # ----------------------------------------------------------------------------------------------------
  125. # Connect actions to functions
  126. self.SIGTERM.connect(self.slot_handleSIGTERM)
  127. # ----------------------------------------------------------------------------------------------------
  128. # Final setup
  129. self.fIdleTimer = self.startTimer(30)
  130. if self.fPipeClient is None:
  131. # testing, show UI only
  132. self.setWindowTitle("TestUI")
  133. self.fNeedsShow = True
  134. # --------------------------------------------------------------------------------------------------------
  135. def closeExternalUI(self):
  136. self.fWebServerThread.stopWait()
  137. if self.fPipeClient is None:
  138. return
  139. if not self.fQuitReceived:
  140. self.send(["exiting"])
  141. gCarla.utils.pipe_client_destroy(self.fPipeClient)
  142. self.fPipeClient = None
  143. def idleStuff(self):
  144. if self.fPipeClient is not None:
  145. gCarla.utils.pipe_client_idle(self.fPipeClient)
  146. self.checkForRepaintChanges()
  147. if self.fSizeSetup:
  148. return
  149. if self.fDocElemement is None or self.fDocElemement.isNull():
  150. return
  151. pedal = self.fDocElemement.findFirst(".mod-pedal")
  152. if pedal.isNull():
  153. return
  154. size = pedal.geometry().size()
  155. if size.width() <= 10 or size.height() <= 10:
  156. return
  157. self.fSizeSetup = True
  158. self.fDocElemement = None
  159. self.setFixedSize(size)
  160. self.fCurrentFrame.setScrollPosition(QPoint(15, 0))
  161. # set initial values
  162. for index in self.fPortValues.keys():
  163. symbol = self.fPortSymbols[index]
  164. value = self.fPortValues[index]
  165. self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f)" % (symbol, value))
  166. if self.fNeedsShow:
  167. self.show()
  168. self.fCanSetValues = True
  169. def checkForRepaintChanges(self):
  170. if not self.fWasRepainted:
  171. return
  172. self.fWasRepainted = False
  173. if not self.fCanSetValues:
  174. return
  175. for index in self.fPortValues.keys():
  176. symbol = self.fPortSymbols[index]
  177. oldValue = self.fPortValues[index]
  178. newValue = self.fCurrentFrame.evaluateJavaScript("icongui.getPortValue('%s')" % (symbol,))
  179. if oldValue != newValue:
  180. self.fPortValues[index] = newValue
  181. self.send(["control", index, newValue])
  182. # --------------------------------------------------------------------------------------------------------
  183. @pyqtSlot(bool)
  184. def slot_webviewLoadFinished(self, ok):
  185. page = self.fWebview.page()
  186. page.repaintRequested.connect(self.slot_repaintRequested)
  187. self.fCurrentFrame = page.currentFrame()
  188. self.fDocElemement = self.fCurrentFrame.documentElement()
  189. def slot_repaintRequested(self):
  190. if self.fCanSetValues:
  191. self.fWasRepainted = True
  192. # --------------------------------------------------------------------------------------------------------
  193. # Callback
  194. def msgCallback(self, msg):
  195. msg = charPtrToString(msg)
  196. if msg == "control":
  197. index = int(self.readlineblock())
  198. value = float(self.readlineblock())
  199. self.dspParameterChanged(index, value)
  200. elif msg == "program":
  201. index = int(self.readlineblock())
  202. self.dspProgramChanged(index)
  203. elif msg == "midiprogram":
  204. bank = int(self.readlineblock())
  205. program = float(self.readlineblock())
  206. self.dspMidiProgramChanged(bank, program)
  207. elif msg == "configure":
  208. key = self.readlineblock()
  209. value = self.readlineblock()
  210. self.dspStateChanged(key, value)
  211. elif msg == "note":
  212. onOff = bool(self.readlineblock() == "true")
  213. channel = int(self.readlineblock())
  214. note = int(self.readlineblock())
  215. velocity = int(self.readlineblock())
  216. self.dspNoteReceived(onOff, channel, note, velocity)
  217. elif msg == "atom":
  218. index = int(self.readlineblock())
  219. size = int(self.readlineblock())
  220. base64atom = self.readlineblock()
  221. # nothing to do yet
  222. elif msg == "urid":
  223. urid = int(self.readlineblock())
  224. uri = self.readlineblock()
  225. # nothing to do yet
  226. elif msg == "uiOptions":
  227. sampleRate = float(self.readlineblock())
  228. useTheme = bool(self.readlineblock() == "true")
  229. useThemeColors = bool(self.readlineblock() == "true")
  230. windowTitle = self.readlineblock()
  231. transWindowId = int(self.readlineblock())
  232. self.uiTitleChanged(windowTitle)
  233. elif msg == "show":
  234. self.uiShow()
  235. elif msg == "focus":
  236. self.uiFocus()
  237. elif msg == "hide":
  238. self.uiHide()
  239. elif msg == "quit":
  240. self.fQuitReceived = True
  241. self.uiQuit()
  242. elif msg == "uiTitle":
  243. uiTitle = self.readlineblock()
  244. self.uiTitleChanged(uiTitle)
  245. else:
  246. print("unknown message: \"" + msg + "\"")
  247. # --------------------------------------------------------------------------------------------------------
  248. def dspParameterChanged(self, index, value):
  249. self.fPortValues[index] = value
  250. if self.fCurrentFrame is not None:
  251. symbol = self.fPortSymbols[index]
  252. self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f)" % (symbol, value))
  253. def dspProgramChanged(self, index):
  254. return
  255. def dspMidiProgramChanged(self, bank, program):
  256. return
  257. def dspStateChanged(self, key, value):
  258. return
  259. def dspNoteReceived(self, onOff, channel, note, velocity):
  260. return
  261. # --------------------------------------------------------------------------------------------------------
  262. def uiShow(self):
  263. if self.fSizeSetup:
  264. self.show()
  265. else:
  266. self.fNeedsShow = True
  267. def uiFocus(self):
  268. if not self.fSizeSetup:
  269. return
  270. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  271. self.show()
  272. self.raise_()
  273. self.activateWindow()
  274. def uiHide(self):
  275. self.hide()
  276. def uiQuit(self):
  277. self.closeExternalUI()
  278. self.close()
  279. app.quit()
  280. def uiTitleChanged(self, uiTitle):
  281. self.setWindowTitle(uiTitle)
  282. # --------------------------------------------------------------------------------------------------------
  283. # Qt events
  284. def closeEvent(self, event):
  285. self.closeExternalUI()
  286. QMainWindow.closeEvent(self, event)
  287. def timerEvent(self, event):
  288. if event.timerId() == self.fIdleTimer:
  289. self.idleStuff()
  290. QMainWindow.timerEvent(self, event)
  291. # --------------------------------------------------------------------------------------------------------
  292. @pyqtSlot()
  293. def slot_handleSIGTERM(self):
  294. print("Got SIGTERM -> Closing now")
  295. self.close()
  296. # --------------------------------------------------------------------------------------------------------
  297. # Internal stuff
  298. def readlineblock(self):
  299. if self.fPipeClient is None:
  300. return ""
  301. return gCarla.utils.pipe_client_readlineblock(self.fPipeClient, 5000)
  302. def send(self, lines):
  303. if self.fPipeClient is None or len(lines) == 0:
  304. return
  305. gCarla.utils.pipe_client_lock(self.fPipeClient)
  306. # this must never fail, we need to unlock at the end
  307. try:
  308. for line in lines:
  309. if line is None:
  310. line2 = "(null)"
  311. elif isinstance(line, str):
  312. line2 = line.replace("\n", "\r")
  313. elif isinstance(line, bool):
  314. line2 = "true" if line else "false"
  315. elif isinstance(line, int):
  316. line2 = "%i" % line
  317. elif isinstance(line, float):
  318. line2 = "%.10f" % line
  319. else:
  320. print("unknown data type to send:", type(line))
  321. return
  322. gCarla.utils.pipe_client_write_msg(self.fPipeClient, line2 + "\n")
  323. except:
  324. pass
  325. gCarla.utils.pipe_client_flush_and_unlock(self.fPipeClient)
  326. # ------------------------------------------------------------------------------------------------------------
  327. # Main
  328. if __name__ == '__main__':
  329. # -------------------------------------------------------------
  330. # Read CLI args
  331. if len(sys.argv) < 3:
  332. print("usage: %s <plugin-uri> <ui-uri>" % sys.argv[0])
  333. sys.exit(1)
  334. libPrefix = os.getenv("CARLA_LIB_PREFIX")
  335. # -------------------------------------------------------------
  336. # App initialization
  337. app = CarlaApplication("Carla2-MODGUI", libPrefix)
  338. # -------------------------------------------------------------
  339. # Init utils
  340. pathBinaries, pathResources = getPaths(libPrefix)
  341. utilsname = "libcarla_utils.%s" % (DLL_EXTENSION)
  342. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, utilsname))
  343. gCarla.utils.set_process_name("carla-bridge-lv2-modgui")
  344. # -------------------------------------------------------------
  345. # Set-up custom signal handling
  346. setUpSignals()
  347. # -------------------------------------------------------------
  348. # Create GUI
  349. gui = HostWindow()
  350. # --------------------------------------------------------------------------------------------------------
  351. # App-Loop
  352. app.exit_exec()
  353. # ------------------------------------------------------------------------------------------------------------