Bug 1007920 - Handle non-primitive types in the AudioNode actor and in the web audio editor tool. r=vp

This commit is contained in:
Jordan Santell 2014-05-26 10:41:00 -04:00
parent 72a30cb72f
commit 7fc416d8ed
11 changed files with 341 additions and 37 deletions

Binary file not shown.

View File

@ -5,11 +5,14 @@ support-files =
doc_simple-context.html
doc_complex-context.html
doc_simple-node-creation.html
doc_buffer-and-array.html
440hz_sine.ogg
head.js
[browser_audionode-actor-get-set-param.js]
[browser_audionode-actor-get-type.js]
[browser_audionode-actor-get-params.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-is-source.js]
[browser_webaudio-actor-simple.js]
@ -21,9 +24,11 @@ support-files =
[browser_wa_graph-render-02.js]
[browser_wa_graph-markers.js]
[browser_wa_inspector.js]
[browser_wa_inspector-toggle.js]
[browser_wa_properties-view.js]
# [browser_wa_properties-view-edit.js]
# Disabled for too many intermittents bug 1010423
[browser_wa_inspector.js]
[browser_wa_inspector-toggle.js]
[browser_wa_properties-view-params.js]
[browser_wa_properties-view-params-objects.js]

View File

@ -23,12 +23,7 @@ function spawnTest () {
nodeTypes.forEach((type, i) => {
let params = allNodeParams[i];
params.forEach(({param, value, flags}) => {
ok(~NODE_PROPERTIES[type].indexOf(param), "expected parameter for " + type);
// Skip over some properties that are undefined by default
if (!/buffer|loop|smoothing|curve|cone/.test(param)) {
ok(value != undefined, param + " is not undefined");
}
ok(param in NODE_DEFAULT_VALUES[type], "expected parameter for " + type);
ok(typeof flags === "object", type + " has a flags object");

View File

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that default properties are returned with the correct type
* from the AudioNode actors.
*/
function spawnTest() {
let [target, debuggee, front] = yield initBackend(SIMPLE_NODES_URL);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 14)
]);
let allParams = yield Promise.all(nodes.map(node => node.getParams()));
let types = [
"AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
"AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
"PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
"DynamicsCompressorNode", "OscillatorNode"
];
allParams.forEach((params, i) => {
compare(params, NODE_DEFAULT_VALUES[types[i]], types[i]);
});
yield removeTab(target.tab);
finish();
}
function compare (actual, expected, type) {
actual.forEach(({ value, param }) => {
value = getGripValue(value);
if (typeof expected[param] === "function") {
ok(expected[param](value), type + " has a passing value for " + param);
}
else {
ise(value, expected[param], type + " has correct default value and type for " + param);
}
});
is(actual.length, Object.keys(expected).length,
type + " has correct amount of properties.");
}

View File

@ -20,7 +20,8 @@ function spawnTest () {
ise(type, "sine", "AudioNode:getParam correctly fetches non-AudioParam");
let type = yield oscNode.getParam("not-a-valid-param");
is(type, undefined, "AudioNode:getParam correctly returns false for invalid param");
ok(type.type === "undefined",
"AudioNode:getParam correctly returns a grip value for `undefined` for an invalid param.");
let resSuccess = yield oscNode.setParam("frequency", 220);
let freq = yield oscNode.getParam("frequency");

View File

@ -31,7 +31,7 @@ function spawnTest() {
let setAndCheck = setAndCheckVariable(panelWin, gVars);
checkVariableView(gVars, 0, {
"type": "\"sine\"",
"type": "sine",
"frequency": 440,
"detune": 0
}, "default loaded string");
@ -44,7 +44,7 @@ function spawnTest() {
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield setAndCheck(0, "type", "square", "\"square\"", "sets string as string");
yield setAndCheck(0, "type", "square", "square", "sets string as string");
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);

View File

@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that params view correctly displays non-primitive properties
* like AudioBuffer and Float32Array in properties of AudioNodes.
*/
function spawnTest() {
let [target, debuggee, panel] = yield initWebAudioEditor(BUFFER_AND_ARRAY_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin;
let gVars = WebAudioInspectorView._propsView;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
getN(gFront, "create-node", 3),
waitForGraphRendered(panelWin, 3, 2)
]);
let nodeIds = actors.map(actor => actor.actorID);
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
checkVariableView(gVars, 0, {
"curve": "Float32Array"
}, "WaveShaper's `curve` is listed as an `Float32Array`.");
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
checkVariableView(gVars, 0, {
"buffer": "AudioBuffer"
}, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that params view correctly displays all properties for nodes
* correctly, with default values and correct types.
*/
function spawnTest() {
let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_NODES_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin;
let gVars = WebAudioInspectorView._propsView;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
getN(gFront, "create-node", 14),
waitForGraphRendered(panelWin, 14, 0)
]);
let nodeIds = actors.map(actor => actor.actorID);
let types = [
"AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
"AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
"PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
"DynamicsCompressorNode", "OscillatorNode"
];
for (let i = 0; i < types.length; i++) {
click(panelWin, findGraphNode(panelWin, nodeIds[i]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
checkVariableView(gVars, 0, NODE_DEFAULT_VALUES[types[i]], types[i]);
}
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,56 @@
<!-- 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 audioURL = "http://example.com/browser/browser/devtools/webaudioeditor/test/440hz_sine.ogg";
let ctx = new AudioContext();
let bufferNode = ctx.createBufferSource();
let shaperNode = ctx.createWaveShaper();
shaperNode.curve = generateWaveShapingCurve();
let xhr = getBuffer(audioURL, () => {
ctx.decodeAudioData(xhr.response, (buffer) => {
bufferNode.buffer = buffer;
bufferNode.connect(shaperNode);
shaperNode.connect(ctx.destination);
});
});
function generateWaveShapingCurve() {
let frames = 65536;
let curve = new Float32Array(frames);
let n = frames;
let n2 = n / 2;
for (let i = 0; i < n; ++i) {
let x = (i - n2) / n2;
let y = Math.atan(5 * x) / (0.5 * Math.PI);
}
return curve;
}
function getBuffer (url, callback) {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onload = callback;
xhr.send();
return xhr;
}
</script>
</body>
</html>

View File

@ -24,6 +24,7 @@ const EXAMPLE_URL = "http://example.com/browser/browser/devtools/webaudioeditor/
const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html";
const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html";
const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html";
// All tests are asynchronous.
waitForExplicitFinish();
@ -219,8 +220,23 @@ function checkVariableView (view, index, hash, description = "") {
let aVar = scope.get(variable);
is(aVar.target.querySelector(".name").getAttribute("value"), variable,
"Correct property name for " + variable);
is(aVar.target.querySelector(".value").getAttribute("value"), hash[variable],
"Correct property value of " + hash[variable] + " for " + variable + " " + description);
let value = aVar.target.querySelector(".value").getAttribute("value");
// Cast value with JSON.parse if possible;
// will fail when displaying Object types like "ArrayBuffer"
// and "Float32Array", but will match the original value.
try {
value = JSON.parse(value);
}
catch (e) {}
if (typeof hash[variable] === "function") {
ok(hash[variable](value),
"Passing property value of " + value + " for " + variable + " " + description);
}
else {
ise(value, hash[variable],
"Correct property value of " + hash[variable] + " for " + variable + " " + description);
}
});
}
@ -292,23 +308,93 @@ function wait (n) {
return promise;
}
/**
* Returns the primitive value of a grip's value, or the
* original form that the string grip.type comes from.
*/
function getGripValue (value) {
if (~["boolean", "string", "number"].indexOf(typeof value)) {
return value;
}
switch (value.type) {
case "undefined": return undefined;
case "Infinity": return Infinity;
case "-Infinity": return -Infinity;
case "NaN": return NaN;
case "-0": return -0;
case "null": return null;
default: return value;
}
}
/**
* List of audio node properties to test against expectations of the AudioNode actor
*/
const NODE_PROPERTIES = {
"OscillatorNode": ["type", "frequency", "detune"],
"GainNode": ["gain"],
"DelayNode": ["delayTime"],
"AudioBufferSourceNode": ["buffer", "playbackRate", "loop", "loopStart", "loopEnd"],
"ScriptProcessorNode": ["bufferSize"],
"PannerNode": ["panningModel", "distanceModel", "refDistance", "maxDistance", "rolloffFactor", "coneInnerAngle", "coneOuterAngle", "coneOuterGain"],
"ConvolverNode": ["buffer", "normalize"],
"DynamicsCompressorNode": ["threshold", "knee", "ratio", "reduction", "attack", "release"],
"BiquadFilterNode": ["type", "frequency", "Q", "detune", "gain"],
"WaveShaperNode": ["curve", "oversample"],
"AnalyserNode": ["fftSize", "minDecibels", "maxDecibels", "smoothingTimeConstraint", "frequencyBinCount"],
"AudioDestinationNode": [],
"ChannelSplitterNode": [],
"ChannelMergerNode": []
const NODE_DEFAULT_VALUES = {
"AudioDestinationNode": {},
"AudioBufferSourceNode": {
"playbackRate": 1,
"loop": false,
"loopStart": 0,
"loopEnd": 0,
"buffer": null
},
"ScriptProcessorNode": {
"bufferSize": 4096
},
"AnalyserNode": {
"fftSize": 2048,
"minDecibels": -100,
"maxDecibels": -30,
"smoothingTimeConstant": 0.8,
"frequencyBinCount": 1024
},
"GainNode": {
"gain": 1
},
"DelayNode": {
"delayTime": 0
},
"BiquadFilterNode": {
"type": "lowpass",
"frequency": 350,
"Q": 1,
"detune": 0,
"gain": 0
},
"WaveShaperNode": {
"curve": null,
"oversample": "none"
},
"PannerNode": {
"panningModel": "HRTF",
"distanceModel": "inverse",
"refDistance": 1,
"maxDistance": 10000,
"rolloffFactor": 1,
"coneInnerAngle": 360,
"coneOuterAngle": 360,
"coneOuterGain": 0
},
"ConvolverNode": {
"buffer": null,
"normalize": true
},
"ChannelSplitterNode": {},
"ChannelMergerNode": {},
"DynamicsCompressorNode": {
"threshold": -24,
"knee": 30,
"ratio": 12,
"reduction": 0,
"attack": 0.003000000026077032,
"release": 0.25
},
"OscillatorNode": {
"type": "sine",
"frequency": 440,
"detune": 0
}
};

View File

@ -11,6 +11,7 @@ const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {})
const events = require("sdk/event/core");
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;
@ -98,7 +99,7 @@ const NODE_PROPERTIES = {
"fftSize": {},
"minDecibels": {},
"maxDecibels": {},
"smoothingTimeConstraint": {},
"smoothingTimeConstant": {},
"frequencyBinCount": { "readonly": true },
},
"AudioDestinationNode": {},
@ -128,7 +129,7 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
protocol.Actor.prototype.initialize.call(this, conn);
this.node = unwrap(node);
try {
this.type = this.node.toString().match(/\[object (.*)\]$/)[1];
this.type = getConstructorName(this.node);
} catch (e) {
this.type = "";
}
@ -188,11 +189,23 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
* Name of the AudioParam to fetch.
*/
getParam: method(function (param) {
// If property does not exist, just return "undefined"
if (!this.node[param])
return undefined;
// Check to see if it's an AudioParam -- if so,
// return the `value` property of the parameter.
let value = isAudioParam(this.node, param) ? this.node[param].value : this.node[param];
return value;
// Return the grip form of the value; at this time,
// there shouldn't be any non-primitives at the moment, other than
// 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;
}, {
request: {
param: Arg(0, "string")
@ -499,7 +512,7 @@ WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS);
* @return Boolean
*/
function isAudioParam (node, prop) {
return /AudioParam/.test(node[prop].toString());
return !!(node[prop] && /AudioParam/.test(node[prop].toString()));
}
/**
@ -516,6 +529,31 @@ function constructError (err) {
};
}
/**
* Takes an object and converts it's `toString()` form, like
* "[object OscillatorNode]" or "[object Float32Array]"
* to a string of just the constructor name, like "OscillatorNode",
* or "Float32Array".
*/
function getConstructorName (obj) {
return obj.toString().match(/\[object (.*)\]$/)[1];
}
/**
* 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.
*/
function createObjectGrip (value) {
return {
type: "object",
preview: {
kind: "ObjectWithText",
text: ""
},
class: getConstructorName(value)
};
}
function unwrap (obj) {
return XPCNativeWrapper.unwrap(obj);
}