Spaces:
Running
Running
File size: 8,039 Bytes
e319a7b | 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 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | # File under the MIT license, see https://github.com/adefossez/julius/LICENSE for details.
# Author: adefossez, 2020
"""
Differentiable, Pytorch based resampling.
Implementation of Julius O. Smith algorithm for resampling.
See https://ccrma.stanford.edu/~jos/resample/ for details.
This implementation is specially optimized for when new_sr / old_sr is a fraction
with a small numerator and denominator when removing the gcd (e.g. new_sr = 700, old_sr = 500).
Very similar to [bmcfee/resampy](https://github.com/bmcfee/resampy) except this implementation
is optimized for the case mentioned before, while resampy is slower but more general.
"""
import math
from typing import Optional
import torch
from torch.nn import functional as F
def sinc(x: torch.Tensor) -> torch.Tensor:
"""sin(x) / x, with the limit value 1 at x == 0.
__Warning__: the input is not multiplied by `pi`!
"""
return torch.where(
x == 0,
torch.tensor(1.0, device=x.device, dtype=x.dtype),
torch.sin(x) / x,
)
class ResampleFrac(torch.nn.Module):
"""
Resampling from the sample rate `old_sr` to `new_sr`.
"""
def __init__(
self, old_sr: int, new_sr: int, zeros: int = 24, rolloff: float = 0.945
):
"""
Args:
old_sr (int): sample rate of the input signal x.
new_sr (int): sample rate of the output.
zeros (int): number of zero crossing to keep in the sinc filter.
rolloff (float): use a lowpass filter that is `rolloff * new_sr / 2`,
to ensure sufficient margin due to the imperfection of the FIR filter used.
Lowering this value will reduce anti-aliasing, but will reduce some of the
highest frequencies.
Shape:
- Input: `[*, T]`
- Output: `[*, T']` with `T' = int(new_sr * T / old_sr)
.. caution::
After dividing `old_sr` and `new_sr` by their GCD, both should be small
for this implementation to be fast.
>>> import torch
>>> resample = ResampleFrac(4, 5)
>>> x = torch.randn(1000)
>>> print(len(resample(x)))
1250
"""
super().__init__()
if not isinstance(old_sr, int) or not isinstance(new_sr, int):
raise ValueError("old_sr and new_sr should be integers")
gcd = math.gcd(old_sr, new_sr)
self.old_sr = old_sr // gcd
self.new_sr = new_sr // gcd
self.zeros = zeros
self.rolloff = rolloff
self._init_kernels()
def _init_kernels(self):
if self.old_sr == self.new_sr:
return
kernels = []
sr = min(self.new_sr, self.old_sr)
# rolloff will perform antialiasing filtering by removing the highest frequencies.
# At first I thought I only needed this when downsampling, but when upsampling
# you will get edge artifacts without this, the edge is equivalent to zero padding,
# which will add high freq artifacts.
sr *= self.rolloff
# The key idea of the algorithm is that x(t) can be exactly reconstructed from x[i] (tensor)
# using the sinc interpolation formula:
# x(t) = sum_i x[i] sinc(pi * old_sr * (i / old_sr - t))
# We can then sample the function x(t) with a different sample rate:
# y[j] = x(j / new_sr)
# or,
# y[j] = sum_i x[i] sinc(pi * old_sr * (i / old_sr - j / new_sr))
# We see here that y[j] is the convolution of x[i] with a specific filter, for which
# we take an FIR approximation, stopping when we see at least `zeros` zeros crossing.
# But y[j+1] is going to have a different set of weights and so on, until y[j + new_sr].
# Indeed:
# y[j + new_sr] = sum_i x[i] sinc(pi * old_sr * ((i / old_sr - (j + new_sr) / new_sr))
# = sum_i x[i] sinc(pi * old_sr * ((i - old_sr) / old_sr - j / new_sr))
# = sum_i x[i + old_sr] sinc(pi * old_sr * (i / old_sr - j / new_sr))
# so y[j+new_sr] uses the same filter as y[j], but on a shifted version of x by `old_sr`.
# This will explain the F.conv1d after, with a stride of old_sr.
self._width = math.ceil(self.zeros * self.old_sr / sr)
# If old_sr is still big after GCD reduction, most filters will be very unbalanced, i.e.,
# they will have a lot of almost zero values to the left or to the right...
# There is probably a way to evaluate those filters more efficiently, but this is kept for
# future work.
idx = torch.arange(-self._width, self._width + self.old_sr).float()
for i in range(self.new_sr):
t = (-i / self.new_sr + idx / self.old_sr) * sr
t = t.clamp_(-self.zeros, self.zeros)
t *= math.pi
window = torch.cos(t / self.zeros / 2) ** 2
kernel = sinc(t) * window
# Renormalize kernel to ensure a constant signal is preserved.
kernel.div_(kernel.sum())
kernels.append(kernel)
self.register_buffer("kernel", torch.stack(kernels).view(self.new_sr, 1, -1))
def forward(
self, x: torch.Tensor, output_length: Optional[int] = None, full: bool = False
):
"""
Resample x.
Args:
x (Tensor): signal to resample, time should be the last dimension
output_length (None or int): This can be set to the desired output length
(last dimension). Allowed values are between 0 and
ceil(length * new_sr / old_sr). When None (default) is specified, the
floored output length will be used. In order to select the largest possible
size, use the `full` argument.
full (bool): return the longest possible output from the input. This can be useful
if you chain resampling operations, and want to give the `output_length` only
for the last one, while passing `full=True` to all the other ones.
"""
if self.old_sr == self.new_sr:
return x
shape = x.shape
length = x.shape[-1]
x = x.reshape(-1, length)
x = F.pad(
x[:, None], (self._width, self._width + self.old_sr), mode="replicate"
)
ys = F.conv1d(x, self.kernel, stride=self.old_sr) # type: ignore
y = ys.transpose(1, 2).reshape(list(shape[:-1]) + [-1])
float_output_length = torch.as_tensor(self.new_sr * length / self.old_sr)
max_output_length = torch.ceil(float_output_length).long()
default_output_length = torch.floor(float_output_length).long()
if output_length is None:
applied_output_length = max_output_length if full else default_output_length
elif output_length < 0 or output_length > max_output_length:
raise ValueError(f"output_length must be between 0 and {max_output_length}")
else:
applied_output_length = torch.tensor(output_length)
if full:
raise ValueError("You cannot pass both full=True and output_length")
return y[..., :applied_output_length] # type: ignore
def __repr__(self):
return (
f"ResampleFrac(old_sr={self.old_sr}, new_sr={self.new_sr}, "
f"zeros={self.zeros}, rolloff={self.rolloff})"
)
def resample_frac(
x: torch.Tensor,
old_sr: int,
new_sr: int,
zeros: int = 24,
rolloff: float = 0.945,
output_length: Optional[int] = None,
full: bool = False,
):
"""
Functional version of `ResampleFrac`, refer to its documentation for more information.
..warning::
If you call repeatidly this functions with the same sample rates, then the
resampling kernel will be recomputed everytime. For best performance, you should use
and cache an instance of `ResampleFrac`.
"""
return ResampleFrac(old_sr, new_sr, zeros, rolloff).to(x)(x, output_length, full)
|