Add back modgui support
Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
parent
0c197e12b1
commit
d0610840bd
19
Makefile
19
Makefile
|
@ -522,6 +522,14 @@ ifeq ($(HAVE_LIBLO),true)
|
|||
$(DESTDIR)$(BINDIR)/carla-control
|
||||
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 -m 644 \
|
||||
source/frontend/carla \
|
||||
|
@ -592,6 +600,7 @@ endif
|
|||
install -m 644 resources/scalable/carla-control.svg $(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps
|
||||
|
||||
# Install resources (re-use python files)
|
||||
$(LINK) ../modgui $(DESTDIR)$(DATADIR)/carla/resources
|
||||
$(LINK) ../patchcanvas $(DESTDIR)$(DATADIR)/carla/resources
|
||||
$(LINK) ../widgets $(DESTDIR)$(DATADIR)/carla/resources
|
||||
$(LINK) ../carla_app.py $(DESTDIR)$(DATADIR)/carla/resources
|
||||
|
@ -690,6 +699,16 @@ ifeq ($(HAVE_PYQT),true)
|
|||
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)
|
||||
install_external_plugins:
|
||||
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();
|
||||
}
|
||||
|
||||
#ifndef BUILD_BRIDGE
|
||||
if (fUI.rdfDescriptor->Type == LV2_UI_MOD)
|
||||
pData->tryTransient();
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -4640,6 +4645,9 @@ public:
|
|||
case LV2_UI_X11:
|
||||
bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-x11";
|
||||
break;
|
||||
case LV2_UI_MOD:
|
||||
bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-modgui";
|
||||
break;
|
||||
#if 0
|
||||
case LV2_UI_EXTERNAL:
|
||||
case LV2_UI_OLD_EXTERNAL:
|
||||
|
@ -5691,8 +5699,8 @@ public:
|
|||
// ---------------------------------------------------------------
|
||||
// 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)
|
||||
const bool preferUiBridges = true;
|
||||
|
@ -5750,6 +5758,9 @@ public:
|
|||
case LV2_UI_OLD_EXTERNAL:
|
||||
iExt = ii;
|
||||
break;
|
||||
case LV2_UI_MOD:
|
||||
eMod = ii;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -5816,8 +5827,14 @@ public:
|
|||
|
||||
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 == eCocoa ||
|
||||
iFinal == eWindows ||
|
||||
iFinal == eX11)
|
||||
iFinal == eX11 ||
|
||||
iFinal == eMod)
|
||||
#ifdef BUILD_BRIDGE
|
||||
&& ! hasShowInterface
|
||||
#endif
|
||||
|
@ -5891,7 +5909,7 @@ public:
|
|||
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);
|
||||
fUI.rdfDescriptor = nullptr;
|
||||
|
|
|
@ -187,7 +187,7 @@ static const CarlaCachedPluginInfo* get_cached_plugin_lv2(Lv2WorldClass& lv2Worl
|
|||
|
||||
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;
|
||||
|
||||
{
|
||||
|
|
|
@ -56,6 +56,7 @@ RES = \
|
|||
$(BINDIR)/resources/carla_control.py \
|
||||
$(BINDIR)/resources/carla_database.py \
|
||||
$(BINDIR)/resources/carla_host.py \
|
||||
$(BINDIR)/resources/carla_modgui.py \
|
||||
$(BINDIR)/resources/carla_settings.py \
|
||||
$(BINDIR)/resources/carla_skin.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_EXTERNAL 8
|
||||
#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_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_EXTERNAL(x) ((x) == LV2_UI_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
|
||||
#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_llmm "http://ll-plugins.nongnu.org/lv2/ext/midimap#"
|
||||
#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__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
|
||||
{
|
||||
const bool hasMODGui(lilvPlugin.get_modgui_resources_directory().as_uri() != nullptr);
|
||||
|
||||
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];
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue