mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-16 03:36:32 +01:00
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:
parent
612c71aa25
commit
50ba8dea96
9 changed files with 191 additions and 145 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (stripId, name) {
|
||||
const domId = `strip-${stripId}`;
|
||||
if (document.getElementById(domId) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
function createStrip (addr, name) {
|
||||
const id = `strip-${addr[0]}`;
|
||||
const strips = document.getElementById('strips');
|
||||
const div = createElem(`<div class="strip" id="${id}"></div>`, strips);
|
||||
createElem(`<label class="comp-name" for="${id}">∿  ${name}</label>`, div);
|
||||
const div = createElem(`<div class="strip" id="${domId}"></div>`, strips);
|
||||
createElem(`<label class="comp-name" for="${domId}">∿  ${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">⨍  ${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 (stripId, pluginId, paramId, name, valueType, min, max, isLog) {
|
||||
const domId = `param-${stripId}-${pluginId}-${paramId}`;
|
||||
if (document.getElementById(domId) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
function createStripPluginParam (addr, name, dataType, min, max, isLog) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -62,6 +73,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);
|
||||
this.send(msg);
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue