Bug 1025864 - Add observer updates when AudioParams change in the Web Audio Editor. r=vp, r=dcamp

This commit is contained in:
Jordan Santell
2014-08-19 08:57:00 -04:00
parent a9aad5af7a
commit 6bee33dfb7
11 changed files with 476 additions and 58 deletions

View File

@@ -4,6 +4,8 @@
"use strict";
let { utils: Cu, interfaces: Ci } = Components;
addMessageListener("devtools:test:history", function ({ data }) {
content.history[data.direction]();
});
@@ -16,3 +18,11 @@ addMessageListener("devtools:test:reload", function ({ data }) {
data = data || {};
content.location.reload(data.forceget);
});
addMessageListener("devtools:test:forceCC", function () {
let DOMWindowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
DOMWindowUtils.cycleCollect();
DOMWindowUtils.garbageCollect();
DOMWindowUtils.garbageCollect();
});

View File

@@ -10,18 +10,23 @@ support-files =
doc_connect-toggle.html
doc_connect-param.html
doc_connect-multi-param.html
doc_change-param.html
440hz_sine.ogg
head.js
[browser_audionode-actor-get-set-param.js]
[browser_audionode-actor-get-type.js]
[browser_audionode-actor-get-param-flags.js]
[browser_audionode-actor-get-params-01.js]
[browser_audionode-actor-get-params-02.js]
[browser_audionode-actor-get-param-flags.js]
[browser_audionode-actor-get-set-param.js]
[browser_audionode-actor-get-type.js]
[browser_audionode-actor-is-source.js]
[browser_webaudio-actor-simple.js]
[browser_webaudio-actor-destroy-node.js]
[browser_webaudio-actor-change-params-01.js]
[browser_webaudio-actor-change-params-02.js]
[browser_webaudio-actor-change-params-03.js]
[browser_webaudio-actor-connect-param.js]
[browser_webaudio-actor-destroy-node.js]
[browser_webaudio-actor-simple.js]
[browser_wa_destroy-node-01.js]
@@ -48,4 +53,5 @@ support-files =
# [browser_wa_properties-view-edit-02.js]
# Disabled for too many intermittents bug 1010423
[browser_wa_properties-view-params.js]
[browser_wa_properties-view-change-params.js]
[browser_wa_properties-view-params-objects.js]

View File

@@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that params view correctly updates changed parameters
* when source code updates them, as well as CHANGE_PARAM events.
*/
function spawnTest() {
let [target, debuggee, panel] = yield initWebAudioEditor(CHANGE_PARAM_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin;
let gVars = WebAudioInspectorView._propsView;
// Set parameter polling to 20ms for tests
panelWin.PARAM_POLLING_FREQUENCY = 20;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
getN(gFront, "create-node", 3),
waitForGraphRendered(panelWin, 3, 0)
]);
let oscId = actors[1].actorID;
click(panelWin, findGraphNode(panelWin, oscId));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
// Yield twice so we get a diff
yield once(panelWin, EVENTS.CHANGE_PARAM);
let [[_, args]] = yield getSpread(panelWin, EVENTS.CHANGE_PARAM);
is(args.actorID, oscId, "EVENTS.CHANGE_PARAM has correct `actorID`");
ok(args.oldValue < args.newValue, "EVENTS.CHANGE_PARAM has correct `newValue` and `oldValue`");
is(args.param, "detune", "EVENTS.CHANGE_PARAM has correct `param`");
let [[_, args]] = yield getSpread(panelWin, EVENTS.CHANGE_PARAM);
checkVariableView(gVars, 0, { "detune": args.newValue }, "`detune` parameter updated.");
let [[_, args]] = yield getSpread(panelWin, EVENTS.CHANGE_PARAM);
checkVariableView(gVars, 0, { "detune": args.newValue }, "`detune` parameter updated.");
yield teardown(panel);
finish();
}

View File

@@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test WebAudioActor `change-param` events and front.[en|dis]ableChangeParamEvents
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(CHANGE_PARAM_URL);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 3)
]);
let osc = nodes[1];
let eventCount = 0;
yield front.enableChangeParamEvents(osc, 20);
front.on("change-param", onChangeParam);
yield getN(front, "change-param", 3);
yield front.disableChangeParamEvents();
let currEventCount = eventCount;
// Be flexible here incase we get an extra counter before the listener is turned off
ok(eventCount >= 3, "Calling `enableChangeParamEvents` should allow front to emit `change-param`.");
yield wait(100);
ok((eventCount - currEventCount) <= 2, "Calling `disableChangeParamEvents` should turn off the listener.");
front.off("change-param", onChangeParam);
yield removeTab(target.tab);
finish();
function onChangeParam ({ newValue, oldValue, param, actorID }) {
is(actorID, osc.actorID, "correct `actorID` in `change-param`.");
is(param, "detune", "correct `param` property in `change-param`.");
ok(newValue > oldValue,
"correct `newValue` (" + newValue + ") and `oldValue` (" + oldValue + ") in `change-param`");
eventCount++;
}
}

View File

@@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that listening to param change polling does not break when the AudioNode is collected.
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(DESTROY_NODES_URL);
let waitUntilDestroyed = getN(front, "destroy-node", 10);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 13)
]);
let bufferNode = nodes[6];
yield front.enableChangeParamEvents(bufferNode, 20);
front.on("change-param", onChangeParam);
forceCC();
yield waitUntilDestroyed;
yield wait(50);
front.off("change-param", onChangeParam);
ok(true, "listening to `change-param` on a dead node doesn't throw.");
yield removeTab(target.tab);
finish();
function onChangeParam (args) {
ok(false, "`change-param` should not be emitted on a node that hasn't changed params or is dead.");
}
}

View File

@@ -0,0 +1,32 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test WebAudioActor `change-param` events on special types.
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(CHANGE_PARAM_URL);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 3)
]);
let shaper = nodes[2];
let eventCount = 0;
yield front.enableChangeParamEvents(shaper, 20);
let onChange = once(front, "change-param");
shaper.setParam("curve", null);
let { newValue, oldValue } = yield onChange;
is(oldValue.type, "object", "`oldValue` should be an object.");
is(oldValue.class, "Float32Array", "`oldValue` should be of class Float32Array.");
is(newValue.type, "null", "`newValue` should be null.");
yield removeTab(target.tab);
finish();
}

View File

@@ -0,0 +1,25 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Web Audio Editor test page</title>
</head>
<body>
<script type="text/javascript;version=1.8">
"use strict";
let ctx = new AudioContext();
let osc = ctx.createOscillator();
let shaperNode = ctx.createWaveShaper();
let detuneVal = 0;
shaperNode.curve = new Float32Array(65536);
setInterval(() => osc.detune.value = ++detuneVal, 10);
</script>
</body>
</html>

View File

@@ -19,7 +19,9 @@ let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.j
let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio");
let TargetFactory = devtools.TargetFactory;
let mm = null;
const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js";
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/webaudioeditor/test/";
const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html";
const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
@@ -30,6 +32,7 @@ const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html";
const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html";
const CONNECT_MULTI_PARAM_URL = EXAMPLE_URL + "doc_connect-multi-param.html";
const CHANGE_PARAM_URL = EXAMPLE_URL + "doc_change-param.html";
// All tests are asynchronous.
waitForExplicitFinish();
@@ -133,6 +136,8 @@ function initBackend(aUrl) {
yield target.makeRemote();
let front = new WebAudioFront(target.client, target.form);
loadFrameScripts();
return [target, debuggee, front];
});
}
@@ -150,6 +155,8 @@ function initWebAudioEditor(aUrl) {
Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor");
let panel = toolbox.getCurrentPanel();
loadFrameScripts();
return [target, debuggee, panel];
});
}
@@ -387,9 +394,12 @@ function countGraphObjects (win) {
* Forces cycle collection and GC, used in AudioNode destruction tests.
*/
function forceCC () {
SpecialPowers.DOMWindowUtils.cycleCollect();
SpecialPowers.DOMWindowUtils.garbageCollect();
SpecialPowers.DOMWindowUtils.garbageCollect();
mm.sendAsyncMessage("devtools:test:forceCC");
}
function loadFrameScripts () {
mm = gBrowser.selectedBrowser.messageManager;
mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
}
/**

View File

@@ -20,9 +20,10 @@ const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
const L10N = new ViewHelpers.L10N(STRINGS_URI);
const Telemetry = require("devtools/shared/telemetry");
const telemetry = new Telemetry();
let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
let PARAM_POLLING_FREQUENCY = 1000;
// The panel's window global is an EventEmitter firing the following events:
const EVENTS = {
// Fired when the first AudioNode has been created, signifying
@@ -173,6 +174,8 @@ let WebAudioEditorController = {
telemetry.toolOpened("webaudioeditor");
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onThemeChange = this._onThemeChange.bind(this);
this._onSelectNode = this._onSelectNode.bind(this);
this._onChangeParam = this._onChangeParam.bind(this);
gTarget.on("will-navigate", this._onTabNavigated);
gTarget.on("navigate", this._onTabNavigated);
gFront.on("start-context", this._onStartContext);
@@ -194,12 +197,15 @@ let WebAudioEditorController = {
window.on(EVENTS.DISCONNECT_NODE, this._onUpdatedContext);
window.on(EVENTS.DESTROY_NODE, this._onUpdatedContext);
window.on(EVENTS.CONNECT_PARAM, this._onUpdatedContext);
// Set up a controller for managing parameter changes per audio node
window.on(EVENTS.UI_SELECT_NODE, this._onSelectNode);
},
/**
* Remove events emitted by the current tab target.
*/
destroy: function() {
destroy: Task.async(function* () {
telemetry.toolClosed("webaudioeditor");
gTarget.off("will-navigate", this._onTabNavigated);
gTarget.off("navigate", this._onTabNavigated);
@@ -215,8 +221,11 @@ let WebAudioEditorController = {
window.off(EVENTS.DISCONNECT_NODE, this._onUpdatedContext);
window.off(EVENTS.DESTROY_NODE, this._onUpdatedContext);
window.off(EVENTS.CONNECT_PARAM, this._onUpdatedContext);
window.off(EVENTS.UI_SELECT_NODE, this._onSelectNode);
gDevTools.off("pref-changed", this._onThemeChange);
},
yield gFront.disableChangeParamEvents();
}),
/**
* Called when page is reloaded to show the reload notice and waiting
@@ -345,9 +354,21 @@ let WebAudioEditorController = {
/**
* Called when a node param is changed.
*/
_onChangeParam: function({ actor, param, value }) {
window.emit(EVENTS.CHANGE_PARAM, getViewNodeByActor(actor), param, value);
}
_onChangeParam: function (args) {
window.emit(EVENTS.CHANGE_PARAM, args);
},
/**
* Called on UI_SELECT_NODE, used to manage
* `change-param` events on that node.
*/
_onSelectNode: function (_, id) {
let node = getViewNodeById(id);
if (node && node.actor) {
gFront.enableChangeParamEvents(node.actor, PARAM_POLLING_FREQUENCY);
}
},
};
/**

View File

@@ -377,6 +377,7 @@ let WebAudioInspectorView = {
this._onNodeSelect = this._onNodeSelect.bind(this);
this._onTogglePaneClick = this._onTogglePaneClick.bind(this);
this._onDestroyNode = this._onDestroyNode.bind(this);
this._onChangeParam = this._onChangeParam.bind(this);
this._inspectorPaneToggleButton.addEventListener("mousedown", this._onTogglePaneClick, false);
this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
@@ -384,6 +385,7 @@ let WebAudioInspectorView = {
window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
window.on(EVENTS.DESTROY_NODE, this._onDestroyNode);
window.on(EVENTS.CHANGE_PARAM, this._onChangeParam);
},
/**
@@ -393,6 +395,7 @@ let WebAudioInspectorView = {
this._inspectorPaneToggleButton.removeEventListener("mousedown", this._onTogglePaneClick);
window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
window.off(EVENTS.DESTROY_NODE, this._onDestroyNode);
window.off(EVENTS.CHANGE_PARAM, this._onChangeParam);
this._inspectorPane = null;
this._inspectorPaneToggleButton = null;
@@ -612,7 +615,22 @@ let WebAudioInspectorView = {
if (this._currentNode && this._currentNode.id === id) {
this.setCurrentAudioNode(null);
}
}
},
/**
* Called when `CHANGE_PARAM` is fired. We should ensure that this event is
* for the same node that is currently selected. We check the existence
* of each part of the scope to make sure that if this event was fired
* during a VariablesView rebuild, then we just ignore it.
*/
_onChangeParam: function (_, { param, newValue, oldValue, actorID }) {
if (!this._currentNode || this._currentNode.actor.actorID !== actorID) return;
let scope = this._getAudioPropertiesScope();
if (!scope) return;
let property = scope.get(param);
if (!property) return;
property.setGrip(newValue);
},
};
/**

View File

@@ -4,16 +4,14 @@
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const Services = require("Services");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const events = require("sdk/event/core");
const { on: systemOn, off: systemOff } = require("sdk/system/events");
const { setTimeout, clearTimeout } = require("sdk/timers");
const protocol = require("devtools/server/protocol");
const { CallWatcherActor, CallWatcherFront } = require("devtools/server/actors/call-watcher");
const { ThreadActor } = require("devtools/server/actors/script");
const { on, once, off, emit } = events;
const { method, Arg, Option, RetVal } = protocol;
@@ -27,6 +25,10 @@ exports.unregister = function(handle) {
handle.removeGlobalActor(WebAudioActor);
};
// In milliseconds, how often should AudioNodes poll to see
// if an AudioParam's value has changed to emit to the client.
const PARAM_POLLING_FREQUENCY = 1000;
const AUDIO_GLOBALS = [
"AudioContext", "AudioNode"
];
@@ -146,6 +148,10 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
}
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
},
/**
* Returns the name of the audio type.
* Examples: "OscillatorNode", "MediaElementAudioSourceNode"
@@ -187,6 +193,7 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
node[param].value = value;
else
node[param] = value;
return undefined;
} catch (e) {
return constructError(e);
@@ -221,14 +228,7 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
// AudioBuffer or Float32Array references and the like,
// so this just formats the value to be displayed in the VariablesView,
// without using real grips and managing via actor pools.
let grip;
try {
grip = ThreadActor.prototype.createValueGrip(value);
}
catch (e) {
grip = createObjectGrip(value);
}
return grip;
return createGrip(value);
}, {
request: {
param: Arg(0, "string")
@@ -252,16 +252,27 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
}),
/**
* Get an array of objects each containing a `param` and `value` property,
* corresponding to a property name and current value of the audio node.
* Get an array of objects each containing a `param`, `value` and `flags` property,
* corresponding to a property name and current value of the audio node, and any
* associated flags as defined by NODE_PROPERTIES.
*/
getParams: method(function (param) {
getParams: method(function () {
let props = Object.keys(NODE_PROPERTIES[this.type]);
return props.map(prop =>
({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) }));
}, {
response: { params: RetVal("json") }
})
}),
/**
* Returns a boolean indicating whether or not
* the underlying AudioNode has been collected yet or not.
*
* @return Boolean
*/
isAlive: function () {
return !!this.node.get();
}
});
/**
@@ -407,6 +418,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
this.tabActor = null;
this._initialized = false;
off(this._callWatcher._contentObserver, "global-destroyed", this._onGlobalDestroyed);
this.disableChangeParamEvents();
this._nativeToActorID = null;
this._callWatcher.eraseRecording();
this._callWatcher.finalize();
@@ -415,6 +427,59 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
oneway: true
}),
/**
* Takes an AudioNodeActor and a duration specifying how often
* should the node's parameters be polled to detect changes. Emits
* `change-param` when a change is found.
*
* Currently, only one AudioNodeActor can be listened to at a time.
*
* `wait` is used in tests to specify the poll timer.
*/
enableChangeParamEvents: method(function (nodeActor, wait) {
// For now, only have one node being polled
this.disableChangeParamEvents();
// Ignore if node is dead
if (!nodeActor.isAlive()) {
return;
}
let previous = mapAudioParams(nodeActor);
// Store the ID of the node being polled
this._pollingID = nodeActor.actorID;
this.poller = new Poller(() => {
// If node has been collected, disable param polling
if (!nodeActor.isAlive()) {
this.disableChangeParamEvents();
return;
}
let current = mapAudioParams(nodeActor);
diffAudioParams(previous, current).forEach(changed => {
this._onChangeParam(nodeActor, changed);
});
previous = current;
}).on(wait || PARAM_POLLING_FREQUENCY);
}, {
request: {
node: Arg(0, "audionode"),
wait: Arg(1, "nullable:number"),
},
oneway: true
}),
disableChangeParamEvents: method(function () {
if (this.poller) {
this.poller.off();
}
this._pollingID = null;
}, {
oneway: true
}),
/**
* Events emitted by this actor.
*/
@@ -437,12 +502,6 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
dest: Option(0, "audionode"),
param: Option(0, "string")
},
"change-param": {
type: "changeParam",
source: Option(0, "audionode"),
param: Option(0, "string"),
value: Option(0, "string")
},
"create-node": {
type: "createNode",
source: Arg(0, "audionode")
@@ -450,6 +509,13 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
"destroy-node": {
type: "destroyNode",
source: Arg(0, "audionode")
},
"change-param": {
type: "changeParam",
param: Option(0, "string"),
newValue: Option(0, "json"),
oldValue: Option(0, "json"),
actorID: Option(0, "string")
}
},
@@ -473,7 +539,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
/**
* Takes an XrayWrapper node, and attaches the node's `nativeID`
* to the AudioParams as `_parentID`, as well as the the type of param
* as a string on `_paramName`.
* as a string on `_paramName`. Used to tag AudioParams for `connect-param` events.
*/
_instrumentParams: function (node) {
let type = getConstructorName(node);
@@ -493,6 +559,14 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
* created), so make a new actor and store that.
*/
_getActorByNativeID: function (nativeID) {
// If the WebAudioActor has already been finalized, the `_nativeToActorID`
// map will already be destroyed -- the lingering destruction events
// seem to only occur in e10s, so add an extra check here to disregard
// these late events
if (!this._nativeToActorID) {
return null;
}
// Ensure we have a Number, rather than a string
// return via notification.
nativeID = ~~nativeID;
@@ -544,15 +618,12 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
},
/**
* Called when a parameter changes on an audio node
* Called when an AudioParam that's being listened to changes.
* Takes an AudioNodeActor and an object with `newValue`, `oldValue`, and `param` name.
*/
_onParamChange: function (node, param, value) {
let actor = this._getActorByNativeID(node.id);
emit(this, "param-change", {
source: actor,
param: param,
value: value
});
_onChangeParam: function (actor, changed) {
changed.actorID = actor.actorID;
emit(this, "change-param", changed);
},
/**
@@ -563,7 +634,8 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
emit(this, "create-node", actor);
},
/** Called when `webaudio-node-demise` is triggered,
/**
* Called when `webaudio-node-demise` is triggered,
* and emits the associated actor to the front if found.
*/
_onDestroyNode: function ({data}) {
@@ -576,6 +648,10 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
// notifications for a document that no longer exists,
// the mapping should not be found, so we do not emit an event.
if (actor) {
// Turn off polling for changes if on for this node
if (this._pollingID === actor.actorID) {
this.disableChangeParamEvents();
}
this._nativeToActorID.delete(nativeID);
emit(this, "destroy-node", actor);
}
@@ -663,17 +739,109 @@ function getConstructorName (obj) {
}
/**
* Create a grip-like object to pass in renderable information
* to the front-end for things like Float32Arrays, AudioBuffers,
* without tracking them in an actor pool.
* Create a value grip for `value`, or fallback to a grip-like object
* for renderable information for the front-end for things like Float32Arrays,
* AudioBuffers, without tracking them in an actor pool.
*/
function createObjectGrip (value) {
return {
type: "object",
preview: {
kind: "ObjectWithText",
text: ""
},
class: getConstructorName(value)
};
function createGrip (value) {
try {
return ThreadActor.prototype.createValueGrip(value);
}
catch (e) {
return {
type: "object",
preview: {
kind: "ObjectWithText",
text: ""
},
class: getConstructorName(value)
};
}
}
/**
* Takes an AudioNodeActor and maps its current parameter values
* to a hash, where the property is the AudioParam name, and value
* is the current value.
*/
function mapAudioParams (node) {
return node.getParams().reduce(function (obj, p) {
obj[p.param] = p.value;
return obj;
}, {});
}
/**
* Takes an object of previous and current values of audio parameters,
* and compares them. If they differ, emit a `change-param` event.
*
* @param Object prev
* Hash of previous set of AudioParam values.
* @param Object current
* Hash of current set of AudioParam values.
*/
function diffAudioParams (prev, current) {
return Object.keys(current).reduce((changed, param) => {
if (!equalGrips(current[param], prev[param])) {
changed.push({
param: param,
oldValue: prev[param],
newValue: current[param]
});
}
return changed;
}, []);
}
/**
* Compares two grip objects to determine if they're equal or not.
*
* @param Any a
* @param Any a
* @return Boolean
*/
function equalGrips (a, b) {
let aType = typeof a;
let bType = typeof b;
if (aType !== bType) {
return false;
} else if (aType === "object") {
// In this case, we are comparing two objects, like an ArrayBuffer or Float32Array,
// or even just plain "null"s (which grip's will have `type` property "null",
// and we have no way of showing more information than its class, so assume
// these are equal since nothing can be updated with information of value.
if (a.type === b.type) {
return true;
}
// Otherwise return false -- this could be a case of a property going from `null`
// to having an ArrayBuffer or an object, in which case we should update it.
return false;
} else {
return a === b;
}
}
/**
* Poller class -- takes a function, and call be turned on and off
* via methods to execute `fn` on the interval specified during `on`.
*/
function Poller (fn) {
this.fn = fn;
}
Poller.prototype.on = function (wait) {
let poller = this;
poller.timer = setTimeout(poll, wait);
function poll () {
poller.fn();
poller.timer = setTimeout(poll, wait);
}
return this;
};
Poller.prototype.off = function () {
if (this.timer) {
clearTimeout(this.timer);
}
return this;
};