Browse Source

Add initial lv2-modgui bridge code

tags/1.9.6
falkTX 9 years ago
parent
commit
725ddf8ac1
5 changed files with 500 additions and 0 deletions
  1. +7
    -0
      Makefile
  2. +6
    -0
      bin/carla-bridge-lv2-modgui
  3. +1
    -0
      bin/carla.lv2/carla-bridge-lv2-modgui
  4. +10
    -0
      data/carla-bridge-lv2-modgui
  5. +476
    -0
      source/carla_modgui.py

+ 7
- 0
Makefile View File

@@ -478,6 +478,11 @@ endif
bin/carla-discovery-* \ bin/carla-discovery-* \
$(DESTDIR)$(PREFIX)/lib/carla/ $(DESTDIR)$(PREFIX)/lib/carla/


# Install the real modgui bridge
install -m 755 \
data/carla-bridge-lv2-modgui \
$(DESTDIR)$(PREFIX)/lib/carla/

# Install theme # Install theme
install -m 644 \ install -m 644 \
bin/styles/* \ bin/styles/* \
@@ -540,6 +545,7 @@ endif
$(LINK) $(PREFIX)/share/carla/carla_config.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(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_database.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/carla_host.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_panels.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/carla_settings.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/ $(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-rack \
$(DESTDIR)$(PREFIX)/bin/carla-single \ $(DESTDIR)$(PREFIX)/bin/carla-single \
$(DESTDIR)$(PREFIX)/bin/carla-settings \ $(DESTDIR)$(PREFIX)/bin/carla-settings \
$(DESTDIR)$(PREFIX)/lib/carla/carla-bridge-lv2-modgui \
$(DESTDIR)$(PREFIX)/lib/pkgconfig/carla-standalone.pc $(DESTDIR)$(PREFIX)/lib/pkgconfig/carla-standalone.pc


# $(DESTDIR)$(PREFIX)/bin/carla-control \ # $(DESTDIR)$(PREFIX)/bin/carla-control \


+ 6
- 0
bin/carla-bridge-lv2-modgui View File

@@ -0,0 +1,6 @@
#!/bin/bash

ASPATH=`readlink -f $0`
BINDIR=`dirname $ASPATH`

exec python3 $BINDIR/../source/carla_modgui.py "$@"

+ 1
- 0
bin/carla.lv2/carla-bridge-lv2-modgui View File

@@ -0,0 +1 @@
../carla-bridge-lv2-modgui

+ 10
- 0
data/carla-bridge-lv2-modgui View File

@@ -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 "$@"

+ 476
- 0
source/carla_modgui.py View File

@@ -0,0 +1,476 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Carla bridge for LV2 modguis
# Copyright (C) 2015 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 (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 <plugin-uri> <ui-uri>" % 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()

# ------------------------------------------------------------------------------------------------------------

Loading…
Cancel
Save