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.

461 lines
15KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla bridge for LV2 modguis
  4. # Copyright (C) 2015-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 doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QSize, QUrl
  20. from PyQt5.QtGui import QImage, QPainter, QPalette
  21. from PyQt5.QtWidgets import QApplication, QMainWindow
  22. from PyQt5.QtWebKit import QWebSettings
  23. from PyQt5.QtWebKitWidgets import QWebView
  24. import sys
  25. # ------------------------------------------------------------------------------------------------------------
  26. # Imports (Custom)
  27. from carla_host import charPtrToString, gCarla
  28. from .webserver import WebServerThread, PORT
  29. # ------------------------------------------------------------------------------------------------------------
  30. # Imports (MOD)
  31. from modtools.utils import get_plugin_info, init as lv2_init
  32. # ------------------------------------------------------------------------------------------------------------
  33. # Host Window
  34. class HostWindow(QMainWindow):
  35. # signals
  36. SIGTERM = pyqtSignal()
  37. SIGUSR1 = pyqtSignal()
  38. # --------------------------------------------------------------------------------------------------------
  39. def __init__(self):
  40. QMainWindow.__init__(self)
  41. gCarla.gui = self
  42. URI = sys.argv[1]
  43. # ----------------------------------------------------------------------------------------------------
  44. # Internal stuff
  45. self.fCurrentFrame = None
  46. self.fDocElemement = None
  47. self.fCanSetValues = False
  48. self.fNeedsShow = False
  49. self.fSizeSetup = False
  50. self.fQuitReceived = False
  51. self.fWasRepainted = False
  52. lv2_init()
  53. self.fPlugin = get_plugin_info(URI)
  54. self.fPorts = self.fPlugin['ports']
  55. self.fPortSymbols = {}
  56. self.fPortValues = {}
  57. for port in self.fPorts['control']['input']:
  58. self.fPortSymbols[port['index']] = (port['symbol'], False)
  59. self.fPortValues [port['index']] = port['ranges']['default']
  60. for port in self.fPorts['control']['output']:
  61. self.fPortSymbols[port['index']] = (port['symbol'], True)
  62. self.fPortValues [port['index']] = port['ranges']['default']
  63. # ----------------------------------------------------------------------------------------------------
  64. # Init pipe
  65. if len(sys.argv) == 7:
  66. self.fPipeClient = gCarla.utils.pipe_client_new(lambda s,msg: self.msgCallback(msg))
  67. else:
  68. self.fPipeClient = None
  69. # ----------------------------------------------------------------------------------------------------
  70. # Init Web server
  71. self.fWebServerThread = WebServerThread(self)
  72. self.fWebServerThread.start()
  73. # ----------------------------------------------------------------------------------------------------
  74. # Set up GUI
  75. self.setContentsMargins(0, 0, 0, 0)
  76. self.fWebview = QWebView(self)
  77. #self.fWebview.setAttribute(Qt.WA_OpaquePaintEvent, False)
  78. #self.fWebview.setAttribute(Qt.WA_TranslucentBackground, True)
  79. self.setCentralWidget(self.fWebview)
  80. page = self.fWebview.page()
  81. page.setViewportSize(QSize(980, 600))
  82. mainFrame = page.mainFrame()
  83. mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
  84. mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
  85. palette = self.fWebview.palette()
  86. palette.setBrush(QPalette.Base, palette.brush(QPalette.Window))
  87. page.setPalette(palette)
  88. self.fWebview.setPalette(palette)
  89. settings = self.fWebview.settings()
  90. settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
  91. self.fWebview.loadFinished.connect(self.slot_webviewLoadFinished)
  92. url = "http://127.0.0.1:%s/icon.html#%s" % (PORT, URI)
  93. print("url:", url)
  94. self.fWebview.load(QUrl(url))
  95. # ----------------------------------------------------------------------------------------------------
  96. # Connect actions to functions
  97. self.SIGTERM.connect(self.slot_handleSIGTERM)
  98. # ----------------------------------------------------------------------------------------------------
  99. # Final setup
  100. self.fIdleTimer = self.startTimer(30)
  101. if self.fPipeClient is None:
  102. # testing, show UI only
  103. self.setWindowTitle("TestUI")
  104. self.fNeedsShow = True
  105. # --------------------------------------------------------------------------------------------------------
  106. def closeExternalUI(self):
  107. self.fWebServerThread.stopWait()
  108. if self.fPipeClient is None:
  109. return
  110. if not self.fQuitReceived:
  111. self.send(["exiting"])
  112. gCarla.utils.pipe_client_destroy(self.fPipeClient)
  113. self.fPipeClient = None
  114. def idleStuff(self):
  115. if self.fPipeClient is not None:
  116. gCarla.utils.pipe_client_idle(self.fPipeClient)
  117. self.checkForRepaintChanges()
  118. if self.fSizeSetup:
  119. return
  120. if self.fDocElemement is None or self.fDocElemement.isNull():
  121. return
  122. pedal = self.fDocElemement.findFirst(".mod-pedal")
  123. if pedal.isNull():
  124. return
  125. size = pedal.geometry().size()
  126. if size.width() <= 10 or size.height() <= 10:
  127. return
  128. # render web frame to image
  129. image = QImage(self.fWebview.page().viewportSize(), QImage.Format_ARGB32_Premultiplied)
  130. image.fill(Qt.transparent)
  131. painter = QPainter(image)
  132. self.fCurrentFrame.render(painter)
  133. painter.end()
  134. #image.save("/tmp/test.png")
  135. # get coordinates and size from image
  136. #x = -1
  137. #y = -1
  138. #lastx = -1
  139. #lasty = -1
  140. #bgcol = self.fHostColor.rgba()
  141. #for h in range(0, image.height()):
  142. #hasNonTransPixels = False
  143. #for w in range(0, image.width()):
  144. #if image.pixel(w, h) not in (0, bgcol): # 0xff070707):
  145. #hasNonTransPixels = True
  146. #if x == -1 or x > w:
  147. #x = w
  148. #lastx = max(lastx, w)
  149. #if hasNonTransPixels:
  150. ##if y == -1:
  151. ##y = h
  152. #lasty = h
  153. # set size and position accordingly
  154. #if -1 not in (x, lastx, lasty):
  155. #self.setFixedSize(lastx-x, lasty)
  156. #self.fCurrentFrame.setScrollPosition(QPoint(x, 0))
  157. #else:
  158. # TODO that^ needs work
  159. if True:
  160. self.setFixedSize(size)
  161. # set initial values
  162. self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue(':bypass', 0, null)")
  163. for index in self.fPortValues.keys():
  164. symbol, isOutput = self.fPortSymbols[index]
  165. value = self.fPortValues[index]
  166. if isOutput:
  167. self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value))
  168. else:
  169. self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f, null)" % (symbol, value))
  170. # final setup
  171. self.fCanSetValues = True
  172. self.fSizeSetup = True
  173. self.fDocElemement = None
  174. if self.fNeedsShow:
  175. self.show()
  176. def checkForRepaintChanges(self):
  177. if not self.fWasRepainted:
  178. return
  179. self.fWasRepainted = False
  180. if not self.fCanSetValues:
  181. return
  182. for index in self.fPortValues.keys():
  183. symbol, isOutput = self.fPortSymbols[index]
  184. if isOutput:
  185. continue
  186. oldValue = self.fPortValues[index]
  187. newValue = self.fCurrentFrame.evaluateJavaScript("icongui.getPortValue('%s')" % (symbol,))
  188. if oldValue != newValue:
  189. self.fPortValues[index] = newValue
  190. self.send(["control", index, newValue])
  191. # --------------------------------------------------------------------------------------------------------
  192. @pyqtSlot(bool)
  193. def slot_webviewLoadFinished(self, ok):
  194. page = self.fWebview.page()
  195. page.repaintRequested.connect(self.slot_repaintRequested)
  196. self.fCurrentFrame = page.currentFrame()
  197. self.fDocElemement = self.fCurrentFrame.documentElement()
  198. def slot_repaintRequested(self):
  199. if self.fCanSetValues:
  200. self.fWasRepainted = True
  201. # --------------------------------------------------------------------------------------------------------
  202. # Callback
  203. def msgCallback(self, msg):
  204. msg = charPtrToString(msg)
  205. if msg == "control":
  206. index = int(self.readlineblock())
  207. value = float(self.readlineblock())
  208. self.dspParameterChanged(index, value)
  209. elif msg == "program":
  210. index = int(self.readlineblock())
  211. self.dspProgramChanged(index)
  212. elif msg == "midiprogram":
  213. bank = int(self.readlineblock())
  214. program = float(self.readlineblock())
  215. self.dspMidiProgramChanged(bank, program)
  216. elif msg == "configure":
  217. key = self.readlineblock()
  218. value = self.readlineblock()
  219. self.dspStateChanged(key, value)
  220. elif msg == "note":
  221. onOff = bool(self.readlineblock() == "true")
  222. channel = int(self.readlineblock())
  223. note = int(self.readlineblock())
  224. velocity = int(self.readlineblock())
  225. self.dspNoteReceived(onOff, channel, note, velocity)
  226. elif msg == "atom":
  227. index = int(self.readlineblock())
  228. size = int(self.readlineblock())
  229. base64atom = self.readlineblock()
  230. # nothing to do yet
  231. elif msg == "urid":
  232. urid = int(self.readlineblock())
  233. uri = self.readlineblock()
  234. # nothing to do yet
  235. elif msg == "uiOptions":
  236. sampleRate = float(self.readlineblock())
  237. uiScale = float(self.readlineblock())
  238. useTheme = bool(self.readlineblock() == "true")
  239. useThemeColors = bool(self.readlineblock() == "true")
  240. windowTitle = self.readlineblock()
  241. transWindowId = int(self.readlineblock())
  242. self.uiTitleChanged(windowTitle)
  243. elif msg == "show":
  244. self.uiShow()
  245. elif msg == "focus":
  246. self.uiFocus()
  247. elif msg == "hide":
  248. self.uiHide()
  249. elif msg == "quit":
  250. self.fQuitReceived = True
  251. self.uiQuit()
  252. elif msg == "uiTitle":
  253. uiTitle = self.readlineblock()
  254. self.uiTitleChanged(uiTitle)
  255. else:
  256. print("unknown message: \"" + msg + "\"")
  257. # --------------------------------------------------------------------------------------------------------
  258. def dspParameterChanged(self, index, value):
  259. self.fPortValues[index] = value
  260. if self.fCurrentFrame is not None and self.fCanSetValues:
  261. symbol, isOutput = self.fPortSymbols[index]
  262. if isOutput:
  263. self.fPortValues[index] = value
  264. self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value))
  265. else:
  266. self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f, null)" % (symbol, value))
  267. def dspProgramChanged(self, index):
  268. return
  269. def dspMidiProgramChanged(self, bank, program):
  270. return
  271. def dspStateChanged(self, key, value):
  272. return
  273. def dspNoteReceived(self, onOff, channel, note, velocity):
  274. return
  275. # --------------------------------------------------------------------------------------------------------
  276. def uiShow(self):
  277. if self.fSizeSetup:
  278. self.show()
  279. else:
  280. self.fNeedsShow = True
  281. def uiFocus(self):
  282. if not self.fSizeSetup:
  283. return
  284. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  285. self.show()
  286. self.raise_()
  287. self.activateWindow()
  288. def uiHide(self):
  289. self.hide()
  290. def uiQuit(self):
  291. self.closeExternalUI()
  292. self.close()
  293. QApplication.instance().quit()
  294. def uiTitleChanged(self, uiTitle):
  295. self.setWindowTitle(uiTitle)
  296. # --------------------------------------------------------------------------------------------------------
  297. # Qt events
  298. def closeEvent(self, event):
  299. self.closeExternalUI()
  300. QMainWindow.closeEvent(self, event)
  301. # there might be other qt windows open which will block carla-modgui from quitting
  302. QApplication.instance().quit()
  303. def timerEvent(self, event):
  304. if event.timerId() == self.fIdleTimer:
  305. self.idleStuff()
  306. QMainWindow.timerEvent(self, event)
  307. # --------------------------------------------------------------------------------------------------------
  308. @pyqtSlot()
  309. def slot_handleSIGTERM(self):
  310. print("Got SIGTERM -> Closing now")
  311. self.close()
  312. # --------------------------------------------------------------------------------------------------------
  313. # Internal stuff
  314. def readlineblock(self):
  315. if self.fPipeClient is None:
  316. return ""
  317. return gCarla.utils.pipe_client_readlineblock(self.fPipeClient, 5000)
  318. def send(self, lines):
  319. if self.fPipeClient is None or len(lines) == 0:
  320. return
  321. gCarla.utils.pipe_client_lock(self.fPipeClient)
  322. # this must never fail, we need to unlock at the end
  323. try:
  324. for line in lines:
  325. if line is None:
  326. line2 = "(null)"
  327. elif isinstance(line, str):
  328. line2 = line.replace("\n", "\r")
  329. elif isinstance(line, bool):
  330. line2 = "true" if line else "false"
  331. elif isinstance(line, int):
  332. line2 = "%i" % line
  333. elif isinstance(line, float):
  334. line2 = "%.10f" % line
  335. else:
  336. print("unknown data type to send:", type(line))
  337. return
  338. gCarla.utils.pipe_client_write_msg(self.fPipeClient, line2 + "\n")
  339. except:
  340. pass
  341. gCarla.utils.pipe_client_flush_and_unlock(self.fPipeClient)