diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /dom/media/webaudio/AudioWorkletNode.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/webaudio/AudioWorkletNode.cpp')
-rw-r--r-- | dom/media/webaudio/AudioWorkletNode.cpp | 896 |
1 files changed, 896 insertions, 0 deletions
diff --git a/dom/media/webaudio/AudioWorkletNode.cpp b/dom/media/webaudio/AudioWorkletNode.cpp new file mode 100644 index 0000000000..2b8afade3c --- /dev/null +++ b/dom/media/webaudio/AudioWorkletNode.cpp @@ -0,0 +1,896 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "AudioWorkletNode.h" + +#include "AudioNodeEngine.h" +#include "AudioParamMap.h" +#include "AudioWorkletImpl.h" +#include "js/Array.h" // JS::{Get,Set}ArrayLength, JS::NewArrayLength +#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable +#include "js/Exception.h" +#include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_GetProperty +#include "mozilla/dom/AudioWorkletNodeBinding.h" +#include "mozilla/dom/AudioParamMapBinding.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/Worklet.h" +#include "nsIScriptGlobalObject.h" +#include "AudioParam.h" +#include "AudioDestinationNode.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/ScopeExit.h" +#include "nsReadableUtils.h" +#include "mozilla/Span.h" +#include "PlayingRefChangeHandler.h" +#include "nsPrintfCString.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(AudioWorkletNode, AudioNode) +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletNode, AudioNode, mPort, + mParameters) + +struct NamedAudioParamTimeline { + explicit NamedAudioParamTimeline(const AudioParamDescriptor& aParamDescriptor) + : mName(aParamDescriptor.mName), + mTimeline(aParamDescriptor.mDefaultValue) {} + const nsString mName; + AudioParamTimeline mTimeline; +}; + +struct ProcessorErrorDetails { + ProcessorErrorDetails() : mLineno(0), mColno(0) {} + unsigned mLineno; + unsigned mColno; + nsString mFilename; + nsString mMessage; +}; + +class WorkletNodeEngine final : public AudioNodeEngine { + public: + WorkletNodeEngine(AudioWorkletNode* aNode, + AudioDestinationNode* aDestinationNode, + nsTArray<NamedAudioParamTimeline>&& aParamTimelines, + const Optional<Sequence<uint32_t>>& aOutputChannelCount) + : AudioNodeEngine(aNode), + mDestination(aDestinationNode->Track()), + mParamTimelines(std::move(aParamTimelines)) { + if (aOutputChannelCount.WasPassed()) { + mOutputChannelCount = aOutputChannelCount.Value(); + } + } + + MOZ_CAN_RUN_SCRIPT + void ConstructProcessor(AudioWorkletImpl* aWorkletImpl, + const nsAString& aName, + NotNull<StructuredCloneHolder*> aSerializedOptions, + UniqueMessagePortId& aPortIdentifier, + AudioNodeTrack* aTrack); + + void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override { + MOZ_ASSERT(mDestination); + WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination); + + if (aIndex < mParamTimelines.Length()) { + mParamTimelines[aIndex].mTimeline.InsertEvent<int64_t>(aEvent); + } else { + NS_ERROR("Bad WorkletNodeEngine timeline event index"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + MOZ_ASSERT(InputCount() <= 1); + MOZ_ASSERT(OutputCount() <= 1); + TRACE("WorkletNodeEngine::ProcessBlock"); + ProcessBlocksOnPorts(aTrack, aFrom, Span(&aInput, InputCount()), + Span(aOutput, OutputCount()), aFinished); + } + + void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, bool* aFinished) override; + + void OnGraphThreadDone() override { ReleaseJSResources(); } + + bool IsActive() const override { return mKeepEngineActive; } + + // Vector<T> supports non-memmovable types such as PersistentRooted + // (without any need to jump through hoops like + // MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR_FOR_TEMPLATE for nsTArray). + // PersistentRooted is used because these AudioWorkletGlobalScope scope + // objects may be kept alive as long as the AudioWorkletNode in the + // main-thread global. + struct Channels { + Vector<JS::PersistentRooted<JSObject*>, GUESS_AUDIO_CHANNELS> + mFloat32Arrays; + JS::PersistentRooted<JSObject*> mJSArray; + // For SetArrayElements(): + operator JS::Handle<JSObject*>() const { return mJSArray; } + }; + struct Ports { + Vector<Channels, 1> mPorts; + JS::PersistentRooted<JSObject*> mJSArray; + }; + struct ParameterValues { + Vector<JS::PersistentRooted<JSObject*>> mFloat32Arrays; + JS::PersistentRooted<JSObject*> mJSObject; + }; + + private: + size_t ParameterCount() { return mParamTimelines.Length(); } + void SendProcessorError(AudioNodeTrack* aTrack, JSContext* aCx); + bool CallProcess(AudioNodeTrack* aTrack, JSContext* aCx, + JS::Handle<JS::Value> aCallable); + void ProduceSilence(AudioNodeTrack* aTrack, Span<AudioBlock> aOutput); + void SendErrorToMainThread(AudioNodeTrack* aTrack, + const ProcessorErrorDetails& aDetails); + + void ReleaseJSResources() { + mInputs.mPorts.clearAndFree(); + mOutputs.mPorts.clearAndFree(); + mParameters.mFloat32Arrays.clearAndFree(); + mInputs.mJSArray.reset(); + mOutputs.mJSArray.reset(); + mParameters.mJSObject.reset(); + mGlobal = nullptr; + // This is equivalent to setting [[callable process]] to false. + mProcessor.reset(); + } + + nsCString mProcessorName; + RefPtr<AudioNodeTrack> mDestination; + nsTArray<uint32_t> mOutputChannelCount; + nsTArray<NamedAudioParamTimeline> mParamTimelines; + // The AudioWorkletGlobalScope-associated objects referenced from + // WorkletNodeEngine are typically kept alive as long as the + // AudioWorkletNode in the main-thread global. The objects must be released + // on the rendering thread, which usually happens simply because + // AudioWorkletNode is such that the last AudioNodeTrack reference is + // released by the MTG. That occurs on the rendering thread except during + // process shutdown, in which case NotifyForcedShutdown() is called on the + // rendering thread. + // + // mInputs, mOutputs and mParameters keep references to all objects passed to + // process(), for reuse of the same objects. The JS objects are all in the + // compartment of the realm of mGlobal. Properties on the objects may be + // replaced by script, so don't assume that getting indexed properties on the + // JS arrays will return the same objects. Only objects and buffers created + // by the implementation are modified or read by the implementation. + Ports mInputs; + Ports mOutputs; + ParameterValues mParameters; + + RefPtr<AudioWorkletGlobalScope> mGlobal; + JS::PersistentRooted<JSObject*> mProcessor; + + // mProcessorIsActive is named [[active source]] in the spec. + // It is initially true and so at least the first process() + // call will not be skipped when there are no active inputs. + bool mProcessorIsActive = true; + // mKeepEngineActive ensures another call to ProcessBlocksOnPorts(), even if + // there are no active inputs. Its transitions to false lag those of + // mProcessorIsActive by one call to ProcessBlocksOnPorts() so that + // downstream engines can addref their nodes before this engine's node is + // released. + bool mKeepEngineActive = true; +}; + +void WorkletNodeEngine::SendErrorToMainThread( + AudioNodeTrack* aTrack, const ProcessorErrorDetails& aDetails) { + RefPtr<AudioNodeTrack> track = aTrack; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "WorkletNodeEngine::SendProcessorError", + [track = std::move(track), aDetails]() mutable { + AudioWorkletNode* node = + static_cast<AudioWorkletNode*>(track->Engine()->NodeMainThread()); + if (!node) { + return; + } + node->DispatchProcessorErrorEvent(aDetails); + })); +} + +void WorkletNodeEngine::SendProcessorError(AudioNodeTrack* aTrack, + JSContext* aCx) { + // Note that once an exception is thrown, the processor will output silence + // throughout its lifetime. + ReleaseJSResources(); + // The processor errored out while getting a context, try to tell the node + // anyways. + if (!aCx || !JS_IsExceptionPending(aCx)) { + ProcessorErrorDetails details; + details.mMessage.Assign(u"Unknown processor error"); + SendErrorToMainThread(aTrack, details); + return; + } + + JS::ExceptionStack exnStack(aCx); + if (JS::StealPendingExceptionStack(aCx, &exnStack)) { + JS::ErrorReportBuilder jsReport(aCx); + if (!jsReport.init(aCx, exnStack, + JS::ErrorReportBuilder::WithSideEffects)) { + ProcessorErrorDetails details; + details.mMessage.Assign(u"Unknown processor error"); + SendErrorToMainThread(aTrack, details); + // Set the exception and stack back to have it in the console with a stack + // trace. + JS::SetPendingExceptionStack(aCx, exnStack); + return; + } + + ProcessorErrorDetails details; + + CopyUTF8toUTF16(mozilla::MakeStringSpan(jsReport.report()->filename), + details.mFilename); + + xpc::ErrorReport::ErrorReportToMessageString(jsReport.report(), + details.mMessage); + details.mLineno = jsReport.report()->lineno; + details.mColno = jsReport.report()->column; + MOZ_ASSERT(!jsReport.report()->isMuted); + + SendErrorToMainThread(aTrack, details); + + // Set the exception and stack back to have it in the console with a stack + // trace. + JS::SetPendingExceptionStack(aCx, exnStack); + } else { + NS_WARNING("No exception, but processor errored out?"); + } +} + +void WorkletNodeEngine::ConstructProcessor( + AudioWorkletImpl* aWorkletImpl, const nsAString& aName, + NotNull<StructuredCloneHolder*> aSerializedOptions, + UniqueMessagePortId& aPortIdentifier, AudioNodeTrack* aTrack) { + MOZ_ASSERT(mInputs.mPorts.empty() && mOutputs.mPorts.empty()); + RefPtr<AudioWorkletGlobalScope> global = aWorkletImpl->GetGlobalScope(); + if (!global) { + // A global was previously used to register this kind of processor. If it + // no longer exists now, that is because the document is going away and so + // there is no need to send an error. + return; + } + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(global))) { + SendProcessorError(aTrack, nullptr); + return; + } + mProcessorName = NS_ConvertUTF16toUTF8(aName); + JSContext* cx = api.cx(); + mProcessor.init(cx); + if (!global->ConstructProcessor(cx, aName, aSerializedOptions, + aPortIdentifier, &mProcessor) || + // mInputs and mOutputs outer arrays are fixed length and so much of the + // initialization need only be performed once (i.e. here). + NS_WARN_IF(!mInputs.mPorts.growBy(InputCount())) || + NS_WARN_IF(!mOutputs.mPorts.growBy(OutputCount()))) { + SendProcessorError(aTrack, cx); + return; + } + mGlobal = std::move(global); + mInputs.mJSArray.init(cx); + mOutputs.mJSArray.init(cx); + for (auto& port : mInputs.mPorts) { + port.mJSArray.init(cx); + } + for (auto& port : mOutputs.mPorts) { + port.mJSArray.init(cx); + } + JSObject* object = JS_NewPlainObject(cx); + if (NS_WARN_IF(!object)) { + SendProcessorError(aTrack, cx); + return; + } + + mParameters.mJSObject.init(cx, object); + if (NS_WARN_IF(!mParameters.mFloat32Arrays.growBy(ParameterCount()))) { + SendProcessorError(aTrack, cx); + return; + } + for (size_t i = 0; i < mParamTimelines.Length(); i++) { + auto& float32ArraysRef = mParameters.mFloat32Arrays; + float32ArraysRef[i].init(cx); + JSObject* array = JS_NewFloat32Array(cx, WEBAUDIO_BLOCK_SIZE); + if (NS_WARN_IF(!array)) { + SendProcessorError(aTrack, cx); + return; + } + + float32ArraysRef[i] = array; + if (NS_WARN_IF(!JS_DefineUCProperty( + cx, mParameters.mJSObject, mParamTimelines[i].mName.get(), + mParamTimelines[i].mName.Length(), float32ArraysRef[i], + JSPROP_ENUMERATE))) { + SendProcessorError(aTrack, cx); + return; + } + } + if (NS_WARN_IF(!JS_FreezeObject(cx, mParameters.mJSObject))) { + SendProcessorError(aTrack, cx); + return; + } +} + +// Type T should support the length() and operator[]() methods and the return +// type of |operator[]() const| should support conversion to Handle<JSObject*>. +template <typename T> +static bool SetArrayElements(JSContext* aCx, const T& aElements, + JS::Handle<JSObject*> aArray) { + for (size_t i = 0; i < aElements.length(); ++i) { + if (!JS_DefineElement(aCx, aArray, i, aElements[i], JSPROP_ENUMERATE)) { + return false; + } + } + + return true; +} + +template <typename T> +static bool PrepareArray(JSContext* aCx, const T& aElements, + JS::MutableHandle<JSObject*> aArray) { + size_t length = aElements.length(); + if (aArray) { + // Attempt to reuse. + uint32_t oldLength; + if (JS::GetArrayLength(aCx, aArray, &oldLength) && + (oldLength == length || JS::SetArrayLength(aCx, aArray, length)) && + SetArrayElements(aCx, aElements, aArray)) { + return true; + } + // Script may have frozen the array. Try again with a new Array. + JS_ClearPendingException(aCx); + } + JSObject* array = JS::NewArrayObject(aCx, length); + if (NS_WARN_IF(!array)) { + return false; + } + aArray.set(array); + return SetArrayElements(aCx, aElements, aArray); +} + +enum class ArrayElementInit { None, Zero }; + +// Exactly when to create new Float32Array and Array objects is not specified. +// This approach aims to minimize garbage creation, while continuing to +// function after objects are modified by content. +// See https://github.com/WebAudio/web-audio-api/issues/1934 and +// https://github.com/WebAudio/web-audio-api/issues/1933 +static bool PrepareBufferArrays(JSContext* aCx, Span<const AudioBlock> aBlocks, + WorkletNodeEngine::Ports* aPorts, + ArrayElementInit aInit) { + MOZ_ASSERT(aBlocks.Length() == aPorts->mPorts.length()); + for (size_t i = 0; i < aBlocks.Length(); ++i) { + size_t channelCount = aBlocks[i].ChannelCount(); + WorkletNodeEngine::Channels& portRef = aPorts->mPorts[i]; + + auto& float32ArraysRef = portRef.mFloat32Arrays; + for (auto& channelRef : float32ArraysRef) { + size_t length = JS_GetTypedArrayLength(channelRef); + if (length != WEBAUDIO_BLOCK_SIZE) { + // Script has detached array buffers. Create new objects. + JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE); + if (NS_WARN_IF(!array)) { + return false; + } + channelRef = array; + } else if (aInit == ArrayElementInit::Zero) { + // Need only zero existing arrays as new arrays are already zeroed. + JS::AutoCheckCannotGC nogc; + bool isShared; + float* elementData = + JS_GetFloat32ArrayData(channelRef, &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + std::fill_n(elementData, WEBAUDIO_BLOCK_SIZE, 0.0f); + } + } + // Enlarge if necessary... + if (NS_WARN_IF(!float32ArraysRef.reserve(channelCount))) { + return false; + } + while (float32ArraysRef.length() < channelCount) { + JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE); + if (NS_WARN_IF(!array)) { + return false; + } + float32ArraysRef.infallibleEmplaceBack(aCx, array); + } + // ... or shrink if necessary. + float32ArraysRef.shrinkTo(channelCount); + + if (NS_WARN_IF(!PrepareArray(aCx, float32ArraysRef, &portRef.mJSArray))) { + return false; + } + } + + return !(NS_WARN_IF(!PrepareArray(aCx, aPorts->mPorts, &aPorts->mJSArray))); +} + +// This runs JS script. MediaTrackGraph control messages, which would +// potentially destroy the WorkletNodeEngine and its AudioNodeTrack, cannot +// be triggered by script. They are not run from an nsIThread event loop and +// do not run until after ProcessBlocksOnPorts() has returned. +bool WorkletNodeEngine::CallProcess(AudioNodeTrack* aTrack, JSContext* aCx, + JS::Handle<JS::Value> aCallable) { + TRACE_COMMENT("AudioWorkletNodeEngine::CallProcess", mProcessorName.get()); + + JS::RootedVector<JS::Value> argv(aCx); + if (NS_WARN_IF(!argv.resize(3))) { + return false; + } + argv[0].setObject(*mInputs.mJSArray); + argv[1].setObject(*mOutputs.mJSArray); + argv[2].setObject(*mParameters.mJSObject); + JS::Rooted<JS::Value> rval(aCx); + if (!JS::Call(aCx, mProcessor, aCallable, argv, &rval)) { + return false; + } + + mProcessorIsActive = JS::ToBoolean(rval); + // Transitions of mProcessorIsActive to false do not trigger + // PlayingRefChangeHandler::RELEASE until silence is produced in the next + // block. This allows downstream engines receiving this non-silence block + // to take a reference to their nodes before this engine's node releases its + // down node references. + if (mProcessorIsActive && !mKeepEngineActive) { + mKeepEngineActive = true; + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + return true; +} + +void WorkletNodeEngine::ProduceSilence(AudioNodeTrack* aTrack, + Span<AudioBlock> aOutput) { + if (mKeepEngineActive) { + mKeepEngineActive = false; + aTrack->ScheduleCheckForInactive(); + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + for (AudioBlock& output : aOutput) { + output.SetNull(WEBAUDIO_BLOCK_SIZE); + } +} + +void WorkletNodeEngine::ProcessBlocksOnPorts(AudioNodeTrack* aTrack, + GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, + bool* aFinished) { + MOZ_ASSERT(aInput.Length() == InputCount()); + MOZ_ASSERT(aOutput.Length() == OutputCount()); + TRACE("WorkletNodeEngine::ProcessBlocksOnPorts"); + + bool isSilent = true; + if (mProcessor) { + if (mProcessorIsActive) { + isSilent = false; // call process() + } else { // [[active source]] is false. + // Call process() only if an input is actively processing. + for (const AudioBlock& input : aInput) { + if (!input.IsNull()) { + isSilent = false; + break; + } + } + } + } + if (isSilent) { + ProduceSilence(aTrack, aOutput); + return; + } + + if (!mOutputChannelCount.IsEmpty()) { + MOZ_ASSERT(mOutputChannelCount.Length() == aOutput.Length()); + for (size_t o = 0; o < aOutput.Length(); ++o) { + aOutput[o].AllocateChannels(mOutputChannelCount[o]); + } + } else if (aInput.Length() == 1 && aOutput.Length() == 1) { + uint32_t channelCount = std::max(aInput[0].ChannelCount(), 1U); + aOutput[0].AllocateChannels(channelCount); + } else { + for (AudioBlock& output : aOutput) { + output.AllocateChannels(1); + } + } + + AutoEntryScript aes(mGlobal, "Worklet Process"); + JSContext* cx = aes.cx(); + auto produceSilenceWithError = MakeScopeExit([this, aTrack, cx, &aOutput] { + SendProcessorError(aTrack, cx); + ProduceSilence(aTrack, aOutput); + }); + + JS::Rooted<JS::Value> process(cx); + if (!JS_GetProperty(cx, mProcessor, "process", &process) || + !process.isObject() || !JS::IsCallable(&process.toObject()) || + !PrepareBufferArrays(cx, aInput, &mInputs, ArrayElementInit::None) || + !PrepareBufferArrays(cx, aOutput, &mOutputs, ArrayElementInit::Zero)) { + // process() not callable or OOM. + return; + } + + // Copy input values to JS objects. + for (size_t i = 0; i < aInput.Length(); ++i) { + const AudioBlock& input = aInput[i]; + size_t channelCount = input.ChannelCount(); + if (channelCount == 0) { + // Null blocks have AUDIO_FORMAT_SILENCE. + // Don't call ChannelData<float>(). + continue; + } + float volume = input.mVolume; + const auto& channelData = input.ChannelData<float>(); + const auto& float32Arrays = mInputs.mPorts[i].mFloat32Arrays; + JS::AutoCheckCannotGC nogc; + for (size_t c = 0; c < channelCount; ++c) { + bool isShared; + float* dest = JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + AudioBlockCopyChannelWithScale(channelData[c], volume, dest); + } + } + + TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); + // Compute and copy parameter values to JS objects. + for (size_t i = 0; i < mParamTimelines.Length(); ++i) { + const auto& float32Arrays = mParameters.mFloat32Arrays[i]; + size_t length = JS_GetTypedArrayLength(float32Arrays); + + // If the Float32Array that is supposed to hold the values for a particular + // AudioParam has been detached, error out. This is being worked on in + // https://github.com/WebAudio/web-audio-api/issues/1933 and + // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486 + if (length != WEBAUDIO_BLOCK_SIZE) { + return; + } + JS::AutoCheckCannotGC nogc; + bool isShared; + float* dest = JS_GetFloat32ArrayData(float32Arrays, &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + + size_t frames = + mParamTimelines[i].mTimeline.HasSimpleValue() ? 1 : WEBAUDIO_BLOCK_SIZE; + mParamTimelines[i].mTimeline.GetValuesAtTime(tick, dest, frames); + // https://bugzilla.mozilla.org/show_bug.cgi?id=1616599 + if (frames == 1) { + std::fill_n(dest + 1, WEBAUDIO_BLOCK_SIZE - 1, dest[0]); + } + } + + if (!CallProcess(aTrack, cx, process)) { + // An exception occurred. + /** + * https://webaudio.github.io/web-audio-api/#dom-audioworkletnode-onprocessorerror + * Note that once an exception is thrown, the processor will output silence + * throughout its lifetime. + */ + return; + } + + // Copy output values from JS objects. + for (size_t o = 0; o < aOutput.Length(); ++o) { + AudioBlock* output = &aOutput[o]; + size_t channelCount = output->ChannelCount(); + const auto& float32Arrays = mOutputs.mPorts[o].mFloat32Arrays; + for (size_t c = 0; c < channelCount; ++c) { + size_t length = JS_GetTypedArrayLength(float32Arrays[c]); + if (length != WEBAUDIO_BLOCK_SIZE) { + // ArrayBuffer has been detached. Behavior is unspecified. + // https://github.com/WebAudio/web-audio-api/issues/1933 and + // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486 + return; + } + JS::AutoCheckCannotGC nogc; + bool isShared; + const float* src = + JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + PodCopy(output->ChannelFloatsForWrite(c), src, WEBAUDIO_BLOCK_SIZE); + } + } + + produceSilenceWithError.release(); // have output and no error +} + +AudioWorkletNode::AudioWorkletNode(AudioContext* aAudioContext, + const nsAString& aName, + const AudioWorkletNodeOptions& aOptions) + : AudioNode(aAudioContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mNodeName(aName), + mInputCount(aOptions.mNumberOfInputs), + mOutputCount(aOptions.mNumberOfOutputs) {} + +void AudioWorkletNode::InitializeParameters( + nsTArray<NamedAudioParamTimeline>* aParamTimelines, ErrorResult& aRv) { + MOZ_ASSERT(!mParameters, "Only initialize the `parameters` attribute once."); + MOZ_ASSERT(aParamTimelines); + + AudioContext* context = Context(); + const AudioParamDescriptorMap* parameterDescriptors = + context->GetParamMapForWorkletName(mNodeName); + MOZ_ASSERT(parameterDescriptors); + + size_t audioParamIndex = 0; + aParamTimelines->SetCapacity(parameterDescriptors->Length()); + + for (size_t i = 0; i < parameterDescriptors->Length(); i++) { + auto& paramEntry = (*parameterDescriptors)[i]; + CreateAudioParam(audioParamIndex++, paramEntry.mName, + paramEntry.mDefaultValue, paramEntry.mMinValue, + paramEntry.mMaxValue); + aParamTimelines->AppendElement(paramEntry); + } +} + +void AudioWorkletNode::SendParameterData( + const Optional<Record<nsString, double>>& aParameterData) { + MOZ_ASSERT(mTrack, "This method only works if the track has been created."); + nsAutoString name; + if (aParameterData.WasPassed()) { + const auto& paramData = aParameterData.Value(); + for (const auto& paramDataEntry : paramData.Entries()) { + for (auto& audioParam : mParams) { + audioParam->GetName(name); + if (paramDataEntry.mKey.Equals(name)) { + audioParam->SetInitialValue(paramDataEntry.mValue); + } + } + } + } +} + +/* static */ +already_AddRefed<AudioWorkletNode> AudioWorkletNode::Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const nsAString& aName, const AudioWorkletNodeOptions& aOptions, + ErrorResult& aRv) { + TRACE_COMMENT("AudioWorkletNode::Constructor", "%s", + NS_ConvertUTF16toUTF8(aName).get()); + /** + * 1. If nodeName does not exist as a key in the BaseAudioContext’s node + * name to parameter descriptor map, throw a InvalidStateError exception + * and abort these steps. + */ + const AudioParamDescriptorMap* parameterDescriptors = + aAudioContext.GetParamMapForWorkletName(aName); + if (!parameterDescriptors) { + // Not using nsPrintfCString in case aName has embedded nulls. + aRv.ThrowInvalidStateError("Unknown AudioWorklet name '"_ns + + NS_ConvertUTF16toUTF8(aName) + "'"_ns); + return nullptr; + } + + // See https://github.com/WebAudio/web-audio-api/issues/2074 for ordering. + RefPtr<AudioWorkletNode> audioWorkletNode = + new AudioWorkletNode(&aAudioContext, aName, aOptions); + audioWorkletNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + /** + * 3. Configure input, output and output channels of node with options. + */ + if (aOptions.mNumberOfInputs == 0 && aOptions.mNumberOfOutputs == 0) { + aRv.ThrowNotSupportedError( + "Must have nonzero numbers of inputs or outputs"); + return nullptr; + } + + if (aOptions.mOutputChannelCount.WasPassed()) { + /** + * 1. If any value in outputChannelCount is zero or greater than the + * implementation’s maximum number of channels, throw a + * NotSupportedError and abort the remaining steps. + */ + for (uint32_t channelCount : aOptions.mOutputChannelCount.Value()) { + if (channelCount == 0 || channelCount > WebAudioUtils::MaxChannelCount) { + aRv.ThrowNotSupportedError( + nsPrintfCString("Channel count (%u) must be in the range [1, max " + "supported channel count]", + channelCount)); + return nullptr; + } + } + /** + * 2. If the length of outputChannelCount does not equal numberOfOutputs, + * throw an IndexSizeError and abort the remaining steps. + */ + if (aOptions.mOutputChannelCount.Value().Length() != + aOptions.mNumberOfOutputs) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Length of outputChannelCount (%zu) does not match " + "numberOfOutputs (%u)", + aOptions.mOutputChannelCount.Value().Length(), + aOptions.mNumberOfOutputs)); + return nullptr; + } + } + // MTG does not support more than UINT16_MAX inputs or outputs. + if (aOptions.mNumberOfInputs > UINT16_MAX) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfInputs"); + return nullptr; + } + if (aOptions.mNumberOfOutputs > UINT16_MAX) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfOutputs"); + return nullptr; + } + + /** + * 4. Let messageChannel be a new MessageChannel. + */ + RefPtr<MessageChannel> messageChannel = + MessageChannel::Constructor(aGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + /* 5. Let nodePort be the value of messageChannel’s port1 attribute. + * 6. Let processorPortOnThisSide be the value of messageChannel’s port2 + * attribute. + * 7. Let serializedProcessorPort be the result of + * StructuredSerializeWithTransfer(processorPortOnThisSide, + * « processorPortOnThisSide »). + */ + UniqueMessagePortId processorPortId; + messageChannel->Port2()->CloneAndDisentangle(processorPortId); + /** + * 8. Convert options dictionary to optionsObject. + */ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JS::Value> optionsVal(cx); + if (NS_WARN_IF(!ToJSValue(cx, aOptions, &optionsVal))) { + aRv.NoteJSContextException(cx); + return nullptr; + } + + /** + * 9. Let serializedOptions be the result of + * StructuredSerialize(optionsObject). + */ + + // This context and the worklet are part of the same agent cluster and they + // can share memory. + JS::CloneDataPolicy cloneDataPolicy; + cloneDataPolicy.allowIntraClusterClonableSharedObjects(); + cloneDataPolicy.allowSharedMemoryObjects(); + + // StructuredCloneHolder does not have a move constructor. Instead allocate + // memory so that the pointer can be passed to the rendering thread. + UniquePtr<StructuredCloneHolder> serializedOptions = + MakeUnique<StructuredCloneHolder>( + StructuredCloneHolder::CloningSupported, + StructuredCloneHolder::TransferringNotSupported, + JS::StructuredCloneScope::SameProcess); + serializedOptions->Write(cx, optionsVal, JS::UndefinedHandleValue, + cloneDataPolicy, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + /** + * 10. Set node’s port to nodePort. + */ + audioWorkletNode->mPort = messageChannel->Port1(); + + /** + * 11. Let parameterDescriptors be the result of retrieval of nodeName from + * node name to parameter descriptor map. + */ + nsTArray<NamedAudioParamTimeline> paramTimelines; + audioWorkletNode->InitializeParameters(¶mTimelines, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + auto engine = new WorkletNodeEngine( + audioWorkletNode, aAudioContext.Destination(), std::move(paramTimelines), + aOptions.mOutputChannelCount); + audioWorkletNode->mTrack = AudioNodeTrack::Create( + &aAudioContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, + aAudioContext.Graph()); + + audioWorkletNode->SendParameterData(aOptions.mParameterData); + + /** + * 12. Queue a control message to invoke the constructor of the + * corresponding AudioWorkletProcessor with the processor construction + * data that consists of: nodeName, node, serializedOptions, and + * serializedProcessorPort. + */ + Worklet* worklet = aAudioContext.GetAudioWorklet(aRv); + MOZ_ASSERT(worklet, "Worklet already existed and so getter shouldn't fail."); + auto workletImpl = static_cast<AudioWorkletImpl*>(worklet->Impl()); + audioWorkletNode->mTrack->SendRunnable(NS_NewRunnableFunction( + "WorkletNodeEngine::ConstructProcessor", + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. + // See bug 1535398. + // + // Note that clang and gcc have mutually incompatible rules about whether + // attributes should come before or after the `mutable` keyword here, so + // use a compatibility hack until we can switch to the standardized + // [[attr]] syntax (bug 1627007). +#ifdef __clang__ +# define AND_MUTABLE(macro) macro mutable +#else +# define AND_MUTABLE(macro) mutable macro +#endif + [track = audioWorkletNode->mTrack, + workletImpl = RefPtr<AudioWorkletImpl>(workletImpl), + name = nsString(aName), options = std::move(serializedOptions), + portId = std::move(processorPortId)]() + AND_MUTABLE(MOZ_CAN_RUN_SCRIPT_BOUNDARY) { + auto engine = static_cast<WorkletNodeEngine*>(track->Engine()); + engine->ConstructProcessor( + workletImpl, name, WrapNotNull(options.get()), portId, track); + })); +#undef AND_MUTABLE + + // [[active source]] is initially true and so at least the first process() + // call will not be skipped when there are no active inputs. + audioWorkletNode->MarkActive(); + + return audioWorkletNode.forget(); +} + +AudioParamMap* AudioWorkletNode::GetParameters(ErrorResult& aRv) { + if (!mParameters) { + RefPtr<AudioParamMap> parameters = new AudioParamMap(this); + nsAutoString name; + for (const auto& audioParam : mParams) { + audioParam->GetName(name); + AudioParamMap_Binding::MaplikeHelpers::Set(parameters, name, *audioParam, + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + mParameters = std::move(parameters); + } + return mParameters.get(); +} + +void AudioWorkletNode::DispatchProcessorErrorEvent( + const ProcessorErrorDetails& aDetails) { + TRACE("AudioWorkletNode::DispatchProcessorErrorEvent"); + if (HasListenersFor(nsGkAtoms::onprocessorerror)) { + RootedDictionary<ErrorEventInit> init(RootingCx()); + init.mMessage = aDetails.mMessage; + init.mFilename = aDetails.mFilename; + init.mLineno = aDetails.mLineno; + init.mColno = aDetails.mColno; + RefPtr<ErrorEvent> errorEvent = + ErrorEvent::Constructor(this, u"processorerror"_ns, init); + MOZ_ASSERT(errorEvent); + DispatchTrustedEvent(errorEvent); + } +} + +JSObject* AudioWorkletNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioWorkletNode_Binding::Wrap(aCx, this, aGivenProto); +} + +size_t AudioWorkletNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t AudioWorkletNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +} // namespace mozilla::dom |