mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-08 15:54:57 +01:00
WebSockets: implement a JavaScript object-oriented client API
Replace previous callback based basic client with an easier to use object-oriented API that further abstracts the low level details of the WebSockets Server surface messaging protocol. All built-in web surface demos were updated to use the new API.
This commit is contained in:
parent
5296ed141f
commit
ae4df127ad
18 changed files with 812 additions and 361 deletions
|
|
@ -55,11 +55,6 @@ WebsocketsDispatcher::dispatch (Client client, const NodeStateMessage& msg)
|
||||||
void
|
void
|
||||||
WebsocketsDispatcher::update_all_nodes (Client client)
|
WebsocketsDispatcher::update_all_nodes (Client client)
|
||||||
{
|
{
|
||||||
update (client, Node::tempo, globals ().tempo ());
|
|
||||||
update (client, Node::position_time, globals ().position_time ());
|
|
||||||
update (client, Node::transport_roll, globals ().transport_roll ());
|
|
||||||
update (client, Node::record_state, globals ().record_state ());
|
|
||||||
|
|
||||||
for (uint32_t strip_n = 0; strip_n < strips ().strip_count (); ++strip_n) {
|
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<Stripable> strip = strips ().nth_strip (strip_n);
|
||||||
|
|
||||||
|
|
@ -140,6 +135,11 @@ WebsocketsDispatcher::update_all_nodes (Client client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update (client, Node::tempo, globals ().tempo ());
|
||||||
|
update (client, Node::position_time, globals ().position_time ());
|
||||||
|
update (client, Node::transport_roll, globals ().transport_roll ());
|
||||||
|
update (client, Node::record_state, globals ().record_state ());
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,6 @@
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This example does not call the API methods in control.js,
|
|
||||||
// instead it couples the widgets directly to the message stream
|
|
||||||
|
|
||||||
import { ANode, Message } from '/shared/message.js';
|
|
||||||
import { ArdourClient } from '/shared/ardour.js';
|
import { ArdourClient } from '/shared/ardour.js';
|
||||||
|
|
||||||
import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
|
import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
|
||||||
|
|
@ -29,108 +25,127 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
|
||||||
|
|
||||||
const MAX_LOG_LINES = 1000;
|
const MAX_LOG_LINES = 1000;
|
||||||
|
|
||||||
const ardour = new ArdourClient(location.host);
|
const ardour = new ArdourClient();
|
||||||
const widgets = {};
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
||||||
function main () {
|
function main () {
|
||||||
|
ardour.handlers = {
|
||||||
|
onConnected: (connected) => {
|
||||||
|
if (connected) {
|
||||||
|
log('Client connected', 'info');
|
||||||
|
} else {
|
||||||
|
log('Client disconnected', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage: (message, inbound) => {
|
||||||
|
if (inbound) {
|
||||||
|
log(`↙ ${message}`, 'message-in');
|
||||||
|
} else {
|
||||||
|
log(`↗ ${message}`, 'message-out');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ardour.getSurfaceManifest().then((manifest) => {
|
ardour.getSurfaceManifest().then((manifest) => {
|
||||||
const div = document.getElementById('manifest');
|
const div = document.getElementById('manifest');
|
||||||
div.innerHTML = `${manifest.name.toUpperCase()} v${manifest.version} — ${manifest.description}`;
|
div.innerHTML = manifest.name.toUpperCase()
|
||||||
|
+ ' v' + manifest.version + ' — ' + manifest.description;
|
||||||
});
|
});
|
||||||
|
|
||||||
ardour.addCallbacks({
|
ardour.mixer.on('ready', () => {
|
||||||
onConnected: (error) => { log('Client connected', 'info'); },
|
const div = document.getElementById('strips');
|
||||||
onDisconnected: (error) => { log('Client disconnected', 'error'); },
|
for (const strip of ardour.mixer.strips) {
|
||||||
onMessage: processMessage,
|
createStrip(strip, div);
|
||||||
onStripDescription: createStrip,
|
}
|
||||||
onStripPluginDescription: createStripPlugin,
|
|
||||||
onStripPluginParamDescription: createStripPluginParam
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ardour.connect();
|
ardour.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStrip (stripId, name, isVca) {
|
function createStrip (strip, parentDiv) {
|
||||||
const domId = `strip-${stripId}`;
|
const domId = `strip-${strip.addrId}`;
|
||||||
if (document.getElementById(domId) != null) {
|
if (document.getElementById(domId) != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strips = document.getElementById('strips');
|
const div = createElem(`<div class="strip" id="${domId}"></div>`, parentDiv);
|
||||||
const div = createElem(`<div class="strip" id="${domId}"></div>`, strips);
|
createElem(`<label class="comp-name" for="${domId}">∿  ${strip.name}</label>`, div);
|
||||||
createElem(`<label class="comp-name" for="${domId}">∿  ${name}</label>`, div);
|
|
||||||
|
|
||||||
// meter
|
// meter
|
||||||
const meter = new StripMeter();
|
const meter = new StripMeter();
|
||||||
meter.el.classList.add('slider-meter');
|
meter.el.classList.add('slider-meter');
|
||||||
meter.appendTo(div);
|
meter.appendTo(div);
|
||||||
connectWidget(meter, ANode.STRIP_METER, stripId);
|
bind(strip, 'meter', meter);
|
||||||
|
|
||||||
// gain
|
// gain
|
||||||
let holder = createElem(`<div class="strip-slider"></div>`, div);
|
let holder = createElem(`<div class="strip-slider"></div>`, div);
|
||||||
createElem(`<label>Gain</label>`, holder);
|
createElem(`<label>Gain</label>`, holder);
|
||||||
const gain = new StripGainSlider();
|
const gain = new StripGainSlider();
|
||||||
gain.appendTo(holder);
|
gain.appendTo(holder);
|
||||||
connectWidget(gain, ANode.STRIP_GAIN, stripId);
|
bind(strip, 'gain', gain);
|
||||||
|
|
||||||
if (!isVca) {
|
if (!strip.isVca) {
|
||||||
// pan
|
// pan
|
||||||
holder = createElem(`<div class="strip-slider"></div>`, div);
|
holder = createElem(`<div class="strip-slider"></div>`, div);
|
||||||
createElem(`<label>Pan</label>`, holder);
|
createElem(`<label>Pan</label>`, holder);
|
||||||
const pan = new StripPanSlider();
|
const pan = new StripPanSlider();
|
||||||
pan.appendTo(holder);
|
pan.appendTo(holder);
|
||||||
connectWidget(pan, ANode.STRIP_PAN, stripId);
|
bind(strip, 'pan', pan);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const plugin of strip.plugins) {
|
||||||
|
createStripPlugin(plugin, div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStripPlugin (stripId, pluginId, name) {
|
function createStripPlugin (plugin, parentDiv) {
|
||||||
const domId = `plugin-${stripId}-${pluginId}`;
|
const domId = `plugin-${plugin.addrId}`;
|
||||||
if (document.getElementById(domId) != null) {
|
if (document.getElementById(domId) != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strip = document.getElementById(`strip-${stripId}`);
|
const div = createElem(`<div class="plugin" id="${domId}"></div>`, parentDiv);
|
||||||
const div = createElem(`<div class="plugin" id="${domId}"></div>`, strip);
|
|
||||||
createElem(`<label class="comp-name">⨍  ${name}</label>`, div);
|
createElem(`<label class="comp-name">⨍  ${name}</label>`, div);
|
||||||
|
|
||||||
const enable = new Switch();
|
const enable = new Switch();
|
||||||
enable.el.classList.add('plugin-enable');
|
enable.el.classList.add('plugin-enable');
|
||||||
enable.appendTo(div);
|
enable.appendTo(div);
|
||||||
connectWidget(enable, ANode.STRIP_PLUGIN_ENABLE, stripId, pluginId);
|
bind(plugin, 'enable', enable);
|
||||||
|
|
||||||
|
for (const param of plugin.parameters) {
|
||||||
|
createStripPluginParam(param, div);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStripPluginParam (stripId, pluginId, paramId, name, valueType, min, max, isLog) {
|
function createStripPluginParam (param, parentDiv) {
|
||||||
const domId = `param-${stripId}-${pluginId}-${paramId}`;
|
const domId = `param-${param.addrId}`;
|
||||||
if (document.getElementById(domId) != null) {
|
if (document.getElementById(domId) != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let param, cssClass;
|
let widget, cssClass;
|
||||||
|
|
||||||
if (valueType == 'b') {
|
if (param.valueType.isBoolean) {
|
||||||
cssClass = 'boolean';
|
cssClass = 'boolean';
|
||||||
param = new Switch();
|
widget = new Switch();
|
||||||
} else if (valueType == 'i') {
|
} else if (param.valueType.isInteger) {
|
||||||
cssClass = 'discrete';
|
cssClass = 'discrete';
|
||||||
param = new DiscreteSlider(min, max);
|
widget = new DiscreteSlider(param.min, param.max);
|
||||||
} else if (valueType == 'd') {
|
} else if (param.valueType.isDouble) {
|
||||||
cssClass = 'continuous';
|
cssClass = 'continuous';
|
||||||
if (isLog) {
|
if (param.isLog) {
|
||||||
param = new LogarithmicSlider(min, max);
|
widget = new LogarithmicSlider(param.min, param.max);
|
||||||
} else {
|
} else {
|
||||||
param = new ContinuousSlider(min, max);
|
widget = new ContinuousSlider(param.min, param.max);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = document.getElementById(`plugin-${stripId}-${pluginId}`);
|
const div = createElem(`<div class="plugin-param ${cssClass}" id="${domId}"></div>`, parentDiv);
|
||||||
const div = createElem(`<div class="plugin-param ${cssClass}" id="${domId}"></div>`, plugin);
|
createElem(`<label for="${domId}">${param.name}</label>`, div);
|
||||||
createElem(`<label for="${domId}">${name}</label>`, div);
|
|
||||||
|
|
||||||
param.el.name = domId;
|
widget.el.name = domId;
|
||||||
param.appendTo(div);
|
widget.appendTo(div);
|
||||||
connectWidget(param, ANode.STRIP_PLUGIN_PARAM_VALUE, stripId, pluginId, paramId);
|
bind(param, 'value', widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createElem (html, parent) {
|
function createElem (html, parent) {
|
||||||
|
|
@ -146,24 +161,12 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectWidget (widget, node, ...addr) {
|
function bind (component, property, widget) {
|
||||||
const nodeAddrId = Message.nodeAddrId(node, addr);
|
// ardour → ui
|
||||||
|
widget.value = component[property];
|
||||||
widgets[nodeAddrId] = widget;
|
component.on(property, (value) => widget.value = value);
|
||||||
|
// ui → ardour
|
||||||
widget.callback = (val) => {
|
widget.callback = (value) => component[property] = value;
|
||||||
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) {
|
function log (message, className) {
|
||||||
|
|
@ -181,4 +184,6 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
|
||||||
output.scrollTop = output.scrollHeight;
|
output.scrollTop = output.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
<WebSurface>
|
<WebSurface>
|
||||||
<Name value="Mixer Demo"/>
|
<Name value="Mixer Demo"/>
|
||||||
<Description value="Mixer control capabilities demo aimed at developers"/>
|
<Description value="Mixer control capabilities demo aimed at developers"/>
|
||||||
<Version value="0.1.0"/>
|
<Version value="0.1.1"/>
|
||||||
</WebSurface>
|
</WebSurface>
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ import { ArdourClient } from '/shared/ardour.js';
|
||||||
(() => {
|
(() => {
|
||||||
|
|
||||||
const dom = {
|
const dom = {
|
||||||
main: document.getElementById('main'),
|
main : document.getElementById('main'),
|
||||||
time: document.getElementById('time'),
|
time : document.getElementById('time'),
|
||||||
roll: document.getElementById('roll'),
|
roll : document.getElementById('roll'),
|
||||||
record: document.getElementById('record'),
|
record : document.getElementById('record'),
|
||||||
fullscreen: document.getElementById('fullscreen')
|
fullscreen : document.getElementById('fullscreen')
|
||||||
};
|
};
|
||||||
|
|
||||||
const ardour = new ArdourClient();
|
const ardour = new ArdourClient();
|
||||||
|
|
@ -36,12 +36,9 @@ import { ArdourClient } from '/shared/ardour.js';
|
||||||
function main () {
|
function main () {
|
||||||
addDomEventListeners();
|
addDomEventListeners();
|
||||||
|
|
||||||
ardour.addCallbacks({
|
ardour.transport.on('time', setPosition);
|
||||||
onError: console.log,
|
ardour.transport.on('roll', setRolling);
|
||||||
onPositionTime: setPosition,
|
ardour.transport.on('record', setRecord);
|
||||||
onTransportRoll: setRolling,
|
|
||||||
onRecordState: setRecord
|
|
||||||
});
|
|
||||||
|
|
||||||
ardour.connect();
|
ardour.connect();
|
||||||
}
|
}
|
||||||
|
|
@ -52,14 +49,14 @@ import { ArdourClient } from '/shared/ardour.js';
|
||||||
|
|
||||||
const roll = () => {
|
const roll = () => {
|
||||||
setRolling(!_rolling);
|
setRolling(!_rolling);
|
||||||
ardour.setTransportRoll(_rolling);
|
ardour.transport.roll = _rolling;
|
||||||
};
|
};
|
||||||
|
|
||||||
dom.roll.addEventListener(touchOrClick, roll);
|
dom.roll.addEventListener(touchOrClick, roll);
|
||||||
|
|
||||||
const record = () => {
|
const record = () => {
|
||||||
setRecord(!_record);
|
setRecord(!_record);
|
||||||
ardour.setRecordState(_record);
|
ardour.transport.record = _record;
|
||||||
};
|
};
|
||||||
|
|
||||||
dom.record.addEventListener(touchOrClick, record);
|
dom.record.addEventListener(touchOrClick, record);
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
<WebSurface>
|
<WebSurface>
|
||||||
<Name value="Transport"/>
|
<Name value="Transport"/>
|
||||||
<Description value="Provides basic transport control"/>
|
<Description value="Provides basic transport control"/>
|
||||||
<Version value="0.1.0"/>
|
<Version value="0.1.1"/>
|
||||||
</WebSurface>
|
</WebSurface>
|
||||||
|
|
|
||||||
|
|
@ -16,40 +16,54 @@
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ControlMixin } from './control.js';
|
import { MessageChannel } from './base/channel.js';
|
||||||
import { MetadataMixin } from './metadata.js';
|
import { StateNode } from './base/protocol.js';
|
||||||
import { Message } from './message.js';
|
import { Mixer } from './components/mixer.js';
|
||||||
import { MessageChannel } from './channel.js';
|
import { Transport } from './components/transport.js';
|
||||||
|
|
||||||
// See ControlMixin and MetadataMixin for available APIs
|
export class ArdourClient {
|
||||||
// See ArdourCallback for an example callback implementation
|
|
||||||
|
|
||||||
class BaseArdourClient {
|
constructor (handlers, options) {
|
||||||
|
this._options = options || {};
|
||||||
constructor (host) {
|
this._components = [];
|
||||||
this._callbacks = [];
|
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
this._pendingRequest = null;
|
|
||||||
this._channel = new MessageChannel(host || location.host);
|
|
||||||
|
|
||||||
this._channel.onError = (error) => {
|
this._channel = new MessageChannel(this._options['host'] || location.host);
|
||||||
this._fireCallbacks('error', error);
|
|
||||||
|
this._channel.onMessage = (msg, inbound) => {
|
||||||
|
this._handleMessage(msg, inbound);
|
||||||
};
|
};
|
||||||
|
|
||||||
this._channel.onMessage = (msg) => {
|
if (!('components' in this._options) || this._options['components']) {
|
||||||
this._onChannelMessage(msg);
|
this._mixer = new Mixer(this._channel);
|
||||||
};
|
this._transport = new Transport(this._channel);
|
||||||
|
this._components.push(this._mixer, this._transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers = handlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCallbacks (callbacks) {
|
set handlers (handlers) {
|
||||||
this._callbacks.push(callbacks);
|
this._handlers = handlers || {};
|
||||||
|
this._channel.onError = this._handlers['onError'] || console.log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Access to the object-oriented API (enabled by default)
|
||||||
|
|
||||||
|
get mixer () {
|
||||||
|
return this._mixer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get transport () {
|
||||||
|
return this._transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Low level control messages flow through a WebSocket
|
||||||
|
|
||||||
async connect (autoReconnect) {
|
async connect (autoReconnect) {
|
||||||
this._channel.onClose = async () => {
|
this._channel.onClose = async () => {
|
||||||
if (this._connected) {
|
if (this._connected) {
|
||||||
this._fireCallbacks('disconnected');
|
this._setConnected(false);
|
||||||
this._connected = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((autoReconnect == null) || autoReconnect) {
|
if ((autoReconnect == null) || autoReconnect) {
|
||||||
|
|
@ -71,50 +85,69 @@ class BaseArdourClient {
|
||||||
this._channel.send(msg);
|
this._channel.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private methods
|
async sendAndReceive (msg) {
|
||||||
|
return await this._channel.sendAndReceive(msg);
|
||||||
async _connect () {
|
|
||||||
await this._channel.open();
|
|
||||||
this._connected = true;
|
|
||||||
this._fireCallbacks('connected');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_send (node, addr, val) {
|
// Surface metadata API goes over HTTP
|
||||||
const msg = new Message(node, addr, val);
|
|
||||||
this.send(msg);
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _sendAndReceive (node, addr, val) {
|
async getAvailableSurfaces () {
|
||||||
return new Promise((resolve, reject) => {
|
const response = await fetch('/surfaces.json');
|
||||||
const nodeAddrId = this._send(node, addr, val).nodeAddrId;
|
|
||||||
this._pendingRequest = {resolve: resolve, nodeAddrId: nodeAddrId};
|
if (response.status == 200) {
|
||||||
});
|
return await response.json();
|
||||||
}
|
|
||||||
|
|
||||||
async _sendRecvSingle (node, addr, val) {
|
|
||||||
return (await this._sendAndReceive (node, addr, val))[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
_onChannelMessage (msg) {
|
|
||||||
if (this._pendingRequest && (this._pendingRequest.nodeAddrId == msg.nodeAddrId)) {
|
|
||||||
this._pendingRequest.resolve(msg.val);
|
|
||||||
this._pendingRequest = null;
|
|
||||||
} else {
|
} else {
|
||||||
this._fireCallbacks('message', msg);
|
throw this._fetchResponseStatusError(response.status);
|
||||||
this._fireCallbacks(msg.node, ...msg.addr, ...msg.val);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fireCallbacks (name, ...args) {
|
async getSurfaceManifest () {
|
||||||
// name_with_underscores -> onNameWithUnderscores
|
const response = await fetch('manifest.xml');
|
||||||
const method = 'on' + name.split('_').map((s) => {
|
|
||||||
return s[0].toUpperCase() + s.slice(1).toLowerCase();
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
for (const callbacks of this._callbacks) {
|
if (response.status == 200) {
|
||||||
if (method in callbacks) {
|
const manifest = {};
|
||||||
callbacks[method](...args)
|
const xmlText = await response.text();
|
||||||
|
const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml');
|
||||||
|
|
||||||
|
for (const child of xmlDoc.children[0].children) {
|
||||||
|
manifest[child.tagName.toLowerCase()] = child.getAttribute('value');
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
} else {
|
||||||
|
throw this._fetchResponseStatusError(response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private methods
|
||||||
|
|
||||||
|
async _sleep (t) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _connect () {
|
||||||
|
await this._channel.open();
|
||||||
|
this._setConnected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setConnected (connected) {
|
||||||
|
this._connected = connected;
|
||||||
|
|
||||||
|
if (this._handlers['onConnected']) {
|
||||||
|
this._handlers['onConnected'](this._connected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMessage (msg, inbound) {
|
||||||
|
if (this._handlers['onMessage']) {
|
||||||
|
this._handlers['onMessage'](msg, inbound);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inbound) {
|
||||||
|
for (const component of this._components) {
|
||||||
|
if (component.handleMessage(msg)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,21 +156,4 @@ class BaseArdourClient {
|
||||||
return new Error(`HTTP response status ${status}`);
|
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 propName of Object.getOwnPropertyNames(srcClass.prototype)) {
|
|
||||||
if (propName != 'constructor') {
|
|
||||||
dstClass.prototype[propName] = srcClass.prototype[propName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dstClass;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,14 @@
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ANode, Message } from './message.js';
|
import { Message } from './protocol.js';
|
||||||
|
|
||||||
export class MessageChannel {
|
export class MessageChannel {
|
||||||
|
|
||||||
constructor (host) {
|
constructor (host) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/URL/host
|
// https://developer.mozilla.org/en-US/docs/Web/API/URL/host
|
||||||
this._host = host;
|
this._host = host;
|
||||||
|
this._pending = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async open () {
|
async open () {
|
||||||
|
|
@ -34,7 +35,14 @@ export class MessageChannel {
|
||||||
this._socket.onerror = (error) => this.onError(error);
|
this._socket.onerror = (error) => this.onError(error);
|
||||||
|
|
||||||
this._socket.onmessage = (event) => {
|
this._socket.onmessage = (event) => {
|
||||||
this.onMessage (Message.fromJsonText(event.data));
|
const msg = Message.fromJsonText(event.data);
|
||||||
|
|
||||||
|
if (this._pending && (this._pending.nodeAddrId == msg.nodeAddrId)) {
|
||||||
|
this._pending.resolve(msg);
|
||||||
|
this._pending = null;
|
||||||
|
} else {
|
||||||
|
this.onMessage(msg, true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._socket.onopen = resolve;
|
this._socket.onopen = resolve;
|
||||||
|
|
@ -43,18 +51,31 @@ export class MessageChannel {
|
||||||
|
|
||||||
close () {
|
close () {
|
||||||
this._socket.close();
|
this._socket.close();
|
||||||
|
|
||||||
|
if (this._pending) {
|
||||||
|
this._pending.reject(Error('MessageChannel: socket closed awaiting response'));
|
||||||
|
this._pending = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
send (msg) {
|
send (msg) {
|
||||||
if (this._socket) {
|
if (this._socket) {
|
||||||
this._socket.send(msg.toJsonText());
|
this._socket.send(msg.toJsonText());
|
||||||
|
this.onMessage(msg, false);
|
||||||
} else {
|
} else {
|
||||||
throw Error('MessageChannel: cannot call send() before open()');
|
this.onError(Error('MessageChannel: cannot call send() before open()'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendAndReceive (msg) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._pending = {resolve: resolve, reject: reject, nodeAddrId: msg.nodeAddrId};
|
||||||
|
this.send(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onClose () {}
|
onClose () {}
|
||||||
onError (error) {}
|
onError (error) {}
|
||||||
onMessage (msg) {}
|
onMessage (msg, inbound) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
93
share/web_surfaces/shared/base/component.js
Normal file
93
share/web_surfaces/shared/base/component.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Message } from './protocol.js';
|
||||||
|
import { Observable } from './observable.js';
|
||||||
|
|
||||||
|
export class Component extends Observable {
|
||||||
|
|
||||||
|
constructor (parent) {
|
||||||
|
super();
|
||||||
|
this._parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
get channel () {
|
||||||
|
return this._parent.channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
on (property, callback) {
|
||||||
|
this.addObserver(property, (self) => callback(self[property]));
|
||||||
|
}
|
||||||
|
|
||||||
|
send (node, addr, val) {
|
||||||
|
this.channel.send(new Message(node, addr, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (node, addr, val) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage (msg) {
|
||||||
|
return this.handle(msg.node, msg.addr, msg.val);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocal (property, value) {
|
||||||
|
this['_' + property] = value;
|
||||||
|
this.notifyObservers(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRemote (property, value, node, addr) {
|
||||||
|
this['_' + property] = value;
|
||||||
|
this.send(node, addr || [], [value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RootComponent extends Component {
|
||||||
|
|
||||||
|
constructor (channel) {
|
||||||
|
super(null);
|
||||||
|
this._channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
get channel () {
|
||||||
|
return this._channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddressableComponent extends Component {
|
||||||
|
|
||||||
|
constructor (parent, addr) {
|
||||||
|
super(parent);
|
||||||
|
this._addr = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
get addr () {
|
||||||
|
return this._addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
get addrId () {
|
||||||
|
return this._addr.join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRemote (property, value, node) {
|
||||||
|
super.updateRemote(property, value, node, this.addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
share/web_surfaces/shared/base/observable.js
Normal file
64
share/web_surfaces/shared/base/observable.js
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Observable {
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this._observers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
addObserver (property, observer) {
|
||||||
|
// property=undefined means the caller is interested in observing all properties
|
||||||
|
if (!(property in this._observers)) {
|
||||||
|
this._observers[property] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._observers[property].push(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeObserver (property, observer) {
|
||||||
|
// property=undefined means the caller is not interested in any property anymore
|
||||||
|
if (typeof(property) == 'undefined') {
|
||||||
|
for (const property in this._observers) {
|
||||||
|
this.removeObserver(property, observer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = this._observers[property].indexOf(observer);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
this._observers[property].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyObservers (property) {
|
||||||
|
// always notify observers that observe all properties
|
||||||
|
if (undefined in this._observers) {
|
||||||
|
for (const observer of this._observers[undefined]) {
|
||||||
|
observer(this, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property in this._observers) {
|
||||||
|
for (const observer of this._observers[property]) {
|
||||||
|
observer(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
export const JSON_INF = 1.0e+128;
|
export const JSON_INF = 1.0e+128;
|
||||||
|
|
||||||
export const ANode = Object.freeze({
|
export const StateNode = Object.freeze({
|
||||||
TEMPO: 'tempo',
|
TEMPO: 'tempo',
|
||||||
POSITION_TIME: 'position_time',
|
POSITION_TIME: 'position_time',
|
||||||
TRANSPORT_ROLL: 'transport_roll',
|
TRANSPORT_ROLL: 'transport_roll',
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Example empty callback
|
|
||||||
|
|
||||||
export class ArdourCallback {
|
|
||||||
|
|
||||||
// Connection status
|
|
||||||
onConnected () {}
|
|
||||||
onDisconnected () {}
|
|
||||||
|
|
||||||
// All messages and errors
|
|
||||||
onMessage (msg) {}
|
|
||||||
onError (error) {}
|
|
||||||
|
|
||||||
// Globals
|
|
||||||
onTempo (bpm) {}
|
|
||||||
onPositionTime (seconds) {}
|
|
||||||
onTransportRoll (value) {}
|
|
||||||
onRecordState (value) {}
|
|
||||||
|
|
||||||
// Strips
|
|
||||||
onStripDescription (stripId, name, isVca) {}
|
|
||||||
onStripMeter (stripId, db) {}
|
|
||||||
onStripGain (stripId, db) {}
|
|
||||||
onStripPan (stripId, value) {}
|
|
||||||
onStripMute (stripId, value) {}
|
|
||||||
|
|
||||||
// Strip plugins
|
|
||||||
onStripPluginDescription (stripId, pluginId, name) {}
|
|
||||||
onStripPluginEnable (stripId, pluginId, value) {}
|
|
||||||
|
|
||||||
// Strip plugin parameters
|
|
||||||
// valueType
|
|
||||||
// 'b' : boolean
|
|
||||||
// 'i' : integer
|
|
||||||
// 'd' : double
|
|
||||||
onStripPluginParamDescription (stripId, pluginId, paramId, name, valueType, min, max, isLog) {}
|
|
||||||
onStripPluginParamValue (stripId, pluginId, paramId, value) {}
|
|
||||||
|
|
||||||
}
|
|
||||||
87
share/web_surfaces/shared/components/mixer.js
Normal file
87
share/web_surfaces/shared/components/mixer.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RootComponent } from '../base/component.js';
|
||||||
|
import { StateNode } from '../base/protocol.js';
|
||||||
|
import { Strip } from './strip.js';
|
||||||
|
|
||||||
|
export class Mixer extends RootComponent {
|
||||||
|
|
||||||
|
constructor (channel) {
|
||||||
|
super(channel);
|
||||||
|
this._strips = {};
|
||||||
|
this._ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ready () {
|
||||||
|
return this._ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
get strips () {
|
||||||
|
return Object.values(this._strips);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStripByName (name) {
|
||||||
|
name = name.trim().toLowerCase();
|
||||||
|
return this.strips.find(strip => strip.name.trim().toLowerCase() == name);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (node, addr, val) {
|
||||||
|
if (node.startsWith('strip')) {
|
||||||
|
if (node == StateNode.STRIP_DESCRIPTION) {
|
||||||
|
this._strips[addr] = new Strip(this, addr, val);
|
||||||
|
this.notifyObservers('strips');
|
||||||
|
} else {
|
||||||
|
const stripAddr = [addr[0]];
|
||||||
|
if (stripAddr in this._strips) {
|
||||||
|
this._strips[stripAddr].handle(node, addr, val);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
RECORD_STATE signals all mixer initial state has been sent because
|
||||||
|
it is the last message to arrive immediately after client connection,
|
||||||
|
see WebsocketsDispatcher::update_all_nodes() in dispatcher.cc
|
||||||
|
|
||||||
|
For this to work the mixer component needs to receive incoming
|
||||||
|
messages before the transport component, otherwise the latter would
|
||||||
|
consume RECORD_STATE.
|
||||||
|
|
||||||
|
Some ideas for a better implementation of mixer readiness detection:
|
||||||
|
|
||||||
|
- Implement message bundles like OSC to pack all initial state
|
||||||
|
updates into a single unit
|
||||||
|
- Move *_DESCRIPTION messages to single message with val={JSON data},
|
||||||
|
currently val only supports primitive data types
|
||||||
|
- Append a termination or mixer ready message in update_all_nodes(),
|
||||||
|
easiest but the least elegant
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!this._ready && (node == StateNode.RECORD_STATE)) {
|
||||||
|
this.updateLocal('ready', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
95
share/web_surfaces/shared/components/parameter.js
Normal file
95
share/web_surfaces/shared/components/parameter.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AddressableComponent } from '../base/component.js';
|
||||||
|
import { StateNode } from '../base/protocol.js';
|
||||||
|
|
||||||
|
class ValueType {
|
||||||
|
|
||||||
|
constructor (rawType) {
|
||||||
|
this._rawType = rawType;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBoolean () {
|
||||||
|
return this._rawType == 'b';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isInteger () {
|
||||||
|
return this._rawType == 'i';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDouble () {
|
||||||
|
return this._rawType == 'd';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Parameter extends AddressableComponent {
|
||||||
|
|
||||||
|
constructor (parent, addr, desc) {
|
||||||
|
super(parent, addr);
|
||||||
|
this._name = desc[0];
|
||||||
|
this._valueType = new ValueType(desc[1]);
|
||||||
|
this._min = desc[2];
|
||||||
|
this._max = desc[3];
|
||||||
|
this._isLog = desc[4];
|
||||||
|
this._value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get plugin () {
|
||||||
|
return this._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name () {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueType () {
|
||||||
|
return this._valueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
get min () {
|
||||||
|
return this._min;
|
||||||
|
}
|
||||||
|
|
||||||
|
get max () {
|
||||||
|
return this._max;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLog () {
|
||||||
|
return this._isLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value () {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value (value) {
|
||||||
|
this.updateRemote('value', value, StateNode.STRIP_PLUGIN_PARAM_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (node, addr, val) {
|
||||||
|
if (node == StateNode.STRIP_PLUGIN_PARAM_VALUE) {
|
||||||
|
this.updateLocal('value', val[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
72
share/web_surfaces/shared/components/plugin.js
Normal file
72
share/web_surfaces/shared/components/plugin.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AddressableComponent } from '../base/component.js';
|
||||||
|
import { Parameter } from './parameter.js';
|
||||||
|
import { StateNode } from '../base/protocol.js';
|
||||||
|
|
||||||
|
export class Plugin extends AddressableComponent {
|
||||||
|
|
||||||
|
constructor (parent, addr, desc) {
|
||||||
|
super(parent, addr);
|
||||||
|
this._parameters = {};
|
||||||
|
this._name = desc[0];
|
||||||
|
this._enable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get strip () {
|
||||||
|
return this._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
get parameters () {
|
||||||
|
return Object.values(this._parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name () {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get enable () {
|
||||||
|
return this._enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
set enable (value) {
|
||||||
|
this.updateRemote('enable', value, StateNode.STRIP_PLUGIN_ENABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (node, addr, val) {
|
||||||
|
if (node.startsWith('strip_plugin_param')) {
|
||||||
|
if (node == StateNode.STRIP_PLUGIN_PARAM_DESCRIPTION) {
|
||||||
|
this._parameters[addr] = new Parameter(this, addr, val);
|
||||||
|
this.notifyObservers('parameters');
|
||||||
|
} else {
|
||||||
|
if (addr in this._parameters) {
|
||||||
|
this._parameters[addr].handle(node, addr, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else if (node == StateNode.STRIP_PLUGIN_ENABLE) {
|
||||||
|
this.updateLocal('enable', val[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
116
share/web_surfaces/shared/components/strip.js
Normal file
116
share/web_surfaces/shared/components/strip.js
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AddressableComponent } from '../base/component.js';
|
||||||
|
import { Plugin } from './plugin.js';
|
||||||
|
import { StateNode } from '../base/protocol.js';
|
||||||
|
|
||||||
|
export class Strip extends AddressableComponent {
|
||||||
|
|
||||||
|
constructor (parent, addr, desc) {
|
||||||
|
super(parent, addr);
|
||||||
|
this._plugins = {};
|
||||||
|
this._name = desc[0];
|
||||||
|
this._isVca = desc[1];
|
||||||
|
this._meter = 0;
|
||||||
|
this._gain = 0;
|
||||||
|
this._pan = 0;
|
||||||
|
this._mute = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get plugins () {
|
||||||
|
return Object.values(this._plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name () {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVca () {
|
||||||
|
return this._isVca;
|
||||||
|
}
|
||||||
|
|
||||||
|
get meter () {
|
||||||
|
return this._meter;
|
||||||
|
}
|
||||||
|
|
||||||
|
get gain () {
|
||||||
|
return this._gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
set gain (db) {
|
||||||
|
this.updateRemote('gain', db, StateNode.STRIP_GAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
get pan () {
|
||||||
|
return this._pan;
|
||||||
|
}
|
||||||
|
|
||||||
|
set pan (value) {
|
||||||
|
this.updateRemote('pan', value, StateNode.STRIP_PAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mute () {
|
||||||
|
return this._mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
set mute (value) {
|
||||||
|
this.updateRemote('mute', value, StateNode.STRIP_MUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (node, addr, val) {
|
||||||
|
if (node.startsWith('strip_plugin')) {
|
||||||
|
if (node == StateNode.STRIP_PLUGIN_DESCRIPTION) {
|
||||||
|
|
||||||
|
this._plugins[addr] = new Plugin(this, addr, val);
|
||||||
|
this.notifyObservers('plugins');
|
||||||
|
} else {
|
||||||
|
const pluginAddr = [addr[0], addr[1]];
|
||||||
|
if (pluginAddr in this._plugins) {
|
||||||
|
this._plugins[pluginAddr].handle(node, addr, val);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
switch (node) {
|
||||||
|
case StateNode.STRIP_METER:
|
||||||
|
this.updateLocal('meter', val[0]);
|
||||||
|
break;
|
||||||
|
case StateNode.STRIP_GAIN:
|
||||||
|
this.updateLocal('gain', val[0]);
|
||||||
|
break;
|
||||||
|
case StateNode.STRIP_PAN:
|
||||||
|
this.updateLocal('pan', val[0]);
|
||||||
|
break;
|
||||||
|
case StateNode.STRIP_MUTE:
|
||||||
|
this.updateLocal('mute', val[0]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
81
share/web_surfaces/shared/components/transport.js
Normal file
81
share/web_surfaces/shared/components/transport.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RootComponent } from '../base/component.js';
|
||||||
|
import { StateNode } from '../base/protocol.js';
|
||||||
|
|
||||||
|
export class Transport extends RootComponent {
|
||||||
|
|
||||||
|
constructor (channel) {
|
||||||
|
super(channel);
|
||||||
|
this._time = 0;
|
||||||
|
this._tempo = 0;
|
||||||
|
this._roll = false;
|
||||||
|
this._record = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get time () {
|
||||||
|
return this._time;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tempo () {
|
||||||
|
return this._tempo;
|
||||||
|
}
|
||||||
|
|
||||||
|
set tempo (bpm) {
|
||||||
|
this.updateRemote('tempo', bpm, StateNode.TEMPO);
|
||||||
|
}
|
||||||
|
|
||||||
|
get roll () {
|
||||||
|
return this._roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
set roll (value) {
|
||||||
|
this.updateRemote('roll', value, StateNode.TRANSPORT_ROLL);
|
||||||
|
}
|
||||||
|
|
||||||
|
get record () {
|
||||||
|
return this._record;
|
||||||
|
}
|
||||||
|
|
||||||
|
set record (value) {
|
||||||
|
this.updateRemote('record', value, StateNode.RECORD_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (node, addr, val) {
|
||||||
|
switch (node) {
|
||||||
|
case StateNode.TEMPO:
|
||||||
|
this.updateLocal('tempo', val[0]);
|
||||||
|
break;
|
||||||
|
case StateNode.POSITION_TIME:
|
||||||
|
this.updateLocal('time', val[0]);
|
||||||
|
break;
|
||||||
|
case StateNode.TRANSPORT_ROLL:
|
||||||
|
this.updateLocal('roll', val[0]);
|
||||||
|
break;
|
||||||
|
case StateNode.RECORD_STATE:
|
||||||
|
this.updateLocal('record', val[0]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ANode } from './message.js';
|
|
||||||
|
|
||||||
// Surface control API over WebSockets
|
|
||||||
|
|
||||||
export class ControlMixin {
|
|
||||||
|
|
||||||
async getTempo () {
|
|
||||||
return await this._sendRecvSingle(ANode.TEMPO);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTransportRoll () {
|
|
||||||
return await this._sendRecvSingle(ANode.TRANSPORT_ROLL);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRecordState () {
|
|
||||||
return await this._sendRecvSingle(ANode.RECORD_STATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStripGain (stripId) {
|
|
||||||
return await this._sendRecvSingle(ANode.STRIP_GAIN, [stripId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStripPan (stripId) {
|
|
||||||
return await this._sendRecvSingle(ANode.STRIP_PAN, [stripId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStripMute (stripId) {
|
|
||||||
return await this._sendRecvSingle(ANode.STRIP_MUTE, [stripId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStripPluginEnable (stripId, pluginId) {
|
|
||||||
return await this._sendRecvSingle(ANode.STRIP_PLUGIN_ENABLE, [stripId, pluginId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStripPluginParamValue (stripId, pluginId, paramId) {
|
|
||||||
return await this._sendRecvSingle(ANode.STRIP_PLUGIN_PARAM_VALUE, [stripId, pluginId, paramId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTempo (bpm) {
|
|
||||||
this._send(ANode.TEMPO, [], [bpm]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransportRoll (value) {
|
|
||||||
this._send(ANode.TRANSPORT_ROLL, [], [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRecordState (value) {
|
|
||||||
this._send(ANode.RECORD_STATE, [], [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStripGain (stripId, db) {
|
|
||||||
this._send(ANode.STRIP_GAIN, [stripId], [db]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStripPan (stripId, value) {
|
|
||||||
this._send(ANode.STRIP_PAN, [stripId], [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStripMute (stripId, value) {
|
|
||||||
this._send(ANode.STRIP_MUTE, [stripId], [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStripPluginEnable (stripId, pluginId, value) {
|
|
||||||
this._send(ANode.STRIP_PLUGIN_ENABLE, [stripId, pluginId], [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStripPluginParamValue (stripId, pluginId, paramId, value) {
|
|
||||||
this._send(ANode.STRIP_PLUGIN_PARAM_VALUE, [stripId, pluginId, paramId], [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Surface metadata API over HTTP
|
|
||||||
|
|
||||||
export class MetadataMixin {
|
|
||||||
|
|
||||||
async getAvailableSurfaces () {
|
|
||||||
const response = await fetch('/surfaces.json');
|
|
||||||
|
|
||||||
if (response.status == 200) {
|
|
||||||
return await response.json();
|
|
||||||
} else {
|
|
||||||
throw this._fetchResponseStatusError(response.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSurfaceManifest () {
|
|
||||||
const response = await fetch('manifest.xml');
|
|
||||||
|
|
||||||
if (response.status == 200) {
|
|
||||||
const manifest = {};
|
|
||||||
const xmlText = await response.text();
|
|
||||||
const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml');
|
|
||||||
|
|
||||||
for (const child of xmlDoc.children[0].children) {
|
|
||||||
manifest[child.tagName.toLowerCase()] = child.getAttribute('value');
|
|
||||||
}
|
|
||||||
|
|
||||||
return manifest;
|
|
||||||
} else {
|
|
||||||
throw this._fetchResponseStatusError(response.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue