'use strict';
const debug = require('debug')('Cantabile');
const EndPoint = require('./EndPoint');
const EventEmitter = require('events');
/**
* Represents an active connection watching a source binding point for changes/invocations
* Returned from the {{#crossLink "Bindings/watch:method"}}{{/crossLink}} method.
*
* @class BindingWatcher
* @extends EventEmitter
*/
class BindingWatcher extends EventEmitter
{
constructor(owner, name, indicies, condition, listener)
{
super();
this.owner = owner;
this._name = name;
this._indicies = indicies;
this._condition = condition;
this._listener = listener;
this._value = null;
}
/**
* Returns the name of the binding point being listened to
*
* @property name
* @type {String}
*/
get name() { return this._name; }
/**
* Returns the indicies of the binding point being listened to
*
* @property indicies
* @type {Number[]}
*/
get indicies() { return this._indicies; }
/**
* Returns the condition of the binding point being listened to
*
* @property condition
* @type {Object}
*/
get condition() { return this._condition; }
/**
* Returns the last received value for the source binding point
*
* @property value
* @type {Object}
*/
get value() { return this._value; }
_start()
{
this.owner.post("/watch", {
name: this._name,
indicies: this._indicies,
condition: this._condition
}).then(r => {
this.owner._registerWatchId(r.data.watchId, this);
this._watchId = r.data.watchId;
if (r.data.value !== null && r.data.value !== undefined)
{
this._value = r.data.value;
this._fireInvoked();
}
});
}
_stop()
{
if (this.owner._epid && this._watchId)
{
this.owner.send("POST", "/unwatch", { watchId: this._watchId})
this.owner._revokeWatchId(this._watchId);
this._watchId = 0;
if (this._value !== null && this._value !== undefined)
{
this._value = null;
this._fireInvoked();
}
}
}
/**
* Stops monitoring this binding source
*
* @method unwatch
*/
unwatch()
{
this._stop();
this.owner._revokeWatcher(this);
}
_update(data)
{
this._value = data.value;
this._fireInvoked();
}
_fireInvoked()
{
// Function listener?
if (this._listener)
this._listener(this._value, this);
/**
* Fired when the source binding point is triggered
*
* @event invoked
* @param {Object} value The value supplied from the source binding
* @param {BindingWatcher} source This object
*/
this.emit('invoked', this.value, this);
}
}
/**
* Provides access to Cantabile's binding points.
*
* Access this object via the {{#crossLink "Cantabile/bindings:property"}}{{/crossLink}} property.
*
* @class Bindings
* @extends EndPoint
*/
class Bindings extends EndPoint
{
constructor(owner)
{
super(owner, "/api/bindings");
this._watchers = [];
this._watchIds = {};
}
_onOpen()
{
for (let i=0; i<this._watchers.length; i++)
{
this._watchers[i]._start();
}
}
_onClose()
{
for (let i=0; i<this._watchers.length; i++)
{
this._watchers[i]._stop();
}
}
/**
* Retrieves a list of available binding points
*
* If Cantabile is running on your local machine you can view this list
* directly at <http://localhost:35007/api/bindings/availableBindingPoints>
*
* @example
*
* let C = new CantabileApi();
* C.connect();
* console.log(await C.bindings.availableBindingPoints());
*
* @method availableBindingPoints
* @return {Promise|BindingPointInfo[]} A promise to return an array of BindingPointInfo
*/
async availableBindingPoints()
{
await this.owner.untilConnected();
return (await this.request("GET", "/availableBindingPoints")).data;
}
/**
* Invokes a target binding point
*
* If Cantabile is running on your local machine a full list of available binding
* points is [available here](http://localhost:35007/api/bindings/availableBindingPoints)
*
* @example
*
* Set the master output level gain
*
* C.bindings.invoke("global.masterLevels.outputGain", 0.5);
*
* @example
*
* Suspend the 2nd plugin in the song
*
* C.bindings.invoke("global.indexedPlugin.suspend", true, [
* 0, // Rack index (zero = song)
* 1 // Plugin index (zero based, 1 = the second plugin)
* ]);
*
*
* @example
*
* Sending a MIDI Controller Event
*
* C.bindings.invoke("midiInputPort.Main Keyboard", new {
* kind: "FineController",
* controller: 10,
* value: 1000,
* });
*
* @example
*
* Sending MIDI Data directly
*
* C.bindings.invoke("midiInputPort.Main Keyboard", [ 0xb0, 23, 99 ]);
*
* @example
*
* Sending MIDI Sysex Data directly
*
* C.bindings.invoke("midiInputPort.Main Keyboard", [ 0xF7, 0x00, 0x00, 0x00, 0xF0 ]);
*
* @example
*
* Some binding points expect a "parameter" value. Parameter values are similar to the `value` parameter
* in that they specify a value to invoke on the target of the binding. The difference is related to the
* way they're managed internally for user created bindings. The `value` comes from the source of the binding
* whereas a `parameter` value is stored with the binding itself.
*
* eg: Load the song with program number 12
*
* C.bindings.invoke("global.setList.loadSpecificSongByProgramInstant", null, null, 12);
*
* @param {String} name The name of the binding point to invoke
* @param {Object} [value] The value to pass to the binding point
* @param {Number[]} [indicies] The integer indicies of the target binding point
* @param {Object} [parameter] The parameter value to invoke the target with
* @method invoke
* @return {Promise} A promise that resolves once the target binding point has been invoked
*/
async invoke(name, value, indicies, parameter)
{
return (await this.request("POST", "/invoke", {
name: name,
value: value,
indicies: indicies,
parameter: parameter,
}));
}
/**
* Queries a source binding point for it's current value.
*
* If Cantabile is running on your local machine a full list of available binding
* points is [available here](http://localhost:35007/api/bindings/availableBindingPoints)
*
* @example
*
* console.log("Current Output Gain:", await C.bindings.query("global.masterLevels.outputGain"));
*
* @method query
* @param {String} name The name of the binding point to query
* @param {Number[]} [indicies] The integer indicies of the binding point
* @return {Object} The current value of the binding source
*/
async query(name, indicies)
{
return (await this.request("POST", "/query", {
name: name,
indicies: indicies,
})).data.value;
}
/**
* Starts watching a source binding point for changes (or invocations)
*
* If Cantabile is running on your local machine a full list of available binding
* points is [available here](http://localhost:35007/api/bindings/availableBindingPoints)
*
* @example
*
* Using a callback function:
*
* let C = new CantabileApi();
*
* // Watch a source binding point using a callback function
* C.bindings.watch("global.masterLevels.outputGain", null, null, function(value) {
* console.log("Master output gain changed to:", value);
* })
*
* // The "bindings" end point must be opened before callbacks will happen
* C.bindings.open();
*
* @example
*
* Using the BindingWatcher class and events:
*
* let C = new CantabileApi();
* let watcher = C.bindings.watch("global.masterLevels.outputGain");
* watcher.on('invoked', function(value) {
* console.log("Master output gain changed to:", value);
* });
*
* // The "bindings" end point must be opened before callbacks will happen
* C.bindings.open();
*
* /// later, stop listening
* watcher.unwatch();
*
* @example
*
* Watching for a MIDI event:
*
* C.bindings.watch("midiInputPort.Onscreen Keyboard", null, {
* channel: 0,
* kind: "ProgramChange",
* controller: -1,
* }, function(value) {
* console.log("Program Change: ", value);
* })
*
* @example
* Watching for a keystroke:
*
* C.bindings.watch("global.pckeyboard.keyPress", null, "Ctrl+Alt+M", function() {
* console.log("Key press!");
* })
*
*
*
*
* @method watch
* @param {String} name The name of the binding point to query
* @param {Number[]} [indicies] The integer indicies of the binding point
* @param {Object} [condition] The condition for triggering the binding
* @param {Function} [callback] Optional callback function to be called when the source binding triggers
*
* The callback function has the form function(resolved, source) where resolved is the resolved display string and source
* is the BindingWatcher instance.
*
* @return {BindingWatcher}
*/
watch(name, indicies, condition, listener)
{
let w = new BindingWatcher(this, name, indicies, condition, listener);
this._watchers.push(w);
if (this._watchers.length == 1)
this.open();
if (this.isOpen)
w._start();
return w;
}
_registerWatchId(watchId, watcher)
{
this._watchIds[watchId] = watcher;
}
_revokeWatchId(watchId)
{
delete this._watchIds[watchId];
}
_revokeWatcher(w)
{
this._watchers = this._watchers.filter(x=>x != w);
if (this._watchers.length == 0)
this.close();
}
_onEvent_invoked(data)
{
// Get the watcher
let w = this._watchIds[data.watchId];
if (w)
{
w._update(data);
}
}
}
module.exports = Bindings;