Bug 980502 - Implement WebAudio actor and AudioNode actor. r=vp
☠☠ backed out by 9afe2a1145bd ☠ ☠
authorJordan Santell <jsantell@gmail.com>
Thu, 06 Mar 2014 16:39:00 -0800
changeset 175299 81f230994acd50c5937b7f3b643369d0c9d10a97
parent 175298 c65a149ccb6ac24b2856b6103bc3e719de0e94a5
child 175300 246f001547ac59a403a742f9c61e72cb4169f008
push id26486
push userkwierso@gmail.com
push dateWed, 26 Mar 2014 03:03:25 +0000
treeherdermozilla-central@140ac04d7454 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp
bugs980502
milestone31.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
Bug 980502 - Implement WebAudio actor and AudioNode actor. r=vp
browser/app/profile/firefox.js
browser/devtools/moz.build
browser/devtools/webaudioeditor/moz.build
browser/devtools/webaudioeditor/test/browser.ini
browser/devtools/webaudioeditor/test/browser_audionode-actor-get-set-param.js
browser/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js
browser/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js
browser/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js
browser/devtools/webaudioeditor/test/doc_complex-context.html
browser/devtools/webaudioeditor/test/doc_simple-context.html
browser/devtools/webaudioeditor/test/doc_simple-node-creation.html
browser/devtools/webaudioeditor/test/head.js
browser/devtools/webaudioeditor/test/moz.build
toolkit/devtools/server/actors/audionode.js
toolkit/devtools/server/actors/webaudio.js
toolkit/devtools/server/main.js
--- 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)