#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Carla bridge for LV2 modguis # Copyright (C) 2015-2019 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 (Global) from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QSize, QUrl from PyQt5.QtGui import QImage, QPainter, QPalette from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView import sys # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) from carla_host import charPtrToString, gCarla from .webserver import WebServerThread, PORT # ------------------------------------------------------------------------------------------------------------ # Imports (MOD) from modtools.utils import get_plugin_info, init as lv2_init # ------------------------------------------------------------------------------------------------------------ # 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 lv2_init() self.fPlugin = get_plugin_info(URI) self.fPorts = self.fPlugin['ports'] self.fPortSymbols = {} self.fPortValues = {} self.fParamTypes = {} self.fParamValues = {} 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'] for parameter in self.fPlugin['parameters']: if parameter['ranges'] is None: continue if parameter['type'] == "http://lv2plug.in/ns/ext/atom#Bool": paramtype = 'b' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Int": paramtype = 'i' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Long": paramtype = 'l' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Float": paramtype = 'f' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Double": paramtype = 'g' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#String": paramtype = 's' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Path": paramtype = 'p' elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#URI": paramtype = 'u' else: continue if paramtype not in ('s','p','u') and parameter['ranges']['minimum'] == parameter['ranges']['maximum']: continue self.fParamTypes [parameter['uri']] = paramtype self.fParamValues[parameter['uri']] = parameter['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.setPortWidgetsValue(':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.setPortWidgetsValue('%s', %f, null)" % (symbol, value)) for uri in self.fParamValues.keys(): ptype = self.fParamTypes[uri] value = str(self.fParamValues[uri]) print("icongui.setWritableParameterValue('%s', '%c', %s, 'from-carla')" % (uri, ptype, value)) self.fCurrentFrame.evaluateJavaScript("icongui.setWritableParameterValue('%s', '%c', %s, 'from-carla')" % ( uri, ptype, 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.controls['%s'].value" % (symbol,)) if oldValue != newValue: self.fPortValues[index] = newValue self.send(["control", index, newValue]) for uri in self.fParamValues.keys(): oldValue = self.fParamValues[uri] newValue = self.fCurrentFrame.evaluateJavaScript("icongui.parameters['%s'].value" % (uri,)) if oldValue != newValue: self.fParamValues[uri] = newValue self.send(["pcontrol", uri, 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 = self.readlineblock_int() value = self.readlineblock_float() self.dspControlChanged(index, value) elif msg == "parameter": uri = self.readlineblock() value = self.readlineblock_float() self.dspParameterChanged(uri, value) elif msg == "program": index = self.readlineblock_int() self.dspProgramChanged(index) elif msg == "midiprogram": bank = self.readlineblock_int() program = self.readlineblock_int() self.dspMidiProgramChanged(bank, program) elif msg == "configure": key = self.readlineblock() value = self.readlineblock() self.dspStateChanged(key, value) elif msg == "note": onOff = self.readlineblock_bool() channel = self.readlineblock_int() note = self.readlineblock_int() velocity = self.readlineblock_int() self.dspNoteReceived(onOff, channel, note, velocity) elif msg == "atom": index = self.readlineblock_int() atomsize = self.readlineblock_int() base64size = self.readlineblock_int() base64atom = self.readlineblock() # nothing to do yet elif msg == "urid": urid = self.readlineblock_int() size = self.readlineblock_int() uri = self.readlineblock() # nothing to do yet elif msg == "uiOptions": sampleRate = self.readlineblock_float() bgColor = self.readlineblock_int() fgColor = self.readlineblock_int() uiScale = self.readlineblock_float() useTheme = self.readlineblock_bool() useThemeColors = self.readlineblock_bool() windowTitle = self.readlineblock() transWindowId = self.readlineblock_int() 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 dspControlChanged(self, index, value): self.fPortValues[index] = value if self.fCurrentFrame is None or not self.fCanSetValues: return symbol, isOutput = self.fPortSymbols[index] if isOutput: self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value)) else: self.fCurrentFrame.evaluateJavaScript("icongui.setPortWidgetsValue('%s', %f, null)" % (symbol, value)) def dspParameterChanged(self, uri, value): print("dspParameterChanged", uri, value) if uri not in self.fParamValues: return self.fParamValues[uri] = value if self.fCurrentFrame is None or not self.fCanSetValues: return ptype = self.fParamTypes[uri] self.fCurrentFrame.evaluateJavaScript("icongui.setWritableParameterValue('%s', '%c', %f, 'from-carla')" % ( uri, ptype, 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() QApplication.instance().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 QApplication.instance().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 readlineblock_bool(self): if self.fPipeClient is None: return False return gCarla.utils.pipe_client_readlineblock_bool(self.fPipeClient, 5000) def readlineblock_int(self): if self.fPipeClient is None: return 0 return gCarla.utils.pipe_client_readlineblock_int(self.fPipeClient, 5000) def readlineblock_float(self): if self.fPipeClient is None: return 0.0 return gCarla.utils.pipe_client_readlineblock_float(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)