blob: 52d7f9af1524bf94120271e6ba49f8fffa830596 [file] [log] [blame]
/*
* Copyright (C) 2010, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
#include "modules/webaudio/PannerNode.h"
#include "bindings/core/v8/ExceptionMessages.h"
#include "bindings/core/v8/ExceptionState.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/ExecutionContext.h"
#include "modules/webaudio/AudioBufferSourceNode.h"
#include "modules/webaudio/AudioNodeInput.h"
#include "modules/webaudio/AudioNodeOutput.h"
#include "modules/webaudio/BaseAudioContext.h"
#include "modules/webaudio/PannerOptions.h"
#include "platform/Histogram.h"
#include "platform/audio/HRTFPanner.h"
#include "wtf/MathExtras.h"
namespace blink {
static void fixNANs(double& x) {
if (std::isnan(x) || std::isinf(x))
x = 0.0;
}
PannerHandler::PannerHandler(AudioNode& node,
float sampleRate,
AudioParamHandler& positionX,
AudioParamHandler& positionY,
AudioParamHandler& positionZ,
AudioParamHandler& orientationX,
AudioParamHandler& orientationY,
AudioParamHandler& orientationZ)
: AudioHandler(NodeTypePanner, node, sampleRate),
m_listener(node.context()->listener()),
m_distanceModel(DistanceEffect::ModelInverse),
m_isAzimuthElevationDirty(true),
m_isDistanceConeGainDirty(true),
m_lastGain(-1.0),
m_cachedAzimuth(0),
m_cachedElevation(0),
m_cachedDistanceConeGain(1.0f),
m_positionX(positionX),
m_positionY(positionY),
m_positionZ(positionZ),
m_orientationX(orientationX),
m_orientationY(orientationY),
m_orientationZ(orientationZ) {
addInput();
addOutput(2);
// Node-specific default mixing rules.
m_channelCount = 2;
setInternalChannelCountMode(ClampedMax);
setInternalChannelInterpretation(AudioBus::Speakers);
// Explicitly set the default panning model here so that the histograms
// include the default value.
setPanningModel("equalpower");
initialize();
}
PassRefPtr<PannerHandler> PannerHandler::create(
AudioNode& node,
float sampleRate,
AudioParamHandler& positionX,
AudioParamHandler& positionY,
AudioParamHandler& positionZ,
AudioParamHandler& orientationX,
AudioParamHandler& orientationY,
AudioParamHandler& orientationZ) {
return adoptRef(new PannerHandler(node, sampleRate, positionX, positionY,
positionZ, orientationX, orientationY,
orientationZ));
}
PannerHandler::~PannerHandler() {
uninitialize();
}
void PannerHandler::process(size_t framesToProcess) {
AudioBus* destination = output(0).bus();
if (!isInitialized() || !input(0).isConnected() || !m_panner.get()) {
destination->zero();
return;
}
AudioBus* source = input(0).bus();
if (!source) {
destination->zero();
return;
}
// The audio thread can't block on this lock, so we call tryLock() instead.
MutexTryLocker tryLocker(m_processLock);
MutexTryLocker tryListenerLocker(listener()->listenerLock());
if (tryLocker.locked() && tryListenerLocker.locked()) {
if (!context()->hasRealtimeConstraint() &&
m_panningModel == Panner::PanningModelHRTF) {
// For an OfflineAudioContext, we need to make sure the HRTFDatabase
// is loaded before proceeding. For realtime contexts, we don't
// have to wait. The HRTF panner handles that case itself.
listener()->waitForHRTFDatabaseLoaderThreadCompletion();
}
if (hasSampleAccurateValues() || listener()->hasSampleAccurateValues()) {
// It's tempting to skip sample-accurate processing if isAzimuthElevationDirty() and
// isDistanceConeGain() both return false. But in general we can't because something
// may scheduled to start in the middle of the rendering quantum. On the other hand,
// the audible effect may be small enough that we can afford to do this optimization.
processSampleAccurateValues(destination, source, framesToProcess);
} else {
// Apply the panning effect.
double azimuth;
double elevation;
// Update dirty state in case something has moved; this can happen if the AudioParam for
// the position or orientation component is set directly.
updateDirtyState();
azimuthElevation(&azimuth, &elevation);
m_panner->pan(azimuth, elevation, source, destination, framesToProcess,
internalChannelInterpretation());
// Get the distance and cone gain.
float totalGain = distanceConeGain();
m_lastGain = totalGain;
// Apply gain in-place with de-zippering.
destination->copyWithGainFrom(*destination, &m_lastGain, totalGain);
}
} else {
// Too bad - The tryLock() failed.
// We must be in the middle of changing the properties of the panner or the listener.
destination->zero();
}
}
void PannerHandler::processSampleAccurateValues(AudioBus* destination,
const AudioBus* source,
size_t framesToProcess) {
RELEASE_ASSERT(framesToProcess <= ProcessingSizeInFrames);
// Get the sample accurate values from all of the AudioParams, including the values from the
// AudioListener.
float pannerX[ProcessingSizeInFrames];
float pannerY[ProcessingSizeInFrames];
float pannerZ[ProcessingSizeInFrames];
float orientationX[ProcessingSizeInFrames];
float orientationY[ProcessingSizeInFrames];
float orientationZ[ProcessingSizeInFrames];
m_positionX->calculateSampleAccurateValues(pannerX, framesToProcess);
m_positionY->calculateSampleAccurateValues(pannerY, framesToProcess);
m_positionZ->calculateSampleAccurateValues(pannerZ, framesToProcess);
m_orientationX->calculateSampleAccurateValues(orientationX, framesToProcess);
m_orientationY->calculateSampleAccurateValues(orientationY, framesToProcess);
m_orientationZ->calculateSampleAccurateValues(orientationZ, framesToProcess);
// Get the automation values from the listener.
const float* listenerX =
listener()->getPositionXValues(ProcessingSizeInFrames);
const float* listenerY =
listener()->getPositionYValues(ProcessingSizeInFrames);
const float* listenerZ =
listener()->getPositionZValues(ProcessingSizeInFrames);
const float* forwardX = listener()->getForwardXValues(ProcessingSizeInFrames);
const float* forwardY = listener()->getForwardYValues(ProcessingSizeInFrames);
const float* forwardZ = listener()->getForwardZValues(ProcessingSizeInFrames);
const float* upX = listener()->getUpXValues(ProcessingSizeInFrames);
const float* upY = listener()->getUpYValues(ProcessingSizeInFrames);
const float* upZ = listener()->getUpZValues(ProcessingSizeInFrames);
// Compute the azimuth, elevation, and total gains for each position.
double azimuth[ProcessingSizeInFrames];
double elevation[ProcessingSizeInFrames];
float totalGain[ProcessingSizeInFrames];
for (unsigned k = 0; k < framesToProcess; ++k) {
FloatPoint3D pannerPosition(pannerX[k], pannerY[k], pannerZ[k]);
FloatPoint3D orientation(orientationX[k], orientationY[k], orientationZ[k]);
FloatPoint3D listenerPosition(listenerX[k], listenerY[k], listenerZ[k]);
FloatPoint3D listenerForward(forwardX[k], forwardY[k], forwardZ[k]);
FloatPoint3D listenerUp(upX[k], upY[k], upZ[k]);
calculateAzimuthElevation(&azimuth[k], &elevation[k], pannerPosition,
listenerPosition, listenerForward, listenerUp);
// Get distance and cone gain
totalGain[k] = calculateDistanceConeGain(pannerPosition, orientation,
listenerPosition);
}
m_panner->panWithSampleAccurateValues(azimuth, elevation, source, destination,
framesToProcess,
internalChannelInterpretation());
destination->copyWithSampleAccurateGainValuesFrom(*destination, totalGain,
framesToProcess);
}
void PannerHandler::initialize() {
if (isInitialized())
return;
m_panner = Panner::create(m_panningModel, sampleRate(),
listener()->hrtfDatabaseLoader());
listener()->addPanner(*this);
// Set the cached values to the current values to start things off. The panner is already
// marked as dirty, so this won't matter.
m_lastPosition = position();
m_lastOrientation = orientation();
AudioHandler::initialize();
}
void PannerHandler::uninitialize() {
if (!isInitialized())
return;
m_panner.reset();
listener()->removePanner(*this);
AudioHandler::uninitialize();
}
AudioListener* PannerHandler::listener() {
return m_listener;
}
String PannerHandler::panningModel() const {
switch (m_panningModel) {
case Panner::PanningModelEqualPower:
return "equalpower";
case Panner::PanningModelHRTF:
return "HRTF";
default:
ASSERT_NOT_REACHED();
return "equalpower";
}
}
void PannerHandler::setPanningModel(const String& model) {
// WebIDL should guarantee that we are never called with an invalid string
// for the model.
if (model == "equalpower")
setPanningModel(Panner::PanningModelEqualPower);
else if (model == "HRTF")
setPanningModel(Panner::PanningModelHRTF);
else
NOTREACHED();
}
// This method should only be called from setPanningModel(const String&)!
bool PannerHandler::setPanningModel(unsigned model) {
DEFINE_STATIC_LOCAL(EnumerationHistogram, panningModelHistogram,
("WebAudio.PannerNode.PanningModel", 2));
panningModelHistogram.count(model);
if (model == Panner::PanningModelHRTF) {
// Load the HRTF database asynchronously so we don't block the
// Javascript thread while creating the HRTF database. It's ok to call
// this multiple times; we won't be constantly loading the database over
// and over.
listener()->createAndLoadHRTFDatabaseLoader(context()->sampleRate());
}
if (!m_panner.get() || model != m_panningModel) {
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_panner =
Panner::create(model, sampleRate(), listener()->hrtfDatabaseLoader());
m_panningModel = model;
}
return true;
}
String PannerHandler::distanceModel() const {
switch (const_cast<PannerHandler*>(this)->m_distanceEffect.model()) {
case DistanceEffect::ModelLinear:
return "linear";
case DistanceEffect::ModelInverse:
return "inverse";
case DistanceEffect::ModelExponential:
return "exponential";
default:
ASSERT_NOT_REACHED();
return "inverse";
}
}
void PannerHandler::setDistanceModel(const String& model) {
if (model == "linear")
setDistanceModel(DistanceEffect::ModelLinear);
else if (model == "inverse")
setDistanceModel(DistanceEffect::ModelInverse);
else if (model == "exponential")
setDistanceModel(DistanceEffect::ModelExponential);
}
bool PannerHandler::setDistanceModel(unsigned model) {
switch (model) {
case DistanceEffect::ModelLinear:
case DistanceEffect::ModelInverse:
case DistanceEffect::ModelExponential:
if (model != m_distanceModel) {
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_distanceEffect.setModel(static_cast<DistanceEffect::ModelType>(model),
true);
m_distanceModel = model;
}
break;
default:
ASSERT_NOT_REACHED();
return false;
}
return true;
}
void PannerHandler::setRefDistance(double distance) {
if (refDistance() == distance)
return;
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_distanceEffect.setRefDistance(distance);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setMaxDistance(double distance) {
if (maxDistance() == distance)
return;
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_distanceEffect.setMaxDistance(distance);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setRolloffFactor(double factor) {
if (rolloffFactor() == factor)
return;
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_distanceEffect.setRolloffFactor(factor);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setConeInnerAngle(double angle) {
if (coneInnerAngle() == angle)
return;
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_coneEffect.setInnerAngle(angle);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setConeOuterAngle(double angle) {
if (coneOuterAngle() == angle)
return;
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_coneEffect.setOuterAngle(angle);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setConeOuterGain(double angle) {
if (coneOuterGain() == angle)
return;
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_coneEffect.setOuterGain(angle);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setPosition(float x, float y, float z) {
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_positionX->setValue(x);
m_positionY->setValue(y);
m_positionZ->setValue(z);
markPannerAsDirty(PannerHandler::AzimuthElevationDirty |
PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::setOrientation(float x, float y, float z) {
// This synchronizes with process().
MutexLocker processLocker(m_processLock);
m_orientationX->setValue(x);
m_orientationY->setValue(y);
m_orientationZ->setValue(z);
markPannerAsDirty(PannerHandler::DistanceConeGainDirty);
}
void PannerHandler::calculateAzimuthElevation(
double* outAzimuth,
double* outElevation,
const FloatPoint3D& position,
const FloatPoint3D& listenerPosition,
const FloatPoint3D& listenerForward,
const FloatPoint3D& listenerUp) {
double azimuth = 0.0;
// Calculate the source-listener vector
FloatPoint3D sourceListener = position - listenerPosition;
// normalize() does nothing if the length of |sourceListener| is zero.
sourceListener.normalize();
// Align axes
FloatPoint3D listenerRight = listenerForward.cross(listenerUp);
listenerRight.normalize();
FloatPoint3D listenerForwardNorm = listenerForward;
listenerForwardNorm.normalize();
FloatPoint3D up = listenerRight.cross(listenerForwardNorm);
float upProjection = sourceListener.dot(up);
FloatPoint3D projectedSource = sourceListener - upProjection * up;
azimuth = rad2deg(projectedSource.angleBetween(listenerRight));
fixNANs(azimuth); // avoid illegal values
// Source in front or behind the listener
double frontBack = projectedSource.dot(listenerForwardNorm);
if (frontBack < 0.0)
azimuth = 360.0 - azimuth;
// Make azimuth relative to "front" and not "right" listener vector
if ((azimuth >= 0.0) && (azimuth <= 270.0))
azimuth = 90.0 - azimuth;
else
azimuth = 450.0 - azimuth;
// Elevation
double elevation = 90 - rad2deg(sourceListener.angleBetween(up));
fixNANs(elevation); // avoid illegal values
if (elevation > 90.0)
elevation = 180.0 - elevation;
else if (elevation < -90.0)
elevation = -180.0 - elevation;
if (outAzimuth)
*outAzimuth = azimuth;
if (outElevation)
*outElevation = elevation;
}
float PannerHandler::calculateDistanceConeGain(
const FloatPoint3D& position,
const FloatPoint3D& orientation,
const FloatPoint3D& listenerPosition) {
double listenerDistance = position.distanceTo(listenerPosition);
double distanceGain = m_distanceEffect.gain(listenerDistance);
double coneGain = m_coneEffect.gain(position, orientation, listenerPosition);
return float(distanceGain * coneGain);
}
void PannerHandler::azimuthElevation(double* outAzimuth, double* outElevation) {
DCHECK(context()->isAudioThread());
// Calculate new azimuth and elevation if the panner or the listener changed
// position or orientation in any way.
if (isAzimuthElevationDirty() || listener()->isListenerDirty()) {
calculateAzimuthElevation(&m_cachedAzimuth, &m_cachedElevation, position(),
listener()->position(), listener()->orientation(),
listener()->upVector());
m_isAzimuthElevationDirty = false;
}
*outAzimuth = m_cachedAzimuth;
*outElevation = m_cachedElevation;
}
float PannerHandler::distanceConeGain() {
DCHECK(context()->isAudioThread());
// Calculate new distance and cone gain if the panner or the listener
// changed position or orientation in any way.
if (isDistanceConeGainDirty() || listener()->isListenerDirty()) {
m_cachedDistanceConeGain = calculateDistanceConeGain(
position(), orientation(), listener()->position());
m_isDistanceConeGainDirty = false;
}
return m_cachedDistanceConeGain;
}
void PannerHandler::markPannerAsDirty(unsigned dirty) {
if (dirty & PannerHandler::AzimuthElevationDirty)
m_isAzimuthElevationDirty = true;
if (dirty & PannerHandler::DistanceConeGainDirty)
m_isDistanceConeGainDirty = true;
}
void PannerHandler::setChannelCount(unsigned long channelCount,
ExceptionState& exceptionState) {
DCHECK(isMainThread());
BaseAudioContext::AutoLocker locker(context());
// A PannerNode only supports 1 or 2 channels
if (channelCount > 0 && channelCount <= 2) {
if (m_channelCount != channelCount) {
m_channelCount = channelCount;
if (internalChannelCountMode() != Max)
updateChannelsForInputs();
}
} else {
exceptionState.throwDOMException(
NotSupportedError,
ExceptionMessages::indexOutsideRange<unsigned long>(
"channelCount", channelCount, 1, ExceptionMessages::InclusiveBound,
2, ExceptionMessages::InclusiveBound));
}
}
void PannerHandler::setChannelCountMode(const String& mode,
ExceptionState& exceptionState) {
DCHECK(isMainThread());
BaseAudioContext::AutoLocker locker(context());
ChannelCountMode oldMode = internalChannelCountMode();
if (mode == "clamped-max") {
m_newChannelCountMode = ClampedMax;
} else if (mode == "explicit") {
m_newChannelCountMode = Explicit;
} else if (mode == "max") {
// This is not supported for a PannerNode, which can only handle 1 or 2 channels.
exceptionState.throwDOMException(NotSupportedError,
"Panner: 'max' is not allowed");
m_newChannelCountMode = oldMode;
} else {
// Do nothing for other invalid values.
m_newChannelCountMode = oldMode;
}
if (m_newChannelCountMode != oldMode)
context()->deferredTaskHandler().addChangedChannelCountMode(this);
}
bool PannerHandler::hasSampleAccurateValues() const {
return m_positionX->hasSampleAccurateValues() ||
m_positionY->hasSampleAccurateValues() ||
m_positionZ->hasSampleAccurateValues() ||
m_orientationX->hasSampleAccurateValues() ||
m_orientationY->hasSampleAccurateValues() ||
m_orientationZ->hasSampleAccurateValues();
}
void PannerHandler::updateDirtyState() {
DCHECK(context()->isAudioThread());
FloatPoint3D currentPosition = position();
FloatPoint3D currentOrientation = orientation();
bool hasMoved = currentPosition != m_lastPosition ||
currentOrientation != m_lastOrientation;
if (hasMoved) {
m_lastPosition = currentPosition;
m_lastOrientation = currentOrientation;
markPannerAsDirty(PannerHandler::AzimuthElevationDirty |
PannerHandler::DistanceConeGainDirty);
}
}
// ----------------------------------------------------------------
PannerNode::PannerNode(BaseAudioContext& context)
: AudioNode(context),
m_positionX(AudioParam::create(context, ParamTypePannerPositionX, 0.0)),
m_positionY(AudioParam::create(context, ParamTypePannerPositionY, 0.0)),
m_positionZ(AudioParam::create(context, ParamTypePannerPositionZ, 0.0)),
m_orientationX(
AudioParam::create(context, ParamTypePannerOrientationX, 1.0)),
m_orientationY(
AudioParam::create(context, ParamTypePannerOrientationY, 0.0)),
m_orientationZ(
AudioParam::create(context, ParamTypePannerOrientationZ, 0.0)) {
setHandler(PannerHandler::create(
*this, context.sampleRate(), m_positionX->handler(),
m_positionY->handler(), m_positionZ->handler(), m_orientationX->handler(),
m_orientationY->handler(), m_orientationZ->handler()));
}
PannerNode* PannerNode::create(BaseAudioContext& context,
ExceptionState& exceptionState) {
DCHECK(isMainThread());
if (context.isContextClosed()) {
context.throwExceptionForClosedState(exceptionState);
return nullptr;
}
return new PannerNode(context);
}
PannerNode* PannerNode::create(BaseAudioContext* context,
const PannerOptions& options,
ExceptionState& exceptionState) {
PannerNode* node = create(*context, exceptionState);
if (!node)
return nullptr;
node->handleChannelOptions(options, exceptionState);
if (options.hasPanningModel())
node->setPanningModel(options.panningModel());
if (options.hasDistanceModel())
node->setDistanceModel(options.distanceModel());
if (options.hasPositionX())
node->positionX()->setValue(options.positionX());
if (options.hasPositionY())
node->positionY()->setValue(options.positionY());
if (options.hasPositionZ())
node->positionZ()->setValue(options.positionZ());
if (options.hasOrientationX())
node->orientationX()->setValue(options.orientationX());
if (options.hasOrientationY())
node->orientationY()->setValue(options.orientationY());
if (options.hasOrientationZ())
node->orientationZ()->setValue(options.orientationZ());
if (options.hasRefDistance())
node->setRefDistance(options.refDistance());
if (options.hasMaxDistance())
node->setMaxDistance(options.maxDistance());
if (options.hasRolloffFactor())
node->setRolloffFactor(options.rolloffFactor());
if (options.hasConeInnerAngle())
node->setConeInnerAngle(options.coneInnerAngle());
if (options.hasConeOuterAngle())
node->setConeOuterAngle(options.coneOuterAngle());
if (options.hasConeOuterGain())
node->setConeOuterGain(options.coneOuterGain());
return node;
}
PannerHandler& PannerNode::pannerHandler() const {
return static_cast<PannerHandler&>(handler());
}
String PannerNode::panningModel() const {
return pannerHandler().panningModel();
}
void PannerNode::setPanningModel(const String& model) {
pannerHandler().setPanningModel(model);
}
void PannerNode::setPosition(float x, float y, float z) {
pannerHandler().setPosition(x, y, z);
}
void PannerNode::setOrientation(float x, float y, float z) {
pannerHandler().setOrientation(x, y, z);
}
void PannerNode::setVelocity(float x, float y, float z) {
// The velocity is not used internally and cannot be read back by scripts,
// so it can be ignored entirely.
}
String PannerNode::distanceModel() const {
return pannerHandler().distanceModel();
}
void PannerNode::setDistanceModel(const String& model) {
pannerHandler().setDistanceModel(model);
}
double PannerNode::refDistance() const {
return pannerHandler().refDistance();
}
void PannerNode::setRefDistance(double distance) {
pannerHandler().setRefDistance(distance);
}
double PannerNode::maxDistance() const {
return pannerHandler().maxDistance();
}
void PannerNode::setMaxDistance(double distance) {
pannerHandler().setMaxDistance(distance);
}
double PannerNode::rolloffFactor() const {
return pannerHandler().rolloffFactor();
}
void PannerNode::setRolloffFactor(double factor) {
pannerHandler().setRolloffFactor(factor);
}
double PannerNode::coneInnerAngle() const {
return pannerHandler().coneInnerAngle();
}
void PannerNode::setConeInnerAngle(double angle) {
pannerHandler().setConeInnerAngle(angle);
}
double PannerNode::coneOuterAngle() const {
return pannerHandler().coneOuterAngle();
}
void PannerNode::setConeOuterAngle(double angle) {
pannerHandler().setConeOuterAngle(angle);
}
double PannerNode::coneOuterGain() const {
return pannerHandler().coneOuterGain();
}
void PannerNode::setConeOuterGain(double gain) {
pannerHandler().setConeOuterGain(gain);
}
DEFINE_TRACE(PannerNode) {
visitor->trace(m_positionX);
visitor->trace(m_positionY);
visitor->trace(m_positionZ);
visitor->trace(m_orientationX);
visitor->trace(m_orientationY);
visitor->trace(m_orientationZ);
AudioNode::trace(visitor);
}
} // namespace blink