Skip to content

Commit

Permalink
feat: Added natural air absorption in attenuation models.
Browse files Browse the repository at this point in the history
Signed-off-by: Axel Nana <[email protected]>
  • Loading branch information
na2axl committed Oct 1, 2024
1 parent b79aac8 commit 22ca37c
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 2 deletions.
24 changes: 24 additions & 0 deletions include/SparkyStudios/Audio/Amplitude/Sound/Attenuation.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ namespace SparkyStudios::Audio::Amplitude
* @return The maximum sound attenuation distance.
*/
[[nodiscard]] virtual AmReal64 GetMaxDistance() const = 0;

/**
* @brief Returns whether air absorption is enabled for this Attenuation.
*
* @return `true` if air absorption is enabled, `false` otherwise.
*/
[[nodiscard]] virtual bool IsAirAbsorptionEnabled() const = 0;

/**
* @brief Evaluates the air absorption effect for a specific frequency band.
*
* This method calculates the attenuation factor due to air absorption
* at a given frequency band for a sound source located at a specific position
* and a listener located at another specific position.
*
* @param soundLocation The location of the sound source.
* @param listenerLocation The location of the listener which is hearing the sound.
* @param band The frequency band for which the air absorption effect is evaluated.
*
* @return The air absorption attenuation factor for the given frequency band.
* The returned value is in decibels (dB).
*/
[[nodiscard]] virtual AmReal32 EvaluateAirAbsorption(
const AmVec3& soundLocation, const AmVec3& listenerLocation, AmUInt32 band) const = 0;
};
} // namespace SparkyStudios::Audio::Amplitude

Expand Down
13 changes: 13 additions & 0 deletions schemas/attenuation_definition.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ table AttenuationShapeDefinition {
max_attenuation_factor:float;
}

/// Schema for air absorption model configuration.
table AttenuationAirAbsorptionDefinition {
/// Whether the attenuation model supports air absorption.
enabled:bool = true;

/// The absorption coefficients to use when generating the 3-band EQ values.
/// This array should have exactly 3 values if set.
coefficients:[float];
}

/// The attenuation definition.
table AttenuationDefinition {
/// The attenuation object ID.
Expand All @@ -42,6 +52,9 @@ table AttenuationDefinition {

/// The curve that the attenuation will use to know the sound gain according to the distance from the listener.
gain_curve:CurveDefinition;

/// The air absorption model for this attenuation.
air_absorption:AttenuationAirAbsorptionDefinition;
}

root_type AttenuationDefinition;
Expand Down
209 changes: 208 additions & 1 deletion src/Mixer/Nodes/AttenuationNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,200 @@

namespace SparkyStudios::Audio::Amplitude
{
constexpr AmReal32 kQ = 0.707107f; // sqrt(0.5)

AirAbsorptionEQFilter::AirAbsorptionEQFilter()
: _eqFilterFactory()
, _lowShelfFilter{ nullptr, nullptr }
, _peakingFilter{ nullptr, nullptr }
, _highShelfFilter{ nullptr, nullptr }
, _currentSet(0)
, _needUpdateGains(false)
{
EnsureFilters();
}

AirAbsorptionEQFilter::~AirAbsorptionEQFilter()
{
for (AmUInt32 i = 0; i < 2; ++i)
{
if (_lowShelfFilter[i] != nullptr)
{
_eqFilterFactory.DestroyInstance(_lowShelfFilter[i]);
_lowShelfFilter[i] = nullptr;
}

if (_peakingFilter[i] != nullptr)
{
_eqFilterFactory.DestroyInstance(_peakingFilter[i]);
_peakingFilter[i] = nullptr;
}

if (_highShelfFilter[i] != nullptr)
{
_eqFilterFactory.DestroyInstance(_highShelfFilter[i]);
_highShelfFilter[i] = nullptr;
}
}
}

void AirAbsorptionEQFilter::SetGains(AmReal32 gainLow, AmReal32 gainMid, AmReal32 gainHigh)
{
const AmReal32 oldGainLow = _lowShelfFilter[_currentSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN);
const AmReal32 oldGainMid = _peakingFilter[_currentSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN);
const AmReal32 oldGainHigh = _highShelfFilter[_currentSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN);

if (std::abs(gainLow - oldGainLow) > kEpsilon)
{
_lowShelfFilter[_currentSet]->SetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN, gainLow);
_needUpdateGains = true;
}

if (std::abs(gainMid - oldGainMid) > kEpsilon)
{
_peakingFilter[_currentSet]->SetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN, gainMid);
_needUpdateGains = true;
}

if (std::abs(gainHigh - oldGainHigh) > kEpsilon)
{
_highShelfFilter[_currentSet]->SetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN, gainHigh);
_needUpdateGains = true;
}
}

void AirAbsorptionEQFilter::Process(const AudioBuffer& input, AudioBuffer& output, AmReal32 sampleRate)
{
if (_needUpdateGains)
{
const AmUInt32 previousSet = _currentSet;
_currentSet = 1 - _currentSet;

_lowShelfFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_FREQUENCY,
_lowShelfFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_FREQUENCY));
_lowShelfFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_RESONANCE,
_lowShelfFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_RESONANCE));
_lowShelfFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_GAIN, _lowShelfFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN));

_peakingFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_FREQUENCY,
_peakingFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_FREQUENCY));
_peakingFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_RESONANCE,
_peakingFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_RESONANCE));
_peakingFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_GAIN, _peakingFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN));

_highShelfFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_FREQUENCY,
_highShelfFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_FREQUENCY));
_highShelfFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_RESONANCE,
_highShelfFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_RESONANCE));
_highShelfFilter[_currentSet]->SetParameter(
BiquadResonantFilter::ATTRIBUTE_GAIN, _highShelfFilter[previousSet]->GetParameter(BiquadResonantFilter::ATTRIBUTE_GAIN));

AudioBuffer temp(input.GetFrameCount(), input.GetChannelCount());

ApplyFilters(previousSet, input, temp, sampleRate);
ApplyFilters(_currentSet, input, output, sampleRate);

auto& o = output[0];
auto& t = temp[0];

for (AmUInt32 i = 0, l = input.GetFrameCount(); i < l; ++i)
{
AmReal32 weight = static_cast<AmReal32>(i) / static_cast<AmReal32>(l);
o[i] = weight * o[i] + (1.0f - weight) * t[i];
}

_needUpdateGains = false;
}
else
{
ApplyFilters(_currentSet, input, output, sampleRate);
}
}

void AirAbsorptionEQFilter::Normalize(std::array<AmReal32, kAmAirAbsorptionBandCount>& gains, AmReal32& overallGain)
{
constexpr AmReal32 kMaxEQGain = 0.0625f;

auto maxGain = std::max({ gains[0], gains[1], gains[2] });

if (maxGain < kEpsilon)
{
overallGain = 0.0f;
for (auto i = 0; i < kAmAirAbsorptionBandCount; ++i)
gains[i] = 1.0f;
}
else
{
for (auto i = 0; i < kAmAirAbsorptionBandCount; ++i)
{
gains[i] /= maxGain;
gains[i] = std::max(gains[i], kMaxEQGain);
}

overallGain *= maxGain;
}
}

void AirAbsorptionEQFilter::EnsureFilters()
{
for (AmUInt32 i = 0; i < 2; ++i)
{
if (_lowShelfFilter[i] == nullptr)
{
_eqFilterFactory.InitializeLowShelf(kHighCutoffFrequencies[0], kQ, 0.0f);
_lowShelfFilter[i] = _eqFilterFactory.CreateInstance();
}

if (_peakingFilter[i] == nullptr)
{
const AmReal32 cutoffFrequency = AM_SqrtF(kLowCutoffFrequencies[1] * kHighCutoffFrequencies[1]);
_eqFilterFactory.InitializePeaking(
cutoffFrequency, cutoffFrequency / (kHighCutoffFrequencies[1] - kLowCutoffFrequencies[1]), 0.0f);
_peakingFilter[i] = _eqFilterFactory.CreateInstance();
}

if (_highShelfFilter[i] == nullptr)
{
_eqFilterFactory.InitializeHighShelf(kLowCutoffFrequencies[2], kQ, 0.0f);
_highShelfFilter[i] = _eqFilterFactory.CreateInstance();
}
}
}

void AirAbsorptionEQFilter::ApplyFilters(AmUInt32 set, const AudioBuffer& input, AudioBuffer& output, AmReal32 sampleRate)
{
_lowShelfFilter[set]->Process(input, output, input.GetFrameCount(), sampleRate);
_peakingFilter[set]->Process(output, output, input.GetFrameCount(), sampleRate);
_highShelfFilter[set]->Process(output, output, input.GetFrameCount(), sampleRate);
}

AttenuationNodeInstance::AttenuationNodeInstance()
: ProcessorNodeInstance(false)
, _output()
, _gains{ 1.0f, 1.0f, 1.0f }
, _eqFilter()
{}

const AudioBuffer* AttenuationNodeInstance::Process(const AudioBuffer* input)
{
const auto* layer = GetLayer();

const Attenuation* attenuation = layer->GetAttenuation();
const Listener& listener = layer->GetListener();

AmReal32 targetGain = 1.0f;

if (attenuation != nullptr)
{
const eSpatialization spatialization = layer->GetSpatialization();
const Listener& listener = layer->GetListener();

if (listener.Valid())
{
Expand Down Expand Up @@ -66,6 +248,19 @@ namespace SparkyStudios::Audio::Amplitude
}
}

// Set and normalize gains
if (attenuation->IsAirAbsorptionEnabled() && listener.Valid())
{
const AmVec3& soundLocation = layer->GetLocation();
const AmVec3& listenerLocation = listener.GetLocation();

for (AmUInt32 i = 0; i < kAmAirAbsorptionBandCount; ++i)
_gains[i] = attenuation->EvaluateAirAbsorption(soundLocation, listenerLocation, i);

_eqFilter.Normalize(_gains, targetGain);
_eqFilter.SetGains(_gains[0], _gains[1], _gains[2]);
}

if (Gain::IsZero(targetGain))
return nullptr;

Expand All @@ -74,9 +269,21 @@ namespace SparkyStudios::Audio::Amplitude

_output = *input;

// Apply gain attenuation
for (AmSize c = 0; c < _output.GetChannelCount(); ++c)
Gain::ApplyReplaceConstantGain(targetGain, input->GetChannel(c), 0, _output[c], 0, input->GetFrameCount());

// Apply air absorption EQ filter
if (attenuation->IsAirAbsorptionEnabled() && listener.Valid())
{
const AmVec3& soundLocation = layer->GetLocation();
const AmVec3& listenerLocation = listener.GetLocation();

auto sampleRate = layer->GetSampleRate();

_eqFilter.Process(_output, _output, sampleRate);
}

return &_output;
}

Expand Down
33 changes: 33 additions & 0 deletions src/Mixer/Nodes/AttenuationNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,50 @@
#include <SparkyStudios/Audio/Amplitude/Core/Memory.h>
#include <SparkyStudios/Audio/Amplitude/Mixer/Node.h>

#include <DSP/Filters/BiquadResonantFilter.h>

namespace SparkyStudios::Audio::Amplitude
{
class AmplimixLayer;

class AirAbsorptionEQFilter final
{
public:
AirAbsorptionEQFilter();
~AirAbsorptionEQFilter();

void SetGains(AmReal32 gainLow, AmReal32 gainMid, AmReal32 gainHigh);

void Process(const AudioBuffer& input, AudioBuffer& output, AmReal32 sampleRate);

void Normalize(std::array<AmReal32, kAmAirAbsorptionBandCount>& gains, AmReal32& overallGain);

private:
void EnsureFilters();
void ApplyFilters(AmUInt32 set, const AudioBuffer& input, AudioBuffer& output, AmReal32 sampleRate);

BiquadResonantFilter _eqFilterFactory;

FilterInstance* _lowShelfFilter[2];
FilterInstance* _peakingFilter[2];
FilterInstance* _highShelfFilter[2];

AmUInt32 _currentSet;
bool _needUpdateGains;
};

class AttenuationNodeInstance final : public ProcessorNodeInstance
{
public:
AttenuationNodeInstance();

const AudioBuffer* Process(const AudioBuffer* input) override;

private:
AudioBuffer _output;

std::array<AmReal32, kAmAirAbsorptionBandCount> _gains;
AirAbsorptionEQFilter _eqFilter;
};

class AttenuationNode final : public Node
Expand Down
20 changes: 20 additions & 0 deletions src/Sound/Attenuation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace SparkyStudios::Audio::Amplitude
: _maxDistance(0.0f)
, _shape(nullptr)
, _gainCurve()
, _airAbsorptionEnabled(true)
, _airAbsorptionCoefficients{ 0.0002f, 0.0017f, 0.0182f }
{}

AttenuationImpl::~AttenuationImpl()
Expand All @@ -46,6 +48,12 @@ namespace SparkyStudios::Audio::Amplitude
return _shape.get();
}

AmReal32 AttenuationImpl::EvaluateAirAbsorption(const AmVec3& soundLocation, const AmVec3& listenerLocation, AmUInt32 band) const
{
const AmReal32 distance = AM_Len(soundLocation - listenerLocation);
return std::exp(-_airAbsorptionCoefficients[band] * distance);
}

bool AttenuationImpl::LoadDefinition(const AttenuationDefinition* definition, EngineInternalState* state)
{
m_id = definition->id();
Expand All @@ -57,6 +65,18 @@ namespace SparkyStudios::Audio::Amplitude

_shape.reset(AttenuationZoneImpl::Create(definition->shape()));

if (const auto* model = definition->air_absorption(); model != nullptr)
{
_airAbsorptionEnabled = model->enabled();

if (_airAbsorptionEnabled)
{
_airAbsorptionCoefficients[0] = model->coefficients()->Get(0);
_airAbsorptionCoefficients[1] = model->coefficients()->Get(1);
_airAbsorptionCoefficients[2] = model->coefficients()->Get(2);
}
}

return true;
}

Expand Down
Loading

0 comments on commit 22ca37c

Please sign in to comment.