nicktup/reverb-extractor
0
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 