diff options
author | Luciano Iam <lucianito@gmail.com> | 2020-02-20 13:12:36 +0100 |
---|---|---|
committer | Robin Gareus <robin@gareus.org> | 2020-02-22 23:10:24 +0100 |
commit | 8db9755d1e075f22275286e94b37af311c6d489f (patch) | |
tree | d72749d8cdc77914303c34c6aa6f5a28e620ddb8 /libs/surfaces | |
parent | 44165309299960e35d4efa2194b2c2db63c2c1b2 (diff) |
Add websockets surface module
Diffstat (limited to 'libs/surfaces')
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() |