@@ -522,6 +522,14 @@ ifeq ($(HAVE_LIBLO),true) | |||||
$(DESTDIR)$(BINDIR)/carla-control | $(DESTDIR)$(BINDIR)/carla-control | ||||
endif | endif | ||||
# Install the real modgui bridge | |||||
install -m 755 \ | |||||
data/carla-bridge-lv2-modgui \ | |||||
$(DESTDIR)$(LIBDIR)/carla | |||||
sed $(SED_ARGS) 's?X-PREFIX-X?$(PREFIX)?' \ | |||||
$(DESTDIR)$(LIBDIR)/carla/carla-bridge-lv2-modgui | |||||
# Install frontend | # Install frontend | ||||
install -m 644 \ | install -m 644 \ | ||||
source/frontend/carla \ | source/frontend/carla \ | ||||
@@ -592,6 +600,7 @@ endif | |||||
install -m 644 resources/scalable/carla-control.svg $(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps | install -m 644 resources/scalable/carla-control.svg $(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps | ||||
# Install resources (re-use python files) | # Install resources (re-use python files) | ||||
$(LINK) ../modgui $(DESTDIR)$(DATADIR)/carla/resources | |||||
$(LINK) ../patchcanvas $(DESTDIR)$(DATADIR)/carla/resources | $(LINK) ../patchcanvas $(DESTDIR)$(DATADIR)/carla/resources | ||||
$(LINK) ../widgets $(DESTDIR)$(DATADIR)/carla/resources | $(LINK) ../widgets $(DESTDIR)$(DATADIR)/carla/resources | ||||
$(LINK) ../carla_app.py $(DESTDIR)$(DATADIR)/carla/resources | $(LINK) ../carla_app.py $(DESTDIR)$(DATADIR)/carla/resources | ||||
@@ -690,6 +699,16 @@ ifeq ($(HAVE_PYQT),true) | |||||
endif | endif | ||||
endif | endif | ||||
# ------------------------------------------------------------------------------------------------------------- | |||||
ifneq ($(HAVE_PYQT),true) | |||||
# Remove gui files for non-gui build | |||||
rm $(DESTDIR)$(LIBDIR)/carla/carla-bridge-lv2-modgui | |||||
rm $(DESTDIR)$(LIBDIR)/lv2/carla.lv2/carla-bridge-lv2-modgui | |||||
endif | |||||
# --------------------------------------------------------------------------------------------------------------------- | |||||
ifneq ($(EXTERNAL_PLUGINS),true) | ifneq ($(EXTERNAL_PLUGINS),true) | ||||
install_external_plugins: | install_external_plugins: | ||||
endif | endif | ||||
@@ -0,0 +1,6 @@ | |||||
#!/bin/bash | |||||
ASPATH=$(readlink -f $0) | |||||
BINDIR=$(dirname $ASPATH) | |||||
exec python3 $BINDIR/../source/frontend/carla_modgui.py "$@" |
@@ -0,0 +1,11 @@ | |||||
#!/bin/bash | |||||
PYTHON=$(which python3 2>/dev/null) | |||||
if [ ! -f ${PYTHON} ]; then | |||||
PYTHON=python | |||||
fi | |||||
INSTALL_PREFIX="X-PREFIX-X" | |||||
export CARLA_LIB_PREFIX="$INSTALL_PREFIX" | |||||
exec $PYTHON "$INSTALL_PREFIX"/share/carla/carla_modgui.py "$@" |
@@ -1512,6 +1512,11 @@ public: | |||||
fPipeServer.flushMessages(); | fPipeServer.flushMessages(); | ||||
} | } | ||||
#ifndef BUILD_BRIDGE | |||||
if (fUI.rdfDescriptor->Type == LV2_UI_MOD) | |||||
pData->tryTransient(); | |||||
#endif | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
@@ -4640,6 +4645,9 @@ public: | |||||
case LV2_UI_X11: | case LV2_UI_X11: | ||||
bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-x11"; | bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-x11"; | ||||
break; | break; | ||||
case LV2_UI_MOD: | |||||
bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-modgui"; | |||||
break; | |||||
#if 0 | #if 0 | ||||
case LV2_UI_EXTERNAL: | case LV2_UI_EXTERNAL: | ||||
case LV2_UI_OLD_EXTERNAL: | case LV2_UI_OLD_EXTERNAL: | ||||
@@ -5691,8 +5699,8 @@ public: | |||||
// --------------------------------------------------------------- | // --------------------------------------------------------------- | ||||
// find the most appropriate ui | // find the most appropriate ui | ||||
int eQt4, eQt5, eGtk2, eGtk3, eCocoa, eWindows, eX11, iCocoa, iWindows, iX11, iExt, iFinal; | |||||
eQt4 = eQt5 = eGtk2 = eGtk3 = eCocoa = eWindows = eX11 = iCocoa = iWindows = iX11 = iExt = iFinal = -1; | |||||
int eQt4, eQt5, eGtk2, eGtk3, eCocoa, eWindows, eX11, eMod, iCocoa, iWindows, iX11, iExt, iFinal; | |||||
eQt4 = eQt5 = eGtk2 = eGtk3 = eCocoa = eWindows = eX11 = eMod = iCocoa = iWindows = iX11 = iExt = iFinal = -1; | |||||
#if defined(LV2_UIS_ONLY_BRIDGES) | #if defined(LV2_UIS_ONLY_BRIDGES) | ||||
const bool preferUiBridges = true; | const bool preferUiBridges = true; | ||||
@@ -5750,6 +5758,9 @@ public: | |||||
case LV2_UI_OLD_EXTERNAL: | case LV2_UI_OLD_EXTERNAL: | ||||
iExt = ii; | iExt = ii; | ||||
break; | break; | ||||
case LV2_UI_MOD: | |||||
eMod = ii; | |||||
break; | |||||
default: | default: | ||||
break; | break; | ||||
} | } | ||||
@@ -5816,8 +5827,14 @@ public: | |||||
if (iFinal < 0) | if (iFinal < 0) | ||||
{ | { | ||||
carla_stderr("Failed to find an appropriate LV2 UI for this plugin"); | |||||
return; | |||||
if (eMod < 0) | |||||
{ | |||||
carla_stderr("Failed to find an appropriate LV2 UI for this plugin"); | |||||
return; | |||||
} | |||||
// use MODGUI as last resort | |||||
iFinal = eMod; | |||||
} | } | ||||
} | } | ||||
@@ -5867,7 +5884,8 @@ public: | |||||
iFinal == eGtk3 || | iFinal == eGtk3 || | ||||
iFinal == eCocoa || | iFinal == eCocoa || | ||||
iFinal == eWindows || | iFinal == eWindows || | ||||
iFinal == eX11) | |||||
iFinal == eX11 || | |||||
iFinal == eMod) | |||||
#ifdef BUILD_BRIDGE | #ifdef BUILD_BRIDGE | ||||
&& ! hasShowInterface | && ! hasShowInterface | ||||
#endif | #endif | ||||
@@ -5891,7 +5909,7 @@ public: | |||||
return; | return; | ||||
} | } | ||||
if (iFinal == eQt4 || iFinal == eQt5 || iFinal == eGtk2 || iFinal == eGtk3) | |||||
if (iFinal == eQt4 || iFinal == eQt5 || iFinal == eGtk2 || iFinal == eGtk3 || iFinal == eMod) | |||||
{ | { | ||||
carla_stderr2("Failed to find UI bridge binary for '%s', cannot use UI", pData->name); | carla_stderr2("Failed to find UI bridge binary for '%s', cannot use UI", pData->name); | ||||
fUI.rdfDescriptor = nullptr; | fUI.rdfDescriptor = nullptr; | ||||
@@ -187,7 +187,7 @@ static const CarlaCachedPluginInfo* get_cached_plugin_lv2(Lv2WorldClass& lv2Worl | |||||
info.hints = 0x0; | info.hints = 0x0; | ||||
if (lilvPlugin.get_uis().size() > 0) | |||||
if (lilvPlugin.get_uis().size() > 0 || lilvPlugin.get_modgui_resources_directory().as_uri() != nullptr) | |||||
info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; | info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; | ||||
{ | { | ||||
@@ -56,6 +56,7 @@ RES = \ | |||||
$(BINDIR)/resources/carla_control.py \ | $(BINDIR)/resources/carla_control.py \ | ||||
$(BINDIR)/resources/carla_database.py \ | $(BINDIR)/resources/carla_database.py \ | ||||
$(BINDIR)/resources/carla_host.py \ | $(BINDIR)/resources/carla_host.py \ | ||||
$(BINDIR)/resources/carla_modgui.py \ | |||||
$(BINDIR)/resources/carla_settings.py \ | $(BINDIR)/resources/carla_settings.py \ | ||||
$(BINDIR)/resources/carla_skin.py \ | $(BINDIR)/resources/carla_skin.py \ | ||||
$(BINDIR)/resources/carla_shared.py \ | $(BINDIR)/resources/carla_shared.py \ | ||||
@@ -0,0 +1,459 @@ | |||||
#!/usr/bin/env python3 | |||||
# -*- coding: utf-8 -*- | |||||
# Carla bridge for LV2 modguis | |||||
# Copyright (C) 2015-2019 Filipe Coelho <falktx@falktx.com> | |||||
# | |||||
# 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 mod.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 = {} | |||||
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() | |||||
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 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) |
@@ -0,0 +1,210 @@ | |||||
#!/usr/bin/env python3 | |||||
# -*- coding: utf-8 -*- | |||||
# Carla bridge for LV2 modguis | |||||
# Copyright (C) 2015-2019 Filipe Coelho <falktx@falktx.com> | |||||
# | |||||
# 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) | |||||
import os | |||||
from PyQt5.QtCore import pyqtSignal, QThread | |||||
# ------------------------------------------------------------------------------------------------------------ | |||||
# Generate a random port number between 9000 and 18000 | |||||
from random import random | |||||
PORTn = 8998 + int(random()*9000) | |||||
# ------------------------------------------------------------------------------------------------------------ | |||||
# 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 | |||||
# ------------------------------------------------------------------------------------------------------------ | |||||
# Set up environment for the webserver | |||||
PORT = str(PORTn) | |||||
ROOT = "/usr/share/mod" | |||||
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) |
@@ -234,6 +234,7 @@ typedef uint32_t LV2_Property; | |||||
#define LV2_UI_X11 7 | #define LV2_UI_X11 7 | ||||
#define LV2_UI_EXTERNAL 8 | #define LV2_UI_EXTERNAL 8 | ||||
#define LV2_UI_OLD_EXTERNAL 9 | #define LV2_UI_OLD_EXTERNAL 9 | ||||
#define LV2_UI_MOD 10 | |||||
#define LV2_IS_UI_GTK2(x) ((x) == LV2_UI_GTK2) | #define LV2_IS_UI_GTK2(x) ((x) == LV2_UI_GTK2) | ||||
#define LV2_IS_UI_GTK3(x) ((x) == LV2_UI_GTK3) | #define LV2_IS_UI_GTK3(x) ((x) == LV2_UI_GTK3) | ||||
@@ -244,6 +245,7 @@ typedef uint32_t LV2_Property; | |||||
#define LV2_IS_UI_X11(x) ((x) == LV2_UI_X11) | #define LV2_IS_UI_X11(x) ((x) == LV2_UI_X11) | ||||
#define LV2_IS_UI_EXTERNAL(x) ((x) == LV2_UI_EXTERNAL) | #define LV2_IS_UI_EXTERNAL(x) ((x) == LV2_UI_EXTERNAL) | ||||
#define LV2_IS_UI_OLD_EXTERNAL(x) ((x) == LV2_UI_OLD_EXTERNAL) | #define LV2_IS_UI_OLD_EXTERNAL(x) ((x) == LV2_UI_OLD_EXTERNAL) | ||||
#define LV2_IS_UI_MOD(x) ((x) == LV2_UI_MOD) | |||||
// Plugin Types | // Plugin Types | ||||
#define LV2_PLUGIN_DELAY 0x000001 | #define LV2_PLUGIN_DELAY 0x000001 | ||||
@@ -99,6 +99,7 @@ typedef std::map<double,const LilvScalePoint*> LilvScalePointMap; | |||||
#define NS_rdfs "http://www.w3.org/2000/01/rdf-schema#" | #define NS_rdfs "http://www.w3.org/2000/01/rdf-schema#" | ||||
#define NS_llmm "http://ll-plugins.nongnu.org/lv2/ext/midimap#" | #define NS_llmm "http://ll-plugins.nongnu.org/lv2/ext/midimap#" | ||||
#define NS_devp "http://lv2plug.in/ns/dev/extportinfo#" | #define NS_devp "http://lv2plug.in/ns/dev/extportinfo#" | ||||
#define NS_mod "http://moddevices.com/ns/modgui#" | |||||
#define LV2_MIDI_Map__CC "http://ll-plugins.nongnu.org/lv2/namespace#CC" | #define LV2_MIDI_Map__CC "http://ll-plugins.nongnu.org/lv2/namespace#CC" | ||||
#define LV2_MIDI_Map__NRPN "http://ll-plugins.nongnu.org/lv2/namespace#NRPN" | #define LV2_MIDI_Map__NRPN "http://ll-plugins.nongnu.org/lv2/namespace#NRPN" | ||||
@@ -2624,9 +2625,13 @@ const LV2_RDF_Descriptor* lv2_rdf_new(const LV2_URI uri, const bool loadPresets) | |||||
// ---------------------------------------------------------------------------------------------------------------- | // ---------------------------------------------------------------------------------------------------------------- | ||||
// Set Plugin UIs | // Set Plugin UIs | ||||
{ | { | ||||
const bool hasMODGui(lilvPlugin.get_modgui_resources_directory().as_uri() != nullptr); | |||||
Lilv::UIs lilvUIs(lilvPlugin.get_uis()); | Lilv::UIs lilvUIs(lilvPlugin.get_uis()); | ||||
if (const uint numUIs = lilvUIs.size()) | |||||
const uint numUIs = lilvUIs.size() + (hasMODGui ? 1 : 0); | |||||
if (numUIs > 0) | |||||
{ | { | ||||
rdfDescriptor->UIs = new LV2_RDF_UI[numUIs]; | rdfDescriptor->UIs = new LV2_RDF_UI[numUIs]; | ||||
@@ -2821,6 +2826,29 @@ const LV2_RDF_Descriptor* lv2_rdf_new(const LV2_URI uri, const bool loadPresets) | |||||
} | } | ||||
} | } | ||||
for (; hasMODGui;) | |||||
{ | |||||
CARLA_SAFE_ASSERT_BREAK(numUsed == numUIs-1); | |||||
LV2_RDF_UI* const rdfUI(&rdfDescriptor->UIs[numUsed++]); | |||||
// ------------------------------------------------------- | |||||
// Set UI Type | |||||
rdfUI->Type = LV2_UI_MOD; | |||||
// ------------------------------------------------------- | |||||
// Set UI Information | |||||
if (const char* const resDir = lilvPlugin.get_modgui_resources_directory().as_uri()) | |||||
rdfUI->URI = carla_strdup_free(lilv_file_uri_parse(resDir, nullptr)); | |||||
if (rdfDescriptor->Bundle != nullptr) | |||||
rdfUI->Bundle = carla_strdup(rdfDescriptor->Bundle); | |||||
break; | |||||
} | |||||
rdfDescriptor->UICount = numUsed; | rdfDescriptor->UICount = numUsed; | ||||
} | } | ||||