AnalyserNode.cpp (12345B)
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/AnalyserNode.h" 8 9 #include "AudioNodeEngine.h" 10 #include "AudioNodeTrack.h" 11 #include "Tracing.h" 12 #include "mozilla/Mutex.h" 13 #include "mozilla/PodOperations.h" 14 #include "mozilla/dom/AnalyserNodeBinding.h" 15 #include "nsMathUtils.h" 16 17 namespace mozilla { 18 19 static const uint32_t MAX_FFT_SIZE = 32768; 20 static const size_t CHUNK_COUNT = MAX_FFT_SIZE >> WEBAUDIO_BLOCK_SIZE_BITS; 21 static_assert(MAX_FFT_SIZE == CHUNK_COUNT * WEBAUDIO_BLOCK_SIZE, 22 "MAX_FFT_SIZE must be a multiple of WEBAUDIO_BLOCK_SIZE"); 23 static_assert((CHUNK_COUNT & (CHUNK_COUNT - 1)) == 0, 24 "CHUNK_COUNT must be power of 2 for remainder behavior"); 25 26 namespace dom { 27 28 class AnalyserNodeEngine final : public AudioNodeEngine { 29 class TransferBuffer final : public Runnable { 30 public: 31 TransferBuffer(AudioNodeTrack* aTrack, const AudioChunk& aChunk) 32 : Runnable("dom::AnalyserNodeEngine::TransferBuffer"), 33 mTrack(aTrack), 34 mChunk(aChunk) {} 35 36 NS_IMETHOD Run() override { 37 RefPtr<AnalyserNode> node = 38 static_cast<AnalyserNode*>(mTrack->Engine()->NodeMainThread()); 39 if (node) { 40 node->AppendChunk(mChunk); 41 } 42 return NS_OK; 43 } 44 45 private: 46 RefPtr<AudioNodeTrack> mTrack; 47 AudioChunk mChunk; 48 }; 49 50 public: 51 explicit AnalyserNodeEngine(AnalyserNode* aNode) : AudioNodeEngine(aNode) { 52 MOZ_ASSERT(NS_IsMainThread()); 53 } 54 55 virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, 56 const AudioBlock& aInput, AudioBlock* aOutput, 57 bool* aFinished) override { 58 TRACE("AnalyserNodeEngine::ProcessBlock"); 59 *aOutput = aInput; 60 61 if (aInput.IsNull()) { 62 // If AnalyserNode::mChunks has only null chunks, then there is no need 63 // to send further null chunks. 64 if (mChunksToProcess == 0) { 65 return; 66 } 67 68 --mChunksToProcess; 69 if (mChunksToProcess == 0) { 70 aTrack->ScheduleCheckForInactive(); 71 } 72 73 } else { 74 // This many null chunks will be required to empty AnalyserNode::mChunks. 75 mChunksToProcess = CHUNK_COUNT; 76 } 77 78 RefPtr<TransferBuffer> transfer = 79 new TransferBuffer(aTrack, aInput.AsAudioChunk()); 80 AbstractThread::MainThread()->Dispatch(transfer.forget()); 81 } 82 83 virtual bool IsActive() const override { return mChunksToProcess != 0; } 84 85 virtual size_t SizeOfIncludingThis( 86 MallocSizeOf aMallocSizeOf) const override { 87 return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); 88 } 89 90 uint32_t mChunksToProcess = 0; 91 }; 92 93 /* static */ 94 already_AddRefed<AnalyserNode> AnalyserNode::Create( 95 AudioContext& aAudioContext, const AnalyserOptions& aOptions, 96 ErrorResult& aRv) { 97 RefPtr<AnalyserNode> analyserNode = new AnalyserNode(&aAudioContext); 98 99 analyserNode->Initialize(aOptions, aRv); 100 if (NS_WARN_IF(aRv.Failed())) { 101 return nullptr; 102 } 103 104 analyserNode->SetFftSize(aOptions.mFftSize, aRv); 105 if (NS_WARN_IF(aRv.Failed())) { 106 return nullptr; 107 } 108 109 analyserNode->SetMinAndMaxDecibels(aOptions.mMinDecibels, 110 aOptions.mMaxDecibels, aRv); 111 if (NS_WARN_IF(aRv.Failed())) { 112 return nullptr; 113 } 114 115 analyserNode->SetSmoothingTimeConstant(aOptions.mSmoothingTimeConstant, aRv); 116 if (NS_WARN_IF(aRv.Failed())) { 117 return nullptr; 118 } 119 120 return analyserNode.forget(); 121 } 122 123 AnalyserNode::AnalyserNode(AudioContext* aContext) 124 : AudioNode(aContext, 2, ChannelCountMode::Max, 125 ChannelInterpretation::Speakers), 126 mAnalysisBlock(2048), 127 mMinDecibels(-100.), 128 mMaxDecibels(-30.), 129 mSmoothingTimeConstant(.8) { 130 mTrack = 131 AudioNodeTrack::Create(aContext, new AnalyserNodeEngine(this), 132 AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); 133 134 // Enough chunks must be recorded to handle the case of fftSize being 135 // increased to maximum immediately before getFloatTimeDomainData() is 136 // called, for example. 137 (void)mChunks.SetLength(CHUNK_COUNT, fallible); 138 139 AllocateBuffer(); 140 } 141 142 size_t AnalyserNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { 143 size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); 144 amount += mAnalysisBlock.SizeOfExcludingThis(aMallocSizeOf); 145 amount += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf); 146 amount += mOutputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); 147 return amount; 148 } 149 150 size_t AnalyserNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { 151 return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); 152 } 153 154 JSObject* AnalyserNode::WrapObject(JSContext* aCx, 155 JS::Handle<JSObject*> aGivenProto) { 156 return AnalyserNode_Binding::Wrap(aCx, this, aGivenProto); 157 } 158 159 void AnalyserNode::SetFftSize(uint32_t aValue, ErrorResult& aRv) { 160 // Disallow values that are not a power of 2 and outside the [32,32768] range 161 if (aValue < 32 || aValue > MAX_FFT_SIZE || (aValue & (aValue - 1)) != 0) { 162 aRv.ThrowIndexSizeError(nsPrintfCString( 163 "FFT size %u is not a power of two in the range 32 to 32768", aValue)); 164 return; 165 } 166 if (FftSize() != aValue) { 167 mAnalysisBlock.SetFFTSize(aValue); 168 AllocateBuffer(); 169 } 170 } 171 172 void AnalyserNode::SetMinDecibels(double aValue, ErrorResult& aRv) { 173 if (aValue >= mMaxDecibels) { 174 aRv.ThrowIndexSizeError(nsPrintfCString( 175 "%g is not strictly smaller than current maxDecibels (%g)", aValue, 176 mMaxDecibels)); 177 return; 178 } 179 mMinDecibels = aValue; 180 } 181 182 void AnalyserNode::SetMaxDecibels(double aValue, ErrorResult& aRv) { 183 if (aValue <= mMinDecibels) { 184 aRv.ThrowIndexSizeError(nsPrintfCString( 185 "%g is not strictly larger than current minDecibels (%g)", aValue, 186 mMinDecibels)); 187 return; 188 } 189 mMaxDecibels = aValue; 190 } 191 192 void AnalyserNode::SetMinAndMaxDecibels(double aMinValue, double aMaxValue, 193 ErrorResult& aRv) { 194 if (aMinValue >= aMaxValue) { 195 aRv.ThrowIndexSizeError(nsPrintfCString( 196 "minDecibels value (%g) must be smaller than maxDecibels value (%g)", 197 aMinValue, aMaxValue)); 198 return; 199 } 200 mMinDecibels = aMinValue; 201 mMaxDecibels = aMaxValue; 202 } 203 204 void AnalyserNode::SetSmoothingTimeConstant(double aValue, ErrorResult& aRv) { 205 if (aValue < 0 || aValue > 1) { 206 aRv.ThrowIndexSizeError( 207 nsPrintfCString("%g is not in the range [0, 1]", aValue)); 208 return; 209 } 210 mSmoothingTimeConstant = aValue; 211 } 212 213 void AnalyserNode::GetFloatFrequencyData(const Float32Array& aArray) { 214 if (!FFTAnalysis()) { 215 // Might fail to allocate memory 216 return; 217 } 218 219 aArray.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { 220 size_t length = std::min(size_t(aData.Length()), mOutputBuffer.Length()); 221 222 for (size_t i = 0; i < length; ++i) { 223 aData[i] = WebAudioUtils::ConvertLinearToDecibels( 224 mOutputBuffer[i], -std::numeric_limits<float>::infinity()); 225 } 226 }); 227 } 228 229 void AnalyserNode::GetByteFrequencyData(const Uint8Array& aArray) { 230 if (!FFTAnalysis()) { 231 // Might fail to allocate memory 232 return; 233 } 234 235 const double rangeScaleFactor = 1.0 / (mMaxDecibels - mMinDecibels); 236 237 aArray.ProcessData([&](const Span<uint8_t>& aData, JS::AutoCheckCannotGC&&) { 238 size_t length = std::min(size_t(aData.Length()), mOutputBuffer.Length()); 239 240 for (size_t i = 0; i < length; ++i) { 241 const double decibels = WebAudioUtils::ConvertLinearToDecibels( 242 mOutputBuffer[i], mMinDecibels); 243 // scale down the value to the range of [0, UCHAR_MAX] 244 const double scaled = std::max( 245 0.0, 246 std::min(double(UCHAR_MAX), 247 UCHAR_MAX * (decibels - mMinDecibels) * rangeScaleFactor)); 248 aData[i] = static_cast<unsigned char>(scaled); 249 } 250 }); 251 } 252 253 void AnalyserNode::GetFloatTimeDomainData(const Float32Array& aArray) { 254 aArray.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { 255 size_t length = std::min(aData.Length(), size_t(FftSize())); 256 257 GetTimeDomainData(aData.Elements(), length); 258 }); 259 } 260 261 void AnalyserNode::GetByteTimeDomainData(const Uint8Array& aArray) { 262 aArray.ProcessData([&](const Span<uint8_t>& aData, JS::AutoCheckCannotGC&&) { 263 size_t length = std::min(aData.Length(), size_t(FftSize())); 264 265 AlignedTArray<float> tmpBuffer; 266 if (!tmpBuffer.SetLength(length, fallible)) { 267 return; 268 } 269 270 GetTimeDomainData(tmpBuffer.Elements(), length); 271 272 unsigned char* buffer = aData.Elements(); 273 for (size_t i = 0; i < length; ++i) { 274 const float value = tmpBuffer[i]; 275 // scale the value to the range of [0, UCHAR_MAX] 276 const float scaled = 277 std::max(0.0f, std::min(float(UCHAR_MAX), 128.0f * (value + 1.0f))); 278 buffer[i] = static_cast<unsigned char>(scaled); 279 } 280 }); 281 } 282 283 bool AnalyserNode::FFTAnalysis() { 284 AlignedTArray<float> tmpBuffer; 285 size_t fftSize = FftSize(); 286 if (!tmpBuffer.SetLength(fftSize, fallible)) { 287 return false; 288 } 289 290 float* inputBuffer = tmpBuffer.Elements(); 291 GetTimeDomainData(inputBuffer, fftSize); 292 ApplyBlackmanWindow(inputBuffer, fftSize); 293 mAnalysisBlock.PerformFFT(inputBuffer); 294 295 // Normalize so than an input sine wave at 0dBfs registers as 0dBfs (undo FFT 296 // scaling factor). 297 const double magnitudeScale = 1.0 / fftSize; 298 299 for (uint32_t i = 0; i < mOutputBuffer.Length(); ++i) { 300 double scalarMagnitude = 301 fdlibm_hypot(mAnalysisBlock.RealData(i), mAnalysisBlock.ImagData(i)) * 302 magnitudeScale; 303 mOutputBuffer[i] = mSmoothingTimeConstant * mOutputBuffer[i] + 304 (1.0 - mSmoothingTimeConstant) * scalarMagnitude; 305 } 306 307 return true; 308 } 309 310 void AnalyserNode::ApplyBlackmanWindow(float* aBuffer, uint32_t aSize) { 311 double alpha = 0.16; 312 double a0 = 0.5 * (1.0 - alpha); 313 double a1 = 0.5; 314 double a2 = 0.5 * alpha; 315 316 for (uint32_t i = 0; i < aSize; ++i) { 317 double x = double(i) / aSize; 318 double window = 319 a0 - a1 * fdlibm_cos(2 * M_PI * x) + a2 * fdlibm_cos(4 * M_PI * x); 320 aBuffer[i] *= window; 321 } 322 } 323 324 bool AnalyserNode::AllocateBuffer() { 325 bool result = true; 326 if (mOutputBuffer.Length() != FrequencyBinCount()) { 327 if (!mOutputBuffer.SetLength(FrequencyBinCount(), fallible)) { 328 return false; 329 } 330 memset(mOutputBuffer.Elements(), 0, sizeof(float) * FrequencyBinCount()); 331 } 332 return result; 333 } 334 335 void AnalyserNode::AppendChunk(const AudioChunk& aChunk) { 336 if (mChunks.Length() == 0) { 337 return; 338 } 339 340 ++mCurrentChunk; 341 mChunks[mCurrentChunk & (CHUNK_COUNT - 1)] = aChunk; 342 } 343 344 // Reads into aData the oldest aLength samples of the fftSize most recent 345 // samples. 346 void AnalyserNode::GetTimeDomainData(float* aData, size_t aLength) { 347 size_t fftSize = FftSize(); 348 MOZ_ASSERT(aLength <= fftSize); 349 350 if (mChunks.Length() == 0) { 351 PodZero(aData, aLength); 352 return; 353 } 354 355 size_t readChunk = 356 mCurrentChunk - ((fftSize - 1) >> WEBAUDIO_BLOCK_SIZE_BITS); 357 size_t readIndex = (0 - fftSize) & (WEBAUDIO_BLOCK_SIZE - 1); 358 MOZ_ASSERT(readIndex == 0 || readIndex + fftSize == WEBAUDIO_BLOCK_SIZE); 359 360 for (size_t writeIndex = 0; writeIndex < aLength;) { 361 const AudioChunk& chunk = mChunks[readChunk & (CHUNK_COUNT - 1)]; 362 const size_t channelCount = chunk.ChannelCount(); 363 size_t copyLength = 364 std::min<size_t>(aLength - writeIndex, WEBAUDIO_BLOCK_SIZE); 365 float* dataOut = &aData[writeIndex]; 366 367 if (channelCount == 0) { 368 PodZero(dataOut, copyLength); 369 } else { 370 float scale = chunk.mVolume / channelCount; 371 { // channel 0 372 auto channelData = 373 static_cast<const float*>(chunk.mChannelData[0]) + readIndex; 374 AudioBufferCopyWithScale(channelData, scale, dataOut, copyLength); 375 } 376 for (uint32_t i = 1; i < channelCount; ++i) { 377 auto channelData = 378 static_cast<const float*>(chunk.mChannelData[i]) + readIndex; 379 AudioBufferAddWithScale(channelData, scale, dataOut, copyLength); 380 } 381 } 382 383 readChunk++; 384 writeIndex += copyLength; 385 } 386 } 387 388 } // namespace dom 389 } // namespace mozilla