From 50ba8dea96415f651b5173c017592dd46a2541ec Mon Sep 17 00:00:00 2001 From: Luciano Iam Date: Tue, 14 Apr 2020 22:58:44 +0200 Subject: WebSockets: improve JS client and demo add methods to callback.js automatically reconnect js client on disconnection mixer-demo do not recreate UI on reconnection NO-OP: indentation in message.js make client JS reconnection optional fix mixer-demo scrolling minor JS client refactor improve mixer-demo readability --- libs/surfaces/websockets/dispatcher.cc | 11 +- libs/surfaces/websockets/server.cc | 6 +- share/web_surfaces/builtin/mixer-demo/css/main.css | 5 + share/web_surfaces/builtin/mixer-demo/js/main.js | 143 +++++++++++---------- share/web_surfaces/builtin/mixer-demo/js/widget.js | 46 +++---- share/web_surfaces/shared/ardour.js | 55 +++++--- share/web_surfaces/shared/callback.js | 28 +++- share/web_surfaces/shared/control.js | 12 +- share/web_surfaces/shared/message.js | 24 ++-- 9 files changed, 188 insertions(+), 142 deletions(-) diff --git a/libs/surfaces/websockets/dispatcher.cc b/libs/surfaces/websockets/dispatcher.cc index b497c24081..62f64268aa 100644 --- a/libs/surfaces/websockets/dispatcher.cc +++ b/libs/surfaces/websockets/dispatcher.cc @@ -29,13 +29,13 @@ using namespace ARDOUR; #define NODE_METHOD_PAIR(x) (Node::x, &WebsocketsDispatcher::x##_handler) WebsocketsDispatcher::NodeMethodMap - WebsocketsDispatcher::_node_to_method = boost::assign::map_list_of + WebsocketsDispatcher::_node_to_method = boost::assign::map_list_of 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); + 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) @@ -105,7 +105,6 @@ WebsocketsDispatcher::update_all_nodes (Client client) val.push_back (std::string ("i")); val.push_back (pd.lower); val.push_back (pd.upper); - val.push_back (pd.integer_step); } else { val.push_back (std::string ("d")); val.push_back (pd.lower); diff --git a/libs/surfaces/websockets/server.cc b/libs/surfaces/websockets/server.cc index 7f52b261de..8bd7eb178b 100644 --- a/libs/surfaces/websockets/server.cc +++ b/libs/surfaces/websockets/server.cc @@ -92,10 +92,10 @@ WebsocketsServer::WebsocketsServer (ArdourSurface::ArdourWebsockets& surface) #if LWS_LIBRARY_VERSION_MAJOR < 3 /* older libwebsockets does not define mime type for svg files */ memset (&_lws_vhost_opt, 0, sizeof (lws_protocol_vhost_options)); - _lws_vhost_opt.name = ".svg"; - _lws_vhost_opt.value = "image/svg+xml"; + _lws_vhost_opt.name = ".svg"; + _lws_vhost_opt.value = "image/svg+xml"; _lws_mnt_index.extra_mimetypes = &_lws_vhost_opt; - _lws_mnt_user.extra_mimetypes = &_lws_vhost_opt; + _lws_mnt_user.extra_mimetypes = &_lws_vhost_opt; #endif } diff --git a/share/web_surfaces/builtin/mixer-demo/css/main.css b/share/web_surfaces/builtin/mixer-demo/css/main.css index 0fd812c39b..23965c3fd0 100644 --- a/share/web_surfaces/builtin/mixer-demo/css/main.css +++ b/share/web_surfaces/builtin/mixer-demo/css/main.css @@ -26,6 +26,7 @@ div { flex: 1; display: flex; flex-direction: column; + overflow: hidden; box-shadow: 0px 0px 10px #000; } @@ -76,6 +77,10 @@ div { color: rgb(172,128,255); } +.info { + color: rgb(99,208,230); +} + .error { color: rgb(249,36,114); } diff --git a/share/web_surfaces/builtin/mixer-demo/js/main.js b/share/web_surfaces/builtin/mixer-demo/js/main.js index 64d5fca33f..b6713664b5 100644 --- a/share/web_surfaces/builtin/mixer-demo/js/main.js +++ b/share/web_surfaces/builtin/mixer-demo/js/main.js @@ -17,8 +17,7 @@ */ // This example does not call the API methods in ardour.js, - // instead it interacts at a lower level by coupling the widgets - // tightly to the message stream + // instead it couples the widgets directly to the message stream import { ANode, Message } from '/shared/message.js'; import { ArdourClient } from '/shared/ardour.js'; @@ -29,8 +28,6 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider, (() => { const MAX_LOG_LINES = 1000; - const FEEDBACK_NODES = [ANode.STRIP_GAIN, ANode.STRIP_PAN, ANode.STRIP_METER, - ANode.STRIP_PLUGIN_ENABLE, ANode.STRIP_PLUGIN_PARAM_VALUE]; const ardour = new ArdourClient(location.host); const widgets = {}; @@ -43,101 +40,95 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider, div.innerHTML = `${manifest.name.toUpperCase()} v${manifest.version} — ${manifest.description}`; }); - ardour.addCallback({ - onMessage: (msg) => { - log(`↙ ${msg}`, 'message-in'); - - if (msg.node == ANode.STRIP_DESC) { - createStrip (msg.addr, ...msg.val); - } else if (msg.node == ANode.STRIP_PLUGIN_DESC) { - createStripPlugin (msg.addr, ...msg.val); - } else if (msg.node == ANode.STRIP_PLUGIN_PARAM_DESC) { - createStripPluginParam (msg.addr, ...msg.val); - } else if (FEEDBACK_NODES.includes(msg.node)) { - if (widgets[msg.hash]) { - widgets[msg.hash].value = msg.val[0]; - } - } - }, - - onError: () => { - log('Client error', 'error'); - } + ardour.addCallbacks({ + onConnected: (error) => { log('Client connected', 'info'); }, + onDisconnected: (error) => { log('Client disconnected', 'error'); }, + onMessage: processMessage, + onStripDesc: createStrip, + onStripPluginDesc: createStripPlugin, + onStripPluginParamDesc: createStripPluginParam }); - ardour.open(); + ardour.connect(); } - function createStrip (addr, name) { - const id = `strip-${addr[0]}`; + function createStrip (stripId, name) { + const domId = `strip-${stripId}`; + if (document.getElementById(domId) != null) { + return; + } + const strips = document.getElementById('strips'); - const div = createElem(`
`, strips); - createElem(``, div); + const div = createElem(`
`, strips); + createElem(``, div); // meter - const meter = new StripMeter(ANode.STRIP_METER, addr); + const meter = new StripMeter(); meter.el.classList.add('slider-meter'); - meter.attach(div); - register(meter); + meter.appendTo(div); + connectWidget(meter, ANode.STRIP_METER, stripId); // gain let holder = createElem(`
`, div); createElem(``, holder); - const gain = new StripGainSlider(ANode.STRIP_GAIN, addr); - gain.attach(holder, (val) => send(gain)); - register(gain); + const gain = new StripGainSlider(); + gain.appendTo(holder); + connectWidget(gain, ANode.STRIP_GAIN, stripId); // pan holder = createElem(`
`, div); createElem(``, holder); - const pan = new StripPanSlider(ANode.STRIP_PAN, addr); - pan.attach(holder, (val) => send(pan)); - register(pan); + const pan = new StripPanSlider(); + pan.appendTo(holder); + connectWidget(pan, ANode.STRIP_PAN, stripId); } - function createStripPlugin (addr, name) { - const strip = document.getElementById(`strip-${addr[0]}`); - const id = `plugin-${addr[0]}-${addr[1]}`; - const div = createElem(`
`, strip); + function createStripPlugin (stripId, pluginId, name) { + const domId = `plugin-${stripId}-${pluginId}`; + if (document.getElementById(domId) != null) { + return; + } + + const strip = document.getElementById(`strip-${stripId}`); + const div = createElem(`
`, strip); createElem(``, div); - const enable = new Switch(ANode.STRIP_PLUGIN_ENABLE, addr); + + const enable = new Switch(); enable.el.classList.add('plugin-enable'); - enable.attach(div, (val) => send(enable)); - register(enable); + enable.appendTo(div); + connectWidget(enable, ANode.STRIP_PLUGIN_ENABLE, stripId, pluginId); } - function createStripPluginParam (addr, name, dataType, min, max, isLog) { + function createStripPluginParam (stripId, pluginId, paramId, name, valueType, min, max, isLog) { + const domId = `param-${stripId}-${pluginId}-${paramId}`; + if (document.getElementById(domId) != null) { + return; + } + let param, cssClass; - if (dataType == 'b') { + if (valueType == 'b') { cssClass = 'boolean'; - param = new Switch(ANode.STRIP_PLUGIN_PARAM_VALUE, addr); - } else if (dataType == 'i') { + param = new Switch(); + } else if (valueType == 'i') { cssClass = 'discrete'; - param = new DiscreteSlider(ANode.STRIP_PLUGIN_PARAM_VALUE, addr, min, max); - } else if (dataType == 'd') { + param = new DiscreteSlider(min, max); + } else if (valueType == 'd') { cssClass = 'continuous'; if (isLog) { - param = new LogarithmicSlider(ANode.STRIP_PLUGIN_PARAM_VALUE, addr, min, max); + param = new LogarithmicSlider(min, max); } else { - param = new ContinuousSlider(ANode.STRIP_PLUGIN_PARAM_VALUE, addr, min, max); + param = new ContinuousSlider(min, max); } } - const plugin = document.getElementById(`plugin-${addr[0]}-${addr[1]}`); - const id = `param-${addr[0]}-${addr[1]}-${addr[2]}`; - const div = createElem(`
`, plugin); - createElem(``, div); + const plugin = document.getElementById(`plugin-${stripId}-${pluginId}`); + const div = createElem(`
`, plugin); + createElem(``, div); - param.attach(div, (val) => send(param)); - param.el.name = id; - register(param); - } - - function send (widget) { - const msg = new Message(widget.node, widget.addr, [widget.value]); - log(`↗ ${msg}`, 'message-out'); - ardour.send(msg); + param.el.name = domId; + param.appendTo(div); + connectWidget(param, ANode.STRIP_PLUGIN_PARAM_VALUE, stripId, pluginId, paramId); } function createElem (html, parent) { @@ -153,8 +144,24 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider, return elem; } - function register (widget) { - widgets[widget.hash] = widget; + function connectWidget (widget, node, ...addr) { + const nodeAddrId = Message.nodeAddrId(node, addr); + + widgets[nodeAddrId] = widget; + + widget.callback = (val) => { + const msg = new Message(node, addr, [val]); + log(`↗ ${msg}`, 'message-out'); + ardour.send(msg); + }; + } + + function processMessage (msg) { + log(`↙ ${msg}`, 'message-in'); + + if (widgets[msg.nodeAddrId]) { + widgets[msg.nodeAddrId].value = msg.val[0]; + } } function log (message, className) { diff --git a/share/web_surfaces/builtin/mixer-demo/js/widget.js b/share/web_surfaces/builtin/mixer-demo/js/widget.js index 335e3d8756..417c0c7c7b 100644 --- a/share/web_surfaces/builtin/mixer-demo/js/widget.js +++ b/share/web_surfaces/builtin/mixer-demo/js/widget.js @@ -20,36 +20,26 @@ import { Message } from '/shared/message.js'; export class Widget { - constructor (node, addr, html) { - this.node = node; - this.addr = addr; + constructor (html) { const template = document.createElement('template'); template.innerHTML = html; this.el = template.content.firstChild; } - attach (parent, callback) { + appendTo (parent) { parent.appendChild(this.el); - - if (callback) { - this.callback = callback; - } } callback (value) { // do nothing by default } - get hash () { - return Message.hash(this.node, this.addr); - } - } export class Switch extends Widget { - constructor (node, addr) { - super (node, addr, ``); + constructor () { + super (``); this.el.addEventListener('input', (ev) => this.callback(this.value)); } @@ -65,10 +55,10 @@ export class Switch extends Widget { export class Slider extends Widget { - constructor (node, addr, min, max, step) { + constructor (min, max, step) { const html = ``; - super(node, addr, html); + super(html); this.min = min; this.max = max; this.el.addEventListener('input', (ev) => this.callback(this.value)); @@ -86,24 +76,24 @@ export class Slider extends Widget { export class DiscreteSlider extends Slider { - constructor (node, addr, min, max) { - super(node, addr, min, max, 1); + constructor (min, max, step) { + super(min, max, step || 1); } } export class ContinuousSlider extends Slider { - constructor (node, addr, min, max) { - super(node, addr, min, max, 0.001); + constructor (min, max) { + super(min, max, 0.001); } } export class LogarithmicSlider extends ContinuousSlider { - constructor (node, addr, min, max) { - super(node, addr, 0, 1.0); + constructor (min, max) { + super(0, 1.0); this.minVal = Math.log(min); this.maxVal = Math.log(max); this.scale = this.maxVal - this.minVal; @@ -121,16 +111,16 @@ export class LogarithmicSlider extends ContinuousSlider { export class StripPanSlider extends ContinuousSlider { - constructor (node, addr) { - super(node, addr, -1.0, 1.0); + constructor () { + super(-1.0, 1.0); } } export class StripGainSlider extends ContinuousSlider { - constructor (node, addr) { - super(node, addr, 0, 1.0) + constructor () { + super(0, 1.0) this.minVal = -58.0; this.maxVal = 6.0; this.scale = (this.maxVal - this.minVal); @@ -148,8 +138,8 @@ export class StripGainSlider extends ContinuousSlider { export class StripMeter extends Widget { - constructor (node, addr) { - super(node, addr, ``); + constructor () { + super(``); } set value (val) { diff --git a/share/web_surfaces/shared/ardour.js b/share/web_surfaces/shared/ardour.js index a7e6978ba2..63a548e174 100644 --- a/share/web_surfaces/shared/ardour.js +++ b/share/web_surfaces/shared/ardour.js @@ -16,17 +16,19 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import { MetadataMixin } from './metadata.js'; import { ControlMixin } from './control.js'; +import { MetadataMixin } from './metadata.js'; import { Message } from './message.js'; import { MessageChannel } from './channel.js'; -// See *Mixin for the available APIs +// See ControlMixin and MetadataMixin for available APIs +// See ArdourCallback for an example callback implementation class BaseArdourClient { constructor () { this._callbacks = []; + this._connected = false; this._pendingRequest = null; this._channel = new MessageChannel(location.host); @@ -39,21 +41,30 @@ class BaseArdourClient { }; } - addCallback (callback) { - this._callbacks.push(callback); + addCallbacks (callbacks) { + this._callbacks.push(callbacks); } - async open () { - this._channel.onClose = () => { - this._fireCallbacks('error', new Error('Message channel unexpectedly closed')); + async connect (autoReconnect) { + this._channel.onClose = async () => { + if (this._connected) { + this._fireCallbacks('disconnected'); + this._connected = false; + } + + if ((autoReconnect == null) || autoReconnect) { + await this._sleep(1000); + await this._connect(); + } }; - await this._channel.open(); + this._connect(); } - close () { + disconnect () { this._channel.onClose = () => {}; this._channel.close(); + this._connected = false; } send (msg) { @@ -61,6 +72,12 @@ class BaseArdourClient { } // Private methods + + async _connect () { + await this._channel.open(); + this._connected = true; + this._fireCallbacks('connected'); + } _send (node, addr, val) { const msg = new Message(node, addr, val); @@ -75,6 +92,10 @@ class BaseArdourClient { }); } + async _sendRecvSingle (node, addr, val) { + return await this._sendAndReceive (node, addr, val)[0]; + } + _onChannelMessage (msg) { if (this._pendingRequest && (this._pendingRequest.hash == msg.hash)) { this._pendingRequest.resolve(msg.val); @@ -91,9 +112,9 @@ class BaseArdourClient { return s[0].toUpperCase() + s.slice(1).toLowerCase(); }).join(''); - for (const callback of this._callbacks) { - if (method in callback) { - callback[method](...args) + for (const callbacks of this._callbacks) { + if (method in callbacks) { + callbacks[method](...args) } } } @@ -102,15 +123,19 @@ class BaseArdourClient { return new Error(`HTTP response status ${status}`); } + async _sleep (t) { + return new Promise(resolve => setTimeout(resolve, 1000)); + } + } export class ArdourClient extends mixin(BaseArdourClient, ControlMixin, MetadataMixin) {} function mixin (dstClass, ...classes) { for (const srcClass of classes) { - for (const methName of Object.getOwnPropertyNames(srcClass.prototype)) { - if (methName != 'constructor') { - dstClass.prototype[methName] = srcClass.prototype[methName]; + for (const propName of Object.getOwnPropertyNames(srcClass.prototype)) { + if (propName != 'constructor') { + dstClass.prototype[propName] = srcClass.prototype[propName]; } } } diff --git a/share/web_surfaces/shared/callback.js b/share/web_surfaces/shared/callback.js index a510b118cf..9da8b420d2 100644 --- a/share/web_surfaces/shared/callback.js +++ b/share/web_surfaces/shared/callback.js @@ -20,14 +20,34 @@ export class ArdourCallback { + // Connection status + onConnected () {} + onDisconnected () {} + + // All messages and errors + onMessage (msg) {} + onError (error) {} + + // Globals onTempo (bpm) {} + + // Strips + onStripDesc (stripId, name) {} + onStripMeter (stripId, db) {} onStripGain (stripId, db) {} onStripPan (stripId, value) {} onStripMute (stripId, value) {} + + // Strip plugins + onStripPluginDesc (stripId, pluginId, name) {} onStripPluginEnable (stripId, pluginId, value) {} - onStripPluginParamValue (stripId, pluginId, paramId, value) {} - onMessage (msg) {} - onError (error) {} + // Strip plugin parameters + // valueType + // 'b' : boolean + // 'i' : integer + // 'd' : double + onStripPluginParamDesc (stripId, pluginId, paramId, name, valueType, min, max, isLog) {} + onStripPluginParamValue (stripId, pluginId, paramId, value) {} - } +} diff --git a/share/web_surfaces/shared/control.js b/share/web_surfaces/shared/control.js index 19116bf4c7..94ffa0a83d 100644 --- a/share/web_surfaces/shared/control.js +++ b/share/web_surfaces/shared/control.js @@ -23,27 +23,27 @@ import { ANode } from './message.js'; export class ControlMixin { async getTempo () { - return (await this._sendAndReceive(ANode.TEMPO))[0]; + return await this._sendRecvSingle(ANode.TEMPO); } async getStripGain (stripId) { - return (await this._sendAndReceive(ANode.STRIP_GAIN, [stripId]))[0]; + return await this._sendRecvSingle(ANode.STRIP_GAIN, [stripId]); } async getStripPan (stripId) { - return (await this._sendAndReceive(ANode.STRIP_PAN, [stripId]))[0]; + return await this._sendRecvSingle(ANode.STRIP_PAN, [stripId]); } async getStripMute (stripId) { - return (await this._sendAndReceive(ANode.STRIP_MUTE, [stripId]))[0]; + return await this._sendRecvSingle(ANode.STRIP_MUTE, [stripId]); } async getStripPluginEnable (stripId, pluginId) { - return (await this._sendAndReceive(ANode.STRIP_PLUGIN_ENABLE, [stripId, pluginId]))[0]; + return await this._sendRecvSingle(ANode.STRIP_PLUGIN_ENABLE, [stripId, pluginId]); } async getStripPluginParamValue (stripId, pluginId, paramId) { - return (await this._sendAndReceive(ANode.STRIP_PLUGIN_PARAM_VALUE, [stripId, pluginId, paramId]))[0]; + return await this._sendRecvSingle(ANode.STRIP_PLUGIN_PARAM_VALUE, [stripId, pluginId, paramId]); } setTempo (bpm) { diff --git a/share/web_surfaces/shared/message.js b/share/web_surfaces/shared/message.js index 6c81cd972f..0252c9fa63 100644 --- a/share/web_surfaces/shared/message.js +++ b/share/web_surfaces/shared/message.js @@ -19,15 +19,15 @@ export const JSON_INF = 1.0e+128; export const ANode = Object.freeze({ - TEMPO: 'tempo', - STRIP_DESC: 'strip_desc', - STRIP_METER: 'strip_meter', - STRIP_GAIN: 'strip_gain', - STRIP_PAN: 'strip_pan', - STRIP_MUTE: 'strip_mute', - STRIP_PLUGIN_DESC: 'strip_plugin_desc', - STRIP_PLUGIN_ENABLE: 'strip_plugin_enable', - STRIP_PLUGIN_PARAM_DESC: 'strip_plugin_param_desc', + TEMPO: 'tempo', + STRIP_DESC: 'strip_desc', + STRIP_METER: 'strip_meter', + STRIP_GAIN: 'strip_gain', + STRIP_PAN: 'strip_pan', + STRIP_MUTE: 'strip_mute', + STRIP_PLUGIN_DESC: 'strip_plugin_desc', + STRIP_PLUGIN_ENABLE: 'strip_plugin_enable', + STRIP_PLUGIN_PARAM_DESC: 'strip_plugin_param_desc', STRIP_PLUGIN_PARAM_VALUE: 'strip_plugin_param_value' }); @@ -49,7 +49,7 @@ export class Message { } } - static hash (node, addr) { + static nodeAddrId (node, addr) { return [node].concat(addr || []).join('_'); } @@ -74,8 +74,8 @@ export class Message { return JSON.stringify({node: this.node, addr: this.addr, val: val}); } - get hash () { - return Message.hash(this.node, this.addr); + get nodeAddrId () { + return Message.nodeAddrId(this.node, this.addr); } toString () { -- cgit v1.2.3