From ca70e59867ef63962c6f448f80716be1ab00306d Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Tue, 5 Nov 2019 22:33:27 +0100 Subject: [PATCH 01/24] Replace lilvlib with own equivalent implementation based on current lilv Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lilvlib.py | 1473 ----------------- .../model/lv2/lv2_effect_builder.py | 14 +- pluginsmanager/model/lv2/lv2_plugin_info.py | 659 ++++++++ 3 files changed, 669 insertions(+), 1477 deletions(-) delete mode 100755 pluginsmanager/model/lv2/lilvlib.py create mode 100644 pluginsmanager/model/lv2/lv2_plugin_info.py diff --git a/pluginsmanager/model/lv2/lilvlib.py b/pluginsmanager/model/lv2/lilvlib.py deleted file mode 100755 index d37ae81..0000000 --- a/pluginsmanager/model/lv2/lilvlib.py +++ /dev/null @@ -1,1473 +0,0 @@ -""" -File copy from https://github.com/moddevices/lilvlib -""" -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------------------------------------ -# Imports - -import json -import lilv -import os - -from math import fmod - -# ------------------------------------------------------------------------------------------------------------ -# Utilities - -def LILV_FOREACH(collection, func): - itr = collection.begin() - while itr: - yield func(collection.get(itr)) - itr = collection.next(itr) - -class NS(object): - def __init__(self, world, base): - self.world = world - self.base = base - self._cache = {} - - def __getattr__(self, attr): - if attr.endswith("_"): - attr = attr[:-1] - if attr not in self._cache: - self._cache[attr] = lilv.Node(self.world.new_uri(self.base+attr)) - return self._cache[attr] - -def is_integer(string): - return string.strip().lstrip("-+").isdigit() - -def get_short_port_name(portName): - if len(portName) <= 16: - return portName - - portName = portName.split("/",1)[0].split(" (",1)[0].split(" [",1)[0].strip() - - # cut stuff if too big - if len(portName) > 16: - portName = portName[0] + portName[1:].replace("a","").replace("e","").replace("i","").replace("o","").replace("u","") - - if len(portName) > 16: - portName = portName[:16] - - return portName.strip() - -# ------------------------------------------------------------------------------------------------------------ - -def get_category(nodes): - lv2_category_indexes = { - 'DelayPlugin': ['Delay'], - 'DistortionPlugin': ['Distortion'], - 'WaveshaperPlugin': ['Distortion', 'Waveshaper'], - 'DynamicsPlugin': ['Dynamics'], - 'AmplifierPlugin': ['Dynamics', 'Amplifier'], - 'CompressorPlugin': ['Dynamics', 'Compressor'], - 'ExpanderPlugin': ['Dynamics', 'Expander'], - 'GatePlugin': ['Dynamics', 'Gate'], - 'LimiterPlugin': ['Dynamics', 'Limiter'], - 'FilterPlugin': ['Filter'], - 'AllpassPlugin': ['Filter', 'Allpass'], - 'BandpassPlugin': ['Filter', 'Bandpass'], - 'CombPlugin': ['Filter', 'Comb'], - 'EQPlugin': ['Filter', 'Equaliser'], - 'MultiEQPlugin': ['Filter', 'Equaliser', 'Multiband'], - 'ParaEQPlugin': ['Filter', 'Equaliser', 'Parametric'], - 'HighpassPlugin': ['Filter', 'Highpass'], - 'LowpassPlugin': ['Filter', 'Lowpass'], - 'GeneratorPlugin': ['Generator'], - 'ConstantPlugin': ['Generator', 'Constant'], - 'InstrumentPlugin': ['Generator', 'Instrument'], - 'OscillatorPlugin': ['Generator', 'Oscillator'], - 'ModulatorPlugin': ['Modulator'], - 'ChorusPlugin': ['Modulator', 'Chorus'], - 'FlangerPlugin': ['Modulator', 'Flanger'], - 'PhaserPlugin': ['Modulator', 'Phaser'], - 'ReverbPlugin': ['Reverb'], - 'SimulatorPlugin': ['Simulator'], - 'SpatialPlugin': ['Spatial'], - 'SpectralPlugin': ['Spectral'], - 'PitchPlugin': ['Spectral', 'Pitch Shifter'], - 'UtilityPlugin': ['Utility'], - 'AnalyserPlugin': ['Utility', 'Analyser'], - 'ConverterPlugin': ['Utility', 'Converter'], - 'FunctionPlugin': ['Utility', 'Function'], - 'MixerPlugin': ['Utility', 'Mixer'], - #'MIDIPlugin': ['MIDI', 'Utility'], - } - mod_category_indexes = { - 'DelayPlugin': ['Delay'], - 'DistortionPlugin': ['Distortion'], - 'DynamicsPlugin': ['Dynamics'], - 'FilterPlugin': ['Filter'], - 'GeneratorPlugin': ['Generator'], - 'ModulatorPlugin': ['Modulator'], - 'ReverbPlugin': ['Reverb'], - 'SimulatorPlugin': ['Simulator'], - 'SpatialPlugin': ['Spatial'], - 'SpectralPlugin': ['Spectral'], - 'UtilityPlugin': ['Utility'], - 'MIDIPlugin': ['Utility', 'MIDI'], - } - - def fill_in_lv2_category(node): - category = node.as_string().replace("http://lv2plug.in/ns/lv2core#","") - if category in lv2_category_indexes.keys(): - return lv2_category_indexes[category] - return [] - - def fill_in_mod_category(node): - category = node.as_string().replace("http://moddevices.com/ns/mod#","") - if category in mod_category_indexes.keys(): - return mod_category_indexes[category] - return [] - - categories = [] - for cat in [cat for catlist in LILV_FOREACH(nodes, fill_in_mod_category) for cat in catlist]: - if cat not in categories: - categories.append(cat) - - if len(categories) > 0: - return categories - - for cat in [cat for catlist in LILV_FOREACH(nodes, fill_in_lv2_category) for cat in catlist]: - if cat not in categories: - categories.append(cat) - - return categories - -def get_port_data(port, subj): - nodes = port.get_value(subj.me) - data = [] - - it = lilv.lilv_nodes_begin(nodes) - while not lilv.lilv_nodes_is_end(nodes, it): - dat = lilv.lilv_nodes_get(nodes, it) - it = lilv.lilv_nodes_next(nodes, it) - if dat is None: - continue - data.append(lilv.lilv_node_as_string(dat)) - - return data - -def get_port_unit(miniuri): - # using label, render, symbol - units = { - 's': ["seconds", "%f s", "s"], - 'ms': ["milliseconds", "%f ms", "ms"], - 'min': ["minutes", "%f mins", "min"], - 'bar': ["bars", "%f bars", "bars"], - 'beat': ["beats", "%f beats", "beats"], - 'frame': ["audio frames", "%f frames", "frames"], - 'm': ["metres", "%f m", "m"], - 'cm': ["centimetres", "%f cm", "cm"], - 'mm': ["millimetres", "%f mm", "mm"], - 'km': ["kilometres", "%f km", "km"], - 'inch': ["inches", """%f\"""", "in"], - 'mile': ["miles", "%f mi", "mi"], - 'db': ["decibels", "%f dB", "dB"], - 'pc': ["percent", "%f%%", "%"], - 'coef': ["coefficient", "* %f", "*"], - 'hz': ["hertz", "%f Hz", "Hz"], - 'khz': ["kilohertz", "%f kHz", "kHz"], - 'mhz': ["megahertz", "%f MHz", "MHz"], - 'bpm': ["beats per minute", "%f BPM", "BPM"], - 'oct': ["octaves", "%f octaves", "oct"], - 'cent': ["cents", "%f ct", "ct"], - 'semitone12TET': ["semitones", "%f semi", "semi"], - 'degree': ["degrees", "%f deg", "deg"], - 'midiNote': ["MIDI note", "MIDI note %d", "note"], - } - if miniuri in units.keys(): - return units[miniuri] - return ("","","") - -# ------------------------------------------------------------------------------------------------------------ -# get_bundle_dirname - -def get_bundle_dirname(bundleuri): - bundle = lilv.lilv_uri_to_path(bundleuri) - - if not os.path.exists(bundle): - raise IOError(bundleuri) - if os.path.isfile(bundle): - bundle = os.path.dirname(bundle) - - return bundle - -# ------------------------------------------------------------------------------------------------------------ -# get_pedalboard_info - -# Get info from an lv2 bundle -# @a bundle is a string, consisting of a directory in the filesystem (absolute pathname). -def get_pedalboard_info(bundle): - # lilv wants the last character as the separator - bundle = os.path.abspath(bundle) - if not bundle.endswith(os.sep): - bundle += os.sep - - # Create our own unique lilv world - # We'll load a single bundle and get all plugins from it - world = lilv.World() - - # this is needed when loading specific bundles instead of load_all - # (these functions are not exposed via World yet) - lilv.lilv_world_load_specifications(world.me) - lilv.lilv_world_load_plugin_classes(world.me) - - # convert bundle string into a lilv node - bundlenode = lilv.lilv_new_file_uri(world.me, None, bundle) - - # load the bundle - world.load_bundle(bundlenode) - - # free bundlenode, no longer needed - lilv.lilv_node_free(bundlenode) - - # get all plugins in the bundle - plugins = world.get_all_plugins() - - # make sure the bundle includes 1 and only 1 plugin (the pedalboard) - if plugins.size() != 1: - raise Exception('get_pedalboard_info(%s) - bundle has 0 or > 1 plugin'.format(bundle)) - - # no indexing in python-lilv yet, just get the first item - plugin = None - for p in plugins: - plugin = p - break - - if plugin is None: - raise Exception('get_pedalboard_info(%s) - failed to get plugin, you are using an old lilv!'.format(bundle)) - - # define the needed stuff - ns_rdf = NS(world, lilv.LILV_NS_RDF) - ns_lv2core = NS(world, lilv.LILV_NS_LV2) - ns_ingen = NS(world, "http://drobilla.net/ns/ingen#") - ns_mod = NS(world, "http://moddevices.com/ns/mod#") - ns_modpedal = NS(world, "http://moddevices.com/ns/modpedal#") - - # check if the plugin is a pedalboard - def fill_in_type(node): - return node.as_string() - plugin_types = [i for i in LILV_FOREACH(plugin.get_value(ns_rdf.type_), fill_in_type)] - - if "http://moddevices.com/ns/modpedal#Pedalboard" not in plugin_types: - raise Exception('get_pedalboard_info(%s) - plugin has no mod:Pedalboard type'.format(bundle)) - - # let's get all the info now - ingenarcs = [] - ingenblocks = [] - - info = { - 'name' : plugin.get_name().as_string(), - 'uri' : plugin.get_uri().as_string(), - 'author': plugin.get_author_name().as_string() or "", # Might be empty - 'hardware': { - # we save this info later - 'audio': { - 'ins' : 0, - 'outs': 0 - }, - 'cv': { - 'ins' : 0, - 'outs': 0 - }, - 'midi': { - 'ins' : 0, - 'outs': 0 - } - }, - 'size': { - 'width' : plugin.get_value(ns_modpedal.width).get_first().as_int(), - 'height': plugin.get_value(ns_modpedal.height).get_first().as_int(), - }, - 'screenshot' : os.path.basename(plugin.get_value(ns_modpedal.screenshot).get_first().as_string() or ""), - 'thumbnail' : os.path.basename(plugin.get_value(ns_modpedal.thumbnail).get_first().as_string() or ""), - 'connections': [], # we save this info later - 'plugins' : [] # we save this info later - } - - # connections - arcs = plugin.get_value(ns_ingen.arc) - it = arcs.begin() - while not arcs.is_end(it): - arc = arcs.get(it) - it = arcs.next(it) - - if arc.me is None: - continue - - head = lilv.lilv_world_get(world.me, arc.me, ns_ingen.head.me, None) - tail = lilv.lilv_world_get(world.me, arc.me, ns_ingen.tail.me, None) - - if head is None or tail is None: - continue - - ingenarcs.append({ - "source": lilv.lilv_uri_to_path(lilv.lilv_node_as_string(tail)).replace(bundle,"",1), - "target": lilv.lilv_uri_to_path(lilv.lilv_node_as_string(head)).replace(bundle,"",1) - }) - - # hardware ports - handled_port_uris = [] - ports = plugin.get_value(ns_lv2core.port) - it = ports.begin() - while not ports.is_end(it): - port = ports.get(it) - it = ports.next(it) - - if port.me is None: - continue - - # check if we already handled this port - port_uri = port.as_uri() - if port_uri in handled_port_uris: - continue - if port_uri.endswith("/control_in") or port_uri.endswith("/control_out"): - continue - handled_port_uris.append(port_uri) - - # get types - port_types = lilv.lilv_world_find_nodes(world.me, port.me, ns_rdf.type_.me, None) - - if port_types is None: - continue - - portDir = "" # input or output - portType = "" # atom, audio or cv - - it2 = lilv.lilv_nodes_begin(port_types) - while not lilv.lilv_nodes_is_end(port_types, it2): - port_type = lilv.lilv_nodes_get(port_types, it2) - it2 = lilv.lilv_nodes_next(port_types, it2) - - if port_type is None: - continue - - port_type_uri = lilv.lilv_node_as_uri(port_type) - - if port_type_uri == "http://lv2plug.in/ns/lv2core#InputPort": - portDir = "input" - elif port_type_uri == "http://lv2plug.in/ns/lv2core#OutputPort": - portDir = "output" - elif port_type_uri == "http://lv2plug.in/ns/lv2core#AudioPort": - portType = "audio" - elif port_type_uri == "http://lv2plug.in/ns/lv2core#CVPort": - portType = "cv" - elif port_type_uri == "http://lv2plug.in/ns/ext/atom#AtomPort": - portType = "atom" - - if not (portDir or portType): - continue - - if portType == "audio": - if portDir == "input": - info['hardware']['audio']['ins'] += 1 - else: - info['hardware']['audio']['outs'] += 1 - - elif portType == "atom": - if portDir == "input": - info['hardware']['midi']['ins'] += 1 - else: - info['hardware']['midi']['outs'] += 1 - - elif portType == "cv": - if portDir == "input": - info['hardware']['cv']['ins'] += 1 - else: - info['hardware']['cv']['outs'] += 1 - - # plugins - blocks = plugin.get_value(ns_ingen.block) - it = blocks.begin() - while not blocks.is_end(it): - block = blocks.get(it) - it = blocks.next(it) - - if block.me is None: - continue - - protouri1 = lilv.lilv_world_get(world.me, block.me, ns_lv2core.prototype.me, None) - protouri2 = lilv.lilv_world_get(world.me, block.me, ns_ingen.prototype.me, None) - - if protouri1 is not None: - proto = protouri1 - elif protouri2 is not None: - proto = protouri2 - else: - continue - - instance = lilv.lilv_uri_to_path(lilv.lilv_node_as_string(block.me)).replace(bundle,"",1) - uri = lilv.lilv_node_as_uri(proto) - - enabled = lilv.lilv_world_get(world.me, block.me, ns_ingen.enabled.me, None) - builder = lilv.lilv_world_get(world.me, block.me, ns_mod.builderVersion.me, None) - release = lilv.lilv_world_get(world.me, block.me, ns_mod.releaseNumber.me, None) - minorver = lilv.lilv_world_get(world.me, block.me, ns_lv2core.minorVersion.me, None) - microver = lilv.lilv_world_get(world.me, block.me, ns_lv2core.microVersion.me, None) - - ingenblocks.append({ - "instance": instance, - "uri" : uri, - "x" : lilv.lilv_node_as_float(lilv.lilv_world_get(world.me, block.me, ns_ingen.canvasX.me, None)), - "y" : lilv.lilv_node_as_float(lilv.lilv_world_get(world.me, block.me, ns_ingen.canvasY.me, None)), - "enabled" : lilv.lilv_node_as_bool(enabled) if enabled is not None else False, - "builder" : lilv.lilv_node_as_int(builder) if builder else 0, - "release" : lilv.lilv_node_as_int(release) if release else 0, - "minorVersion": lilv.lilv_node_as_int(minorver) if minorver else 0, - "microVersion": lilv.lilv_node_as_int(microver) if microver else 0, - }) - - info['connections'] = ingenarcs - info['plugins'] = ingenblocks - - return info - -# ------------------------------------------------------------------------------------------------------------ -# get_pedalboard_name - -# Faster version of get_pedalboard_info when we just need to know the pedalboard name -# @a bundle is a string, consisting of a directory in the filesystem (absolute pathname). -def get_pedalboard_name(bundle): - # lilv wants the last character as the separator - bundle = os.path.abspath(bundle) - if not bundle.endswith(os.sep): - bundle += os.sep - - # Create our own unique lilv world - # We'll load a single bundle and get all plugins from it - world = lilv.World() - - # this is needed when loading specific bundles instead of load_all - # (these functions are not exposed via World yet) - lilv.lilv_world_load_specifications(world.me) - lilv.lilv_world_load_plugin_classes(world.me) - - # convert bundle string into a lilv node - bundlenode = lilv.lilv_new_file_uri(world.me, None, bundle) - - # load the bundle - world.load_bundle(bundlenode) - - # free bundlenode, no longer needed - lilv.lilv_node_free(bundlenode) - - # get all plugins in the bundle - plugins = world.get_all_plugins() - - # make sure the bundle includes 1 and only 1 plugin (the pedalboard) - if plugins.size() != 1: - raise Exception('get_pedalboard_info(%s) - bundle has 0 or > 1 plugin'.format(bundle)) - - # no indexing in python-lilv yet, just get the first item - plugin = None - for p in plugins: - plugin = p - break - - if plugin is None: - raise Exception('get_pedalboard_info(%s) - failed to get plugin, you are using an old lilv!'.format(bundle)) - - # define the needed stuff - ns_rdf = NS(world, lilv.LILV_NS_RDF) - - # check if the plugin is a pedalboard - def fill_in_type(node): - return node.as_string() - plugin_types = [i for i in LILV_FOREACH(plugin.get_value(ns_rdf.type_), fill_in_type)] - - if "http://moddevices.com/ns/modpedal#Pedalboard" not in plugin_types: - raise Exception('get_pedalboard_info(%s) - plugin has no mod:Pedalboard type'.format(bundle)) - - return plugin.get_name().as_string() - -# ------------------------------------------------------------------------------------------------------------ -# plugin_has_modgui - -# Check if a plugin has modgui -def plugin_has_modgui(world, plugin): - # define the needed stuff - ns_modgui = NS(world, "http://moddevices.com/ns/modgui#") - - # -------------------------------------------------------------------------------------------------------- - # get the proper modgui - - modguigui = None - - nodes = plugin.get_value(ns_modgui.gui) - it = nodes.begin() - while not nodes.is_end(it): - mgui = nodes.get(it) - it = nodes.next(it) - if mgui.me is None: - continue - resdir = world.find_nodes(mgui.me, ns_modgui.resourcesDirectory.me, None).get_first() - if resdir.me is None: - continue - modguigui = mgui - if os.path.expanduser("~") in lilv.lilv_uri_to_path(resdir.as_string()): - # found a modgui in the home dir, stop here and use it - break - - del nodes, it - - # -------------------------------------------------------------------------------------------------------- - # check selected modgui - - if modguigui is None or modguigui.me is None: - return False - - # resourcesDirectory *must* be present - modgui_resdir = world.find_nodes(modguigui.me, ns_modgui.resourcesDirectory.me, None).get_first() - - if modgui_resdir.me is None: - return False - - return os.path.exists(lilv.lilv_uri_to_path(modgui_resdir.as_string())) - -# ------------------------------------------------------------------------------------------------------------ -# get_plugin_info - -# Get info from a lilv plugin -# This is used in get_plugins_info below and MOD-SDK -def get_plugin_info(world, plugin, useAbsolutePath = True): - # define the needed stuff - ns_doap = NS(world, lilv.LILV_NS_DOAP) - ns_foaf = NS(world, lilv.LILV_NS_FOAF) - ns_rdf = NS(world, lilv.LILV_NS_RDF) - ns_rdfs = NS(world, lilv.LILV_NS_RDFS) - ns_lv2core = NS(world, lilv.LILV_NS_LV2) - ns_atom = NS(world, "http://lv2plug.in/ns/ext/atom#") - ns_midi = NS(world, "http://lv2plug.in/ns/ext/midi#") - ns_morph = NS(world, "http://lv2plug.in/ns/ext/morph#") - ns_pprops = NS(world, "http://lv2plug.in/ns/ext/port-props#") - ns_pset = NS(world, "http://lv2plug.in/ns/ext/presets#") - ns_units = NS(world, "http://lv2plug.in/ns/extensions/units#") - ns_mod = NS(world, "http://moddevices.com/ns/mod#") - ns_modgui = NS(world, "http://moddevices.com/ns/modgui#") - - bundleuri = plugin.get_bundle_uri().as_string() - bundle = lilv.lilv_uri_to_path(bundleuri) - - errors = [] - warnings = [] - - # -------------------------------------------------------------------------------------------------------- - # uri - - uri = plugin.get_uri().as_string() or "" - - if not uri: - errors.append("plugin uri is missing or invalid") - elif uri.startswith("file:"): - errors.append("plugin uri is local, and thus not suitable for redistribution") - #elif not (uri.startswith("http:") or uri.startswith("https:")): - #warnings.append("plugin uri is not a real url") - - # -------------------------------------------------------------------------------------------------------- - # name - - name = plugin.get_name().as_string() or "" - - if not name: - errors.append("plugin name is missing") - - # -------------------------------------------------------------------------------------------------------- - # binary - - binary = lilv.lilv_uri_to_path(plugin.get_library_uri().as_string() or "") - - if not binary: - errors.append("plugin binary is missing") - elif not useAbsolutePath: - binary = binary.replace(bundle,"",1) - - # -------------------------------------------------------------------------------------------------------- - # license - - license = plugin.get_value(ns_doap.license).get_first().as_string() or "" - - if not license: - prj = plugin.get_value(ns_lv2core.project).get_first() - if prj.me is not None: - licsnode = lilv.lilv_world_get(world.me, prj.me, ns_doap.license.me, None) - if licsnode is not None: - license = lilv.lilv_node_as_string(licsnode) - del licsnode - del prj - - if not license: - errors.append("plugin license is missing") - - elif license.startswith(bundleuri): - license = license.replace(bundleuri,"",1) - warnings.append("plugin license entry is a local path instead of a string") - - # -------------------------------------------------------------------------------------------------------- - # comment - - comment = (plugin.get_value(ns_rdfs.comment).get_first().as_string() or "").strip() - - # sneaky empty comments! - if len(comment) > 0 and comment == len(comment) * comment[0]: - comment = "" - - if not comment: - errors.append("plugin comment is missing") - - # -------------------------------------------------------------------------------------------------------- - # version - - microver = plugin.get_value(ns_lv2core.microVersion).get_first() - minorver = plugin.get_value(ns_lv2core.minorVersion).get_first() - - if microver.me is None and minorver.me is None: - errors.append("plugin is missing version information") - minorVersion = 0 - microVersion = 0 - - else: - if minorver.me is None: - errors.append("plugin is missing minorVersion") - minorVersion = 0 - else: - minorVersion = minorver.as_int() - - if microver.me is None: - errors.append("plugin is missing microVersion") - microVersion = 0 - else: - microVersion = microver.as_int() - - del minorver - del microver - - version = "%d.%d" % (minorVersion, microVersion) - - # 0.x is experimental - if minorVersion == 0: - stability = "experimental" - - # odd x.2 or 2.x is testing/development - elif minorVersion % 2 != 0 or microVersion % 2 != 0: - stability = "testing" - - # otherwise it's stable - else: - stability = "stable" - - # -------------------------------------------------------------------------------------------------------- - # author - - author = { - 'name' : plugin.get_author_name().as_string() or "", - 'homepage': plugin.get_author_homepage().as_string() or "", - 'email' : plugin.get_author_email().as_string() or "", - } - - if not author['name']: - errors.append("plugin author name is missing") - - if not author['homepage']: - prj = plugin.get_value(ns_lv2core.project).get_first() - if prj.me is not None: - maintainer = lilv.lilv_world_get(world.me, prj.me, ns_doap.maintainer.me, None) - if maintainer is not None: - homepage = lilv.lilv_world_get(world.me, maintainer, ns_foaf.homepage.me, None) - if homepage is not None: - author['homepage'] = lilv.lilv_node_as_string(homepage) - del homepage - del maintainer - del prj - - if not author['homepage']: - warnings.append("plugin author homepage is missing") - - if not author['email']: - pass - elif author['email'].startswith(bundleuri): - author['email'] = author['email'].replace(bundleuri,"",1) - warnings.append("plugin author email entry is missing 'mailto:' prefix") - elif author['email'].startswith("mailto:"): - author['email'] = author['email'].replace("mailto:","",1) - - # -------------------------------------------------------------------------------------------------------- - # brand - - brand = plugin.get_value(ns_mod.brand).get_first().as_string() or "" - - if not brand: - brand = author['name'].split(" - ",1)[0].split(" ",1)[0] - brand = brand.rstrip(",").rstrip(";") - if len(brand) > 11: - brand = brand[:11] - warnings.append("plugin brand is missing") - - elif len(brand) > 11: - brand = brand[:11] - errors.append("plugin brand has more than 11 characters") - - # -------------------------------------------------------------------------------------------------------- - # label - - label = plugin.get_value(ns_mod.label).get_first().as_string() or "" - - if not label: - if len(name) <= 16: - label = name - else: - labels = name.split(" - ",1)[0].split(" ") - if labels[0].lower() in bundle.lower() and len(labels) > 1 and not labels[1].startswith(("(","[")): - label = labels[1] - else: - label = labels[0] - - if len(label) > 16: - label = label[:16] - - warnings.append("plugin label is missing") - del labels - - elif len(label) > 16: - label = label[:16] - errors.append("plugin label has more than 16 characters") - - # -------------------------------------------------------------------------------------------------------- - # bundles - - bundles = [] - - if useAbsolutePath: - bnodes = lilv.lilv_plugin_get_data_uris(plugin.me) - - it = lilv.lilv_nodes_begin(bnodes) - while not lilv.lilv_nodes_is_end(bnodes, it): - bnode = lilv.lilv_nodes_get(bnodes, it) - it = lilv.lilv_nodes_next(bnodes, it) - - if bnode is None: - continue - if not lilv.lilv_node_is_uri(bnode): - continue - - bpath = os.path.abspath(os.path.dirname(lilv.lilv_uri_to_path(lilv.lilv_node_as_uri(bnode)))) - - if not bpath.endswith(os.sep): - bpath += os.sep - - if bpath not in bundles: - bundles.append(bpath) - - if bundle not in bundles: - bundles.append(bundle) - - del bnodes, it - - # -------------------------------------------------------------------------------------------------------- - # get the proper modgui - - modguigui = None - - nodes = plugin.get_value(ns_modgui.gui) - it = nodes.begin() - while not nodes.is_end(it): - mgui = nodes.get(it) - it = nodes.next(it) - if mgui.me is None: - continue - resdir = world.find_nodes(mgui.me, ns_modgui.resourcesDirectory.me, None).get_first() - if resdir.me is None: - continue - modguigui = mgui - if not useAbsolutePath: - # special build, use first modgui found - break - if os.path.expanduser("~") in lilv.lilv_uri_to_path(resdir.as_string()): - # found a modgui in the home dir, stop here and use it - break - - del nodes, it - - # -------------------------------------------------------------------------------------------------------- - # gui - - gui = {} - - if modguigui is None or modguigui.me is None: - warnings.append("no modgui available") - - else: - # resourcesDirectory *must* be present - modgui_resdir = world.find_nodes(modguigui.me, ns_modgui.resourcesDirectory.me, None).get_first() - - if modgui_resdir.me is None: - errors.append("modgui has no resourcesDirectory data") - - else: - if useAbsolutePath: - gui['resourcesDirectory'] = lilv.lilv_uri_to_path(modgui_resdir.as_string()) - - # check if modgui is defined in a separate file - gui['usingSeeAlso'] = os.path.exists(os.path.join(bundle, "modgui.ttl")) - - # check if the modgui definition is on its own file and in the user dir - gui['modificableInPlace'] = bool((bundle not in gui['resourcesDirectory'] or gui['usingSeeAlso']) and - os.path.expanduser("~") in gui['resourcesDirectory']) - else: - gui['resourcesDirectory'] = modgui_resdir.as_string().replace(bundleuri,"",1) - - # icon and settings templates - modgui_icon = world.find_nodes(modguigui.me, ns_modgui.iconTemplate .me, None).get_first() - modgui_setts = world.find_nodes(modguigui.me, ns_modgui.settingsTemplate.me, None).get_first() - - if modgui_icon.me is None: - errors.append("modgui has no iconTemplate data") - else: - iconFile = lilv.lilv_uri_to_path(modgui_icon.as_string()) - if os.path.exists(iconFile): - gui['iconTemplate'] = iconFile if useAbsolutePath else iconFile.replace(bundle,"",1) - else: - errors.append("modgui iconTemplate file is missing") - del iconFile - - if modgui_setts.me is not None: - settingsFile = lilv.lilv_uri_to_path(modgui_setts.as_string()) - if os.path.exists(settingsFile): - gui['settingsTemplate'] = settingsFile if useAbsolutePath else settingsFile.replace(bundle,"",1) - else: - errors.append("modgui settingsTemplate file is missing") - del settingsFile - - # javascript and stylesheet files - modgui_script = world.find_nodes(modguigui.me, ns_modgui.javascript.me, None).get_first() - modgui_style = world.find_nodes(modguigui.me, ns_modgui.stylesheet.me, None).get_first() - - if modgui_script.me is not None: - javascriptFile = lilv.lilv_uri_to_path(modgui_script.as_string()) - if os.path.exists(javascriptFile): - gui['javascript'] = javascriptFile if useAbsolutePath else javascriptFile.replace(bundle,"",1) - else: - errors.append("modgui javascript file is missing") - del javascriptFile - - if modgui_style.me is None: - errors.append("modgui has no stylesheet data") - else: - stylesheetFile = lilv.lilv_uri_to_path(modgui_style.as_string()) - if os.path.exists(stylesheetFile): - gui['stylesheet'] = stylesheetFile if useAbsolutePath else stylesheetFile.replace(bundle,"",1) - else: - errors.append("modgui stylesheet file is missing") - del stylesheetFile - - # template data for backwards compatibility - # FIXME remove later once we got rid of all templateData files - modgui_templ = world.find_nodes(modguigui.me, ns_modgui.templateData.me, None).get_first() - - if modgui_templ.me is not None: - warnings.append("modgui is using old deprecated templateData") - templFile = lilv.lilv_uri_to_path(modgui_templ.as_string()) - if os.path.exists(templFile): - with open(templFile, 'r') as fd: - try: - data = json.loads(fd.read()) - except: - data = {} - keys = list(data.keys()) - - if 'author' in keys: - gui['brand'] = data['author'] - if 'label' in keys: - gui['label'] = data['label'] - if 'color' in keys: - gui['color'] = data['color'] - if 'knob' in keys: - gui['knob'] = data['knob'] - if 'controls' in keys: - index = 0 - ports = [] - for ctrl in data['controls']: - ports.append({ - 'index' : index, - 'name' : ctrl['name'], - 'symbol': ctrl['symbol'], - }) - index += 1 - gui['ports'] = ports - del templFile - - # screenshot and thumbnail - modgui_scrn = world.find_nodes(modguigui.me, ns_modgui.screenshot.me, None).get_first() - modgui_thumb = world.find_nodes(modguigui.me, ns_modgui.thumbnail .me, None).get_first() - - if modgui_scrn.me is not None: - gui['screenshot'] = lilv.lilv_uri_to_path(modgui_scrn.as_string()) - if not os.path.exists(gui['screenshot']): - errors.append("modgui screenshot file is missing") - if not useAbsolutePath: - gui['screenshot'] = gui['screenshot'].replace(bundle,"",1) - else: - errors.append("modgui has no screnshot data") - - if modgui_thumb.me is not None: - gui['thumbnail'] = lilv.lilv_uri_to_path(modgui_thumb.as_string()) - if not os.path.exists(gui['thumbnail']): - errors.append("modgui thumbnail file is missing") - if not useAbsolutePath: - gui['thumbnail'] = gui['thumbnail'].replace(bundle,"",1) - else: - errors.append("modgui has no thumbnail data") - - # extra stuff, all optional - modgui_brand = world.find_nodes(modguigui.me, ns_modgui.brand.me, None).get_first() - modgui_label = world.find_nodes(modguigui.me, ns_modgui.label.me, None).get_first() - modgui_model = world.find_nodes(modguigui.me, ns_modgui.model.me, None).get_first() - modgui_panel = world.find_nodes(modguigui.me, ns_modgui.panel.me, None).get_first() - modgui_color = world.find_nodes(modguigui.me, ns_modgui.color.me, None).get_first() - modgui_knob = world.find_nodes(modguigui.me, ns_modgui.knob .me, None).get_first() - - if modgui_brand.me is not None: - gui['brand'] = modgui_brand.as_string() - if modgui_label.me is not None: - gui['label'] = modgui_label.as_string() - if modgui_model.me is not None: - gui['model'] = modgui_model.as_string() - if modgui_panel.me is not None: - gui['panel'] = modgui_panel.as_string() - if modgui_color.me is not None: - gui['color'] = modgui_color.as_string() - if modgui_knob.me is not None: - gui['knob'] = modgui_knob.as_string() - - # ports - errpr = False - sybls = [] - ports = [] - nodes = world.find_nodes(modguigui.me, ns_modgui.port.me, None) - it = lilv.lilv_nodes_begin(nodes.me) - while not lilv.lilv_nodes_is_end(nodes.me, it): - port = lilv.lilv_nodes_get(nodes.me, it) - it = lilv.lilv_nodes_next(nodes.me, it) - if port is None: - break - port_indx = world.find_nodes(port, ns_lv2core.index .me, None).get_first() - port_symb = world.find_nodes(port, ns_lv2core.symbol.me, None).get_first() - port_name = world.find_nodes(port, ns_lv2core.name .me, None).get_first() - - if None in (port_indx.me, port_name.me, port_symb.me): - if not errpr: - errors.append("modgui has some invalid port data") - errpr = True - continue - - port_indx = port_indx.as_int() - port_symb = port_symb.as_string() - port_name = port_name.as_string() - - ports.append({ - 'index' : port_indx, - 'symbol': port_symb, - 'name' : port_name, - }) - - if port_symb not in sybls: - sybls.append(port_symb) - elif not errpr: - errors.append("modgui has some duplicated port symbols") - errpr = True - - # sort ports - if len(ports) > 0: - ports2 = {} - - for port in ports: - ports2[port['index']] = port - gui['ports'] = [ports2[i] for i in ports2] - - del ports2 - - # cleanup - del ports, nodes, it - - # -------------------------------------------------------------------------------------------------------- - # ports - - index = 0 - ports = { - 'audio' : { 'input': [], 'output': [] }, - 'control': { 'input': [], 'output': [] }, - 'midi' : { 'input': [], 'output': [] } - } - - portsymbols = [] - portnames = [] - - # function for filling port info - def fill_port_info(port): - # base data - portname = lilv.lilv_node_as_string(port.get_name()) or "" - - if not portname: - portname = "_%i" % index - errors.append("port with index %i has no name" % index) - - portsymbol = lilv.lilv_node_as_string(port.get_symbol()) or "" - - if not portsymbol: - portsymbol = "_%i" % index - errors.append("port with index %i has no symbol" % index) - - # check for duplicate names - if portname in portsymbols: - warnings.append("port name '%s' is not unique" % portname) - else: - portnames.append(portname) - - # check for duplicate symbols - if portsymbol in portsymbols: - errors.append("port symbol '%s' is not unique" % portsymbol) - else: - portsymbols.append(portsymbol) - - # short name - psname = lilv.lilv_nodes_get_first(port.get_value(ns_lv2core.shortName.me)) - - if psname is not None: - psname = lilv.lilv_node_as_string(psname) or "" - - if not psname: - psname = get_short_port_name(portname) - if len(psname) > 16: - warnings.append("port '%s' name is too big, reduce the name size or provide a shortName" % portname) - - elif len(psname) > 16: - psname = psname[:16] - errors.append("port '%s' short name has more than 16 characters" % portname) - - # check for old style shortName - if port.get_value(ns_lv2core.shortname.me) is not None: - errors.append("port '%s' short name is using old style 'shortname' instead of 'shortName'" % portname) - - # port types - types = [typ.rsplit("#",1)[-1].replace("Port","",1) for typ in get_port_data(port, ns_rdf.type_)] - - if "Atom" in types \ - and port.supports_event(ns_midi.MidiEvent.me) \ - and lilv.Nodes(port.get_value(ns_atom.bufferType.me)).get_first() == ns_atom.Sequence: - types.append("MIDI") - - #if "Morph" in types: - #morphtyp = lilv.lilv_nodes_get_first(port.get_value(ns_morph.supportsType.me)) - #if morphtyp is not None: - #morphtyp = lilv.lilv_node_as_uri(morphtyp) - #if morphtyp: - #types.append(morphtyp.rsplit("#",1)[-1].replace("Port","",1)) - - # port comment - pcomment = (get_port_data(port, ns_rdfs.comment) or [""])[0] - - # port designation - designation = (get_port_data(port, ns_lv2core.designation) or [""])[0] - - # port rangeSteps - rangeSteps = (get_port_data(port, ns_mod.rangeSteps) or get_port_data(port, ns_pprops.rangeSteps) or [None])[0] - - # port properties - properties = [typ.rsplit("#",1)[-1] for typ in get_port_data(port, ns_lv2core.portProperty)] - - # data - ranges = {} - scalepoints = [] - - # unit block - ulabel = "" - urender = "" - usymbol = "" - - # control and cv must contain ranges, might contain scale points - if "Control" in types or "CV" in types: - isInteger = "integer" in properties - - if isInteger and "CV" in types: - errors.append("port '%s' has integer property and CV type" % portname) - - xdefault = lilv.lilv_nodes_get_first(port.get_value(ns_mod.default.me)) or \ - lilv.lilv_nodes_get_first(port.get_value(ns_lv2core.default.me)) - xminimum = lilv.lilv_nodes_get_first(port.get_value(ns_mod.minimum.me)) or \ - lilv.lilv_nodes_get_first(port.get_value(ns_lv2core.minimum.me)) - xmaximum = lilv.lilv_nodes_get_first(port.get_value(ns_mod.maximum.me)) or \ - lilv.lilv_nodes_get_first(port.get_value(ns_lv2core.maximum.me)) - - if xminimum is not None and xmaximum is not None: - if isInteger: - if is_integer(lilv.lilv_node_as_string(xminimum)): - ranges['minimum'] = lilv.lilv_node_as_int(xminimum) - else: - ranges['minimum'] = lilv.lilv_node_as_float(xminimum) - if fmod(ranges['minimum'], 1.0) == 0.0: - warnings.append("port '%s' has integer property but minimum value is float" % portname) - else: - errors.append("port '%s' has integer property but minimum value has non-zero decimals" % portname) - ranges['minimum'] = int(ranges['minimum']) - - if is_integer(lilv.lilv_node_as_string(xmaximum)): - ranges['maximum'] = lilv.lilv_node_as_int(xmaximum) - else: - ranges['maximum'] = lilv.lilv_node_as_float(xmaximum) - if fmod(ranges['maximum'], 1.0) == 0.0: - warnings.append("port '%s' has integer property but maximum value is float" % portname) - else: - errors.append("port '%s' has integer property but maximum value has non-zero decimals" % portname) - ranges['maximum'] = int(ranges['maximum']) - - else: - ranges['minimum'] = lilv.lilv_node_as_float(xminimum) - ranges['maximum'] = lilv.lilv_node_as_float(xmaximum) - - if is_integer(lilv.lilv_node_as_string(xminimum)): - warnings.append("port '%s' minimum value is an integer" % portname) - - if is_integer(lilv.lilv_node_as_string(xmaximum)): - warnings.append("port '%s' maximum value is an integer" % portname) - - if ranges['minimum'] >= ranges['maximum']: - ranges['maximum'] = ranges['minimum'] + (1 if isInteger else 0.1) - errors.append("port '%s' minimum value is equal or higher than its maximum" % portname) - - if xdefault is not None: - if isInteger: - if is_integer(lilv.lilv_node_as_string(xdefault)): - ranges['default'] = lilv.lilv_node_as_int(xdefault) - else: - ranges['default'] = lilv.lilv_node_as_float(xdefault) - if fmod(ranges['default'], 1.0) == 0.0: - warnings.append("port '%s' has integer property but default value is float" % portname) - else: - errors.append("port '%s' has integer property but default value has non-zero decimals" % portname) - ranges['default'] = int(ranges['default']) - else: - ranges['default'] = lilv.lilv_node_as_float(xdefault) - - if is_integer(lilv.lilv_node_as_string(xdefault)): - warnings.append("port '%s' default value is an integer" % portname) - - testmin = ranges['minimum'] - testmax = ranges['maximum'] - - if "sampleRate" in properties: - testmin *= 48000 - testmax *= 48000 - - if not (testmin <= ranges['default'] <= testmax): - ranges['default'] = ranges['minimum'] - errors.append("port '%s' default value is out of bounds" % portname) - - else: - ranges['default'] = ranges['minimum'] - - if "Input" in types: - errors.append("port '%s' is missing default value" % portname) - - else: - if isInteger: - ranges['minimum'] = 0 - ranges['maximum'] = 1 - ranges['default'] = 0 - else: - ranges['minimum'] = -1.0 if "CV" in types else 0.0 - ranges['maximum'] = 1.0 - ranges['default'] = 0.0 - - if "CV" not in types and designation != "http://lv2plug.in/ns/lv2core#latency": - errors.append("port '%s' is missing value ranges" % portname) - - nodes = port.get_scale_points() - - if nodes is not None: - scalepoints_unsorted = [] - - it = lilv.lilv_scale_points_begin(nodes) - while not lilv.lilv_scale_points_is_end(nodes, it): - sp = lilv.lilv_scale_points_get(nodes, it) - it = lilv.lilv_scale_points_next(nodes, it) - - if sp is None: - continue - - label = lilv.lilv_scale_point_get_label(sp) - value = lilv.lilv_scale_point_get_value(sp) - - if label is None: - errors.append("a port scalepoint is missing its label") - continue - - label = lilv.lilv_node_as_string(label) or "" - - if not label: - errors.append("a port scalepoint is missing its label") - continue - - if value is None: - errors.append("port scalepoint '%s' is missing its value" % label) - continue - - if isInteger: - if is_integer(lilv.lilv_node_as_string(value)): - value = lilv.lilv_node_as_int(value) - else: - value = lilv.lilv_node_as_float(value) - if fmod(value, 1.0) == 0.0: - warnings.append("port '%s' has integer property but scalepoint '%s' value is float" % (portname, label)) - else: - errors.append("port '%s' has integer property but scalepoint '%s' value has non-zero decimals" % (portname, label)) - value = int(value) - else: - if is_integer(lilv.lilv_node_as_string(value)): - warnings.append("port '%s' scalepoint '%s' value is an integer" % (portname, label)) - value = lilv.lilv_node_as_float(value) - - if ranges['minimum'] <= value <= ranges['maximum']: - scalepoints_unsorted.append((value, label)) - else: - errors.append(("port scalepoint '%s' has an out-of-bounds value:\n" % label) + - ("%d < %d < %d" if isInteger else "%f < %f < %f") % (ranges['minimum'], value, ranges['maximum'])) - - if len(scalepoints_unsorted) != 0: - unsorted = dict(s for s in scalepoints_unsorted) - values = list(v for v, l in scalepoints_unsorted) - values.sort() - scalepoints = list({ 'value': v, 'label': unsorted[v] } for v in values) - del unsorted, values - - del scalepoints_unsorted - - if "enumeration" in properties and len(scalepoints) <= 1: - errors.append("port '%s' wants to use enumeration but doesn't have enough values" % portname) - properties.remove("enumeration") - - # control ports might contain unit - if "Control" in types: - # unit - uunit = lilv.lilv_nodes_get_first(port.get_value(ns_units.unit.me)) - - if uunit is not None: - uuri = lilv.lilv_node_as_uri(uunit) - - # using pre-existing lv2 unit - if uuri is not None and uuri.startswith("http://lv2plug.in/ns/"): - uuri = uuri.replace("http://lv2plug.in/ns/extensions/units#","",1) - alnum = uuri.isalnum() - - if not alnum: - errors.append("port '%s' has wrong lv2 unit uri" % portname) - uuri = uuri.rsplit("#",1)[-1].rsplit("/",1)[-1] - - ulabel, urender, usymbol = get_port_unit(uuri) - - if alnum and not (ulabel and urender and usymbol): - errors.append("port '%s' has unknown lv2 unit (our bug?, data is '%s', '%s', '%s')" % (portname, - ulabel, - urender, - usymbol)) - - # using custom unit - else: - xlabel = world.find_nodes(uunit, ns_rdfs .label.me, None).get_first() - xrender = world.find_nodes(uunit, ns_units.render.me, None).get_first() - xsymbol = world.find_nodes(uunit, ns_units.symbol.me, None).get_first() - - if xlabel.me is not None: - ulabel = xlabel.as_string() - else: - errors.append("port '%s' has custom unit with no label" % portname) - - if xrender.me is not None: - urender = xrender.as_string() - else: - errors.append("port '%s' has custom unit with no render" % portname) - - if xsymbol.me is not None: - usymbol = xsymbol.as_string() - else: - errors.append("port '%s' has custom unit with no symbol" % portname) - - return (types, { - 'name' : portname, - 'symbol' : portsymbol, - 'ranges' : ranges, - 'units' : { - 'label' : ulabel, - 'render': urender, - 'symbol': usymbol, - } if "Control" in types and ulabel and urender and usymbol else {}, - 'comment' : pcomment, - 'designation': designation, - 'properties' : properties, - 'rangeSteps' : rangeSteps, - 'scalePoints': scalepoints, - 'shortName' : psname, - }) - - for p in (plugin.get_port_by_index(i) for i in range(plugin.get_num_ports())): - types, info = fill_port_info(p) - - info['index'] = index - index += 1 - - isInput = "Input" in types - types.remove("Input" if isInput else "Output") - - for typ in [typl.lower() for typl in types]: - if typ not in ports.keys(): - ports[typ] = { 'input': [], 'output': [] } - ports[typ]["input" if isInput else "output"].append(info) - - # -------------------------------------------------------------------------------------------------------- - # presets - - def get_preset_data(preset): - world.load_resource(preset.me) - - uri = preset.as_string() or "" - label = world.find_nodes(preset.me, ns_rdfs.label.me, None).get_first().as_string() or "" - - if not uri: - errors.append("preset with label '%s' has no uri" % (label or "")) - if not label: - errors.append("preset with uri '%s' has no label" % (uri or "")) - - return (uri, label) - - presets = [] - - presets_related = plugin.get_related(ns_pset.Preset) - presets_data = list(LILV_FOREACH(presets_related, get_preset_data)) - - if len(presets_data) != 0: - unsorted = dict(p for p in presets_data) - uris = list(unsorted.keys()) - uris.sort() - presets = list({ 'uri': p, 'label': unsorted[p] } for p in uris) - del unsorted, uris - - del presets_related - - # -------------------------------------------------------------------------------------------------------- - # done - - return { - 'uri' : uri, - 'name': name, - - 'binary' : binary, - 'brand' : brand, - 'label' : label, - 'license': license, - 'comment': comment, - - 'category' : get_category(plugin.get_value(ns_rdf.type_)), - 'microVersion': microVersion, - 'minorVersion': minorVersion, - - 'version' : version, - 'stability': stability, - - 'author' : author, - 'bundles': bundles, - 'gui' : gui, - 'ports' : ports, - 'presets': presets, - - 'errors' : errors, - 'warnings': warnings, - } - -# ------------------------------------------------------------------------------------------------------------ -# get_plugin_info_helper - -# Get info from a simple URI, without the need of your own lilv world -# This is used by get_plugins_info in MOD-SDK -def get_plugin_info_helper(uri): - world = lilv.World() - world.load_all() - plugins = world.get_all_plugins() - return [get_plugin_info(world, p, False) for p in plugins] - -# ------------------------------------------------------------------------------------------------------------ -# get_plugins_info - -# Get plugin-related info from a list of lv2 bundles -# @a bundles is a list of strings, consisting of directories in the filesystem (absolute pathnames). -def get_plugins_info(bundles): - # if empty, do nothing - if len(bundles) == 0: - raise Exception('get_plugins_info() - no bundles provided') - - # Create our own unique lilv world - # We'll load the selected bundles and get all plugins from it - world = lilv.World() - - # this is needed when loading specific bundles instead of load_all - # (these functions are not exposed via World yet) - lilv.lilv_world_load_specifications(world.me) - lilv.lilv_world_load_plugin_classes(world.me) - - # load all bundles - for bundle in bundles: - # lilv wants the last character as the separator - bundle = os.path.abspath(bundle) - if not bundle.endswith(os.sep): - bundle += os.sep - - # convert bundle string into a lilv node - bundlenode = lilv.lilv_new_file_uri(world.me, None, bundle) - - # load the bundle - world.load_bundle(bundlenode) - - # free bundlenode, no longer needed - lilv.lilv_node_free(bundlenode) - - # get all plugins available in the selected bundles - plugins = world.get_all_plugins() - - # make sure the bundles include something - if plugins.size() == 0: - raise Exception('get_plugins_info() - selected bundles have no plugins') - - # return all the info - return [get_plugin_info(world, p, False) for p in plugins] - -# ------------------------------------------------------------------------------------------------------------ - -if __name__ == '__main__': - from sys import argv, exit - from pprint import pprint - #get_plugins_info(argv[1:]) - #for i in get_plugins_info(argv[1:]): pprint(i) - #exit(0) - for i in get_plugins_info(argv[1:]): - warnings = i['warnings'].copy() - - if 'plugin brand is missing' in warnings: - i['warnings'].remove('plugin brand is missing') - - if 'plugin label is missing' in warnings: - i['warnings'].remove('plugin label is missing') - - if 'no modgui available' in warnings: - i['warnings'].remove('no modgui available') - - for warn in warnings: - if "has no short name" in warn: - i['warnings'].remove(warn) - - pprint({ - 'uri' : i['uri'], - 'errors' : i['errors'], - 'warnings': i['warnings'] - }, width=200) - -# ------------------------------------------------------------------------------------------------------------ diff --git a/pluginsmanager/model/lv2/lv2_effect_builder.py b/pluginsmanager/model/lv2/lv2_effect_builder.py index 4dd16be..2f1769b 100644 --- a/pluginsmanager/model/lv2/lv2_effect_builder.py +++ b/pluginsmanager/model/lv2/lv2_effect_builder.py @@ -16,8 +16,9 @@ import json import subprocess -from pluginsmanager.model.lv2.lv2_plugin import Lv2Plugin from pluginsmanager.model.lv2.lv2_effect import Lv2Effect +from pluginsmanager.model.lv2.lv2_plugin import Lv2Plugin +from pluginsmanager.model.lv2.lv2_plugin_info import get_plugins_info class Lv2EffectBuilderError(Exception): @@ -154,13 +155,18 @@ def lv2_plugins_data(self): :return list: lv2 audio plugins metadata """ - import lilvlib + return get_plugins_info() - return lilvlib.get_plugin_info_helper('') if __name__ == '__main__': + import json + builder = Lv2EffectBuilder() print('Total plugins before reload:', len(builder.plugins)) - builder.reload(builder.lv2_plugins_data()) + plugins_data = builder.lv2_plugins_data() + builder.reload(plugins_data) print('Total plugins after reload:', len(builder.plugins)) + + with open(os.path.join(os.path.dirname(__file__), 'plugins.json'), 'w') as outfile: + json.dump(plugins_data, outfile, sort_keys=True) diff --git a/pluginsmanager/model/lv2/lv2_plugin_info.py b/pluginsmanager/model/lv2/lv2_plugin_info.py new file mode 100644 index 0000000..c442bef --- /dev/null +++ b/pluginsmanager/model/lv2/lv2_plugin_info.py @@ -0,0 +1,659 @@ +#!/usr/bin/env python +"""Generate JSON document with information about a single or all installed LV2 plugins.""" + +import os +from math import fmod +from os.path import dirname + +import lilv + + +NS_PRESET = 'http://lv2plug.in/ns/ext/presets#' +NS_MOD = "http://moddevices.com/ns/mod#" +NS_PORT_PROPERTIES = "http://lv2plug.in/ns/ext/port-props#" +NS_UNITS = "http://lv2plug.in/ns/extensions/units#" + +LV2_CATEGORIES = { + 'AllpassPlugin': ('Filter', 'Allpass'), + 'AmplifierPlugin': ('Dynamics', 'Amplifier'), + 'AnalyserPlugin': ('Utility', 'Analyser'), + 'BandpassPlugin': ('Filter', 'Bandpass'), + 'ChorusPlugin': ('Modulator', 'Chorus'), + 'CombPlugin': ('Filter', 'Comb'), + 'CompressorPlugin': ('Dynamics', 'Compressor'), + 'ConstantPlugin': ('Generator', 'Constant'), + 'ConverterPlugin': ('Utility', 'Converter'), + 'DelayPlugin': ('Delay',), + 'DistortionPlugin': ('Distortion',), + 'DynamicsPlugin': ('Dynamics',), + 'EQPlugin': ('Filter', 'Equaliser'), + 'ExpanderPlugin': ('Dynamics', 'Expander'), + 'FilterPlugin': ('Filter',), + 'FlangerPlugin': ('Modulator', 'Flanger'), + 'FunctionPlugin': ('Utility', 'Function'), + 'GatePlugin': ('Dynamics', 'Gate'), + 'GeneratorPlugin': ('Generator',), + 'HighpassPlugin': ('Filter', 'Highpass'), + 'InstrumentPlugin': ('Generator', 'Instrument'), + 'LimiterPlugin': ('Dynamics', 'Limiter'), + 'LowpassPlugin': ('Filter', 'Lowpass'), + 'MIDIPlugin': ('MIDI', 'Utility'), + 'MixerPlugin': ('Utility', 'Mixer'), + 'ModulatorPlugin': ('Modulator',), + 'MultiEQPlugin': ('Filter', 'Equaliser', 'Multiband'), + 'OscillatorPlugin': ('Generator', 'Oscillator'), + 'ParaEQPlugin': ('Filter', 'Equaliser', 'Parametric'), + 'PhaserPlugin': ('Modulator', 'Phaser'), + 'PitchPlugin': ('Spectral', 'Pitch Shifter'), + 'ReverbPlugin': ('Reverb',), + 'SimulatorPlugin': ('Simulator',), + 'SpatialPlugin': ('Spatial',), + 'SpectralPlugin': ('Spectral',), + 'UtilityPlugin': ('Utility',), + 'WaveshaperPlugin': ('Distortion', 'Waveshaper'), +} + +LV2_UNITS = units = { + 'bar': ("bars", "%f bars", "bars"), + 'beat': ("beats", "%f beats", "beats"), + 'bpm': ("beats per minute", "%f BPM", "BPM"), + 'cent': ("cents", "%f ct", "ct"), + 'cm': ("centimetres", "%f cm", "cm"), + 'coef': ("coefficient", "* %f", "*"), + 'db': ("decibels", "%f dB", "dB"), + 'degree': ("degrees", "%f deg", "deg"), + 'frame': ("audio frames", "%f frames", "frames"), + 'hz': ("hertz", "%f Hz", "Hz"), + 'inch': ("inches", """%f\"""", "in"), + 'khz': ("kilohertz", "%f kHz", "kHz"), + 'km': ("kilometres", "%f km", "km"), + 'mhz': ("megahertz", "%f MHz", "MHz"), + 'midiNote': ("MIDI note", "MIDI note %d", "note"), + 'mile': ("miles", "%f mi", "mi"), + 'min': ("minutes", "%f mins", "min"), + 'm': ("metres", "%f m", "m"), + 'mm': ("millimetres", "%f mm", "mm"), + 'ms': ("milliseconds", "%f ms", "ms"), + 'oct': ("octaves", "%f octaves", "oct"), + 'pc': ("percent", "%f%%", "%"), + 'semitone12TET': ("semitones", "%f semi", "semi"), + 's': ("seconds", "%f s", "s"), +} + + +def node2str(node, strip=True): + """Return lilv.Node to string. + + By default, strips whitespace surrounding string value. + + If passed node is None, return None. + + """ + if node is not None: + node = str(node) + + if strip: + node = node.strip() + + return node + + +def getfirst(obj, uri, strip=True): + """Return string value of first item returned by obj.get_value(uri). + + By default, strips whitespace surrounding string value. + + If colection is empty, returns None + + """ + data = obj.get_value(uri) + + if data: + data = str(data[0]) + + if strip: + data = data.strip() + + return data + else: + return None + + +def _get_port_info(ctx, port): + world = ctx.world + warnings = ctx.warnings + errors = ctx.errors + portnames = ctx.portnames + portsymbols = ctx.portsymbols + + # base data + portname = port.get_name() + + if portname is None: + portname = "_%i" % index + errors.append("port with index %i has no name" % index) + else: + portname = str(portname) + + portsymbol = port.get_symbol() + + if portsymbol is None: + portsymbol = "_%i" % index + errors.append("port with index %i has no symbol" % index) + else: + portsymbol = str(portsymbol) + + # check for duplicate names + if portname in portsymbols: + warnings.append("port name '%s' is not unique" % portname) + else: + portnames.add(portname) + + # check for duplicate symbols + if portsymbol in portsymbols: + errors.append("port symbol '%s' is not unique" % portsymbol) + else: + portsymbols.add(portsymbol) + + # short name + psname = getfirst(port, world.ns.lv2.shortName) + + if psname is None: + psname = portname[:16] + elif len(psname) > 16: + errors.append("port '%s' short name has more than 16 characters" % portname) + + # check for old style shortName + if port.get_value(world.ns.lv2.shortname): + errors.append("port '%s' short name is using old style 'shortname' instead of 'shortName'" % portname) + + # port types + types = [str(t).rsplit("#", 1)[-1][:-4] for t in port.get_value(world.ns.rdf.type)] + buffer_type = port.get_value(world.ns.atom.bufferType) + + if ("Atom" in types and port.supports_event(world.ns.midi.MidiEvent) and buffer_type + and str(buffer_type[0]) == world.ns.atom.Sequence): + types.append("MIDI") + + # port comment + pcomment = getfirst(port, world.ns.rdfs.comment) + + # port designation + designation = getfirst(port, world.ns.lv2.designation) + + # port rangeSteps + rangesteps = getfirst(port, world.ns.mod.rangeSteps) or getfirst(port, world.ns.pprops.rangeSteps) + + # port properties + properties = sorted([str(t).rsplit("#", 1)[-1] for t in port.get_value(world.ns.lv2.portProperty)]) + + # data + ranges = {} + scalepoints = [] + + # control and cv must contain ranges, might contain scale points + if "Control" in types or "CV" in types: + is_int = "integer" in properties + + if is_int and "CV" in types: + errors.append("port '%s' has integer property and CV type" % portname) + + xdefault, xminimum, xmaximum = port.get_range() + + if xminimum is not None and xmaximum is not None: + if is_int: + if xminimum.is_int(): + ranges['minimum'] = int(xminimum) + else: + ranges['minimum'] = float(xminimum) + if fmod(ranges['minimum'], 1.0) == 0.0: + warnings.append("port '%s' has integer property but minimum value is float" % portname) + else: + errors.append("port '%s' has integer property but minimum value has non-zero decimals" % portname) + + ranges['minimum'] = int(ranges['minimum']) + + if xmaximum.is_int(): + ranges['maximum'] = int(xmaximum) + else: + ranges['maximum'] = float(xmaximum) + if fmod(ranges['maximum'], 1.0) == 0.0: + warnings.append("port '%s' has integer property but maximum value is float" % portname) + else: + errors.append("port '%s' has integer property but maximum value has non-zero decimals" % portname) + + ranges['maximum'] = int(ranges['maximum']) + + else: + if xminimum.is_int(): + warnings.append("port '%s' minimum value is an integer" % portname) + ranges['minimum'] = int(xminimum) * 1.0 + else: + ranges['minimum'] = float(xminimum) + + if xmaximum.is_int(): + warnings.append("port '%s' maximum value is an integer" % portname) + ranges['maximum'] = int(xmaximum) * 1.0 + else: + ranges['maximum'] = float(xmaximum) + + if ranges['minimum'] >= ranges['maximum']: + ranges['maximum'] = ranges['minimum'] + (1 if is_int else 0.1) + errors.append("port '%s' minimum value is equal or higher than its maximum" % portname) + + if xdefault is not None: + if is_int: + if xdefault.is_int(): + ranges['default'] = int(xdefault) + else: + ranges['default'] = float(xdefault) + if fmod(ranges['default'], 1.0) == 0.0: + warnings.append("port '%s' has integer property but default value is float" % portname) + else: + errors.append("port '%s' has integer property but default value has non-zero decimals" % portname) + ranges['default'] = int(ranges['default']) + else: + if xdefault.is_int(): + warnings.append("port '%s' default value is an integer" % portname) + ranges['default'] = int(xdefault) * 1.0 + else: + ranges['default'] = float(xdefault) + + testmin = ranges['minimum'] + testmax = ranges['maximum'] + + if "sampleRate" in properties: + testmin *= 48000 + testmax *= 48000 + + if not (testmin <= ranges['default'] <= testmax): + ranges['default'] = ranges['minimum'] + errors.append("port '%s' default value is out of bounds" % portname) + + else: + ranges['default'] = ranges['minimum'] + + if "Input" in types: + errors.append("port '%s' is missing default value" % portname) + + else: + if is_int: + ranges['minimum'] = 0 + ranges['maximum'] = 1 + ranges['default'] = 0 + else: + ranges['minimum'] = -1.0 if "CV" in types else 0.0 + ranges['maximum'] = 1.0 + ranges['default'] = 0.0 + + if "CV" not in types and designation != str(world.ns.lv2.latency): + errors.append("port '%s' is missing value ranges" % portname) + + scale_points = port.get_scale_points() + + if scale_points is not None: + scalepoints_unsorted = [] + + for sp in scale_points: + label = node2str(sp.get_label()) + value = sp.get_value() + + if label is None: + errors.append("a port scalepoint is missing its label") + continue + + if value is None: + errors.append("port scalepoint '%s' is missing its value" % label) + continue + + if is_int: + if value.is_int(): + value = int(value) + else: + value = float(value) + if fmod(value, 1.0) == 0.0: + warnings.append("port '%s' has integer property but scalepoint '%s' value is float" % (portname, label)) + else: + errors.append("port '%s' has integer property but scalepoint '%s' value has non-zero decimals" % (portname, label)) + value = int(value) + else: + if value.is_int(): + warnings.append("port '%s' scalepoint '%s' value is an integer" % (portname, label)) + value = int(value) * 1.0 + else: + value = float(value) + + if ranges['minimum'] <= value <= ranges['maximum']: + scalepoints_unsorted.append((value, label)) + else: + errors.append(("port scalepoint '%s' has an out-of-bounds value:\n" % label) + + ("%d < %d < %d" if is_int else "%f < %f < %f") % (ranges['minimum'], value, ranges['maximum'])) + + if scalepoints_unsorted: + unsorted = dict(s for s in scalepoints_unsorted) + values = list(s[0] for s in scalepoints_unsorted) + values.sort() + scalepoints = list({'value': v, 'label': unsorted[v]} for v in values) + + if "enumeration" in properties and len(scalepoints) <= 1: + errors.append("port '%s' wants to use enumeration but doesn't have enough values" % portname) + properties.remove("enumeration") + + # control ports might contain unit + units = {} + if "Control" in types: + # unit + uunit = port.get_value(world.ns.units.unit) + ulabel = urender = usymbol = None + + if uunit: + uuri = str(uunit[0]) + + # using pre-existing lv2 unit + if uuri.startswith(str(world.ns.units)): + uuri = uuri.rsplit('#', 1)[-1] + + if uuri not in LV2_UNITS: + errors.append("port '%s' has invalid lv2 unit '%s'" % (portname, uuri)) + else: + ulabel, urender, usymbol = LV2_UNITS[uuri] + + # using custom unit + else: + xlabel = world.find_nodes(uunit[0], world.ns.rdfs.label, None) + xrender = world.find_nodes(uunit[0], world.ns.units.render, None) + xsymbol = world.find_nodes(uunit[0], world.ns.units.symbol, None) + + if xlabel: + ulabel = str(xlabel[0]) + else: + errors.append("port '%s' has custom unit with no label" % portname) + + if xrender: + urender = str(xrender[0]) + else: + errors.append("port '%s' has custom unit with no render" % portname) + + if xsymbol: + usymbol = str(xsymbol[0]) + else: + errors.append("port '%s' has custom unit with no symbol" % portname) + + if ulabel and urender and usymbol: + units = { + 'label': ulabel, + 'render': urender, + 'symbol': usymbol, + } + + return (types, { + 'name': portname, + 'symbol': portsymbol, + 'ranges': ranges, + 'units': units, + 'comment': pcomment, + 'designation': designation, + 'properties': properties, + 'rangeSteps': rangesteps, + 'scalePoints': scalepoints, + 'shortName': psname, + }) + + +def _get_plugin_ports(ctx, plugin): + index = 0 + ports = { + 'audio': { + 'input': [], + 'output': [] + }, + 'control': { + 'input': [], + 'output': [] + }, + 'midi': { + 'input': [], + 'output': [] + } + } + + ctx.portsymbols = set() + ctx.portnames = set() + + for i in range(plugin.get_num_ports()): + port = plugin.get_port_by_index(i) + types, info = _get_port_info(ctx, port) + info['index'] = i + + isInput = "Input" in types + types.remove("Input" if isInput else "Output") + + for typ in [typl.lower() for typl in types]: + if typ not in ports.keys(): + ports[typ] = { 'input': [], 'output': [] } + ports[typ]["input" if isInput else "output"].append(info) + + return ports + + +def _get_plugin_presets(ctx, plugin): + world = ctx.world + presets = plugin.get_related(world.ns.presets.Preset) + preset_list = [] + + for preset in presets: + world.load_resource(preset) + labels = world.find_nodes(preset, world.ns.rdfs.label, None) + + if labels: + label = str(labels[0]) + else: + label = None + ctx.errors.append("Preset '%s' has no rdfs:label" % preset) + + preset_list.append({'label': label, 'uri': str(preset)}) + + return sorted(preset_list, key=lambda x: x['label'] or '') + + +def _get_plugin_info(ctx, plugin): + world = ctx.world + world.ns.mod = lilv.Namespace(world, NS_MOD) + world.ns.pprops = lilv.Namespace(world, NS_PORT_PROPERTIES) + world.ns.units = lilv.Namespace(world, NS_UNITS) + world.ns.presets = lilv.Namespace(world, NS_PRESET) + + ctx.errors = errors = [] + ctx.warnings = warnings = [] + + # uri + uri = plugin.get_uri() + + if uri is None: + errors.append("plugin uri is missing or invalid") + elif str(uri).startswith("file:"): + errors.append("plugin uri is local, and thus not suitable for redistribution") + + # name + name = plugin.get_name() + + if name is None: + errors.append("plugin name is missing") + + # label + label = getfirst(plugin, world.ns.mod.label) + + if label is None: + warnings.append("plugin label is missing") + if name is not None: + label = str(name)[:16] + elif len(label) > 16: + warnings.append("plugin label has more than 16 characters") + + # author + author_name = plugin.get_author_name() + author_email = plugin.get_author_email() + author_homepage = plugin.get_author_homepage() + + # binary + binary = plugin.get_library_uri() + + if binary is None: + errors.append("plugin binary is missing") + else: + binary = binary.get_path() + + # brand + brand = getfirst(plugin, world.ns.mod.brand) + + if brand is None: + warnings.append("plugin brand is missing") + elif len(brand) > 16: + warnings.append("plugin brand has more than 11 characters") + + # license + license = getfirst(plugin, world.ns.doap.license) + + if license is None: + errors.append("plugin license is missing") + + # comment + comment = getfirst(plugin, world.ns.rdfs.comment) + + if comment is None: + errors.append("plugin comment is missing") + + # version + microver = plugin.get_value(world.ns.lv2.microVersion) + minorver = plugin.get_value(world.ns.lv2.minorVersion) + + if not microver and not minorver: + errors.append("plugin is missing version information") + minor_version = 0 + micro_version = 0 + else: + if not minorver: + errors.append("plugin is missing minorVersion") + minor_version = 0 + else: + minor_version = int(minorver[0]) + + if not microver: + errors.append("plugin is missing microVersion") + micro_version = 0 + else: + micro_version = int(microver[0]) + + version = "%d.%d" % (minor_version, micro_version) + + if minor_version == 0: + # 0.x is experimental + stability = "experimental" + elif minor_version % 2 != 0 or micro_version % 2 != 0: + # odd x.2 or 2.x is testing/development + stability = "testing" + else: + # otherwise it's stable + stability = "stable" + + # category + categories = plugin.get_value(world.ns.rdf.type) + category = set() + + if categories: + for node in categories: + category.update(LV2_CATEGORIES.get(str(node).split('#', 1)[-1], [])) + + # bundles + bundle = plugin.get_bundle_uri() + bundlepath = bundle.get_path().rstrip(os.sep) + bundles = plugin.get_data_uris() + + if bundles: + bundles = {dirname(node.get_path().rstrip(os.sep)) for node in bundles} + bundles.add(bundlepath) + bundles = list(bundles) + else: + bundles = [bundlepath] + + # ports + ports = _get_plugin_ports(ctx, plugin) + + # presets + presets = _get_plugin_presets(ctx, plugin) + + return { + 'uri': node2str(uri), + 'name': node2str(name), + 'binary': binary, + 'brand': brand, + 'label': label, + 'license': license, + 'comment': comment, + 'category': list(category), + 'microVersion': micro_version, + 'minorVersion': minor_version, + 'version': version, + 'stability': stability, + 'author': { + 'name': node2str(author_name), + 'email': node2str(author_email), + 'homepage': node2str(author_homepage), + }, + 'bundles': sorted(bundles), + #'ui': ui, + 'ports': ports, + 'presets': presets, + 'errors': sorted(errors), + 'warnings': sorted(warnings), + } + + +def get_plugins_info(uri=None): + class _context(): + pass + + ctx = _context() + ctx.world = lilv.World() + ctx.world.load_all() + plugins = ctx.world.get_all_plugins() + + if uri: + uri = ctx.world.new_uri(uri) + return _get_plugin_info(ctx, plugins[uri]) + else: + return [_get_plugin_info(ctx, p) for p in plugins] + + +if __name__ == '__main__': + import argparse + import pprint + import json + import sys + + ap = argparse.ArgumentParser() + ap.add_argument( + '-d', '--debug', + action="store_true", + help="Print debugging information to standard error") + ap.add_argument( + '-p', '--pretty-format', + action="store_true", + help="Pretty format JSON output") + ap.add_argument( + 'plugin_uri', + nargs='?', + metavar='URI', help='Plugin URI') + + args = ap.parse_args() + plugin_data = get_plugins_info(args.plugin_uri) + + if args.debug: + print(pprint.pformat(plugin_data), file=sys.stderr) + + if args.pretty_format: + kw = {'indent': 4} + else: + kw = {} + + print(json.dumps(plugin_data, sort_keys=True, **kw)) From bccd5b7c31779f33505b43bfbb9f2e700c083a04 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Tue, 5 Nov 2019 22:34:42 +0100 Subject: [PATCH 02/24] Make dependency on pyaudio optional Signed-off-by: Christopher Arndt --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b491390..83ce754 100644 --- a/setup.py +++ b/setup.py @@ -60,9 +60,12 @@ def readme(): }, install_requires=[ 'JACK-Client', - 'pyaudio' ], + extras_require={ + 'pyaudio': ['pyaudio',], + }, + test_suite='test', tests_require=['JACK-Client', 'pytest', 'pytest-cov'], From 3cf8a8d0771e92cfa882980f1b38e1dc3d1bb759 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Tue, 5 Nov 2019 22:35:12 +0100 Subject: [PATCH 03/24] Update classifiers in setup.py for current Python versions Signed-off-by: Christopher Arndt --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 83ce754..bd813b9 100644 --- a/setup.py +++ b/setup.py @@ -74,9 +74,11 @@ def readme(): 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Topic :: Multimedia :: Sound/Audio', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3 : Only', 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6' + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], keywords='pedal-pi mod-host lv2 audio plugins-manager carla', From b3006696b8d09257d76ac548826a7566f31558a4 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 7 Nov 2019 17:17:35 +0100 Subject: [PATCH 04/24] Variable renaming Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_plugin_info.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_plugin_info.py b/pluginsmanager/model/lv2/lv2_plugin_info.py index c442bef..4260c24 100644 --- a/pluginsmanager/model/lv2/lv2_plugin_info.py +++ b/pluginsmanager/model/lv2/lv2_plugin_info.py @@ -103,7 +103,7 @@ def getfirst(obj, uri, strip=True): By default, strips whitespace surrounding string value. - If colection is empty, returns None + If collection is empty, returns None """ data = obj.get_value(uri) @@ -289,12 +289,12 @@ def _get_port_info(ctx, port): if "CV" not in types and designation != str(world.ns.lv2.latency): errors.append("port '%s' is missing value ranges" % portname) - scale_points = port.get_scale_points() + scalepoints = port.get_scale_points() - if scale_points is not None: + if scalepoints is not None: scalepoints_unsorted = [] - for sp in scale_points: + for sp in scalepoints: label = node2str(sp.get_label()) value = sp.get_value() @@ -425,13 +425,13 @@ def _get_plugin_ports(ctx, plugin): types, info = _get_port_info(ctx, port) info['index'] = i - isInput = "Input" in types - types.remove("Input" if isInput else "Output") + is_input = "Input" in types + types.remove("Input" if is_input else "Output") for typ in [typl.lower() for typl in types]: if typ not in ports.keys(): ports[typ] = { 'input': [], 'output': [] } - ports[typ]["input" if isInput else "output"].append(info) + ports[typ]["input" if is_input else "output"].append(info) return ports From b4caa610aa34831da0a72e5abc3a420a9ce1aa55 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Fri, 8 Nov 2019 18:53:02 +0100 Subject: [PATCH 05/24] Add 'pyaudio' to tests_require Signed-off-by: Christopher Arndt --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bd813b9..3b9a983 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def readme(): }, test_suite='test', - tests_require=['JACK-Client', 'pytest', 'pytest-cov'], + tests_require=['JACK-Client', 'pyaudio', 'pytest', 'pytest-cov'], classifiers=[ 'Development Status :: 4 - Beta', From 73b0ae1002d436ce4019b36b172e5a7d1fb131a9 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Fri, 8 Nov 2019 20:36:11 +0100 Subject: [PATCH 06/24] General improvements of `lv2_effect_builder` * Renamed ingnore_unsupported_plugins` argument to `allow_uninstalled_plugins`. * Discover installed plugins with lilv instead of calling `lv2ls` and cache `lilv.World` and resulting list of plugin URIs. * Allow to pass file name to save metadata JSON to when called as a script. * Improved docstrings. * Added logging. Signed-off-by: Christopher Arndt --- .../model/lv2/lv2_effect_builder.py | 152 ++++++++++-------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_effect_builder.py b/pluginsmanager/model/lv2/lv2_effect_builder.py index 2f1769b..c262024 100644 --- a/pluginsmanager/model/lv2/lv2_effect_builder.py +++ b/pluginsmanager/model/lv2/lv2_effect_builder.py @@ -12,39 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +import logging import json -import subprocess +from os.path import abspath, dirname, join + +import lilv from pluginsmanager.model.lv2.lv2_effect import Lv2Effect from pluginsmanager.model.lv2.lv2_plugin import Lv2Plugin from pluginsmanager.model.lv2.lv2_plugin_info import get_plugins_info +log = logging.getLogger(__name__) + + class Lv2EffectBuilderError(Exception): pass class Lv2EffectBuilder(object): - """ - Generates lv2 audio plugins instance (as :class:`.Lv2Effect` object). + """Generate LV2 plugin instances (as :class:`.Lv2Effect` object). .. note:: - In the current implementation, the data plugins are persisted - in *plugins.json*. + In the current implementation, the plugins metatdata is cached + in the file *plugins.json* inside the package + :mod:`pluginsmanager.model.lv2`. :param Path plugins_json: Plugins json path file - :param bool ignore_unsupported_plugins: Not allows instantiation of uninstalled or unrecognized audio plugins? + :param bool allow_uninstalled_plugins: allow instantiation of uninstalled + or unrecognized audio plugins? + """ - plugins_json_file = os.path.dirname(os.path.abspath(__file__)) + '/plugins.json' + plugins_json_file = join(dirname(abspath(__file__)), 'plugins.json') """ - Informs the path of the `plugins.json` file. This file contains the lv2 plugins metadata info + Stores the path of the `plugins.json` file. This file contains the LV2 plugins metadata. """ - def __init__(self, plugins_json=None, ignore_unsupported_plugins=True): + def __init__(self, plugins_json=None, allow_uninstalled_plugins=False): self._plugins = {} + self._lilv_world = None + self._installed_plugins = None if plugins_json is None: plugins_json = Lv2EffectBuilder.plugins_json_file @@ -52,25 +61,40 @@ def __init__(self, plugins_json=None, ignore_unsupported_plugins=True): with open(str(plugins_json)) as data_file: data = json.load(data_file) - self.reload(data, ignore_unsupported_plugins=ignore_unsupported_plugins) + self.reload(data, allow_uninstalled_plugins=allow_uninstalled_plugins) - def reload(self, metadata, ignore_unsupported_plugins=True): - """ - Loads the metadata. They will be used so that it is possible to generate lv2 audio plugins. + def reload(self, metadata, allow_uninstalled_plugins=False): + """Load data of all LV2 plugins listed in given metadata. + + :param list metadata: LV2 plugins metadata + :param bool allow_uninstalled_plugins: allow instantiation of + uninstalled or unrecognized audio plugins? - :param list metadata: lv2 audio plugins metadata - :param bool ignore_unsupported_plugins: Not allows instantiation of uninstalled or unrecognized audio plugins? """ - supported_plugins = self._supported_plugins + installed_plugins = set(self.installed_plugins) + log.debug("Installed plugins: %r", installed_plugins) for plugin in metadata: - if not ignore_unsupported_plugins \ - or plugin['uri'] in supported_plugins: - self._plugins[plugin['uri']] = Lv2Plugin(plugin) + uri = plugin['uri'] + msg = "Checking for '%s'... %s" + if allow_uninstalled_plugins or uri in installed_plugins: + self._plugins[uri] = Lv2Plugin(plugin) + log.info(msg, uri, "found") + else: + log.info(msg, uri, "NOT found") @property - def _supported_plugins(self): - return str(subprocess.check_output(['lv2ls'])).split('\\n') + def installed_plugins(self): + if self._lilv_world is None: + self._lilv_world = lilv.World() + + if self._installed_plugins is None: + log.info("Discovering installed plugins...") + self._lilv_world.load_all() + self._installed_plugins = [str(p.get_uri()) + for p in self._lilv_world.get_all_plugins()] + + return self._installed_plugins @property def all(self): @@ -81,92 +105,88 @@ def plugins(self): return self._plugins.keys() def build(self, lv2_uri): - """ - Returns a new :class:`.Lv2Effect` by the valid lv2_uri + """Return a new :class:`.Lv2Effect` instance given by lv2_uri. :param string lv2_uri: :return Lv2Effect: Effect created + :raises Lv2EffectBuilderError: plugin with given lv2_uri was not found + """ try: plugin = self._plugins[lv2_uri] except KeyError: raise Lv2EffectBuilderError( - "Lv2EffectBuilder not contains metadata information about the plugin '{}'. \n" - "Try re-scan the installed plugins using the reload method::\n" - " >>> lv2_effect_builder.reload(lv2_effect_builder.lv2_plugins_data())".format(lv2_uri)) + "Lv2EffectBuilder does not have metadata information about the plugin '{}'.\n" + "Try re-scanning the installed plugins using the reload method:\n\n" + " >>> lv2_effect_builder.reload(lv2_effect_builder.lv2_plugins_data())" + .format(lv2_uri)) return Lv2Effect(plugin) def lv2_plugins_data(self): - """ - Generates a file with all plugins data info. It uses the `lilvlib`_ library. + """Generate metadata with information about all installed LV2 plugins. - PluginsManager can manage lv2 audio plugins through previously obtained metadata - from the lv2 audio plugins descriptor files. + PluginsManager can only use LV2 plugins it knows about, i.e. those, about which it has + stored metadata, which was generated from the LV2 TTL descriptor files of the installed + plugins. - To speed up usage, data has been pre-generated and loaded into this piped packet. - This avoids a dependency installation in order to obtain the metadata. + To speed up usage, the metadata is pre-generated and stored as JSON data in a file in + the package. The cached data is loaded from this file into this class on instantiation. + This reduces the loading time considerably, especially if there are many (i.e. hundreds of) + LV2 plugins installed. - However, this measure makes it not possible to manage audio plugins that were not - included in the list. + However, this makes it impossible to use plugins that were not installed when the meta + data cache file was generated. - To work around this problem, this method - using the `lilvlib`_ library - can get - the information from the audio plugins. You can use this data to generate a file - containing the settings:: + So in order to use all installed plugins, you have to generate a new plugin metadata cache + file with:: + >>> import json >>> builder = Lv2EffectBuilder() >>> plugins_data = builder.lv2_plugins_data() - - >>> import json >>> with open('plugins.json', 'w') as outfile: >>> json.dump(plugins_data, outfile) - The next time you instantiate this class, you can pass the configuration file:: + The next time you instantiate this class, you can pass the path of the cache file like + this:: - >>> builder = Lv2EffectBuilder(os.path.abspath('plugins.json')) + >>> builder = Lv2EffectBuilder('plugins.json') - Or, if you want to load the data without having to create a new instance of this class:: + Or, to generate the data on-the-fly and load it into an existing instance of this class + (this may take a while), do this:: >>> builder.reload(builder.lv2_plugins_data()) .. warning:: - To use this method, it is necessary that the system has the `lilv`_ in a version equal - to or greater than `0.22.0`_. Many linux systems currently have previous versions on - their package lists, so you need to compile them manually. - - In order to ease the work, Pedal Pi has compiled lilv for some versions of linux. - You can get the list of .deb packages in https://github.com/PedalPi/lilvlib/releases. - - .. code-block:: bash - - # Example - wget https://github.com/PedalPi/lilvlib/releases/download/v1.0.0/python3-lilv_0.22.1.git20160613_amd64.deb - sudo dpkg -i python3-lilv_0.22.1+git20160613_amd64.deb + To query the plugin metadata, the Python bindings for the `lilv`_ C library are used. + This requires lilv version `>= 0.24.0`. - - If the architecture of your computer is not contemplated, moddevices provided - a script to generate the package. - Go to https://github.com/moddevices/lilvlib to get the script in its most up-to-date version. - - .. _lilvlib: https://github.com/moddevices/lilvlib - .. _0.22.0: http://git.drobilla.net/cgit.cgi/lilv.git/tag/?id=v0.22.0 .. _lilv: http://drobilla.net/software/lilv :return list: lv2 audio plugins metadata + """ + log.info("Scanning metadata of installed plugins...") return get_plugins_info() if __name__ == '__main__': - import json + import sys + + logging.basicConfig(level=logging.INFO, format="%(message)s") + + if len(sys.argv) >= 2: + cache_path = sys.argv[1] + else: + cache_path = join(abspath(dirname(__file__)), 'plugins.json') builder = Lv2EffectBuilder() - print('Total plugins before reload:', len(builder.plugins)) + log.info('Total plugins before re-scan: %i', len(builder.plugins)) plugins_data = builder.lv2_plugins_data() builder.reload(plugins_data) - print('Total plugins after reload:', len(builder.plugins)) + log.info('Total plugins after re-scan: %i', len(builder.plugins)) - with open(os.path.join(os.path.dirname(__file__), 'plugins.json'), 'w') as outfile: + with open(cache_path, 'w') as outfile: json.dump(plugins_data, outfile, sort_keys=True) From 3fe7c08bf4577006a69596b5e5acb2c584b8f78d Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 9 Nov 2019 01:52:47 +0100 Subject: [PATCH 07/24] Keep order of init arguments the same as base class Signed-off-by: Christopher Arndt --- pluginsmanager/jack/jack_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pluginsmanager/jack/jack_client.py b/pluginsmanager/jack/jack_client.py index 8a06b75..34be0eb 100644 --- a/pluginsmanager/jack/jack_client.py +++ b/pluginsmanager/jack/jack_client.py @@ -31,11 +31,12 @@ class JackClient(object): >>> client.close() - :param bool no_start_server: False if starts a new JACK server - True if uses a already started jack (ex: using `jackdump`) :param name: Jack client name. Default: `JackClient` + :param bool no_start_server: False if starts a new JACK server + True if uses a already started jack (ex: using `jackdbus`) + """ - def __init__(self, no_start_server=True, name=None): + def __init__(self, name=None, no_start_server=True): if name is None: name = self.__class__.__name__ From 1d245fbe819156610dad080724b04b0607f271f1 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 9 Nov 2019 01:55:01 +0100 Subject: [PATCH 08/24] Delegate LV2Effect item access to LV2Plugin instance Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_effect.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pluginsmanager/model/lv2/lv2_effect.py b/pluginsmanager/model/lv2/lv2_effect.py index 9afaf04..df329eb 100644 --- a/pluginsmanager/model/lv2/lv2_effect.py +++ b/pluginsmanager/model/lv2/lv2_effect.py @@ -71,6 +71,13 @@ def __repr__(self): id(self) ) + def __getitem__(self, key): + """ + :param string key: Property key + :return: Returns a Plugin property + """ + return self.plugin[key] + @property def __dict__(self): return { From 8fbc54e1524c0684e3829a08a41f3d8bf3c8e101 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 9 Nov 2019 01:57:04 +0100 Subject: [PATCH 09/24] Optimize LV2Plugin property access Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_plugin.py | 33 ++++++++++---------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_plugin.py b/pluginsmanager/model/lv2/lv2_plugin.py index ff34453..862e884 100644 --- a/pluginsmanager/model/lv2/lv2_plugin.py +++ b/pluginsmanager/model/lv2/lv2_plugin.py @@ -25,34 +25,25 @@ def __getitem__(self, key): """ return self.json[key] - @property - def json(self): - """ - :return: Json decodable representation of this plugin based in moddevices `lilvlib`_. - - .. _lilvlib: https://github.com/moddevices/lilvlib - """ - return self._json - - @property - def data(self): - """ - :return: Json decodable representation of this plugin based in moddevices `lilvlib`_. - - .. _lilvlib: https://github.com/moddevices/lilvlib - """ - return self.json - def __str__(self): return self['name'] def __repr__(self): - return "<{} object as {} at 0x{:x}>".format( + return "<{} ({}) at 0x{:x}>".format( self.__class__.__name__, - str(self), + self, id(self) ) + @property + def json(self): + """ + :return: JSON encodable representation of this plugin. + """ + return self._json + + data = json + @property def version(self): - return '' if 'version' not in self.json else self.json['version'] + return self.json.get('version', '') From 4aaf1bfe3691eeba57ff6a1cda9132b38c6d7bff Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 9 Nov 2019 01:58:05 +0100 Subject: [PATCH 10/24] Fix swapped logic in use of mod-host bypass command Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/protocol_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pluginsmanager/observer/mod_host/protocol_parser.py b/pluginsmanager/observer/mod_host/protocol_parser.py index 834d44c..e056999 100644 --- a/pluginsmanager/observer/mod_host/protocol_parser.py +++ b/pluginsmanager/observer/mod_host/protocol_parser.py @@ -308,7 +308,7 @@ def bypass(effect): """ return 'bypass {} {}'.format( effect.instance, - 1 if effect.active else 0 + 0 if effect.active else 1 ) @staticmethod From 64e9af33c48397fefd92145451881819291d477d Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 9 Nov 2019 02:43:52 +0100 Subject: [PATCH 11/24] Less verbose logging Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_effect_builder.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_effect_builder.py b/pluginsmanager/model/lv2/lv2_effect_builder.py index c262024..1bdc72d 100644 --- a/pluginsmanager/model/lv2/lv2_effect_builder.py +++ b/pluginsmanager/model/lv2/lv2_effect_builder.py @@ -72,16 +72,15 @@ def reload(self, metadata, allow_uninstalled_plugins=False): """ installed_plugins = set(self.installed_plugins) - log.debug("Installed plugins: %r", installed_plugins) for plugin in metadata: uri = plugin['uri'] msg = "Checking for '%s'... %s" if allow_uninstalled_plugins or uri in installed_plugins: + log.debug(msg, uri, "found") self._plugins[uri] = Lv2Plugin(plugin) - log.info(msg, uri, "found") else: - log.info(msg, uri, "NOT found") + log.debug(msg, uri, "NOT found") @property def installed_plugins(self): From dfda6035bcb9febc020635b052f69030e6f4eacb Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 9 Nov 2019 03:23:36 +0100 Subject: [PATCH 12/24] Comment out printing of messages send to mod-host Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pluginsmanager/observer/mod_host/connection.py b/pluginsmanager/observer/mod_host/connection.py index 5b8a616..c965458 100644 --- a/pluginsmanager/observer/mod_host/connection.py +++ b/pluginsmanager/observer/mod_host/connection.py @@ -37,7 +37,7 @@ def send(self, message): :param string message: Message that will be sent for *mod-host* """ - print(message.encode('utf-8')) + #print(message.encode('utf-8')) self.client.send(message.encode('utf-8')) received = self.client.recv(1024) From 1d3f5d8c10c78c142bc3b00e978f5d789c8587a7 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:06:11 +0100 Subject: [PATCH 13/24] Export JackError exception Signed-off-by: Christopher Arndt --- pluginsmanager/jack/jack_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pluginsmanager/jack/jack_client.py b/pluginsmanager/jack/jack_client.py index 34be0eb..9bce380 100644 --- a/pluginsmanager/jack/jack_client.py +++ b/pluginsmanager/jack/jack_client.py @@ -13,7 +13,9 @@ # limitations under the License. import logging -import jack + + +from jack import Client, JackError class JackClient(object): @@ -40,7 +42,7 @@ def __init__(self, name=None, no_start_server=True): if name is None: name = self.__class__.__name__ - self.client = jack.Client(name=name, no_start_server=no_start_server) + self.client = Client(name=name, no_start_server=no_start_server) self.xrun_callback = lambda delay: ... self.shutdown_callback = lambda status, reason: ... From c6498c7ba5b63b6a4d11fdaa03f17589547fd2da Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:07:16 +0100 Subject: [PATCH 14/24] Docstring grammar fixes Signed-off-by: Christopher Arndt --- pluginsmanager/model/system/system_effect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pluginsmanager/model/system/system_effect.py b/pluginsmanager/model/system/system_effect.py index 47aea8d..4da7b94 100644 --- a/pluginsmanager/model/system/system_effect.py +++ b/pluginsmanager/model/system/system_effect.py @@ -144,14 +144,14 @@ def __dict__(self): @property def is_possible_connect_itself(self): """ - return bool: Is possible connect the with it self? + return bool: Is it possible to connect the plugin to itself? """ return True @property def is_unique_for_all_pedalboards(self): """ - return bool: Is unique for all pedalboards? + return bool: Is the plugin unique for all pedalboards? Example: :class:`.SystemEffect` is unique for all pedalboards """ return True @@ -159,6 +159,6 @@ def is_unique_for_all_pedalboards(self): @property def use_real_identifier(self): """ - return bool: For this audio plugin, is necessary use the real effect identifier? + return bool: For this plugin, is it necessary use the real effect identifier? """ return True From 2b995ee3e80ed57ae93bf18b6cfc2b68f511ec9f Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:07:54 +0100 Subject: [PATCH 15/24] Set socket timeout before attempting connection Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pluginsmanager/observer/mod_host/connection.py b/pluginsmanager/observer/mod_host/connection.py index c965458..a49e966 100644 --- a/pluginsmanager/observer/mod_host/connection.py +++ b/pluginsmanager/observer/mod_host/connection.py @@ -23,8 +23,8 @@ class Connection(object): def __init__(self, socket_port=5555, address='localhost'): self.client = socket.socket() - self.client.connect((address, socket_port)) self.client.settimeout(5) + self.client.connect((address, socket_port)) def send(self, message): """ From 1d0c8ca259b426d36ab3fe4658d8e9da3b8585b1 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:09:58 +0100 Subject: [PATCH 16/24] Exception error message grammer and formatting fix Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pluginsmanager/observer/mod_host/host.py b/pluginsmanager/observer/mod_host/host.py index 0ffc26c..5213596 100644 --- a/pluginsmanager/observer/mod_host/host.py +++ b/pluginsmanager/observer/mod_host/host.py @@ -32,7 +32,7 @@ def __init__(self, address='localhost', port=5555): try: self.connection = Connection(port, address) except ConnectionRefusedError as e: - raise ConnectionRefusedError(str(e) + '. Do you starts mod-host?') from e + raise ConnectionRefusedError('%s. Did you start mod-host?' % e) from e # - For callback? try: From b9989f3bc0c7a805e58a11a6b7dc7c8bcff1c460 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:11:41 +0100 Subject: [PATCH 17/24] Fix exception error message quoting Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/mod_host.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pluginsmanager/observer/mod_host/mod_host.py b/pluginsmanager/observer/mod_host/mod_host.py index 198f82b..93b355d 100644 --- a/pluginsmanager/observer/mod_host/mod_host.py +++ b/pluginsmanager/observer/mod_host/mod_host.py @@ -107,8 +107,8 @@ def start(self): This function is experimental. There is no guarantee that the process will actually be initiated. """ if self.address != 'localhost': - raise ModHostError('The host configured in the constructor isn''t "localhost". ' - 'It is not possible to start a process on another device.') + raise ModHostError("The host configured in the constructor is not 'localhost'. " + "It is not possible to start a process on another device.") try: subprocess.call([self.process, '-p', str(self.port)]) From ff4ada232b39b33a77153e94e6eafd0635758a78 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:12:47 +0100 Subject: [PATCH 18/24] Don't try to close connection if it isn't open Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/mod_host.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pluginsmanager/observer/mod_host/mod_host.py b/pluginsmanager/observer/mod_host/mod_host.py index 93b355d..3f5bbb1 100644 --- a/pluginsmanager/observer/mod_host/mod_host.py +++ b/pluginsmanager/observer/mod_host/mod_host.py @@ -112,7 +112,6 @@ def start(self): try: subprocess.call([self.process, '-p', str(self.port)]) - except FileNotFoundError as e: exception = ModHostError( 'mod-host not found. Did you install it? ' @@ -144,7 +143,8 @@ def __del__(self): If the mod-host process has been created with :meth:`~pluginsmanager.observer.mod_host.ModHost.start()` method, it will be finished. """ - self.close() + if getattr(self, 'host', None): + self.close() def close(self): """ From b8214de212ed5d4ce091ec0760be922f2b4ded81 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:16:17 +0100 Subject: [PATCH 19/24] Add implementation fo preset_load/save commands Signed-off-by: Christopher Arndt --- pluginsmanager/observer/mod_host/protocol_parser.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pluginsmanager/observer/mod_host/protocol_parser.py b/pluginsmanager/observer/mod_host/protocol_parser.py index e056999..6257161 100644 --- a/pluginsmanager/observer/mod_host/protocol_parser.py +++ b/pluginsmanager/observer/mod_host/protocol_parser.py @@ -117,7 +117,7 @@ def disconnect(connection): ) @staticmethod - def preset_load(): + def preset_load(effect, preset_uri): """ ``preset_load `` @@ -127,14 +127,11 @@ def preset_load(): preset_load 0 "http://drobilla.net/plugins/mda/presets#JX10-moogcury-lite" - .. note:: - - Not implemented yet """ - pass + return 'preset_load {} "{}"'.format(effect.instance, preset_uri) @staticmethod - def preset_save(): + def preset_save(effect, preset_uri, dirpath, filename): """ ``preset_save `` @@ -148,7 +145,7 @@ def preset_save(): Not implemented yet """ - pass + return 'preset_load {} "{}" {} {}'.format(effect.instance, preset_name, dirpath, filename) @staticmethod def preset_show(): From 9323a2218ba2d56d050b50731a80e2d2c5662355 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:17:12 +0100 Subject: [PATCH 20/24] Improve string representaton and repr() of Param class Signed-off-by: Christopher Arndt --- pluginsmanager/model/param.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pluginsmanager/model/param.py b/pluginsmanager/model/param.py index fadeb30..fea88a9 100644 --- a/pluginsmanager/model/param.py +++ b/pluginsmanager/model/param.py @@ -126,15 +126,19 @@ def symbol(self): """ pass - def __repr__(self, *args, **kwargs): - return "<{} object as value={} [{} - {}] at 0x{:x}>".format( + def __repr__(self): + return "<{} '{}' value={} [{} - {}] at 0x{:x}>".format( self.__class__.__name__, + self.symbol, self.value, self.minimum, self.maximum, id(self) ) + def __str__(self): + return "{}: {}".format(self.symbol, self.value) + @property def json(self): """ From 4f91295fb0c739720f91b32ba27c1e9e5111a5ac Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:17:58 +0100 Subject: [PATCH 21/24] Improve string representaton of LV2Effect class Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_effect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_effect.py b/pluginsmanager/model/lv2/lv2_effect.py index df329eb..4f5a697 100644 --- a/pluginsmanager/model/lv2/lv2_effect.py +++ b/pluginsmanager/model/lv2/lv2_effect.py @@ -64,10 +64,10 @@ def __str__(self): return str(self.plugin) def __repr__(self): - return "<{} object as '{}' {} active at 0x{:x}>".format( + return "<{} ('{}') {}active at 0x{:x}>".format( self.__class__.__name__, - str(self), - '' if self.active else 'not', + self['name'], + '' if self.active else 'not ', id(self) ) From 4701ca0cbd7cae6aff2c7b524b0a9b7e41879bc1 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 13 Nov 2019 03:30:19 +0100 Subject: [PATCH 22/24] Add read/writeable properties to plugin info Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_plugin_info.py | 51 ++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_plugin_info.py b/pluginsmanager/model/lv2/lv2_plugin_info.py index 4260c24..0c0f161 100644 --- a/pluginsmanager/model/lv2/lv2_plugin_info.py +++ b/pluginsmanager/model/lv2/lv2_plugin_info.py @@ -8,9 +8,10 @@ import lilv -NS_PRESET = 'http://lv2plug.in/ns/ext/presets#' NS_MOD = "http://moddevices.com/ns/mod#" +NS_PATCH = 'http://lv2plug.in/ns/ext/patch#' NS_PORT_PROPERTIES = "http://lv2plug.in/ns/ext/port-props#" +NS_PRESET = 'http://lv2plug.in/ns/ext/presets#' NS_UNITS = "http://lv2plug.in/ns/extensions/units#" LV2_CATEGORIES = { @@ -456,12 +457,51 @@ def _get_plugin_presets(ctx, plugin): return sorted(preset_list, key=lambda x: x['label'] or '') +def _get_plugin_properties(ctx, plugin_uri): + world = ctx.world + + properties = {} + readable = [(node, False) + for node in world.find_nodes(plugin_uri, world.ns.patch.readable, None)] + writeable = [(node, True) + for node in world.find_nodes(plugin_uri, world.ns.patch.writable, None)] + + for prop_uri, is_writable in readable + writeable: + prop_node = world.find_nodes(prop_uri, world.ns.rdf.type, world.ns.lv2.Parameter) + + if not prop_node: + ctx.errors.append( + "Could not find defintion of property '%s'." % prop_uri) + continue + + label = world.find_nodes(prop_uri, world.ns.rdfs.label, None) + + if label: + label = str(label[0]) + + range_ = world.find_nodes(prop_uri, world.ns.rdfs.range, None) + + if range_: + range_ = str(range_[0]) + + prop_uri = str(prop_uri) + properties[prop_uri] = { + 'uri': prop_uri, + 'label': label, + 'type': range_, + 'writable': is_writable, + } + + return properties + + def _get_plugin_info(ctx, plugin): world = ctx.world world.ns.mod = lilv.Namespace(world, NS_MOD) + world.ns.patch = lilv.Namespace(world, NS_PATCH) world.ns.pprops = lilv.Namespace(world, NS_PORT_PROPERTIES) - world.ns.units = lilv.Namespace(world, NS_UNITS) world.ns.presets = lilv.Namespace(world, NS_PRESET) + world.ns.units = lilv.Namespace(world, NS_UNITS) ctx.errors = errors = [] ctx.warnings = warnings = [] @@ -474,6 +514,9 @@ def _get_plugin_info(ctx, plugin): elif str(uri).startswith("file:"): errors.append("plugin uri is local, and thus not suitable for redistribution") + # load all resources in bundle + world.load_resource(uri) + # name name = plugin.get_name() @@ -582,6 +625,9 @@ def _get_plugin_info(ctx, plugin): # presets presets = _get_plugin_presets(ctx, plugin) + # properties + properties = _get_plugin_properties(ctx, uri) + return { 'uri': node2str(uri), 'name': node2str(name), @@ -604,6 +650,7 @@ def _get_plugin_info(ctx, plugin): #'ui': ui, 'ports': ports, 'presets': presets, + 'properties': properties, 'errors': sorted(errors), 'warnings': sorted(warnings), } From c7166dc6445ac13cfaf5261287215b440176b37e Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Fri, 15 Nov 2019 03:16:41 +0100 Subject: [PATCH 23/24] Provisionary support for setting LV2 plugin properties Signed-off-by: Christopher Arndt --- pluginsmanager/model/effect.py | 8 ++++++++ pluginsmanager/model/lv2/lv2_effect.py | 18 +++++++++++++++--- .../observer/host_observer/host_observer.py | 18 +++++++++++++++++- pluginsmanager/observer/mod_host/host.py | 8 ++++++++ pluginsmanager/observer/mod_host/mod_host.py | 3 +++ .../observer/mod_host/protocol_parser.py | 15 +++++++++++++++ pluginsmanager/observer/observer_manager.py | 5 +++++ pluginsmanager/observer/updates_observer.py | 9 +++++++++ 8 files changed, 80 insertions(+), 4 deletions(-) diff --git a/pluginsmanager/model/effect.py b/pluginsmanager/model/effect.py index 7cfedd7..0fe7001 100644 --- a/pluginsmanager/model/effect.py +++ b/pluginsmanager/model/effect.py @@ -54,6 +54,7 @@ def __init__(self): self._active = True self._params = () + self._properties = {} self._inputs = DictTuple([], lambda: None) self._outputs = DictTuple([], lambda: None) self._midi_inputs = DictTuple([], lambda: None) @@ -79,6 +80,13 @@ def params(self): """ return self._params + @property + def properties(self): + """ + :return list[dict]: Properties of effect + """ + return self._properties + @property def inputs(self): """ diff --git a/pluginsmanager/model/lv2/lv2_effect.py b/pluginsmanager/model/lv2/lv2_effect.py index 4f5a697..6a3ca18 100644 --- a/pluginsmanager/model/lv2/lv2_effect.py +++ b/pluginsmanager/model/lv2/lv2_effect.py @@ -13,12 +13,11 @@ # limitations under the License. from pluginsmanager.model.effect import Effect -from pluginsmanager.model.lv2.lv2_param import Lv2Param from pluginsmanager.model.lv2.lv2_input import Lv2Input -from pluginsmanager.model.lv2.lv2_output import Lv2Output from pluginsmanager.model.lv2.lv2_midi_input import Lv2MidiInput from pluginsmanager.model.lv2.lv2_midi_output import Lv2MidiOutput - +from pluginsmanager.model.lv2.lv2_output import Lv2Output +from pluginsmanager.model.lv2.lv2_param import Lv2Param from pluginsmanager.util.dict_tuple import DictTuple @@ -46,6 +45,8 @@ def __init__(self, plugin): params = [Lv2Param(self, param) for param in plugin["ports"]["control"]["input"]] self._params = DictTuple(params, lambda param: param.symbol) + self._properties = plugin['properties'] + inputs = [Lv2Input(self, effect_input) for effect_input in plugin['ports']['audio']['input']] self._inputs = DictTuple(inputs, lambda _input: _input.symbol) @@ -94,3 +95,14 @@ def version(self): :return string: Version of plugin of effect """ return self.plugin.version + + def set_property(self, prop): + property = self._properties[prop.key] + prop = { + 'instance': self.instance, + 'uri': property['uri'], + 'label': property['label'], + 'type': property['type'], + 'value': prop.value + } + self.observer.on_set_property(prop) diff --git a/pluginsmanager/observer/host_observer/host_observer.py b/pluginsmanager/observer/host_observer/host_observer.py index 30d5645..6a4c643 100644 --- a/pluginsmanager/observer/host_observer/host_observer.py +++ b/pluginsmanager/observer/host_observer/host_observer.py @@ -36,7 +36,7 @@ class HostObserver(UpdatesObserver, metaclass=ABCMeta): abstract methods that hosts needs to implements, usually only with the important part. """ - + def __init__(self): super(HostObserver, self).__init__() self._pedalboard = None @@ -112,6 +112,7 @@ def on_effect_updated(self, effect, update_type, index, origin, **kwargs): if update_type == UpdateType.CREATED: self._add_effect(effect) self._load_params_of(effect) + self._load_properties(effect) self.on_effect_status_toggled(effect) if update_type == UpdateType.DELETED: @@ -126,6 +127,14 @@ def _load_params_of(self, effect): if param.value != param.default: self._set_param_value(param) + def _load_properties(self, effect): + """ + Called only when a effect has been created + """ + pass + #for prop in effect.properties: + # self._set_property(prop) + def on_effect_status_toggled(self, effect, **kwargs): if effect.pedalboard != self.pedalboard: return @@ -138,6 +147,9 @@ def on_param_value_changed(self, param, **kwargs): self._set_param_value(param) + def on_set_property(self, prop): + self._set_property(prop) + def on_connection_updated(self, connection, update_type, pedalboard, **kwargs): if pedalboard != self.pedalboard: return @@ -230,6 +242,10 @@ def _disconnect(self, connection): def _set_param_value(self, param): pass + @abstractmethod + def _set_property(self, prop): + pass + @abstractmethod def _set_effect_status(self, effect): pass diff --git a/pluginsmanager/observer/mod_host/host.py b/pluginsmanager/observer/mod_host/host.py index 5213596..1e21326 100644 --- a/pluginsmanager/observer/mod_host/host.py +++ b/pluginsmanager/observer/mod_host/host.py @@ -86,6 +86,14 @@ def set_param_value(self, param): """ self.connection.send(ProtocolParser.param_set(param)) + def set_property(self, prop): + """ + Send property message + + :param dict prop: Property that will be sent + """ + self.connection.send(ProtocolParser.property_set(prop)) + def set_status(self, effect): """ Toggle effect processing diff --git a/pluginsmanager/observer/mod_host/mod_host.py b/pluginsmanager/observer/mod_host/mod_host.py index 3f5bbb1..f1b0f5b 100644 --- a/pluginsmanager/observer/mod_host/mod_host.py +++ b/pluginsmanager/observer/mod_host/mod_host.py @@ -172,6 +172,9 @@ def close(self): def _set_param_value(self, param): self.host.set_param_value(param) + def _set_property(self, prop): + self.host.set_property(prop) + def _remove_effect(self, effect): self.host.remove(effect) diff --git a/pluginsmanager/observer/mod_host/protocol_parser.py b/pluginsmanager/observer/mod_host/protocol_parser.py index 6257161..716d2b2 100644 --- a/pluginsmanager/observer/mod_host/protocol_parser.py +++ b/pluginsmanager/observer/mod_host/protocol_parser.py @@ -181,6 +181,21 @@ def param_set(param): return 'param_set {} {} {}'.format(instance, param.symbol, param.value) + @staticmethod + def property_set(prop): + """ + ``param_set `` + + send a property message to the event input port + + e.g.:: + + param_set 0 "Blink" 1 + + :param dict prop: Parameter that will be updated your value + """ + return 'param_set {} "{}" "{}"'.format(prop['instance'], prop['label'], prop['value']) + @staticmethod def param_get(param): """ diff --git a/pluginsmanager/observer/observer_manager.py b/pluginsmanager/observer/observer_manager.py index 4f562f1..c046f5c 100644 --- a/pluginsmanager/observer/observer_manager.py +++ b/pluginsmanager/observer/observer_manager.py @@ -71,6 +71,11 @@ def on_param_value_changed(self, param, **kwargs): if observer != self.scope: observer.on_param_value_changed(param, **kwargs) + def on_set_property(self, prop, **kwargs): + for observer in self.observers: + if observer != self.scope: + observer.on_set_property(prop, **kwargs) + def on_connection_updated(self, connection, update_type, pedalboard, **kwargs): for observer in self.observers: if observer != self.scope: diff --git a/pluginsmanager/observer/updates_observer.py b/pluginsmanager/observer/updates_observer.py index e6fabb5..fcc81a6 100644 --- a/pluginsmanager/observer/updates_observer.py +++ b/pluginsmanager/observer/updates_observer.py @@ -94,6 +94,15 @@ def on_param_value_changed(self, param, **kwargs): """ pass + @abstractmethod + def on_set_property(self, prop, **kwargs): + """ + Called when a property is set + + :param dict prop: property dict + """ + pass + @abstractmethod def on_connection_updated(self, connection, update_type, pedalboard, **kwargs): """ From e7832947bc1c546f766b1358e53e0918b39682c5 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Fri, 15 Nov 2019 03:17:13 +0100 Subject: [PATCH 24/24] Minor docstring fixes Signed-off-by: Christopher Arndt --- pluginsmanager/model/lv2/lv2_param.py | 6 +++--- pluginsmanager/model/param.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pluginsmanager/model/lv2/lv2_param.py b/pluginsmanager/model/lv2/lv2_param.py index b4ae0da..ffa03ca 100644 --- a/pluginsmanager/model/lv2/lv2_param.py +++ b/pluginsmanager/model/lv2/lv2_param.py @@ -16,8 +16,7 @@ class Lv2Param(Param): - """ - Representation of a Lv2 `input control port`_ instance. + """Representation of a Lv2 `input control port`_ instance. For general input use, see :class:`.Param` class documentation. @@ -25,8 +24,8 @@ class Lv2Param(Param): :param dict data: *input control port* json representation .. _input control port: http://lv2plug.in/ns/lv2core/#Parameter - """ + """ def __init__(self, effect, data): super(Lv2Param, self).__init__(effect, data['ranges']['default']) self._data = data @@ -34,6 +33,7 @@ def __init__(self, effect, data): @property def data(self): return self._data + @property def maximum(self): return self.data['ranges']['maximum'] diff --git a/pluginsmanager/model/param.py b/pluginsmanager/model/param.py index fea88a9..8c93e2c 100644 --- a/pluginsmanager/model/param.py +++ b/pluginsmanager/model/param.py @@ -106,7 +106,7 @@ def value(self, new_value): @abstractmethod def minimum(self): """ - :return: Smaller value that the parameter can assume + :return: Smallest value that the parameter can assume """ pass @@ -114,7 +114,7 @@ def minimum(self): @abstractmethod def maximum(self): """ - :return: Greater value that the parameter can assume + :return: Biggest value that the parameter can assume """ pass