/* 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 "AlignmentUtils.h"
#include "AudioBufferSourceNode.h"
#include "AudioDestinationNode.h"
#include "AudioListener.h"
#include "AudioNodeEngine.h"
#include "AudioNodeTrack.h"
#include "PanningUtils.h"
#include "PlayingRefChangeHandler.h"
#include "Tracing.h"
#include "blink/HRTFDatabaseLoader.h"
#include "blink/HRTFPanner.h"
using WebCore::HRTFDatabaseLoader;
using WebCore::HRTFPanner;
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_CLASS(PannerNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PannerNode, AudioNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPositionX, mPositionY, mPositionZ,
mOrientationX, mOrientationY, mOrientationZ)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PannerNode, AudioNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositionX, mPositionY, mPositionZ,
mOrientationX, mOrientationY, mOrientationZ)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PannerNode)
NS_INTERFACE_MAP_END_INHERITING(AudioNode)
NS_IMPL_ADDREF_INHERITED(PannerNode, AudioNode)
NS_IMPL_RELEASE_INHERITED(PannerNode, AudioNode)
class PannerNodeEngine final : public AudioNodeEngine {
public:
explicit PannerNodeEngine(AudioNode* aNode,
AudioDestinationNode* aDestination,
AudioListenerEngine* aListenerEngine)
: AudioNodeEngine(aNode),
mDestination(aDestination->Track()),
mListenerEngine(aListenerEngine)
// Please keep these default values consistent with
// PannerNode::PannerNode below.
,
mPanningModelFunction(&PannerNodeEngine::EqualPowerPanningFunction),
mDistanceModelFunction(&PannerNodeEngine::InverseGainFunction),
mPositionX(
0.),
mPositionY(
0.),
mPositionZ(
0.),
mOrientationX(
1.),
mOrientationY(
0.),
mOrientationZ(
0.),
mRefDistance(
1.),
mMaxDistance(
10000.),
mRolloffFactor(
1.),
mConeInnerAngle(
360.),
mConeOuterAngle(
360.),
mConeOuterGain(
0.),
mLeftOverData(INT_MIN) {}
void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override {
MOZ_ASSERT(mDestination);
aEvent.ConvertToTicks(mDestination);
switch (aIndex) {
case PannerNode::POSITIONX:
mPositionX.InsertEvent<int64_t>(aEvent);
break;
case PannerNode::POSITIONY:
mPositionY.InsertEvent<int64_t>(aEvent);
break;
case PannerNode::POSITIONZ:
mPositionZ.InsertEvent<int64_t>(aEvent);
break;
case PannerNode::ORIENTATIONX:
mOrientationX.InsertEvent<int64_t>(aEvent);
break;
case PannerNode::ORIENTATIONY:
mOrientationY.InsertEvent<int64_t>(aEvent);
break;
case PannerNode::ORIENTATIONZ:
mOrientationZ.InsertEvent<int64_t>(aEvent);
break;
default:
NS_ERROR(
"Bad PannerNode TimelineParameter");
}
}
void CreateHRTFPanner() {
MOZ_ASSERT(NS_IsMainThread());
if (mHRTFPanner) {
return;
}
// HRTFDatabaseLoader needs to be fetched on the main thread.
RefPtr<HRTFDatabaseLoader> loader =
HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(
NodeMainThread()->Context()->SampleRate());
mHRTFPanner = MakeUnique<HRTFPanner>(
NodeMainThread()->Context()->SampleRate(), loader.forget());
}
void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override {
switch (aIndex) {
case PannerNode::PANNING_MODEL:
switch (PanningModelType(aParam)) {
case PanningModelType::Equalpower:
mPanningModelFunction =
&PannerNodeEngine::EqualPowerPanningFunction;
break;
case PanningModelType::HRTF:
mPanningModelFunction = &PannerNodeEngine::HRTFPanningFunction;
break;
default:
MOZ_ASSERT_UNREACHABLE(
"We should never see alternate names here");
break;
}
break;
case PannerNode::DISTANCE_MODEL:
switch (DistanceModelType(aParam)) {
case DistanceModelType::Inverse:
mDistanceModelFunction = &PannerNodeEngine::InverseGainFunction;
break;
case DistanceModelType::Linear:
mDistanceModelFunction = &PannerNodeEngine::LinearGainFunction;
break;
case DistanceModelType::Exponential:
mDistanceModelFunction = &PannerNodeEngine::ExponentialGainFunction;
break;
default:
MOZ_ASSERT_UNREACHABLE(
"We should never see alternate names here");
break;
}
break;
default:
NS_ERROR(
"Bad PannerNodeEngine Int32Parameter");
}
}
void SetDoubleParameter(uint32_t aIndex,
double aParam) override {
switch (aIndex) {
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");
}
}
void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
const AudioBlock& aInput, AudioBlock* aOutput,
bool* aFinished) override {
TRACE(
"PannerNodeEngine::ProcessBlock");
if (aInput.IsNull()) {
// mLeftOverData != INT_MIN means that the panning model was HRTF and a
// tail-time reference was added. Even if the model is now equalpower,
// the reference will need to be removed.
if (mLeftOverData >
0 &&
mPanningModelFunction == &PannerNodeEngine::HRTFPanningFunction) {
mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
}
else {
if (mLeftOverData != INT_MIN) {
mLeftOverData = INT_MIN;
aTrack->ScheduleCheckForInactive();
mHRTFPanner->reset();
RefPtr<PlayingRefChangeHandler> refchanged =
new PlayingRefChangeHandler(aTrack,
PlayingRefChangeHandler::RELEASE);
aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
}
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
return;
}
}
else if (mPanningModelFunction ==
&PannerNodeEngine::HRTFPanningFunction) {
if (mLeftOverData == INT_MIN) {
RefPtr<PlayingRefChangeHandler> refchanged =
new PlayingRefChangeHandler(aTrack,
PlayingRefChangeHandler::ADDREF);
aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
}
mLeftOverData = mHRTFPanner->maxTailFrames();
}
TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
(this->*mPanningModelFunction)(aInput, aOutput, tick);
}
bool IsActive()
const override {
return mLeftOverData != INT_MIN; }
void ComputeAzimuthAndElevation(
const ThreeDPoint& position,
float& aAzimuth,
float& aElevation);
float ComputeConeGain(
const ThreeDPoint& position,
const ThreeDPoint& orientation);
// Compute how much the distance contributes to the gain reduction.
double ComputeDistanceGain(
const ThreeDPoint& position);
void EqualPowerPanningFunction(
const AudioBlock& aInput, AudioBlock* aOutput,
TrackTime tick);
void HRTFPanningFunction(
const AudioBlock& aInput, AudioBlock* aOutput,
TrackTime tick);
float LinearGainFunction(
double aDistance);
float InverseGainFunction(
double aDistance);
float ExponentialGainFunction(
double aDistance);
ThreeDPoint ConvertAudioParamTimelineTo3DP(AudioParamTimeline& aX,
AudioParamTimeline& aY,
AudioParamTimeline& aZ,
TrackTime& tick);
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)
const override {
size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
if (mHRTFPanner) {
amount += mHRTFPanner->sizeOfIncludingThis(aMallocSizeOf);
}
return amount;
}
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
const override {
return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
}
RefPtr<AudioNodeTrack> mDestination;
// This member is set on the main thread, but is not accessed on the rendering
// thread untile mPanningModelFunction has changed, and this happens strictly
// later, via a MediaTrackGraph ControlMessage.
UniquePtr<HRTFPanner> mHRTFPanner;
RefPtr<AudioListenerEngine> mListenerEngine;
using PanningModelFunction =
void (PannerNodeEngine::*)(
const AudioBlock&,
AudioBlock*,
TrackTime);
PanningModelFunction mPanningModelFunction;
using DistanceModelFunction =
float (PannerNodeEngine::*)(
double);
DistanceModelFunction mDistanceModelFunction;
AudioParamTimeline mPositionX;
AudioParamTimeline mPositionY;
AudioParamTimeline mPositionZ;
AudioParamTimeline mOrientationX;
AudioParamTimeline mOrientationY;
AudioParamTimeline mOrientationZ;
double mRefDistance;
double mMaxDistance;
double mRolloffFactor;
double mConeInnerAngle;
double mConeOuterAngle;
double mConeOuterGain;
int mLeftOverData;
};
PannerNode::PannerNode(AudioContext* aContext)
: AudioNode(aContext,
2, ChannelCountMode::Clamped_max,
ChannelInterpretation::Speakers)
// Please keep these default values consistent with
// PannerNodeEngine::PannerNodeEngine above.
,
mPanningModel(PanningModelType::Equalpower),
mDistanceModel(DistanceModelType::Inverse),
mRefDistance(
1.),
mMaxDistance(
10000.),
mRolloffFactor(
1.),
mConeInnerAngle(
360.),
mConeOuterAngle(
360.),
mConeOuterGain(
0.) {
mPositionX = CreateAudioParam(PannerNode::POSITIONX, u
"PositionX"_ns,
0.f);
mPositionY = CreateAudioParam(PannerNode::POSITIONY, u
"PositionY"_ns,
0.f);
mPositionZ = CreateAudioParam(PannerNode::POSITIONZ, u
"PositionZ"_ns,
0.f);
mOrientationX =
CreateAudioParam(PannerNode::ORIENTATIONX, u
"OrientationX"_ns,
1.
0f);
mOrientationY =
CreateAudioParam(PannerNode::ORIENTATIONY, u
"OrientationY"_ns,
0.f);
mOrientationZ =
CreateAudioParam(PannerNode::ORIENTATIONZ, u
"OrientationZ"_ns,
0.f);
mTrack = AudioNodeTrack::Create(
aContext,
new PannerNodeEngine(this, aContext->Destination(),
aContext->Listener()->Engine()),
AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
}
/* static */
already_AddRefed<PannerNode> PannerNode::Create(AudioContext& aAudioContext,
const PannerOptions& aOptions,
ErrorResult& aRv) {
RefPtr<PannerNode> audioNode =
new PannerNode(&aAudioContext);
audioNode->Initialize(aOptions, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
audioNode->SetPanningModel(aOptions.mPanningModel);
audioNode->SetDistanceModel(aOptions.mDistanceModel);
audioNode->mPositionX->SetInitialValue(aOptions.mPositionX);
audioNode->mPositionY->SetInitialValue(aOptions.mPositionY);
audioNode->mPositionZ->SetInitialValue(aOptions.mPositionZ);
audioNode->mOrientationX->SetInitialValue(aOptions.mOrientationX);
audioNode->mOrientationY->SetInitialValue(aOptions.mOrientationY);
audioNode->mOrientationZ->SetInitialValue(aOptions.mOrientationZ);
audioNode->SetRefDistance(aOptions.mRefDistance, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
audioNode->SetMaxDistance(aOptions.mMaxDistance, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
audioNode->SetRolloffFactor(aOptions.mRolloffFactor, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
audioNode->SetConeInnerAngle(aOptions.mConeInnerAngle);
audioNode->SetConeOuterAngle(aOptions.mConeOuterAngle);
audioNode->SetConeOuterGain(aOptions.mConeOuterGain, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
return audioNode.forget();
}
void PannerNode::SetPanningModel(PanningModelType aPanningModel) {
mPanningModel = aPanningModel;
if (mPanningModel == PanningModelType::HRTF) {
// We can set the engine's `mHRTFPanner` member here from the main thread,
// because the engine will not touch it from the MediaTrackGraph
// thread until the PANNING_MODEL message sent below is received.
static_cast<PannerNodeEngine*>(mTrack->Engine())->CreateHRTFPanner();
}
SendInt32ParameterToTrack(PANNING_MODEL, int32_t(mPanningModel));
}
void PannerNode::SetPosition(
float aX,
float aY,
float aZ, ErrorResult& aRv) {
mPositionX->SetValue(aX, aRv);
if (aRv.Failed()) {
return;
}
mPositionY->SetValue(aY, aRv);
if (aRv.Failed()) {
return;
}
mPositionZ->SetValue(aZ, aRv);
}
void PannerNode::SetOrientation(
float aX,
float aY,
float aZ,
ErrorResult& aRv) {
mOrientationX->SetValue(aX, aRv);
if (aRv.Failed()) {
return;
}
mOrientationY->SetValue(aY, aRv);
if (aRv.Failed()) {
return;
}
mOrientationZ->SetValue(aZ, aRv);
}
size_t PannerNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)
const {
return AudioNode::SizeOfExcludingThis(aMallocSizeOf);
}
size_t PannerNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
const {
return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
}
JSObject* PannerNode::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return PannerNode_Binding::Wrap(aCx, this, aGivenProto);
}
// Those three functions are described in the spec.
float PannerNodeEngine::LinearGainFunction(
double aDistance) {
double clampedRollof = std::clamp(mRolloffFactor,
0.
0,
1.
0);
if (mMaxDistance <= mRefDistance) {
return AssertedCast<
float>(
1.
0 - clampedRollof);
}
return AssertedCast<
float>(
1.
0 - clampedRollof *
(std::max(std::min(aDistance, mMaxDistance), mRefDistance) -
mRefDistance) /
(mMaxDistance - mRefDistance));
}
float PannerNodeEngine::InverseGainFunction(
double aDistance) {
return mRefDistance /
(mRefDistance +
mRolloffFactor * (std::max(aDistance, mRefDistance) - mRefDistance));
}
float PannerNodeEngine::ExponentialGainFunction(
double aDistance) {
return fdlibm_pow(std::max(aDistance, mRefDistance) / mRefDistance,
-mRolloffFactor);
}
void PannerNodeEngine::HRTFPanningFunction(
const AudioBlock& aInput,
AudioBlock* aOutput,
TrackTime tick) {
// The output of this node is always stereo, no matter what the inputs are.
aOutput->AllocateChannels(
2);
float azimuth, elevation;
ThreeDPoint position =
ConvertAudioParamTimelineTo3DP(mPositionX, mPositionY, mPositionZ, tick);
ThreeDPoint orientation = ConvertAudioParamTimelineTo3DP(
mOrientationX, mOrientationY, mOrientationZ, tick);
if (!orientation.IsZero()) {
orientation.Normalize();
}
ComputeAzimuthAndElevation(position, azimuth, elevation);
AudioBlock input = aInput;
// Gain is applied before the delay and convolution of the HRTF.
input.mVolume *=
ComputeConeGain(position, orientation) * ComputeDistanceGain(position);
if (mDestination->Graph()->IsNonRealtime()) {
mHRTFPanner->DatabaseLoader()->waitForLoaderThreadCompletion();
}
mHRTFPanner->pan(azimuth, elevation, &input, aOutput);
}
ThreeDPoint PannerNodeEngine::ConvertAudioParamTimelineTo3DP(
AudioParamTimeline& aX, AudioParamTimeline& aY, AudioParamTimeline& aZ,
TrackTime& tick) {
return ThreeDPoint(aX.GetValueAtTime(tick), aY.GetValueAtTime(tick),
aZ.GetValueAtTime(tick));
}
void PannerNodeEngine::EqualPowerPanningFunction(
const AudioBlock& aInput,
AudioBlock* aOutput,
TrackTime tick) {
float azimuth, elevation, gainL, gainR, normalizedAzimuth, distanceGain,
coneGain;
int inputChannels = aInput.ChannelCount();
// Optimize the case where the position and orientation is constant for this
// processing block: we can just apply a constant gain on the left and right
// channel
if (mPositionX.HasSimpleValue() && mPositionY.HasSimpleValue() &&
mPositionZ.HasSimpleValue() && mOrientationX.HasSimpleValue() &&
mOrientationY.HasSimpleValue() && mOrientationZ.HasSimpleValue()) {
ThreeDPoint position(mPositionX.GetValue(), mPositionY.GetValue(),
mPositionZ.GetValue());
ThreeDPoint orientation(mOrientationX.GetValue(), mOrientationY.GetValue(),
mOrientationZ.GetValue());
if (!orientation.IsZero()) {
orientation.Normalize();
}
// For a stereo source, when both the listener and the panner are in
// the same spot, and no cone gain is specified, this node is noop.
if (inputChannels ==
2 && mListenerEngine->Position() == position &&
mConeInnerAngle ==
360 && mConeOuterAngle ==
360) {
*aOutput = aInput;
return;
}
ComputeAzimuthAndElevation(position, azimuth, elevation);
coneGain = ComputeConeGain(position, orientation);
// The following algorithm is described in the spec.
// Clamp azimuth in the [-90, 90] range.
azimuth = std::min(
180.f, std::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(position);
// Actually compute the left and right gain.
gainL = fdlibm_cos(
0.
5 * M_PI * normalizedAzimuth);
gainR = fdlibm_sin(
0.
5 * M_PI * normalizedAzimuth);
// Compute the output.
ApplyStereoPanning(aInput, aOutput, gainL, gainR, azimuth <=
0);
aOutput->mVolume *= distanceGain * coneGain;
}
else {
float positionX[WEBAUDIO_BLOCK_SIZE];
float positionY[WEBAUDIO_BLOCK_SIZE];
float positionZ[WEBAUDIO_BLOCK_SIZE];
float orientationX[WEBAUDIO_BLOCK_SIZE];
float orientationY[WEBAUDIO_BLOCK_SIZE];
float orientationZ[WEBAUDIO_BLOCK_SIZE];
if (!mPositionX.HasSimpleValue()) {
mPositionX.GetValuesAtTime(tick, positionX, WEBAUDIO_BLOCK_SIZE);
}
else {
positionX[
0] = mPositionX.GetValue();
}
if (!mPositionY.HasSimpleValue()) {
mPositionY.GetValuesAtTime(tick, positionY, WEBAUDIO_BLOCK_SIZE);
}
else {
positionY[
0] = mPositionY.GetValue();
}
if (!mPositionZ.HasSimpleValue()) {
mPositionZ.GetValuesAtTime(tick, positionZ, WEBAUDIO_BLOCK_SIZE);
}
else {
positionZ[
0] = mPositionZ.GetValue();
}
if (!mOrientationX.HasSimpleValue()) {
mOrientationX.GetValuesAtTime(tick, orientationX, WEBAUDIO_BLOCK_SIZE);
}
else {
orientationX[
0] = mOrientationX.GetValue();
}
if (!mOrientationY.HasSimpleValue()) {
mOrientationY.GetValuesAtTime(tick, orientationY, WEBAUDIO_BLOCK_SIZE);
}
else {
orientationY[
0] = mOrientationY.GetValue();
}
if (!mOrientationZ.HasSimpleValue()) {
mOrientationZ.GetValuesAtTime(tick, orientationZ, WEBAUDIO_BLOCK_SIZE);
}
else {
orientationZ[
0] = mOrientationZ.GetValue();
}
float buffer[
3 * WEBAUDIO_BLOCK_SIZE +
4];
alignas(
16)
bool onLeft[WEBAUDIO_BLOCK_SIZE];
float* alignedPanningL = ALIGNED16(buffer);
float* alignedPanningR = alignedPanningL + WEBAUDIO_BLOCK_SIZE;
float* alignedGain = alignedPanningR + WEBAUDIO_BLOCK_SIZE;
ASSERT_ALIGNED16(alignedPanningL);
ASSERT_ALIGNED16(alignedPanningR);
ASSERT_ALIGNED16(alignedGain);
for (size_t counter =
0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
ThreeDPoint position(
mPositionX.HasSimpleValue() ? positionX[
0] : positionX[counter],
mPositionY.HasSimpleValue() ? positionY[
0] : positionY[counter],
mPositionZ.HasSimpleValue() ? positionZ[
0] : positionZ[counter]);
ThreeDPoint orientation(
mOrientationX.HasSimpleValue() ? orientationX[
0]
: orientationX[counter],
mOrientationY.HasSimpleValue() ? orientationY[
0]
: orientationY[counter],
mOrientationZ.HasSimpleValue() ? orientationZ[
0]
: orientationZ[counter]);
if (!orientation.IsZero()) {
orientation.Normalize();
}
ComputeAzimuthAndElevation(position, azimuth, elevation);
coneGain = ComputeConeGain(position, orientation);
// The following algorithm is described in the spec.
// Clamp azimuth in the [-90, 90] range.
azimuth = std::min(
180.f, std::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(position);
// Actually compute the left and right gain.
float gainL = fdlibm_cos(
0.
5 * M_PI * normalizedAzimuth);
float gainR = fdlibm_sin(
0.
5 * M_PI * normalizedAzimuth);
alignedPanningL[counter] = gainL;
alignedPanningR[counter] = gainR;
alignedGain[counter] = distanceGain * coneGain;
onLeft[counter] = azimuth <=
0;
}
// Apply the panning to the output buffer
ApplyStereoPanning(aInput, aOutput, alignedPanningL, alignedPanningR,
onLeft);
// Apply the input volume, cone and distance gain to the output buffer.
float* outputL = aOutput->ChannelFloatsForWrite(
0);
float* outputR = aOutput->ChannelFloatsForWrite(
1);
AudioBlockInPlaceScale(outputL, alignedGain);
AudioBlockInPlaceScale(outputR, alignedGain);
}
}
// With infinite precision, this would be equivalent to the algorithm in
// https://webaudio.github.io/web-audio-api/#azimuth-elevation.
// atan2() is used in place of acos() because the precision of acos(x)
// degrades rapidly as x approaches 1 or -1.
// acos() amplifies small errors in x when x is near 1 or -1.
void PannerNodeEngine::ComputeAzimuthAndElevation(
const ThreeDPoint& position,
float& aAzimuth,
float& aElevation) {
ThreeDPoint sourceListener = position - mListenerEngine->Position();
if (sourceListener.IsZero()) {
aAzimuth =
0.
0;
aElevation =
0.
0;
return;
}
const ThreeDPoint& listenerFront = mListenerEngine->FrontVector();
const ThreeDPoint& listenerRight = mListenerEngine->RightVector();
ThreeDPoint listenerUp = listenerRight.CrossProduct(listenerFront);
// Project source - listener vector onto each listener axis.
double frontProjection = sourceListener.DotProduct(listenerFront);
double rightProjection = sourceListener.DotProduct(listenerRight);
double upProjection = sourceListener.DotProduct(listenerUp);
// Magnitude of source - listener vector projected onto the front-right plane
double planeMagnitude = fdlibm_hypot(frontProjection, rightProjection);
// Up is elevation 90; down is -90; right-front plane is zero.
// Divide by M_PI before multiplying by 180 so that integer power of two
// factors of pi convert exactly.
aElevation = fdlibm_atan2(upProjection, planeMagnitude) / M_PI *
180;
WebAudioUtils::FixNaN(aElevation);
MOZ_ASSERT(aElevation <=
90);
MOZ_ASSERT(aElevation >= -
90);
if (planeMagnitude ==
0.
0) {
// source - listener direction is up or down.
aAzimuth =
0.
0;
return;
}
// Compute the azimuth, and convert to degrees.
// Right is 90, left is -90; front is zero.
aAzimuth = fdlibm_atan2(rightProjection, frontProjection) / M_PI *
180;
// Back is 180, even when rightProjection is -0.
if (aAzimuth == -
180) {
aAzimuth =
180;
}
else {
WebAudioUtils::FixNaN(aAzimuth);
}
MOZ_ASSERT(aAzimuth <=
180);
MOZ_ASSERT(aAzimuth > -
180);
}
// This algorithm is described in the WebAudio spec.
float PannerNodeEngine::ComputeConeGain(
const ThreeDPoint& position,
const ThreeDPoint& orientation) {
// Omnidirectional source
if (orientation.IsZero() ||
((mConeInnerAngle ==
360) && (mConeOuterAngle ==
360))) {
return 1;
}
// Normalized source-listener vector
ThreeDPoint sourceToListener = mListenerEngine->Position() - position;
sourceToListener.Normalize();
// Angle between the source orientation vector and the source-listener vector
double dotProduct = sourceToListener.DotProduct(orientation);
double angle =
180 * fdlibm_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;
}
double PannerNodeEngine::ComputeDistanceGain(
const ThreeDPoint& position) {
ThreeDPoint distanceVec = position - mListenerEngine->Position();
float distance = sqrt(distanceVec.DotProduct(distanceVec));
return std::max(
0.
0f, (this->*mDistanceModelFunction)(distance));
}
}
// namespace mozilla::dom