--- 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. ![Per-conversion bars with the fused and bank paths labeled and taps per output shown](https://huggingface.co/kernels/phanerozoic/resample-poly/resolve/main/media/hero.gif) *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.