Bug 1265408 - Implement IIRFilterNode; r=padenot
authorDan Minor <dminor@mozilla.com>
Fri, 03 Jun 2016 13:42:03 -0400
changeset 300888 78eb3b1b93ce2f5b455a1467d630003a077fe78a
parent 300887 814c1a3fd2e14557f8be23e5986e583b9f22f98a
child 300889 e8ac527f4d47372fe793bd1495c1f584bbb644cc
push id78119
push userdminor@mozilla.com
push dateTue, 07 Jun 2016 16:03:45 +0000
treeherdermozilla-inbound@021ffc7c02e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspadenot
bugs1265408
milestone50.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 1265408 - Implement IIRFilterNode; r=padenot MozReview-Commit-ID: EZvTIwTDVPE
dom/locales/en-US/chrome/dom/dom.properties
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/IIRFilterNode.cpp
dom/media/webaudio/IIRFilterNode.h
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -224,8 +224,9 @@ TargetPrincipalDoesNotMatch=Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘%S’) does not match the recipient window’s origin (‘%S’).
 RewriteYoutubeEmbed=Rewriting old-style Youtube Flash embed (%S) to iframe embed (%S). Please update page to use iframe instead of embed/object, if possible.
 # LOCALIZATION NOTE: Do not translate 'youtube'. %S values are origins, like https://domain.com:port
 RewriteYoutubeEmbedInvalidQuery=Rewriting old-style Youtube Flash embed (%S) to iframe embed (%S). Query was invalid and removed from URL. Please update page to use iframe instead of embed/object, if possible.
 # LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is the ServiceWorker scope URL. %2$S is an error string.
 PushMessageDecryptionFailure=The ServiceWorker for scope ‘%1$S’ encountered an error decrypting a push message: ‘%2$S’. For help with encryption, please see https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API#Encryption
 # LOCALIZATION NOTE: %1$S is the type of a DOM event. 'passive' is a literal parameter from the DOM spec.
 PreventDefaultFromPassiveListenerWarning=Ignoring ‘preventDefault()’ call on event of type ‘%1$S’ from a listener registered as ‘passive’.
 FileLastModifiedDateWarning=File.lastModifiedDate is deprecated. Use File.lastModified instead.
+IIRFilterChannelCountChangeWarning=IIRFilterNode channel count changes may produce audio glitches.
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -514,18 +514,40 @@ already_AddRefed<IIRFilterNode>
 AudioContext::CreateIIRFilter(const mozilla::dom::binding_detail::AutoSequence<double>& aFeedforward,
                               const mozilla::dom::binding_detail::AutoSequence<double>& aFeedback,
                               mozilla::ErrorResult& aRv)
 {
   if (CheckClosed(aRv)) {
     return nullptr;
   }
 
+  if (aFeedforward.Length() == 0 || aFeedforward.Length() > 20) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
+  if (aFeedback.Length() == 0 || aFeedback.Length() > 20) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
+  bool feedforwardAllZeros = true;
+  for (size_t i = 0; i < aFeedforward.Length(); ++i) {
+    if (aFeedforward.Elements()[i] != 0.0) {
+      feedforwardAllZeros = false;
+    }
+  }
+
+  if (feedforwardAllZeros || aFeedback.Elements()[0] == 0.0) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return nullptr;
+  }
+
   RefPtr<IIRFilterNode> filterNode =
-    new IIRFilterNode(this);
+    new IIRFilterNode(this, aFeedforward, aFeedback);
   return filterNode.forget();
 }
 
 already_AddRefed<OscillatorNode>
 AudioContext::CreateOscillator(ErrorResult& aRv)
 {
   if (CheckClosed(aRv)) {
     return nullptr;
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/IIRFilterNode.cpp
@@ -0,0 +1,225 @@
+/* -*- 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 "IIRFilterNode.h"
+#include "AudioNodeEngine.h"
+
+#include "blink/IIRFilter.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS_INHERITED0(IIRFilterNode, AudioNode)
+
+class IIRFilterNodeEngine final : public AudioNodeEngine
+{
+public:
+  IIRFilterNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination,
+                      const AudioDoubleArray &aFeedforward,
+                      const AudioDoubleArray &aFeedback,
+                      uint64_t aWindowID)
+    : AudioNodeEngine(aNode)
+    , mDestination(aDestination->Stream())
+    , mFeedforward(aFeedforward)
+    , mFeedback(aFeedback)
+    , mWindowID(aWindowID)
+  {
+  }
+
+  void ProcessBlock(AudioNodeStream* aStream,
+                    GraphTime aFrom,
+                    const AudioBlock& aInput,
+                    AudioBlock* aOutput,
+                    bool* aFinished) override
+  {
+    float inputBuffer[WEBAUDIO_BLOCK_SIZE + 4];
+    float* alignedInputBuffer = ALIGNED16(inputBuffer);
+    ASSERT_ALIGNED16(alignedInputBuffer);
+
+    if (aInput.IsNull()) {
+      if (!mIIRFilters.IsEmpty()) {
+        bool allZero = true;
+        for (uint32_t i = 0; i < mIIRFilters.Length(); ++i) {
+          allZero &= mIIRFilters[i]->buffersAreZero();
+        }
+
+        // all filter buffer values are zero, so the output will be zero
+        // as well.
+        if (allZero) {
+          mIIRFilters.Clear();
+          aStream->ScheduleCheckForInactive();
+
+          RefPtr<PlayingRefChangeHandler> refchanged =
+            new PlayingRefChangeHandler(aStream, PlayingRefChangeHandler::RELEASE);
+          aStream->Graph()->
+            DispatchToMainThreadAfterStreamStateUpdate(refchanged.forget());
+
+          aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+          return;
+        }
+
+        PodZero(alignedInputBuffer, WEBAUDIO_BLOCK_SIZE);
+      }
+    } else if(mIIRFilters.Length() != aInput.ChannelCount()){
+      if (mIIRFilters.IsEmpty()) {
+        RefPtr<PlayingRefChangeHandler> refchanged =
+          new PlayingRefChangeHandler(aStream, PlayingRefChangeHandler::ADDREF);
+        aStream->Graph()->
+          DispatchToMainThreadAfterStreamStateUpdate(refchanged.forget());
+      } else {
+        WebAudioUtils::LogToDeveloperConsole(mWindowID,
+                                             "IIRFilterChannelCountChangeWarning");
+      }
+
+      // Adjust the number of filters based on the number of channels
+      mIIRFilters.SetLength(aInput.ChannelCount());
+      for (size_t i = 0; i < aInput.ChannelCount(); ++i) {
+        mIIRFilters[i] = new blink::IIRFilter(&mFeedforward, &mFeedback);
+      }
+    }
+
+    uint32_t numberOfChannels = mIIRFilters.Length();
+    aOutput->AllocateChannels(numberOfChannels);
+
+    for (uint32_t i = 0; i < numberOfChannels; ++i) {
+      const float* input;
+      if (aInput.IsNull()) {
+        input = alignedInputBuffer;
+      } else {
+        input = static_cast<const float*>(aInput.mChannelData[i]);
+        if (aInput.mVolume != 1.0) {
+          AudioBlockCopyChannelWithScale(input, aInput.mVolume, alignedInputBuffer);
+          input = alignedInputBuffer;
+        }
+      }
+
+      mIIRFilters[i]->process(input,
+                              aOutput->ChannelFloatsForWrite(i),
+                              aInput.GetDuration());
+    }
+  }
+
+  bool IsActive() const override
+  {
+    return !mIIRFilters.IsEmpty();
+  }
+
+  size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
+  {
+    // Not owned:
+    // - mDestination - probably not owned
+    // - AudioParamTimelines - counted in the AudioNode
+    size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+    amount += mIIRFilters.ShallowSizeOfExcludingThis(aMallocSizeOf);
+    return amount;
+  }
+
+  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override
+  {
+    return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+  }
+
+private:
+  AudioNodeStream* mDestination;
+  nsTArray<nsAutoPtr<blink::IIRFilter>> mIIRFilters;
+  AudioDoubleArray mFeedforward;
+  AudioDoubleArray mFeedback;
+  uint64_t mWindowID;
+};
+
+IIRFilterNode::IIRFilterNode(AudioContext* aContext,
+                             const mozilla::dom::binding_detail::AutoSequence<double>& aFeedforward,
+                             const mozilla::dom::binding_detail::AutoSequence<double>& aFeedback)
+  : AudioNode(aContext,
+              2,
+              ChannelCountMode::Max,
+              ChannelInterpretation::Speakers)
+{
+  mFeedforward.SetLength(aFeedforward.Length());
+  PodCopy(mFeedforward.Elements(), aFeedforward.Elements(), aFeedforward.Length());
+  mFeedback.SetLength(aFeedback.Length());
+  PodCopy(mFeedback.Elements(), aFeedback.Elements(), aFeedback.Length());
+
+  // Scale coefficients -- we guarantee that mFeedback != 0 when creating
+  // the IIRFilterNode.
+  double scale = mFeedback[0];
+  double* elements = mFeedforward.Elements();
+  for (size_t i = 0; i < mFeedforward.Length(); ++i) {
+    elements[i] /= scale;
+  }
+
+  elements = mFeedback.Elements();
+  for (size_t i = 0; i < mFeedback.Length(); ++i) {
+    elements[i] /= scale;
+  }
+
+  // We check that this is exactly equal to one later in blink/IIRFilter.cpp
+  elements[0] = 1.0;
+
+  uint64_t windowID = aContext->GetParentObject()->WindowID();
+  IIRFilterNodeEngine* engine = new IIRFilterNodeEngine(this, aContext->Destination(), mFeedforward, mFeedback, windowID);
+  mStream = AudioNodeStream::Create(aContext, engine,
+                                    AudioNodeStream::NO_STREAM_FLAGS);
+}
+
+IIRFilterNode::~IIRFilterNode()
+{
+}
+
+size_t
+IIRFilterNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+{
+  size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+  return amount;
+}
+
+size_t
+IIRFilterNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
+{
+  return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject*
+IIRFilterNode::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return IIRFilterNodeBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+IIRFilterNode::GetFrequencyResponse(const Float32Array& aFrequencyHz,
+                                    const Float32Array& aMagResponse,
+                                    const Float32Array& aPhaseResponse)
+{
+  aFrequencyHz.ComputeLengthAndData();
+  aMagResponse.ComputeLengthAndData();
+  aPhaseResponse.ComputeLengthAndData();
+
+  uint32_t length = std::min(std::min(aFrequencyHz.Length(),
+                                      aMagResponse.Length()),
+                             aPhaseResponse.Length());
+  if (!length) {
+    return;
+  }
+
+  auto frequencies = MakeUnique<float[]>(length);
+  float* frequencyHz = aFrequencyHz.Data();
+  const double nyquist = Context()->SampleRate() * 0.5;
+
+  // Normalize the frequencies
+  for (uint32_t i = 0; i < length; ++i) {
+    if (frequencyHz[i] >= 0 && frequencyHz[i] <= nyquist) {
+        frequencies[i] = static_cast<float>(frequencyHz[i] / nyquist);
+    } else {
+        frequencies[i] = std::numeric_limits<float>::quiet_NaN();
+    }
+  }
+
+  blink::IIRFilter filter(&mFeedforward, &mFeedback);
+  filter.getFrequencyResponse(int(length), frequencies.get(), aMagResponse.Data(), aPhaseResponse.Data());
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/IIRFilterNode.h
@@ -0,0 +1,55 @@
+/* -*- 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 IIRFilterNode_h_
+#define IIRFilterNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+#include "mozilla/dom/IIRFilterNodeBinding.h"
+
+namespace mozilla {
+namespace dom {
+
+class AudioContext;
+
+class IIRFilterNode final : public AudioNode
+{
+public:
+  explicit IIRFilterNode(AudioContext* aContext,
+                         const mozilla::dom::binding_detail::AutoSequence<double>& aFeedforward,
+                         const mozilla::dom::binding_detail::AutoSequence<double>& aFeedback);
+
+  NS_DECL_ISUPPORTS_INHERITED
+
+  JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+
+  void GetFrequencyResponse(const Float32Array& aFrequencyHz,
+                            const Float32Array& aMagResponse,
+                            const Float32Array& aPhaseResponse);
+
+  const char* NodeType() const override
+  {
+    return "IIRFilterNode";
+  }
+
+  size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+protected:
+  virtual ~IIRFilterNode();
+
+private:
+    nsTArray<double> mFeedback;
+    nsTArray<double> mFeedforward;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
+