diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/JackDbusDriver.cpp | 1066 | ||||
-rw-r--r-- | src/JackDbusDriver.hpp | 179 | ||||
-rw-r--r-- | src/JackDriver.cpp | 29 | ||||
-rw-r--r-- | src/JackDriver.hpp | 4 | ||||
-rw-r--r-- | src/Makefile.am | 20 | ||||
-rw-r--r-- | src/Patchage.cpp | 42 | ||||
-rw-r--r-- | src/PatchageCanvas.cpp | 7 | ||||
-rw-r--r-- | src/PatchageEvent.cpp | 5 | ||||
-rw-r--r-- | src/PatchageEvent.hpp | 2 | ||||
-rw-r--r-- | src/main.cpp | 1 | ||||
-rw-r--r-- | src/patchage.glade | 3 |
11 files changed, 1326 insertions, 32 deletions
diff --git a/src/JackDbusDriver.cpp b/src/JackDbusDriver.cpp new file mode 100644 index 0000000..07a301f --- /dev/null +++ b/src/JackDbusDriver.cpp @@ -0,0 +1,1066 @@ +// -*- Mode: C++ ; indent-tabs-mode: t -*- +/* This file is part of Patchage. + * Copyright (C) 2008 Nedko Arnaudov <nedko@arnaudov.name> + * + * Patchage is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * Patchage is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include <cassert> +#include <cstring> +#include <string> +#include <set> +#include <iostream> + +#include CONFIG_H_PATH + +#include <glib.h> +#include <dbus/dbus.h> +#include <dbus/dbus-glib.h> +#include <dbus/dbus-glib-lowlevel.h> + +#include <boost/format.hpp> +#include <raul/SharedPtr.hpp> + +#include "PatchageCanvas.hpp" +#include "PatchageEvent.hpp" +#include "Patchage.hpp" +#include "PatchageModule.hpp" +#include "Driver.hpp" +#include "JackDbusDriver.hpp" + +using namespace std; +using namespace FlowCanvas; + +#define JACKDBUS_SERVICE "org.jackaudio.service" +#define JACKDBUS_OBJECT "/org/jackaudio/Controller" +#define JACKDBUS_IFACE_CONTROL "org.jackaudio.JackControl" +#define JACKDBUS_IFACE_PATCHBAY "org.jackaudio.JackPatchbay" +#define JACKDBUS_CALL_DEFAULT_TIMEOUT 1000 // in milliseconds + +#define JACKDBUS_PORT_FLAG_INPUT 0x00000001 +#define JACKDBUS_PORT_FLAG_OUTPUT 0x00000002 +#define JACKDBUS_PORT_FLAG_PHYSICAL 0x00000004 +#define JACKDBUS_PORT_FLAG_CAN_MONITOR 0x00000008 +#define JACKDBUS_PORT_FLAG_TERMINAL 0x00000010 + +#define JACKDBUS_PORT_TYPE_AUDIO 0 +#define JACKDBUS_PORT_TYPE_MIDI 1 + +//#define LOG_TO_STD +#define LOG_TO_STATUS + +//#define USE_FULL_REFRESH + +JackDriver::JackDriver(Patchage* app) + : Driver(128) + , _app(app) + , _dbus_connection(0) + , _server_started(false) + , _server_responding(false) + , _graph_version(0) + , _max_dsp_load(0.0) +{ + //info_msg(__func__); + + dbus_error_init(&_dbus_error); +} + +JackDriver::~JackDriver() +{ + //info_msg(__func__); + + detach(); + + if (dbus_error_is_set(&_dbus_error)) { + dbus_error_free(&_dbus_error); + } +} + +/** Destroy all JACK (canvas) ports. + */ +void +JackDriver::destroy_all_ports() +{ + ItemList modules = _app->canvas()->items(); // copy + for (ItemList::iterator m = modules.begin(); m != modules.end(); ++m) { + SharedPtr<Module> module = PtrCast<Module>(*m); + if (!module) + continue; + PortVector ports = module->ports(); // copy + for (PortVector::iterator p = ports.begin(); p != ports.end(); ++p) { + boost::shared_ptr<PatchagePort> port = boost::dynamic_pointer_cast<PatchagePort>(*p); + if (port && port->type() == JACK_AUDIO || port->type() == JACK_MIDI) { + module->remove_port(port); + port->hide(); + } + } + + if (module->ports().empty()) + _app->canvas()->remove_item(module); + else + module->resize(); + } +} + +DBusHandlerResult +JackDriver::dbus_message_hook( + DBusConnection *connection, + DBusMessage *message, + void *jack_driver) +{ + dbus_uint64_t new_graph_version; + dbus_uint64_t client_id; + const char *client_name; + dbus_uint64_t port_id; + const char *port_name; + dbus_uint32_t port_flags; + dbus_uint32_t port_type; + dbus_uint64_t client2_id; + const char *client2_name; + dbus_uint64_t port2_id; + const char *port2_name; + dbus_uint64_t connection_id; + + assert(jack_driver); + JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); + assert(me->_dbus_connection); + + //me->info_msg("dbus_message_hook() called."); + + // Handle signals we have subscribed for in attach() + +#if defined(USE_FULL_REFRESH) + if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "GraphChanged")) { + if (!dbus_message_get_args(message, &me->_dbus_error, DBUS_TYPE_UINT64, &new_graph_version, DBUS_TYPE_INVALID)) { + me->error_msg(str(boost::format("dbus_message_get_args() failed to extract GraphChanged signal arguments (%s)") % me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + //me->info_msg(str(boost::format("GraphChanged, new version is %llu") % new_graph_version)); + + if (new_graph_version > me->_graph_version) { + me->refresh_internal(false); + } + + return DBUS_HANDLER_RESULT_HANDLED; + } +#else +// if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "ClientAppeared")) { +// me->info_msg("ClientAppeared"); +// return DBUS_HANDLER_RESULT_HANDLED; +// } + +// if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "ClientDisappeared")) { +// me->info_msg("ClientDisappeared"); +// return DBUS_HANDLER_RESULT_HANDLED; +// } + + if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "PortAppeared")) { + if (!dbus_message_get_args( + message, + &me->_dbus_error, + DBUS_TYPE_UINT64, + &new_graph_version, + DBUS_TYPE_UINT64, + &client_id, + DBUS_TYPE_STRING, + &client_name, + DBUS_TYPE_UINT64, + &port_id, + DBUS_TYPE_STRING, + &port_name, + DBUS_TYPE_UINT32, + &port_flags, + DBUS_TYPE_UINT32, + &port_type, + DBUS_TYPE_INVALID)) { + me->error_msg(str(boost::format("dbus_message_get_args() failed to extract PortAppeared signal arguments (%s)") % me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + //me->info_msg(str(boost::format("PortAppeared, %s(%llu):%s(%llu), %lu, %lu") % client_name % client_id % port_name % port_id % port_flags % port_type)); + + me->add_port(client_id, client_name, port_id, port_name, port_flags, port_type); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "PortDisappeared")) { + if (!dbus_message_get_args( + message, + &me->_dbus_error, + DBUS_TYPE_UINT64, + &new_graph_version, + DBUS_TYPE_UINT64, + &client_id, + DBUS_TYPE_STRING, + &client_name, + DBUS_TYPE_UINT64, + &port_id, + DBUS_TYPE_STRING, + &port_name, + DBUS_TYPE_INVALID)) { + me->error_msg(str(boost::format("dbus_message_get_args() failed to extract PortDisappeared signal arguments (%s)") % me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + //me->info_msg(str(boost::format("PortDisappeared, %s(%llu):%s(%llu)") % client_name % client_id % port_name % port_id)); + + me->remove_port(client_id, client_name, port_id, port_name); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "PortsConnected")) { + if (!dbus_message_get_args( + message, + &me->_dbus_error, + DBUS_TYPE_UINT64, + &new_graph_version, + DBUS_TYPE_UINT64, + &client_id, + DBUS_TYPE_STRING, + &client_name, + DBUS_TYPE_UINT64, + &port_id, + DBUS_TYPE_STRING, + &port_name, + DBUS_TYPE_UINT64, + &client2_id, + DBUS_TYPE_STRING, + &client2_name, + DBUS_TYPE_UINT64, + &port2_id, + DBUS_TYPE_STRING, + &port2_name, + DBUS_TYPE_UINT64, + &connection_id, + DBUS_TYPE_INVALID)) { + me->error_msg(str(boost::format("dbus_message_get_args() failed to extract PortsConnected signal arguments (%s)") % me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + +// me->info_msg(str(boost::format("ports connected (%llu) %s(%llu):%s(%llu) <-> %s(%llu):%s(%llu)") % +// connection_id % +// client_name % +// client_id % +// port_name % +// port_id % +// client2_name % +// client2_id % +// port2_name % +// port2_id)); + + me->connect_ports( + connection_id, + client_id, + client_name, + port_id, + port_name, + client2_id, + client2_name, + port2_id, + port2_name); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, JACKDBUS_IFACE_PATCHBAY, "PortsDisconnected")) { + if (!dbus_message_get_args( + message, + &me->_dbus_error, + DBUS_TYPE_UINT64, + &new_graph_version, + DBUS_TYPE_UINT64, + &client_id, + DBUS_TYPE_STRING, + &client_name, + DBUS_TYPE_UINT64, + &port_id, + DBUS_TYPE_STRING, + &port_name, + DBUS_TYPE_UINT64, + &client2_id, + DBUS_TYPE_STRING, + &client2_name, + DBUS_TYPE_UINT64, + &port2_id, + DBUS_TYPE_STRING, + &port2_name, + DBUS_TYPE_UINT64, + &connection_id, + DBUS_TYPE_INVALID)) { + me->error_msg(str(boost::format("dbus_message_get_args() failed to extract PortsConnected signal arguments (%s)") % me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + +// me->info_msg(str(boost::format("ports disconnected (%llu) %s(%llu):%s(%llu) <-> %s(%llu):%s(%llu)") % +// connection_id % +// client_name % +// client_id % +// port_name % +// port_id % +// client2_name % +// client2_id % +// port2_name % +// port2_id)); + + me->disconnect_ports( + connection_id, + client_id, + client_name, + port_id, + port_name, + client2_id, + client2_name, + port2_id, + port2_name); + + return DBUS_HANDLER_RESULT_HANDLED; + } +#endif + + //me->info_msg("not yet handled."); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +bool +JackDriver::call( + const char* iface, + const char* method, + DBusMessage ** reply_ptr_ptr, + int in_type, + ...) +{ + DBusMessage* request_ptr; + DBusMessage* reply_ptr; + va_list ap; + + request_ptr = dbus_message_new_method_call( + JACKDBUS_SERVICE, + JACKDBUS_OBJECT, + iface, + method); + if (!request_ptr) { + throw std::runtime_error("dbus_message_new_method_call() returned 0"); + } + + va_start(ap, in_type); + + dbus_message_append_args_valist( + request_ptr, + in_type, + ap); + + va_end(ap); + + // send message and get a handle for a reply + reply_ptr = dbus_connection_send_with_reply_and_block(_dbus_connection, request_ptr, JACKDBUS_CALL_DEFAULT_TIMEOUT, &_dbus_error); + + dbus_message_unref(request_ptr); + + if (!reply_ptr) { + error_msg(_dbus_error.message); + _server_responding = false; + dbus_error_free(&_dbus_error); + } else { + _server_responding = true; + *reply_ptr_ptr = reply_ptr; + } + + return reply_ptr; +} + +bool +JackDriver::is_started() +{ + DBusMessage* reply_ptr; + dbus_bool_t started; + + if (!call(JACKDBUS_IFACE_CONTROL, "IsStarted", &reply_ptr, DBUS_TYPE_INVALID)) { + return false; + } + + if (!dbus_message_get_args(reply_ptr, &_dbus_error, DBUS_TYPE_BOOLEAN, &started, DBUS_TYPE_INVALID)) { + dbus_message_unref(reply_ptr); + dbus_error_free(&_dbus_error); + error_msg("decoding reply of IsStarted failed."); + return false; + } + + dbus_message_unref(reply_ptr); + + return started; +} + +void +JackDriver::attach(bool launch_daemon) +{ + // connect to the bus + _dbus_connection = dbus_bus_get(DBUS_BUS_SESSION, &_dbus_error); + if (dbus_error_is_set(&_dbus_error)) { + error_msg("dbus_bus_get() failed"); + error_msg(_dbus_error.message); + dbus_error_free(&_dbus_error); + return; + } + + dbus_connection_setup_with_g_main(_dbus_connection, NULL); + +#if defined(USE_FULL_REFRESH) + dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=GraphChanged", NULL); +#else +// dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=ClientAppeared", NULL); +// dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=ClientDisappeared", NULL); + dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=PortAppeared", NULL); + dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=PortDisappeared", NULL); + dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=PortsConnected", NULL); + dbus_bus_add_match(_dbus_connection, "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY "',member=PortsDisconnected", NULL); +#endif + dbus_connection_add_filter(_dbus_connection, dbus_message_hook, this, NULL); + + _server_started = is_started(); + + if (_server_responding) { + signal_attached.emit(); + } +} + +void +JackDriver::detach() +{ + if (_dbus_connection) { + dbus_connection_flush(_dbus_connection); + _dbus_connection = NULL; + signal_detached.emit(); + } +} + +bool +JackDriver::is_attached() const +{ + return _dbus_connection && _server_responding; +} + +void +JackDriver::add_port( + boost::shared_ptr<PatchageModule>& module, + PortType type, + const std::string& name, + bool is_input) +{ + if (module->get_port(name)) { + return; + } + + module->add_port( + boost::shared_ptr<PatchagePort>( + new PatchagePort( + module, + type, + name, + is_input, + _app->state_manager()->get_port_color(type)))); + + module->resize(); +} + +void +JackDriver::add_port( + dbus_uint64_t client_id, + const char *client_name, + dbus_uint64_t port_id, + const char *port_name, + dbus_uint32_t port_flags, + dbus_uint32_t port_type) +{ + PortType local_port_type; + + switch (port_type) { + case JACKDBUS_PORT_TYPE_AUDIO: + local_port_type = JACK_AUDIO; + break; + case JACKDBUS_PORT_TYPE_MIDI: + local_port_type = JACK_MIDI; + break; + default: + error_msg("Unknown JACK D-Bus port type"); + return; + } + + ModuleType type = InputOutput; + if (_app->state_manager()->get_module_split(client_name, port_flags & JACKDBUS_PORT_FLAG_TERMINAL)) { + if (port_flags & JACKDBUS_PORT_FLAG_INPUT) { + type = Input; + } else { + type = Output; + } + } + + boost::shared_ptr<PatchageModule> module = find_or_create_module(type, client_name); + + add_port(module, local_port_type, port_name, port_flags & JACKDBUS_PORT_FLAG_INPUT); +} + +void +JackDriver::remove_port( + dbus_uint64_t client_id, + const char *client_name, + dbus_uint64_t port_id, + const char *port_name) +{ + boost::shared_ptr<PatchagePort> port = PtrCast<PatchagePort>(_app->canvas()->get_port(client_name, port_name)); + if (!port) { + error_msg("Unable to remove unknown port"); + return; + } + + boost::shared_ptr<PatchageModule> module = PtrCast<PatchageModule>(port->module().lock()); + + module->remove_port(port); + port.reset(); + + // No empty modules (for now) + if (module->num_ports() == 0) { + _app->canvas()->remove_item(module); + module.reset(); + } else { + module->resize(); + } +} + +boost::shared_ptr<PatchageModule> +JackDriver::find_or_create_module( + ModuleType type, + const std::string& name) +{ + boost::shared_ptr<PatchageModule> module = _app->canvas()->find_module(name, type); + + if (!module) { + module = boost::shared_ptr<PatchageModule>(new PatchageModule(_app, name, type)); + module->load_location(); + _app->canvas()->add_item(module); + } + + return module; +} + +void +JackDriver::connect_ports( + dbus_uint64_t connection_id, + dbus_uint64_t client1_id, + const char *client1_name, + dbus_uint64_t port1_id, + const char *port1_name, + dbus_uint64_t client2_id, + const char *client2_name, + dbus_uint64_t port2_id, + const char *port2_name) +{ + boost::shared_ptr<PatchagePort> port1 = PtrCast<PatchagePort>(_app->canvas()->get_port(client1_name, port1_name)); + if (!port1) { + error_msg((string)"Unable to connect unknown port '" + port1_name + "' of client '" + client1_name + "'"); + return; + } + + boost::shared_ptr<PatchagePort> port2 = PtrCast<PatchagePort>(_app->canvas()->get_port(client2_name, port2_name)); + if (!port2) { + error_msg((string)"Unable to connect unknown port '" + port2_name + "' of client '" + client2_name + "'"); + return; + } + + _app->canvas()->add_connection(port1, port2, port1->color() + 0x22222200); +} + +void +JackDriver::disconnect_ports( + dbus_uint64_t connection_id, + dbus_uint64_t client1_id, + const char *client1_name, + dbus_uint64_t port1_id, + const char *port1_name, + dbus_uint64_t client2_id, + const char *client2_name, + dbus_uint64_t port2_id, + const char *port2_name) +{ + boost::shared_ptr<PatchagePort> port1 = PtrCast<PatchagePort>(_app->canvas()->get_port(client1_name, port1_name)); + if (!port1) { + error_msg((string)"Unable to disconnect unknown port '" + port1_name + "' of client '" + client1_name + "'"); + return; + } + + boost::shared_ptr<PatchagePort> port2 = PtrCast<PatchagePort>(_app->canvas()->get_port(client2_name, port2_name)); + if (!port2) { + error_msg((string)"Unable to disconnect unknown port '" + port2_name + "' of client '" + client2_name + "'"); + return; + } + + _app->canvas()->remove_connection(port1, port2); +} + +void +JackDriver::refresh_internal(bool force) +{ + DBusMessage* reply_ptr; + DBusMessageIter iter; + int type; + dbus_uint64_t version; + const char * reply_signature; + DBusMessageIter clients_array_iter; + DBusMessageIter client_struct_iter; + DBusMessageIter ports_array_iter; + DBusMessageIter port_struct_iter; + DBusMessageIter connections_array_iter; + DBusMessageIter connection_struct_iter; + dbus_uint64_t client_id; + const char *client_name; + dbus_uint64_t port_id; + const char *port_name; + dbus_uint32_t port_flags; + dbus_uint32_t port_type; + dbus_uint64_t client2_id; + const char *client2_name; + dbus_uint64_t port2_id; + const char *port2_name; + dbus_uint64_t connection_id; + + if (force) { + version = 0; // workaround module split/join stupidity + } else { + version = _graph_version; + } + + if (!call(JACKDBUS_IFACE_PATCHBAY, "GetGraph", &reply_ptr, DBUS_TYPE_UINT64, &version, DBUS_TYPE_INVALID)) { + error_msg("GetGraph() failed."); + return; + } + + reply_signature = dbus_message_get_signature(reply_ptr); + + if (strcmp(reply_signature, "ta(tsa(tsuu))a(tstststst)") != 0) { + error_msg((string )"GetGraph() reply signature mismatch. " + reply_signature); + goto unref; + } + + dbus_message_iter_init(reply_ptr, &iter); + + //info_msg((string)"version " + (char)dbus_message_iter_get_arg_type(&iter)); + dbus_message_iter_get_basic(&iter, &version); + dbus_message_iter_next(&iter); + + if (!force && version <= _graph_version) { + goto unref; + } + + destroy_all_ports(); + +// info_msg(str(boost::format("got new graph version %llu") % version)); + _graph_version = version; + + //info_msg((string)"clients " + (char)dbus_message_iter_get_arg_type(&iter)); + + for (dbus_message_iter_recurse(&iter, &clients_array_iter); + dbus_message_iter_get_arg_type(&clients_array_iter) != DBUS_TYPE_INVALID; + dbus_message_iter_next(&clients_array_iter)) { + //info_msg((string)"a client " + (char)dbus_message_iter_get_arg_type(&clients_array_iter)); + dbus_message_iter_recurse(&clients_array_iter, &client_struct_iter); + + dbus_message_iter_get_basic(&client_struct_iter, &client_id); + dbus_message_iter_next(&client_struct_iter); + + dbus_message_iter_get_basic(&client_struct_iter, &client_name); + dbus_message_iter_next(&client_struct_iter); + +// info_msg((string)"client '" + client_name + "'"); + + for (dbus_message_iter_recurse(&client_struct_iter, &ports_array_iter); + dbus_message_iter_get_arg_type(&ports_array_iter) != DBUS_TYPE_INVALID; + dbus_message_iter_next(&ports_array_iter)) { + //info_msg((string)"a port " + (char)dbus_message_iter_get_arg_type(&ports_array_iter)); + dbus_message_iter_recurse(&ports_array_iter, &port_struct_iter); + + dbus_message_iter_get_basic(&port_struct_iter, &port_id); + dbus_message_iter_next(&port_struct_iter); + + dbus_message_iter_get_basic(&port_struct_iter, &port_name); + dbus_message_iter_next(&port_struct_iter); + + dbus_message_iter_get_basic(&port_struct_iter, &port_flags); + dbus_message_iter_next(&port_struct_iter); + + dbus_message_iter_get_basic(&port_struct_iter, &port_type); + dbus_message_iter_next(&port_struct_iter); + +// info_msg((string)"port: " + port_name); + + add_port(client_id, client_name, port_id, port_name, port_flags, port_type); + } + + dbus_message_iter_next(&client_struct_iter); + } + + dbus_message_iter_next(&iter); + + for (dbus_message_iter_recurse(&iter, &connections_array_iter); + dbus_message_iter_get_arg_type(&connections_array_iter) != DBUS_TYPE_INVALID; + dbus_message_iter_next(&connections_array_iter)) { + //info_msg((string)"a connection " + (char)dbus_message_iter_get_arg_type(&connections_array_iter)); + dbus_message_iter_recurse(&connections_array_iter, &connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &client_id); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &client_name); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &port_id); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &port_name); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &client2_id); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &client2_name); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &port2_id); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &port2_name); + dbus_message_iter_next(&connection_struct_iter); + + dbus_message_iter_get_basic(&connection_struct_iter, &connection_id); + dbus_message_iter_next(&connection_struct_iter); + +// info_msg(str(boost::format("connection(%llu) %s(%llu):%s(%llu) <-> %s(%llu):%s(%llu)") % +// connection_id % +// client_name % +// client_id % +// port_name % +// port_id % +// client2_name % +// client2_id % +// port2_name % +// port2_id)); + + connect_ports( + connection_id, + client_id, + client_name, + port_id, + port_name, + client2_id, + client2_name, + port2_id, + port2_name); + } + +unref: + dbus_message_unref(reply_ptr); +} + +void +JackDriver::refresh() +{ + refresh_internal(true); +} + +bool +JackDriver::connect( + boost::shared_ptr<PatchagePort> src, + boost::shared_ptr<PatchagePort> dst) +{ + const char *client1_name; + const char *port1_name; + const char *client2_name; + const char *port2_name; + DBusMessage* reply_ptr; + + client1_name = src->module().lock()->name().c_str(); + port1_name = src->name().c_str(); + client2_name = dst->module().lock()->name().c_str(); + port2_name = dst->name().c_str(); + + if (!call( + JACKDBUS_IFACE_PATCHBAY, + "ConnectPortsByName", + &reply_ptr, + DBUS_TYPE_STRING, + &client1_name, + DBUS_TYPE_STRING, + &port1_name, + DBUS_TYPE_STRING, + &client2_name, + DBUS_TYPE_STRING, + &port2_name, + DBUS_TYPE_INVALID)) { + error_msg("ConnectPortsByName() failed."); + return false; + } + + return true; +} + +bool +JackDriver::disconnect( + boost::shared_ptr<PatchagePort> src, + boost::shared_ptr<PatchagePort> dst) +{ + const char *client1_name; + const char *port1_name; + const char *client2_name; + const char *port2_name; + DBusMessage* reply_ptr; + + client1_name = src->module().lock()->name().c_str(); + port1_name = src->name().c_str(); + client2_name = dst->module().lock()->name().c_str(); + port2_name = dst->name().c_str(); + + if (!call( + JACKDBUS_IFACE_PATCHBAY, + "DisconnectPortsByName", + &reply_ptr, + DBUS_TYPE_STRING, + &client1_name, + DBUS_TYPE_STRING, + &port1_name, + DBUS_TYPE_STRING, + &client2_name, + DBUS_TYPE_STRING, + &port2_name, + DBUS_TYPE_INVALID)) { + error_msg("DisconnectPortsByName() failed."); + return false; + } + + return true; +} + +jack_nframes_t +JackDriver::buffer_size() +{ + DBusMessage* reply_ptr; + dbus_uint32_t buffer_size; + + if (!call(JACKDBUS_IFACE_CONTROL, "GetBufferSize", &reply_ptr, DBUS_TYPE_INVALID)) { + return false; + } + + if (!dbus_message_get_args(reply_ptr, &_dbus_error, DBUS_TYPE_UINT32, &buffer_size, DBUS_TYPE_INVALID)) { + dbus_message_unref(reply_ptr); + dbus_error_free(&_dbus_error); + error_msg("decoding reply of GetBufferSize failed."); + return false; + } + + dbus_message_unref(reply_ptr); + + return buffer_size; +} + +bool +JackDriver::set_buffer_size(jack_nframes_t size) +{ + DBusMessage* reply_ptr; + dbus_uint32_t buffer_size; + + buffer_size = size; + + if (!call(JACKDBUS_IFACE_CONTROL, "SetBufferSize", &reply_ptr, DBUS_TYPE_UINT32, &buffer_size, DBUS_TYPE_INVALID)) { + return false; + } + + dbus_message_unref(reply_ptr); +} + +float +JackDriver::sample_rate() +{ + DBusMessage* reply_ptr; + double sample_rate; + + if (!call(JACKDBUS_IFACE_CONTROL, "GetSampleRate", &reply_ptr, DBUS_TYPE_INVALID)) { + return false; + } + + if (!dbus_message_get_args(reply_ptr, &_dbus_error, DBUS_TYPE_DOUBLE, &sample_rate, DBUS_TYPE_INVALID)) { + dbus_message_unref(reply_ptr); + dbus_error_free(&_dbus_error); + error_msg("decoding reply of GetSampleRate failed."); + return false; + } + + dbus_message_unref(reply_ptr); + + return sample_rate; +} + +bool +JackDriver::is_realtime() const +{ + DBusMessage* reply_ptr; + dbus_bool_t realtime; + + if (!((JackDriver *)this)->call(JACKDBUS_IFACE_CONTROL, "IsRealtime", &reply_ptr, DBUS_TYPE_INVALID)) { + return false; + } + + if (!dbus_message_get_args(reply_ptr, &((JackDriver *)this)->_dbus_error, DBUS_TYPE_BOOLEAN, &realtime, DBUS_TYPE_INVALID)) { + dbus_message_unref(reply_ptr); + dbus_error_free(&((JackDriver *)this)->_dbus_error); + error_msg("decoding reply of IsRealtime failed."); + return false; + } + + dbus_message_unref(reply_ptr); + + return realtime; +} + +size_t +JackDriver::xruns() +{ + DBusMessage* reply_ptr; + dbus_uint32_t xruns; + + if (!call(JACKDBUS_IFACE_CONTROL, "GetXruns", &reply_ptr, DBUS_TYPE_INVALID)) { + return false; + } + + if (!dbus_message_get_args(reply_ptr, &_dbus_error, DBUS_TYPE_UINT32, &xruns, DBUS_TYPE_INVALID)) { + dbus_message_unref(reply_ptr); + dbus_error_free(&_dbus_error); + error_msg("decoding reply of GetXruns failed."); + return 0; + } + + dbus_message_unref(reply_ptr); + + return xruns; +} + +void +JackDriver::reset_xruns() +{ + DBusMessage* reply_ptr; + + if (!call(JACKDBUS_IFACE_CONTROL, "ResetXruns", &reply_ptr, DBUS_TYPE_INVALID)) { + return; + } + + dbus_message_unref(reply_ptr); +} + +float +JackDriver::get_max_dsp_load() +{ + DBusMessage* reply_ptr; + double load; + + if (!call(JACKDBUS_IFACE_CONTROL, "GetLoad", &reply_ptr, DBUS_TYPE_INVALID)) { + return false; + } + + if (!dbus_message_get_args(reply_ptr, &_dbus_error, DBUS_TYPE_DOUBLE, &load, DBUS_TYPE_INVALID)) { + dbus_message_unref(reply_ptr); + dbus_error_free(&_dbus_error); + error_msg("decoding reply of GetLoad failed."); + return false; + } + + dbus_message_unref(reply_ptr); + + load /= 100.0; // dbus returns it in percents, we use 0..1 + + if (load > _max_dsp_load) + { + _max_dsp_load = load; + } + + return _max_dsp_load; +} + +void +JackDriver::reset_max_dsp_load() +{ + _max_dsp_load = 0.0; +} + +void +JackDriver::start_transport() +{ +// info_msg(__func__); +} + +void +JackDriver::stop_transport() +{ +// info_msg(__func__); +} + +void +JackDriver::rewind_transport() +{ +// info_msg(__func__); +} + +boost::shared_ptr<PatchagePort> +JackDriver::find_port_view( + Patchage * patchage, + const PatchageEvent::PortRef& ref) +{ + assert(false); // we dont use events at all +} + +boost::shared_ptr<PatchagePort> +JackDriver::create_port_view( + Patchage * patchage, + const PatchageEvent::PortRef& ref) +{ + assert(false); // we dont use events at all +} + +void +JackDriver::error_msg(const std::string& msg) const +{ +#if defined(LOG_TO_STATUS) + _app->status_message((std::string)"[JACKDBUS] " + msg); +#endif + +#if defined(LOG_TO_STD) + cerr << (std::string)"[JACKDBUS] " << msg << endl; +#endif +} + +void +JackDriver::info_msg(const std::string& msg) const +{ +#if defined(LOG_TO_STATUS) + _app->status_message((std::string)"[JACKDBUS] " + msg); +#endif + +#if defined(LOG_TO_STD) + cerr << (std::string)"[JACKDBUS] " << msg << endl; +#endif +} diff --git a/src/JackDbusDriver.hpp b/src/JackDbusDriver.hpp new file mode 100644 index 0000000..3a5ccea --- /dev/null +++ b/src/JackDbusDriver.hpp @@ -0,0 +1,179 @@ +// -*- Mode: C++ ; indent-tabs-mode: t -*- +/* This file is part of Patchage. + * Copyright (C) 2008 Nedko Arnaudov <nedko@arnaudov.name> + * + * Patchage is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * Patchage is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef JACKDBUSDRIVER_HPP__B02CBCEF_31D6_4546_9457_C7DF6D6B0300__INCLUDED +#define JACKDBUSDRIVER_HPP__B02CBCEF_31D6_4546_9457_C7DF6D6B0300__INCLUDED + +#include <iostream> +#include <boost/shared_ptr.hpp> +#include <jack/jack.h> +#include <jack/statistics.h> +#include <glibmm/thread.h> +#include <raul/AtomicPtr.hpp> +#include <dbus/dbus.h> + +#include "Driver.hpp" +#include "Patchage.hpp" +#include "PatchageModule.hpp" + +class PatchageEvent; +class PatchageFlowCanvas; +class PatchagePort; + +class JackDriver : public Driver +{ +public: + JackDriver(Patchage* app); + ~JackDriver(); + + void attach(bool launch_daemon); + void detach(); + + bool is_attached() const; + bool is_realtime() const; + + void refresh(); + + bool + connect( + boost::shared_ptr<PatchagePort> src, + boost::shared_ptr<PatchagePort> dst); + + bool + disconnect( + boost::shared_ptr<PatchagePort> src, + boost::shared_ptr<PatchagePort> dst); + void start_transport(); + void stop_transport(); + void rewind_transport(); + + void reset_xruns(); + + jack_nframes_t buffer_size(); + bool set_buffer_size(jack_nframes_t size); + + float sample_rate(); + + size_t xruns(); + + float get_max_dsp_load(); + void reset_max_dsp_load(); + + boost::shared_ptr<PatchagePort> + find_port_view( + Patchage * patchage, + const PatchageEvent::PortRef& ref); + + boost::shared_ptr<PatchagePort> + create_port_view( + Patchage * patchage, + const PatchageEvent::PortRef& ref); + +private: + void error_msg(const std::string& msg) const; + void info_msg(const std::string& msg) const; + + boost::shared_ptr<PatchageModule> + find_or_create_module( + ModuleType type, + const std::string& name); + + void + add_port( + boost::shared_ptr<PatchageModule>& module, + PortType type, + const std::string& name, + bool is_input); + + void + add_port( + dbus_uint64_t client_id, + const char *client_name, + dbus_uint64_t port_id, + const char *port_name, + dbus_uint32_t port_flags, + dbus_uint32_t port_type); + + void + remove_port( + dbus_uint64_t client_id, + const char *client_name, + dbus_uint64_t port_id, + const char *port_name); + + void + connect_ports( + dbus_uint64_t connection_id, + dbus_uint64_t client1_id, + const char *client1_name, + dbus_uint64_t port1_id, + const char *port1_name, + dbus_uint64_t client2_id, + const char *client2_name, + dbus_uint64_t port2_id, + const char *port2_name); + + void + disconnect_ports( + dbus_uint64_t connection_id, + dbus_uint64_t client1_id, + const char *client1_name, + dbus_uint64_t port1_id, + const char *port1_name, + dbus_uint64_t client2_id, + const char *client2_name, + dbus_uint64_t port2_id, + const char *port2_name); + + bool + call( + const char* iface, + const char* method, + DBusMessage ** reply_ptr_ptr, + int in_type, + ...); + + bool + is_started(); + + void + destroy_all_ports(); + + void refresh_internal(bool force); + + static + DBusHandlerResult + dbus_message_hook( + DBusConnection *connection, + DBusMessage *message, + void *me); + +private: + Patchage* _app; + DBusError _dbus_error; + DBusConnection* _dbus_connection; + + bool _server_responding; + bool _server_started; + + dbus_uint64_t _graph_version; + + float _max_dsp_load; +}; + +#endif // #ifndef JACKDBUSDRIVER_HPP__B02CBCEF_31D6_4546_9457_C7DF6D6B0300__INCLUDED diff --git a/src/JackDriver.cpp b/src/JackDriver.cpp index f77fd32..fc6472b 100644 --- a/src/JackDriver.cpp +++ b/src/JackDriver.cpp @@ -489,7 +489,7 @@ JackDriver::jack_buffer_size_cb(jack_nframes_t buffer_size, void* jack_driver) me->_buffer_size = buffer_size; me->reset_xruns(); - me->reset_delay(); + me->reset_max_dsp_load(); return 0; } @@ -570,3 +570,30 @@ JackDriver::set_buffer_size(jack_nframes_t size) } } +float +JackDriver::get_max_dsp_load() +{ + float max_load; + float max_delay; + + max_delay = jack_get_max_delayed_usecs(_client); + + const float rate = sample_rate(); + const float size = buffer_size(); + const float period = size / rate * 1000000; // usec + + if (max_delay > period) { + max_load = 1.0; + jack_reset_max_delayed_usecs(_client); + } else { + max_load = max_delay / period; + } + + return max_load; +} + +void +JackDriver::reset_max_dsp_load() +{ + jack_reset_max_delayed_usecs(_client); +} diff --git a/src/JackDriver.hpp b/src/JackDriver.hpp index b75c826..6fc74d1 100644 --- a/src/JackDriver.hpp +++ b/src/JackDriver.hpp @@ -69,7 +69,7 @@ public: void stop_transport() { jack_transport_stop(_client); } void reset_xruns(); - void reset_delay() { jack_reset_max_delayed_usecs(_client); } + void reset_max_dsp_load(); void rewind_transport() { jack_position_t zero; @@ -87,7 +87,7 @@ public: inline size_t xruns() { return _xruns; } - inline float max_delay() { return jack_get_max_delayed_usecs(_client); } + float get_max_dsp_load(); private: diff --git a/src/Makefile.am b/src/Makefile.am index a87563b..3d8c76b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,5 +1,5 @@ -AM_CXXFLAGS = -I.. -I$(top_srcdir)/raul -I$(top_srcdir)/flowcanvas -DDATA_DIR=\"$(pkgdatadir)\" @GTHREAD_CFLAGS@ @LIBGLADEMM_CFLAGS@ @GNOMECANVASMM_CFLAGS@ @JACK_CFLAGS@ @ALSA_CFLAGS@ @LASH_CFLAGS@ -patchage_LDADD = @GTHREAD_LIBS@ @LIBGLADEMM_LIBS@ @GNOMECANVASMM_LIBS@ @JACK_LIBS@ @ALSA_LIBS@ @LASH_LIBS@ @RAUL_LIBS@ @FLOWCANVAS_LIBS@ +AM_CXXFLAGS = -I.. -I$(top_srcdir)/raul -I$(top_srcdir)/flowcanvas -DDATA_DIR=\"$(pkgdatadir)\" @GTHREAD_CFLAGS@ @LIBGLADEMM_CFLAGS@ @GNOMECANVASMM_CFLAGS@ @ALSA_CFLAGS@ @LASH_CFLAGS@ +patchage_LDADD = @GTHREAD_LIBS@ @LIBGLADEMM_LIBS@ @GNOMECANVASMM_LIBS@ @ALSA_LIBS@ @LASH_LIBS@ @RAUL_LIBS@ @FLOWCANVAS_LIBS@ EXTRA_DIST = patchage.gladep @@ -10,8 +10,6 @@ bin_PROGRAMS = patchage patchage_SOURCES = \ Driver.hpp \ GladeFile.hpp \ - JackDriver.cpp \ - JackDriver.hpp \ JackSettingsDialog.hpp \ Patchage.cpp \ Patchage.hpp \ @@ -26,6 +24,20 @@ patchage_SOURCES = \ Widget.hpp \ main.cpp +if WITH_JACK_DBUS +AM_CXXFLAGS += @DBUS_CFLAGS@ +patchage_SOURCES += JackDbusDriver.cpp +patchage_SOURCES += JackDbusDriver.hpp +patchage_LDADD += @DBUS_LIBS@ +else +if WITH_JACK +AM_CXXFLAGS += @JACK_CFLAGS@ +patchage_SOURCES += JackDriver.cpp +patchage_SOURCES += JackDriver.hpp +patchage_LDADD += @JACK_LIBS@ +endif +endif + if WITH_LASH patchage_SOURCES += LashDriver.hpp LashDriver.cpp endif diff --git a/src/Patchage.cpp b/src/Patchage.cpp index 6396526..3803a6b 100644 --- a/src/Patchage.cpp +++ b/src/Patchage.cpp @@ -27,7 +27,12 @@ #include CONFIG_H_PATH #include "GladeFile.hpp" +#ifdef HAVE_JACK #include "JackDriver.hpp" +#endif +#ifdef HAVE_JACKDBUS +#include "JackDbusDriver.hpp" +#endif #include "JackSettingsDialog.hpp" #include "Patchage.hpp" #include "PatchageCanvas.hpp" @@ -168,10 +173,6 @@ Patchage::Patchage(int argc, char** argv) sigc::mem_fun(_canvas.get(), &PatchageCanvas::zoom_full)); _menu_jack_settings->signal_activate().connect(sigc::hide_return( sigc::mem_fun(_jack_settings_dialog, &JackSettingsDialog::run))); - _menu_jack_connect->signal_activate().connect(sigc::bind( - sigc::mem_fun(_jack_driver, &JackDriver::attach), true)); - _menu_jack_disconnect->signal_activate().connect( - sigc::mem_fun(_jack_driver, &JackDriver::detach)); #ifdef HAVE_LASH _menu_open_session->signal_activate().connect( @@ -234,6 +235,11 @@ Patchage::Patchage(int argc, char** argv) _jack_driver = new JackDriver(this); _jack_driver->signal_detached.connect(sigc::mem_fun(this, &Patchage::queue_refresh)); + + _menu_jack_connect->signal_activate().connect(sigc::bind( + sigc::mem_fun(_jack_driver, &JackDriver::attach), true)); + _menu_jack_disconnect->signal_activate().connect( + sigc::mem_fun(_jack_driver, &JackDriver::detach)); #ifdef HAVE_ALSA _alsa_driver = new AlsaDriver(this); @@ -346,28 +352,18 @@ Patchage::update_load() if (!_jack_driver->is_attached()) return true; - static float last_delay = 0; + char tmp_buf[8]; + snprintf(tmp_buf, 8, "%zd", _jack_driver->xruns()); - const float max_delay = _jack_driver->max_delay(); + _main_xrun_progress->set_text(string(tmp_buf) + " Dropouts"); - if (max_delay != last_delay) { - const float sample_rate = _jack_driver->sample_rate(); - const float buffer_size = _jack_driver->buffer_size(); - const float period = buffer_size / sample_rate * 1000000; // usec - - _main_xrun_progress->set_fraction(max_delay / period); + static float last_max_dsp_load = 0; - char tmp_buf[8]; - snprintf(tmp_buf, 8, "%zd", _jack_driver->xruns()); - - _main_xrun_progress->set_text(string(tmp_buf) + " Dropouts"); - - if (max_delay > period) { - _main_xrun_progress->set_fraction(1.0); - _jack_driver->reset_delay(); - } + const float max_dsp_load = _jack_driver->get_max_dsp_load(); - last_delay = max_delay; + if (max_dsp_load != last_max_dsp_load) { + _main_xrun_progress->set_fraction(max_dsp_load); + last_max_dsp_load = max_dsp_load; } return true; @@ -430,7 +426,7 @@ Patchage::clear_load() { _main_xrun_progress->set_fraction(0.0); _jack_driver->reset_xruns(); - _jack_driver->reset_delay(); + _jack_driver->reset_max_dsp_load(); } diff --git a/src/PatchageCanvas.cpp b/src/PatchageCanvas.cpp index ffc5627..aa7ef64 100644 --- a/src/PatchageCanvas.cpp +++ b/src/PatchageCanvas.cpp @@ -19,7 +19,12 @@ #include CONFIG_H_PATH #include "PatchageCanvas.hpp" #include "Patchage.hpp" +#ifdef HAVE_JACK #include "JackDriver.hpp" +#endif +#ifdef HAVE_JACKDBUS +#include "JackDbusDriver.hpp" +#endif #include "PatchageModule.hpp" #include "PatchagePort.hpp" #ifdef HAVE_ALSA @@ -72,6 +77,7 @@ PatchageCanvas::find_port(const PatchageEvent::PortRef& ref) // TODO: filthy. keep a port map and make this O(log(n)) switch (ref.type) { +#if HAVE_JACK case PatchageEvent::PortRef::JACK_ID: jack_port = jack_port_by_id(_app->jack_driver()->client(), ref.id.jack_id); if (!jack_port) @@ -88,6 +94,7 @@ PatchageCanvas::find_port(const PatchageEvent::PortRef& ref) return boost::shared_ptr<PatchagePort>(); break; +#endif #ifdef HAVE_ALSA case PatchageEvent::PortRef::ALSA_ADDR: diff --git a/src/PatchageEvent.cpp b/src/PatchageEvent.cpp index d123188..afab9ae 100644 --- a/src/PatchageEvent.cpp +++ b/src/PatchageEvent.cpp @@ -22,7 +22,12 @@ #include "PatchageModule.hpp" #include "PatchageEvent.hpp" #include "Driver.hpp" +#ifdef HAVE_JACK #include "JackDriver.hpp" +#endif +#ifdef HAVE_JACKDBUS +#include "JackDbusDriver.hpp" +#endif #ifdef HAVE_ALSA #include "AlsaDriver.hpp" #endif diff --git a/src/PatchageEvent.hpp b/src/PatchageEvent.hpp index e9357ba..3fe0623 100644 --- a/src/PatchageEvent.hpp +++ b/src/PatchageEvent.hpp @@ -74,8 +74,10 @@ public: struct PortRef { PortRef() : type(NULL_PORT_REF) { memset(&id, 0, sizeof(id)); } +#ifdef HAVE_JACK PortRef(jack_port_id_t jack_id, bool ign=false) : type(JACK_ID) { id.jack_id = jack_id; } +#endif #ifdef HAVE_ALSA PortRef(snd_seq_addr_t addr, bool in) diff --git a/src/main.cpp b/src/main.cpp index 6cf479c..0e61d9d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,6 @@ #include <glibmm/exception.h> #include "Patchage.hpp" -#include "JackDriver.hpp" #ifdef HAVE_LASH #include <lash/lash.h> diff --git a/src/patchage.glade b/src/patchage.glade index 9872708..963ce5d 100644 --- a/src/patchage.glade +++ b/src/patchage.glade @@ -566,7 +566,8 @@ You should have received a copy of the GNU General Public License along with Patchage; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA </property> - <property name="authors">Dave Robillard <dave@drobilla.net></property> + <property name="authors">Dave Robillard <dave@drobilla.net> +JACK D-Bus driver by Nedko Arnaudov <nedko@arnaudov.name></property> <property name="translator_credits" translatable="yes" comments="TRANSLATORS: Replace this string with your names, one name per line.">translator-credits</property> <property name="artists">Icon: Lapo Calamandrei</property> |