Skip to main content

ferraus_denoise/
dfn3.rs

1//! DeepFilterNet 3 (Schröter et al., 2023) on ONNX Runtime.
2//!
3//! The registry entry `denoise/dfn3` is the upstream ONNX export: three graphs (`enc.onnx`,
4//! `erb_dec.onnx`, `df_dec.onnx`) plus `config.ini`. Everything around the graphs is
5//! implemented here, following the reference (`libDF` and `df/enhance.py`, MIT/Apache-2.0):
6//!
7//! 1. 48 kHz, Vorbis-windowed STFT with a 960-sample window and 480 hop, normalized by
8//!    `hop·2 / window²` in the analysis only; the signal is padded with one window of zeros
9//!    and the result trimmed by `window − hop` samples (the STFT delay).
10//! 2. Features per frame: 32 ERB band energies in dB (rectangular bands of at least two
11//!    bins), exponentially mean-normalized (`α = 0.99`, initial state −60…−90 dB, divided by
12//!    40); the first 96 complex bins unit-normalized by the square root of an exponential
13//!    mean of their magnitude (initial state 0.001…0.0001). Both are shifted two frames
14//!    earlier (`conv_lookahead`), zero-padded at the end.
15//! 3. Encoder → ERB decoder: 32 gains in 0..1, applied to every bin of their band.
16//! 4. Encoder → DF decoder: order-5 complex filters for the first 96 bins, applied to the
17//!    *noisy* spectrum over frames `t−2 … t+2` (`df_lookahead = 2`); the gained spectrum is
18//!    kept above bin 96.
19//! 5. Inverse STFT with overlap-add.
20//!
21//! The graphs process whole sequences (their GRUs start from zero state on every call).
22//! Long inputs run in 30 s segments with 1 s of discarded warm-up context and a 0.5 s
23//! crossfade of the gains and filter coefficients, so memory stays bounded; features are
24//! always computed over the whole file, so segmenting changes nothing upstream of the graphs.
25
26use ferraus_core::resample::{resample, Quality};
27use ferraus_core::{AudioBuffer, Error, OpCtx, Result};
28use ferraus_ml::session::{load_for_hinted, ModelSession, TensorF32};
29use realfft::num_complex::Complex32;
30use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
31use std::path::Path;
32use std::sync::Arc;
33
34/// Registry name of the model.
35pub const MODEL: &str = "denoise/dfn3";
36/// Members of the archive this runtime needs.
37pub const MEMBERS: [&str; 4] = ["enc.onnx", "erb_dec.onnx", "df_dec.onnx", "config.ini"];
38
39const MEAN_NORM_INIT: [f32; 2] = [-60.0, -90.0];
40const UNIT_NORM_INIT: [f32; 2] = [0.001, 0.0001];
41
42/// Model and DSP parameters (`config.ini`, sections `[df]` and `[deepfilternet]`).
43#[derive(Clone, Debug, PartialEq)]
44pub struct Params {
45    pub sr: usize,
46    pub fft_size: usize,
47    pub hop_size: usize,
48    pub nb_erb: usize,
49    pub nb_df: usize,
50    pub df_order: usize,
51    pub df_lookahead: usize,
52    pub conv_lookahead: usize,
53    pub min_nb_erb_freqs: usize,
54    pub norm_tau: f64,
55}
56
57impl Default for Params {
58    /// The DeepFilterNet 3 release values.
59    fn default() -> Self {
60        Params {
61            sr: 48_000,
62            fft_size: 960,
63            hop_size: 480,
64            nb_erb: 32,
65            nb_df: 96,
66            df_order: 5,
67            df_lookahead: 2,
68            conv_lookahead: 2,
69            min_nb_erb_freqs: 2,
70            norm_tau: 1.0,
71        }
72    }
73}
74
75impl Params {
76    /// Parse `config.ini`; missing keys keep the release defaults.
77    pub fn from_ini(text: &str) -> Result<Params> {
78        let mut p = Params::default();
79        let mut section = String::new();
80        for line in text.lines() {
81            let line = line.trim();
82            if line.starts_with('[') && line.ends_with(']') {
83                section = line[1..line.len() - 1].trim().to_ascii_lowercase();
84                continue;
85            }
86            let Some((k, v)) = line.split_once('=') else { continue };
87            let (k, v) = (k.trim(), v.trim());
88            let num = || v.parse::<usize>().map_err(|_| Error::processing(format!("DFN3 config.ini: bad {k} = {v}")));
89            match (section.as_str(), k) {
90                ("df", "sr") => p.sr = num()?,
91                ("df", "fft_size") => p.fft_size = num()?,
92                ("df", "hop_size") => p.hop_size = num()?,
93                ("df", "nb_erb") => p.nb_erb = num()?,
94                ("df", "nb_df") => p.nb_df = num()?,
95                ("df", "df_order") => p.df_order = num()?,
96                ("df", "df_lookahead") => p.df_lookahead = num()?,
97                ("df", "min_nb_erb_freqs") => p.min_nb_erb_freqs = num()?,
98                ("df", "norm_tau") => {
99                    p.norm_tau =
100                        v.parse().map_err(|_| Error::processing(format!("DFN3 config.ini: bad norm_tau = {v}")))?
101                }
102                ("deepfilternet", "conv_lookahead") => p.conv_lookahead = num()?,
103                _ => {}
104            }
105        }
106        if p.hop_size * 2 != p.fft_size || p.nb_df > p.fft_size / 2 + 1 || p.df_lookahead >= p.df_order || p.nb_erb < 2
107        {
108            return Err(Error::unsupported(format!("DFN3 config.ini describes an unsupported layout: {p:?}")));
109        }
110        Ok(p)
111    }
112
113    /// Exponential-mean factor, rounded as the reference does (0.99 for 10 ms hops, τ = 1 s).
114    pub fn alpha(&self) -> f32 {
115        let a = (-(self.hop_size as f64 / self.sr as f64) / self.norm_tau).exp();
116        let mut precision = 3;
117        loop {
118            let p = 10f64.powi(precision);
119            let r = (a * p).round() / p;
120            if r < 1.0 {
121                return r as f32;
122            }
123            precision += 1;
124        }
125    }
126
127    pub fn freq_bins(&self) -> usize {
128        self.fft_size / 2 + 1
129    }
130}
131
132fn freq2erb(f: f32) -> f32 {
133    9.265 * (f / (24.7 * 9.265)).ln_1p()
134}
135
136fn erb2freq(e: f32) -> f32 {
137    24.7 * 9.265 * ((e / 9.265).exp() - 1.0)
138}
139
140/// Bins per ERB band (libDF `erb_fb`): equal ERB-scale steps rounded to bins, at least
141/// `min_nb_freqs` bins each, summing to `fft_size / 2 + 1`.
142pub fn erb_widths(sr: usize, fft_size: usize, nb_bands: usize, min_nb_freqs: usize) -> Vec<usize> {
143    let nyq = (sr / 2) as f32;
144    let freq_width = sr as f32 / fft_size as f32;
145    let erb_low = freq2erb(0.0);
146    let erb_high = freq2erb(nyq);
147    let step = (erb_high - erb_low) / nb_bands as f32;
148    let min = min_nb_freqs as i32;
149    let mut erb = vec![0usize; nb_bands];
150    let (mut prev, mut over) = (0i32, 0i32);
151    for i in 1..=nb_bands {
152        let f = erb2freq(erb_low + i as f32 * step);
153        let fb = (f / freq_width).round() as i32;
154        let mut nb = fb - prev - over;
155        if nb < min {
156            over = min - nb;
157            nb = min;
158        } else {
159            over = 0;
160        }
161        erb[i - 1] = nb as usize;
162        prev = fb;
163    }
164    erb[nb_bands - 1] += 1;
165    let total: usize = erb.iter().sum();
166    let bins = fft_size / 2 + 1;
167    if total > bins {
168        erb[nb_bands - 1] -= total - bins;
169    }
170    erb
171}
172
173/// The Vorbis window `sin(π/2 · sin²(π(n + ½)/N))`, power complementary at 50% overlap.
174pub fn vorbis_window(n: usize) -> Vec<f32> {
175    let half = (n / 2) as f64;
176    (0..n)
177        .map(|i| {
178            let s = (0.5 * std::f64::consts::PI * (i as f64 + 0.5) / half).sin();
179            (0.5 * std::f64::consts::PI * s * s).sin() as f32
180        })
181        .collect()
182}
183
184/// libDF's frame-by-frame STFT (analysis memory of one hop, synthesis overlap-add).
185pub struct Stft {
186    fwd: Arc<dyn RealToComplex<f32>>,
187    inv: Arc<dyn ComplexToReal<f32>>,
188    window: Vec<f32>,
189    wnorm: f32,
190    hop: usize,
191}
192
193impl Stft {
194    pub fn new(fft_size: usize, hop: usize) -> Stft {
195        let mut planner = RealFftPlanner::<f32>::new();
196        Stft {
197            fwd: planner.plan_fft_forward(fft_size),
198            inv: planner.plan_fft_inverse(fft_size),
199            window: vorbis_window(fft_size),
200            wnorm: 1.0 / ((fft_size * fft_size) as f32 / (2 * hop) as f32),
201            hop,
202        }
203    }
204
205    /// Spectra of `x` (`len / hop` frames; frame `t` covers samples `(t−1)·hop … (t+1)·hop`).
206    pub fn analysis(&self, x: &[f32]) -> Vec<Vec<Complex32>> {
207        let n = self.window.len();
208        let frames = x.len() / self.hop;
209        let mut buf = self.fwd.make_input_vec();
210        let mut scratch = self.fwd.make_scratch_vec();
211        let mut out = Vec::with_capacity(frames);
212        for t in 0..frames {
213            for (i, b) in buf.iter_mut().enumerate() {
214                let idx = (t * self.hop + i) as isize - (n - self.hop) as isize;
215                let v = if idx >= 0 { x[idx as usize] } else { 0.0 };
216                *b = v * self.window[i];
217            }
218            let mut spec = self.fwd.make_output_vec();
219            // lengths always match the plan
220            let _ = self.fwd.process_with_scratch(&mut buf, &mut spec, &mut scratch);
221            for s in spec.iter_mut() {
222                *s *= self.wnorm;
223            }
224            out.push(spec);
225        }
226        out
227    }
228
229    /// Overlap-add synthesis: `frames × hop` samples.
230    pub fn synthesis(&self, spec: &mut [Vec<Complex32>]) -> Vec<f32> {
231        let n = self.window.len();
232        let mut out = vec![0f32; spec.len() * self.hop];
233        let mut mem = vec![0f32; n - self.hop];
234        let mut buf = self.inv.make_output_vec();
235        let mut scratch = self.inv.make_scratch_vec();
236        for (t, s) in spec.iter_mut().enumerate() {
237            // the DC and Nyquist bins must be real for the inverse real FFT
238            s[0].im = 0.0;
239            if let Some(last) = s.last_mut() {
240                last.im = 0.0;
241            }
242            let _ = self.inv.process_with_scratch(s, &mut buf, &mut scratch);
243            for (b, w) in buf.iter_mut().zip(&self.window) {
244                *b *= w;
245            }
246            for i in 0..self.hop {
247                out[t * self.hop + i] = buf[i] + mem[i];
248            }
249            mem.copy_from_slice(&buf[self.hop..]);
250        }
251        out
252    }
253}
254
255/// Encoder inputs for a whole signal: `erb` is `[T][nb_erb]`, `spec` is `[2][T][nb_df]`
256/// (real, imaginary), both already shifted by `conv_lookahead`.
257pub struct Features {
258    pub frames: usize,
259    pub erb: Vec<f32>,
260    pub spec: Vec<f32>,
261}
262
263/// Compute the normalized ERB and complex features of `spec` (reference `df_features` and
264/// `DfNet.pad_feat`).
265pub fn features(spec: &[Vec<Complex32>], erb: &[usize], p: &Params) -> Features {
266    let t_n = spec.len();
267    let (ne, nd) = (p.nb_erb, p.nb_df);
268    let alpha = p.alpha();
269    let lin = |a: f32, b: f32, n: usize| -> Vec<f32> {
270        (0..n).map(|i| if n > 1 { a + (b - a) * i as f32 / (n - 1) as f32 } else { a }).collect()
271    };
272    let mut mean_state = lin(MEAN_NORM_INIT[0], MEAN_NORM_INIT[1], ne);
273    let mut unit_state = lin(UNIT_NORM_INIT[0], UNIT_NORM_INIT[1], nd);
274    let mut feat_erb = vec![0f32; t_n * ne];
275    let mut feat_spec = vec![0f32; 2 * t_n * nd];
276    let shift = p.conv_lookahead;
277    for (t, x) in spec.iter().enumerate() {
278        // features of frame t go to t − conv_lookahead (dropped when negative)
279        let dst = t.checked_sub(shift);
280        let mut start = 0;
281        for (b, (&w, s)) in erb.iter().zip(mean_state.iter_mut()).enumerate() {
282            let pow: f32 = x[start..start + w].iter().map(|c| c.norm_sqr()).sum::<f32>() / w as f32;
283            start += w;
284            let db = (pow + 1e-10).log10() * 10.0;
285            *s = db * (1.0 - alpha) + *s * alpha;
286            if let Some(d) = dst {
287                feat_erb[d * ne + b] = (db - *s) / 40.0;
288            }
289        }
290        for (f, s) in unit_state.iter_mut().enumerate() {
291            *s = x[f].norm() * (1.0 - alpha) + *s * alpha;
292            if let Some(d) = dst {
293                let v = x[f] / s.sqrt();
294                feat_spec[d * nd + f] = v.re;
295                feat_spec[t_n * nd + d * nd + f] = v.im;
296            }
297        }
298    }
299    Features { frames: t_n, erb: feat_erb, spec: feat_spec }
300}
301
302/// Apply the ERB gains (`[T][nb_erb]`) and deep filter (`[T][nb_df][order·2]`) to the noisy
303/// spectrum, returning the enhanced spectrum.
304pub fn apply(spec: &[Vec<Complex32>], gains: &[f32], coefs: &[f32], erb: &[usize], p: &Params) -> Vec<Vec<Complex32>> {
305    let (ne, nd, order) = (p.nb_erb, p.nb_df, p.df_order);
306    let past = (order - 1 - p.df_lookahead) as isize;
307    let t_n = spec.len();
308    let mut out = Vec::with_capacity(t_n);
309    for t in 0..t_n {
310        let mut y = spec[t].clone();
311        let mut start = 0;
312        for (b, &w) in erb.iter().enumerate() {
313            let g = gains[t * ne + b];
314            for v in &mut y[start..start + w] {
315                *v *= g;
316            }
317            start += w;
318        }
319        let c = &coefs[t * nd * order * 2..(t + 1) * nd * order * 2];
320        for f in 0..nd {
321            let mut acc = Complex32::new(0.0, 0.0);
322            for n in 0..order {
323                let tt = t as isize - past + n as isize;
324                if tt < 0 || tt as usize >= t_n {
325                    continue;
326                }
327                let k = (f * order + n) * 2;
328                acc += spec[tt as usize][f] * Complex32::new(c[k], c[k + 1]);
329            }
330            y[f] = acc;
331        }
332        out.push(y);
333    }
334    out
335}
336
337/// A loaded DeepFilterNet 3.
338pub struct Dfn3 {
339    pub params: Params,
340    erb: Vec<usize>,
341    stft: Stft,
342    enc: Arc<ModelSession>,
343    erb_dec: Arc<ModelSession>,
344    df_dec: Arc<ModelSession>,
345    /// Frames per inference segment (whole sequence when shorter).
346    pub segment: usize,
347    warmup: usize,
348    crossfade: usize,
349}
350
351/// Segmenting of long inputs (frames of 10 ms).
352const SEGMENT: usize = 3000;
353const WARMUP: usize = 100;
354const CROSSFADE: usize = 50;
355/// Frames discarded after each segment's end (convolution context).
356const TAIL: usize = 8;
357
358impl Dfn3 {
359    /// Resolve `denoise/dfn3` (download policy of `ctx`, provenance), unpack it once next to
360    /// the cached archive, and load the three graphs on the context's device.
361    pub fn load(ctx: &mut OpCtx) -> Result<Dfn3> {
362        let m = ferraus_models::ensure_model(ctx, MODEL)?;
363        let dir = m.path.with_file_name("extracted");
364        let paths = ferraus_ml::archive::extract_members(&m.path, &dir, &MEMBERS)?;
365        let hints = m.spec.provider_hints.clone();
366        let params = Params::from_ini(&std::fs::read_to_string(&paths[3])?)?;
367        let enc = load_for_hinted(ctx, &paths[0], &hints)?;
368        let erb_dec = load_for_hinted(ctx, &paths[1], &hints)?;
369        let df_dec = load_for_hinted(ctx, &paths[2], &hints)?;
370        Ok(Dfn3::new(params, enc, erb_dec, df_dec))
371    }
372
373    /// Load from an unpacked directory (tests, tools).
374    pub fn load_dir(dir: &Path, opts: &ferraus_ml::SessionOptions) -> Result<Dfn3> {
375        let params = Params::from_ini(&std::fs::read_to_string(dir.join("config.ini"))?)?;
376        let enc = ferraus_ml::load(&dir.join("enc.onnx"), opts)?.0;
377        let erb_dec = ferraus_ml::load(&dir.join("erb_dec.onnx"), opts)?.0;
378        let df_dec = ferraus_ml::load(&dir.join("df_dec.onnx"), opts)?.0;
379        Ok(Dfn3::new(params, enc, erb_dec, df_dec))
380    }
381
382    fn new(params: Params, enc: Arc<ModelSession>, erb_dec: Arc<ModelSession>, df_dec: Arc<ModelSession>) -> Dfn3 {
383        let erb = erb_widths(params.sr, params.fft_size, params.nb_erb, params.min_nb_erb_freqs);
384        let stft = Stft::new(params.fft_size, params.hop_size);
385        Dfn3 { params, erb, stft, enc, erb_dec, df_dec, segment: SEGMENT, warmup: WARMUP, crossfade: CROSSFADE }
386    }
387
388    /// Run the three graphs on frames `a..b` of the features. Returns gains `[b−a][nb_erb]`
389    /// and coefficients `[b−a][nb_df][order·2]`.
390    fn infer(&self, feat: &Features, a: usize, b: usize) -> Result<(Vec<f32>, Vec<f32>)> {
391        let (ne, nd) = (self.params.nb_erb, self.params.nb_df);
392        let s = b - a;
393        let erb = feat.erb[a * ne..b * ne].to_vec();
394        let mut spec = Vec::with_capacity(2 * s * nd);
395        for ch in 0..2 {
396            let off = ch * feat.frames * nd;
397            spec.extend_from_slice(&feat.spec[off + a * nd..off + b * nd]);
398        }
399        let enc = self.enc.run(vec![
400            TensorF32::new("feat_erb", vec![1, 1, s as i64, ne as i64], erb),
401            TensorF32::new("feat_spec", vec![1, 2, s as i64, nd as i64], spec),
402        ])?;
403        let get = |name: &str| -> Result<TensorF32> {
404            enc.iter()
405                .find(|t| t.name == name)
406                .cloned()
407                .ok_or_else(|| Error::processing(format!("DFN3 encoder produced no '{name}' output")))
408        };
409        let m = self.erb_dec.run(vec![get("emb")?, get("e3")?, get("e2")?, get("e1")?, get("e0")?])?;
410        let c = self.df_dec.run(vec![get("emb")?, get("c0")?])?;
411        let gains = m.into_iter().next().ok_or_else(|| Error::processing("DFN3 ERB decoder has no output"))?;
412        let coefs = c
413            .into_iter()
414            .find(|t| t.name == "coefs")
415            .ok_or_else(|| Error::processing("DFN3 DF decoder produced no 'coefs' output"))?;
416        let want_c = s * nd * self.params.df_order * 2;
417        if gains.data.len() != s * ne || coefs.data.len() != want_c {
418            return Err(Error::processing(format!(
419                "DFN3 decoders returned shapes {:?} and {:?} for {s} frames",
420                gains.shape, coefs.shape
421            )));
422        }
423        Ok((gains.data, coefs.data))
424    }
425
426    /// Gains and coefficients for every frame, segmenting long inputs.
427    fn infer_all(&self, feat: &Features, tick: &mut dyn FnMut(f64) -> Result<()>) -> Result<(Vec<f32>, Vec<f32>)> {
428        let t_n = feat.frames;
429        let (ne, nc) = (self.params.nb_erb, self.params.nb_df * self.params.df_order * 2);
430        if t_n <= self.segment {
431            let r = self.infer(feat, 0, t_n)?;
432            tick(1.0)?;
433            return Ok(r);
434        }
435        let mut gains = vec![0f32; t_n * ne];
436        let mut coefs = vec![0f32; t_n * nc];
437        let mut wsum = vec![0f32; t_n];
438        let step = self.segment - self.crossfade;
439        let mut start = 0;
440        while start < t_n {
441            let end = (start + self.segment).min(t_n);
442            let a = start.saturating_sub(self.warmup);
443            let b = (end + TAIL).min(t_n);
444            let (g, c) = self.infer(feat, a, b)?;
445            for t in start..end {
446                // linear crossfade over the overlap with the neighbouring segments
447                let mut w = 1.0f32;
448                if start > 0 && t < start + self.crossfade {
449                    w = (t - start) as f32 / self.crossfade as f32 + 0.5 / self.crossfade as f32;
450                }
451                if end < t_n && t >= end - self.crossfade {
452                    w = w.min((end - t) as f32 / self.crossfade as f32 - 0.5 / self.crossfade as f32);
453                }
454                let lt = t - a;
455                wsum[t] += w;
456                for (o, v) in gains[t * ne..(t + 1) * ne].iter_mut().zip(&g[lt * ne..(lt + 1) * ne]) {
457                    *o += w * v;
458                }
459                for (o, v) in coefs[t * nc..(t + 1) * nc].iter_mut().zip(&c[lt * nc..(lt + 1) * nc]) {
460                    *o += w * v;
461                }
462            }
463            tick(end as f64 / t_n as f64)?;
464            if end == t_n {
465                break;
466            }
467            start += step;
468        }
469        for t in 0..t_n {
470            let w = wsum[t].max(1e-6);
471            gains[t * ne..(t + 1) * ne].iter_mut().for_each(|v| *v /= w);
472            coefs[t * nc..(t + 1) * nc].iter_mut().for_each(|v| *v /= w);
473        }
474        Ok((gains, coefs))
475    }
476
477    /// Enhance one 48 kHz channel; the output has the input's length and timing.
478    pub fn enhance_channel(&self, x: &[f32], tick: &mut dyn FnMut(f64) -> Result<()>) -> Result<Vec<f32>> {
479        if x.is_empty() {
480            return Ok(Vec::new());
481        }
482        let p = &self.params;
483        let mut padded = x.to_vec();
484        padded.resize(x.len() + p.fft_size, 0.0);
485        let spec = self.stft.analysis(&padded);
486        let feat = features(&spec, &self.erb, p);
487        let (gains, coefs) = self.infer_all(&feat, tick)?;
488        let mut enhanced = apply(&spec, &gains, &coefs, &self.erb, p);
489        let y = self.stft.synthesis(&mut enhanced);
490        let d = p.fft_size - p.hop_size;
491        Ok((0..x.len()).map(|i| y.get(i + d).copied().unwrap_or(0.0)).collect())
492    }
493
494    /// Enhance every channel independently (resampled to 48 kHz and back).
495    pub fn enhance(&self, buf: &AudioBuffer, tick: &mut dyn FnMut(f64) -> Result<()>) -> Result<AudioBuffer> {
496        let rate = self.params.sr as u32;
497        let x = resample(buf, rate, Quality::High);
498        let channels = x.channels().max(1);
499        let mut data = Vec::with_capacity(x.channels());
500        for (c, ch) in x.data.iter().enumerate() {
501            let mut sub = |f: f64| tick((c as f64 + f) / channels as f64);
502            data.push(self.enhance_channel(ch, &mut sub)?);
503        }
504        let y = AudioBuffer::from_planar(rate, data);
505        let mut y = resample(&y, buf.sample_rate, Quality::High);
506        for c in &mut y.data {
507            c.resize(buf.frames(), 0.0);
508        }
509        Ok(y)
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn erb_bands_match_libdf() {
519        let p = Params::default();
520        let e = erb_widths(p.sr, p.fft_size, p.nb_erb, p.min_nb_erb_freqs);
521        assert_eq!(e.len(), 32);
522        assert_eq!(e.iter().sum::<usize>(), 481);
523        assert!(e.iter().all(|w| *w >= 2));
524        // low bands are the 2-bin minimum, high bands are wide
525        assert_eq!(&e[..4], &[2, 2, 2, 2]);
526        assert!(e[31] > 30, "{e:?}");
527        assert!(e.windows(2).all(|w| w[1] >= w[0]), "{e:?}");
528    }
529
530    #[test]
531    fn alpha_is_rounded_like_the_reference() {
532        assert_eq!(Params::default().alpha(), 0.99);
533    }
534
535    #[test]
536    fn config_ini_parses() {
537        let ini = "[train]\nseed = 43\n[df]\nsr = 48000\nfft_size = 960\nhop_size = 480\nnb_erb = 32\nnb_df = 96\nnorm_tau = 1\ndf_order = 5\ndf_lookahead = 2\n[deepfilternet]\nconv_lookahead = 2\n";
538        assert_eq!(Params::from_ini(ini).unwrap(), Params::default());
539        assert!(Params::from_ini("[df]\nhop_size = 240\n").is_err());
540        assert!(Params::from_ini("[df]\nnb_df = x\n").is_err());
541    }
542
543    #[test]
544    fn stft_reconstructs_perfectly() {
545        let p = Params::default();
546        let stft = Stft::new(p.fft_size, p.hop_size);
547        let x: Vec<f32> =
548            (0..48_000).map(|i| ((i as f32) * 0.031).sin() * 0.5 + ((i * 7 % 11) as f32 - 5.0) * 0.01).collect();
549        let mut padded = x.clone();
550        padded.resize(x.len() + p.fft_size, 0.0);
551        let mut spec = stft.analysis(&padded);
552        let y = stft.synthesis(&mut spec);
553        let d = p.fft_size - p.hop_size;
554        let err = x.iter().enumerate().map(|(i, v)| (v - y[i + d]).abs()).fold(0f32, f32::max);
555        assert!(err < 1e-5, "max reconstruction error {err}");
556        // analysis normalization: a full-scale sine at a bin centre reads about 0.5·Σw/N
557        let f = 100.0 * 48_000.0 / 960.0;
558        let s: Vec<f32> =
559            (0..9600).map(|i| (2.0 * std::f64::consts::PI * f * i as f64 / 48_000.0).sin() as f32).collect();
560        let sp = stft.analysis(&s);
561        let mag = sp[5][100].norm();
562        let wsum: f32 = vorbis_window(960).iter().sum();
563        assert!((mag - 0.5 * wsum / 960.0).abs() < 1e-3, "{mag}");
564    }
565
566    #[test]
567    fn unity_gains_and_identity_filter_return_the_input() {
568        let p = Params::default();
569        let erb = erb_widths(p.sr, p.fft_size, p.nb_erb, p.min_nb_erb_freqs);
570        let stft = Stft::new(p.fft_size, p.hop_size);
571        let x: Vec<f32> = (0..9600).map(|i| ((i as f32) * 0.05).sin()).collect();
572        let spec = stft.analysis(&x);
573        let t = spec.len();
574        let gains = vec![1f32; t * p.nb_erb];
575        // identity filter: coefficient 1 on the current frame (n = order − 1 − lookahead)
576        let mut coefs = vec![0f32; t * p.nb_df * p.df_order * 2];
577        let cur = p.df_order - 1 - p.df_lookahead;
578        for tt in 0..t {
579            for f in 0..p.nb_df {
580                coefs[((tt * p.nb_df + f) * p.df_order + cur) * 2] = 1.0;
581            }
582        }
583        let y = apply(&spec, &gains, &coefs, &erb, &p);
584        for (a, b) in y.iter().zip(&spec) {
585            for (u, v) in a.iter().zip(b) {
586                assert!((u - v).norm() < 1e-6);
587            }
588        }
589        // a one-frame delay filter picks the previous frame for the DF bins only
590        let mut delay = vec![0f32; coefs.len()];
591        for tt in 0..t {
592            for f in 0..p.nb_df {
593                delay[((tt * p.nb_df + f) * p.df_order + cur - 1) * 2] = 1.0;
594            }
595        }
596        let y = apply(&spec, &vec![0.5; t * p.nb_erb], &delay, &erb, &p);
597        assert!((y[10][5] - spec[9][5]).norm() < 1e-6);
598        assert!((y[10][200] - spec[10][200] * 0.5).norm() < 1e-6);
599    }
600
601    #[test]
602    fn features_follow_the_reference_normalization() {
603        let p = Params::default();
604        let erb = erb_widths(p.sr, p.fft_size, p.nb_erb, p.min_nb_erb_freqs);
605        // a constant spectrum: the mean state converges to its level, so features → 0
606        let spec = vec![vec![Complex32::new(0.01, 0.0); 481]; 2000];
607        let f = features(&spec, &erb, &p);
608        assert_eq!(f.erb.len(), 2000 * 32);
609        let late = &f.erb[1900 * 32..1901 * 32];
610        assert!(late.iter().all(|v| v.abs() < 1e-3), "{late:?}");
611        // first frame (shifted to index −2 and dropped); index 0 holds frame 2
612        let db = (1e-4f32 + 1e-10).log10() * 10.0;
613        let s0 = -60.0f32;
614        let s1 = db * 0.01 + s0 * 0.99;
615        let s2 = db * 0.01 + s1 * 0.99;
616        let s3 = db * 0.01 + s2 * 0.99;
617        assert!((f.erb[0] - (db - s3) / 40.0).abs() < 1e-4, "{} vs {}", f.erb[0], (db - s3) / 40.0);
618        // the last conv_lookahead frames are zero padding
619        assert!(f.erb[1998 * 32..].iter().all(|v| *v == 0.0));
620        // unit norm: |x| / sqrt(state) with state → |x|
621        let re = f.spec[1900 * 96];
622        assert!((re - 0.01 / 0.01f32.sqrt()).abs() < 1e-3, "{re}");
623        assert_eq!(f.spec[2000 * 96 + 1900 * 96], 0.0);
624    }
625}