Bug 1308432 - Implement ConstantSourceNode; r?padenot
MozReview-Commit-ID: DZ6Zq1rzRwP
--- 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>