13
0

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
This commit is contained in:
Luciano Iam 2020-04-14 22:58:44 +02:00 committed by Robin Gareus
parent 612c71aa25
commit 50ba8dea96
Signed by: rgareus
GPG Key ID: A090BCE02CF57F04
9 changed files with 191 additions and 145 deletions

View File

@ -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);

View File

@ -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
}

View File

@ -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);
}

View File

@ -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(`<div class="strip" id="${id}"></div>`, strips);
createElem(`<label class="comp-name" for="${id}">∿&emsp;&emsp;${name}</label>`, div);
const div = createElem(`<div class="strip" id="${domId}"></div>`, strips);
createElem(`<label class="comp-name" for="${domId}">∿&emsp;&emsp;${name}</label>`, 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 class="strip-slider"></div>`, div);
createElem(`<label>Gain</label>`, 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 class="strip-slider"></div>`, div);
createElem(`<label>Pan</label>`, 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(`<div class="plugin" id="${id}"></div>`, 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(`<div class="plugin" id="${domId}"></div>`, strip);
createElem(`<label class="comp-name">⨍&emsp;&emsp;${name}</label>`, 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(`<div class="plugin-param ${cssClass}" id="${id}"></div>`, plugin);
createElem(`<label for="${id}">${name}</label>`, div);
const plugin = document.getElementById(`plugin-${stripId}-${pluginId}`);
const div = createElem(`<div class="plugin-param ${cssClass}" id="${domId}"></div>`, plugin);
createElem(`<label for="${domId}">${name}</label>`, 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) {

View File

@ -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, `<input type="checkbox" class="widget-switch">`);
constructor () {
super (`<input type="checkbox" class="widget-switch">`);
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 = `<input type="range" class="widget-slider"
min="${min}" max="${max}" step="${step}">`;
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, `<label></label>`);
constructor () {
super(`<label></label>`);
}
set value (val) {

View File

@ -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];
}
}
}

View File

@ -20,14 +20,34 @@
export class ArdourCallback {
onTempo (bpm) {}
onStripGain (stripId, db) {}
onStripPan (stripId, value) {}
onStripMute (stripId, value) {}
onStripPluginEnable (stripId, pluginId, value) {}
onStripPluginParamValue (stripId, pluginId, paramId, value) {}
// 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) {}
// Strip plugin parameters
// valueType
// 'b' : boolean
// 'i' : integer
// 'd' : double
onStripPluginParamDesc (stripId, pluginId, paramId, name, valueType, min, max, isLog) {}
onStripPluginParamValue (stripId, pluginId, paramId, value) {}
}

View File

@ -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) {

View File

@ -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 () {