| --- |
| license: apache-2.0 |
| library_name: kernels |
| tags: |
| - kernel |
| - audio |
| - resampling |
| - dsp |
| - cpu |
| --- |
| |
| # resample-poly |
|
|
| Polyphase rational resampling for audio, loadable through `kernels`. Filter |
| construction matches torchaudio's, so this is a drop-in replacement and |
| results agree to floating-point rounding. The reference baseline is the |
| `conv1d` filter-bank formulation torchaudio uses. |
|
|
| Sample-rate conversion sits at the front of every speech pipeline, and the |
| standard implementation stores each polyphase filter padded to a common |
| width: at 44.1k to 16k a kernel row is 475 entries of which 33 are support, |
| and the padding is multiplied and summed like everything else. This kernel |
| stores only each phase's real support, then picks between two formulations |
| by the one thing that decides which is faster, taps per output, so the |
| result is never slower than the baseline it replaces and the gain lands on |
| downsampling, the direction speech pipelines run. |
|
|
|  |
|
|
| *30 s of mono audio on 14 x86 threads: 44.1k to 16k in 1.73 ms against the |
| conv1d bank's 2.22 (1.28x) on the fused path at 33.4 taps per output, and |
| 16k to 24k at parity on the bank path at 12.3 taps, which is exactly where |
| the plan switches.* |
|
|
| ## Usage |
|
|
| ```python |
| import kernels |
| |
| rp = kernels.get_kernel("phanerozoic/resample-poly", version=1, trust_remote_code=True) |
| |
| r = rp.Resampler(44100, 16000) |
| y = r(x) # [T] or [B, T] |
| |
| y = rp.resample(x, 44100, 16000) # one-shot |
| ``` |
|
|
| `version` selects the release branch; `trust_remote_code` is required by |
| `kernels` for publishers without the trusted-publisher mark. |
|
|
| ## API |
|
|
| | Symbol | Purpose | |
| |---|---| |
| | `Resampler(orig_freq, new_freq, ...)` | cached plan and filters; call it on audio | |
| | `resample(x, orig_freq, new_freq, ...)` | one-shot form | |
| | `plan.fused` / `plan.taps_per_output` | which path was selected and why | |
|
|
| ## Method |
|
|
| Resampling by L/M is nominally zero-stuff by L, low-pass, keep every Mth |
| sample. The polyphase identity removes both the zeros and the discards: |
| output `n` depends only on filter phase `(n*M) mod L` and a short run of |
| input around `floor(n*M/L)`. torchaudio expresses this as a `conv1d` with |
| `new_freq` output channels and stride `orig_freq`, storing per-phase filters |
| padded to a common width; here each phase keeps only its own support, as an |
| offset and a run of taps, which is 7% of the padded bank. |
|
|
| Fewer multiplies did not translate into speed everywhere, and measuring that |
| was the point. The fused path computes one dot product per output and |
| reduces it, and that reduction is fixed cost per output sample: at 44.1k to |
| 48k it does 13x fewer multiplies than the bank and still ran 0.5x, because |
| a phase there carries only 12 taps. The crossover measured between 12 and 17 |
| taps per output, so the plan picks: above the threshold the fused kernel, |
| below it the bank formulation. Both produce the same output, and the |
| selected path is exposed as `plan.fused` and `plan.taps_per_output`. |
|
|
| The filter is a windowed sinc, `lowpass_filter_width` taps per side, |
| `rolloff` fraction of Nyquist, with `sinc_interp_hann` or |
| `sinc_interp_kaiser` windows, all matching torchaudio. Entries clamped past |
| the filter half width carry a window value of exactly `cos(pi/2)^2`, so the |
| trimmed support is exact rather than thresholded, and the test suite asserts |
| that every dropped entry is zero. |
|
|
| ## Measured |
|
|
| 30 s of mono audio, best of 25 runs after warmup: |
|
|
| | conversion | resample-poly | conv1d bank | speedup | taps/out | path | |
| |---|---:|---:|---:|---:|:--| |
| | **x86-64** (i9-12900H, 14 threads) | | | | | | |
| | 44.1k to 16k | 1.73 ms | 2.22 ms | 1.28x | 33.4 | fused | |
| | 48k to 16k | 3.56 ms | 4.19 ms | 1.18x | 37.0 | fused | |
| | 16k to 24k | 0.87 ms | 0.87 ms | 1.00x | 12.3 | bank | |
| | 44.1k to 48k | 2.69 ms | 2.73 ms | 1.02x | 12.1 | bank | |
| | **aarch64** (Cortex-A76) | | | | | | |
| | 44.1k to 16k | 6.31 ms | 14.41 ms | 2.28x | 33.4 | fused | |
| | 48k to 16k | 9.79 ms | 24.21 ms | 2.47x | 37.0 | fused | |
| | 16k to 24k | 9.03 ms | 9.05 ms | 1.00x | 12.3 | bank | |
| | 44.1k to 48k | 21.11 ms | 20.98 ms | 0.99x | 12.1 | bank | |
|
|
| 44.1k to 16k runs 30 s of audio in 1.73 ms on x86-64 (a 16,000x realtime |
| factor) and 6.31 ms on a Raspberry Pi 5. The stored taps are 5,345 of 76,000 |
| padded-bank entries, 7.0%. Speedups shift with thread count, since the fused |
| path parallelizes over output blocks and the bank over convolution work. |
|
|
| ## Correctness |
|
|
| Output agrees with the torchaudio filter-bank formulation to floating-point |
| rounding across all four conversions. At equal rates the bank collapses to |
| one phase but the low-pass still applies, so the result is band-limited |
| rather than an identity, which is torchaudio's behavior too. |
|
|
| ## Requirements and limits |
|
|
| - CPU, `x86_64-linux` and `aarch64-linux`, for PyTorch 2.11, 2.12, and 2.13 |
| (six prebuilt variants), each architecture built on its own host and |
| validated against the prebuilt binary. |
| - Rational conversions only; the plan is fixed at construction for a given |
| rate pair. |
|
|
| ## References |
|
|
| The polyphase decomposition of rational resampling; torchaudio's windowed |
| sinc filter construction as the compatibility target. |
|
|
| ## License |
|
|
| Apache-2.0. |
|
|