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"