CoolFace
Apppublic

nicktup/reverb-extractor

sourceHugging Facemitupdated 16d agoView on Hugging Face
0likes
rir.py89 linesDownload Raw Back to dfdn
1"""Target RIR helpers: load a real WAV or synthesise a plausible one.2 3The synthetic RIR is intentionally realistic for the smoke test: a direct4impulse, a handful of *sparse* early reflections, and an exponentially decaying5Gaussian-noise late tail. This produces an echo density that rises from sparse6(near 0) to diffuse (near 1) -- exactly the structure the soft-EDP loss exists7to match -- and a clean exponential EDC set by a target T60.8 9It is CLEARLY SYNTHETIC and labelled as such wherever it is used.10"""11 12from __future__ import annotations13 14import numpy as np15import torch16 17 18def t60_to_tau(t60: float, sample_rate: int) -> float:19    """Decay time constant (samples) for a given T60 (seconds).20 21    -60 dB == amplitude factor 1e-3 == exp(-t/tau_amp). T60 is a -60 dB *energy*22    decay, so amplitude decays 3*ln(10) over T60 seconds.23    """24    return t60 * sample_rate / (3.0 * np.log(10.0))25 26 27def synth_rir(28    t60: float = 0.5,29    sample_rate: int = 16000,30    length: int | None = None,31    n_early: int = 8,32    mixing_time_ms: float = 40.0,33    seed: int = 0,34) -> np.ndarray:35    """Synthesise a plausible room impulse response (NOT a measurement)."""36    rng = np.random.default_rng(seed)37    if length is None:38        length = int(round(1.2 * t60 * sample_rate))39    n = np.arange(length)40    tau = t60_to_tau(t60, sample_rate)41    envelope = np.exp(-n / tau)42 43    h = np.zeros(length, dtype=np.float64)44 45    # Direct sound.46    h[0] = 1.047 48    # Sparse early reflections within the mixing time.49    mix = int(round(mixing_time_ms * 1e-3 * sample_rate))50    early_idx = rng.choice(np.arange(1, max(mix, 2)), size=min(n_early, mix - 1),51                           replace=False)52    for i in early_idx:53        h[i] += rng.uniform(0.3, 0.8) * rng.choice([-1.0, 1.0])54 55    # Diffuse late tail: Gaussian noise, faded in around the mixing time so the56    # echo density rises smoothly from sparse to diffuse.57    noise = rng.standard_normal(length)58    fade = 1.0 / (1.0 + np.exp(-(n - mix) / (0.25 * mix + 1e-9)))  # logistic fade-in59    tail = 0.35 * noise * fade60    h += tail61 62    # Apply the global decay envelope and normalise.63    h *= envelope64    h /= np.max(np.abs(h)) + 1e-1265    return h.astype(np.float32)66 67 68def load_rir(path: str, sample_rate: int = 16000) -> np.ndarray:69    """Load a WAV RIR, downmix to mono, resample to ``sample_rate``, trim silence."""70    import soundfile as sf71    from scipy.signal import resample_poly72    from math import gcd73 74    x, sr = sf.read(path, always_2d=True)75    x = x.mean(axis=1)  # downmix76    if sr != sample_rate:77        g = gcd(int(sr), int(sample_rate))78        x = resample_poly(x, sample_rate // g, sr // g)79    # Strip leading silence up to the direct-sound onset.80    peak = int(np.argmax(np.abs(x)))81    onset = max(0, peak - int(0.001 * sample_rate))  # keep ~1 ms before peak82    x = x[onset:]83    x = x / (np.max(np.abs(x)) + 1e-12)84    return x.astype(np.float32)85 86 87def to_tensor(h: np.ndarray, device=None) -> torch.Tensor:88    return torch.as_tensor(h, dtype=torch.float32, device=device)89