reverb-extractor / dfdn /delay.py
Nick Martins
Demo safety: curated examples only (no upload), 0.3s RIR + iter cap → bounded, no stall
41bf0d3
Raw History Blame Contribute Delete
2.9 kB
"""Differentiable fractional delay lines (the novel ingredient of the paper).
Integer delays implemented by reading a circular buffer are *not*
differentiable w.r.t. the delay length. The paper makes them differentiable by
realising each delay in the frequency domain with the Pei--Lai closed-form
variable fractional-delay filter, whose frequency response is a smooth function
of the (real-valued) delay ``m``. Gradients therefore flow through the FFT to
``m`` itself.
For a K-point DFT the delay ``z^{-m}`` has the frequency response (Eq. in
Sec. 3 of the paper)::
D[0] = 1
D[k] = exp(-j m (2*pi/K) k) for k = 1 .. K/2 - 1
D[K/2] = cos(m * pi) (Nyquist bin, forced real)
D[K-k] = conj(D[k]) (Hermitian symmetry)
The Nyquist bin is forced real so that the inverse transform of a real signal
stays real; in the time domain this corresponds to a windowed-sinc FIR
interpolator. We only build the non-redundant half spectrum (k = 0 .. K/2) and
rely on ``torch.fft.irfft`` to supply the Hermitian mirror automatically.
"""
from __future__ import annotations
import math
import torch
def fractional_delay_frequency_response(
m: torch.Tensor, n_fft: int
) -> torch.Tensor:
"""Half-spectrum (rfft layout) frequency response of ``z^{-m}``.
Parameters
----------
m
Delay lengths in samples, shape ``(N,)`` (may be fractional / real).
n_fft
FFT length ``K``. Must be even.
Returns
-------
torch.Tensor
Complex tensor of shape ``(N, K // 2 + 1)`` giving ``D_i[k]`` for
``k = 0 .. K/2``. Differentiable w.r.t. ``m``.
"""
if n_fft % 2 != 0:
raise ValueError("n_fft must be even")
m = torch.as_tensor(m)
n_bins = n_fft // 2 + 1
k = torch.arange(n_bins, device=m.device, dtype=m.dtype) # 0 .. K/2
# Phase of a pure delay: -m * (2*pi/K) * k, broadcast over lines.
phase = -m.unsqueeze(-1) * (2.0 * math.pi / n_fft) * k.unsqueeze(0) # (N, n_bins)
resp = torch.polar(torch.ones_like(phase), phase) # unit-modulus complex
# Force the Nyquist bin (k = K/2) to be real = cos(m*pi) so that irfft of a
# real spectrum is real. This is the Pei-Lai construction.
nyq = torch.cos(m * math.pi).to(resp.dtype) # (N,)
resp = resp.clone()
resp[:, -1] = nyq
# k = 0 bin is exactly 1 by construction (phase 0), left as is.
return resp
def apply_fractional_delay(x: torch.Tensor, m: torch.Tensor) -> torch.Tensor:
"""Delay each row of ``x`` by ``m`` samples via the frequency domain.
Provided for testing / standalone use. ``x`` has shape ``(N, L)`` and ``m``
shape ``(N,)``. Returns a tensor of the same shape as ``x``.
"""
N, L = x.shape
X = torch.fft.rfft(x, n=L, dim=-1)
D = fractional_delay_frequency_response(m, L)
return torch.fft.irfft(X * D, n=L, dim=-1)