diff options
Diffstat (limited to 'src')
68 files changed, 6377 insertions, 5388 deletions
diff --git a/src/Action.hpp b/src/Action.hpp new file mode 100644 index 0000000..c791445 --- /dev/null +++ b/src/Action.hpp @@ -0,0 +1,90 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_ACTION_HPP +#define PATCHAGE_ACTION_HPP + +#include "ClientID.hpp" +#include "PortID.hpp" +#include "Setting.hpp" +#include "SignalDirection.hpp" + +#include <variant> + +namespace patchage { +namespace action { + +struct ChangeSetting { + Setting setting; +}; + +struct ConnectPorts { + PortID tail; + PortID head; +}; + +struct DecreaseFontSize {}; + +struct DisconnectClient { + ClientID client; + SignalDirection direction; +}; + +struct DisconnectPort { + PortID port; +}; + +struct DisconnectPorts { + PortID tail; + PortID head; +}; + +struct IncreaseFontSize {}; + +struct MoveModule { + ClientID client; + SignalDirection direction; + double x; + double y; +}; + +struct Refresh {}; + +struct ResetFontSize {}; + +struct SplitModule { + ClientID client; +}; + +struct UnsplitModule { + ClientID client; +}; + +struct ZoomFull {}; +struct ZoomIn {}; +struct ZoomNormal {}; +struct ZoomOut {}; + +} // namespace action + +/// A high-level action from the user +using Action = std::variant<action::ChangeSetting, + action::ConnectPorts, + action::DecreaseFontSize, + action::DisconnectClient, + action::DisconnectPort, + action::DisconnectPorts, + action::IncreaseFontSize, + action::MoveModule, + action::Refresh, + action::ResetFontSize, + action::SplitModule, + action::UnsplitModule, + action::ZoomFull, + action::ZoomIn, + action::ZoomNormal, + action::ZoomOut>; + +} // namespace patchage + +#endif // PATCHAGE_ACTION_HPP diff --git a/src/ActionSink.hpp b/src/ActionSink.hpp new file mode 100644 index 0000000..7e023f8 --- /dev/null +++ b/src/ActionSink.hpp @@ -0,0 +1,18 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_ACTION_SINK_HPP +#define PATCHAGE_ACTION_SINK_HPP + +#include "Action.hpp" + +#include <functional> + +namespace patchage { + +/// Sink function for user actions +using ActionSink = std::function<void(Action)>; + +} // namespace patchage + +#endif // PATCHAGE_ACTION_SINK_HPP diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 1ebd12d..2771abd 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -1,585 +1,531 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "make_alsa_driver.hpp" + +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "ClientType.hpp" +#include "Driver.hpp" +#include "Event.hpp" +#include "ILog.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" +#include "PortType.hpp" +#include "SignalDirection.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS + +#include <alsa/asoundlib.h> +#include <pthread.h> #include <cassert> +#include <cstdint> +#include <functional> +#include <limits> +#include <memory> +#include <optional> #include <set> -#include <string> #include <utility> -#include <boost/format.hpp> +namespace patchage { +namespace { -#include "AlsaDriver.hpp" -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageModule.hpp" -#include "PatchagePort.hpp" +/// Driver for ALSA Sequencer ports +class AlsaDriver : public Driver +{ +public: + explicit AlsaDriver(ILog& log, EventSink emit_event); -using std::endl; -using std::string; -using boost::format; + AlsaDriver(const AlsaDriver&) = delete; + AlsaDriver& operator=(const AlsaDriver&) = delete; -AlsaDriver::AlsaDriver(Patchage* app) - : _app(app) - , _seq(NULL) -{ -} + AlsaDriver(AlsaDriver&&) = delete; + AlsaDriver& operator=(AlsaDriver&&) = delete; -AlsaDriver::~AlsaDriver() -{ - detach(); -} + ~AlsaDriver() override; -/** Attach to ALSA. */ -void -AlsaDriver::attach(bool /*launch_daemon*/) -{ - int ret = snd_seq_open(&_seq, "default", SND_SEQ_OPEN_DUPLEX, 0); - if (ret) { - _app->error_msg("Alsa: Unable to attach."); - _seq = NULL; - } else { - _app->info_msg("Alsa: Attached."); + void attach(bool launch_daemon) override; + void detach() override; - snd_seq_set_client_name(_seq, "Patchage"); + bool is_attached() const override { return (_seq != nullptr); } - pthread_attr_t attr; - pthread_attr_init(&attr); - pthread_attr_setstacksize(&attr, 50000); + void refresh(const EventSink& sink) override; - ret = pthread_create(&_refresh_thread, &attr, &AlsaDriver::refresh_main, this); - if (ret) - _app->error_msg("Alsa: Failed to start refresh thread."); + bool connect(const PortID& tail_id, const PortID& head_id) override; - signal_attached.emit(); - } -} + bool disconnect(const PortID& tail_id, const PortID& head_id) override; -void -AlsaDriver::detach() -{ - if (_seq) { - pthread_cancel(_refresh_thread); - pthread_join(_refresh_thread, NULL); - snd_seq_close(_seq); - _seq = NULL; - signal_detached.emit(); - _app->info_msg("Alsa: Detached."); - } -} +private: + bool create_refresh_port(); + static void* refresh_main(void* me); + void _refresh_main(); + + ILog& _log; + snd_seq_t* _seq{nullptr}; + pthread_t _refresh_thread{}; + + struct SeqAddrComparator { + bool operator()(const snd_seq_addr_t& a, const snd_seq_addr_t& b) const + { + return ((a.client < b.client) || + ((a.client == b.client) && a.port < b.port)); + } + }; + + using Ignored = std::set<snd_seq_addr_t, SeqAddrComparator>; + + Ignored _ignored; + + bool ignore(const snd_seq_addr_t& addr, bool add = true); +}; -static bool -is_alsa_port(const PatchagePort* port) +PortID +addr_to_id(const snd_seq_addr_t& addr, const bool is_input) { - return port->type() == ALSA_MIDI; + return PortID::alsa(addr.client, addr.port, is_input); } -/** Destroy all JACK (canvas) ports. - */ -void -AlsaDriver::destroy_all() +SignalDirection +port_direction(const snd_seq_port_info_t* const pinfo) { - _app->canvas()->remove_ports(is_alsa_port); - _modules.clear(); - _port_addrs.clear(); + const int caps = snd_seq_port_info_get_capability(pinfo); + + if ((caps & SND_SEQ_PORT_CAP_READ) && (caps & SND_SEQ_PORT_CAP_WRITE)) { + return SignalDirection::duplex; + } + + if (caps & SND_SEQ_PORT_CAP_READ) { + return SignalDirection::output; + } + + if (caps & SND_SEQ_PORT_CAP_WRITE) { + return SignalDirection::input; + } + + return SignalDirection::duplex; } -/** Refresh all Alsa Midi ports and connections. - */ -void -AlsaDriver::refresh() +ClientInfo +client_info(snd_seq_client_info_t* const cinfo) { - if (!is_attached()) - return; - - assert(_seq); - - _modules.clear(); - _ignored.clear(); - _port_addrs.clear(); - - snd_seq_client_info_t* cinfo; - snd_seq_client_info_alloca(&cinfo); - snd_seq_client_info_set_client(cinfo, -1); - - snd_seq_port_info_t* pinfo; - snd_seq_port_info_alloca(&pinfo); - - PatchageModule* parent = NULL; - PatchagePort* port = NULL; - - // Create port views - while (snd_seq_query_next_client(_seq, cinfo) >= 0) { - snd_seq_port_info_set_client(pinfo, snd_seq_client_info_get_client(cinfo)); - snd_seq_port_info_set_port(pinfo, -1); - while (snd_seq_query_next_port(_seq, pinfo) >= 0) { - const snd_seq_addr_t& addr = *snd_seq_port_info_get_addr(pinfo); - if (ignore(addr)) { - continue; - } - - create_port_view_internal(_app, addr, parent, port); - } - } - - // Create connections - snd_seq_client_info_set_client(cinfo, -1); - while (snd_seq_query_next_client(_seq, cinfo) >= 0) { - snd_seq_port_info_set_client(pinfo, snd_seq_client_info_get_client(cinfo)); - snd_seq_port_info_set_port(pinfo, -1); - while (snd_seq_query_next_port(_seq, pinfo) >= 0) { - const snd_seq_addr_t* addr = snd_seq_port_info_get_addr(pinfo); - if (ignore(*addr)) { - continue; - } - - PatchagePort* port = _app->canvas()->find_port(PortID(*addr, false)); - if (!port) { - continue; - } - - snd_seq_query_subscribe_t* subsinfo; - snd_seq_query_subscribe_alloca(&subsinfo); - snd_seq_query_subscribe_set_root(subsinfo, addr); - snd_seq_query_subscribe_set_index(subsinfo, 0); - while (!snd_seq_query_port_subscribers(_seq, subsinfo)) { - const snd_seq_addr_t* addr2 = snd_seq_query_subscribe_get_addr(subsinfo); - if (addr2) { - const PortID id2(*addr2, true); - PatchagePort* port2 = _app->canvas()->find_port(id2); - if (port2 && !_app->canvas()->get_edge(port, port2)) { - _app->canvas()->make_connection(port, port2); - } - } - - snd_seq_query_subscribe_set_index( - subsinfo, snd_seq_query_subscribe_get_index(subsinfo) + 1); - } - } - } + return {snd_seq_client_info_get_name(cinfo)}; } -PatchagePort* -AlsaDriver::create_port_view(Patchage* patchage, - const PortID& id) +PortInfo +port_info(const snd_seq_port_info_t* const pinfo) { - PatchageModule* parent = NULL; - PatchagePort* port = NULL; - create_port_view_internal(patchage, id.id.alsa_addr, parent, port); - return port; + const int type = snd_seq_port_info_get_type(pinfo); + + return {snd_seq_port_info_get_name(pinfo), + PortType::alsa_midi, + port_direction(pinfo), + snd_seq_port_info_get_port(pinfo), + (type & SND_SEQ_PORT_TYPE_APPLICATION) == 0}; } -PatchageModule* -AlsaDriver::find_module(uint8_t client_id, ModuleType type) +AlsaDriver::AlsaDriver(ILog& log, EventSink emit_event) + : Driver{std::move(emit_event)} + , _log(log) +{} + +AlsaDriver::~AlsaDriver() { - const Modules::const_iterator i = _modules.find(client_id); - if (i == _modules.end()) - return NULL; - - PatchageModule* io_module = NULL; - for (Modules::const_iterator j = i; - j != _modules.end() && j->first == client_id; - ++j) { - if (j->second->type() == type) { - return j->second; - } else if (j->second->type() == InputOutput) { - io_module = j->second; - } - } - - // Return InputOutput module for Input or Output, or NULL if not found - return io_module; + detach(); } -PatchageModule* -AlsaDriver::find_or_create_module( - Patchage* patchage, - uint8_t client_id, - const std::string& client_name, - ModuleType type) +void +AlsaDriver::attach(bool /*launch_daemon*/) { - PatchageModule* m = find_module(client_id, type); - if (!m) { - m = new PatchageModule(patchage, client_name, type); - m->load_location(); - _app->canvas()->add_module(client_name, m); - _modules.insert(std::make_pair(client_id, m)); - } - return m; + int ret = snd_seq_open(&_seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + if (ret) { + _log.error("[ALSA] Unable to attach"); + _seq = nullptr; + } else { + _emit_event(event::DriverAttached{ClientType::alsa}); + + snd_seq_set_client_name(_seq, "Patchage"); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 50000); + + ret = + pthread_create(&_refresh_thread, &attr, &AlsaDriver::refresh_main, this); + if (ret) { + _log.error("[ALSA] Failed to start refresh thread"); + } + } } void -AlsaDriver::create_port_view_internal( - Patchage* patchage, - snd_seq_addr_t addr, - PatchageModule*& m, - PatchagePort*& port) +AlsaDriver::detach() { - if (ignore(addr)) - return; - - snd_seq_client_info_t* cinfo; - snd_seq_client_info_alloca(&cinfo); - snd_seq_client_info_set_client(cinfo, addr.client); - snd_seq_get_any_client_info(_seq, addr.client, cinfo); - - snd_seq_port_info_t* pinfo; - snd_seq_port_info_alloca(&pinfo); - snd_seq_port_info_set_client(pinfo, addr.client); - snd_seq_port_info_set_port(pinfo, addr.port); - snd_seq_get_any_port_info(_seq, addr.client, addr.port, pinfo); - - const string client_name = snd_seq_client_info_get_name(cinfo); - const string port_name = snd_seq_port_info_get_name(pinfo); - bool is_input = false; - bool is_duplex = false; - bool is_application = true; - - int caps = snd_seq_port_info_get_capability(pinfo); - int type = snd_seq_port_info_get_type(pinfo); - - // Figure out direction - if ((caps & SND_SEQ_PORT_CAP_READ) && (caps & SND_SEQ_PORT_CAP_WRITE)) - is_duplex = true; - else if (caps & SND_SEQ_PORT_CAP_READ) - is_input = false; - else if (caps & SND_SEQ_PORT_CAP_WRITE) - is_input = true; - - is_application = (type & SND_SEQ_PORT_TYPE_APPLICATION); - - // Because there would be name conflicts, we must force a split if (stupid) - // alsa duplex ports are present on the client - bool split = false; - if (is_duplex) { - split = true; - if (!_app->conf()->get_module_split(client_name, !is_application)) { - _app->conf()->set_module_split(client_name, true); - } - } else { - split = _app->conf()->get_module_split(client_name, !is_application); - } - - /*cout << "ALSA PORT: " << client_name << " : " << port_name - << " is_application = " << is_application - << " is_duplex = " << is_duplex - << " split = " << split << endl;*/ - - if (!split) { - m = find_or_create_module(_app, addr.client, client_name, InputOutput); - if (!m->get_port(port_name)) { - port = create_port(*m, port_name, is_input, addr); - port->show(); - } - - } else { // split - ModuleType type = ((is_input) ? Input : Output); - m = find_or_create_module(_app, addr.client, client_name, type); - if (!m->get_port(port_name)) { - port = create_port(*m, port_name, is_input, addr); - port->show(); - } - - if (is_duplex) { - type = ((!is_input) ? Input : Output); - m = find_or_create_module(_app, addr.client, client_name, type); - if (!m->get_port(port_name)) { - port = create_port(*m, port_name, !is_input, addr); - port->show(); - } - } - } + if (_seq) { + pthread_cancel(_refresh_thread); + pthread_join(_refresh_thread, nullptr); + snd_seq_close(_seq); + _seq = nullptr; + _emit_event(event::DriverDetached{ClientType::alsa}); + } } -PatchagePort* -AlsaDriver::create_port(PatchageModule& parent, - const string& name, bool is_input, snd_seq_addr_t addr) +void +AlsaDriver::refresh(const EventSink& sink) { - PatchagePort* ret = new PatchagePort( - parent, ALSA_MIDI, name, "", is_input, - _app->conf()->get_port_color(ALSA_MIDI), - _app->show_human_names()); - - dynamic_cast<PatchageCanvas*>(parent.canvas())->index_port( - PortID(addr, is_input), ret); - - _app->canvas()->index_port(PortID(addr, is_input), ret); - _port_addrs.insert(std::make_pair(ret, PortID(addr, is_input))); - return ret; + if (!is_attached() || !_seq) { + return; + } + + _ignored.clear(); + + snd_seq_client_info_t* cinfo = nullptr; + snd_seq_client_info_alloca(&cinfo); + snd_seq_client_info_set_client(cinfo, -1); + + snd_seq_port_info_t* pinfo = nullptr; + snd_seq_port_info_alloca(&pinfo); + + // Emit all clients + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(_seq, cinfo) >= 0) { + const auto client_id = snd_seq_client_info_get_client(cinfo); + + assert(client_id < std::numeric_limits<uint8_t>::max()); + sink({event::ClientCreated{ClientID::alsa(static_cast<uint8_t>(client_id)), + client_info(cinfo)}}); + } + + // Emit all ports + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(_seq, cinfo) >= 0) { + const auto client_id = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client_id); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(_seq, pinfo) >= 0) { + const auto addr = *snd_seq_port_info_get_addr(pinfo); + if (!ignore(addr)) { + const auto caps = snd_seq_port_info_get_capability(pinfo); + auto info = port_info(pinfo); + + if (caps & SND_SEQ_PORT_CAP_READ) { + info.direction = SignalDirection::output; + sink({event::PortCreated{addr_to_id(addr, false), info}}); + } + + if (caps & SND_SEQ_PORT_CAP_WRITE) { + info.direction = SignalDirection::input; + sink({event::PortCreated{addr_to_id(addr, true), info}}); + } + } + } + } + + // Emit all connections + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(_seq, cinfo) >= 0) { + const auto client_id = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client_id); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(_seq, pinfo) >= 0) { + const auto port_addr = *snd_seq_port_info_get_addr(pinfo); + const auto caps = snd_seq_port_info_get_capability(pinfo); + + if (!ignore(port_addr) && (caps & SND_SEQ_PORT_CAP_READ)) { + const auto tail_id = addr_to_id(port_addr, false); + + snd_seq_query_subscribe_t* sinfo = nullptr; + snd_seq_query_subscribe_alloca(&sinfo); + snd_seq_query_subscribe_set_type(sinfo, SND_SEQ_QUERY_SUBS_READ); + snd_seq_query_subscribe_set_root(sinfo, &port_addr); + snd_seq_query_subscribe_set_index(sinfo, 0); + while (!snd_seq_query_port_subscribers(_seq, sinfo)) { + const auto head_addr = *snd_seq_query_subscribe_get_addr(sinfo); + const auto head_id = addr_to_id(head_addr, true); + + sink({event::PortsConnected{tail_id, head_id}}); + + snd_seq_query_subscribe_set_index( + sinfo, snd_seq_query_subscribe_get_index(sinfo) + 1); + } + } + } + } } bool AlsaDriver::ignore(const snd_seq_addr_t& addr, bool add) { - if (_ignored.find(addr) != _ignored.end()) - return true; - - if (!add) - return false; - - snd_seq_client_info_t* cinfo; - snd_seq_client_info_alloca(&cinfo); - snd_seq_client_info_set_client(cinfo, addr.client); - snd_seq_get_any_client_info(_seq, addr.client, cinfo); - - snd_seq_port_info_t* pinfo; - snd_seq_port_info_alloca(&pinfo); - snd_seq_port_info_set_client(pinfo, addr.client); - snd_seq_port_info_set_port(pinfo, addr.port); - snd_seq_get_any_port_info(_seq, addr.client, addr.port, pinfo); - - const int type = snd_seq_port_info_get_type(pinfo); - const int caps = snd_seq_port_info_get_capability(pinfo); - - if (caps & SND_SEQ_PORT_CAP_NO_EXPORT) { - _ignored.insert(addr); - return true; - } else if ( !( (caps & SND_SEQ_PORT_CAP_READ) - || (caps & SND_SEQ_PORT_CAP_WRITE) - || (caps & SND_SEQ_PORT_CAP_DUPLEX))) { - _ignored.insert(addr); - return true; - } else if ((snd_seq_client_info_get_type(cinfo) != SND_SEQ_USER_CLIENT) - && ((type == SND_SEQ_PORT_SYSTEM_TIMER - || type == SND_SEQ_PORT_SYSTEM_ANNOUNCE))) { - _ignored.insert(addr); - return true; - } - - return false; + if (_ignored.find(addr) != _ignored.end()) { + return true; + } + + if (!add) { + return false; + } + + snd_seq_client_info_t* cinfo = nullptr; + snd_seq_client_info_alloca(&cinfo); + snd_seq_client_info_set_client(cinfo, addr.client); + snd_seq_get_any_client_info(_seq, addr.client, cinfo); + + snd_seq_port_info_t* pinfo = nullptr; + snd_seq_port_info_alloca(&pinfo); + snd_seq_port_info_set_client(pinfo, addr.client); + snd_seq_port_info_set_port(pinfo, addr.port); + snd_seq_get_any_port_info(_seq, addr.client, addr.port, pinfo); + + const int type = snd_seq_port_info_get_type(pinfo); + const int caps = snd_seq_port_info_get_capability(pinfo); + + if (caps & SND_SEQ_PORT_CAP_NO_EXPORT) { + _ignored.insert(addr); + return true; + } + + if (!((caps & SND_SEQ_PORT_CAP_READ) || (caps & SND_SEQ_PORT_CAP_WRITE) || + (caps & SND_SEQ_PORT_CAP_DUPLEX))) { + _ignored.insert(addr); + return true; + } + + if ((snd_seq_client_info_get_type(cinfo) != SND_SEQ_USER_CLIENT) && + ((type == SND_SEQ_PORT_SYSTEM_TIMER || + type == SND_SEQ_PORT_SYSTEM_ANNOUNCE))) { + _ignored.insert(addr); + return true; + } + + return false; } -/** Connects two Alsa Midi ports. - * - * \return Whether connection succeeded. - */ bool -AlsaDriver::connect(PatchagePort* src_port, - PatchagePort* dst_port) +AlsaDriver::connect(const PortID& tail_id, const PortID& head_id) { - PortAddrs::const_iterator s = _port_addrs.find(src_port); - PortAddrs::const_iterator d = _port_addrs.find(dst_port); - - if (s == _port_addrs.end() || d == _port_addrs.end()) { - _app->error_msg("Alsa: Attempt to connect port with no address."); - return false; - } - - const PortID src = s->second; - const PortID dst = d->second; - - if (src.id.alsa_addr.client == dst.id.alsa_addr.client - && src.id.alsa_addr.port == dst.id.alsa_addr.port) { - _app->warning_msg("Alsa: Refusing to connect port to itself."); - return false; - } - - bool result = true; - - snd_seq_port_subscribe_t* subs; - snd_seq_port_subscribe_malloc(&subs); - snd_seq_port_subscribe_set_sender(subs, &src.id.alsa_addr); - snd_seq_port_subscribe_set_dest(subs, &dst.id.alsa_addr); - snd_seq_port_subscribe_set_exclusive(subs, 0); - snd_seq_port_subscribe_set_time_update(subs, 0); - snd_seq_port_subscribe_set_time_real(subs, 0); - - // Already connected (shouldn't happen) - if (!snd_seq_get_port_subscription(_seq, subs)) { - _app->error_msg("Alsa: Attempt to double subscribe ports."); - result = false; - } - - int ret = snd_seq_subscribe_port(_seq, subs); - if (ret < 0) { - _app->error_msg((format("Alsa: Subscription failed (%1%).") - % snd_strerror(ret)).str()); - result = false; - } - - if (result) - _app->info_msg(string("Alsa: Connected ") - + src_port->full_name() + " => " + dst_port->full_name()); - else - _app->error_msg(string("Alsa: Unable to connect ") - + src_port->full_name() + " => " + dst_port->full_name()); - - return (!result); + if (tail_id.type() != PortID::Type::alsa || + head_id.type() != PortID::Type::alsa) { + _log.error("[ALSA] Attempt to connect non-ALSA ports"); + return false; + } + + const snd_seq_addr_t tail_addr = {tail_id.alsa_client(), tail_id.alsa_port()}; + const snd_seq_addr_t head_addr = {head_id.alsa_client(), head_id.alsa_port()}; + + if (tail_addr.client == head_addr.client && + tail_addr.port == head_addr.port) { + _log.warning("[ALSA] Refusing to connect port to itself"); + return false; + } + + bool result = true; + + snd_seq_port_subscribe_t* subs = nullptr; + snd_seq_port_subscribe_malloc(&subs); + snd_seq_port_subscribe_set_sender(subs, &tail_addr); + snd_seq_port_subscribe_set_dest(subs, &head_addr); + snd_seq_port_subscribe_set_exclusive(subs, 0); + snd_seq_port_subscribe_set_time_update(subs, 0); + snd_seq_port_subscribe_set_time_real(subs, 0); + + // Already connected (shouldn't happen) + if (!snd_seq_get_port_subscription(_seq, subs)) { + _log.error("[ALSA] Attempt to double subscribe ports"); + result = false; + } + + const int ret = snd_seq_subscribe_port(_seq, subs); + if (ret < 0) { + _log.error( + fmt::format("[ALSA] Subscription failed ({})", snd_strerror(ret))); + result = false; + } + + if (!result) { + _log.error( + fmt::format("[ALSA] Failed to connect {} => {}", tail_id, head_id)); + } + + return (!result); } -/** Disconnects two Alsa Midi ports. - * - * \return Whether disconnection succeeded. - */ bool -AlsaDriver::disconnect(PatchagePort* src_port, - PatchagePort* dst_port) +AlsaDriver::disconnect(const PortID& tail_id, const PortID& head_id) { - PortAddrs::const_iterator s = _port_addrs.find(src_port); - PortAddrs::const_iterator d = _port_addrs.find(dst_port); - - if (s == _port_addrs.end() || d == _port_addrs.end()) { - _app->error_msg("Alsa: Attempt to connect port with no address"); - return false; - } - - const PortID src = s->second; - const PortID dst = d->second; - - snd_seq_port_subscribe_t* subs; - snd_seq_port_subscribe_malloc(&subs); - snd_seq_port_subscribe_set_sender(subs, &src.id.alsa_addr); - snd_seq_port_subscribe_set_dest(subs, &dst.id.alsa_addr); - snd_seq_port_subscribe_set_exclusive(subs, 0); - snd_seq_port_subscribe_set_time_update(subs, 0); - snd_seq_port_subscribe_set_time_real(subs, 0); - - // Not connected (shouldn't happen) - if (snd_seq_get_port_subscription(_seq, subs) != 0) { - _app->error_msg("Alsa: Attempt to unsubscribe ports that are not subscribed."); - return false; - } - - int ret = snd_seq_unsubscribe_port(_seq, subs); - if (ret < 0) { - _app->error_msg(string("Alsa: Unable to disconnect ") - + src_port->full_name() + " => " + dst_port->full_name() - + "(" + snd_strerror(ret) + ")"); - return false; - } - - _app->info_msg(string("Alsa: Disconnected ") - + src_port->full_name() + " => " + dst_port->full_name()); - - return true; + if (tail_id.type() != PortID::Type::alsa || + head_id.type() != PortID::Type::alsa) { + _log.error("[ALSA] Attempt to disconnect non-ALSA ports"); + return false; + } + + const snd_seq_addr_t tail_addr = {tail_id.alsa_client(), tail_id.alsa_port()}; + const snd_seq_addr_t head_addr = {head_id.alsa_client(), head_id.alsa_port()}; + + snd_seq_port_subscribe_t* subs = nullptr; + snd_seq_port_subscribe_malloc(&subs); + snd_seq_port_subscribe_set_sender(subs, &tail_addr); + snd_seq_port_subscribe_set_dest(subs, &head_addr); + snd_seq_port_subscribe_set_exclusive(subs, 0); + snd_seq_port_subscribe_set_time_update(subs, 0); + snd_seq_port_subscribe_set_time_real(subs, 0); + + // Not connected (shouldn't happen) + if (snd_seq_get_port_subscription(_seq, subs) != 0) { + _log.error("[ALSA] Attempt to unsubscribe ports that are not subscribed"); + return false; + } + + const int ret = snd_seq_unsubscribe_port(_seq, subs); + if (ret < 0) { + _log.error(fmt::format("[ALSA] Failed to disconnect {} => {} ({})", + tail_id, + head_id, + snd_strerror(ret))); + return false; + } + + return true; } bool AlsaDriver::create_refresh_port() { - snd_seq_port_info_t* port_info; - snd_seq_port_info_alloca(&port_info); - snd_seq_port_info_set_name(port_info, "System Announcement Reciever"); - snd_seq_port_info_set_type(port_info, SND_SEQ_PORT_TYPE_APPLICATION); - snd_seq_port_info_set_capability(port_info, - SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE|SND_SEQ_PORT_CAP_NO_EXPORT); - - int ret = snd_seq_create_port(_seq, port_info); - if (ret) { - _app->error_msg((format("Alsa: Error creating port (%1%): ") - % snd_strerror(ret)).str()); - return false; - } - - // Subscribe the port to the system announcer - ret = snd_seq_connect_from(_seq, - snd_seq_port_info_get_port(port_info), - SND_SEQ_CLIENT_SYSTEM, - SND_SEQ_PORT_SYSTEM_ANNOUNCE); - if (ret) { - _app->error_msg((format("Alsa: Failed to connect to system announce port (%1%)") - % snd_strerror(ret)).str()); - return false; - } - - return true; + snd_seq_port_info_t* port_info = nullptr; + snd_seq_port_info_alloca(&port_info); + snd_seq_port_info_set_name(port_info, "System Announcement Receiver"); + snd_seq_port_info_set_type(port_info, SND_SEQ_PORT_TYPE_APPLICATION); + snd_seq_port_info_set_capability(port_info, + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE | + SND_SEQ_PORT_CAP_NO_EXPORT); + + int ret = snd_seq_create_port(_seq, port_info); + if (ret) { + _log.error( + fmt::format("[ALSA] Error creating port ({})", snd_strerror(ret))); + return false; + } + + // Subscribe the port to the system announcer + ret = snd_seq_connect_from(_seq, + snd_seq_port_info_get_port(port_info), + SND_SEQ_CLIENT_SYSTEM, + SND_SEQ_PORT_SYSTEM_ANNOUNCE); + if (ret) { + _log.error( + fmt::format("[ALSA] Failed to connect to system announce port ({})", + snd_strerror(ret))); + return false; + } + + return true; } void* AlsaDriver::refresh_main(void* me) { - AlsaDriver* ad = (AlsaDriver*)me; - ad->_refresh_main(); - return NULL; + auto* ad = static_cast<AlsaDriver*>(me); + ad->_refresh_main(); + return nullptr; } void AlsaDriver::_refresh_main() { - if (!create_refresh_port()) { - _app->error_msg("Alsa: Could not create listen port, auto-refresh disabled."); - return; - } - - int caps = 0; - - snd_seq_client_info_t* cinfo; - snd_seq_client_info_alloca(&cinfo); - - snd_seq_port_info_t* pinfo; - snd_seq_port_info_alloca(&pinfo); - - snd_seq_event_t* ev; - while (snd_seq_event_input(_seq, &ev) > 0) { - assert(ev); - - Glib::Mutex::Lock lock(_events_mutex); - - switch (ev->type) { - case SND_SEQ_EVENT_PORT_SUBSCRIBED: - if (!ignore(ev->data.connect.sender) && !ignore(ev->data.connect.dest)) - _events.push(PatchageEvent(PatchageEvent::CONNECTION, - ev->data.connect.sender, ev->data.connect.dest)); - break; - case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: - if (!ignore(ev->data.connect.sender) && !ignore(ev->data.connect.dest)) - _events.push(PatchageEvent(PatchageEvent::DISCONNECTION, - ev->data.connect.sender, ev->data.connect.dest)); - break; - case SND_SEQ_EVENT_PORT_START: - snd_seq_get_any_client_info(_seq, ev->data.addr.client, cinfo); - snd_seq_get_any_port_info(_seq, ev->data.addr.client, ev->data.addr.port, pinfo); - caps = snd_seq_port_info_get_capability(pinfo); - - if (!ignore(ev->data.addr)) - _events.push(PatchageEvent(PatchageEvent::PORT_CREATION, - PortID(ev->data.addr, (caps & SND_SEQ_PORT_CAP_READ)))); - break; - case SND_SEQ_EVENT_PORT_EXIT: - if (!ignore(ev->data.addr, false)) { - // Note: getting caps at this point does not work - // Delete both inputs and outputs (in case this is a duplex port) - _events.push(PatchageEvent(PatchageEvent::PORT_DESTRUCTION, - PortID(ev->data.addr, true))); - _events.push(PatchageEvent(PatchageEvent::PORT_DESTRUCTION, - PortID(ev->data.addr, false))); - _port_addrs.erase(_app->canvas()->find_port( - PortID(ev->data.addr, false))); - _port_addrs.erase(_app->canvas()->find_port( - PortID(ev->data.addr, true))); - } - break; - case SND_SEQ_EVENT_CLIENT_CHANGE: - case SND_SEQ_EVENT_CLIENT_EXIT: - case SND_SEQ_EVENT_CLIENT_START: - case SND_SEQ_EVENT_PORT_CHANGE: - case SND_SEQ_EVENT_RESET: - default: - //_events.push(PatchageEvent(PatchageEvent::REFRESH)); - break; - } - } + if (!create_refresh_port()) { + _log.error("[ALSA] Could not create listen port, auto-refresh disabled"); + return; + } + + int caps = 0; + + snd_seq_client_info_t* cinfo = nullptr; + snd_seq_client_info_alloca(&cinfo); + + snd_seq_port_info_t* pinfo = nullptr; + snd_seq_port_info_alloca(&pinfo); + + snd_seq_event_t* ev = nullptr; + while (snd_seq_event_input(_seq, &ev) > 0) { + assert(ev); + + switch (ev->type) { + case SND_SEQ_EVENT_CLIENT_START: + snd_seq_get_any_client_info(_seq, ev->data.addr.client, cinfo); + _emit_event(event::ClientCreated{ + ClientID::alsa(ev->data.addr.client), + client_info(cinfo), + }); + break; + + case SND_SEQ_EVENT_CLIENT_EXIT: + _emit_event(event::ClientDestroyed{ + ClientID::alsa(ev->data.addr.client), + }); + break; + + case SND_SEQ_EVENT_CLIENT_CHANGE: + break; + + case SND_SEQ_EVENT_PORT_START: + snd_seq_get_any_client_info(_seq, ev->data.addr.client, cinfo); + snd_seq_get_any_port_info( + _seq, ev->data.addr.client, ev->data.addr.port, pinfo); + caps = snd_seq_port_info_get_capability(pinfo); + + if (!ignore(ev->data.addr)) { + _emit_event(event::PortCreated{ + addr_to_id(ev->data.addr, (caps & SND_SEQ_PORT_CAP_WRITE)), + port_info(pinfo), + }); + } + break; + + case SND_SEQ_EVENT_PORT_EXIT: + if (!ignore(ev->data.addr, false)) { + // Note: getting caps at this point does not work + // Delete both inputs and outputs (to handle duplex ports) + _emit_event(event::PortDestroyed{addr_to_id(ev->data.addr, true)}); + _emit_event(event::PortDestroyed{addr_to_id(ev->data.addr, false)}); + } + break; + + case SND_SEQ_EVENT_PORT_CHANGE: + break; + + case SND_SEQ_EVENT_PORT_SUBSCRIBED: + if (!ignore(ev->data.connect.sender) && !ignore(ev->data.connect.dest)) { + _emit_event( + event::PortsConnected{addr_to_id(ev->data.connect.sender, false), + addr_to_id(ev->data.connect.dest, true)}); + } + break; + + case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: + if (!ignore(ev->data.connect.sender) && !ignore(ev->data.connect.dest)) { + _emit_event( + event::PortsDisconnected{addr_to_id(ev->data.connect.sender, false), + addr_to_id(ev->data.connect.dest, true)}); + } + break; + + case SND_SEQ_EVENT_RESET: + default: + break; + } + } } -void -AlsaDriver::process_events(Patchage* app) +} // namespace + +std::unique_ptr<Driver> +make_alsa_driver(ILog& log, Driver::EventSink emit_event) { - Glib::Mutex::Lock lock(_events_mutex); - while (!_events.empty()) { - PatchageEvent& ev = _events.front(); - ev.execute(app); - _events.pop(); - } + return std::unique_ptr<Driver>{new AlsaDriver{log, std::move(emit_event)}}; } + +} // namespace patchage diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp deleted file mode 100644 index 8bf837a..0000000 --- a/src/AlsaDriver.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_ALSADRIVER_HPP -#define PATCHAGE_ALSADRIVER_HPP - -#include <queue> -#include <set> -#include <string> -#include <map> - -#include <alsa/asoundlib.h> -#include <pthread.h> - -#include "Driver.hpp" -#include "PatchageModule.hpp" - -class Patchage; -class PatchagePort; - -/** Handles all externally driven functionality, registering ports etc. - */ -class AlsaDriver : public Driver -{ -public: - explicit AlsaDriver(Patchage* app); - ~AlsaDriver(); - - void attach(bool launch_daemon = false); - void detach(); - - bool is_attached() const { return (_seq != NULL); } - - void refresh(); - void destroy_all(); - - PatchagePort* create_port_view( - Patchage* patchage, - const PortID& id); - - bool connect(PatchagePort* src_port, - PatchagePort* dst_port); - - bool disconnect(PatchagePort* src_port, - PatchagePort* dst_port); - - void print_addr(snd_seq_addr_t addr); - - void process_events(Patchage* app); - -private: - bool create_refresh_port(); - static void* refresh_main(void* me); - void _refresh_main(); - - PatchageModule* find_module(uint8_t client_id, ModuleType type); - - PatchageModule* - find_or_create_module( - Patchage* patchage, - uint8_t client_id, - const std::string& client_name, - ModuleType type); - - void - create_port_view_internal( - Patchage* patchage, - snd_seq_addr_t addr, - PatchageModule*& parent, - PatchagePort*& port); - - PatchagePort* create_port( - PatchageModule& parent, - const std::string& name, - bool is_input, - snd_seq_addr_t addr); - - Patchage* _app; - snd_seq_t* _seq; - pthread_t _refresh_thread; - - Glib::Mutex _events_mutex; - std::queue<PatchageEvent> _events; - - struct SeqAddrComparator { - bool operator() (const snd_seq_addr_t& a, const snd_seq_addr_t& b) const { - return ((a.client < b.client) || ((a.client == b.client) && a.port < b.port)); - } - }; - - typedef std::set<snd_seq_addr_t, SeqAddrComparator> Ignored; - Ignored _ignored; - - typedef std::multimap<uint8_t, PatchageModule*> Modules; - Modules _modules; - - typedef std::map<PatchagePort*, PortID> PortAddrs; - PortAddrs _port_addrs; - - bool ignore(const snd_seq_addr_t& addr, bool add=true); -}; - -#endif // PATCHAGE_ALSADRIVER_HPP diff --git a/src/AlsaStubDriver.cpp b/src/AlsaStubDriver.cpp new file mode 100644 index 0000000..5dc8f0b --- /dev/null +++ b/src/AlsaStubDriver.cpp @@ -0,0 +1,18 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "AudioDriver.hpp" +#include "Driver.hpp" +#include "make_alsa_driver.hpp" + +#include <memory> + +namespace patchage { + +std::unique_ptr<Driver> +make_alsa_driver(ILog&, Driver::EventSink) +{ + return nullptr; +} + +} // namespace patchage diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp new file mode 100644 index 0000000..ac3d5ec --- /dev/null +++ b/src/AudioDriver.hpp @@ -0,0 +1,40 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_AUDIODRIVER_HPP +#define PATCHAGE_AUDIODRIVER_HPP + +#include "Driver.hpp" + +#include <cstdint> +#include <utility> + +namespace patchage { + +/// Base class for drivers that work with an audio system +class AudioDriver : public Driver +{ +public: + explicit AudioDriver(EventSink emit_event) + : Driver{std::move(emit_event)} + {} + + /// Return the number of xruns (dropouts) since the last reset + virtual uint32_t xruns() = 0; + + /// Reset the xrun count + virtual void reset_xruns() = 0; + + /// Return the current buffer size in frames + virtual uint32_t buffer_size() = 0; + + /// Try to set the current buffer size in frames, return true on success + virtual bool set_buffer_size(uint32_t frames) = 0; + + /// Return the current sample rate in Hz + virtual uint32_t sample_rate() = 0; +}; + +} // namespace patchage + +#endif // PATCHAGE_AUDIODRIVER_HPP diff --git a/src/Canvas.cpp b/src/Canvas.cpp new file mode 100644 index 0000000..3624933 --- /dev/null +++ b/src/Canvas.cpp @@ -0,0 +1,342 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Canvas.hpp" + +#include "Action.hpp" +#include "ActionSink.hpp" +#include "CanvasModule.hpp" +#include "CanvasPort.hpp" +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "ClientType.hpp" +#include "Configuration.hpp" +#include "Coord.hpp" +#include "ILog.hpp" +#include "Metadata.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" +#include "PortNames.hpp" +#include "Setting.hpp" +#include "SignalDirection.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_GANV_WARNINGS +#include "ganv/Canvas.hpp" +#include "ganv/Edge.hpp" +#include "ganv/Module.hpp" +#include "ganv/Node.hpp" +#include "ganv/Port.hpp" +#include "ganv/module.h" +#include "ganv/types.h" +PATCHAGE_RESTORE_WARNINGS + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS + +#include <gdk/gdkkeysyms.h> +#include <sigc++/functors/mem_fun.h> +#include <sigc++/signal.h> + +#include <cassert> +#include <cstdlib> +#include <functional> +#include <optional> +#include <set> +#include <string> +#include <utility> + +namespace patchage { +namespace { + +struct RemovePortsData { + using Predicate = bool (*)(const CanvasPort*); + + explicit RemovePortsData(Predicate p) + : pred(p) + {} + + Predicate pred; + std::set<ClientID> empty_clients; +}; + +void +delete_port_if_matches(GanvPort* port, void* cdata) +{ + auto* data = static_cast<RemovePortsData*>(cdata); + auto* pport = dynamic_cast<CanvasPort*>(Glib::wrap(port)); + if (pport && data->pred(pport)) { + delete pport; + } +} + +void +remove_ports_matching(GanvNode* node, void* cdata) +{ + if (!GANV_IS_MODULE(node)) { + return; + } + + Ganv::Module* cmodule = Glib::wrap(GANV_MODULE(node)); + auto* pmodule = dynamic_cast<CanvasModule*>(cmodule); + if (!pmodule) { + return; + } + + auto* data = static_cast<RemovePortsData*>(cdata); + + pmodule->for_each_port(delete_port_if_matches, data); + + if (pmodule->num_ports() == 0) { + data->empty_clients.insert(pmodule->id()); + } +} + +} // namespace + +Canvas::Canvas(ILog& log, ActionSink& action_sink, int width, int height) + : Ganv::Canvas(width, height) + , _log(log) + , _action_sink(action_sink) +{ + signal_event.connect(sigc::mem_fun(this, &Canvas::on_event)); + signal_connect.connect(sigc::mem_fun(this, &Canvas::on_connect)); + signal_disconnect.connect(sigc::mem_fun(this, &Canvas::on_disconnect)); +} + +CanvasPort* +Canvas::create_port(Configuration& conf, + const Metadata& metadata, + const PortID& id, + const PortInfo& info) +{ + const auto client_id = id.client(); + + const auto port_name = + ((id.type() == PortID::Type::alsa) ? info.label : PortNames(id).port()); + + // Figure out the client name, for ALSA we need the metadata cache + std::string client_name; + if (id.type() == PortID::Type::alsa) { + const auto client_info = metadata.client(client_id); + if (!client_info) { + _log.error(fmt::format( + u8"(Unable to add port “{}”, client “{}” is unknown)", id, client_id)); + + return nullptr; + } + + client_name = client_info->label; + } else { + client_name = PortNames(id).client(); + } + + // Determine the module type to place the port on in case of splitting + SignalDirection module_type = SignalDirection::duplex; + if (conf.get_module_split(client_name, info.is_terminal)) { + module_type = info.direction; + } + + // Find or create parent module + CanvasModule* parent = find_module(client_id, module_type); + if (!parent) { + // Determine initial position + Coord loc; + if (!conf.get_module_location(client_name, module_type, loc)) { + // No position saved, come up with a pseudo-random one + loc.x = 20 + rand() % 640; + loc.y = 20 + rand() % 480; + + conf.set_module_location(client_name, module_type, loc); + } + + parent = new CanvasModule( + *this, _action_sink, client_name, module_type, client_id, loc.x, loc.y); + + add_module(client_id, parent); + } + + if (parent->get_port(id)) { + // TODO: Update existing port? + _log.error(fmt::format( + u8"(Module “{}” already has port “{}”)", client_name, port_name)); + return nullptr; + } + + auto* const port = new CanvasPort(*parent, + info.type, + id, + port_name, + info.label, + info.direction == SignalDirection::input, + conf.get_port_color(info.type), + conf.get<setting::HumanNames>(), + info.order); + + _port_index.insert(std::make_pair(id, port)); + + return port; +} + +CanvasModule* +Canvas::find_module(const ClientID& id, const SignalDirection type) +{ + auto i = _module_index.find(id); + + CanvasModule* io_module = nullptr; + for (; i != _module_index.end() && i->first == id; ++i) { + if (i->second->type() == type) { + return i->second; + } + + if (i->second->type() == SignalDirection::duplex) { + io_module = i->second; + } + } + + // Return duplex module for input or output (or nullptr if not found) + return io_module; +} + +void +Canvas::remove_module(const ClientID& id) +{ + auto i = _module_index.find(id); + while (i != _module_index.end() && i->first == id) { + delete i->second; + i = _module_index.erase(i); + } +} + +CanvasPort* +Canvas::find_port(const PortID& id) +{ + auto i = _port_index.find(id); + if (i != _port_index.end()) { + assert(i->second->get_module()); + return i->second; + } + + return nullptr; +} + +void +Canvas::remove_port(const PortID& id) +{ + CanvasPort* const port = find_port(id); + _port_index.erase(id); + delete port; +} + +void +Canvas::remove_ports(bool (*pred)(const CanvasPort*)) +{ + RemovePortsData data(pred); + + for_each_node(remove_ports_matching, &data); + + for (auto i = _port_index.begin(); i != _port_index.end();) { + auto next = i; + ++next; + if (pred(i->second)) { + _port_index.erase(i); + } + i = next; + } + + for (const ClientID& id : data.empty_clients) { + remove_module(id); + } +} + +void +Canvas::on_connect(Ganv::Node* port1, Ganv::Node* port2) +{ + auto* const p1 = dynamic_cast<CanvasPort*>(port1); + auto* const p2 = dynamic_cast<CanvasPort*>(port2); + + if (p1 && p2) { + if (p1->is_output() && p2->is_input()) { + _action_sink(action::ConnectPorts{p1->id(), p2->id()}); + } else if (p2->is_output() && p1->is_input()) { + _action_sink(action::ConnectPorts{p2->id(), p1->id()}); + } + } +} + +void +Canvas::on_disconnect(Ganv::Node* port1, Ganv::Node* port2) +{ + auto* const p1 = dynamic_cast<CanvasPort*>(port1); + auto* const p2 = dynamic_cast<CanvasPort*>(port2); + + if (p1 && p2) { + if (p1->is_output() && p2->is_input()) { + _action_sink(action::DisconnectPorts{p1->id(), p2->id()}); + } else if (p2->is_output() && p1->is_input()) { + _action_sink(action::DisconnectPorts{p2->id(), p1->id()}); + } + } +} + +void +Canvas::add_module(const ClientID& id, CanvasModule* module) +{ + _module_index.emplace(id, module); + + // Join partners, if applicable + CanvasModule* in_module = nullptr; + CanvasModule* out_module = nullptr; + if (module->type() == SignalDirection::input) { + in_module = module; + out_module = find_module(id, SignalDirection::output); + } else if (module->type() == SignalDirection::output) { + in_module = find_module(id, SignalDirection::input); + out_module = module; + } + + if (in_module && out_module) { + out_module->set_partner(in_module); + } +} + +void +disconnect_edge(GanvEdge* edge, void* data) +{ + auto* canvas = static_cast<Canvas*>(data); + Ganv::Edge* edgemm = Glib::wrap(edge); + + if (canvas && edgemm) { + canvas->on_disconnect(edgemm->get_tail(), edgemm->get_head()); + } +} + +bool +Canvas::on_event(GdkEvent* ev) +{ + if (ev->type == GDK_KEY_PRESS && ev->key.keyval == GDK_KEY_Delete) { + for_each_selected_edge(disconnect_edge, this); + clear_selection(); + return true; + } + + return false; +} + +bool +Canvas::make_connection(Ganv::Node* tail, Ganv::Node* head) +{ + new Ganv::Edge(*this, tail, head); + return true; +} + +void +Canvas::clear() +{ + _port_index.clear(); + _module_index.clear(); + Ganv::Canvas::clear(); +} + +} // namespace patchage diff --git a/src/Canvas.hpp b/src/Canvas.hpp new file mode 100644 index 0000000..81e4d61 --- /dev/null +++ b/src/Canvas.hpp @@ -0,0 +1,81 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_CANVAS_HPP +#define PATCHAGE_CANVAS_HPP + +#include "ActionSink.hpp" +#include "ClientID.hpp" +#include "PortID.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_GANV_WARNINGS +#include "ganv/Canvas.hpp" +#include "ganv/types.h" +PATCHAGE_RESTORE_WARNINGS + +#include <gdk/gdk.h> + +#include <map> + +namespace Ganv { +class Node; +} // namespace Ganv + +namespace patchage { + +enum class SignalDirection; + +struct PortInfo; + +class CanvasModule; +class CanvasPort; +class ILog; +class Metadata; +class Configuration; + +class Canvas : public Ganv::Canvas +{ +public: + Canvas(ILog& log, ActionSink& action_sink, int width, int height); + + CanvasPort* create_port(Configuration& conf, + const Metadata& metadata, + const PortID& id, + const PortInfo& info); + + CanvasModule* find_module(const ClientID& id, SignalDirection type); + CanvasPort* find_port(const PortID& id); + + void remove_module(const ClientID& id); + + void remove_ports(bool (*pred)(const CanvasPort*)); + + void add_module(const ClientID& id, CanvasModule* module); + + bool make_connection(Ganv::Node* tail, Ganv::Node* head); + + void remove_port(const PortID& id); + + void clear() override; + +private: + using PortIndex = std::map<const PortID, CanvasPort*>; + using ModuleIndex = std::multimap<const ClientID, CanvasModule*>; + + friend void disconnect_edge(GanvEdge*, void*); + + bool on_event(GdkEvent* ev); + + void on_connect(Ganv::Node* port1, Ganv::Node* port2); + void on_disconnect(Ganv::Node* port1, Ganv::Node* port2); + + ILog& _log; + ActionSink& _action_sink; + PortIndex _port_index; + ModuleIndex _module_index; +}; + +} // namespace patchage + +#endif // PATCHAGE_CANVAS_HPP diff --git a/src/CanvasModule.cpp b/src/CanvasModule.cpp new file mode 100644 index 0000000..4015819 --- /dev/null +++ b/src/CanvasModule.cpp @@ -0,0 +1,155 @@ +// Copyright 2010-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CanvasModule.hpp" + +#include "Action.hpp" +#include "ActionSink.hpp" +#include "Canvas.hpp" +#include "CanvasPort.hpp" +#include "ClientID.hpp" +#include "PortID.hpp" +#include "SignalDirection.hpp" +#include "i18n.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_GANV_WARNINGS +#include "ganv/Module.hpp" +#include "ganv/Port.hpp" +PATCHAGE_RESTORE_WARNINGS + +#include <gtkmm/menu.h> +#include <gtkmm/menu_elems.h> +#include <gtkmm/menuitem.h> +#include <gtkmm/menushell.h> +#include <sigc++/functors/mem_fun.h> +#include <sigc++/signal.h> + +#include <cassert> +#include <functional> +#include <memory> +#include <utility> + +namespace patchage { + +CanvasModule::CanvasModule(Canvas& canvas, + ActionSink& action_sink, + const std::string& name, + SignalDirection type, + ClientID id, + double x, + double y) + : Module(canvas, name, x, y) + , _action_sink(action_sink) + , _name(name) + , _type(type) + , _id(std::move(id)) +{ + signal_event().connect(sigc::mem_fun(this, &CanvasModule::on_event)); + signal_moved().connect(sigc::mem_fun(this, &CanvasModule::on_moved)); +} + +void +CanvasModule::update_menu() +{ + if (!_menu) { + return; + } + + if (_type == SignalDirection::duplex) { + bool has_in = false; + bool has_out = false; + for (const auto* p : *this) { + if (p) { + if (p->is_input()) { + has_in = true; + } else { + has_out = true; + } + + if (has_in && has_out) { + break; + } + } + } + + if (has_in && has_out) { + _menu->items()[0].show(); // Show "Split" menu item + } else { + _menu->items()[0].hide(); // Hide "Split" menu item + } + } +} + +bool +CanvasModule::show_menu(GdkEventButton* ev) +{ + _menu = std::make_unique<Gtk::Menu>(); + + Gtk::Menu::MenuList& items = _menu->items(); + + if (_type == SignalDirection::duplex) { + items.push_back(Gtk::Menu_Helpers::MenuElem( + T("_Split"), sigc::mem_fun(this, &CanvasModule::on_split))); + update_menu(); + } else { + items.push_back(Gtk::Menu_Helpers::MenuElem( + T("_Join"), sigc::mem_fun(this, &CanvasModule::on_join))); + } + + items.push_back(Gtk::Menu_Helpers::MenuElem( + T("_Disconnect"), sigc::mem_fun(this, &CanvasModule::on_disconnect))); + + _menu->popup(ev->button, ev->time); + return true; +} + +bool +CanvasModule::on_event(GdkEvent* ev) +{ + if (ev->type == GDK_BUTTON_PRESS && ev->button.button == 3) { + return show_menu(&ev->button); + } + return false; +} + +void +CanvasModule::on_moved(double x, double y) +{ + _action_sink(action::MoveModule{_id, _type, x, y}); +} + +void +CanvasModule::on_split() +{ + assert(_type == SignalDirection::duplex); + _action_sink(action::SplitModule{_id}); +} + +void +CanvasModule::on_join() +{ + assert(_type != SignalDirection::duplex); + _action_sink(action::UnsplitModule{_id}); +} + +void +CanvasModule::on_disconnect() +{ + _action_sink(action::DisconnectClient{_id, _type}); +} + +CanvasPort* +CanvasModule::get_port(const PortID& id) +{ + for (Ganv::Port* p : *this) { + auto* pport = dynamic_cast<CanvasPort*>(p); + if (pport && pport->id() == id) { + return pport; + } + } + + return nullptr; +} + +} // namespace patchage diff --git a/src/CanvasModule.hpp b/src/CanvasModule.hpp new file mode 100644 index 0000000..e15a6b8 --- /dev/null +++ b/src/CanvasModule.hpp @@ -0,0 +1,74 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_CANVASMODULE_HPP +#define PATCHAGE_CANVASMODULE_HPP + +#include "ActionSink.hpp" +#include "ClientID.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_GANV_WARNINGS +#include "ganv/Module.hpp" +PATCHAGE_RESTORE_WARNINGS + +#include <gdk/gdk.h> +#include <gtkmm/menu.h> + +#include <memory> +#include <string> + +namespace patchage { + +enum class SignalDirection; + +struct PortID; + +class Canvas; +class CanvasPort; + +class CanvasModule : public Ganv::Module +{ +public: + CanvasModule(Canvas& canvas, + ActionSink& action_sink, + const std::string& name, + SignalDirection type, + ClientID id, + double x, + double y); + + CanvasModule(const CanvasModule&) = delete; + CanvasModule& operator=(const CanvasModule&) = delete; + + CanvasModule(CanvasModule&&) = delete; + CanvasModule& operator=(CanvasModule&&) = delete; + + ~CanvasModule() override = default; + + bool show_menu(GdkEventButton* ev); + void update_menu(); + + CanvasPort* get_port(const PortID& id); + + SignalDirection type() const { return _type; } + ClientID id() const { return _id; } + const std::string& name() const { return _name; } + +protected: + bool on_event(GdkEvent* ev) override; + void on_moved(double x, double y); + void on_split(); + void on_join(); + void on_disconnect(); + + ActionSink& _action_sink; + std::unique_ptr<Gtk::Menu> _menu; + std::string _name; + SignalDirection _type; + ClientID _id; +}; + +} // namespace patchage + +#endif // PATCHAGE_CANVASMODULE_HPP diff --git a/src/CanvasPort.hpp b/src/CanvasPort.hpp new file mode 100644 index 0000000..0fc2f04 --- /dev/null +++ b/src/CanvasPort.hpp @@ -0,0 +1,108 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_CANVASPORT_HPP +#define PATCHAGE_CANVASPORT_HPP + +#include "PortID.hpp" +#include "PortType.hpp" +#include "i18n.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_GANV_WARNINGS +#include "ganv/Port.hpp" +PATCHAGE_RESTORE_WARNINGS + +#include <gdk/gdk.h> +#include <gtkmm/menu.h> +#include <gtkmm/menu_elems.h> +#include <gtkmm/menushell.h> +#include <gtkmm/object.h> +#include <sigc++/functors/mem_fun.h> +#include <sigc++/signal.h> + +#include <cstdint> +#include <optional> +#include <string> +#include <utility> + +namespace Ganv { +class Module; +} // namespace Ganv + +namespace patchage { + +/// A port on a CanvasModule +class CanvasPort : public Ganv::Port +{ +public: + CanvasPort(Ganv::Module& module, + PortType type, + PortID id, + const std::string& name, + const std::string& human_name, + bool is_input, + uint32_t color, + bool show_human_name, + std::optional<int> order = std::optional<int>()) + : Port(module, + (show_human_name && !human_name.empty()) ? human_name : name, + is_input, + color) + , _type(type) + , _id(std::move(id)) + , _name(name) + , _human_name(human_name) + , _order(order) + { + signal_event().connect(sigc::mem_fun(this, &CanvasPort::on_event)); + } + + CanvasPort(const CanvasPort&) = delete; + CanvasPort& operator=(const CanvasPort&) = delete; + + CanvasPort(CanvasPort&&) = delete; + CanvasPort& operator=(CanvasPort&&) = delete; + + ~CanvasPort() override = default; + + void show_human_name(bool human) + { + if (human && !_human_name.empty()) { + set_label(_human_name.c_str()); + } else { + set_label(_name.c_str()); + } + } + + bool on_event(GdkEvent* ev) override + { + if (ev->type != GDK_BUTTON_PRESS || ev->button.button != 3) { + return false; + } + + Gtk::Menu* menu = Gtk::manage(new Gtk::Menu()); + menu->items().push_back(Gtk::Menu_Helpers::MenuElem( + T("Disconnect"), sigc::mem_fun(this, &Port::disconnect))); + + menu->popup(ev->button.button, ev->button.time); + return true; + } + + PortType type() const { return _type; } + PortID id() const { return _id; } + const std::string& name() const { return _name; } + const std::string& human_name() const { return _human_name; } + const std::optional<int>& order() const { return _order; } + +private: + PortType _type; + PortID _id; + std::string _name; + std::string _human_name; + std::optional<int> _order; +}; + +} // namespace patchage + +#endif // PATCHAGE_CANVASPORT_HPP diff --git a/src/ClientID.hpp b/src/ClientID.hpp new file mode 100644 index 0000000..d73e45e --- /dev/null +++ b/src/ClientID.hpp @@ -0,0 +1,123 @@ +// Copyright 2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_CLIENTID_HPP +#define PATCHAGE_CLIENTID_HPP + +#include "ClientType.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +#include <fmt/ostream.h> +PATCHAGE_RESTORE_WARNINGS + +#include <cassert> +#include <cstdint> +#include <ostream> +#include <string> +#include <utility> + +namespace patchage { + +/// An ID for some client (program) that has ports +struct ClientID { + using Type = ClientType; + + ClientID(const ClientID& copy) = default; + ClientID& operator=(const ClientID& copy) = default; + + ClientID(ClientID&& id) = default; + ClientID& operator=(ClientID&& id) = default; + + ~ClientID() = default; + + /// Return an ID for a JACK client by name + static ClientID jack(std::string name) + { + return ClientID{Type::jack, std::move(name)}; + } + + /// Return an ID for an ALSA Sequencer client by ID + static ClientID alsa(const uint8_t id) { return ClientID{Type::alsa, id}; } + + Type type() const { return _type; } + const std::string& jack_name() const { return _jack_name; } + uint8_t alsa_id() const { return _alsa_id; } + +private: + ClientID(const Type type, std::string jack_name) + : _type{type} + , _jack_name{std::move(jack_name)} + { + assert(_type == Type::jack); + } + + ClientID(const Type type, const uint8_t alsa_id) + : _type{type} + , _alsa_id{alsa_id} + { + assert(_type == Type::alsa); + } + + Type _type; ///< Determines which field is active + std::string _jack_name{}; ///< Client name for Type::jack + uint8_t _alsa_id{}; ///< Client ID for Type::alsa +}; + +inline std::ostream& +operator<<(std::ostream& os, const ClientID& id) +{ + switch (id.type()) { + case ClientID::Type::jack: + return os << "jack:" << id.jack_name(); + case ClientID::Type::alsa: + return os << "alsa:" << int(id.alsa_id()); + } + + assert(false); + return os; +} + +inline bool +operator==(const ClientID& lhs, const ClientID& rhs) +{ + if (lhs.type() != rhs.type()) { + return false; + } + + switch (lhs.type()) { + case ClientID::Type::jack: + return lhs.jack_name() == rhs.jack_name(); + case ClientID::Type::alsa: + return lhs.alsa_id() == rhs.alsa_id(); + } + + assert(false); + return false; +} + +inline bool +operator<(const ClientID& lhs, const ClientID& rhs) +{ + if (lhs.type() != rhs.type()) { + return lhs.type() < rhs.type(); + } + + switch (lhs.type()) { + case ClientID::Type::jack: + return lhs.jack_name() < rhs.jack_name(); + case ClientID::Type::alsa: + return lhs.alsa_id() < rhs.alsa_id(); + } + + assert(false); + return false; +} + +} // namespace patchage + +template<> +struct fmt::formatter<patchage::ClientID> : fmt::ostream_formatter {}; + +#endif // PATCHAGE_CLIENTID_HPP diff --git a/src/ClientInfo.hpp b/src/ClientInfo.hpp new file mode 100644 index 0000000..2dd755e --- /dev/null +++ b/src/ClientInfo.hpp @@ -0,0 +1,18 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_CLIENTINFO_HPP +#define PATCHAGE_CLIENTINFO_HPP + +#include <string> + +namespace patchage { + +/// Extra information about a client (program) not expressed in its ID +struct ClientInfo { + std::string label; ///< Human-friendly label +}; + +} // namespace patchage + +#endif // PATCHAGE_CLIENTINFO_HPP diff --git a/src/ClientType.hpp b/src/ClientType.hpp new file mode 100644 index 0000000..7a3f87a --- /dev/null +++ b/src/ClientType.hpp @@ -0,0 +1,42 @@ +// Copyright 2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_CLIENTTYPE_HPP +#define PATCHAGE_CLIENTTYPE_HPP + +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +#include <fmt/ostream.h> +PATCHAGE_RESTORE_WARNINGS + +#include <ostream> + +namespace patchage { + +/// A type of client (program) with supported ports +enum class ClientType { + jack, + alsa, +}; + +inline std::ostream& +operator<<(std::ostream& os, const ClientType type) +{ + switch (type) { + case ClientType::jack: + return os << "JACK"; + case ClientType::alsa: + return os << "ALSA"; + } + + return os; +} + +} // namespace patchage + +template<> +struct fmt::formatter<patchage::ClientType> : fmt::ostream_formatter {}; + +#endif // PATCHAGE_CLIENTTYPE_HPP diff --git a/src/Configuration.cpp b/src/Configuration.cpp index d9537c0..9bb2ac6 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -1,333 +1,374 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later -#include <ctype.h> -#include <stdlib.h> +#include "Configuration.hpp" +#include "Coord.hpp" +#include "PortType.hpp" +#include "Setting.hpp" +#include "SignalDirection.hpp" +#include "patchage_config.h" + +#include <cctype> +#include <cstdlib> #include <fstream> -#include <ios> #include <iostream> #include <limits> -#include <stdexcept> +#include <utility> #include <vector> -#include "Configuration.hpp" -#include "Patchage.hpp" - -static const char* port_type_names[N_PORT_TYPES] = { - "JACK_AUDIO", - "JACK_MIDI", - "ALSA_MIDI", - "JACK_OSC", - "JACK_CV" -}; - -Configuration::Configuration() - : _window_location(0, 0) - , _window_size(640, 480) - , _zoom(1.0) - , _font_size(12.0) - , _messages_height(0) - , _show_toolbar(true) - , _show_messages(false) - , _sort_ports(true) +// IWYU pragma: no_include <algorithm> + +namespace patchage { +namespace { + +/// Return a vector of filenames in descending order by preference +std::vector<std::string> +get_filenames() +{ + std::vector<std::string> filenames; + const std::string prefix; + + const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char* home = getenv("HOME"); + + // XDG spec + if (xdg_config_home) { + filenames.push_back(std::string(xdg_config_home) + "/patchagerc"); + } else if (home) { + filenames.push_back(std::string(home) + "/.config/patchagerc"); + } + + // Old location + if (home) { + filenames.push_back(std::string(home) + "/.patchagerc"); + } + + // Current directory (bundle or last-ditch effort) + filenames.emplace_back("patchagerc"); + + return filenames; +} + +} // namespace + +static const char* const port_type_names[Configuration::n_port_types] = + {"JACK_AUDIO", "JACK_MIDI", "ALSA_MIDI", "JACK_OSC", "JACK_CV"}; + +Configuration::Configuration(std::function<void(const Setting&)> on_change) + : _on_change(std::move(on_change)) { -#ifdef PATCHAGE_USE_LIGHT_THEME - _port_colors[JACK_AUDIO] = _default_port_colors[JACK_AUDIO] = 0xA4BC8CFF; - _port_colors[JACK_MIDI] = _default_port_colors[JACK_MIDI] = 0xC89595FF; - _port_colors[ALSA_MIDI] = _default_port_colors[ALSA_MIDI] = 0x8F7198FF; - _port_colors[JACK_OSC] = _default_port_colors[JACK_OSC] = 0x7E8EAAFF; - _port_colors[JACK_CV] = _default_port_colors[JACK_CV] = 0x83AFABFF; + std::get<setting::FontSize>(_settings).value = 12.0f; + std::get<setting::WindowLocation>(_settings).value = Coord{0.0, 0.0}; + std::get<setting::WindowSize>(_settings).value = Coord{960.0, 540.0}; + std::get<setting::Zoom>(_settings).value = 1.0f; + +#if PATCHAGE_USE_LIGHT_THEME + _port_colors[static_cast<unsigned>(PortType::jack_audio)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_audio)] = + 0xA4BC8CFF; + + _port_colors[static_cast<unsigned>(PortType::jack_midi)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_midi)] = + 0xC89595FF; + + _port_colors[static_cast<unsigned>(PortType::alsa_midi)] = + _default_port_colors[static_cast<unsigned>(PortType::alsa_midi)] = + 0x8F7198FF; + + _port_colors[static_cast<unsigned>(PortType::jack_osc)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_osc)] = + 0x7E8EAAFF; + + _port_colors[static_cast<unsigned>(PortType::jack_cv)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_cv)] = 0x83AFABFF; #else - _port_colors[JACK_AUDIO] = _default_port_colors[JACK_AUDIO] = 0x3E5E00FF; - _port_colors[JACK_MIDI] = _default_port_colors[JACK_MIDI] = 0x650300FF; - _port_colors[ALSA_MIDI] = _default_port_colors[ALSA_MIDI] = 0x2D0043FF; - _port_colors[JACK_OSC] = _default_port_colors[JACK_OSC] = 0x4100FEFF; - _port_colors[JACK_CV] = _default_port_colors[JACK_CV] = 0x005E4EFF; + _port_colors[static_cast<unsigned>(PortType::jack_audio)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_audio)] = + 0x3E5E00FF; + + _port_colors[static_cast<unsigned>(PortType::jack_midi)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_midi)] = + 0x650300FF; + + _port_colors[static_cast<unsigned>(PortType::alsa_midi)] = + _default_port_colors[static_cast<unsigned>(PortType::alsa_midi)] = + 0x2D0043FF; + + _port_colors[static_cast<unsigned>(PortType::jack_osc)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_osc)] = + 0x4100FEFF; + + _port_colors[static_cast<unsigned>(PortType::jack_cv)] = + _default_port_colors[static_cast<unsigned>(PortType::jack_cv)] = 0x005E4EFF; #endif } bool -Configuration::get_module_location(const std::string& name, ModuleType type, Coord& loc) +Configuration::get_module_location(const std::string& name, + SignalDirection type, + Coord& loc) const { - std::map<std::string, ModuleSettings>::const_iterator i = _module_settings.find(name); - if (i == _module_settings.end()) { - return false; - } - - const ModuleSettings& settings = (*i).second; - if (type == Input && settings.input_location) { - loc = *settings.input_location; - } else if (type == Output && settings.output_location) { - loc = *settings.output_location; - } else if (type == InputOutput && settings.inout_location) { - loc = *settings.inout_location; - } else { - return false; - } - - return true; + auto i = _module_settings.find(name); + if (i == _module_settings.end()) { + return false; + } + + const ModuleSettings& settings = (*i).second; + if (type == SignalDirection::input && settings.input_location) { + loc = *settings.input_location; + } else if (type == SignalDirection::output && settings.output_location) { + loc = *settings.output_location; + } else if (type == SignalDirection::duplex && settings.inout_location) { + loc = *settings.inout_location; + } else { + return false; + } + + return true; } void -Configuration::set_module_location(const std::string& name, ModuleType type, Coord loc) +Configuration::set_module_location(const std::string& name, + SignalDirection type, + Coord loc) { - std::map<std::string, ModuleSettings>::iterator i = _module_settings.find(name); - if (i == _module_settings.end()) { - i = _module_settings.insert( - std::make_pair(name, ModuleSettings(type != InputOutput))).first; - } - - ModuleSettings& settings = (*i).second; - switch (type) { - case Input: - settings.input_location = loc; - break; - case Output: - settings.output_location = loc; - break; - case InputOutput: - settings.inout_location = loc; - break; - default: - break; // shouldn't reach here - } + if (name.empty()) { + return; + } + + auto i = _module_settings.find(name); + if (i == _module_settings.end()) { + i = _module_settings + .insert(std::make_pair( + name, ModuleSettings(type != SignalDirection::duplex))) + .first; + } + + ModuleSettings& settings = (*i).second; + switch (type) { + case SignalDirection::input: + settings.input_location = loc; + break; + case SignalDirection::output: + settings.output_location = loc; + break; + case SignalDirection::duplex: + settings.inout_location = loc; + break; + } } /** Returns whether or not this module should be split. * - * If nothing is known about the given module, `default_val` is returned (this is - * to allow driver's to request terminal ports get split by default). + * If nothing is known about the given module, `default_val` is returned (this + * is to allow driver's to request terminal ports get split by default). */ bool Configuration::get_module_split(const std::string& name, bool default_val) const { - std::map<std::string, ModuleSettings>::const_iterator i = _module_settings.find(name); - if (i == _module_settings.end()) { - return default_val; - } + auto i = _module_settings.find(name); + if (i == _module_settings.end()) { + return default_val; + } - return (*i).second.split; + return (*i).second.split; } void Configuration::set_module_split(const std::string& name, bool split) { - _module_settings[name].split = split; -} - -/** Return a vector of filenames in descending order by preference. */ -static std::vector<std::string> -get_filenames() -{ - std::vector<std::string> filenames; - std::string prefix; - - const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); - const char* home = getenv("HOME"); - - // XDG spec - if (xdg_config_home) { - filenames.push_back(std::string(xdg_config_home) + "/patchagerc"); - } else if (home) { - filenames.push_back(std::string(home) + "/.config/patchagerc"); - } - - // Old location - if (home) { - filenames.push_back(std::string(home) + "/.patchagerc"); - } - - // Current directory (bundle or last-ditch effort) - filenames.push_back("patchagerc"); - - return filenames; + if (!name.empty()) { + _module_settings[name].split = split; + } } void Configuration::load() { - // Try to find a readable configuration file - const std::vector<std::string> filenames = get_filenames(); - std::ifstream file; - for (size_t i = 0; i < filenames.size(); ++i) { - file.open(filenames[i].c_str(), std::ios::in); - if (file.good()) { - std::cout << "Loading configuration from " << filenames[i] << std::endl; - break; - } - } - - if (!file.good()) { - std::cout << "No configuration file present" << std::endl; - return; - } - - _module_settings.clear(); - while (file.good()) { - std::string key; - if (file.peek() == '\"') { - /* Old versions omitted the module_position key and listed - positions starting with module name in quotes. */ - key = "module_position"; - } else { - file >> key; - } - - if (key == "window_location") { - file >> _window_location.x >> _window_location.y; - } else if (key == "window_size") { - file >> _window_size.x >> _window_size.y; - } else if (key == "zoom_level") { - file >> _zoom; - } else if (key == "font_size") { - file >> _font_size; - } else if (key == "show_toolbar") { - file >> _show_toolbar; - } else if (key == "sprung_layout") { - file >> _sprung_layout; - } else if (key == "show_messages") { - file >> _show_messages; - } else if (key == "sort_ports") { - file >> _sort_ports; - } else if (key == "messages_height") { - file >> _messages_height; - } else if (key == "port_color") { - std::string type_name; - uint32_t rgba; - file >> type_name; - file.ignore(1, '#'); - file >> std::hex >> std::uppercase; - file >> rgba; - file >> std::dec >> std::nouppercase; - - bool found = false; - for (int i = 0; i < N_PORT_TYPES; ++i) { - if (type_name == port_type_names[i]) { - _port_colors[i] = rgba; - found = true; - break; - } - } - if (!found) { - std::cerr << "error: color for unknown port type `" - << type_name << "'" << std::endl; - } - } else if (key == "module_position" || key[0] == '\"') { - Coord loc; - std::string name; - file.ignore(1, '\"'); - std::getline(file, name, '\"'); - - ModuleType type; - std::string type_str; - file >> type_str; - if (type_str == "input") { - type = Input; - } else if (type_str == "output") { - type = Output; - } else if (type_str == "inputoutput") { - type = InputOutput; - } else { - std::cerr << "error: bad position type `" << type_str - << "' for module `" << name << "'" << std::endl; - file.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); - continue; - } - - file >> loc.x; - file >> loc.y; - - set_module_location(name, type, loc); - } else { - std::cerr << "warning: unknown configuration key `" << key << "'" - << std::endl; - file.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); - } - - // Skip trailing whitespace, including newline - while (file.good() && isspace(file.peek())) { - file.ignore(1); - } - } - - file.close(); + // Try to find a readable configuration file + const std::vector<std::string> filenames = get_filenames(); + std::ifstream file; + for (const auto& filename : filenames) { + file.open(filename.c_str(), std::ios::in); + if (file.good()) { + std::cout << "Loading configuration from " << filename << std::endl; + break; + } + } + + if (!file.good()) { + std::cout << "No configuration file present" << std::endl; + return; + } + + _module_settings.clear(); + while (file.good()) { + std::string key; + if (file.peek() == '\"') { + /* Old versions omitted the module_position key and listed + positions starting with module name in quotes. */ + key = "module_position"; + } else { + file >> key; + } + + if (key == "window_location") { + auto& setting = std::get<setting::WindowLocation>(_settings); + file >> setting.value.x >> setting.value.y; + } else if (key == "window_size") { + auto& setting = std::get<setting::WindowSize>(_settings); + file >> setting.value.x >> setting.value.y; + } else if (key == "zoom_level") { + file >> std::get<setting::Zoom>(_settings).value; + } else if (key == "font_size") { + file >> std::get<setting::FontSize>(_settings).value; + } else if (key == "show_toolbar") { + file >> std::get<setting::ToolbarVisible>(_settings).value; + } else if (key == "sprung_layout") { + file >> std::get<setting::SprungLayout>(_settings).value; + } else if (key == "show_messages") { + file >> std::get<setting::MessagesVisible>(_settings).value; + } else if (key == "sort_ports") { + file >> std::get<setting::SortedPorts>(_settings).value; + } else if (key == "messages_height") { + file >> std::get<setting::MessagesHeight>(_settings).value; + } else if (key == "human_names") { + file >> std::get<setting::HumanNames>(_settings).value; + } else if (key == "port_color") { + std::string type_name; + uint32_t rgba = 0u; + file >> type_name; + file.ignore(1, '#'); + file >> std::hex >> std::uppercase; + file >> rgba; + file >> std::dec >> std::nouppercase; + + bool found = false; + for (unsigned i = 0U; i < n_port_types; ++i) { + if (type_name == port_type_names[i]) { + _port_colors[i] = rgba; + found = true; + break; + } + } + if (!found) { + std::cerr << "error: color for unknown port type `" << type_name << "'" + << std::endl; + } + } else if (key == "module_position") { + Coord loc; + std::string name; + file.ignore(std::numeric_limits<std::streamsize>::max(), '\"'); + std::getline(file, name, '\"'); + + SignalDirection type = SignalDirection::input; + std::string type_str; + file >> type_str; + if (type_str == "input") { + type = SignalDirection::input; + } else if (type_str == "output") { + type = SignalDirection::output; + } else if (type_str == "inputoutput") { + type = SignalDirection::duplex; + } else { + std::cerr << "error: bad position type `" << type_str + << "' for module `" << name << "'" << std::endl; + file.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); + continue; + } + + file >> loc.x; + file >> loc.y; + + set_module_location(name, type, loc); + } else { + std::cerr << "warning: unknown configuration key `" << key << "'" + << std::endl; + file.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); + } + + // Skip trailing whitespace, including newline + while (file.good() && isspace(file.peek())) { + file.ignore(1); + } + } + + file.close(); } -static inline void +inline void write_module_position(std::ofstream& os, const std::string& name, const char* type, const Coord& loc) { - os << "module_position \"" << name << "\"" - << " " << type << " " << loc.x << " " << loc.y << std::endl; + os << "module_position \"" << name << "\"" + << " " << type << " " << loc.x << " " << loc.y << std::endl; } void Configuration::save() { - // Try to find a writable configuration file - const std::vector<std::string> filenames = get_filenames(); - std::ofstream file; - for (size_t i = 0; i < filenames.size(); ++i) { - file.open(filenames[i].c_str(), std::ios::out); - if (file.good()) { - std::cout << "Writing configuration to " << filenames[i] << std::endl; - break; - } - } - - if (!file.good()) { - std::cout << "Unable to open configuration file to write" << std::endl; - return; - } - - file << "window_location " << _window_location.x << " " << _window_location.y << std::endl; - file << "window_size " << _window_size.x << " " << _window_size.y << std::endl; - file << "zoom_level " << _zoom << std::endl; - file << "font_size " << _font_size << std::endl; - file << "show_toolbar " << _show_toolbar << std::endl; - file << "sprung_layout " << _sprung_layout << std::endl; - file << "show_messages " << _show_messages << std::endl; - file << "sort_ports " << _sort_ports << std::endl; - file << "messages_height " << _messages_height << std::endl; - - file << std::hex << std::uppercase; - for (int i = 0; i < N_PORT_TYPES; ++i) { - if (_port_colors[i] != _default_port_colors[i]) { - file << "port_color " << port_type_names[i] << " " << _port_colors[i] << std::endl; - } - } - file << std::dec << std::nouppercase; - - for (std::map<std::string, ModuleSettings>::iterator i = _module_settings.begin(); - i != _module_settings.end(); ++i) { - const ModuleSettings& settings = (*i).second; - const std::string& name = (*i).first; - - if (settings.split) { - if (settings.input_location && settings.output_location) { - write_module_position(file, name, "input", *settings.input_location); - write_module_position(file, name, "output", *settings.output_location); - } - } else if (settings.inout_location) { - write_module_position(file, name, "inputoutput", *settings.inout_location); - } - } - - file.close(); + // Try to find a writable configuration file + const std::vector<std::string> filenames = get_filenames(); + std::ofstream file; + for (const std::string& filename : filenames) { + file.open(filename.c_str(), std::ios::out); + if (file.good()) { + std::cout << "Writing configuration to " << filename << std::endl; + break; + } + } + + if (!file.good()) { + std::cout << "Unable to open configuration file to write" << std::endl; + return; + } + + file << "window_location " << get<setting::WindowLocation>().x << " " + << get<setting::WindowLocation>().y << std::endl; + + file << "window_size " << get<setting::WindowSize>().x << " " + << get<setting::WindowSize>().y << std::endl; + + file << "zoom_level " << get<setting::Zoom>() << std::endl; + file << "font_size " << get<setting::FontSize>() << std::endl; + file << "show_toolbar " << get<setting::ToolbarVisible>() << std::endl; + file << "sprung_layout " << get<setting::SprungLayout>() << std::endl; + file << "show_messages " << get<setting::MessagesVisible>() << std::endl; + file << "sort_ports " << get<setting::SortedPorts>() << std::endl; + file << "messages_height " << get<setting::MessagesHeight>() << std::endl; + file << "human_names " << get<setting::HumanNames>() << std::endl; + + file << std::hex << std::uppercase; + for (unsigned i = 0U; i < n_port_types; ++i) { + if (_port_colors[i] != _default_port_colors[i]) { + file << "port_color " << port_type_names[i] << " " << _port_colors[i] + << std::endl; + } + } + file << std::dec << std::nouppercase; + + for (const auto& s : _module_settings) { + const std::string& name = s.first; + const ModuleSettings& settings = s.second; + + if (settings.split) { + if (settings.input_location) { + write_module_position(file, name, "input", *settings.input_location); + } + + if (settings.output_location) { + write_module_position(file, name, "output", *settings.output_location); + } + } else if (settings.inout_location) { + write_module_position( + file, name, "inputoutput", *settings.inout_location); + } + } + + file.close(); } + +} // namespace patchage diff --git a/src/Configuration.hpp b/src/Configuration.hpp index 127a4a8..a92d4af 100644 --- a/src/Configuration.hpp +++ b/src/Configuration.hpp @@ -1,109 +1,147 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifndef PATCHAGE_CONFIGURATION_HPP #define PATCHAGE_CONFIGURATION_HPP -#include <stdint.h> +#include "Coord.hpp" +#include "Setting.hpp" -#include <string> -#include <list> +#include <cstdint> +#include <functional> #include <map> +#include <optional> +#include <string> +#include <tuple> -#include <boost/optional.hpp> - -enum ModuleType { Input, Output, InputOutput }; - -enum PortType { JACK_AUDIO, JACK_MIDI, ALSA_MIDI, JACK_OSC, JACK_CV }; +namespace patchage { -#define N_PORT_TYPES 5 - -struct Coord { - Coord(double x_=0, double y_=0) : x(x_), y(y_) {} - double x; - double y; -}; +enum class SignalDirection; +enum class PortType; class Configuration { public: - Configuration(); - - void load(); - void save(); - - bool get_module_location(const std::string& name, ModuleType type, Coord& loc); - void set_module_location(const std::string& name, ModuleType type, Coord loc); - - void set_module_split(const std::string& name, bool split); - bool get_module_split(const std::string& name, bool default_val) const; - - float get_zoom() const { return _zoom; } - void set_zoom(float zoom) { _zoom = zoom; } - float get_font_size() const { return _font_size; } - void set_font_size(float font_size) { _font_size = font_size; } - - float get_show_toolbar() const { return _show_toolbar; } - void set_show_toolbar(float show_toolbar) { _show_toolbar = show_toolbar; } - - float get_sprung_layout() const { return _sprung_layout; } - void set_sprung_layout(float sprung_layout) { _sprung_layout = sprung_layout; } - - bool get_show_messages() const { return _show_messages; } - void set_show_messages(bool show_messages) { _show_messages = show_messages; } - - bool get_sort_ports() const { return _sort_ports; } - void set_sort_ports(bool sort_ports) { _sort_ports = sort_ports; } - - int get_messages_height() const { return _messages_height; } - void set_messages_height(int height) { _messages_height = height; } - - uint32_t get_port_color(PortType type) const { return _port_colors[type]; } - void set_port_color(PortType type, uint32_t rgba) { - _port_colors[type] = rgba; - } - - Coord get_window_location() { return _window_location; } - void set_window_location(Coord loc) { _window_location = loc; } - Coord get_window_size() { return _window_size; } - void set_window_size(Coord size) { _window_size = size; } + static constexpr unsigned n_port_types = 5U; + + explicit Configuration(std::function<void(const Setting&)> on_change); + + void load(); + void save(); + + bool get_module_location(const std::string& name, + SignalDirection type, + Coord& loc) const; + + void set_module_location(const std::string& name, + SignalDirection type, + Coord loc); + + void set_module_split(const std::string& name, bool split); + bool get_module_split(const std::string& name, bool default_val) const; + + uint32_t get_port_color(PortType type) const + { + return _port_colors[static_cast<unsigned>(type)]; + } + + void set_port_color(PortType type, uint32_t rgba) + { + _port_colors[static_cast<unsigned>(type)] = rgba; + _on_change(setting::PortColor{type, rgba}); + } + + // Set a global configuration setting + template<class S> + void set(decltype(S::value) value) + { + S& setting = std::get<S>(_settings); + + if (setting.value != value) { + setting.value = std::move(value); + _on_change(setting); + } + } + + // Set a global configuration setting + template<class S> + void set_setting(S new_setting) + { + set<S>(new_setting.value); + } + + // Set a global port color setting + void set_setting(setting::PortColor new_setting) + { + auto& color = _port_colors[static_cast<unsigned>(new_setting.type)]; + + if (color != new_setting.color) { + set_port_color(new_setting.type, new_setting.color); + } + } + + // Get a global configuration setting + template<class S> + decltype(S::value) get() const + { + return std::get<S>(_settings).value; + } + + /// Call `visitor` once with each configuration setting + template<class Visitor> + void each(Visitor visitor) + { + visitor(std::get<setting::FontSize>(_settings)); + visitor(std::get<setting::HumanNames>(_settings)); + visitor(std::get<setting::MessagesHeight>(_settings)); + visitor(std::get<setting::MessagesVisible>(_settings)); + visitor(std::get<setting::SortedPorts>(_settings)); + visitor(std::get<setting::SprungLayout>(_settings)); + visitor(std::get<setting::ToolbarVisible>(_settings)); + visitor(std::get<setting::WindowLocation>(_settings)); + visitor(std::get<setting::WindowSize>(_settings)); + visitor(std::get<setting::Zoom>(_settings)); + + for (auto i = 0u; i < n_port_types; ++i) { + visitor(setting::PortColor{static_cast<PortType>(i), _port_colors[i]}); + } + } private: - struct ModuleSettings { - ModuleSettings(bool s=false) : split(s) {} - boost::optional<Coord> input_location; - boost::optional<Coord> output_location; - boost::optional<Coord> inout_location; - bool split; - }; - - std::map<std::string, ModuleSettings> _module_settings; - - uint32_t _default_port_colors[N_PORT_TYPES]; - uint32_t _port_colors[N_PORT_TYPES]; - - Coord _window_location; - Coord _window_size; - float _zoom; - float _font_size; - int _messages_height; - bool _show_toolbar; - bool _sprung_layout; - bool _show_messages; - bool _sort_ports; + struct ModuleSettings { + explicit ModuleSettings(bool s = false) + : split(s) + {} + + std::optional<Coord> input_location; + std::optional<Coord> output_location; + std::optional<Coord> inout_location; + bool split; + }; + + std::map<std::string, ModuleSettings> _module_settings; + + uint32_t _default_port_colors[n_port_types] = {}; + uint32_t _port_colors[n_port_types] = {}; + + using Settings = std::tuple<setting::AlsaAttached, + setting::FontSize, + setting::HumanNames, + setting::JackAttached, + setting::MessagesHeight, + setting::MessagesVisible, + setting::SortedPorts, + setting::SprungLayout, + setting::ToolbarVisible, + setting::WindowLocation, + setting::WindowSize, + setting::Zoom>; + + Settings _settings; + + std::function<void(const Setting&)> _on_change; }; +} // namespace patchage + #endif // PATCHAGE_CONFIGURATION_HPP diff --git a/src/Coord.hpp b/src/Coord.hpp new file mode 100644 index 0000000..b24d7dc --- /dev/null +++ b/src/Coord.hpp @@ -0,0 +1,28 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_COORD_HPP +#define PATCHAGE_COORD_HPP + +namespace patchage { + +struct Coord { + double x{0.0}; + double y{0.0}; +}; + +inline bool +operator==(const Coord& lhs, const Coord& rhs) +{ + return lhs.x == rhs.x && lhs.y == rhs.y; +} + +inline bool +operator!=(const Coord& lhs, const Coord& rhs) +{ + return !(lhs == rhs); +} + +} // namespace patchage + +#endif // PATCHAGE_COORD_HPP diff --git a/src/Driver.hpp b/src/Driver.hpp index 3837382..4cb890b 100644 --- a/src/Driver.hpp +++ b/src/Driver.hpp @@ -1,55 +1,58 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifndef PATCHAGE_DRIVER_HPP #define PATCHAGE_DRIVER_HPP -#include <boost/shared_ptr.hpp> -#include <sigc++/sigc++.h> +#include "Event.hpp" -#include "PatchageEvent.hpp" +#include <functional> +#include <utility> -class PatchagePort; -class PatchageCanvas; +namespace patchage { -/** Trival driver base class */ -class Driver { +struct PortID; + +/// Base class for drivers that handle system clients and ports +class Driver +{ public: - virtual ~Driver() {} + using EventSink = std::function<void(const Event&)>; + + explicit Driver(EventSink emit_event) + : _emit_event{std::move(emit_event)} + {} + + Driver(const Driver&) = delete; + Driver& operator=(const Driver&) = delete; - virtual void process_events(Patchage* app) = 0; + Driver(Driver&&) = delete; + Driver& operator=(Driver&&) = delete; - virtual void attach(bool launch_daemon) = 0; - virtual void detach() = 0; - virtual bool is_attached() const = 0; + virtual ~Driver() = default; - virtual void refresh() = 0; - virtual void destroy_all() {} + /// Connect to the underlying system API + virtual void attach(bool launch_daemon) = 0; - virtual PatchagePort* create_port_view(Patchage* patchage, - const PortID& id) = 0; + /// Disconnect from the underlying system API + virtual void detach() = 0; - virtual bool connect(PatchagePort* src_port, - PatchagePort* dst_port) = 0; + /// Return true iff the driver is active and connected to the system + virtual bool is_attached() const = 0; - virtual bool disconnect(PatchagePort* src_port, - PatchagePort* dst_port) = 0; + /// Send events to `sink` that describe the complete current system state + virtual void refresh(const EventSink& sink) = 0; - sigc::signal<void> signal_attached; - sigc::signal<void> signal_detached; + /// Make a connection between ports + virtual bool connect(const PortID& tail_id, const PortID& head_id) = 0; + + /// Remove a connection between ports + virtual bool disconnect(const PortID& tail_id, const PortID& head_id) = 0; + +protected: + EventSink _emit_event; ///< Sink for emitting "live" events }; +} // namespace patchage + #endif // PATCHAGE_DRIVER_HPP diff --git a/src/Drivers.cpp b/src/Drivers.cpp new file mode 100644 index 0000000..6de2459 --- /dev/null +++ b/src/Drivers.cpp @@ -0,0 +1,56 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Drivers.hpp" + +#include "AudioDriver.hpp" +#include "ClientType.hpp" +#include "Driver.hpp" +#include "Event.hpp" +#include "make_alsa_driver.hpp" +#include "make_jack_driver.hpp" + +#include <functional> +#include <utility> + +namespace patchage { + +Drivers::Drivers(ILog& log, Driver::EventSink emit_event) + : _log{log} + , _emit_event{std::move(emit_event)} + , _alsa_driver{make_alsa_driver( + log, + [this](const Event& event) { _emit_event(event); })} + , _jack_driver{make_jack_driver(_log, [this](const Event& event) { + _emit_event(event); + })} +{} + +void +Drivers::refresh() +{ + _emit_event(event::Cleared{}); + + if (_alsa_driver) { + _alsa_driver->refresh(_emit_event); + } + + if (_jack_driver) { + _jack_driver->refresh(_emit_event); + } +} + +Driver* +Drivers::driver(const ClientType type) +{ + switch (type) { + case ClientType::jack: + return _jack_driver.get(); + case ClientType::alsa: + return _alsa_driver.get(); + } + + return nullptr; +} + +} // namespace patchage diff --git a/src/Drivers.hpp b/src/Drivers.hpp new file mode 100644 index 0000000..6bb9eb4 --- /dev/null +++ b/src/Drivers.hpp @@ -0,0 +1,52 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_DRIVERS_HPP +#define PATCHAGE_DRIVERS_HPP + +#include "AudioDriver.hpp" +#include "Driver.hpp" + +#include <memory> + +namespace patchage { + +class ILog; +enum class ClientType; + +/// Manager for all drivers +class Drivers +{ +public: + Drivers(ILog& log, Driver::EventSink emit_event); + + Drivers(const Drivers&) = delete; + Drivers& operator=(const Drivers&) = delete; + + Drivers(Drivers&&) = delete; + Drivers& operator=(Drivers&&) = delete; + + ~Drivers() = default; + + /// Refresh all drivers and emit results to the event sink + void refresh(); + + /// Return a pointer to the driver for the given client type (or null) + Driver* driver(ClientType type); + + /// Return a pointer to the ALSA driver (or null) + const std::unique_ptr<Driver>& alsa() { return _alsa_driver; } + + /// Return a pointer to the JACK driver (or null) + const std::unique_ptr<AudioDriver>& jack() { return _jack_driver; } + +protected: + ILog& _log; + Driver::EventSink _emit_event; + std::unique_ptr<Driver> _alsa_driver; + std::unique_ptr<AudioDriver> _jack_driver; +}; + +} // namespace patchage + +#endif // PATCHAGE_DRIVER_HPP diff --git a/src/Event.hpp b/src/Event.hpp new file mode 100644 index 0000000..36166e7 --- /dev/null +++ b/src/Event.hpp @@ -0,0 +1,71 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_EVENT_HPP +#define PATCHAGE_EVENT_HPP + +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "ClientType.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" + +#include <variant> + +namespace patchage { +namespace event { + +struct Cleared {}; + +struct ClientCreated { + ClientID id; + ClientInfo info; +}; + +struct ClientDestroyed { + ClientID id; +}; + +struct DriverAttached { + ClientType type; +}; + +struct DriverDetached { + ClientType type; +}; + +struct PortCreated { + PortID id; + PortInfo info; +}; + +struct PortDestroyed { + PortID id; +}; + +struct PortsConnected { + PortID tail; + PortID head; +}; + +struct PortsDisconnected { + PortID tail; + PortID head; +}; + +} // namespace event + +/// An event from drivers that represents a change to the system +using Event = std::variant<event::Cleared, + event::ClientCreated, + event::ClientDestroyed, + event::DriverAttached, + event::DriverDetached, + event::PortCreated, + event::PortDestroyed, + event::PortsConnected, + event::PortsDisconnected>; + +} // namespace patchage + +#endif // PATCHAGE_EVENT_HPP diff --git a/src/ILog.hpp b/src/ILog.hpp new file mode 100644 index 0000000..5e00602 --- /dev/null +++ b/src/ILog.hpp @@ -0,0 +1,32 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_ILOG_HPP +#define PATCHAGE_ILOG_HPP + +#include <string> + +namespace patchage { + +/// Interface for writing log messages +class ILog +{ +public: + ILog() = default; + + ILog(const ILog&) = default; + ILog& operator=(const ILog&) = default; + + ILog(ILog&&) = default; + ILog& operator=(ILog&&) = default; + + virtual ~ILog() = default; + + virtual void info(const std::string& msg) = 0; + virtual void warning(const std::string& msg) = 0; + virtual void error(const std::string& msg) = 0; +}; + +} // namespace patchage + +#endif // PATCHAGE_ILOG_HPP diff --git a/src/JackDbusDriver.cpp b/src/JackDbusDriver.cpp index 7953051..81aca11 100644 --- a/src/JackDbusDriver.cpp +++ b/src/JackDbusDriver.cpp @@ -1,1048 +1,957 @@ -/* This file is part of Patchage. - * Copyright 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2008-2020 David Robillard <d@drobilla.net> +// Copyright 2008 Nedko Arnaudov <nedko@arnaudov.name> +// SPDX-License-Identifier: GPL-3.0-or-later -#include <cassert> -#include <cstring> -#include <string> -#include <set> - -#include "patchage_config.h" +#include "AudioDriver.hpp" +#include "ClientType.hpp" +#include "Driver.hpp" +#include "Event.hpp" +#include "ILog.hpp" +#include "PortNames.hpp" +#include "PortType.hpp" +#include "SignalDirection.hpp" +#include "make_jack_driver.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS -#include <glib.h> -#include <dbus/dbus.h> -#include <dbus/dbus-glib.h> #include <dbus/dbus-glib-lowlevel.h> +#include <dbus/dbus-glib.h> +#include <dbus/dbus.h> +#include <glib.h> -#include <boost/format.hpp> +#include <cassert> +#include <cstdint> +#include <cstring> +#include <set> +#include <string> -#include "Driver.hpp" -#include "JackDbusDriver.hpp" -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageEvent.hpp" -#include "PatchageModule.hpp" - -#define JACKDBUS_SERVICE "org.jackaudio.service" -#define JACKDBUS_OBJECT "/org/jackaudio/Controller" -#define JACKDBUS_IFACE_CONTROL "org.jackaudio.JackControl" +#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_FLAG_INPUT 0x00000001 +#define JACKDBUS_PORT_FLAG_TERMINAL 0x00000010 #define JACKDBUS_PORT_TYPE_AUDIO 0 -#define JACKDBUS_PORT_TYPE_MIDI 1 +#define JACKDBUS_PORT_TYPE_MIDI 1 -//#define USE_FULL_REFRESH +namespace patchage { +namespace { -JackDriver::JackDriver(Patchage* app) - : _app(app) - , _dbus_connection(0) - , _max_dsp_load(0) - , _server_responding(false) - , _server_started(false) - , _graph_version(0) +/// Driver for JACK audio and midi ports that uses D-Bus +class JackDriver : public AudioDriver { - dbus_error_init(&_dbus_error); -} +public: + explicit JackDriver(ILog& log, EventSink emit_event); -JackDriver::~JackDriver() -{ - if (_dbus_connection) { - dbus_connection_flush(_dbus_connection); - } + JackDriver(const JackDriver&) = delete; + JackDriver& operator=(const JackDriver&) = delete; - if (dbus_error_is_set(&_dbus_error)) { - dbus_error_free(&_dbus_error); - } -} + JackDriver(JackDriver&&) = delete; + JackDriver& operator=(JackDriver&&) = delete; + + ~JackDriver() override; + + // Driver interface + void attach(bool launch_daemon) override; + void detach() override; + bool is_attached() const override; + void refresh(const EventSink& sink) override; + bool connect(const PortID& tail_id, const PortID& head_id) override; + bool disconnect(const PortID& tail_id, const PortID& head_id) override; + + // AudioDriver interface + uint32_t xruns() override; + void reset_xruns() override; + uint32_t buffer_size() override; + bool set_buffer_size(uint32_t frames) override; + uint32_t sample_rate() override; + +private: + PortType patchage_port_type(dbus_uint32_t dbus_port_type) const; + + PortInfo port_info(const std::string& port_name, + dbus_uint32_t port_type, + dbus_uint32_t port_flags) const; + + void error_msg(const std::string& msg) const; + void info_msg(const std::string& msg) const; + + bool call(bool response_expected, + const char* iface, + const char* method, + DBusMessage** reply_ptr_ptr, + int in_type, + ...); + + void update_attached(); + + bool is_started(); -static bool -is_jack_port(const PatchagePort* port) + void start_server(); + + void stop_server(); + + static DBusHandlerResult dbus_message_hook(DBusConnection* connection, + DBusMessage* message, + void* jack_driver); + + void on_jack_appeared(); + + void on_jack_disappeared(); + + ILog& _log; + DBusError _dbus_error; + DBusConnection* _dbus_connection; + + mutable bool _server_responding; + bool _server_started; + + dbus_uint64_t _graph_version; +}; + +JackDriver::JackDriver(ILog& log, EventSink emit_event) + : AudioDriver{std::move(emit_event)} + , _log(log) + , _dbus_error() + , _dbus_connection(nullptr) + , _server_responding(false) + , _server_started(false) + , _graph_version(0) { - return port->type() == JACK_AUDIO || port->type() == JACK_MIDI; + dbus_error_init(&_dbus_error); } -/** Destroy all JACK (canvas) ports. - */ -void -JackDriver::destroy_all() +JackDriver::~JackDriver() { - _app->canvas()->remove_ports(is_jack_port); + if (_dbus_connection) { + dbus_connection_flush(_dbus_connection); + } + + if (dbus_error_is_set(&_dbus_error)) { + dbus_error_free(&_dbus_error); + } } void JackDriver::update_attached() { - bool was_attached = _server_started; - _server_started = is_started(); - - if (!_server_responding) { - if (was_attached) { - signal_detached.emit(); - } - return; - } - - if (_server_started && !was_attached) { - signal_attached.emit(); - return; - } - - if (!_server_started && was_attached) { - signal_detached.emit(); - return; - } + bool was_attached = _server_started; + _server_started = is_started(); + + if (!_server_responding) { + if (was_attached) { + _emit_event(event::DriverDetached{ClientType::jack}); + } + return; + } + + if (_server_started && !was_attached) { + _emit_event(event::DriverAttached{ClientType::jack}); + return; + } + + if (!_server_started && was_attached) { + _emit_event(event::DriverDetached{ClientType::jack}); + return; + } } void JackDriver::on_jack_appeared() { - info_msg("JACK appeared."); - update_attached(); + info_msg("Server appeared"); + update_attached(); } void JackDriver::on_jack_disappeared() { - info_msg("JACK disappeared."); + info_msg("Server disappeared"); - // we are not calling update_attached() here, because it will activate jackdbus + // we are not calling update_attached() here, because it will activate + // jackdbus - _server_responding = false; + _server_responding = false; - if (_server_started) { - signal_detached.emit(); - } + if (_server_started) { + _emit_event(event::DriverDetached{ClientType::jack}); + } - _server_started = false; + _server_started = false; } -/** Handle signals we have subscribed for in attach(). */ DBusHandlerResult -JackDriver::dbus_message_hook(DBusConnection* connection, - DBusMessage* message, - void* jack_driver) -{ - const char* client2_name; - const char* client_name; - const char* new_owner; - const char* object_name; - const char* old_owner; - const char* port2_name; - const char* port_name; - dbus_uint32_t port_flags; - dbus_uint32_t port_type; - dbus_uint64_t client2_id; - dbus_uint64_t client_id; - dbus_uint64_t connection_id; - dbus_uint64_t new_graph_version; - dbus_uint64_t port2_id; - dbus_uint64_t port_id; - - assert(jack_driver); - JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); - assert(me->_dbus_connection); - - if (dbus_message_is_signal(message, DBUS_INTERFACE_DBUS, "NameOwnerChanged")) { - if (!dbus_message_get_args( message, &me->_dbus_error, - DBUS_TYPE_STRING, &object_name, - DBUS_TYPE_STRING, &old_owner, - DBUS_TYPE_STRING, &new_owner, - DBUS_TYPE_INVALID)) { - me->error_msg(str(boost::format("dbus_message_get_args() failed to extract " - "NameOwnerChanged signal arguments (%s)") % me->_dbus_error.message)); - dbus_error_free(&me->_dbus_error); - return DBUS_HANDLER_RESULT_HANDLED; - } - - if (old_owner[0] == '\0') { - me->on_jack_appeared(); - } else if (new_owner[0] == '\0') { - me->on_jack_disappeared(); - } - } - -#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; - } - - if (!me->_server_started) { - me->_server_started = true; - me->signal_attached.emit(); - } - - if (new_graph_version > me->_graph_version) { - me->refresh_internal(false); - } +JackDriver::dbus_message_hook(DBusConnection* /*connection*/, + DBusMessage* message, + void* jack_driver) +{ + const char* client2_name = nullptr; + const char* client_name = nullptr; + const char* new_owner = nullptr; + const char* object_name = nullptr; + const char* old_owner = nullptr; + const char* port2_name = nullptr; + const char* port_name = nullptr; + dbus_uint32_t port_flags = 0u; + dbus_uint32_t port_type = 0u; + dbus_uint64_t client2_id = 0u; + dbus_uint64_t client_id = 0u; + dbus_uint64_t connection_id = 0u; + dbus_uint64_t new_graph_version = 0u; + dbus_uint64_t port2_id = 0u; + dbus_uint64_t port_id = 0u; + + assert(jack_driver); + auto* me = static_cast<JackDriver*>(jack_driver); + assert(me->_dbus_connection); + + if (dbus_message_is_signal( + message, DBUS_INTERFACE_DBUS, "NameOwnerChanged")) { + if (!dbus_message_get_args(message, + &me->_dbus_error, + DBUS_TYPE_STRING, + &object_name, + DBUS_TYPE_STRING, + &old_owner, + DBUS_TYPE_STRING, + &new_owner, + DBUS_TYPE_INVALID)) { + me->error_msg(fmt::format("dbus_message_get_args() failed to extract " + "NameOwnerChanged signal arguments ({})", + me->_dbus_error.message)); + + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (old_owner[0] == '\0') { + me->on_jack_appeared(); + } else if (new_owner[0] == '\0') { + me->on_jack_disappeared(); + } + } + + 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(fmt::format("dbus_message_get_args() failed to extract " + "PortAppeared signal arguments ({})", + me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (!me->_server_started) { + me->_server_started = true; + me->_emit_event(event::DriverAttached{ClientType::jack}); + } + + me->_emit_event( + event::PortCreated{PortID::jack(client_name, port_name), + me->port_info(port_name, port_type, port_flags)}); 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)); - - if (!me->_server_started) { - me->_server_started = true; - me->signal_attached.emit(); - } - - 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)); - - if (!me->_server_started) { - me->_server_started = true; - me->signal_attached.emit(); - } - - 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; - } - - if (!me->_server_started) { - me->_server_started = true; - me->signal_attached.emit(); - } - - 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; - } - - if (!me->_server_started) { - me->_server_started = true; - me->signal_attached.emit(); - } - - 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 - - return DBUS_HANDLER_RESULT_NOT_YET_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(fmt::format("dbus_message_get_args() failed to extract " + "PortDisappeared signal arguments ({})", + me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (!me->_server_started) { + me->_server_started = true; + me->_emit_event(event::DriverAttached{ClientType::jack}); + } + + me->_emit_event(event::PortDestroyed{PortID::jack(client_name, 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(fmt::format("dbus_message_get_args() failed to extract " + "PortsConnected signal arguments ({})", + me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (!me->_server_started) { + me->_server_started = true; + me->_emit_event(event::DriverAttached{ClientType::jack}); + } + + me->_emit_event( + event::PortsConnected{PortID::jack(client_name, port_name), + PortID::jack(client2_name, 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(fmt::format("dbus_message_get_args() failed to extract " + "PortsDisconnected signal arguments ({})", + me->_dbus_error.message)); + dbus_error_free(&me->_dbus_error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (!me->_server_started) { + me->_server_started = true; + me->_emit_event(event::DriverAttached{ClientType::jack}); + } + + me->_emit_event( + event::PortsDisconnected{PortID::jack(client_name, port_name), + PortID::jack(client2_name, port2_name)}); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } bool -JackDriver::call( - bool response_expected, - const char* iface, - const char* method, - DBusMessage** reply_ptr_ptr, - int in_type, ...) +JackDriver::call(bool response_expected, + 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) { - if (response_expected) { - error_msg(str(boost::format("no reply from server when calling method '%s'" - ", error is '%s'") % method % _dbus_error.message)); - } - _server_responding = false; - dbus_error_free(&_dbus_error); - } else { - _server_responding = true; - *reply_ptr_ptr = reply_ptr; - } - - return reply_ptr; + DBusMessage* request_ptr = nullptr; + DBusMessage* reply_ptr = nullptr; + 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) { + if (response_expected) { + error_msg(fmt::format("No reply from server when calling method {} ({})", + method, + _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; + DBusMessage* reply_ptr = nullptr; + dbus_bool_t started = false; - if (!call(false, JACKDBUS_IFACE_CONTROL, "IsStarted", &reply_ptr, DBUS_TYPE_INVALID)) { - return false; - } + if (!call(false, + 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; - } + 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); + dbus_message_unref(reply_ptr); - return started; + return started; } void JackDriver::start_server() { - DBusMessage* reply_ptr; + DBusMessage* reply_ptr = nullptr; - if (!call(false, JACKDBUS_IFACE_CONTROL, "StartServer", &reply_ptr, DBUS_TYPE_INVALID)) { - return; - } + if (!call(false, + JACKDBUS_IFACE_CONTROL, + "StartServer", + &reply_ptr, + DBUS_TYPE_INVALID)) { + return; + } - dbus_message_unref(reply_ptr); + dbus_message_unref(reply_ptr); - update_attached(); + update_attached(); } void JackDriver::stop_server() { - DBusMessage* reply_ptr; + DBusMessage* reply_ptr = nullptr; - if (!call(false, JACKDBUS_IFACE_CONTROL, "StopServer", &reply_ptr, DBUS_TYPE_INVALID)) { - return; - } + if (!call(false, + JACKDBUS_IFACE_CONTROL, + "StopServer", + &reply_ptr, + DBUS_TYPE_INVALID)) { + error_msg("Error stopping JACK server"); + } - dbus_message_unref(reply_ptr); - - if (!_server_started) { - _server_started = false; - signal_detached.emit(); - } + dbus_message_unref(reply_ptr); + _emit_event(event::DriverDetached{ClientType::jack}); } 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); - - dbus_bus_add_match(_dbus_connection, "type='signal',interface='" DBUS_INTERFACE_DBUS "',member=NameOwnerChanged,arg0='org.jackaudio.service'", 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); - - update_attached(); - - if (!_server_responding) { - return; - } - - if (launch_daemon) { - start_server(); - } + // Connect to the bus + _dbus_connection = dbus_bus_get(DBUS_BUS_SESSION, &_dbus_error); + if (dbus_error_is_set(&_dbus_error)) { + error_msg(fmt::format("dbus_bus_get() failed ({})", _dbus_error.message)); + dbus_error_free(&_dbus_error); + return; + } + + dbus_connection_setup_with_g_main(_dbus_connection, nullptr); + + dbus_bus_add_match(_dbus_connection, + "type='signal',interface='" DBUS_INTERFACE_DBUS + "',member=NameOwnerChanged,arg0='org.jackaudio.service'", + nullptr); + dbus_bus_add_match(_dbus_connection, + "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY + "',member=PortAppeared", + nullptr); + dbus_bus_add_match(_dbus_connection, + "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY + "',member=PortDisappeared", + nullptr); + dbus_bus_add_match(_dbus_connection, + "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY + "',member=PortsConnected", + nullptr); + dbus_bus_add_match(_dbus_connection, + "type='signal',interface='" JACKDBUS_IFACE_PATCHBAY + "',member=PortsDisconnected", + nullptr); + + dbus_connection_add_filter( + _dbus_connection, dbus_message_hook, this, nullptr); + + update_attached(); + + if (!_server_responding) { + return; + } + + if (launch_daemon) { + start_server(); + } + + _log.info("[JACK] Attached to bus"); } void JackDriver::detach() { - stop_server(); + stop_server(); } bool JackDriver::is_attached() const { - return _dbus_connection && _server_responding; + return _dbus_connection && _server_responding; } void -JackDriver::add_port(PatchageModule* module, - PortType type, - const std::string& name, - bool is_input) -{ - if (module->get_port(name)) { - return; - } - - new PatchagePort( - *module, - type, - name, - "", // TODO: pretty name - is_input, - _app->conf()->get_port_color(type), - _app->show_human_names()); -} +JackDriver::refresh(const EventSink& sink) +{ + DBusMessage* reply_ptr = nullptr; + DBusMessageIter iter = {}; + dbus_uint64_t version = 0u; + const char* reply_signature = nullptr; + 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 = 0u; + const char* client_name = nullptr; + dbus_uint64_t port_id = 0u; + const char* port_name = nullptr; + dbus_uint32_t port_flags = 0u; + dbus_uint32_t port_type = 0u; + dbus_uint64_t client2_id = 0u; + const char* client2_name = nullptr; + dbus_uint64_t port2_id = 0u; + const char* port2_name = nullptr; + dbus_uint64_t connection_id = 0u; + + if (!call(true, + JACKDBUS_IFACE_PATCHBAY, + "GetGraph", + &reply_ptr, + DBUS_TYPE_UINT64, + &version, + DBUS_TYPE_INVALID)) { + error_msg("GetGraph() failed"); + return; + } -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->conf()->get_module_split(client_name, port_flags & JACKDBUS_PORT_FLAG_TERMINAL)) { - if (port_flags & JACKDBUS_PORT_FLAG_INPUT) { - type = Input; - } else { - type = Output; - } - } - - PatchageModule* module = find_or_create_module(type, client_name); - - add_port(module, local_port_type, port_name, port_flags & JACKDBUS_PORT_FLAG_INPUT); -} + reply_signature = dbus_message_get_signature(reply_ptr); -void -JackDriver::remove_port(dbus_uint64_t client_id, - const char* client_name, - dbus_uint64_t port_id, - const char* port_name) -{ - PatchagePort* port = _app->canvas()->find_port_by_name(client_name, port_name); - if (!port) { - error_msg("Unable to remove unknown port"); - return; - } + if (strcmp(reply_signature, "ta(tsa(tsuu))a(tstststst)") != 0) { + error_msg(std::string{"GetGraph() reply signature mismatch. "} + + reply_signature); + dbus_message_unref(reply_ptr); + return; + } - PatchageModule* module = dynamic_cast<PatchageModule*>(port->get_module()); + dbus_message_iter_init(reply_ptr, &iter); - delete port; + dbus_message_iter_get_basic(&iter, &version); + dbus_message_iter_next(&iter); - // No empty modules (for now) - if (module->num_ports() == 0) { - delete module; - } + _graph_version = version; - if (_app->canvas()->empty()) { - if (_server_started) { - signal_detached.emit(); - } + // Emit all clients and ports + 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)) { + dbus_message_iter_recurse(&clients_array_iter, &client_struct_iter); - _server_started = false; - } -} + dbus_message_iter_get_basic(&client_struct_iter, &client_id); + dbus_message_iter_next(&client_struct_iter); -PatchageModule* -JackDriver::find_or_create_module( - ModuleType type, - const std::string& name) -{ - PatchageModule* module = _app->canvas()->find_module(name, type); + dbus_message_iter_get_basic(&client_struct_iter, &client_name); + dbus_message_iter_next(&client_struct_iter); - if (!module) { - module = new PatchageModule(_app, name, type); - module->load_location(); - _app->canvas()->add_module(name, module); - } + // TODO: Pretty name? + sink({event::ClientCreated{ClientID::jack(client_name), {client_name}}}); - return module; -} + 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)) { + dbus_message_iter_recurse(&ports_array_iter, &port_struct_iter); -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) -{ - PatchagePort* port1 = _app->canvas()->find_port_by_name(client1_name, port1_name); - if (!port1) { - error_msg((std::string)"Unable to connect unknown port '" + port1_name + "' of client '" + client1_name + "'"); - return; - } - - PatchagePort* port2 = _app->canvas()->find_port_by_name(client2_name, port2_name); - if (!port2) { - error_msg((std::string)"Unable to connect unknown port '" + port2_name + "' of client '" + client2_name + "'"); - return; - } - - _app->canvas()->connect(port1, port2); -} + dbus_message_iter_get_basic(&port_struct_iter, &port_id); + dbus_message_iter_next(&port_struct_iter); -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) -{ - PatchagePort* port1 = _app->canvas()->find_port_by_name(client1_name, port1_name); - if (!port1) { - error_msg((std::string)"Unable to disconnect unknown port '" + port1_name + "' of client '" + client1_name + "'"); - return; - } - - PatchagePort* port2 = _app->canvas()->find_port_by_name(client2_name, port2_name); - if (!port2) { - error_msg((std::string)"Unable to disconnect unknown port '" + port2_name + "' of client '" + client2_name + "'"); - return; - } - - _app->canvas()->disconnect(port1, port2); -} + dbus_message_iter_get_basic(&port_struct_iter, &port_name); + dbus_message_iter_next(&port_struct_iter); -void -JackDriver::refresh_internal(bool force) -{ - DBusMessage* reply_ptr; - DBusMessageIter iter; - 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; + dbus_message_iter_get_basic(&port_struct_iter, &port_flags); + dbus_message_iter_next(&port_struct_iter); - if (force) { - version = 0; // workaround module split/join stupidity - } else { - version = _graph_version; - } + dbus_message_iter_get_basic(&port_struct_iter, &port_type); + dbus_message_iter_next(&port_struct_iter); - if (!call(true, JACKDBUS_IFACE_PATCHBAY, "GetGraph", &reply_ptr, DBUS_TYPE_UINT64, &version, DBUS_TYPE_INVALID)) { - error_msg("GetGraph() failed."); - return; - } + sink({event::PortCreated{PortID::jack(client_name, port_name), + port_info(port_name, port_type, port_flags)}}); + } - reply_signature = dbus_message_get_signature(reply_ptr); + dbus_message_iter_next(&client_struct_iter); + } - if (strcmp(reply_signature, "ta(tsa(tsuu))a(tstststst)") != 0) { - error_msg((std::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(); - - //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_next(&iter); - dbus_message_iter_get_basic(&connection_struct_iter, &client_name); - dbus_message_iter_next(&connection_struct_iter); + // Emit all connections + 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)) { + dbus_message_iter_recurse(&connections_array_iter, &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, &client_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, &client_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, &port_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, &port_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, &client2_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, &client2_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); + dbus_message_iter_get_basic(&connection_struct_iter, &port2_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)); + dbus_message_iter_get_basic(&connection_struct_iter, &port2_name); + dbus_message_iter_next(&connection_struct_iter); - connect_ports( - connection_id, - client_id, client_name, - port_id, port_name, - client2_id, client2_name, - port2_id, port2_name); - } + dbus_message_iter_get_basic(&connection_struct_iter, &connection_id); + dbus_message_iter_next(&connection_struct_iter); -unref: - dbus_message_unref(reply_ptr); + sink({event::PortsConnected{PortID::jack(client_name, port_name), + PortID::jack(client2_name, port2_name)}}); + } } -void -JackDriver::refresh() -{ - refresh_internal(true); +bool +JackDriver::connect(const PortID& tail_id, const PortID& head_id) +{ + const auto tail_names = PortNames(tail_id); + const auto head_names = PortNames(head_id); + const char* const tail_client_name = tail_names.client().c_str(); + const char* const tail_port_name = tail_names.port().c_str(); + const char* const head_client_name = head_names.client().c_str(); + const char* const head_port_name = head_names.port().c_str(); + + DBusMessage* reply_ptr = nullptr; + + if (!call(true, + JACKDBUS_IFACE_PATCHBAY, + "ConnectPortsByName", + &reply_ptr, + DBUS_TYPE_STRING, + &tail_client_name, + DBUS_TYPE_STRING, + &tail_port_name, + DBUS_TYPE_STRING, + &head_client_name, + DBUS_TYPE_STRING, + &head_port_name, + DBUS_TYPE_INVALID)) { + error_msg("ConnectPortsByName() failed"); + return false; + } + + return true; } bool -JackDriver::connect(PatchagePort* src, - PatchagePort* dst) -{ - const char* client1_name = src->get_module()->get_label(); - const char* port1_name = src->get_label(); - const char* client2_name = dst->get_module()->get_label(); - const char* port2_name = dst->get_label(); - - DBusMessage* reply_ptr; - if (!call(true, 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; +JackDriver::disconnect(const PortID& tail_id, const PortID& head_id) +{ + const auto tail_names = PortNames(tail_id); + const auto head_names = PortNames(head_id); + const char* const tail_client_name = tail_names.client().c_str(); + const char* const tail_port_name = tail_names.port().c_str(); + const char* const head_client_name = head_names.client().c_str(); + const char* const head_port_name = head_names.port().c_str(); + + DBusMessage* reply_ptr = nullptr; + + if (!call(true, + JACKDBUS_IFACE_PATCHBAY, + "DisconnectPortsByName", + &reply_ptr, + DBUS_TYPE_STRING, + &tail_client_name, + DBUS_TYPE_STRING, + &tail_port_name, + DBUS_TYPE_STRING, + &head_client_name, + DBUS_TYPE_STRING, + &head_port_name, + DBUS_TYPE_INVALID)) { + error_msg("DisconnectPortsByName() failed"); + return false; + } + + return true; +} + +uint32_t +JackDriver::xruns() +{ + DBusMessage* reply_ptr = nullptr; + dbus_uint32_t xruns = 0u; + + if (_server_responding && !_server_started) { + return 0; + } + + if (!call(true, + JACKDBUS_IFACE_CONTROL, + "GetXruns", + &reply_ptr, + DBUS_TYPE_INVALID)) { + return 0; + } + + 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; } -bool -JackDriver::disconnect(PatchagePort* src, - PatchagePort* dst) +void +JackDriver::reset_xruns() { - const char* client1_name = src->get_module()->get_label(); - const char* port1_name = src->get_label(); - const char* client2_name = dst->get_module()->get_label(); - const char* port2_name = dst->get_label(); - - DBusMessage* reply_ptr; - if (!call(true, 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; + DBusMessage* reply_ptr = nullptr; + + if (!call(true, + JACKDBUS_IFACE_CONTROL, + "ResetXruns", + &reply_ptr, + DBUS_TYPE_INVALID)) { + return; + } + + dbus_message_unref(reply_ptr); } -jack_nframes_t +uint32_t JackDriver::buffer_size() { - DBusMessage* reply_ptr; - dbus_uint32_t buffer_size; - - if (_server_responding && !_server_started) { - goto fail; - } + DBusMessage* reply_ptr = nullptr; + dbus_uint32_t buffer_size = 0u; - if (!call(true, JACKDBUS_IFACE_CONTROL, "GetBufferSize", &reply_ptr, DBUS_TYPE_INVALID)) { - goto fail; - } + if (_server_responding && !_server_started) { + return 4096; + } - 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."); - goto fail; - } + if (!call(true, + JACKDBUS_IFACE_CONTROL, + "GetBufferSize", + &reply_ptr, + DBUS_TYPE_INVALID)) { + return 4096; + } - dbus_message_unref(reply_ptr); + 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 4096; + } - return buffer_size; + dbus_message_unref(reply_ptr); -fail: - return 4096; // something fake, patchage needs it to match combobox value + return buffer_size; } bool -JackDriver::set_buffer_size(jack_nframes_t size) +JackDriver::set_buffer_size(const uint32_t frames) { - DBusMessage* reply_ptr; - dbus_uint32_t buffer_size; - - buffer_size = size; + DBusMessage* reply_ptr = nullptr; + dbus_uint32_t buffer_size = frames; - if (!call(true, JACKDBUS_IFACE_CONTROL, "SetBufferSize", &reply_ptr, DBUS_TYPE_UINT32, &buffer_size, DBUS_TYPE_INVALID)) { - return false; - } + if (!call(true, + JACKDBUS_IFACE_CONTROL, + "SetBufferSize", + &reply_ptr, + DBUS_TYPE_UINT32, + &buffer_size, + DBUS_TYPE_INVALID)) { + return false; + } - dbus_message_unref(reply_ptr); + dbus_message_unref(reply_ptr); - return true; + return true; } -float +uint32_t JackDriver::sample_rate() { - DBusMessage* reply_ptr; - double sample_rate; + DBusMessage* reply_ptr = nullptr; + dbus_uint32_t sample_rate = 0u; - if (!call(true, JACKDBUS_IFACE_CONTROL, "GetSampleRate", &reply_ptr, DBUS_TYPE_INVALID)) { - return false; - } + if (!call(true, + 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; - } + if (!dbus_message_get_args(reply_ptr, + &_dbus_error, + DBUS_TYPE_UINT32, + &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); + dbus_message_unref(reply_ptr); - return sample_rate; + return sample_rate; } -bool -JackDriver::is_realtime() const -{ - DBusMessage* reply_ptr; - dbus_bool_t realtime; - - JackDriver* me = const_cast<JackDriver*>(this); - if (!me->call(true, JACKDBUS_IFACE_CONTROL, "IsRealtime", - &reply_ptr, DBUS_TYPE_INVALID)) { - return false; - } - - if (!dbus_message_get_args(reply_ptr, &me->_dbus_error, DBUS_TYPE_BOOLEAN, - &realtime, DBUS_TYPE_INVALID)) { - dbus_message_unref(reply_ptr); - dbus_error_free(&me->_dbus_error); - error_msg("decoding reply of IsRealtime failed."); - return false; - } - - dbus_message_unref(reply_ptr); - - return realtime; -} - -size_t -JackDriver::get_xruns() +PortType +JackDriver::patchage_port_type(const dbus_uint32_t dbus_port_type) const { - DBusMessage* reply_ptr; - dbus_uint32_t xruns; - - if (_server_responding && !_server_started) { - return 0; - } - - if (!call(true, JACKDBUS_IFACE_CONTROL, "GetXruns", &reply_ptr, DBUS_TYPE_INVALID)) { - return 0; - } - - 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); + switch (dbus_port_type) { + case JACKDBUS_PORT_TYPE_AUDIO: + return PortType::jack_audio; + case JACKDBUS_PORT_TYPE_MIDI: + return PortType::jack_midi; + default: + break; + } - return xruns; + error_msg(fmt::format("Unknown JACK D-Bus port type {}", dbus_port_type)); + return PortType::jack_audio; } -void -JackDriver::reset_xruns() +PortInfo +JackDriver::port_info(const std::string& port_name, + const dbus_uint32_t port_type, + const dbus_uint32_t port_flags) const { - DBusMessage* reply_ptr; + const SignalDirection direction = + ((port_flags & JACKDBUS_PORT_FLAG_INPUT) ? SignalDirection::input + : SignalDirection::output); - if (!call(true, JACKDBUS_IFACE_CONTROL, "ResetXruns", &reply_ptr, DBUS_TYPE_INVALID)) { - return; - } - - dbus_message_unref(reply_ptr); + // TODO: Metadata? + return {port_name, + patchage_port_type(port_type), + direction, + {}, + bool(port_flags & JACKDBUS_PORT_FLAG_TERMINAL)}; } -float -JackDriver::get_max_dsp_load() +void +JackDriver::error_msg(const std::string& msg) const { - DBusMessage* reply_ptr; - double load; - - if (_server_responding && !_server_started) { - return 0.0; - } - - if (!call(true, JACKDBUS_IFACE_CONTROL, "GetLoad", &reply_ptr, DBUS_TYPE_INVALID)) { - return 0.0; - } - - 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 0.0; - } - - dbus_message_unref(reply_ptr); - - load /= 100.0; // convert from percent to [0..1] - - if (load > _max_dsp_load) { - _max_dsp_load = load; - } - - return _max_dsp_load; + _log.error(std::string{"[JACK] "} + msg); } - void -JackDriver::reset_max_dsp_load() +JackDriver::info_msg(const std::string& msg) const { - _max_dsp_load = 0.0; + _log.info(std::string{"[JACK] "} + msg); } -PatchagePort* -JackDriver::create_port_view(Patchage* patchage, - const PortID& id) -{ - assert(false); // we dont use events at all - return NULL; -} +} // namespace -void -JackDriver::error_msg(const std::string& msg) const +std::unique_ptr<AudioDriver> +make_jack_driver(ILog& log, Driver::EventSink emit_event) { - _app->error_msg((std::string)"Jack: " + msg); + return std::unique_ptr<AudioDriver>{ + new JackDriver{log, std::move(emit_event)}}; } -void -JackDriver::info_msg(const std::string& msg) const -{ - _app->info_msg((std::string)"Jack: " + msg); -} +} // namespace patchage diff --git a/src/JackDbusDriver.hpp b/src/JackDbusDriver.hpp deleted file mode 100644 index 69cc0a5..0000000 --- a/src/JackDbusDriver.hpp +++ /dev/null @@ -1,161 +0,0 @@ -/* This file is part of Patchage. - * Copyright 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_JACKDBUSDRIVER_HPP -#define PATCHAGE_JACKDBUSDRIVER_HPP - -#include <string> -#include <boost/shared_ptr.hpp> -#include <jack/jack.h> -#include <jack/statistics.h> -#include <glibmm/thread.h> -#include <dbus/dbus.h> -#include "Driver.hpp" -#include "Patchage.hpp" -#include "PatchageModule.hpp" - -class PatchageEvent; -class PatchageCanvas; -class PatchagePort; - -class JackDriver : public Driver -{ -public: - explicit JackDriver(Patchage* app); - ~JackDriver(); - - void attach(bool launch_daemon); - void detach(); - - bool is_attached() const; - bool is_realtime() const; - - void refresh(); - void destroy_all(); - - bool connect( - PatchagePort* src, - PatchagePort* dst); - - bool disconnect( - PatchagePort* src, - PatchagePort* dst); - - size_t get_xruns(); - void reset_xruns(); - float get_max_dsp_load(); - void reset_max_dsp_load(); - - float sample_rate(); - jack_nframes_t buffer_size(); - bool set_buffer_size(jack_nframes_t size); - - void process_events(Patchage* app) {} - - PatchagePort* create_port_view( - Patchage* patchage, - const PortID& ref); - -private: - void error_msg(const std::string& msg) const; - void info_msg(const std::string& msg) const; - - PatchageModule* find_or_create_module( - ModuleType type, - const std::string& name); - - void add_port( - 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( - bool response_expected, - const char* iface, - const char* method, - DBusMessage** reply_ptr_ptr, - int in_type, - ...); - - void update_attached(); - - bool is_started(); - - void start_server(); - - void stop_server(); - - void refresh_internal(bool force); - - static DBusHandlerResult dbus_message_hook( - DBusConnection *connection, - DBusMessage *message, - void *me); - - void on_jack_appeared(); - - void on_jack_disappeared(); - -private: - Patchage* _app; - DBusError _dbus_error; - DBusConnection* _dbus_connection; - float _max_dsp_load; - - bool _server_responding; - bool _server_started; - - dbus_uint64_t _graph_version; -}; - -#endif // PATCHAGE_JACKDBUSDRIVER_HPP diff --git a/src/JackDriver.cpp b/src/JackDriver.cpp deleted file mode 100644 index 5daedae..0000000 --- a/src/JackDriver.cpp +++ /dev/null @@ -1,588 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <cassert> -#include <cstring> -#include <set> -#include <string> - -#include <boost/format.hpp> - -#include <jack/jack.h> -#include <jack/statistics.h> - -#include "JackDriver.hpp" -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageEvent.hpp" -#include "PatchageModule.hpp" -#include "Queue.hpp" -#include "patchage_config.h" -#ifdef HAVE_JACK_METADATA -#include <jack/metadata.h> -#include "jackey.h" -#endif - -using std::endl; -using std::string; -using boost::format; - -JackDriver::JackDriver(Patchage* app) - : _app(app) - , _client(NULL) - , _events(128) - , _xruns(0) - , _xrun_delay(0) - , _is_activated(false) -{ - _last_pos.frame = 0; - _last_pos.valid = (jack_position_bits_t)0; -} - -JackDriver::~JackDriver() -{ - detach(); -} - -/** Connect to Jack. - */ -void -JackDriver::attach(bool launch_daemon) -{ - // Already connected - if (_client) - return; - - jack_options_t options = (!launch_daemon) ? JackNoStartServer : JackNullOption; - _client = jack_client_open("Patchage", options, NULL); - if (_client == NULL) { - _app->error_msg("Jack: Unable to create client."); - _is_activated = false; - } else { - jack_client_t* const client = _client; - - jack_on_shutdown(client, jack_shutdown_cb, this); - jack_set_client_registration_callback(client, jack_client_registration_cb, this); - jack_set_port_registration_callback(client, jack_port_registration_cb, this); - jack_set_port_connect_callback(client, jack_port_connect_cb, this); - jack_set_xrun_callback(client, jack_xrun_cb, this); - - _buffer_size = jack_get_buffer_size(client); - - if (!jack_activate(client)) { - _is_activated = true; - signal_attached.emit(); - std::stringstream ss; - _app->info_msg("Jack: Attached."); - } else { - _app->error_msg("Jack: Client activation failed."); - _is_activated = false; - } - } -} - -void -JackDriver::detach() -{ - Glib::Mutex::Lock lock(_shutdown_mutex); - if (_client) { - jack_deactivate(_client); - jack_client_close(_client); - _client = NULL; - } - _is_activated = false; - signal_detached.emit(); - _app->info_msg("Jack: Detached."); -} - -static bool -is_jack_port(const PatchagePort* port) -{ - return (port->type() == JACK_AUDIO || - port->type() == JACK_MIDI || - port->type() == JACK_OSC || - port->type() == JACK_CV); -} - -/** Destroy all JACK (canvas) ports. - */ -void -JackDriver::destroy_all() -{ - if (_app->canvas()) { - _app->canvas()->remove_ports(is_jack_port); - } -} - -PatchagePort* -JackDriver::create_port_view(Patchage* patchage, - const PortID& id) -{ - assert(id.type == PortID::JACK_ID); - - jack_port_t* jack_port = jack_port_by_id(_client, id.id.jack_id); - if (!jack_port) { - _app->error_msg((format("Jack: Failed to find port with ID `%1%'.") - % id).str());; - return NULL; - } - - const int jack_flags = jack_port_flags(jack_port); - - string module_name, port_name; - port_names(id, module_name, port_name); - - ModuleType type = InputOutput; - if (_app->conf()->get_module_split( - module_name, (jack_flags & JackPortIsTerminal))) { - if (jack_flags & JackPortIsInput) { - type = Input; - } else { - type = Output; - } - } - - PatchageModule* parent = _app->canvas()->find_module(module_name, type); - if (!parent) { - parent = new PatchageModule(patchage, module_name, type); - parent->load_location(); - patchage->canvas()->add_module(module_name, parent); - } - - if (parent->get_port(port_name)) { - _app->error_msg((format("Jack: Module `%1%' already has port `%2%'.") - % module_name % port_name).str()); - return NULL; - } - - PatchagePort* port = create_port(*parent, jack_port, id); - port->show(); - if (port->is_input()) { - parent->set_is_source(false); - } - - return port; -} - -#ifdef HAVE_JACK_METADATA -static std::string -get_property(jack_uuid_t subject, const char* key) -{ - std::string result; - - char* value = NULL; - char* datatype = NULL; - if (!jack_get_property(subject, key, &value, &datatype)) { - result = value; - } - jack_free(datatype); - jack_free(value); - - return result; -} -#endif - -PatchagePort* -JackDriver::create_port(PatchageModule& parent, jack_port_t* port, PortID id) -{ - if (!port) { - return NULL; - } - - std::string label; - boost::optional<int> order; - -#ifdef HAVE_JACK_METADATA - const jack_uuid_t uuid = jack_port_uuid(port); - if (_app->conf()->get_sort_ports()) { - const std::string order_str = get_property(uuid, JACKEY_ORDER); - label = get_property(uuid, JACK_METADATA_PRETTY_NAME); - if (!order_str.empty()) { - order = atoi(order_str.c_str()); - } - } -#endif - - const char* const type_str = jack_port_type(port); - PortType port_type; - if (!strcmp(type_str, JACK_DEFAULT_AUDIO_TYPE)) { - port_type = JACK_AUDIO; -#ifdef HAVE_JACK_METADATA - if (get_property(uuid, JACKEY_SIGNAL_TYPE) == "CV") { - port_type = JACK_CV; - } -#endif - } else if (!strcmp(type_str, JACK_DEFAULT_MIDI_TYPE)) { - port_type = JACK_MIDI; -#ifdef HAVE_JACK_METADATA - if (get_property(uuid, JACKEY_EVENT_TYPES) == "OSC") { - port_type = JACK_OSC; - } -#endif - } else { - _app->warning_msg((format("Jack: Port `%1%' has unknown type `%2%'.") - % jack_port_name(port) % type_str).str()); - return NULL; - } - - PatchagePort* ret( - new PatchagePort(parent, port_type, jack_port_short_name(port), - label, - (jack_port_flags(port) & JackPortIsInput), - _app->conf()->get_port_color(port_type), - _app->show_human_names(), - order)); - - if (id.type != PortID::NULL_PORT_ID) { - dynamic_cast<PatchageCanvas*>(parent.canvas())->index_port(id, ret); - } - - return ret; -} - -void -JackDriver::shutdown() -{ - signal_detached.emit(); -} - -/** Refresh all Jack audio ports/connections. - * To be called from GTK thread only. - */ -void -JackDriver::refresh() -{ - const char** ports; - jack_port_t* port; - - // Jack can take _client away from us at any time throughout here :/ - // Shortest locks possible is the best solution I can figure out - - Glib::Mutex::Lock lock(_shutdown_mutex); - - if (_client == NULL) { - shutdown(); - return; - } - - ports = jack_get_ports(_client, NULL, NULL, 0); // get all existing ports - - if (!ports) { - return; - } - - string client1_name; - string port1_name; - string client2_name; - string port2_name; - size_t colon; - - // Add all ports - for (int i = 0; ports[i]; ++i) { - port = jack_port_by_name(_client, ports[i]); - - client1_name = ports[i]; - client1_name = client1_name.substr(0, client1_name.find(":")); - - ModuleType type = InputOutput; - if (_app->conf()->get_module_split( - client1_name, - (jack_port_flags(port) & JackPortIsTerminal))) { - if (jack_port_flags(port) & JackPortIsInput) { - type = Input; - } else { - type = Output; - } - } - - PatchageModule* m = _app->canvas()->find_module(client1_name, type); - - if (!m) { - m = new PatchageModule(_app, client1_name, type); - m->load_location(); - _app->canvas()->add_module(client1_name, m); - } - - if (!m->get_port(jack_port_short_name(port))) - create_port(*m, port, PortID()); - } - - // Add all connections - for (int i = 0; ports[i]; ++i) { - port = jack_port_by_name(_client, ports[i]); - const char** connected_ports = jack_port_get_all_connections(_client, port); - - client1_name = ports[i]; - colon = client1_name.find(':'); - port1_name = client1_name.substr(colon + 1); - client1_name = client1_name.substr(0, colon); - - const ModuleType port1_type = (jack_port_flags(port) & JackPortIsInput) - ? Input : Output; - - PatchageModule* client1_module - = _app->canvas()->find_module(client1_name, port1_type); - - if (connected_ports) { - for (int j = 0; connected_ports[j]; ++j) { - - client2_name = connected_ports[j]; - colon = client2_name.find(':'); - port2_name = client2_name.substr(colon+1); - client2_name = client2_name.substr(0, colon); - - const ModuleType port2_type = (port1_type == Input) ? Output : Input; - - PatchageModule* client2_module - = _app->canvas()->find_module(client2_name, port2_type); - - Ganv::Port* port1 = client1_module->get_port(port1_name); - Ganv::Port* port2 = client2_module->get_port(port2_name); - - if (!port1 || !port2) - continue; - - Ganv::Port* src = NULL; - Ganv::Port* dst = NULL; - - if (port1->is_output() && port2->is_input()) { - src = port1; - dst = port2; - } else { - src = port2; - dst = port1; - } - - if (src && dst && !_app->canvas()->get_edge(src, dst)) - _app->canvas()->make_connection(src, dst); - } - - jack_free(connected_ports); - } - } - - jack_free(ports); -} - -bool -JackDriver::port_names(const PortID& id, - string& module_name, - string& port_name) -{ - jack_port_t* jack_port = NULL; - - if (id.type == PortID::JACK_ID) - jack_port = jack_port_by_id(_client, id.id.jack_id); - - if (!jack_port) { - module_name.clear(); - port_name.clear(); - return false; - } - - const string full_name = jack_port_name(jack_port); - - module_name = full_name.substr(0, full_name.find(":")); - port_name = full_name.substr(full_name.find(":")+1); - - return true; -} - -/** Connects two Jack audio ports. - * To be called from GTK thread only. - * \return Whether connection succeeded. - */ -bool -JackDriver::connect(PatchagePort* src_port, - PatchagePort* dst_port) -{ - if (_client == NULL) - return false; - - int result = jack_connect(_client, src_port->full_name().c_str(), dst_port->full_name().c_str()); - - if (result == 0) - _app->info_msg(string("Jack: Connected ") - + src_port->full_name() + " => " + dst_port->full_name()); - else - _app->error_msg(string("Jack: Unable to connect ") - + src_port->full_name() + " => " + dst_port->full_name()); - - return (!result); -} - -/** Disconnects two Jack audio ports. - * To be called from GTK thread only. - * \return Whether disconnection succeeded. - */ -bool -JackDriver::disconnect(PatchagePort* const src_port, - PatchagePort* const dst_port) -{ - if (_client == NULL) - return false; - - int result = jack_disconnect(_client, src_port->full_name().c_str(), dst_port->full_name().c_str()); - - if (result == 0) - _app->info_msg(string("Jack: Disconnected ") - + src_port->full_name() + " => " + dst_port->full_name()); - else - _app->error_msg(string("Jack: Unable to disconnect ") - + src_port->full_name() + " => " + dst_port->full_name()); - - return (!result); -} - -void -JackDriver::jack_client_registration_cb(const char* name, int registered, void* jack_driver) -{ - JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); - assert(me->_client); - - if (registered) { - me->_events.push(PatchageEvent(PatchageEvent::CLIENT_CREATION, name)); - } else { - me->_events.push(PatchageEvent(PatchageEvent::CLIENT_DESTRUCTION, name)); - } -} - -void -JackDriver::jack_port_registration_cb(jack_port_id_t port_id, int registered, void* jack_driver) -{ - JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); - assert(me->_client); - - if (registered) { - me->_events.push(PatchageEvent(PatchageEvent::PORT_CREATION, port_id)); - } else { - me->_events.push(PatchageEvent(PatchageEvent::PORT_DESTRUCTION, port_id)); - } -} - -void -JackDriver::jack_port_connect_cb(jack_port_id_t src, jack_port_id_t dst, int connect, void* jack_driver) -{ - JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); - assert(me->_client); - - if (connect) { - me->_events.push(PatchageEvent(PatchageEvent::CONNECTION, src, dst)); - } else { - me->_events.push(PatchageEvent(PatchageEvent::DISCONNECTION, src, dst)); - } -} - -int -JackDriver::jack_xrun_cb(void* jack_driver) -{ - JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); - assert(me->_client); - - ++me->_xruns; - me->_xrun_delay = jack_get_xrun_delayed_usecs(me->_client); - - jack_reset_max_delayed_usecs(me->_client); - - return 0; -} - -void -JackDriver::jack_shutdown_cb(void* jack_driver) -{ - assert(jack_driver); - JackDriver* me = reinterpret_cast<JackDriver*>(jack_driver); - me->_app->info_msg("Jack: Shutdown."); - Glib::Mutex::Lock lock(me->_shutdown_mutex); - me->_client = NULL; - me->_is_activated = false; - me->signal_detached.emit(); -} - -jack_nframes_t -JackDriver::buffer_size() -{ - if (_is_activated) - return _buffer_size; - else - return jack_get_buffer_size(_client); -} - -void -JackDriver::reset_xruns() -{ - _xruns = 0; - _xrun_delay = 0; -} - -float -JackDriver::get_max_dsp_load() -{ - float max_load = 0.0f; - if (_client) { - const float 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() -{ - if (_client) { - jack_reset_max_delayed_usecs(_client); - } -} - -bool -JackDriver::set_buffer_size(jack_nframes_t size) -{ - if (buffer_size() == size) { - return true; - } - - if (!_client) { - _buffer_size = size; - return true; - } - - if (jack_set_buffer_size(_client, size)) { - _app->error_msg("[JACK] Unable to set buffer size"); - return false; - } else { - _buffer_size = size; - return true; - } -} - -void -JackDriver::process_events(Patchage* app) -{ - while (!_events.empty()) { - PatchageEvent& ev = _events.front(); - ev.execute(app); - _events.pop(); - } -} diff --git a/src/JackDriver.hpp b/src/JackDriver.hpp deleted file mode 100644 index 875bd61..0000000 --- a/src/JackDriver.hpp +++ /dev/null @@ -1,109 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_JACKDRIVER_HPP -#define PATCHAGE_JACKDRIVER_HPP - -#include <string> - -#include <jack/jack.h> - -#include <glibmm/thread.h> - -#include "Driver.hpp" -#include "Queue.hpp" - -class Patchage; -class PatchageEvent; -class PatchageCanvas; -class PatchagePort; -class PatchageModule; - -/** Handles all externally driven functionality, registering ports etc. - * - * Jack callbacks and connect methods and things like that live here. - * Right now just for jack ports, but that will change... - */ -class JackDriver : public Driver -{ -public: - explicit JackDriver(Patchage* app); - ~JackDriver(); - - void attach(bool launch_daemon); - void detach(); - - bool is_attached() const { return (_client != NULL); } - bool is_realtime() const { return _client && jack_is_realtime(_client); } - - void refresh(); - void destroy_all(); - - bool port_names(const PortID& id, - std::string& module_name, - std::string& port_name); - - PatchagePort* create_port_view(Patchage* patchage, - const PortID& id); - - bool connect(PatchagePort* src, - PatchagePort* dst); - - bool disconnect(PatchagePort* src, - PatchagePort* dst); - - uint32_t get_xruns() { return _xruns; } - void reset_xruns(); - float get_max_dsp_load(); - void reset_max_dsp_load(); - - jack_client_t* client() { return _client; } - - jack_nframes_t sample_rate() { return jack_get_sample_rate(_client); } - jack_nframes_t buffer_size(); - bool set_buffer_size(jack_nframes_t size); - - void process_events(Patchage* app); - -private: - PatchagePort* create_port( - PatchageModule& parent, - jack_port_t* port, - PortID id); - - void shutdown(); - - static void jack_client_registration_cb(const char* name, int registered, void* me); - static void jack_port_registration_cb(jack_port_id_t port_id, int registered, void* me); - static void jack_port_connect_cb(jack_port_id_t src, jack_port_id_t dst, int connect, void* me); - static int jack_xrun_cb(void* me); - static void jack_shutdown_cb(void* me); - - Patchage* _app; - jack_client_t* _client; - - Queue<PatchageEvent> _events; - - Glib::Mutex _shutdown_mutex; - - jack_position_t _last_pos; - jack_nframes_t _buffer_size; - uint32_t _xruns; - float _xrun_delay; - bool _is_activated :1; -}; - -#endif // PATCHAGE_JACKDRIVER_HPP diff --git a/src/JackLibDriver.cpp b/src/JackLibDriver.cpp new file mode 100644 index 0000000..5133bc6 --- /dev/null +++ b/src/JackLibDriver.cpp @@ -0,0 +1,490 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "AudioDriver.hpp" +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "ClientType.hpp" +#include "Driver.hpp" +#include "Event.hpp" +#include "ILog.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" +#include "PortNames.hpp" +#include "PortType.hpp" +#include "SignalDirection.hpp" +#include "jackey.h" +#include "make_jack_driver.hpp" +#include "patchage_config.h" +#include "warnings.hpp" + +#if USE_JACK_METADATA +# include <jack/metadata.h> +#endif + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS + +#include <jack/jack.h> +#include <jack/types.h> + +#include <cstdint> +#include <cstring> +#include <functional> +#include <memory> +#include <mutex> +#include <optional> +#include <set> +#include <string> +#include <unordered_set> +#include <utility> + +namespace patchage { +namespace { + +/// Driver for JACK audio and midi ports that uses libjack +class JackLibDriver : public AudioDriver +{ +public: + explicit JackLibDriver(ILog& log, EventSink emit_event); + + JackLibDriver(const JackLibDriver&) = delete; + JackLibDriver& operator=(const JackLibDriver&) = delete; + + JackLibDriver(JackLibDriver&&) = delete; + JackLibDriver& operator=(JackLibDriver&&) = delete; + + ~JackLibDriver() override; + + // Driver interface + void attach(bool launch_daemon) override; + void detach() override; + bool is_attached() const override; + void refresh(const EventSink& sink) override; + bool connect(const PortID& tail_id, const PortID& head_id) override; + bool disconnect(const PortID& tail_id, const PortID& head_id) override; + + // AudioDriver interface + uint32_t xruns() override; + void reset_xruns() override; + uint32_t buffer_size() override; + bool set_buffer_size(uint32_t frames) override; + uint32_t sample_rate() override; + +private: + ClientInfo get_client_info(const char* name); + PortInfo get_port_info(const jack_port_t* port); + + static void on_client(const char* name, int registered, void* driver); + + static void on_port(jack_port_id_t port_id, int registered, void* driver); + + static void on_connection(jack_port_id_t src, + jack_port_id_t dst, + int connect, + void* driver); + + static int on_xrun(void* driver); + + static void on_shutdown(void* driver); + + ILog& _log; + std::mutex _shutdown_mutex; + + jack_client_t* _client = nullptr; + jack_nframes_t _buffer_size = 0u; + uint32_t _xruns = 0u; + bool _is_activated = false; +}; + +JackLibDriver::JackLibDriver(ILog& log, EventSink emit_event) + : AudioDriver{std::move(emit_event)} + , _log{log} +{} + +JackLibDriver::~JackLibDriver() +{ + detach(); +} + +void +JackLibDriver::attach(const bool launch_daemon) +{ + if (_client) { + return; // Already connected + } + + const jack_options_t options = + (!launch_daemon) ? JackNoStartServer : JackNullOption; + + if (!(_client = jack_client_open("Patchage", options, nullptr))) { + _log.error("[JACK] Unable to create client"); + _is_activated = false; + return; + } + + jack_on_shutdown(_client, on_shutdown, this); + jack_set_client_registration_callback(_client, on_client, this); + jack_set_port_registration_callback(_client, on_port, this); + jack_set_port_connect_callback(_client, on_connection, this); + jack_set_xrun_callback(_client, on_xrun, this); + + if (jack_activate(_client)) { + _log.error("[JACK] Client activation failed"); + _is_activated = false; + _buffer_size = 0; + return; + } + + _is_activated = true; + _buffer_size = jack_get_buffer_size(_client); + + _emit_event(event::DriverAttached{ClientType::jack}); +} + +void +JackLibDriver::detach() +{ + const std::lock_guard<std::mutex> lock{_shutdown_mutex}; + + if (_client) { + jack_deactivate(_client); + jack_client_close(_client); + _client = nullptr; + } + + _is_activated = false; + _emit_event(event::DriverDetached{ClientType::jack}); +} + +bool +JackLibDriver::is_attached() const +{ + return _client != nullptr; +} + +std::string +get_property(const jack_uuid_t subject, const char* const key) +{ + std::string result; + +#if USE_JACK_METADATA + char* value = nullptr; + char* datatype = nullptr; + if (!jack_get_property(subject, key, &value, &datatype)) { + result = value; + } + jack_free(datatype); + jack_free(value); +#else + (void)subject; + (void)key; +#endif + + return result; +} + +ClientInfo +JackLibDriver::get_client_info(const char* const name) +{ + return {name}; // TODO: Pretty name? +} + +PortInfo +JackLibDriver::get_port_info(const jack_port_t* const port) +{ + const auto uuid = jack_port_uuid(port); + const auto flags = jack_port_flags(port); + const std::string name = jack_port_name(port); + auto label = PortNames{name}.port(); + + // Get pretty name to use as a label, if present +#if USE_JACK_METADATA + const auto pretty_name = get_property(uuid, JACK_METADATA_PRETTY_NAME); + if (!pretty_name.empty()) { + label = pretty_name; + } +#endif + + // Determine detailed type, using metadata for fancy types if possible + const char* const type_str = jack_port_type(port); + PortType type = PortType::jack_audio; + if (!strcmp(type_str, JACK_DEFAULT_AUDIO_TYPE)) { + if (get_property(uuid, JACKEY_SIGNAL_TYPE) == "CV") { + type = PortType::jack_cv; + } + } else if (!strcmp(type_str, JACK_DEFAULT_MIDI_TYPE)) { + type = PortType::jack_midi; + if (get_property(uuid, JACKEY_EVENT_TYPES) == "OSC") { + type = PortType::jack_osc; + } + } else { + _log.warning( + fmt::format(R"([JACK] Port "{}" has unknown type "{}")", name, type_str)); + } + + // Get direction from port flags + const SignalDirection direction = + ((flags & JackPortIsInput) ? SignalDirection::input + : SignalDirection::output); + + // Get port order from metadata if possible + std::optional<int> order; + const std::string order_str = get_property(uuid, JACKEY_ORDER); + if (!order_str.empty()) { + order = std::stoi(order_str); + } + + return {label, type, direction, order, bool(flags & JackPortIsTerminal)}; +} + +void +JackLibDriver::refresh(const EventSink& sink) +{ + const std::lock_guard<std::mutex> lock{_shutdown_mutex}; + + if (!_client) { + return; + } + + // Get all existing ports + const char** const ports = jack_get_ports(_client, nullptr, nullptr, 0); + if (!ports) { + return; + } + + // Get all client names (to only send a creation event once for each) + std::unordered_set<std::string> client_names; + for (auto i = 0u; ports[i]; ++i) { + client_names.insert(PortID::jack(ports[i]).client().jack_name()); + } + + // Emit all clients + for (const auto& client_name : client_names) { + sink({event::ClientCreated{ClientID::jack(client_name), + get_client_info(client_name.c_str())}}); + } + + // Emit all ports + for (auto i = 0u; ports[i]; ++i) { + const jack_port_t* const port = jack_port_by_name(_client, ports[i]); + + sink({event::PortCreated{PortID::jack(ports[i]), get_port_info(port)}}); + } + + // Get all connections (again to only create them once) + std::set<std::pair<std::string, std::string>> connections; + for (auto i = 0u; ports[i]; ++i) { + const jack_port_t* const port = jack_port_by_name(_client, ports[i]); + const char** const peers = jack_port_get_all_connections(_client, port); + + if (peers) { + if (jack_port_flags(port) & JackPortIsInput) { + for (auto j = 0u; peers[j]; ++j) { + connections.emplace(peers[j], ports[i]); + } + } else { + for (auto j = 0u; peers[j]; ++j) { + connections.emplace(ports[i], peers[j]); + } + } + + jack_free(peers); + } + } + + // Emit all connections + for (const auto& connection : connections) { + sink({event::PortsConnected{PortID::jack(connection.first), + PortID::jack(connection.second)}}); + } + + jack_free(ports); +} + +bool +JackLibDriver::connect(const PortID& tail_id, const PortID& head_id) +{ + if (!_client) { + return false; + } + + const auto& tail_name = tail_id.jack_name(); + const auto& head_name = head_id.jack_name(); + + const int result = + jack_connect(_client, tail_name.c_str(), head_name.c_str()); + + if (result) { + _log.error( + fmt::format("[JACK] Failed to connect {} => {}", tail_name, head_name)); + return false; + } + + return true; +} + +bool +JackLibDriver::disconnect(const PortID& tail_id, const PortID& head_id) +{ + if (!_client) { + return false; + } + + const auto& tail_name = tail_id.jack_name(); + const auto& head_name = head_id.jack_name(); + + const int result = + jack_disconnect(_client, tail_name.c_str(), head_name.c_str()); + + if (result) { + _log.error(fmt::format( + "[JACK] Failed to disconnect {} => {}", tail_name, head_name)); + return false; + } + + return true; +} + +uint32_t +JackLibDriver::xruns() +{ + return _xruns; +} + +void +JackLibDriver::reset_xruns() +{ + _xruns = 0; +} + +uint32_t +JackLibDriver::buffer_size() +{ + return _is_activated ? _buffer_size : jack_get_buffer_size(_client); +} + +bool +JackLibDriver::set_buffer_size(const uint32_t frames) +{ + if (!_client) { + _buffer_size = frames; + return true; + } + + if (buffer_size() == frames) { + return true; + } + + if (jack_set_buffer_size(_client, frames)) { + _log.error("[JACK] Unable to set buffer size"); + return false; + } + + _buffer_size = frames; + return true; +} + +uint32_t +JackLibDriver::sample_rate() +{ + return jack_get_sample_rate(_client); +} + +void +JackLibDriver::on_client(const char* const name, + const int registered, + void* const driver) +{ + auto* const me = static_cast<JackLibDriver*>(driver); + + if (registered) { + me->_emit_event(event::ClientCreated{ClientID::jack(name), {name}}); + } else { + me->_emit_event(event::ClientDestroyed{ClientID::jack(name)}); + } +} + +void +JackLibDriver::on_port(const jack_port_id_t port_id, + const int registered, + void* const driver) +{ + auto* const me = static_cast<JackLibDriver*>(driver); + + jack_port_t* const port = jack_port_by_id(me->_client, port_id); + const char* const name = jack_port_name(port); + const auto id = PortID::jack(name); + + if (registered) { + me->_emit_event(event::PortCreated{id, me->get_port_info(port)}); + } else { + me->_emit_event(event::PortDestroyed{id}); + } +} + +void +JackLibDriver::on_connection(const jack_port_id_t src, + const jack_port_id_t dst, + const int connect, + void* const driver) +{ + auto* const me = static_cast<JackLibDriver*>(driver); + + jack_port_t* const src_port = jack_port_by_id(me->_client, src); + jack_port_t* const dst_port = jack_port_by_id(me->_client, dst); + const char* const src_name = jack_port_name(src_port); + const char* const dst_name = jack_port_name(dst_port); + + if (connect) { + me->_emit_event( + event::PortsConnected{PortID::jack(src_name), PortID::jack(dst_name)}); + } else { + me->_emit_event( + event::PortsDisconnected{PortID::jack(src_name), PortID::jack(dst_name)}); + } +} + +int +JackLibDriver::on_xrun(void* const driver) +{ + auto* const me = static_cast<JackLibDriver*>(driver); + + ++me->_xruns; + + return 0; +} + +void +JackLibDriver::on_shutdown(void* const driver) +{ + auto* const me = static_cast<JackLibDriver*>(driver); + + /* Note that the JACK documentation lies about this situation. It says the + client must not call jack_client_close() here... except that is exactly + what libjack does if a shutdown callback isn't registered. Despite + that, doing so here hangs forever. Handling it "properly" like a signal + handler and calling jack_client_close() in another thread also hangs. + + So, since JACK is a hot mess and it's impossible to gracefully handle + this situation, just leak the client. */ + + const std::lock_guard<std::mutex> lock{me->_shutdown_mutex}; + + me->_client = nullptr; + me->_is_activated = false; + + me->_emit_event(event::DriverDetached{ClientType::jack}); +} + +} // namespace + +std::unique_ptr<AudioDriver> +make_jack_driver(ILog& log, Driver::EventSink emit_event) +{ + return std::unique_ptr<AudioDriver>{ + new JackLibDriver{log, std::move(emit_event)}}; +} + +} // namespace patchage diff --git a/src/JackStubDriver.cpp b/src/JackStubDriver.cpp new file mode 100644 index 0000000..a062df1 --- /dev/null +++ b/src/JackStubDriver.cpp @@ -0,0 +1,18 @@ +// Copyright 2020-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "AudioDriver.hpp" +#include "Driver.hpp" +#include "make_jack_driver.hpp" + +#include <memory> + +namespace patchage { + +std::unique_ptr<AudioDriver> +make_jack_driver(ILog&, Driver::EventSink) +{ + return nullptr; +} + +} // namespace patchage diff --git a/src/Legend.cpp b/src/Legend.cpp new file mode 100644 index 0000000..1ef833e --- /dev/null +++ b/src/Legend.cpp @@ -0,0 +1,82 @@ +// Copyright 2014-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Legend.hpp" + +#include "Configuration.hpp" +#include "PortType.hpp" +#include "i18n.hpp" +#include "patchage_config.h" + +#include <gdkmm/color.h> +#include <glibmm/signalproxy.h> +#include <gtkmm/box.h> +#include <gtkmm/colorbutton.h> +#include <gtkmm/label.h> +#include <gtkmm/object.h> +#include <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> + +#include <string> + +namespace patchage { + +Legend::Legend(const Configuration& configuration) +{ + add_button(PortType::jack_audio, + T("Audio"), + configuration.get_port_color(PortType::jack_audio)); + +#if USE_JACK_METADATA + add_button( + PortType::jack_cv, "CV", configuration.get_port_color(PortType::jack_cv)); + add_button(PortType::jack_osc, + "OSC", + configuration.get_port_color(PortType::jack_osc)); +#endif + + add_button(PortType::jack_midi, + "MIDI", + configuration.get_port_color(PortType::jack_midi)); + + add_button(PortType::alsa_midi, + "ALSA MIDI", + configuration.get_port_color(PortType::alsa_midi)); + + show_all_children(); +} + +void +Legend::add_button(const PortType id, const std::string& label, uint32_t rgba) +{ + Gdk::Color col; + col.set_rgb(((rgba >> 24) & 0xFF) * 0x100, + ((rgba >> 16) & 0xFF) * 0x100, + ((rgba >> 8) & 0xFF) * 0x100); + + auto* box = new Gtk::HBox(); + auto* but = new Gtk::ColorButton(col); + but->set_use_alpha(false); + but->signal_color_set().connect( + sigc::bind(sigc::mem_fun(this, &Legend::on_color_set), id, label, but)); + + box->pack_end(*Gtk::manage(but)); + box->pack_end(*Gtk::manage(new Gtk::Label(label)), false, false, 2); + + this->pack_start(*Gtk::manage(box), false, false, 6); +} + +void +Legend::on_color_set(const PortType id, + const std::string& label, + const Gtk::ColorButton* but) +{ + const Gdk::Color col = but->get_color(); + const uint32_t rgba = + (((col.get_red() / 0x100) << 24) | ((col.get_green() / 0x100) << 16) | + ((col.get_blue() / 0x100) << 8) | 0xFF); + + signal_color_changed.emit(id, label, rgba); +} + +} // namespace patchage diff --git a/src/Legend.hpp b/src/Legend.hpp index b95d30c..c73a74e 100644 --- a/src/Legend.hpp +++ b/src/Legend.hpp @@ -1,71 +1,40 @@ -/* This file is part of Patchage. - * Copyright 2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2014-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifndef PATCHAGE_LEGEND_HPP #define PATCHAGE_LEGEND_HPP -#include <gtkmm/colorbutton.h> #include <gtkmm/box.h> +#include <sigc++/signal.h> -#include "Configuration.hpp" +#include <cstdint> +#include <string> -class Legend : public Gtk::HBox { -public: - Legend(const Configuration& configuration) { - add_button(JACK_AUDIO, "Audio", configuration.get_port_color(JACK_AUDIO)); -#ifdef HAVE_JACK_METADATA - add_button(JACK_CV, "CV", configuration.get_port_color(JACK_CV)); - add_button(JACK_OSC, "OSC", configuration.get_port_color(JACK_OSC)); -#endif - add_button(JACK_MIDI, "MIDI", configuration.get_port_color(JACK_MIDI)); - add_button(ALSA_MIDI, "ALSA MIDI", configuration.get_port_color(ALSA_MIDI)); - show_all_children(); - } +namespace Gtk { +class ColorButton; +} // namespace Gtk + +namespace patchage { - void add_button(int id, const std::string& label, uint32_t rgba) { - Gdk::Color col; - col.set_rgb(((rgba >> 24) & 0xFF) * 0x100, - ((rgba>> 16) & 0xFF) * 0x100, - ((rgba >> 8) & 0xFF) * 0x100); - Gtk::HBox* box = new Gtk::HBox(); - Gtk::ColorButton* but = new Gtk::ColorButton(col); - but->set_use_alpha(false); - but->signal_color_set().connect( - sigc::bind(sigc::mem_fun(this, &Legend::on_color_set), - id, label, but)); +enum class PortType; - box->pack_end(*Gtk::manage(but)); - box->pack_end(*Gtk::manage(new Gtk::Label(label)), false, false, 2); +class Configuration; - this->pack_start(*Gtk::manage(box), false, false, 6); - } +class Legend : public Gtk::HBox +{ +public: + explicit Legend(const Configuration& configuration); - void on_color_set(const int id, - const std::string& label, - const Gtk::ColorButton* but) { - const Gdk::Color col = but->get_color(); - const uint32_t rgba = (((col.get_red() / 0x100) << 24) | - ((col.get_green() / 0x100) << 16) | - ((col.get_blue() / 0x100) << 8) | - 0xFF); + sigc::signal<void, PortType, std::string, uint32_t> signal_color_changed; - signal_color_changed.emit(id, label, rgba); - } +private: + void add_button(PortType id, const std::string& label, uint32_t rgba); - sigc::signal<void, int, std::string, uint32_t> signal_color_changed; + void on_color_set(PortType id, + const std::string& label, + const Gtk::ColorButton* but); }; +} // namespace patchage + #endif // PATCHAGE_LEGEND_HPP diff --git a/src/Metadata.cpp b/src/Metadata.cpp new file mode 100644 index 0000000..929b090 --- /dev/null +++ b/src/Metadata.cpp @@ -0,0 +1,72 @@ +// Copyright 2014-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Metadata.hpp" + +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" + +#include <optional> +#include <utility> + +namespace patchage { + +std::optional<ClientInfo> +Metadata::client(const ClientID& id) const +{ + const auto i = _client_data.find(id); + if (i == _client_data.end()) { + return {}; + } + + return i->second; +} + +std::optional<PortInfo> +Metadata::port(const PortID& id) const +{ + const auto i = _port_data.find(id); + if (i == _port_data.end()) { + return {}; + } + + return i->second; +} + +void +Metadata::set_client(const ClientID& id, const ClientInfo& info) +{ + const auto i = _client_data.find(id); + if (i == _client_data.end()) { + _client_data.emplace(id, info); + } else { + i->second = info; + } +} + +void +Metadata::set_port(const PortID& id, const PortInfo& info) +{ + const auto i = _port_data.find(id); + if (i == _port_data.end()) { + _port_data.emplace(id, info); + } else { + i->second = info; + } +} + +void +Metadata::erase_client(const ClientID& id) +{ + _client_data.erase(id); +} + +void +Metadata::erase_port(const PortID& id) +{ + _port_data.erase(id); +} + +} // namespace patchage diff --git a/src/Metadata.hpp b/src/Metadata.hpp new file mode 100644 index 0000000..520b0ce --- /dev/null +++ b/src/Metadata.hpp @@ -0,0 +1,42 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_METADATA_HPP +#define PATCHAGE_METADATA_HPP + +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" + +#include <map> +#include <optional> + +namespace patchage { + +/// Cache of metadata about clients and ports beyond their IDs +class Metadata +{ +public: + Metadata() = default; + + std::optional<ClientInfo> client(const ClientID& id) const; + std::optional<PortInfo> port(const PortID& id) const; + + void set_client(const ClientID& id, const ClientInfo& info); + void set_port(const PortID& id, const PortInfo& info); + + void erase_client(const ClientID& id); + void erase_port(const PortID& id); + +private: + using ClientData = std::map<ClientID, ClientInfo>; + using PortData = std::map<PortID, PortInfo>; + + ClientData _client_data; + PortData _port_data; +}; + +} // namespace patchage + +#endif // PATCHAGE_METADATA_HPP diff --git a/src/Options.hpp b/src/Options.hpp new file mode 100644 index 0000000..76846aa --- /dev/null +++ b/src/Options.hpp @@ -0,0 +1,16 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_OPTIONS_HPP +#define PATCHAGE_OPTIONS_HPP + +namespace patchage { + +struct Options { + bool alsa_driver_autoattach = true; + bool jack_driver_autoattach = true; +}; + +} // namespace patchage + +#endif // PATCHAGE_OPTIONS_HPP diff --git a/src/Patchage.cpp b/src/Patchage.cpp index eae2ef9..785cd2d 100644 --- a/src/Patchage.cpp +++ b/src/Patchage.cpp @@ -1,1078 +1,915 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <stdlib.h> -#include <pthread.h> +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later -#include <cmath> -#include <fstream> +#include "Patchage.hpp" -#include <glib.h> -#include <glib/gstdio.h> -#include <gtk/gtkwindow.h> +#include "Action.hpp" +#include "AudioDriver.hpp" +#include "Canvas.hpp" +#include "CanvasModule.hpp" +#include "CanvasPort.hpp" +#include "Configuration.hpp" +#include "Coord.hpp" +#include "Driver.hpp" +#include "Drivers.hpp" +#include "Event.hpp" +#include "Legend.hpp" +#include "Options.hpp" +#include "PortType.hpp" +#include "Reactor.hpp" +#include "Setting.hpp" +#include "TextViewLog.hpp" +#include "UIFile.hpp" +#include "Widget.hpp" +#include "event_to_string.hpp" +#include "handle_event.hpp" +#include "i18n.hpp" +#include "patchage_config.h" // IWYU pragma: keep +#include "warnings.hpp" + +PATCHAGE_DISABLE_GANV_WARNINGS +#include "ganv/Edge.hpp" +#include "ganv/Module.hpp" +#include "ganv/Node.hpp" +#include "ganv/Port.hpp" +#include "ganv/module.h" +#include "ganv/types.h" +PATCHAGE_RESTORE_WARNINGS -#include <boost/format.hpp> +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS -#include <gtkmm/button.h> +#include <glib-object.h> +#include <glib.h> +#include <glibmm/fileutils.h> +#include <glibmm/main.h> +#include <glibmm/miscutils.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/signalproxy.h> +#include <glibmm/ustring.h> +#include <gobject/gclosure.h> +#include <gtk/gtk.h> +#include <gtkmm/aboutdialog.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/alignment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/checkmenuitem.h> +#include <gtkmm/combobox.h> +#include <gtkmm/dialog.h> +#include <gtkmm/enums.h> +#include <gtkmm/filechooser.h> #include <gtkmm/filechooserdialog.h> +#include <gtkmm/filefilter.h> +#include <gtkmm/imagemenuitem.h> +#include <gtkmm/label.h> +#include <gtkmm/layout.h> #include <gtkmm/liststore.h> +#include <gtkmm/menubar.h> #include <gtkmm/menuitem.h> #include <gtkmm/messagedialog.h> +#include <gtkmm/object.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> #include <gtkmm/stock.h> +#include <gtkmm/textbuffer.h> +#include <gtkmm/texttag.h> +#include <gtkmm/textview.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/toolbutton.h> +#include <gtkmm/treeiter.h> #include <gtkmm/treemodel.h> +#include <gtkmm/window.h> +#include <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> +#include <sigc++/functors/ptr_fun.h> +#include <sigc++/signal.h> -#include "ganv/Module.hpp" -#include "ganv/Edge.hpp" - -#include "Configuration.hpp" -#include "Legend.hpp" -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageEvent.hpp" -#include "UIFile.hpp" -#include "patchage_config.h" - -#if defined(HAVE_JACK_DBUS) - #include "JackDbusDriver.hpp" -#elif defined(PATCHAGE_LIBJACK) - #include "JackDriver.hpp" - #include <jack/statistics.h> -#endif +#include <algorithm> +#include <cmath> +#include <cstdint> +#include <cstdlib> +#include <functional> +#include <map> +#include <optional> +#include <utility> +#include <variant> -#ifdef PATCHAGE_JACK_SESSION - #include <jack/session.h> -#endif +#ifdef PATCHAGE_GTK_OSX -#ifdef HAVE_ALSA - #include "AlsaDriver.hpp" -#endif +# include <gtkmm/main.h> +# include <gtkosxapplication.h> -#ifdef PATCHAGE_GTK_OSX - #include <gtkosxapplication.h> +namespace { -static gboolean -can_activate_cb(GtkWidget* widget, guint signal_id, gpointer data) +gboolean +can_activate_cb(GtkWidget* widget, guint, gpointer) { return gtk_widget_is_sensitive(widget); } -static void -terminate_cb(GtkosxApplication* app, gpointer data) +void +terminate_cb(GtkosxApplication*, gpointer data) { - Patchage* patchage = (Patchage*)data; - patchage->save(); - Gtk::Main::quit(); + auto* patchage = static_cast<patchage::Patchage*>(data); + patchage->save(); + Gtk::Main::quit(); } +} // namespace + #endif -static bool -configure_cb(GtkWindow* parentWindow, GdkEvent* event, gpointer data) +namespace patchage { + +namespace { + +bool +configure_cb(GtkWindow*, GdkEvent*, gpointer data) { - ((Patchage*)data)->store_window_location(); - return FALSE; + static_cast<Patchage*>(data)->store_window_location(); + return FALSE; } -static int -port_order(const GanvPort* a, const GanvPort* b, void* data) +int +port_order(const GanvPort* a, const GanvPort* b, void*) { - const PatchagePort* pa = dynamic_cast<const PatchagePort*>(Glib::wrap(a)); - const PatchagePort* pb = dynamic_cast<const PatchagePort*>(Glib::wrap(b)); - if (pa && pb) { - if (pa->order() && pb->order()) { - return *pa->order() - *pb->order(); - } else if (pa->order()) { - return -1; - } else if (pb->order()) { - return 1; - } - return pa->name().compare(pb->name()); - } - return 0; -} + const auto* pa = dynamic_cast<const CanvasPort*>(Glib::wrap(a)); + const auto* pb = dynamic_cast<const CanvasPort*>(Glib::wrap(b)); + if (pa && pb) { + const auto oa = pa->order(); + const auto ob = pb->order(); + if (oa && ob) { + return *oa - *ob; + } -struct ProjectList_column_record : public Gtk::TreeModel::ColumnRecord { - Gtk::TreeModelColumn<Glib::ustring> label; -}; + if (pa->order()) { + return -1; + } -using std::cout; -using std::endl; -using std::string; + if (pb->order()) { + return 1; + } -#define INIT_WIDGET(x) x(_xml, ((const char*)#x) + 1) + return pa->name().compare(pb->name()); + } + return 0; +} -Patchage::Patchage(int argc, char** argv) - : _xml(UIFile::open("patchage")) -#ifdef HAVE_ALSA - , _alsa_driver(NULL) -#endif - , _jack_driver(NULL) - , _conf(NULL) - , INIT_WIDGET(_about_win) - , INIT_WIDGET(_main_scrolledwin) - , INIT_WIDGET(_main_win) - , INIT_WIDGET(_main_vbox) - , INIT_WIDGET(_menubar) - , INIT_WIDGET(_menu_alsa_connect) - , INIT_WIDGET(_menu_alsa_disconnect) - , INIT_WIDGET(_menu_file_quit) - , INIT_WIDGET(_menu_export_image) - , INIT_WIDGET(_menu_help_about) - , INIT_WIDGET(_menu_jack_connect) - , INIT_WIDGET(_menu_jack_disconnect) - , INIT_WIDGET(_menu_open_session) - , INIT_WIDGET(_menu_save_session) - , INIT_WIDGET(_menu_save_close_session) - , INIT_WIDGET(_menu_view_arrange) - , INIT_WIDGET(_menu_view_sprung_layout) - , INIT_WIDGET(_menu_view_messages) - , INIT_WIDGET(_menu_view_toolbar) - , INIT_WIDGET(_menu_view_refresh) - , INIT_WIDGET(_menu_view_human_names) - , INIT_WIDGET(_menu_view_sort_ports) - , INIT_WIDGET(_menu_zoom_in) - , INIT_WIDGET(_menu_zoom_out) - , INIT_WIDGET(_menu_zoom_normal) - , INIT_WIDGET(_menu_zoom_full) - , INIT_WIDGET(_menu_increase_font_size) - , INIT_WIDGET(_menu_decrease_font_size) - , INIT_WIDGET(_menu_normal_font_size) - , INIT_WIDGET(_toolbar) - , INIT_WIDGET(_clear_load_but) - , INIT_WIDGET(_xrun_progress) - , INIT_WIDGET(_buf_size_combo) - , INIT_WIDGET(_latency_label) - , INIT_WIDGET(_legend_alignment) - , INIT_WIDGET(_main_paned) - , INIT_WIDGET(_log_scrolledwindow) - , INIT_WIDGET(_status_text) - , _legend(NULL) - , _pane_initialized(false) - , _attach(true) - , _driver_detached(false) - , _refresh(false) - , _enable_refresh(true) - , _jack_driver_autoattach(true) -#ifdef HAVE_ALSA - , _alsa_driver_autoattach(true) -#endif +template<class S> +void +on_setting_toggled(Reactor* const reactor, const Gtk::CheckMenuItem* const item) { - _conf = new Configuration(); - _canvas = boost::shared_ptr<PatchageCanvas>(new PatchageCanvas(this, 1600*2, 1200*2)); - - while (argc > 0) { - if (!strcmp(*argv, "-h") || !strcmp(*argv, "--help")) { - cout << "Usage: patchage [OPTION]..." << endl; - cout << "Visually connect JACK and ALSA Audio/MIDI ports." << endl << endl; - cout << "Options:" << endl; - cout << "\t-h --help Show this help" << endl; - cout << "\t-A --no-alsa Do not automatically attach to ALSA" << endl; - cout << "\t-J --no-jack Do not automatically attack to JACK" << endl; - exit(0); -#ifdef HAVE_ALSA - } else if (!strcmp(*argv, "-A") || !strcmp(*argv, "--no-alsa")) { - _alsa_driver_autoattach = false; -#endif -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - } else if (!strcmp(*argv, "-J") || !strcmp(*argv, "--no-jack")) { - _jack_driver_autoattach = false; -#endif - } - - argv++; - argc--; - } - - Glib::set_application_name("Patchage"); - _about_win->property_program_name() = "Patchage"; - _about_win->property_logo_icon_name() = "patchage"; - gtk_window_set_default_icon_name("patchage"); - - // Create list model for buffer size selector - Glib::RefPtr<Gtk::ListStore> buf_size_store = Gtk::ListStore::create(_buf_size_columns); - for (size_t i = 32; i <= 4096; i *= 2) { - Gtk::TreeModel::Row row = *(buf_size_store->append()); - row[_buf_size_columns.label] = std::to_string(i); - } - - _buf_size_combo->set_model(buf_size_store); - _buf_size_combo->pack_start(_buf_size_columns.label); - - _main_scrolledwin->add(_canvas->widget()); - - _main_scrolledwin->property_hadjustment().get_value()->set_step_increment(10); - _main_scrolledwin->property_vadjustment().get_value()->set_step_increment(10); - - _main_scrolledwin->signal_scroll_event().connect( - sigc::mem_fun(this, &Patchage::on_scroll)); - _clear_load_but->signal_clicked().connect( - sigc::mem_fun(this, &Patchage::clear_load)); - _buf_size_combo->signal_changed().connect( - sigc::mem_fun(this, &Patchage::buffer_size_changed)); - _status_text->signal_size_allocate().connect( - sigc::mem_fun(this, &Patchage::on_messages_resized)); - -#ifdef PATCHAGE_JACK_SESSION - _menu_open_session->signal_activate().connect( - sigc::mem_fun(this, &Patchage::show_open_session_dialog)); - _menu_save_session->signal_activate().connect( - sigc::mem_fun(this, &Patchage::show_save_session_dialog)); - _menu_save_close_session->signal_activate().connect( - sigc::mem_fun(this, &Patchage::show_save_close_session_dialog)); -#else - _menu_open_session->hide(); - _menu_save_session->hide(); - _menu_save_close_session->hide(); -#endif - -#ifdef HAVE_ALSA - _menu_alsa_connect->signal_activate().connect( - sigc::mem_fun(this, &Patchage::menu_alsa_connect)); - _menu_alsa_disconnect->signal_activate().connect( - sigc::mem_fun(this, &Patchage::menu_alsa_disconnect)); -#else - _menu_alsa_connect->set_sensitive(false); - _menu_alsa_disconnect->set_sensitive(false); -#endif - - _menu_file_quit->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_quit)); - _menu_export_image->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_export_image)); - _menu_view_refresh->signal_activate().connect( - sigc::mem_fun(this, &Patchage::refresh)); - _menu_view_human_names->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_view_human_names)); - _menu_view_sort_ports->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_view_sort_ports)); - _menu_view_arrange->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_arrange)); - _menu_view_sprung_layout->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_sprung_layout_toggled)); - _menu_view_messages->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_view_messages)); - _menu_view_toolbar->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_view_toolbar)); - _menu_help_about->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_help_about)); - _menu_zoom_in->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_zoom_in)); - _menu_zoom_out->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_zoom_out)); - _menu_zoom_normal->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_zoom_normal)); - _menu_zoom_full->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_zoom_full)); - _menu_increase_font_size->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_increase_font_size)); - _menu_decrease_font_size->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_decrease_font_size)); - _menu_normal_font_size->signal_activate().connect( - sigc::mem_fun(this, &Patchage::on_normal_font_size)); - - if (_canvas->supports_sprung_layout()) { - _menu_view_sprung_layout->set_active(true); - } else { - _menu_view_sprung_layout->set_active(false); - _menu_view_sprung_layout->set_sensitive(false); - } - - for (int s = Gtk::STATE_NORMAL; s <= Gtk::STATE_INSENSITIVE; ++s) { - _status_text->modify_base((Gtk::StateType)s, Gdk::Color("#000000")); - _status_text->modify_text((Gtk::StateType)s, Gdk::Color("#FFFFFF")); - } - - _error_tag = Gtk::TextTag::create(); - _error_tag->property_foreground() = "#CC0000"; - _status_text->get_buffer()->get_tag_table()->add(_error_tag); - - _warning_tag = Gtk::TextTag::create(); - _warning_tag->property_foreground() = "#C4A000"; - _status_text->get_buffer()->get_tag_table()->add(_warning_tag); - - _canvas->widget().show(); - _main_win->present(); - - _conf->set_font_size(_canvas->get_default_font_size()); - _conf->load(); - _canvas->set_zoom(_conf->get_zoom()); - _canvas->set_font_size(_conf->get_font_size()); - if (_conf->get_sort_ports()) { - _canvas->set_port_order(port_order, NULL); - } - - _main_win->resize( - static_cast<int>(_conf->get_window_size().x), - static_cast<int>(_conf->get_window_size().y)); - - _main_win->move( - static_cast<int>(_conf->get_window_location().x), - static_cast<int>(_conf->get_window_location().y)); - - _legend = new Legend(*_conf); - _legend->signal_color_changed.connect( - sigc::mem_fun(this, &Patchage::on_legend_color_change)); - _legend_alignment->add(*Gtk::manage(_legend)); - _legend->show_all(); - - _about_win->set_transient_for(*_main_win); -#ifdef __APPLE__ - try { - _about_win->set_logo( - Gdk::Pixbuf::create_from_file( - bundle_location() + "/Resources/Patchage.icns")); - } catch (const Glib::Exception& e) { - error_msg((boost::format("failed to set logo (%s)") % e.what()).str()); - } -#endif + (*reactor)(action::ChangeSetting{{S{item->get_active()}}}); +} -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - _jack_driver = new JackDriver(this); - _jack_driver->signal_detached.connect(sigc::mem_fun(this, &Patchage::driver_detached)); +void +update_labels(GanvNode* node, void* data) +{ + const bool human_names = *static_cast<const bool*>(data); + if (GANV_IS_MODULE(node)) { + Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); + auto* pmod = dynamic_cast<CanvasModule*>(gmod); + if (pmod) { + for (Ganv::Port* gport : *gmod) { + auto* pport = dynamic_cast<CanvasPort*>(gport); + if (pport) { + pport->show_human_name(human_names); + } + } + } + } +} + +inline guint +highlight_color(guint c, guint delta) +{ + const guint max_char = 255; + const guint r = MIN((c >> 24) + delta, max_char); + const guint g = MIN(((c >> 16) & 0xFF) + delta, max_char); + const guint b = MIN(((c >> 8) & 0xFF) + delta, max_char); + const guint a = c & 0xFF; - _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)); -#endif + return ((r << 24u) | (g << 16u) | (b << 8u) | a); +} -#ifdef HAVE_ALSA - _alsa_driver = new AlsaDriver(this); -#endif +void +update_port_colors(GanvNode* node, void* data) +{ + auto* patchage = static_cast<Patchage*>(data); + if (!GANV_IS_MODULE(node)) { + return; + } - connect_widgets(); - update_state(); - _menu_view_toolbar->set_active(_conf->get_show_toolbar()); - _menu_view_sprung_layout->set_active(_conf->get_sprung_layout()); - _menu_view_sort_ports->set_active(_conf->get_sort_ports()); - _status_text->set_pixels_inside_wrap(2); - _status_text->set_left_margin(4); - _status_text->set_right_margin(4); - _status_text->set_pixels_below_lines(2); + Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); + auto* pmod = dynamic_cast<CanvasModule*>(gmod); + if (!pmod) { + return; + } - g_signal_connect(_main_win->gobj(), "configure-event", - G_CALLBACK(configure_cb), this); + for (Ganv::Port* p : *pmod) { + auto* port = dynamic_cast<CanvasPort*>(p); + if (port) { + const uint32_t rgba = patchage->conf().get_port_color(port->type()); + port->set_fill_color(rgba); + port->set_border_color(highlight_color(rgba, 0x20)); + } + } +} - _canvas->widget().grab_focus(); +void +update_edge_color(GanvEdge* edge, void* data) +{ + auto* patchage = static_cast<Patchage*>(data); + Ganv::Edge* edgemm = Glib::wrap(edge); + + if (edgemm) { + auto* tail = dynamic_cast<CanvasPort*>((edgemm)->get_tail()); + if (tail) { + edgemm->set_color(patchage->conf().get_port_color(tail->type())); + } + } +} + +} // namespace + +#define INIT_WIDGET(x) x(_xml, (#x) + 1) + +Patchage::Patchage(Options options) + : _xml(UIFile::open("patchage")) + , INIT_WIDGET(_about_win) + , INIT_WIDGET(_main_scrolledwin) + , INIT_WIDGET(_main_win) + , INIT_WIDGET(_main_vbox) + , INIT_WIDGET(_menubar) + , INIT_WIDGET(_menu_alsa_connect) + , INIT_WIDGET(_menu_alsa_disconnect) + , INIT_WIDGET(_menu_file_quit) + , INIT_WIDGET(_menu_export_image) + , INIT_WIDGET(_menu_help_about) + , INIT_WIDGET(_menu_jack_connect) + , INIT_WIDGET(_menu_jack_disconnect) + , INIT_WIDGET(_menu_view_arrange) + , INIT_WIDGET(_menu_view_sprung_layout) + , INIT_WIDGET(_menu_view_messages) + , INIT_WIDGET(_menu_view_toolbar) + , INIT_WIDGET(_menu_view_refresh) + , INIT_WIDGET(_menu_view_human_names) + , INIT_WIDGET(_menu_view_sort_ports) + , INIT_WIDGET(_menu_zoom_in) + , INIT_WIDGET(_menu_zoom_out) + , INIT_WIDGET(_menu_zoom_normal) + , INIT_WIDGET(_menu_zoom_full) + , INIT_WIDGET(_menu_increase_font_size) + , INIT_WIDGET(_menu_decrease_font_size) + , INIT_WIDGET(_menu_normal_font_size) + , INIT_WIDGET(_toolbar) + , INIT_WIDGET(_clear_load_but) + , INIT_WIDGET(_dropouts_label) + , INIT_WIDGET(_buf_size_combo) + , INIT_WIDGET(_latency_label) + , INIT_WIDGET(_legend_alignment) + , INIT_WIDGET(_main_paned) + , INIT_WIDGET(_log_scrolledwindow) + , INIT_WIDGET(_status_text) + , _conf([this](const Setting& setting) { on_conf_change(setting); }) + , _log(_status_text) + , _canvas(new Canvas{_log, _action_sink, 1600 * 2, 1200 * 2}) + , _drivers(_log, [this](const Event& event) { on_driver_event(event); }) + , _reactor(_conf, _drivers, *_canvas, _log) + , _action_sink([this](const Action& action) { _reactor(action); }) + , _options{options} +{ + Glib::set_application_name("Patchage"); + _about_win->property_program_name() = "Patchage"; + _about_win->property_logo_icon_name() = "patchage"; + gtk_window_set_default_icon_name("patchage"); + + // Create list model for buffer size selector + const Glib::RefPtr<Gtk::ListStore> buf_size_store = + Gtk::ListStore::create(_buf_size_columns); + for (size_t i = 32; i <= 4096; i *= 2) { + const Gtk::TreeModel::Row row = *(buf_size_store->append()); + row[_buf_size_columns.label] = std::to_string(i); + } + + _buf_size_combo->set_model(buf_size_store); + _buf_size_combo->pack_start(_buf_size_columns.label); + + _main_scrolledwin->add(_canvas->widget()); + + _main_scrolledwin->property_hadjustment().get_value()->set_step_increment(10); + _main_scrolledwin->property_vadjustment().get_value()->set_step_increment(10); + + _main_scrolledwin->signal_scroll_event().connect( + sigc::mem_fun(this, &Patchage::on_scroll)); + _clear_load_but->signal_clicked().connect( + sigc::mem_fun(this, &Patchage::clear_load)); + _buf_size_combo->signal_changed().connect( + sigc::mem_fun(this, &Patchage::buffer_size_changed)); + _status_text->signal_size_allocate().connect( + sigc::mem_fun(this, &Patchage::on_messages_resized)); + + _menu_file_quit->signal_activate().connect( + sigc::mem_fun(this, &Patchage::on_quit)); + _menu_export_image->signal_activate().connect( + sigc::mem_fun(this, &Patchage::on_export_image)); + _menu_view_refresh->signal_activate().connect(sigc::bind( + sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::Refresh{}})); + + _menu_view_human_names->signal_activate().connect( + sigc::bind(sigc::ptr_fun(&on_setting_toggled<setting::HumanNames>), + &_reactor, + _menu_view_human_names.get())); + + _menu_view_sort_ports->signal_activate().connect( + sigc::bind(sigc::ptr_fun(&on_setting_toggled<setting::SortedPorts>), + &_reactor, + _menu_view_sort_ports.get())); + + _menu_view_arrange->signal_activate().connect( + sigc::mem_fun(this, &Patchage::on_arrange)); + + _menu_view_sprung_layout->signal_activate().connect( + sigc::bind(sigc::ptr_fun(&on_setting_toggled<setting::SprungLayout>), + &_reactor, + _menu_view_sprung_layout.get())); + + _menu_view_messages->signal_activate().connect( + sigc::bind(sigc::ptr_fun(&on_setting_toggled<setting::MessagesVisible>), + &_reactor, + _menu_view_messages.get())); + + _menu_view_toolbar->signal_activate().connect( + sigc::bind(sigc::ptr_fun(&on_setting_toggled<setting::ToolbarVisible>), + &_reactor, + _menu_view_toolbar.get())); + + _menu_help_about->signal_activate().connect( + sigc::mem_fun(this, &Patchage::on_help_about)); + + _menu_zoom_in->signal_activate().connect(sigc::bind( + sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ZoomIn{}})); + _menu_zoom_out->signal_activate().connect(sigc::bind( + sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ZoomOut{}})); + _menu_zoom_normal->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), + Action{action::ZoomNormal{}})); + _menu_zoom_full->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), + Action{action::ZoomFull{}})); + _menu_increase_font_size->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), + Action{action::IncreaseFontSize{}})); + _menu_decrease_font_size->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), + Action{action::DecreaseFontSize{}})); + _menu_normal_font_size->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), + Action{action::ResetFontSize{}})); + + if (_canvas->supports_sprung_layout()) { + _menu_view_sprung_layout->set_active(true); + } else { + _menu_view_sprung_layout->set_active(false); + _menu_view_sprung_layout->set_sensitive(false); + } + + // Present window so that display attributes like font size are available + _canvas->widget().show(); + _main_win->present(); + + // Set the default font size based on the current GUI environment + _conf.set<setting::FontSize>(_canvas->get_default_font_size()); + + // Load configuration file (but do not apply it yet, see below) + _conf.load(); + + _legend = new Legend(_conf); + _legend->signal_color_changed.connect( + sigc::mem_fun(this, &Patchage::on_legend_color_change)); + _legend_alignment->add(*Gtk::manage(_legend)); + _legend->show_all(); + + _about_win->set_transient_for(*_main_win); - // Idle callback, check if we need to refresh - Glib::signal_timeout().connect( - sigc::mem_fun(this, &Patchage::idle_callback), 100); +#ifdef __APPLE__ + try { + _about_win->set_logo(Gdk::Pixbuf::create_from_file( + bundle_location() + "/Resources/Patchage.icns")); + } catch (const Glib::Exception& e) { + _log.error(fmt::format("Failed to set logo ({})", std::string(e.what()))); + } +#endif + + // Enable JACK menu items if driver is present + if (_drivers.jack()) { + _menu_jack_connect->signal_activate().connect(sigc::bind( + sigc::mem_fun(_drivers.jack().get(), &AudioDriver::attach), true)); + _menu_jack_disconnect->signal_activate().connect( + sigc::mem_fun(_drivers.jack().get(), &AudioDriver::detach)); + } else { + _menu_jack_connect->set_sensitive(false); + _menu_jack_disconnect->set_sensitive(false); + } + + // Enable ALSA menu items if driver is present + if (_drivers.alsa()) { + _menu_alsa_connect->signal_activate().connect( + sigc::bind(sigc::mem_fun(_drivers.alsa().get(), &Driver::attach), false)); + _menu_alsa_disconnect->signal_activate().connect( + sigc::mem_fun(_drivers.alsa().get(), &Driver::detach)); + } else { + _menu_alsa_connect->set_sensitive(false); + _menu_alsa_disconnect->set_sensitive(false); + } + + g_signal_connect( + _main_win->gobj(), "configure-event", G_CALLBACK(configure_cb), this); + + _canvas->widget().grab_focus(); #ifdef PATCHAGE_GTK_OSX - // Set up Mac menu bar - GtkosxApplication* osxapp = (GtkosxApplication*)g_object_new( - GTKOSX_TYPE_APPLICATION, NULL); - _menubar->hide(); - _menu_file_quit->hide(); - gtkosx_application_set_menu_bar(osxapp, GTK_MENU_SHELL(_menubar->gobj())); - gtkosx_application_insert_app_menu_item( - osxapp, GTK_WIDGET(_menu_help_about->gobj()), 0); - g_signal_connect(_menubar->gobj(), "can-activate-accel", - G_CALLBACK(can_activate_cb), NULL); - g_signal_connect(osxapp, "NSApplicationWillTerminate", - G_CALLBACK(terminate_cb), this); - gtkosx_application_ready(osxapp); + // Set up Mac menu bar + GtkosxApplication* osxapp = static_cast<GtkosxApplication*>( + g_object_new(GTKOSX_TYPE_APPLICATION, nullptr)); + + _menubar->hide(); + _menu_file_quit->hide(); + gtkosx_application_set_menu_bar(osxapp, GTK_MENU_SHELL(_menubar->gobj())); + gtkosx_application_insert_app_menu_item( + osxapp, GTK_WIDGET(_menu_help_about->gobj()), 0); + g_signal_connect(_menubar->gobj(), + "can-activate-accel", + G_CALLBACK(can_activate_cb), + nullptr); + g_signal_connect( + osxapp, "NSApplicationWillTerminate", G_CALLBACK(terminate_cb), this); + gtkosx_application_ready(osxapp); #endif + + // Apply all configuration settings to ensure the GUI is synced + _conf.each([this](const Setting& setting) { on_conf_change(setting); }); + + // Set up an idle callback to process events and update the GUI if necessary + Glib::signal_timeout().connect(sigc::mem_fun(this, &Patchage::idle_callback), + 100); } Patchage::~Patchage() { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - delete _jack_driver; -#endif -#ifdef HAVE_ALSA - delete _alsa_driver; -#endif - - delete _conf; - - _about_win.destroy(); - _xml.reset(); + _about_win.destroy(); + _xml.reset(); } void Patchage::attach() { - _enable_refresh = false; + if (_drivers.jack() && _options.jack_driver_autoattach) { + _drivers.jack()->attach(true); + } -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - if (_jack_driver_autoattach) - _jack_driver->attach(true); -#endif - -#ifdef HAVE_ALSA - if (_alsa_driver_autoattach) - _alsa_driver->attach(); -#endif + if (_drivers.alsa() && _options.alsa_driver_autoattach) { + _drivers.alsa()->attach(false); + } - _enable_refresh = true; - - refresh(); - update_toolbar(); + process_events(); + update_toolbar(); } bool Patchage::idle_callback() { - // Initial run, attach - if (_attach) { - attach(); - _menu_view_messages->set_active(_conf->get_show_messages()); - _attach = false; - } - - // Process any JACK events -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - if (_jack_driver) { - _jack_driver->process_events(this); - } -#endif + // Initial run, attach + if (_attach) { + attach(); + _menu_view_messages->set_active(_conf.get<setting::MessagesVisible>()); + _attach = false; + } - // Process any ALSA events -#ifdef HAVE_ALSA - if (_alsa_driver) { - _alsa_driver->process_events(this); - } -#endif - - // Do a full refresh - if (_refresh) { - refresh(); - } else if (_driver_detached) { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - if (_jack_driver && !_jack_driver->is_attached()) - _jack_driver->destroy_all(); -#endif -#ifdef HAVE_ALSA - if (_alsa_driver && !_alsa_driver->is_attached()) - _alsa_driver->destroy_all(); -#endif - } - - _refresh = false; - _driver_detached = false; + // Process any events from drivers + process_events(); - // Update load every 5 idle callbacks - static int count = 0; - if (++count == 5) { - update_load(); - count = 0; - } + // Update load every 5 idle callbacks + static int count = 0; + if (++count == 5) { + update_load(); + count = 0; + } - return true; + return true; } void Patchage::update_toolbar() { - static bool updating = false; - if (updating) { - return; - } else { - updating = true; - } - -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - if (_jack_driver->is_attached()) { - const jack_nframes_t buffer_size = _jack_driver->buffer_size(); - const jack_nframes_t sample_rate = _jack_driver->sample_rate(); - if (sample_rate != 0) { - const int latency_ms = lrintf(buffer_size * 1000 / (float)sample_rate); - std::stringstream ss; - ss << " frames @ " << (sample_rate / 1000) - << "kHz (" << latency_ms << "ms)"; - _latency_label->set_label(ss.str()); - _latency_label->set_visible(true); - _buf_size_combo->set_active((int)log2f(_jack_driver->buffer_size()) - 5); - updating = false; - return; - } - } -#endif - _latency_label->set_visible(false); - updating = false; + static bool updating = false; + if (updating) { + return; + } + + updating = true; + + if (_drivers.jack() && _drivers.jack()->is_attached()) { + const auto buffer_size = _drivers.jack()->buffer_size(); + const auto sample_rate = _drivers.jack()->sample_rate(); + if (sample_rate != 0) { + const auto sample_rate_khz = sample_rate / 1000.0; + const auto latency_ms = buffer_size / sample_rate_khz; + + _latency_label->set_label(" " + + fmt::format(T("frames at {} kHz ({:0.2f} ms)"), + sample_rate_khz, + latency_ms)); + + _latency_label->set_visible(true); + _buf_size_combo->set_active( + static_cast<int>(log2f(_drivers.jack()->buffer_size()) - 5)); + updating = false; + return; + } + } + + _latency_label->set_visible(false); + updating = false; } bool Patchage::update_load() { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - if (_jack_driver->is_attached()) { - char buf[8]; - snprintf(buf, sizeof(buf), "%u", _jack_driver->get_xruns()); - _xrun_progress->set_text(std::string(buf) + " Dropouts"); - _xrun_progress->set_fraction(_jack_driver->get_max_dsp_load()); - } -#endif + if (_drivers.jack() && _drivers.jack()->is_attached()) { + const auto xruns = _drivers.jack()->xruns(); - return true; -} + _dropouts_label->set_text(" " + fmt::format(T("Dropouts: {}"), xruns)); -void -Patchage::zoom(double z) -{ - _conf->set_zoom(z); - _canvas->set_zoom(z); + if (xruns > 0u) { + _dropouts_label->show(); + _clear_load_but->show(); + } else { + _dropouts_label->hide(); + _clear_load_but->hide(); + } + } + + return true; } void -Patchage::refresh() +Patchage::store_window_location() { - if (_canvas && _enable_refresh) { - _canvas->clear(); + int loc_x = 0; + int loc_y = 0; + _main_win->get_position(loc_x, loc_y); -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - if (_jack_driver) - _jack_driver->refresh(); -#endif + int size_x = 0; + int size_y = 0; + _main_win->get_size(size_x, size_y); -#ifdef HAVE_ALSA - if (_alsa_driver) - _alsa_driver->refresh(); -#endif - } -} + _conf.set<setting::WindowLocation>( + {static_cast<double>(loc_x), static_cast<double>(loc_y)}); -void -Patchage::store_window_location() -{ - int loc_x, loc_y, size_x, size_y; - _main_win->get_position(loc_x, loc_y); - _main_win->get_size(size_x, size_y); - Coord window_location; - window_location.x = loc_x; - window_location.y = loc_y; - Coord window_size; - window_size.x = size_x; - window_size.y = size_y; - _conf->set_window_location(window_location); - _conf->set_window_size(window_size); + _conf.set<setting::WindowSize>( + {static_cast<double>(size_x), static_cast<double>(size_y)}); } void Patchage::clear_load() { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - _xrun_progress->set_fraction(0.0); - _jack_driver->reset_xruns(); - _jack_driver->reset_max_dsp_load(); -#endif + _dropouts_label->set_text(" " + fmt::format(T("Dropouts: {}"), 0U)); + _dropouts_label->hide(); + _clear_load_but->hide(); + if (_drivers.jack()) { + _drivers.jack()->reset_xruns(); + } } void -Patchage::error_msg(const std::string& msg) +Patchage::operator()(const setting::AlsaAttached& setting) { - Glib::RefPtr<Gtk::TextBuffer> buffer = _status_text->get_buffer(); - buffer->insert_with_tag(buffer->end(), std::string("\n") + msg, _error_tag); - _status_text->scroll_to_mark(buffer->get_insert(), 0); - _menu_view_messages->set_active(true); -} + if (setting.value) { + _menu_alsa_connect->set_sensitive(false); + _menu_alsa_disconnect->set_sensitive(true); -void -Patchage::info_msg(const std::string& msg) -{ - Glib::RefPtr<Gtk::TextBuffer> buffer = _status_text->get_buffer(); - buffer->insert(buffer->end(), std::string("\n") + msg); - _status_text->scroll_to_mark(buffer->get_insert(), 0); + if (_drivers.alsa()) { + _drivers.alsa()->refresh([this](const Event& event) { + handle_event(_conf, _metadata, *_canvas, _log, event); + }); + } + } else { + _menu_alsa_connect->set_sensitive(true); + _menu_alsa_disconnect->set_sensitive(false); + + _canvas->remove_ports([](const CanvasPort* port) { + return port->type() == PortType::alsa_midi; + }); + } } void -Patchage::warning_msg(const std::string& msg) +Patchage::operator()(const setting::JackAttached& setting) { - Glib::RefPtr<Gtk::TextBuffer> buffer = _status_text->get_buffer(); - buffer->insert_with_tag(buffer->end(), std::string("\n") + msg, _warning_tag); - _status_text->scroll_to_mark(buffer->get_insert(), 0); -} + if (setting.value) { + _menu_jack_connect->set_sensitive(false); + _menu_jack_disconnect->set_sensitive(true); -static void -load_module_location(GanvNode* node, void* data) -{ - if (GANV_IS_MODULE(node)) { - Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); - PatchageModule* pmod = dynamic_cast<PatchageModule*>(gmod); - if (pmod) { - pmod->load_location(); - } - } + if (_drivers.jack()) { + _drivers.jack()->refresh([this](const Event& event) { + handle_event(_conf, _metadata, *_canvas, _log, event); + }); + } + } else { + _menu_jack_connect->set_sensitive(true); + _menu_jack_disconnect->set_sensitive(false); + + _canvas->remove_ports([](const CanvasPort* port) { + return (port->type() == PortType::jack_audio || + port->type() == PortType::jack_midi || + port->type() == PortType::jack_osc || + port->type() == PortType::jack_cv); + }); + } } void -Patchage::update_state() +Patchage::operator()(const setting::FontSize& setting) { - _canvas->for_each_node(load_module_location, NULL); + if (static_cast<float>(_canvas->get_font_size()) != setting.value) { + _canvas->set_font_size(setting.value); + } } -/** Update the sensitivity status of menus to reflect the present. - * - * (eg. disable "Connect to Jack" when Patchage is already connected to Jack) - */ void -Patchage::connect_widgets() +Patchage::operator()(const setting::HumanNames& setting) { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - _jack_driver->signal_attached.connect(sigc::bind( - sigc::mem_fun(*_menu_jack_connect, &Gtk::MenuItem::set_sensitive), false)); - _jack_driver->signal_attached.connect( - sigc::mem_fun(this, &Patchage::refresh)); - _jack_driver->signal_attached.connect(sigc::bind( - sigc::mem_fun(*_menu_jack_disconnect, &Gtk::MenuItem::set_sensitive), true)); - - _jack_driver->signal_detached.connect(sigc::bind( - sigc::mem_fun(*_menu_jack_connect, &Gtk::MenuItem::set_sensitive), true)); - _jack_driver->signal_detached.connect(sigc::bind( - sigc::mem_fun(*_menu_jack_disconnect, &Gtk::MenuItem::set_sensitive), false)); -#endif - -#ifdef HAVE_ALSA - _alsa_driver->signal_attached.connect(sigc::bind( - sigc::mem_fun(*_menu_alsa_connect, &Gtk::MenuItem::set_sensitive), false)); - _alsa_driver->signal_attached.connect(sigc::bind( - sigc::mem_fun(*_menu_alsa_disconnect, &Gtk::MenuItem::set_sensitive), true)); + bool human_names = setting.value; - _alsa_driver->signal_detached.connect(sigc::bind( - sigc::mem_fun(*_menu_alsa_connect, &Gtk::MenuItem::set_sensitive), true)); - _alsa_driver->signal_detached.connect(sigc::bind( - sigc::mem_fun(*_menu_alsa_disconnect, &Gtk::MenuItem::set_sensitive), false)); -#endif + _menu_view_human_names->set_active(human_names); + _canvas->for_each_node(update_labels, &human_names); } -#ifdef PATCHAGE_JACK_SESSION void -Patchage::show_open_session_dialog() -{ - Gtk::FileChooserDialog dialog(*_main_win, "Open Session", - Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER); - - dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); - Gtk::Button* open_but = dialog.add_button(Gtk::Stock::OPEN, Gtk::RESPONSE_OK); - open_but->property_has_default() = true; - - if (dialog.run() != Gtk::RESPONSE_OK) { - return; - } - - const std::string dir = dialog.get_filename(); - if (g_chdir(dir.c_str())) { - error_msg("Failed to switch to session directory " + dir); - return; - } - - if (system("./jack-session") < 0) { - error_msg("Error executing `./jack-session' in " + dir); - } else { - info_msg("Loaded session " + dir); - } -} - -static void -print_edge(GanvEdge* edge, void* data) +Patchage::operator()(const setting::MessagesHeight& setting) { - std::ofstream* script = (std::ofstream*)data; - Ganv::Edge* edgemm = Glib::wrap(edge); - - PatchagePort* src = dynamic_cast<PatchagePort*>((edgemm)->get_tail()); - PatchagePort* dst = dynamic_cast<PatchagePort*>((edgemm)->get_head()); + if (_log_scrolledwindow->is_visible()) { + const int min_height = _log.min_height(); + const int max_pos = _main_paned->get_allocation().get_height(); + const int conf_height = setting.value; - if (!src || !dst || src->type() == ALSA_MIDI || dst->type() == ALSA_MIDI) { - return; - } - - (*script) << "jack_connect '" << src->full_name() - << "' '" << dst->full_name() << "' &" << endl; + _main_paned->set_position(max_pos - std::max(conf_height, min_height)); + } } void -Patchage::save_session(bool close) +Patchage::operator()(const setting::MessagesVisible& setting) { - Gtk::FileChooserDialog dialog(*_main_win, "Save Session", - Gtk::FILE_CHOOSER_ACTION_SAVE); - - dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); - Gtk::Button* save_but = dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); - save_but->property_has_default() = true; - - if (dialog.run() != Gtk::RESPONSE_OK) { - return; - } - - std::string path = dialog.get_filename(); - if (g_mkdir_with_parents(path.c_str(), 0740)) { - error_msg("Failed to create session directory " + path); - return; - } - - path += '/'; - jack_session_command_t* cmd = jack_session_notify( - _jack_driver->client(), - NULL, - close ? JackSessionSaveAndQuit : JackSessionSave, - path.c_str()); - - const std::string script_path = path + "jack-session"; - std::ofstream script(script_path.c_str()); - script << "#!/bin/sh" << endl << endl; - - const std::string var("${SESSION_DIR}"); - for (int c = 0; cmd[c].uuid; ++c) { - std::string command = cmd[c].command; - const size_t index = command.find(var); - if (index != string::npos) { - command.replace(index, var.length(), cmd[c].client_name); - } - - script << command << " &" << endl; - } - - script << endl; - script << "sleep 3" << endl; - script << endl; - - _canvas->for_each_edge(print_edge, &script); - - script.close(); - g_chmod(script_path.c_str(), 0740); -} + if (setting.value) { + _log_scrolledwindow->show(); + _status_text->scroll_to_mark(_status_text->get_buffer()->get_insert(), 0); + } else { + _log_scrolledwindow->hide(); + } -void -Patchage::show_save_session_dialog() -{ - save_session(false); + _menu_view_messages->set_active(setting.value); } void -Patchage::show_save_close_session_dialog() +Patchage::operator()(const setting::PortColor&) { - save_session(true); + _canvas->for_each_node(update_port_colors, this); + _canvas->for_each_edge(update_edge_color, this); } -#endif - -#ifdef HAVE_ALSA void -Patchage::menu_alsa_connect() +Patchage::operator()(const setting::SortedPorts& setting) { - _alsa_driver->attach(false); - _alsa_driver->refresh(); + _menu_view_sort_ports->set_active(setting.value); + if (setting.value) { + _canvas->set_port_order(port_order, nullptr); + } else { + _canvas->set_port_order(nullptr, nullptr); + } } void -Patchage::menu_alsa_disconnect() +Patchage::operator()(const setting::SprungLayout& setting) { - _alsa_driver->detach(); - refresh(); + _canvas->set_sprung_layout(setting.value); + _menu_view_sprung_layout->set_active(setting.value); } -#endif void -Patchage::on_arrange() +Patchage::operator()(const setting::ToolbarVisible& setting) { - if (_canvas) { - _canvas->arrange(); - } + if (setting.value) { + _toolbar->show(); + _menu_view_toolbar->set_active(true); + } else { + _toolbar->hide(); + _menu_view_toolbar->set_active(false); + } } void -Patchage::on_sprung_layout_toggled() +Patchage::operator()(const setting::WindowLocation& setting) { - const bool sprung = _menu_view_sprung_layout->get_active(); + const int new_x = static_cast<int>(setting.value.x); + const int new_y = static_cast<int>(setting.value.y); + + int current_x = 0; + int current_y = 0; + _main_win->get_position(current_x, current_y); - _canvas->set_sprung_layout(sprung); - _conf->set_sprung_layout(sprung); + if (new_x != current_x || new_y != current_y) { + _main_win->move(new_x, new_y); + } } void -Patchage::on_help_about() +Patchage::operator()(const setting::WindowSize& setting) { - _about_win->run(); - _about_win->hide(); -} + const int new_w = static_cast<int>(setting.value.x); + const int new_h = static_cast<int>(setting.value.y); -static void -update_labels(GanvNode* node, void* data) -{ - const bool human_names = *(const bool*)data; - if (GANV_IS_MODULE(node)) { - Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); - PatchageModule* pmod = dynamic_cast<PatchageModule*>(gmod); - if (pmod) { - for (Ganv::Port* gport : *gmod) { - PatchagePort* pport = dynamic_cast<PatchagePort*>(gport); - if (pport) { - pport->show_human_name(human_names); - } - } - } - } -} + int current_w = 0; + int current_h = 0; + _main_win->get_size(current_w, current_h); -void -Patchage::on_view_human_names() -{ - bool human_names = show_human_names(); - _canvas->for_each_node(update_labels, &human_names); + if (new_w != current_w || new_h != current_h) { + _main_win->resize(new_w, new_h); + } } void -Patchage::on_view_sort_ports() +Patchage::operator()(const setting::Zoom& setting) { - const bool sort_ports = this->sort_ports(); - _canvas->set_port_order(sort_ports ? port_order : NULL, NULL); - _conf->set_sort_ports(sort_ports); - refresh(); + if (static_cast<float>(_canvas->get_zoom()) != setting.value) { + _canvas->set_zoom(setting.value); + } } void -Patchage::on_zoom_in() +Patchage::on_driver_event(const Event& event) { - const float zoom = _canvas->get_zoom() * 1.25; - _canvas->set_zoom(zoom); - _conf->set_zoom(zoom); -} + const std::lock_guard<std::mutex> lock{_events_mutex}; -void -Patchage::on_zoom_out() -{ - const float zoom = _canvas->get_zoom() * 0.75; - _canvas->set_zoom(zoom); - _conf->set_zoom(zoom); + _driver_events.emplace(event); } void -Patchage::on_zoom_normal() +Patchage::process_events() { - _canvas->set_zoom(1.0); - _conf->set_zoom(1.0); -} + const std::lock_guard<std::mutex> lock{_events_mutex}; -void -Patchage::on_zoom_full() -{ - _canvas->zoom_full(); - _conf->set_zoom(_canvas->get_zoom()); -} + while (!_driver_events.empty()) { + const Event& event = _driver_events.front(); -void -Patchage::on_increase_font_size() -{ - const float points = _canvas->get_font_size() + 1.0; - _canvas->set_font_size(points); - _conf->set_font_size(points); -} + _log.info(event_to_string(event)); + handle_event(_conf, _metadata, *_canvas, _log, event); -void -Patchage::on_decrease_font_size() -{ - const float points = _canvas->get_font_size() - 1.0; - _canvas->set_font_size(points); - _conf->set_font_size(points); + _driver_events.pop(); + } } void -Patchage::on_normal_font_size() +Patchage::on_conf_change(const Setting& setting) { - _canvas->set_font_size(_canvas->get_default_font_size()); - _conf->set_font_size(_canvas->get_default_font_size()); + std::visit(*this, setting); } -static inline guint -highlight_color(guint c, guint delta) +void +Patchage::on_arrange() { - const guint max_char = 255; - const guint r = MIN((c >> 24) + delta, max_char); - const guint g = MIN(((c >> 16) & 0xFF) + delta, max_char); - const guint b = MIN(((c >> 8) & 0xFF) + delta, max_char); - const guint a = c & 0xFF; - - return ((((guint)(r)) << 24) | - (((guint)(g)) << 16) | - (((guint)(b)) << 8) | - (((guint)(a)))); + if (_canvas) { + _canvas->arrange(); + } } -static void -update_port_colors(GanvNode* node, void* data) +void +Patchage::on_help_about() { - Patchage* patchage = (Patchage*)data; - if (!GANV_IS_MODULE(node)) { - return; - } - - Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); - PatchageModule* pmod = dynamic_cast<PatchageModule*>(gmod); - if (!pmod) { - return; - } - - for (PatchageModule::iterator i = pmod->begin(); i != pmod->end(); ++i) { - PatchagePort* port = dynamic_cast<PatchagePort*>(*i); - if (port) { - const uint32_t rgba = patchage->conf()->get_port_color(port->type()); - port->set_fill_color(rgba); - port->set_border_color(highlight_color(rgba, 0x20)); - } - } + _about_win->run(); + _about_win->hide(); } -static void -update_edge_color(GanvEdge* edge, void* data) +void +Patchage::on_legend_color_change(PortType id, const std::string&, uint32_t rgba) { - Patchage* patchage = (Patchage*)data; - Ganv::Edge* edgemm = Glib::wrap(edge); - - PatchagePort* tail = dynamic_cast<PatchagePort*>((edgemm)->get_tail()); - if (tail) { - edgemm->set_color(patchage->conf()->get_port_color(tail->type())); - } + _reactor(action::ChangeSetting{{setting::PortColor{id, rgba}}}); } void -Patchage::on_legend_color_change(int id, const std::string& label, uint32_t rgba) +Patchage::on_messages_resized(Gtk::Allocation&) { - _conf->set_port_color((PortType)id, rgba); - _canvas->for_each_node(update_port_colors, this); - _canvas->for_each_edge(update_edge_color, this); + const int max_pos = _main_paned->get_allocation().get_height(); + + _conf.set<setting::MessagesHeight>(max_pos - _main_paned->get_position()); } void -Patchage::on_messages_resized(Gtk::Allocation& alloc) +Patchage::save() { - const int max_pos = _main_paned->get_allocation().get_height(); - _conf->set_messages_height(max_pos - _main_paned->get_position()); + _conf.set<setting::Zoom>(_canvas->get_zoom()); // Can be changed by ganv + _conf.save(); } void -Patchage::save() +Patchage::quit() { - _conf->set_zoom(_canvas->get_zoom()); // Can be changed by ganv - _conf->save(); + _main_win->hide(); } void Patchage::on_quit() { -#ifdef HAVE_ALSA - _alsa_driver->detach(); -#endif -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - _jack_driver->detach(); -#endif - _main_win->hide(); + if (_drivers.alsa()) { + _drivers.alsa()->detach(); + } + + if (_drivers.jack()) { + _drivers.jack()->detach(); + } + + _main_win->hide(); } void Patchage::on_export_image() { - Gtk::FileChooserDialog dialog("Export Image", Gtk::FILE_CHOOSER_ACTION_SAVE); - dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); - dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); - dialog.set_default_response(Gtk::RESPONSE_OK); - dialog.set_transient_for(*_main_win); - - typedef std::map<std::string, std::string> Types; - Types types; - types["*.dot"] = "Graphviz DOT"; - types["*.pdf"] = "Portable Document Format"; - types["*.ps"] = "PostScript"; - types["*.svg"] = "Scalable Vector Graphics"; - for (Types::const_iterator t = types.begin(); t != types.end(); ++t) { - Gtk::FileFilter filt; - filt.add_pattern(t->first); - filt.set_name(t->second); - dialog.add_filter(filt); - } - - Gtk::CheckButton* bg_but = new Gtk::CheckButton("Draw _Background", true); - Gtk::Alignment* extra = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0); - bg_but->set_active(true); - extra->add(*Gtk::manage(bg_but)); - extra->show_all(); - dialog.set_extra_widget(*Gtk::manage(extra)); - - if (dialog.run() == Gtk::RESPONSE_OK) { - const std::string filename = dialog.get_filename(); - if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { - Gtk::MessageDialog confirm( - std::string("File exists! Overwrite ") + filename + "?", - true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); - confirm.set_transient_for(dialog); - if (confirm.run() != Gtk::RESPONSE_YES) { - return; - } - } - _canvas->export_image(filename.c_str(), bg_but->get_active()); - } + Gtk::FileChooserDialog dialog(T("Export Image"), + Gtk::FILE_CHOOSER_ACTION_SAVE); + + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dialog.set_default_response(Gtk::RESPONSE_OK); + dialog.set_transient_for(*_main_win); + + using Types = std::map<std::string, std::string>; + + Types types; + types["*.dot"] = "Graphviz DOT"; + types["*.pdf"] = "Portable Document Format"; + types["*.ps"] = "PostScript"; + types["*.svg"] = "Scalable Vector Graphics"; + for (const auto& t : types) { + Gtk::FileFilter filt; + filt.add_pattern(t.first); + filt.set_name(t.second); + dialog.add_filter(filt); + } + + auto* bg_but = new Gtk::CheckButton(T("Draw _Background"), true); + auto* extra = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0); + bg_but->set_active(true); + extra->add(*Gtk::manage(bg_but)); + extra->show_all(); + dialog.set_extra_widget(*Gtk::manage(extra)); + + if (dialog.run() == Gtk::RESPONSE_OK) { + const std::string filename = dialog.get_filename(); + if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { + Gtk::MessageDialog confirm( + fmt::format(T("File exists! Overwrite {}?"), filename), + true, + Gtk::MESSAGE_WARNING, + Gtk::BUTTONS_YES_NO, + true); + confirm.set_transient_for(dialog); + if (confirm.run() != Gtk::RESPONSE_YES) { + return; + } + } + _canvas->export_image(filename.c_str(), bg_but->get_active()); + } } -void -Patchage::on_view_messages() +bool +Patchage::on_scroll(GdkEventScroll*) { - if (_menu_view_messages->get_active()) { - Glib::RefPtr<Gtk::TextBuffer> buffer = _status_text->get_buffer(); - if (!_pane_initialized) { - int y, line_height; - _status_text->get_line_yrange(buffer->begin(), y, line_height); - const int pad = _status_text->get_pixels_inside_wrap(); - const int max_pos = _main_paned->get_allocation().get_height(); - const int min_height = (line_height + 2 * pad); - const int conf_height = _conf->get_messages_height(); - _main_paned->set_position(max_pos - std::max(conf_height, min_height)); - _pane_initialized = true; - } - - _log_scrolledwindow->show(); - _status_text->scroll_to_mark( - _status_text->get_buffer()->get_insert(), 0); - _conf->set_show_messages(true); - } else { - _log_scrolledwindow->hide(); - _conf->set_show_messages(false); - } + return false; } void -Patchage::on_view_toolbar() -{ - if (_menu_view_toolbar->get_active()) { - _toolbar->show(); - } else { - _toolbar->hide(); - } - _conf->set_show_toolbar(_menu_view_toolbar->get_active()); -} - -bool -Patchage::on_scroll(GdkEventScroll* ev) +Patchage::on_menu_action(const Action& action) { - return false; + _reactor(action); } void Patchage::buffer_size_changed() { -#if defined(HAVE_JACK) || defined(HAVE_JACK_DBUS) - const int selected = _buf_size_combo->get_active_row_number(); - - if (selected == -1) { - update_toolbar(); - } else { - const jack_nframes_t buffer_size = 1 << (selected + 5); - _jack_driver->set_buffer_size(buffer_size); - update_toolbar(); - } -#endif + if (_drivers.jack()) { + const int selected = _buf_size_combo->get_active_row_number(); + + if (selected == -1) { + update_toolbar(); + } else { + const uint32_t buffer_size = 1u << (selected + 5); + _drivers.jack()->set_buffer_size(buffer_size); + update_toolbar(); + } + } } +} // namespace patchage diff --git a/src/Patchage.hpp b/src/Patchage.hpp index cf550f0..8f3cca0 100644 --- a/src/Patchage.hpp +++ b/src/Patchage.hpp @@ -1,212 +1,199 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifndef PATCHAGE_PATCHAGE_HPP #define PATCHAGE_PATCHAGE_HPP -#include <set> -#include <string> - -#include <boost/shared_ptr.hpp> - -#include <gtkmm/aboutdialog.h> -#include <gtkmm/alignment.h> -#include <gtkmm/builder.h> -#include <gtkmm/button.h> -#include <gtkmm/checkmenuitem.h> -#include <gtkmm/combobox.h> -#include <gtkmm/dialog.h> -#include <gtkmm/imagemenuitem.h> -#include <gtkmm/label.h> -#include <gtkmm/main.h> -#include <gtkmm/menubar.h> -#include <gtkmm/menuitem.h> -#include <gtkmm/paned.h> -#include <gtkmm/progressbar.h> -#include <gtkmm/scrolledwindow.h> -#include <gtkmm/textview.h> -#include <gtkmm/toolbar.h> -#include <gtkmm/toolbutton.h> -#include <gtkmm/viewport.h> -#include <gtkmm/window.h> - -#include "patchage_config.h" +#include <gdk/gdk.h> +#include <glibmm/refptr.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treemodelcolumn.h> +#include <gtkmm/widget.h> + +#include "Action.hpp" +#include "ActionSink.hpp" +#include "Configuration.hpp" +#include "Drivers.hpp" +#include "Event.hpp" +#include "Metadata.hpp" +#include "Options.hpp" +#include "Reactor.hpp" +#include "Setting.hpp" +#include "TextViewLog.hpp" #include "Widget.hpp" -#include "Legend.hpp" -class AlsaDriver; -class JackDriver; -class PatchageCanvas; -class Configuration; - -namespace Ganv { class Module; } +#include <cstdint> +#include <memory> +#include <mutex> +#include <queue> +#include <string> -class Patchage { +namespace Glib { +class ustring; +} // namespace Glib + +namespace Gtk { +class AboutDialog; +class Alignment; +class Builder; +class CheckMenuItem; +class ComboBox; +class ImageMenuItem; +class Label; +class MenuBar; +class MenuItem; +class Paned; +class ScrolledWindow; +class TextTag; +class TextView; +class ToolButton; +class Toolbar; +class VBox; +class Window; +} // namespace Gtk + +namespace patchage { + +enum class PortType; + +class Canvas; +class ILog; +class Legend; + +/// Main application class +class Patchage +{ public: - Patchage(int argc, char** argv); - ~Patchage(); - - const boost::shared_ptr<PatchageCanvas>& canvas() const { return _canvas; } - - Gtk::Window* window() { return _main_win.get(); } - - Configuration* conf() const { return _conf; } - JackDriver* jack_driver() const { return _jack_driver; } -#ifdef HAVE_ALSA - AlsaDriver* alsa_driver() const { return _alsa_driver; } -#endif -#ifdef PATCHAGE_JACK_SESSION - void show_open_session_dialog(); - void show_save_session_dialog(); - void show_save_close_session_dialog(); -#endif - - Glib::RefPtr<Gtk::Builder> xml() { return _xml; } - - void attach(); - void save(); - void quit() { _main_win->hide(); } - - void refresh(); - inline void queue_refresh() { _refresh = true; } - inline void driver_detached() { _driver_detached = true; } - - void info_msg(const std::string& msg); - void error_msg(const std::string& msg); - void warning_msg(const std::string& msg); - - void update_state(); - void store_window_location(); - - bool show_human_names() const { return _menu_view_human_names->get_active(); } - bool sort_ports() const { return _menu_view_sort_ports->get_active(); } + explicit Patchage(Options options); + ~Patchage(); + + Patchage(const Patchage&) = delete; + Patchage& operator=(const Patchage&) = delete; + + Patchage(Patchage&&) = delete; + Patchage& operator=(Patchage&&) = delete; + + void operator()(const setting::AlsaAttached& setting); + void operator()(const setting::FontSize& setting); + void operator()(const setting::HumanNames& setting); + void operator()(const setting::JackAttached& setting); + void operator()(const setting::MessagesHeight& setting); + void operator()(const setting::MessagesVisible& setting); + void operator()(const setting::PortColor& setting); + void operator()(const setting::SortedPorts& setting); + void operator()(const setting::SprungLayout& setting); + void operator()(const setting::ToolbarVisible& setting); + void operator()(const setting::WindowLocation& setting); + void operator()(const setting::WindowSize& setting); + void operator()(const setting::Zoom& setting); + + void attach(); + void save(); + void quit(); + + void store_window_location(); + + Canvas& canvas() const { return *_canvas; } + Gtk::Window* window() { return _main_win.get(); } + ILog& log() { return _log; } + Metadata& metadata() { return _metadata; } + const Configuration& conf() const { return _conf; } + Configuration& conf() { return _conf; } protected: - class BufferSizeColumns : public Gtk::TreeModel::ColumnRecord { - public: - BufferSizeColumns() { add(label); } - - Gtk::TreeModelColumn<Glib::ustring> label; - }; - - void connect_widgets(); - - void on_arrange(); - void on_sprung_layout_toggled(); - void on_help_about(); - void on_quit(); - void on_export_image(); - void on_view_messages(); - void on_view_toolbar(); - void on_store_positions(); - void on_view_human_names(); - void on_view_sort_ports(); - void on_zoom_in(); - void on_zoom_out(); - void on_zoom_normal(); - void on_zoom_full(); - void on_increase_font_size(); - void on_decrease_font_size(); - void on_normal_font_size(); - void on_legend_color_change(int id, const std::string& label, uint32_t rgba); - void on_messages_resized(Gtk::Allocation& alloc); - - bool on_scroll(GdkEventScroll* ev); - - void zoom(double z); - bool idle_callback(); - void clear_load(); - bool update_load(); - void update_toolbar(); - - void buffer_size_changed(); - - Glib::RefPtr<Gtk::Builder> _xml; - -#ifdef HAVE_ALSA - AlsaDriver* _alsa_driver; - void menu_alsa_connect(); - void menu_alsa_disconnect(); -#endif - -#ifdef PATCHAGE_JACK_SESSION - void save_session(bool close); -#endif - - boost::shared_ptr<PatchageCanvas> _canvas; - - JackDriver* _jack_driver; - Configuration* _conf; - - Gtk::Main* _gtk_main; - - BufferSizeColumns _buf_size_columns; - - Widget<Gtk::AboutDialog> _about_win; - Widget<Gtk::ScrolledWindow> _main_scrolledwin; - Widget<Gtk::Window> _main_win; - Widget<Gtk::VBox> _main_vbox; - Widget<Gtk::MenuBar> _menubar; - Widget<Gtk::MenuItem> _menu_alsa_connect; - Widget<Gtk::MenuItem> _menu_alsa_disconnect; - Widget<Gtk::MenuItem> _menu_file_quit; - Widget<Gtk::MenuItem> _menu_export_image; - Widget<Gtk::MenuItem> _menu_help_about; - Widget<Gtk::MenuItem> _menu_jack_connect; - Widget<Gtk::MenuItem> _menu_jack_disconnect; - Widget<Gtk::MenuItem> _menu_open_session; - Widget<Gtk::MenuItem> _menu_save_session; - Widget<Gtk::MenuItem> _menu_save_close_session; - Widget<Gtk::MenuItem> _menu_view_arrange; - Widget<Gtk::CheckMenuItem> _menu_view_sprung_layout; - Widget<Gtk::CheckMenuItem> _menu_view_messages; - Widget<Gtk::CheckMenuItem> _menu_view_toolbar; - Widget<Gtk::MenuItem> _menu_view_refresh; - Widget<Gtk::CheckMenuItem> _menu_view_human_names; - Widget<Gtk::CheckMenuItem> _menu_view_sort_ports; - Widget<Gtk::ImageMenuItem> _menu_zoom_in; - Widget<Gtk::ImageMenuItem> _menu_zoom_out; - Widget<Gtk::ImageMenuItem> _menu_zoom_normal; - Widget<Gtk::ImageMenuItem> _menu_zoom_full; - Widget<Gtk::MenuItem> _menu_increase_font_size; - Widget<Gtk::MenuItem> _menu_decrease_font_size; - Widget<Gtk::MenuItem> _menu_normal_font_size; - Widget<Gtk::Toolbar> _toolbar; - Widget<Gtk::ToolButton> _clear_load_but; - Widget<Gtk::ProgressBar> _xrun_progress; - Widget<Gtk::ComboBox> _buf_size_combo; - Widget<Gtk::Label> _latency_label; - Widget<Gtk::Alignment> _legend_alignment; - Widget<Gtk::Paned> _main_paned; - Widget<Gtk::ScrolledWindow> _log_scrolledwindow; - Widget<Gtk::TextView> _status_text; - Legend* _legend; - - Glib::RefPtr<Gtk::TextTag> _error_tag; - Glib::RefPtr<Gtk::TextTag> _warning_tag; - - bool _pane_initialized; - bool _attach; - bool _driver_detached; - bool _refresh; - bool _enable_refresh; - bool _jack_driver_autoattach; -#ifdef HAVE_ALSA - bool _alsa_driver_autoattach; -#endif + class BufferSizeColumns : public Gtk::TreeModel::ColumnRecord + { + public: + BufferSizeColumns() { add(label); } + + Gtk::TreeModelColumn<Glib::ustring> label; + }; + + void on_driver_event(const Event& event); + void process_events(); + + void on_conf_change(const Setting& setting); + + void on_arrange(); + void on_help_about(); + void on_quit(); + void on_export_image(); + void on_store_positions(); + + void on_legend_color_change(PortType id, + const std::string& label, + uint32_t rgba); + + void on_messages_resized(Gtk::Allocation& alloc); + + bool on_scroll(GdkEventScroll* ev); + + void on_menu_action(const Action& action); + + bool idle_callback(); + void clear_load(); + bool update_load(); + void update_toolbar(); + + void buffer_size_changed(); + + Glib::RefPtr<Gtk::Builder> _xml; + + Widget<Gtk::AboutDialog> _about_win; + Widget<Gtk::ScrolledWindow> _main_scrolledwin; + Widget<Gtk::Window> _main_win; + Widget<Gtk::VBox> _main_vbox; + Widget<Gtk::MenuBar> _menubar; + Widget<Gtk::MenuItem> _menu_alsa_connect; + Widget<Gtk::MenuItem> _menu_alsa_disconnect; + Widget<Gtk::MenuItem> _menu_file_quit; + Widget<Gtk::MenuItem> _menu_export_image; + Widget<Gtk::MenuItem> _menu_help_about; + Widget<Gtk::MenuItem> _menu_jack_connect; + Widget<Gtk::MenuItem> _menu_jack_disconnect; + Widget<Gtk::MenuItem> _menu_view_arrange; + Widget<Gtk::CheckMenuItem> _menu_view_sprung_layout; + Widget<Gtk::CheckMenuItem> _menu_view_messages; + Widget<Gtk::CheckMenuItem> _menu_view_toolbar; + Widget<Gtk::MenuItem> _menu_view_refresh; + Widget<Gtk::CheckMenuItem> _menu_view_human_names; + Widget<Gtk::CheckMenuItem> _menu_view_sort_ports; + Widget<Gtk::ImageMenuItem> _menu_zoom_in; + Widget<Gtk::ImageMenuItem> _menu_zoom_out; + Widget<Gtk::ImageMenuItem> _menu_zoom_normal; + Widget<Gtk::ImageMenuItem> _menu_zoom_full; + Widget<Gtk::MenuItem> _menu_increase_font_size; + Widget<Gtk::MenuItem> _menu_decrease_font_size; + Widget<Gtk::MenuItem> _menu_normal_font_size; + Widget<Gtk::Toolbar> _toolbar; + Widget<Gtk::ToolButton> _clear_load_but; + Widget<Gtk::Label> _dropouts_label; + Widget<Gtk::ComboBox> _buf_size_combo; + Widget<Gtk::Label> _latency_label; + Widget<Gtk::Alignment> _legend_alignment; + Widget<Gtk::Paned> _main_paned; + Widget<Gtk::ScrolledWindow> _log_scrolledwindow; + Widget<Gtk::TextView> _status_text; + + Configuration _conf; + TextViewLog _log; + std::unique_ptr<Canvas> _canvas; + std::mutex _events_mutex; + std::queue<Event> _driver_events; + BufferSizeColumns _buf_size_columns; + Legend* _legend{nullptr}; + Metadata _metadata; + Drivers _drivers; + Reactor _reactor; + ActionSink _action_sink; + + Glib::RefPtr<Gtk::TextTag> _error_tag; + Glib::RefPtr<Gtk::TextTag> _warning_tag; + + Options _options; + bool _attach{true}; }; +} // namespace patchage + #endif // PATCHAGE_PATCHAGE_HPP diff --git a/src/PatchageCanvas.cpp b/src/PatchageCanvas.cpp deleted file mode 100644 index 4d63a4b..0000000 --- a/src/PatchageCanvas.cpp +++ /dev/null @@ -1,338 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <boost/format.hpp> - -#include "patchage_config.h" - -#if defined(HAVE_JACK_DBUS) - #include "JackDbusDriver.hpp" -#elif defined(PATCHAGE_LIBJACK) - #include "JackDriver.hpp" -#endif -#ifdef HAVE_ALSA - #include "AlsaDriver.hpp" -#endif - -#include "ganv/Edge.hpp" - -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageModule.hpp" -#include "PatchagePort.hpp" - -using std::string; -using boost::format; - -PatchageCanvas::PatchageCanvas(Patchage* app, int width, int height) - : Ganv::Canvas(width, height) - , _app(app) -{ - signal_event.connect( - sigc::mem_fun(this, &PatchageCanvas::on_event)); - signal_connect.connect( - sigc::mem_fun(this, &PatchageCanvas::connect)); - signal_disconnect.connect( - sigc::mem_fun(this, &PatchageCanvas::disconnect)); -} - -PatchageModule* -PatchageCanvas::find_module(const string& name, ModuleType type) -{ - const ModuleIndex::const_iterator i = _module_index.find(name); - if (i == _module_index.end()) - return NULL; - - PatchageModule* io_module = NULL; - for (ModuleIndex::const_iterator j = i; j != _module_index.end() && j->first == name; ++j) { - if (j->second->type() == type) { - return j->second; - } else if (j->second->type() == InputOutput) { - io_module = j->second; - } - } - - // Return InputOutput module for Input or Output (or NULL if not found at all) - return io_module; -} - -void -PatchageCanvas::remove_module(const string& name) -{ - ModuleIndex::iterator i = _module_index.find(name); - while (i != _module_index.end()) { - PatchageModule* mod = i->second; - _module_index.erase(i); - i = _module_index.find(name); - delete mod; - } -} - -PatchagePort* -PatchageCanvas::find_port(const PortID& id) -{ - PatchagePort* pp = NULL; - - PortIndex::iterator i = _port_index.find(id); - if (i != _port_index.end()) { - assert(i->second->get_module()); - return i->second; - } - -#ifdef PATCHAGE_LIBJACK - // Alsa ports are always indexed (or don't exist at all) - if (id.type == PortID::JACK_ID) { - jack_port_t* jack_port = jack_port_by_id(_app->jack_driver()->client(), id.id.jack_id); - if (!jack_port) - return NULL; - - string module_name; - string port_name; - _app->jack_driver()->port_names(id, module_name, port_name); - - PatchageModule* module = find_module( - module_name, (jack_port_flags(jack_port) & JackPortIsInput) ? Input : Output); - - if (module) - pp = dynamic_cast<PatchagePort*>(module->get_port(port_name)); - - if (pp) - index_port(id, pp); - } -#endif // PATCHAGE_LIBJACK - - return pp; -} - -void -PatchageCanvas::remove_port(const PortID& id) -{ - PatchagePort* const port = find_port(id); - _port_index.erase(id); - delete port; -} - -struct RemovePortsData { - typedef bool (*Predicate)(const PatchagePort*); - - RemovePortsData(Predicate p) : pred(p) {} - - Predicate pred; - std::set<PatchageModule*> empty; -}; - -static void -delete_port_if_matches(GanvPort* port, void* cdata) -{ - RemovePortsData* data = (RemovePortsData*)cdata; - PatchagePort* pport = dynamic_cast<PatchagePort*>(Glib::wrap(port)); - if (pport && data->pred(pport)) { - delete pport; - } -} - -static void -remove_ports_matching(GanvNode* node, void* cdata) -{ - if (!GANV_IS_MODULE(node)) { - return; - } - - Ganv::Module* cmodule = Glib::wrap(GANV_MODULE(node)); - PatchageModule* pmodule = dynamic_cast<PatchageModule*>(cmodule); - if (!pmodule) { - return; - } - - RemovePortsData* data = (RemovePortsData*)cdata; - - pmodule->for_each_port(delete_port_if_matches, data); - - if (pmodule->num_ports() == 0) { - data->empty.insert(pmodule); - } -} - -void -PatchageCanvas::remove_ports(bool (*pred)(const PatchagePort*)) -{ - RemovePortsData data(pred); - - for_each_node(remove_ports_matching, &data); - - for (PortIndex::iterator i = _port_index.begin(); - i != _port_index.end();) { - PortIndex::iterator next = i; - ++next; - if (pred(i->second)) { - _port_index.erase(i); - } - i = next; - } - - for (std::set<PatchageModule*>::iterator i = data.empty.begin(); - i != data.empty.end(); ++i) { - delete *i; - } -} - -PatchagePort* -PatchageCanvas::find_port_by_name(const std::string& client_name, - const std::string& port_name) -{ - const ModuleIndex::const_iterator i = _module_index.find(client_name); - if (i == _module_index.end()) - return NULL; - - for (ModuleIndex::const_iterator j = i; j != _module_index.end() && j->first == client_name; ++j) { - PatchagePort* port = dynamic_cast<PatchagePort*>(j->second->get_port(port_name)); - if (port) - return port; - } - - return NULL; -} - -void -PatchageCanvas::connect(Ganv::Node* port1, - Ganv::Node* port2) -{ - PatchagePort* p1 = dynamic_cast<PatchagePort*>(port1); - PatchagePort* p2 = dynamic_cast<PatchagePort*>(port2); - if (!p1 || !p2) - return; - - if ((p1->type() == JACK_AUDIO && p2->type() == JACK_AUDIO) || - (p1->type() == JACK_MIDI && p2->type() == JACK_MIDI) || - (p1->type() == JACK_AUDIO && p2->type() == JACK_CV) || - (p1->type() == JACK_CV && p2->type() == JACK_CV) || - (p1->type() == JACK_OSC && p2->type() == JACK_OSC)) { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - _app->jack_driver()->connect(p1, p2); -#endif -#ifdef HAVE_ALSA - } else if (p1->type() == ALSA_MIDI && p2->type() == ALSA_MIDI) { - _app->alsa_driver()->connect(p1, p2); -#endif - } else { - _app->warning_msg("Cannot make connection, incompatible port types."); - } -} - -void -PatchageCanvas::disconnect(Ganv::Node* port1, - Ganv::Node* port2) -{ - PatchagePort* input = dynamic_cast<PatchagePort*>(port1); - PatchagePort* output = dynamic_cast<PatchagePort*>(port2); - if (!input || !output) - return; - - if (input->is_output() && output->is_input()) { - // Damn, guessed wrong - PatchagePort* swap = input; - input = output; - output = swap; - } - - if (!input || !output || input->is_output() || output->is_input()) { - _app->error_msg("Attempt to disconnect mismatched/unknown ports."); - return; - } - - if (input->type() == JACK_AUDIO || - input->type() == JACK_MIDI || - input->type() == JACK_CV || - input->type() == JACK_OSC) { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - _app->jack_driver()->disconnect(output, input); -#endif -#ifdef HAVE_ALSA - } else if (input->type() == ALSA_MIDI) { - _app->alsa_driver()->disconnect(output, input); -#endif - } else { - _app->error_msg("Attempt to disconnect ports with strange types."); - } -} - -void -PatchageCanvas::add_module(const std::string& name, PatchageModule* module) -{ - _module_index.insert(std::make_pair(name, module)); - - // Join partners, if applicable - PatchageModule* in_module = NULL; - PatchageModule* out_module = NULL; - if (module->type() == Input) { - in_module = module; - out_module = find_module(name, Output); - } else if (module->type() == Output) { - in_module = find_module(name, Output); - out_module = module; - } - - if (in_module && out_module) - out_module->set_partner(in_module); -} - -static void -disconnect_edge(GanvEdge* edge, void* data) -{ - PatchageCanvas* canvas = (PatchageCanvas*)data; - Ganv::Edge* edgemm = Glib::wrap(edge); - canvas->disconnect(edgemm->get_tail(), edgemm->get_head()); -} - -bool -PatchageCanvas::on_event(GdkEvent* ev) -{ - if (ev->type == GDK_KEY_PRESS && ev->key.keyval == GDK_Delete) { - for_each_selected_edge(disconnect_edge, this); - clear_selection(); - return true; - } - - return false; -} - -bool -PatchageCanvas::make_connection(Ganv::Node* tail, Ganv::Node* head) -{ - new Ganv::Edge(*this, tail, head); - return true; -} - -void -PatchageCanvas::remove_module(PatchageModule* module) -{ - // Remove module from cache - for (ModuleIndex::iterator i = _module_index.find(module->get_label()); - i != _module_index.end() && i->first == module->get_label(); ++i) { - if (i->second == module) { - _module_index.erase(i); - return; - } - } -} - -void -PatchageCanvas::clear() -{ - _port_index.clear(); - _module_index.clear(); - Ganv::Canvas::clear(); -} diff --git a/src/PatchageCanvas.hpp b/src/PatchageCanvas.hpp deleted file mode 100644 index 4b5fac1..0000000 --- a/src/PatchageCanvas.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_PATCHAGECANVAS_HPP -#define PATCHAGE_PATCHAGECANVAS_HPP - -#include <map> -#include <string> - -#include "patchage_config.h" - -#ifdef HAVE_ALSA - #include <alsa/asoundlib.h> -#endif - -#include "ganv/Canvas.hpp" - -#include "PatchageEvent.hpp" -#include "PatchageModule.hpp" -#include "PortID.hpp" - -class Patchage; -class PatchageModule; -class PatchagePort; - -class PatchageCanvas : public Ganv::Canvas { -public: - PatchageCanvas(Patchage* _app, int width, int height); - - PatchageModule* find_module(const std::string& name, ModuleType type); - PatchagePort* find_port(const PortID& id); - - void remove_module(const std::string& name); - void remove_module(PatchageModule* module); - - PatchagePort* find_port_by_name(const std::string& client_name, - const std::string& port_name); - - void connect(Ganv::Node* port1, - Ganv::Node* port2); - - void disconnect(Ganv::Node* port1, - Ganv::Node* port2); - - void index_port(const PortID& id, PatchagePort* port) { - _port_index.insert(std::make_pair(id, port)); - } - - void remove_ports(bool (*pred)(const PatchagePort*)); - - void add_module(const std::string& name, PatchageModule* module); - - bool make_connection(Ganv::Node* tail, Ganv::Node* head); - - void remove_port(const PortID& id); - - void clear(); - -private: - Patchage* _app; - - bool on_event(GdkEvent* ev); - bool on_connection_event(Ganv::Edge* c, GdkEvent* ev); - - typedef std::map<const PortID, PatchagePort*> PortIndex; - PortIndex _port_index; - - typedef std::multimap<const std::string, PatchageModule*> ModuleIndex; - ModuleIndex _module_index; -}; - -#endif // PATCHAGE_PATCHAGECANVAS_HPP diff --git a/src/PatchageEvent.cpp b/src/PatchageEvent.cpp deleted file mode 100644 index ea9a758..0000000 --- a/src/PatchageEvent.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <boost/format.hpp> - -#include "patchage_config.h" -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageModule.hpp" -#include "PatchageEvent.hpp" -#include "Driver.hpp" -#if defined(HAVE_JACK_DBUS) -# include "JackDbusDriver.hpp" -#elif defined(PATCHAGE_LIBJACK) -# include "JackDriver.hpp" -#endif -#ifdef HAVE_ALSA -# include "AlsaDriver.hpp" -#endif - -using std::endl; -using boost::format; - -void -PatchageEvent::execute(Patchage* patchage) -{ - if (_type == REFRESH) { - patchage->refresh(); - - } else if (_type == CLIENT_CREATION) { - // No empty modules (for now) - g_free(_str); - _str = NULL; - - } else if (_type == CLIENT_DESTRUCTION) { - patchage->canvas()->remove_module(_str); - g_free(_str); - _str = NULL; - - } else if (_type == PORT_CREATION) { - - Driver* driver = NULL; - if (_port_1.type == PortID::JACK_ID) { -#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) - driver = patchage->jack_driver(); -#endif -#ifdef HAVE_ALSA - } else if (_port_1.type == PortID::ALSA_ADDR) { - driver = patchage->alsa_driver(); -#endif - } - - if (driver) { - PatchagePort* port = driver->create_port_view(patchage, _port_1); - if (!port) { - patchage->error_msg( - (format("Unable to create view for port `%1%'") - % _port_1).str()); - } - } else { - patchage->error_msg( - (format("Unknown type for port `%1%'") % _port_1).str()); - } - - } else if (_type == PORT_DESTRUCTION) { - - patchage->canvas()->remove_port(_port_1); - - } else if (_type == CONNECTION) { - - PatchagePort* port_1 = patchage->canvas()->find_port(_port_1); - PatchagePort* port_2 = patchage->canvas()->find_port(_port_2); - - if (!port_1) - patchage->error_msg((format("Unable to find port `%1%' to connect") - % _port_1).str()); - else if (!port_2) - patchage->error_msg((format("Unable to find port `%1%' to connect") - % _port_2).str()); - else - patchage->canvas()->make_connection(port_1, port_2); - - } else if (_type == DISCONNECTION) { - - PatchagePort* port_1 = patchage->canvas()->find_port(_port_1); - PatchagePort* port_2 = patchage->canvas()->find_port(_port_2); - - if (!port_1) - patchage->error_msg((format("Unable to find port `%1%' to disconnect") - % _port_1).str()); - else if (!port_2) - patchage->error_msg((format("Unable to find port `%1%' to disconnect") - % _port_2).str()); - else - patchage->canvas()->remove_edge_between(port_1, port_2); - } -} diff --git a/src/PatchageEvent.hpp b/src/PatchageEvent.hpp deleted file mode 100644 index 899f77f..0000000 --- a/src/PatchageEvent.hpp +++ /dev/null @@ -1,87 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_PATCHAGEEVENT_HPP -#define PATCHAGE_PATCHAGEEVENT_HPP - -#include <cstring> - -#include "patchage_config.h" - -#ifdef PATCHAGE_LIBJACK - #include <jack/jack.h> -#endif -#ifdef HAVE_ALSA - #include <alsa/asoundlib.h> -#endif - -#include "PatchagePort.hpp" -#include "PortID.hpp" - -class Patchage; - -/** A Driver event to be processed by the GUI thread. - */ -class PatchageEvent { -public: - enum Type { - NULL_EVENT = 0, - REFRESH, - CLIENT_CREATION, - CLIENT_DESTRUCTION, - PORT_CREATION, - PORT_DESTRUCTION, - CONNECTION, - DISCONNECTION - }; - - explicit PatchageEvent(Type type=NULL_EVENT) - : _str(NULL) - , _type(type) - {} - - PatchageEvent(Type type, const char* str) - : _str(g_strdup(str)) - , _type(type) - {} - - template <typename P> - PatchageEvent(Type type, P port) - : _str(NULL) - , _port_1(port) - , _type(type) - {} - - template <typename P> - PatchageEvent(Type type, P port_1, P port_2) - : _str(NULL) - , _port_1(port_1, false) - , _port_2(port_2, true) - , _type(type) - {} - - void execute(Patchage* patchage); - - inline Type type() const { return (Type)_type; } - -private: - char* _str; - PortID _port_1; - PortID _port_2; - uint8_t _type; -}; - -#endif // PATCHAGE_PATCHAGEEVENT_HPP diff --git a/src/PatchageModule.cpp b/src/PatchageModule.cpp deleted file mode 100644 index 8ba5296..0000000 --- a/src/PatchageModule.cpp +++ /dev/null @@ -1,157 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2010-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#include "Patchage.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageModule.hpp" -#include "PatchagePort.hpp" - -PatchageModule::PatchageModule( - Patchage* app, const std::string& name, ModuleType type, double x, double y) - : Module(*app->canvas().get(), name, x, y) - , _app(app) - , _menu(NULL) - , _name(name) - , _type(type) -{ - signal_event().connect( - sigc::mem_fun(this, &PatchageModule::on_event)); - - signal_moved().connect( - sigc::mem_fun(this, &PatchageModule::store_location)); - - // Set as source by default, turned off if input ports added - set_is_source(true); -} - -PatchageModule::~PatchageModule() -{ - _app->canvas()->remove_module(this); - delete _menu; - _menu = NULL; -} - -void -PatchageModule::update_menu() -{ - if (!_menu) - return; - - if (_type == InputOutput) { - bool has_in = false; - bool has_out = false; - for (const_iterator p = begin(); p != end(); ++p) { - if ((*p)->is_input()) { - has_in = true; - } else { - has_out = true; - } - if (has_in && has_out) { - _menu->items()[0].show(); // Show "Split" menu item - return; - } - } - _menu->items()[0].hide(); // Hide "Split" menu item - } -} - -bool -PatchageModule::show_menu(GdkEventButton* ev) -{ - _menu = new Gtk::Menu(); - Gtk::Menu::MenuList& items = _menu->items(); - if (_type == InputOutput) { - items.push_back( - Gtk::Menu_Helpers::MenuElem( - "_Split", sigc::mem_fun(this, &PatchageModule::split))); - update_menu(); - } else { - items.push_back( - Gtk::Menu_Helpers::MenuElem( - "_Join", sigc::mem_fun(this, &PatchageModule::join))); - } - items.push_back( - Gtk::Menu_Helpers::MenuElem( - "_Disconnect All", - sigc::mem_fun(this, &PatchageModule::menu_disconnect_all))); - - _menu->popup(ev->button, ev->time); - return true; -} - -bool -PatchageModule::on_event(GdkEvent* ev) -{ - if (ev->type == GDK_BUTTON_PRESS && ev->button.button == 3) { - return show_menu(&ev->button); - } - return false; -} - -void -PatchageModule::load_location() -{ - Coord loc; - - if (_app->conf()->get_module_location(_name, _type, loc)) - move_to(loc.x, loc.y); - else - move_to(20 + rand() % 640, - 20 + rand() % 480); -} - -void -PatchageModule::store_location(double x, double y) -{ - Coord loc(get_x(), get_y()); - _app->conf()->set_module_location(_name, _type, loc); -} - -void -PatchageModule::split() -{ - assert(_type == InputOutput); - _app->conf()->set_module_split(_name, true); - _app->refresh(); -} - -void -PatchageModule::join() -{ - assert(_type != InputOutput); - _app->conf()->set_module_split(_name, false); - _app->refresh(); -} - -void -PatchageModule::menu_disconnect_all() -{ - for (iterator p = begin(); p != end(); ++p) - (*p)->disconnect(); -} - -PatchagePort* -PatchageModule::get_port(const std::string& name) -{ - for (iterator p = begin(); p != end(); ++p) { - PatchagePort* pport = dynamic_cast<PatchagePort*>(*p); - if (pport && pport->name() == name) { - return pport; - } - } - - return NULL; -} diff --git a/src/PatchageModule.hpp b/src/PatchageModule.hpp deleted file mode 100644 index 99527ac..0000000 --- a/src/PatchageModule.hpp +++ /dev/null @@ -1,67 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_PATCHAGEMODULE_HPP -#define PATCHAGE_PATCHAGEMODULE_HPP - -#include <string> - -#include <gtkmm/menu_elems.h> - -#include "ganv/Module.hpp" -#include "ganv/Port.hpp" - -#include "Configuration.hpp" - -class Patchage; -class PatchagePort; - -class PatchageModule : public Ganv::Module -{ -public: - PatchageModule(Patchage* app, - const std::string& name, - ModuleType type, - double x = 0, - double y = 0); - ~PatchageModule(); - - void split(); - void join(); - - bool show_menu(GdkEventButton* ev); - void update_menu(); - - PatchagePort* get_port(const std::string& name); - - void load_location(); - void menu_disconnect_all(); - void show_dialog() {} - void store_location(double x, double y); - - ModuleType type() const { return _type; } - const std::string& name() const { return _name; } - -protected: - bool on_event(GdkEvent* ev); - - Patchage* _app; - Gtk::Menu* _menu; - std::string _name; - ModuleType _type; -}; - -#endif // PATCHAGE_PATCHAGEMODULE_HPP diff --git a/src/PatchagePort.hpp b/src/PatchagePort.hpp deleted file mode 100644 index d5d6cb3..0000000 --- a/src/PatchagePort.hpp +++ /dev/null @@ -1,104 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef PATCHAGE_PATCHAGEPORT_HPP -#define PATCHAGE_PATCHAGEPORT_HPP - -#include <string> - -#include <boost/shared_ptr.hpp> - -#include <gtkmm/menu.h> -#include <gtkmm/menushell.h> - -#include "ganv/Port.hpp" -#include "ganv/Module.hpp" - -#include "Configuration.hpp" -#include "PatchageCanvas.hpp" -#include "PatchageModule.hpp" -#include "PortID.hpp" -#include "patchage_config.h" - -/** A Port on a PatchageModule - */ -class PatchagePort : public Ganv::Port -{ -public: - PatchagePort(Ganv::Module& module, - PortType type, - const std::string& name, - const std::string& human_name, - bool is_input, - uint32_t color, - bool show_human_name, - boost::optional<int> order=boost::optional<int>()) - : Port(module, - (show_human_name && !human_name.empty()) ? human_name : name, - is_input, - color) - , _type(type) - , _name(name) - , _human_name(human_name) - , _order(order) - { - signal_event().connect( - sigc::mem_fun(this, &PatchagePort::on_event)); - } - - virtual ~PatchagePort() {} - - /** Returns the full name of this port, as "modulename:portname" */ - std::string full_name() const { - PatchageModule* pmod = dynamic_cast<PatchageModule*>(get_module()); - return std::string(pmod->name()) + ":" + _name; - } - - void show_human_name(bool human) { - if (human && !_human_name.empty()) { - set_label(_human_name.c_str()); - } else { - set_label(_name.c_str()); - } - } - - bool on_event(GdkEvent* ev) { - if (ev->type != GDK_BUTTON_PRESS || ev->button.button != 3) { - return false; - } - - Gtk::Menu* menu = Gtk::manage(new Gtk::Menu()); - menu->items().push_back( - Gtk::Menu_Helpers::MenuElem( - "Disconnect", sigc::mem_fun(this, &Port::disconnect))); - - menu->popup(ev->button.button, ev->button.time); - return true; - } - - PortType type() const { return _type; } - const std::string& name() const { return _name; } - const std::string& human_name() const { return _human_name; } - const boost::optional<int>& order() const { return _order; } - -private: - PortType _type; - std::string _name; - std::string _human_name; - boost::optional<int> _order; -}; - -#endif // PATCHAGE_PATCHAGEPORT_HPP diff --git a/src/PortID.hpp b/src/PortID.hpp index 3f916c0..54e4c5d 100644 --- a/src/PortID.hpp +++ b/src/PortID.hpp @@ -1,120 +1,178 @@ -/* This file is part of Patchage. - * Copyright 2008-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2008-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifndef PATCHAGE_PORTID_HPP #define PATCHAGE_PORTID_HPP -#include <cstring> +#include "ClientID.hpp" +#include "ClientType.hpp" +#include "warnings.hpp" + +#include <cassert> +#include <cstddef> +#include <cstdint> +#include <functional> #include <iostream> +#include <string> +#include <tuple> +#include <utility> + +namespace patchage { -#include "patchage_config.h" +/// An ID for some port on a client (program) +struct PortID { + using Type = ClientType; -#ifdef PATCHAGE_LIBJACK - #include <jack/jack.h> -#endif -#ifdef HAVE_ALSA - #include <alsa/asoundlib.h> -#endif + PortID(const PortID& copy) = default; + PortID& operator=(const PortID& copy) = default; -#include "PatchagePort.hpp" + PortID(PortID&& id) = default; + PortID& operator=(PortID&& id) = default; -struct PortID { - PortID() : type(NULL_PORT_ID) { memset(&id, 0, sizeof(id)); } - PortID(const PortID& copy) : type(copy.type) { - memcpy(&id, ©.id, sizeof(id)); - } - - enum { NULL_PORT_ID, JACK_ID, ALSA_ADDR } type; - -#ifdef PATCHAGE_LIBJACK - PortID(jack_port_id_t jack_id, bool ign=false) - : type(JACK_ID) { id.jack_id = jack_id; } -#endif - -#ifdef HAVE_ALSA - PortID(snd_seq_addr_t addr, bool in) - : type(ALSA_ADDR) { id.alsa_addr = addr; id.is_input = in; } -#endif - - union { -#ifdef PATCHAGE_LIBJACK - jack_port_id_t jack_id; -#endif -#ifdef HAVE_ALSA - struct { - snd_seq_addr_t alsa_addr; - bool is_input : 1; - }; -#endif - } id; + ~PortID() = default; + + /// Return an ID for a JACK port by full name (like "client:port") + static PortID jack(std::string name) + { + return PortID{Type::jack, std::move(name)}; + } + + /// Return an ID for a JACK port by separate client and port name + static PortID jack(const std::string& client_name, + const std::string& port_name) + { + return PortID{Type::jack, client_name + ":" + port_name}; + } + + /// Return an ID for an ALSA Sequencer port by ID + static PortID alsa(const uint8_t client_id, + const uint8_t port, + const bool is_input) + { + return PortID{Type::alsa, client_id, port, is_input}; + } + + /// Return the ID of the client that hosts this port + ClientID client() const + { + switch (_type) { + case Type::jack: + return ClientID::jack(_jack_name.substr(0, _jack_name.find(':'))); + case Type::alsa: + return ClientID::alsa(_alsa_client); + } + + PATCHAGE_UNREACHABLE(); + } + + Type type() const { return _type; } + const std::string& jack_name() const { return _jack_name; } + uint8_t alsa_client() const { return _alsa_client; } + uint8_t alsa_port() const { return _alsa_port; } + bool alsa_is_input() const { return _alsa_is_input; } + +private: + PortID(const Type type, std::string jack_name) + : _type{type} + , _jack_name{std::move(jack_name)} + { + assert(_type == Type::jack); + assert(_jack_name.find(':') != std::string::npos); + assert(_jack_name.find(':') > 0); + assert(_jack_name.find(':') < _jack_name.length() - 1); + } + + PortID(const Type type, + const uint8_t alsa_client, + const uint8_t alsa_port, + const bool is_input) + : _type{type} + , _alsa_client{alsa_client} + , _alsa_port{alsa_port} + , _alsa_is_input{is_input} + { + assert(_type == Type::alsa); + } + + Type _type; ///< Determines which field is active + std::string _jack_name; ///< Full port name for Type::jack + uint8_t _alsa_client{}; ///< Client ID for Type::alsa + uint8_t _alsa_port{}; ///< Port ID for Type::alsa + bool _alsa_is_input{}; ///< Input flag for Type::alsa }; -static inline std::ostream& +inline std::ostream& operator<<(std::ostream& os, const PortID& id) { - switch (id.type) { - case PortID::NULL_PORT_ID: - return os << "(null)"; - case PortID::JACK_ID: -#ifdef PATCHAGE_LIBJACK - return os << "jack:" << id.id.jack_id; -#endif - break; - case PortID::ALSA_ADDR: -#ifdef HAVE_ALSA - return os << "alsa:" << (int)id.id.alsa_addr.client << ":" << (int)id.id.alsa_addr.port - << ":" << (id.id.is_input ? "in" : "out"); -#endif - break; - } - assert(false); - return os; + switch (id.type()) { + case PortID::Type::jack: + return os << "jack:" << id.jack_name(); + case PortID::Type::alsa: + return os << "alsa:" << int(id.alsa_client()) << ":" << int(id.alsa_port()) + << ":" << (id.alsa_is_input() ? "in" : "out"); + } + + assert(false); + return os; } -static inline bool -operator<(const PortID& a, const PortID& b) +inline bool +operator==(const PortID& lhs, const PortID& rhs) { - if (a.type != b.type) - return a.type < b.type; - - switch (a.type) { - case PortID::NULL_PORT_ID: - return true; - case PortID::JACK_ID: -#ifdef PATCHAGE_LIBJACK - return a.id.jack_id < b.id.jack_id; -#endif - break; - case PortID::ALSA_ADDR: -#ifdef HAVE_ALSA - if ((a.id.alsa_addr.client < b.id.alsa_addr.client) - || ((a.id.alsa_addr.client == b.id.alsa_addr.client) - && a.id.alsa_addr.port < b.id.alsa_addr.port)) { - return true; - } else if (a.id.alsa_addr.client == b.id.alsa_addr.client - && a.id.alsa_addr.port == b.id.alsa_addr.port) { - return (a.id.is_input < b.id.is_input); - } else { - return false; - } -#endif - break; - } - assert(false); - return false; + if (lhs.type() != rhs.type()) { + return false; + } + + switch (lhs.type()) { + case PortID::Type::jack: + return lhs.jack_name() == rhs.jack_name(); + case PortID::Type::alsa: + return std::make_tuple( + lhs.alsa_client(), lhs.alsa_port(), lhs.alsa_is_input()) == + std::make_tuple( + rhs.alsa_client(), rhs.alsa_port(), rhs.alsa_is_input()); + } + + assert(false); + return false; +} + +inline bool +operator<(const PortID& lhs, const PortID& rhs) +{ + if (lhs.type() != rhs.type()) { + return lhs.type() < rhs.type(); + } + + switch (lhs.type()) { + case PortID::Type::jack: + return lhs.jack_name() < rhs.jack_name(); + case PortID::Type::alsa: + return std::make_tuple( + lhs.alsa_client(), lhs.alsa_port(), lhs.alsa_is_input()) < + std::make_tuple( + rhs.alsa_client(), rhs.alsa_port(), rhs.alsa_is_input()); + } + + assert(false); + return false; } +} // namespace patchage + +namespace std { + +template<> +struct hash<patchage::PortID::Type> { + size_t operator()(const patchage::PortID::Type& v) const noexcept + { + return hash<unsigned>()(static_cast<unsigned>(v)); + } +}; + +} // namespace std + +template<> +struct fmt::formatter<patchage::PortID> : fmt::ostream_formatter {}; + #endif // PATCHAGE_PORTID_HPP diff --git a/src/PortInfo.hpp b/src/PortInfo.hpp new file mode 100644 index 0000000..b6d88b5 --- /dev/null +++ b/src/PortInfo.hpp @@ -0,0 +1,26 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_PORTINFO_HPP +#define PATCHAGE_PORTINFO_HPP + +#include "PortType.hpp" +#include "SignalDirection.hpp" + +#include <optional> +#include <string> + +namespace patchage { + +/// Extra information about a port not expressed in its ID +struct PortInfo { + std::string label; ///< Human-friendly label + PortType type; ///< Detailed port type + SignalDirection direction; ///< Signal direction + std::optional<int> order; ///< Order key on client + bool is_terminal; ///< True if this is a system port +}; + +} // namespace patchage + +#endif // PATCHAGE_PORTINFO_HPP diff --git a/src/PortNames.hpp b/src/PortNames.hpp new file mode 100644 index 0000000..4dce7c6 --- /dev/null +++ b/src/PortNames.hpp @@ -0,0 +1,44 @@ +// Copyright 2008-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_PORTNAMES_HPP +#define PATCHAGE_PORTNAMES_HPP + +#include "PortID.hpp" + +#include <cassert> +#include <string> + +namespace patchage { + +/// Utility class that splits a Jack port ID into client and client names +class PortNames +{ +public: + explicit PortNames(const std::string& jack_name) + { + const auto colon = jack_name.find(':'); + + if (colon != std::string::npos) { + _client_name = jack_name.substr(0, colon); + _port_name = jack_name.substr(colon + 1); + } + } + + explicit PortNames(const PortID& id) + : PortNames(id.jack_name()) + { + assert(id.type() == PortID::Type::jack); + } + + const std::string& client() const { return _client_name; } + const std::string& port() const { return _port_name; } + +private: + std::string _client_name; + std::string _port_name; +}; + +} // namespace patchage + +#endif // PATCHAGE_PORTNAMES_HPP diff --git a/src/PortType.hpp b/src/PortType.hpp new file mode 100644 index 0000000..97ecaab --- /dev/null +++ b/src/PortType.hpp @@ -0,0 +1,50 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_PORTTYPE_HPP +#define PATCHAGE_PORTTYPE_HPP + +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +#include <fmt/ostream.h> +PATCHAGE_RESTORE_WARNINGS + +#include <ostream> + +namespace patchage { + +enum class PortType { + jack_audio, + jack_midi, + alsa_midi, + jack_osc, + jack_cv, +}; + +inline std::ostream& +operator<<(std::ostream& os, const PortType port_type) +{ + switch (port_type) { + case PortType::jack_audio: + return os << "JACK audio"; + case PortType::jack_midi: + return os << "JACK MIDI"; + case PortType::alsa_midi: + return os << "ALSA MIDI"; + case PortType::jack_osc: + return os << "JACK OSC"; + case PortType::jack_cv: + return os << "JACK CV"; + } + + PATCHAGE_UNREACHABLE(); +} + +} // namespace patchage + +template<> +struct fmt::formatter<patchage::PortType> : fmt::ostream_formatter {}; + +#endif // PATCHAGE_PORTTYPE_HPP diff --git a/src/Queue.hpp b/src/Queue.hpp deleted file mode 100644 index ab47aed..0000000 --- a/src/Queue.hpp +++ /dev/null @@ -1,131 +0,0 @@ -/* This file is part of Patchage. - * Copyright 2007-2017 David Robillard <http://drobilla.net> - * - * 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 3 of the License, or 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 - * Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef QUEUE_HPP_INCLUDED -#define QUEUE_HPP_INCLUDED - -#include <atomic> -#include <cassert> - -/** Realtime-safe single-reader single-writer queue */ -template <typename T> -class Queue -{ -public: - /** @param size Size in number of elements */ - explicit Queue(size_t size); - ~Queue(); - - // Any thread: - - inline size_t capacity() const { return _size - 1; } - - // Write thread(s): - - inline bool full() const; - inline bool push(const T& obj); - - // Read thread: - - inline bool empty() const; - inline T& front() const; - inline void pop(); - -private: - std::atomic<size_t> _front; ///< Index to front of queue - std::atomic<size_t> _back; ///< Index to back of queue (one past end) - const size_t _size; ///< Size of `_objects` (at most _size-1) - T* const _objects; ///< Fixed array containing queued elements -}; - -template<typename T> -Queue<T>::Queue(size_t size) - : _front(0) - , _back(0) - , _size(size + 1) - , _objects(new T[_size]) -{ - assert(size > 1); -} - -template <typename T> -Queue<T>::~Queue() -{ - delete[] _objects; -} - -/** Return whether or not the queue is empty. - */ -template <typename T> -inline bool -Queue<T>::empty() const -{ - return (_back.load() == _front.load()); -} - -/** Return whether or not the queue is full. - */ -template <typename T> -inline bool -Queue<T>::full() const -{ - return (((_front.load() - _back.load() + _size) % _size) == 1); -} - -/** Return the element at the front of the queue without removing it - */ -template <typename T> -inline T& -Queue<T>::front() const -{ - return _objects[_front.load()]; -} - -/** Push an item onto the back of the Queue - realtime-safe, not thread-safe. - * - * @returns true if `elem` was successfully pushed onto the queue, - * false otherwise (queue is full). - */ -template <typename T> -inline bool -Queue<T>::push(const T& elem) -{ - if (full()) { - return false; - } else { - unsigned back = _back.load(); - _objects[back] = elem; - _back = (back + 1) % _size; - return true; - } -} - -/** Pop an item off the front of the queue - realtime-safe, not thread-safe. - * - * It is a fatal error to call pop() when the queue is empty. - * - * @returns the element popped. - */ -template <typename T> -inline void -Queue<T>::pop() -{ - assert(!empty()); - assert(_size > 0); - - _front = (_front.load() + 1) % (_size); -} - -#endif // QUEUE_HPP_INCLUDED diff --git a/src/Reactor.cpp b/src/Reactor.cpp new file mode 100644 index 0000000..49cbe5a --- /dev/null +++ b/src/Reactor.cpp @@ -0,0 +1,216 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Reactor.hpp" + +#include "Action.hpp" +#include "Canvas.hpp" +#include "CanvasModule.hpp" +#include "CanvasPort.hpp" +#include "ClientType.hpp" +#include "Configuration.hpp" +#include "Driver.hpp" +#include "Drivers.hpp" +#include "ILog.hpp" +#include "PortID.hpp" +#include "Setting.hpp" +#include "SignalDirection.hpp" +#include "warnings.hpp" + +#include "ganv/Port.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS + +#include <variant> + +namespace patchage { + +class SettingVisitor +{ +public: + explicit SettingVisitor(Configuration& conf) + : _conf{conf} + {} + + template<class S> + void operator()(const S& setting) const + { + _conf.set_setting(setting); + } + +private: + Configuration& _conf; +}; + +Reactor::Reactor(Configuration& conf, + Drivers& drivers, + Canvas& canvas, + ILog& log) + : _conf{conf} + , _drivers{drivers} + , _canvas{canvas} + , _log{log} +{} + +void +Reactor::operator()(const action::ChangeSetting& action) +{ + const SettingVisitor visitor{_conf}; + std::visit(visitor, action.setting); +} + +void +Reactor::operator()(const action::ConnectPorts& action) +{ + if (action.tail.type() == action.head.type()) { + if (auto* d = _drivers.driver(action.tail.type())) { + d->connect(action.tail, action.head); + } else { + _log.error(fmt::format("No driver for {}", action.tail.type())); + } + } else { + _log.warning("Unable to connect incompatible port"); + } +} + +void +Reactor::operator()(const action::DecreaseFontSize&) +{ + _conf.set<setting::FontSize>(_conf.get<setting::FontSize>() - 1.0f); +} + +void +Reactor::operator()(const action::DisconnectClient& action) +{ + if (CanvasModule* mod = find_module(action.client, action.direction)) { + for (Ganv::Port* p : *mod) { + if (p) { + p->disconnect(); + } + } + } +} + +void +Reactor::operator()(const action::DisconnectPort& action) +{ + if (CanvasPort* port = find_port(action.port)) { + port->disconnect(); + } +} + +void +Reactor::operator()(const action::DisconnectPorts& action) +{ + if (action.tail.type() == action.head.type()) { + if (auto* d = _drivers.driver(action.tail.type())) { + d->disconnect(action.tail, action.head); + } else { + _log.error(fmt::format("No driver available to disconnect ports")); + } + } else { + _log.error("Unable to disconnect incompatible ports"); + } +} + +void +Reactor::operator()(const action::IncreaseFontSize&) +{ + _conf.set<setting::FontSize>(_conf.get<setting::FontSize>() + 1.0f); +} + +void +Reactor::operator()(const action::MoveModule& action) +{ + _conf.set_module_location( + module_name(action.client), action.direction, {action.x, action.y}); +} + +void +Reactor::operator()(const action::Refresh&) +{ + _drivers.refresh(); +} + +void +Reactor::operator()(const action::ResetFontSize&) +{ + _conf.set<setting::FontSize>(_canvas.get_default_font_size()); +} + +void +Reactor::operator()(const action::SplitModule& action) +{ + _conf.set_module_split(module_name(action.client), true); + _drivers.refresh(); +} + +void +Reactor::operator()(const action::UnsplitModule& action) +{ + _conf.set_module_split(module_name(action.client), false); + _drivers.refresh(); +} + +void +Reactor::operator()(const action::ZoomFull&) +{ + _canvas.zoom_full(); + _conf.set<setting::Zoom>(_canvas.get_zoom()); +} + +void +Reactor::operator()(const action::ZoomIn&) +{ + _conf.set<setting::Zoom>(_conf.get<setting::Zoom>() * 1.25f); +} + +void +Reactor::operator()(const action::ZoomNormal&) +{ + _conf.set<setting::Zoom>(1.0); +} + +void +Reactor::operator()(const action::ZoomOut&) +{ + _conf.set<setting::Zoom>(_conf.get<setting::Zoom>() * 0.75f); +} + +void +Reactor::operator()(const Action& action) +{ + std::visit(*this, action); +} + +std::string +Reactor::module_name(const ClientID& client) +{ + // Note that split modules always have the same name + + if (CanvasModule* mod = find_module(client, SignalDirection::input)) { + return mod->name(); + } + + if (CanvasModule* mod = find_module(client, SignalDirection::output)) { + return mod->name(); + } + + return std::string{}; +} + +CanvasModule* +Reactor::find_module(const ClientID& client, const SignalDirection type) +{ + return _canvas.find_module(client, type); +} + +CanvasPort* +Reactor::find_port(const PortID& port) +{ + return _canvas.find_port(port); +} + +} // namespace patchage diff --git a/src/Reactor.hpp b/src/Reactor.hpp new file mode 100644 index 0000000..a5bc9e9 --- /dev/null +++ b/src/Reactor.hpp @@ -0,0 +1,75 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_REACTOR_HPP +#define PATCHAGE_REACTOR_HPP + +#include "Action.hpp" + +#include <string> + +namespace patchage { + +enum class SignalDirection; + +struct ClientID; +struct PortID; + +class Canvas; +class CanvasModule; +class CanvasPort; +class Configuration; +class Drivers; +class ILog; + +/// Reacts to actions from the user +class Reactor +{ +public: + explicit Reactor(Configuration& conf, + Drivers& drivers, + Canvas& canvas, + ILog& log); + + Reactor(const Reactor&) = delete; + Reactor& operator=(const Reactor&) = delete; + + Reactor(Reactor&&) = delete; + Reactor& operator=(Reactor&&) = delete; + + ~Reactor() = default; + + void operator()(const action::ChangeSetting& action); + void operator()(const action::ConnectPorts& action); + void operator()(const action::DecreaseFontSize& action); + void operator()(const action::DisconnectClient& action); + void operator()(const action::DisconnectPort& action); + void operator()(const action::DisconnectPorts& action); + void operator()(const action::IncreaseFontSize& action); + void operator()(const action::MoveModule& action); + void operator()(const action::Refresh& action); + void operator()(const action::ResetFontSize& action); + void operator()(const action::SplitModule& action); + void operator()(const action::UnsplitModule& action); + void operator()(const action::ZoomFull& action); + void operator()(const action::ZoomIn& action); + void operator()(const action::ZoomNormal& action); + void operator()(const action::ZoomOut& action); + + void operator()(const Action& action); + +private: + std::string module_name(const ClientID& client); + + CanvasModule* find_module(const ClientID& client, SignalDirection type); + CanvasPort* find_port(const PortID& port); + + Configuration& _conf; + Drivers& _drivers; + Canvas& _canvas; + ILog& _log; +}; + +} // namespace patchage + +#endif // PATCHAGE_REACTOR_HPP diff --git a/src/Setting.hpp b/src/Setting.hpp new file mode 100644 index 0000000..4bcfc81 --- /dev/null +++ b/src/Setting.hpp @@ -0,0 +1,88 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_SETTING_HPP +#define PATCHAGE_SETTING_HPP + +#include "Coord.hpp" +#include "PortType.hpp" + +#include <cstdint> +#include <variant> + +namespace patchage { +namespace setting { + +struct AlsaAttached { + bool value{}; +}; + +struct FontSize { + float value{}; +}; + +struct HumanNames { + bool value{}; +}; + +struct JackAttached { + bool value{}; +}; + +struct MessagesHeight { + int value{}; +}; + +struct MessagesVisible { + bool value{}; +}; + +struct PortColor { + PortType type{}; + uint32_t color{}; +}; + +struct SortedPorts { + bool value{}; +}; + +struct SprungLayout { + bool value{}; +}; + +struct ToolbarVisible { + bool value{}; +}; + +struct WindowLocation { + Coord value{}; +}; + +struct WindowSize { + Coord value{}; +}; + +struct Zoom { + float value{}; +}; + +} // namespace setting + +/// A configuration setting +using Setting = std::variant<setting::AlsaAttached, + setting::FontSize, + setting::HumanNames, + setting::JackAttached, + setting::MessagesHeight, + setting::MessagesVisible, + setting::PortColor, + setting::SortedPorts, + setting::SprungLayout, + setting::ToolbarVisible, + setting::WindowLocation, + setting::WindowSize, + setting::Zoom>; + +} // namespace patchage + +#endif // PATCHAGE_SETTING_HPP diff --git a/src/SignalDirection.hpp b/src/SignalDirection.hpp new file mode 100644 index 0000000..84c3cc2 --- /dev/null +++ b/src/SignalDirection.hpp @@ -0,0 +1,44 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_SIGNALDIRECTION_HPP +#define PATCHAGE_SIGNALDIRECTION_HPP + +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +#include <fmt/ostream.h> +PATCHAGE_RESTORE_WARNINGS + +#include <ostream> + +namespace patchage { + +enum class SignalDirection { + input, + output, + duplex, +}; + +inline std::ostream& +operator<<(std::ostream& os, const SignalDirection direction) +{ + switch (direction) { + case SignalDirection::input: + return os << "input"; + case SignalDirection::output: + return os << "output"; + case SignalDirection::duplex: + return os << "duplex"; + } + + PATCHAGE_UNREACHABLE(); +} + +} // namespace patchage + +template<> +struct fmt::formatter<patchage::SignalDirection> : fmt::ostream_formatter {}; + +#endif // PATCHAGE_SIGNALDIRECTION_HPP diff --git a/src/TextViewLog.cpp b/src/TextViewLog.cpp new file mode 100644 index 0000000..712b760 --- /dev/null +++ b/src/TextViewLog.cpp @@ -0,0 +1,81 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "TextViewLog.hpp" + +#include "Widget.hpp" + +#include <gdkmm/color.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> +#include <gtkmm/enums.h> +#include <gtkmm/textbuffer.h> +#include <gtkmm/texttag.h> +#include <gtkmm/texttagtable.h> +#include <gtkmm/textview.h> + +#include <string> + +namespace patchage { + +TextViewLog::TextViewLog(Widget<Gtk::TextView>& text_view) + : _error_tag{Gtk::TextTag::create()} + , _warning_tag{Gtk::TextTag::create()} + , _text_view{text_view} +{ + for (int s = Gtk::STATE_NORMAL; s <= Gtk::STATE_INSENSITIVE; ++s) { + _text_view->modify_base(static_cast<Gtk::StateType>(s), + Gdk::Color("#000000")); + _text_view->modify_text(static_cast<Gtk::StateType>(s), + Gdk::Color("#FFFFFF")); + } + + _error_tag->property_foreground() = "#CC0000"; + _text_view->get_buffer()->get_tag_table()->add(_error_tag); + + _warning_tag->property_foreground() = "#C4A000"; + _text_view->get_buffer()->get_tag_table()->add(_warning_tag); + + _text_view->set_pixels_inside_wrap(2); + _text_view->set_left_margin(4); + _text_view->set_right_margin(4); + _text_view->set_pixels_below_lines(2); +} + +void +TextViewLog::info(const std::string& msg) +{ + const Glib::RefPtr<Gtk::TextBuffer> buffer = _text_view->get_buffer(); + buffer->insert(buffer->end(), std::string("\n") + msg); + _text_view->scroll_to_mark(buffer->get_insert(), 0); +} + +void +TextViewLog::warning(const std::string& msg) +{ + const Glib::RefPtr<Gtk::TextBuffer> buffer = _text_view->get_buffer(); + buffer->insert_with_tag(buffer->end(), std::string("\n") + msg, _warning_tag); + _text_view->scroll_to_mark(buffer->get_insert(), 0); +} + +void +TextViewLog::error(const std::string& msg) +{ + const Glib::RefPtr<Gtk::TextBuffer> buffer = _text_view->get_buffer(); + buffer->insert_with_tag(buffer->end(), std::string("\n") + msg, _error_tag); + _text_view->scroll_to_mark(buffer->get_insert(), 0); +} + +int +TextViewLog::min_height() const +{ + const Glib::RefPtr<Gtk::TextBuffer> buffer = _text_view->get_buffer(); + + int y = 0; + int line_height = 0; + _text_view->get_line_yrange(buffer->begin(), y, line_height); + + return line_height + 2 * _text_view->get_pixels_inside_wrap(); +} + +} // namespace patchage diff --git a/src/TextViewLog.hpp b/src/TextViewLog.hpp new file mode 100644 index 0000000..40d0782 --- /dev/null +++ b/src/TextViewLog.hpp @@ -0,0 +1,54 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_TEXTVIEWLOG_HPP +#define PATCHAGE_TEXTVIEWLOG_HPP + +#include "ILog.hpp" + +#include <glibmm/refptr.h> +#include <gtkmm/texttag.h> + +#include <string> + +namespace Gtk { +class TextView; +} // namespace Gtk + +namespace patchage { + +template<typename W> +class Widget; + +/// Log that writes colored messages to a Gtk TextView +class TextViewLog : public ILog +{ +public: + explicit TextViewLog(Widget<Gtk::TextView>& text_view); + + TextViewLog(const TextViewLog&) = delete; + TextViewLog& operator=(const TextViewLog&) = delete; + + TextViewLog(TextViewLog&&) = delete; + TextViewLog& operator=(TextViewLog&&) = delete; + + ~TextViewLog() override = default; + + void info(const std::string& msg) override; + void error(const std::string& msg) override; + void warning(const std::string& msg) override; + + int min_height() const; + + const Widget<Gtk::TextView>& text_view() const { return _text_view; } + Widget<Gtk::TextView>& text_view() { return _text_view; } + +private: + Glib::RefPtr<Gtk::TextTag> _error_tag; + Glib::RefPtr<Gtk::TextTag> _warning_tag; + Widget<Gtk::TextView>& _text_view; +}; + +} // namespace patchage + +#endif // PATCHAGE_TEXTVIEWLOG_HPP diff --git a/src/UIFile.hpp b/src/UIFile.hpp index f1ab5f8..36be144 100644 --- a/src/UIFile.hpp +++ b/src/UIFile.hpp @@ -1,66 +1,65 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later -#ifndef PATCHAGE_GLADEFILE_HPP -#define PATCHAGE_GLADEFILE_HPP +#ifndef PATCHAGE_UIFILE_HPP +#define PATCHAGE_UIFILE_HPP + +#include "patchage_config.h" + +#if PATCHAGE_BUNDLED +# include "binary_location.h" +#endif + +#include <glibmm/refptr.h> +#include <gtkmm/builder.h> #include <fstream> #include <iostream> #include <sstream> +#include <stdexcept> #include <string> -#include <gtkmm/builder.h> +namespace patchage { -#include "patchage_config.h" -#ifdef PATCHAGE_BINLOC -#include "binary_location.h" -#endif - -class UIFile { +class UIFile +{ public: - inline static bool is_readable(const std::string& filename) { - std::ifstream fs(filename.c_str()); - const bool fail = fs.fail(); - fs.close(); - return !fail; - } + inline static bool is_readable(const std::string& filename) + { + std::ifstream fs(filename.c_str()); + const bool fail = fs.fail(); + fs.close(); + return !fail; + } - static Glib::RefPtr<Gtk::Builder> open(const std::string& base_name) { - std::string ui_filename; -#ifdef PATCHAGE_BINLOC - const std::string bundle = bundle_location(); - if (!bundle.empty()) { - ui_filename = bundle + "/" + base_name + ".ui"; - if (is_readable(ui_filename)) { - std::cout << "Loading UI file " << ui_filename << std::endl; - return Gtk::Builder::create_from_file(ui_filename); - } - } + static Glib::RefPtr<Gtk::Builder> open(const std::string& base_name) + { + std::string ui_filename = base_name + ".ui"; + +#if PATCHAGE_BUNDLED + const std::string bundle = bundle_location(); + if (!bundle.empty()) { + const std::string bundle_ui_filename = bundle + "/" + ui_filename; + if (is_readable(bundle_ui_filename)) { + std::cout << "Loading UI file " << bundle_ui_filename << std::endl; + return Gtk::Builder::create_from_file(bundle_ui_filename); + } + } #endif - ui_filename = std::string(PATCHAGE_DATA_DIR) + "/" + base_name + ".ui"; - if (is_readable(ui_filename)) { - std::cout << "Loading UI file " << ui_filename << std::endl; - return Gtk::Builder::create_from_file(ui_filename); - } - std::stringstream ss; - ss << "Unable to find " << base_name << std::endl; - throw std::runtime_error(ss.str()); - return Glib::RefPtr<Gtk::Builder>(); - } + ui_filename = std::string(PATCHAGE_DATA_DIR) + "/" + ui_filename; + if (is_readable(ui_filename)) { + std::cout << "Loading UI file " << ui_filename << std::endl; + return Gtk::Builder::create_from_file(ui_filename); + } + + std::stringstream ss; + ss << "Unable to find " << ui_filename << std::endl; + throw std::runtime_error(ss.str()); + return {}; + } }; -#endif // PATCHAGE_GLADEFILE_HPP +} // namespace patchage + +#endif // PATCHAGE_UIFILE_HPP diff --git a/src/Widget.hpp b/src/Widget.hpp index 038f880..1c3b6ee 100644 --- a/src/Widget.hpp +++ b/src/Widget.hpp @@ -1,46 +1,46 @@ -/* This file is part of Patchage - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifndef PATCHAGE_WIDGET_HPP #define PATCHAGE_WIDGET_HPP -#include <string> +#include <glibmm/refptr.h> +#include <gtkmm/builder.h> // IWYU pragma: keep -#include <boost/utility.hpp> +#include <string> -#include <gtkmm/builder.h> +namespace patchage { -template <typename W> -class Widget : public boost::noncopyable { +template<typename W> +class Widget +{ public: - Widget(Glib::RefPtr<Gtk::Builder> xml, const std::string& name) { - xml->get_widget(name, _me); - } + Widget(const Glib::RefPtr<Gtk::Builder>& xml, const std::string& name) + { + xml->get_widget(name, _me); + } + + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; - void destroy() { delete _me; } + Widget(Widget&&) = delete; + Widget& operator=(Widget&&) = delete; - W* get() { return _me; } - const W* get() const { return _me; } - W* operator->() { return _me; } - const W* operator->() const { return _me; } - W& operator*() { return *_me; } - const W& operator*() const { return *_me; } + ~Widget() = default; + + void destroy() { delete _me; } + + W* get() { return _me; } + const W* get() const { return _me; } + W* operator->() { return _me; } + const W* operator->() const { return _me; } + W& operator*() { return *_me; } + const W& operator*() const { return *_me; } private: - W* _me; + W* _me; }; +} // namespace patchage + #endif // PATCHAGE_WIDGET_HPP diff --git a/src/binary_location.h b/src/binary_location.h index 303a3bd..12f726e 100644 --- a/src/binary_location.h +++ b/src/binary_location.h @@ -1,54 +1,43 @@ -/* This file is part of Patchage. - * Copyright 2008-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef _GNU_SOURCE - #define _GNU_SOURCE -#endif - -#include <assert.h> -#include <limits.h> -#include <stdlib.h> +// Copyright 2008-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + #include <dlfcn.h> +#include <climits> +#include <cstdlib> #include <string> +namespace patchage { + /** Return the absolute path of the binary. */ -static std::string +inline std::string binary_location() { - Dl_info dli; - std::string loc; - const int ret = dladdr((void*)&binary_location, &dli); - if (ret) { - char* const bin_loc = (char*)calloc(PATH_MAX, 1); - if (realpath(dli.dli_fname, bin_loc)) { - loc = bin_loc; - } - free(bin_loc); - } - return loc; + Dl_info dli = {}; + const int ret = dladdr(reinterpret_cast<void*>(&binary_location), &dli); + if (!ret) { + return ""; + } + + char* const bin_loc = realpath(dli.dli_fname, nullptr); + if (!bin_loc) { + return ""; + } + + std::string loc{bin_loc}; + free(bin_loc); + return loc; } /** Return the absolute path of the bundle (binary parent directory). */ -static std::string +inline std::string bundle_location() { - const std::string binary = binary_location(); - if (binary.empty()) { - return ""; - } - return binary.substr(0, binary.find_last_of('/')); + const std::string binary = binary_location(); + if (binary.empty()) { + return ""; + } + return binary.substr(0, binary.find_last_of('/')); } + +} // namespace patchage diff --git a/src/event_to_string.cpp b/src/event_to_string.cpp new file mode 100644 index 0000000..499966a --- /dev/null +++ b/src/event_to_string.cpp @@ -0,0 +1,110 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "event_to_string.hpp" + +#include "ClientID.hpp" +#include "ClientInfo.hpp" +#include "ClientType.hpp" +#include "Event.hpp" +#include "PortID.hpp" +#include "PortInfo.hpp" +#include "PortType.hpp" +#include "SignalDirection.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS + +#include <optional> +#include <string> +#include <variant> + +namespace patchage { + +namespace { + +struct EventPrinter { + std::string operator()(const ClientType type) + { + switch (type) { + case ClientType::jack: + return "JACK"; + case ClientType::alsa: + return "ALSA"; + } + + PATCHAGE_UNREACHABLE(); + } + + std::string operator()(const event::Cleared&) { return "Cleared"; } + + std::string operator()(const event::DriverAttached& event) + { + return fmt::format("Attached to {}", (*this)(event.type)); + } + + std::string operator()(const event::DriverDetached& event) + { + return fmt::format("Detached from {}", (*this)(event.type)); + } + + std::string operator()(const event::ClientCreated& event) + { + return fmt::format(R"(Add client "{}" ("{}"))", event.id, event.info.label); + } + + std::string operator()(const event::ClientDestroyed& event) + { + return fmt::format(R"(Remove client "{}")", event.id); + } + + std::string operator()(const event::PortCreated& event) + { + auto result = fmt::format(R"(Add {}{} {} "{}" ("{}"))", + event.info.type, + event.info.is_terminal ? " terminal" : "", + event.info.direction, + event.id, + event.info.label); + + if (event.info.order) { + result += fmt::format(" order: {}", *event.info.order); + } + + return result; + } + + std::string operator()(const event::PortDestroyed& event) + { + return fmt::format(R"(Remove port "{}")", event.id); + } + + std::string operator()(const event::PortsConnected& event) + { + return fmt::format(R"(Connect "{}" to "{}")", event.tail, event.head); + } + + std::string operator()(const event::PortsDisconnected& event) + { + return fmt::format(R"(Disconnect "{}" from "{}")", event.tail, event.head); + } +}; + +} // namespace + +std::string +event_to_string(const Event& event) +{ + EventPrinter printer; + return std::visit(printer, event); +} + +std::ostream& +operator<<(std::ostream& os, const Event& event) +{ + return os << event_to_string(event); +} + +} // namespace patchage diff --git a/src/event_to_string.hpp b/src/event_to_string.hpp new file mode 100644 index 0000000..926065c --- /dev/null +++ b/src/event_to_string.hpp @@ -0,0 +1,22 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_EVENT_TO_STRING_HPP +#define PATCHAGE_EVENT_TO_STRING_HPP + +#include "Event.hpp" + +#include <iosfwd> +#include <string> + +namespace patchage { + +std::string +event_to_string(const Event& event); + +std::ostream& +operator<<(std::ostream& os, const Event& event); + +} // namespace patchage + +#endif // PATCHAGE_EVENT_TO_STRING_HPP diff --git a/src/handle_event.cpp b/src/handle_event.cpp new file mode 100644 index 0000000..d75c42d --- /dev/null +++ b/src/handle_event.cpp @@ -0,0 +1,149 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "handle_event.hpp" + +#include "Canvas.hpp" +#include "CanvasPort.hpp" +#include "ClientType.hpp" +#include "Configuration.hpp" +#include "Event.hpp" +#include "ILog.hpp" +#include "Metadata.hpp" +#include "PortID.hpp" +#include "Setting.hpp" +#include "warnings.hpp" + +PATCHAGE_DISABLE_FMT_WARNINGS +#include <fmt/core.h> +PATCHAGE_RESTORE_WARNINGS + +#include <variant> + +namespace patchage { + +namespace { + +class EventHandler +{ +public: + explicit EventHandler(Configuration& conf, + Metadata& metadata, + Canvas& canvas, + ILog& log) + : _conf{conf} + , _metadata{metadata} + , _canvas{canvas} + , _log{log} + {} + + void operator()(const event::Cleared&) { _canvas.clear(); } + + void operator()(const event::DriverAttached& event) + { + switch (event.type) { + case ClientType::alsa: + _conf.set<setting::AlsaAttached>(true); + break; + case ClientType::jack: + _conf.set<setting::JackAttached>(true); + break; + } + } + + void operator()(const event::DriverDetached& event) + { + switch (event.type) { + case ClientType::alsa: + _conf.set<setting::AlsaAttached>(false); + break; + case ClientType::jack: + _conf.set<setting::JackAttached>(false); + break; + } + } + + void operator()(const event::ClientCreated& event) + { + // Don't create empty modules, they will be created when ports are added + _metadata.set_client(event.id, event.info); + } + + void operator()(const event::ClientDestroyed& event) + { + _canvas.remove_module(event.id); + _metadata.erase_client(event.id); + } + + void operator()(const event::PortCreated& event) + { + _metadata.set_port(event.id, event.info); + + auto* const port = + _canvas.create_port(_conf, _metadata, event.id, event.info); + + if (!port) { + _log.error( + fmt::format("Unable to create view for port \"{}\"", event.id)); + } + } + + void operator()(const event::PortDestroyed& event) + { + _canvas.remove_port(event.id); + _metadata.erase_port(event.id); + } + + void operator()(const event::PortsConnected& event) + { + CanvasPort* port_1 = _canvas.find_port(event.tail); + CanvasPort* port_2 = _canvas.find_port(event.head); + + if (!port_1) { + _log.error( + fmt::format("Unable to find port \"{}\" to connect", event.tail)); + } else if (!port_2) { + _log.error( + fmt::format("Unable to find port \"{}\" to connect", event.head)); + } else { + _canvas.make_connection(port_1, port_2); + } + } + + void operator()(const event::PortsDisconnected& event) + { + CanvasPort* port_1 = _canvas.find_port(event.tail); + CanvasPort* port_2 = _canvas.find_port(event.head); + + if (!port_1) { + _log.error( + fmt::format("Unable to find port \"{}\" to disconnect", event.tail)); + } else if (!port_2) { + _log.error( + fmt::format("Unable to find port \"{}\" to disconnect", event.head)); + } else { + _canvas.remove_edge_between(port_1, port_2); + } + } + +private: + Configuration& _conf; + Metadata& _metadata; + Canvas& _canvas; + ILog& _log; +}; + +} // namespace + +void +handle_event(Configuration& conf, + Metadata& metadata, + Canvas& canvas, + ILog& log, + const Event& event) +{ + EventHandler handler{conf, metadata, canvas, log}; + std::visit(handler, event); +} + +} // namespace patchage diff --git a/src/handle_event.hpp b/src/handle_event.hpp new file mode 100644 index 0000000..fae6d78 --- /dev/null +++ b/src/handle_event.hpp @@ -0,0 +1,26 @@ +// Copyright 2007-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_HANDLE_EVENT_HPP +#define PATCHAGE_HANDLE_EVENT_HPP + +#include "Event.hpp" + +namespace patchage { + +class Configuration; +class Metadata; +class Canvas; +class ILog; + +/// Handle an event from the system by updating the GUI as necessary +void +handle_event(Configuration& conf, + Metadata& metadata, + Canvas& canvas, + ILog& log, + const Event& event); + +} // namespace patchage + +#endif // PATCHAGE_HANDLE_EVENT_HPP diff --git a/src/i18n.hpp b/src/i18n.hpp new file mode 100644 index 0000000..ebf8fd1 --- /dev/null +++ b/src/i18n.hpp @@ -0,0 +1,12 @@ +// Copyright 2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_I18N_HPP +#define PATCHAGE_I18N_HPP + +#include <libintl.h> + +/// Mark a string literal as translatable +#define T(msgid) gettext(msgid) + +#endif // PATCHAGE_I18N_HPP diff --git a/src/jackey.h b/src/jackey.h index 02a7735..607eadb 100644 --- a/src/jackey.h +++ b/src/jackey.h @@ -1,18 +1,8 @@ -/* - Copyright 2014 David Robillard <http://drobilla.net> +// Copyright 2014-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ +#ifndef JACKEY_H +#define JACKEY_H /** The supported event types of an event port. @@ -70,3 +60,5 @@ other relevance to order values. */ #define JACKEY_ORDER "http://jackaudio.org/metadata/order" + +#endif // JACKEY_H diff --git a/src/main.cpp b/src/main.cpp index 4822d3d..6d09fe4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,93 +1,159 @@ -/* This file is part of Patchage. - * Copyright 2007-2014 David Robillard <http://drobilla.net> - * - * 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 3 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 Patchage. If not, see <http://www.gnu.org/licenses/>. - */ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later #ifdef __APPLE__ -#include <stdlib.h> -#include <unistd.h> -#include <string> -#include <gtk/gtkrc.h> -#include "binary_location.h" +# include "binary_location.h" + +# include <gtk/gtkrc.h> + +# include <unistd.h> + +# include <cstdlib> +# include <string> #endif -#include <iostream> +#include "Options.hpp" +#include "Patchage.hpp" +#include "patchage_config.h" #include <glibmm/exception.h> +#include <glibmm/thread.h> +#include <glibmm/ustring.h> +#include <gtkmm/main.h> -#include "Patchage.hpp" +#if USE_GETTEXT +# include <libintl.h> + +# include <clocale> +#endif + +#include <cstring> +#include <exception> +#include <iostream> + +namespace { #ifdef __APPLE__ void set_bundle_environment() { - const std::string bundle = bundle_location(); - const std::string lib_path = bundle + "/lib"; - if (!Glib::file_test(lib_path, Glib::FILE_TEST_EXISTS)) { - // If lib does not exist, we have not been bundleified, do nothing - return; - } - - setenv("GTK_PATH", lib_path.c_str(), 1); - setenv("DYLD_LIBRARY_PATH", lib_path.c_str(), 1); - - const std::string pangorc_path(bundle + "/Resources/pangorc"); - if (Glib::file_test(pangorc_path, Glib::FILE_TEST_EXISTS)) { - setenv("PANGO_RC_FILE", pangorc_path.c_str(), 1); - } - - const std::string fonts_conf_path(bundle + "/Resources/fonts.conf"); - if (Glib::file_test(fonts_conf_path, Glib::FILE_TEST_EXISTS)) { - setenv("FONTCONFIG_FILE", fonts_conf_path.c_str(), 1); - } - - const std::string loaders_cache_path(bundle + "/Resources/loaders.cache"); - if (Glib::file_test(loaders_cache_path, Glib::FILE_TEST_EXISTS)) { - setenv("GDK_PIXBUF_MODULE_FILE", loaders_cache_path.c_str(), 1); - } - - const std::string gtkrc_path(bundle + "/Resources/gtkrc"); - if (Glib::file_test(gtkrc_path, Glib::FILE_TEST_EXISTS)) { - gtk_rc_parse(gtkrc_path.c_str()); - } + const std::string bundle = patchage::bundle_location(); + const std::string lib_path = bundle + "/lib"; + if (!Glib::file_test(lib_path, Glib::FILE_TEST_EXISTS)) { + // If lib does not exist, we have not been bundleified, do nothing + return; + } + + setenv("GTK_PATH", lib_path.c_str(), 1); + setenv("DYLD_LIBRARY_PATH", lib_path.c_str(), 1); + + const std::string pangorc_path(bundle + "/Resources/pangorc"); + if (Glib::file_test(pangorc_path, Glib::FILE_TEST_EXISTS)) { + setenv("PANGO_RC_FILE", pangorc_path.c_str(), 1); + } + + const std::string fonts_conf_path(bundle + "/Resources/fonts.conf"); + if (Glib::file_test(fonts_conf_path, Glib::FILE_TEST_EXISTS)) { + setenv("FONTCONFIG_FILE", fonts_conf_path.c_str(), 1); + } + + const std::string loaders_cache_path(bundle + "/Resources/loaders.cache"); + if (Glib::file_test(loaders_cache_path, Glib::FILE_TEST_EXISTS)) { + setenv("GDK_PIXBUF_MODULE_FILE", loaders_cache_path.c_str(), 1); + } + + const std::string gtkrc_path(bundle + "/Resources/gtkrc"); + if (Glib::file_test(gtkrc_path, Glib::FILE_TEST_EXISTS)) { + gtk_rc_parse(gtkrc_path.c_str()); + } } #endif +void +print_usage() +{ + std::cout << "Usage: patchage [OPTION]...\n"; + std::cout << "Visually connect JACK and ALSA Audio and MIDI ports.\n\n"; + std::cout << "Options:\n"; + std::cout << " -h, --help Display this help and exit.\n"; + std::cout << " -A, --no-alsa Do not automatically attach to ALSA.\n"; + std::cout << " -J, --no-jack Do not automatically attack to JACK.\n"; +} + +void +print_version() +{ + std::cout << "Patchage " PATCHAGE_VERSION << R"( +Copyright 2007-2022 David Robillard <d@drobilla.net>. +License GPLv3+: <http://gnu.org/licenses/gpl.html>. +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +)"; +} + +} // namespace + int main(int argc, char** argv) { #ifdef __APPLE__ - set_bundle_environment(); + set_bundle_environment(); #endif - try { - - Glib::thread_init(); +#if USE_GETTEXT + if (!setlocale(LC_ALL, "")) { + std::cerr << "patchage: failed to set locale\n"; + } - Gtk::Main app(argc, argv); - - Patchage patchage(argc, argv); - app.run(*patchage.window()); - patchage.save(); - - } catch (std::exception& e) { - std::cerr << "patchage: error: " << e.what() << std::endl; - return 1; - } catch (Glib::Exception& e) { - std::cerr << "patchage: error: " << e.what() << std::endl; - return 1; - } + bindtextdomain("patchage", PATCHAGE_LOCALE_DIR); + bind_textdomain_codeset("patchage", "UTF-8"); + textdomain("patchage"); +#endif - return 0; + try { + Glib::thread_init(); + + const Gtk::Main app(argc, argv); + ++argv; + --argc; + + // Parse command line options + patchage::Options options; + while (argc > 0) { + if (!strcmp(*argv, "-h") || !strcmp(*argv, "--help")) { + print_usage(); + return 0; + } + + if (!strcmp(*argv, "-A") || !strcmp(*argv, "--no-alsa")) { + options.alsa_driver_autoattach = false; + } else if (!strcmp(*argv, "-J") || !strcmp(*argv, "--no-jack")) { + options.jack_driver_autoattach = false; + } else if (!strcmp(*argv, "-V") || !strcmp(*argv, "--version")) { + print_version(); + return 0; + } else { + std::cerr << "patchage: invalid option -- '" << *argv << "'\n"; + print_usage(); + return 1; + } + + ++argv; + --argc; + } + + // Run until main loop is finished + patchage::Patchage patchage(options); + Gtk::Main::run(*patchage.window()); + patchage.save(); + + } catch (std::exception& e) { + std::cerr << "patchage: error: " << e.what() << std::endl; + return 1; + } catch (Glib::Exception& e) { + std::cerr << "patchage: error: " << e.what() << std::endl; + return 1; + } + + return 0; } diff --git a/src/make_alsa_driver.hpp b/src/make_alsa_driver.hpp new file mode 100644 index 0000000..7f3b594 --- /dev/null +++ b/src/make_alsa_driver.hpp @@ -0,0 +1,20 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_MAKE_ALSA_DRIVER_HPP +#define PATCHAGE_MAKE_ALSA_DRIVER_HPP + +#include "Driver.hpp" + +#include <memory> + +namespace patchage { + +class ILog; + +std::unique_ptr<Driver> +make_alsa_driver(ILog& log, Driver::EventSink emit_event); + +} // namespace patchage + +#endif // PATCHAGE_MAKE_ALSA_DRIVER_HPP diff --git a/src/make_jack_driver.hpp b/src/make_jack_driver.hpp new file mode 100644 index 0000000..79c6eb5 --- /dev/null +++ b/src/make_jack_driver.hpp @@ -0,0 +1,21 @@ +// Copyright 2007-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_MAKE_JACK_DRIVER_HPP +#define PATCHAGE_MAKE_JACK_DRIVER_HPP + +#include "Driver.hpp" + +#include <memory> + +namespace patchage { + +class AudioDriver; +class ILog; + +std::unique_ptr<AudioDriver> +make_jack_driver(ILog& log, Driver::EventSink emit_event); + +} // namespace patchage + +#endif // PATCHAGE_MAKE_JACK_DRIVER_HPP diff --git a/src/patchage.ui b/src/patchage.ui.in index 355d4dd..a7078d5 100644 --- a/src/patchage.ui +++ b/src/patchage.ui.in @@ -5,7 +5,7 @@ <object class="GtkWindow" id="main_win"> <property name="can_focus">False</property> <property name="border_width">1</property> - <property name="title" translatable="yes">Patchage</property> + <property name="title" translatable="no">Patchage</property> <child> <object class="GtkVBox" id="main_vbox"> <property name="visible">True</property> @@ -24,39 +24,10 @@ <object class="GtkMenu" id="file_menu_menu"> <property name="can_focus">False</property> <child> - <object class="GtkImageMenuItem" id="menu_open_session"> - <property name="label">gtk-open</property> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <accelerator key="O" signal="activate" modifiers="GDK_CONTROL_MASK"/> - <signal name="activate" handler="on_open_session_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkImageMenuItem" id="menu_save_session"> - <property name="label">gtk-save</property> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="use_underline">True</property> - <property name="use_stock">True</property> - <accelerator key="s" signal="activate" modifiers="GDK_CONTROL_MASK"/> - </object> - </child> - <child> - <object class="GtkMenuItem" id="menu_save_close_session"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">Save and _Close</property> - <property name="use_underline">True</property> - </object> - </child> - <child> <object class="GtkMenuItem" id="menu_export_image"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes">_Export Image...</property> + <property name="label" translatable="yes">_Export Image…</property> <property name="use_underline">True</property> <accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> @@ -93,7 +64,7 @@ <property name="can_focus">False</property> <child> <object class="GtkImageMenuItem" id="menu_jack_connect"> - <property name="label">Connect to _Jack</property> + <property name="label" translatable="yes">Connect to _JACK</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="use_underline">True</property> @@ -104,7 +75,7 @@ </child> <child> <object class="GtkImageMenuItem" id="menu_jack_disconnect"> - <property name="label">Disconnect from Jack</property> + <property name="label" translatable="yes">Disconnect from JACK</property> <property name="visible">True</property> <property name="sensitive">False</property> <property name="can_focus">False</property> @@ -121,7 +92,7 @@ </child> <child> <object class="GtkImageMenuItem" id="menu_alsa_connect"> - <property name="label">Connect to _Alsa</property> + <property name="label" translatable="yes">Connect to _ALSA</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="use_underline">True</property> @@ -132,7 +103,7 @@ </child> <child> <object class="GtkImageMenuItem" id="menu_alsa_disconnect"> - <property name="label">Disconnect from ALSA</property> + <property name="label" translatable="yes">Disconnect from ALSA</property> <property name="visible">True</property> <property name="sensitive">False</property> <property name="can_focus">False</property> @@ -298,7 +269,7 @@ </child> <child> <object class="GtkImageMenuItem" id="menu_view_arrange"> - <property name="label">_Arrange</property> + <property name="label" translatable="yes">_Arrange</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="use_underline">True</property> @@ -313,6 +284,7 @@ <property name="can_focus">False</property> <property name="label" translatable="yes">Sprung Layou_t</property> <property name="use_underline">True</property> + <property name="active">False</property> <accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> @@ -359,51 +331,6 @@ <property name="icon_size">1</property> <property name="icon_size_set">True</property> <child> - <object class="GtkToolButton" id="clear_load_but"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="has_tooltip">True</property> - <property name="tooltip_markup">Clear the dropout indicator</property> - <property name="tooltip_text" translatable="yes">Clear dropout indicator.</property> - <property name="stock_id">gtk-clear</property> - </object> - <packing> - <property name="expand">False</property> - <property name="homogeneous">True</property> - </packing> - </child> - <child> - <object class="GtkToolItem" id="toolitem30"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <child> - <object class="GtkAlignment" id="alignment3"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="yscale">0</property> - <child> - <object class="GtkProgressBar" id="xrun_progress"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="has_tooltip">True</property> - <property name="tooltip_markup">Drouput (XRun) Indicator - -The bar represents the percentage of available time used for audio processing (i.e. the DSP load). If the bar reaches 100%, a dropout will occur.</property> - <property name="tooltip_text" translatable="yes">Load and dropout gauge. The bar shows the percentage of available time used for audio processing. If it reaches 100%, a dropout will occur, and the bar is reset. Click to reset.</property> - <property name="show_text">True</property> - <property name="pulse_step">0.10000000149</property> - <property name="text" translatable="yes">0 Dropouts</property> - <property name="discrete_blocks">100</property> - </object> - </child> - </object> - </child> - </object> - <packing> - <property name="expand">False</property> - </packing> - </child> - <child> <object class="GtkToolItem" id="toolitem28"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -412,31 +339,19 @@ The bar represents the percentage of available time used for audio processing (i <object class="GtkAlignment" id="alignment2"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes">Jack buffer size and sample rate.</property> + <property name="tooltip_text" translatable="yes">JACK buffer size and sample rate.</property> <property name="yscale">0</property> <child> <object class="GtkHBox" id="hbox4"> <property name="visible">True</property> <property name="can_focus">False</property> <child> - <object class="GtkLabel" id="label10"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes"> / </property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - <child> <object class="GtkComboBox" id="buf_size_combo"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="has_tooltip">True</property> - <property name="tooltip_markup">Jack buffer length in frames</property> - <property name="tooltip_text" translatable="yes">Jack buffer length in frames</property> + <property name="tooltip_markup">JACK buffer length in frames</property> + <property name="tooltip_text" translatable="yes">JACK buffer length in frames.</property> <property name="border_width">1</property> </object> <packing> @@ -448,7 +363,7 @@ The bar represents the percentage of available time used for audio processing (i <child> <object class="GtkLabel" id="latency_label"> <property name="can_focus">False</property> - <property name="label" translatable="yes">frames @ ? kHz (? ms)</property> + <property name="label" translatable="yes">frames at ? kHz (? ms)</property> </object> <packing> <property name="expand">False</property> @@ -467,6 +382,36 @@ The bar represents the percentage of available time used for audio processing (i </packing> </child> <child> + <object class="GtkToolItem" id="toolitem30"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="dropouts_label"> + <property name="can_focus">False</property> + <property name="visible">False</property> + <property name="label" translatable="yes">Dropouts: {}</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="clear_load_but"> + <property name="visible">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_markup">Clear the dropout indicator</property> + <property name="tooltip_text" translatable="yes">Clear dropout indicator.</property> + <property name="stock_id">gtk-clear</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> <object class="GtkToolItem" id="toolitem1"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -556,11 +501,11 @@ The bar represents the percentage of available time used for audio processing (i <property name="transient_for">main_win</property> <property name="program_name">Patchage</property> <property name="version">@PATCHAGE_VERSION@</property> - <property name="copyright" translatable="yes">© 2005-2017 David Robillard + <property name="copyright" translatable="no">© 2005-2022 David Robillard © 2008 Nedko Arnaudov</property> - <property name="comments" translatable="yes">A JACK and ALSA front-end.</property> + <property name="comments" translatable="yes">A modular patchbay for JACK and ALSA applications.</property> <property name="website">http://drobilla.net/software/patchage</property> - <property name="license" translatable="yes"> GNU GENERAL PUBLIC LICENSE + <property name="license" translatable="no"> GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> diff --git a/src/patchage_config.h b/src/patchage_config.h new file mode 100644 index 0000000..6c4f09f --- /dev/null +++ b/src/patchage_config.h @@ -0,0 +1,112 @@ +// Copyright 2021-2023 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +/* + Configuration header that defines reasonable defaults at compile-time. + + This allows configuration from the command-line (usually by the build system) + while still allowing the code to compile "as-is" with reasonable default + features on supported platforms. + + This system is designed so that, ideally, no command-line or build-system + configuration is needed, but automatic feature detection can be disabled or + overridden for maximum control. It should never be necessary to edit the + source code to achieve a given configuration. + + Usage: + + - By default, features are enabled if they can be detected or assumed to be + available from the build environment, unless `PATCHAGE_NO_DEFAULT_CONFIG` + is defined, which disables everything by default. + + - If a symbol like `HAVE_SOMETHING` is defined to non-zero, then the + "something" feature is assumed to be available. + + Code rules: + + - To check for a feature, this header must be included, and the symbol + `USE_SOMETHING` used as a boolean in an `#if` expression. + + - None of the other configuration symbols described here may be used + directly. In particular, this header should be the only place in the + source code that touches `HAVE` symbols. +*/ + +#ifndef PATCHAGE_CONFIG_H +#define PATCHAGE_CONFIG_H + +// Define version unconditionally so a warning will catch a mismatch +#define PATCHAGE_VERSION "1.0.11" + +#if !defined(PATCHAGE_NO_DEFAULT_CONFIG) + +// Classic UNIX: dladdr() +# ifndef HAVE_DLADDR +# ifdef __has_include +# if __has_include(<dlfcn.h>) +# define HAVE_DLADDR 1 +# endif +# elif defined(__unix__) || defined(__APPLE__) +# define HAVE_DLADDR 1 +# endif +# endif + +// GNU gettext() +# ifndef HAVE_GETTEXT +# ifdef __has_include +# if __has_include(<libintl.h>) +# define HAVE_GETTEXT 1 +# endif +# endif +# endif + +// JACK metadata API +# ifndef HAVE_JACK_METADATA +# ifdef __has_include +# if __has_include(<jack/metadata.h>) +# define HAVE_JACK_METADATA 1 +# endif +# endif +# endif + +#endif // !defined(PATCHAGE_NO_DEFAULT_CONFIG) + +/* + Make corresponding USE_FEATURE defines based on the HAVE_FEATURE defines from + above or the command line. The code checks for these using #if (not #ifdef), + so there will be an undefined warning if it checks for an unknown feature, + and this header is always required by any code that checks for features, even + if the build system defines them all. +*/ + +#if defined(HAVE_DLADDR) && HAVE_DLADDR +# define USE_DLADDR 1 +#else +# define USE_DLADDR 0 +#endif + +#if defined(HAVE_GETTEXT) && HAVE_GETTEXT +# define USE_GETTEXT 1 +#else +# define USE_GETTEXT 0 +#endif + +#if defined(HAVE_JACK_METADATA) && HAVE_JACK_METADATA +# define USE_JACK_METADATA 1 +#else +# define USE_JACK_METADATA 0 +#endif + +#if !defined(PATCHAGE_USE_LIGHT_THEME) +# define PATCHAGE_USE_LIGHT_THEME 0 +#endif + +#ifndef PATCHAGE_BUNDLED +# ifdef __APPLE__ +# define PATCHAGE_BUNDLED 1 +# else +# define PATCHAGE_BUNDLED 0 +# endif +#endif + +#endif // PATCHAGE_CONFIG_H diff --git a/src/warnings.hpp b/src/warnings.hpp new file mode 100644 index 0000000..ee9b3d8 --- /dev/null +++ b/src/warnings.hpp @@ -0,0 +1,45 @@ +// Copyright 2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PATCHAGE_WARNINGS_HPP +#define PATCHAGE_WARNINGS_HPP + +#if defined(__clang__) + +# define PATCHAGE_DISABLE_FMT_WARNINGS \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wdocumentation-unknown-command\"") \ + _Pragma("clang diagnostic ignored \"-Wglobal-constructors\"") \ + _Pragma("clang diagnostic ignored \"-Wsigned-enum-bitfield\"") + +// clang-format off +# define PATCHAGE_DISABLE_GANV_WARNINGS \ + _Pragma("clang diagnostic push") \ + _Pragma( \ + "clang diagnostic ignored \"-Wdocumentation-unknown-command\"") +// clang-format on + +# define PATCHAGE_RESTORE_WARNINGS _Pragma("clang diagnostic pop") + +#elif defined(__GNUC__) + +# define PATCHAGE_DISABLE_FMT_WARNINGS _Pragma("GCC diagnostic push") + +# define PATCHAGE_DISABLE_GANV_WARNINGS + +# define PATCHAGE_RESTORE_WARNINGS _Pragma("GCC diagnostic pop") + +#else + +# define PATCHAGE_DISABLE_GANV_WARNINGS +# define PATCHAGE_RESTORE_WARNINGS + +#endif + +#if defined(__GNUC__) +# define PATCHAGE_UNREACHABLE() __builtin_unreachable() +#else +# define PATCHAGE_UNREACHABLE() +#endif + +#endif // PATCHAGE_WARNINGS_HPP |