From f356ce71899ce42a40847ada4385fa117c328356 Mon Sep 17 00:00:00 2001 From: SrMouraSilva Date: Fri, 14 Dec 2018 18:45:04 -0300 Subject: [PATCH 1/3] Issue #91 Add effect, add connection, remove effect, set effect status, set param value --- .../observer/carla/callback_manager.py | 94 ++++++++++++++++ pluginsmanager/observer/carla/carla.py | 68 +++++------- pluginsmanager/observer/carla/carla_host.py | 104 ++++++++++++++++++ 3 files changed, 225 insertions(+), 41 deletions(-) create mode 100644 pluginsmanager/observer/carla/callback_manager.py create mode 100644 pluginsmanager/observer/carla/carla_host.py diff --git a/pluginsmanager/observer/carla/callback_manager.py b/pluginsmanager/observer/carla/callback_manager.py new file mode 100644 index 0000000..46ee41b --- /dev/null +++ b/pluginsmanager/observer/carla/callback_manager.py @@ -0,0 +1,94 @@ +class CallbackManager: + + def __init__(self, backend): + self.backend = backend + self.elements = Elements() + + def callback(self, *args): + none, opcode, identifier, value1, value2, value3, value_str = args + #print('Args:', args) + + if opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CLIENT_ADDED: + element = CarlaClient(self.backend, identifier, value_str) + self.elements[element.identifier] = element + + elif opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_PORT_ADDED: + element = self.elements[identifier] + element.add_port(CarlaPort(self.backend, identifier=value1, name=value_str, type=value2)) + + elif opcode == self.backend.ENGINE_CALLBACK_PLUGIN_ADDED \ + or opcode == self.backend.ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED: + # Audio plugin events not relevant now + pass + + elif opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CONNECTION_ADDED \ + or opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CONNECTION_REMOVED: + # effect connections + pass + + elif opcode == self.backend.ENGINE_CALLBACK_ENGINE_STARTED \ + or opcode == self.backend.ENGINE_CALLBACK_ENGINE_STOPPED: + # Relevant, but not about it implemented + pass + + elif opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CLIENT_REMOVED \ + or opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_PORT_REMOVED: + # Occur when the engine is stoppend or a effect is removed + pass + + else: + print('Args:', args) + + +class Elements: + def __init__(self): + self.elements_by_id = {} + self.elements_by_name = {} + + def __getitem__(self, item): + return self.elements_by_id[item] + + def __setitem__(self, key, value): + self.elements_by_id[key] = value + self.elements_by_name[value.name] = value + + def by_name(self, name): + return self.elements_by_name[name] + + +class CarlaCallbackObject: + + def __init__(self, backend, identifier, name): + self.backend = backend + self.identifier = identifier + self.name = name.decode('utf-8') + + +class CarlaClient(CarlaCallbackObject): + def __init__(self, backend, identifier, name): + super().__init__(backend, identifier, name) + + self.inputs = {} + self.outputs = {} + self.midi_inputs = {} + self.midi_outputs = {} + + def add_port(self, port: 'CarlaPort'): + # https://github.com/moddevices/mod-ui/blob/master/mod/host_carla.py#L401 + if port.type & self.backend.PATCHBAY_PORT_TYPE_AUDIO: + if port.type & self.backend.PATCHBAY_PORT_IS_INPUT: + self.inputs[port.name] = port + else: + self.outputs[port.name] = port + + elif port.type & self.backend.PATCHBAY_PORT_TYPE_MIDI: + if port.type & self.backend.PATCHBAY_PORT_IS_INPUT: + self.midi_inputs[port.name] = port + else: + self.midi_outputs[port.name] = port + + +class CarlaPort(CarlaCallbackObject): + def __init__(self, backend, identifier, name, type): + super().__init__(backend, identifier, name) + self.type = type diff --git a/pluginsmanager/observer/carla/carla.py b/pluginsmanager/observer/carla/carla.py index e17ca0f..c275eff 100644 --- a/pluginsmanager/observer/carla/carla.py +++ b/pluginsmanager/observer/carla/carla.py @@ -11,16 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path +from pluginsmanager.observer.carla.carla_host import CarlaHost, CarlaError from pluginsmanager.observer.host_observer.host_observer import HostObserver -#FIXME -#from carla_backend import CarlaHostDLL, ENGINE_OPTION_PATH_BINARIES, BINARY_NATIVE, PLUGIN_LV2 - - -class CarlaError(Exception): - pass - class Carla(HostObserver): """ @@ -75,53 +70,44 @@ class Carla(HostObserver): .. _Demystifying JACK – A Beginners Guide to Getting Started with JACK: http://libremusicproduction.com/articles/demystifying-jack-%E2%80%93-beginners-guide-getting-started-jack """ - def __init__(self, path): + def __init__(self, path: Path): super(Carla, self).__init__() - self.index = 0 + self.host = CarlaHost(path, 'Pedal Pi') + self.index = 0 # Needs to start with zero to works correctly (carlaHostDLL.carla_get_current_plugin_count()) + + def connect(self): + raise CarlaError("Please, call start instead connect") - self.host = CarlaHostDLL(path / "libcarla_standalone2.so", False) - self.host.set_engine_option(ENGINE_OPTION_PATH_BINARIES, 0, path) + def start(self): + self.host.start() + + def close(self): + self.host.stop() def _connect(self, connection): - # TODO - # https://github.com/moddevices/mod-ui/blob/master/mod/host_carla.py#L185-L198 - split_from = port_from.split("/") - if len(split_from) != 3: - return - if split_from[1] == "system": - groupIdA = self._client_id_system - portIdA = int(split_from[2].rsplit("_",1)[-1]) - instance_from, port_from = port_from.rsplit("/", 1) - else: - groupIdB = self._getPluginId(split_from[:1].join("/")) - portIdB = int(split_from[2].rsplit("_",1)[-1]) - instance_from, port_from = port_from.rsplit("/", 1) - - self.host.patchbay_connect(groupIdA, portIdA, groupIdB, portIdB) + self.host.connect(connection.output, connection.input) def _disconnect(self, connection): + #self.host.disconnect(connection.output, connection.input) pass def _add_effect(self, effect): - if not self.host.add_plugin( - BINARY_NATIVE, - PLUGIN_LV2, - None,#"/usr/lib/lv2/gx_echo.lv2/gx_echo.so", # Fixme - "effect_{}".format(effect.index), - effect.plugin.json['uri'], - 0, - None, - 0): - CarlaError("Failed to load plugin, possible reasons:\n%s" % self.host.get_last_error()) + plugin_bin = f"/usr/lib/lv2/{effect.plugin.data['bundle_name']}/{effect.plugin.data['binary']}" + plugin_uri = effect.plugin.data['uri'] + effect_name = f"effect_{self.index}" + + self.index += 1 + + self.host.add_effect(plugin_bin, plugin_uri, effect_name, effect) def _remove_effect(self, effect): - self.host.add_plugin(effect.index) + #raise NotImplementedError() + self.host.remove_effect(effect) + pass def _set_effect_status(self, effect): - self.host.set_active(effect.index, effect.active) + self.host.set_active(effect, effect.active) def _set_param_value(self, param): - effect_index = param.effect.index - param_index = param.data['index'] - self.host.set_parameter_value(effect_index, param_index, param.value) + self.host.set_parameter_value(param) diff --git a/pluginsmanager/observer/carla/carla_host.py b/pluginsmanager/observer/carla/carla_host.py new file mode 100644 index 0000000..5d0acd9 --- /dev/null +++ b/pluginsmanager/observer/carla/carla_host.py @@ -0,0 +1,104 @@ +import sys +from pathlib import Path + +from pluginsmanager.model.effect import Effect +from pluginsmanager.model.param import Param +from pluginsmanager.observer.carla.callback_manager import CallbackManager + + +class CarlaError(Exception): + pass + + +class CarlaHost(object): + + def __init__(self, path: Path, client_name): + self.backend = self._lazy_import(path) + self.callback_manager = CallbackManager(self.backend) + + self.host_dll = self._init_host(path) + + self._path = path + self.client_name = client_name + + self.pedalboard_metadata = {} + + def _lazy_import(self, path_carla: Path): + path_carla_python = path_carla / 'source/frontend/' + sys.path.append(path_carla_python.as_posix()) + + import carla_backend as backend + return backend + + def _init_host(self, path_carla: Path): + path_carla_bin = path_carla / "bin" + + host = self.backend.CarlaHostDLL(path_carla_bin / "libcarla_standalone2.so", loadGlobal=False) + host.set_engine_callback(lambda *args: self.callback_manager.callback(*args)) + host.set_engine_option(self.backend.ENGINE_OPTION_PATH_BINARIES, 0, path_carla_bin.as_posix()) + + return host + + def start(self): + if not self.host_dll.engine_init("JACK", "Pedal Pi"): + raise CarlaError(f"Engine failed to initialize, possible reasons:\n{self.host_dll.get_last_error()}") + + def stop(self): + self.host_dll.engine_close() + + def add_effect(self, plugin_bin, plugin_uri, effect_name, effect): + self.pedalboard_metadata[effect] = effect_name + + if not self.host_dll.add_plugin( + self.backend.BINARY_NATIVE, self.backend.PLUGIN_LV2, + plugin_bin, effect_name, + plugin_uri, + 0, None, 0x0 + ): + raise CarlaError(f"Failed to load plugin, possible reasons:\n{self.host_dll.get_last_error()}") + + def remove_effect(self, effect: Effect): + effect_id = self._effect_id(effect) + + if not self.host_dll.remove_plugin(effect_id): + raise CarlaError(f"Failed to remove effect, possible reasons:\n{self.host_dll.get_last_error()}") + + def connect(self, output_port, input_port): + output_effect_name = self.pedalboard_metadata[output_port.effect] + output_effect_carla = self.callback_manager.elements.by_name(output_effect_name) + + input_effect_name = self.pedalboard_metadata[input_port.effect] + input_effect_carla = self.callback_manager.elements.by_name(input_effect_name) + + output_port_carla = output_effect_carla.outputs[str(output_port)] + input_port_carla = input_effect_carla.inputs[str(input_port)] + + if not self.host_dll.patchbay_connect( + output_effect_carla.identifier, output_port_carla.identifier, + input_effect_carla.identifier, input_port_carla.identifier + ): + raise CarlaError(f"Failed to connect effects, possible reasons:\n{self.host_dll.get_last_error()}") + + def disconnect(self, output_port, input_port): + id = None + + if not self.host_dll.patchbay_disconnect(id): + raise CarlaError(f"Failed to disconnect effects ports, possible reasons:\n{self.host_dll.get_last_error()}") + + def set_active(self, effect: Effect, active: bool): + effect_id = self._effect_id(effect) + + # Returns None + self.host_dll.set_active(effect_id, active) + + def _effect_id(self, effect: Effect): + effect_name = self.pedalboard_metadata[effect] + effect_id = effect_name.split('_')[-1] + return int(effect_id) + + def set_parameter_value(self, param: Param): + effect_id = self._effect_id(param.effect) + param_id = param.index + + # Returns None + self.host_dll.set_parameter_value(effect_id, param_id, param.value) From edbf0a4801eb28ba1116a0d337603d0da3d4f3c8 Mon Sep 17 00:00:00 2001 From: SrMouraSilva Date: Fri, 14 Dec 2018 18:45:52 -0300 Subject: [PATCH 2/3] Simple fixes (refactoring and error clarification) --- pluginsmanager/model/effect.py | 2 +- pluginsmanager/observer/mod_host/mod_host.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pluginsmanager/model/effect.py b/pluginsmanager/model/effect.py index 7cfedd7..417b135 100644 --- a/pluginsmanager/model/effect.py +++ b/pluginsmanager/model/effect.py @@ -162,7 +162,7 @@ def index(self): Returns the first occurrence of the effect in your pedalboard """ if self.pedalboard is None: - raise IndexError('Effect not contains a pedalboard') + raise IndexError('Effect is not associated with a pedalboard') return self.pedalboard.effects.index(self) diff --git a/pluginsmanager/observer/mod_host/mod_host.py b/pluginsmanager/observer/mod_host/mod_host.py index 198f82b..a10e3d4 100644 --- a/pluginsmanager/observer/mod_host/mod_host.py +++ b/pluginsmanager/observer/mod_host/mod_host.py @@ -144,7 +144,7 @@ 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() + super().close() def close(self): """ From 0094415595b7d084cc6cf8399a9bffa014a6a43b Mon Sep 17 00:00:00 2001 From: SrMouraSilva Date: Fri, 14 Dec 2018 18:59:43 -0300 Subject: [PATCH 3/3] Issue #91 Add Carla support: remove effect, disconnect --- .../observer/carla/callback_manager.py | 15 +++++++++++---- pluginsmanager/observer/carla/carla.py | 5 +---- pluginsmanager/observer/carla/carla_host.py | 17 +++++++++++++++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/pluginsmanager/observer/carla/callback_manager.py b/pluginsmanager/observer/carla/callback_manager.py index 46ee41b..9d2c56c 100644 --- a/pluginsmanager/observer/carla/callback_manager.py +++ b/pluginsmanager/observer/carla/callback_manager.py @@ -3,10 +3,10 @@ class CallbackManager: def __init__(self, backend): self.backend = backend self.elements = Elements() + self.connections = {} def callback(self, *args): none, opcode, identifier, value1, value2, value3, value_str = args - #print('Args:', args) if opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CLIENT_ADDED: element = CarlaClient(self.backend, identifier, value_str) @@ -21,9 +21,13 @@ def callback(self, *args): # Audio plugin events not relevant now pass - elif opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CONNECTION_ADDED \ - or opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CONNECTION_REMOVED: - # effect connections + elif opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CONNECTION_ADDED: + connection = CarlaConnection(self.backend, identifier=identifier, name=value_str) + self.connections[connection.name] = connection + + elif opcode == self.backend.ENGINE_CALLBACK_PATCHBAY_CONNECTION_REMOVED: + # The remotion is a CarlaHost responsability + #del self.connections[connection.name] pass elif opcode == self.backend.ENGINE_CALLBACK_ENGINE_STARTED \ @@ -92,3 +96,6 @@ class CarlaPort(CarlaCallbackObject): def __init__(self, backend, identifier, name, type): super().__init__(backend, identifier, name) self.type = type + +class CarlaConnection(CarlaCallbackObject): + pass diff --git a/pluginsmanager/observer/carla/carla.py b/pluginsmanager/observer/carla/carla.py index c275eff..f3a0d3e 100644 --- a/pluginsmanager/observer/carla/carla.py +++ b/pluginsmanager/observer/carla/carla.py @@ -89,8 +89,7 @@ def _connect(self, connection): self.host.connect(connection.output, connection.input) def _disconnect(self, connection): - #self.host.disconnect(connection.output, connection.input) - pass + self.host.disconnect(connection.output, connection.input) def _add_effect(self, effect): plugin_bin = f"/usr/lib/lv2/{effect.plugin.data['bundle_name']}/{effect.plugin.data['binary']}" @@ -102,9 +101,7 @@ def _add_effect(self, effect): self.host.add_effect(plugin_bin, plugin_uri, effect_name, effect) def _remove_effect(self, effect): - #raise NotImplementedError() self.host.remove_effect(effect) - pass def _set_effect_status(self, effect): self.host.set_active(effect, effect.active) diff --git a/pluginsmanager/observer/carla/carla_host.py b/pluginsmanager/observer/carla/carla_host.py index 5d0acd9..2d9cf22 100644 --- a/pluginsmanager/observer/carla/carla_host.py +++ b/pluginsmanager/observer/carla/carla_host.py @@ -80,11 +80,24 @@ def connect(self, output_port, input_port): raise CarlaError(f"Failed to connect effects, possible reasons:\n{self.host_dll.get_last_error()}") def disconnect(self, output_port, input_port): - id = None + output_effect_name = self.pedalboard_metadata[output_port.effect] + output_effect_carla = self.callback_manager.elements.by_name(output_effect_name) + + input_effect_name = self.pedalboard_metadata[input_port.effect] + input_effect_carla = self.callback_manager.elements.by_name(input_effect_name) - if not self.host_dll.patchbay_disconnect(id): + output_port_carla = output_effect_carla.outputs[str(output_port)] + input_port_carla = input_effect_carla.inputs[str(input_port)] + + name = f"{output_effect_carla.identifier}:{output_port_carla.identifier}:{input_effect_carla.identifier}:{input_port_carla.identifier}" + + identifier = self.callback_manager.connections[name].identifier + + if not self.host_dll.patchbay_disconnect(identifier): raise CarlaError(f"Failed to disconnect effects ports, possible reasons:\n{self.host_dll.get_last_error()}") + del self.callback_manager.connections[name] + def set_active(self, effect: Effect, active: bool): effect_id = self._effect_id(effect)