blob: ce5795fc53068024a38257e8bea237ecff0f07c9 [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 "config.h"
#include "PannerNode.h"
#if ENABLE(WEB_AUDIO)
#include "AudioBufferSourceNode.h"
#include "AudioBus.h"
#include "AudioContext.h"
#include "AudioNodeInput.h"
#include "AudioNodeOutput.h"
#include "AudioUtilities.h"
#include "ChannelCountMode.h"
#include "HRTFDatabaseLoader.h"
#include "HRTFPanner.h"
#include "ScriptExecutionContext.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/MathExtras.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(PannerNode);
static void fixNANs(double &x)
{
if (std::isnan(x) || std::isinf(x))
x = 0.0;
}
ExceptionOr<Ref<PannerNode>> PannerNode::create(BaseAudioContext& context, const PannerOptions& options)
{
auto panner = adoptRef(*new PannerNode(context, options));
auto result = panner->handleAudioNodeOptions(options, { 2, ChannelCountMode::ClampedMax, ChannelInterpretation::Speakers });
if (result.hasException())
return result.releaseException();
result = panner->setMaxDistanceForBindings(options.maxDistance);
if (result.hasException())
return result.releaseException();
result = panner->setRefDistanceForBindings(options.refDistance);
if (result.hasException())
return result.releaseException();
result = panner->setRolloffFactorForBindings(options.rolloffFactor);
if (result.hasException())
return result.releaseException();
result = panner->setConeOuterGainForBindings(options.coneOuterGain);
if (result.hasException())
return result.releaseException();
return panner;
}
PannerNode::PannerNode(BaseAudioContext& context, const PannerOptions& options)
: AudioNode(context, NodeTypePanner)
// Load the HRTF database asynchronously so we don't block the Javascript thread while creating the HRTF database.
, m_hrtfDatabaseLoader(HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(context.sampleRate()))
, m_panningModel(options.panningModel)
, m_panner(Panner::create(m_panningModel, sampleRate(), m_hrtfDatabaseLoader.ptr()))
, m_positionX(AudioParam::create(context, "positionX"_s, options.positionX, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
, m_positionY(AudioParam::create(context, "positionY"_s, options.positionY, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
, m_positionZ(AudioParam::create(context, "positionZ"_s, options.positionZ, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
, m_orientationX(AudioParam::create(context, "orientationX"_s, options.orientationX, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
, m_orientationY(AudioParam::create(context, "orientationY"_s, options.orientationY, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
, m_orientationZ(AudioParam::create(context, "orientationZ"_s, options.orientationZ, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
{
setDistanceModelForBindings(options.distanceModel);
setConeInnerAngleForBindings(options.coneInnerAngle);
setConeOuterAngleForBindings(options.coneOuterAngle);
addInput();
addOutput(2);
initialize();
}
PannerNode::~PannerNode()
{
uninitialize();
}
void PannerNode::process(size_t framesToProcess)
{
AudioBus* destination = output(0)->bus();
if (!isInitialized() || !input(0)->isConnected()) {
destination->zero();
return;
}
AudioBus* source = input(0)->bus();
if (!source) {
destination->zero();
return;
}
// The audio thread can't block on this lock, so we use tryLock() instead.
if (!m_processLock.tryLock()) {
// Too bad - tryLock() failed. We must be in the middle of changing the panner.
destination->zero();
return;
}
Locker locker { AdoptLock, m_processLock };
if (!m_panner) {
destination->zero();
return;
}
// HRTFDatabase should be loaded before proceeding for offline audio context when m_panningModel is "HRTF".
if (m_panningModel == PanningModelType::HRTF && !m_hrtfDatabaseLoader->isLoaded()) {
if (context().isOfflineContext())
m_hrtfDatabaseLoader->waitForLoaderThreadCompletion();
else {
destination->zero();
return;
}
}
invalidateCachedPropertiesIfNecessary();
if ((hasSampleAccurateValues() || listener().hasSampleAccurateValues()) && (shouldUseARate() || listener().shouldUseARate())) {
processSampleAccurateValues(destination, source, framesToProcess);
return;
}
// Apply the panning effect.
auto [azimuth, elevation] = azimuthElevation();
m_panner->pan(azimuth, elevation, source, destination, framesToProcess);
// Get the distance and cone gain.
double totalGain = distanceConeGain();
// Apply gain in-place.
destination->copyWithGainFrom(*destination, totalGain);
}
void PannerNode::processOnlyAudioParams(size_t framesToProcess)
{
ASSERT(context().isAudioThread());
if (!m_processLock.tryLock())
return;
Locker locker { AdoptLock, m_processLock };
float values[AudioUtilities::renderQuantumSize];
ASSERT(framesToProcess <= AudioUtilities::renderQuantumSize);
m_positionX->calculateSampleAccurateValues(values, framesToProcess);
m_positionY->calculateSampleAccurateValues(values, framesToProcess);
m_positionZ->calculateSampleAccurateValues(values, framesToProcess);
m_orientationX->calculateSampleAccurateValues(values, framesToProcess);
m_orientationY->calculateSampleAccurateValues(values, framesToProcess);
m_orientationZ->calculateSampleAccurateValues(values, framesToProcess);
listener().updateValuesIfNeeded(framesToProcess);
}
void PannerNode::processSampleAccurateValues(AudioBus* destination, const AudioBus* source, size_t framesToProcess)
{
// Get the sample accurate values from all of the AudioParams, including the
// values from the AudioListener.
float pannerX[AudioUtilities::renderQuantumSize];
float pannerY[AudioUtilities::renderQuantumSize];
float pannerZ[AudioUtilities::renderQuantumSize];
float orientationX[AudioUtilities::renderQuantumSize];
float orientationY[AudioUtilities::renderQuantumSize];
float orientationZ[AudioUtilities::renderQuantumSize];
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().positionXValues(AudioUtilities::renderQuantumSize);
const float* listenerY = listener().positionYValues(AudioUtilities::renderQuantumSize);
const float* listenerZ = listener().positionZValues(AudioUtilities::renderQuantumSize);
const float* forwardX = listener().forwardXValues(AudioUtilities::renderQuantumSize);
const float* forwardY = listener().forwardYValues(AudioUtilities::renderQuantumSize);
const float* forwardZ = listener().forwardZValues(AudioUtilities::renderQuantumSize);
const float* upX = listener().upXValues(AudioUtilities::renderQuantumSize);
const float* upY = listener().upYValues(AudioUtilities::renderQuantumSize);
const float* upZ = listener().upZValues(AudioUtilities::renderQuantumSize);
// Compute the azimuth, elevation, and total gains for each position.
double azimuth[AudioUtilities::renderQuantumSize];
double elevation[AudioUtilities::renderQuantumSize];
float totalGain[AudioUtilities::renderQuantumSize];
for (size_t 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 listenerFront(forwardX[k], forwardY[k], forwardZ[k]);
FloatPoint3D listenerUp(upX[k], upY[k], upZ[k]);
auto [calculatedAzimuth, calculatedElevation] = calculateAzimuthElevation(pannerPosition, listenerPosition, listenerFront, listenerUp);
azimuth[k] = calculatedAzimuth;
elevation[k] = calculatedElevation;
// Get distance and cone gain
totalGain[k] = calculateDistanceConeGain(pannerPosition, orientation, listenerPosition, m_distanceEffect, m_coneEffect);
}
m_panner->panWithSampleAccurateValues(azimuth, elevation, source, destination, framesToProcess);
destination->copyWithSampleAccurateGainValuesFrom(*destination, totalGain, framesToProcess);
}
bool PannerNode::hasSampleAccurateValues() const
{
return m_positionX->hasSampleAccurateValues()
|| m_positionY->hasSampleAccurateValues()
|| m_positionZ->hasSampleAccurateValues()
|| m_orientationX->hasSampleAccurateValues()
|| m_orientationY->hasSampleAccurateValues()
|| m_orientationZ->hasSampleAccurateValues();
}
bool PannerNode::shouldUseARate() const
{
return m_positionX->automationRate() == AutomationRate::ARate
|| m_positionY->automationRate() == AutomationRate::ARate
|| m_positionZ->automationRate() == AutomationRate::ARate
|| m_orientationX->automationRate() == AutomationRate::ARate
|| m_orientationY->automationRate() == AutomationRate::ARate
|| m_orientationZ->automationRate() == AutomationRate::ARate;
}
AudioListener& PannerNode::listener()
{
return context().listener();
}
void PannerNode::setPanningModelForBindings(PanningModelType model)
{
ASSERT(isMainThread());
// This synchronizes with process().
Locker locker { m_processLock };
if (!m_panner || model != m_panningModel) {
m_panner = Panner::create(model, sampleRate(), m_hrtfDatabaseLoader.ptr());
m_panningModel = model;
}
}
FloatPoint3D PannerNode::position() const
{
return FloatPoint3D(m_positionX->value(), m_positionY->value(), m_positionZ->value());
}
ExceptionOr<void> PannerNode::setPosition(float x, float y, float z)
{
ASSERT(isMainThread());
// This synchronizes with process().
Locker locker { m_processLock };
auto now = context().currentTime();
auto result = m_positionX->setValueAtTime(x, now);
if (result.hasException())
return result.releaseException();
result = m_positionY->setValueAtTime(y, now);
if (result.hasException())
return result.releaseException();
result = m_positionZ->setValueAtTime(z, now);
if (result.hasException())
return result.releaseException();
return { };
}
FloatPoint3D PannerNode::orientation() const
{
return FloatPoint3D(m_orientationX->value(), m_orientationY->value(), m_orientationZ->value());
}
ExceptionOr<void> PannerNode::setOrientation(float x, float y, float z)
{
ASSERT(isMainThread());
// This synchronizes with process().
Locker locker { m_processLock };
auto now = context().currentTime();
auto result = m_orientationX->setValueAtTime(x, now);
if (result.hasException())
return result.releaseException();
result = m_orientationY->setValueAtTime(y, now);
if (result.hasException())
return result.releaseException();
result = m_orientationZ->setValueAtTime(z, now);
if (result.hasException())
return result.releaseException();
return { };
}
DistanceModelType PannerNode::distanceModelForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS
{
ASSERT(isMainThread());
return m_distanceEffect.model();
}
void PannerNode::setDistanceModelForBindings(DistanceModelType model)
{
ASSERT(isMainThread());
// This synchronizes with process().
Locker locker { m_processLock };
if (m_distanceEffect.model() == model)
return;
m_distanceEffect.setModel(model, true);
m_cachedConeGain = std::nullopt;
}
ExceptionOr<void> PannerNode::setRefDistanceForBindings(double refDistance)
{
ASSERT(isMainThread());
if (refDistance < 0)
return Exception { RangeError, "refDistance cannot be set to a negative value"_s };
// This synchronizes with process().
Locker locker { m_processLock };
if (m_distanceEffect.refDistance() == refDistance)
return { };
m_distanceEffect.setRefDistance(refDistance);
m_cachedConeGain = std::nullopt;
return { };
}
ExceptionOr<void> PannerNode::setMaxDistanceForBindings(double maxDistance)
{
ASSERT(isMainThread());
if (maxDistance <= 0)
return Exception { RangeError, "maxDistance cannot be set to a non-positive value"_s };
// This synchronizes with process().
Locker locker { m_processLock };
if (m_distanceEffect.maxDistance() == maxDistance)
return { };
m_distanceEffect.setMaxDistance(maxDistance);
m_cachedConeGain = std::nullopt;
return { };
}
ExceptionOr<void> PannerNode::setRolloffFactorForBindings(double rolloffFactor)
{
ASSERT(isMainThread());
if (rolloffFactor < 0)
return Exception { RangeError, "rolloffFactor cannot be set to a negative value"_s };
// This synchronizes with process().
Locker locker { m_processLock };
if (m_distanceEffect.rolloffFactor() == rolloffFactor)
return { };
m_distanceEffect.setRolloffFactor(rolloffFactor);
m_cachedConeGain = std::nullopt;
return { };
}
ExceptionOr<void> PannerNode::setConeOuterGainForBindings(double gain)
{
ASSERT(isMainThread());
if (gain < 0 || gain > 1)
return Exception { InvalidStateError, "coneOuterGain must be in [0, 1]"_s };
// This synchronizes with process().
Locker locker { m_processLock };
if (m_coneEffect.outerGain() == gain)
return { };
m_coneEffect.setOuterGain(gain);
m_cachedConeGain = std::nullopt;
return { };
}
void PannerNode::setConeOuterAngleForBindings(double angle)
{
ASSERT(isMainThread());
// This synchronizes with process().
Locker locker { m_processLock };
if (m_coneEffect.outerAngle() == angle)
return;
m_coneEffect.setOuterAngle(angle);
m_cachedConeGain = std::nullopt;
}
void PannerNode::setConeInnerAngleForBindings(double angle)
{
ASSERT(isMainThread());
// This synchronizes with process().
Locker locker { m_processLock };
if (m_coneEffect.innerAngle() == angle)
return;
m_coneEffect.setInnerAngle(angle);
m_cachedConeGain = std::nullopt;
}
ExceptionOr<void> PannerNode::setChannelCount(unsigned channelCount)
{
ASSERT(isMainThread());
if (channelCount > 2)
return Exception { NotSupportedError, "PannerNode's channelCount cannot be greater than 2"_s };
return AudioNode::setChannelCount(channelCount);
}
ExceptionOr<void> PannerNode::setChannelCountMode(ChannelCountMode mode)
{
ASSERT(isMainThread());
if (mode == ChannelCountMode::Max)
return Exception { NotSupportedError, "PannerNode's channelCountMode cannot be max"_s };
return AudioNode::setChannelCountMode(mode);
}
auto PannerNode::calculateAzimuthElevation(const FloatPoint3D& position, const FloatPoint3D& listenerPosition, const FloatPoint3D& listenerFront, const FloatPoint3D& listenerUp) -> AzimuthElevation
{
// Calculate the source-listener vector
FloatPoint3D sourceListener = position - listenerPosition;
if (sourceListener.isZero()) {
// degenerate case if source and listener are at the same point
return { };
}
sourceListener.normalize();
// Align axes
FloatPoint3D listenerRight = listenerFront.cross(listenerUp);
listenerRight.normalize();
FloatPoint3D listenerFrontNorm = listenerFront;
listenerFrontNorm.normalize();
FloatPoint3D up = listenerRight.cross(listenerFrontNorm);
float upProjection = sourceListener.dot(up);
FloatPoint3D projectedSource = sourceListener - upProjection * up;
projectedSource.normalize();
double azimuth = rad2deg(std::acos(std::clamp(projectedSource.dot(listenerRight), -1.0f, 1.0f)));
fixNANs(azimuth); // avoid illegal values
// Source in front or behind the listener
double frontBack = projectedSource.dot(listenerFrontNorm);
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.0 - 180.0 * acos(sourceListener.dot(up)) / piDouble;
fixNANs(elevation); // avoid illegal values
if (elevation > 90.0)
elevation = 180.0 - elevation;
else if (elevation < -90.0)
elevation = -180.0 - elevation;
return { azimuth, elevation };
}
auto PannerNode::azimuthElevation() -> const AzimuthElevation&
{
ASSERT(context().isAudioThread());
auto& listener = this->listener();
if (!m_cachedAzimuthElevation)
m_cachedAzimuthElevation = calculateAzimuthElevation(position(), listener.position(), listener.orientation(), listener.upVector());
return *m_cachedAzimuthElevation;
}
bool PannerNode::requiresTailProcessing() const
{
if (!m_processLock.tryLock())
return true;
Locker locker { AdoptLock, m_processLock };
// If there's no internal panner method set up yet, assume we require tail
// processing in case the HRTF panner is set later, which does require tail
// processing.
return !m_panner || m_panner->requiresTailProcessing();
}
float PannerNode::calculateDistanceConeGain(const FloatPoint3D& sourcePosition, const FloatPoint3D& orientation, const FloatPoint3D& listenerPosition, const DistanceEffect& distanceEffect, const ConeEffect& coneEffect)
{
double listenerDistance = sourcePosition.distanceTo(listenerPosition);
double distanceGain = distanceEffect.gain(listenerDistance);
double coneGain = coneEffect.gain(sourcePosition, orientation, listenerPosition);
return float(distanceGain * coneGain);
}
float PannerNode::distanceConeGain()
{
ASSERT(context().isAudioThread());
if (!m_cachedConeGain)
m_cachedConeGain = calculateDistanceConeGain(position(), orientation(), listener().position(), m_distanceEffect, m_coneEffect);
return *m_cachedConeGain;
}
double PannerNode::tailTime() const
{
if (!m_processLock.tryLock())
return std::numeric_limits<double>::infinity();
Locker locker { AdoptLock, m_processLock };
return m_panner ? m_panner->tailTime() : 0;
}
double PannerNode::latencyTime() const
{
if (!m_processLock.tryLock())
return std::numeric_limits<double>::infinity();
Locker locker { AdoptLock, m_processLock };
return m_panner ? m_panner->latencyTime() : 0;
}
void PannerNode::invalidateCachedPropertiesIfNecessary()
{
auto lastPosition = std::exchange(m_lastPosition, position());
bool hasPositionChanged = m_lastPosition != lastPosition;
auto lastOrientation = std::exchange(m_lastOrientation, position());
bool hasOrientationChanged = m_lastOrientation != lastOrientation;
auto& listener = this->listener();
if (hasPositionChanged || listener.isPositionDirty() || listener.isOrientationDirty() || listener.isUpVectorDirty())
m_cachedAzimuthElevation = std::nullopt;
if (hasPositionChanged || hasOrientationChanged || listener.isPositionDirty())
m_cachedConeGain = std::nullopt;
}
} // namespace WebCore
#endif // ENABLE(WEB_AUDIO)