gecko/content/media/webaudio/PannerNode.cpp
Karl Tomlinson 74e3f50648 b=907986 handle zero front-right plane projection without NaNs r=padenot
The azimuth calculation in the Web Audio spec becomes undefined in this
situation as it requires normalizing a zero vector.  The panning effect should
not depend on azimuth when the source is directly above the listener (because
position does not depend on azimuth), but the specified "equalpower" panning
model does depend on azimuth even at this elevation.  Setting azimuth to zero
produces the same result as if the normalized projection were replaced with a
zero vector.

--HG--
extra : transplant_source : f%A4h%CB7%7Bp%87%AE%09%9F%2Cu%D7%CD%9D%5E%A8%EC%0D
2013-09-04 21:20:59 +12:00

570 lines
18 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 "PannerNode.h"
#include "AudioNodeEngine.h"
#include "AudioNodeStream.h"
#include "AudioListener.h"
#include "AudioBufferSourceNode.h"
#include "blink/HRTFPanner.h"
#include "blink/HRTFDatabaseLoader.h"
using WebCore::HRTFDatabaseLoader;
using WebCore::HRTFPanner;
namespace mozilla {
namespace dom {
using namespace std;
NS_IMPL_CYCLE_COLLECTION_CLASS(PannerNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PannerNode)
if (tmp->Context()) {
tmp->Context()->UnregisterPannerNode(tmp);
}
NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PannerNode, AudioNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(PannerNode)
NS_INTERFACE_MAP_END_INHERITING(AudioNode)
NS_IMPL_ADDREF_INHERITED(PannerNode, AudioNode)
NS_IMPL_RELEASE_INHERITED(PannerNode, AudioNode)
class PannerNodeEngine : public AudioNodeEngine
{
public:
explicit PannerNodeEngine(AudioNode* aNode)
: AudioNodeEngine(aNode)
// Please keep these default values consistent with PannerNode::PannerNode below.
, mPanningModel(PanningModelType::HRTF)
, mPanningModelFunction(&PannerNodeEngine::HRTFPanningFunction)
, mDistanceModel(DistanceModelType::Inverse)
, mDistanceModelFunction(&PannerNodeEngine::InverseGainFunction)
, mPosition()
, mOrientation(1., 0., 0.)
, mVelocity()
, mRefDistance(1.)
, mMaxDistance(10000.)
, mRolloffFactor(1.)
, mConeInnerAngle(360.)
, mConeOuterAngle(360.)
, mConeOuterGain(0.)
// These will be initialized when a PannerNode is created, so just initialize them
// to some dummy values here.
, mListenerDopplerFactor(0.)
, mListenerSpeedOfSound(0.)
{
// HRTFDatabaseLoader needs to be fetched on the main thread.
TemporaryRef<HRTFDatabaseLoader> loader =
HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(aNode->Context()->SampleRate());
mHRTFPanner = new HRTFPanner(aNode->Context()->SampleRate(), loader);
}
virtual void SetInt32Parameter(uint32_t aIndex, int32_t aParam) MOZ_OVERRIDE
{
switch (aIndex) {
case PannerNode::PANNING_MODEL:
mPanningModel = PanningModelType(aParam);
switch (mPanningModel) {
case PanningModelType::Equalpower:
mPanningModelFunction = &PannerNodeEngine::EqualPowerPanningFunction;
break;
case PanningModelType::HRTF:
mPanningModelFunction = &PannerNodeEngine::HRTFPanningFunction;
break;
default:
NS_NOTREACHED("We should never see the alternate names here");
break;
}
break;
case PannerNode::DISTANCE_MODEL:
mDistanceModel = DistanceModelType(aParam);
switch (mDistanceModel) {
case DistanceModelType::Inverse:
mDistanceModelFunction = &PannerNodeEngine::InverseGainFunction;
break;
case DistanceModelType::Linear:
mDistanceModelFunction = &PannerNodeEngine::LinearGainFunction;
break;
case DistanceModelType::Exponential:
mDistanceModelFunction = &PannerNodeEngine::ExponentialGainFunction;
break;
default:
NS_NOTREACHED("We should never see the alternate names here");
break;
}
break;
default:
NS_ERROR("Bad PannerNodeEngine Int32Parameter");
}
}
virtual void SetThreeDPointParameter(uint32_t aIndex, const ThreeDPoint& aParam) MOZ_OVERRIDE
{
switch (aIndex) {
case PannerNode::LISTENER_POSITION: mListenerPosition = aParam; break;
case PannerNode::LISTENER_FRONT_VECTOR: mListenerFrontVector = aParam; break;
case PannerNode::LISTENER_RIGHT_VECTOR: mListenerRightVector = aParam; break;
case PannerNode::LISTENER_VELOCITY: mListenerVelocity = aParam; break;
case PannerNode::POSITION: mPosition = aParam; break;
case PannerNode::ORIENTATION: mOrientation = aParam; break;
case PannerNode::VELOCITY: mVelocity = aParam; break;
default:
NS_ERROR("Bad PannerNodeEngine ThreeDPointParameter");
}
}
virtual void SetDoubleParameter(uint32_t aIndex, double aParam) MOZ_OVERRIDE
{
switch (aIndex) {
case PannerNode::LISTENER_DOPPLER_FACTOR: mListenerDopplerFactor = aParam; break;
case PannerNode::LISTENER_SPEED_OF_SOUND: mListenerSpeedOfSound = aParam; break;
case PannerNode::REF_DISTANCE: mRefDistance = aParam; break;
case PannerNode::MAX_DISTANCE: mMaxDistance = aParam; break;
case PannerNode::ROLLOFF_FACTOR: mRolloffFactor = aParam; break;
case PannerNode::CONE_INNER_ANGLE: mConeInnerAngle = aParam; break;
case PannerNode::CONE_OUTER_ANGLE: mConeOuterAngle = aParam; break;
case PannerNode::CONE_OUTER_GAIN: mConeOuterGain = aParam; break;
default:
NS_ERROR("Bad PannerNodeEngine DoubleParameter");
}
}
virtual void ProduceAudioBlock(AudioNodeStream* aStream,
const AudioChunk& aInput,
AudioChunk* aOutput,
bool *aFinished) MOZ_OVERRIDE
{
(this->*mPanningModelFunction)(aInput, aOutput);
}
void ComputeAzimuthAndElevation(float& aAzimuth, float& aElevation);
void DistanceAndConeGain(AudioChunk* aChunk, float aGain);
float ComputeConeGain();
// Compute how much the distance contributes to the gain reduction.
float ComputeDistanceGain();
void GainMonoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
float aGainL, float aGainR);
void GainStereoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
float aGainL, float aGainR, double aAzimuth);
void EqualPowerPanningFunction(const AudioChunk& aInput, AudioChunk* aOutput);
void HRTFPanningFunction(const AudioChunk& aInput, AudioChunk* aOutput);
float LinearGainFunction(float aDistance);
float InverseGainFunction(float aDistance);
float ExponentialGainFunction(float aDistance);
nsAutoPtr<HRTFPanner> mHRTFPanner;
PanningModelType mPanningModel;
typedef void (PannerNodeEngine::*PanningModelFunction)(const AudioChunk& aInput, AudioChunk* aOutput);
PanningModelFunction mPanningModelFunction;
DistanceModelType mDistanceModel;
typedef float (PannerNodeEngine::*DistanceModelFunction)(float aDistance);
DistanceModelFunction mDistanceModelFunction;
ThreeDPoint mPosition;
ThreeDPoint mOrientation;
ThreeDPoint mVelocity;
double mRefDistance;
double mMaxDistance;
double mRolloffFactor;
double mConeInnerAngle;
double mConeOuterAngle;
double mConeOuterGain;
ThreeDPoint mListenerPosition;
ThreeDPoint mListenerFrontVector;
ThreeDPoint mListenerRightVector;
ThreeDPoint mListenerVelocity;
double mListenerDopplerFactor;
double mListenerSpeedOfSound;
};
PannerNode::PannerNode(AudioContext* aContext)
: AudioNode(aContext,
2,
ChannelCountMode::Clamped_max,
ChannelInterpretation::Speakers)
// Please keep these default values consistent with PannerNodeEngine::PannerNodeEngine above.
, mPanningModel(PanningModelType::HRTF)
, mDistanceModel(DistanceModelType::Inverse)
, mPosition()
, mOrientation(1., 0., 0.)
, mVelocity()
, mRefDistance(1.)
, mMaxDistance(10000.)
, mRolloffFactor(1.)
, mConeInnerAngle(360.)
, mConeOuterAngle(360.)
, mConeOuterGain(0.)
{
mStream = aContext->Graph()->CreateAudioNodeStream(new PannerNodeEngine(this),
MediaStreamGraph::INTERNAL_STREAM);
// We should register once we have set up our stream and engine.
Context()->Listener()->RegisterPannerNode(this);
}
PannerNode::~PannerNode()
{
if (Context()) {
Context()->UnregisterPannerNode(this);
}
}
JSObject*
PannerNode::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
{
return PannerNodeBinding::Wrap(aCx, aScope, this);
}
void PannerNode::DestroyMediaStream()
{
if (Context()) {
Context()->UnregisterPannerNode(this);
}
AudioNode::DestroyMediaStream();
}
// Those three functions are described in the spec.
float
PannerNodeEngine::LinearGainFunction(float aDistance)
{
return 1 - mRolloffFactor * (aDistance - mRefDistance) / (mMaxDistance - mRefDistance);
}
float
PannerNodeEngine::InverseGainFunction(float aDistance)
{
return mRefDistance / (mRefDistance + mRolloffFactor * (aDistance - mRefDistance));
}
float
PannerNodeEngine::ExponentialGainFunction(float aDistance)
{
return pow(aDistance / mRefDistance, -mRolloffFactor);
}
void
PannerNodeEngine::HRTFPanningFunction(const AudioChunk& aInput,
AudioChunk* aOutput)
{
int numChannels = aInput.mChannelData.Length();
// The output of this node is always stereo, no matter what the inputs are.
AllocateAudioBlock(2, aOutput);
float azimuth, elevation;
ComputeAzimuthAndElevation(azimuth, elevation);
AudioChunk input = aInput;
// Gain is applied before the delay and convolution of the HRTF
if (!input.IsNull()) {
float gain = ComputeConeGain() * ComputeDistanceGain() * aInput.mVolume;
if (gain != 1.0f) {
AllocateAudioBlock(numChannels, &input);
for (int i = 0; i < numChannels; ++i) {
const float* src = static_cast<const float*>(aInput.mChannelData[i]);
float* dest =
static_cast<float*>(const_cast<void*>(input.mChannelData[i]));
AudioBlockCopyChannelWithScale(src, gain, dest);
}
}
}
mHRTFPanner->pan(azimuth, elevation, &input, aOutput, WEBAUDIO_BLOCK_SIZE);
}
void
PannerNodeEngine::EqualPowerPanningFunction(const AudioChunk& aInput,
AudioChunk* aOutput)
{
if (aInput.IsNull()) {
*aOutput = aInput;
return;
}
float azimuth, elevation, gainL, gainR, normalizedAzimuth, distanceGain, coneGain;
int inputChannels = aInput.mChannelData.Length();
// If both the listener are in the same spot, and no cone gain is specified,
// this node is noop.
if (mListenerPosition == mPosition &&
mConeInnerAngle == 360 &&
mConeOuterAngle == 360) {
*aOutput = aInput;
return;
}
// The output of this node is always stereo, no matter what the inputs are.
AllocateAudioBlock(2, aOutput);
ComputeAzimuthAndElevation(azimuth, elevation);
coneGain = ComputeConeGain();
// The following algorithm is described in the spec.
// Clamp azimuth in the [-90, 90] range.
azimuth = min(180.f, max(-180.f, azimuth));
// Wrap around
if (azimuth < -90.f) {
azimuth = -180.f - azimuth;
} else if (azimuth > 90) {
azimuth = 180.f - azimuth;
}
// Normalize the value in the [0, 1] range.
if (inputChannels == 1) {
normalizedAzimuth = (azimuth + 90.f) / 180.f;
} else {
if (azimuth <= 0) {
normalizedAzimuth = (azimuth + 90.f) / 90.f;
} else {
normalizedAzimuth = azimuth / 90.f;
}
}
distanceGain = ComputeDistanceGain();
// Actually compute the left and right gain.
gainL = cos(0.5 * M_PI * normalizedAzimuth) * aInput.mVolume;
gainR = sin(0.5 * M_PI * normalizedAzimuth) * aInput.mVolume;
// Compute the output.
if (inputChannels == 1) {
GainMonoToStereo(aInput, aOutput, gainL, gainR);
} else {
GainStereoToStereo(aInput, aOutput, gainL, gainR, azimuth);
}
DistanceAndConeGain(aOutput, distanceGain * coneGain);
}
void
PannerNodeEngine::GainMonoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
float aGainL, float aGainR)
{
float* outputL = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[0]));
float* outputR = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[1]));
const float* input = static_cast<float*>(const_cast<void*>(aInput.mChannelData[0]));
AudioBlockPanMonoToStereo(input, aGainL, aGainR, outputL, outputR);
}
void
PannerNodeEngine::GainStereoToStereo(const AudioChunk& aInput, AudioChunk* aOutput,
float aGainL, float aGainR, double aAzimuth)
{
float* outputL = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[0]));
float* outputR = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[1]));
const float* inputL = static_cast<float*>(const_cast<void*>(aInput.mChannelData[0]));
const float* inputR = static_cast<float*>(const_cast<void*>(aInput.mChannelData[1]));
AudioBlockPanStereoToStereo(inputL, inputR, aGainL, aGainR, aAzimuth <= 0, outputL, outputR);
}
void
PannerNodeEngine::DistanceAndConeGain(AudioChunk* aChunk, float aGain)
{
float* samples = static_cast<float*>(const_cast<void*>(*aChunk->mChannelData.Elements()));
uint32_t channelCount = aChunk->mChannelData.Length();
AudioBufferInPlaceScale(samples, channelCount, aGain);
}
// This algorithm is specified in the webaudio spec.
void
PannerNodeEngine::ComputeAzimuthAndElevation(float& aAzimuth, float& aElevation)
{
ThreeDPoint sourceListener = mPosition - mListenerPosition;
if (sourceListener.IsZero()) {
aAzimuth = 0.0;
aElevation = 0.0;
return;
}
sourceListener.Normalize();
// Project the source-listener vector on the x-z plane.
const ThreeDPoint& listenerFront = mListenerFrontVector;
const ThreeDPoint& listenerRight = mListenerRightVector;
ThreeDPoint up = listenerRight.CrossProduct(listenerFront);
double upProjection = sourceListener.DotProduct(up);
aElevation = 90 - 180 * acos(upProjection) / M_PI;
if (aElevation > 90) {
aElevation = 180 - aElevation;
} else if (aElevation < -90) {
aElevation = -180 - aElevation;
}
ThreeDPoint projectedSource = sourceListener - up * upProjection;
if (projectedSource.IsZero()) {
// source - listener direction is up or down.
aAzimuth = 0.0;
return;
}
projectedSource.Normalize();
// Actually compute the angle, and convert to degrees
double projection = projectedSource.DotProduct(listenerRight);
aAzimuth = 180 * acos(projection) / M_PI;
// Compute whether the source is in front or behind the listener.
double frontBack = projectedSource.DotProduct(listenerFront);
if (frontBack < 0) {
aAzimuth = 360 - aAzimuth;
}
// Rotate the azimuth so it is relative to the listener front vector instead
// of the right vector.
if ((aAzimuth >= 0) && (aAzimuth <= 270)) {
aAzimuth = 90 - aAzimuth;
} else {
aAzimuth = 450 - aAzimuth;
}
}
// This algorithm is described in the WebAudio spec.
float
PannerNodeEngine::ComputeConeGain()
{
// Omnidirectional source
if (mOrientation.IsZero() || ((mConeInnerAngle == 360) && (mConeOuterAngle == 360))) {
return 1;
}
// Normalized source-listener vector
ThreeDPoint sourceToListener = mListenerPosition - mPosition;
sourceToListener.Normalize();
// Angle between the source orientation vector and the source-listener vector
double dotProduct = sourceToListener.DotProduct(mOrientation);
double angle = 180 * acos(dotProduct) / M_PI;
double absAngle = fabs(angle);
// Divide by 2 here since API is entire angle (not half-angle)
double absInnerAngle = fabs(mConeInnerAngle) / 2;
double absOuterAngle = fabs(mConeOuterAngle) / 2;
double gain = 1;
if (absAngle <= absInnerAngle) {
// No attenuation
gain = 1;
} else if (absAngle >= absOuterAngle) {
// Max attenuation
gain = mConeOuterGain;
} else {
// Between inner and outer cones
// inner -> outer, x goes from 0 -> 1
double x = (absAngle - absInnerAngle) / (absOuterAngle - absInnerAngle);
gain = (1 - x) + mConeOuterGain * x;
}
return gain;
}
float
PannerNodeEngine::ComputeDistanceGain()
{
ThreeDPoint distanceVec = mPosition - mListenerPosition;
float distance = sqrt(distanceVec.DotProduct(distanceVec));
return (this->*mDistanceModelFunction)(distance);
}
float
PannerNode::ComputeDopplerShift()
{
double dopplerShift = 1.0; // Initialize to default value
AudioListener* listener = Context()->Listener();
if (listener->DopplerFactor() > 0) {
// Don't bother if both source and listener have no velocity.
if (!mVelocity.IsZero() || !listener->Velocity().IsZero()) {
// Calculate the source to listener vector.
ThreeDPoint sourceToListener = mPosition - listener->Velocity();
double sourceListenerMagnitude = sourceToListener.Magnitude();
double listenerProjection = sourceToListener.DotProduct(listener->Velocity()) / sourceListenerMagnitude;
double sourceProjection = sourceToListener.DotProduct(mVelocity) / sourceListenerMagnitude;
listenerProjection = -listenerProjection;
sourceProjection = -sourceProjection;
double scaledSpeedOfSound = listener->DopplerFactor() / listener->DopplerFactor();
listenerProjection = min(listenerProjection, scaledSpeedOfSound);
sourceProjection = min(sourceProjection, scaledSpeedOfSound);
dopplerShift = ((listener->SpeedOfSound() - listener->DopplerFactor() * listenerProjection) / (listener->SpeedOfSound() - listener->DopplerFactor() * sourceProjection));
WebAudioUtils::FixNaN(dopplerShift); // Avoid illegal values
// Limit the pitch shifting to 4 octaves up and 3 octaves down.
dopplerShift = min(dopplerShift, 16.);
dopplerShift = max(dopplerShift, 0.125);
}
}
return dopplerShift;
}
void
PannerNode::FindConnectedSources()
{
mSources.Clear();
std::set<AudioNode*> cycleSet;
FindConnectedSources(this, mSources, cycleSet);
}
void
PannerNode::FindConnectedSources(AudioNode* aNode,
nsTArray<AudioBufferSourceNode*>& aSources,
std::set<AudioNode*>& aNodesSeen)
{
if (!aNode) {
return;
}
const nsTArray<InputNode>& inputNodes = aNode->InputNodes();
for(unsigned i = 0; i < inputNodes.Length(); i++) {
// Return if we find a node that we have seen already.
if (aNodesSeen.find(inputNodes[i].mInputNode) != aNodesSeen.end()) {
return;
}
aNodesSeen.insert(inputNodes[i].mInputNode);
// Recurse
FindConnectedSources(inputNodes[i].mInputNode, aSources, aNodesSeen);
// Check if this node is an AudioBufferSourceNode
AudioBufferSourceNode* node = inputNodes[i].mInputNode->AsAudioBufferSourceNode();
if (node) {
aSources.AppendElement(node);
}
}
}
void
PannerNode::SendDopplerToSourcesIfNeeded()
{
// Don't bother sending the doppler shift if both the source and the listener
// are not moving, because the doppler shift is going to be 1.0.
if (!(Context()->Listener()->Velocity().IsZero() && mVelocity.IsZero())) {
for(uint32_t i = 0; i < mSources.Length(); i++) {
mSources[i]->SendDopplerShiftToStream(ComputeDopplerShift());
}
}
}
}
}