audiosronnx 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 TigreGóticoLda
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include audiosronnx/data *
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: audiosronnx
3
+ Version: 0.0.1
4
+ Summary: Pure-ONNX multi-engine audio super-resolution / bandwidth extension (upscale speech to 48 kHz)
5
+ Author-email: JarbasAi <jarbasai@mailfence.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/TigreGotico/audiosronnx
8
+ Project-URL: Repository, https://github.com/TigreGotico/audiosronnx
9
+ Keywords: audio super resolution,bandwidth extension,speech enhancement,onnx,upsampling,lavasr,novasr,48khz
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
18
+ Classifier: Intended Audience :: Developers
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy
23
+ Requires-Dist: onnxruntime>=1.17
24
+ Requires-Dist: huggingface_hub
25
+ Requires-Dist: scipy
26
+ Requires-Dist: soundfile
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest; extra == "test"
29
+ Requires-Dist: pytest-cov; extra == "test"
30
+ Provides-Extra: convert
31
+ Requires-Dist: torch; extra == "convert"
32
+ Requires-Dist: onnx; extra == "convert"
33
+ Requires-Dist: huggingface_hub; extra == "convert"
34
+ Requires-Dist: vocos; extra == "convert"
35
+ Requires-Dist: einops; extra == "convert"
36
+ Dynamic: license-file
37
+
38
+ # audiosronnx
39
+
40
+ Pure-ONNX, multi-engine **audio super-resolution / bandwidth extension**. Upscale
41
+ low-sample-rate or low-bandwidth speech — 8 kHz telephony, muffled recordings,
42
+ low-fidelity TTS — to a clean **48 kHz** signal. Inference runs entirely on
43
+ onnxruntime; there is **no torch at runtime**.
44
+
45
+ audiosronnx is the super-resolution sibling of the TigreGotico pure-ONNX speech
46
+ libraries ([`vadonnx`](https://github.com/TigreGotico/vadonnx),
47
+ [`voiceclonnx`](https://github.com/TigreGotico/voiceclonnx),
48
+ [`speakeronnx`](https://github.com/TigreGotico/speakeronnx)) and mirrors their
49
+ conventions: a single `load_sr(engine=...)` entry point, an engine registry, a
50
+ HuggingFace-backed model resolver with an XDG cache, and a small CLI.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install audiosronnx
56
+ ```
57
+
58
+ Runtime dependencies: `numpy`, `onnxruntime`, `scipy`, `soundfile`, `huggingface_hub`.
59
+ ONNX models are downloaded on first use from the Hugging Face Hub and cached under
60
+ `~/.local/share/audiosronnx`.
61
+
62
+ ## Engines
63
+
64
+ | Engine | Input SR | Output SR | Size | CPU speed | License | Status |
65
+ |--------|----------|-----------|------|-----------|---------|--------|
66
+ | **lavasr** (default) | 8–48 kHz | 48 kHz | ~52 MB | ~50× realtime | Apache-2.0 | shipped |
67
+ | **novasr** | 16 kHz | 48 kHz | ~52 KB | ~1000× realtime | Apache-2.0 | shipped |
68
+
69
+ - **lavasr** — a Vocos-based bandwidth-extension model with a Linkwitz-Riley spectral
70
+ merge that preserves the original low band, plus an optional UL-UNAS denoiser. It
71
+ accepts any input rate from 8 to 48 kHz and is the best-quality engine. All spectral
72
+ DSP (STFT, ISTFT, mel filterbank, resampling, merge) runs in numpy/scipy, so no
73
+ `torch.stft` ever enters an ONNX graph.
74
+ - **novasr** — a tiny conv1d / BigVGAN-snake generator that upsamples 16 kHz to 48 kHz
75
+ in a single time-domain pass. Extremely fast and memory-light, at lower fidelity than
76
+ lavasr; useful for on-device enhancement and quick dataset restoration.
77
+
78
+ Both engines are derived from the LavaSR/NovaSR projects by Yatharth Sharma
79
+ ([LavaSR](https://github.com/ysharma3501/LavaSR),
80
+ [NovaSR](https://github.com/ysharma3501/NovaSR)), Apache-2.0.
81
+
82
+ ## Quickstart
83
+
84
+ ```python
85
+ from audiosronnx import load_sr
86
+
87
+ sr = load_sr("lavasr") # default engine
88
+
89
+ # low-level primitive: numpy array or path in, (float32 mono, 48000) out
90
+ out, rate = sr.upscale("telephone_8k.wav") # -> (np.ndarray float32, 48000)
91
+
92
+ # array input (you supply the sample rate)
93
+ import soundfile as sf
94
+ audio, in_sr = sf.read("input.wav")
95
+ out, rate = sr.upscale(audio, in_sr)
96
+
97
+ # file / directory helpers
98
+ sr.upscale_file("in.wav", "out_48k.wav")
99
+ sr.upscale_dir("clips_in/", "clips_out_48k/")
100
+
101
+ # the fast tiny engine instead
102
+ fast = load_sr("novasr")
103
+ fast.upscale_file("in_16k.wav", "out_48k.wav")
104
+ ```
105
+
106
+ LavaSR options:
107
+
108
+ ```python
109
+ sr = load_sr("lavasr", denoise=True, cutoff_hz=6000)
110
+ ```
111
+
112
+ ## CLI
113
+
114
+ ```bash
115
+ audiosronnx list # list engines
116
+ audiosronnx probe input.wav # print format / duration
117
+ audiosronnx upscale in.wav out.wav # default engine (lavasr)
118
+ audiosronnx upscale in.wav out.wav --engine novasr
119
+ audiosronnx upscale in.wav out.wav --denoise # lavasr denoiser stage
120
+ audiosronnx upscale-dir clips_in/ clips_out/
121
+ ```
122
+
123
+ ## API
124
+
125
+ - `load_sr(engine="lavasr", *, providers=None, cache_dir=None, revision=None, **kwargs) -> SRModel`
126
+ - `SRModel.upscale(audio, sample_rate=None) -> (np.ndarray float32 mono, 48000)` —
127
+ `audio` is a path, raw int16 PCM bytes, or a numpy array. The low-level primitive.
128
+ - `SRModel.upscale_file(in_path, out_path) -> str`
129
+ - `SRModel.upscale_dir(in_dir, out_dir) -> list[str]`
130
+ - `available_models()`, `get_engine(alias)`, `register_engine(entry)`, `ENGINE_REGISTRY`
131
+
132
+ ### Custom / third-party engines
133
+
134
+ Subclass `SRModel`, implement `_upscale_array(audio, sample_rate) -> np.ndarray` (a
135
+ 48 kHz mono float32 array), and register an `EngineEntry`:
136
+
137
+ ```python
138
+ from audiosronnx import SRModel, EngineEntry, register_engine
139
+
140
+ class MySR(SRModel):
141
+ output_sample_rate = 48000
142
+ def _upscale_array(self, audio, sample_rate):
143
+ ... # run your ONNX session, return a 48 kHz float32 array
144
+
145
+ register_engine(EngineEntry(alias="mysr", adapter_class=MySR, license="MIT"))
146
+ ```
147
+
148
+ ## Model hosting
149
+
150
+ Exported ONNX weights are hosted on the Hugging Face Hub and pinned by revision:
151
+
152
+ - `TigreGotico/audiosronnx-lavasr` — `backbone.onnx`, `spec_head.onnx`, `denoiser_core.onnx`
153
+ - `TigreGotico/audiosronnx-novasr` — `novasr.onnx`
154
+
155
+ The export scripts under `conversion/` reproduce these from the upstream PyTorch
156
+ checkpoints and validate each ONNX graph against its PyTorch submodule (max absolute
157
+ error).
158
+
159
+ ## Evaluated but not shipped
160
+
161
+ The audio super-resolution landscape was surveyed for other models to export. These
162
+ were evaluated and are **not** shipped, for the reasons given:
163
+
164
+ | Model | License | Reason not shipped |
165
+ |-------|---------|--------------------|
166
+ | **AP-BWE** | MIT | Permissive and ONNX-friendly (convolutional amplitude/phase predictor; STFT stays outside the graph). A strong candidate not yet packaged here. |
167
+ | **HiFi-GAN-BWE** | MIT | Permissive, time-domain, ships its own export script. A strong candidate not yet packaged here. |
168
+ | **FLowHigh** | MIT | Single-step flow matching, but depends on an external BigVGAN vocoder plus a mel/STFT front-end — a multi-component export rather than one clean graph. |
169
+ | **AudioSR** | MIT | ~6 GB latent-diffusion model (VAE + LDM + vocoder, iterative sampler, ~0.6× realtime on GPU). Not CPU-runnable at usable latency; impractical to export. |
170
+ | **NU-Wave2** | none | Diffusion (iterative sampler) and the repository ships no license file. |
171
+ | **mdctGAN** | unclear (NOASSERTION) | Unclear license, and its MDCT front-end relies on `torch.fft`, which exports to ONNX unreliably. |
172
+ | **VoiceFixer / NVSR** | MIT | Speech *restoration* rather than pure bandwidth extension; two-stage mel-predictor + neural vocoder, heavier and less focused than the shipped engines. |
173
+
174
+ An engine is only shipped when it exports cleanly to ONNX, runs on CPU via
175
+ onnxruntime, carries a permissive license, and produces verified 48 kHz output.
176
+
177
+ ## License
178
+
179
+ Apache-2.0. The shipped model weights are Apache-2.0 (LavaSR, NovaSR).
@@ -0,0 +1,142 @@
1
+ # audiosronnx
2
+
3
+ Pure-ONNX, multi-engine **audio super-resolution / bandwidth extension**. Upscale
4
+ low-sample-rate or low-bandwidth speech — 8 kHz telephony, muffled recordings,
5
+ low-fidelity TTS — to a clean **48 kHz** signal. Inference runs entirely on
6
+ onnxruntime; there is **no torch at runtime**.
7
+
8
+ audiosronnx is the super-resolution sibling of the TigreGotico pure-ONNX speech
9
+ libraries ([`vadonnx`](https://github.com/TigreGotico/vadonnx),
10
+ [`voiceclonnx`](https://github.com/TigreGotico/voiceclonnx),
11
+ [`speakeronnx`](https://github.com/TigreGotico/speakeronnx)) and mirrors their
12
+ conventions: a single `load_sr(engine=...)` entry point, an engine registry, a
13
+ HuggingFace-backed model resolver with an XDG cache, and a small CLI.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install audiosronnx
19
+ ```
20
+
21
+ Runtime dependencies: `numpy`, `onnxruntime`, `scipy`, `soundfile`, `huggingface_hub`.
22
+ ONNX models are downloaded on first use from the Hugging Face Hub and cached under
23
+ `~/.local/share/audiosronnx`.
24
+
25
+ ## Engines
26
+
27
+ | Engine | Input SR | Output SR | Size | CPU speed | License | Status |
28
+ |--------|----------|-----------|------|-----------|---------|--------|
29
+ | **lavasr** (default) | 8–48 kHz | 48 kHz | ~52 MB | ~50× realtime | Apache-2.0 | shipped |
30
+ | **novasr** | 16 kHz | 48 kHz | ~52 KB | ~1000× realtime | Apache-2.0 | shipped |
31
+
32
+ - **lavasr** — a Vocos-based bandwidth-extension model with a Linkwitz-Riley spectral
33
+ merge that preserves the original low band, plus an optional UL-UNAS denoiser. It
34
+ accepts any input rate from 8 to 48 kHz and is the best-quality engine. All spectral
35
+ DSP (STFT, ISTFT, mel filterbank, resampling, merge) runs in numpy/scipy, so no
36
+ `torch.stft` ever enters an ONNX graph.
37
+ - **novasr** — a tiny conv1d / BigVGAN-snake generator that upsamples 16 kHz to 48 kHz
38
+ in a single time-domain pass. Extremely fast and memory-light, at lower fidelity than
39
+ lavasr; useful for on-device enhancement and quick dataset restoration.
40
+
41
+ Both engines are derived from the LavaSR/NovaSR projects by Yatharth Sharma
42
+ ([LavaSR](https://github.com/ysharma3501/LavaSR),
43
+ [NovaSR](https://github.com/ysharma3501/NovaSR)), Apache-2.0.
44
+
45
+ ## Quickstart
46
+
47
+ ```python
48
+ from audiosronnx import load_sr
49
+
50
+ sr = load_sr("lavasr") # default engine
51
+
52
+ # low-level primitive: numpy array or path in, (float32 mono, 48000) out
53
+ out, rate = sr.upscale("telephone_8k.wav") # -> (np.ndarray float32, 48000)
54
+
55
+ # array input (you supply the sample rate)
56
+ import soundfile as sf
57
+ audio, in_sr = sf.read("input.wav")
58
+ out, rate = sr.upscale(audio, in_sr)
59
+
60
+ # file / directory helpers
61
+ sr.upscale_file("in.wav", "out_48k.wav")
62
+ sr.upscale_dir("clips_in/", "clips_out_48k/")
63
+
64
+ # the fast tiny engine instead
65
+ fast = load_sr("novasr")
66
+ fast.upscale_file("in_16k.wav", "out_48k.wav")
67
+ ```
68
+
69
+ LavaSR options:
70
+
71
+ ```python
72
+ sr = load_sr("lavasr", denoise=True, cutoff_hz=6000)
73
+ ```
74
+
75
+ ## CLI
76
+
77
+ ```bash
78
+ audiosronnx list # list engines
79
+ audiosronnx probe input.wav # print format / duration
80
+ audiosronnx upscale in.wav out.wav # default engine (lavasr)
81
+ audiosronnx upscale in.wav out.wav --engine novasr
82
+ audiosronnx upscale in.wav out.wav --denoise # lavasr denoiser stage
83
+ audiosronnx upscale-dir clips_in/ clips_out/
84
+ ```
85
+
86
+ ## API
87
+
88
+ - `load_sr(engine="lavasr", *, providers=None, cache_dir=None, revision=None, **kwargs) -> SRModel`
89
+ - `SRModel.upscale(audio, sample_rate=None) -> (np.ndarray float32 mono, 48000)` —
90
+ `audio` is a path, raw int16 PCM bytes, or a numpy array. The low-level primitive.
91
+ - `SRModel.upscale_file(in_path, out_path) -> str`
92
+ - `SRModel.upscale_dir(in_dir, out_dir) -> list[str]`
93
+ - `available_models()`, `get_engine(alias)`, `register_engine(entry)`, `ENGINE_REGISTRY`
94
+
95
+ ### Custom / third-party engines
96
+
97
+ Subclass `SRModel`, implement `_upscale_array(audio, sample_rate) -> np.ndarray` (a
98
+ 48 kHz mono float32 array), and register an `EngineEntry`:
99
+
100
+ ```python
101
+ from audiosronnx import SRModel, EngineEntry, register_engine
102
+
103
+ class MySR(SRModel):
104
+ output_sample_rate = 48000
105
+ def _upscale_array(self, audio, sample_rate):
106
+ ... # run your ONNX session, return a 48 kHz float32 array
107
+
108
+ register_engine(EngineEntry(alias="mysr", adapter_class=MySR, license="MIT"))
109
+ ```
110
+
111
+ ## Model hosting
112
+
113
+ Exported ONNX weights are hosted on the Hugging Face Hub and pinned by revision:
114
+
115
+ - `TigreGotico/audiosronnx-lavasr` — `backbone.onnx`, `spec_head.onnx`, `denoiser_core.onnx`
116
+ - `TigreGotico/audiosronnx-novasr` — `novasr.onnx`
117
+
118
+ The export scripts under `conversion/` reproduce these from the upstream PyTorch
119
+ checkpoints and validate each ONNX graph against its PyTorch submodule (max absolute
120
+ error).
121
+
122
+ ## Evaluated but not shipped
123
+
124
+ The audio super-resolution landscape was surveyed for other models to export. These
125
+ were evaluated and are **not** shipped, for the reasons given:
126
+
127
+ | Model | License | Reason not shipped |
128
+ |-------|---------|--------------------|
129
+ | **AP-BWE** | MIT | Permissive and ONNX-friendly (convolutional amplitude/phase predictor; STFT stays outside the graph). A strong candidate not yet packaged here. |
130
+ | **HiFi-GAN-BWE** | MIT | Permissive, time-domain, ships its own export script. A strong candidate not yet packaged here. |
131
+ | **FLowHigh** | MIT | Single-step flow matching, but depends on an external BigVGAN vocoder plus a mel/STFT front-end — a multi-component export rather than one clean graph. |
132
+ | **AudioSR** | MIT | ~6 GB latent-diffusion model (VAE + LDM + vocoder, iterative sampler, ~0.6× realtime on GPU). Not CPU-runnable at usable latency; impractical to export. |
133
+ | **NU-Wave2** | none | Diffusion (iterative sampler) and the repository ships no license file. |
134
+ | **mdctGAN** | unclear (NOASSERTION) | Unclear license, and its MDCT front-end relies on `torch.fft`, which exports to ONNX unreliably. |
135
+ | **VoiceFixer / NVSR** | MIT | Speech *restoration* rather than pure bandwidth extension; two-stage mel-predictor + neural vocoder, heavier and less focused than the shipped engines. |
136
+
137
+ An engine is only shipped when it exports cleanly to ONNX, runs on CPU via
138
+ onnxruntime, carries a permissive license, and produces verified 48 kHz output.
139
+
140
+ ## License
141
+
142
+ Apache-2.0. The shipped model weights are Apache-2.0 (LavaSR, NovaSR).
@@ -0,0 +1,39 @@
1
+ """audiosronnx — pure-ONNX multi-engine audio super-resolution / bandwidth extension.
2
+
3
+ Upscale low-sample-rate or low-bandwidth speech (e.g. 8 kHz telephony) to a clean
4
+ 48 kHz signal. Inference runs entirely on onnxruntime — no torch at runtime.
5
+
6
+ Quick start::
7
+
8
+ from audiosronnx import load_sr
9
+
10
+ sr = load_sr("lavasr") # default engine
11
+ out, rate = sr.upscale("telephone_8k.wav") # -> (float32 mono array, 48000)
12
+ sr.upscale_file("in.wav", "out_48k.wav")
13
+
14
+ Engines are downloaded on first use from Hugging Face Hub and cached under an XDG
15
+ data directory. ``load_sr("novasr")`` selects the tiny fast engine instead.
16
+ """
17
+ from .api import load_sr
18
+ from .base import (
19
+ ENGINE_REGISTRY,
20
+ OUTPUT_SAMPLE_RATE,
21
+ EngineEntry,
22
+ SRModel,
23
+ available_models,
24
+ get_engine,
25
+ register_engine,
26
+ )
27
+ from .version import __version__
28
+
29
+ __all__ = [
30
+ "load_sr",
31
+ "available_models",
32
+ "SRModel",
33
+ "EngineEntry",
34
+ "ENGINE_REGISTRY",
35
+ "register_engine",
36
+ "get_engine",
37
+ "OUTPUT_SAMPLE_RATE",
38
+ "__version__",
39
+ ]
@@ -0,0 +1,45 @@
1
+ """Public entry point: :func:`load_sr`."""
2
+ from __future__ import annotations
3
+
4
+ from typing import List, Optional
5
+
6
+ from .base import SRModel, available_models, get_engine
7
+
8
+ # Importing the engines package registers every built-in adapter.
9
+ from . import engines # noqa: F401
10
+
11
+ __all__ = ["load_sr", "available_models"]
12
+
13
+ DEFAULT_ENGINE = "lavasr"
14
+
15
+
16
+ def load_sr(
17
+ engine: str = DEFAULT_ENGINE,
18
+ *,
19
+ providers: Optional[List[str]] = None,
20
+ cache_dir: Optional[str] = None,
21
+ revision: Optional[str] = None,
22
+ **engine_kwargs,
23
+ ) -> SRModel:
24
+ """Load an audio super-resolution engine behind the unified :class:`SRModel` API.
25
+
26
+ Args:
27
+ engine: registry alias (``"lavasr"`` (default) or ``"novasr"``).
28
+ providers: onnxruntime execution providers (default CPU).
29
+ cache_dir: override the model download cache directory.
30
+ revision: pin a HuggingFace revision for the model weights.
31
+ **engine_kwargs: forwarded to the engine adapter (e.g. ``denoise=True``,
32
+ ``cutoff_hz=6000`` for LavaSR).
33
+
34
+ Returns:
35
+ A ready-to-use :class:`~audiosronnx.base.SRModel`.
36
+
37
+ Example:
38
+ >>> from audiosronnx import load_sr
39
+ >>> sr = load_sr("lavasr")
40
+ >>> out, rate = sr.upscale("telephone_8k.wav") # -> (float32 array, 48000)
41
+ """
42
+ entry = get_engine(engine)
43
+ return entry.adapter_class(
44
+ providers=providers, cache_dir=cache_dir, revision=revision, **engine_kwargs
45
+ )
@@ -0,0 +1,128 @@
1
+ """Audio I/O and resampling helpers (numpy core, soundfile for file I/O).
2
+
3
+ Everything in :mod:`audiosronnx` works internally with mono ``float32`` PCM in the
4
+ range ``[-1, 1]``. These helpers convert the shapes users pass in (a WAV path or a
5
+ numpy array of various dtypes) into that canonical form, and resample between rates.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import wave
11
+ from typing import Tuple, Union
12
+
13
+ import numpy as np
14
+
15
+ AudioLike = Union[str, bytes, bytearray, memoryview, np.ndarray]
16
+
17
+
18
+ def to_float32_mono(audio: Union[bytes, bytearray, memoryview, np.ndarray]) -> np.ndarray:
19
+ """Convert audio of various dtypes/containers to mono float32 in [-1, 1].
20
+
21
+ Accepts raw little-endian ``int16`` PCM bytes, or a numpy array of dtype
22
+ ``int16`` / ``int32`` / ``uint8`` / ``float32`` / ``float64``. Multi-channel
23
+ arrays (shape ``(n, channels)``) are downmixed by averaging channels.
24
+ """
25
+ if isinstance(audio, (bytes, bytearray, memoryview)):
26
+ return np.frombuffer(bytes(audio), dtype="<i2").astype(np.float32) / 32768.0
27
+
28
+ if not isinstance(audio, np.ndarray):
29
+ raise TypeError(f"audio must be bytes or np.ndarray, got {type(audio).__name__}")
30
+
31
+ arr = audio
32
+ if arr.ndim == 2:
33
+ arr = arr.mean(axis=1)
34
+ elif arr.ndim > 2:
35
+ raise ValueError(f"audio array must be 1-D or 2-D, got {arr.ndim}-D")
36
+
37
+ if arr.dtype == np.float32:
38
+ out = arr
39
+ elif arr.dtype == np.float64:
40
+ out = arr.astype(np.float32)
41
+ elif arr.dtype == np.int16:
42
+ out = arr.astype(np.float32) / 32768.0
43
+ elif arr.dtype == np.int32:
44
+ out = arr.astype(np.float32) / 2147483648.0
45
+ elif arr.dtype == np.uint8:
46
+ out = (arr.astype(np.float32) - 128.0) / 128.0
47
+ else:
48
+ raise TypeError(f"unsupported audio dtype: {arr.dtype}")
49
+
50
+ return np.ascontiguousarray(out, dtype=np.float32)
51
+
52
+
53
+ def resample(x: np.ndarray, src_sr: int, dst_sr: int) -> np.ndarray:
54
+ """Resample mono float32 audio from ``src_sr`` to ``dst_sr``.
55
+
56
+ Uses ``scipy.signal.resample_poly`` when SciPy is installed (the default for a
57
+ full install), otherwise falls back to linear interpolation via
58
+ :func:`numpy.interp`.
59
+ """
60
+ if src_sr == dst_sr or x.size == 0:
61
+ return x.astype(np.float32, copy=False)
62
+ try:
63
+ from scipy.signal import resample_poly # type: ignore
64
+
65
+ g = math.gcd(int(src_sr), int(dst_sr))
66
+ up, down = int(dst_sr) // g, int(src_sr) // g
67
+ return resample_poly(x, up, down).astype(np.float32)
68
+ except Exception:
69
+ n_out = int(round(x.shape[0] * dst_sr / src_sr))
70
+ if n_out <= 0:
71
+ return np.zeros(0, dtype=np.float32)
72
+ src_idx = np.linspace(0.0, x.shape[0] - 1, num=n_out, dtype=np.float64)
73
+ return np.interp(src_idx, np.arange(x.shape[0]), x).astype(np.float32)
74
+
75
+
76
+ def read_wav(path: str) -> Tuple[np.ndarray, int]:
77
+ """Read audio into mono float32. Returns ``(audio, sample_rate)``.
78
+
79
+ Uses :mod:`soundfile` when available (handles WAV/FLAC/OGG and any subtype),
80
+ otherwise falls back to the standard-library :mod:`wave` module for PCM WAV.
81
+ """
82
+ try:
83
+ import soundfile as sf # type: ignore
84
+
85
+ audio, sr = sf.read(str(path), dtype="float32", always_2d=False)
86
+ if audio.ndim > 1:
87
+ audio = audio.mean(axis=1)
88
+ return np.ascontiguousarray(audio, dtype=np.float32), int(sr)
89
+ except ImportError:
90
+ pass
91
+
92
+ with wave.open(str(path), "rb") as wf:
93
+ sr = wf.getframerate()
94
+ n_channels = wf.getnchannels()
95
+ sampwidth = wf.getsampwidth()
96
+ raw = wf.readframes(wf.getnframes())
97
+
98
+ if sampwidth == 2:
99
+ arr = np.frombuffer(raw, dtype="<i2").astype(np.float32) / 32768.0
100
+ elif sampwidth == 4:
101
+ arr = np.frombuffer(raw, dtype="<i4").astype(np.float32) / 2147483648.0
102
+ elif sampwidth == 1:
103
+ arr = (np.frombuffer(raw, dtype=np.uint8).astype(np.float32) - 128.0) / 128.0
104
+ else:
105
+ raise ValueError(f"unsupported WAV sample width: {sampwidth} bytes")
106
+
107
+ if n_channels > 1:
108
+ arr = arr.reshape(-1, n_channels).mean(axis=1)
109
+ return np.ascontiguousarray(arr, dtype=np.float32), sr
110
+
111
+
112
+ def write_wav(path: str, audio: np.ndarray, sample_rate: int) -> None:
113
+ """Write mono float32 ``audio`` as 16-bit PCM WAV to ``path``."""
114
+ audio = np.clip(np.asarray(audio, dtype=np.float32), -1.0, 1.0)
115
+ try:
116
+ import soundfile as sf # type: ignore
117
+
118
+ sf.write(str(path), audio, int(sample_rate), subtype="PCM_16")
119
+ return
120
+ except ImportError:
121
+ pass
122
+
123
+ pcm = (audio * 32767.0).astype("<i2")
124
+ with wave.open(str(path), "wb") as wf:
125
+ wf.setnchannels(1)
126
+ wf.setsampwidth(2)
127
+ wf.setframerate(int(sample_rate))
128
+ wf.writeframes(pcm.tobytes())