diff --git a/include/SparkyStudios/Audio/Amplitude/Sound/Attenuation.h b/include/SparkyStudios/Audio/Amplitude/Sound/Attenuation.h index c1f6c13d..a6d2b91a 100644 --- a/include/SparkyStudios/Audio/Amplitude/Sound/Attenuation.h +++ b/include/SparkyStudios/Audio/Amplitude/Sound/Attenuation.h @@ -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 diff --git a/schemas/attenuation_definition.fbs b/schemas/attenuation_definition.fbs index b6c92131..9aedf074 100644 --- a/schemas/attenuation_definition.fbs +++ b/schemas/attenuation_definition.fbs @@ -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. @@ -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; diff --git a/src/Mixer/Nodes/AttenuationNode.cpp b/src/Mixer/Nodes/AttenuationNode.cpp index a96ec812..9291b3d3 100644 --- a/src/Mixer/Nodes/AttenuationNode.cpp +++ b/src/Mixer/Nodes/AttenuationNode.cpp @@ -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(i) / static_cast(l); + o[i] = weight * o[i] + (1.0f - weight) * t[i]; + } + + _needUpdateGains = false; + } + else + { + ApplyFilters(_currentSet, input, output, sampleRate); + } + } + + void AirAbsorptionEQFilter::Normalize(std::array& 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()) { @@ -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; @@ -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; } diff --git a/src/Mixer/Nodes/AttenuationNode.h b/src/Mixer/Nodes/AttenuationNode.h index b591ec94..a2a40fbf 100644 --- a/src/Mixer/Nodes/AttenuationNode.h +++ b/src/Mixer/Nodes/AttenuationNode.h @@ -20,17 +20,50 @@ #include #include +#include + 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& 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 _gains; + AirAbsorptionEQFilter _eqFilter; }; class AttenuationNode final : public Node diff --git a/src/Sound/Attenuation.cpp b/src/Sound/Attenuation.cpp index d5a87251..0d8ed442 100644 --- a/src/Sound/Attenuation.cpp +++ b/src/Sound/Attenuation.cpp @@ -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() @@ -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(); @@ -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; } diff --git a/src/Sound/Attenuation.h b/src/Sound/Attenuation.h index f0dd159b..a8478538 100644 --- a/src/Sound/Attenuation.h +++ b/src/Sound/Attenuation.h @@ -112,11 +112,25 @@ namespace SparkyStudios::Audio::Amplitude /** * @copydoc Attenuation::GetMaxDistance */ - [[nodiscard]] AmReal64 GetMaxDistance() const override + [[nodiscard]] AM_INLINE AmReal64 GetMaxDistance() const override { return _maxDistance; } + /** + * @copydoc Attenuation::IsAirAbsorptionEnabled + */ + [[nodiscard]] AM_INLINE bool IsAirAbsorptionEnabled() const override + { + return _airAbsorptionEnabled; + } + + /** + * @copydoc Attenuation::EvaluateAirAbsorption + */ + [[nodiscard]] AmReal32 EvaluateAirAbsorption( + const AmVec3& soundLocation, const AmVec3& listenerLocation, AmUInt32 band) const override; + /** * @copydoc AssetImpl::LoadDefinition */ @@ -133,6 +147,9 @@ namespace SparkyStudios::Audio::Amplitude AmUniquePtr _shape; Curve _gainCurve; + + bool _airAbsorptionEnabled; + std::array _airAbsorptionCoefficients; }; } // namespace SparkyStudios::Audio::Amplitude