CoolFace
Apppublic

nicktup/reverb-extractor

sourceHugging Facemitupdated 16d agoView on Hugging Face
0likes
fdn.py142 linesDownload Raw Back to dfdn
1"""The differentiable Feedback Delay Network (FDN).2 3State-space description of the single-input single-output FDN (paper Sec. 2)::4 5    y[n]        = c^T s[n] + d u[n]6    s_i[n]      = ( b_i u[n] + sum_j A_ij s_j[n] )  delayed by m_i samples7 8with feedback matrix ``A = U @ diag(gamma)``. The transfer function is9 10    H(z) = c^T ( I - D(z) A )^{-1} D(z) b + d ,   D(z) = diag(z^{-m_i}) .11 12We render the RIR by *frequency sampling*: evaluate ``H`` on the DFT grid using13the differentiable fractional-delay response ``D_i[k]`` from ``delay.py`` and14take the inverse real FFT. Every parameter -- input/output gains ``b, c``,15direct gain ``d``, feedback matrix ``A`` and, crucially, the delay lengths16``m`` -- is a differentiable function of an unconstrained proxy, so the whole17network is trainable by backpropagation.18 19Reparameterisations (paper Sec. 3-4):20    * delays   m   = clamp(|m_tilde|, 0, Q-1)         (nonnegative, causal)21    * orthog.  U   = expm(triu(W) - triu(W)^T)        (unilossless feedback)22    * absorp.  gamma = sigmoid(gamma_tilde) in (0,1)  (decoupled from m)23    * gains    b,c,d = |.|                             (nonnegative)24"""25 26from __future__ import annotations27 28from dataclasses import dataclass29import math30import torch31import torch.nn as nn32 33from .delay import fractional_delay_frequency_response34 35 36@dataclass37class FDNConfig:38    n_delays: int = 6          # N, number of delay lines39    sample_rate: int = 16000   # fs (Hz)40    n_fft: int = 16384         # K, frequency-sampling grid (even; also sets Q=K/2)41    max_delay_ms: float = 64.0  # init scale psi -> ~64 ms max (paper)42    beta_a: float = 1.1        # Beta(alpha, beta) delay init43    beta_b: float = 6.044 45 46class DifferentiableFDN(nn.Module):47    """A fully differentiable FDN whose delay lines are learnable."""48 49    def __init__(self, config: FDNConfig | None = None):50        super().__init__()51        self.cfg = config or FDNConfig()52        N = self.cfg.n_delays53        self.Q = self.cfg.n_fft // 2  # buffer length; delays bounded by Q-154 55        # --- delay lines: proxy m_tilde, m = clamp(|m_tilde|, 0, Q-1) ---56        psi = self.cfg.max_delay_ms * 1e-3 * self.cfg.sample_rate  # samples at max57        beta = torch.distributions.Beta(self.cfg.beta_a, self.cfg.beta_b)58        m0 = psi * beta.sample((N,))  # mean ~ psi * a/(a+b) ~ 10 ms59        self.m_tilde = nn.Parameter(m0)60 61        # --- orthogonal feedback matrix via skew-symmetric exponential map ---62        self.W = nn.Parameter(torch.randn(N, N) / math.sqrt(N))63 64        # --- absorption coefficients gamma = sigmoid(gamma_tilde) in (0,1) ---65        # init ~0.9 (light per-hop loss -> audible decay). logit(0.9) ~ 2.266        self.gamma_tilde = nn.Parameter(torch.full((N,), 2.2))67 68        # --- input / output / direct gains (nonnegative via abs) ---69        self.b_tilde = nn.Parameter(torch.randn(N) / math.sqrt(N))70        self.c_tilde = nn.Parameter(torch.full((N,), 1.0 / N))71        self.d_tilde = nn.Parameter(torch.tensor(1.0))72 73    # ------------------------------------------------------------------ #74    # Constrained parameters (differentiable functions of the proxies)   #75    # ------------------------------------------------------------------ #76    @property77    def delays(self) -> torch.Tensor:78        """Delay lengths m in samples, in [0, Q-1]."""79        return torch.clamp(torch.abs(self.m_tilde), 0.0, self.Q - 1)80 81    @property82    def U(self) -> torch.Tensor:83        """Orthogonal (unilossless) matrix via matrix exponential of a skew form."""84        tri = torch.triu(self.W, diagonal=1)85        skew = tri - tri.transpose(-1, -2)86        return torch.matrix_exp(skew)87 88    @property89    def gamma(self) -> torch.Tensor:90        return torch.sigmoid(self.gamma_tilde)91 92    @property93    def feedback_matrix(self) -> torch.Tensor:94        """A = U @ diag(gamma). Spectral radius < 1 => stable."""95        return self.U * self.gamma.unsqueeze(0)  # column scaling == U @ diag(gamma)96 97    @property98    def b(self) -> torch.Tensor:99        return torch.abs(self.b_tilde)100 101    @property102    def c(self) -> torch.Tensor:103        return torch.abs(self.c_tilde)104 105    @property106    def d(self) -> torch.Tensor:107        return torch.abs(self.d_tilde)108 109    # ------------------------------------------------------------------ #110    # Rendering                                                          #111    # ------------------------------------------------------------------ #112    def transfer_function(self, n_fft: int | None = None) -> torch.Tensor:113        """Evaluate H on the rfft grid. Returns complex tensor (n_fft//2+1,)."""114        K = n_fft or self.cfg.n_fft115        m = self.delays116        D = fractional_delay_frequency_response(m, K)  # (N, n_bins) complex117        D = D.transpose(0, 1)                          # (n_bins, N)118        n_bins = D.shape[0]119        N = self.cfg.n_delays120 121        A = self.feedback_matrix.to(D.dtype)           # (N, N) complex122        I = torch.eye(N, dtype=D.dtype, device=D.device)123 124        # M[k] = I - diag(D[k]) @ A  ==  I - D[k, :, None] * A125        M = I.unsqueeze(0) - D.unsqueeze(-1) * A.unsqueeze(0)  # (n_bins, N, N)126        rhs = (D * self.b.to(D.dtype)).unsqueeze(-1)           # (n_bins, N, 1)127        x = torch.linalg.solve(M, rhs).squeeze(-1)            # (n_bins, N)128        H = (x * self.c.to(D.dtype)).sum(-1) + self.d.to(D.dtype)  # (n_bins,)129        return H130 131    def render(self, length: int, n_fft: int | None = None) -> torch.Tensor:132        """Render the RIR (real, length ``length``)."""133        K = n_fft or self.cfg.n_fft134        if K < length:135            raise ValueError(f"n_fft ({K}) must be >= length ({length})")136        H = self.transfer_function(K)137        h = torch.fft.irfft(H, n=K)138        return h[:length]139 140    def forward(self, length: int) -> torch.Tensor:141        return self.render(length)142