Download torch-ext/resample_poly/__init__.py from phanerozoic/resample-poly: direct link, hf CLI and curl.
- Browser
- Download file 6.35 kB
-
https://huggingface.co/kernels/phanerozoic/resample-poly/resolve/v1/torch-ext/resample_poly/__init__.py
- Command line
-
hf download hf://phanerozoic/resample-poly@v1/torch-ext/resample_poly/__init__.py
-
curl -L -o __init__.py https://huggingface.co/kernels/phanerozoic/resample-poly/resolve/v1/torch-ext/resample_poly/__init__.py
6.35 kB
| """resample-poly: polyphase rational resampling for audio. | |
| Each filter phase stores only its own support rather than being padded to a | |
| common width, so an output sample multiplies through its taps and nothing else. | |
| The filter matches torchaudio's construction, so results agree to floating-point | |
| rounding and this is a drop-in replacement. | |
| High-level API: | |
| Resampler(orig_freq, new_freq, ...) plan holding the polyphase bank | |
| resample(x, orig_freq, new_freq) one-shot convenience | |
| """ | |
| import math | |
| from typing import Optional | |
| import torch | |
| from ._ops import ops | |
| def sinc_kernel(orig_freq: int, new_freq: int, lowpass_filter_width: int = 6, | |
| rolloff: float = 0.99, method: str = "sinc_interp_hann", | |
| beta: Optional[float] = None): | |
| """Windowed-sinc polyphase bank, matching torchaudio's construction. | |
| Returns (kernels [L, W], width), with frequencies already reduced by their | |
| greatest common divisor. | |
| """ | |
| if orig_freq <= 0 or new_freq <= 0: | |
| raise ValueError("frequencies must be positive") | |
| if lowpass_filter_width <= 0: | |
| raise ValueError("lowpass_filter_width must be positive") | |
| g = math.gcd(int(orig_freq), int(new_freq)) | |
| M = int(orig_freq) // g # input samples per block | |
| L = int(new_freq) // g # output samples per block | |
| base_freq = min(M, L) * rolloff | |
| width = math.ceil(lowpass_filter_width * M / base_freq) | |
| idx = torch.arange(-width, width + M, dtype=torch.float64) / M | |
| t = (torch.arange(0, -L, -1, dtype=torch.float64)[:, None] / L) + idx[None, :] | |
| t = t * base_freq | |
| t = t.clamp_(-lowpass_filter_width, lowpass_filter_width) | |
| if method == "sinc_interp_hann": | |
| window = torch.cos(t * math.pi / lowpass_filter_width / 2) ** 2 | |
| elif method == "sinc_interp_kaiser": | |
| b = 14.769656459379492 if beta is None else float(beta) | |
| window = (torch.i0(b * torch.sqrt( | |
| 1 - (t / lowpass_filter_width) ** 2).clamp(min=0)) / torch.i0( | |
| torch.tensor(b, dtype=torch.float64))) | |
| else: | |
| raise ValueError(f"unknown method {method!r}") | |
| tp = t * math.pi | |
| k = torch.where(tp == 0, torch.ones_like(tp), torch.sin(tp) / tp) | |
| k = k * window * (base_freq / M) | |
| return k.to(torch.float32), width, L, M | |
| class Resampler: | |
| """Cached polyphase plan for a fixed rate pair.""" | |
| def __init__(self, orig_freq: int, new_freq: int, | |
| lowpass_filter_width: int = 6, rolloff: float = 0.99, | |
| method: str = "sinc_interp_hann", beta: Optional[float] = None): | |
| kern, width, L, M = sinc_kernel(orig_freq, new_freq, | |
| lowpass_filter_width, rolloff, | |
| method, beta) | |
| self.orig_freq, self.new_freq = int(orig_freq), int(new_freq) | |
| self.L, self.M, self.width = L, M, width | |
| self.kernel = kern | |
| # Keep only each phase's support. Entries clamped past the filter half | |
| # width carry a window value of exactly zero, so the support is exact | |
| # rather than a threshold. | |
| starts, lens, offs, taps = [], [], [], [] | |
| cursor = 0 | |
| for i in range(L): | |
| row = kern[i] | |
| nz = torch.nonzero(row, as_tuple=False).flatten() | |
| if nz.numel() == 0: | |
| starts.append(-width); lens.append(0); offs.append(cursor) | |
| continue | |
| j0, j1 = int(nz[0]), int(nz[-1]) + 1 | |
| starts.append(j0 - width) | |
| lens.append(j1 - j0) | |
| offs.append(cursor) | |
| taps.append(row[j0:j1].contiguous()) | |
| cursor += j1 - j0 | |
| self.start = torch.tensor(starts, dtype=torch.int32) | |
| self.len = torch.tensor(lens, dtype=torch.int32) | |
| self.off = torch.tensor(offs, dtype=torch.int64) | |
| self.taps = (torch.cat(taps) if taps else | |
| torch.zeros(0, dtype=torch.float32)).contiguous() | |
| # The fused path computes one dot product per output and reduces it. | |
| # That reduction is fixed cost per output, so it pays only when a phase | |
| # carries enough taps to amortize it. Measured against the conv1d bank, | |
| # the crossover sits between 12 and 17 taps per output: 16.7 taps wins | |
| # 1.5-1.8x, 12.1 loses 0.5x even while doing 13x fewer multiplies. Below | |
| # the threshold the bank formulation is used instead, so this is never | |
| # slower than the baseline it replaces. | |
| self.taps_per_output = float(self.taps.numel()) / float(L) | |
| self.fused = self.taps_per_output >= 16.0 | |
| def density(self) -> float: | |
| """Stored taps over the padded-kernel size the conv1d form would use.""" | |
| return float(self.taps.numel()) / float(self.kernel.numel()) | |
| def _bank(self, x: torch.Tensor) -> torch.Tensor: | |
| """Padded-bank formulation, for rate pairs whose phases are too short | |
| for the fused path to pay for its reduction.""" | |
| B, T = x.shape | |
| pad = torch.nn.functional.pad(x, (self.width, self.width + self.M)) | |
| r = torch.nn.functional.conv1d(pad[:, None], self.kernel[:, None, :], | |
| stride=self.M) | |
| return r.transpose(1, 2).reshape(B, -1)[:, :self.out_len(T)] | |
| def out_len(self, n: int) -> int: | |
| return int(math.ceil(self.L * n / self.M)) | |
| def __call__(self, x: torch.Tensor) -> torch.Tensor: | |
| """x [T] or [B, T] -> resampled, matching torchaudio's output length.""" | |
| squeeze = x.dim() == 1 | |
| if squeeze: | |
| x = x.unsqueeze(0) | |
| if x.dim() != 2: | |
| raise ValueError("x must be [T] or [B, T]") | |
| x = x.to(torch.float32).contiguous() | |
| B, T = x.shape | |
| if not self.fused: | |
| y = self._bank(x) | |
| return y.squeeze(0) if squeeze else y | |
| out = torch.empty(B, self.out_len(T), dtype=torch.float32) | |
| ops.rp_resample(out, x, self.taps, self.start, self.len, self.off, | |
| self.L, self.M) | |
| return out.squeeze(0) if squeeze else out | |
| def resample(x: torch.Tensor, orig_freq: int, new_freq: int, | |
| **kw) -> torch.Tensor: | |
| """One-shot resample. Prefer Resampler when converting repeatedly.""" | |
| return Resampler(orig_freq, new_freq, **kw)(x) | |
| __all__ = ["Resampler", "resample", "sinc_kernel"] | |