AudioData.cpp (28774B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 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 http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/AudioData.h" 8 9 #include <utility> 10 11 #include "AudioSampleFormat.h" 12 #include "WebCodecsUtils.h" 13 #include "js/StructuredClone.h" 14 #include "mozilla/Assertions.h" 15 #include "mozilla/CheckedInt.h" 16 #include "mozilla/Logging.h" 17 #include "mozilla/Maybe.h" 18 #include "mozilla/Result.h" 19 #include "mozilla/dom/AudioDataBinding.h" 20 #include "mozilla/dom/BufferSourceBinding.h" 21 #include "mozilla/dom/Promise.h" 22 #include "mozilla/dom/StructuredCloneTags.h" 23 #include "nsFmtString.h" 24 #include "nsStringFwd.h" 25 #include "nsTHashSet.h" 26 27 extern mozilla::LazyLogModule gWebCodecsLog; 28 29 namespace mozilla::dom { 30 31 #define LOGD(fmt, ...) \ 32 MOZ_LOG_FMT(gWebCodecsLog, LogLevel::Debug, fmt, ##__VA_ARGS__) 33 34 #define LOGE(fmt, ...) \ 35 MOZ_LOG_FMT(gWebCodecsLog, LogLevel::Error, fmt, ##__VA_ARGS__) 36 37 [[nodiscard]] Result<Ok, nsCString> LogAndReturnErr(const char* aLiteral) { 38 MOZ_LOG(gWebCodecsLog, LogLevel::Debug, ("%s", aLiteral)); 39 return Err(nsCString(aLiteral)); 40 } 41 42 template <typename... Args> 43 [[nodiscard]] Result<Ok, nsCString> LogAndReturnErr( 44 fmt::format_string<Args...> aFmt, Args&&... aArgs) { 45 nsAutoCStringN<100> str; 46 str.AppendVfmt(aFmt, fmt::make_format_args(aArgs...)); 47 MOZ_LOG(gWebCodecsLog, LogLevel::Debug, ("%s", str.get())); 48 return Err(str); 49 } 50 51 // Only needed for refcounted objects. 52 // 53 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AudioData) 54 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioData) 55 tmp->CloseIfNeeded(); 56 NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) 57 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER 58 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioData) 60 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) 61 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 62 63 NS_IMPL_CYCLE_COLLECTING_ADDREF(AudioData) 64 // AudioData should be released as soon as its refcount drops to zero, 65 // without waiting for async deletion by the cycle collector, since it may hold 66 // a large-size PCM buffer. 67 NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(AudioData, CloseIfNeeded()) 68 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioData) 69 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 70 NS_INTERFACE_MAP_ENTRY(nsISupports) 71 NS_INTERFACE_MAP_END 72 73 /* 74 * W3C Webcodecs AudioData implementation 75 */ 76 77 AudioData::AudioData(nsIGlobalObject* aParent, 78 const AudioDataSerializedData& aData) 79 : mParent(aParent), 80 mTimestamp(aData.mTimestamp), 81 mNumberOfChannels(aData.mNumberOfChannels), 82 mNumberOfFrames(aData.mNumberOfFrames), 83 mSampleRate(aData.mSampleRate), 84 mAudioSampleFormat(aData.mAudioSampleFormat), 85 // The resource is not copied, but referenced 86 mResource(aData.mResource) { 87 MOZ_ASSERT(mParent); 88 MOZ_ASSERT(mResource, 89 "Resource should always be present then receiving a transfer."); 90 } 91 92 AudioData::AudioData(const AudioData& aOther) 93 : mParent(aOther.mParent), 94 mTimestamp(aOther.mTimestamp), 95 mNumberOfChannels(aOther.mNumberOfChannels), 96 mNumberOfFrames(aOther.mNumberOfFrames), 97 mSampleRate(aOther.mSampleRate), 98 mAudioSampleFormat(aOther.mAudioSampleFormat), 99 // The resource is not copied, but referenced 100 mResource(aOther.mResource) { 101 MOZ_ASSERT(mParent); 102 } 103 104 Result<already_AddRefed<AudioDataResource>, nsresult> 105 AudioDataResource::Construct(const OwningAllowSharedBufferSource& aInit) { 106 FallibleTArray<uint8_t> copied; 107 uint8_t* rv = ProcessTypedArraysFixed( 108 aInit, [&](const Span<uint8_t>& aData) -> uint8_t* { 109 return copied.AppendElements(aData.Elements(), aData.Length(), 110 fallible); 111 }); 112 if (!rv) { 113 LOGE("AudioDataResource::Ctor: OOM"); 114 return Err(NS_ERROR_OUT_OF_MEMORY); 115 } 116 return MakeAndAddRef<AudioDataResource>(std::move(copied)); 117 } 118 119 AudioData::AudioData( 120 nsIGlobalObject* aParent, 121 already_AddRefed<mozilla::dom::AudioDataResource> aResource, 122 const AudioDataInit& aInit) 123 : mParent(aParent), 124 mTimestamp(aInit.mTimestamp), 125 mNumberOfChannels(aInit.mNumberOfChannels), 126 mNumberOfFrames(aInit.mNumberOfFrames), 127 mSampleRate(aInit.mSampleRate), 128 mAudioSampleFormat(Some(aInit.mFormat)), 129 mResource(std::move(aResource)) { 130 MOZ_ASSERT(mParent); 131 } 132 133 AudioData::AudioData( 134 nsIGlobalObject* aParent, 135 already_AddRefed<mozilla::dom::AudioDataResource> aResource, 136 int64_t aTimestamp, uint32_t aNumberOfChannels, uint32_t aNumberOfFrames, 137 float aSampleRate, AudioSampleFormat aAudioSampleFormat) 138 : mParent(aParent), 139 mTimestamp(aTimestamp), 140 mNumberOfChannels(aNumberOfChannels), 141 mNumberOfFrames(aNumberOfFrames), 142 mSampleRate(aSampleRate), 143 mAudioSampleFormat(Some(aAudioSampleFormat)), 144 mResource(aResource) { 145 MOZ_ASSERT(mParent); 146 } 147 148 nsIGlobalObject* AudioData::GetParentObject() const { 149 AssertIsOnOwningThread(); 150 151 return mParent.get(); 152 } 153 154 JSObject* AudioData::WrapObject(JSContext* aCx, 155 JS::Handle<JSObject*> aGivenProto) { 156 AssertIsOnOwningThread(); 157 158 return AudioData_Binding::Wrap(aCx, this, aGivenProto); 159 } 160 161 Result<Ok, nsCString> IsValidAudioDataInit(const AudioDataInit& aInit) { 162 // The sample rate is an uint32_t within Gecko 163 uint32_t integerSampleRate = SaturatingCast<uint32_t>(aInit.mSampleRate); 164 if (integerSampleRate == 0) { 165 return LogAndReturnErr("sampleRate must be positive"); 166 } 167 if (aInit.mNumberOfFrames == 0) { 168 return LogAndReturnErr("mNumberOfFrames must be positive"); 169 } 170 if (aInit.mNumberOfChannels == 0) { 171 return LogAndReturnErr("mNumberOfChannels must be positive"); 172 } 173 174 CheckedInt<uint64_t> bytesNeeded = aInit.mNumberOfFrames; 175 bytesNeeded *= aInit.mNumberOfChannels; 176 bytesNeeded *= BytesPerSamples(aInit.mFormat); 177 178 if (!bytesNeeded.isValid()) { 179 return LogAndReturnErr( 180 FMT_STRING("Overflow when computing the number of bytes needed to hold " 181 "audio samples ({}*{}*{})"), 182 aInit.mNumberOfFrames, aInit.mNumberOfChannels, 183 BytesPerSamples(aInit.mFormat)); 184 } 185 186 uint64_t arraySizeBytes = ProcessTypedArraysFixed( 187 aInit.mData, [&](const Span<uint8_t>& aData) -> uint64_t { 188 return aData.LengthBytes(); 189 }); 190 if (arraySizeBytes < bytesNeeded.value()) { 191 return LogAndReturnErr( 192 FMT_STRING("Array of size {} not big enough, should be at least {}"), 193 arraySizeBytes, bytesNeeded.value()); 194 } 195 return Ok(); 196 } 197 198 /* static */ 199 already_AddRefed<AudioData> AudioData::Constructor(const GlobalObject& aGlobal, 200 const AudioDataInit& aInit, 201 ErrorResult& aRv) { 202 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 203 LOGD("[{}] AudioData(fmt: {}, rate: {}, ch: {}, ts: {})", 204 fmt::ptr(global.get()), GetEnumString(aInit.mFormat).get(), 205 aInit.mSampleRate, aInit.mNumberOfChannels, aInit.mTimestamp); 206 if (!global) { 207 LOGE("Global unavailable"); 208 aRv.Throw(NS_ERROR_FAILURE); 209 return nullptr; 210 } 211 nsString errorMessage; 212 auto rv = IsValidAudioDataInit(aInit); 213 if (rv.isErr()) { 214 LOGD("AudioData::Constructor failure (IsValidAudioDataInit)"); 215 aRv.ThrowTypeError(rv.inspectErr()); 216 return nullptr; 217 } 218 219 nsTHashSet<const JSObject*> transferSet; 220 for (const auto& buffer : aInit.mTransfer) { 221 if (transferSet.Contains(buffer.Obj())) { 222 // 9.2.2.2. If init.transfer contains more than one reference to 223 // the same ArrayBuffer, then throw a DataCloneError DOMException. 224 LOGE("AudioData Constructor -- duplicate transferred ArrayBuffer"); 225 aRv.ThrowDataCloneError( 226 "Transfer contains duplicate ArrayBuffer objects"); 227 return nullptr; 228 } 229 transferSet.Insert(buffer.Obj()); 230 } 231 232 for (const auto& buffer : aInit.mTransfer) { 233 if (JS::IsDetachedArrayBufferObject(buffer.Obj())) { 234 // 9.2.2.3.1. If [[Detached]] internal slot is true, then 235 // throw a DataCloneError DOMException. 236 LOGE("AudioData Constructor -- detached transferred ArrayBuffer"); 237 aRv.ThrowDataCloneError("Transfer contains detached ArrayBuffer objects"); 238 return nullptr; 239 } 240 } 241 242 // 9.2.2.4.7. If init.transfer contains an ArrayBuffer referenced by init.data 243 // the User Agent MAY choose to: 244 // 9.2.2.4.7.1. Let resource be a new media resource referencing sample data 245 // in data. 246 size_t transferLen = 0; 247 size_t transferOffset = 0; 248 Maybe<JS::Rooted<JSObject*>> transferView; 249 Maybe<JS::Rooted<JSObject*>> transferBuffer; 250 const auto& data = aInit.mData; 251 if (data.IsArrayBuffer()) { 252 transferBuffer.emplace(aGlobal.Context(), data.GetAsArrayBuffer().Obj()); 253 if (transferSet.Contains(*transferBuffer)) { 254 transferLen = JS::GetArrayBufferByteLength(*transferBuffer); 255 } 256 } else if (data.IsArrayBufferView()) { 257 // store rooted ArrayBufferView object in outer variable to prolong 258 // its lifetime (for JS::Rooted's tracking). 259 transferView.emplace(aGlobal.Context(), data.GetAsArrayBufferView().Obj()); 260 bool isShared; 261 transferBuffer.emplace(aGlobal.Context(), 262 JS_GetArrayBufferViewBuffer( 263 aGlobal.Context(), *transferView, &isShared)); 264 if (transferSet.Contains(*transferBuffer)) { 265 transferOffset = JS_GetArrayBufferViewByteOffset(*transferView); 266 transferLen = JS_GetArrayBufferViewByteLength(*transferView); 267 } 268 } 269 UniquePtr<uint8_t[], JS::FreePolicy> transferData; 270 if (transferLen) { 271 void* bufferContents = 272 JS::StealArrayBufferContents(aGlobal.Context(), *transferBuffer); 273 transferData = UniquePtr<uint8_t[], JS::FreePolicy>( 274 static_cast<uint8_t*>(bufferContents)); 275 } 276 277 auto resource = 278 transferData ? MakeAndAddRef<AudioDataResource>( 279 std::move(transferData), transferOffset, transferLen) 280 : AudioDataResource::Construct(data); 281 if (resource.isErr()) { 282 LOGD("AudioData::Constructor failure (OOM)"); 283 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 284 return nullptr; 285 } 286 287 // 9.2.2.3. For each transferable in init.transfer: 288 // 9.2.2.3.1. Perform DetachArrayBuffer on transferable 289 for (const auto& buffer : aInit.mTransfer) { 290 JS::Rooted<JSObject*> obj(aGlobal.Context(), buffer.Obj()); 291 JS::DetachArrayBuffer(aGlobal.Context(), obj); 292 } 293 return MakeAndAddRef<AudioData>(global, resource.unwrap(), aInit); 294 } 295 296 // https://w3c.github.io/webcodecs/#dom-audiodata-format 297 Nullable<mozilla::dom::AudioSampleFormat> AudioData::GetFormat() const { 298 AssertIsOnOwningThread(); 299 return MaybeToNullable(mAudioSampleFormat); 300 } 301 302 // https://w3c.github.io/webcodecs/#dom-audiodata-samplerate 303 float AudioData::SampleRate() const { 304 AssertIsOnOwningThread(); 305 return mSampleRate; 306 } 307 308 // https://w3c.github.io/webcodecs/#dom-audiodata-numberofframes 309 uint32_t AudioData::NumberOfFrames() const { 310 AssertIsOnOwningThread(); 311 return mNumberOfFrames; 312 } 313 314 // https://w3c.github.io/webcodecs/#dom-audiodata-numberofchannels 315 uint32_t AudioData::NumberOfChannels() const { 316 AssertIsOnOwningThread(); 317 return mNumberOfChannels; 318 } 319 320 // https://w3c.github.io/webcodecs/#dom-audiodata-duration 321 uint64_t AudioData::Duration() const { 322 AssertIsOnOwningThread(); 323 if (!mNumberOfFrames) { 324 return 0; 325 } 326 // The spec isn't clear in which direction to convert to integer. 327 // https://github.com/w3c/webcodecs/issues/726 328 return static_cast<uint64_t>( 329 static_cast<float>(USECS_PER_S * mNumberOfFrames) / mSampleRate); 330 } 331 332 // https://w3c.github.io/webcodecs/#dom-audiodata-timestamp 333 int64_t AudioData::Timestamp() const { 334 AssertIsOnOwningThread(); 335 return mTimestamp; 336 } 337 338 struct CopyToSpec { 339 CopyToSpec(uint32_t aFrameCount, uint32_t aFrameOffset, uint32_t mPlaneIndex, 340 AudioSampleFormat aFormat) 341 : mFrameCount(aFrameCount), 342 mFrameOffset(aFrameOffset), 343 mPlaneIndex(mPlaneIndex), 344 mFormat(aFormat) {} 345 346 const uint32_t mFrameCount; 347 const uint32_t mFrameOffset; 348 const uint32_t mPlaneIndex; 349 const AudioSampleFormat mFormat; 350 }; 351 352 bool IsInterleaved(const AudioSampleFormat& aFormat) { 353 switch (aFormat) { 354 case AudioSampleFormat::U8: 355 case AudioSampleFormat::S16: 356 case AudioSampleFormat::S32: 357 case AudioSampleFormat::F32: 358 return true; 359 case AudioSampleFormat::U8_planar: 360 case AudioSampleFormat::S16_planar: 361 case AudioSampleFormat::S32_planar: 362 case AudioSampleFormat::F32_planar: 363 return false; 364 }; 365 MOZ_ASSERT_UNREACHABLE("Invalid enum value"); 366 return false; 367 } 368 369 size_t AudioData::ComputeCopyElementCount( 370 const AudioDataCopyToOptions& aOptions, ErrorResult& aRv) { 371 // https://w3c.github.io/webcodecs/#compute-copy-element-count 372 // 1, 2 373 auto destFormat = mAudioSampleFormat; 374 if (aOptions.mFormat.WasPassed()) { 375 destFormat = OptionalToMaybe(aOptions.mFormat); 376 } 377 // 3, 4 378 MOZ_ASSERT(destFormat.isSome()); 379 if (IsInterleaved(destFormat.value())) { 380 if (aOptions.mPlaneIndex > 0) { 381 auto msg = "Interleaved format, but plane index > 0"_ns; 382 LOGD("{}", msg.get()); 383 aRv.ThrowRangeError(msg); 384 return 0; 385 } 386 } else { 387 if (aOptions.mPlaneIndex >= mNumberOfChannels) { 388 auto msg = nsFmtCString(FMT_STRING("Plane index {} greater or equal " 389 "than the number of channels {}"), 390 aOptions.mPlaneIndex, mNumberOfChannels); 391 LOGD("{}", msg.get()); 392 aRv.ThrowRangeError(msg); 393 return 0; 394 } 395 } 396 // 5 -- conversion between all formats supported 397 // 6 -- all planes have the same number of frames, always 398 uint64_t frameCount = mNumberOfFrames; 399 // 7 400 if (aOptions.mFrameOffset >= frameCount) { 401 auto msg = nsFmtCString( 402 FMT_STRING("Frame offset of {} greater or equal than frame count {}"), 403 aOptions.mFrameOffset, frameCount); 404 LOGD("{}", msg.get()); 405 aRv.ThrowRangeError(msg); 406 return 0; 407 } 408 // 8, 9 409 uint64_t copyFrameCount = frameCount - aOptions.mFrameOffset; 410 if (aOptions.mFrameCount.WasPassed()) { 411 if (aOptions.mFrameCount.Value() > copyFrameCount) { 412 auto msg = nsFmtCString(FMT_STRING("Passed copy frame count of {} " 413 "greater than available source frames " 414 "for copy of {}"), 415 aOptions.mFrameCount.Value(), copyFrameCount); 416 LOGD("{}", msg.get()); 417 aRv.ThrowRangeError(msg); 418 return 0; 419 } 420 copyFrameCount = aOptions.mFrameCount.Value(); 421 } 422 // 10, 11 423 uint64_t elementCount = copyFrameCount; 424 if (IsInterleaved(destFormat.value())) { 425 elementCount *= mNumberOfChannels; 426 } 427 return elementCount; 428 } 429 430 // https://w3c.github.io/webcodecs/#dom-audiodata-allocationsize 431 // This method returns an int, that can be zero in case of success or error. 432 // Caller should check aRv to determine success or error. 433 uint32_t AudioData::AllocationSize(const AudioDataCopyToOptions& aOptions, 434 ErrorResult& aRv) { 435 AssertIsOnOwningThread(); 436 if (!mResource) { 437 auto msg = "allocationSize called on detached AudioData"_ns; 438 LOGD("{}", msg.get()); 439 aRv.ThrowInvalidStateError(msg); 440 return 0; 441 } 442 size_t copyElementCount = ComputeCopyElementCount(aOptions, aRv); 443 if (aRv.Failed()) { 444 LOGD("AudioData::AllocationSize failure"); 445 // ComputeCopyElementCount has set the exception type. 446 return 0; 447 } 448 Maybe<mozilla::dom::AudioSampleFormat> destFormat = mAudioSampleFormat; 449 if (aOptions.mFormat.WasPassed()) { 450 destFormat = OptionalToMaybe(aOptions.mFormat); 451 } 452 if (destFormat.isNothing()) { 453 auto msg = "AudioData has an unknown format"_ns; 454 LOGD("{}", msg.get()); 455 // See https://github.com/w3c/webcodecs/issues/727 -- it isn't clear yet 456 // what to do here 457 aRv.ThrowRangeError(msg); 458 return 0; 459 } 460 CheckedInt<size_t> bytesPerSample = BytesPerSamples(destFormat.ref()); 461 462 auto res = bytesPerSample * copyElementCount; 463 if (res.isValid()) { 464 return res.value(); 465 } 466 aRv.ThrowRangeError("Allocation size too large"); 467 return 0; 468 } 469 470 template <typename S, typename D> 471 void CopySamples(Span<S> aSource, Span<D> aDest, uint32_t aSourceChannelCount, 472 const AudioSampleFormat aSourceFormat, 473 const CopyToSpec& aCopyToSpec) { 474 if (IsInterleaved(aSourceFormat) && IsInterleaved(aCopyToSpec.mFormat)) { 475 MOZ_ASSERT(aCopyToSpec.mPlaneIndex == 0); 476 MOZ_ASSERT(aDest.Length() >= aCopyToSpec.mFrameCount); 477 MOZ_ASSERT(aSource.Length() - aCopyToSpec.mFrameOffset >= 478 aCopyToSpec.mFrameCount); 479 // This turns into a regular memcpy if the types are in fact equal 480 ConvertAudioSamples(aSource.data() + aCopyToSpec.mFrameOffset, aDest.data(), 481 aCopyToSpec.mFrameCount * aSourceChannelCount); 482 return; 483 } 484 if (IsInterleaved(aSourceFormat) && !IsInterleaved(aCopyToSpec.mFormat)) { 485 DebugOnly<size_t> sourceFrameCount = aSource.Length() / aSourceChannelCount; 486 MOZ_ASSERT(aDest.Length() >= aCopyToSpec.mFrameCount); 487 MOZ_ASSERT(aSource.Length() - aCopyToSpec.mFrameOffset >= 488 aCopyToSpec.mFrameCount); 489 // Interleaved to planar -- only copy samples of the correct channel to the 490 // destination 491 size_t readIndex = aCopyToSpec.mFrameOffset * aSourceChannelCount + 492 aCopyToSpec.mPlaneIndex; 493 for (size_t i = 0; i < aCopyToSpec.mFrameCount; i++) { 494 aDest[i] = ConvertAudioSample<D>(aSource[readIndex]); 495 readIndex += aSourceChannelCount; 496 } 497 return; 498 } 499 500 if (!IsInterleaved(aSourceFormat) && IsInterleaved(aCopyToSpec.mFormat)) { 501 // Planar to interleaved -- copy of all channels of the source into the 502 // destination buffer. 503 MOZ_ASSERT(aCopyToSpec.mPlaneIndex == 0); 504 MOZ_ASSERT(aDest.Length() >= aCopyToSpec.mFrameCount * aSourceChannelCount); 505 MOZ_ASSERT(aSource.Length() - 506 aCopyToSpec.mFrameOffset * aSourceChannelCount >= 507 aCopyToSpec.mFrameCount * aSourceChannelCount); 508 size_t writeIndex = 0; 509 // Scan the source linearly and put each sample at the right position in the 510 // destination interleaved buffer. 511 size_t readIndex = 0; 512 for (size_t channel = 0; channel < aSourceChannelCount; channel++) { 513 writeIndex = channel; 514 for (size_t i = 0; i < aCopyToSpec.mFrameCount; i++) { 515 aDest[writeIndex] = ConvertAudioSample<D>(aSource[readIndex]); 516 readIndex++; 517 writeIndex += aSourceChannelCount; 518 } 519 } 520 return; 521 } 522 if (!IsInterleaved(aSourceFormat) && !IsInterleaved(aCopyToSpec.mFormat)) { 523 // Planar to Planar / convert + copy from the right index in the source. 524 size_t framePerPlane = aSource.Length() / aSourceChannelCount; 525 size_t offset = aCopyToSpec.mPlaneIndex * framePerPlane; 526 MOZ_ASSERT(aDest.Length() >= aCopyToSpec.mFrameCount, 527 "Destination buffer too small"); 528 MOZ_ASSERT(aSource.Length() >= offset + aCopyToSpec.mFrameCount, 529 "Source buffer too small"); 530 for (uint32_t i = 0; i < aCopyToSpec.mFrameCount; i++) { 531 aDest[i] = 532 ConvertAudioSample<D>(aSource[offset + aCopyToSpec.mFrameOffset + i]); 533 } 534 } 535 } 536 537 nsCString AudioData::ToString() const { 538 if (!mResource) { 539 return nsCString("AudioData[detached]"); 540 } 541 return nsFmtCString(FMT_STRING("AudioData[{} bytes {} {}Hz {} x {}ch]"), 542 mResource->Data().LengthBytes(), 543 GetEnumString(mAudioSampleFormat.value()).get(), 544 mSampleRate, mNumberOfFrames, mNumberOfChannels); 545 } 546 547 nsCString CopyToToString(size_t aDestBufSize, 548 const AudioDataCopyToOptions& aOptions) { 549 return nsFmtCString( 550 FMT_STRING( 551 "AudioDataCopyToOptions[data: {} bytes, {}, frame count: {}, frame " 552 "offset: {}, plane: {}]"), 553 aDestBufSize, 554 aOptions.mFormat.WasPassed() 555 ? GetEnumString(aOptions.mFormat.Value()).get() 556 : "null", 557 aOptions.mFrameCount.WasPassed() ? aOptions.mFrameCount.Value() : 0, 558 aOptions.mFrameOffset, aOptions.mPlaneIndex); 559 } 560 561 using DataSpanType = 562 Variant<Span<uint8_t>, Span<int16_t>, Span<int32_t>, Span<float>>; 563 564 DataSpanType GetDataSpan(Span<uint8_t> aSpan, const AudioSampleFormat aFormat) { 565 const size_t Length = aSpan.Length() / BytesPerSamples(aFormat); 566 // TODO: Check size so Span can be reasonably constructed? 567 switch (aFormat) { 568 case AudioSampleFormat::U8: 569 case AudioSampleFormat::U8_planar: 570 return AsVariant(aSpan); 571 case AudioSampleFormat::S16: 572 case AudioSampleFormat::S16_planar: 573 return AsVariant(Span(reinterpret_cast<int16_t*>(aSpan.data()), Length)); 574 case AudioSampleFormat::S32: 575 case AudioSampleFormat::S32_planar: 576 return AsVariant(Span(reinterpret_cast<int32_t*>(aSpan.data()), Length)); 577 case AudioSampleFormat::F32: 578 case AudioSampleFormat::F32_planar: 579 return AsVariant(Span(reinterpret_cast<float*>(aSpan.data()), Length)); 580 } 581 MOZ_ASSERT_UNREACHABLE("Invalid enum value"); 582 return AsVariant(aSpan); 583 } 584 585 void CopySamples(DataSpanType& aSource, DataSpanType& aDest, 586 uint32_t aSourceChannelCount, 587 const AudioSampleFormat aSourceFormat, 588 const CopyToSpec& aCopyToSpec) { 589 aSource.match([&](auto& src) { 590 aDest.match([&](auto& dst) { 591 CopySamples(src, dst, aSourceChannelCount, aSourceFormat, aCopyToSpec); 592 }); 593 }); 594 } 595 596 void DoCopy(Span<uint8_t> aSource, Span<uint8_t> aDest, 597 const uint32_t aSourceChannelCount, 598 const AudioSampleFormat aSourceFormat, 599 const CopyToSpec& aCopyToSpec) { 600 DataSpanType source = GetDataSpan(aSource, aSourceFormat); 601 DataSpanType dest = GetDataSpan(aDest, aCopyToSpec.mFormat); 602 CopySamples(source, dest, aSourceChannelCount, aSourceFormat, aCopyToSpec); 603 } 604 605 // https://w3c.github.io/webcodecs/#dom-audiodata-copyto 606 void AudioData::CopyTo(const AllowSharedBufferSource& aDestination, 607 const AudioDataCopyToOptions& aOptions, 608 ErrorResult& aRv) { 609 AssertIsOnOwningThread(); 610 611 size_t destLength = ProcessTypedArraysFixed( 612 aDestination, [&](const Span<uint8_t>& aData) -> size_t { 613 return aData.LengthBytes(); 614 }); 615 616 LOGD("AudioData::CopyTo {} -> {}", ToString().get(), 617 CopyToToString(destLength, aOptions).get(), 4); 618 619 if (!mResource) { 620 auto msg = "copyTo called on closed AudioData"_ns; 621 LOGD("{}", msg.get()); 622 aRv.ThrowInvalidStateError(msg); 623 return; 624 } 625 626 uint64_t copyElementCount = ComputeCopyElementCount(aOptions, aRv); 627 if (aRv.Failed()) { 628 LOGD("AudioData::CopyTo failed in ComputeCopyElementCount"); 629 return; 630 } 631 auto destFormat = mAudioSampleFormat; 632 if (aOptions.mFormat.WasPassed()) { 633 destFormat = OptionalToMaybe(aOptions.mFormat); 634 } 635 636 uint32_t bytesPerSample = BytesPerSamples(destFormat.value()); 637 CheckedInt<uint32_t> copyLength = bytesPerSample; 638 copyLength *= copyElementCount; 639 if (copyLength.value() > destLength) { 640 auto msg = nsFmtCString(FMT_STRING("destination buffer of length {} too " 641 "small for copying {} elements"), 642 destLength, bytesPerSample * copyElementCount); 643 LOGD("{}", msg.get()); 644 aRv.ThrowRangeError(msg); 645 return; 646 } 647 648 uint32_t framesToCopy = mNumberOfFrames - aOptions.mFrameOffset; 649 if (aOptions.mFrameCount.WasPassed()) { 650 framesToCopy = aOptions.mFrameCount.Value(); 651 } 652 653 CopyToSpec copyToSpec(framesToCopy, aOptions.mFrameOffset, 654 aOptions.mPlaneIndex, destFormat.value()); 655 656 // Now a couple layers of macros to type the pointers and perform the actual 657 // copy. 658 ProcessTypedArraysFixed(aDestination, [&](const Span<uint8_t>& aData) { 659 DoCopy(mResource->Data(), aData, mNumberOfChannels, 660 mAudioSampleFormat.value(), copyToSpec); 661 }); 662 } 663 664 // https://w3c.github.io/webcodecs/#dom-audiodata-clone 665 already_AddRefed<AudioData> AudioData::Clone(ErrorResult& aRv) { 666 AssertIsOnOwningThread(); 667 668 if (!mResource) { 669 auto msg = "No media resource in the AudioData now"_ns; 670 LOGD("{}", msg.get()); 671 aRv.ThrowInvalidStateError(msg); 672 return nullptr; 673 } 674 675 return MakeAndAddRef<AudioData>(*this); 676 } 677 678 // https://w3c.github.io/webcodecs/#close-audiodata 679 void AudioData::Close() { 680 AssertIsOnOwningThread(); 681 682 mResource = nullptr; 683 mSampleRate = 0; 684 mNumberOfFrames = 0; 685 mNumberOfChannels = 0; 686 mAudioSampleFormat = Nothing(); 687 } 688 689 bool AudioData::IsClosed() const { return !mResource; } 690 691 // https://w3c.github.io/webcodecs/#ref-for-deserialization-steps%E2%91%A1 692 /* static */ 693 JSObject* AudioData::ReadStructuredClone(JSContext* aCx, 694 nsIGlobalObject* aGlobal, 695 JSStructuredCloneReader* aReader, 696 const AudioDataSerializedData& aData) { 697 JS::Rooted<JS::Value> value(aCx, JS::NullValue()); 698 // To avoid a rooting hazard error from returning a raw JSObject* before 699 // running the RefPtr destructor, RefPtr needs to be destructed before 700 // returning the raw JSObject*, which is why the RefPtr<AudioData> is created 701 // in the scope below. Otherwise, the static analysis infers the RefPtr cannot 702 // be safely destructed while the unrooted return JSObject* is on the stack. 703 { 704 RefPtr<AudioData> frame = MakeAndAddRef<AudioData>(aGlobal, aData); 705 if (!GetOrCreateDOMReflector(aCx, frame, &value) || !value.isObject()) { 706 LOGE("GetOrCreateDOMReflect failure"); 707 return nullptr; 708 } 709 } 710 return value.toObjectOrNull(); 711 } 712 713 // https://w3c.github.io/webcodecs/#ref-for-audiodata%E2%91%A2%E2%91%A2 714 bool AudioData::WriteStructuredClone(JSStructuredCloneWriter* aWriter, 715 StructuredCloneHolder* aHolder) const { 716 AssertIsOnOwningThread(); 717 718 // AudioData closed 719 if (!mResource) { 720 LOGD("AudioData was already close in WriteStructuredClone"); 721 return false; 722 } 723 const uint32_t index = aHolder->AudioData().Length(); 724 // https://github.com/w3c/webcodecs/issues/717 725 // For now, serialization is only allowed in the same address space, it's OK 726 // to send a refptr here instead of copying the backing buffer. 727 aHolder->AudioData().AppendElement(AudioDataSerializedData(*this)); 728 729 return !NS_WARN_IF(!JS_WriteUint32Pair(aWriter, SCTAG_DOM_AUDIODATA, index)); 730 } 731 732 // https://w3c.github.io/webcodecs/#ref-for-transfer-steps 733 UniquePtr<AudioData::TransferredData> AudioData::Transfer() { 734 AssertIsOnOwningThread(); 735 736 if (!mResource) { 737 // Closed 738 LOGD("AudioData was already close in Transfer"); 739 return nullptr; 740 } 741 742 // This adds a ref to the resource 743 auto serialized = MakeUnique<AudioDataSerializedData>(*this); 744 // This removes the ref to the resource, effectively transfering the backing 745 // storage. 746 Close(); 747 return serialized; 748 } 749 750 // https://w3c.github.io/webcodecs/#ref-for-transfer-receiving-steps 751 /* static */ 752 already_AddRefed<AudioData> AudioData::FromTransferred(nsIGlobalObject* aGlobal, 753 TransferredData* aData) { 754 MOZ_ASSERT(aData); 755 756 return MakeAndAddRef<AudioData>(aGlobal, *aData); 757 } 758 759 void AudioData::CloseIfNeeded() { 760 if (mResource) { 761 mResource = nullptr; 762 } 763 } 764 765 RefPtr<mozilla::AudioData> AudioData::ToAudioData() const { 766 // Always convert to f32 interleaved for now, as this Gecko's prefered 767 // internal audio representation for encoding and decoding. 768 // mResource can be bigger than needed. 769 Span<uint8_t> data = mResource->Data(); 770 CheckedUint64 sampleCount = mNumberOfFrames; 771 sampleCount *= mNumberOfChannels; 772 if (!sampleCount.isValid()) { 773 LOGE("Overflow AudioData::ToAudioData when computing the number of frames"); 774 return nullptr; 775 } 776 AlignedAudioBuffer buf(sampleCount.value()); 777 if (!buf.Length()) { 778 LOGE("OOM when allocating storage for AudioData conversion"); 779 return nullptr; 780 } 781 Span<uint8_t> storage(reinterpret_cast<uint8_t*>(buf.Data()), buf.Size()); 782 783 CopyToSpec spec(mNumberOfFrames, 0, 0, AudioSampleFormat::F32); 784 785 DoCopy(data, storage, mNumberOfChannels, mAudioSampleFormat.value(), spec); 786 787 return MakeRefPtr<mozilla::AudioData>( 788 0, media::TimeUnit::FromMicroseconds(mTimestamp), std::move(buf), 789 mNumberOfChannels, SaturatingCast<uint32_t>(mSampleRate)); 790 } 791 792 #undef LOGD 793 #undef LOGE 794 795 } // namespace mozilla::dom