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

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

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.

Downloads last month
-
kernel
audio
resampling
dsp
cpu
apache-2.0
OS
linux
Arch
x86_64aarch64
Kernel Builder
19aaa64