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.

carla_modgui.py 17KB

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