Skip to content

Commit

Permalink
Merge pull request #354 from iver56/ij/fix-air-absorption-bug
Browse files Browse the repository at this point in the history
Fix a bug where AirAbsorption often chose the wrong humidity bucket
  • Loading branch information
iver56 authored Sep 30, 2024
2 parents 3299179 + 17b250d commit 9812fd1
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 42 deletions.
30 changes: 18 additions & 12 deletions audiomentations/augmentations/air_absorption.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ def next_power_of_2(x: int) -> int:
return 1 if x == 0 else 2 ** (x - 1).bit_length()


def get_temperature_humidity_key(temperature: float, humidity: float) -> str:
key = str(int(temperature)) + "C"
bounds = [30, 50, 70, 90]
for n in range(1, len(bounds)):
if bounds[n - 1] <= humidity <= bounds[n]:
key += f"_{bounds[n-1]}-{bounds[n]}%"
break
return key


class AirAbsorption(BaseWaveformTransform):
"""
Apply a Lowpass-like filterbank with variable octave attenuation that simulates attenuation of
Expand All @@ -28,7 +38,7 @@ class AirAbsorption(BaseWaveformTransform):
is adapted from a lookup table by pyroomacoustics [1]. It can also be seen as a lowpass filter
with variable octave attenuation.
Note: This only "simulates" the dampening of high frequencies, and does not
Note: This only "simulates" the damping of high frequencies, and does not
attenuate according to the distance law. Gain augmentation needs to be done separately.
[1] https://github.com/LCAV/pyroomacoustics
Expand Down Expand Up @@ -122,23 +132,19 @@ def randomize_parameters(self, samples: NDArray[np.float32], sample_rate: int):
self.min_distance, self.max_distance
)

def apply(self, samples: NDArray[np.float32], sample_rate: int) -> NDArray[np.float32]:
def apply(
self, samples: NDArray[np.float32], sample_rate: int
) -> NDArray[np.float32]:
assert samples.dtype == np.float32

humidity = self.parameters["humidity"]
distance = self.parameters["distance"]

# Choose correct absorption coefficients
key = str(int(self.parameters["temperature"])) + "C"
bounds = [30, 50, 70, 90]
for n in range(1, len(bounds)):
if bounds[n - 1] <= humidity or humidity <= bounds[n]:
key += f"_{bounds[n-1]}-{bounds[n]}%"
break
key = get_temperature_humidity_key(
self.parameters["temperature"], self.parameters["humidity"]
)

# Convert to attenuations
attenuation_values = np.exp(
-distance * np.array(self.air_absorption_table[key])
-self.parameters["distance"] * np.array(self.air_absorption_table[key])
)

# Calculate n_fft so that the lowest band can be stored in a single
Expand Down
2 changes: 1 addition & 1 deletion docs/waveform_transforms/air_absorption.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ It can also be seen as a lowpass filter with variable octave attenuation.
Note that since this transform mostly affects high frequencies, it is only
suitable for audio with sufficiently high sample rate, like 32 kHz and above.

Note also that this transform only "simulates" the dampening of high frequencies, and
Note also that this transform only "simulates" the damping of high frequencies, and
does not attenuate according to the distance law. Gain augmentation needs to be done
separately.

Expand Down
67 changes: 38 additions & 29 deletions tests/test_air_absorption.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from audiomentations.augmentations.air_absorption import get_temperature_humidity_key

DEBUG = False

from audiomentations import AirAbsorption
Expand All @@ -14,32 +16,39 @@ def get_chirp_test(sample_rate, duration):
return samples.astype(np.float32)


class TestAirAbsorptionTransform:
@pytest.mark.parametrize("temperature", [10, 20])
@pytest.mark.parametrize("humidity", [30, 50, 70, 90])
@pytest.mark.parametrize("distance", [5, 10, 20, 100])
@pytest.mark.parametrize("sample_rate", [8000, 16000, 48000])
def test_input_shapes(self, temperature, humidity, distance, sample_rate):
np.random.seed(1)

samples = get_chirp_test(sample_rate, 10)

augment = AirAbsorption(
min_temperature=temperature,
max_temperature=temperature,
min_humidity=humidity,
max_humidity=humidity,
min_distance=distance,
max_distance=distance,
)

# Test 1D case
processed_samples = augment(samples, sample_rate=sample_rate)
assert processed_samples.shape == samples.shape
assert processed_samples.dtype == np.float32

# Test 2D case
samples = np.tile(samples, (2, 1))
processed_samples = augment(samples, sample_rate=sample_rate)
assert processed_samples.shape == samples.shape
assert processed_samples.dtype == np.float32
@pytest.mark.parametrize("temperature", [10, 20])
@pytest.mark.parametrize("humidity", [30, 50, 70, 90])
@pytest.mark.parametrize("distance", [5, 10, 20, 100])
@pytest.mark.parametrize("sample_rate", [8000, 16000, 48000])
def test_input_shapes(temperature, humidity, distance, sample_rate):
np.random.seed(1)

samples = get_chirp_test(sample_rate, 10)

augment = AirAbsorption(
min_temperature=temperature,
max_temperature=temperature,
min_humidity=humidity,
max_humidity=humidity,
min_distance=distance,
max_distance=distance,
)

# Test 1D case
processed_samples = augment(samples, sample_rate=sample_rate)
assert processed_samples.shape == samples.shape
assert processed_samples.dtype == np.float32

# Test 2D case
samples = np.tile(samples, (2, 1))
processed_samples = augment(samples, sample_rate=sample_rate)
assert processed_samples.shape == samples.shape
assert processed_samples.dtype == np.float32

def test_get_temperature_humidity_key():
assert get_temperature_humidity_key(10.0, 30.0) == "10C_30-50%"
assert get_temperature_humidity_key(10.0, 45.0) == "10C_30-50%"
assert get_temperature_humidity_key(10.0, 50.0) == "10C_30-50%"
assert get_temperature_humidity_key(10.0, 51.0) == "10C_50-70%"
assert get_temperature_humidity_key(10.0, 65.0) == "10C_50-70%"
assert get_temperature_humidity_key(20.0, 90.0) == "20C_70-90%"

0 comments on commit 9812fd1

Please sign in to comment.