File size: 5,343 Bytes
6aa3bd6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
---
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.