michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu, Cr} = require("chrome"); michael@0: michael@0: const Services = require("Services"); michael@0: michael@0: const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const events = require("sdk/event/core"); michael@0: const protocol = require("devtools/server/protocol"); michael@0: const { CallWatcherActor, CallWatcherFront } = require("devtools/server/actors/call-watcher"); michael@0: michael@0: const { on, once, off, emit } = events; michael@0: const { method, Arg, Option, RetVal } = protocol; michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addTabActor(WebAudioActor, "webaudioActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(WebAudioActor); michael@0: }; michael@0: michael@0: const AUDIO_GLOBALS = [ michael@0: "AudioContext", "AudioNode" michael@0: ]; michael@0: michael@0: const NODE_CREATION_METHODS = [ michael@0: "createBufferSource", "createMediaElementSource", "createMediaStreamSource", michael@0: "createMediaStreamDestination", "createScriptProcessor", "createAnalyser", michael@0: "createGain", "createDelay", "createBiquadFilter", "createWaveShaper", michael@0: "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger", michael@0: "createDynamicsCompressor", "createOscillator" michael@0: ]; michael@0: michael@0: const NODE_ROUTING_METHODS = [ michael@0: "connect", "disconnect" michael@0: ]; michael@0: michael@0: const NODE_PROPERTIES = { michael@0: "OscillatorNode": { michael@0: "type": {}, michael@0: "frequency": {}, michael@0: "detune": {} michael@0: }, michael@0: "GainNode": { michael@0: "gain": {} michael@0: }, michael@0: "DelayNode": { michael@0: "delayTime": {} michael@0: }, michael@0: "AudioBufferSourceNode": { michael@0: "buffer": { "Buffer": true }, michael@0: "playbackRate": {}, michael@0: "loop": {}, michael@0: "loopStart": {}, michael@0: "loopEnd": {} michael@0: }, michael@0: "ScriptProcessorNode": { michael@0: "bufferSize": { "readonly": true } michael@0: }, michael@0: "PannerNode": { michael@0: "panningModel": {}, michael@0: "distanceModel": {}, michael@0: "refDistance": {}, michael@0: "maxDistance": {}, michael@0: "rolloffFactor": {}, michael@0: "coneInnerAngle": {}, michael@0: "coneOuterAngle": {}, michael@0: "coneOuterGain": {} michael@0: }, michael@0: "ConvolverNode": { michael@0: "buffer": { "Buffer": true }, michael@0: "normalize": {}, michael@0: }, michael@0: "DynamicsCompressorNode": { michael@0: "threshold": {}, michael@0: "knee": {}, michael@0: "ratio": {}, michael@0: "reduction": {}, michael@0: "attack": {}, michael@0: "release": {} michael@0: }, michael@0: "BiquadFilterNode": { michael@0: "type": {}, michael@0: "frequency": {}, michael@0: "Q": {}, michael@0: "detune": {}, michael@0: "gain": {} michael@0: }, michael@0: "WaveShaperNode": { michael@0: "curve": { "Float32Array": true }, michael@0: "oversample": {} michael@0: }, michael@0: "AnalyserNode": { michael@0: "fftSize": {}, michael@0: "minDecibels": {}, michael@0: "maxDecibels": {}, michael@0: "smoothingTimeConstraint": {}, michael@0: "frequencyBinCount": { "readonly": true }, michael@0: }, michael@0: "AudioDestinationNode": {}, michael@0: "ChannelSplitterNode": {}, michael@0: "ChannelMergerNode": {} michael@0: }; michael@0: michael@0: /** michael@0: * Track an array of audio nodes michael@0: michael@0: /** michael@0: * An Audio Node actor allowing communication to a specific audio node in the michael@0: * Audio Context graph. michael@0: */ michael@0: let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({ michael@0: typeName: "audionode", michael@0: michael@0: /** michael@0: * Create the Audio Node actor. michael@0: * michael@0: * @param DebuggerServerConnection conn michael@0: * The server connection. michael@0: * @param AudioNode node michael@0: * The AudioNode that was created. michael@0: */ michael@0: initialize: function (conn, node) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this.node = unwrap(node); michael@0: try { michael@0: this.type = this.node.toString().match(/\[object (.*)\]$/)[1]; michael@0: } catch (e) { michael@0: this.type = ""; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the name of the audio type. michael@0: * Examples: "OscillatorNode", "MediaElementAudioSourceNode" michael@0: */ michael@0: getType: method(function () { michael@0: return this.type; michael@0: }, { michael@0: response: { type: RetVal("string") } michael@0: }), michael@0: michael@0: /** michael@0: * Returns a boolean indicating if the node is a source node, michael@0: * like BufferSourceNode, MediaElementAudioSourceNode, OscillatorNode, etc. michael@0: */ michael@0: isSource: method(function () { michael@0: return !!~this.type.indexOf("Source") || this.type === "OscillatorNode"; michael@0: }, { michael@0: response: { source: RetVal("boolean") } michael@0: }), michael@0: michael@0: /** michael@0: * Changes a param on the audio node. Responds with a `string` that's either michael@0: * an empty string `""` on success, or a description of the error upon michael@0: * param set failure. michael@0: * michael@0: * @param String param michael@0: * Name of the AudioParam to change. michael@0: * @param String value michael@0: * Value to change AudioParam to. michael@0: */ michael@0: setParam: method(function (param, value) { michael@0: // Strip quotes because sometimes UIs include that for strings michael@0: if (typeof value === "string") { michael@0: value = value.replace(/[\'\"]*/g, ""); michael@0: } michael@0: try { michael@0: if (isAudioParam(this.node, param)) michael@0: this.node[param].value = value; michael@0: else michael@0: this.node[param] = value; michael@0: return undefined; michael@0: } catch (e) { michael@0: return constructError(e); michael@0: } michael@0: }, { michael@0: request: { michael@0: param: Arg(0, "string"), michael@0: value: Arg(1, "nullable:primitive") michael@0: }, michael@0: response: { error: RetVal("nullable:json") } michael@0: }), michael@0: michael@0: /** michael@0: * Gets a param on the audio node. michael@0: * michael@0: * @param String param michael@0: * Name of the AudioParam to fetch. michael@0: */ michael@0: getParam: method(function (param) { michael@0: // If property does not exist, just return "undefined" michael@0: if (!this.node[param]) michael@0: return undefined; michael@0: let value = isAudioParam(this.node, param) ? this.node[param].value : this.node[param]; michael@0: return value; michael@0: }, { michael@0: request: { michael@0: param: Arg(0, "string") michael@0: }, michael@0: response: { text: RetVal("nullable:primitive") } michael@0: }), michael@0: michael@0: /** michael@0: * Get an object containing key-value pairs of additional attributes michael@0: * to be consumed by a front end, like if a property should be read only, michael@0: * or is a special type (Float32Array, Buffer, etc.) michael@0: * michael@0: * @param String param michael@0: * Name of the AudioParam whose flags are desired. michael@0: */ michael@0: getParamFlags: method(function (param) { michael@0: return (NODE_PROPERTIES[this.type] || {})[param]; michael@0: }, { michael@0: request: { param: Arg(0, "string") }, michael@0: response: { flags: RetVal("nullable:primitive") } michael@0: }), michael@0: michael@0: /** michael@0: * Get an array of objects each containing a `param` and `value` property, michael@0: * corresponding to a property name and current value of the audio node. michael@0: */ michael@0: getParams: method(function (param) { michael@0: let props = Object.keys(NODE_PROPERTIES[this.type]); michael@0: return props.map(prop => michael@0: ({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) })); michael@0: }, { michael@0: response: { params: RetVal("json") } michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the AudioNodeActor. michael@0: */ michael@0: let AudioNodeFront = protocol.FrontClass(AudioNodeActor, { michael@0: initialize: function (client, form) { michael@0: protocol.Front.prototype.initialize.call(this, client, form); michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * The Web Audio Actor handles simple interaction with an AudioContext michael@0: * high-level methods. After instantiating this actor, you'll need to set it michael@0: * up by calling setup(). michael@0: */ michael@0: let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({ michael@0: typeName: "webaudio", michael@0: initialize: function(conn, tabActor) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this.tabActor = tabActor; michael@0: this._onContentFunctionCall = this._onContentFunctionCall.bind(this); michael@0: }, michael@0: michael@0: destroy: function(conn) { michael@0: protocol.Actor.prototype.destroy.call(this, conn); michael@0: this.finalize(); michael@0: }, michael@0: michael@0: /** michael@0: * Starts waiting for the current tab actor's document global to be michael@0: * created, in order to instrument the Canvas context and become michael@0: * aware of everything the content does with Web Audio. michael@0: * michael@0: * See ContentObserver and WebAudioInstrumenter for more details. michael@0: */ michael@0: setup: method(function({ reload }) { michael@0: if (this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = true; michael@0: michael@0: // Weak map mapping audio nodes to their corresponding actors michael@0: this._nodeActors = new Map(); michael@0: michael@0: this._callWatcher = new CallWatcherActor(this.conn, this.tabActor); michael@0: this._callWatcher.onCall = this._onContentFunctionCall; michael@0: this._callWatcher.setup({ michael@0: tracedGlobals: AUDIO_GLOBALS, michael@0: startRecording: true, michael@0: performReload: reload michael@0: }); michael@0: michael@0: // Used to track when something is happening with the web audio API michael@0: // the first time, to ultimately fire `start-context` event michael@0: this._firstNodeCreated = false; michael@0: }, { michael@0: request: { reload: Option(0, "boolean") }, michael@0: oneway: true michael@0: }), michael@0: michael@0: /** michael@0: * Invoked whenever an instrumented function is called, like an AudioContext michael@0: * method or an AudioNode method. michael@0: */ michael@0: _onContentFunctionCall: function(functionCall) { michael@0: let { name } = functionCall.details; michael@0: michael@0: // All Web Audio nodes inherit from AudioNode's prototype, so michael@0: // hook into the `connect` and `disconnect` methods michael@0: if (WebAudioFront.NODE_ROUTING_METHODS.has(name)) { michael@0: this._handleRoutingCall(functionCall); michael@0: } michael@0: else if (WebAudioFront.NODE_CREATION_METHODS.has(name)) { michael@0: this._handleCreationCall(functionCall); michael@0: } michael@0: }, michael@0: michael@0: _handleRoutingCall: function(functionCall) { michael@0: let { caller, args, window, name } = functionCall.details; michael@0: let source = unwrap(caller); michael@0: let dest = unwrap(args[0]); michael@0: let isAudioParam = dest instanceof unwrap(window.AudioParam); michael@0: michael@0: // audionode.connect(param) michael@0: if (name === "connect" && isAudioParam) { michael@0: this._onConnectParam(source, dest); michael@0: } michael@0: // audionode.connect(node) michael@0: else if (name === "connect") { michael@0: this._onConnectNode(source, dest); michael@0: } michael@0: // audionode.disconnect() michael@0: else if (name === "disconnect") { michael@0: this._onDisconnectNode(source); michael@0: } michael@0: }, michael@0: michael@0: _handleCreationCall: function (functionCall) { michael@0: let { caller, result } = functionCall.details; michael@0: // Keep track of the first node created, so we can alert michael@0: // the front end that an audio context is being used since michael@0: // we're not hooking into the constructor itself, just its michael@0: // instance's methods. michael@0: if (!this._firstNodeCreated) { michael@0: // Fire the start-up event if this is the first node created michael@0: // and trigger a `create-node` event for the context destination michael@0: this._onStartContext(); michael@0: this._onCreateNode(unwrap(caller.destination)); michael@0: this._firstNodeCreated = true; michael@0: } michael@0: this._onCreateNode(result); michael@0: }, michael@0: michael@0: /** michael@0: * Stops listening for document global changes and puts this actor michael@0: * to hibernation. This method is called automatically just before the michael@0: * actor is destroyed. michael@0: */ michael@0: finalize: method(function() { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = false; michael@0: this._callWatcher.eraseRecording(); michael@0: michael@0: this._callWatcher.finalize(); michael@0: this._callWatcher = null; michael@0: }, { michael@0: oneway: true michael@0: }), michael@0: michael@0: /** michael@0: * Events emitted by this actor. michael@0: */ michael@0: events: { michael@0: "start-context": { michael@0: type: "startContext" michael@0: }, michael@0: "connect-node": { michael@0: type: "connectNode", michael@0: source: Option(0, "audionode"), michael@0: dest: Option(0, "audionode") michael@0: }, michael@0: "disconnect-node": { michael@0: type: "disconnectNode", michael@0: source: Arg(0, "audionode") michael@0: }, michael@0: "connect-param": { michael@0: type: "connectParam", michael@0: source: Arg(0, "audionode"), michael@0: param: Arg(1, "string") michael@0: }, michael@0: "change-param": { michael@0: type: "changeParam", michael@0: source: Option(0, "audionode"), michael@0: param: Option(0, "string"), michael@0: value: Option(0, "string") michael@0: }, michael@0: "create-node": { michael@0: type: "createNode", michael@0: source: Arg(0, "audionode") michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Helper for constructing an AudioNodeActor, assigning to michael@0: * internal weak map, and tracking via `manage` so it is assigned michael@0: * an `actorID`. michael@0: */ michael@0: _constructAudioNode: function (node) { michael@0: let actor = new AudioNodeActor(this.conn, node); michael@0: this.manage(actor); michael@0: this._nodeActors.set(node, actor); michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Takes an AudioNode and returns the stored actor for it. michael@0: * In some cases, we won't have an actor stored (for example, michael@0: * connecting to an AudioDestinationNode, since it's implicitly michael@0: * created), so make a new actor and store that. michael@0: */ michael@0: _actorFor: function (node) { michael@0: let actor = this._nodeActors.get(node); michael@0: if (!actor) { michael@0: actor = this._constructAudioNode(node); michael@0: } michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Called on first audio node creation, signifying audio context usage michael@0: */ michael@0: _onStartContext: function () { michael@0: events.emit(this, "start-context"); michael@0: }, michael@0: michael@0: /** michael@0: * Called when one audio node is connected to another. michael@0: */ michael@0: _onConnectNode: function (source, dest) { michael@0: let sourceActor = this._actorFor(source); michael@0: let destActor = this._actorFor(dest); michael@0: events.emit(this, "connect-node", { michael@0: source: sourceActor, michael@0: dest: destActor michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called when an audio node is connected to an audio param. michael@0: * Implement in bug 986705 michael@0: */ michael@0: _onConnectParam: function (source, dest) { michael@0: // TODO bug 986705 michael@0: }, michael@0: michael@0: /** michael@0: * Called when an audio node is disconnected. michael@0: */ michael@0: _onDisconnectNode: function (node) { michael@0: let actor = this._actorFor(node); michael@0: events.emit(this, "disconnect-node", actor); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a parameter changes on an audio node michael@0: */ michael@0: _onParamChange: function (node, param, value) { michael@0: let actor = this._actorFor(node); michael@0: events.emit(this, "param-change", { michael@0: source: actor, michael@0: param: param, michael@0: value: value michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called on node creation. michael@0: */ michael@0: _onCreateNode: function (node) { michael@0: let actor = this._constructAudioNode(node); michael@0: events.emit(this, "create-node", actor); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the WebAudioActor. michael@0: */ michael@0: let WebAudioFront = exports.WebAudioFront = protocol.FrontClass(WebAudioActor, { michael@0: initialize: function(client, { webaudioActor }) { michael@0: protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor }); michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: } michael@0: }); michael@0: michael@0: WebAudioFront.NODE_CREATION_METHODS = new Set(NODE_CREATION_METHODS); michael@0: WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS); michael@0: michael@0: /** michael@0: * Determines whether or not property is an AudioParam. michael@0: * michael@0: * @param AudioNode node michael@0: * An AudioNode. michael@0: * @param String prop michael@0: * Property of `node` to evaluate to see if it's an AudioParam. michael@0: * @return Boolean michael@0: */ michael@0: function isAudioParam (node, prop) { michael@0: return /AudioParam/.test(node[prop].toString()); michael@0: } michael@0: michael@0: /** michael@0: * Takes an `Error` object and constructs a JSON-able response michael@0: * michael@0: * @param Error err michael@0: * A TypeError, RangeError, etc. michael@0: * @return Object michael@0: */ michael@0: function constructError (err) { michael@0: return { michael@0: message: err.message, michael@0: type: err.constructor.name michael@0: }; michael@0: } michael@0: michael@0: function unwrap (obj) { michael@0: return XPCNativeWrapper.unwrap(obj); michael@0: }