summaryrefslogtreecommitdiff
path: root/libs/surfaces
diff options
context:
space:
mode:
authorLuciano Iam <lucianito@gmail.com>2020-02-20 13:12:36 +0100
committerRobin Gareus <robin@gareus.org>2020-02-22 23:10:24 +0100
commit8db9755d1e075f22275286e94b37af311c6d489f (patch)
treed72749d8cdc77914303c34c6aa6f5a28e620ddb8 /libs/surfaces
parent44165309299960e35d4efa2194b2c2db63c2c1b2 (diff)
Add websockets surface module
Diffstat (limited to 'libs/surfaces')
-rw-r--r--libs/surfaces/websockets/ardour_websockets.cc132
-rw-r--r--libs/surfaces/websockets/ardour_websockets.h98
-rw-r--r--libs/surfaces/websockets/client.cc71
-rw-r--r--libs/surfaces/websockets/client.h58
-rw-r--r--libs/surfaces/websockets/component.cc62
-rw-r--r--libs/surfaces/websockets/component.h61
-rw-r--r--libs/surfaces/websockets/dispatcher.cc194
-rw-r--r--libs/surfaces/websockets/dispatcher.h58
-rw-r--r--libs/surfaces/websockets/feedback.cc171
-rw-r--r--libs/surfaces/websockets/feedback.h56
-rw-r--r--libs/surfaces/websockets/globals.cc39
-rw-r--r--libs/surfaces/websockets/globals.h36
-rw-r--r--libs/surfaces/websockets/interface.cc73
-rw-r--r--libs/surfaces/websockets/message.cc193
-rw-r--r--libs/surfaces/websockets/message.h45
-rw-r--r--libs/surfaces/websockets/server.cc383
-rw-r--r--libs/surfaces/websockets/server.h95
-rw-r--r--libs/surfaces/websockets/state.cc118
-rw-r--r--libs/surfaces/websockets/state.h95
-rw-r--r--libs/surfaces/websockets/strips.cc227
-rw-r--r--libs/surfaces/websockets/strips.h70
-rw-r--r--libs/surfaces/websockets/typed_value.cc152
-rw-r--r--libs/surfaces/websockets/typed_value.h65
-rw-r--r--libs/surfaces/websockets/wscript42
-rw-r--r--libs/surfaces/wscript6
25 files changed, 2600 insertions, 0 deletions
diff --git a/libs/surfaces/websockets/ardour_websockets.cc b/libs/surfaces/websockets/ardour_websockets.cc
new file mode 100644
index 0000000000..0dd3bea4c2
--- /dev/null
+++ b/libs/surfaces/websockets/ardour_websockets.cc
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
+ * Copyright (C) 2009-2014 David Robillard <d@drobilla.net>
+ * Copyright (C) 2009-2017 Paul Davis <paul@linuxaudiosystems.com>
+ * Copyright (C) 2012-2016 Tim Mayberry <mojofunk@gmail.com>
+ * Copyright (C) 2013-2019 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015-2016 Ben Loftis <ben@harrisonconsoles.com>
+ * Copyright (C) 2015-2018 John Emmas <john@creativepost.co.uk>
+ * Copyright (C) 2015 Johannes Mueller <github@johannes-mueller.org>
+ * Copyright (C) 2016-2018 Len Ovens <len@ovenwerks.net>
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "ardour/session.h"
+
+#include "ardour_websockets.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+
+#include "pbd/abstract_ui.cc" // instantiate template
+
+ArdourWebsockets::ArdourWebsockets (Session& s)
+ : ControlProtocol (s, X_(SURFACE_NAME))
+ , AbstractUI<ArdourWebsocketsUIRequest> (name ())
+ , _strips (*this)
+ , _globals (*this)
+ , _feedback (*this)
+ , _server (*this)
+ , _dispatcher (*this)
+{
+ _components = { &_strips, &_globals, &_server, &_feedback, &_dispatcher };
+}
+
+ArdourWebsockets::~ArdourWebsockets ()
+{
+ stop();
+}
+
+void*
+ArdourWebsockets::request_factory (uint32_t num_requests)
+{
+ /* AbstractUI<T>::request_buffer_factory() is a template method only
+ instantiated in this source module. To provide something visible for
+ use in the interface/descriptor, we have this static method that is
+ template-free.
+ */
+ return request_buffer_factory (num_requests);
+}
+
+int
+ArdourWebsockets::set_active (bool yn)
+{
+ if (yn != active ()) {
+ if (yn) {
+ if (start ()) {
+ return -1;
+ }
+ } else {
+ if (stop ()) {
+ return -1;
+ }
+ }
+ }
+
+ return ControlProtocol::set_active (yn);
+}
+
+void
+ArdourWebsockets::thread_init ()
+{
+ pthread_set_name (event_loop_name ().c_str ());
+ PBD::notify_event_loops_about_thread_creation (pthread_self (), event_loop_name (), 2048);
+ SessionEvent::create_per_thread_pool (event_loop_name (), 128);
+}
+
+void
+ArdourWebsockets::do_request (ArdourWebsocketsUIRequest* req)
+{
+ if (req->type == CallSlot) {
+ call_slot (MISSING_INVALIDATOR, req->the_slot);
+ } else if (req->type == Quit) {
+ stop ();
+ }
+}
+
+int
+ArdourWebsockets::start ()
+{
+ // startup the event loop thread
+ BaseUI::run ();
+
+ for (std::vector<SurfaceComponent *>::iterator it = _components.begin ();
+ it != _components.end (); ++it) {
+ int rc = (*it)->start ();
+ if (rc != 0) {
+ return -1;
+ }
+ }
+
+ PBD::info << "ArdourWebsockets: started" << endmsg;
+
+ return 0;
+}
+
+int
+ArdourWebsockets::stop () {
+ for (std::vector<SurfaceComponent *>::iterator it = _components.begin ();
+ it != _components.end (); ++it) {
+ (*it)->stop ();
+ }
+
+ BaseUI::quit ();
+
+ PBD::info << "ArdourWebsockets: stopped" << endmsg;
+
+ return 0;
+}
diff --git a/libs/surfaces/websockets/ardour_websockets.h b/libs/surfaces/websockets/ardour_websockets.h
new file mode 100644
index 0000000000..8837cc367d
--- /dev/null
+++ b/libs/surfaces/websockets/ardour_websockets.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2009-2011 Carl Hetherington <carl@carlh.net>
+ * Copyright (C) 2009-2017 Paul Davis <paul@linuxaudiosystems.com>
+ * Copyright (C) 2009 David Robillard <d@drobilla.net>
+ * Copyright (C) 2013-2016 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015 Johannes Mueller <github@johannes-mueller.org>
+ * Copyright (C) 2016-2018 Len Ovens <len@ovenwerks.net>
+ * Copyright (C) 2016 Ben Loftis <ben@harrisonconsoles.com>
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef ardour_websockets_h
+#define ardour_websockets_h
+
+#define ABSTRACT_UI_EXPORTS
+#include "pbd/abstract_ui.h"
+
+#include "ardour/plugin.h"
+#include "control_protocol/control_protocol.h"
+
+#include "component.h"
+#include "strips.h"
+#include "globals.h"
+#include "server.h"
+#include "feedback.h"
+#include "dispatcher.h"
+
+#define SURFACE_NAME "WebSockets Server (Experimental)"
+#define SURFACE_ID "uri://ardour.org/surfaces/ardour_websockets:0"
+
+namespace ArdourSurface {
+
+struct ArdourWebsocketsUIRequest : public BaseUI::BaseRequestObject {
+ public:
+ ArdourWebsocketsUIRequest () {}
+ ~ArdourWebsocketsUIRequest () {}
+};
+
+class ArdourWebsockets : public ARDOUR::ControlProtocol,
+ public AbstractUI<ArdourWebsocketsUIRequest>
+{
+ public:
+
+ ArdourWebsockets (ARDOUR::Session&);
+ virtual ~ArdourWebsockets ();
+
+ static void* request_factory (uint32_t);
+
+ int set_active (bool);
+
+ ARDOUR::Session& ardour_session () { return *session; }
+ ArdourStrips& strips_component () { return _strips; }
+ ArdourGlobals& globals_component () { return _globals; }
+ WebsocketsServer& server_component () { return _server; }
+ WebsocketsDispatcher& dispatcher_component () { return _dispatcher; }
+
+ // ControlProtocol
+ void stripable_selection_changed () {}
+
+ protected:
+
+ // BaseUI
+ void thread_init ();
+
+ // AbstractUI
+ void do_request (ArdourWebsocketsUIRequest*);
+
+ private:
+
+ ArdourStrips _strips;
+ ArdourGlobals _globals;
+ ArdourFeedback _feedback;
+ WebsocketsServer _server;
+ WebsocketsDispatcher _dispatcher;
+ std::vector<SurfaceComponent *> _components;
+
+ int start ();
+ int stop ();
+
+};
+
+} // namespace
+
+#endif // ardour_websockets_h
diff --git a/libs/surfaces/websockets/client.cc b/libs/surfaces/websockets/client.cc
new file mode 100644
index 0000000000..745596e17a
--- /dev/null
+++ b/libs/surfaces/websockets/client.cc
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <sstream>
+
+#include "client.h"
+
+bool
+ClientContext::has_state (const NodeState& node_state)
+{
+ ClientState::iterator it = _state.find (node_state);
+
+ if (it == _state.end ()) {
+ return false;
+ }
+
+ int n_val = node_state.n_val ();
+
+ if (it->n_val () != n_val) {
+ return false;
+ }
+
+ for (int i = 0; i < n_val; i++) {
+ if (it->nth_val (i) != node_state.nth_val (i)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+ClientContext::update_state (const NodeState& node_state)
+{
+ ClientState::iterator it = _state.find (node_state);
+
+ if (it != _state.end ()) {
+ _state.erase (it);
+ }
+
+ _state.insert (node_state);
+}
+
+std::string
+ClientContext::debug_str ()
+{
+ std::stringstream ss;
+
+ ss << "client = " << std::hex << _wsi << std::endl;
+
+ for (ClientState::iterator it = _state.begin (); it != _state.end (); ++it) {
+ ss << " - " << it->debug_str () << std::endl;
+ }
+
+ return ss.str ();
+}
diff --git a/libs/surfaces/websockets/client.h b/libs/surfaces/websockets/client.h
new file mode 100644
index 0000000000..1f75163338
--- /dev/null
+++ b/libs/surfaces/websockets/client.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef client_context_h
+#define client_context_h
+
+#include <unordered_set>
+#include <list>
+
+#include "state.h"
+#include "message.h"
+
+typedef struct lws* Client;
+typedef std::list<NodeStateMessage> ClientOutputBuffer;
+
+class ClientContext
+{
+ public:
+
+ ClientContext (Client wsi) : _wsi(wsi) {};
+ virtual ~ClientContext () {};
+
+ Client wsi () const { return _wsi; }
+
+ bool has_state (const NodeState&);
+ void update_state (const NodeState&);
+
+ ClientOutputBuffer& output_buf () { return _output_buf; }
+
+ std::string debug_str ();
+
+ private:
+
+ Client _wsi;
+
+ typedef std::unordered_set<NodeState> ClientState;
+ ClientState _state;
+
+ ClientOutputBuffer _output_buf;
+
+};
+
+#endif // client_context_h
diff --git a/libs/surfaces/websockets/component.cc b/libs/surfaces/websockets/component.cc
new file mode 100644
index 0000000000..2c1d8b9fe0
--- /dev/null
+++ b/libs/surfaces/websockets/component.cc
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "component.h"
+#include "ardour_websockets.h"
+
+PBD::EventLoop*
+SurfaceComponent::event_loop () const
+{
+ return static_cast<PBD::EventLoop*>(&_surface);
+}
+
+Glib::RefPtr<Glib::MainLoop>
+SurfaceComponent::main_loop () const
+{
+ return _surface.main_loop ();
+}
+
+ARDOUR::Session&
+SurfaceComponent::session () const
+{
+ return _surface.ardour_session ();
+}
+
+ArdourStrips&
+SurfaceComponent::strips () const
+{
+ return _surface.strips_component ();
+}
+
+ArdourGlobals&
+SurfaceComponent::globals () const
+{
+ return _surface.globals_component ();
+}
+
+WebsocketsServer&
+SurfaceComponent::server () const
+{
+ return _surface.server_component ();
+}
+
+WebsocketsDispatcher&
+SurfaceComponent::dispatcher () const
+{
+ return _surface.dispatcher_component ();
+}
diff --git a/libs/surfaces/websockets/component.h b/libs/surfaces/websockets/component.h
new file mode 100644
index 0000000000..5ee3900a4c
--- /dev/null
+++ b/libs/surfaces/websockets/component.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef surface_component_h
+#define surface_component_h
+
+#include <glibmm.h>
+
+#include "ardour/session.h"
+#include "pbd/event_loop.h"
+
+namespace ArdourSurface {
+ class ArdourWebsockets;
+}
+
+class ArdourStrips;
+class ArdourGlobals;
+class WebsocketsServer;
+class WebsocketsDispatcher;
+
+class SurfaceComponent
+{
+ public:
+
+ SurfaceComponent (ArdourSurface::ArdourWebsockets& surface) : _surface (surface) {};
+
+ virtual ~SurfaceComponent () {};
+
+ virtual int start () { return 0; }
+ virtual int stop () { return 0; }
+
+ PBD::EventLoop* event_loop () const;
+ Glib::RefPtr<Glib::MainLoop> main_loop() const;
+ ARDOUR::Session& session () const;
+ ArdourStrips& strips () const;
+ ArdourGlobals& globals () const;
+ WebsocketsServer& server () const;
+ WebsocketsDispatcher& dispatcher () const;
+
+ protected:
+
+ ArdourSurface::ArdourWebsockets& _surface;
+
+};
+
+#endif // surface_component_h
diff --git a/libs/surfaces/websockets/dispatcher.cc b/libs/surfaces/websockets/dispatcher.cc
new file mode 100644
index 0000000000..dbb627a027
--- /dev/null
+++ b/libs/surfaces/websockets/dispatcher.cc
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "ardour/plugin_insert.h"
+
+#include "dispatcher.h"
+#include "ardour_websockets.h"
+#include "state.h"
+
+using namespace ARDOUR;
+
+#define NODE_METHOD_PAIR(x) { Node::x, &WebsocketsDispatcher::x ## _handler }
+
+WebsocketsDispatcher::NodeMethodMap
+WebsocketsDispatcher::_node_to_method = {
+ NODE_METHOD_PAIR(tempo),
+ NODE_METHOD_PAIR(strip_gain),
+ NODE_METHOD_PAIR(strip_pan),
+ NODE_METHOD_PAIR(strip_mute),
+ NODE_METHOD_PAIR(strip_plugin_enable),
+ NODE_METHOD_PAIR(strip_plugin_param_value)
+};
+
+void
+WebsocketsDispatcher::dispatch (Client client, const NodeStateMessage& msg)
+{
+ NodeMethodMap::iterator it = _node_to_method.find (msg.state ().node ());
+ if (it != _node_to_method.end ()) {
+ try {
+ (this->*it->second) (client, msg);
+ } catch (const std::exception& e) {
+ std::cerr << e.what() << std::endl;
+ }
+ }
+}
+
+void
+WebsocketsDispatcher::update_all_nodes (Client client)
+{
+ update (client, Node::tempo, {}, { globals ().tempo () });
+
+ for (uint32_t strip_n = 0; strip_n < strips ().strip_count (); ++strip_n) {
+ boost::shared_ptr<Stripable> strip = strips ().nth_strip (strip_n);
+ boost::shared_ptr<Route> route = boost::dynamic_pointer_cast<Route> (strip);
+ if (!route) {
+ continue;
+ }
+
+ update (client, Node::strip_desc, { strip_n }, { strip->name () });
+ update (client, Node::strip_gain, { strip_n }, { strips ().strip_gain (strip_n) });
+ update (client, Node::strip_pan, { strip_n }, { strips ().strip_pan (strip_n) });
+ update (client, Node::strip_mute, { strip_n }, { strips ().strip_mute (strip_n) });
+
+ for (uint32_t plugin_n = 0 ; ; ++plugin_n) {
+ boost::shared_ptr<PluginInsert> insert = strips ()
+ .strip_plugin_insert (strip_n, plugin_n);
+ if (!insert) {
+ break;
+ }
+
+ boost::shared_ptr<Plugin> plugin = insert->plugin ();
+ update (client, Node::strip_plugin_desc, { strip_n, plugin_n },
+ { static_cast<std::string>(plugin->name ()) });
+
+ update (client, Node::strip_plugin_enable, { strip_n, plugin_n },
+ { strips ().strip_plugin_enabled (strip_n, plugin_n) });
+
+ for (uint32_t param_n = 0; param_n < plugin->parameter_count (); ++param_n) {
+ boost::shared_ptr<AutomationControl> a_ctrl =
+ strips ().strip_plugin_param_control (strip_n, plugin_n, param_n);
+ if (!a_ctrl) {
+ continue;
+ }
+
+ // possible flags: enumeration, integer_step, logarithmic, sr_dependent, toggled
+ ParameterDescriptor pd = a_ctrl->desc ();
+
+ if (pd.toggled) {
+ update (client, Node::strip_plugin_param_desc, { strip_n, plugin_n, param_n },
+ { a_ctrl->name (), std::string("b") });
+ } else if (pd.enumeration || pd.integer_step) {
+ update (client, Node::strip_plugin_param_desc, { strip_n, plugin_n, param_n },
+ { a_ctrl->name (), std::string("i"), pd.lower, pd.upper, pd.integer_step });
+ } else {
+ update (client, Node::strip_plugin_param_desc, { strip_n, plugin_n, param_n },
+ { a_ctrl->name (), std::string("d"), pd.lower, pd.upper, pd.logarithmic });
+ }
+
+ TypedValue value = strips ().strip_plugin_param_value (strip_n, plugin_n, param_n);
+ update (client, Node::strip_plugin_param_value, { strip_n, plugin_n, param_n },
+ { value });
+ }
+ }
+ }
+}
+
+void
+WebsocketsDispatcher::tempo_handler (Client client, const NodeStateMessage& msg)
+{
+ if (msg.is_write ()) {
+ globals ().set_tempo (msg.state ().nth_val (0));
+ } else {
+ update (client, Node::tempo, {}, { globals ().tempo () });
+ }
+}
+
+void
+WebsocketsDispatcher::strip_gain_handler (Client client, const NodeStateMessage& msg)
+{
+ uint32_t strip_id = msg.state ().nth_addr (0);
+
+ if (msg.is_write ()) {
+ strips ().set_strip_gain (strip_id, msg.state ().nth_val (0));
+ } else {
+ update (client, Node::strip_gain, { strip_id }, { strips ().strip_gain (strip_id) });
+ }
+}
+
+void
+WebsocketsDispatcher::strip_pan_handler (Client client, const NodeStateMessage& msg)
+{
+ uint32_t strip_id = msg.state ().nth_addr (0);
+
+ if (msg.is_write ()) {
+ strips ().set_strip_pan (strip_id, msg.state ().nth_val (0));
+ } else {
+ update (client, Node::strip_pan, { strip_id }, { strips ().strip_pan(strip_id) });
+ }
+}
+
+void
+WebsocketsDispatcher::strip_mute_handler (Client client, const NodeStateMessage& msg)
+{
+ uint32_t strip_id = msg.state ().nth_addr (0);
+
+ if (msg.is_write ()) {
+ strips ().set_strip_mute (strip_id, msg.state ().nth_val (0));
+ } else {
+ update (client, Node::strip_mute, { strip_id }, { strips ().strip_mute (strip_id) });
+ }
+}
+
+void
+WebsocketsDispatcher::strip_plugin_enable_handler (Client client, const NodeStateMessage& msg)
+{
+ uint32_t strip_id = msg.state ().nth_addr (0);
+ uint32_t plugin_id = msg.state ().nth_addr (1);
+
+ if (msg.is_write ()) {
+ strips ().set_strip_plugin_enabled (strip_id, plugin_id, msg.state ().nth_val (0));
+ } else {
+ update (client, Node::strip_plugin_enable, { strip_id, plugin_id },
+ { strips ().strip_plugin_enabled (strip_id, plugin_id) });
+ }
+}
+
+void
+WebsocketsDispatcher::strip_plugin_param_value_handler (Client client, const NodeStateMessage& msg)
+{
+ uint32_t strip_id = msg.state ().nth_addr (0);
+ uint32_t plugin_id = msg.state ().nth_addr (1);
+ uint32_t param_id = msg.state ().nth_addr (2);
+
+ if (msg.is_write ()) {
+ strips ().set_strip_plugin_param_value (strip_id, plugin_id, param_id,
+ msg.state ().nth_val (0));
+ } else {
+ TypedValue value = strips ().strip_plugin_param_value (strip_id, plugin_id, param_id);
+ update (client, Node::strip_plugin_param_value, { strip_id, plugin_id, param_id },
+ { value });
+ }
+}
+
+void
+WebsocketsDispatcher::update (Client client, std::string node,
+ std::initializer_list<uint32_t> addr, std::initializer_list<TypedValue> val)
+{
+ server ().update_client (client, { node, addr, val }, true);
+}
diff --git a/libs/surfaces/websockets/dispatcher.h b/libs/surfaces/websockets/dispatcher.h
new file mode 100644
index 0000000000..07739ee218
--- /dev/null
+++ b/libs/surfaces/websockets/dispatcher.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef websockets_dispatcher_h
+#define websockets_dispatcher_h
+
+#include <unordered_map>
+
+#include "component.h"
+#include "client.h"
+#include "message.h"
+
+class WebsocketsDispatcher : public SurfaceComponent
+{
+
+ public:
+
+ WebsocketsDispatcher (ArdourSurface::ArdourWebsockets& surface) : SurfaceComponent (surface) {};
+ virtual ~WebsocketsDispatcher () {};
+
+ void dispatch (Client, const NodeStateMessage&);
+ void update_all_nodes (Client);
+
+ private:
+
+ typedef void (WebsocketsDispatcher::*DispatcherMethod) (Client, const NodeStateMessage&);
+ typedef std::unordered_map<std::string, DispatcherMethod> NodeMethodMap;
+
+ static NodeMethodMap _node_to_method;
+
+ void tempo_handler (Client, const NodeStateMessage&);
+ void strip_gain_handler (Client, const NodeStateMessage&);
+ void strip_pan_handler (Client, const NodeStateMessage&);
+ void strip_mute_handler (Client, const NodeStateMessage&);
+ void strip_plugin_enable_handler (Client, const NodeStateMessage&);
+ void strip_plugin_param_value_handler (Client, const NodeStateMessage&);
+
+ void update (Client, std::string, std::initializer_list<uint32_t>,
+ std::initializer_list<TypedValue>);
+
+};
+
+#endif // websockets_dispatcher_h
diff --git a/libs/surfaces/websockets/feedback.cc b/libs/surfaces/websockets/feedback.cc
new file mode 100644
index 0000000000..7cb52a76ef
--- /dev/null
+++ b/libs/surfaces/websockets/feedback.cc
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "ardour/session.h"
+#include "ardour/tempo.h"
+#include "ardour/plugin_insert.h"
+#include "ardour/meter.h"
+
+#include "feedback.h"
+#include "strips.h"
+#include "globals.h"
+#include "state.h"
+#include "server.h"
+
+using namespace ARDOUR;
+
+typedef boost::function<void ()> SignalObserver;
+
+int
+ArdourFeedback::start ()
+{
+ observe_globals ();
+ observe_strips ();
+
+ // some things need polling like the strip meters
+ Glib::RefPtr<Glib::TimeoutSource> periodic_timeout = Glib::TimeoutSource::create (100); // ms
+ _periodic_connection = periodic_timeout->connect (sigc::mem_fun (*this,
+ &ArdourFeedback::poll));
+ periodic_timeout->attach (main_loop ()->get_context ());
+
+ return 0;
+}
+
+int
+ArdourFeedback::stop ()
+{
+ _periodic_connection.disconnect ();
+ _signal_connections.drop_connections ();
+ return 0;
+}
+
+bool
+ArdourFeedback::poll () const
+{
+ for (uint32_t strip_n = 0; strip_n < strips ().strip_count (); ++strip_n) {
+ // meters
+ boost::shared_ptr<Stripable> strip = strips ().nth_strip (strip_n);
+ boost::shared_ptr<PeakMeter> meter = strip->peak_meter ();
+ float db = meter ? meter->meter_level (0, MeterMCP) : -193;
+ update_all (Node::strip_meter, { strip_n }, static_cast<double>(db));
+ }
+
+ return true;
+}
+
+void
+ArdourFeedback::observe_globals ()
+{
+ // tempo
+ SignalObserver observer = [this] () {
+ update_all (Node::tempo, {}, globals ().tempo ());
+ };
+
+ session ().tempo_map ().PropertyChanged.connect (_signal_connections, MISSING_INVALIDATOR,
+ boost::bind<void> (observer), event_loop ());
+}
+
+void
+ArdourFeedback::observe_strips ()
+{
+ for (uint32_t strip_n = 0; strip_n < strips ().strip_count (); ++strip_n) {
+ boost::shared_ptr<Stripable> strip = strips ().nth_strip (strip_n);
+
+ // gain
+ SignalObserver observer = [this, strip_n] () {
+ // fires multiple times (4x as of ardour 6.0)
+ update_all (Node::strip_gain, { strip_n }, strips ().strip_gain (strip_n));
+ };
+ strip->gain_control ()->Changed.connect (_signal_connections, MISSING_INVALIDATOR,
+ boost::bind<void> (observer), event_loop ());
+
+ // pan
+ observer = [this, strip_n] () {
+ update_all (Node::strip_pan, { strip_n }, strips ().strip_pan (strip_n));
+ };
+ strip->pan_azimuth_control ()->Changed.connect (_signal_connections, MISSING_INVALIDATOR,
+ boost::bind<void> (observer), event_loop ());
+
+ // mute
+ observer = [this, strip_n] () {
+ update_all (Node::strip_mute, { strip_n }, strips ().strip_mute (strip_n));
+ };
+ strip->mute_control ()->Changed.connect (_signal_connections, MISSING_INVALIDATOR,
+ boost::bind<void> (observer), event_loop ());
+
+ observe_strip_plugins (strip_n, strip);
+ }
+}
+
+void
+ArdourFeedback::observe_strip_plugins (uint32_t strip_n, boost::shared_ptr<ARDOUR::Stripable> strip)
+{
+ for (uint32_t plugin_n = 0 ; ; ++plugin_n) {
+ boost::shared_ptr<PluginInsert> insert = strips ().strip_plugin_insert (strip_n, plugin_n);
+ if (!insert) {
+ break;
+ }
+
+ SignalObserver observer = [this, strip_n, plugin_n] () {
+ update_all (Node::strip_plugin_enable, { strip_n, plugin_n },
+ strips ().strip_plugin_enabled (strip_n, plugin_n));
+ };
+
+ uint32_t bypass = insert->plugin ()->designated_bypass_port ();
+ Evoral::Parameter param = Evoral::Parameter (PluginAutomation, 0, bypass);
+ boost::shared_ptr<AutomationControl> control = insert->automation_control (param);
+
+ if (control) {
+ control->Changed.connect (_signal_connections, MISSING_INVALIDATOR,
+ boost::bind<void> (observer), event_loop ());
+ }
+
+ observe_strip_plugin_param_values (strip_n, plugin_n, insert);
+ }
+}
+
+void
+ArdourFeedback::observe_strip_plugin_param_values (uint32_t strip_n,
+ uint32_t plugin_n, boost::shared_ptr<ARDOUR::PluginInsert> insert)
+{
+ boost::shared_ptr<Plugin> plugin = insert->plugin ();
+
+ for (uint32_t param_n = 0; param_n < plugin->parameter_count (); ++param_n) {
+ boost::shared_ptr<AutomationControl> control = strips ().strip_plugin_param_control (
+ strip_n, plugin_n, param_n);
+
+ if (!control) {
+ continue;
+ }
+
+ SignalObserver observer = [this, control, strip_n, plugin_n, param_n] () {
+ update_all (Node::strip_plugin_param_value, { strip_n, plugin_n, param_n },
+ ArdourStrips::plugin_param_value (control));
+ };
+
+ control->Changed.connect (_signal_connections, MISSING_INVALIDATOR,
+ boost::bind<void> (observer), event_loop ());
+ }
+}
+
+void
+ArdourFeedback::update_all (std::string node, std::initializer_list<uint32_t> addr,
+ TypedValue val) const
+{
+ server ().update_all_clients ({ node, addr, { val }}, false);
+}
diff --git a/libs/surfaces/websockets/feedback.h b/libs/surfaces/websockets/feedback.h
new file mode 100644
index 0000000000..8a8c6d33d4
--- /dev/null
+++ b/libs/surfaces/websockets/feedback.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef ardour_feedback_h
+#define ardour_feedback_h
+
+#include <glibmm/main.h>
+#include <boost/shared_ptr.hpp>
+
+#include "component.h"
+#include "typed_value.h"
+
+class ArdourFeedback : public SurfaceComponent
+{
+ public:
+
+ ArdourFeedback (ArdourSurface::ArdourWebsockets& surface) : SurfaceComponent (surface) {};
+ virtual ~ArdourFeedback () {};
+
+ int start ();
+ int stop ();
+
+ private:
+
+ Glib::Threads::Mutex _client_state_lock;
+ PBD::ScopedConnectionList _signal_connections;
+ sigc::connection _periodic_connection;
+
+ bool poll () const;
+
+ void observe_globals ();
+ void observe_strips ();
+ void observe_strip_plugins (uint32_t, boost::shared_ptr<ARDOUR::Stripable>);
+ void observe_strip_plugin_param_values (uint32_t, uint32_t,
+ boost::shared_ptr<ARDOUR::PluginInsert>);
+
+ void update_all (std::string, std::initializer_list<uint32_t>, TypedValue) const;
+
+};
+
+#endif // ardour_feedback_h
diff --git a/libs/surfaces/websockets/globals.cc b/libs/surfaces/websockets/globals.cc
new file mode 100644
index 0000000000..decd9ad6c2
--- /dev/null
+++ b/libs/surfaces/websockets/globals.cc
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "ardour/tempo.h"
+
+#include "globals.h"
+
+using namespace ARDOUR;
+
+double
+ArdourGlobals::tempo () const
+{
+ Tempo tempo = session ().tempo_map ().tempo_at_sample (0);
+ return tempo.note_type () * tempo.pulses_per_minute ();
+}
+
+void
+ArdourGlobals::set_tempo (double bpm)
+{
+ bpm = max (0.01, bpm);
+ TempoMap& tempo_map = session ().tempo_map ();
+ Tempo tempo (bpm, tempo_map.tempo_at_sample (0).note_type (), bpm);
+ tempo_map.add_tempo (tempo, 0.0, 0, AudioTime);
+}
diff --git a/libs/surfaces/websockets/globals.h b/libs/surfaces/websockets/globals.h
new file mode 100644
index 0000000000..270e3d0652
--- /dev/null
+++ b/libs/surfaces/websockets/globals.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef ardour_globals_h
+#define ardour_globals_h
+
+#include "component.h"
+
+class ArdourGlobals : public SurfaceComponent
+{
+ public:
+
+ ArdourGlobals (ArdourSurface::ArdourWebsockets& surface) : SurfaceComponent (surface) {};
+ virtual ~ArdourGlobals () {};
+
+ double tempo () const;
+ void set_tempo (double);
+
+};
+
+#endif // ardour_globals_h
diff --git a/libs/surfaces/websockets/interface.cc b/libs/surfaces/websockets/interface.cc
new file mode 100644
index 0000000000..5bdd16754b
--- /dev/null
+++ b/libs/surfaces/websockets/interface.cc
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2006-2015 Paul Davis <paul@linuxaudiosystems.com>
+ * Copyright (C) 2009 David Robillard <d@drobilla.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "ardour/rc_configuration.h"
+#include "control_protocol/control_protocol.h"
+
+#include "ardour_websockets.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+
+static ControlProtocol*
+new_ardour_websockets_protocol (ControlProtocolDescriptor* /*descriptor*/,
+ Session* s)
+{
+ ArdourWebsockets* surface = new ArdourWebsockets (*s);
+
+ surface->set_active (true);
+
+ return surface;
+}
+
+static void
+delete_ardour_websockets_protocol (ControlProtocolDescriptor* /*descriptor*/,
+ ControlProtocol* cp)
+{
+ delete cp;
+}
+
+static bool
+probe_ardour_websockets_protocol (ControlProtocolDescriptor* /*descriptor*/)
+{
+ return true;
+}
+
+static void*
+ardour_websockets_request_buffer_factory (uint32_t num_requests)
+{
+ return ArdourWebsockets::request_factory (num_requests);
+}
+
+static ControlProtocolDescriptor ardour_websockets_descriptor = {
+ /*name : */ SURFACE_NAME,
+ /*id : */ SURFACE_ID,
+ /*ptr : */ 0,
+ /*module : */ 0,
+ /*mandatory : */ 0,
+ /*supports_feedback : */ true,
+ /*probe : */ probe_ardour_websockets_protocol,
+ /*initialize : */ new_ardour_websockets_protocol,
+ /*destroy : */ delete_ardour_websockets_protocol,
+ /*request_buffer_factory */ ardour_websockets_request_buffer_factory
+};
+
+extern "C" ARDOURSURFACE_API ControlProtocolDescriptor* protocol_descriptor () {
+ return &ardour_websockets_descriptor;
+}
diff --git a/libs/surfaces/websockets/message.cc b/libs/surfaces/websockets/message.cc
new file mode 100644
index 0000000000..941e01f539
--- /dev/null
+++ b/libs/surfaces/websockets/message.cc
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef DEBUG
+#include <iostream>
+#endif
+
+#include <sstream>
+#include <boost/lexical_cast.hpp>
+#include <boost/property_tree/json_parser.hpp>
+#include <boost/property_tree/ptree.hpp>
+
+#include "message.h"
+
+// JSON does not support Infinity or NaN
+#define XSTR(s) STR(s)
+#define STR(s) #s
+#define JSON_INF 1.0e+128
+#define JSON_INF_STR XSTR(JSON_INF)
+
+namespace pt = boost::property_tree;
+
+NodeStateMessage::NodeStateMessage (const NodeState& state)
+ : _valid (true)
+ , _state (state)
+{
+ _write = state.n_val () > 0;
+}
+
+NodeStateMessage::NodeStateMessage (void *buf, size_t len)
+ : _valid (false)
+ , _write (false)
+{
+ try {
+ std::string s { static_cast<char *>(buf), len };
+
+ std::istringstream is { s };
+ pt::ptree root;
+ pt::read_json (is, root);
+
+ _state = NodeState { root.get<std::string> ("node") };
+
+ pt::ptree addr = root.get_child ("addr", pt::ptree ());
+
+ for (pt::ptree::iterator it = addr.begin (); it != addr.end (); ++it) {
+ // throws if datatype not uint32_t
+ _state.add_addr (boost::lexical_cast<uint32_t>(it->second.data ()));
+ }
+
+ pt::ptree val = root.get_child ("val", pt::ptree ());
+
+ for (pt::ptree::iterator it = val.begin (); it != val.end (); ++it) {
+
+ std::string val = it->second.data ();
+
+ try {
+ _state.add_val (boost::lexical_cast<int>(val));
+ } catch (const boost::bad_lexical_cast&) {
+ try {
+ double d = boost::lexical_cast<double>(val);
+ if (d >= JSON_INF) {
+ d = std::numeric_limits<double>::infinity ();
+ } else if (d <= -JSON_INF) {
+ d = -std::numeric_limits<double>::infinity ();
+ }
+ _state.add_val (d);
+ } catch (const boost::bad_lexical_cast&) {
+ if (val == "false") {
+ _state.add_val (false);
+ } else if (val == "true") {
+ _state.add_val (true);
+ } else {
+ _state.add_val (val);
+ }
+ }
+ }
+ }
+
+ if (_state.n_val () > 0) {
+ _write = true;
+ }
+
+ _valid = true;
+
+ } catch (const std::exception& exc) {
+#ifdef DEBUG
+ std::cerr << "cannot parse message - " << exc.what () << std::endl;
+#endif
+ }
+}
+
+size_t
+NodeStateMessage::serialize (void *buf, size_t len) const
+{
+ // boost json writes all values as strings, we do not want that
+
+ if (len == 0) {
+ return -1;
+ }
+
+ std::stringstream ss;
+
+ ss << "{\"node\":\"" << _state.node () << "\"";
+
+ int n_addr = _state.n_addr ();
+
+ if (n_addr > 0) {
+ ss << ",\"addr\":[";
+
+ for (int i = 0; i < n_addr; i++) {
+ if (i > 0) {
+ ss << ',';
+ }
+
+ ss << _state.nth_addr (i);
+ }
+
+ ss << "]";
+ }
+
+ int n_val = _state.n_val ();
+
+ if (n_val > 0) {
+ ss << ",\"val\":[";
+
+ for (int i = 0; i < n_val; i++) {
+ if (i > 0) {
+ ss << ',';
+ }
+
+ TypedValue val = _state.nth_val (i);
+
+ switch (val.type ()) {
+ case TypedValue::Empty:
+ ss << "null";
+ break;
+ case TypedValue::Bool:
+ ss << (static_cast<bool>(val) ? "true" : "false");
+ break;
+ case TypedValue::Int:
+ ss << static_cast<int>(val);
+ break;
+ case TypedValue::Double: {
+ double d = static_cast<double>(val);
+ if (d == std::numeric_limits<double>::infinity ()) {
+ ss << JSON_INF_STR;
+ } else if (d == -std::numeric_limits<double>::infinity ()) {
+ ss << "-" JSON_INF_STR;
+ } else {
+ ss << d;
+ }
+ break;
+ }
+ case TypedValue::String:
+ ss << '"' << static_cast<std::string>(val) << '"';
+ break;
+ default:
+ break;
+ }
+ }
+
+ ss << "]";
+ }
+
+ ss << '}';
+
+ std::string s = ss.str ();
+ const char *cs = s.c_str ();
+ size_t cs_sz = strlen (cs);
+
+ if (len < cs_sz) {
+ return -1;
+ }
+
+ memcpy (buf, cs, cs_sz);
+
+ return cs_sz;
+}
diff --git a/libs/surfaces/websockets/message.h b/libs/surfaces/websockets/message.h
new file mode 100644
index 0000000000..eb11617697
--- /dev/null
+++ b/libs/surfaces/websockets/message.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef websockets_message_h
+#define websockets_message_h
+
+#include "state.h"
+
+class NodeStateMessage
+{
+ public:
+
+ NodeStateMessage (const NodeState& state);
+ NodeStateMessage (void *, size_t);
+
+ size_t serialize (void *, size_t) const;
+
+ bool is_valid () const { return _valid; }
+ bool is_write () const { return _write; }
+ const NodeState& state () const { return _state; }
+
+ private:
+
+ bool _valid;
+ bool _write;
+ NodeState _state;
+
+};
+
+#endif // websockets_message_h
diff --git a/libs/surfaces/websockets/server.cc b/libs/surfaces/websockets/server.cc
new file mode 100644
index 0000000000..e3f8fe7b7e
--- /dev/null
+++ b/libs/surfaces/websockets/server.cc
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the/GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef DEBUG
+#include <iostream>
+#endif
+
+#include "server.h"
+#include "dispatcher.h"
+
+using namespace Glib;
+
+WebsocketsServer::WebsocketsServer
+ (ArdourSurface::ArdourWebsockets& surface)
+ : SurfaceComponent (surface)
+ , _lws_context (0)
+{
+ // keep references to all config for libwebsockets 2
+ _lws_proto[0] = {
+ "lws-ardour", // name
+ WebsocketsServer::lws_callback, // callback
+ 0, // per_session_data_size
+ 0, // rx_buffer_size
+ 0, // id
+ 0, // user
+#if LWS_LIBRARY_VERSION_MAJOR >= 3
+ 0 // tx_packet_size
+#endif
+ };
+ _lws_proto[1] = {}; // sentinel
+
+ _lws_info = {};
+ _lws_info.port = WEBSOCKET_LISTEN_PORT;
+ _lws_info.protocols = _lws_proto;
+ _lws_info.uid = -1;
+ _lws_info.gid = -1;
+ _lws_info.user = this;
+}
+
+int
+WebsocketsServer::start ()
+{
+ _lws_context = lws_create_context (&_lws_info);
+
+ if (!_lws_context) {
+ PBD::error << "ArdourWebsockets: could not create libwebsockets context" << endmsg;
+ return -1;
+ }
+
+ // add_poll_fd() should have been called once during lws_create_context()
+ // if _fd_ctx is empty then LWS_CALLBACK_ADD_POLL_FD was not called
+ // this means libwesockets was not compiled with LWS_WITH_EXTERNAL_POLL
+ // - macos homebrew libwebsockets: disabled (3.2.2 as of Feb 2020)
+ // - linux ubuntu libwebsockets-dev: enabled (2.0.3 as of Feb 2020) but
+ // #if defined(LWS_WITH_EXTERNAL_POLL) check is not reliable -- constant
+ // missing from /usr/include/lws_config.h
+
+ if (_fd_ctx.empty ()) {
+ PBD::error << "ArdourWebsockets: check your libwebsockets was compiled"
+ " with LWS_WITH_EXTERNAL_POLL enabled" << endmsg;
+ return -1;
+ }
+
+ return 0;
+}
+
+int
+WebsocketsServer::stop ()
+{
+ for (LwsPollFdGlibSourceMap::iterator it = _fd_ctx.begin (); it != _fd_ctx.end (); ++it) {
+ it->second.rg_iosrc->destroy ();
+
+ if (it->second.wg_iosrc) {
+ it->second.wg_iosrc->destroy ();
+ }
+ }
+
+ _fd_ctx.clear ();
+
+ if (_lws_context) {
+ lws_context_destroy (_lws_context);
+ _lws_context = 0;
+ }
+
+ return 0;
+}
+
+void
+WebsocketsServer::update_client (Client wsi, const NodeState& state, bool force)
+{
+ ClientContextMap::iterator it = _client_ctx.find (wsi);
+ if (it == _client_ctx.end ()) {
+ return;
+ }
+
+ if (force || !it->second.has_state (state)) {
+ // write to client only if state was updated
+ it->second.update_state (state);
+ it->second.output_buf ().push_back (NodeStateMessage { state });
+ lws_callback_on_writable (wsi);
+ }
+}
+
+void
+WebsocketsServer::update_all_clients (const NodeState& state, bool force)
+{
+ for (ClientContextMap::iterator it = _client_ctx.begin (); it != _client_ctx.end (); ++it) {
+ update_client (it->second.wsi (), state, force);
+ }
+}
+
+void
+WebsocketsServer::add_poll_fd (struct lws_pollargs *pa)
+{
+ // fd can be SOCKET or int depending platform
+ lws_sockfd_type fd = pa->fd;
+
+#ifdef PLATFORM_WINDOWS
+ RefPtr<IOChannel> g_channel = IOChannel::create_from_win32_socket (fd);
+#else
+ RefPtr<IOChannel> g_channel = IOChannel::create_from_fd (fd);
+#endif
+ RefPtr<IOSource> rg_iosrc { IOSource::create (g_channel, events_to_ioc (pa->events)) };
+ rg_iosrc->connect (sigc::bind (sigc::mem_fun (*this, &WebsocketsServer::io_handler), fd));
+ rg_iosrc->attach (main_loop ()->get_context ());
+
+ struct lws_pollfd lws_pfd;
+ lws_pfd.fd = pa->fd;
+ lws_pfd.events = pa->events;
+ lws_pfd.revents = 0;
+ _fd_ctx[fd] = LwsPollFdGlibSource { lws_pfd, g_channel, rg_iosrc, { } };
+}
+
+void
+WebsocketsServer::mod_poll_fd (struct lws_pollargs *pa)
+{
+ LwsPollFdGlibSourceMap::iterator it = _fd_ctx.find (pa->fd);
+ if (it == _fd_ctx.end ()) {
+ return;
+ }
+
+ it->second.lws_pfd.events = pa->events;
+
+ if (pa->events & POLLOUT) {
+ // libwebsockets wants to write but cannot find a way to update
+ // an existing glib::iosource event flags using glibmm,
+ // create another iosource and set to IO_OUT, it will be destroyed
+ // after clearing POLLOUT (see 'else' body below)
+
+ if (it->second.wg_iosrc) {
+ // already polling for write
+ return;
+ }
+
+ RefPtr<IOSource> wg_iosrc = it->second.g_channel->create_watch (IOCondition::IO_OUT);
+ wg_iosrc->connect (sigc::bind (sigc::mem_fun (*this, &WebsocketsServer::io_handler), pa->fd));
+ wg_iosrc->attach (main_loop ()->get_context ());
+ it->second.wg_iosrc = wg_iosrc;
+ } else {
+ if (it->second.wg_iosrc) {
+ it->second.wg_iosrc->destroy ();
+ it->second.wg_iosrc = { };
+ }
+ }
+}
+
+void
+WebsocketsServer::del_poll_fd (struct lws_pollargs *pa)
+{
+ LwsPollFdGlibSourceMap::iterator it = _fd_ctx.find (pa->fd);
+ if (it == _fd_ctx.end ()) {
+ return;
+ }
+
+ it->second.rg_iosrc->destroy ();
+
+ if (it->second.wg_iosrc) {
+ it->second.wg_iosrc->destroy ();
+ }
+
+ _fd_ctx.erase (it);
+}
+
+void
+WebsocketsServer::add_client (Client wsi)
+{
+ _client_ctx.emplace (wsi, ClientContext { wsi });
+ dispatcher ().update_all_nodes (wsi); // send all state
+}
+
+void
+WebsocketsServer::del_client (Client wsi)
+{
+ ClientContextMap::iterator it = _client_ctx.find (wsi);
+ if (it != _client_ctx.end ()) {
+ _client_ctx.erase (it);
+ }
+}
+
+void
+WebsocketsServer::recv_client (Client wsi, void *buf, size_t len)
+{
+ NodeStateMessage msg { buf, len };
+ if (!msg.is_valid ()) {
+ return;
+ }
+
+#ifdef DEBUG
+ std::cerr << "RX " << msg.state ().debug_str () << std::endl;
+#endif
+
+ ClientContextMap::iterator it = _client_ctx.find (wsi);
+ if (it == _client_ctx.end ()) {
+ return;
+ }
+
+ // avoid echo
+ it->second.update_state (msg.state ());
+
+ dispatcher ().dispatch (wsi, msg);
+}
+
+void
+WebsocketsServer::write_client (Client wsi)
+{
+ ClientContextMap::iterator it = _client_ctx.find (wsi);
+ if (it == _client_ctx.end ()) {
+ return;
+ }
+
+ ClientOutputBuffer& pending = it->second.output_buf ();
+ if (pending.empty ()) {
+ return;
+ }
+
+ // one lws_write() call per LWS_CALLBACK_SERVER_WRITEABLE callback
+
+ NodeStateMessage msg = pending.front ();
+ pending.pop_front ();
+
+ unsigned char out_buf[1024];
+ size_t len = msg.serialize (out_buf + LWS_PRE, 1024 - LWS_PRE);
+
+ if (len > 0) {
+#ifdef DEBUG
+ std::cerr << "TX " << msg.state ().debug_str () << std::endl;
+#endif
+ lws_write (wsi, out_buf + LWS_PRE, len, LWS_WRITE_TEXT);
+ } else {
+ PBD::error << "ArdourWebsockets: cannot serialize message" << endmsg;
+ }
+
+ if (!pending.empty ()) {
+ lws_callback_on_writable (wsi);
+ }
+}
+
+bool
+WebsocketsServer::io_handler (Glib::IOCondition ioc, lws_sockfd_type fd)
+{
+ // IO_IN=1, IO_PRI=2, IO_ERR=8, IO_HUP=16
+ //printf ("io_handler ioc = %d\n", ioc);
+
+ LwsPollFdGlibSourceMap::iterator it = _fd_ctx.find (fd);
+ if (it == _fd_ctx.end ()) {
+ return false;
+ }
+
+ struct lws_pollfd *lws_pfd = &it->second.lws_pfd;
+ lws_pfd->revents = ioc_to_events (ioc);
+
+ if (lws_service_fd (_lws_context, lws_pfd) < 0) {
+ return false;
+ }
+
+ return ioc & (IO_IN | IO_OUT);
+}
+
+IOCondition
+WebsocketsServer::events_to_ioc (int events)
+{
+ IOCondition ioc = { };
+
+ if (events & POLLIN) {
+ ioc |= IO_IN;
+ }
+
+ if (events & POLLOUT) {
+ ioc |= IO_OUT;
+ }
+
+ if (events & POLLHUP) {
+ ioc |= IO_HUP;
+ }
+
+ if (events & POLLERR) {
+ ioc |= IO_ERR;
+ }
+
+ return ioc;
+}
+
+int
+WebsocketsServer::ioc_to_events (IOCondition ioc)
+{
+ int events = 0;
+
+ if (ioc & IO_IN) {
+ events |= POLLIN;
+ }
+
+ if (ioc & IO_OUT) {
+ events |= POLLOUT;
+ }
+
+ if (ioc & IO_HUP) {
+ events |= POLLHUP;
+ }
+
+ if (ioc & IO_ERR) {
+ events |= POLLERR;
+ }
+
+ return events;
+}
+
+int
+WebsocketsServer::lws_callback(struct lws* wsi, enum lws_callback_reasons reason,
+ void *user, void *in, size_t len)
+{
+ void *ctx_userdata = lws_context_user (lws_get_context (wsi));
+ WebsocketsServer *server = static_cast<WebsocketsServer *>(ctx_userdata);
+
+ switch (reason) {
+ case LWS_CALLBACK_ADD_POLL_FD:
+ server->add_poll_fd (static_cast<struct lws_pollargs *>(in));
+ break;
+
+ case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
+ server->mod_poll_fd (static_cast<struct lws_pollargs *>(in));
+ break;
+
+ case LWS_CALLBACK_DEL_POLL_FD:
+ server->del_poll_fd (static_cast<struct lws_pollargs *>(in));
+ break;
+
+ case LWS_CALLBACK_ESTABLISHED:
+ server->add_client (wsi);
+ break;
+
+ case LWS_CALLBACK_CLOSED:
+ server->del_client (wsi);
+ break;
+
+ case LWS_CALLBACK_RECEIVE:
+ server->recv_client (wsi, in, len);
+ break;
+
+ case LWS_CALLBACK_SERVER_WRITEABLE:
+ server->write_client (wsi);
+ break;
+
+ default:
+ break;
+ }
+
+ return 0;
+}
diff --git a/libs/surfaces/websockets/server.h b/libs/surfaces/websockets/server.h
new file mode 100644
index 0000000000..afb136f442
--- /dev/null
+++ b/libs/surfaces/websockets/server.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef websockets_server_h
+#define websockets_server_h
+
+#include <unordered_map>
+#include <libwebsockets.h>
+
+#if LWS_LIBRARY_VERSION_MAJOR < 3
+// <libwebsockets.h> includes <uv.h> which in turn includes
+// /usr/include/asm-generic/param.h on linux which defines HZ
+// and conflicts with libs/ardour/ardour/parameter_descriptor.h
+#undef HZ
+#else
+// also libwebsockets >=3 already includes integration with the glib event loop
+// but ubuntu default repositories are stuck at version 2, hold until requiring
+// version 3 in order to keep things simpler for the end user
+#endif
+
+#include "component.h"
+#include "client.h"
+#include "state.h"
+#include "message.h"
+
+#define WEBSOCKET_LISTEN_PORT 9000
+
+struct LwsPollFdGlibSource {
+ struct lws_pollfd lws_pfd;
+ Glib::RefPtr<Glib::IOChannel> g_channel;
+ Glib::RefPtr<Glib::IOSource> rg_iosrc;
+ Glib::RefPtr<Glib::IOSource> wg_iosrc;
+};
+
+class WebsocketsServer : public SurfaceComponent
+{
+ public:
+
+ WebsocketsServer (ArdourSurface::ArdourWebsockets&);
+ virtual ~WebsocketsServer () {};
+
+ int start ();
+ int stop ();
+
+ void update_client (Client, const NodeState&, bool);
+ void update_all_clients (const NodeState&, bool);
+
+ private:
+
+ struct lws_protocols _lws_proto[2];
+ struct lws_context_creation_info _lws_info;
+ struct lws_context *_lws_context;
+
+ Glib::RefPtr<Glib::IOChannel> _channel;
+
+ typedef std::unordered_map<lws_sockfd_type, LwsPollFdGlibSource> LwsPollFdGlibSourceMap;
+ LwsPollFdGlibSourceMap _fd_ctx;
+
+ typedef std::unordered_map<Client, ClientContext> ClientContextMap;
+ ClientContextMap _client_ctx;
+
+ void add_poll_fd (struct lws_pollargs*);
+ void mod_poll_fd (struct lws_pollargs*);
+ void del_poll_fd (struct lws_pollargs*);
+
+ void add_client (Client);
+ void del_client (Client);
+ void recv_client (Client, void *buf, size_t len);
+ void write_client (Client);
+
+ bool io_handler (Glib::IOCondition, lws_sockfd_type);
+
+ Glib::IOCondition events_to_ioc (int);
+ int ioc_to_events (Glib::IOCondition);
+
+ static int lws_callback(struct lws*, enum lws_callback_reasons, void *, void *, size_t);
+
+};
+
+#endif // websockets_server_h
diff --git a/libs/surfaces/websockets/state.cc b/libs/surfaces/websockets/state.cc
new file mode 100644
index 0000000000..ed31103863
--- /dev/null
+++ b/libs/surfaces/websockets/state.cc
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <sstream>
+
+#include "state.h"
+
+NodeState::NodeState ()
+{
+ update_node_addr_hash ();
+}
+
+NodeState::NodeState (std::string node)
+ : _node (node)
+{
+ update_node_addr_hash ();
+}
+
+NodeState::NodeState (std::string node, std::initializer_list<uint32_t> addr,
+ std::initializer_list<TypedValue> val)
+ : _node (node)
+ , _addr (addr)
+ , _val (val)
+{
+ update_node_addr_hash ();
+}
+
+std::string
+NodeState::debug_str () const
+{
+ std::stringstream s;
+ s << "node = " << _node;
+
+ if (!_addr.empty ()) {
+ s << std::endl << " addr = ";
+
+ for (std::vector<uint32_t>::const_iterator it = _addr.begin (); it != _addr.end (); ++it) {
+ s << *it << ";";
+ }
+ }
+
+ for (std::vector<TypedValue>::const_iterator it = _val.begin (); it != _val.end (); ++it) {
+ s << std::endl << " val " << it->debug_str ();
+ }
+
+ s << std::endl << " hash = " << _node_addr_hash;
+
+ return s.str ();
+}
+
+int
+NodeState::n_addr () const
+{
+ return static_cast<int>(_addr.size ());
+}
+
+uint32_t
+NodeState::nth_addr (int n) const
+{
+ return _addr[n];
+}
+
+void
+NodeState::add_addr (uint32_t addr)
+{
+ _addr.push_back (addr);
+ update_node_addr_hash ();
+}
+
+int
+NodeState::n_val () const
+{
+ return static_cast<int>(_val.size ());
+}
+
+TypedValue
+NodeState::nth_val (int n) const
+{
+ if (n_val () < n) {
+ return TypedValue ();
+ }
+
+ return _val[n];
+}
+
+void
+NodeState::add_val (TypedValue val)
+{
+ _val.push_back (val);
+}
+
+void
+NodeState::update_node_addr_hash ()
+{
+ std::stringstream ss;
+ ss << _node;
+
+ for (std::vector<uint32_t>::iterator it = _addr.begin (); it != _addr.end (); ++it) {
+ ss << "_" << *it;
+ }
+
+ _node_addr_hash = ss.str ();
+}
diff --git a/libs/surfaces/websockets/state.h b/libs/surfaces/websockets/state.h
new file mode 100644
index 0000000000..814eb3ccd4
--- /dev/null
+++ b/libs/surfaces/websockets/state.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef node_state_h
+#define node_state_h
+
+#include <vector>
+#include <cmath>
+#include <cstring>
+
+#include "typed_value.h"
+
+namespace Node {
+ const std::string tempo = "tempo";
+ const std::string strip_desc = "strip_desc";
+ const std::string strip_meter = "strip_meter";
+ const std::string strip_gain = "strip_gain";
+ const std::string strip_pan = "strip_pan";
+ const std::string strip_mute = "strip_mute";
+ const std::string strip_plugin_desc = "strip_plugin_desc";
+ const std::string strip_plugin_enable = "strip_plugin_enable";
+ const std::string strip_plugin_param_desc = "strip_plugin_param_desc";
+ const std::string strip_plugin_param_value = "strip_plugin_param_value";
+};
+
+class NodeState {
+
+ public:
+
+ NodeState ();
+ NodeState (std::string);
+ NodeState (std::string, std::initializer_list<uint32_t>,
+ std::initializer_list<TypedValue> = {});
+
+ std::string debug_str () const;
+
+ std::string node () const { return _node; }
+
+ int n_addr () const;
+ uint32_t nth_addr (int) const;
+ void add_addr (uint32_t);
+
+ int n_val () const;
+ TypedValue nth_val (int) const;
+ void add_val (TypedValue);
+
+ private:
+
+ std::string _node;
+ std::vector<uint32_t> _addr;
+ std::vector<TypedValue> _val;
+ std::string _node_addr_hash;
+
+ void update_node_addr_hash ();
+
+ friend struct std::hash<NodeState>;
+ friend struct std::equal_to<NodeState>;
+
+};
+
+namespace std {
+ template <>
+ struct hash<NodeState> {
+ size_t operator () (const NodeState &state) const {
+ // std::hash<const char*> produces a hash of the value of the
+ // pointer (the memory address), it does not examine the contents
+ // of any character array.
+ return std::hash<std::string>()(state._node_addr_hash);
+ }
+ };
+
+ template<>
+ struct equal_to<NodeState> {
+ bool operator() (const NodeState& lhs, const NodeState& rhs) const {
+ return lhs._node_addr_hash == rhs._node_addr_hash;
+ }
+ };
+}
+
+#endif // node_state_h
diff --git a/libs/surfaces/websockets/strips.cc b/libs/surfaces/websockets/strips.cc
new file mode 100644
index 0000000000..bde8978967
--- /dev/null
+++ b/libs/surfaces/websockets/strips.cc
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "ardour/session.h"
+#include "ardour/plugin_insert.h"
+#include "ardour/dB.h"
+#include "pbd/controllable.h"
+
+#include "strips.h"
+
+using namespace ARDOUR;
+
+int
+ArdourStrips::start ()
+{
+ // take an indexed snapshot of current strips
+ StripableList strips;
+ session ().get_stripables (strips, PresentationInfo::AllStripables);
+
+ for (StripableList::iterator strip = strips.begin (); strip != strips.end (); ++strip) {
+ _strips.push_back (*strip);
+ }
+
+ return 0;
+}
+
+int
+ArdourStrips::stop ()
+{
+ _strips.clear ();
+ return 0;
+}
+
+double
+ArdourStrips::to_db (double k)
+{
+ if (k == 0) {
+ return -std::numeric_limits<double>::infinity ();
+ }
+
+ float db = accurate_coefficient_to_dB (static_cast<float>(k));
+
+ return static_cast<double>(db);
+}
+
+double
+ArdourStrips::from_db (double db)
+{
+ if (db < -192) {
+ return 0;
+ }
+
+ float k = dB_to_coefficient (static_cast<float>(db));
+
+ return static_cast<double>(k);
+}
+
+double
+ArdourStrips::strip_gain (uint32_t strip_n) const
+{
+ return to_db (nth_strip (strip_n)->gain_control ()->get_value ());
+}
+
+void
+ArdourStrips::set_strip_gain (uint32_t strip_n, double db)
+{
+ nth_strip (strip_n)->gain_control ()->set_value (from_db (db), PBD::Controllable::NoGroup);
+}
+
+double
+ArdourStrips::strip_pan (uint32_t strip_n) const
+{
+ // scale from [0.0 ; 1.0] to [-1.0 ; 1.0]
+ return 2.0 * nth_strip (strip_n)->pan_azimuth_control ()->get_value () - 1.0;
+}
+
+void
+ArdourStrips::set_strip_pan (uint32_t strip_n, double value)
+{
+ value = (value + 1.0) / 2.0;
+ nth_strip (strip_n)->pan_azimuth_control ()->set_value (value, PBD::Controllable::NoGroup);
+}
+
+bool
+ArdourStrips::strip_mute (uint32_t strip_n) const
+{
+ return nth_strip (strip_n)->mute_control ()->muted ();
+}
+
+void
+ArdourStrips::set_strip_mute (uint32_t strip_n, bool mute)
+{
+ nth_strip (strip_n)->mute_control ()->set_value (mute ? 1.0 : 0.0, PBD::Controllable::NoGroup);
+}
+
+bool
+ArdourStrips::strip_plugin_enabled (uint32_t strip_n, uint32_t plugin_n) const
+{
+ return strip_plugin_insert (strip_n, plugin_n)->enabled ();
+}
+
+void
+ArdourStrips::set_strip_plugin_enabled (uint32_t strip_n, uint32_t plugin_n, bool enabled)
+{
+ strip_plugin_insert (strip_n, plugin_n)->enable (enabled);
+}
+
+TypedValue
+ArdourStrips::strip_plugin_param_value (uint32_t strip_n, uint32_t plugin_n,
+ uint32_t param_n) const
+{
+ return plugin_param_value (strip_plugin_param_control (strip_n, plugin_n, param_n));
+}
+
+void
+ArdourStrips::set_strip_plugin_param_value (uint32_t strip_n, uint32_t plugin_n,
+ uint32_t param_n, TypedValue value)
+{
+ boost::shared_ptr<AutomationControl> control = strip_plugin_param_control (
+ strip_n, plugin_n, param_n);
+
+ if (control) {
+ ParameterDescriptor pd = control->desc ();
+ double dbl_val;
+
+ if (pd.toggled) {
+ dbl_val = static_cast<double>(static_cast<bool>(value));
+ } else if (pd.enumeration || pd.integer_step) {
+ dbl_val = static_cast<double>(static_cast<int>(value));
+ } else {
+ dbl_val = static_cast<double>(value);
+ }
+
+ control->set_value (dbl_val, PBD::Controllable::NoGroup);
+ }
+}
+
+uint32_t
+ArdourStrips::strip_count () const
+{
+ return _strips.size ();
+}
+
+boost::shared_ptr<Stripable>
+ArdourStrips::nth_strip (uint32_t strip_n) const
+{
+ if (strip_n < _strips.size ()) {
+ return _strips[strip_n];
+ }
+
+ return boost::shared_ptr<Stripable>();
+}
+
+TypedValue
+ArdourStrips::plugin_param_value (boost::shared_ptr<ARDOUR::AutomationControl> control)
+{
+ TypedValue value = TypedValue ();
+
+ if (control) {
+ ParameterDescriptor pd = control->desc ();
+
+ if (pd.toggled) {
+ value = TypedValue (static_cast<bool>(control->get_value ()));
+ } else if (pd.enumeration || pd.integer_step) {
+ value = TypedValue (static_cast<int>(control->get_value ()));
+ } else {
+ value = TypedValue (control->get_value ());
+ }
+ }
+
+ return value;
+}
+
+boost::shared_ptr<PluginInsert>
+ArdourStrips::strip_plugin_insert (uint32_t strip_n, uint32_t plugin_n) const
+{
+ boost::shared_ptr<Stripable> strip = nth_strip (strip_n);
+ boost::shared_ptr<Route> route = boost::dynamic_pointer_cast<Route> (strip);
+ boost::shared_ptr<Processor> processor = route->nth_plugin (plugin_n);
+
+ if (processor) {
+ boost::shared_ptr<PluginInsert> insert =
+ boost::static_pointer_cast<PluginInsert> (processor);
+
+ if (insert) {
+ return insert;
+ }
+ }
+
+ return boost::shared_ptr<PluginInsert>();
+}
+
+boost::shared_ptr<AutomationControl>
+ArdourStrips::strip_plugin_param_control (uint32_t strip_n, uint32_t plugin_n,
+ uint32_t param_n) const
+{
+ boost::shared_ptr<PluginInsert> insert = strip_plugin_insert (strip_n, plugin_n);
+
+ if (insert) {
+ bool ok = false;
+ boost::shared_ptr<Plugin> plugin = insert->plugin ();
+ uint32_t control_id = plugin->nth_parameter (param_n, ok);
+
+ if (ok && plugin->parameter_is_input (control_id)) {
+ boost::shared_ptr<AutomationControl> control =
+ insert->automation_control (Evoral::Parameter(PluginAutomation, 0, control_id));
+ return control;
+ }
+ }
+
+ return boost::shared_ptr<AutomationControl>();
+}
diff --git a/libs/surfaces/websockets/strips.h b/libs/surfaces/websockets/strips.h
new file mode 100644
index 0000000000..fd740c1bad
--- /dev/null
+++ b/libs/surfaces/websockets/strips.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef ardour_strips_h
+#define ardour_strips_h
+
+#include "component.h"
+#include "typed_value.h"
+
+class ArdourStrips : public SurfaceComponent
+{
+ public:
+
+ ArdourStrips (ArdourSurface::ArdourWebsockets& surface) : SurfaceComponent (surface) {};
+ virtual ~ArdourStrips () {};
+
+ int start ();
+ int stop ();
+
+ static double to_db (double);
+ static double from_db (double);
+
+ double strip_gain (uint32_t) const;
+ void set_strip_gain (uint32_t, double);
+
+ double strip_pan (uint32_t) const;
+ void set_strip_pan (uint32_t, double);
+
+ bool strip_mute (uint32_t) const;
+ void set_strip_mute (uint32_t, bool);
+
+ bool strip_plugin_enabled (uint32_t, uint32_t) const;
+ void set_strip_plugin_enabled (uint32_t, uint32_t, bool);
+
+ TypedValue strip_plugin_param_value (uint32_t, uint32_t, uint32_t) const;
+ void set_strip_plugin_param_value (uint32_t, uint32_t, uint32_t, TypedValue);
+
+ uint32_t strip_count () const;
+ boost::shared_ptr<ARDOUR::Stripable> nth_strip (uint32_t) const;
+
+ boost::shared_ptr<ARDOUR::PluginInsert> strip_plugin_insert (uint32_t, uint32_t) const;
+
+ boost::shared_ptr<ARDOUR::AutomationControl> strip_plugin_param_control(
+ uint32_t, uint32_t, uint32_t) const;
+
+ static TypedValue plugin_param_value (boost::shared_ptr<ARDOUR::AutomationControl>);
+
+ private:
+
+ typedef std::vector<boost::shared_ptr<ARDOUR::Stripable>> StripableVector;
+ StripableVector _strips;
+
+};
+
+#endif // ardour_strips_h
diff --git a/libs/surfaces/websockets/typed_value.cc b/libs/surfaces/websockets/typed_value.cc
new file mode 100644
index 0000000000..808142a36f
--- /dev/null
+++ b/libs/surfaces/websockets/typed_value.cc
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <cmath>
+#include <limits>
+#include <string>
+
+#include "typed_value.h"
+
+#define DBL_TOLERANCE 0.001
+
+TypedValue::operator
+bool () const
+{
+ switch (_type) {
+ case Bool:
+ return _b;
+ case Int:
+ return _i != 0;
+ case Double:
+ return _d != 0;
+ case String:
+ return _s == "true";
+ default:
+ return false;
+ }
+}
+
+TypedValue::operator
+int () const
+{
+ switch (_type) {
+ case Int:
+ return _i;
+ case Bool:
+ return _b ? 1 : 0;
+ case Double:
+ return static_cast<int>(_d);
+ case String:
+ try {
+ return std::stoi (_s);
+ } catch (const std::exception&) {
+ return 0;
+ }
+ default:
+ return 0;
+ }
+}
+
+TypedValue::operator
+double () const
+{
+ switch (_type) {
+ case Double:
+ return _d;
+ case Bool:
+ return _b ? 1.f : 0;
+ case Int:
+ return static_cast<double>(_i);
+ case String:
+ try {
+ return std::stod (_s);
+ } catch (const std::exception&) {
+ return 0;
+ }
+ default:
+ return 0;
+ }
+}
+
+TypedValue::operator
+std::string () const
+{
+ switch (_type) {
+ case String:
+ return _s;
+ case Bool:
+ return _b ? "true" : "false";
+ case Int:
+ return std::to_string (_i);
+ case Double:
+ return std::to_string (_d);
+ default:
+ return "";
+ }
+}
+
+bool
+TypedValue::operator== (const TypedValue& other) const
+{
+ if (_type != other._type) {
+ // make an exception when comparing doubles and ints
+ // for example browser json implementations will send
+ // 1 instead of 1.0 removing any type hint
+ if ((_type == Int) && (other._type == Double)) {
+ return fabs (static_cast<double>(_i) - other._d) < DBL_TOLERANCE;
+ } else if ((_type == Double) && (other._type == Int)) {
+ return fabs (_d - static_cast<double>(other._i)) < DBL_TOLERANCE;
+ }
+
+ return false;
+ }
+
+ switch (_type) {
+ case Bool:
+ return _b == other._b;
+ case Int:
+ return _i == other._i;
+ case Double: {
+ double inf = std::numeric_limits<double>::infinity ();
+ return ((_d == inf) && (other._d == inf))
+ || ((_d == -inf) && (other._d == -inf))
+ || (fabs (_d - other._d) < DBL_TOLERANCE);
+ }
+ case String:
+ return _s == other._s;
+ default:
+ return false;
+ }
+}
+
+bool
+TypedValue::operator!= (const TypedValue& other) const
+{
+ return !(*this == other);
+}
+
+std::string
+TypedValue::debug_str () const
+{
+ char s[256];
+
+ sprintf(s, "type = %d; b = %d; i = %d; d = %f; s = \"%s\"",
+ _type, _b, _i, _d, _s.c_str ());
+
+ return s;
+}
diff --git a/libs/surfaces/websockets/typed_value.h b/libs/surfaces/websockets/typed_value.h
new file mode 100644
index 0000000000..0f4680d693
--- /dev/null
+++ b/libs/surfaces/websockets/typed_value.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 Luciano Iam <lucianito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <string>
+
+#ifndef typed_value_h
+#define typed_value_h
+
+class TypedValue
+{
+ public:
+
+ enum Type {
+ Empty,
+ Bool,
+ Int,
+ Double,
+ String
+ };
+
+ TypedValue (): _type (Empty) { }
+ TypedValue (bool value): _type { Bool }, _b (value) { }
+ TypedValue (int value): _type { Int }, _i (value) { }
+ TypedValue (double value): _type { Double }, _d (value) { }
+ TypedValue (std::string value): _type { String }, _s (value) { }
+
+ bool empty () const { return _type == Empty; };
+ Type type () const { return _type; };
+
+ operator bool () const;
+ operator int () const;
+ operator double () const;
+ operator std::string () const;
+
+ bool operator== (const TypedValue& other) const;
+ bool operator!= (const TypedValue& other) const;
+
+ std::string debug_str () const;
+
+ private:
+
+ Type _type;
+ bool _b = false;
+ int _i = 0;
+ double _d = 0.0;
+ std::string _s;
+
+};
+
+#endif // typed_value_h
diff --git a/libs/surfaces/websockets/wscript b/libs/surfaces/websockets/wscript
new file mode 100644
index 0000000000..84ce6676c6
--- /dev/null
+++ b/libs/surfaces/websockets/wscript
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+from waflib.extras import autowaf as autowaf
+import os
+
+# Mandatory variables
+top = '.'
+out = 'build'
+
+def options(opt):
+ autowaf.set_options(opt)
+
+def configure(conf):
+ autowaf.configure(conf)
+
+def build(bld):
+ obj = bld(features = 'cxx cxxshlib')
+ obj.source = '''
+ interface.cc
+ ardour_websockets.cc
+ typed_value.cc
+ state.cc
+ message.cc
+ client.cc
+ component.cc
+ strips.cc
+ globals.cc
+ server.cc
+ feedback.cc
+ dispatcher.cc
+ '''
+ obj.export_includes = ['.']
+ obj.defines = [ 'PACKAGE="ardour_websockets"' ]
+ obj.defines += [ 'ARDOURSURFACE_DLL_EXPORTS' ]
+ obj.includes = ['.', './websockets']
+ obj.name = 'libardour_websockets'
+ obj.target = 'ardour_websockets'
+ obj.uselib = 'GLIBMM XML WEBSOCKETS OPENSSL'
+ obj.use = 'libardour libardour_cp libgtkmm2ext libpbd'
+ obj.install_path = os.path.join(bld.env['LIBDIR'], 'surfaces')
+
+def shutdown():
+ autowaf.shutdown()
diff --git a/libs/surfaces/wscript b/libs/surfaces/wscript
index 502a2bd500..061bba8099 100644
--- a/libs/surfaces/wscript
+++ b/libs/surfaces/wscript
@@ -76,6 +76,10 @@ def configure(conf):
else:
print('You are missing the cwiid headers needed to compile wiimote support')
+ autowaf.check_pkg(conf, 'libwebsockets', uselib_store='WEBSOCKETS', atleast_version='2.0.0', mandatory=False)
+ if conf.is_defined('HAVE_WEBSOCKETS'):
+ children += [ 'websockets' ]
+
for i in children:
sub_config_and_use(conf, i)
@@ -101,6 +105,8 @@ def build(bld):
bld.recurse('contourdesign')
if bld.is_defined('BUILD_MASCHINE'):
bld.recurse('maschine2')
+ if bld.is_defined ('HAVE_WEBSOCKETS'):
+ bld.recurse('websockets')
def shutdown():
autowaf.shutdown()