teamml-audio-augment 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- teamml_audio_augment-0.1.0/LICENSE +7 -0
- teamml_audio_augment-0.1.0/PKG-INFO +58 -0
- teamml_audio_augment-0.1.0/README.md +33 -0
- teamml_audio_augment-0.1.0/pyproject.toml +41 -0
- teamml_audio_augment-0.1.0/setup.cfg +4 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/__init__.py +17 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/advanced/__init__.py +4 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/advanced/esc_background_noise.py +70 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/advanced/location_based_rir.py +55 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/advanced/mad_background_noise.py +103 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/advanced/realistic_background_noise.py +55 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/add_background_noise.py +133 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/add_gaussian_snr.py +80 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/air_absorption.py +243 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/apply_impulse_response.py +118 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/gain.py +73 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/high_pass_filter.py +167 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/infrasound.py +162 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/limiter.py +165 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/normalize.py +62 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/augmentations/shift.py +194 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/core/__init__.py +0 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/core/_advanced.py +122 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/core/composition.py +249 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/core/transforms_interface.py +106 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment/core/utils.py +150 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment.egg-info/PKG-INFO +58 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment.egg-info/SOURCES.txt +29 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment.egg-info/dependency_links.txt +1 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment.egg-info/requires.txt +9 -0
- teamml_audio_augment-0.1.0/teamml_audio_augment.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright © 2026 Certus Innovations
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: teamml_audio_augment
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audio augmentations for TEAM-ML models and data
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/Stonewall-Defense/audio-augment
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: audiomlspectools>=0.11.2
|
|
17
|
+
Requires-Dist: numpy>=2.5.1
|
|
18
|
+
Requires-Dist: numpy-audio-limiter>=0.2.0
|
|
19
|
+
Requires-Dist: scipy>=1.18.0
|
|
20
|
+
Requires-Dist: tinytag>=2.2.1
|
|
21
|
+
Requires-Dist: torch>=2.10.0
|
|
22
|
+
Provides-Extra: demo
|
|
23
|
+
Requires-Dist: streamlit>=1.59.2; extra == "demo"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Audio Augmentations
|
|
27
|
+
|
|
28
|
+
New takes on audio augmentations for TEAM-ML models and data. If you are just stumbling across this package, you should probably use [Audiomentations](https://github.com/iver56/audiomentations) or [torch-audiomentations](https://github.com/iver56/torch-audiomentations).
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
- Python 3.14 runtime
|
|
33
|
+
- Pip for package installation
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Pip is the classic
|
|
39
|
+
pip install teamml_audio_augment
|
|
40
|
+
|
|
41
|
+
# Should also work ith `uv`
|
|
42
|
+
uv add teamml_audio_augment
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Credit Where Credit is Due
|
|
46
|
+
|
|
47
|
+
- In the [`AirAbsorption` class](teamml_audio_augment/augmentations/air_absorption.py), the `_fft_freq` function is adapted from [`librosa`](https://librosa.org/doc/latest/generated/librosa.fft_frequencies.html)
|
|
48
|
+
- In the [`Infrasound` class](teamml_audio_augment/augmentations/infrasound.py), the `_oscillator_bank` function is adapted from [`torchaudio`](https://docs.pytorch.org/audio/2.7.0/generated/torchaudio.prototype.functional.oscillator_bank.html)
|
|
49
|
+
- In [_advanced.py](teamml_audio_augment/core/_advanced.py), the functions are adapted from several advanced and/or compiled libraries:
|
|
50
|
+
- The `rms` function is adapted from [`numpy-rms`](https://pypi.org/project/numpy-rms/)
|
|
51
|
+
- The `minmax` function is adapted from [`numpy-minmax`](https://pypi.org/project/numpy-minmax/)
|
|
52
|
+
- The `_next_fast_len` function is adapted from [`scipy.signal`](https://docs.scipy.org/doc/scipy/reference/signal.html)
|
|
53
|
+
|
|
54
|
+
Our contributions include everything in [advanced](teamml_audio_augment/advanced/) and the `Infrasound` class.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Audio Augmentations
|
|
2
|
+
|
|
3
|
+
New takes on audio augmentations for TEAM-ML models and data. If you are just stumbling across this package, you should probably use [Audiomentations](https://github.com/iver56/audiomentations) or [torch-audiomentations](https://github.com/iver56/torch-audiomentations).
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Python 3.14 runtime
|
|
8
|
+
- Pip for package installation
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Pip is the classic
|
|
14
|
+
pip install teamml_audio_augment
|
|
15
|
+
|
|
16
|
+
# Should also work ith `uv`
|
|
17
|
+
uv add teamml_audio_augment
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Credit Where Credit is Due
|
|
21
|
+
|
|
22
|
+
- In the [`AirAbsorption` class](teamml_audio_augment/augmentations/air_absorption.py), the `_fft_freq` function is adapted from [`librosa`](https://librosa.org/doc/latest/generated/librosa.fft_frequencies.html)
|
|
23
|
+
- In the [`Infrasound` class](teamml_audio_augment/augmentations/infrasound.py), the `_oscillator_bank` function is adapted from [`torchaudio`](https://docs.pytorch.org/audio/2.7.0/generated/torchaudio.prototype.functional.oscillator_bank.html)
|
|
24
|
+
- In [_advanced.py](teamml_audio_augment/core/_advanced.py), the functions are adapted from several advanced and/or compiled libraries:
|
|
25
|
+
- The `rms` function is adapted from [`numpy-rms`](https://pypi.org/project/numpy-rms/)
|
|
26
|
+
- The `minmax` function is adapted from [`numpy-minmax`](https://pypi.org/project/numpy-minmax/)
|
|
27
|
+
- The `_next_fast_len` function is adapted from [`scipy.signal`](https://docs.scipy.org/doc/scipy/reference/signal.html)
|
|
28
|
+
|
|
29
|
+
Our contributions include everything in [advanced](teamml_audio_augment/advanced/) and the `Infrasound` class.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "teamml_audio_augment"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
|
|
5
|
+
description = "Audio augmentations for TEAM-ML models and data"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Natural Language :: English",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
]
|
|
16
|
+
license = "MIT"
|
|
17
|
+
license-files = ["LICENSE*"]
|
|
18
|
+
|
|
19
|
+
dependencies = [
|
|
20
|
+
"audiomlspectools>=0.11.2",
|
|
21
|
+
"numpy>=2.5.1",
|
|
22
|
+
"numpy-audio-limiter>=0.2.0",
|
|
23
|
+
"scipy>=1.18.0",
|
|
24
|
+
"tinytag>=2.2.1",
|
|
25
|
+
"torch>=2.10.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
demo = [
|
|
30
|
+
"streamlit>=1.59.2",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Stonewall-Defense/audio-augment"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["setuptools"]
|
|
38
|
+
build-backend = "setuptools.build_meta"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["teamml_audio_augment*"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .augmentations.add_background_noise import AddBackgroundNoise
|
|
2
|
+
from .augmentations.add_gaussian_snr import AddGaussianSNR
|
|
3
|
+
from .augmentations.air_absorption import AirAbsorption
|
|
4
|
+
from .augmentations.apply_impulse_response import ApplyImpulseResponse
|
|
5
|
+
from .augmentations.gain import Gain
|
|
6
|
+
from .augmentations.high_pass_filter import HighPassFilter
|
|
7
|
+
from .augmentations.infrasound import Infrasound
|
|
8
|
+
from .augmentations.limiter import Limiter
|
|
9
|
+
from .augmentations.normalize import Normalize
|
|
10
|
+
from .augmentations.shift import Shift
|
|
11
|
+
|
|
12
|
+
from .core.composition import Compose, OneOf, SomeOf
|
|
13
|
+
|
|
14
|
+
from .advanced.esc_background_noise import create_esc_augment, EscClass
|
|
15
|
+
from .advanced.location_based_rir import make_location_rir, RirLocation, RIR_LOC
|
|
16
|
+
from .advanced.mad_background_noise import make_mad_bg_single_class, make_mad_bg_multi_class, make_mad_bg_exclude, MadClass, MAD_CLASS
|
|
17
|
+
from .advanced.realistic_background_noise import make_background_noise_source, BackgroundNoise, BG_NOISE_TYPE
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
from .esc_background_noise import create_esc_augment
|
|
2
|
+
from .location_based_rir import make_location_rir, RirLocation
|
|
3
|
+
from .mad_background_noise import make_mad_bg_exclude, make_mad_bg_multi_class, make_mad_bg_single_class, MadClass
|
|
4
|
+
from .realistic_background_noise import make_background_noise_source, BackgroundNoise
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Global Imports
|
|
3
|
+
###############################################################################
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
###############################################################################
|
|
11
|
+
# Local Imports
|
|
12
|
+
###############################################################################
|
|
13
|
+
from teamml_audio_augment.augmentations.add_background_noise import AddBackgroundNoise
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
###############################################################################
|
|
17
|
+
# Enums
|
|
18
|
+
###############################################################################
|
|
19
|
+
class EscClass(Enum):
|
|
20
|
+
ANIMALS = 0
|
|
21
|
+
NATURAL_SOUNDSCAPES = 10
|
|
22
|
+
HUMAN_NONSPEECH = 20
|
|
23
|
+
INTERIOR_DOMESTIC = 30
|
|
24
|
+
EXTERIOR_URBAN = 40
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
###############################################################################
|
|
28
|
+
# Constants
|
|
29
|
+
###############################################################################
|
|
30
|
+
ESC_REGEX = re.compile(r'^\d+-\w+-[A-Z]-(?P<class_id>\d+)\.wav$')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
###############################################################################
|
|
34
|
+
# Helpers
|
|
35
|
+
###############################################################################
|
|
36
|
+
def _file_matches_classes(filename: str, classes: list[EscClass]) -> bool:
|
|
37
|
+
matches = re.match(ESC_REGEX, filename)
|
|
38
|
+
if not matches:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
class_id = int(matches.group("class_id"))
|
|
42
|
+
|
|
43
|
+
for c in classes:
|
|
44
|
+
min_val = c.value
|
|
45
|
+
max_val = min_val + 9
|
|
46
|
+
if min_val <= class_id <= max_val:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
###############################################################################
|
|
53
|
+
# ! EXPORTS
|
|
54
|
+
###############################################################################
|
|
55
|
+
def create_esc_augment(path_to_esc50: str,
|
|
56
|
+
classes: list[EscClass],
|
|
57
|
+
*,
|
|
58
|
+
sample_rate: Optional[int] = None,
|
|
59
|
+
min_snr_db: float = 3.0,
|
|
60
|
+
max_snr_db: float = 30.0,
|
|
61
|
+
p: float = 0.5,
|
|
62
|
+
max_samples: int = 1_000,
|
|
63
|
+
) -> AddBackgroundNoise:
|
|
64
|
+
classes = classes or [val for val in EscClass]
|
|
65
|
+
|
|
66
|
+
audio_files = [os.path.join(path_to_esc50, f) for f in os.listdir(path_to_esc50) if f.endswith(".wav") if _file_matches_classes(f, classes)]
|
|
67
|
+
random.shuffle(audio_files)
|
|
68
|
+
audio_files = audio_files[:max_samples]
|
|
69
|
+
|
|
70
|
+
return AddBackgroundNoise(audio_files, sample_rate=sample_rate, min_snr_db=min_snr_db, max_snr_db=max_snr_db, p=p)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Global Imports
|
|
3
|
+
###############################################################################
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal, Optional
|
|
8
|
+
|
|
9
|
+
###############################################################################
|
|
10
|
+
# 3PP Imports
|
|
11
|
+
###############################################################################
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
###############################################################################
|
|
15
|
+
# Local Imports
|
|
16
|
+
###############################################################################
|
|
17
|
+
from teamml_audio_augment.augmentations.apply_impulse_response import ApplyImpulseResponse
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
###############################################################################
|
|
21
|
+
# Enums
|
|
22
|
+
###############################################################################
|
|
23
|
+
class RirLocation(Enum):
|
|
24
|
+
INDOOR = "indoor"
|
|
25
|
+
OUTDOOR = "outdoor"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RIR_LOC = Literal[
|
|
29
|
+
"indoor",
|
|
30
|
+
"outdoor",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
###############################################################################
|
|
35
|
+
# Helpers
|
|
36
|
+
###############################################################################
|
|
37
|
+
def _filter(metadata: pd.DataFrame, location: RirLocation | RIR_LOC):
|
|
38
|
+
loc = location.value if isinstance(location, RirLocation) else location
|
|
39
|
+
return [f for f in list(metadata.loc[metadata["class_name"] == loc]["filename"])]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
###############################################################################
|
|
43
|
+
# ! EXPORTS
|
|
44
|
+
###############################################################################
|
|
45
|
+
def make_location_rir(path_to_dataset: str | Path,
|
|
46
|
+
rir_location: RirLocation | RIR_LOC,
|
|
47
|
+
*,
|
|
48
|
+
sample_rate: Optional[int] = None,
|
|
49
|
+
p: float = 0.5,
|
|
50
|
+
cahce_size: int = 128,
|
|
51
|
+
):
|
|
52
|
+
fq_metadata_file = os.path.join(path_to_dataset, "metadata.csv")
|
|
53
|
+
metadata = pd.read_csv(fq_metadata_file)
|
|
54
|
+
filtered = [os.path.join(path_to_dataset, "data", f) for f in _filter(metadata, rir_location)]
|
|
55
|
+
return ApplyImpulseResponse(filtered, sample_rate=sample_rate, p=p, lru_cache_size=cahce_size)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Global Imports
|
|
3
|
+
###############################################################################
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
from typing import Literal, Optional
|
|
10
|
+
|
|
11
|
+
###############################################################################
|
|
12
|
+
# Local Imports
|
|
13
|
+
###############################################################################
|
|
14
|
+
from teamml_audio_augment.augmentations.add_background_noise import AddBackgroundNoise
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
###############################################################################
|
|
18
|
+
# Enums
|
|
19
|
+
###############################################################################
|
|
20
|
+
class MadClass(Enum):
|
|
21
|
+
COMMUNICATION = "communications"
|
|
22
|
+
GUNSHOT = "gunshot"
|
|
23
|
+
FOOTSTEPS = "footsteps"
|
|
24
|
+
SHELLING = "shelling"
|
|
25
|
+
VEHICLE = "vehicle"
|
|
26
|
+
HELICOPTER = "helicopter"
|
|
27
|
+
FIGHTER = "fighter"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
MAD_CLASS = Literal[
|
|
31
|
+
"communications",
|
|
32
|
+
"gunshot",
|
|
33
|
+
"footsteps",
|
|
34
|
+
"shelling",
|
|
35
|
+
"vehicle",
|
|
36
|
+
"helicopter",
|
|
37
|
+
"fighter",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
###############################################################################
|
|
42
|
+
# Helpers
|
|
43
|
+
###############################################################################
|
|
44
|
+
def _make_mad_bg_common(mad_files: list[str],
|
|
45
|
+
sample_rate: Optional[int],
|
|
46
|
+
min_snr_db: float,
|
|
47
|
+
max_snr_db: float,
|
|
48
|
+
p: float,
|
|
49
|
+
max_samples: int,
|
|
50
|
+
):
|
|
51
|
+
random.shuffle(mad_files)
|
|
52
|
+
mad_files = mad_files[:max_samples]
|
|
53
|
+
|
|
54
|
+
return AddBackgroundNoise(mad_files, sample_rate=sample_rate, min_snr_db=min_snr_db, max_snr_db=max_snr_db, p=p)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
###############################################################################
|
|
58
|
+
# ! EXPORTS
|
|
59
|
+
###############################################################################
|
|
60
|
+
def make_mad_bg_single_class(path_to_mad: str | Path,
|
|
61
|
+
mad_class: MadClass | MAD_CLASS,
|
|
62
|
+
*,
|
|
63
|
+
sample_rate: Optional[int] = None,
|
|
64
|
+
min_snr_db: float = 3.0,
|
|
65
|
+
max_snr_db: float = 30.0,
|
|
66
|
+
p: float = 0.5,
|
|
67
|
+
max_samples: int = 10_000,
|
|
68
|
+
):
|
|
69
|
+
mc = mad_class.value if isinstance(mad_class, MadClass) else mad_class
|
|
70
|
+
|
|
71
|
+
mad_files = [os.path.join(path_to_mad, f) for f in os.listdir(path_to_mad) if f.endswith(".wav") and mc in f]
|
|
72
|
+
return _make_mad_bg_common(mad_files, sample_rate, min_snr_db, max_snr_db, p, max_samples)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def make_mad_bg_exclude(path_to_mad: str | Path,
|
|
76
|
+
class_to_exclude: MadClass | MAD_CLASS,
|
|
77
|
+
*,
|
|
78
|
+
sample_rate: Optional[int] = None,
|
|
79
|
+
min_snr_db: float = 3.0,
|
|
80
|
+
max_snr_db: float = 30.0,
|
|
81
|
+
p: float = 0.5,
|
|
82
|
+
max_samples: int = 10_000,
|
|
83
|
+
):
|
|
84
|
+
cte = class_to_exclude.value if isinstance(class_to_exclude, MadClass) else class_to_exclude
|
|
85
|
+
|
|
86
|
+
mad_files = [os.path.join(path_to_mad, f) for f in os.listdir(path_to_mad) if f.endswith(".wav") and cte not in f]
|
|
87
|
+
return _make_mad_bg_common(mad_files, sample_rate, min_snr_db, max_snr_db, p, max_samples)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def make_mad_bg_multi_class(path_to_mad: str | Path,
|
|
91
|
+
mad_classes: list[MadClass] | list[MAD_CLASS],
|
|
92
|
+
*,
|
|
93
|
+
sample_rate: Optional[int] = None,
|
|
94
|
+
min_snr_db: float = 3.0,
|
|
95
|
+
max_snr_db: float = 30.0,
|
|
96
|
+
p: float = 0.5,
|
|
97
|
+
max_samples: int = 10_000,
|
|
98
|
+
):
|
|
99
|
+
mc = [mad.value if isinstance(mad, MadClass) else mad for mad in mad_classes]
|
|
100
|
+
|
|
101
|
+
pattern = re.compile(f"(?:{'|'.join(c[:3] for c in mc)}).+.wav")
|
|
102
|
+
mad_files = [os.path.join(path_to_mad, f) for f in os.listdir(path_to_mad) if re.search(pattern, f)]
|
|
103
|
+
return _make_mad_bg_common(mad_files, sample_rate, min_snr_db, max_snr_db, p, max_samples)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Global Imports
|
|
3
|
+
###############################################################################
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal, Optional
|
|
8
|
+
|
|
9
|
+
###############################################################################
|
|
10
|
+
# 3PP Imports
|
|
11
|
+
###############################################################################
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
###############################################################################
|
|
15
|
+
# Local Imports
|
|
16
|
+
###############################################################################
|
|
17
|
+
from teamml_audio_augment.augmentations.add_background_noise import AddBackgroundNoise
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
###############################################################################
|
|
21
|
+
# Enums
|
|
22
|
+
###############################################################################
|
|
23
|
+
class BackgroundNoise(Enum):
|
|
24
|
+
AMBIENT = "ambient"
|
|
25
|
+
ANIMAL = "animal"
|
|
26
|
+
HUMAN = "human"
|
|
27
|
+
VEHICLE = "vehicle"
|
|
28
|
+
WEATHER = "weather"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
BG_NOISE_TYPE = Literal[
|
|
32
|
+
"ambient",
|
|
33
|
+
"animal",
|
|
34
|
+
"human",
|
|
35
|
+
"vehicle",
|
|
36
|
+
"weather",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
###############################################################################
|
|
41
|
+
# ! EXPORTS
|
|
42
|
+
###############################################################################
|
|
43
|
+
def make_background_noise_source(path_to_dataset: str | Path,
|
|
44
|
+
sound_class: BackgroundNoise | BG_NOISE_TYPE,
|
|
45
|
+
*,
|
|
46
|
+
sample_rate: Optional[int] = None,
|
|
47
|
+
min_snr_db: float = 3.0,
|
|
48
|
+
max_snr_db: float = 20.0,
|
|
49
|
+
p: float = 0.5,
|
|
50
|
+
):
|
|
51
|
+
sc = sound_class.value if isinstance(sound_class, BackgroundNoise) else sound_class
|
|
52
|
+
fq_metadata_file = os.path.join(path_to_dataset, "metadata.csv")
|
|
53
|
+
metadata = pd.read_csv(fq_metadata_file)
|
|
54
|
+
filtered = [os.path.join(path_to_dataset, "data", f) for f in list(metadata.loc[metadata["class_name"] == sc]["filename"])]
|
|
55
|
+
return AddBackgroundNoise(filtered, sample_rate=sample_rate, min_snr_db=min_snr_db, max_snr_db=max_snr_db, p=p)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Global Imports
|
|
3
|
+
###############################################################################
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
###############################################################################
|
|
11
|
+
# 3PP Imports
|
|
12
|
+
###############################################################################
|
|
13
|
+
from tinytag import TinyTag
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
###############################################################################
|
|
17
|
+
# Certus Imports
|
|
18
|
+
###############################################################################
|
|
19
|
+
from AudioMlSpecTools import WavReader
|
|
20
|
+
|
|
21
|
+
###############################################################################
|
|
22
|
+
# Local Imports
|
|
23
|
+
###############################################################################
|
|
24
|
+
from teamml_audio_augment.core.transforms_interface import BaseWaveformTransform
|
|
25
|
+
from teamml_audio_augment.core.utils import (
|
|
26
|
+
calculate_desired_noise_rms,
|
|
27
|
+
calculate_rms,
|
|
28
|
+
find_audio_files_in_paths,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
###############################################################################
|
|
33
|
+
# Config
|
|
34
|
+
###############################################################################
|
|
35
|
+
LOGGER = logging.getLogger("teamMl")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
###############################################################################
|
|
39
|
+
# Exports
|
|
40
|
+
###############################################################################
|
|
41
|
+
class AddBackgroundNoise(BaseWaveformTransform):
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
sounds_path: list[Path] | list[str] | Path | str,
|
|
45
|
+
*,
|
|
46
|
+
sample_rate: Optional[int] = None,
|
|
47
|
+
min_snr_db: float = 3.0,
|
|
48
|
+
max_snr_db: float = 30.0,
|
|
49
|
+
p: float = 0.5,
|
|
50
|
+
lru_cache_size: int | None = None,
|
|
51
|
+
):
|
|
52
|
+
super().__init__(sample_rate, p=p)
|
|
53
|
+
|
|
54
|
+
# Boundary parameters
|
|
55
|
+
self.sounds_path = sounds_path
|
|
56
|
+
self.sound_file_paths = find_audio_files_in_paths(self.sounds_path)
|
|
57
|
+
self.sound_file_paths = [str(p) for p in self.sound_file_paths]
|
|
58
|
+
|
|
59
|
+
assert len(self.sound_file_paths) > 0
|
|
60
|
+
|
|
61
|
+
if min_snr_db > max_snr_db:
|
|
62
|
+
raise ValueError("min_snr_db must not be greater than max_snr_db")
|
|
63
|
+
self.min_snr_db = min_snr_db
|
|
64
|
+
self.max_snr_db = max_snr_db
|
|
65
|
+
|
|
66
|
+
# Changing parameters
|
|
67
|
+
self.snr_db = self.min_snr_db
|
|
68
|
+
self.noise_file_path = self.sound_file_paths[0]
|
|
69
|
+
self.offset = 0.0
|
|
70
|
+
self.duration = 0.0
|
|
71
|
+
|
|
72
|
+
# Helper parameters
|
|
73
|
+
if lru_cache_size is not None:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Passing lru_cache_size is no longer supported, as the cache has been removed (since v0.43.0)."
|
|
76
|
+
)
|
|
77
|
+
self.time_info_arr = torch.full((len(self.sound_file_paths),), -1.0)
|
|
78
|
+
|
|
79
|
+
def randomize_parameters(self, samples: torch.Tensor):
|
|
80
|
+
super().randomize_parameters(samples)
|
|
81
|
+
|
|
82
|
+
if self.should_apply:
|
|
83
|
+
self.snr_db = random.uniform(self.min_snr_db, self.max_snr_db)
|
|
84
|
+
|
|
85
|
+
file_idx = random.randint(0, len(self.sound_file_paths) - 1)
|
|
86
|
+
self.noise_file_path = self.sound_file_paths[file_idx]
|
|
87
|
+
|
|
88
|
+
if self.time_info_arr[file_idx] == -1.0:
|
|
89
|
+
duration = TinyTag.get(self.noise_file_path).duration or 0.0
|
|
90
|
+
if duration == 0.0:
|
|
91
|
+
warnings.warn(f"Empty noise file: {self.noise_file_path}")
|
|
92
|
+
self.time_info_arr[file_idx] = duration
|
|
93
|
+
|
|
94
|
+
noise_duration = float(self.time_info_arr[file_idx])
|
|
95
|
+
signal_duration = samples.shape[-1] / self.sample_rate
|
|
96
|
+
|
|
97
|
+
min_noise_offset = 0.0
|
|
98
|
+
max_noise_offset = max(0.0, noise_duration - signal_duration)
|
|
99
|
+
|
|
100
|
+
self.offset = random.uniform(min_noise_offset, max_noise_offset)
|
|
101
|
+
self.duration = signal_duration
|
|
102
|
+
|
|
103
|
+
def apply(self, samples: torch.Tensor) -> torch.Tensor:
|
|
104
|
+
end_sec = self.offset + self.duration
|
|
105
|
+
noise_sound = WavReader(target_sr=self.sample_rate).read(self.noise_file_path, start_sec=self.offset, end_sec=end_sec)
|
|
106
|
+
|
|
107
|
+
noise_rms = calculate_rms(noise_sound)
|
|
108
|
+
if noise_rms < 1e-9:
|
|
109
|
+
warnings.warn(
|
|
110
|
+
"The file {} is too silent to be added as noise. Returning the input"
|
|
111
|
+
" unchanged.".format(self.noise_file_path)
|
|
112
|
+
)
|
|
113
|
+
return samples
|
|
114
|
+
|
|
115
|
+
clean_rms = calculate_rms(samples)
|
|
116
|
+
|
|
117
|
+
desired_noise_rms = calculate_desired_noise_rms(clean_rms, self.snr_db)
|
|
118
|
+
|
|
119
|
+
# Adjust the noise to match the desired noise RMS
|
|
120
|
+
noise_sound = noise_sound * (desired_noise_rms / noise_rms)
|
|
121
|
+
|
|
122
|
+
# Repeat the sound if it shorter than the input sound
|
|
123
|
+
num_samples = samples.shape[-1]
|
|
124
|
+
while noise_sound.shape[-1] < num_samples:
|
|
125
|
+
noise_sound = torch.concatenate((noise_sound, noise_sound), dim=-1)
|
|
126
|
+
|
|
127
|
+
if noise_sound.shape[-1] > num_samples:
|
|
128
|
+
noise_sound = noise_sound[..., 0:num_samples]
|
|
129
|
+
|
|
130
|
+
LOGGER.debug(f"Appied background noise @ {self.snr_db:.03f} dB")
|
|
131
|
+
|
|
132
|
+
# Return a mix of the input sound and the background noise sound
|
|
133
|
+
return samples + noise_sound
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Global Imports
|
|
3
|
+
###############################################################################
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
###############################################################################
|
|
9
|
+
# 3PP Imports
|
|
10
|
+
###############################################################################
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
###############################################################################
|
|
14
|
+
# Local Imports
|
|
15
|
+
###############################################################################
|
|
16
|
+
from teamml_audio_augment.core.transforms_interface import BaseWaveformTransform
|
|
17
|
+
from teamml_audio_augment.core.utils import (
|
|
18
|
+
calculate_desired_noise_rms,
|
|
19
|
+
calculate_rms,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
###############################################################################
|
|
24
|
+
# Config
|
|
25
|
+
###############################################################################
|
|
26
|
+
LOGGER = logging.getLogger("teamMl")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
###############################################################################
|
|
30
|
+
# Exports
|
|
31
|
+
###############################################################################
|
|
32
|
+
class AddGaussianSNR(BaseWaveformTransform):
|
|
33
|
+
"""
|
|
34
|
+
Add gaussian noise to the input. A random Signal to Noise Ratio (SNR) will be picked
|
|
35
|
+
uniformly in the Decibel scale. This aligns with human hearing, which is more
|
|
36
|
+
logarithmic than linear.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
supports_multichannel = True
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
sample_rate: Optional[int] = None,
|
|
45
|
+
min_snr_db: float = 5.0,
|
|
46
|
+
max_snr_db: float = 40.0,
|
|
47
|
+
p: float = 0.5,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
:param min_snr_db: Minimum signal-to-noise ratio in dB. A lower number means more noise.
|
|
51
|
+
:param max_snr_db: Maximum signal-to-noise ratio in dB. A greater number means less noise.
|
|
52
|
+
:param p: The probability of applying this transform
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(sample_rate, p=p)
|
|
55
|
+
|
|
56
|
+
if min_snr_db > max_snr_db:
|
|
57
|
+
raise ValueError("min_snr_db must not be greater than max_snr_db")
|
|
58
|
+
self.min_snr_db = min_snr_db
|
|
59
|
+
self.max_snr_db = max_snr_db
|
|
60
|
+
|
|
61
|
+
self.noise_std = 0.0
|
|
62
|
+
|
|
63
|
+
def randomize_parameters(self, samples: torch.Tensor):
|
|
64
|
+
super().randomize_parameters(samples)
|
|
65
|
+
if self.should_apply:
|
|
66
|
+
# Pick SNR in decibel scale
|
|
67
|
+
snr = random.uniform(self.min_snr_db, self.max_snr_db)
|
|
68
|
+
|
|
69
|
+
clean_rms = calculate_rms(samples)
|
|
70
|
+
noise_rms = calculate_desired_noise_rms(clean_rms=clean_rms, snr=snr)
|
|
71
|
+
|
|
72
|
+
# In gaussian noise, the RMS gets roughly equal to the std
|
|
73
|
+
self.noise_std = float(noise_rms)
|
|
74
|
+
|
|
75
|
+
def apply(self, samples: torch.Tensor) -> torch.Tensor:
|
|
76
|
+
noise = torch.normal(0.0, self.noise_std, size=samples.shape)
|
|
77
|
+
|
|
78
|
+
LOGGER.debug(f"Appied Gaussian SNR @ {self.noise_std:.03f}")
|
|
79
|
+
|
|
80
|
+
return samples + noise
|