AudioBuffer.cpp (17784B)
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 "AudioBuffer.h" 8 9 #include <numeric> 10 11 #include "AudioChannelFormat.h" 12 #include "AudioNodeEngine.h" 13 #include "AudioSegment.h" 14 #include "js/ArrayBuffer.h" // JS::StealArrayBufferContents 15 #include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer 16 #include "jsfriendapi.h" 17 #include "mozilla/ErrorResult.h" 18 #include "mozilla/HoldDropJSObjects.h" 19 #include "mozilla/MemoryReporting.h" 20 #include "mozilla/PodOperations.h" 21 #include "mozilla/dom/AudioBufferBinding.h" 22 #include "nsPrintfCString.h" 23 #include "nsTHashSet.h" 24 25 namespace mozilla::dom { 26 27 NS_IMPL_CYCLE_COLLECTION_CLASS(AudioBuffer) 28 29 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioBuffer) 30 NS_IMPL_CYCLE_COLLECTION_UNLINK(mJSChannels) 31 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER 32 tmp->ClearJSChannels(); 33 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 34 35 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioBuffer) 36 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 37 38 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AudioBuffer) 39 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER 40 for (uint32_t i = 0; i < tmp->mJSChannels.Length(); ++i) { 41 NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mJSChannels[i]) 42 } 43 NS_IMPL_CYCLE_COLLECTION_TRACE_END 44 45 /** 46 * AudioBuffers can be shared between AudioContexts, so we need a separate 47 * mechanism to track their memory usage. This thread-safe class keeps track of 48 * all the AudioBuffers, and gets called back by the memory reporting system 49 * when a memory report is needed, reporting how much memory is used by the 50 * buffers backing AudioBuffer objects. */ 51 class AudioBufferMemoryTracker : public nsIMemoryReporter { 52 NS_DECL_THREADSAFE_ISUPPORTS 53 NS_DECL_NSIMEMORYREPORTER 54 55 private: 56 AudioBufferMemoryTracker(); 57 virtual ~AudioBufferMemoryTracker(); 58 59 public: 60 /* Those methods can be called on any thread. */ 61 static void RegisterAudioBuffer(const AudioBuffer* aAudioBuffer); 62 static void UnregisterAudioBuffer(const AudioBuffer* aAudioBuffer); 63 64 private: 65 static AudioBufferMemoryTracker* GetInstance(); 66 /* Those methods must be called with the lock held. */ 67 void RegisterAudioBufferInternal(const AudioBuffer* aAudioBuffer); 68 /* Returns the number of buffers still present in the hash table. */ 69 uint32_t UnregisterAudioBufferInternal(const AudioBuffer* aAudioBuffer); 70 void Init(); 71 72 /* This protects all members of this class. */ 73 static StaticMutex sMutex MOZ_UNANNOTATED; 74 static StaticRefPtr<AudioBufferMemoryTracker> sSingleton; 75 nsTHashSet<const AudioBuffer*> mBuffers; 76 }; 77 78 StaticRefPtr<AudioBufferMemoryTracker> AudioBufferMemoryTracker::sSingleton; 79 StaticMutex AudioBufferMemoryTracker::sMutex; 80 81 NS_IMPL_ISUPPORTS(AudioBufferMemoryTracker, nsIMemoryReporter); 82 83 AudioBufferMemoryTracker* AudioBufferMemoryTracker::GetInstance() { 84 sMutex.AssertCurrentThreadOwns(); 85 if (!sSingleton) { 86 sSingleton = new AudioBufferMemoryTracker(); 87 sSingleton->Init(); 88 } 89 return sSingleton; 90 } 91 92 AudioBufferMemoryTracker::AudioBufferMemoryTracker() = default; 93 94 void AudioBufferMemoryTracker::Init() { RegisterWeakMemoryReporter(this); } 95 96 AudioBufferMemoryTracker::~AudioBufferMemoryTracker() { 97 UnregisterWeakMemoryReporter(this); 98 } 99 100 void AudioBufferMemoryTracker::RegisterAudioBuffer( 101 const AudioBuffer* aAudioBuffer) { 102 StaticMutexAutoLock lock(sMutex); 103 AudioBufferMemoryTracker* tracker = AudioBufferMemoryTracker::GetInstance(); 104 tracker->RegisterAudioBufferInternal(aAudioBuffer); 105 } 106 107 void AudioBufferMemoryTracker::UnregisterAudioBuffer( 108 const AudioBuffer* aAudioBuffer) { 109 StaticMutexAutoLock lock(sMutex); 110 AudioBufferMemoryTracker* tracker = AudioBufferMemoryTracker::GetInstance(); 111 uint32_t count; 112 count = tracker->UnregisterAudioBufferInternal(aAudioBuffer); 113 if (count == 0) { 114 sSingleton = nullptr; 115 } 116 } 117 118 void AudioBufferMemoryTracker::RegisterAudioBufferInternal( 119 const AudioBuffer* aAudioBuffer) { 120 sMutex.AssertCurrentThreadOwns(); 121 mBuffers.Insert(aAudioBuffer); 122 } 123 124 uint32_t AudioBufferMemoryTracker::UnregisterAudioBufferInternal( 125 const AudioBuffer* aAudioBuffer) { 126 sMutex.AssertCurrentThreadOwns(); 127 mBuffers.Remove(aAudioBuffer); 128 return mBuffers.Count(); 129 } 130 131 MOZ_DEFINE_MALLOC_SIZE_OF(AudioBufferMemoryTrackerMallocSizeOf) 132 133 NS_IMETHODIMP 134 AudioBufferMemoryTracker::CollectReports(nsIHandleReportCallback* aHandleReport, 135 nsISupports* aData, bool) { 136 StaticMutexAutoLock lock(sMutex); 137 const size_t amount = 138 std::accumulate(mBuffers.cbegin(), mBuffers.cend(), size_t(0), 139 [](size_t val, const AudioBuffer* buffer) { 140 return val + buffer->SizeOfIncludingThis( 141 AudioBufferMemoryTrackerMallocSizeOf); 142 }); 143 144 MOZ_COLLECT_REPORT("explicit/webaudio/audiobuffer", KIND_HEAP, UNITS_BYTES, 145 amount, "Memory used by AudioBuffer objects (Web Audio)."); 146 147 return NS_OK; 148 } 149 150 AudioBuffer::AudioBuffer(nsPIDOMWindowInner* aWindow, 151 uint32_t aNumberOfChannels, uint32_t aLength, 152 float aSampleRate, ErrorResult& aRv) 153 : mOwnerWindow(do_GetWeakReference(aWindow)), mSampleRate(aSampleRate) { 154 // Note that a buffer with zero channels is permitted here for the sake of 155 // AudioProcessingEvent, where channel counts must match parameters passed 156 // to createScriptProcessor(), one of which may be zero. 157 if (aSampleRate < WebAudioUtils::MinSampleRate || 158 aSampleRate > WebAudioUtils::MaxSampleRate) { 159 aRv.ThrowNotSupportedError( 160 nsPrintfCString("Sample rate (%g) is out of range", aSampleRate)); 161 return; 162 } 163 164 if (aNumberOfChannels > WebAudioUtils::MaxChannelCount) { 165 aRv.ThrowNotSupportedError(nsPrintfCString( 166 "Number of channels (%u) is out of range", aNumberOfChannels)); 167 return; 168 } 169 170 if (!aLength || aLength > INT32_MAX) { 171 aRv.ThrowNotSupportedError( 172 nsPrintfCString("Length (%u) is out of range", aLength)); 173 return; 174 } 175 176 mSharedChannels.mDuration = aLength; 177 mJSChannels.SetLength(aNumberOfChannels); 178 mozilla::HoldJSObjects(this); 179 AudioBufferMemoryTracker::RegisterAudioBuffer(this); 180 } 181 182 AudioBuffer::~AudioBuffer() { 183 AudioBufferMemoryTracker::UnregisterAudioBuffer(this); 184 ClearJSChannels(); 185 mozilla::DropJSObjects(this); 186 } 187 188 /* static */ 189 already_AddRefed<AudioBuffer> AudioBuffer::Constructor( 190 const GlobalObject& aGlobal, const AudioBufferOptions& aOptions, 191 ErrorResult& aRv) { 192 if (!aOptions.mNumberOfChannels) { 193 aRv.ThrowNotSupportedError("Must have nonzero number of channels"); 194 return nullptr; 195 } 196 197 nsCOMPtr<nsPIDOMWindowInner> window = 198 do_QueryInterface(aGlobal.GetAsSupports()); 199 200 return Create(window, aOptions.mNumberOfChannels, aOptions.mLength, 201 aOptions.mSampleRate, aRv); 202 } 203 204 void AudioBuffer::ClearJSChannels() { mJSChannels.Clear(); } 205 206 void AudioBuffer::SetSharedChannels( 207 already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) { 208 RefPtr<ThreadSharedFloatArrayBufferList> buffer = aBuffer; 209 uint32_t channelCount = buffer->GetChannels(); 210 mSharedChannels.mChannelData.SetLength(channelCount); 211 for (uint32_t i = 0; i < channelCount; ++i) { 212 mSharedChannels.mChannelData[i] = buffer->GetData(i); 213 } 214 mSharedChannels.mBuffer = std::move(buffer); 215 mSharedChannels.mBufferFormat = AUDIO_FORMAT_FLOAT32; 216 } 217 218 /* static */ 219 already_AddRefed<AudioBuffer> AudioBuffer::Create( 220 nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels, uint32_t aLength, 221 float aSampleRate, 222 already_AddRefed<ThreadSharedFloatArrayBufferList> aInitialContents, 223 ErrorResult& aRv) { 224 RefPtr<ThreadSharedFloatArrayBufferList> initialContents = aInitialContents; 225 RefPtr<AudioBuffer> buffer = 226 new AudioBuffer(aWindow, aNumberOfChannels, aLength, aSampleRate, aRv); 227 if (aRv.Failed()) { 228 return nullptr; 229 } 230 231 if (initialContents) { 232 MOZ_ASSERT(initialContents->GetChannels() == aNumberOfChannels); 233 buffer->SetSharedChannels(initialContents.forget()); 234 } 235 236 return buffer.forget(); 237 } 238 239 /* static */ 240 already_AddRefed<AudioBuffer> AudioBuffer::Create( 241 nsPIDOMWindowInner* aWindow, float aSampleRate, 242 AudioChunk&& aInitialContents) { 243 AudioChunk initialContents = aInitialContents; 244 ErrorResult rv; 245 RefPtr<AudioBuffer> buffer = 246 new AudioBuffer(aWindow, initialContents.ChannelCount(), 247 initialContents.mDuration, aSampleRate, rv); 248 if (rv.Failed()) { 249 return nullptr; 250 } 251 buffer->mSharedChannels = std::move(aInitialContents); 252 253 return buffer.forget(); 254 } 255 256 JSObject* AudioBuffer::WrapObject(JSContext* aCx, 257 JS::Handle<JSObject*> aGivenProto) { 258 return AudioBuffer_Binding::Wrap(aCx, this, aGivenProto); 259 } 260 261 static void CopyChannelDataToFloat(const AudioChunk& aChunk, uint32_t aChannel, 262 uint32_t aSrcOffset, float* aOutput, 263 uint32_t aLength) { 264 MOZ_ASSERT(aChunk.mVolume == 1.0f); 265 if (aChunk.mBufferFormat == AUDIO_FORMAT_FLOAT32) { 266 mozilla::PodCopy( 267 aOutput, aChunk.ChannelData<float>()[aChannel] + aSrcOffset, aLength); 268 } else { 269 MOZ_ASSERT(aChunk.mBufferFormat == AUDIO_FORMAT_S16); 270 ConvertAudioSamples(aChunk.ChannelData<int16_t>()[aChannel] + aSrcOffset, 271 aOutput, aLength); 272 } 273 } 274 275 bool AudioBuffer::RestoreJSChannelData(JSContext* aJSContext) { 276 nsPIDOMWindowInner* global = GetParentObject(); 277 if (!global || !global->AsGlobal()->HasJSGlobal()) { 278 return false; 279 } 280 281 JSAutoRealm ar(aJSContext, global->AsGlobal()->GetGlobalJSObject()); 282 283 for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { 284 if (mJSChannels[i]) { 285 // Already have data in JS array. 286 continue; 287 } 288 289 // The following code first zeroes the array and then copies our data 290 // into it. We could avoid this with additional JS APIs to construct 291 // an array (or ArrayBuffer) containing initial data. 292 JS::Rooted<JSObject*> array(aJSContext, 293 JS_NewFloat32Array(aJSContext, Length())); 294 if (!array) { 295 return false; 296 } 297 if (!mSharedChannels.IsNull()) { 298 // "4. Attach ArrayBuffers containing copies of the data to the 299 // AudioBuffer, to be returned by the next call to getChannelData." 300 JS::AutoCheckCannotGC nogc; 301 bool isShared; 302 float* jsData = JS_GetFloat32ArrayData(array, &isShared, nogc); 303 MOZ_ASSERT(!isShared); // Was created as unshared above 304 CopyChannelDataToFloat(mSharedChannels, i, 0, jsData, Length()); 305 } 306 mJSChannels[i] = array; 307 } 308 309 mSharedChannels.SetNull(Length()); 310 311 return true; 312 } 313 314 void AudioBuffer::CopyFromChannel(const Float32Array& aDestination, 315 uint32_t aChannelNumber, 316 uint32_t aBufferOffset, ErrorResult& aRv) { 317 if (aChannelNumber >= NumberOfChannels()) { 318 aRv.ThrowIndexSizeError( 319 nsPrintfCString("Channel number (%u) is out of range", aChannelNumber)); 320 return; 321 } 322 uint32_t length = Length(); 323 if (aBufferOffset >= length) { 324 return; 325 } 326 JS::AutoCheckCannotGC nogc; 327 MOZ_RELEASE_ASSERT(!JS_GetTypedArraySharedness(aDestination.Obj())); 328 auto calculateCount = [=](uint32_t aLength) -> uint32_t { 329 return std::min(length - aBufferOffset, aLength); 330 }; 331 332 JSObject* channelArray = mJSChannels[aChannelNumber]; 333 if (channelArray) { 334 if (JS_GetTypedArrayLength(channelArray) != length) { 335 // The array's buffer was detached. 336 return; 337 } 338 bool isShared = false; 339 const float* sourceData = 340 JS_GetFloat32ArrayData(channelArray, &isShared, nogc); 341 // The sourceData arrays should all have originated in 342 // RestoreJSChannelData, where they are created unshared. 343 MOZ_ASSERT(!isShared); 344 aDestination.ProcessData( 345 [&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { 346 PodMove(aData.Elements(), sourceData + aBufferOffset, 347 calculateCount(aData.Length())); 348 }); 349 return; 350 } 351 352 if (!mSharedChannels.IsNull()) { 353 aDestination.ProcessData([&](const Span<float>& aData, 354 JS::AutoCheckCannotGC&&) { 355 CopyChannelDataToFloat(mSharedChannels, aChannelNumber, aBufferOffset, 356 aData.Elements(), calculateCount(aData.Length())); 357 }); 358 return; 359 } 360 361 aDestination.ProcessData( 362 [&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { 363 PodZero(aData.Elements(), calculateCount(aData.Length())); 364 }); 365 } 366 367 void AudioBuffer::CopyToChannel(JSContext* aJSContext, 368 const Float32Array& aSource, 369 uint32_t aChannelNumber, uint32_t aBufferOffset, 370 ErrorResult& aRv) { 371 if (aChannelNumber >= NumberOfChannels()) { 372 aRv.ThrowIndexSizeError( 373 nsPrintfCString("Channel number (%u) is out of range", aChannelNumber)); 374 return; 375 } 376 377 if (!RestoreJSChannelData(aJSContext)) { 378 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 379 return; 380 } 381 382 JS::AutoCheckCannotGC nogc; 383 JSObject* channelArray = mJSChannels[aChannelNumber]; 384 // This may differ from Length() if the buffer has been detached. 385 uint32_t length = JS_GetTypedArrayLength(channelArray); 386 if (aBufferOffset >= length) { 387 return; 388 } 389 390 int64_t offset = aBufferOffset; 391 aSource.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { 392 MOZ_ASSERT_IF(std::numeric_limits<decltype(aData.Length())>::max() > 393 std::numeric_limits<int64_t>::max(), 394 aData.Length() <= std::numeric_limits<int64_t>::max()); 395 int64_t srcLength = int64_t(aData.Length()); 396 size_t count = std::max(int64_t(0), std::min(length - offset, srcLength)); 397 bool isShared = false; 398 float* channelData = JS_GetFloat32ArrayData(channelArray, &isShared, nogc); 399 // The channelData arrays should all have originated in 400 // RestoreJSChannelData, where they are created unshared. 401 MOZ_ASSERT(!isShared); 402 PodMove(channelData + aBufferOffset, aData.Elements(), count); 403 }); 404 } 405 406 void AudioBuffer::GetChannelData(JSContext* aJSContext, uint32_t aChannel, 407 JS::MutableHandle<JSObject*> aRetval, 408 ErrorResult& aRv) { 409 if (aChannel >= NumberOfChannels()) { 410 aRv.ThrowIndexSizeError( 411 nsPrintfCString("Channel number (%u) is out of range", aChannel)); 412 return; 413 } 414 415 if (!RestoreJSChannelData(aJSContext)) { 416 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 417 return; 418 } 419 420 aRetval.set(mJSChannels[aChannel]); 421 } 422 423 already_AddRefed<ThreadSharedFloatArrayBufferList> 424 AudioBuffer::StealJSArrayDataIntoSharedChannels(JSContext* aJSContext) { 425 nsPIDOMWindowInner* global = GetParentObject(); 426 if (!global || !global->AsGlobal()->HasJSGlobal()) { 427 return nullptr; 428 } 429 430 JSAutoRealm ar(aJSContext, global->AsGlobal()->GetGlobalJSObject()); 431 432 // "1. If any of the AudioBuffer's ArrayBuffer have been detached, abort 433 // these steps, and return a zero-length channel data buffers to the 434 // invoker." 435 for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { 436 JSObject* channelArray = mJSChannels[i]; 437 if (!channelArray || Length() != JS_GetTypedArrayLength(channelArray)) { 438 // Either empty buffer or one of the arrays' buffers was detached. 439 return nullptr; 440 } 441 } 442 443 // "2. Detach all ArrayBuffers for arrays previously returned by 444 // getChannelData on this AudioBuffer." 445 // "3. Retain the underlying data buffers from those ArrayBuffers and return 446 // references to them to the invoker." 447 RefPtr<ThreadSharedFloatArrayBufferList> result = 448 new ThreadSharedFloatArrayBufferList(mJSChannels.Length()); 449 for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { 450 JS::Rooted<JSObject*> arrayBufferView(aJSContext, mJSChannels[i]); 451 bool isSharedMemory; 452 JS::Rooted<JSObject*> arrayBuffer( 453 aJSContext, JS_GetArrayBufferViewBuffer(aJSContext, arrayBufferView, 454 &isSharedMemory)); 455 // The channel data arrays should all have originated in 456 // RestoreJSChannelData, where they are created unshared. 457 MOZ_ASSERT(!isSharedMemory); 458 auto stolenData = arrayBuffer 459 ? static_cast<float*>(JS::StealArrayBufferContents( 460 aJSContext, arrayBuffer)) 461 : nullptr; 462 if (stolenData) { 463 result->SetData(i, stolenData, js_free, stolenData); 464 } else { 465 NS_ASSERTION(i == 0, "some channels lost when contents not acquired"); 466 return nullptr; 467 } 468 } 469 470 for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { 471 mJSChannels[i] = nullptr; 472 } 473 474 return result.forget(); 475 } 476 477 const AudioChunk& AudioBuffer::GetThreadSharedChannelsForRate( 478 JSContext* aJSContext) { 479 if (mSharedChannels.IsNull()) { 480 // mDuration is set in constructor 481 RefPtr<ThreadSharedFloatArrayBufferList> buffer = 482 StealJSArrayDataIntoSharedChannels(aJSContext); 483 484 if (buffer) { 485 SetSharedChannels(buffer.forget()); 486 } 487 } 488 489 return mSharedChannels; 490 } 491 492 size_t AudioBuffer::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { 493 size_t amount = aMallocSizeOf(this); 494 amount += mJSChannels.ShallowSizeOfExcludingThis(aMallocSizeOf); 495 amount += mSharedChannels.SizeOfExcludingThis(aMallocSizeOf, false); 496 return amount; 497 } 498 499 } // namespace mozilla::dom