#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Carla bridge for LV2 modguis # Copyright (C) 2015-2016 Filipe Coelho # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # For a full copy of the GNU General Public License see the doc/GPL.txt file. # ------------------------------------------------------------------------------------------------------------ # Imports (Config) from carla_config import * # ------------------------------------------------------------------------------------------------------------ # Imports (Global) import json if config_UseQt5: from PyQt5.QtCore import pyqtSlot, QPoint, QThread, QSize, QUrl from PyQt5.QtGui import QImage, QPainter, QPalette from PyQt5.QtWidgets import QMainWindow from PyQt5.QtWebKit import QWebElement, QWebSettings from PyQt5.QtWebKitWidgets import QWebView else: from PyQt4.QtCore import pyqtSlot, QPoint, QThread, QSize, QUrl from PyQt4.QtGui import QImage, QPainter, QPalette from PyQt4.QtGui import QMainWindow from PyQt4.QtWebKit import QWebElement, QWebSettings, QWebView # ------------------------------------------------------------------------------------------------------------ # Imports (tornado) from tornado.log import enable_pretty_logging from tornado.ioloop import IOLoop from tornado.util import unicode_type from tornado.web import HTTPError from tornado.web import Application, RequestHandler, StaticFileHandler # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) from carla_app import * from carla_utils import * # ------------------------------------------------------------------------------------------------------------ # Generate a random port number between 9000 and 18000 from random import random PORTn = 8998 + int(random()*9000) # ------------------------------------------------------------------------------------------------------------ # Set up environment for the webserver PORT = str(PORTn) ROOT = "/usr/share/mod" #ROOT = "/home/falktx/FOSS/GIT-mine/MOD/mod-app/source/modules/mod-ui" DATA_DIR = os.path.expanduser("~/.local/share/mod-data/") HTML_DIR = os.path.join(ROOT, "html") os.environ['MOD_DEV_HOST'] = "1" os.environ['MOD_DEV_HMI'] = "1" os.environ['MOD_DESKTOP'] = "1" os.environ['MOD_DATA_DIR'] = DATA_DIR os.environ['MOD_HTML_DIR'] = HTML_DIR os.environ['MOD_KEY_PATH'] = os.path.join(DATA_DIR, "keys") os.environ['MOD_CLOUD_PUB'] = os.path.join(ROOT, "keys", "cloud_key.pub") os.environ['MOD_PLUGIN_LIBRARY_DIR'] = os.path.join(DATA_DIR, "lib") os.environ['MOD_PHANTOM_BINARY'] = "/usr/bin/phantomjs" os.environ['MOD_SCREENSHOT_JS'] = os.path.join(ROOT, "screenshot.js") os.environ['MOD_DEVICE_WEBSERVER_PORT'] = PORT # ------------------------------------------------------------------------------------------------------------ # Imports (MOD) from mod.utils import get_plugin_info, get_plugin_gui, get_plugin_gui_mini, init as lv2_init # ------------------------------------------------------------------------------------------------------------ # MOD related classes class JsonRequestHandler(RequestHandler): def write(self, data): if isinstance(data, (bytes, unicode_type, dict)): RequestHandler.write(self, data) self.finish() return elif data is True: data = "true" self.set_header("Content-Type", "application/json; charset=UTF-8") elif data is False: data = "false" self.set_header("Content-Type", "application/json; charset=UTF-8") else: data = json.dumps(data) self.set_header("Content-Type", "application/json; charset=UTF-8") RequestHandler.write(self, data) self.finish() class EffectGet(JsonRequestHandler): def get(self): uri = self.get_argument('uri') try: data = get_plugin_info(uri) except: print("ERROR: get_plugin_info for '%s' failed" % uri) raise HTTPError(404) self.write(data) class EffectFile(StaticFileHandler): def initialize(self): # return custom type directly. The browser will do the parsing self.custom_type = None uri = self.get_argument('uri') try: self.modgui = get_plugin_gui(uri) except: raise HTTPError(404) try: root = self.modgui['resourcesDirectory'] except: raise HTTPError(404) return StaticFileHandler.initialize(self, root) def parse_url_path(self, prop): try: path = self.modgui[prop] except: raise HTTPError(404) if prop in ("iconTemplate", "settingsTemplate", "stylesheet", "javascript"): self.custom_type = "text/plain" return path def get_content_type(self): if self.custom_type is not None: return self.custom_type return StaticFileHandler.get_content_type(self) class EffectResource(StaticFileHandler): def initialize(self): # Overrides StaticFileHandler initialize pass def get(self, path): try: uri = self.get_argument('uri') except: return self.shared_resource(path) try: modgui = get_plugin_gui_mini(uri) except: raise HTTPError(404) try: root = modgui['resourcesDirectory'] except: raise HTTPError(404) try: super(EffectResource, self).initialize(root) return super(EffectResource, self).get(path) except HTTPError as e: if e.status_code != 404: raise e return self.shared_resource(path) except IOError: raise HTTPError(404) def shared_resource(self, path): super(EffectResource, self).initialize(os.path.join(HTML_DIR, 'resources')) return super(EffectResource, self).get(path) # ------------------------------------------------------------------------------------------------------------ # WebServer Thread class WebServerThread(QThread): # signals running = pyqtSignal() def __init__(self, parent=None): QThread.__init__(self, parent) self.fApplication = Application( [ (r"/effect/get/?", EffectGet), (r"/effect/file/(.*)", EffectFile), (r"/resources/(.*)", EffectResource), (r"/(.*)", StaticFileHandler, {"path": HTML_DIR}), ], debug=True) self.fPrepareWasCalled = False def run(self): if not self.fPrepareWasCalled: self.fPrepareWasCalled = True self.fApplication.listen(PORT, address="0.0.0.0") if int(os.getenv("MOD_LOG", "0")): enable_pretty_logging() self.running.emit() IOLoop.instance().start() def stopWait(self): IOLoop.instance().stop() return self.wait(5000) # ------------------------------------------------------------------------------------------------------------ # Host Window class HostWindow(QMainWindow): # signals SIGTERM = pyqtSignal() SIGUSR1 = pyqtSignal() # -------------------------------------------------------------------------------------------------------- def __init__(self): QMainWindow.__init__(self) gCarla.gui = self URI = sys.argv[1] # ---------------------------------------------------------------------------------------------------- # Internal stuff self.fCurrentFrame = None self.fDocElemement = None self.fCanSetValues = False self.fNeedsShow = False self.fSizeSetup = False self.fQuitReceived = False self.fWasRepainted = False self.fPlugin = get_plugin_info(URI) self.fPorts = self.fPlugin['ports'] self.fPortSymbols = {} self.fPortValues = {} for port in self.fPorts['control']['input']: self.fPortSymbols[port['index']] = (port['symbol'], False) self.fPortValues [port['index']] = port['ranges']['default'] for port in self.fPorts['control']['output']: self.fPortSymbols[port['index']] = (port['symbol'], True) self.fPortValues [port['index']] = port['ranges']['default'] # ---------------------------------------------------------------------------------------------------- # Init pipe if len(sys.argv) == 7: self.fPipeClient = gCarla.utils.pipe_client_new(lambda s,msg: self.msgCallback(msg)) else: self.fPipeClient = None # ---------------------------------------------------------------------------------------------------- # Init Web server self.fWebServerThread = WebServerThread(self) self.fWebServerThread.start() # ---------------------------------------------------------------------------------------------------- # Set up GUI self.setContentsMargins(0, 0, 0, 0) self.fWebview = QWebView(self) #self.fWebview.setAttribute(Qt.WA_OpaquePaintEvent, False) #self.fWebview.setAttribute(Qt.WA_TranslucentBackground, True) self.setCentralWidget(self.fWebview) page = self.fWebview.page() page.setViewportSize(QSize(980, 600)) mainFrame = page.mainFrame() mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) palette = self.fWebview.palette() palette.setBrush(QPalette.Base, palette.brush(QPalette.Window)) page.setPalette(palette) self.fWebview.setPalette(palette) settings = self.fWebview.settings() settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) self.fWebview.loadFinished.connect(self.slot_webviewLoadFinished) url = "http://127.0.0.1:%s/icon.html#%s" % (PORT, URI) print("url:", url) self.fWebview.load(QUrl(url)) # ---------------------------------------------------------------------------------------------------- # Connect actions to functions self.SIGTERM.connect(self.slot_handleSIGTERM) # ---------------------------------------------------------------------------------------------------- # Final setup self.fIdleTimer = self.startTimer(30) if self.fPipeClient is None: # testing, show UI only self.setWindowTitle("TestUI") self.fNeedsShow = True # -------------------------------------------------------------------------------------------------------- def closeExternalUI(self): self.fWebServerThread.stopWait() if self.fPipeClient is None: return if not self.fQuitReceived: self.send(["exiting"]) gCarla.utils.pipe_client_destroy(self.fPipeClient) self.fPipeClient = None def idleStuff(self): if self.fPipeClient is not None: gCarla.utils.pipe_client_idle(self.fPipeClient) self.checkForRepaintChanges() if self.fSizeSetup: return if self.fDocElemement is None or self.fDocElemement.isNull(): return pedal = self.fDocElemement.findFirst(".mod-pedal") if pedal.isNull(): return size = pedal.geometry().size() if size.width() <= 10 or size.height() <= 10: return # render web frame to image image = QImage(self.fWebview.page().viewportSize(), QImage.Format_ARGB32_Premultiplied) image.fill(Qt.transparent) painter = QPainter(image) self.fCurrentFrame.render(painter) painter.end() #image.save("/tmp/test.png") # get coordinates and size from image #x = -1 #y = -1 #lastx = -1 #lasty = -1 #bgcol = self.fHostColor.rgba() #for h in range(0, image.height()): #hasNonTransPixels = False #for w in range(0, image.width()): #if image.pixel(w, h) not in (0, bgcol): # 0xff070707): #hasNonTransPixels = True #if x == -1 or x > w: #x = w #lastx = max(lastx, w) #if hasNonTransPixels: ##if y == -1: ##y = h #lasty = h # set size and position accordingly #if -1 not in (x, lastx, lasty): #self.setFixedSize(lastx-x, lasty) #self.fCurrentFrame.setScrollPosition(QPoint(x, 0)) #else: # TODO that^ needs work if True: self.setFixedSize(size) # set initial values self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue(':bypass', 0, null)") for index in self.fPortValues.keys(): symbol, isOutput = self.fPortSymbols[index] value = self.fPortValues[index] if isOutput: self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value)) else: self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f, null)" % (symbol, value)) # final setup self.fCanSetValues = True self.fSizeSetup = True self.fDocElemement = None if self.fNeedsShow: self.show() def checkForRepaintChanges(self): if not self.fWasRepainted: return self.fWasRepainted = False if not self.fCanSetValues: return for index in self.fPortValues.keys(): symbol, isOutput = self.fPortSymbols[index] if isOutput: continue oldValue = self.fPortValues[index] newValue = self.fCurrentFrame.evaluateJavaScript("icongui.getPortValue('%s')" % (symbol,)) if oldValue != newValue: self.fPortValues[index] = newValue self.send(["control", index, newValue]) # -------------------------------------------------------------------------------------------------------- @pyqtSlot(bool) def slot_webviewLoadFinished(self, ok): page = self.fWebview.page() page.repaintRequested.connect(self.slot_repaintRequested) self.fCurrentFrame = page.currentFrame() self.fDocElemement = self.fCurrentFrame.documentElement() def slot_repaintRequested(self): if self.fCanSetValues: self.fWasRepainted = True # -------------------------------------------------------------------------------------------------------- # Callback def msgCallback(self, msg): msg = charPtrToString(msg) if msg == "control": index = int(self.readlineblock()) value = float(self.readlineblock()) self.dspParameterChanged(index, value) elif msg == "program": index = int(self.readlineblock()) self.dspProgramChanged(index) elif msg == "midiprogram": bank = int(self.readlineblock()) program = float(self.readlineblock()) self.dspMidiProgramChanged(bank, program) elif msg == "configure": key = self.readlineblock() value = self.readlineblock() self.dspStateChanged(key, value) elif msg == "note": onOff = bool(self.readlineblock() == "true") channel = int(self.readlineblock()) note = int(self.readlineblock()) velocity = int(self.readlineblock()) self.dspNoteReceived(onOff, channel, note, velocity) elif msg == "atom": index = int(self.readlineblock()) size = int(self.readlineblock()) base64atom = self.readlineblock() # nothing to do yet elif msg == "urid": urid = int(self.readlineblock()) uri = self.readlineblock() # nothing to do yet elif msg == "uiOptions": sampleRate = float(self.readlineblock()) useTheme = bool(self.readlineblock() == "true") useThemeColors = bool(self.readlineblock() == "true") windowTitle = self.readlineblock() transWindowId = int(self.readlineblock()) self.uiTitleChanged(windowTitle) elif msg == "show": self.uiShow() elif msg == "focus": self.uiFocus() elif msg == "hide": self.uiHide() elif msg == "quit": self.fQuitReceived = True self.uiQuit() elif msg == "uiTitle": uiTitle = self.readlineblock() self.uiTitleChanged(uiTitle) else: print("unknown message: \"" + msg + "\"") # -------------------------------------------------------------------------------------------------------- def dspParameterChanged(self, index, value): self.fPortValues[index] = value if self.fCurrentFrame is not None and self.fCanSetValues: symbol, isOutput = self.fPortSymbols[index] if isOutput: self.fPortValues[index] = value self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value)) else: self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f, null)" % (symbol, value)) def dspProgramChanged(self, index): return def dspMidiProgramChanged(self, bank, program): return def dspStateChanged(self, key, value): return def dspNoteReceived(self, onOff, channel, note, velocity): return # -------------------------------------------------------------------------------------------------------- def uiShow(self): if self.fSizeSetup: self.show() else: self.fNeedsShow = True def uiFocus(self): if not self.fSizeSetup: return self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.show() self.raise_() self.activateWindow() def uiHide(self): self.hide() def uiQuit(self): self.closeExternalUI() self.close() app.quit() def uiTitleChanged(self, uiTitle): self.setWindowTitle(uiTitle) # -------------------------------------------------------------------------------------------------------- # Qt events def closeEvent(self, event): self.closeExternalUI() QMainWindow.closeEvent(self, event) # there might be other qt windows open which will block carla-modgui from quitting app.quit() def timerEvent(self, event): if event.timerId() == self.fIdleTimer: self.idleStuff() QMainWindow.timerEvent(self, event) # -------------------------------------------------------------------------------------------------------- @pyqtSlot() def slot_handleSIGTERM(self): print("Got SIGTERM -> Closing now") self.close() # -------------------------------------------------------------------------------------------------------- # Internal stuff def readlineblock(self): if self.fPipeClient is None: return "" return gCarla.utils.pipe_client_readlineblock(self.fPipeClient, 5000) def send(self, lines): if self.fPipeClient is None or len(lines) == 0: return gCarla.utils.pipe_client_lock(self.fPipeClient) # this must never fail, we need to unlock at the end try: for line in lines: if line is None: line2 = "(null)" elif isinstance(line, str): line2 = line.replace("\n", "\r") elif isinstance(line, bool): line2 = "true" if line else "false" elif isinstance(line, int): line2 = "%i" % line elif isinstance(line, float): line2 = "%.10f" % line else: print("unknown data type to send:", type(line)) return gCarla.utils.pipe_client_write_msg(self.fPipeClient, line2 + "\n") except: pass gCarla.utils.pipe_client_flush_and_unlock(self.fPipeClient) # ------------------------------------------------------------------------------------------------------------ # Main if __name__ == '__main__': # ------------------------------------------------------------- # Read CLI args if len(sys.argv) < 2: print("usage: %s " % sys.argv[0]) sys.exit(1) libPrefix = os.getenv("CARLA_LIB_PREFIX") # ------------------------------------------------------------- # App initialization app = CarlaApplication("Carla2-MODGUI", libPrefix) # ------------------------------------------------------------- # Init utils pathBinaries, pathResources = getPaths(libPrefix) utilsname = "libcarla_utils.%s" % (DLL_EXTENSION) gCarla.utils = CarlaUtils(os.path.join(pathBinaries, utilsname)) gCarla.utils.set_process_name("carla-bridge-lv2-modgui") # ------------------------------------------------------------- # Set-up custom signal handling setUpSignals() # ------------------------------------------------------------- # Init LV2 lv2_init() # ------------------------------------------------------------- # Create GUI gui = HostWindow() # -------------------------------------------------------------------------------------------------------- # App-Loop app.exit_exec() # ------------------------------------------------------------------------------------------------------------