diff --git a/Makefile b/Makefile index 3b2e932d8..071cbb20e 100644 --- a/Makefile +++ b/Makefile @@ -478,6 +478,11 @@ endif bin/carla-discovery-* \ $(DESTDIR)$(PREFIX)/lib/carla/ + # Install the real modgui bridge + install -m 755 \ + data/carla-bridge-lv2-modgui \ + $(DESTDIR)$(PREFIX)/lib/carla/ + # Install theme install -m 644 \ bin/styles/* \ @@ -540,6 +545,7 @@ endif $(LINK) $(PREFIX)/share/carla/carla_config.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/carla_database.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/carla_host.py $(DESTDIR)$(PREFIX)/share/carla/resources/ + $(LINK) $(PREFIX)/share/carla/carla_modgui.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/carla_panels.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/carla_settings.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/carla_skin.py $(DESTDIR)$(PREFIX)/share/carla/resources/ @@ -601,6 +607,7 @@ endif $(DESTDIR)$(PREFIX)/bin/carla-rack \ $(DESTDIR)$(PREFIX)/bin/carla-single \ $(DESTDIR)$(PREFIX)/bin/carla-settings \ + $(DESTDIR)$(PREFIX)/lib/carla/carla-bridge-lv2-modgui \ $(DESTDIR)$(PREFIX)/lib/pkgconfig/carla-standalone.pc # $(DESTDIR)$(PREFIX)/bin/carla-control \ diff --git a/bin/carla-bridge-lv2-modgui b/bin/carla-bridge-lv2-modgui new file mode 100755 index 000000000..027b5b7b6 --- /dev/null +++ b/bin/carla-bridge-lv2-modgui @@ -0,0 +1,6 @@ +#!/bin/bash + +ASPATH=`readlink -f $0` +BINDIR=`dirname $ASPATH` + +exec python3 $BINDIR/../source/carla_modgui.py "$@" diff --git a/bin/carla.lv2/carla-bridge-lv2-modgui b/bin/carla.lv2/carla-bridge-lv2-modgui new file mode 120000 index 000000000..8c122ef0e --- /dev/null +++ b/bin/carla.lv2/carla-bridge-lv2-modgui @@ -0,0 +1 @@ +../carla-bridge-lv2-modgui \ No newline at end of file diff --git a/data/carla-bridge-lv2-modgui b/data/carla-bridge-lv2-modgui new file mode 100755 index 000000000..f1a7e74ec --- /dev/null +++ b/data/carla-bridge-lv2-modgui @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ -f /usr/bin/python3 ]; then + PYTHON=/usr/bin/python3 +else + PYTHON=python +fi + +INSTALL_PREFIX="X-PREFIX-X" +exec $PYTHON "$INSTALL_PREFIX"/share/carla/carla_modgui.py "$@" diff --git a/source/carla_modgui.py b/source/carla_modgui.py new file mode 100755 index 000000000..534a2d125 --- /dev/null +++ b/source/carla_modgui.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Carla bridge for LV2 modguis +# Copyright (C) 2015 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) + +if config_UseQt5: + from PyQt5.QtCore import pyqtSlot, QThread, QUrl + from PyQt5.QtWidgets import QMainWindow + from PyQt5.QtWebKitWidgets import QWebView +else: + from PyQt4.QtCore import pyqtSlot, QPoint, QTimer, QThread, QUrl + from PyQt4.QtGui import QMainWindow + from PyQt4.QtWebKit import QWebElement, QWebSettings, QWebView + +# ------------------------------------------------------------------------------------------------------------ +# 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" +ROOT = "/home/falktx/Personal/FOSS/Git-mine/mod-app/source/modules" +DATA_DIR = os.path.expanduser("~/.local/share/mod-data/") + +os.environ['MOD_DEV_HOST'] = "1" +os.environ['MOD_DEV_HMI'] = "1" +os.environ['MOD_DESKTOP'] = "1" +os.environ['MOD_LOG'] = "0" + +os.environ['MOD_DATA_DIR'] = DATA_DIR +os.environ['MOD_HTML_DIR'] = os.path.join(ROOT, "mod-ui", "html") +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, "mod-ui", "screenshot.js") +os.environ['MOD_DEVICE_WEBSERVER_PORT'] = PORT + +#sys.path = [os.path.join(ROOT, "mod-ui")] + sys.path + +# ------------------------------------------------------------------------------------------------------------ +# Imports (MOD) + +from mod import webserver +from mod.lv2 import PluginSerializer +from mod.session import SESSION + +# Dummy monitor var, we don't need it +SESSION.monitor_server = "skip" + +# ------------------------------------------------------------------------------------------------------------ +# WebServer Thread + +class WebServerThread(QThread): + def __init__(self, parent=None): + QThread.__init__(self, parent) + + def run(self): + webserver.prepare() + webserver.start() + + def stopWait(self): + webserver.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.fDocElemement = None + self.fNeedsShow = False + self.fSizeSetup = False + self.fQuitReceived = False + + self.fControlBypass = None + self.fControlPorts = [] + + self.fPlugin = PluginSerializer(URI) + self.fPorts = self.fPlugin.data['ports'] + + # ---------------------------------------------------------------------------------------------------- + # 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.fWebview = QWebView(self) + self.setCentralWidget(self.fWebview) + self.setContentsMargins(0, 0, 0, 0) + + mainFrame = self.fWebview.page().mainFrame() + mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) + mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) + + 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?v=0#%s" % (PORT, URI) + 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 + + # -------------------------------------------------------------------------------------------------------- + + @pyqtSlot(bool) + def slot_webviewLoadFinished(self, ok): + print("webview finished", ok, self.fWebview.title()) + + self.fDocElemement = self.fWebview.page().currentFrame().documentElement() + + def trySetSizeIfNeeded(self): + 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 + + self.fSizeSetup = True + self.fDocElemement = None + + self.setFixedSize(size) + self.fWebview.page().currentFrame().setScrollPosition(QPoint(15, 0)) + + if self.fNeedsShow: + self.show() + + for i in pedal.findAll("*"): + if "mod-port-symbol" in i.attributeNames(): + if i.attribute("mod-role") == "input-control-port": + self.fControlPorts.append((i.attribute("mod-port-symbol"), i)) + + elif "mod-role" in i.attributeNames(): + if i.attribute("mod-role") == "bypass": + self.fControlBypass = i + + def getPortByIndex(self, index): + for port in self.fPorts['control']['input']: + if port['index'] == index: + return port + return None + + def setKnobValue(self, port, value): + for portSymbol, portElem in self.fControlPorts: + if portSymbol != port['symbol']: + continue + + height = int(portElem.styleProperty("height", QWebElement.ComputedStyle).replace("px","")) + + norm = (value-port['minimum'])/(port['maximum']-port['minimum']) + real = int(norm*height*64) + aprox = real-(real%height) + + valueStr = "%s%ipx 0px" % ("-" if aprox > 0 else "", aprox) + portElem.setStyleProperty("background-position", valueStr) + break + + # -------------------------------------------------------------------------------------------------------- + + 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 idleExternalUI(self): + if self.fPipeClient is not None: + gCarla.utils.pipe_client_idle(self.fPipeClient) + + # -------------------------------------------------------------------------------------------------------- + # 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): + print("dspParameterChanged", index, value) + self.setKnobValue(self.getPortByIndex(index), 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) + + def timerEvent(self, event): + if event.timerId() == self.fIdleTimer: + self.trySetSizeIfNeeded() + self.idleExternalUI() + + 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) < 3: + 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() + + # ------------------------------------------------------------- + # Create GUI + + gui = HostWindow() + + # -------------------------------------------------------------------------------------------------------- + # App-Loop + + app.exit_exec() + +# ------------------------------------------------------------------------------------------------------------