summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Action.hpp90
-rw-r--r--src/ActionSink.hpp18
-rw-r--r--src/AlsaDriver.cpp974
-rw-r--r--src/AlsaDriver.hpp116
-rw-r--r--src/AlsaStubDriver.cpp18
-rw-r--r--src/AudioDriver.hpp40
-rw-r--r--src/Canvas.cpp342
-rw-r--r--src/Canvas.hpp81
-rw-r--r--src/CanvasModule.cpp155
-rw-r--r--src/CanvasModule.hpp74
-rw-r--r--src/CanvasPort.hpp108
-rw-r--r--src/ClientID.hpp123
-rw-r--r--src/ClientInfo.hpp18
-rw-r--r--src/ClientType.hpp42
-rw-r--r--src/Configuration.cpp607
-rw-r--r--src/Configuration.hpp222
-rw-r--r--src/Coord.hpp28
-rw-r--r--src/Driver.hpp77
-rw-r--r--src/Drivers.cpp56
-rw-r--r--src/Drivers.hpp52
-rw-r--r--src/Event.hpp71
-rw-r--r--src/ILog.hpp32
-rw-r--r--src/JackDbusDriver.cpp1661
-rw-r--r--src/JackDbusDriver.hpp161
-rw-r--r--src/JackDriver.cpp588
-rw-r--r--src/JackDriver.hpp109
-rw-r--r--src/JackLibDriver.cpp490
-rw-r--r--src/JackStubDriver.cpp18
-rw-r--r--src/Legend.cpp82
-rw-r--r--src/Legend.hpp79
-rw-r--r--src/Metadata.cpp72
-rw-r--r--src/Metadata.hpp42
-rw-r--r--src/Options.hpp16
-rw-r--r--src/Patchage.cpp1579
-rw-r--r--src/Patchage.hpp385
-rw-r--r--src/PatchageCanvas.cpp338
-rw-r--r--src/PatchageCanvas.hpp85
-rw-r--r--src/PatchageEvent.cpp110
-rw-r--r--src/PatchageEvent.hpp87
-rw-r--r--src/PatchageModule.cpp157
-rw-r--r--src/PatchageModule.hpp67
-rw-r--r--src/PatchagePort.hpp104
-rw-r--r--src/PortID.hpp260
-rw-r--r--src/PortInfo.hpp26
-rw-r--r--src/PortNames.hpp44
-rw-r--r--src/PortType.hpp50
-rw-r--r--src/Queue.hpp131
-rw-r--r--src/Reactor.cpp216
-rw-r--r--src/Reactor.hpp75
-rw-r--r--src/Setting.hpp88
-rw-r--r--src/SignalDirection.hpp44
-rw-r--r--src/TextViewLog.cpp81
-rw-r--r--src/TextViewLog.hpp54
-rw-r--r--src/UIFile.hpp103
-rw-r--r--src/Widget.hpp62
-rw-r--r--src/binary_location.h71
-rw-r--r--src/event_to_string.cpp110
-rw-r--r--src/event_to_string.hpp22
-rw-r--r--src/handle_event.cpp149
-rw-r--r--src/handle_event.hpp26
-rw-r--r--src/i18n.hpp12
-rw-r--r--src/jackey.h20
-rw-r--r--src/main.cpp204
-rw-r--r--src/make_alsa_driver.hpp20
-rw-r--r--src/make_jack_driver.hpp21
-rw-r--r--src/patchage.ui.in (renamed from src/patchage.ui)145
-rw-r--r--src/patchage_config.h112
-rw-r--r--src/warnings.hpp45
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, &copy.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. &lt;http://fsf.org/&gt;
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