☠☠ backed out by 9afe2a1145bd ☠ ☠ | |
author | Jordan Santell <jsantell@gmail.com> |
Thu, 06 Mar 2014 16:39:00 -0800 | |
changeset 175299 | 81f230994acd50c5937b7f3b643369d0c9d10a97 |
parent 175298 | c65a149ccb6ac24b2856b6103bc3e719de0e94a5 |
child 175300 | 246f001547ac59a403a742f9c61e72cb4169f008 |
push id | 26486 |
push user | kwierso@gmail.com |
push date | Wed, 26 Mar 2014 03:03:25 +0000 |
treeherder | mozilla-central@140ac04d7454 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | vp |
bugs | 980502 |
milestone | 31.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1200,16 +1200,19 @@ pref("devtools.scratchpad.enableCodeFold // Enable the Style Editor. pref("devtools.styleeditor.enabled", true); pref("devtools.styleeditor.source-maps-enabled", false); pref("devtools.styleeditor.autocompletion-enabled", true); // Enable the Shader Editor. pref("devtools.shadereditor.enabled", false); +// Enable the Web Audio Editor +pref("devtools.webaudioeditor.enabled", false); + // Enable tools for Chrome development. pref("devtools.chrome.enabled", false); // Default theme ("dark" or "light") pref("devtools.theme", "light"); // Display the introductory text pref("devtools.gcli.hideIntro", false);
--- a/browser/devtools/moz.build +++ b/browser/devtools/moz.build @@ -18,17 +18,18 @@ DIRS += [ 'responsivedesign', 'scratchpad', 'shadereditor', 'shared', 'sourceeditor', 'styleeditor', 'styleinspector', 'tilt', + 'webaudioeditor', 'webconsole', ] EXTRA_COMPONENTS += [ 'devtools-clhandler.js', 'devtools-clhandler.manifest', ] -JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file +JAR_MANIFESTS += ['jar.mn']
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# 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/. + +TEST_DIRS += ['test'] + +JS_MODULES_PATH = 'modules/devtools/webaudioeditor' + +EXTRA_JS_MODULES += [ +] +
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser.ini @@ -0,0 +1,12 @@ +[DEFAULT] + +support-files = + doc_simple-context.html + doc_complex-context.html + doc_simple-node-creation.html + head.js + +[browser_webaudio-actor-simple.js] +[browser_audionode-actor-get-set-param.js] +[browser_audionode-actor-is-source.js] +[browser_audionode-actor-get-type.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test AudioNode#getParam() / AudioNode#setParam() + */ + +function spawnTest () { + let [target, debuggee, front] = yield initBackend(SIMPLE_CONTEXT_URL); + let [_, [destNode, oscNode, gainNode]] = yield Promise.all([ + front.setup({ reload: true }), + get3(front, "create-node") + ]); + + let freq = yield oscNode.getParam("frequency"); + info(typeof freq); + ise(freq, 440, "AudioNode:getParam correctly fetches AudioParam"); + + let type = yield oscNode.getParam("type"); + 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"); + + let resSuccess = yield oscNode.setParam("frequency", "220", "number"); + let freq = yield oscNode.getParam("frequency"); + ise(freq, 220, "AudioNode:setParam correctly sets a `number` AudioParam"); + is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam"); + + resSuccess = yield oscNode.setParam("type", "square", "string"); + let type = yield oscNode.getParam("type"); + ise(type, "square", "AudioNode:setParam correctly sets a `string` non-AudioParam"); + is(resSuccess, undefined, "AudioNode:setParam returns undefined for correctly set AudioParam"); + + resSuccess = yield oscNode.setParam("type", "\"triangle\"", "string"); + type = yield oscNode.getParam("type"); + ise(type, "triangle", "AudioNode:setParam correctly removes quotes in `string` non-AudioParam"); + + try { + yield oscNode.setParam("frequency", "hello", "string"); + ok(false, "setParam with invalid types should throw"); + } catch (e) { + ok(/is not a finite floating-point/.test(e.message), "AudioNode:setParam returns error with correct message when attempting an invalid assignment"); + is(e.type, "TypeError", "AudioNode:setParam returns error with correct type when attempting an invalid assignment"); + freq = yield oscNode.getParam("frequency"); + ise(freq, 220, "AudioNode:setParam does not modify value when an error occurs"); + } + + yield removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test AudioNode#getType() + */ + +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 actualTypes = yield Promise.all(nodes.map(node => node.getType())); + let expectedTypes = [ + "AudioDestinationNode", + "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode", + "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode", + "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode" + ]; + + expectedTypes.forEach((type, i) => { + is(actualTypes[i], type, type + " successfully created with correct type"); + }); + + yield removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test AudioNode#isSource() + */ + +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 actualTypes = yield Promise.all(nodes.map(node => node.getType())); + let isSourceResult = yield Promise.all(nodes.map(node => node.isSource())); + + actualTypes.forEach((type, i) => { + let shouldBeSource = type === "AudioBufferSourceNode" || type === "OscillatorNode"; + if (shouldBeSource) + is(isSourceResult[i], true, type + "'s isSource() yields into `true`"); + else + is(isSourceResult[i], false, type + "'s isSource() yields into `false`"); + }); + + yield removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test basic communication of Web Audio actor + */ + +function spawnTest () { + let [target, debuggee, front] = yield initBackend(SIMPLE_CONTEXT_URL); + let [_, __, [destNode, oscNode, gainNode], [connect1, connect2]] = yield Promise.all([ + front.setup({ reload: true }), + once(front, "start-context"), + get3(front, "create-node"), + get2(front, "connect-node") + ]); + + let destType = yield destNode.getType(); + let oscType = yield oscNode.getType(); + let gainType = yield gainNode.getType(); + + is(destType, "AudioDestinationNode", "WebAudioActor:create-node returns AudioNodeActor for AudioDestination"); + is(oscType, "OscillatorNode", "WebAudioActor:create-node returns AudioNodeActor"); + is(gainType, "GainNode", "WebAudioActor:create-node returns AudioNodeActor"); + + let { source, dest } = connect1; + is(source.actorID, oscNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (osc->gain)"); + is(dest.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (osc->gain)"); + + let { source, dest } = connect2; + is(source.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (gain->dest)"); + is(dest.actorID, destNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (gain->dest)"); + + yield removeTab(target.tab); + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/doc_complex-context.html @@ -0,0 +1,44 @@ +<!-- 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"; + +/* + ↱ proc + osc → gain → + osc → gain → destination + buffer →↳ filter → +*/ + let ctx = new AudioContext(); + let osc1 = ctx.createOscillator(); + let gain1 = ctx.createGain(); + let proc = ctx.createScriptProcessor(); + osc1.connect(gain1); + osc1.connect(proc); + gain1.connect(ctx.destination); + + let osc2 = ctx.createOscillator(); + let gain2 = ctx.createGain(); + osc2.connect(gain2); + gain2.connect(ctx.destination); + + let buf = ctx.createBufferSource(); + let filter = ctx.createBiquadFilter(); + buf.connect(filter); + osc2.connect(filter); + filter.connect(ctx.destination); + + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/doc_simple-context.html @@ -0,0 +1,26 @@ +<!-- 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 gain = ctx.createGain(); + gain.gain.value = 0; + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(0); + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/doc_simple-node-creation.html @@ -0,0 +1,28 @@ +<!-- 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 NODE_CREATION_METHODS = [ + "createBufferSource", "createScriptProcessor", "createAnalyser", + "createGain", "createDelay", "createBiquadFilter", "createWaveShaper", + "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger", + "createDynamicsCompressor", "createOscillator" + ]; + let nodes = NODE_CREATION_METHODS.map(method => ctx[method]()); + + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/head.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +// Enable logging for all the tests. Both the debugger server and frontend will +// be affected by this pref. +let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", true); + +let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); +let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); + +let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio"); +let TargetFactory = devtools.TargetFactory; +let Toolbox = devtools.Toolbox; + +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"; +const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html"; + +// All tests are asynchronous. +waitForExplicitFinish(); + +let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled"); + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", gToolEnabled); + Cu.forceGC(); +}); + +function addTab(aUrl, aWindow) { + info("Adding tab: " + aUrl); + + let deferred = Promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + + targetWindow.focus(); + let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); + let linkedBrowser = tab.linkedBrowser; + + linkedBrowser.addEventListener("load", function onLoad() { + linkedBrowser.removeEventListener("load", onLoad, true); + info("Tab added and finished loading: " + aUrl); + deferred.resolve(tab); + }, true); + + return deferred.promise; +} + +function removeTab(aTab, aWindow) { + info("Removing tab."); + + let deferred = Promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + let tabContainer = targetBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + targetBrowser.removeTab(aTab); + return deferred.promise; +} + +function handleError(aError) { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); +} + +function once(aTarget, aEventName, aUseCapture = false) { + info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); + + let deferred = Promise.defer(); + + for (let [add, remove] of [ + ["on", "off"], // Use event emitter before DOM events for consistency + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"] + ]) { + if ((add in aTarget) && (remove in aTarget)) { + aTarget[add](aEventName, function onEvent(...aArgs) { + aTarget[remove](aEventName, onEvent, aUseCapture); + deferred.resolve(...aArgs); + }, aUseCapture); + break; + } + } + + return deferred.promise; +} + +function test () { + Task.spawn(spawnTest).then(finish, handleError); +} + +function initBackend(aUrl) { + info("Initializing a web audio editor front."); + + if (!DebuggerServer.initialized) { + DebuggerServer.init(() => true); + DebuggerServer.addBrowserActors(); + } + + return Task.spawn(function*() { + let tab = yield addTab(aUrl); + let target = TargetFactory.forTab(tab); + let debuggee = target.window.wrappedJSObject; + + yield target.makeRemote(); + + let front = new WebAudioFront(target.client, target.form); + return [target, debuggee, front]; + }); +} + +// Due to web audio will fire most events synchronously back-to-back, +// and we can't yield them in a chain without missing actors, this allows +// us to listen for `n` events and return a promise resolving to them. +// +// Takes a `front` object that is an event emitter, the number of +// programs that should be listened to and waited on, and an optional +// `onAdd` function that calls with the entire actors array on program link +function getN (front, eventName, count, spread) { + let actors = []; + let deferred = Promise.defer(); + front.on(eventName, function onEvent (...args) { + let actor = args[0]; + if (actors.length !== count) { + actors.push(spread ? args : actor); + } + if (actors.length === count) { + front.off(eventName, onEvent); + deferred.resolve(actors); + } + }); + return deferred.promise; +} + +function get (front, eventName) { return getN(front, eventName, 1); } +function get2 (front, eventName) { return getN(front, eventName, 2); } +function get3 (front, eventName) { return getN(front, eventName, 3); } +function getSpread (front, eventName) { return getN(front, eventName, 1, true); } +function get2Spread (front, eventName) { return getN(front, eventName, 2, true); } +function get3Spread (front, eventName) { return getN(front, eventName, 3, true); } +function getNSpread (front, eventName, count) { return getN(front, eventName, count, true); }
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# 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/. + +BROWSER_CHROME_MANIFESTS += ['browser.ini']
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/actors/audionode.js @@ -0,0 +1,173 @@ +/* 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"; + +const {Cc, Ci, Cu, Cr} = require("chrome"); +const protocol = require("devtools/server/protocol"); +const { method, Arg, Option, RetVal } = protocol; + +// Add a grip type for our `getParam` method, as the type can be +// unknown. +protocol.types.addDictType("audio-node-param-grip", { + type: "string", + value: "nullable:primitive" +}); + +/** + * An Audio Node actor allowing communication to a specific audio node in the + * Audio Context graph. + */ +let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({ + typeName: "audionode", + + /** + * Create the Audio Node actor. + * + * @param DebuggerServerConnection conn + * The server connection. + * @param AudioNode node + * The AudioNode that was created. + */ + initialize: function (conn, node) { + protocol.Actor.prototype.initialize.call(this, conn); + this.node = XPCNativeWrapper.unwrap(node); + try { + this.type = this.node.toString().match(/\[object (.*)\]$/)[1]; + } catch (e) { + this.type = ""; + } + }, + + /** + * Returns the name of the audio type. + * Examples: "OscillatorNode", "MediaElementAudioSourceNode" + */ + getType: method(function () { + return this.type; + }, { + response: { type: RetVal("string") } + }), + + /** + * Returns a boolean indicating if the node is a source node, + * like BufferSourceNode, MediaElementAudioSourceNode, OscillatorNode, etc. + */ + isSource: method(function () { + return !!~this.type.indexOf("Source") || this.type === "OscillatorNode"; + }, { + response: { source: RetVal("boolean") } + }), + + /** + * Changes a param on the audio node. Responds with a `string` that's either + * an empty string `""` on success, or a description of the error upon + * param set failure. + * + * @param String param + * Name of the AudioParam to change. + * @param String value + * Value to change AudioParam to. Subsequently cast via `type`. + * @param String type + * Datatype that `value` should be cast to. + */ + setParam: method(function (param, value, dataType) { + // Strip quotes because sometimes UIs include that for strings + if (dataType === "string") { + value = value.replace(/[\'\"]*/g, ""); + } + try { + if (isAudioParam(this.node, param)) + this.node[param].value = cast(value, dataType); + else + this.node[param] = cast(value, dataType); + return undefined; + } catch (e) { + return constructError(e); + } + }, { + request: { + param: Arg(0, "string"), + value: Arg(1, "string"), + dataType: Arg(2, "string") + }, + response: { error: RetVal("nullable:json") } + }), + + /** + * Gets a param on the audio node. + * + * @param String param + * Name of the AudioParam to fetch. + */ + getParam: method(function (param) { + // If property does not exist, just return "undefined" + if (!this.node[param]) + return undefined; + let value = isAudioParam(this.node, param) ? this.node[param].value : this.node[param]; + let type = typeof type; + return value; + return { type: type, value: value }; + }, { + request: { + param: Arg(0, "string") + }, + response: { text: RetVal("nullable:primitive") } + }), +}); + +/** + * Casts string `value` to specified `type`. + * + * @param String value + * The string to cast. + * @param String type + * The datatype to cast `value` to. + */ +function cast (value, type) { + if (!type || type === "string") + return value; + if (type === "number") + return parseFloat(value); + if (type === "boolean") + return value === "true"; + return undefined; +} + +/** + * Determines whether or not property is an AudioParam. + * + * @param AudioNode node + * An AudioNode. + * @param String prop + * Property of `node` to evaluate to see if it's an AudioParam. + * @return Boolean + */ +function isAudioParam (node, prop) { + return /AudioParam/.test(node[prop].toString()); +} + +/** + * Takes an `Error` object and constructs a JSON-able response + * + * @param Error err + * A TypeError, RangeError, etc. + * @return Object + */ +function constructError (err) { + return { + message: err.message, + type: err.constructor.name + }; +} + +/** + * The corresponding Front object for the AudioNodeActor. + */ +let AudioNodeFront = protocol.FrontClass(AudioNodeActor, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + client.addActorPool(this); + this.manage(this); + } +});
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/server/actors/webaudio.js @@ -0,0 +1,427 @@ +/* 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"; + +const {Cc, Ci, Cu, Cr} = require("chrome"); + +const Services = require("Services"); + +const events = require("sdk/event/core"); +const protocol = require("devtools/server/protocol"); + +const { on, once, off, emit } = events; +const { method, Arg, Option, RetVal } = protocol; +const { AudioNodeActor } = require("devtools/server/actors/audionode"); +const console = Cu.import("resource://gre/modules/devtools/Console.jsm").console; + +exports.register = function(handle) { + handle.addTabActor(WebAudioActor, "webaudioActor"); +}; + +exports.unregister = function(handle) { + handle.removeTabActor(WebAudioActor); +}; + +/** + * A WebGL Shader contributing to building a WebGL Program. + +/** + * The Web Audio Actor handles simple interaction with an AudioContext + * high-level methods. After instantiating this actor, you'll need to set it + * up by calling setup(). + */ +let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({ + typeName: "webaudio", + initialize: function(conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, conn); + this.tabActor = tabActor; + this._onGlobalCreated = this._onGlobalCreated.bind(this); + this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this); + + this._onStartContext = this._onStartContext.bind(this); + this._onConnectNode = this._onConnectNode.bind(this); + this._onConnectParam = this._onConnectParam.bind(this); + this._onDisconnectNode = this._onDisconnectNode.bind(this); + this._onParamChange = this._onParamChange.bind(this); + this._onCreateNode = this._onCreateNode.bind(this); + }, + + destroy: function(conn) { + protocol.Actor.prototype.destroy.call(this, conn); + this.finalize(); + }, + + /** + * Starts waiting for the current tab actor's document global to be + * created, in order to instrument the Canvas context and become + * aware of everything the content does with Web Audio. + * + * See ContentObserver and WebAudioInstrumenter for more details. + */ + setup: method(function({ reload }) { + if (this._initialized) { + return; + } + this._initialized = true; + + // Weak map mapping audio nodes to their corresponding actors + this._nodeActors = new Map(); + + this._contentObserver = new ContentObserver(this.tabActor); + this._webaudioObserver = new WebAudioObserver(); + + on(this._contentObserver, "global-created", this._onGlobalCreated); + on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed); + + on(this._webaudioObserver, "start-context", this._onStartContext); + on(this._webaudioObserver, "connect-node", this._onConnectNode); + on(this._webaudioObserver, "connect-param", this._onConnectParam); + on(this._webaudioObserver, "disconnect-node", this._onDisconnectNode); + on(this._webaudioObserver, "param-change", this._onParamChange); + on(this._webaudioObserver, "create-node", this._onCreateNode); + + if (reload) { + this.tabActor.window.location.reload(); + } + }, { + request: { reload: Option(0, "boolean") }, + oneway: true + }), + + /** + * Stops listening for document global changes and puts this actor + * to hibernation. This method is called automatically just before the + * actor is destroyed. + */ + finalize: method(function() { + if (!this._initialized) { + return; + } + this._initialized = false; + + this._contentObserver.stopListening(); + off(this._contentObserver, "global-created", this._onGlobalCreated); + off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed); + + off(this._webaudioObserver, "start-context", this._onStartContext); + off(this._webaudioObserver, "connect-node", this._onConnectNode); + off(this._webaudioObserver, "connect-param", this._onConnectParam); + off(this._webaudioObserver, "disconnect-node", this._onDisconnectNode); + off(this._webaudioObserver, "param-change", this._onParamChange); + off(this._webaudioObserver, "create-node", this._onCreateNode); + + this._contentObserver = null; + this._webaudioObserver = null; + }, { + oneway: true + }), + + /** + * Events emitted by this actor. + */ + events: { + "start-context": { + type: "startContext" + }, + "connect-node": { + type: "connectNode", + source: Option(0, "audionode"), + dest: Option(0, "audionode") + }, + "disconnect-node": { + type: "disconnectNode", + source: Arg(0, "audionode") + }, + "connect-param": { + type: "connectParam", + source: Arg(0, "audionode"), + param: Arg(1, "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") + } + }, + + /** + * Invoked whenever the current tab actor's document global is created. + */ + _onGlobalCreated: function(window) { + WebAudioInstrumenter.handle(window, this._webaudioObserver); + }, + + /** + * Invoked whenever the current tab actor's inner window is destroyed. + */ + _onGlobalDestroyed: function(id) { + }, + + /** + * Helper for constructing an AudioNodeActor, assigning to + * internal weak map, and tracking via `manage` so it is assigned + * an `actorID`. + */ + _constructAudioNode: function (node) { + let actor = new AudioNodeActor(this.conn, node); + this.manage(actor); + this._nodeActors.set(node, actor); + return actor; + }, + + /** + * Takes an AudioNode and returns the stored actor for it. + * In some cases, we won't have an actor stored (for example, + * connecting to an AudioDestinationNode, since it's implicitly + * created), so make a new actor and store that. + */ + _actorFor: function (node) { + let actor = this._nodeActors.get(node); + if (!actor) { + actor = this._constructAudioNode(node); + } + return actor; + }, + + /** + * Called on first audio node creation, signifying audio context usage + */ + _onStartContext: function () { + events.emit(this, "start-context"); + }, + + /** + * Called when one audio node is connected to another. + */ + _onConnectNode: function (source, dest) { + let sourceActor = this._actorFor(source); + let destActor = this._actorFor(dest); + events.emit(this, "connect-node", { + source: sourceActor, + dest: destActor + }); + }, + + /** + * Called when an audio node is connected to an audio param. + * Implement in bug 986705 + */ + _onConnectParam: function (source, dest) { + // TODO bug 986705 + }, + + /** + * Called when an audio node is disconnected. + */ + _onDisconnectNode: function (node) { + let actor = this._actorFor(node); + events.emit(this, "disconnect-node", actor); + }, + + /** + * Called when a parameter changes on an audio node + */ + _onParamChange: function (node, param, value) { + let actor = this._actorFor(node); + events.emit(this, "param-change", { + source: actor, + param: param, + value: value + }); + }, + + /** + * Called on node creation. + */ + _onCreateNode: function (node) { + let actor = this._constructAudioNode(node); + events.emit(this, "create-node", actor); + } +}); + +/** + * The corresponding Front object for the WebAudioActor. + */ +let WebAudioFront = exports.WebAudioFront = protocol.FrontClass(WebAudioActor, { + initialize: function(client, { webaudioActor }) { + protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor }); + client.addActorPool(this); + this.manage(this); + } +}); + +/** + * Handles adding an observer for the creation of content document globals, + * event sent immediately after a web content document window has been set up, + * but before any script code has been executed. This will allow us to + * instrument the HTMLCanvasElement with the appropriate inspection methods. + * TODO use ContentObserver from bug 917226 once landed, bug 986704 + */ +function ContentObserver(tabActor) { + this._contentWindow = tabActor.window; + this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this); + this._onInnerWindowDestroyed = this._onInnerWindowDestroyed.bind(this); + this.startListening(); +} + +ContentObserver.prototype = { + /** + * Starts listening for the required observer messages. + */ + startListening: function() { + Services.obs.addObserver( + this._onContentGlobalCreated, "content-document-global-created", false); + Services.obs.addObserver( + this._onInnerWindowDestroyed, "inner-window-destroyed", false); + }, + + /** + * Stops listening for the required observer messages. + */ + stopListening: function() { + Services.obs.removeObserver( + this._onContentGlobalCreated, "content-document-global-created", false); + Services.obs.removeObserver( + this._onInnerWindowDestroyed, "inner-window-destroyed", false); + }, + + /** + * Fired immediately after a web content document window has been set up. + */ + _onContentGlobalCreated: function(subject, topic, data) { + if (subject == this._contentWindow) { + emit(this, "global-created", subject); + } + }, + + /** + * Fired when an inner window is removed from the backward/forward cache. + */ + _onInnerWindowDestroyed: function(subject, topic, data) { + let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + emit(this, "global-destroyed", id); + } +}; + +/** + * Instruments an AudioContext with inspector methods. + * TODO refactor with CallWatcherActor, bug 986704 + */ +let WebAudioInstrumenter = { + /** + * Overrides all AudioContext methods. + * + * @param nsIDOMWindow window + * The window to perform the instrumentation in. + * @param WebAudioObserver observer + * The observer watching function calls in the context. + */ + handle: function(window, observer) { + let self = this; + + let AudioContext = unwrap(window.AudioContext); + let AudioNode = unwrap(window.AudioNode); + let ctxProto = AudioContext.prototype; + let nodeProto = AudioNode.prototype; + + // All Web Audio nodes inherit from AudioNode's prototype, so + // hook into the `connect` and `disconnect` methods + + // audionode.connect(node|param); + let originalConnect = nodeProto.connect; + nodeProto.connect = function (...args) { + let source = unwrap(this); + let nodeOrParam = unwrap(args[0]); + originalConnect.apply(source, args); + + // Alert observer differently if connecting to an AudioNode or AudioParam + if (nodeOrParam instanceof AudioNode) + observer.connectNode(source, nodeOrParam); + else + observer.connectParam(source, nodeOrParam); + }; + + // audionode.disconnect() + let originalDisconnect = nodeProto.disconnect; + nodeProto.disconnect = function (...args) { + let source = unwrap(this); + originalDisconnect.apply(source, args); + observer.disconnectNode(source); + }; + + + // Keep track of the first node created, so we can alert + // the front end that an audio context is being used since + // we're not hooking into the constructor itself, just its + // instance's methods. + let firstNodeCreated = false; + + // Patch all of AudioContext's methods that create an audio node + // and hook into the observer + NODE_CREATION_METHODS.forEach(method => { + let originalMethod = ctxProto[method]; + ctxProto[method] = function (...args) { + let node = originalMethod.apply(this, args); + // Fire the start-up event if this is the first node created + // and trigger a `create-node` event for the context destination + if (!firstNodeCreated) { + firstNodeCreated = true; + observer.startContext(); + observer.createNode(node.context.destination); + } + observer.createNode(node); + return node; + }; + }); + } +}; + +/** + * An observer that captures an Audio Context's actions and emits + * events + */ +function WebAudioObserver () {} + +WebAudioObserver.prototype = { + startContext: function () { + emit(this, "start-context"); + }, + + connectNode: function (source, dest) { + emit(this, "connect-node", source, dest); + }, + + connectParam: function (source, param) { + emit(this, "connect-param", source, param); + }, + + disconnectNode: function (source) { + emit(this, "disconnect-node", source); + }, + + createNode: function (source) { + emit(this, "create-node", source); + }, + + paramChange: function (node, param, val) { + emit(this, "param-change", node, param, val); + } +}; + +function unwrap (obj) { + return XPCNativeWrapper.unwrap(obj); +} + +let NODE_CREATION_METHODS = [ + "createBufferSource", "createMediaElementSource", "createMediaStreamSource", + "createMediaStreamDestination", "createScriptProcessor", "createAnalyser", + "createGain", "createDelay", "createBiquadFilter", "createWaveShaper", + "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger", + "createDynamicsCompressor", "createOscillator" +];
--- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -387,16 +387,17 @@ var DebuggerServer = { /** * Install tab actors. */ addTabActors: function() { this.addActors("resource://gre/modules/devtools/server/actors/script.js"); this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js"); this.registerModule("devtools/server/actors/inspector"); this.registerModule("devtools/server/actors/webgl"); + this.registerModule("devtools/server/actors/webaudio"); this.registerModule("devtools/server/actors/stylesheets"); this.registerModule("devtools/server/actors/styleeditor"); this.registerModule("devtools/server/actors/storage"); this.registerModule("devtools/server/actors/gcli"); this.registerModule("devtools/server/actors/tracer"); this.registerModule("devtools/server/actors/memory"); this.registerModule("devtools/server/actors/eventlooplag"); if ("nsIProfiler" in Ci)