Bug 1308432 - Implement ConstantSourceNode; r=padenot
authorDan Minor <dminor@mozilla.com>
Thu, 13 Oct 2016 15:17:04 -0400
changeset 427625 1dba8e563e1cf53e3d17a260365da0aea6c45c8d
parent 427624 61c7dc758b5b90d1eeafaae5bdb1bf0838727ced
child 427626 a39915ca6ab440c37e382686dd4c8c1980008f5a
push id33070
push userbmo:gps@mozilla.com
push dateThu, 20 Oct 2016 17:11:05 +0000
reviewerspadenot
bugs1308432
milestone52.0a1
Bug 1308432 - Implement ConstantSourceNode; r=padenot MozReview-Commit-ID: DZ6Zq1rzRwP
dom/media/webaudio/ConstantSourceNode.cpp
dom/media/webaudio/ConstantSourceNode.h
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html
--- a/dom/media/webaudio/ConstantSourceNode.cpp
+++ b/dom/media/webaudio/ConstantSourceNode.cpp
@@ -16,22 +16,151 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(Const
                                    mOffset)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ConstantSourceNode)
 NS_INTERFACE_MAP_END_INHERITING(AudioNode)
 
 NS_IMPL_ADDREF_INHERITED(ConstantSourceNode, AudioNode)
 NS_IMPL_RELEASE_INHERITED(ConstantSourceNode, AudioNode)
 
+class ConstantSourceNodeEngine final : public AudioNodeEngine
+{
+public:
+  ConstantSourceNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination)
+    : AudioNodeEngine(aNode)
+    , mSource(nullptr)
+    , mDestination(aDestination->Stream())
+    , mStart(-1)
+    , mStop(STREAM_TIME_MAX)
+    // Keep the default values in sync with ConstantSourceNode::ConstantSourceNode.
+    , mOffset(1.0f)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+  void SetSourceStream(AudioNodeStream* aSource)
+  {
+    mSource = aSource;
+  }
+
+  enum Parameters {
+    OFFSET,
+    START,
+    STOP,
+  };
+  void RecvTimelineEvent(uint32_t aIndex,
+                         AudioTimelineEvent& aEvent) override
+  {
+    MOZ_ASSERT(mDestination);
+
+    WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent,
+                                                    mDestination);
+
+    switch (aIndex) {
+    case OFFSET:
+      mOffset.InsertEvent<int64_t>(aEvent);
+      break;
+    default:
+      NS_ERROR("Bad ConstantSourceNodeEngine TimelineParameter");
+    }
+  }
+
+  void SetStreamTimeParameter(uint32_t aIndex, StreamTime aParam) override
+  {
+    switch (aIndex) {
+    case START:
+      mStart = aParam;
+      mSource->SetActive();
+      break;
+    case STOP: mStop = aParam; break;
+    default:
+      NS_ERROR("Bad ConstantSourceNodeEngine StreamTimeParameter");
+    }
+  }
+
+  void ProcessBlock(AudioNodeStream* aStream,
+                    GraphTime aFrom,
+                    const AudioBlock& aInput,
+                    AudioBlock* aOutput,
+                    bool* aFinished) override
+  {
+    MOZ_ASSERT(mSource == aStream, "Invalid source stream");
+
+    StreamTime ticks = mDestination->GraphTimeToStreamTime(aFrom);
+    if (mStart == -1) {
+      aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+      return;
+    }
+
+    if (ticks + WEBAUDIO_BLOCK_SIZE <= mStart || ticks >= mStop) {
+      aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+    } else {
+      aOutput->AllocateChannels(1);
+      float* output = aOutput->ChannelFloatsForWrite(0);
+
+      if (mOffset.HasSimpleValue()) {
+        for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+          output[i] = mOffset.GetValueAtTime(aFrom, 0);
+        }
+      } else {
+        mOffset.GetValuesAtTime(ticks, output, WEBAUDIO_BLOCK_SIZE);
+      }
+    }
+
+    if (ticks + WEBAUDIO_BLOCK_SIZE >= mStop) {
+      // We've finished playing.
+      *aFinished = true;
+    }
+  }
+
+  bool IsActive() const override
+  {
+    // start() has been called.
+    return mStart != -1;
+  }
+
+  size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
+  {
+    size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+
+    // Not owned:
+    // - mSource
+    // - mDestination
+    // - mOffset (internal ref owned by node)
+
+    return amount;
+  }
+
+  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override
+  {
+    return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+  }
+
+  AudioNodeStream* mSource;
+  AudioNodeStream* mDestination;
+  StreamTime mStart;
+  StreamTime mStop;
+  AudioParamTimeline mOffset;
+};
+
 ConstantSourceNode::ConstantSourceNode(AudioContext* aContext)
   : AudioNode(aContext,
               1,
               ChannelCountMode::Max,
               ChannelInterpretation::Speakers)
+  , mOffset(new AudioParam(this, ConstantSourceNodeEngine::OFFSET,
+                           1.0, "offset"))
+  , mStartCalled(false)
 {
+  ConstantSourceNodeEngine* engine = new ConstantSourceNodeEngine(this, aContext->Destination());
+  mStream = AudioNodeStream::Create(aContext, engine,
+                                    AudioNodeStream::NEED_MAIN_THREAD_FINISHED,
+                                    aContext->Graph());
+  engine->SetSourceStream(mStream);
+  mStream->AddMainThreadListener(this);
 }
 
 ConstantSourceNode::~ConstantSourceNode()
 {
 }
 
 size_t
 ConstantSourceNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
@@ -51,36 +180,107 @@ ConstantSourceNode::SizeOfIncludingThis(
 JSObject*
 ConstantSourceNode::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return ConstantSourceNodeBinding::Wrap(aCx, this, aGivenProto);
 }
 
 already_AddRefed<ConstantSourceNode>
 ConstantSourceNode::Constructor(const GlobalObject& aGlobal,
-                                const AudioContext& aContext,
+                                AudioContext& aContext,
                                 const ConstantSourceOptions& aOptions,
                                 ErrorResult& aRv)
 {
+  RefPtr<ConstantSourceNode> object = new ConstantSourceNode(&aContext);
+  object->mOffset->SetValue(aOptions.mOffset);
+  return object.forget();
 }
 
 void
 ConstantSourceNode::DestroyMediaStream()
 {
+  if (mStream) {
+    mStream->RemoveMainThreadListener(this);
+  }
+  AudioNode::DestroyMediaStream();
 }
 
 void
-ConstantSourceNode::Start(double aWhen, ErrorResult& rv)
+ConstantSourceNode::Start(double aWhen, ErrorResult& aRv)
 {
+  if (!WebAudioUtils::IsTimeValid(aWhen)) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
+
+  if (mStartCalled) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+  mStartCalled = true;
+
+  if (!mStream) {
+    return;
+  }
+
+  mStream->SetStreamTimeParameter(ConstantSourceNodeEngine::START,
+                                  Context(), aWhen);
+
+  MarkActive();
 }
 
 void
-ConstantSourceNode::Stop(double aWhen, ErrorResult& rv)
+ConstantSourceNode::Stop(double aWhen, ErrorResult& aRv)
 {
+  if (!WebAudioUtils::IsTimeValid(aWhen)) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
+
+  if (!mStartCalled) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+
+  if (!mStream || !Context()) {
+    return;
+  }
+
+  mStream->SetStreamTimeParameter(ConstantSourceNodeEngine::STOP,
+                                  Context(), std::max(0.0, aWhen));
 }
 
 void
 ConstantSourceNode::NotifyMainThreadStreamFinished()
 {
+  MOZ_ASSERT(mStream->IsFinished());
+
+  class EndedEventDispatcher final : public Runnable
+  {
+  public:
+    explicit EndedEventDispatcher(ConstantSourceNode* aNode)
+      : mNode(aNode) {}
+    NS_IMETHOD Run() override
+    {
+      // If it's not safe to run scripts right now, schedule this to run later
+      if (!nsContentUtils::IsSafeToRunScript()) {
+        nsContentUtils::AddScriptRunner(this);
+        return NS_OK;
+      }
+
+      mNode->DispatchTrustedEvent(NS_LITERAL_STRING("ended"));
+      // Release stream resources.
+      mNode->DestroyMediaStream();
+      return NS_OK;
+    }
+  private:
+    RefPtr<ConstantSourceNode> mNode;
+  };
+
+  NS_DispatchToMainThread(new EndedEventDispatcher(this));
+
+  // Drop the playing reference
+  // Warning: The below line might delete this.
+  MarkInactive();
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/media/webaudio/ConstantSourceNode.h
+++ b/dom/media/webaudio/ConstantSourceNode.h
@@ -24,17 +24,17 @@ public:
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ConstantSourceNode, AudioNode)
 
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<ConstantSourceNode>
   Constructor(const GlobalObject& aGlobal,
-              const AudioContext& aContext,
+              AudioContext& aContext,
               const ConstantSourceOptions& aOptions,
               ErrorResult& aRv);
 
   void DestroyMediaStream() override;
 
   uint16_t NumberOfInputs() const final override
   {
     return 0;
@@ -60,16 +60,17 @@ public:
   size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
   size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
 
 protected:
   virtual ~ConstantSourceNode();
 
 private:
   RefPtr<AudioParam> mOffset;
+  bool mStartCalled;
 
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif
 
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -37757,16 +37757,22 @@
           }
         ],
         "web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html": [
           {
             "path": "web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html",
             "url": "/web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html"
           }
         ],
+        "webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html": [
+          {
+            "path": "webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html",
+            "url": "/webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html"
+          }
+        ],
         "workers/Worker_ErrorEvent_error.htm": [
           {
             "path": "workers/Worker_ErrorEvent_error.htm",
             "url": "/workers/Worker_ErrorEvent_error.htm"
           }
         ]
       }
     },
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test the ConstantSourceNode Interface</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(function(t) {
+  var ac = new AudioContext();
+
+  var csn = ac.createConstantSource();
+  assert_true(csn.offset.value == 1.0, "Default offset is 1.0");
+
+  csn = new ConstantSourceNode(ac);
+  assert_true(csn.offset.value == 1.0, "Default offset is 1.0");
+
+  csn = new ConstantSourceNode(ac, {offset: -0.25});
+  assert_true(csn.offset.value == -0.25, "Offset can be set during construction");
+}, "ConstantSourceNode can be constructed");
+
+test(function(t) {
+  var ac = new AudioContext();
+
+  var csn = ac.createConstantSource();
+
+  assert_throws("InvalidStateError", function() {
+    csn.stop(1);
+  }, "Start must be called before stop");
+
+  assert_throws("NotSupportedError", function() {
+    csn.start(-1);
+  }, "When can not be negative");
+
+  csn.start(0);
+  assert_throws("NotSupportedError", function() {
+    csn.stop(-1);
+  }, "When can not be negative");
+}, "ConstantSourceNode stop and start");
+
+async_test(function(t) {
+  var ac = new OfflineAudioContext(1, 2048, 44100);
+  var csn = ac.createConstantSource();
+  csn.connect(ac.destination);
+  csn.start()
+  csn.stop(1024/44100)
+  csn.onended = function(e) {
+    t.step(function() {
+      assert_true(e.type == "ended", "Event type should be 'ended', received: " + e.type);
+    });
+    t.done();
+  }
+  ac.startRendering();
+}, "ConstantSourceNode onended event");
+
+async_test(function(t) {
+  var ac = new OfflineAudioContext(1, 2048, 44100);
+  var csn = ac.createConstantSource();
+  csn.connect(ac.destination);
+  csn.start(512/44100)
+  csn.stop(1024/44100)
+
+  ac.oncomplete = function(e) {
+    t.step(function() {
+      var result = e.renderedBuffer.getChannelData(0);
+      for (var i = 0; i < 2048; ++i) {
+        if (i >= 512 && i < 1024) {
+          assert_true(result[i] == 1.0, "sample " + i + " should equal 1.0");
+        } else {
+          assert_true(result[i] == 0.0, "sample " + i + " should equal 0.0");
+        }
+      }
+    });
+    t.done();
+  }
+
+  ac.startRendering();
+}, "ConstantSourceNode start and stop when work");
+
+async_test(function(t) {
+  var ac = new OfflineAudioContext(1, 2048, 44100);
+  var csn = ac.createConstantSource();
+  csn.offset.value = 0.25;
+  csn.connect(ac.destination);
+  csn.start()
+
+  ac.oncomplete = function(e) {
+    t.step(function() {
+      var result = e.renderedBuffer.getChannelData(0);
+      for (var i = 0; i < 2048; ++i) {
+        assert_true(result[i] == 0.25, "sample " + i + " should equal 0.25");
+      }
+    });
+    t.done();
+  }
+
+  ac.startRendering();
+}, "ConstantSourceNode with no automation");
+
+async_test(function(t) {
+  var ac = new OfflineAudioContext(1, 2048, 44100);
+
+  var timeConstant = 2.0;
+  var offsetStart = 0.25;
+  var offsetEnd = 0.1;
+
+  var csn = ac.createConstantSource();
+  csn.offset.value = offsetStart;
+  csn.offset.setTargetAtTime(offsetEnd, 1024/ac.sampleRate, timeConstant);
+  csn.connect(ac.destination);
+  csn.start()
+
+  ac.oncomplete = function(e) {
+    t.step(function() {
+      // create buffer with expected values
+      var buffer = ac.createBuffer(1, 2048, ac.sampleRate);
+      for (var i = 0; i < 2048; ++i) {
+        if (i < 1024) {
+          buffer.getChannelData(0)[i] = offsetStart;
+        } else {
+          time = (i-1024)/ac.sampleRate;
+          buffer.getChannelData(0)[i] = offsetEnd + (offsetStart - offsetEnd)*Math.exp(-time/timeConstant);
+        }
+      }
+
+      var result = e.renderedBuffer.getChannelData(0);
+      var expected = buffer.getChannelData(0);
+      for (var i = 0; i < 2048; ++i) {
+        assert_true(Math.abs(result[i] - expected[i]) < 1e-6, "sample " + i + " should equal " + expected[i]);
+      }
+    });
+    t.done();
+  }
+
+  ac.startRendering();
+}, "ConstantSourceNode with automation");
+</script>