JPL Spatial
Sound spatialization and propagation library
Loading...
Searching...
No Matches
AirAbsorption.h
Go to the documentation of this file.
1//
2// ██╗██████╗ ██╗ ██╗██████╗ ███████╗
3// ██║██╔══██╗ ██║ ██║██╔══██╗██╔════╝ ** JPL Spatial **
4// ██║██████╔╝ ██║ ██║██████╔╝███████╗
5// ██ ██║██╔═══╝ ██║ ██║██╔══██╗╚════██║ https://github.com/Jaytheway/JPLSpatial
6// ╚█████╔╝██║ ███████╗██║██████╔╝███████║
7// ╚════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝
8//
9// Copyright 2026 Jaroslav Pevno, JPL Spatial is offered under the terms of the ISC license:
10//
11// Permission to use, copy, modify, and/or distribute this software for any purpose with or
12// without fee is hereby granted, provided that the above copyright notice and this permission
13// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
14// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
16// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
17// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20#pragma once
21
22#include "JPLSpatial/Core.h"
27
28namespace JPL
29{
30 // Conversion factor from dB to reciprocal meters: 1 / (20 * log10(e))
31 constexpr float JPL_DB_TO_RECIP_M = 0.1151292546497022842f;
32
33 // Conversion factor from reciprocal meters to dB: 20 * log10(e)
34 constexpr float JPL_RECIP_M_TO_DB = 8.68588963807f;
35
36 //==========================================================================
40 {
41 float AirTemperatureC = 20.0f;
43 float AtmosphericPressureKPa = 101.325f;
44 };
45
47
48 //==========================================================================
53 {
56
61
67
74
78
79 //======================================================================
81 struct
82 {
83 const simd_mask HasKnee; // Flag whether frequncy band requires transition to absorption of the lower third
84 const simd DistanceToKnee; // rk
85 const simd LinearizedHFLossFactor; // m(f1) * rk + m(f2) * (r(t) - rk): with r(t) taken out as "lowerThirdFrequencyLoss"
87 };
88
89 //==========================================================================
97 {
98 public:
99
100 //======================================================================
101 [[nodiscard]] static inline AirAbsorptionCache CacheParameters(const FrequencyBands& frequencyBands,
102 const AirAbsorptionParams& params,
103 float nyquist = 22050.0f);
104
109 [[nodiscard]] static inline simd ComputeForDistance(float distance, const AirAbsorptionCache& aaCache);
110
111 //======================================================================
120 [[nodiscard]] static inline float ComputeForFrequency(float frequencyHz, const AirAbsorptionParams& params);
121
123 [[nodiscard]] static inline AbsorptionCoeffs ComputeForFrequencies(const FrequencyBands& frequenciesHz, const AirAbsorptionParams& params);
124
129 [[nodiscard]] static inline FrequencyBands ComputeHFWeightedBandCenters(const SplitFrequencies& splitFrequenciesHz);
130
131 //======================================================================
132 explicit AirAbsorption(const AirAbsorptionParams& params);
133
135 [[nodiscard]] float ComputeAbsorptionPerMeter(float frequencyHz) const;
136 [[nodiscard]] simd ComputeAbsorptionPerMeter(simd frequenciesHz) const;
137
138 private:
139 // Classical absorption contribution.
140 float mClassicalAbsorption;
141
142 // Temperature scaling applied to the molecular relaxation terms.
143 float mRelaxationTemperatureScale;
144
145 // Oxygen relaxation frequency, frO, in Hz.
146 float mOxygenRelaxationFrequency;
147
148 // Nitrogen relaxation frequency, frN, in Hz.
149 float mNitrogenRelaxationFrequency;
150
151 // Oxygen molecular relaxation coefficient.
152 float mOxygenRelaxationCoefficient;
153
154 // Nitrogen molecular relaxation coefficient.
155 float mNitrogenRelaxationCoefficient;
156
157 // 1.0f / mOxygenRelaxationFrequency
158 float mOxygenRelaxationFrequencyInv;
159
160 // 1.0f / mNitrogenRelaxationFrequency
161 float mNitrogenRelaxationFrequencyInv;
162 };
163
164 //==========================================================================
167
168} // namespace JPL
169
170//==============================================================================
171//
172// Code beyond this point is implementation detail...
173//
174//==============================================================================
175
176namespace JPL
177{
179 const AirAbsorptionParams& params,
180 float nyquist)
181 {
182 AirAbsorption aa(params);
183
184 const SplitFrequencies splits = SplitFrequenciesFromBandCenters(frequencyBands, nyquist);
185 const FrequencyBands lowerThirdCenters = ComputeBandLowerThirdCenters(splits);
186
187 static constexpr float weightingThresholdHz = 1000.0f;
188 const FrequencyBands weightedBands = simd::select(frequencyBands >= simd(weightingThresholdHz), lowerThirdCenters, frequencyBands);
189
190 const AbsorptionCoeffs centerAbsorption = aa.ComputeAbsorptionPerMeter(frequencyBands);
191 const AbsorptionCoeffs lowerThirdAbsorption = aa.ComputeAbsorptionPerMeter(lowerThirdCenters);
192 const AbsorptionCoeffs weightedAbsorption = aa.ComputeAbsorptionPerMeter(weightedBands);
193
194 // At 500 Hz, knee should be at 6 dB, we use 400 Hz to soften up the requirement
195 const simd_mask hasKnee = frequencyBands > simd(400.0f);
196
197 // Knee point is at 14dB for >= 1kHz and 6dB for 500Hz
198 const simd deltaLk = simd::select(frequencyBands >= 1000.0f, 14.0f, 6.0f);
199 const simd distanceToKnee = deltaLk / centerAbsorption;
200
201 return AirAbsorptionCache{
202 .FrequencyBandCenters = frequencyBands,
203 .FrequencyBandLowerThirdCenters = lowerThirdCenters,
204 .CenterFrequencyAbsorption_dB = centerAbsorption,
205 .LowerThirdBandAbsorption_dB = lowerThirdAbsorption,
206 .HighFreqWeightedAbsorption_dB = weightedAbsorption,
207 .FrequencyDecaySlope = weightedAbsorption * SpeedOfSound::ForTemperature(params.AirTemperatureC),
208 .LinAprox = {
209 .HasKnee = hasKnee,
210 .DistanceToKnee = distanceToKnee,
211 .LinearizedHFLossFactor = distanceToKnee * (centerAbsorption - lowerThirdAbsorption)
212 }
213 };
214 }
215
216 inline simd AirAbsorption::ComputeForDistance(float distance, const AirAbsorptionCache& aaCache)
217 {
218 // Formula as per Rindel (2024):
219 //
220 // if (r(t) < rk)
221 // return centerAbsorption * distance;
222 // else
223 // return centerAbsorption * rk + lowerThirdAbsorption * (r(t) - rk);
224 //
225 // ----------------------------------------------------------------------
226 //
227 // const simd linearizedHighBandLoss = centerAbsorption * rk + lowerThirdAbsorption * (distance - rk);
228 // ...simplifies to this:
229 //const simd linearizedHighBandLoss = rk * (centerAbsorption - lowerThirdAbsorption) + lowerThirdFrequencyLoss;
230
231 // const simd linearizedHighBandLoss = lowerThirdFrequencyLoss + aaCache.LinAprox.LinearizedHFLossFactor;
232 const simd linearizedHighBandLoss =
234
235 const simd centerFrequencyLoss = distance * aaCache.CenterFrequencyAbsorption_dB;
236
237 return simd::select(aaCache.LinAprox.HasKnee & (simd(distance) >= aaCache.LinAprox.DistanceToKnee),
238 linearizedHighBandLoss,
239 centerFrequencyLoss);
240 }
241
243 {
244 // Rindel (2024): "European Norm EN 12354-6:
245 // For the 1 kHz to the 8 kHz octave bands, the coefficients are the pure-tone attenuation coefficients of the center frequency
246 // of the lower one-third octave within the octave band.
247 // This is assumed to handle the spectral change within the octave band during the sound propagation."
248
249 // Note: because we can't guarantee that the frequencies are split in octave bands,
250 // we are going to compute centers of geometric lower third.
251 // This means we can't just multiply lower edge by a fixed ratio.
252
253 const FrequencyBands bandCenters = ComputeBandCenters(splitFrequenciesHz);
254 const FrequencyBands lowerThirdCenters = ComputeBandLowerThirdCenters(splitFrequenciesHz);
255 return round(simd::select(bandCenters >= simd(1000.0f), lowerThirdCenters, bandCenters));
256 }
257
258 //==========================================================================
259 inline float AirAbsorption::ComputeForFrequency(float frequencyHz, const AirAbsorptionParams& params)
260 {
261 // Spelled out formula for computed air absorption
262
263 const float f = frequencyHz;
264 const float f2 = f * f;
265 const float pa = params.AtmosphericPressureKPa;
266
267 // Reference atmospheric pressure
268 const float pr = 101.325f;
269
270 const float T = params.AirTemperatureC + 273.15f;
271
272 // Reference temperature (C)
273 const float To = 20.0f + 273.15f;
274
275 // Tripple-point iusotherm temp (C)
276 const float To1 = 0.01f + 273.15f;
277
278 // Saturation vapor pressure
279 const float psat = pr * ::pow(10, -6.8346 * ::pow(To1 / T, 1.261f) + 4.6151f);
280
281 const float hr = params.RelativeHumidityPercent;
282
283 // Molar concentration of water vapor, as a percentage
284 const float h = hr * (psat / pa);
285
286 // Oxygen relaxation frequency
287 const float frO = (pa / pr) * (24.0f + 4.04f * ::pow(10, 4) * h * ((0.02f + h) / (0.391f + h)));
288
289 // Nitrogen relaxation frequency
290 const float frN = (pa / pr) * ::pow(T / To, -0.5f) * (9 + 280 * h * ::exp(-4.170 * (::pow(T / To, -1.0f / 3.0f) - 1.0f)));
291
292 const float z = 0.1068f * ::exp(-3352 / T) * ::pow(frN + f2 / frN, -1);
293
294 const float y = ::pow(T / To, -2.5f) * (0.01275f * ::exp(-2239.1 / T) * ::pow(frO + f2 / frO, -1) + z);
295
296 // Attenuation coefficient
297 const float a = JPL_RECIP_M_TO_DB * f2 * ((1.84f * ::pow(10, -11) * ::pow(pa / pr, -1) * ::pow(T / To, 0.5f)) + y);
298
299 return a;
300 }
301
303 {
304 return AirAbsorption(params).ComputeAbsorptionPerMeter(frequenciesHz);
305 }
306
307 //==========================================================================
309 {
310 const float referencePressureKPa = 101.325f;
311 const float absoluteTemperatureK = params.AirTemperatureC + 273.15f;
312 const float referenceTemperatureK = 293.15f; // 20 C
313 const float triplePointTemperatureK = 273.16f; // 0.01 C
314 const float relativeTemperature = absoluteTemperatureK / referenceTemperatureK;
315 const float relativePressure = params.AtmosphericPressureKPa / referencePressureKPa;
316
317 // Saturation vapor pressure, in kPa.
318 const float saturationVaporPressureKPa =
319 referencePressureKPa *
320 ::pow(
321 10.0f,
322 -6.8346f * ::pow(triplePointTemperatureK / absoluteTemperatureK, 1.261f) + 4.6151f);
323
324 // Molar concentration of water vapor as a percentage.
325 const float waterVaporConcentrationPercent =
326 params.RelativeHumidityPercent * (saturationVaporPressureKPa / params.AtmosphericPressureKPa);
327
328 const float h = waterVaporConcentrationPercent;
329
330 mOxygenRelaxationFrequency =
331 relativePressure *
332 (24.0f +
333 4.04e4f * h *
334 ((0.02f + h) / (0.391f + h)));
335
336 mNitrogenRelaxationFrequency =
337 relativePressure *
338 ::pow(relativeTemperature, -0.5f) *
339 (9.0f +
340 280.0f * h * ::exp(-4.170f * (::pow(relativeTemperature, -1.0f / 3.0f) - 1.0f)));
341
342 mClassicalAbsorption =
343 1.84e-11f *
344 ::pow(relativePressure, -1.0f) *
345 ::sqrt(relativeTemperature);
346
347 mRelaxationTemperatureScale =
348 ::pow(relativeTemperature, -2.5f);
349
350 mOxygenRelaxationCoefficient =
351 0.01275f *
352 ::exp(-2239.1f / absoluteTemperatureK);
353
354 mNitrogenRelaxationCoefficient =
355 0.1068f *
356 ::exp(-3352.0f / absoluteTemperatureK);
357
358 mOxygenRelaxationFrequencyInv = 1.0f / mOxygenRelaxationFrequency;
359 mNitrogenRelaxationFrequencyInv = 1.0f / mNitrogenRelaxationFrequency;
360 }
361
362 inline float AirAbsorption::ComputeAbsorptionPerMeter(float frequencyHz) const
363 {
364 const float f2 = frequencyHz * frequencyHz;
365
366 const float oxygenRelaxationAbsorption = mOxygenRelaxationCoefficient / Math::FMA(f2, mOxygenRelaxationFrequencyInv, mOxygenRelaxationFrequency);
367 const float nitrogenRelaxationAbsorption = mNitrogenRelaxationCoefficient / Math::FMA(f2, mNitrogenRelaxationFrequencyInv, mNitrogenRelaxationFrequency);
368 const float molecularRelaxationAbsorption = mRelaxationTemperatureScale * (oxygenRelaxationAbsorption + nitrogenRelaxationAbsorption);
369 const float totalAbsorptionBeforeFrequencyScale = mClassicalAbsorption + molecularRelaxationAbsorption;
370
371 return JPL_RECIP_M_TO_DB * f2 * totalAbsorptionBeforeFrequencyScale;
372 }
373
375 {
376 const simd f2 = frequenciesHz * frequenciesHz;
377
378 const simd oxygenRelaxationAbsorption = simd(mOxygenRelaxationCoefficient) / Math::FMA(f2, mOxygenRelaxationFrequencyInv, simd(mOxygenRelaxationFrequency));
379 const simd nitrogenRelaxationAbsorption = simd(mNitrogenRelaxationCoefficient) / Math::FMA(f2, mNitrogenRelaxationFrequencyInv, simd(mNitrogenRelaxationFrequency));
380 const simd molecularRelaxationAbsorption = simd(mRelaxationTemperatureScale) * (oxygenRelaxationAbsorption + nitrogenRelaxationAbsorption);
381 const simd totalAbsorptionBeforeFrequencyScale = simd(mClassicalAbsorption) + molecularRelaxationAbsorption;
382
383 return JPL_RECIP_M_TO_DB * f2 * totalAbsorptionBeforeFrequencyScale;
384 }
385} // namespac JPL
Definition AirAbsorption.h:97
static simd ComputeForDistance(float distance, const AirAbsorptionCache &aaCache)
Definition AirAbsorption.h:216
AirAbsorption(const AirAbsorptionParams &params)
Definition AirAbsorption.h:308
static AbsorptionCoeffs ComputeForFrequencies(const FrequencyBands &frequenciesHz, const AirAbsorptionParams &params)
Compute pure-tone sound attenuation in four frequency bands.
Definition AirAbsorption.h:302
static FrequencyBands ComputeHFWeightedBandCenters(const SplitFrequencies &splitFrequenciesHz)
Definition AirAbsorption.h:242
static float ComputeForFrequency(float frequencyHz, const AirAbsorptionParams &params)
Definition AirAbsorption.h:259
float ComputeAbsorptionPerMeter(float frequencyHz) const
Compute pure-tone sound attenuation coefficient in dB/m, for atmospheric absorption.
Definition AirAbsorption.h:362
static AirAbsorptionCache CacheParameters(const FrequencyBands &frequencyBands, const AirAbsorptionParams &params, float nyquist=22050.0f)
Definition AirAbsorption.h:178
static constexpr float ForTemperature(float temperatureC)
Definition SpeedOfSound.h:38
JPL_INLINE constexpr T FMA(T a, T b, T c) noexcept
Inlined fuse multiply-add. Compiler in some circumstances is more eager to optimize this than std::fm...
Definition Math.h:186
Definition AcousticMaterial.h:36
simd exp(simd x) noexcept
Exponent for 4-wide 32-bit float vector.
Definition SIMDMath.h:298
const FreqBandCenters cBandCenters
Definition FrequencyBands.h:89
constexpr float JPL_RECIP_M_TO_DB
Definition AirAbsorption.h:34
constexpr float JPL_DB_TO_RECIP_M
Definition AirAbsorption.h:31
JPL_INLINE SplitFrequencies SplitFrequenciesFromBandCenters(const FreqBandCenters &bandCenters, float nyquist=22050.0f)
Definition FrequencyBands.h:69
JPL_INLINE FreqBandCenters ComputeBandLowerThirdCenters(const SplitFrequencies &splitFrequenciesHz, float nyquist=22050.0f)
Definition FrequencyBands.h:58
JPL_INLINE simd round(const simd &vec) noexcept
Element-wise round to nearest integer value.
Definition SIMD.h:1930
simd pow(const simd &x, const simd &y) noexcept
Definition SIMDMath.h:372
JPL_INLINE FreqBandCenters ComputeBandCenters(const SplitFrequencies &splitFrequenciesHz, float nyquist=22050.0f)
Definition FrequencyBands.h:51
const AirAbsorptionCache cDefaultAirAbsCache
Default air absorption used in JPL Spatial.
Definition AirAbsorption.h:166
Definition AirAbsorption.h:53
const AbsorptionCoeffs HighFreqWeightedAbsorption_dB
Definition AirAbsorption.h:73
struct JPL::AirAbsorptionCache::@0 LinAprox
Linearized approximation for non-linear frequency loss by distance.
const simd_mask HasKnee
Definition AirAbsorption.h:83
const AbsorptionCoeffs FrequencyDecaySlope
Definition AirAbsorption.h:77
const simd LinearizedHFLossFactor
Definition AirAbsorption.h:85
const AbsorptionCoeffs LowerThirdBandAbsorption_dB
Definition AirAbsorption.h:66
const simd DistanceToKnee
Definition AirAbsorption.h:84
const AbsorptionCoeffs CenterFrequencyAbsorption_dB
Definition AirAbsorption.h:60
const FrequencyBands FrequencyBandLowerThirdCenters
Definition AirAbsorption.h:55
const FrequencyBands FrequencyBandCenters
Definition AirAbsorption.h:54
Definition AirAbsorption.h:40
float AtmosphericPressureKPa
Definition AirAbsorption.h:43
float RelativeHumidityPercent
Definition AirAbsorption.h:42
float AirTemperatureC
Definition AirAbsorption.h:41
Split frequencies points for crossover.
Definition FrequencyBands.h:41
Definition SIMD.h:207
Minimal 4-wide 32-bit float vector implementation for SIMD.
Definition SIMD.h:60
static JPL_INLINE simd select(const simd_mask &mask, const simd &a, const simd &b) noexcept
Component-wise select, returns 'a' if mask is true, 'b' otherwise.
Definition SIMD.h:1034