Metadata-Version: 2.4
Name: si-quick-resampler
Version: 0.1.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Rust
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
Requires-Dist: numpy>=1.23.5,<3
Requires-Dist: scipy>=1.10 ; extra == 'benchmark'
Requires-Dist: soxr>=0.5 ; extra == 'benchmark'
Requires-Dist: resampy>=0.4 ; extra == 'benchmark'
Requires-Dist: maturin>=1.9,<2 ; extra == 'dev'
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: scipy>=1.10 ; extra == 'dev'
Requires-Dist: ruff>=0.9 ; extra == 'dev'
Requires-Dist: twine>=6 ; extra == 'dev'
Requires-Dist: pytest>=8 ; extra == 'test'
Requires-Dist: scipy>=1.10 ; extra == 'test'
Provides-Extra: benchmark
Provides-Extra: dev
Provides-Extra: test
License-File: LICENSE
License-File: NOTICE
Summary: Fast, portable Rust audio resampling for NumPy, based on Google's QResampler
Keywords: audio,resampling,numpy,rust,dsp
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/samliu/si-quick-resampler#readme
Project-URL: Homepage, https://github.com/samliu/si-quick-resampler
Project-URL: Issues, https://github.com/samliu/si-quick-resampler/issues
Project-URL: Repository, https://github.com/samliu/si-quick-resampler

# si-quick-resampler

Fast NumPy audio resampling with a portable Rust implementation of
[Google's QResampler](https://github.com/google/multichannel-audio-tools/blob/e7d26a79405983039bc84760a202b80d6ab16ca8/audio/dsp/resampler_q.h).
Supports whole arrays, batches, multichannel audio, and streaming.

```bash
pip install si-quick-resampler
```

```python
import numpy as np
from si_quick_resampler import resample

audio = np.zeros(48000, dtype=np.float32)
converted = resample(audio, 48000, 16000)
assert converted.shape == (16000,)
```

The default `quick` preset uses the upstream QResampler filter parameters.
It prioritizes throughput and low latency. Use `quality="high"` for a sharper
filter, or customize the filter explicitly. See
[performance and quality measurements](https://github.com/samliu/si-quick-resampler/blob/main/docs/benchmarks.md)
before choosing a preset. On an M2 Max, quick is 1.50–2.93x faster than SciPy
with identical filtering across the 24 measured cases. Soxr is faster in some
cases and offers stronger near-Nyquist rejection.

## Platforms

The release workflow builds wheels for:

| Operating system | CPUs | Wheel baseline |
| --- | --- | --- |
| macOS | Apple Silicon ARM64 | macOS 11+ |
| macOS | Intel x86-64 | macOS 10.12+ |
| Linux with glibc | Intel/AMD x86-64, ARM64 | manylinux2014, glibc 2.17+ |
| Windows | Intel/AMD x86-64 | 64-bit Windows |

The wheels use CPython's stable ABI for Python 3.10–3.14. The release workflow
tests the minimum NumPy 1.23.5 on Python 3.10 and NumPy 2.x on Python 3.14 on every platform,
plus Python 3.11–3.13 on Linux x86-64.

ARM64 uses baseline NEON instructions; x86-64 uses baseline SSE2. No AVX,
AVX2, or `target-cpu=native` requirement. Users installing a supported wheel do
not need Rust, a C compiler, or a separate audio library. Other platforms may
build from source with Rust 1.80+; they are not covered by the wheel test matrix.
PyPy, free-threaded CPython, Alpine/musl, Windows ARM64, and 32-bit platforms
are not supported by this release's wheels.

## Arrays and reusable filters

```python
from si_quick_resampler import QResampler, resample

# soundfile-style audio: (frames, channels)
stereo = np.zeros((48000, 2), dtype=np.float32)
converted = resample(stereo, 48000, 16000, axis=0)

# Batch audio: (batch, channels, frames). The default axis is the last.
batch = np.zeros((8, 2, 48000), dtype=np.float32)
converted_batch = resample(batch, 48000, 16000)

# Reuse one filter bank across recordings and threads.
converter = QResampler(44100, 48000, quality="high")
converted = converter.resample(audio)
print(converter.ratio)                  # (147, 160): input / output
print(converter.effective_output_rate)  # 48000.0
print(converter.output_frames(44100))   # 48000
```

`resample(audio, input_rate, output_rate, *, axis=-1, quality="quick")`
caches up to 16 filter banks. `QResampler` gives explicit ownership of a reusable
filter bank, custom settings, and independent streams.

Input must be a NumPy-compatible array of native-endian `float32` or `float64`.
The result preserves dtype and every non-time dimension. Strided and reversed
arrays are accepted. Integer PCM, half precision, and complex audio are rejected;
convert integer PCM explicitly, for example `pcm.astype(np.float32) / 32768.0`
for signed 16-bit PCM. Samples are not clipped, normalized, or dithered.
NaNs and infinities are not sanitized and can propagate through the filter.

The output begins at time zero and contains
`ceil(input_frames * ratio[1] / ratio[0])` frames, with zero padding at the
boundaries. Equal input/output rates return an independent copy without filtering.
Empty inputs are supported.

## Streaming

```python
converter = QResampler(48000, 16000)
stream = converter.stream(channels=2, dtype="float32")

chunks = [stereo[:1000], stereo[1000:17000], stereo[17000:]]
outputs = [stream.process(chunk) for chunk in chunks]
outputs.append(stream.flush())
converted = np.concatenate(outputs, axis=0)

# Exactly the same samples as whole-array processing, regardless of chunk sizes.
np.testing.assert_array_equal(converted, converter.resample(stereo, axis=0))
stream.reset()
```

Mono streams accept one-dimensional chunks. Multichannel streams accept
`(frames, channels)` arrays. Dtype and channel count must remain constant.

`process()` waits for the filter's future input samples, so output chunk sizes
vary and may be zero. `converter.radius / converter.input_rate` is the filter
lookahead in seconds. Chunk delivery may add further latency. `flush()` pads
the end to the exact output duration and closes the stream; subsequent flushes
return empty arrays. Call `reset()` before processing a new stream.

Retained input history is bounded by the filter length, not the recording length.
Calls allocate output and working buffers; this API is **not allocation-free**
and does not promise hard real-time scheduling. For fixed-size playback blocks,
use an application-level output FIFO.

One converter can be shared by multiple threads. Each stream has its own state;
calls on the same stream are serialized. Rust owns a snapshot of the input before
releasing the GIL for computation. As with other NumPy operations, callers should
not mutate or resize input arrays concurrently with a call.

## Filter controls and rate approximation

| Preset | Radius factor | Cutoff proportion | Kaiser beta |
| --- | ---: | ---: | ---: |
| `quick` | 5 | 0.90 | 6 |
| `high` | 17 | 0.95 | 8.6 |

```python
converter = QResampler(
    48000,
    16000,
    filter_radius=10.0,
    cutoff=0.92,
    beta=7.0,
    max_denominator=1000,
)
```

Explicit parameters override the selected preset. Radius is measured in samples
of the slower rate; increasing it sharpens the transition band and increases
work and latency. Cutoff is a fraction of the smaller Nyquist frequency and must
be in `(0, 1]`. Beta controls the Kaiser window and must be in `(0, 20]`.

The input/output rate ratio is approximated by a rational number with at most
`max_denominator` phases (default 1000, allowed 1–100000). Common audio-rate
pairs are exact. For unusual rates, inspect `ratio` and `effective_output_rate`:
duration and streaming positions follow the **effective** ratio. This is a
fixed-ratio converter, not an adaptive clock-drift corrector.

Invalid rates, unsupported dtypes, invalid filter parameters, and oversized
filter banks raise Python exceptions. A filter bank is limited to 8 million
coefficients and a radius of 1 million input frames.

## Development and release

```bash
python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install maturin pytest numpy scipy soxr resampy ruff
maturin develop --release --locked
pytest -q
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
python benchmarks/benchmark.py --output benchmarks/results/local.json
```

Build distributions with `maturin build --release --locked` and `maturin sdist`.
Do not set CPU-specific `RUSTFLAGS` when building redistributable wheels.

[`release.yml`](https://github.com/samliu/si-quick-resampler/blob/main/.github/workflows/release.yml)
builds and tests the wheels,
rebuilds and tests the source distribution, and benchmarks the Linux wheel.
A `v*` tag publishes only after every required job succeeds, using the PyPI
trusted publisher for `samliu/si-quick-resampler`, workflow `release.yml`,
environment `pypi`. Normal branch pushes and pull requests do not publish.

## Provenance

Apache-2.0 licensed. The kernel and phase-bank construction are adapted from
Google's `multichannel-audio-tools` commit
`e7d26a79405983039bc84760a202b80d6ab16ca8`; see
[NOTICE](https://github.com/samliu/si-quick-resampler/blob/main/NOTICE).
This is an independent Rust implementation, not a Google-supported binding.

The filter equations and default parameters follow QResampler. Floating-point
rounding can differ from the original C++/Eigen implementation. This API trims
the flush to the resampled duration, returns copies for equal rates, and uses
explicit stream reset; the original C++ helper emits an additional filter tail
and its flush resets automatically. Tests compare the kernel to independent
NumPy equations and verify streaming matches one-shot processing.

