summaryrefslogtreecommitdiffstats
path: root/devtools/client/webaudioeditor/models.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webaudioeditor/models.js')
-rw-r--r--devtools/client/webaudioeditor/models.js288
1 files changed, 288 insertions, 0 deletions
diff --git a/devtools/client/webaudioeditor/models.js b/devtools/client/webaudioeditor/models.js
new file mode 100644
index 000000000..b4659d8ce
--- /dev/null
+++ b/devtools/client/webaudioeditor/models.js
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Import as different name `coreEmit`, so we don't conflict
+// with the global `window` listener itself.
+const { emit: coreEmit } = require("sdk/event/core");
+
+/**
+ * Representational wrapper around AudioNodeActors. Adding and destroying
+ * AudioNodes should be performed through the AudioNodes collection.
+ *
+ * Events:
+ * - `connect`: node, destinationNode, parameter
+ * - `disconnect`: node
+ */
+const AudioNodeModel = Class({
+ extends: EventTarget,
+
+ // Will be added via AudioNodes `add`
+ collection: null,
+
+ initialize: function (actor) {
+ this.actor = actor;
+ this.id = actor.actorID;
+ this.type = actor.type;
+ this.bypassable = actor.bypassable;
+ this._bypassed = false;
+ this.connections = [];
+ },
+
+ /**
+ * Stores connection data inside this instance of this audio node connecting
+ * to another node (destination). If connecting to another node's AudioParam,
+ * the second argument (param) must be populated with a string.
+ *
+ * Connecting nodes is idempotent. Upon new connection, emits "connect" event.
+ *
+ * @param AudioNodeModel destination
+ * @param String param
+ */
+ connect: function (destination, param) {
+ let edge = findWhere(this.connections, { destination: destination.id, param: param });
+
+ if (!edge) {
+ this.connections.push({ source: this.id, destination: destination.id, param: param });
+ coreEmit(this, "connect", this, destination, param);
+ }
+ },
+
+ /**
+ * Clears out all internal connection data. Emits "disconnect" event.
+ */
+ disconnect: function () {
+ this.connections.length = 0;
+ coreEmit(this, "disconnect", this);
+ },
+
+ /**
+ * Gets the bypass status of the audio node.
+ *
+ * @return Boolean
+ */
+ isBypassed: function () {
+ return this._bypassed;
+ },
+
+ /**
+ * Sets the bypass value of an AudioNode.
+ *
+ * @param Boolean enable
+ * @return Promise
+ */
+ bypass: function (enable) {
+ this._bypassed = enable;
+ return this.actor.bypass(enable).then(() => coreEmit(this, "bypass", this, enable));
+ },
+
+ /**
+ * Returns a promise that resolves to an array of objects containing
+ * both a `param` name property and a `value` property.
+ *
+ * @return Promise->Object
+ */
+ getParams: function () {
+ return this.actor.getParams();
+ },
+
+ /**
+ * Returns a promise that resolves to an object containing an
+ * array of event information and an array of automation data.
+ *
+ * @param String paramName
+ * @return Promise->Array
+ */
+ getAutomationData: function (paramName) {
+ return this.actor.getAutomationData(paramName);
+ },
+
+ /**
+ * Takes a `dagreD3.Digraph` object and adds this node to
+ * the graph to be rendered.
+ *
+ * @param dagreD3.Digraph
+ */
+ addToGraph: function (graph) {
+ graph.addNode(this.id, {
+ type: this.type,
+ label: this.type.replace(/Node$/, ""),
+ id: this.id,
+ bypassed: this._bypassed
+ });
+ },
+
+ /**
+ * Takes a `dagreD3.Digraph` object and adds edges to
+ * the graph to be rendered. Separate from `addToGraph`,
+ * as while we depend on D3/Dagre's constraints, we cannot
+ * add edges for nodes that have not yet been added to the graph.
+ *
+ * @param dagreD3.Digraph
+ */
+ addEdgesToGraph: function (graph) {
+ for (let edge of this.connections) {
+ let options = {
+ source: this.id,
+ target: edge.destination
+ };
+
+ // Only add `label` if `param` specified, as this is an AudioParam
+ // connection then. `label` adds the magic to render with dagre-d3,
+ // and `param` is just more explicitly the param, ignoring
+ // implementation details.
+ if (edge.param) {
+ options.label = options.param = edge.param;
+ }
+
+ graph.addEdge(null, this.id, edge.destination, options);
+ }
+ },
+
+ toString: () => "[object AudioNodeModel]",
+});
+
+
+/**
+ * Constructor for a Collection of `AudioNodeModel` models.
+ *
+ * Events:
+ * - `add`: node
+ * - `remove`: node
+ * - `connect`: node, destinationNode, parameter
+ * - `disconnect`: node
+ */
+const AudioNodesCollection = Class({
+ extends: EventTarget,
+
+ model: AudioNodeModel,
+
+ initialize: function () {
+ this.models = new Set();
+ this._onModelEvent = this._onModelEvent.bind(this);
+ },
+
+ /**
+ * Iterates over all models within the collection, calling `fn` with the
+ * model as the first argument.
+ *
+ * @param Function fn
+ */
+ forEach: function (fn) {
+ this.models.forEach(fn);
+ },
+
+ /**
+ * Creates a new AudioNodeModel, passing through arguments into the AudioNodeModel
+ * constructor, and adds the model to the internal collection store of this
+ * instance.
+ *
+ * Emits "add" event on instance when completed.
+ *
+ * @param Object obj
+ * @return AudioNodeModel
+ */
+ add: function (obj) {
+ let node = new this.model(obj);
+ node.collection = this;
+
+ this.models.add(node);
+
+ node.on("*", this._onModelEvent);
+ coreEmit(this, "add", node);
+ return node;
+ },
+
+ /**
+ * Removes an AudioNodeModel from the internal collection. Calls `delete` method
+ * on the model, and emits "remove" on this instance.
+ *
+ * @param AudioNodeModel node
+ */
+ remove: function (node) {
+ this.models.delete(node);
+ coreEmit(this, "remove", node);
+ },
+
+ /**
+ * Empties out the internal collection of all AudioNodeModels.
+ */
+ reset: function () {
+ this.models.clear();
+ },
+
+ /**
+ * Takes an `id` from an AudioNodeModel and returns the corresponding
+ * AudioNodeModel within the collection that matches that id. Returns `null`
+ * if not found.
+ *
+ * @param Number id
+ * @return AudioNodeModel|null
+ */
+ get: function (id) {
+ return findWhere(this.models, { id: id });
+ },
+
+ /**
+ * Returns the count for how many models are a part of this collection.
+ *
+ * @return Number
+ */
+ get length() {
+ return this.models.size;
+ },
+
+ /**
+ * Returns detailed information about the collection. used during tests
+ * to query state. Returns an object with information on node count,
+ * how many edges are within the data graph, as well as how many of those edges
+ * are for AudioParams.
+ *
+ * @return Object
+ */
+ getInfo: function () {
+ let info = {
+ nodes: this.length,
+ edges: 0,
+ paramEdges: 0
+ };
+
+ this.models.forEach(node => {
+ let paramEdgeCount = node.connections.filter(edge => edge.param).length;
+ info.edges += node.connections.length - paramEdgeCount;
+ info.paramEdges += paramEdgeCount;
+ });
+ return info;
+ },
+
+ /**
+ * Adds all nodes within the collection to the passed in graph,
+ * as well as their corresponding edges.
+ *
+ * @param dagreD3.Digraph
+ */
+ populateGraph: function (graph) {
+ this.models.forEach(node => node.addToGraph(graph));
+ this.models.forEach(node => node.addEdgesToGraph(graph));
+ },
+
+ /**
+ * Called when a stored model emits any event. Used to manage
+ * event propagation, or listening to model events to react, like
+ * removing a model from the collection when it's destroyed.
+ */
+ _onModelEvent: function (eventName, node, ...args) {
+ if (eventName === "remove") {
+ // If a `remove` event from the model, remove it
+ // from the collection, and let the method handle the emitting on
+ // the collection
+ this.remove(node);
+ } else {
+ // Pipe the event to the collection
+ coreEmit(this, eventName, node, ...args);
+ }
+ },
+
+ toString: () => "[object AudioNodeCollection]",
+});