gecko/content/media/webaudio/AnalyserNode.cpp
Ehsan Akhgari 8353ddad3c Bug 815643 - Part 2: Refactor our Web Audio FFT code into the FFTBlock class; r=roc
This is useful in order for us to be able to borrow code from Blink with
fewer changes, and also to explore faster platform dependent FFT
implementations in the future if needed.

--HG--
extra : rebase_source : 34bbded736e1b385754756513c59ee27ce7552a5
2013-06-10 16:08:21 -04:00

304 lines
8.4 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/AnalyserNode.h"
#include "mozilla/dom/AnalyserNodeBinding.h"
#include "AudioNodeEngine.h"
#include "AudioNodeStream.h"
#include "mozilla/Mutex.h"
#include "mozilla/PodOperations.h"
namespace mozilla {
namespace dom {
NS_IMPL_ISUPPORTS_INHERITED0(AnalyserNode, AudioNode)
class AnalyserNodeEngine : public AudioNodeEngine
{
class TransferBuffer : public nsRunnable
{
public:
TransferBuffer(AudioNodeStream* aStream,
const AudioChunk& aChunk)
: mStream(aStream)
, mChunk(aChunk)
{
}
NS_IMETHOD Run()
{
nsRefPtr<AnalyserNode> node;
{
// No need to keep holding the lock for the whole duration of this
// function, since we're holding a strong reference to it, so if
// we can obtain the reference, we will hold the node alive in
// this function.
MutexAutoLock lock(mStream->Engine()->NodeMutex());
node = static_cast<AnalyserNode*>(mStream->Engine()->Node());
}
if (node) {
node->AppendChunk(mChunk);
}
return NS_OK;
}
private:
nsRefPtr<AudioNodeStream> mStream;
AudioChunk mChunk;
};
public:
explicit AnalyserNodeEngine(AnalyserNode* aNode)
: AudioNodeEngine(aNode)
{
MOZ_ASSERT(NS_IsMainThread());
}
virtual void ProduceAudioBlock(AudioNodeStream* aStream,
const AudioChunk& aInput,
AudioChunk* aOutput,
bool* aFinished) MOZ_OVERRIDE
{
*aOutput = aInput;
MutexAutoLock lock(NodeMutex());
if (Node() &&
aInput.mChannelData.Length() > 0) {
nsRefPtr<TransferBuffer> transfer = new TransferBuffer(aStream, aInput);
NS_DispatchToMainThread(transfer);
}
}
};
AnalyserNode::AnalyserNode(AudioContext* aContext)
: AudioNode(aContext,
1,
ChannelCountMode::Explicit,
ChannelInterpretation::Speakers)
, mAnalysisBlock(2048)
, mMinDecibels(-100.)
, mMaxDecibels(-30.)
, mSmoothingTimeConstant(.8)
, mWriteIndex(0)
{
mStream = aContext->Graph()->CreateAudioNodeStream(new AnalyserNodeEngine(this),
MediaStreamGraph::INTERNAL_STREAM);
AllocateBuffer();
}
JSObject*
AnalyserNode::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
{
return AnalyserNodeBinding::Wrap(aCx, aScope, this);
}
void
AnalyserNode::SetFftSize(uint32_t aValue, ErrorResult& aRv)
{
// Disallow values that are not a power of 2 and outside the [32,2048] range
if (aValue < 32 ||
aValue > 2048 ||
(aValue & (aValue - 1)) != 0) {
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
return;
}
if (FftSize() != aValue) {
mAnalysisBlock.SetFFTSize(aValue);
AllocateBuffer();
}
}
void
AnalyserNode::SetMinDecibels(double aValue, ErrorResult& aRv)
{
if (aValue >= mMaxDecibels) {
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
return;
}
mMinDecibels = aValue;
}
void
AnalyserNode::SetMaxDecibels(double aValue, ErrorResult& aRv)
{
if (aValue <= mMinDecibels) {
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
return;
}
mMaxDecibels = aValue;
}
void
AnalyserNode::SetSmoothingTimeConstant(double aValue, ErrorResult& aRv)
{
if (aValue < 0 || aValue > 1) {
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
return;
}
mSmoothingTimeConstant = aValue;
}
void
AnalyserNode::GetFloatFrequencyData(Float32Array& aArray)
{
if (!FFTAnalysis()) {
// Might fail to allocate memory
return;
}
float* buffer = aArray.Data();
uint32_t length = std::min(aArray.Length(), mOutputBuffer.Length());
for (uint32_t i = 0; i < length; ++i) {
buffer[i] = WebAudioUtils::ConvertLinearToDecibels(mOutputBuffer[i], mMinDecibels);
}
}
void
AnalyserNode::GetByteFrequencyData(Uint8Array& aArray)
{
if (!FFTAnalysis()) {
// Might fail to allocate memory
return;
}
const double rangeScaleFactor = 1.0 / (mMaxDecibels - mMinDecibels);
unsigned char* buffer = aArray.Data();
uint32_t length = std::min(aArray.Length(), mOutputBuffer.Length());
for (uint32_t i = 0; i < length; ++i) {
const double decibels = WebAudioUtils::ConvertLinearToDecibels(mOutputBuffer[i], mMinDecibels);
// scale down the value to the range of [0, UCHAR_MAX]
const double scaled = std::max(0.0, std::min(double(UCHAR_MAX),
UCHAR_MAX * (decibels - mMinDecibels) * rangeScaleFactor));
buffer[i] = static_cast<unsigned char>(scaled);
}
}
void
AnalyserNode::GetByteTimeDomainData(Uint8Array& aArray)
{
unsigned char* buffer = aArray.Data();
uint32_t length = std::min(aArray.Length(), mBuffer.Length());
for (uint32_t i = 0; i < length; ++i) {
const float value = mBuffer[(i + mWriteIndex) % mBuffer.Length()];
// scale the value to the range of [0, UCHAR_MAX]
const float scaled = std::max(0.0f, std::min(float(UCHAR_MAX),
128.0f * (value + 1.0f)));
buffer[i] = static_cast<unsigned char>(scaled);
}
}
bool
AnalyserNode::FFTAnalysis()
{
float* inputBuffer;
bool allocated = false;
if (mWriteIndex == 0) {
inputBuffer = mBuffer.Elements();
} else {
inputBuffer = static_cast<float*>(moz_malloc(FftSize() * sizeof(float)));
if (!inputBuffer) {
return false;
}
memcpy(inputBuffer, mBuffer.Elements() + mWriteIndex, sizeof(float) * (FftSize() - mWriteIndex));
memcpy(inputBuffer + FftSize() - mWriteIndex, mBuffer.Elements(), sizeof(float) * mWriteIndex);
allocated = true;
}
ApplyBlackmanWindow(inputBuffer, FftSize());
mAnalysisBlock.PerformFFT(inputBuffer);
// Normalize so than an input sine wave at 0dBfs registers as 0dBfs (undo FFT scaling factor).
const double magnitudeScale = 1.0 / FftSize();
for (uint32_t i = 0; i < mOutputBuffer.Length(); ++i) {
double scalarMagnitude = NS_hypot(mAnalysisBlock.RealData(i),
mAnalysisBlock.ImagData(i)) *
magnitudeScale;
mOutputBuffer[i] = mSmoothingTimeConstant * mOutputBuffer[i] +
(1.0 - mSmoothingTimeConstant) * scalarMagnitude;
}
if (allocated) {
moz_free(inputBuffer);
}
return true;
}
void
AnalyserNode::ApplyBlackmanWindow(float* aBuffer, uint32_t aSize)
{
double alpha = 0.16;
double a0 = 0.5 * (1.0 - alpha);
double a1 = 0.5;
double a2 = 0.5 * alpha;
for (uint32_t i = 0; i < aSize; ++i) {
double x = double(i) / aSize;
double window = a0 - a1 * cos(2 * M_PI * x) + a2 * cos(4 * M_PI * x);
aBuffer[i] *= window;
}
}
bool
AnalyserNode::AllocateBuffer()
{
bool result = true;
if (mBuffer.Length() != FftSize()) {
result = mBuffer.SetLength(FftSize());
if (result) {
memset(mBuffer.Elements(), 0, sizeof(float) * FftSize());
mWriteIndex = 0;
result = mOutputBuffer.SetLength(FrequencyBinCount());
if (result) {
memset(mOutputBuffer.Elements(), 0, sizeof(float) * FrequencyBinCount());
}
}
}
return result;
}
void
AnalyserNode::AppendChunk(const AudioChunk& aChunk)
{
const uint32_t bufferSize = mBuffer.Length();
const uint32_t channelCount = aChunk.mChannelData.Length();
uint32_t chunkDuration = aChunk.mDuration;
MOZ_ASSERT((bufferSize & (bufferSize - 1)) == 0); // Must be a power of two!
MOZ_ASSERT(channelCount > 0);
MOZ_ASSERT(chunkDuration == WEBAUDIO_BLOCK_SIZE);
if (chunkDuration > bufferSize) {
// Copy a maximum bufferSize samples.
chunkDuration = bufferSize;
}
PodCopy(mBuffer.Elements() + mWriteIndex, static_cast<const float*>(aChunk.mChannelData[0]), chunkDuration);
for (uint32_t i = 1; i < channelCount; ++i) {
AudioBlockAddChannelWithScale(static_cast<const float*>(aChunk.mChannelData[i]), 1.0f,
mBuffer.Elements() + mWriteIndex);
}
if (channelCount > 1) {
AudioBlockInPlaceScale(mBuffer.Elements() + mWriteIndex, 1,
1.0f / aChunk.mChannelData.Length());
}
mWriteIndex += chunkDuration;
MOZ_ASSERT(mWriteIndex <= bufferSize);
if (mWriteIndex >= bufferSize) {
mWriteIndex = 0;
}
}
}
}