diff --git a/CLAUDE.md b/CLAUDE.md
index 6273da4..56c488f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,15 +2,17 @@
**Always update this file when making notable changes** (new commands, new presets, architectural decisions, scoring changes, dependency additions).
+**Always update README.md** when changing presets, CLI flags, output format, or any user-facing behavior.
+
## Project
-`noisekit` is a `uvx`-compatible Python CLI that generates degraded speech datasets from clean HuggingFace corpora. It simulates three real-world audio degradation scenarios — telecommunication (G.711 calls), bad audio encoding (low-bitrate codecs), and noisy environments (real ambient noise) — for ASR noise-robustness benchmarking. A `clean_reference` control completes the catalog.
+`noisekit` is a `uvx`-compatible Python CLI that generates degraded speech datasets from clean HuggingFace corpora. It simulates seven atomic audio degradation scenarios — telecom (G.711 calls), bad audio encoding (low-bitrate codecs), noisy environments (real ambient noise), far-field reverb, transmission dropout, and clipping distortion — plus compound multi-condition scenarios built by chaining atomic presets. Designed for ASR noise-robustness benchmarking. A `clean_reference` control completes the catalog.
## Package Management
Use **UV** for everything: `uv add`, `uv run`, `uv sync`. Never use pip directly.
-Key runtime dependencies: `audiomentations>=0.38`, `lameenc>=1.4` (pure-Python MP3 encoder used by `Mp3Compression` in `telecommunication` and `bad_audio_encoding`; no system ffmpeg needed), `torchmetrics>=1.7.0` (NISQA scoring — downloads ~50 MB model weights to `~/.torchmetrics/NISQA/` on first use).
+Key runtime dependencies: `audiomentations>=0.38`, `lameenc>=1.4` (pure-Python MP3 encoder used by `Mp3Compression` in `telecom` and `bad_audio_encoding`; no system ffmpeg needed), `torchmetrics>=1.7.0` (NISQA scoring — downloads ~50 MB model weights to `~/.torchmetrics/NISQA/` on first use), `pyroomacoustics` (room acoustics simulation for `reverb_far_field` — now a core dependency, no extra install needed).
## Architecture
@@ -61,29 +63,78 @@ transforms:
Built-in presets:
-| Preset | Scenario | Bandwidth | PESQ mode | Target MOS |
-| -------------------- | -------------------------------------------- | ------------------- | --------- | ---------- |
-| `clean_reference` | Minimal gain normalization (PESQ ceiling) | full | WB 16 kHz | 4.0-4.5 |
-| `telecommunication` | G.711 call + low-bitrate MP3 codec artifacts | 300-3400 Hz @ 8 kHz | NB 8 kHz | 2.0-3.5 |
-| `bad_audio_encoding` | Aggressive low-bitrate MP3 (16-32 kbps) | 80-7500 Hz @ 16 kHz | WB 16 kHz | 1.5-2.5 |
-| `noisy_environment` | Real ambient noise via `AddBackgroundNoise` | up to 8-12 kHz | WB 16 kHz | 2.0-3.5 |
+### Atomic Presets
+
+| Preset | Scenario | Bandwidth | PESQ mode | Target MOS |
+| ---------------------- | ----------------------------------------------------- | ------------------- | --------- | ---------- |
+| `clean_reference` | Minimal gain normalization (PESQ ceiling) | full | WB 16 kHz | 4.0-4.5 |
+| `telecom` | G.711 call + low-bitrate MP3 codec artifacts | 300-3400 Hz @ 8 kHz | NB 8 kHz | 2.0-3.5 |
+| `bad_audio_encoding` | Aggressive low-bitrate MP3 (16-32 kbps) | 80-7500 Hz @ 16 kHz | WB 16 kHz | 1.5-2.5 |
+| `noisy_environment` | Real ambient noise via `AddBackgroundNoise` | up to 8-12 kHz | WB 16 kHz | 2.0-3.5 |
+| `clipping_distortion` | Microphone overload / ADC saturation (`ClippingDistortion` 10-25%) | full | WB 16 kHz | 2.0-3.5 |
+| `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows | full | WB 16 kHz | 1.5-3.0 |
+| `reverb_far_field` | Far-field reverberant room via `RoomSimulator` | full | WB 16 kHz | 2.0-3.5 |
+
+`telecom` and any compound preset ending with `telecom` use the 8 kHz PESQ NB scoring split (see below). All other presets score in PESQ WB at 16 kHz.
+
+### Compound Presets
+
+Compound presets chain two or more atomic presets together. Noise is added first (acoustic environment), then codec/dropout (digital processing of the already-degraded signal).
+
+| Preset | Chain | Requires | PESQ mode | Target MOS |
+| ------------------ | ----------------------------------------- | ------------- | --------- | ---------- |
+| `noisy_telecom` | `noisy_environment` → `telecom` | `--noise-dir` | NB 8 kHz | 1.5-2.5 |
+| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 16 kHz | 1.0-2.5 |
+| `clipping_telecom` | `clipping_distortion` → `telecom` | — | NB 8 kHz | 1.0-2.5 |
+
+### Compound Preset YAML Format
+
+A preset can use `chain:` instead of `transforms:` to apply multiple atomic presets sequentially:
-`telecommunication` uses the 8 kHz PESQ NB scoring split (see below). All other presets score in PESQ WB at 16 kHz.
+```yaml
+name: my_compound
+description: "..."
+chain:
+ - atomic_preset_a
+ - atomic_preset_b
+```
+
+Rules:
+- `chain` and `transforms` are mutually exclusive.
+- Chained entries must be names of built-in atomic presets (no nesting chains).
+- `${NOISE_DIR}` resolution and the PESQ NB scoring split are detected automatically across the full concatenated chain.
+- `reverb_far_field` uses `pyroomacoustics` (bundled as a core dependency — no extra install needed).
### Why no white noise
The catalog deliberately avoids `AddGaussianSNR` — white Gaussian noise sounds artificial and doesn't reflect real production audio. Instead:
-- `telecommunication` and `bad_audio_encoding` rely on `Mp3Compression` at 16-32 kbps for realistic codec smearing/pre-echo.
+- `telecom` and `bad_audio_encoding` rely on `Mp3Compression` at 16-32 kbps for realistic codec smearing/pre-echo.
- `noisy_environment` uses `AddBackgroundNoise` over a user-supplied WAV corpus (MUSAN/DEMAND/FSD50K), so the noise floor matches the real environment you care about.
## PESQ Scoring — Important Design Decision
-For `telecommunication`, PESQ is computed at **8 kHz narrowband** on the audio **before** the final `Resample(16000)` restoration step. Output WAV files are still saved at 16 kHz.
+For `telecom`, PESQ is computed at **8 kHz narrowband** on the audio **before** the final `Resample(16000)` restoration step. Output WAV files are still saved at 16 kHz.
**Why:** Computing PESQ NB by downsampling the 16 kHz output (8k→16k→8k round-trip) collapses all telephony scores to ~1.1 regardless of noise level. Scoring at the 8 kHz intermediate stage gives proper stratification.
-**BitCrush + Normalize:** `telecommunication` inserts `Normalize(p=1.0)` immediately before `BitCrush`. HuggingFace speech datasets (e.g., FLEURS) often have very low peak amplitude (~0.001-0.02). At 8-bit depth the quantization step is 0.0078 — a peak below one step rounds the entire signal to zero. Normalizing to ±1 before quantization ensures all 256 levels are used.
+**BitCrush + Normalize:** `telecom` inserts `Normalize(p=1.0)` immediately before `BitCrush`. HuggingFace speech datasets (e.g., FLEURS) often have very low peak amplitude (~0.001-0.02). At 8-bit depth the quantization step is 0.0078 — a peak below one step rounds the entire signal to zero. Normalizing to ±1 before quantization ensures all 256 levels are used.
+
+## Input Normalization — Global Pipeline Decision
+
+`pipeline.py` peak-normalizes every input sample to amplitude 1.0 immediately after resampling to 16 kHz, before any preset transforms run:
+
+```python
+peak = np.abs(ref_16k).max()
+if peak > 1e-9:
+ ref_16k = ref_16k / peak
+```
+
+**Why:** HuggingFace datasets often have peaks as low as 0.001–0.02. Without normalization, `AddBackgroundNoise` (relative SNR mode) scales noise proportional to that tiny signal RMS — both speech and noise end up inaudible, and 16-bit PCM quantization noise dominates. Peak normalization guarantees all presets receive a full-scale signal.
+
+**Safety:** The same normalized `ref_16k` is used as both the transform input and the PESQ/SNR reference, so all quality metrics remain valid relative comparisons. The mid-chain `Normalize` inside `telecom.yaml` (before `BitCrush`) is still needed separately — the bandpass filter removes energy and that step re-normalizes before quantization.
+
+**`noisy_environment` also pre-normalizes:** `noisy_environment.yaml` adds a `Normalize` as its first transform. This handles the `reverb_noisy` compound case: `RoomSimulator` can attenuate the signal by ~10× at large mic distances; without the mid-chain normalize, `AddBackgroundNoise` would see the attenuated level and mix noise too quietly. All compound presets using `noisy_environment` inherit this fix automatically.
`transforms.py` auto-detects this split: if the last transform is `Resample(16000)`, it creates a `scoring` Compose (all-but-last) alongside the `full` Compose.
@@ -97,11 +148,11 @@ Uses `datasets` with `Audio(decode=False)` + manual `soundfile` decoding — avo
```json
{
- "file_name": "audio/common_voice_en_23136613_telecommunication.wav",
+ "file_name": "audio/common_voice_en_23136613_telecom.wav",
"source": "common_voice_en_23136613.mp3",
"dataset": "google/fleurs",
"language": "en-US",
- "preset": "telecommunication",
+ "preset": "telecom",
"transcript": "...",
"snr_db": 1.8,
"pesq_mos": 2.86,
@@ -126,16 +177,40 @@ uv run noisekit list-presets --verbose
uv run noisekit generate \
--dataset google/fleurs \
--config en_us --split test \
- --samples 3 --presets clean_reference telecommunication bad_audio_encoding \
+ --samples 3 --presets clean_reference telecom bad_audio_encoding \
--output ./test_out --seed 42
cat test_out/metadata.jsonl
+# New atomic presets — no external dependencies
+uv run noisekit generate \
+ --dataset google/fleurs --config en_us --split test \
+ --samples 3 --presets clipping_distortion transmission_dropout \
+ --no-nisqa --output ./test_atomic --seed 42
+
# noisy_environment — auto-downloads MUSAN noise-only clips on first run
uv run noisekit generate \
--dataset google/fleurs --config en_us --split test \
--samples 3 --presets noisy_environment \
--output ./test_noise --seed 42
+# Compound presets (auto-downloads MUSAN noise on first run)
+uv run noisekit generate \
+ --dataset google/fleurs --config en_us --split test \
+ --samples 3 --presets noisy_telecom \
+ --no-nisqa --output ./test_compound --seed 42
+
+# clipping_telecom — no noise dir needed
+uv run noisekit generate \
+ --dataset google/fleurs --config en_us --split test \
+ --samples 3 --presets clipping_telecom \
+ --no-nisqa --output ./test_clipping_telecom --seed 42
+
+# Far-field reverb
+uv run noisekit generate \
+ --dataset google/fleurs --config en_us --split test \
+ --samples 3 --presets reverb_far_field reverb_noisy \
+ --no-nisqa --output ./test_reverb --seed 42
+
# noisy_environment with your own noise corpus (skips auto-download)
uv run noisekit generate \
--dataset google/fleurs --config en_us --split test \
@@ -144,6 +219,8 @@ uv run noisekit generate \
--output ./test_noise --seed 42
```
-Expected PESQ spread: clean ~4.6, telecommunication ~2.5-3.5 (NB), bad_audio_encoding ~1.5-2.5 (WB), noisy_environment ~1.0-2.5 (WB).
+Expected PESQ spread: clean ~4.6, telecom ~2.5-3.5 (NB), bad_audio_encoding ~1.5-2.5 (WB), noisy_environment ~1.0-2.5 (WB), clipping_distortion ~2.0-3.5 (WB), transmission_dropout ~1.5-3.0 (WB), reverb_far_field ~2.0-3.5 (WB).
+
+Compound preset PESQ: noisy_telecom ~1.5-2.5 (NB), clipping_telecom ~1.0-2.5 (NB), reverb_noisy ~1.0-2.5 (WB).
Expected NISQA spread: clean ~4.0-4.5, degraded presets ~1.5-3.0. NISQA model weights (~50 MB) are downloaded on first run.
diff --git a/README.md b/README.md
index 7a2c4f0..40d6bc5 100644
--- a/README.md
+++ b/README.md
@@ -10,22 +10,23 @@
-Generate noise-stratified speech datasets for ASR benchmark studies.
+Generate degraded speech datasets for noise-robust ASR benchmarking.
-Takes a clean speech-to-text dataset from HuggingFace, applies real-world degradation presets via [audiomentations](https://github.com/iver56/audiomentations), and scores each output with PESQ + SNR + NISQA — producing a JSONL manifest ready for noise-robustness benchmarking.
+Takes a clean HuggingFace speech dataset, applies real-world degradation presets via [audiomentations](https://github.com/iver56/audiomentations), and scores each output with PESQ, SNR, and NISQA, producing a JSONL manifest ready for noise-robustness benchmarking.
-Three scenarios are covered out of the box: **telecommunication** (G.711 + low-bitrate MP3 codec artifacts), **bad audio encoding** (aggressive low-bitrate compression), and **noisy environment** (real ambient noise from a user-supplied corpus).
+Seven atomic degradation scenarios are built in: telephony (G.711 + low-bitrate codec), wideband codec compression, ambient noise, clipping distortion, transmission dropout, and far-field reverb. Atomic presets compose into compound multi-condition scenarios.
+
+> [!NOTE]
+> Degradations are programmatically simulated. Scores may not generalize to genuine production recordings; validate final benchmarks on annotated real-world data.
## How it works
```mermaid
flowchart LR
A[("HuggingFace\nDataset")] --> B["noisekit generate"]
- B --> C["telecommunication\nG.711 + MP3"]
- B --> D["bad_audio_encoding\n16-32 kbps MP3"]
- B --> E["noisy_environment\nReal ambient noise"]
- B --> F["clean_reference\nControl"]
- C & D & E & F --> G[("WAVs +\nmetadata.jsonl\nPESQ · SNR · NISQA")]
+ B --> C["7 atomic presets\ncodec · noise · reverb\ndropout · clipping"]
+ B --> D["3 compound presets\nmulti-condition chains"]
+ C & D --> E[("WAVs + metadata.jsonl\nPESQ · SNR · NISQA")]
```
## Install
@@ -55,7 +56,7 @@ uvx noisekit generate \
--config en_us \
--split test \
--samples 300 \
- --presets telecommunication bad_audio_encoding \
+ --presets telecom bad_audio_encoding \
--output ./benchmark_dataset \
--seed 42
```
@@ -76,7 +77,7 @@ Output:
benchmark_dataset/
├── metadata.jsonl # one entry per generated file (AudioFolder format)
└── audio/
- ├── sample_0000_telecommunication.wav
+ ├── sample_0000_telecom.wav
├── sample_0001_bad_audio_encoding.wav
└── ...
```
@@ -92,11 +93,11 @@ Each `metadata.jsonl` entry:
```json
{
- "file_name": "audio/sample_0042_telecommunication.wav",
+ "file_name": "audio/sample_0042_telecom.wav",
"source": "common_voice_en_23136613.mp3",
"dataset": "google/fleurs",
"language": "en-US",
- "preset": "telecommunication",
+ "preset": "telecom",
"transcript": "the cat sat on the mat",
"snr_db": 5.2,
"pesq_mos": 2.78,
@@ -130,19 +131,46 @@ uvx noisekit list-presets --verbose # show full transform stack
## Presets
-Four built-in presets — three real-world scenarios plus a clean control. None use synthetic white noise; codec artifacts and real ambient recordings produce the degradation instead.
+Ten built-in presets: seven atomic scenarios, three compound multi-condition presets, and a clean reference control. None use synthetic white noise; codec artifacts, real ambient recordings, and room simulation produce the degradation instead.
+
+### Atomic presets
+
+| Preset | Description | PESQ |
+| ---------------------- | ------------------------------------------------------------------------ | ---------- |
+| `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 |
+| `telecom` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 |
+| `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 |
+| `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 5-15 dB | WB 1.0-2.5 |
+| `clipping_distortion` | Microphone overload: clips the loudest 10-25% of samples | WB 2.0-3.5 |
+| `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows (60-180 ms each) | WB 1.5-3.0 |
+| `reverb_far_field` | Far-field room reverb at 1-3 m mic distance | WB 2.0-3.5 |
-| Preset | Description | PESQ |
-| -------------------- | ------------------------------------------------------------------------ | ---------- |
-| `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 |
-| `telecommunication` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 |
-| `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 |
-| `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 3-20 dB | WB 1.0-2.5 |
+`telecom` is scored with PESQ narrowband at 8 kHz (before the final upsample); all other presets are scored wideband at 16 kHz.
-`telecommunication` is scored with PESQ narrowband at 8 kHz (before the final upsample); all other presets are scored wideband at 16 kHz.
+All atomic presets require no noise corpus. All dependencies, including `pyroomacoustics` (used by `reverb_far_field`), are bundled with no extra install needed.
`noisy_environment` requires `--noise-dir` pointing at a directory of background-noise WAVs (e.g. MUSAN, DEMAND, FSD50K). If omitted, noisekit auto-downloads a small MUSAN noise-only subset (~120 MB) from HuggingFace on first use.
+### Compound presets
+
+Compound presets chain two atomic presets together. Noise is applied first (acoustic environment), then codec or dropout (digital processing on the already-degraded signal).
+
+| Preset | Chain | Requires | PESQ |
+| ------------------ | ---------------------------------------- | ------------- | ---------- |
+| `noisy_telecom` | `noisy_environment` → `telecom` | `--noise-dir` | NB 1.5-2.5 |
+| `clipping_telecom` | `clipping_distortion` → `telecom` | (none) | NB 1.0-2.5 |
+| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 1.0-2.5 |
+
+You can also define your own compound preset with a `chain:` key in a YAML file:
+
+```yaml
+name: my_compound
+description: "Noisy environment then telephony codec"
+chain:
+ - noisy_environment
+ - telecom
+```
+
### Custom presets
Pass your own YAML file with `--preset-file`:
@@ -179,10 +207,10 @@ transforms:
p: 1.0
```
-Any transform from [audiomentations](https://github.com/iver56/audiomentations) is supported. Use `${NOISE_DIR}` as a placeholder for `--noise-dir` inside your preset YAML.
+Any transform from [audiomentations](https://github.com/iver56/audiomentations) is supported. Use `${NOISE_DIR}` as a placeholder for `--noise-dir` inside your preset YAML. Use `chain:` instead of `transforms:` to compose built-in atomic presets sequentially.
## Requirements
- Python ≥ 3.10
- [uv](https://docs.astral.sh/uv/) for `uvx` usage
-- No system dependencies — MP3 encoding uses pure-Python `lameenc`, no ffmpeg needed
+- No system dependencies: MP3 encoding uses pure-Python `lameenc`, no ffmpeg needed
diff --git a/assets/banner.svg b/assets/banner.svg
index 3021680..3b5ab1e 100644
--- a/assets/banner.svg
+++ b/assets/banner.svg
@@ -9,7 +9,7 @@
noisekit
- Noise-stratified speech datasets for ASR robustness
+ Noisy speech datasets for ASR robustness
diff --git a/noisekit/cli.py b/noisekit/cli.py
index a3e60ff..bb9c011 100644
--- a/noisekit/cli.py
+++ b/noisekit/cli.py
@@ -10,7 +10,7 @@
app = typer.Typer(
name="noisekit",
- help="Generate noise-stratified speech datasets for ASR benchmark studies.",
+ help="Generate noisy speech datasets for ASR benchmark studies.",
add_completion=False,
)
console = Console()
@@ -44,6 +44,12 @@ def generate(
] = True,
) -> None:
"""Generate a degraded speech dataset by applying audio presets to a clean source dataset."""
+ console.print(
+ "[bold yellow]⚠ Synthetic pipeline[/bold yellow] — outputs approximate real-world degradation "
+ "but are not a substitute for true annotated production audio. "
+ "Validate final benchmarks on real degraded recordings.",
+ style="yellow",
+ )
from .pipeline import run_generate
resolved_output = output if output is not None else Path("./output") / datetime.now().strftime("%Y-%m-%d_%H%M%S")
@@ -100,7 +106,10 @@ def list_presets(
table.add_column("Transforms")
for p in presets:
- transforms_str = " → ".join(f"{t['type']}(p={t.get('p', 1.0)})" for t in p.get("transforms", []))
+ if "chain" in p:
+ transforms_str = "chain: " + " → ".join(p["chain"])
+ else:
+ transforms_str = " → ".join(f"{t['type']}(p={t.get('p', 1.0)})" for t in p.get("transforms", []))
if verbose:
table.add_row(p["name"], p["description"], transforms_str)
else:
diff --git a/noisekit/pipeline.py b/noisekit/pipeline.py
index a91040c..7167b56 100644
--- a/noisekit/pipeline.py
+++ b/noisekit/pipeline.py
@@ -58,6 +58,9 @@ def run_generate(
ref_array, ref_sr, transcript = extract_audio_and_text(sample)
language = extract_language(sample, config)
ref_16k = _resample_to_16k(ref_array, ref_sr)
+ peak = np.abs(ref_16k).max()
+ if peak > 1e-9:
+ ref_16k = ref_16k / peak
raw_path = sample.get("audio", {}).get("path") or ""
raw_stem = Path(raw_path).stem if raw_path else f"sample_{i:04d}"
diff --git a/noisekit/presets/clipping_distortion.yaml b/noisekit/presets/clipping_distortion.yaml
new file mode 100644
index 0000000..daa5bac
--- /dev/null
+++ b/noisekit/presets/clipping_distortion.yaml
@@ -0,0 +1,13 @@
+name: clipping_distortion
+description: "Microphone overload / ADC saturation: clips 10-25% of peak samples. PESQ WB target: 2.0-3.5"
+transforms:
+ - type: ClippingDistortion
+ parameters:
+ min_percentile_threshold: 10
+ max_percentile_threshold: 25
+ p: 1.0
+ - type: Gain
+ parameters:
+ min_gain_db: -3.0
+ max_gain_db: 3.0
+ p: 0.5
diff --git a/noisekit/presets/clipping_telecom.yaml b/noisekit/presets/clipping_telecom.yaml
new file mode 100644
index 0000000..441eeea
--- /dev/null
+++ b/noisekit/presets/clipping_telecom.yaml
@@ -0,0 +1,5 @@
+name: clipping_telecom
+description: "Clipped microphone signal over a phone line: ADC saturation + G.711 codec. PESQ NB target: 1.0-2.5"
+chain:
+ - clipping_distortion
+ - telecom
diff --git a/noisekit/presets/noisy_environment.yaml b/noisekit/presets/noisy_environment.yaml
index ff2e234..544bf40 100644
--- a/noisekit/presets/noisy_environment.yaml
+++ b/noisekit/presets/noisy_environment.yaml
@@ -1,6 +1,9 @@
name: noisy_environment
description: "Real-world ambient noise (cafe/street/office) mixed in at variable SNR. Requires --noise-dir. PESQ WB target: 1.0–2.5"
transforms:
+ - type: Normalize
+ parameters: {}
+ p: 1.0
- type: AddBackgroundNoise
parameters:
sounds_path: ${NOISE_DIR}
diff --git a/noisekit/presets/noisy_telecom.yaml b/noisekit/presets/noisy_telecom.yaml
new file mode 100644
index 0000000..f049be8
--- /dev/null
+++ b/noisekit/presets/noisy_telecom.yaml
@@ -0,0 +1,5 @@
+name: noisy_telecom
+description: "Telephony call in a noisy environment: ambient noise + G.711 codec. Requires --noise-dir. PESQ NB target: 1.5-2.5"
+chain:
+ - noisy_environment
+ - telecom
diff --git a/noisekit/presets/reverb_far_field.yaml b/noisekit/presets/reverb_far_field.yaml
new file mode 100644
index 0000000..11c6431
--- /dev/null
+++ b/noisekit/presets/reverb_far_field.yaml
@@ -0,0 +1,18 @@
+name: reverb_far_field
+description: "Far-field recording in a reverberant room (lecture hall / open office). Requires pyroomacoustics. PESQ WB target: 2.0-3.5"
+transforms:
+ - type: RoomSimulator
+ parameters:
+ min_size_x: 5.0
+ max_size_x: 10.0
+ min_size_y: 5.0
+ max_size_y: 10.0
+ min_size_z: 2.5
+ max_size_z: 4.0
+ min_absorption_value: 0.05
+ max_absorption_value: 0.20
+ min_mic_distance: 1.0
+ max_mic_distance: 3.0
+ use_ray_tracing: false
+ leave_length_unchanged: true
+ p: 1.0
diff --git a/noisekit/presets/reverb_noisy.yaml b/noisekit/presets/reverb_noisy.yaml
new file mode 100644
index 0000000..cea6450
--- /dev/null
+++ b/noisekit/presets/reverb_noisy.yaml
@@ -0,0 +1,5 @@
+name: reverb_noisy
+description: "Far-field reverberant recording with ambient noise. Requires --noise-dir and pyroomacoustics. PESQ WB target: 1.0-2.5"
+chain:
+ - reverb_far_field
+ - noisy_environment
diff --git a/noisekit/presets/telecommunication.yaml b/noisekit/presets/telecom.yaml
similarity index 97%
rename from noisekit/presets/telecommunication.yaml
rename to noisekit/presets/telecom.yaml
index c164a29..a922879 100644
--- a/noisekit/presets/telecommunication.yaml
+++ b/noisekit/presets/telecom.yaml
@@ -1,4 +1,4 @@
-name: telecommunication
+name: telecom
description: "G.711-style call: 8 kHz codec + 300–3400 Hz bandpass + low-bitrate MP3 artifacts. PESQ NB target: 2.0–3.5"
transforms:
- type: Resample
diff --git a/noisekit/presets/transmission_dropout.yaml b/noisekit/presets/transmission_dropout.yaml
new file mode 100644
index 0000000..4859e40
--- /dev/null
+++ b/noisekit/presets/transmission_dropout.yaml
@@ -0,0 +1,21 @@
+name: transmission_dropout
+description: "VoIP/streaming packet loss: 1-3 silent dropout windows per utterance. PESQ WB target: 1.5-3.0"
+transforms:
+ - type: TimeMask
+ parameters:
+ min_band_part: 0.02
+ max_band_part: 0.06
+ fade_duration: 0.002
+ p: 1.0
+ - type: TimeMask
+ parameters:
+ min_band_part: 0.02
+ max_band_part: 0.06
+ fade_duration: 0.002
+ p: 0.8
+ - type: TimeMask
+ parameters:
+ min_band_part: 0.02
+ max_band_part: 0.06
+ fade_duration: 0.002
+ p: 0.5
diff --git a/noisekit/transforms.py b/noisekit/transforms.py
index b3be0f1..cc472e0 100644
--- a/noisekit/transforms.py
+++ b/noisekit/transforms.py
@@ -6,6 +6,7 @@
import audiomentations
import numpy as np
import yaml
+from audiomentations.core.transforms_interface import BaseWaveformTransform
class _SRTrackingCompose:
@@ -19,7 +20,7 @@ class _SRTrackingCompose:
the effective rate from the length ratio before calling the next transform.
"""
- def __init__(self, transforms: list[audiomentations.BaseWaveformTransform]) -> None:
+ def __init__(self, transforms: list[BaseWaveformTransform]) -> None:
self._transforms = transforms
def __call__(self, samples: np.ndarray, sample_rate: int) -> np.ndarray:
@@ -67,10 +68,42 @@ def preset_requires_noise_dir(name: str, preset_file: Path | None = None) -> boo
for v in t.get("parameters", {}).values():
if v == _NOISE_DIR_PLACEHOLDER:
return True
- return False
+ return any(preset_requires_noise_dir(chained_name, preset_file) for chained_name in cfg.get("chain", []))
-def _make_transform(t: dict, noise_dir: Path | None = None) -> audiomentations.BaseWaveformTransform:
+def _collect_t_configs(cfg: dict, preset_file: Path | None, noise_dir: Path | None) -> list[dict]:
+ """Resolve a preset config to a flat list of transform dicts.
+
+ Atomic presets return cfg['transforms'] directly. Compound presets
+ (chain: [name, ...]) load each named preset's transforms and concatenate
+ them. Nesting chains inside chains is not supported.
+ """
+ if "chain" in cfg and "transforms" in cfg:
+ raise ValueError(
+ f"Preset '{cfg.get('name', '?')}' defines both 'chain' and 'transforms'. Use one or the other."
+ )
+ if "transforms" in cfg:
+ return list(cfg["transforms"])
+ if "chain" not in cfg:
+ raise ValueError(f"Preset '{cfg.get('name', '?')}' has neither 'transforms' nor 'chain' key.")
+ presets_dir = Path(__file__).parent / "presets"
+ combined: list[dict] = []
+ for chained_name in cfg["chain"]:
+ if preset_file is not None:
+ candidate = preset_file.parent / f"{chained_name}.yaml"
+ chained_path = candidate if candidate.exists() else presets_dir / f"{chained_name}.yaml"
+ else:
+ chained_path = presets_dir / f"{chained_name}.yaml"
+ if not chained_path.exists():
+ raise FileNotFoundError(f"Chained preset '{chained_name}' not found at {chained_path}.")
+ chained_cfg = yaml.safe_load(chained_path.read_text())
+ if "chain" in chained_cfg:
+ raise ValueError(f"Chained preset '{chained_name}' is itself a compound preset. Nesting not supported.")
+ combined.extend(chained_cfg.get("transforms", []))
+ return combined
+
+
+def _make_transform(t: dict, noise_dir: Path | None = None) -> BaseWaveformTransform:
cls_name = t["type"]
if not hasattr(audiomentations, cls_name):
raise ValueError(
@@ -94,7 +127,7 @@ def load_preset(
raise FileNotFoundError(f"Preset '{name}' not found. Run 'noisekit list-presets' to see available presets.")
cfg = yaml.safe_load(path.read_text())
- t_configs = cfg["transforms"]
+ t_configs = _collect_t_configs(cfg, preset_file, noise_dir)
full = _SRTrackingCompose([_make_transform(t, noise_dir) for t in t_configs])
diff --git a/pyproject.toml b/pyproject.toml
index 8169282..956f233 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "noisekit"
version = "0.1.0"
-description = "Generate noise-stratified speech datasets for ASR benchmark studies"
+description = "Generate noisy speech datasets for ASR benchmark studies"
license = {text = "MIT"}
requires-python = ">=3.10"
dependencies = [
@@ -18,6 +18,7 @@ dependencies = [
"soundfile>=0.12",
"numpy>=1.26",
"scipy>=1.13",
+ "pyroomacoustics>=0.7",
"pyyaml>=6.0",
"librosa>=0.10",
"lameenc>=1.4",
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index a9e7736..27ac89e 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -17,9 +17,32 @@ def test_list_builtin_presets() -> None:
from noisekit.transforms import list_builtin_presets
presets = list_builtin_presets()
- assert len(presets) == 4
+ assert len(presets) == 10
names = {p["name"] for p in presets}
assert "clean_reference" in names
- assert "telecommunication" in names
+ assert "telecom" in names
assert "bad_audio_encoding" in names
assert "noisy_environment" in names
+ assert "clipping_distortion" in names
+ assert "transmission_dropout" in names
+ assert "reverb_far_field" in names
+ assert "noisy_telecom" in names
+ assert "reverb_noisy" in names
+ assert "clipping_telecom" in names
+
+
+def test_load_compound_preset_scoring_split(tmp_path) -> None:
+ import numpy as np
+ import soundfile as sf
+
+ from noisekit.transforms import load_preset
+
+ # AddBackgroundNoise scans sounds_path at construction — write a minimal WAV.
+ sf.write(tmp_path / "noise.wav", np.zeros(16000, dtype=np.float32), 16000)
+
+ # noisy_telecom chains noisy_environment → telecom.
+ # The concatenated transform list ends with Resample(16000), so the NB 8 kHz
+ # scoring split should be detected automatically.
+ pt = load_preset("noisy_telecom", noise_dir=tmp_path)
+ assert pt.scoring is not None, "noisy_telecom should inherit telecom's NB scoring split"
+ assert pt.scoring_sr == 8000, "scoring_sr should be 8000 from telecom's Resample"
diff --git a/uv.lock b/uv.lock
index 8cb00cd..20dec93 100644
--- a/uv.lock
+++ b/uv.lock
@@ -573,6 +573,44 @@ nvtx = [
{ name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
]
+[[package]]
+name = "cython"
+version = "3.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/10/720e0fb84eab4c927c4dd6b61eb7993f7732dd83d29ba6d73083874eade9/cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e", size = 2960836, upload-time = "2026-01-04T14:14:51.103Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/3d/b26f29092c71c36e0462752885bdfb18c23c176af4de953fdae2772a8941/cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845", size = 3370134, upload-time = "2026-01-04T14:14:53.627Z" },
+ { url = "https://files.pythonhosted.org/packages/56/9e/539fb0d09e4f5251b5b14f8daf77e71fee021527f1013791038234618b6b/cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02", size = 3537552, upload-time = "2026-01-04T14:14:56.852Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c6/82d19a451c050d1be0f05b1a3302267463d391db548f013ee88b5348a8e9/cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86", size = 2766191, upload-time = "2026-01-04T14:14:58.709Z" },
+ { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" },
+ { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" },
+ { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" },
+ { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" },
+ { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f3/4275cd3ea0a4cf4606f9b92e7f8766478192010b95a7f516d1b7cf22cb10/cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235", size = 2756457, upload-time = "2026-01-04T14:15:14.67Z" },
+ { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" },
+ { url = "https://files.pythonhosted.org/packages/71/bb/8f28c39c342621047fea349a82fac712a5e2b37546d2f737bbde48d5143d/cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc", size = 3213148, upload-time = "2026-01-04T14:15:18.804Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d2/16fa02f129ed2b627e88d9d9ebd5ade3eeb66392ae5ba85b259d2d52b047/cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8", size = 3375764, upload-time = "2026-01-04T14:15:20.817Z" },
+ { url = "https://files.pythonhosted.org/packages/91/3f/deb8f023a5c10c0649eb81332a58c180fad27c7533bb4aae138b5bc34d92/cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e", size = 2754238, upload-time = "2026-01-04T14:15:23.001Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" },
+ { url = "https://files.pythonhosted.org/packages/89/ed/1021ffc80b9c4720b7ba869aea8422c82c84245ef117ebe47a556bdc00c3/cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e", size = 3256146, upload-time = "2026-01-04T14:15:26.741Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/51/ca221ec7e94b3c5dc4138dcdcbd41178df1729c1e88c5dfb25f9d30ba3da/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb", size = 3383458, upload-time = "2026-01-04T14:15:28.425Z" },
+ { url = "https://files.pythonhosted.org/packages/79/2e/1388fc0243240cd54994bb74f26aaaf3b2e22f89d3a2cf8da06d75d46ca2/cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e", size = 2791241, upload-time = "2026-01-04T14:15:30.448Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" },
+ { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" },
+ { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
+]
+
[[package]]
name = "datasets"
version = "4.8.5"
@@ -1419,6 +1457,7 @@ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pesq" },
+ { name = "pyroomacoustics" },
{ name = "pyyaml" },
{ name = "rich" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -1444,6 +1483,7 @@ requires-dist = [
{ name = "librosa", specifier = ">=0.10" },
{ name = "numpy", specifier = ">=1.26" },
{ name = "pesq", specifier = ">=0.0.4" },
+ { name = "pyroomacoustics", specifier = ">=0.7" },
{ name = "pyyaml", specifier = ">=6.0" },
{ name = "rich", specifier = ">=13.7" },
{ name = "scipy", specifier = ">=1.13" },
@@ -2265,6 +2305,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
+[[package]]
+name = "pyroomacoustics"
+version = "0.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cython" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f9/063c249181cdbbbd80eb621f871a4173e95df439e5d8fd22de4df44fed60/pyroomacoustics-0.10.1.tar.gz", hash = "sha256:26aa7d680e68d0b947220026b95b1c5a05eee08f38c53ff49a7804672c0a2886", size = 553656, upload-time = "2026-05-01T13:35:45.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/78/3d0508ee207786acaa4008c7819c7d1c365c05659e32b65f3bb2a8f87c7f/pyroomacoustics-0.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57b859bc68c938849c5acdb80e19e95a98a910b59f7b31ce78771d9231612819", size = 1083316, upload-time = "2026-05-01T13:35:13.236Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/13/a33caea3cf278f9a7bf10ceb37b12f42608164063901f7db61d9165aa4b7/pyroomacoustics-0.10.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69a4cfe1cae1f3f747fbd5a5bee7eb4e551e70b5e97efd74f0473c0f808a8b6c", size = 1271756, upload-time = "2026-05-01T13:35:15.015Z" },
+ { url = "https://files.pythonhosted.org/packages/55/ea/0877e70c2864808695e4282eaa98c1013ddf566392ad9bcef422336ce964/pyroomacoustics-0.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:e514f586f0f4702d28529c93889c6383f46588b0435be39c9d2cc6b4d3debcf4", size = 704480, upload-time = "2026-05-01T13:35:16.11Z" },
+ { url = "https://files.pythonhosted.org/packages/96/c8/f6c17e65d561904ced5147dd44b6fa8f2dc70bdc69c8608e84f91eb2872c/pyroomacoustics-0.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bf2aff3cbfe5533d63c747788aced4f0786846b46dee427d16d27f1982bbffff", size = 1085595, upload-time = "2026-05-01T13:35:18.06Z" },
+ { url = "https://files.pythonhosted.org/packages/32/97/0b923cbce476573eec7d428ab108526c3728c3e7628f865bf181a6b81f1e/pyroomacoustics-0.10.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea84429fbfb55d0dab65f5acdbbd34bfc42f94436cf16d672ec8dff7df889837", size = 1269208, upload-time = "2026-05-01T13:35:19.898Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/fe/f1ba24fa63490800e1700d90e62a61756e45844f896e8bb0801347645fcf/pyroomacoustics-0.10.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f729cf7e62a8663522a381629200e82ba9775bf5b75daed09c3155d85f164ee2", size = 1297491, upload-time = "2026-05-01T13:35:21.091Z" },
+ { url = "https://files.pythonhosted.org/packages/53/8c/aae7fa470d6dc87b3ee7d1c912102b0a7bc8d1a8fbee00c929d5596e7e77/pyroomacoustics-0.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:15cf8809d99449854d20f6ff343d0bb13bf9639db6efd0362a25866cc8cfae20", size = 704540, upload-time = "2026-05-01T13:35:22.517Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/72/750082f5c94505160ffa82c6e582afcba965837bf3a6d1b58911b9c99fc3/pyroomacoustics-0.10.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:523ba1baeaa594b0ea9c02df9ea279780dc054dd6a0f79f6823874f026da3dbf", size = 1091627, upload-time = "2026-05-01T13:35:24.115Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/af/67a3d41d5fdd4a811b186426d2deb70bed38c068ed006500dc2f696edf86/pyroomacoustics-0.10.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce0d8bf51b1994ef88db94ed984f493a2db2238b9f93a0aae967375d4b0e9d3d", size = 1249529, upload-time = "2026-05-01T13:35:25.542Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/55/8af80433f3318549573c60c3527a8adf7080bf45bc2f57280ad7325d16a3/pyroomacoustics-0.10.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1b1077cfcafed9775d1b826dbbaf25fb4090aa95d21e9bc6dac795f88e8875c", size = 1286561, upload-time = "2026-05-01T13:35:27.086Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/34/19706fee63d5171d8edfc0b6f85cd17b651cd89822abc80cda8b47dc50ba/pyroomacoustics-0.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:421fa320b6ad31465dc59e137a7b0e1033687cb821febcc8cb4d76f67a4c7b57", size = 706414, upload-time = "2026-05-01T13:35:28.389Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/b1/b6c02e0f3374ddc6776e6a58fe19e2bdeb5cae0192893b9030810f3b47a7/pyroomacoustics-0.10.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d58f9d819554d552cb1d228ef6cffaff5a308aabbf9db2a72faea3c96e7b9445", size = 1090496, upload-time = "2026-05-01T13:35:29.976Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/ac/fdbf223e09843c90a825665e1e156f3f095d2b742bd1f186028aeee68bab/pyroomacoustics-0.10.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af4d4f672982d616a46df4de0f7ec734703fb904d9c984d9f07ddb1e8c222818", size = 1244884, upload-time = "2026-05-01T13:35:31.325Z" },
+ { url = "https://files.pythonhosted.org/packages/84/20/90361ee35389cff48165fb08072af7b75551eac3387f839804b3016d20e6/pyroomacoustics-0.10.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c73af58bca4109bcf4bb567f0a3db9d5d94684a16705be4b674223f4f048f2ad", size = 1282528, upload-time = "2026-05-01T13:35:32.662Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/df/fd267974029c8a1426b35ed00f30e68afbb7810acde50cd31d7c40ba7022/pyroomacoustics-0.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:abcc05fe21315b6f0244ba77e96b4d0be63efdf99288614b50c33871a2d4ed2d", size = 706197, upload-time = "2026-05-01T13:35:33.955Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/f1/0771da3406c62addc43916251952bc2448c3550bdc219cfacf997b808dce/pyroomacoustics-0.10.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bbefe48fcb2117fb0731a5be3f82d1d029e603d5a56bdc7fbfa2c8ccc4f6ace5", size = 1092272, upload-time = "2026-05-01T13:35:35.148Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/24/1881600551e7d01aced3b1b048b2790667210e07f9a962a242f88bdea2ef/pyroomacoustics-0.10.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8f13eeb593e4394b724e71b7302dcce77f3727bbb204b966a54c999cee32078", size = 1244143, upload-time = "2026-05-01T13:35:36.942Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/1b/fceb2596b9d511eef464bab56c950f7280af2e83e8254a922e1ba33d2387/pyroomacoustics-0.10.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef6d038374cc205c567d47203eda1fd745d7e34c06f3fb89a13289f065807ead", size = 1277224, upload-time = "2026-05-01T13:35:38.609Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e5/14c6055896cd94e98cdd2fb74559a77cc2567fe2b2f89c42d3be407cb194/pyroomacoustics-0.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:ce6743905c967c9362f546883f57f5e4e0a508c44222c0efb947074b9d0d39fe", size = 718676, upload-time = "2026-05-01T13:35:40.146Z" },
+]
+
[[package]]
name = "pytest"
version = "9.0.3"