Bug 1100349 - Implement StereoPannerNode. r=ehsan,smaug
authorPaul Adenot <paul@paul.cx>
Wed, 19 Nov 2014 18:15:13 +0100
changeset 244236 d112575f39f997e1e25602d89cd93f5bb18c347e
parent 244235 c8c84020620648802db0faf54619fb0d8547d19a
child 244237 61277c6b999e51301b650e9caf50b363d7ce4e36
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, smaug
bugs1100349
milestone37.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 1100349 - Implement StereoPannerNode. r=ehsan,smaug
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/AudioContext.h
dom/media/webaudio/AudioNodeEngine.cpp
dom/media/webaudio/AudioNodeEngine.h
dom/media/webaudio/PannerNode.cpp
dom/media/webaudio/PanningUtils.h
dom/media/webaudio/StereoPannerNode.cpp
dom/media/webaudio/StereoPannerNode.h
dom/media/webaudio/moz.build
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/AudioContext.webidl
dom/webidl/StereoPannerNode.webidl
dom/webidl/moz.build
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -22,16 +22,17 @@
 #include "MediaElementAudioSourceNode.h"
 #include "MediaStreamAudioSourceNode.h"
 #include "DelayNode.h"
 #include "PannerNode.h"
 #include "AudioListener.h"
 #include "DynamicsCompressorNode.h"
 #include "BiquadFilterNode.h"
 #include "ScriptProcessorNode.h"
+#include "StereoPannerNode.h"
 #include "ChannelMergerNode.h"
 #include "ChannelSplitterNode.h"
 #include "MediaStreamAudioDestinationNode.h"
 #include "WaveShaperNode.h"
 #include "PeriodicWave.h"
 #include "ConvolverNode.h"
 #include "OscillatorNode.h"
 #include "nsNetUtil.h"
@@ -273,16 +274,23 @@ AudioContext::CreateScriptProcessor(uint
 
 already_AddRefed<AnalyserNode>
 AudioContext::CreateAnalyser()
 {
   nsRefPtr<AnalyserNode> analyserNode = new AnalyserNode(this);
   return analyserNode.forget();
 }
 
+already_AddRefed<StereoPannerNode>
+AudioContext::CreateStereoPanner()
+{
+  nsRefPtr<StereoPannerNode> stereoPannerNode = new StereoPannerNode(this);
+  return stereoPannerNode.forget();
+}
+
 already_AddRefed<MediaElementAudioSourceNode>
 AudioContext::CreateMediaElementSource(HTMLMediaElement& aMediaElement,
                                        ErrorResult& aRv)
 {
   if (mIsOffline) {
     aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return nullptr;
   }
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -54,16 +54,17 @@ class GainNode;
 class HTMLMediaElement;
 class MediaElementAudioSourceNode;
 class GlobalObject;
 class MediaStreamAudioDestinationNode;
 class MediaStreamAudioSourceNode;
 class OscillatorNode;
 class PannerNode;
 class ScriptProcessorNode;
+class StereoPannerNode;
 class WaveShaperNode;
 class PeriodicWave;
 class Promise;
 
 class AudioContext MOZ_FINAL : public DOMEventTargetHelper,
                                public nsIMemoryReporter
 {
   AudioContext(nsPIDOMWindow* aParentWindow,
@@ -138,16 +139,19 @@ public:
   CreateMediaStreamDestination(ErrorResult& aRv);
 
   already_AddRefed<ScriptProcessorNode>
   CreateScriptProcessor(uint32_t aBufferSize,
                         uint32_t aNumberOfInputChannels,
                         uint32_t aNumberOfOutputChannels,
                         ErrorResult& aRv);
 
+  already_AddRefed<StereoPannerNode>
+  CreateStereoPanner();
+
   already_AddRefed<AnalyserNode>
   CreateAnalyser();
 
   already_AddRefed<GainNode>
   CreateGain();
 
   already_AddRefed<WaveShaperNode>
   CreateWaveShaper();
--- a/dom/media/webaudio/AudioNodeEngine.cpp
+++ b/dom/media/webaudio/AudioNodeEngine.cpp
@@ -182,16 +182,27 @@ AudioBufferInPlaceScale(float* aBlock,
 #endif
   for (uint32_t i = 0; i < aSize; ++i) {
     *aBlock++ *= aScale;
   }
 }
 
 void
 AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
+                          float aGainL[WEBAUDIO_BLOCK_SIZE],
+                          float aGainR[WEBAUDIO_BLOCK_SIZE],
+                          float aOutputL[WEBAUDIO_BLOCK_SIZE],
+                          float aOutputR[WEBAUDIO_BLOCK_SIZE])
+{
+  AudioBlockCopyChannelWithScale(aInput, aGainL, aOutputL);
+  AudioBlockCopyChannelWithScale(aInput, aGainR, aOutputR);
+}
+
+void
+AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
                           float aGainL, float aGainR,
                           float aOutputL[WEBAUDIO_BLOCK_SIZE],
                           float aOutputR[WEBAUDIO_BLOCK_SIZE])
 {
   AudioBlockCopyChannelWithScale(aInput, aGainL, aOutputL);
   AudioBlockCopyChannelWithScale(aInput, aGainR, aOutputR);
 }
 
@@ -210,23 +221,48 @@ AudioBlockPanStereoToStereo(const float 
     return;
   }
 #endif
 
   uint32_t i;
 
   if (aIsOnTheLeft) {
     for (i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
-      *aOutputL++ = *aInputL++ + *aInputR * aGainL;
-      *aOutputR++ = *aInputR++ * aGainR;
+      aOutputL[i] = aInputL[i] + aInputR[i] * aGainL;
+      aOutputR[i] = aInputR[i] * aGainR;
     }
   } else {
     for (i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
-      *aOutputL++ = *aInputL * aGainL;
-      *aOutputR++ = *aInputR++ + *aInputL++ * aGainR;
+      aOutputL[i] = aInputL[i] * aGainL;
+      aOutputR[i] = aInputR[i] + aInputL[i] * aGainR;
+    }
+  }
+}
+
+void
+AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
+                            const float aInputR[WEBAUDIO_BLOCK_SIZE],
+                            float aGainL[WEBAUDIO_BLOCK_SIZE],
+                            float aGainR[WEBAUDIO_BLOCK_SIZE],
+                            bool  aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE],
+                            float aOutputL[WEBAUDIO_BLOCK_SIZE],
+                            float aOutputR[WEBAUDIO_BLOCK_SIZE])
+{
+#ifdef BUILD_ARM_NEON
+  // No NEON version yet: bug 1105513
+#endif
+
+  uint32_t i;
+  for (i = 0; i < WEBAUDIO_BLOCK_SIZE; i++) {
+    if (aIsOnTheLeft[i]) {
+      aOutputL[i] = aInputL[i] + aInputR[i] * aGainL[i];
+      aOutputR[i] = aInputR[i] * aGainR[i];
+    } else {
+      aOutputL[i] = aInputL[i] * aGainL[i];
+      aOutputR[i] = aInputR[i] + aInputL[i] * aGainR[i];
     }
   }
 }
 
 float
 AudioBufferSumOfSquares(const float* aInput, uint32_t aLength)
 {
   float sum = 0.0f;
--- a/dom/media/webaudio/AudioNodeEngine.h
+++ b/dom/media/webaudio/AudioNodeEngine.h
@@ -192,27 +192,42 @@ void AudioBufferInPlaceScale(float* aBlo
  * different gain value.
  * This algorithm is specified in the WebAudio spec.
  */
 void
 AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
                           float aGainL, float aGainR,
                           float aOutputL[WEBAUDIO_BLOCK_SIZE],
                           float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+
+void
+AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
+                          float aGainL[WEBAUDIO_BLOCK_SIZE],
+                          float aGainR[WEBAUDIO_BLOCK_SIZE],
+                          float aOutputL[WEBAUDIO_BLOCK_SIZE],
+                          float aOutputR[WEBAUDIO_BLOCK_SIZE]);
 /**
  * Pan a stereo source according to right and left gain, and the position
  * (whether the listener is on the left of the source or not).
  * This algorithm is specified in the WebAudio spec.
  */
 void
 AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
                             const float aInputR[WEBAUDIO_BLOCK_SIZE],
                             float aGainL, float aGainR, bool aIsOnTheLeft,
                             float aOutputL[WEBAUDIO_BLOCK_SIZE],
                             float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+void
+AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
+                            const float aInputR[WEBAUDIO_BLOCK_SIZE],
+                            float aGainL[WEBAUDIO_BLOCK_SIZE],
+                            float aGainR[WEBAUDIO_BLOCK_SIZE],
+                            bool  aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE],
+                            float aOutputL[WEBAUDIO_BLOCK_SIZE],
+                            float aOutputR[WEBAUDIO_BLOCK_SIZE]);
 
 /**
  * Return the sum of squares of all of the samples in the input.
  */
 float
 AudioBufferSumOfSquares(const float* aInput, uint32_t aLength);
 
 /**
--- a/dom/media/webaudio/PannerNode.cpp
+++ b/dom/media/webaudio/PannerNode.cpp
@@ -3,16 +3,17 @@
 /* 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/. */
 
 #include "PannerNode.h"
 #include "AudioNodeEngine.h"
 #include "AudioNodeStream.h"
 #include "AudioListener.h"
+#include "PanningUtils.h"
 #include "AudioBufferSourceNode.h"
 #include "PlayingRefChangeHandler.h"
 #include "blink/HRTFPanner.h"
 #include "blink/HRTFDatabaseLoader.h"
 
 using WebCore::HRTFDatabaseLoader;
 using WebCore::HRTFPanner;
 
@@ -171,21 +172,16 @@ public:
     (this->*mPanningModelFunction)(aInput, aOutput);
   }
 
   void ComputeAzimuthAndElevation(float& aAzimuth, float& aElevation);
   float ComputeConeGain();
   // Compute how much the distance contributes to the gain reduction.
   float ComputeDistanceGain();
 
-  void GainMonoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
-                        float aGainL, float aGainR);
-  void GainStereoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
-                          float aGainL, float aGainR, double aAzimuth);
-
   void EqualPowerPanningFunction(const AudioChunk& aInput, AudioChunk* aOutput);
   void HRTFPanningFunction(const AudioChunk& aInput, AudioChunk* aOutput);
 
   float LinearGainFunction(float aDistance);
   float InverseGainFunction(float aDistance);
   float ExponentialGainFunction(float aDistance);
 
   virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const MOZ_OVERRIDE
@@ -367,48 +363,21 @@ PannerNodeEngine::EqualPowerPanningFunct
 
   distanceGain = ComputeDistanceGain();
 
   // Actually compute the left and right gain.
   gainL = cos(0.5 * M_PI * normalizedAzimuth);
   gainR = sin(0.5 * M_PI * normalizedAzimuth);
 
   // Compute the output.
-  if (inputChannels == 1) {
-    GainMonoToStereo(aInput, aOutput, gainL, gainR);
-  } else {
-    GainStereoToStereo(aInput, aOutput, gainL, gainR, azimuth);
-  }
+  ApplyStereoPanning(aInput, aOutput, gainL, gainR, azimuth <= 0);
 
   aOutput->mVolume = aInput.mVolume * distanceGain * coneGain;
 }
 
-void
-PannerNodeEngine::GainMonoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
-                                   float aGainL, float aGainR)
-{
-  float* outputL = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[0]));
-  float* outputR = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[1]));
-  const float* input = static_cast<float*>(const_cast<void*>(aInput.mChannelData[0]));
-
-  AudioBlockPanMonoToStereo(input, aGainL, aGainR, outputL, outputR);
-}
-
-void
-PannerNodeEngine::GainStereoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
-                                     float aGainL, float aGainR, double aAzimuth)
-{
-  float* outputL = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[0]));
-  float* outputR = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[1]));
-  const float* inputL = static_cast<float*>(const_cast<void*>(aInput.mChannelData[0]));
-  const float* inputR = static_cast<float*>(const_cast<void*>(aInput.mChannelData[1]));
-
-  AudioBlockPanStereoToStereo(inputL, inputR, aGainL, aGainR, aAzimuth <= 0, outputL, outputR);
-}
-
 // This algorithm is specified in the webaudio spec.
 void
 PannerNodeEngine::ComputeAzimuthAndElevation(float& aAzimuth, float& aElevation)
 {
   ThreeDPoint sourceListener = mPosition - mListenerPosition;
 
   if (sourceListener.IsZero()) {
     aAzimuth = 0.0;
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/PanningUtils.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef PANNING_UTILS_H
+#define PANNING_UTILS_H
+
+#include "AudioSegment.h"
+#include "AudioNodeEngine.h"
+
+namespace mozilla {
+namespace dom {
+
+template<typename T>
+void
+GainMonoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
+                 T aGainL, T aGainR)
+{
+  float* outputL = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[0]));
+  float* outputR = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[1]));
+  const float* input = static_cast<float*>(const_cast<void*>(aInput.mChannelData[0]));
+
+  MOZ_ASSERT(aInput.ChannelCount() == 1);
+  MOZ_ASSERT(aOutput->ChannelCount() == 2);
+
+  AudioBlockPanMonoToStereo(input, aGainL, aGainR, outputL, outputR);
+}
+
+// T can be float or an array of float, and  U can be bool or an array of bool,
+// depending if the value of the parameters are constant for this block.
+template<typename T, typename U>
+void
+GainStereoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
+                   T aGainL, T aGainR, U aOnLeft)
+{
+  float* outputL = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[0]));
+  float* outputR = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[1]));
+  const float* inputL = static_cast<float*>(const_cast<void*>(aInput.mChannelData[0]));
+  const float* inputR = static_cast<float*>(const_cast<void*>(aInput.mChannelData[1]));
+
+  MOZ_ASSERT(aInput.ChannelCount() == 2);
+  MOZ_ASSERT(aOutput->ChannelCount() == 2);
+
+  AudioBlockPanStereoToStereo(inputL, inputR, aGainL, aGainR, aOnLeft, outputL, outputR);
+}
+
+// T can be float or an array of float, and  U can be bool or an array of bool,
+// depending if the value of the parameters are constant for this block.
+template<typename T, typename U>
+void ApplyStereoPanning(const AudioChunk& aInput, AudioChunk* aOutput,
+                        T aGainL, T aGainR, U aOnLeft)
+{
+  if (aInput.mChannelData.Length() == 1) {
+    GainMonoToStereo(aInput, aOutput, aGainL, aGainR);
+  } else {
+    GainStereoToStereo(aInput, aOutput, aGainL, aGainR, aOnLeft);
+  }
+}
+
+} // dom
+} // mozilla
+
+#endif // PANNING_UTILS_H
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/StereoPannerNode.cpp
@@ -0,0 +1,221 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "StereoPannerNode.h"
+#include "mozilla/dom/StereoPannerNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeStream.h"
+#include "AudioDestinationNode.h"
+#include "WebAudioUtils.h"
+#include "PanningUtils.h"
+#include "AudioParamTimeline.h"
+#include "AudioParam.h"
+
+namespace mozilla {
+namespace dom {
+
+using namespace std;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(StereoPannerNode, AudioNode, mPan)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(StereoPannerNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(StereoPannerNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(StereoPannerNode, AudioNode)
+
+class StereoPannerNodeEngine : public AudioNodeEngine
+{
+public:
+  StereoPannerNodeEngine(AudioNode* aNode,
+                         AudioDestinationNode* aDestination)
+    : AudioNodeEngine(aNode)
+    , mSource(nullptr)
+    , mDestination(static_cast<AudioNodeStream*>(aDestination->Stream()))
+    // Keep the default value in sync with the default value in
+    // StereoPannerNode::StereoPannerNode.
+    , mPan(0.f)
+  {
+  }
+
+  void SetSourceStream(AudioNodeStream* aSource)
+  {
+    mSource = aSource;
+  }
+
+  enum Parameters {
+    PAN
+  };
+  void SetTimelineParameter(uint32_t aIndex,
+                            const AudioParamTimeline& aValue,
+                            TrackRate aSampleRate) MOZ_OVERRIDE
+  {
+    switch (aIndex) {
+    case PAN:
+      MOZ_ASSERT(mSource && mDestination);
+      mPan = aValue;
+      WebAudioUtils::ConvertAudioParamToTicks(mPan, mSource, mDestination);
+      break;
+    default:
+      NS_ERROR("Bad StereoPannerNode TimelineParameter");
+    }
+  }
+
+  void GetGainValuesForPanning(float aPanning,
+                               bool aMonoToStereo,
+                               float& aLeftGain,
+                               float& aRightGain)
+  {
+    // Clamp and normalize the panning in [0; 1]
+    aPanning = std::min(std::max(aPanning, -1.f), 1.f);
+
+    if (aMonoToStereo) {
+      aPanning += 1;
+      aPanning /= 2;
+    } else if (aPanning <= 0) {
+      aPanning += 1;
+    }
+
+    aLeftGain  = cos(0.5 * M_PI * aPanning);
+    aRightGain = sin(0.5 * M_PI * aPanning);
+  }
+
+  void SetToSilentStereoBlock(AudioChunk* aChunk)
+  {
+    for (uint32_t channel = 0; channel < 2; channel++) {
+      float* samples = static_cast<float*>(const_cast<void*>(aChunk->mChannelData[channel]));
+      for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; i++) {
+        samples[i] = 0.f;
+      }
+    }
+  }
+
+  void UpmixToStereoIfNeeded(const AudioChunk& aInput, AudioChunk* aOutput)
+  {
+    if (aInput.ChannelCount() == 2) {
+      *aOutput = aInput;
+    } else {
+      MOZ_ASSERT(aInput.ChannelCount() == 1);
+      AllocateAudioBlock(2, aOutput);
+      const float* input = static_cast<const float*>(aInput.mChannelData[0]);
+      for (uint32_t channel = 0; channel < 2; channel++) {
+        float* output = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[channel]));
+        PodCopy(output, input, WEBAUDIO_BLOCK_SIZE);
+      }
+    }
+  }
+
+  virtual void ProcessBlock(AudioNodeStream* aStream,
+                            const AudioChunk& aInput,
+                            AudioChunk* aOutput,
+                            bool *aFinished) MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(mSource == aStream, "Invalid source stream");
+
+    // The output of this node is always stereo, no matter what the inputs are.
+    MOZ_ASSERT(aInput.ChannelCount() <= 2);
+    AllocateAudioBlock(2, aOutput);
+    bool monoToStereo = aInput.ChannelCount() == 1;
+
+    if (aInput.IsNull()) {
+      // If input is silent, so is the output
+      SetToSilentStereoBlock(aOutput);
+    } else if (mPan.HasSimpleValue()) {
+      float panning = mPan.GetValue();
+      // If the panning is 0.0, we can simply copy the input to the
+      // output, up-mixing to stereo if needed.
+      if (panning == 0.0f) {
+        UpmixToStereoIfNeeded(aInput, aOutput);
+      } else {
+        // Optimize the case where the panning is constant for this processing
+        // block: we can just apply a constant gain on the left and right
+        // channel
+        float gainL, gainR;
+
+        GetGainValuesForPanning(panning, monoToStereo, gainL, gainR);
+        ApplyStereoPanning(aInput, aOutput,
+                           gainL * aInput.mVolume,
+                           gainR * aInput.mVolume,
+                           panning <= 0);
+      }
+    } else {
+      float computedGain[2][WEBAUDIO_BLOCK_SIZE];
+      bool onLeft[WEBAUDIO_BLOCK_SIZE];
+
+      for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
+        StreamTime tick = aStream->GetCurrentPosition();
+        float left, right;
+        float panning = mPan.GetValueAtTime(tick, counter);
+        GetGainValuesForPanning(panning, monoToStereo, left, right);
+
+        computedGain[0][counter] = left * aInput.mVolume;
+        computedGain[1][counter] = right * aInput.mVolume;
+        onLeft[counter] = panning <= 0;
+      }
+
+      // Apply the gain to the output buffer
+      ApplyStereoPanning(aInput, aOutput, computedGain[0], computedGain[1], onLeft);
+    }
+  }
+
+  virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const MOZ_OVERRIDE
+  {
+    return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+  }
+
+  AudioNodeStream* mSource;
+  AudioNodeStream* mDestination;
+  AudioParamTimeline mPan;
+};
+
+StereoPannerNode::StereoPannerNode(AudioContext* aContext)
+  : AudioNode(aContext,
+              2,
+              ChannelCountMode::Clamped_max,
+              ChannelInterpretation::Speakers)
+  , mPan(new AudioParam(MOZ_THIS_IN_INITIALIZER_LIST(),
+                        SendPanToStream, 0.f))
+{
+  StereoPannerNodeEngine* engine = new StereoPannerNodeEngine(this, aContext->Destination());
+  mStream = aContext->Graph()->CreateAudioNodeStream(engine,
+                                                     MediaStreamGraph::INTERNAL_STREAM);
+  engine->SetSourceStream(static_cast<AudioNodeStream*>(mStream.get()));
+}
+
+StereoPannerNode::~StereoPannerNode()
+{
+}
+
+size_t
+StereoPannerNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+{
+  size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+  amount += mPan->SizeOfIncludingThis(aMallocSizeOf);
+  return amount;
+}
+
+size_t
+StereoPannerNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
+{
+  return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject*
+StereoPannerNode::WrapObject(JSContext* aCx)
+{
+  return StereoPannerNodeBinding::Wrap(aCx, this);
+}
+
+void
+StereoPannerNode::SendPanToStream(AudioNode* aNode)
+{
+  StereoPannerNode* This = static_cast<StereoPannerNode*>(aNode);
+  SendTimelineParameterToStream(This, StereoPannerNodeEngine::PAN, *This->mPan);
+}
+
+}
+}
+
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/StereoPannerNode.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef StereoPannerNode_h_
+#define StereoPannerNode_h_
+
+#include "AudioNode.h"
+#include "mozilla/dom/StereoPannerNodeBinding.h"
+
+namespace mozilla {
+namespace dom {
+
+class AudioContext;
+
+class StereoPannerNode : public AudioNode
+{
+public:
+  MOZ_DECLARE_REFCOUNTED_TYPENAME(StereoPannerNode)
+  explicit StereoPannerNode(AudioContext* aContext);
+
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+  virtual void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) MOZ_OVERRIDE
+  {
+    if (aChannelCount > 2) {
+      aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+      return;
+    }
+    AudioNode::SetChannelCount(aChannelCount, aRv);
+  }
+  virtual void SetChannelCountModeValue(ChannelCountMode aMode, ErrorResult& aRv) MOZ_OVERRIDE
+  {
+    if (aMode == ChannelCountMode::Max) {
+      aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+      return;
+    }
+    AudioNode::SetChannelCountModeValue(aMode, aRv);
+  }
+
+  AudioParam* Pan() const
+  {
+    return mPan;
+  }
+
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(StereoPannerNode, AudioNode)
+
+  virtual const char* NodeType() const
+  {
+    return "StereoPannerNode";
+  }
+
+  virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const MOZ_OVERRIDE;
+  virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const MOZ_OVERRIDE;
+
+protected:
+  virtual ~StereoPannerNode();
+
+private:
+  static void SendPanToStream(AudioNode* aNode);
+  nsRefPtr<AudioParam> mPan;
+};
+
+}
+}
+
+#endif
+
--- a/dom/media/webaudio/moz.build
+++ b/dom/media/webaudio/moz.build
@@ -53,16 +53,17 @@ EXPORTS.mozilla.dom += [
     'MediaElementAudioSourceNode.h',
     'MediaStreamAudioDestinationNode.h',
     'MediaStreamAudioSourceNode.h',
     'OfflineAudioCompletionEvent.h',
     'OscillatorNode.h',
     'PannerNode.h',
     'PeriodicWave.h',
     'ScriptProcessorNode.h',
+    'StereoPannerNode.h',
     'WaveShaperNode.h',
 ]
 
 UNIFIED_SOURCES += [
     'AnalyserNode.cpp',
     'AudioBuffer.cpp',
     'AudioBufferSourceNode.cpp',
     'AudioContext.cpp',
@@ -88,16 +89,17 @@ UNIFIED_SOURCES += [
     'MediaElementAudioSourceNode.cpp',
     'MediaStreamAudioDestinationNode.cpp',
     'MediaStreamAudioSourceNode.cpp',
     'OfflineAudioCompletionEvent.cpp',
     'OscillatorNode.cpp',
     'PannerNode.cpp',
     'PeriodicWave.cpp',
     'ScriptProcessorNode.cpp',
+    'StereoPannerNode.cpp',
     'ThreeDPoint.cpp',
     'WaveShaperNode.cpp',
     'WebAudioUtils.cpp',
 ]
 
 if CONFIG['CPU_ARCH'] == 'arm' and CONFIG['BUILD_ARM_NEON']:
     SOURCES += ['AudioNodeEngineNEON.cpp']
     SOURCES['AudioNodeEngineNEON.cpp'].flags += ['-mfpu=neon']
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -951,16 +951,18 @@ var interfaceNamesInGlobalScope =
     {name: "SpeechSynthesis", b2g: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "SpeechSynthesisUtterance", b2g: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "SpeechSynthesisVoice", b2g: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "SpecialPowers", xbl: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "StereoPannerNode",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "Storage",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "StorageEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "StyleSheet",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "StyleSheetList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/AudioContext.webidl
+++ b/dom/webidl/AudioContext.webidl
@@ -37,16 +37,18 @@ interface AudioContext : EventTarget {
     MediaStreamAudioDestinationNode createMediaStreamDestination();
 
     [NewObject, Throws]
     ScriptProcessorNode createScriptProcessor(optional unsigned long bufferSize = 0,
                                               optional unsigned long numberOfInputChannels = 2,
                                               optional unsigned long numberOfOutputChannels = 2);
 
     [NewObject]
+    StereoPannerNode createStereoPanner();
+    [NewObject]
     AnalyserNode createAnalyser();
     [NewObject, Throws]
     MediaElementAudioSourceNode createMediaElementSource(HTMLMediaElement mediaElement);
     [NewObject, Throws]
     MediaStreamAudioSourceNode createMediaStreamSource(MediaStream mediaStream);
     [NewObject]
     GainNode createGain();
     [NewObject, Throws]
new file mode 100644
--- /dev/null
+++ b/dom/webidl/StereoPannerNode.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ *
+ * The origin of this IDL file is
+ * https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
+ *
+ * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
+ * liability, trademark and document use rules apply.
+ */
+
+interface StereoPannerNode : AudioNode {
+  readonly attribute AudioParam pan;
+};
+
+// Mozilla extension
+StereoPannerNode implements AudioNodePassThrough;
+
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -369,16 +369,17 @@ WEBIDL_FILES = [
     'SettingsManager.webidl',
     'ShadowRoot.webidl',
     'SharedWorker.webidl',
     'SharedWorkerGlobalScope.webidl',
     'SimpleGestureEvent.webidl',
     'SocketCommon.webidl',
     'SourceBuffer.webidl',
     'SourceBufferList.webidl',
+    'StereoPannerNode.webidl',
     'Storage.webidl',
     'StorageEvent.webidl',
     'StorageType.webidl',
     'StyleSheet.webidl',
     'StyleSheetList.webidl',
     'SubtleCrypto.webidl',
     'SVGAElement.webidl',
     'SVGAltGlyphElement.webidl',