AudioWorkletNode.cpp (34469B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 6 7 #include "AudioWorkletNode.h" 8 9 #include "AudioDestinationNode.h" 10 #include "AudioNodeEngine.h" 11 #include "AudioParam.h" 12 #include "AudioParamMap.h" 13 #include "AudioWorklet.h" 14 #include "AudioWorkletImpl.h" 15 #include "PlayingRefChangeHandler.h" 16 #include "Tracing.h" 17 #include "js/Array.h" // JS::{Get,Set}ArrayLength, JS::NewArrayLength 18 #include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable 19 #include "js/Exception.h" 20 #include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_GetProperty 21 #include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer 22 #include "mozilla/ScopeExit.h" 23 #include "mozilla/Span.h" 24 #include "mozilla/dom/AudioParamMapBinding.h" 25 #include "mozilla/dom/AudioWorkletNodeBinding.h" 26 #include "mozilla/dom/AutoEntryScript.h" 27 #include "mozilla/dom/ErrorEvent.h" 28 #include "mozilla/dom/MessageChannel.h" 29 #include "mozilla/dom/MessagePort.h" 30 #include "mozilla/dom/RootedDictionary.h" 31 #include "mozilla/dom/Worklet.h" 32 #include "nsIScriptGlobalObject.h" 33 #include "nsPrintfCString.h" 34 #include "nsReadableUtils.h" 35 36 namespace mozilla::dom { 37 38 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(AudioWorkletNode, AudioNode) 39 NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletNode, AudioNode, mPort, 40 mParameters) 41 42 struct NamedAudioParamTimeline { 43 explicit NamedAudioParamTimeline(const AudioParamDescriptor& aParamDescriptor) 44 : mName(aParamDescriptor.mName), 45 mTimeline(aParamDescriptor.mDefaultValue) {} 46 const nsString mName; 47 AudioParamTimeline mTimeline; 48 }; 49 50 struct ProcessorErrorDetails { 51 ProcessorErrorDetails() : mLineno(0), mColno(0) {} 52 // Line number (1-origin). 53 unsigned mLineno; 54 // Column number in UTF-16 code units (1-origin). 55 unsigned mColno; 56 nsCString mFilename; 57 nsString mMessage; 58 }; 59 60 class WorkletNodeEngine final : public AudioNodeEngine { 61 public: 62 WorkletNodeEngine(AudioWorkletNode* aNode, 63 AudioDestinationNode* aDestinationNode, 64 nsTArray<NamedAudioParamTimeline>&& aParamTimelines, 65 const Optional<Sequence<uint32_t>>& aOutputChannelCount) 66 : AudioNodeEngine(aNode), 67 mDestination(aDestinationNode->Track()), 68 mParamTimelines(std::move(aParamTimelines)) { 69 if (aOutputChannelCount.WasPassed()) { 70 mOutputChannelCount = aOutputChannelCount.Value(); 71 } 72 } 73 74 MOZ_CAN_RUN_SCRIPT 75 void ConstructProcessor(AudioWorkletImpl* aWorkletImpl, 76 const nsAString& aName, 77 NotNull<StructuredCloneHolder*> aSerializedOptions, 78 UniqueMessagePortId& aPortIdentifier, 79 AudioNodeTrack* aTrack); 80 81 void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { 82 MOZ_ASSERT(mDestination); 83 aEvent.ConvertToTicks(mDestination); 84 85 if (aIndex < mParamTimelines.Length()) { 86 mParamTimelines[aIndex].mTimeline.InsertEvent<int64_t>(aEvent); 87 } else { 88 NS_ERROR("Bad WorkletNodeEngine timeline event index"); 89 } 90 } 91 92 void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, 93 const AudioBlock& aInput, AudioBlock* aOutput, 94 bool* aFinished) override { 95 MOZ_ASSERT(InputCount() <= 1); 96 MOZ_ASSERT(OutputCount() <= 1); 97 TRACE("WorkletNodeEngine::ProcessBlock"); 98 ProcessBlocksOnPorts(aTrack, aFrom, Span(&aInput, InputCount()), 99 Span(aOutput, OutputCount()), aFinished); 100 } 101 102 void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom, 103 Span<const AudioBlock> aInput, 104 Span<AudioBlock> aOutput, bool* aFinished) override; 105 106 void OnGraphThreadDone() override { ReleaseJSResources(); } 107 108 bool IsActive() const override { return mKeepEngineActive; } 109 110 // Vector<T> supports non-memmovable types such as PersistentRooted 111 // (without any need to jump through hoops like 112 // MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR_FOR_TEMPLATE for nsTArray). 113 // PersistentRooted is used because these AudioWorkletGlobalScope scope 114 // objects may be kept alive as long as the AudioWorkletNode in the 115 // main-thread global. 116 struct Channels { 117 Vector<JS::PersistentRooted<JSObject*>, GUESS_AUDIO_CHANNELS> 118 mFloat32Arrays; 119 JS::PersistentRooted<JSObject*> mJSArray; 120 // For SetArrayElements(): 121 operator JS::Handle<JSObject*>() const { return mJSArray; } 122 }; 123 struct Ports { 124 Vector<Channels, 1> mPorts; 125 JS::PersistentRooted<JSObject*> mJSArray; 126 }; 127 struct ParameterValues { 128 Vector<JS::PersistentRooted<JSObject*>> mFloat32Arrays; 129 JS::PersistentRooted<JSObject*> mJSObject; 130 }; 131 132 private: 133 size_t ParameterCount() { return mParamTimelines.Length(); } 134 void SendProcessorError(AudioNodeTrack* aTrack, JSContext* aCx); 135 bool CallProcess(AudioNodeTrack* aTrack, JSContext* aCx, 136 JS::Handle<JS::Value> aCallable); 137 void ProduceSilence(AudioNodeTrack* aTrack, Span<AudioBlock> aOutput); 138 void SendErrorToMainThread(AudioNodeTrack* aTrack, 139 const ProcessorErrorDetails& aDetails); 140 141 void ReleaseJSResources() { 142 mInputs.mPorts.clearAndFree(); 143 mOutputs.mPorts.clearAndFree(); 144 mParameters.mFloat32Arrays.clearAndFree(); 145 mInputs.mJSArray.reset(); 146 mOutputs.mJSArray.reset(); 147 mParameters.mJSObject.reset(); 148 mGlobal = nullptr; 149 // This is equivalent to setting [[callable process]] to false. 150 mProcessor.reset(); 151 } 152 153 nsCString mProcessorName; 154 RefPtr<AudioNodeTrack> mDestination; 155 nsTArray<uint32_t> mOutputChannelCount; 156 nsTArray<NamedAudioParamTimeline> mParamTimelines; 157 // The AudioWorkletGlobalScope-associated objects referenced from 158 // WorkletNodeEngine are typically kept alive as long as the 159 // AudioWorkletNode in the main-thread global. The objects must be released 160 // on the rendering thread, which usually happens simply because 161 // AudioWorkletNode is such that the last AudioNodeTrack reference is 162 // released by the MTG. That occurs on the rendering thread except during 163 // process shutdown, in which case NotifyForcedShutdown() is called on the 164 // rendering thread. 165 // 166 // mInputs, mOutputs and mParameters keep references to all objects passed to 167 // process(), for reuse of the same objects. The JS objects are all in the 168 // compartment of the realm of mGlobal. Properties on the objects may be 169 // replaced by script, so don't assume that getting indexed properties on the 170 // JS arrays will return the same objects. Only objects and buffers created 171 // by the implementation are modified or read by the implementation. 172 Ports mInputs; 173 Ports mOutputs; 174 ParameterValues mParameters; 175 176 RefPtr<AudioWorkletGlobalScope> mGlobal; 177 JS::PersistentRooted<JSObject*> mProcessor; 178 179 // mProcessorIsActive is named [[active source]] in the spec. 180 // It is initially true and so at least the first process() 181 // call will not be skipped when there are no active inputs. 182 bool mProcessorIsActive = true; 183 // mKeepEngineActive ensures another call to ProcessBlocksOnPorts(), even if 184 // there are no active inputs. Its transitions to false lag those of 185 // mProcessorIsActive by one call to ProcessBlocksOnPorts() so that 186 // downstream engines can addref their nodes before this engine's node is 187 // released. 188 bool mKeepEngineActive = true; 189 }; 190 191 void WorkletNodeEngine::SendErrorToMainThread( 192 AudioNodeTrack* aTrack, const ProcessorErrorDetails& aDetails) { 193 RefPtr<AudioNodeTrack> track = aTrack; 194 NS_DispatchToMainThread(NS_NewRunnableFunction( 195 "WorkletNodeEngine::SendProcessorError", 196 [track = std::move(track), aDetails]() mutable { 197 AudioWorkletNode* node = 198 static_cast<AudioWorkletNode*>(track->Engine()->NodeMainThread()); 199 if (!node) { 200 return; 201 } 202 node->DispatchProcessorErrorEvent(aDetails); 203 })); 204 } 205 206 void WorkletNodeEngine::SendProcessorError(AudioNodeTrack* aTrack, 207 JSContext* aCx) { 208 // Note that once an exception is thrown, the processor will output silence 209 // throughout its lifetime. 210 ReleaseJSResources(); 211 // The processor errored out while getting a context, try to tell the node 212 // anyways. 213 if (!aCx || !JS_IsExceptionPending(aCx)) { 214 ProcessorErrorDetails details; 215 details.mMessage.Assign(u"Unknown processor error"); 216 SendErrorToMainThread(aTrack, details); 217 return; 218 } 219 220 JS::ExceptionStack exnStack(aCx); 221 if (JS::StealPendingExceptionStack(aCx, &exnStack)) { 222 JS::ErrorReportBuilder jsReport(aCx); 223 if (!jsReport.init(aCx, exnStack, 224 JS::ErrorReportBuilder::WithSideEffects)) { 225 ProcessorErrorDetails details; 226 details.mMessage.Assign(u"Unknown processor error"); 227 SendErrorToMainThread(aTrack, details); 228 // Set the exception and stack back to have it in the console with a stack 229 // trace. 230 JS::SetPendingExceptionStack(aCx, exnStack); 231 return; 232 } 233 234 ProcessorErrorDetails details; 235 details.mFilename.Assign(jsReport.report()->filename.c_str()); 236 xpc::ErrorReport::ErrorReportToMessageString(jsReport.report(), 237 details.mMessage); 238 details.mLineno = jsReport.report()->lineno; 239 details.mColno = jsReport.report()->column.oneOriginValue(); 240 MOZ_ASSERT(!jsReport.report()->isMuted); 241 242 SendErrorToMainThread(aTrack, details); 243 244 // Set the exception and stack back to have it in the console with a stack 245 // trace. 246 JS::SetPendingExceptionStack(aCx, exnStack); 247 } else { 248 NS_WARNING("No exception, but processor errored out?"); 249 } 250 } 251 252 void WorkletNodeEngine::ConstructProcessor( 253 AudioWorkletImpl* aWorkletImpl, const nsAString& aName, 254 NotNull<StructuredCloneHolder*> aSerializedOptions, 255 UniqueMessagePortId& aPortIdentifier, AudioNodeTrack* aTrack) { 256 MOZ_ASSERT(mInputs.mPorts.empty() && mOutputs.mPorts.empty()); 257 RefPtr<AudioWorkletGlobalScope> global = aWorkletImpl->GetGlobalScope(); 258 if (!global) { 259 // A global was previously used to register this kind of processor. If it 260 // no longer exists now, that is because the document is going away and so 261 // there is no need to send an error. 262 return; 263 } 264 AutoJSAPI api; 265 if (NS_WARN_IF(!api.Init(global))) { 266 SendProcessorError(aTrack, nullptr); 267 return; 268 } 269 mProcessorName = NS_ConvertUTF16toUTF8(aName); 270 JSContext* cx = api.cx(); 271 mProcessor.init(cx); 272 if (!global->ConstructProcessor(cx, aName, aSerializedOptions, 273 aPortIdentifier, &mProcessor) || 274 // mInputs and mOutputs outer arrays are fixed length and so much of the 275 // initialization need only be performed once (i.e. here). 276 NS_WARN_IF(!mInputs.mPorts.growBy(InputCount())) || 277 NS_WARN_IF(!mOutputs.mPorts.growBy(OutputCount()))) { 278 SendProcessorError(aTrack, cx); 279 return; 280 } 281 mGlobal = std::move(global); 282 mInputs.mJSArray.init(cx); 283 mOutputs.mJSArray.init(cx); 284 for (auto& port : mInputs.mPorts) { 285 port.mJSArray.init(cx); 286 } 287 for (auto& port : mOutputs.mPorts) { 288 port.mJSArray.init(cx); 289 } 290 JSObject* object = JS_NewPlainObject(cx); 291 if (NS_WARN_IF(!object)) { 292 SendProcessorError(aTrack, cx); 293 return; 294 } 295 296 mParameters.mJSObject.init(cx, object); 297 if (NS_WARN_IF(!mParameters.mFloat32Arrays.growBy(ParameterCount()))) { 298 SendProcessorError(aTrack, cx); 299 return; 300 } 301 for (size_t i = 0; i < mParamTimelines.Length(); i++) { 302 auto& float32ArraysRef = mParameters.mFloat32Arrays; 303 float32ArraysRef[i].init(cx); 304 JSObject* array = JS_NewFloat32Array(cx, WEBAUDIO_BLOCK_SIZE); 305 if (NS_WARN_IF(!array)) { 306 SendProcessorError(aTrack, cx); 307 return; 308 } 309 310 float32ArraysRef[i] = array; 311 if (NS_WARN_IF(!JS_DefineUCProperty( 312 cx, mParameters.mJSObject, mParamTimelines[i].mName.get(), 313 mParamTimelines[i].mName.Length(), float32ArraysRef[i], 314 JSPROP_ENUMERATE))) { 315 SendProcessorError(aTrack, cx); 316 return; 317 } 318 } 319 if (NS_WARN_IF(!JS_FreezeObject(cx, mParameters.mJSObject))) { 320 SendProcessorError(aTrack, cx); 321 return; 322 } 323 } 324 325 // Type T should support the length() and operator[]() methods and the return 326 // type of |operator[]() const| should support conversion to Handle<JSObject*>. 327 template <typename T> 328 static bool SetArrayElements(JSContext* aCx, const T& aElements, 329 JS::Handle<JSObject*> aArray) { 330 for (size_t i = 0; i < aElements.length(); ++i) { 331 if (!JS_DefineElement(aCx, aArray, i, aElements[i], JSPROP_ENUMERATE)) { 332 return false; 333 } 334 } 335 336 return true; 337 } 338 339 template <typename T> 340 static bool PrepareArray(JSContext* aCx, const T& aElements, 341 JS::MutableHandle<JSObject*> aArray) { 342 size_t length = aElements.length(); 343 if (aArray) { 344 // Attempt to reuse. 345 uint32_t oldLength; 346 if (JS::GetArrayLength(aCx, aArray, &oldLength) && 347 (oldLength == length || JS::SetArrayLength(aCx, aArray, length)) && 348 SetArrayElements(aCx, aElements, aArray)) { 349 return true; 350 } 351 // Script may have frozen the array. Try again with a new Array. 352 JS_ClearPendingException(aCx); 353 } 354 JSObject* array = JS::NewArrayObject(aCx, length); 355 if (NS_WARN_IF(!array)) { 356 return false; 357 } 358 aArray.set(array); 359 return SetArrayElements(aCx, aElements, aArray); 360 } 361 362 enum class ArrayElementInit { None, Zero }; 363 364 // Exactly when to create new Float32Array and Array objects is not specified. 365 // This approach aims to minimize garbage creation, while continuing to 366 // function after objects are modified by content. 367 // See https://github.com/WebAudio/web-audio-api/issues/1934 and 368 // https://github.com/WebAudio/web-audio-api/issues/1933 369 static bool PrepareBufferArrays(JSContext* aCx, Span<const AudioBlock> aBlocks, 370 WorkletNodeEngine::Ports* aPorts, 371 ArrayElementInit aInit) { 372 MOZ_ASSERT(aBlocks.Length() == aPorts->mPorts.length()); 373 for (size_t i = 0; i < aBlocks.Length(); ++i) { 374 size_t channelCount = aBlocks[i].ChannelCount(); 375 WorkletNodeEngine::Channels& portRef = aPorts->mPorts[i]; 376 377 auto& float32ArraysRef = portRef.mFloat32Arrays; 378 for (auto& channelRef : float32ArraysRef) { 379 size_t length = JS_GetTypedArrayLength(channelRef); 380 if (length != WEBAUDIO_BLOCK_SIZE) { 381 // Script has detached array buffers. Create new objects. 382 JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE); 383 if (NS_WARN_IF(!array)) { 384 return false; 385 } 386 channelRef = array; 387 } else if (aInit == ArrayElementInit::Zero) { 388 // Need only zero existing arrays as new arrays are already zeroed. 389 JS::AutoCheckCannotGC nogc; 390 bool isShared; 391 float* elementData = 392 JS_GetFloat32ArrayData(channelRef, &isShared, nogc); 393 MOZ_ASSERT(!isShared); // Was created as unshared 394 std::fill_n(elementData, WEBAUDIO_BLOCK_SIZE, 0.0f); 395 } 396 } 397 // Enlarge if necessary... 398 if (NS_WARN_IF(!float32ArraysRef.reserve(channelCount))) { 399 return false; 400 } 401 while (float32ArraysRef.length() < channelCount) { 402 JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE); 403 if (NS_WARN_IF(!array)) { 404 return false; 405 } 406 float32ArraysRef.infallibleEmplaceBack(aCx, array); 407 } 408 // ... or shrink if necessary. 409 float32ArraysRef.shrinkTo(channelCount); 410 411 if (NS_WARN_IF(!PrepareArray(aCx, float32ArraysRef, &portRef.mJSArray))) { 412 return false; 413 } 414 } 415 416 return !(NS_WARN_IF(!PrepareArray(aCx, aPorts->mPorts, &aPorts->mJSArray))); 417 } 418 419 // This runs JS script. MediaTrackGraph control messages, which would 420 // potentially destroy the WorkletNodeEngine and its AudioNodeTrack, cannot 421 // be triggered by script. They are not run from an nsIThread event loop and 422 // do not run until after ProcessBlocksOnPorts() has returned. 423 bool WorkletNodeEngine::CallProcess(AudioNodeTrack* aTrack, JSContext* aCx, 424 JS::Handle<JS::Value> aCallable) { 425 TRACE_COMMENT("AudioWorkletNodeEngine::CallProcess", mProcessorName.get()); 426 427 JS::RootedVector<JS::Value> argv(aCx); 428 if (NS_WARN_IF(!argv.resize(3))) { 429 return false; 430 } 431 argv[0].setObject(*mInputs.mJSArray); 432 argv[1].setObject(*mOutputs.mJSArray); 433 argv[2].setObject(*mParameters.mJSObject); 434 JS::Rooted<JS::Value> rval(aCx); 435 if (!JS::Call(aCx, mProcessor, aCallable, argv, &rval)) { 436 return false; 437 } 438 439 mProcessorIsActive = JS::ToBoolean(rval); 440 // Transitions of mProcessorIsActive to false do not trigger 441 // PlayingRefChangeHandler::RELEASE until silence is produced in the next 442 // block. This allows downstream engines receiving this non-silence block 443 // to take a reference to their nodes before this engine's node releases its 444 // down node references. 445 if (mProcessorIsActive && !mKeepEngineActive) { 446 mKeepEngineActive = true; 447 RefPtr<PlayingRefChangeHandler> refchanged = 448 new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::ADDREF); 449 aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); 450 } 451 return true; 452 } 453 454 void WorkletNodeEngine::ProduceSilence(AudioNodeTrack* aTrack, 455 Span<AudioBlock> aOutput) { 456 if (mKeepEngineActive) { 457 mKeepEngineActive = false; 458 aTrack->ScheduleCheckForInactive(); 459 RefPtr<PlayingRefChangeHandler> refchanged = 460 new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::RELEASE); 461 aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); 462 } 463 for (AudioBlock& output : aOutput) { 464 output.SetNull(WEBAUDIO_BLOCK_SIZE); 465 } 466 } 467 468 void WorkletNodeEngine::ProcessBlocksOnPorts(AudioNodeTrack* aTrack, 469 GraphTime aFrom, 470 Span<const AudioBlock> aInput, 471 Span<AudioBlock> aOutput, 472 bool* aFinished) { 473 MOZ_ASSERT(aInput.Length() == InputCount()); 474 MOZ_ASSERT(aOutput.Length() == OutputCount()); 475 TRACE("WorkletNodeEngine::ProcessBlocksOnPorts"); 476 477 bool isSilent = true; 478 if (mProcessor) { 479 if (mProcessorIsActive) { 480 isSilent = false; // call process() 481 } else { // [[active source]] is false. 482 // Call process() only if an input is actively processing. 483 for (const AudioBlock& input : aInput) { 484 if (!input.IsNull()) { 485 isSilent = false; 486 break; 487 } 488 } 489 } 490 } 491 if (isSilent) { 492 ProduceSilence(aTrack, aOutput); 493 return; 494 } 495 496 if (!mOutputChannelCount.IsEmpty()) { 497 MOZ_ASSERT(mOutputChannelCount.Length() == aOutput.Length()); 498 for (size_t o = 0; o < aOutput.Length(); ++o) { 499 aOutput[o].AllocateChannels(mOutputChannelCount[o]); 500 } 501 } else if (aInput.Length() == 1 && aOutput.Length() == 1) { 502 uint32_t channelCount = std::max(aInput[0].ChannelCount(), 1U); 503 aOutput[0].AllocateChannels(channelCount); 504 } else { 505 for (AudioBlock& output : aOutput) { 506 output.AllocateChannels(1); 507 } 508 } 509 510 AutoEntryScript aes(mGlobal, "Worklet Process"); 511 JSContext* cx = aes.cx(); 512 auto produceSilenceWithError = MakeScopeExit([this, aTrack, cx, &aOutput] { 513 SendProcessorError(aTrack, cx); 514 ProduceSilence(aTrack, aOutput); 515 }); 516 517 JS::Rooted<JS::Value> process(cx); 518 if (!JS_GetProperty(cx, mProcessor, "process", &process) || 519 !process.isObject() || !JS::IsCallable(&process.toObject()) || 520 !PrepareBufferArrays(cx, aInput, &mInputs, ArrayElementInit::None) || 521 !PrepareBufferArrays(cx, aOutput, &mOutputs, ArrayElementInit::Zero)) { 522 // process() not callable or OOM. 523 return; 524 } 525 526 // Copy input values to JS objects. 527 for (size_t i = 0; i < aInput.Length(); ++i) { 528 const AudioBlock& input = aInput[i]; 529 size_t channelCount = input.ChannelCount(); 530 if (channelCount == 0) { 531 // Null blocks have AUDIO_FORMAT_SILENCE. 532 // Don't call ChannelData<float>(). 533 continue; 534 } 535 float volume = input.mVolume; 536 const auto& channelData = input.ChannelData<float>(); 537 const auto& float32Arrays = mInputs.mPorts[i].mFloat32Arrays; 538 JS::AutoCheckCannotGC nogc; 539 for (size_t c = 0; c < channelCount; ++c) { 540 bool isShared; 541 float* dest = JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc); 542 MOZ_ASSERT(!isShared); // Was created as unshared 543 AudioBlockCopyChannelWithScale(channelData[c], volume, dest); 544 } 545 } 546 547 TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); 548 // Compute and copy parameter values to JS objects. 549 for (size_t i = 0; i < mParamTimelines.Length(); ++i) { 550 const auto& float32Arrays = mParameters.mFloat32Arrays[i]; 551 size_t length = JS_GetTypedArrayLength(float32Arrays); 552 553 // If the Float32Array that is supposed to hold the values for a particular 554 // AudioParam has been detached, error out. This is being worked on in 555 // https://github.com/WebAudio/web-audio-api/issues/1933 and 556 // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486 557 if (length != WEBAUDIO_BLOCK_SIZE) { 558 return; 559 } 560 JS::AutoCheckCannotGC nogc; 561 bool isShared; 562 float* dest = JS_GetFloat32ArrayData(float32Arrays, &isShared, nogc); 563 MOZ_ASSERT(!isShared); // Was created as unshared 564 565 size_t frames = 566 mParamTimelines[i].mTimeline.HasSimpleValue() ? 1 : WEBAUDIO_BLOCK_SIZE; 567 mParamTimelines[i].mTimeline.GetValuesAtTime(tick, dest, frames); 568 // https://bugzilla.mozilla.org/show_bug.cgi?id=1616599 569 if (frames == 1) { 570 std::fill_n(dest + 1, WEBAUDIO_BLOCK_SIZE - 1, dest[0]); 571 } 572 } 573 574 if (!CallProcess(aTrack, cx, process)) { 575 // An exception occurred. 576 /** 577 * https://webaudio.github.io/web-audio-api/#dom-audioworkletnode-onprocessorerror 578 * Note that once an exception is thrown, the processor will output silence 579 * throughout its lifetime. 580 */ 581 return; 582 } 583 584 // Copy output values from JS objects. 585 for (size_t o = 0; o < aOutput.Length(); ++o) { 586 AudioBlock* output = &aOutput[o]; 587 size_t channelCount = output->ChannelCount(); 588 const auto& float32Arrays = mOutputs.mPorts[o].mFloat32Arrays; 589 for (size_t c = 0; c < channelCount; ++c) { 590 size_t length = JS_GetTypedArrayLength(float32Arrays[c]); 591 if (length != WEBAUDIO_BLOCK_SIZE) { 592 // ArrayBuffer has been detached. Behavior is unspecified. 593 // https://github.com/WebAudio/web-audio-api/issues/1933 and 594 // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486 595 return; 596 } 597 JS::AutoCheckCannotGC nogc; 598 bool isShared; 599 const float* src = 600 JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc); 601 MOZ_ASSERT(!isShared); // Was created as unshared 602 PodCopy(output->ChannelFloatsForWrite(c), src, WEBAUDIO_BLOCK_SIZE); 603 } 604 } 605 606 produceSilenceWithError.release(); // have output and no error 607 } 608 609 AudioWorkletNode::AudioWorkletNode(AudioContext* aAudioContext, 610 const nsAString& aName, 611 const AudioWorkletNodeOptions& aOptions) 612 : AudioNode(aAudioContext, 2, ChannelCountMode::Max, 613 ChannelInterpretation::Speakers), 614 mNodeName(aName), 615 mInputCount(aOptions.mNumberOfInputs), 616 mOutputCount(aOptions.mNumberOfOutputs) {} 617 618 void AudioWorkletNode::InitializeParameters( 619 nsTArray<NamedAudioParamTimeline>* aParamTimelines, ErrorResult& aRv) { 620 MOZ_ASSERT(!mParameters, "Only initialize the `parameters` attribute once."); 621 MOZ_ASSERT(aParamTimelines); 622 623 AudioContext* context = Context(); 624 const AudioParamDescriptorMap* parameterDescriptors = 625 context->GetParamMapForWorkletName(mNodeName); 626 MOZ_ASSERT(parameterDescriptors); 627 628 size_t audioParamIndex = 0; 629 aParamTimelines->SetCapacity(parameterDescriptors->Length()); 630 631 for (size_t i = 0; i < parameterDescriptors->Length(); i++) { 632 auto& paramEntry = (*parameterDescriptors)[i]; 633 CreateAudioParam(audioParamIndex++, paramEntry.mName, 634 paramEntry.mDefaultValue, paramEntry.mMinValue, 635 paramEntry.mMaxValue); 636 aParamTimelines->AppendElement(paramEntry); 637 } 638 } 639 640 void AudioWorkletNode::SendParameterData( 641 const Optional<Record<nsString, double>>& aParameterData) { 642 MOZ_ASSERT(mTrack, "This method only works if the track has been created."); 643 nsAutoString name; 644 if (aParameterData.WasPassed()) { 645 const auto& paramData = aParameterData.Value(); 646 for (const auto& paramDataEntry : paramData.Entries()) { 647 for (auto& audioParam : mParams) { 648 audioParam->GetName(name); 649 if (paramDataEntry.mKey.Equals(name)) { 650 audioParam->SetInitialValue(paramDataEntry.mValue); 651 } 652 } 653 } 654 } 655 } 656 657 /* static */ 658 already_AddRefed<AudioWorkletNode> AudioWorkletNode::Constructor( 659 const GlobalObject& aGlobal, AudioContext& aAudioContext, 660 const nsAString& aName, const AudioWorkletNodeOptions& aOptions, 661 ErrorResult& aRv) { 662 TRACE_COMMENT("AudioWorkletNode::Constructor", "%s", 663 NS_ConvertUTF16toUTF8(aName).get()); 664 /** 665 * 1. If nodeName does not exist as a key in the BaseAudioContext’s node 666 * name to parameter descriptor map, throw a InvalidStateError exception 667 * and abort these steps. 668 */ 669 const AudioParamDescriptorMap* parameterDescriptors = 670 aAudioContext.GetParamMapForWorkletName(aName); 671 if (!parameterDescriptors) { 672 // Not using nsPrintfCString in case aName has embedded nulls. 673 aRv.ThrowInvalidStateError("Unknown AudioWorklet name '"_ns + 674 NS_ConvertUTF16toUTF8(aName) + "'"_ns); 675 return nullptr; 676 } 677 678 // See https://github.com/WebAudio/web-audio-api/issues/2074 for ordering. 679 RefPtr<AudioWorkletNode> audioWorkletNode = 680 new AudioWorkletNode(&aAudioContext, aName, aOptions); 681 audioWorkletNode->Initialize(aOptions, aRv); 682 if (NS_WARN_IF(aRv.Failed())) { 683 return nullptr; 684 } 685 686 /** 687 * 3. Configure input, output and output channels of node with options. 688 */ 689 if (aOptions.mNumberOfInputs == 0 && aOptions.mNumberOfOutputs == 0) { 690 aRv.ThrowNotSupportedError( 691 "Must have nonzero numbers of inputs or outputs"); 692 return nullptr; 693 } 694 695 if (aOptions.mOutputChannelCount.WasPassed()) { 696 /** 697 * 1. If any value in outputChannelCount is zero or greater than the 698 * implementation’s maximum number of channels, throw a 699 * NotSupportedError and abort the remaining steps. 700 */ 701 for (uint32_t channelCount : aOptions.mOutputChannelCount.Value()) { 702 if (channelCount == 0 || channelCount > WebAudioUtils::MaxChannelCount) { 703 aRv.ThrowNotSupportedError( 704 nsPrintfCString("Channel count (%u) must be in the range [1, max " 705 "supported channel count]", 706 channelCount)); 707 return nullptr; 708 } 709 } 710 /** 711 * 2. If the length of outputChannelCount does not equal numberOfOutputs, 712 * throw an IndexSizeError and abort the remaining steps. 713 */ 714 if (aOptions.mOutputChannelCount.Value().Length() != 715 aOptions.mNumberOfOutputs) { 716 aRv.ThrowIndexSizeError( 717 nsPrintfCString("Length of outputChannelCount (%zu) does not match " 718 "numberOfOutputs (%u)", 719 aOptions.mOutputChannelCount.Value().Length(), 720 aOptions.mNumberOfOutputs)); 721 return nullptr; 722 } 723 } 724 // MTG does not support more than UINT16_MAX inputs or outputs. 725 if (aOptions.mNumberOfInputs > UINT16_MAX) { 726 aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfInputs"); 727 return nullptr; 728 } 729 if (aOptions.mNumberOfOutputs > UINT16_MAX) { 730 aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfOutputs"); 731 return nullptr; 732 } 733 734 /** 735 * 4. Let messageChannel be a new MessageChannel. 736 */ 737 RefPtr<MessageChannel> messageChannel = 738 MessageChannel::Constructor(aGlobal, aRv); 739 if (NS_WARN_IF(aRv.Failed())) { 740 return nullptr; 741 } 742 /* 5. Let nodePort be the value of messageChannel’s port1 attribute. 743 * 6. Let processorPortOnThisSide be the value of messageChannel’s port2 744 * attribute. 745 * 7. Let serializedProcessorPort be the result of 746 * StructuredSerializeWithTransfer(processorPortOnThisSide, 747 * « processorPortOnThisSide »). 748 */ 749 UniqueMessagePortId processorPortId; 750 messageChannel->Port2()->CloneAndDisentangle(processorPortId); 751 /** 752 * 8. Convert options dictionary to optionsObject. 753 */ 754 JSContext* cx = aGlobal.Context(); 755 JS::Rooted<JS::Value> optionsVal(cx); 756 if (NS_WARN_IF(!ToJSValue(cx, aOptions, &optionsVal))) { 757 aRv.NoteJSContextException(cx); 758 return nullptr; 759 } 760 761 /** 762 * 9. Let serializedOptions be the result of 763 * StructuredSerialize(optionsObject). 764 */ 765 766 // This context and the worklet are part of the same agent cluster and they 767 // can share memory. 768 JS::CloneDataPolicy cloneDataPolicy; 769 cloneDataPolicy.allowIntraClusterClonableSharedObjects(); 770 cloneDataPolicy.allowSharedMemoryObjects(); 771 772 // StructuredCloneHolder does not have a move constructor. Instead allocate 773 // memory so that the pointer can be passed to the rendering thread. 774 UniquePtr<StructuredCloneHolder> serializedOptions = 775 MakeUnique<StructuredCloneHolder>( 776 StructuredCloneHolder::CloningSupported, 777 StructuredCloneHolder::TransferringNotSupported, 778 JS::StructuredCloneScope::SameProcess); 779 serializedOptions->Write(cx, optionsVal, JS::UndefinedHandleValue, 780 cloneDataPolicy, aRv); 781 if (NS_WARN_IF(aRv.Failed())) { 782 return nullptr; 783 } 784 /** 785 * 10. Set node’s port to nodePort. 786 */ 787 audioWorkletNode->mPort = messageChannel->Port1(); 788 789 /** 790 * 11. Let parameterDescriptors be the result of retrieval of nodeName from 791 * node name to parameter descriptor map. 792 */ 793 nsTArray<NamedAudioParamTimeline> paramTimelines; 794 audioWorkletNode->InitializeParameters(¶mTimelines, aRv); 795 if (NS_WARN_IF(aRv.Failed())) { 796 return nullptr; 797 } 798 799 auto engine = new WorkletNodeEngine( 800 audioWorkletNode, aAudioContext.Destination(), std::move(paramTimelines), 801 aOptions.mOutputChannelCount); 802 audioWorkletNode->mTrack = AudioNodeTrack::Create( 803 &aAudioContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, 804 aAudioContext.Graph()); 805 806 audioWorkletNode->SendParameterData(aOptions.mParameterData); 807 808 /** 809 * 12. Queue a control message to invoke the constructor of the 810 * corresponding AudioWorkletProcessor with the processor construction 811 * data that consists of: nodeName, node, serializedOptions, and 812 * serializedProcessorPort. 813 */ 814 Worklet* worklet = aAudioContext.GetAudioWorklet(aRv); 815 MOZ_ASSERT(worklet, "Worklet already existed and so getter shouldn't fail."); 816 auto workletImpl = static_cast<AudioWorkletImpl*>(worklet->Impl()); 817 audioWorkletNode->mTrack->SendRunnable(NS_NewRunnableFunction( 818 "WorkletNodeEngine::ConstructProcessor", 819 // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. 820 // See bug 1535398. 821 // 822 // Note that clang and gcc have mutually incompatible rules about whether 823 // attributes should come before or after the `mutable` keyword here, so 824 // use a compatibility hack until we can switch to the standardized 825 // [[attr]] syntax (bug 1627007). 826 #ifdef __clang__ 827 # define AND_MUTABLE(macro) macro mutable 828 #else 829 # define AND_MUTABLE(macro) mutable macro 830 #endif 831 [track = audioWorkletNode->mTrack, 832 workletImpl = RefPtr<AudioWorkletImpl>(workletImpl), 833 name = nsString(aName), options = std::move(serializedOptions), 834 portId = std::move(processorPortId)]() 835 AND_MUTABLE(MOZ_CAN_RUN_SCRIPT_BOUNDARY) { 836 auto engine = static_cast<WorkletNodeEngine*>(track->Engine()); 837 engine->ConstructProcessor( 838 workletImpl, name, WrapNotNull(options.get()), portId, track); 839 })); 840 #undef AND_MUTABLE 841 842 // [[active source]] is initially true and so at least the first process() 843 // call will not be skipped when there are no active inputs. 844 audioWorkletNode->MarkActive(); 845 846 return audioWorkletNode.forget(); 847 } 848 849 AudioParamMap* AudioWorkletNode::GetParameters(ErrorResult& aRv) { 850 if (!mParameters) { 851 RefPtr<AudioParamMap> parameters = new AudioParamMap(this); 852 nsAutoString name; 853 for (const auto& audioParam : mParams) { 854 audioParam->GetName(name); 855 AudioParamMap_Binding::MaplikeHelpers::Set(parameters, name, *audioParam, 856 aRv); 857 if (NS_WARN_IF(aRv.Failed())) { 858 return nullptr; 859 } 860 } 861 mParameters = std::move(parameters); 862 } 863 return mParameters.get(); 864 } 865 866 void AudioWorkletNode::DispatchProcessorErrorEvent( 867 const ProcessorErrorDetails& aDetails) { 868 TRACE("AudioWorkletNode::DispatchProcessorErrorEvent"); 869 if (HasListenersFor(nsGkAtoms::onprocessorerror)) { 870 RootedDictionary<ErrorEventInit> init(RootingCx()); 871 init.mMessage = aDetails.mMessage; 872 init.mFilename = aDetails.mFilename; 873 init.mLineno = aDetails.mLineno; 874 init.mColno = aDetails.mColno; 875 RefPtr<ErrorEvent> errorEvent = 876 ErrorEvent::Constructor(this, u"processorerror"_ns, init); 877 MOZ_ASSERT(errorEvent); 878 DispatchTrustedEvent(errorEvent); 879 } 880 } 881 882 JSObject* AudioWorkletNode::WrapObject(JSContext* aCx, 883 JS::Handle<JSObject*> aGivenProto) { 884 return AudioWorkletNode_Binding::Wrap(aCx, this, aGivenProto); 885 } 886 887 size_t AudioWorkletNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { 888 size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); 889 return amount; 890 } 891 892 size_t AudioWorkletNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { 893 return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); 894 } 895 896 } // namespace mozilla::dom